diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 5e13c147..7b8cec9f 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -13,6 +13,7 @@ | P1 #9 | 公共配置 Redis 列表缓存 | ✅ | | P1 参数中心 | 系统配置 Otter RAM 缓存 + 统一失效 + 多节点 pub/sub | ✅ | | P1 CAPTCHA | 运行时配置快照 + 批量加载 + pub/sub 失效 | ✅ | +| P0 前端 #12–#19 | dynamic 分割、React Query、登录并行、Tooltip、lazy、barrel 收窄 | ✅ | --- @@ -72,7 +73,7 @@ flowchart LR 当前最大的结构性问题(2026-06-17 更新): -1. **前端**:全客户端渲染仍为主模式;~~全局认证瀑布流~~ ✅ 已改为 layout 即时渲染 + 子页面 `RequireAuth` 自行处理未登录态。Admin 大 bundle、无 `dynamic()` 分割仍待优化。 +1. **前端**:~~全局认证瀑布流~~ ✅ 已改为 layout 即时渲染 + 子页面 `RequireAuth` 自行处理未登录态;~~Admin 重模块无 `dynamic()` 分割~~ ✅ database/logs/settings 已懒加载子模块。其余路由 `page.tsx` 仍为 `"use client"`(静态导出下 RSC 收益有限,待逐步薄壳化)。 2. **后端**:文件服务路径(`/f/{id}`)仍是最高频热点;~~磁盘缓存全局互斥锁~~ ✅ 已改为 `RWMutex` + `singleflight`,但 WebP miss 仍在请求线程内同步编码,部署预热与异步回退原图尚未落地。 3. **参数中心**:~~`GetByKey` 每次直打 Redis~~ ✅ 已加 Otter v2 进程内缓存(`pkg/cache/ram`),读路径为 RAM → Redis → DB;管理员写配置后统一失效 RAM + Redis,并通过 pub/sub 同步多节点本地缓存。 @@ -333,13 +334,13 @@ if (loading || !user) { | 10 | 存储迁移后二次 SHA 全量读取验证 | `storage_migration_task.go` | 迁移期间对象 I/O 翻倍 | | 11 | Admin 状态页 5s 轮询 | `frontend/components/common/admin/status.tsx` | Tab 常驻时持续打后端 | | 12 | 路由切换 500ms fade 动画 | `frontend/app/(main)/layout.tsx:53-60` | 即使数据已缓存,感知仍慢 | -| 13 | 无 `next/dynamic` 代码分割 | 全项目 | Admin 首包 300–450KB+ | -| 14 | 19/24 个 `page.tsx` 为 `"use client"` | 各路由 | 无法 RSC 预取,bundle 偏大 | -| 15 | Admin 部分页面用 `useEffect` 而非 React Query | `access-logs.tsx`, `task-executions.tsx` 等 | 无缓存去重,重复请求 | -| 16 | 登录页 OIDC sources 等待 public config | `frontend/components/auth/login-form.tsx` | 多 1 次 RTT 瀑布 | -| 17 | Users 表每行嵌套 3 个 `TooltipProvider` | `frontend/app/(main)/admin/users/page.tsx` | 50+ 行时不必要重渲染 | -| 18 | 缩略图用原生 `` 无 lazy loading | `file-list.tsx`, `file-manager.tsx` | 文件管理页初始解码压力大 | -| 19 | `@/lib/services` barrel 导入 | ~40 个文件 | 单路由 bundle 膨胀 10–30KB | +| 13 | ~~无 `next/dynamic` 代码分割~~ ✅ | `database/`, `logs/`, `settings/` page-client | Admin 重模块拆分为独立 chunk | +| 14 | 19/24 个 `page.tsx` 为 `"use client"` `🔶` | 各路由 | database/logs/settings 已薄壳化;其余待迁移 | +| 15 | ~~Admin 部分页面用 `useEffect` 而非 React Query~~ ✅ | `access-logs.tsx`, `task-executions.tsx` | 列表/详情走 React Query 缓存去重 | +| 16 | ~~登录页 OIDC sources 等待 public config~~ ✅ | `login-form.tsx` | public config 与 auth sources 并行请求 | +| 17 | ~~Users 表每行嵌套 3 个 `TooltipProvider`~~ ✅ | `admin/users/page.tsx` | 表格外层单一 Provider | +| 18 | ~~缩略图用原生 `` 无 lazy loading~~ ✅ | `file-list.tsx`, `file-manager.tsx` | `loading="lazy"` + `decoding="async"` | +| 19 | ~~`@/lib/services` barrel 导入~~ ✅ | 全前端消费侧 | 改为 `@/lib/services/` 直接导入 | | 20 | SQLite 模式无连接池调优 | `internal/db/postgres.go` | 默认 SQLite 写锁瓶颈 | | 21 | Session Redis 仅用第一个地址 | `internal/router/router.go` | Sentinel/Cluster 场景不一致 | @@ -357,7 +358,7 @@ if (loading || !user) { | 4 | `GetFileStats` 增量统计表 | `stats.go`, `w_upload_stats` | Admin 统计从 O(n) → O(1) | 低 | ✅ | | 5 | 新增 `w_uploads` 复合索引 | goose migration | 清理/迁移/秒传全面加速 | 低 | ✅ | | 6 | 前端日志虚拟化 | `app-logs.tsx` | Admin 日志 Tab 流畅度质变 | 低 | ✅ | -| 7 | Admin 重模块 `dynamic()` 懒加载 | `database/page.tsx`, `logs/page.tsx`, `settings/page.tsx` 等 | 首包 JS ↓ 150–300KB | 低 | ⬜ | +| 7 | Admin 重模块 `dynamic()` 懒加载 | `database/page-client.tsx`, `logs/page-client.tsx`, `settings/page-client.tsx` | 首包 JS ↓ 150–300KB | 低 | ✅ | ### P1 — 短期(2–4 周) @@ -369,8 +370,8 @@ if (loading || !user) { | 11 | CAPTCHA 运行时配置快照 | 验证码路径配置读取 → O(1) 快照 | ✅ | | 12 | OIDC Provider/JWKS 进程内缓存(TTL 1h) | 登录延迟 ↓ 100–500ms | ⬜ | | 13 | 批量下载限制(max 50)或异步任务 | 消除网关超时风险 | ⬜ | -| 14 | Admin `useEffect` 数据获取迁移到 React Query | 去重、缓存、后台刷新 | ⬜ | -| 15 | 登录页并行请求 public config + auth sources | 登录页 ↓ 100–300ms | ⬜ | +| 14 | Admin `useEffect` 数据获取迁移到 React Query | 去重、缓存、后台刷新 | 🔶 access-logs / task-executions 已完成 | +| 15 | 登录页并行请求 public config + auth sources | 登录页 ↓ 100–300ms | ✅ | | 16 | 状态轮询在 `document.hidden` 时暂停 | 降低后台 + 客户端负载 | ⬜ | ### P2 — 中期架构演进 @@ -382,9 +383,9 @@ if (loading || !user) { | 18 | 推送通知 target 批量解析(`WHERE id IN ?`) | 通知风暴 DB 查询 ↓ N 倍 | | 19 | 上传清理改为批量 UPDATE + 异步存储删除 | 减少 DB commit 频率 | | 20 | 路由动画 0.5s → 0.15s 或纯 CSS | 导航感知速度 ↑ | -| 21 | 服务导入收窄(直接 import 具体 Service) | 每路由 bundle ↓ 10–30KB | +| 21 | ~~服务导入收窄(直接 import 具体 Service)~~ ✅ | 每路由 bundle ↓ 10–30KB | | 22 | Admin 路由级 `loading.tsx` + Suspense | 渐进式渲染体验 | -| 23 | 缩略图 `loading="lazy"` + 固定尺寸 | 文件管理页初始 paint 加速 | +| 23 | ~~缩略图 `loading="lazy"` + 固定尺寸~~ ✅ | 文件管理页初始 paint 加速 | --- @@ -427,7 +428,7 @@ if (loading || !user) { |------|-------------|-----------| | 图片站 / 公开相册 | WebP miss(锁/白名单已优化) | P0 #1 预热待做 | | 文件量 10 万+ | 清理慢(统计/索引已优化) | P2 #19 清理批量化 | -| 管理端日常使用 | 大 bundle(认证/日志已优化) | P0 #7 dynamic import | +| 管理端日常使用 | ~~大 bundle~~(dynamic 分割 + barrel 收窄已落地) | P2 #22 路由 loading.tsx | | 存储迁移进行中 | Redis 日志风暴 | P2 #17 | | 登录高峰 | OIDC discovery 无缓存 | P1 #12 OIDC | | 多租户 / 跨域前端 | CORS 仍每次调 `GetByKey`(`server_address` 已 RAM 缓存) | 可选 CORS 快照 | @@ -442,7 +443,7 @@ if (loading || !user) { 1. ~~**WebP 路径解耦**~~ ✅ `singleflight` + `RWMutex` 已落地;**下一步**:部署后预热 + miss 异步回退原图 2. ~~**文件路径查询缓存**~~ ✅ 迁移状态 + 白名单进程内缓存已落地 -3. ~~**前端认证与首屏并行化**~~ ✅ 全局 auth gate 已移除;**下一步**:Admin `dynamic()` 代码分割 +3. ~~**前端认证与首屏并行化**~~ ✅ 全局 auth gate 已移除;~~Admin `dynamic()` 代码分割~~ ✅ 已落地;**下一步**:其余 Admin 路由薄壳化 + `loading.tsx` ### 实施检查清单 @@ -458,15 +459,20 @@ P0 后端 P0 前端 [x] app-logs.tsx 虚拟滚动 ✅ 2026-06-17 -[ ] SQLConsole / Recharts / Settings Tabs dynamic import +[x] SQLConsole / Settings Tabs / Logs Tabs dynamic import ✅ 2026-06-17 [x] 认证 gate 并行化 ✅ 2026-06-17 +[x] 登录页 public config + auth sources 并行 ✅ 2026-06-17 +[x] access-logs / task-executions → React Query ✅ 2026-06-17 +[x] Users TooltipProvider 合并 ✅ 2026-06-17 +[x] 缩略图 loading="lazy" ✅ 2026-06-17 +[x] @/lib/services barrel 导入收窄 ✅ 2026-06-17 P1 [x] ListVisibleSystemConfigs Redis 缓存 ✅ 2026-06-17 [x] 系统配置 Otter RAM 缓存 + 统一失效 + pub/sub ✅ 2026-06-17 [x] CAPTCHA 运行时配置快照 ✅ 2026-06-17 [ ] OIDC Provider 缓存 -[ ] Admin useEffect → React Query 统一 +[ ] Admin useEffect → React Query 统一(database overview 等待) [ ] 状态轮询 visibility 感知 [ ] Server Component session 预取 ``` diff --git a/frontend/app/(main)/admin/database/page-client.tsx b/frontend/app/(main)/admin/database/page-client.tsx new file mode 100644 index 00000000..74737302 --- /dev/null +++ b/frontend/app/(main)/admin/database/page-client.tsx @@ -0,0 +1,330 @@ +"use client" + +import dynamic from "next/dynamic" +import * as React from "react" +import {useCallback, useEffect, useState} from "react" +import {toast} from "sonner" +import { + Activity, + Cpu, + Database, + Download, + FileText, + HardDrive, + Layers, + RefreshCw, + Server, + Terminal, +} from "lucide-react" + +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" +import {Button} from "@/components/ui/button" +import {Skeleton} from "@/components/ui/skeleton" +import {AdminService} from "@/lib/services/admin" +import {DbManageService} from "@/lib/services/db-manage" +import type {DBOverview} from "@/lib/services/db-manage" + +const sectionFallback = ( +
+) + +const TableBrowser = dynamic( + () => import("@/components/common/admin/database-table-browser").then((mod) => mod.TableBrowser), + { loading: () => sectionFallback }, +) + +const CacheManager = dynamic( + () => import("@/components/common/admin/database-cache-manager").then((mod) => mod.CacheManager), + { loading: () => sectionFallback }, +) + +const SQLConsole = dynamic( + () => import("@/components/common/admin/database-sql-console").then((mod) => mod.SQLConsole), + { ssr: false }, +) + +/** + * 格式化数字,每3位加逗号 + */ +const formatNumber = (num: number | string) => { + if (num === undefined || num === null) return "0" + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") +} + +export function DatabasePageClient() { + // 核心状态 + const [overview, setOverview] = useState(null) + const [tables, setTables] = useState([]) + const [loadingOverview, setLoadingOverview] = useState(true) + const [loadingTables, setLoadingTables] = useState(true) + const [exporting, setExporting] = useState(false) + + // 协调子组件刷新与视图切换状态 + const [showConsole, setShowConsole] = useState(false) + const [refreshTrigger, setRefreshTrigger] = useState(0) + + // 1. 获取运行概览 + const fetchOverview = useCallback(async (isSilent = false) => { + if (!isSilent) setLoadingOverview(true) + try { + const data = await DbManageService.getOverview() + setOverview(data) + } catch (err) { + toast.error("获取数据库概览失败", { + description: err instanceof Error ? err.message : "未知错误", + }) + } finally { + setLoadingOverview(false) + } + }, []) + + // 2. 获取表列表 + const fetchTables = useCallback(async () => { + setLoadingTables(true) + try { + const data = await DbManageService.listTables() + setTables(data) + } catch (err) { + toast.error("获取数据库数据表列表失败", { + description: err instanceof Error ? err.message : "未知错误", + }) + } finally { + setLoadingTables(false) + } + }, []) + + // 3. 协调刷新 + const handleRefreshAll = () => { + fetchOverview() + fetchTables() + setRefreshTrigger((prev) => prev + 1) + } + + // 4. 导出数据库备份 + const handleExport = async () => { + setExporting(true) + try { + const { blob, filename } = await AdminService.exportDatabase() + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + a.remove() + URL.revokeObjectURL(url) + toast.success("数据库导出成功", { description: `已下载归档文件: ${filename}` }) + } catch (err) { + toast.error("数据库导出失败", { + description: err instanceof Error ? err.message : "导出异常", + }) + } finally { + setExporting(false) + } + } + + // 初始化加载 + useEffect(() => { + fetchOverview() + fetchTables() + }, [fetchOverview, fetchTables]) + + // 渲染概览卡片骨架 + const renderOverviewSkeleton = () => ( +
+ {Array.from({ length: 6 }).map((_, i) => ( + + + + + + + + + ))} +
+ ) + + // 渲染 SQL 查询控台全屏视图 + if (showConsole) { + return ( + setShowConsole(false)} + /> + ) + } + + // 正常页面排版布局 + return ( +
+ {/* 顶部控制与标题 */} +
+
+ +
+

