feat: 增加了摸鱼排行榜

This commit is contained in:
liuliu 2025-12-17 12:04:06 +08:00
parent e5c643fd18
commit a2569df522
3 changed files with 187 additions and 1 deletions

View File

@ -154,6 +154,58 @@ export const api = new Elysia({ prefix: "/api" })
}); });
return { content: summaryContent }; 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; export type App = typeof api;

View File

@ -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 { client } from '../client';
import type { Conversation } from '../data/mock'; import type { Conversation } from '../data/mock';
import { MessageBubble } from './MessageBubble'; import { MessageBubble } from './MessageBubble';
import { SummaryModal } from './SummaryModal'; import { SummaryModal } from './SummaryModal';
import { LeaderboardModal } from './LeaderboardModal';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
@ -25,6 +26,7 @@ export function ChatWindow({ conversation }: ChatWindowProps) {
// Summary modal // Summary modal
const [showSummary, setShowSummary] = useState(false); const [showSummary, setShowSummary] = useState(false);
const [showLeaderboard, setShowLeaderboard] = useState(false);
const fetchMessages = async (isLoadMore = false, query = '', start = '', end = '') => { const fetchMessages = async (isLoadMore = false, query = '', start = '', end = '') => {
@ -114,6 +116,7 @@ export function ChatWindow({ conversation }: ChatWindowProps) {
// Reset modal when conversation changes // Reset modal when conversation changes
useEffect(() => { useEffect(() => {
setShowSummary(false); setShowSummary(false);
setShowLeaderboard(false);
}, [conversation?.id]); }, [conversation?.id]);
// Initial load when conversation changes // 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 */} {/* Messages */}
<div <div
className="flex-1 overflow-y-auto p-6 scrollbar-thin" 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 <Sparkles className="w-4 h-4" /> AI
</button> </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> </div>
{/* Text Area */} {/* Text Area */}

View 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>
);
}