diff --git a/web/src/App.jsx b/web/src/App.jsx index 1b2dab4..b57da89 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -46,6 +46,7 @@ import ModelPage from './pages/Model'; import ModelDeploymentPage from './pages/ModelDeployment'; import ModelHeatPage from './pages/ModelHeat'; import Playground from './pages/Playground'; +import ApiTester from './pages/ApiTester'; import Subscription from './pages/Subscription'; import OAuth2Callback from './components/auth/OAuth2Callback'; import PersonalSetting from './components/settings/PersonalSetting'; @@ -58,6 +59,10 @@ import Suppliers from './pages/SupplierAdmin/list'; import Setup from './pages/Setup'; import SetupCheck from './components/layout/SetupCheck'; import OperationLog from './pages/OperationLog'; +import LuckyBag from './pages/LuckyBag'; +import Drawing from './pages/Drawing'; +import ProfitDashboard from './pages/ProfitDashboard'; +import SettingsAntiAbuse from './pages/Setting/Operation/SettingsAntiAbuse'; const Home = lazy(() => import('./pages/Home')); const Dashboard = lazy(() => import('./pages/Dashboard')); @@ -177,6 +182,14 @@ function App() { } /> + + + + } + /> } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { Activity, CreditCard, Hash, Layers, Gauge } from 'lucide-react'; +import { renderQuota } from '../../helpers'; + +const formatNumber = (n) => { + if (n == null || isNaN(n)) return '—'; + const v = Number(n); + if (v >= 1_000_000) return (v / 1_000_000).toFixed(1) + 'M'; + if (v >= 1_000) return (v / 1_000).toFixed(1) + 'K'; + return v.toLocaleString(); +}; + +const formatLatency = (v) => { + if (v == null) return '—'; + if (v < 1) return `${Math.round(v * 1000)}ms`; + return `${v.toFixed(2)}s`; +}; + +const BentoStat = ({ label, value, unit, sub, delta, icon: Icon, className = '' }) => { + return ( +
+
+
+ +
+
{label}
+
+ +
+
+ + {value} + + {unit && {unit}} +
+
+ +
+ {delta != null && ( +
0 + ? 'text-red-500 dark:text-red-400' + : 'text-slate-500 dark:text-white/50' + }`} + > + + {delta > 0 ? '↑' : delta < 0 ? '↓' : '·'} {Math.abs(delta).toFixed(0)}% + + 对比上月 +
+ )} + {sub && ( +
{sub}
+ )} +
+
+ ); +}; + +const BentoStats = ({ metrics, monthDelta }) => { + return ( + <> + + + {formatNumber(metrics.today.tokens)} + {' '} + tokens + · + {renderQuota(metrics.today.cost)} + + } + icon={Activity} + /> + + + + + {metrics.month.requests} + {' '} + 次调用 + · + {formatNumber(metrics.month.tokens)} tokens + + } + delta={monthDelta} + icon={CreditCard} + /> + + + ); +}; + +export default BentoStats; diff --git a/web/src/components/dashboard/BentoTrend.jsx b/web/src/components/dashboard/BentoTrend.jsx new file mode 100644 index 0000000..26752d6 --- /dev/null +++ b/web/src/components/dashboard/BentoTrend.jsx @@ -0,0 +1,136 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useMemo } from 'react'; +import { TrendingUp } from 'lucide-react'; +import Sparkline from './Sparkline'; +import { renderQuota } from '../../helpers'; + +const WEEKDAYS_SHORT = ['日', '一', '二', '三', '四', '五', '六']; + +const BentoTrend = ({ + dailyStats = [], + topModels = [], + className = '', +}) => { + const counts = useMemo(() => dailyStats.map((d) => d.count), [dailyStats]); + const costs = useMemo(() => dailyStats.map((d) => d.cost), [dailyStats]); + + const totalCalls = counts.reduce((s, v) => s + v, 0); + const totalCost = costs.reduce((s, v) => s + v, 0); + const avg = totalCalls > 0 ? (totalCalls / Math.max(counts.length, 1)).toFixed(1) : '0.0'; + const peak = counts.length > 0 ? Math.max(...counts) : 0; + const topMax = topModels[0]?.count || 0; + + return ( +
+
+
+
+ +
+
+
Token 使用趋势
+
+ 近 7 天 + {totalCalls} 次 + · 日均{' '} + {avg} +
+
+
+
+
+ 花费 +
+
+ {totalCost > 0 ? renderQuota(totalCost) : '—'} +
+
+
+ +
+ +
+ +
+
+ {dailyStats.map((d, i) => ( + 0 + ? 'font-semibold text-slate-900 dark:text-white' + : 'text-slate-300 dark:text-white/30' + }`} + title={`周${WEEKDAYS_SHORT[d.date.getDay()]} ${d.count} 次`} + > + {WEEKDAYS_SHORT[d.date.getDay()]} + + ))} +
+
+ 峰值{' '} + {peak} 次 +
+
+ + {topModels.length > 0 && ( +
+
+ Top 5 模型 + 近 30 日 +
+
    + {topModels.map((m, i) => { + const pct = topMax > 0 ? (m.count / topMax) * 100 : 0; + return ( +
  • + + {i + 1} + + + {m.name} + +
    +
    +
    + + {m.count} + +
  • + ); + })} +
+
+ )} +
+ ); +}; + +export default BentoTrend; diff --git a/web/src/components/dashboard/Hero.jsx b/web/src/components/dashboard/Hero.jsx new file mode 100644 index 0000000..1599d53 --- /dev/null +++ b/web/src/components/dashboard/Hero.jsx @@ -0,0 +1,56 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useMemo } from 'react'; + +const getGreeting = () => { + const h = new Date().getHours(); + if (h < 5) return '深夜好'; + if (h < 12) return '早上好'; + if (h < 14) return '中午好'; + if (h < 18) return '下午好'; + return '晚上好'; +}; + +const Hero = ({ user }) => { + const dateStr = useMemo(() => { + const now = new Date(); + const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; + return `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日 · ${weekdays[now.getDay()]}`; + }, []); + + return ( +
+
+
+ + {dateStr} +
+

+ {getGreeting()}, + + {user?.username || '访客'} + +

+
+
+ ); +}; + +export default Hero; \ No newline at end of file diff --git a/web/src/components/dashboard/MonthlyForecast.jsx b/web/src/components/dashboard/MonthlyForecast.jsx new file mode 100644 index 0000000..7c0421f --- /dev/null +++ b/web/src/components/dashboard/MonthlyForecast.jsx @@ -0,0 +1,130 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { Receipt, TrendingUp, TrendingDown, Minus } from 'lucide-react'; +import { renderQuota } from '../../helpers'; + +const SubStat = ({ label, value, hint }) => ( +
+
+ {label} +
+
+ {value} +
+ {hint && ( +
+ {hint} +
+ )} +
+); + +const MonthlyForecast = ({ forecast = {}, className = '' }) => { + const { + spent = 0, + balance = 0, + dailyAvg = 0, + daysInMonth = 0, + daysElapsed = 0, + projected = 0, + monthDelta = null, + } = forecast; + + const daysRemaining = Math.max(0, daysInMonth - daysElapsed); + const hasData = spent > 0; + const overBudget = hasData && balance > 0 && projected > balance; + + const DeltaIcon = + monthDelta == null ? Minus : monthDelta > 0 ? TrendingUp : TrendingDown; + const deltaTone = + monthDelta == null + ? 'text-slate-500 dark:text-white/50' + : monthDelta > 5 + ? 'text-red-500 dark:text-red-400' + : monthDelta < -5 + ? 'text-emerald-600 dark:text-emerald-400' + : 'text-slate-500 dark:text-white/50'; + + return ( +
+
+
+ +
+
+
+ 本月账单预测 +
+
+ 已过 {daysElapsed} / {daysInMonth} 天 · 剩 {daysRemaining} 天 +
+
+
+ +
+
+ 月底预计 +
+
+ {hasData ? renderQuota(projected) : '—'} +
+ {monthDelta != null && ( +
+ + + {monthDelta > 0 ? '+' : ''} + {monthDelta.toFixed(0)}% + + 较上月 +
+ )} +
+ +
+ + + 0 ? renderQuota(balance) : '—'} + hint={overBudget ? '预计不足' : '可继续使用'} + /> +
+
+ ); +}; + +export default MonthlyForecast; diff --git a/web/src/components/dashboard/RecentActivity.jsx b/web/src/components/dashboard/RecentActivity.jsx new file mode 100644 index 0000000..2703738 --- /dev/null +++ b/web/src/components/dashboard/RecentActivity.jsx @@ -0,0 +1,137 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useMemo } from 'react'; +import { ArrowUpRight, Clock, Inbox } from 'lucide-react'; +import { renderQuota, renderNumber, timestamp2string } from '../../helpers'; + +const isSuccess = (l) => + l?.type === 0 || l?.status === 0 || l?.type === 'success' || l?.type === undefined; + +const RecentActivity = ({ logs = [], className = '' }) => { + const recent = useMemo(() => logs.slice(0, 10), [logs]); + + return ( +
+
+
+
+ +
+
+
最近调用
+
+ 共{' '} + + {recent.length} + {' '} + 条记录 +
+
+
+ + 查看全部 + +
+ + {recent.length === 0 ? ( +
+
+ +
+

暂无使用记录

+

+ 开始使用 API 后,您的使用历史将显示在这里 +

+
+ ) : ( +
+ + + + + + + + + + + + {recent.map((l) => { + const ok = isSuccess(l); + return ( + + + + + + + + ); + })} + +
+ time + + model + + tokens + + cost + + status +
+ {timestamp2string(l.created_at).slice(5, 16)} + + {l.model_name || '—'} + + {renderNumber(l.token_used || 0)} + + {renderQuota(l.quota || 0)} + + + + {ok ? '成功' : '失败'} + +
+
+ )} +
+ ); +}; + +export default RecentActivity; diff --git a/web/src/components/dashboard/Sparkline.jsx b/web/src/components/dashboard/Sparkline.jsx new file mode 100644 index 0000000..1b7929a --- /dev/null +++ b/web/src/components/dashboard/Sparkline.jsx @@ -0,0 +1,70 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; + +const Sparkline = ({ + data = [], + width = 200, + height = 40, + stroke = 'currentColor', + className = '', +}) => { + if (!data || data.length === 0) return null; + const max = Math.max(...data, 1); + const step = data.length > 1 ? width / (data.length - 1) : 0; + const points = data + .map((v, i) => { + const x = i * step; + const y = height - (v / max) * (height - 4) - 2; + return `${x.toFixed(1)},${y.toFixed(1)}`; + }) + .join(' '); + + const areaPoints = `0,${height} ${points} ${width},${height}`; + + return ( + + + + + + + + + + + ); +}; + +export default Sparkline; diff --git a/web/src/components/dashboard/TopBar.jsx b/web/src/components/dashboard/TopBar.jsx new file mode 100644 index 0000000..2b3aceb --- /dev/null +++ b/web/src/components/dashboard/TopBar.jsx @@ -0,0 +1,75 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { Search, RefreshCw, Wallet } from 'lucide-react'; +import { renderQuota } from '../../helpers'; + +const TopBar = ({ onSearch, onRefresh, loading, user, unreadCount = 0 }) => { + const initial = (user?.username || '?').charAt(0).toUpperCase(); + const balance = Number(user?.quota || 0); + return ( +
+
+ + TokenFactory + +
+ +
+ + + + + + {renderQuota(balance)} + + +
+ {initial} + {unreadCount > 0 && ( + + {unreadCount > 99 ? '99+' : unreadCount} + + )} +
+
+
+ ); +}; + +export default TopBar; diff --git a/web/src/components/dashboard/index.jsx b/web/src/components/dashboard/index.jsx index 3f50c5f..15481c7 100644 --- a/web/src/components/dashboard/index.jsx +++ b/web/src/components/dashboard/index.jsx @@ -17,260 +17,72 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React, { useContext, useEffect } from 'react'; -import { getRelativeTime, userIsDistributorUser } from '../../helpers'; +import React, { useContext } from 'react'; import { UserContext } from '../../context/User'; -import { StatusContext } from '../../context/Status'; - -import DashboardHeader from './DashboardHeader'; -import StatsCards from './StatsCards'; -import ChartsPanel from './ChartsPanel'; -import ApiInfoPanel from './ApiInfoPanel'; -import AnnouncementsPanel from './AnnouncementsPanel'; -import FaqPanel from './FaqPanel'; -import UptimePanel from './UptimePanel'; -import SearchModal from './modals/SearchModal'; -import DistributorAnalyticsBoard from '../distributor/DistributorAnalyticsBoard'; - +import { useUserMessageUnreadCount } from '../../hooks/common/useUserMessageUnreadCount'; import { useDashboardData } from '../../hooks/dashboard/useDashboardData'; -import { useDashboardStats } from '../../hooks/dashboard/useDashboardStats'; -import { useDashboardCharts } from '../../hooks/dashboard/useDashboardCharts'; -import { - CHART_CONFIG, - CARD_PROPS, - FLEX_CENTER_GAP2, - ILLUSTRATION_SIZE, - ANNOUNCEMENT_LEGEND_DATA, - UPTIME_STATUS_MAP, -} from '../../constants/dashboard.constants'; -import { - getTrendSpec, - handleCopyUrl, - handleSpeedTest, - getUptimeStatusColor, - getUptimeStatusText, - renderMonitorList, -} from '../../helpers/dashboard'; +import TopBar from './TopBar'; +import Hero from './Hero'; +import BentoStats from './BentoStats'; +import BentoTrend from './BentoTrend'; +import MonthlyForecast from './MonthlyForecast'; +import RecentActivity from './RecentActivity'; const Dashboard = () => { - // ========== Context ========== - const [userState, userDispatch] = useContext(UserContext); - const [statusState, statusDispatch] = useContext(StatusContext); + const [userState] = useContext(UserContext); + const user = userState?.user; - // ========== 主要数据管理 ========== - const dashboardData = useDashboardData(userState, userDispatch, statusState); - - // ========== 图表管理 ========== - const dashboardCharts = useDashboardCharts( - dashboardData.dataExportDefaultTime, - dashboardData.setTrendData, - dashboardData.setConsumeQuota, - dashboardData.setTimes, - dashboardData.setConsumeTokens, - dashboardData.setPieData, - dashboardData.setLineData, - dashboardData.setModelColors, - dashboardData.t, - ); - - // ========== 统计数据 ========== - const { groupedStatsData } = useDashboardStats( - userState, - dashboardData.consumeQuota, - dashboardData.consumeTokens, - dashboardData.times, - dashboardData.trendData, - dashboardData.performanceMetrics, - dashboardData.navigate, - dashboardData.t, - ); - - // ========== 数据处理 ========== - const initChart = async () => { - await dashboardData.loadQuotaData().then((data) => { - if (data && data.length > 0) { - dashboardCharts.updateChartData(data); - } - }); - await dashboardData.loadUptimeData(); - }; - - const handleRefresh = async () => { - const data = await dashboardData.refresh(); - if (data && data.length > 0) { - dashboardCharts.updateChartData(data); - } - }; - - const handleSearchConfirm = async () => { - await dashboardData.handleSearchConfirm(dashboardCharts.updateChartData); - }; - - // ========== 数据准备 ========== - const apiInfoData = statusState?.status?.api_info || []; - const announcementData = (statusState?.status?.announcements || []).map( - (item) => { - const pubDate = item?.publishDate ? new Date(item.publishDate) : null; - const absoluteTime = - pubDate && !isNaN(pubDate.getTime()) - ? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}` - : item?.publishDate || ''; - const relativeTime = getRelativeTime(item.publishDate); - return { - ...item, - time: absoluteTime, - relative: relativeTime, - }; - }, - ); - const faqData = statusState?.status?.faq || []; - - const uptimeLegendData = Object.entries(UPTIME_STATUS_MAP).map( - ([status, info]) => ({ - status: Number(status), - color: info.color, - label: dashboardData.t(info.label), - }), - ); - - // ========== Effects ========== - useEffect(() => { - initChart(); - }, []); + const dashboard = useDashboardData(userState); + const { unreadCount } = useUserMessageUnreadCount(user); return ( -
- +
+
+ {}} + onRefresh={dashboard.refresh} + loading={dashboard.loading} + user={user} + unreadCount={unreadCount} + /> - +
+ - - - {userIsDistributorUser(userState?.user) ? ( - - ) : null} - - {/* API信息和图表面板 */} -
-
- - - {dashboardData.hasApiInfoPanel && ( - handleCopyUrl(url, dashboardData.t)} - handleSpeedTest={handleSpeedTest} - CARD_PROPS={CARD_PROPS} - FLEX_CENTER_GAP2={FLEX_CENTER_GAP2} - ILLUSTRATION_SIZE={ILLUSTRATION_SIZE} - t={dashboardData.t} + {/* 第一行:5 个核心指标卡 */} +
+ - )} +
+ + {/* 第二行:七日用量趋势 + 本月账单预测 */} +
+ + +
+ + {/* 第三行:最近调用 */} + +
+ +
+ TokenFactory · 2026
- - {/* 系统公告和常见问答卡片 */} - {dashboardData.hasInfoPanels && ( -
-
- {/* 公告卡片 */} - {dashboardData.announcementsEnabled && ( - ({ - ...item, - label: dashboardData.t(item.label), - }), - )} - CARD_PROPS={CARD_PROPS} - ILLUSTRATION_SIZE={ILLUSTRATION_SIZE} - t={dashboardData.t} - /> - )} - - {/* 常见问答卡片 */} - {dashboardData.faqEnabled && ( - - )} - - {/* 大模型部署定制服务 / 可用性监控卡片 */} - {dashboardData.uptimeEnabled && ( - - renderMonitorList( - monitors, - (status) => getUptimeStatusColor(status, UPTIME_STATUS_MAP), - (status) => - getUptimeStatusText( - status, - UPTIME_STATUS_MAP, - dashboardData.t, - ), - dashboardData.t, - ) - } - CARD_PROPS={CARD_PROPS} - ILLUSTRATION_SIZE={ILLUSTRATION_SIZE} - t={dashboardData.t} - /> - )} -
-
- )}
); }; -export default Dashboard; +export default Dashboard; \ No newline at end of file diff --git a/web/src/components/settings/OperationSetting.jsx b/web/src/components/settings/OperationSetting.jsx index 2e88469..83b935b 100644 --- a/web/src/components/settings/OperationSetting.jsx +++ b/web/src/components/settings/OperationSetting.jsx @@ -28,6 +28,7 @@ import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit'; import SettingsDistributor from '../../pages/Setting/Operation/SettingsDistributor'; import SettingsCheckin from '../../pages/Setting/Operation/SettingsCheckin'; +import SettingsAntiAbuse from '../../pages/Setting/Operation/SettingsAntiAbuse'; import { API, showError, toBoolean } from '../../helpers'; const OperationSetting = () => { @@ -169,6 +170,10 @@ const OperationSetting = () => { + {/* 风控设置 */} + + + ); diff --git a/web/src/components/settings/PersonalSetting.jsx b/web/src/components/settings/PersonalSetting.jsx index c5da209..ff6021a 100644 --- a/web/src/components/settings/PersonalSetting.jsx +++ b/web/src/components/settings/PersonalSetting.jsx @@ -33,7 +33,7 @@ import { } from '../../helpers'; import { normalizeSmsVerificationEnabled } from '../../helpers/data'; import { UserContext } from '../../context/User'; -import { Modal, Card, Button, Typography } from '@douyinfe/semi-ui'; +import { Modal } from '@douyinfe/semi-ui'; import { useTranslation } from 'react-i18next'; // 导入子组件 @@ -555,26 +555,24 @@ const PersonalSetting = () => { return ( -
-
-
- {/* 顶部用户信息区域 */} - +
+ {/* 顶部用户信息区域 */} + - {/* 签到日历 - 仅在启用时显示 */} - {status?.checkin_enabled && ( -
- -
- )} + {/* 签到日历 - 仅在启用时显示 */} + {status?.checkin_enabled && ( +
+ +
+ )} - {/* 账户管理和其他设置 */} -
+ {/* 账户管理和其他设置 */} +
{/* 左侧:账户管理设置 */}
{ handleNotificationSettingChange={handleNotificationSettingChange} saveNotificationSettings={saveNotificationSettings} /> -
-
{/* 模态框组件 */} diff --git a/web/src/components/settings/personal/cards/AccountManagement.jsx b/web/src/components/settings/personal/cards/AccountManagement.jsx index 7e8541f..455472c 100644 --- a/web/src/components/settings/personal/cards/AccountManagement.jsx +++ b/web/src/components/settings/personal/cards/AccountManagement.jsx @@ -20,26 +20,30 @@ For commercial licensing, please contact support@quantumnous.com import React from 'react'; import { Button, - Card, Input, - Space, Typography, - Avatar, Tabs, TabPane, Popover, Modal, } from '@douyinfe/semi-ui'; import { - IconMail, - IconShield, + IconAlertTriangle, IconGithubLogo, - IconKey, - IconLock, - IconDelete, } from '@douyinfe/semi-icons'; import { SiTelegram, SiWechat, SiLinux, SiDiscord } from 'react-icons/si'; -import { UserPlus, ShieldCheck, Phone } from 'lucide-react'; +import { + UserCog, + Lock, + Phone, + Fingerprint, + UserX, + Link2, + Mail, + ShieldCheck, + Stamp, + KeySquare, +} from 'lucide-react'; import TelegramLoginButton from 'react-telegram-login'; import { API, @@ -54,6 +58,52 @@ import { } from '../../../../helpers'; import TwoFASetting from '../components/TwoFASetting'; +const iconWrap = + 'flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl bg-slate-100 text-slate-600 dark:bg-white/5 dark:text-white/70'; + +const BindingRow = ({ icon, label, info, action }) => ( +
+
+
{icon}
+
+
+ {label} +
+
+ {info} +
+
+
+ {action &&
{action}
} +
+); + +const SecurityRow = ({ iconWrapClass, icon, title, description, action, footer }) => ( +
+
+
+
+ {icon} +
+
+
+ {title} +
+
+ {description} +
+ {footer &&
{footer}
} +
+
+ {action && ( +
{action}
+ )} +
+
+); + const AccountManagement = ({ t, userState, @@ -76,23 +126,25 @@ const AccountManagement = ({ }) => { const renderAccountInfo = (accountId, label) => { if (!accountId || accountId === '') { - return {t('未绑定')}; + return ( + {t('未绑定')} + ); } const popContent = ( -
+
{accountId} {label ? ( -
{label}
+
{label}
) : null}
); return ( - + {accountId} @@ -181,17 +233,17 @@ const AccountManagement = ({ : t('尚未使用'); return ( - +
{/* 卡片头部 */} -
- - - +
+
+ +
- +
{t('账户管理')} - -
+
+
{t('账户绑定、安全设置和身份验证')}
@@ -202,320 +254,191 @@ const AccountManagement = ({ - + {t('账户绑定')}
} itemKey='binding' >
-
+
{/* 邮箱绑定 */} - -
-
-
- -
-
-
- {t('邮箱')} -
-
- {renderAccountInfo( - userState.user?.email, - t('邮箱地址'), - )} -
-
-
-
- -
-
-
+ } + label={t('邮箱')} + info={renderAccountInfo(userState.user?.email, t('邮箱地址'))} + action={ + + } + /> {/* 手机号绑定 */} - -
-
-
- -
-
-
- {t('手机号')} -
-
- {renderAccountInfo( - userState.user?.phone, - t('手机号'), - )} -
-
-
-
- -
-
-
+ } + label={t('手机号')} + info={renderAccountInfo(userState.user?.phone, t('手机号'))} + action={ + + } + /> {/* 微信绑定 */} - -
-
-
- -
-
-
- {t('微信')} -
-
- {!status.wechat_login - ? t('未启用') - : isBound(userState.user?.wechat_id) - ? t('已绑定') - : t('未绑定')} -
-
-
-
- -
-
-
+ } + label={t('微信')} + info={ + !status.wechat_login + ? t('未启用') + : isBound(userState.user?.wechat_id) + ? t('已绑定') + : t('未绑定') + } + action={ + + } + /> {/* GitHub绑定 */} - -
-
-
- -
-
-
- {t('GitHub')} -
-
- {renderAccountInfo( - userState.user?.github_id, - t('GitHub ID'), - )} -
-
-
-
- -
-
-
+ } + label={t('GitHub')} + info={renderAccountInfo(userState.user?.github_id, t('GitHub ID'))} + action={ + + } + /> {/* Discord绑定 */} - -
-
-
- -
-
-
- {t('Discord')} -
-
- {renderAccountInfo( - userState.user?.discord_id, - t('Discord ID'), - )} -
-
-
-
- -
-
-
+ } + label={t('Discord')} + info={renderAccountInfo( + userState.user?.discord_id, + t('Discord ID'), + )} + action={ + + } + /> {/* OIDC绑定 */} - -
-
-
- -
-
-
- {t('OIDC')} -
-
- {renderAccountInfo( - userState.user?.oidc_id, - t('OIDC ID'), - )} -
-
-
-
- -
-
-
+ } + label={t('OIDC')} + info={renderAccountInfo(userState.user?.oidc_id, t('OIDC ID'))} + action={ + + } + /> {/* Telegram绑定 */} - -
-
-
- -
-
-
- {t('Telegram')} -
-
- {renderAccountInfo( - userState.user?.telegram_id, - t('Telegram ID'), - )} -
-
-
-
- {status.telegram_oauth ? ( - isBound(userState.user?.telegram_id) ? ( - - ) : ( - - ) + } + label={t('Telegram')} + info={renderAccountInfo( + userState.user?.telegram_id, + t('Telegram ID'), + )} + action={ + status.telegram_oauth ? ( + isBound(userState.user?.telegram_id) ? ( + ) : ( - )} -
-
-
+ ) + ) : ( + + ) + } + /> setShowTelegramBindModal(false)} footer={null} > -
+
{t('点击下方按钮通过 Telegram 完成绑定')}
@@ -529,45 +452,27 @@ const AccountManagement = ({ {/* LinuxDO绑定 */} - -
-
-
- -
-
-
- {t('LinuxDO')} -
-
- {renderAccountInfo( - userState.user?.linux_do_id, - t('LinuxDO ID'), - )} -
-
-
-
- -
-
-
+ } + label={t('LinuxDO')} + info={renderAccountInfo( + userState.user?.linux_do_id, + t('LinuxDO ID'), + )} + action={ + + } + /> {/* 自定义 OAuth 提供商绑定 */} {status.custom_oauth_providers && @@ -575,58 +480,49 @@ const AccountManagement = ({ const bound = isCustomOAuthBound(provider.id); const binding = getCustomOAuthBinding(provider.id); return ( - -
-
-
- {getOAuthProviderIcon( - provider.icon || binding?.provider_icon || '', - 20, - )} -
-
-
- {provider.name} -
-
- {bound - ? renderAccountInfo( - binding?.provider_user_id, - t('{{name}} ID', { name: provider.name }), - ) - : t('未绑定')} -
-
-
-
- {bound ? ( - - ) : ( - - )} -
-
-
+ + handleUnbindCustomOAuth( + provider.id, + provider.name, + ) + } + > + {t('解绑')} + + ) : ( + + ) + } + /> ); })}
@@ -637,192 +533,145 @@ const AccountManagement = ({ - + {t('安全设置')}
} itemKey='security' > -
-
- - {/* 系统访问令牌 */} - -
-
-
- -
-
- - {t('系统访问令牌')} - - - {t('用于API调用的身份验证令牌,请妥善保管')} - - {systemToken && ( -
- } - /> -
- )} -
-
- -
-
+
+ {/* 系统访问令牌 */} + } + title={t('系统访问令牌')} + description={t('用于API调用的身份验证令牌,请妥善保管')} + footer={ + systemToken && ( + } + /> + ) + } + action={ + + } + /> - {/* 密码管理 */} - -
-
-
- -
-
- - {t('密码管理')} - - - {t('定期更改密码可以提高账户安全性')} - -
-
- -
-
+ {/* 密码管理 */} + } + title={t('密码管理')} + description={t('定期更改密码可以提高账户安全性')} + action={ + + } + /> - {/* Passkey 设置 */} - -
-
-
- -
-
- - {t('Passkey 登录')} - - - {passkeyEnabled - ? t('已启用 Passkey,无需密码即可登录') - : t('使用 Passkey 实现免密且更安全的登录体验')} - -
-
- {t('最后使用时间')}:{lastUsedLabel} -
- {/*{passkeyEnabled && (*/} - {/*
*/} - {/* {t('备份支持')}:*/} - {/* {passkeyStatus?.backup_eligible*/} - {/* ? t('支持备份')*/} - {/* : t('不支持')}*/} - {/* ,{t('备份状态')}:*/} - {/* {passkeyStatus?.backup_state ? t('已备份') : t('未备份')}*/} - {/*
*/} - {/*)}*/} - {!passkeySupported && ( -
- {t('当前设备不支持 Passkey')} -
- )} -
-
-
- + {/* Passkey 设置 */} + } + title={t('Passkey 登录')} + description={ + passkeyEnabled + ? t('已启用 Passkey,无需密码即可登录') + : t('使用 Passkey 实现免密且更安全的登录体验') + } + footer={ +
+
+ {t('最后使用时间')}:{lastUsedLabel}
- - - {/* 两步验证设置 */} - - - {/* 危险区域 */} - -
-
-
- -
-
- - {t('删除账户')} - - - {t('此操作不可逆,所有数据将被永久删除')} - -
+ {!passkeySupported && ( +
+ {t('当前设备不支持 Passkey')}
- -
- - + )} +
+ } + action={ + + } + /> + + {/* 两步验证设置 */} +
+
+ + {/* 危险区域 */} + } + title={t('删除账户')} + description={t('此操作不可逆,所有数据将被永久删除')} + action={ + + } + />
- +
); }; diff --git a/web/src/components/settings/personal/cards/CheckinCalendar.jsx b/web/src/components/settings/personal/cards/CheckinCalendar.jsx index 4c4ef29..72c0bc3 100644 --- a/web/src/components/settings/personal/cards/CheckinCalendar.jsx +++ b/web/src/components/settings/personal/cards/CheckinCalendar.jsx @@ -19,18 +19,15 @@ For commercial licensing, please contact support@quantumnous.com import React, { useState, useEffect, useMemo } from 'react'; import { - Card, Calendar, Button, - Typography, - Avatar, Spin, Tooltip, Collapsible, Modal, } from '@douyinfe/semi-ui'; import { - CalendarCheck, + Sparkles, Gift, Check, ChevronDown, @@ -213,7 +210,7 @@ const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => { }; return ( - +
{ {/* 卡片头部 */}
setIsCollapsed(!isCollapsed)} > - - - -
+
+ +
+
- + {t('每日签到')} - + {isCollapsed ? ( - + ) : ( - + )}
-
+
{!initialLoaded ? t('正在加载签到状态...') : checkinData.stats?.checked_in_today @@ -276,7 +273,7 @@ const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => { onClick={() => doCheckin()} loading={checkinLoading || !initialLoaded} disabled={!initialLoaded || checkinData.stats?.checked_in_today} - className='!bg-green-600 hover:!bg-green-700' + className='!rounded-lg' > {!initialLoaded ? t('加载中...') @@ -289,30 +286,36 @@ const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => { {/* 可折叠内容 */} {/* 签到统计 */} -
-
-
+
+
+
{checkinData.stats?.total_checkins || 0}
-
{t('累计签到')}
+
+ {t('累计签到')} +
-
-
+
+
{renderQuota(monthlyQuota)}
-
{t('本月获得')}
+
+ {t('本月获得')} +
-
-
+
+
{renderQuota(checkinData.stats?.total_quota || 0)}
-
{t('累计获得')}
+
+ {t('累计获得')} +
{/* 签到日历 - 使用更紧凑的样式 */} -
+