数据管理

+
+
+ +
+ + {/* 1. 概览指标卡片组 */} + {loadingOverview ? ( + renderOverviewSkeleton() + ) : ( + overview && ( +
+ {/* 卡片1: 数据库类型 */} + + + 数据库类型 + + + +
{overview.type}
+
+
+ + {/* 卡片2: 数据库版本 */} + + + 版本信息 + + + +
+ {overview.version.split(" ").slice(0, 2).join(" ")} +
+
+
+ + {/* 卡片3: 数据库名称 */} + + + 名称/路径 + + + +
+ {overview.name.substring(overview.name.lastIndexOf("/") + 1)} +
+
+
+ + {/* 卡片4: 数据库大小 */} + + + 数据库大小 + + + +
{overview.size}
+
+
+ + {/* 卡片5: 表数量 */} + + + 物理数据表 + + + +
{formatNumber(overview.table_count)}
+
+
+ + {/* 卡片6: 连接数 */} + + + 活跃连接数 + + + +
{formatNumber(overview.connections)}
+
+
+
+ ) + )} + + {/* 2. 数据表浏览器区块 */} + + + {/* 3. 缓存管理区块 */} + + + {/* 4. 底部功能卡片区 */} + + + 功能区 + 数据库导出及自定义高级 SQL 执行终端 + + +
+ {/* 功能一: 数据库导出 */} +
+
+

+ + 数据库备份导出 +

+

+ 直接导出并下载物理数据库镜像文件(SQLite 导出 .db,PostgreSQL 导出为打包的 .sql 文本文件) +

+
+ +
+ + {/* 功能二: SQL 查询终端 */} +
+
+

+ + SQL 查询控台 +

+

+ 打开在线 SQL 控制终端,执行原生的 SQL 进行数据筛选、更新、调试或性能优化 +

+
+ +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/app/(main)/admin/database/page.tsx b/frontend/app/(main)/admin/database/page.tsx index 9d9e9b4c..24d40dea 100644 --- a/frontend/app/(main)/admin/database/page.tsx +++ b/frontend/app/(main)/admin/database/page.tsx @@ -1,313 +1,5 @@ -"use client" - -import * as React from "react" -import {useCallback, useEffect, useState} from "react" -import {toast} from "sonner" -import { - Activity, - Cpu, - Database, - Download, - FileText, - HardDrive, - Layers, - RefreshCw, - Server, - Terminal, -} from "lucide-react" - -import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" -import {Button} from "@/components/ui/button" -import {Skeleton} from "@/components/ui/skeleton" -import type {DBOverview} from "@/lib/services" -import services, {AdminService} from "@/lib/services" - -import {TableBrowser} from "@/components/common/admin/database-table-browser" -import {CacheManager} from "@/components/common/admin/database-cache-manager" -import {SQLConsole} from "@/components/common/admin/database-sql-console" - -/** - * 格式化数字,每3位加逗号 - */ -const formatNumber = (num: number | string) => { - if (num === undefined || num === null) return "0" - return num.toString().replace(/\B(?=(\d{3})+(?!\B))/g, ",") -} +import {DatabasePageClient} from "./page-client" export default function DatabasePage() { - // 核心状态 - const [overview, setOverview] = useState(null) - const [tables, setTables] = useState([]) - const [loadingOverview, setLoadingOverview] = useState(true) - const [loadingTables, setLoadingTables] = useState(true) - const [exporting, setExporting] = useState(false) - - // 协调子组件刷新与视图切换状态 - const [showConsole, setShowConsole] = useState(false) - const [refreshTrigger, setRefreshTrigger] = useState(0) - - // 1. 获取运行概览 - const fetchOverview = useCallback(async (isSilent = false) => { - if (!isSilent) setLoadingOverview(true) - try { - const data = await services.dbManage.getOverview() - setOverview(data) - } catch (err) { - toast.error("获取数据库概览失败", { - description: err instanceof Error ? err.message : "未知错误", - }) - } finally { - setLoadingOverview(false) - } - }, []) - - // 2. 获取表列表 - const fetchTables = useCallback(async () => { - setLoadingTables(true) - try { - const data = await services.dbManage.listTables() - setTables(data) - } catch (err) { - toast.error("获取数据库数据表列表失败", { - description: err instanceof Error ? err.message : "未知错误", - }) - } finally { - setLoadingTables(false) - } - }, []) - - // 3. 协调刷新 - const handleRefreshAll = () => { - fetchOverview() - fetchTables() - setRefreshTrigger((prev) => prev + 1) - } - - // 4. 导出数据库备份 - const handleExport = async () => { - setExporting(true) - try { - const { blob, filename } = await AdminService.exportDatabase() - const url = URL.createObjectURL(blob) - const a = document.createElement("a") - a.href = url - a.download = filename - document.body.appendChild(a) - a.click() - a.remove() - URL.revokeObjectURL(url) - toast.success("数据库导出成功", { description: `已下载归档文件: ${filename}` }) - } catch (err) { - toast.error("数据库导出失败", { - description: err instanceof Error ? err.message : "导出异常", - }) - } finally { - setExporting(false) - } - } - - // 初始化加载 - useEffect(() => { - fetchOverview() - fetchTables() - }, [fetchOverview, fetchTables]) - - // 渲染概览卡片骨架 - const renderOverviewSkeleton = () => ( -
- {Array.from({ length: 6 }).map((_, i) => ( - - - - - - - - - ))} -
- ) - - // 渲染 SQL 查询控台全屏视图 - if (showConsole) { - return ( - setShowConsole(false)} - /> - ) - } - - // 正常页面排版布局 - return ( -
- {/* 顶部控制与标题 */} -
-
- -
-

