From 1057adcd24592b1f3065cefe0a472323d94c4473 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Oct 2026 05:47:03 +0000 Subject: [PATCH 1/3] Initial plan From 6d13ea47fae24f03f3c6311ebd90396500b33578 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 25 Oct 2026 05:58:04 +0000 Subject: [PATCH 2/3] Improve sidebar and profile views Co-authored-by: PythonSmall-Q <106425289+PythonSmall-Q@users.noreply.github.com> --- electron/profile.ts | 69 +++++++- electron/sources.ts | 2 + src/components/AppSidebar.vue | 14 +- src/components/NotificationToasts.vue | 2 +- src/components/ui/sidebar/Sidebar.vue | 20 ++- src/components/ui/sidebar/SidebarProvider.vue | 24 ++- src/components/ui/sidebar/utils.ts | 1 + src/pages/Discussion.vue | 8 +- src/pages/Home.vue | 155 +++++++++--------- src/pages/SourceView.vue | 2 +- src/pages/UserProfile.vue | 119 ++++++++++++++ src/shared/types.ts | 12 ++ src/stores/navigation.ts | 16 ++ tests/native-views.ts | 39 +++++ 14 files changed, 387 insertions(+), 96 deletions(-) diff --git a/electron/profile.ts b/electron/profile.ts index 594af89..6b1d1e8 100644 --- a/electron/profile.ts +++ b/electron/profile.ts @@ -28,14 +28,81 @@ export function parseUserProfile(html: string, username: string): UserProfile { const solvedMatch = text.match(/解决\s*(\d+)/); const solvedTotal = solvedMatch ? Number.parseInt(solvedMatch[1], 10) : null; const problems = parseSolvedProblems(html); + const rows = root.querySelectorAll('#statics tr').map((row) => row.querySelectorAll('td')); + const stats = rows + .filter((cells) => cells.length > 1 && + !/^(解决|solved)/i.test(normaliseText(cells[0].textContent)) && + cells[1].querySelector('a[href*="jresult="]')) + .map((cells) => ({ + label: normaliseText(cells[0].textContent), + count: parseCount(cells[1].textContent), + result: Number(cells[1].querySelector('a[href*="jresult="]')?.getAttribute('href')?.match(/[?&]jresult=(\d+)/)?.[1]), + })) + .filter((entry) => Number.isFinite(entry.result) && entry.count !== null); + const resultStats = stats.map(({ label, count }) => ({ label, count: count ?? 0 })); + const submissionTotal = findCount(rows, /^(提交|提交次数|submissions?)$/i); + const acceptedTotal = findCount(rows, /^(正确|通过|accepted)$/i); + const solved = solvedTotal ?? problems.length; + const failures = stats + .filter((entry) => entry.result >= 5 && entry.result <= 10) + .reduce((total, entry) => total + (entry.count ?? 0), 0); + const activity = parseActivity(root); return { username, nick, - solvedTotal: solvedTotal ?? problems.length, + solvedTotal: solved, + submissionTotal: submissionTotal ?? 0, + acceptedTotal: acceptedTotal ?? 0, + rating: calculateRating(solved, failures, activity), + resultStats, + activity, problems, profileUrl: `${XMOJ_ORIGIN}/userinfo.php?user=${encodeURIComponent(username)}`, }; } +function parseCount(value: string): number | null { + const match = normaliseText(value).replace(/,/g, '').match(/\d+/); + return match ? Number.parseInt(match[0], 10) : null; +} +function findCount(rows: ReturnType['querySelectorAll']>[], label: RegExp): number | null { + for (const cells of rows) { + if (cells.length > 1 && label.test(normaliseText(cells[0].textContent))) + return parseCount(cells[1].textContent); + } + return null; +} +function parseActivity(root: ReturnType): UserProfile['activity'] { + const script = root.querySelectorAll('script').find((node) => + /\$\.plot\s*\(/.test(node.textContent) && /["']#submission["']/.test(node.textContent)); + if (!script) + return []; + const series = [new Map(), new Map()]; + for (const match of script.textContent.matchAll(/\bd([12])\.push\(\s*\[\s*(\d+)\s*,\s*(\d+)\s*\]\s*\)\s*;?/g)) { + const timestamp = Number(match[2]); + const count = Number(match[3]); + if (!Number.isSafeInteger(timestamp) || timestamp <= 0 || timestamp > 8640000000000000 || !Number.isSafeInteger(count)) + continue; + series[Number(match[1]) - 1].set(timestamp, count); + } + const timestamps = new Set([...series[0].keys(), ...series[1].keys()]); + return [...timestamps].sort((a, b) => a - b).map((timestamp) => ({ + timestamp, + submissions: series[0].get(timestamp) ?? 0, + accepted: series[1].get(timestamp) ?? 0, + })); +} +function calculateRating( + solved: number, + failures: number, + activity: UserProfile['activity'], +): number { + if (solved === 0) + return 0; + const solveProgress = Math.min(1, Math.log1p(solved) / Math.log1p(2000)); + const successRate = (solved + 4) / (solved + failures + 10); + const activityProgress = Math.min(1, activity.length / 365); + return Math.round(3000 * (0.7 * solveProgress + 0.2 * successRate + 0.1 * activityProgress)); +} function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } diff --git a/electron/sources.ts b/electron/sources.ts index f39fc46..2e62895 100644 --- a/electron/sources.ts +++ b/electron/sources.ts @@ -21,6 +21,8 @@ export async function fetchSource(http: HttpClient, solutionId: number): Promise const text = response.body; if (/import { computed, ref } from 'vue'; -import { BookText, CalendarDays, ChartNoAxesColumn, ChartPie, ChevronsUpDown, Download, FolderDown, House, Info, Loader2, LogIn, LogOut, Mail, MessagesSquare, RefreshCw, Settings, User, } from '@lucide/vue'; +import { BookText, CalendarDays, ChartNoAxesColumn, ChartPie, ChevronsUpDown, Download, FolderDown, House, Info, Loader2, LogIn, LogOut, Mail, MessagesSquare, Pin, RefreshCw, Settings, User, } from '@lucide/vue'; import UserAvatar from '@/components/UserAvatar.vue'; import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem, } from '@/components/ui/sidebar'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; @@ -7,10 +7,12 @@ import { useSession } from '@/stores/session'; import { useNavigation, type ViewId } from '@/stores/navigation'; import { usePanel } from '@/stores/panel'; import { useToasts } from '@/stores/toasts'; +import { useSidebar } from '@/components/ui/sidebar/utils'; import { checkAppUpdate, showAboutDialog, showUpdateDialog } from '@/services/update'; const session = useSession(); const navigation = useNavigation(); const panel = usePanel(); +const sidebar = useSidebar(); const mainItems = [ { id: 'home' as ViewId, title: '首页', icon: House }, { id: 'problemset' as ViewId, title: '题库', icon: BookText }, @@ -54,6 +56,9 @@ async function handleLogout(): Promise { function handleSignIn(): void { navigation.navigate('problemset'); } +function togglePinnedSidebar(): void { + sidebar.setOpen(!sidebar.pinnedOpen.value); +}