feat: 增加了摸鱼排行榜
This commit is contained in:
parent
e5c643fd18
commit
a2569df522
@ -154,6 +154,58 @@ export const api = new Elysia({ prefix: "/api" })
|
||||
});
|
||||
|
||||
return { content: summaryContent };
|
||||
})
|
||||
.get("/leaderboard/:conversationId", ({ params, query }) => {
|
||||
const { conversationId } = params;
|
||||
const type = (query.type as string) || 'day'; // day, week, month
|
||||
|
||||
let startTime = 0;
|
||||
let endTime = Date.now();
|
||||
const now = new Date();
|
||||
|
||||
// Calculate time range
|
||||
if (type === 'day') {
|
||||
now.setHours(0, 0, 0, 0);
|
||||
startTime = now.getTime();
|
||||
now.setHours(23, 59, 59, 999);
|
||||
endTime = now.getTime();
|
||||
} else if (type === 'week') {
|
||||
const day = now.getDay() || 7; // Get current day number, converting Sun (0) to 7
|
||||
if (day !== 1) now.setHours(-24 * (day - 1)); // Go back to Monday
|
||||
now.setHours(0, 0, 0, 0);
|
||||
startTime = now.getTime();
|
||||
} else if (type === 'month') {
|
||||
now.setDate(1);
|
||||
now.setHours(0, 0, 0, 0);
|
||||
startTime = now.getTime();
|
||||
}
|
||||
|
||||
const stats = db.query(`
|
||||
SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.avatar,
|
||||
COUNT(m.id) as count
|
||||
FROM messages m
|
||||
JOIN users u ON m.sender_id = u.id
|
||||
WHERE m.conversation_id = $cid
|
||||
AND m.timestamp >= $start
|
||||
AND m.timestamp <= $end
|
||||
GROUP BY u.id
|
||||
ORDER BY count DESC
|
||||
LIMIT 20
|
||||
`).all({
|
||||
$cid: conversationId,
|
||||
$start: startTime,
|
||||
$end: endTime
|
||||
}) as any[];
|
||||
|
||||
return stats.map(s => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
avatar: s.avatar,
|
||||
count: s.count
|
||||
}));
|
||||
});
|
||||
|
||||
export type App = typeof api;
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { MoreHorizontal, Smile, FolderOpen, Scissors, Clipboard, Search, X, Sparkles } from 'lucide-react';
|
||||
import { MoreHorizontal, Smile, FolderOpen, Scissors, Clipboard, Search, X, Sparkles, Trophy } from 'lucide-react';
|
||||
import { client } from '../client';
|
||||
import type { Conversation } from '../data/mock';
|
||||
import { MessageBubble } from './MessageBubble';
|
||||
import { SummaryModal } from './SummaryModal';
|
||||
import { LeaderboardModal } from './LeaderboardModal';
|
||||
import { clsx } from 'clsx';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
@ -25,6 +26,7 @@ export function ChatWindow({ conversation }: ChatWindowProps) {
|
||||
|
||||
// Summary modal
|
||||
const [showSummary, setShowSummary] = useState(false);
|
||||
const [showLeaderboard, setShowLeaderboard] = useState(false);
|
||||
|
||||
|
||||
const fetchMessages = async (isLoadMore = false, query = '', start = '', end = '') => {
|
||||
@ -114,6 +116,7 @@ export function ChatWindow({ conversation }: ChatWindowProps) {
|
||||
// Reset modal when conversation changes
|
||||
useEffect(() => {
|
||||
setShowSummary(false);
|
||||
setShowLeaderboard(false);
|
||||
}, [conversation?.id]);
|
||||
|
||||
// Initial load when conversation changes
|
||||
@ -212,6 +215,14 @@ export function ChatWindow({ conversation }: ChatWindowProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Leaderboard Modal */}
|
||||
{showLeaderboard && conversation && (
|
||||
<LeaderboardModal
|
||||
conversationId={conversation.id}
|
||||
onClose={() => setShowLeaderboard(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Messages */}
|
||||
<div
|
||||
className="flex-1 overflow-y-auto p-6 scrollbar-thin"
|
||||
@ -261,6 +272,13 @@ export function ChatWindow({ conversation }: ChatWindowProps) {
|
||||
>
|
||||
<Sparkles className="w-4 h-4" /> AI 总结
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowLeaderboard(true)}
|
||||
className="flex items-center gap-1 text-orange-600 hover:text-orange-700 transition-colors text-sm font-medium"
|
||||
title="Fish Touching Leaderboard"
|
||||
>
|
||||
<Trophy className="w-4 h-4" /> 摸鱼榜
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Text Area */}
|
||||
|
||||
116
src/frontend/components/LeaderboardModal.tsx
Normal file
116
src/frontend/components/LeaderboardModal.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, Trophy, Loader2 } from 'lucide-react';
|
||||
import { client } from '../client';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
interface LeaderboardModalProps {
|
||||
conversationId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type TimeRange = 'day' | 'week' | 'month';
|
||||
|
||||
interface RankUser {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export function LeaderboardModal({ conversationId, onClose }: LeaderboardModalProps) {
|
||||
const [range, setRange] = useState<TimeRange>('day');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [users, setUsers] = useState<RankUser[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLeaderboard = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const { data, error } = await client.api.leaderboard({ conversationId }).get({
|
||||
query: { type: range }
|
||||
});
|
||||
|
||||
if (data && !error) {
|
||||
// @ts-ignore
|
||||
setUsers(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchLeaderboard();
|
||||
}, [conversationId, range]);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-sm flex flex-col max-h-[85vh]">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b shrink-0 bg-gradient-to-r from-yellow-50 to-orange-50 rounded-t-lg">
|
||||
<h3 className="text-lg font-bold flex items-center gap-2 text-orange-600">
|
||||
<Trophy className="w-5 h-5 fill-yellow-500 text-yellow-600" />
|
||||
摸鱼排行榜
|
||||
</h3>
|
||||
<button onClick={onClose} className="p-1 hover:bg-white/50 rounded-full transition-colors">
|
||||
<X className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex p-2 gap-2 border-b bg-gray-50 text-sm">
|
||||
{['day', 'week', 'month'].map((r) => (
|
||||
<button
|
||||
key={r}
|
||||
onClick={() => setRange(r as TimeRange)}
|
||||
className={clsx(
|
||||
"flex-1 py-1.5 rounded-md font-medium transition-all",
|
||||
range === r
|
||||
? "bg-white text-orange-600 shadow-sm border border-gray-100"
|
||||
: "text-gray-500 hover:bg-gray-200"
|
||||
)}
|
||||
>
|
||||
{r === 'day' ? '今日' : r === 'week' ? '本周' : '本月'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div className="p-0 overflow-y-auto flex-1 min-h-[300px]">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-full text-gray-400 gap-2">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
统计中...
|
||||
</div>
|
||||
) : users.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full text-gray-400">
|
||||
暂无数据
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-100">
|
||||
{users.map((user, index) => (
|
||||
<div key={user.id} className="flex items-center px-4 py-3 hover:bg-gray-50 transition-colors gap-3">
|
||||
<div className={clsx(
|
||||
"w-6 h-6 flex items-center justify-center rounded-full text-xs font-bold",
|
||||
index === 0 ? "bg-yellow-100 text-yellow-700" :
|
||||
index === 1 ? "bg-gray-200 text-gray-700" :
|
||||
index === 2 ? "bg-orange-100 text-orange-700" :
|
||||
"text-gray-400"
|
||||
)}>
|
||||
{index + 1}
|
||||
</div>
|
||||
<img src={user.avatar} className="w-8 h-8 rounded-full bg-gray-100" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm text-gray-900 truncate">{user.name}</div>
|
||||
</div>
|
||||
<div className="text-sm font-bold text-orange-500">{user.count} <span className="text-xs font-normal text-gray-400">条</span></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user