数据管理

-
-
- -
- - {/* 1. 概览指标卡片组 */} - {loadingOverview ? ( - renderOverviewSkeleton() - ) : ( - overview && ( -
- {/* 卡片1: 数据库类型 */} - - - 数据库类型 - - - -
{overview.type}
-
-
- - {/* 卡片2: 数据库版本 */} - - - 版本信息 - - - -
- {overview.version.split(" ").slice(0, 2).join(" ")} -
-
-
- - {/* 卡片3: 数据库名称 */} - - - 名称/路径 - - - -
- {overview.name.substring(overview.name.lastIndexOf("/") + 1)} -
-
-
- - {/* 卡片4: 数据库大小 */} - - - 数据库大小 - - - -
{overview.size}
-
-
- - {/* 卡片5: 表数量 */} - - - 物理数据表 - - - -
{formatNumber(overview.table_count)}
-
-
- - {/* 卡片6: 连接数 */} - - - 活跃连接数 - - - -
{formatNumber(overview.connections)}
-
-
-
- ) - )} - - {/* 2. 数据表浏览器区块 */} - - - {/* 3. 缓存管理区块 */} - - - {/* 4. 底部功能卡片区 */} - - - 功能区 - 数据库导出及自定义高级 SQL 执行终端 - - -
- {/* 功能一: 数据库导出 */} -
-
-

- - 数据库备份导出 -

-

- 直接导出并下载物理数据库镜像文件(SQLite 导出 .db,PostgreSQL 导出为打包的 .sql 文本文件) -

-
- -
- - {/* 功能二: SQL 查询终端 */} -
-
-

- - SQL 查询控台 -

-

- 打开在线 SQL 控制终端,执行原生的 SQL 进行数据筛选、更新、调试或性能优化 -

-
- -
-
-
-
-
- ) -} + return +} \ No newline at end of file diff --git a/frontend/app/(main)/admin/logs/page-client.tsx b/frontend/app/(main)/admin/logs/page-client.tsx new file mode 100644 index 00000000..8630b455 --- /dev/null +++ b/frontend/app/(main)/admin/logs/page-client.tsx @@ -0,0 +1,66 @@ +"use client" + +import dynamic from "next/dynamic" +import {Activity, BarChart3, Terminal} from "lucide-react" + +import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" + +const tabFallback = ( +
+) + +const AccessAnalytics = dynamic( + () => import("@/components/common/admin/access-analytics").then((mod) => mod.AccessAnalytics), + { loading: () => tabFallback }, +) + +const AccessLogs = dynamic( + () => import("@/components/common/admin/access-logs").then((mod) => mod.AccessLogs), + { loading: () => tabFallback }, +) + +const AppLogs = dynamic( + () => import("@/components/common/admin/app-logs").then((mod) => mod.AppLogs), + { loading: () => tabFallback }, +) + +export function LogsPageClient() { + return ( +
+ {/* Header */} +
+ +
+

系统日志

+
+
+ {/* Tabs Layout */} + + + + + 访问分析 + + + + 用户访问日志 + + + + 应用运行日志 + + + + + + + + + + + + + +
+ ) +} \ No newline at end of file diff --git a/frontend/app/(main)/admin/logs/page.tsx b/frontend/app/(main)/admin/logs/page.tsx index 488485c3..09e44106 100644 --- a/frontend/app/(main)/admin/logs/page.tsx +++ b/frontend/app/(main)/admin/logs/page.tsx @@ -14,53 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -"use client" - -import {Activity, BarChart3, Terminal} from "lucide-react" - -import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" -import {AppLogs} from "@/components/common/admin/app-logs" -import {AccessLogs} from "@/components/common/admin/access-logs" -import {AccessAnalytics} from "@/components/common/admin/access-analytics" +import {LogsPageClient} from "./page-client" export default function LogsPage() { - return ( -
- {/* Header */} -
- -
-

系统日志

-
-
- {/* Tabs Layout */} - - - - - 访问分析 - - - - 用户访问日志 - - - - 应用运行日志 - - - - - - - - - - - - - -
- ) -} - + return +} \ No newline at end of file diff --git a/frontend/app/(main)/admin/settings/page-client.tsx b/frontend/app/(main)/admin/settings/page-client.tsx new file mode 100644 index 00000000..f09c8365 --- /dev/null +++ b/frontend/app/(main)/admin/settings/page-client.tsx @@ -0,0 +1,144 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +"use client" + +import dynamic from "next/dynamic" +import {useEffect, useMemo} from "react" +import {useQuery} from "@tanstack/react-query" +import {Loader2, Settings} from "lucide-react" +import {useRouter} from "next/navigation" +import {motion} from "motion/react" + +import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" +import {useAuth} from "@/components/providers/auth-provider" +import {AdminService} from "@/lib/services/admin" +import type {SystemConfig} from "@/lib/services/admin" + +const tabFallback = ( +
+) + +const SecurityTab = dynamic( + () => import("@/components/common/settings/security-tab").then((mod) => mod.SecurityTab), + { loading: () => tabFallback }, +) + +const OperationTab = dynamic( + () => import("@/components/common/settings/operation-tab").then((mod) => mod.OperationTab), + { loading: () => tabFallback }, +) + +const SystemTab = dynamic( + () => import("@/components/common/settings/system-tab").then((mod) => mod.SystemTab), + { loading: () => tabFallback }, +) + +const OtherTab = dynamic( + () => import("@/components/common/settings/other-tab").then((mod) => mod.OtherTab), + { loading: () => tabFallback }, +) + +const InfoTab = dynamic( + () => import("@/components/common/settings/info-tab").then((mod) => mod.InfoTab), + { loading: () => tabFallback }, +) + +const SystemStatusManager = dynamic( + () => import("@/components/common/admin/status").then((mod) => mod.SystemStatusManager), + { loading: () => tabFallback }, +) + +function systemConfigMap(configs: SystemConfig[]) { + return configs.reduce>((accumulator, config) => { + accumulator[config.key] = config + return accumulator + }, {}) +} + +export function AdminSettingsPageClient() { + const { user, loading } = useAuth() + const router = useRouter() + + const systemConfigsQuery = useQuery({ + queryKey: ["admin", "system-configs"], + queryFn: () => AdminService.listSystemConfigs("system"), + enabled: !!user?.is_admin, + }) + + const configs = useMemo( + () => systemConfigMap(systemConfigsQuery.data ?? []), + [systemConfigsQuery.data], + ) + + useEffect(() => { + if (!loading && (!user || !user.is_admin)) { + router.replace("/settings/profile") + } + }, [user, loading, router]) + + if (loading || !user || !user.is_admin) { + return ( +
+ +
+ ) + } + + return ( + +
+ +
+

系统设置

+
+
+ + + + 安全设置 + + + 业务设置 + + + 系统设置 + + + 其他设置 + + + 系统状态 + + + 系统信息 + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} \ No newline at end of file diff --git a/frontend/app/(main)/admin/settings/page.tsx b/frontend/app/(main)/admin/settings/page.tsx index 0f71c5bb..44be3884 100644 --- a/frontend/app/(main)/admin/settings/page.tsx +++ b/frontend/app/(main)/admin/settings/page.tsx @@ -1,117 +1,8 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -"use client" - -import {useEffect, useMemo} from "react" -import {useQuery} from "@tanstack/react-query" -import {Loader2, Settings} from "lucide-react" -import {useRouter} from "next/navigation" -import {motion} from "motion/react" - -import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" -import {useAuth} from "@/components/providers/auth-provider" -import {AdminService} from "@/lib/services" -import type {SystemConfig} from "@/lib/services/admin" -import {SystemStatusManager} from "@/components/common/admin/status" - -// 引入位于 components/common/settings/ 目录下的具体业务 Tab -import {SecurityTab} from "@/components/common/settings/security-tab" -import {OperationTab} from "@/components/common/settings/operation-tab" -import {SystemTab} from "@/components/common/settings/system-tab" -import {OtherTab} from "@/components/common/settings/other-tab" -import {InfoTab} from "@/components/common/settings/info-tab" - -function systemConfigMap(configs: SystemConfig[]) { - return configs.reduce>((accumulator, config) => { - accumulator[config.key] = config - return accumulator - }, {}) -} +import {AdminSettingsPageClient} from "./page-client" export default function AdminSettingsPage() { - const { user, loading } = useAuth() - const router = useRouter() - - const systemConfigsQuery = useQuery({ - queryKey: ["admin", "system-configs"], - queryFn: () => AdminService.listSystemConfigs("system"), - enabled: !!user?.is_admin, - }) - - const configs = useMemo( - () => systemConfigMap(systemConfigsQuery.data ?? []), - [systemConfigsQuery.data], - ) - - useEffect(() => { - if (!loading && (!user || !user.is_admin)) { - router.replace("/settings/profile") - } - }, [user, loading, router]) - - if (loading || !user || !user.is_admin) { - return ( -
- -
- ) - } - - return ( - -
- -
-

系统设置

-
-
- - - - 安全设置 - - - 业务设置 - - - 系统设置 - - - 其他设置 - - - 系统状态 - - - 系统信息 - - - - - - - - - - - - - - - - - - - - - - -
- ) -} + return +} \ No newline at end of file diff --git a/frontend/app/(main)/admin/system/page.tsx b/frontend/app/(main)/admin/system/page.tsx index fb6f54ca..83ef1332 100644 --- a/frontend/app/(main)/admin/system/page.tsx +++ b/frontend/app/(main)/admin/system/page.tsx @@ -8,7 +8,7 @@ import {Tabs, TabsList, TabsTrigger} from "@/components/ui/tabs" import {ShieldCheck} from "lucide-react" import {formatDateTime} from "@/lib/utils" -import type {SystemConfig} from "@/lib/services" +import type {SystemConfig} from "@/lib/services/admin" import {AdminProvider, useAdmin} from "@/contexts/admin-context" diff --git a/frontend/app/(main)/admin/users/page.tsx b/frontend/app/(main)/admin/users/page.tsx index fb4ce452..8d43221b 100644 --- a/frontend/app/(main)/admin/users/page.tsx +++ b/frontend/app/(main)/admin/users/page.tsx @@ -19,7 +19,7 @@ import { AlertDialogTitle } from "@/components/ui/alert-dialog" -import {AdminUser} from "@/lib/services" +import type {AdminUser} from "@/lib/services/admin" import {formatDateTime} from "@/lib/utils" import {EmptyStateWithBorder} from "@/components/layout/empty" import {LoadingStateWithBorder} from "@/components/layout/loading" @@ -120,6 +120,7 @@ export default function UsersPage() { ) : (
+ @@ -174,7 +175,6 @@ export default function UsersPage() { e.stopPropagation()}>
-
@@ -190,9 +190,7 @@ export default function UsersPage() { {user.is_admin ? '管理员账户' : user.is_active ? '禁用账户' : '启用账户'} - -
@@ -230,6 +225,7 @@ export default function UsersPage() { ))}
+
)} diff --git a/frontend/components/auth/login-form.tsx b/frontend/components/auth/login-form.tsx index c89fd715..97784f2a 100644 --- a/frontend/components/auth/login-form.tsx +++ b/frontend/components/auth/login-form.tsx @@ -16,7 +16,8 @@ import {Field, FieldGroup, FieldLabel} from "@/components/ui/field" import {CapWidget} from "@/components/auth/cap-widget" import {AuthHeading} from "@/components/auth/auth-shell" import {OTPForm} from "./otp-form" -import services from "@/lib/services" +import {AuthService} from "@/lib/services/auth" +import {ConfigService} from "@/lib/services/config" import type {LoginRequest} from "@/lib/services/auth/types" import {safeRedirectTarget} from "@/lib/utils" @@ -64,13 +65,12 @@ export function LoginForm({ onOTPStateChange }: { onOTPStateChange?: (show: bool const publicConfigQuery = useQuery({ queryKey: ["public-config"], - queryFn: () => services.config.getPublicConfig(), + queryFn: () => ConfigService.getPublicConfig(), }) const authSourcesQuery = useQuery({ queryKey: ["auth-sources"], - queryFn: () => services.auth.getAuthSources(), - enabled: configBool(publicConfigQuery.data?.oidc_login_enabled, true), + queryFn: () => AuthService.getAuthSources(), }) const capEnabled = configBool(publicConfigQuery.data?.cap_login_enabled, false) @@ -85,7 +85,7 @@ export function LoginForm({ onOTPStateChange }: { onOTPStateChange?: (show: bool capTokenRef.current = null setCapReady(false) } - return services.auth.login(req, Object.keys(headers).length ? headers : undefined) + return AuthService.login(req, Object.keys(headers).length ? headers : undefined) }, onSuccess: (user) => { setUser(user) @@ -170,7 +170,7 @@ export function LoginForm({ onOTPStateChange }: { onOTPStateChange?: (show: bool try { setErrorMessage("") persistRedirectTarget(searchParams) - const { authorize_url } = await services.auth.getAuthorizeUrl(sourceName) + const { authorize_url } = await AuthService.getAuthorizeUrl(sourceName) window.location.href = authorize_url } catch (error) { toast.error(error instanceof Error ? error.message : "第三方登录失败") @@ -194,7 +194,8 @@ export function LoginForm({ onOTPStateChange }: { onOTPStateChange?: (show: bool configBool(publicConfigQuery.data?.password_register_enabled, true) const passwordLoginEnabled = configBool(publicConfigQuery.data?.password_login_enabled, true) - const authSources = authSourcesQuery.data ?? [] + const oidcLoginEnabled = configBool(publicConfigQuery.data?.oidc_login_enabled, true) + const authSources = oidcLoginEnabled ? (authSourcesQuery.data ?? []) : [] const loginDisabled = !passwordLoginEnabled || diff --git a/frontend/components/auth/login-page.tsx b/frontend/components/auth/login-page.tsx index 52a4ba14..11fe7b09 100644 --- a/frontend/components/auth/login-page.tsx +++ b/frontend/components/auth/login-page.tsx @@ -9,7 +9,7 @@ import {LoginForm} from "@/components/auth/login-form" import {AuthShell} from "@/components/auth/auth-shell" import {Check} from "lucide-react" -import services from "@/lib/services" +import {AuthService} from "@/lib/services/auth" import {useAuth} from "@/components/providers/auth-provider" import {safeRedirectTarget} from "@/lib/utils" @@ -84,7 +84,7 @@ export function LoginPage() { setIsProcessingCallback(true) try { - const result = await services.auth.handleCallback({ state, code }) + const result = await AuthService.handleCallback({ state, code }) if (result.status === "need_bind") { toast.info("您的第三方账号未绑定本地账号,系统已关闭注册。请登录已有本地账号进行绑定。") setIsProcessingCallback(false) diff --git a/frontend/components/auth/register-form.tsx b/frontend/components/auth/register-form.tsx index e9008542..d5112c98 100644 --- a/frontend/components/auth/register-form.tsx +++ b/frontend/components/auth/register-form.tsx @@ -13,7 +13,8 @@ import {Spinner} from "@/components/ui/spinner" import {Field, FieldGroup, FieldLabel} from "@/components/ui/field" import {AuthHeading} from "@/components/auth/auth-shell" import {CapWidget} from "@/components/auth/cap-widget" -import services from "@/lib/services" +import {AuthService} from "@/lib/services/auth" +import {ConfigService} from "@/lib/services/config" import type {RegisterRequest} from "@/lib/services/auth/types" import {safeRedirectTarget} from "@/lib/utils" @@ -53,7 +54,7 @@ export function RegisterForm() { const publicConfigQuery = useQuery({ queryKey: ["public-config"], - queryFn: () => services.config.getPublicConfig(), + queryFn: () => ConfigService.getPublicConfig(), }) const redirectTarget = useMemo( @@ -110,7 +111,7 @@ export function RegisterForm() { capTokenRef.current = null setCapReady(false) } - return services.auth.register(req, Object.keys(headers).length ? headers : undefined) + return AuthService.register(req, Object.keys(headers).length ? headers : undefined) }, onSuccess: (user) => { setUser(user) @@ -135,7 +136,7 @@ export function RegisterForm() { capTokenRef.current = null setCapReady(false) } - return services.auth.sendEmailCode(targetEmail, "register", Object.keys(headers).length ? headers : undefined) + return AuthService.sendEmailCode(targetEmail, "register", Object.keys(headers).length ? headers : undefined) }, onSuccess: () => { setRegisterCooldown(60) diff --git a/frontend/components/common/admin/access-analytics.tsx b/frontend/components/common/admin/access-analytics.tsx index 8421ebfa..cb9f7e4c 100644 --- a/frontend/components/common/admin/access-analytics.tsx +++ b/frontend/components/common/admin/access-analytics.tsx @@ -20,7 +20,7 @@ import {useCallback, useEffect, useState} from "react" import {BarChart3, Globe, RefreshCw, TrendingUp, Users, XCircle} from "lucide-react" import {Area, AreaChart, CartesianGrid, XAxis, YAxis} from "recharts" -import {AdminService} from "@/lib/services" +import {AdminService} from "@/lib/services/admin" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import {EmptyStateWithBorder} from "@/components/layout/empty" diff --git a/frontend/components/common/admin/access-logs.tsx b/frontend/components/common/admin/access-logs.tsx index 2341e909..df20fdd1 100644 --- a/frontend/components/common/admin/access-logs.tsx +++ b/frontend/components/common/admin/access-logs.tsx @@ -16,12 +16,13 @@ limitations under the License. "use client" -import {useCallback, useEffect, useState} from "react" +import {useMemo, useState} from "react" +import {useQuery} from "@tanstack/react-query" import {toast} from "sonner" import {format} from "date-fns" import {Activity, ChevronLeft, ChevronRight, Copy, Eye, RotateCcw, Search, XCircle} from "lucide-react" -import {AdminService} from "@/lib/services" +import {AdminService} from "@/lib/services/admin" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import {EmptyStateWithBorder} from "@/components/layout/empty" @@ -47,6 +48,8 @@ interface AccessLog { created_at: string } +const PAGE_SIZE = 15 + function formatDateTime(value?: string | null) { if (!value) return "-" const date = new Date(value) @@ -66,15 +69,13 @@ function statusVariant(status: number) { return "secondary" } -export function AccessLogs() { - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [clickhouseDisabled, setClickhouseDisabled] = useState(false) +function isClickhouseDisabledError(error: Error) { + const errMsg = error.message || "" + return errMsg.includes("ClickHouse") || errMsg.includes("未启用") +} - const [logs, setLogs] = useState([]) - const [total, setTotal] = useState(0) +export function AccessLogs() { const [page, setPage] = useState(1) - const pageSize = 15 // 搜索过滤条件 const [usernameFilter, setUsernameFilter] = useState("") @@ -93,12 +94,9 @@ export function AccessLogs() { const [selectedLog, setSelectedLog] = useState(null) const [detailOpen, setDetailOpen] = useState(false) - const fetchAccessLogs = useCallback(async (currentPage: number = page) => { - try { - setLoading(true) - setError(null) - - // 转换起止时间格式 + const logsQuery = useQuery({ + queryKey: ["admin", "access-logs", page, searchParams], + queryFn: async () => { let startISO = "" if (searchParams.start_time) { startISO = new Date(searchParams.start_time).toISOString() @@ -108,34 +106,25 @@ export function AccessLogs() { endISO = new Date(searchParams.end_time).toISOString() } - const data = await AdminService.getAccessLogs({ - page: currentPage, - page_size: pageSize, + return AdminService.getAccessLogs({ + page, + page_size: PAGE_SIZE, username: searchParams.username || undefined, path: searchParams.path || undefined, start_time: startISO || undefined, end_time: endISO || undefined, }) + }, + retry: false, + }) - setLogs(data.list || []) - setTotal(data.total || 0) - setClickhouseDisabled(false) - } catch (err) { - const errorInstance = err instanceof Error ? err : new Error("获取访问日志失败") - const errMsg = errorInstance.message || "" - if (errMsg.includes("ClickHouse") || errMsg.includes("未启用")) { - setClickhouseDisabled(true) - } else { - setError(errorInstance) - } - } finally { - setLoading(false) - } - }, [page, searchParams]) + const logs = logsQuery.data?.list ?? [] + const total = logsQuery.data?.total ?? 0 + const loading = logsQuery.isPending || logsQuery.isFetching + const clickhouseDisabled = logsQuery.isError && isClickhouseDisabledError(logsQuery.error) + const error = logsQuery.isError && !clickhouseDisabled ? logsQuery.error : null - useEffect(() => { - fetchAccessLogs() - }, [fetchAccessLogs]) + const totalPages = useMemo(() => Math.max(1, Math.ceil(total / PAGE_SIZE)), [total]) const handleSearch = (e: React.FormEvent) => { e.preventDefault() @@ -164,7 +153,6 @@ export function AccessLogs() { const handlePageChange = (newPage: number) => { setPage(newPage) - fetchAccessLogs(newPage) } const copyToClipboard = (text: string, subject: string) => { @@ -172,8 +160,6 @@ export function AccessLogs() { toast.success(`${subject}已复制到剪贴板`) } - const totalPages = Math.max(1, Math.ceil(total / pageSize)) - // Prettify Headers JSON string const getPrettyHeaders = (headersRaw?: string) => { if (!headersRaw) return "暂无头部数据" @@ -198,7 +184,7 @@ export function AccessLogs() { } if (error) { - return fetchAccessLogs(1)} /> + return void logsQuery.refetch()} /> } return ( @@ -465,4 +451,4 @@ export function AccessLogs() {
) -} +} \ No newline at end of file diff --git a/frontend/components/common/admin/app-logs.tsx b/frontend/components/common/admin/app-logs.tsx index 2045e4cd..93eed714 100644 --- a/frontend/components/common/admin/app-logs.tsx +++ b/frontend/components/common/admin/app-logs.tsx @@ -21,7 +21,7 @@ import {useVirtualizer} from "@tanstack/react-virtual" import {toast} from "sonner" import {ArrowDown, ChevronUp, Loader2, Pause, Play} from "lucide-react" -import {AdminService} from "@/lib/services" +import {AdminService} from "@/lib/services/admin" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import {Badge} from "@/components/ui/badge" diff --git a/frontend/components/common/admin/database-cache-manager.tsx b/frontend/components/common/admin/database-cache-manager.tsx index 8547ddde..5b854a53 100644 --- a/frontend/components/common/admin/database-cache-manager.tsx +++ b/frontend/components/common/admin/database-cache-manager.tsx @@ -19,8 +19,8 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog" -import type {CacheStatus} from "@/lib/services" -import {AdminService} from "@/lib/services" +import type {CacheStatus} from "@/lib/services/admin" +import {AdminService} from "@/lib/services/admin" /** * 格式化字节大小 diff --git a/frontend/components/common/admin/database-sql-console.tsx b/frontend/components/common/admin/database-sql-console.tsx index 99a2767d..2d896534 100644 --- a/frontend/components/common/admin/database-sql-console.tsx +++ b/frontend/components/common/admin/database-sql-console.tsx @@ -11,8 +11,8 @@ import {ArrowLeft, Play, RefreshCw, Terminal, Trash2,} from "lucide-react" import {Button} from "@/components/ui/button" import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select" import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table" -import type {ExecuteSQLResponse} from "@/lib/services" -import services from "@/lib/services" +import type {ExecuteSQLResponse} from "@/lib/services/db-manage" +import {DbManageService} from "@/lib/services/db-manage" /** * 格式化表格单元格内容 @@ -81,7 +81,7 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) { setSqlResult(null) setSqlError(null) try { - const result = await services.dbManage.executeSQL(sqlQuery) + const result = await DbManageService.executeSQL(sqlQuery) setSqlResult(result) toast.success("SQL 执行成功") } catch (err) { diff --git a/frontend/components/common/admin/database-table-browser.tsx b/frontend/components/common/admin/database-table-browser.tsx index 0ba1a52b..9a73f308 100644 --- a/frontend/components/common/admin/database-table-browser.tsx +++ b/frontend/components/common/admin/database-table-browser.tsx @@ -10,8 +10,8 @@ import {Skeleton} from "@/components/ui/skeleton" import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select" import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table" import {Button} from "@/components/ui/button" -import type {TableDataResponse} from "@/lib/services" -import services from "@/lib/services" +import type {TableDataResponse} from "@/lib/services/db-manage" +import {DbManageService} from "@/lib/services/db-manage" /** * 格式化数字,每3位加逗号 @@ -55,7 +55,7 @@ export function TableBrowser({ tables, loadingTables, refreshTrigger }: TableBro if (!tableName) return setLoadingData(true) try { - const data = await services.dbManage.getTableData({ + const data = await DbManageService.getTableData({ table: tableName, page: targetPage, pageSize: size, diff --git a/frontend/components/common/admin/file-list.tsx b/frontend/components/common/admin/file-list.tsx index 51d89c2c..6d2ce1c3 100644 --- a/frontend/components/common/admin/file-list.tsx +++ b/frontend/components/common/admin/file-list.tsx @@ -292,6 +292,8 @@ export function FileList() { {file.file_name} { ;(e.currentTarget as HTMLImageElement).style.display = "none" @@ -408,6 +410,8 @@ export function FileList() { {detailTarget.file_name} ) : ( diff --git a/frontend/components/common/admin/push/events-tab.tsx b/frontend/components/common/admin/push/events-tab.tsx index 774e6178..857ee964 100644 --- a/frontend/components/common/admin/push/events-tab.tsx +++ b/frontend/components/common/admin/push/events-tab.tsx @@ -33,7 +33,8 @@ import {LoadingStateWithBorder} from "@/components/layout/loading" import {EmptyStateWithBorder} from "@/components/layout/empty" -import services from "@/lib/services" +import {AdminService} from "@/lib/services/admin" +import {PushService} from "@/lib/services/push" import type {CreatePushEventRequest, PushEvent, UpdatePushEventRequest,} from "@/lib/services/push" export function EventsTab() { @@ -42,7 +43,7 @@ export function EventsTab() { // --- 获取所有自定义消息通道 --- const channelsQuery = useQuery({ queryKey: ["admin", "push-channels"], - queryFn: () => services.push.listChannels(), + queryFn: () => PushService.listChannels(), }) const availableChannels = React.useMemo(() => { @@ -55,23 +56,23 @@ export function EventsTab() { // --- 获取通知事件 --- const eventsQuery = useQuery({ queryKey: ["admin", "push-events"], - queryFn: () => services.push.listEvents(), + queryFn: () => PushService.listEvents(), }) const builtInEventsQuery = useQuery({ queryKey: ["admin", "push-builtin-events"], - queryFn: () => services.push.listBuiltInEvents(), + queryFn: () => PushService.listBuiltInEvents(), }) // --- 获取所有系统可调度任务类型 --- const taskTypesQuery = useQuery({ queryKey: ["admin", "task-types"], - queryFn: () => services.admin.getTaskTypes(), + queryFn: () => AdminService.getTaskTypes(), }) // --- 修改保存事件 Mutation --- const updateEventMutation = useMutation({ - mutationFn: ({ id, data }: { id: number; data: UpdatePushEventRequest }) => services.push.updateEvent(id, data), + mutationFn: ({ id, data }: { id: number; data: UpdatePushEventRequest }) => PushService.updateEvent(id, data), onSuccess: () => { toast.success("事件更新成功") queryClient.invalidateQueries({ queryKey: ["admin", "push-events"] }) @@ -83,7 +84,7 @@ export function EventsTab() { }) const toggleEventMutation = useMutation({ - mutationFn: (id: number) => services.push.toggleEvent(id), + mutationFn: (id: number) => PushService.toggleEvent(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["admin", "push-events"] }) }, @@ -93,7 +94,7 @@ export function EventsTab() { }) const createEventMutation = useMutation({ - mutationFn: (data: CreatePushEventRequest) => services.push.createEvent(data), + mutationFn: (data: CreatePushEventRequest) => PushService.createEvent(data), onSuccess: () => { toast.success("事件创建成功") queryClient.invalidateQueries({ queryKey: ["admin", "push-events"] }) @@ -110,7 +111,7 @@ export function EventsTab() { }) const deleteEventMutation = useMutation({ - mutationFn: (id: number) => services.push.deleteEvent(id), + mutationFn: (id: number) => PushService.deleteEvent(id), onSuccess: () => { toast.success("配置删除成功") queryClient.invalidateQueries({ queryKey: ["admin", "push-events"] }) diff --git a/frontend/components/common/admin/push/histories-tab.tsx b/frontend/components/common/admin/push/histories-tab.tsx index 4849c99c..e651b02a 100644 --- a/frontend/components/common/admin/push/histories-tab.tsx +++ b/frontend/components/common/admin/push/histories-tab.tsx @@ -26,7 +26,7 @@ import { DialogTitle, } from "@/components/ui/dialog" -import services from "@/lib/services" +import {PushService} from "@/lib/services/push" import type {PushHistory} from "@/lib/services/push" function getLevelBadgeVariant(level: string): "outline" | "secondary" | "destructive" | "default" { @@ -56,7 +56,7 @@ export function HistoriesTab() { const historiesQuery = useQuery({ queryKey: ["admin", "push-histories", historyPage, historySearch, historyStatus], queryFn: () => - services.push.listHistories({ + PushService.listHistories({ page: historyPage, page_size: 10, event_key: historySearch || undefined, diff --git a/frontend/components/common/admin/push/settings-tab.tsx b/frontend/components/common/admin/push/settings-tab.tsx index 2dae2721..2c921cd6 100644 --- a/frontend/components/common/admin/push/settings-tab.tsx +++ b/frontend/components/common/admin/push/settings-tab.tsx @@ -29,7 +29,7 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/ import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" -import services from "@/lib/services" +import {PushService} from "@/lib/services/push" import type {ChannelDefinition, CreateChannelRequest, PushChannel, UpdateChannelRequest} from "@/lib/services/push" export function SettingsTab() { @@ -38,19 +38,19 @@ export function SettingsTab() { // --- 获取所有自定义消息通道 --- const channelsQuery = useQuery({ queryKey: ["admin", "push-channels"], - queryFn: () => services.push.listChannels(), + queryFn: () => PushService.listChannels(), }) // --- 获取动态通道表单字段定义 --- const definitionsQuery = useQuery({ queryKey: ["admin", "push-channels-definitions"], - queryFn: () => services.push.listChannelDefinitions(), + queryFn: () => PushService.listChannelDefinitions(), }) // --- 消息通道 CRUD Mutations --- const createChannelMutation = useMutation({ - mutationFn: (data: CreateChannelRequest) => services.push.createChannel(data), + mutationFn: (data: CreateChannelRequest) => PushService.createChannel(data), onSuccess: () => { toast.success("通道创建成功") queryClient.invalidateQueries({ queryKey: ["admin", "push-channels"] }) @@ -63,7 +63,7 @@ export function SettingsTab() { const updateChannelMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: UpdateChannelRequest }) => - services.push.updateChannel(id, data), + PushService.updateChannel(id, data), onSuccess: () => { toast.success("通道更新成功") queryClient.invalidateQueries({ queryKey: ["admin", "push-channels"] }) @@ -75,7 +75,7 @@ export function SettingsTab() { }) const deleteChannelMutation = useMutation({ - mutationFn: (id: number) => services.push.deleteChannel(id), + mutationFn: (id: number) => PushService.deleteChannel(id), onSuccess: () => { toast.success("通道删除成功") queryClient.invalidateQueries({ queryKey: ["admin", "push-channels"] }) @@ -237,7 +237,7 @@ export function SettingsTab() { try { setIsTestingChannel(true) toast.info("正在发送测试推送...") - await services.push.testChannel({ + await PushService.testChannel({ name: testChannelName, target: testChannelTarget || undefined, }) diff --git a/frontend/components/common/admin/status.tsx b/frontend/components/common/admin/status.tsx index daf2d4a6..b85896ce 100644 --- a/frontend/components/common/admin/status.tsx +++ b/frontend/components/common/admin/status.tsx @@ -11,8 +11,8 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/ import {Badge} from "@/components/ui/badge" import {Progress} from "@/components/ui/progress" import {Skeleton} from "@/components/ui/skeleton" -import type {SystemStatus} from "@/lib/services" -import services from "@/lib/services" +import type {SystemStatus} from "@/lib/services/admin" +import {AdminService} from "@/lib/services/admin" /** * 格式化数字,每3位加逗号 @@ -40,7 +40,7 @@ export function SystemStatusManager() { const fetchStatus = useCallback(async (isSilent = false) => { if (!isSilent) setWavelet(true) try { - const data = await services.admin.getSystemStatus() + const data = await AdminService.getSystemStatus() // 检测变化的字段用于微动画效果 if (prevStatusRef.current) { diff --git a/frontend/components/common/admin/task-executions.tsx b/frontend/components/common/admin/task-executions.tsx index c7250960..aab06abd 100644 --- a/frontend/components/common/admin/task-executions.tsx +++ b/frontend/components/common/admin/task-executions.tsx @@ -1,11 +1,13 @@ "use client" -import {useCallback, useEffect, useState} from "react" +import {useMemo, useState} from "react" +import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query" import {toast} from "sonner" import {format} from "date-fns" import {Activity, ChevronLeft, ChevronRight, RefreshCw, RotateCcw} from "lucide-react" -import {AdminService, TaskExecution, TaskExecutionStatus, TaskMeta} from "@/lib/services" +import {AdminService} from "@/lib/services/admin" +import type {TaskExecution, TaskExecutionStatus, TaskMeta} from "@/lib/services/admin" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import {EmptyStateWithBorder} from "@/components/layout/empty" @@ -31,6 +33,8 @@ const TRIGGER_LABELS: Record = { schedule: "定时", } +const PAGE_SIZE = 10 + function formatDateTime(value?: string | null) { if (!value) return "-" const date = new Date(value) @@ -51,108 +55,85 @@ function statusVariant(status: TaskExecutionStatus) { } export function TaskExecutionsManager() { - const [taskTypes, setTaskTypes] = useState([]) - const [executionsLoading, setExecutionsLoading] = useState(false) - const [executionsError, setExecutionsError] = useState(null) - const [executions, setExecutions] = useState([]) - const [executionsTotal, setExecutionsTotal] = useState(0) + const queryClient = useQueryClient() const [executionsPage, setExecutionsPage] = useState(1) const [executionStatus, setExecutionStatus] = useState("all") const [executionTaskType, setExecutionTaskType] = useState("all") - const [selectedExecution, setSelectedExecution] = useState(null) + const [selectedExecutionId, setSelectedExecutionId] = useState(null) + const [executionPreview, setExecutionPreview] = useState(null) const [detailOpen, setDetailOpen] = useState(false) - const [detailLoading, setDetailLoading] = useState(false) - const [retrying, setRetrying] = useState(false) - const fetchTaskTypes = useCallback(async () => { - try { - const data = await AdminService.getTaskTypes() - setTaskTypes(data) - } catch { - setTaskTypes([]) - } - }, []) + const taskTypesQuery = useQuery({ + queryKey: ["admin", "task-types"], + queryFn: () => AdminService.getTaskTypes(), + }) - const fetchTaskExecutions = useCallback(async (overrides?: { - page?: number - status?: TaskExecutionStatus | "all" - taskType?: string - }) => { - try { - setExecutionsLoading(true) - setExecutionsError(null) - const page = overrides?.page ?? executionsPage - const status = overrides?.status ?? executionStatus - const taskType = overrides?.taskType ?? executionTaskType - const data = await AdminService.listTaskExecutions({ - page, - page_size: 10, - status: status === "all" ? undefined : status, - task_type: taskType === "all" ? undefined : taskType, - }) - setExecutions(data.items || []) - setExecutionsTotal(data.total || 0) - } catch (err) { - setExecutionsError(err instanceof Error ? err : new Error("加载任务执行记录失败")) - } finally { - setExecutionsLoading(false) - } - }, [executionStatus, executionTaskType, executionsPage]) + const executionsQuery = useQuery({ + queryKey: ["admin", "task-executions", executionsPage, executionStatus, executionTaskType], + queryFn: () => AdminService.listTaskExecutions({ + page: executionsPage, + page_size: PAGE_SIZE, + status: executionStatus === "all" ? undefined : executionStatus, + task_type: executionTaskType === "all" ? undefined : executionTaskType, + }), + }) - useEffect(() => { - fetchTaskTypes() - }, [fetchTaskTypes]) + const detailQuery = useQuery({ + queryKey: ["admin", "task-execution", selectedExecutionId], + queryFn: () => AdminService.getTaskExecution(selectedExecutionId!), + enabled: detailOpen && !!selectedExecutionId, + }) - useEffect(() => { - fetchTaskExecutions() - }, [fetchTaskExecutions]) - - useEffect(() => { - setExecutionsPage(1) - }, [executionStatus, executionTaskType]) - - const openExecutionDetail = async (execution: TaskExecution) => { - setSelectedExecution(execution) - setDetailOpen(true) - try { - setDetailLoading(true) - const detail = await AdminService.getTaskExecution(execution.id) - setSelectedExecution(detail) - } catch (err) { - toast.error("任务详情加载失败", { - description: err instanceof Error ? err.message : "未知错误", - }) - } finally { - setDetailLoading(false) - } - } - - const handleRetryExecution = async () => { - if (!selectedExecution) return - try { - setRetrying(true) - const taskID = await AdminService.retryTaskExecution(selectedExecution.id) + const retryMutation = useMutation({ + mutationFn: (id: string) => AdminService.retryTaskExecution(id), + onSuccess: (taskID) => { toast.success("任务已重新下发", { description: `新任务 ID:${ taskID }`, }) - await fetchTaskExecutions() - } catch (err) { + void queryClient.invalidateQueries({ queryKey: ["admin", "task-executions"] }) + }, + onError: (err: Error) => { toast.error("任务重试失败", { - description: err instanceof Error ? err.message : "未知错误", + description: err.message || "未知错误", }) - } finally { - setRetrying(false) - } + }, + }) + + const taskTypes: TaskMeta[] = taskTypesQuery.data ?? [] + const executions = executionsQuery.data?.items ?? [] + const executionsTotal = executionsQuery.data?.total ?? 0 + const executionsLoading = executionsQuery.isPending || executionsQuery.isFetching + const executionsError = executionsQuery.isError ? executionsQuery.error : null + const selectedExecution = detailQuery.data ?? executionPreview + const detailLoading = detailQuery.isFetching + + const totalPages = useMemo( + () => Math.max(1, Math.ceil(executionsTotal / PAGE_SIZE)), + [executionsTotal], + ) + + const openExecutionDetail = (execution: TaskExecution) => { + setExecutionPreview(execution) + setSelectedExecutionId(execution.id) + setDetailOpen(true) } - const totalPages = Math.max(1, Math.ceil(executionsTotal / 10)) + const handleStatusChange = (value: TaskExecutionStatus | "all") => { + setExecutionStatus(value) + setExecutionsPage(1) + } + + const handleTaskTypeChange = (value: string) => { + setExecutionTaskType(value) + setExecutionsPage(1) + } return (

- handleStatusChange(value as TaskExecutionStatus | "all")}> @@ -164,7 +145,7 @@ export function TaskExecutionsManager() { 失败 - @@ -175,7 +156,7 @@ export function TaskExecutionsManager() { ))} - @@ -184,7 +165,7 @@ export function TaskExecutionsManager() { {executionsError ? (
- + void executionsQuery.refetch()} className="justify-center" />
) : executionsLoading && executions.length === 0 ? ( @@ -375,13 +356,13 @@ export function TaskExecutionsManager() {
- {selectedExecution && selectedExecution.status === "failed" && selectedExecution.retryable && ( - )} @@ -390,4 +371,4 @@ export function TaskExecutionsManager() {
) -} +} \ No newline at end of file diff --git a/frontend/components/common/admin/task-manager.tsx b/frontend/components/common/admin/task-manager.tsx index 66b0a071..b7053979 100644 --- a/frontend/components/common/admin/task-manager.tsx +++ b/frontend/components/common/admin/task-manager.tsx @@ -11,7 +11,7 @@ import {Spinner} from "@/components/ui/spinner" import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from "@/components/ui/dialog" import {Calendar as CalendarIcon, Clock, Info, Layers, Play} from "lucide-react" -import {AdminService, DispatchTaskRequest, TaskMeta} from "@/lib/services" +import {AdminService, DispatchTaskRequest, TaskMeta} from "@/lib/services/admin" import {buildTaskPayload} from "@/lib/task-param-utils" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" diff --git a/frontend/components/common/admin/task-schedules.tsx b/frontend/components/common/admin/task-schedules.tsx index 454a0ffd..e21bf227 100644 --- a/frontend/components/common/admin/task-schedules.tsx +++ b/frontend/components/common/admin/task-schedules.tsx @@ -12,7 +12,7 @@ import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, Di import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table" import {Clock, Edit2, Info, Plus, RefreshCw, Trash2} from "lucide-react" -import {AdminService, CreateScheduleRequest, Schedule, TaskMeta, UpdateScheduleRequest} from "@/lib/services" +import {AdminService, CreateScheduleRequest, Schedule, TaskMeta, UpdateScheduleRequest} from "@/lib/services/admin" import {buildTaskPayload} from "@/lib/task-param-utils" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" diff --git a/frontend/components/common/admin/user-detail-sheet.tsx b/frontend/components/common/admin/user-detail-sheet.tsx index 80024bc2..662b5b72 100644 --- a/frontend/components/common/admin/user-detail-sheet.tsx +++ b/frontend/components/common/admin/user-detail-sheet.tsx @@ -7,7 +7,7 @@ import {Button} from "@/components/ui/button" import {Badge} from "@/components/ui/badge" import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar" import {Sheet, SheetContent, SheetTitle} from "@/components/ui/sheet" -import type {AdminUser} from "@/lib/services" +import type {AdminUser} from "@/lib/services/admin" import {cn, formatDateTime} from "@/lib/utils" interface UserDetailSheetProps { diff --git a/frontend/components/common/settings/access-token.tsx b/frontend/components/common/settings/access-token.tsx index 24a12015..a55cb732 100644 --- a/frontend/components/common/settings/access-token.tsx +++ b/frontend/components/common/settings/access-token.tsx @@ -28,7 +28,7 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from "@/components/ui/breadcrumb" -import {UserService} from "@/lib/services" +import {UserService} from "@/lib/services/user" import type {CreateTokenResponse} from "@/lib/services/user" import {useAuth} from "@/components/providers/auth-provider" import {toast} from "sonner" diff --git a/frontend/components/common/settings/auth-source-modal.tsx b/frontend/components/common/settings/auth-source-modal.tsx index ec418508..ac0ec6c6 100644 --- a/frontend/components/common/settings/auth-source-modal.tsx +++ b/frontend/components/common/settings/auth-source-modal.tsx @@ -7,7 +7,7 @@ import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} fro import {Input} from "@/components/ui/input" import {Label} from "@/components/ui/label" import {Switch} from "@/components/ui/switch" -import {AdminService} from "@/lib/services" +import {AdminService} from "@/lib/services/admin" import type {AuthSource, AuthSourceRequest} from "@/lib/services/admin" const emptyForm: AuthSourceRequest = { diff --git a/frontend/components/common/settings/info-tab.tsx b/frontend/components/common/settings/info-tab.tsx index 54fa2d73..c47760ff 100644 --- a/frontend/components/common/settings/info-tab.tsx +++ b/frontend/components/common/settings/info-tab.tsx @@ -4,7 +4,7 @@ import {useMutation, useQuery} from "@tanstack/react-query" import {ExternalLink, RefreshCw, Sparkles} from "lucide-react" import {toast} from "sonner" -import {AdminService} from "@/lib/services" +import {AdminService} from "@/lib/services/admin" import { AlertDialog, AlertDialogAction, diff --git a/frontend/components/common/settings/operation-tab.tsx b/frontend/components/common/settings/operation-tab.tsx index 8fbf03c2..536af9f0 100644 --- a/frontend/components/common/settings/operation-tab.tsx +++ b/frontend/components/common/settings/operation-tab.tsx @@ -6,7 +6,7 @@ import {KeyRound, ShieldAlert, X} from "lucide-react" import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" import {Badge} from "@/components/ui/badge" import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select" -import {AdminService} from "@/lib/services" +import {AdminService} from "@/lib/services/admin" import type {SystemConfig} from "@/lib/services/admin" import {TemplatesManager} from "./templates" import {toast} from "sonner" diff --git a/frontend/components/common/settings/other-tab.tsx b/frontend/components/common/settings/other-tab.tsx index 8312986e..700c2907 100644 --- a/frontend/components/common/settings/other-tab.tsx +++ b/frontend/components/common/settings/other-tab.tsx @@ -21,7 +21,7 @@ import { import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" import {Switch} from "@/components/ui/switch" -import {AdminService} from "@/lib/services" +import {AdminService} from "@/lib/services/admin" import type {SystemConfig} from "@/lib/services/admin" import {toast} from "sonner" diff --git a/frontend/components/common/settings/profile.tsx b/frontend/components/common/settings/profile.tsx index ba417382..dc857c75 100644 --- a/frontend/components/common/settings/profile.tsx +++ b/frontend/components/common/settings/profile.tsx @@ -45,7 +45,8 @@ import { } from "@/components/ui/dialog" import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select" import {Label} from "@/components/ui/label" -import {AuthService, ChangePasswordRequest, UpdateProfileRequest} from "@/lib/services" +import {AuthService} from "@/lib/services/auth" +import type {ChangePasswordRequest, UpdateProfileRequest} from "@/lib/services/auth" import {UploadService} from "@/lib/services/upload/upload.service" import {ImageCrop, ImageCropApply, ImageCropContent, ImageCropReset} from "@/components/ui/image-crop" import {toast} from "sonner" diff --git a/frontend/components/common/settings/security-tab.tsx b/frontend/components/common/settings/security-tab.tsx index 1ed5debd..d38d4f88 100644 --- a/frontend/components/common/settings/security-tab.tsx +++ b/frontend/components/common/settings/security-tab.tsx @@ -24,7 +24,7 @@ import {Input} from "@/components/ui/input" import {Label} from "@/components/ui/label" import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select" import {AuthSourceModal} from "@/components/common/settings/auth-source-modal" -import {AdminService} from "@/lib/services" +import {AdminService} from "@/lib/services/admin" import type {AuthSource, SystemConfig} from "@/lib/services/admin" import {toast} from "sonner" diff --git a/frontend/components/common/settings/system-tab.tsx b/frontend/components/common/settings/system-tab.tsx index 7e279fb7..391d9714 100644 --- a/frontend/components/common/settings/system-tab.tsx +++ b/frontend/components/common/settings/system-tab.tsx @@ -11,7 +11,7 @@ import {Label} from "@/components/ui/label" import {Switch} from "@/components/ui/switch" import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from "@/components/ui/dialog" import {Badge} from "@/components/ui/badge" -import {AdminService} from "@/lib/services" +import {AdminService} from "@/lib/services/admin" import type {SystemConfig} from "@/lib/services/admin" import {toast} from "sonner" diff --git a/frontend/components/common/settings/templates.tsx b/frontend/components/common/settings/templates.tsx index edfe3d16..5d2ef7eb 100644 --- a/frontend/components/common/settings/templates.tsx +++ b/frontend/components/common/settings/templates.tsx @@ -10,7 +10,7 @@ import {Input} from "@/components/ui/input" import {Label} from "@/components/ui/label" import {Textarea} from "@/components/ui/textarea" import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from "@/components/ui/dialog" -import {AdminService} from "@/lib/services" +import {AdminService} from "@/lib/services/admin" import type {Template} from "@/lib/services/admin/types" import {toast} from "sonner" diff --git a/frontend/components/common/user/file-manager.tsx b/frontend/components/common/user/file-manager.tsx index 14708f76..c71d93a7 100644 --- a/frontend/components/common/user/file-manager.tsx +++ b/frontend/components/common/user/file-manager.tsx @@ -242,6 +242,8 @@ export function UserFileManager() { {file.file_name} { ;(e.currentTarget as HTMLImageElement).style.display = "none" diff --git a/frontend/components/providers/title-updater.tsx b/frontend/components/providers/title-updater.tsx index 54281001..3ac8576e 100644 --- a/frontend/components/providers/title-updater.tsx +++ b/frontend/components/providers/title-updater.tsx @@ -2,14 +2,14 @@ import {useQuery} from '@tanstack/react-query' import {useEffect} from 'react' -import services from '@/lib/services' +import {ConfigService} from '@/lib/services/config' import {usePathname} from 'next/navigation' export function SiteTitleUpdater() { const pathname = usePathname() const publicConfigQuery = useQuery({ queryKey: ["public-config"], - queryFn: () => services.config.getPublicConfig(), + queryFn: () => ConfigService.getPublicConfig(), }) useEffect(() => { diff --git a/frontend/contexts/admin-context.tsx b/frontend/contexts/admin-context.tsx index 5aadca9a..e54fc079 100644 --- a/frontend/contexts/admin-context.tsx +++ b/frontend/contexts/admin-context.tsx @@ -4,8 +4,8 @@ import * as React from "react" import {createContext, useCallback, useContext, useRef, useState} from "react" import {useQueryClient} from "@tanstack/react-query" -import type {SystemConfig, UpdateSystemConfigRequest} from "@/lib/services" -import services from "@/lib/services" +import type {SystemConfig, UpdateSystemConfigRequest} from "@/lib/services/admin" +import {AdminService} from "@/lib/services/admin" import {handleContextError} from "@/lib/utils/error-handling" @@ -49,7 +49,7 @@ export function AdminProvider({ children }: { children: React.ReactNode }) { try { setSystemConfigsLoading(true) setSystemConfigsError(null) - const data = await services.admin.listSystemConfigs(type) + const data = await AdminService.listSystemConfigs(type) if (requestId !== systemRequestIdRef.current) { return @@ -71,7 +71,7 @@ export function AdminProvider({ children }: { children: React.ReactNode }) { /** 更新系统配置 */ const updateSystemConfig = useCallback(async (key: string, data: UpdateSystemConfigRequest) => { try { - await services.admin.updateSystemConfig(key, data) + await AdminService.updateSystemConfig(key, data) await queryClient.invalidateQueries({ queryKey: ['public-config'] }) await refetchSystemConfigs(lastConfigTypeRef.current) } catch (error) { diff --git a/frontend/contexts/admin-users-context.tsx b/frontend/contexts/admin-users-context.tsx index 1706bd94..c9e38954 100644 --- a/frontend/contexts/admin-users-context.tsx +++ b/frontend/contexts/admin-users-context.tsx @@ -3,7 +3,7 @@ import * as React from "react" import {createContext, useCallback, useContext, useEffect, useRef, useState} from "react" import {toast} from "sonner" -import {AdminService, AdminUser, CreateUserRequest, ListUsersRequest} from "@/lib/services" +import {AdminService, AdminUser, CreateUserRequest, ListUsersRequest} from "@/lib/services/admin" /** 用户列表查询参数 */ export interface UserQueryParams { diff --git a/frontend/contexts/user-context.tsx b/frontend/contexts/user-context.tsx index ee55eaaa..d7640dd0 100644 --- a/frontend/contexts/user-context.tsx +++ b/frontend/contexts/user-context.tsx @@ -2,7 +2,7 @@ import {createContext, ReactNode, useCallback, useContext, useEffect, useRef, useState} from 'react' -import services from '@/lib/services' +import {AuthService} from '@/lib/services/auth' import {User} from '@/lib/services/auth/types' @@ -54,7 +54,7 @@ export function UserProvider({ children }: { children: ReactNode }) { const fetchUser = useCallback(async () => { try { setState(prev => ({ ...prev, loading: true, error: null })) - const user = await services.auth.getUserInfo() + const user = await AuthService.getUserInfo() if (!isMountedRef.current) return @@ -83,7 +83,7 @@ export function UserProvider({ children }: { children: ReactNode }) { /** 用户登出 */ const logout = useCallback(async () => { try { - await services.auth.logout() + await AuthService.logout() if (!isMountedRef.current) return diff --git a/frontend/hooks/use-public-config.ts b/frontend/hooks/use-public-config.ts index e0dee0c5..99507826 100644 --- a/frontend/hooks/use-public-config.ts +++ b/frontend/hooks/use-public-config.ts @@ -1,5 +1,5 @@ import {useQuery} from '@tanstack/react-query' -import {ConfigService} from '@/lib/services' +import {ConfigService} from '@/lib/services/config' /** * 获取公共配置(使用 React Query 进行统一缓存与自动同步) diff --git a/frontend/lib/utils/error-handling.ts b/frontend/lib/utils/error-handling.ts index b6e0e54a..83d3b6e9 100644 --- a/frontend/lib/utils/error-handling.ts +++ b/frontend/lib/utils/error-handling.ts @@ -1,4 +1,4 @@ -import { isCancelError } from '@/lib/services' +import { isCancelError } from '@/lib/services/core' import { toast } from 'sonner' /**