From beae8d2dd979706542947e383114747add45df2f Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 18 Jun 2026 10:55:14 +0800 Subject: [PATCH] refactor(frontend): colocate admin components and split service layer Move route-specific admin components from components/common/admin into app/(main)/admin//components/. Split AdminService god object into domain services, fix UploadService to use BaseService, add AdminUploadService, consolidate DB export into DbManageService, and migrate consumers to the unified services entry. --- .../database/components/cache-manager.tsx} | 8 +- .../database/components/sql-console.tsx} | 0 .../database/components/table-browser.tsx} | 0 .../app/(main)/admin/database/page-client.tsx | 15 +- frontend/app/(main)/admin/demo/page.tsx | 4 +- .../admin/files/components}/file-list.tsx | 10 +- .../admin/files/components}/file-stats.tsx | 4 +- .../files/components}/storage-config-tab.tsx | 12 +- frontend/app/(main)/admin/files/page.tsx | 6 +- .../logs/components}/access-analytics.tsx | 4 +- .../admin/logs/components}/access-logs.tsx | 4 +- .../admin/logs/components}/app-logs.tsx | 4 +- .../app/(main)/admin/logs/page-client.tsx | 6 +- .../admin/push/components}/events-tab.tsx | 4 +- .../admin/push/components}/histories-tab.tsx | 0 .../admin/push/components}/settings-tab.tsx | 0 frontend/app/(main)/admin/push/page.tsx | 6 +- .../settings/components/system-status.tsx} | 4 +- .../app/(main)/admin/settings/page-client.tsx | 6 +- .../tasks/components}/task-executions.tsx | 10 +- .../admin/tasks/components}/task-manager.tsx | 7 +- .../tasks/components}/task-schedules.tsx | 15 +- frontend/app/(main)/admin/tasks/page.tsx | 6 +- .../users/components}/create-user-modal.tsx | 0 .../users/components}/user-detail-sheet.tsx | 0 .../users/components}/user-filter-bar.tsx | 0 frontend/app/(main)/admin/users/page.tsx | 7 +- .../common/settings/auth-source-modal.tsx | 6 +- .../components/common/settings/info-tab.tsx | 6 +- .../common/settings/operation-tab.tsx | 6 +- .../components/common/settings/other-tab.tsx | 4 +- .../components/common/settings/profile.tsx | 4 +- .../common/settings/security-tab.tsx | 14 +- .../components/common/settings/system-tab.tsx | 12 +- .../components/common/settings/templates.tsx | 10 +- .../components/common/user/file-manager.tsx | 10 +- frontend/contexts/admin-context.tsx | 6 +- frontend/contexts/admin-users-context.tsx | 17 +- frontend/hooks/use-public-config.ts | 4 +- frontend/lib/services/admin/admin.service.ts | 543 ------------------ .../lib/services/admin/auth-source.service.ts | 26 + frontend/lib/services/admin/cache.service.ts | 18 + frontend/lib/services/admin/index.ts | 38 +- frontend/lib/services/admin/log.service.ts | 53 ++ frontend/lib/services/admin/status.service.ts | 18 + .../services/admin/system-config.service.ts | 44 ++ frontend/lib/services/admin/task.service.ts | 75 +++ .../lib/services/admin/template.service.ts | 26 + frontend/lib/services/admin/types.ts | 12 - frontend/lib/services/admin/user.service.ts | 32 ++ .../services/db-manage/db-manage.service.ts | 21 + frontend/lib/services/index.ts | 89 +-- .../services/upload/admin-upload.service.ts | 40 ++ frontend/lib/services/upload/index.ts | 5 +- .../lib/services/upload/upload.service.ts | 183 ++---- frontend/lib/services/upload/utils.ts | 18 + 56 files changed, 594 insertions(+), 888 deletions(-) rename frontend/{components/common/admin/database-cache-manager.tsx => app/(main)/admin/database/components/cache-manager.tsx} (98%) rename frontend/{components/common/admin/database-sql-console.tsx => app/(main)/admin/database/components/sql-console.tsx} (100%) rename frontend/{components/common/admin/database-table-browser.tsx => app/(main)/admin/database/components/table-browser.tsx} (100%) rename frontend/{components/common/admin => app/(main)/admin/files/components}/file-list.tsx (98%) rename frontend/{components/common/admin => app/(main)/admin/files/components}/file-stats.tsx (99%) rename frontend/{components/common/admin => app/(main)/admin/files/components}/storage-config-tab.tsx (97%) rename frontend/{components/common/admin => app/(main)/admin/logs/components}/access-analytics.tsx (99%) rename frontend/{components/common/admin => app/(main)/admin/logs/components}/access-logs.tsx (99%) rename frontend/{components/common/admin => app/(main)/admin/logs/components}/app-logs.tsx (99%) rename frontend/{components/common/admin/push => app/(main)/admin/push/components}/events-tab.tsx (99%) rename frontend/{components/common/admin/push => app/(main)/admin/push/components}/histories-tab.tsx (100%) rename frontend/{components/common/admin/push => app/(main)/admin/push/components}/settings-tab.tsx (100%) rename frontend/{components/common/admin/status.tsx => app/(main)/admin/settings/components/system-status.tsx} (99%) rename frontend/{components/common/admin => app/(main)/admin/tasks/components}/task-executions.tsx (98%) rename frontend/{components/common/admin => app/(main)/admin/tasks/components}/task-manager.tsx (98%) rename frontend/{components/common/admin => app/(main)/admin/tasks/components}/task-schedules.tsx (97%) rename frontend/{components/common/admin => app/(main)/admin/users/components}/create-user-modal.tsx (100%) rename frontend/{components/common/admin => app/(main)/admin/users/components}/user-detail-sheet.tsx (100%) rename frontend/{components/common/admin => app/(main)/admin/users/components}/user-filter-bar.tsx (100%) delete mode 100644 frontend/lib/services/admin/admin.service.ts create mode 100644 frontend/lib/services/admin/auth-source.service.ts create mode 100644 frontend/lib/services/admin/cache.service.ts create mode 100644 frontend/lib/services/admin/log.service.ts create mode 100644 frontend/lib/services/admin/status.service.ts create mode 100644 frontend/lib/services/admin/system-config.service.ts create mode 100644 frontend/lib/services/admin/task.service.ts create mode 100644 frontend/lib/services/admin/template.service.ts create mode 100644 frontend/lib/services/admin/user.service.ts create mode 100644 frontend/lib/services/upload/admin-upload.service.ts create mode 100644 frontend/lib/services/upload/utils.ts diff --git a/frontend/components/common/admin/database-cache-manager.tsx b/frontend/app/(main)/admin/database/components/cache-manager.tsx similarity index 98% rename from frontend/components/common/admin/database-cache-manager.tsx rename to frontend/app/(main)/admin/database/components/cache-manager.tsx index 5b854a53..b0214fdf 100644 --- a/frontend/components/common/admin/database-cache-manager.tsx +++ b/frontend/app/(main)/admin/database/components/cache-manager.tsx @@ -20,7 +20,7 @@ import { DialogTitle, } from "@/components/ui/dialog" import type {CacheStatus} from "@/lib/services/admin" -import {AdminService} from "@/lib/services/admin" +import services from "@/lib/services" /** * 格式化字节大小 @@ -61,7 +61,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { const fetchCacheStatus = useCallback(async (isSilent = false) => { if (!isSilent) setLoadingCache(true) try { - const data = await AdminService.getCacheStatus() + const data = await services.adminCache.getCacheStatus() setCacheStatus(data) setMaxSizeMB(data.max_size_mb.toString()) setTtlMinutes(data.ttl_minutes.toString()) @@ -90,7 +90,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { } setSavingConfig(true) try { - await AdminService.updateCacheConfig({ + await services.adminCache.updateCacheConfig({ max_size_mb: size, ttl_minutes: ttl, lru_enabled: lruEnabled, @@ -110,7 +110,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) { const handleClearCache = async () => { setClearingCache(true) try { - await AdminService.clearCache() + await services.adminCache.clearCache() toast.success("清空成功", { description: "缓存数据已全部清除" }) setShowClearConfirm(false) await fetchCacheStatus(true) diff --git a/frontend/components/common/admin/database-sql-console.tsx b/frontend/app/(main)/admin/database/components/sql-console.tsx similarity index 100% rename from frontend/components/common/admin/database-sql-console.tsx rename to frontend/app/(main)/admin/database/components/sql-console.tsx diff --git a/frontend/components/common/admin/database-table-browser.tsx b/frontend/app/(main)/admin/database/components/table-browser.tsx similarity index 100% rename from frontend/components/common/admin/database-table-browser.tsx rename to frontend/app/(main)/admin/database/components/table-browser.tsx diff --git a/frontend/app/(main)/admin/database/page-client.tsx b/frontend/app/(main)/admin/database/page-client.tsx index 74737302..cdd1f579 100644 --- a/frontend/app/(main)/admin/database/page-client.tsx +++ b/frontend/app/(main)/admin/database/page-client.tsx @@ -20,8 +20,7 @@ import { 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 services from "@/lib/services" import type {DBOverview} from "@/lib/services/db-manage" const sectionFallback = ( @@ -29,17 +28,17 @@ const sectionFallback = ( ) const TableBrowser = dynamic( - () => import("@/components/common/admin/database-table-browser").then((mod) => mod.TableBrowser), + () => import("./components/table-browser").then((mod) => mod.TableBrowser), { loading: () => sectionFallback }, ) const CacheManager = dynamic( - () => import("@/components/common/admin/database-cache-manager").then((mod) => mod.CacheManager), + () => import("./components/cache-manager").then((mod) => mod.CacheManager), { loading: () => sectionFallback }, ) const SQLConsole = dynamic( - () => import("@/components/common/admin/database-sql-console").then((mod) => mod.SQLConsole), + () => import("./components/sql-console").then((mod) => mod.SQLConsole), { ssr: false }, ) @@ -67,7 +66,7 @@ export function DatabasePageClient() { const fetchOverview = useCallback(async (isSilent = false) => { if (!isSilent) setLoadingOverview(true) try { - const data = await DbManageService.getOverview() + const data = await services.dbManage.getOverview() setOverview(data) } catch (err) { toast.error("获取数据库概览失败", { @@ -82,7 +81,7 @@ export function DatabasePageClient() { const fetchTables = useCallback(async () => { setLoadingTables(true) try { - const data = await DbManageService.listTables() + const data = await services.dbManage.listTables() setTables(data) } catch (err) { toast.error("获取数据库数据表列表失败", { @@ -104,7 +103,7 @@ export function DatabasePageClient() { const handleExport = async () => { setExporting(true) try { - const { blob, filename } = await AdminService.exportDatabase() + const { blob, filename } = await services.dbManage.exportDatabase() const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url diff --git a/frontend/app/(main)/admin/demo/page.tsx b/frontend/app/(main)/admin/demo/page.tsx index d9796459..f35a5b32 100644 --- a/frontend/app/(main)/admin/demo/page.tsx +++ b/frontend/app/(main)/admin/demo/page.tsx @@ -46,8 +46,8 @@ export default function HeaderDemoPage() {
  • 容器与上边距对齐:使用 flex items-center gap-2 作为标题行基础容器(不含下边框),外层统一使用 py-6 px-1 (或 py-6) 进行上边距的视觉对齐,确保切换菜单时顶部高度感一致。
  • 图标呈现:直接将 Lucide 图标组件嵌套于标题容器中,应用 size-5 text-primary 样式。禁止为图标包裹任何背景色小卡片、圆角或装饰性边框。
  • 文本样式标准:文本使用且仅使用 {'h1 className="text-2xl font-semibold tracking-tight"'},禁用字重加粗(如 font-bold)或任何渐变色,保证全站字体一致性。
  • -
  • Tabs 模块化拆分:凡是带有多个 Tab 页切换的页面,**禁止**将所有 Tab 的渲染代码堆积在同一个物理页面主文件内。每个 Tab 页对应的 Content 必须单独封装进全局 components/ 目录下专属文件夹中,主页面文件只负责维护当前 Tab 的激活状态及外层骨架布局。这有利于模块复用、编译优化以及提升单文件可读性。
  • -
  • 复杂度驱动的区块拆分:不仅是 Tabs 切换,当一个页面为单页界面且代码行数超过 800 行时,必须将不同的物理块内容拆分为子组件。为保持路由树的清爽和逻辑纯粹,所有拆分出的非复用子组件也应统一存放在全局组件文件夹中(如 components/common/admin/),而路由入口 page.tsx 仅用于定义整个页面的布局排版与全局框架。典型标杆案例参考“数据管理” (/admin/database) 的重构。
  • +
  • Tabs 模块化拆分:凡是带有多个 Tab 页切换的页面,**禁止**将所有 Tab 的渲染代码堆积在同一个物理页面主文件内。每个 Tab 页对应的 Content 必须单独拆分为独立组件文件(如 tabs/events-tab.tsx 或就近放在 components/ 下),主页面文件只负责维护当前 Tab 的激活状态及外层骨架布局。
  • +
  • 复杂度驱动的区块拆分:不仅是 Tabs 切换,当一个页面代码行数超过 600 行时,必须将高密度区块拆分为子组件。路由专属、不复用的子组件应放在对应路由的特征目录中(如 app/(main)/admin/database/components/),只有真正跨页面复用的组件才进入全局 components/common/。典型标杆案例参考“数据管理” (/admin/database) 的 table-browser.tsx、cache-manager.tsx 与 sql-console.tsx。
  • diff --git a/frontend/components/common/admin/file-list.tsx b/frontend/app/(main)/admin/files/components/file-list.tsx similarity index 98% rename from frontend/components/common/admin/file-list.tsx rename to frontend/app/(main)/admin/files/components/file-list.tsx index 6d2ce1c3..aae424e4 100644 --- a/frontend/components/common/admin/file-list.tsx +++ b/frontend/app/(main)/admin/files/components/file-list.tsx @@ -35,7 +35,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog" import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle} from "@/components/ui/sheet" -import {formatFileSize, getFileUrl, UploadService} from "@/lib/services/upload/upload.service" +import services, {formatFileSize, getFileUrl} from "@/lib/services" import type {Upload as UploadRecord} from "@/lib/services/upload/types" /* ─── 工具函数 ─────────────────────────────────────────── */ @@ -81,7 +81,7 @@ export function FileList() { // 文件列表查询 const listQuery = useQuery({ queryKey: ["files", "all", page, pageSize, debouncedKeyword], - queryFn: () => UploadService.listUploads(page, pageSize, debouncedKeyword || undefined), + queryFn: () => services.adminUpload.listUploads(page, pageSize, debouncedKeyword || undefined), }) const files = listQuery.data?.items ?? [] @@ -109,7 +109,7 @@ export function FileList() { // 删除单文件 const deleteMutation = useMutation({ - mutationFn: (id: string) => UploadService.deleteFile(id), + mutationFn: (id: string) => services.adminUpload.deleteFile(id), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ["files", "my"] }) void queryClient.invalidateQueries({ queryKey: ["files", "stats"] }) @@ -121,7 +121,7 @@ export function FileList() { // 批量 ZIP 下载 const batchDownloadMutation = useMutation({ - mutationFn: (ids: string[]) => UploadService.batchDownload(ids), + mutationFn: (ids: string[]) => services.adminUpload.batchDownload(ids), onSuccess: (blob) => { const url = URL.createObjectURL(blob) const a = document.createElement("a") @@ -151,7 +151,7 @@ export function FileList() { const selectAll = () => setSelectedIds(new Set(files.map((f) => f.id))) const handleDownload = (file: UploadRecord) => { - const url = UploadService.getDownloadUrl(file.id) + const url = services.adminUpload.getDownloadUrl(file.id) const a = document.createElement("a") a.href = url a.download = file.file_name diff --git a/frontend/components/common/admin/file-stats.tsx b/frontend/app/(main)/admin/files/components/file-stats.tsx similarity index 99% rename from frontend/components/common/admin/file-stats.tsx rename to frontend/app/(main)/admin/files/components/file-stats.tsx index e9337470..42c33206 100644 --- a/frontend/components/common/admin/file-stats.tsx +++ b/frontend/app/(main)/admin/files/components/file-stats.tsx @@ -32,7 +32,7 @@ import { ChartLegend, ChartLegendContent, } from "@/components/ui/chart" -import {formatFileSize, UploadService} from "@/lib/services/upload/upload.service" +import services, {formatFileSize} from "@/lib/services" const categoryMap: Record = { "图片": "images", @@ -83,7 +83,7 @@ export function FileStats() { const statsQuery = useQuery({ queryKey: ["files", "stats"], - queryFn: () => UploadService.getFileStats(), + queryFn: () => services.adminUpload.getFileStats(), }) const stats = statsQuery.data diff --git a/frontend/components/common/admin/storage-config-tab.tsx b/frontend/app/(main)/admin/files/components/storage-config-tab.tsx similarity index 97% rename from frontend/components/common/admin/storage-config-tab.tsx rename to frontend/app/(main)/admin/files/components/storage-config-tab.tsx index b12f912b..1b31c613 100644 --- a/frontend/components/common/admin/storage-config-tab.tsx +++ b/frontend/app/(main)/admin/files/components/storage-config-tab.tsx @@ -13,7 +13,7 @@ import {Input} from "@/components/ui/input" import {Progress} from "@/components/ui/progress" import {Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select" import {Switch} from "@/components/ui/switch" -import {AdminService} from "@/lib/services/admin/admin.service" +import services from "@/lib/services" import type { ObjectStorageConfig, StorageConfig, @@ -116,8 +116,8 @@ export function StorageConfigTab() { queryKey: ["admin", "storage-config"], queryFn: async () => { const [configRecord, executions] = await Promise.all([ - AdminService.getSystemConfig(storageConfigKey), - AdminService.listTaskExecutions({task_type: storageMigrationTaskType, page: 1, page_size: 1}), + services.adminSystemConfig.getSystemConfig(storageConfigKey), + services.adminTask.listTaskExecutions({task_type: storageMigrationTaskType, page: 1, page_size: 1}), ]) const current = normalizeConfig(JSON.parse(configRecord.value) as StorageConfig) const migration = latestMigration(executions.items, current) @@ -154,7 +154,7 @@ export function StorageConfigTab() { ...value, driver: activeDriver, } - await AdminService.updateSystemConfig(storageConfigKey, { + await services.adminSystemConfig.updateSystemConfig(storageConfigKey, { value: JSON.stringify(valueToSave), }) }, @@ -171,7 +171,7 @@ export function StorageConfigTab() { const migrateMutation = useMutation({ mutationFn: async (value: StorageConfig) => { const payload: StorageMigrationPayload = {target: value} - return AdminService.dispatchTask({ + return services.adminTask.dispatchTask({ task_type: storageMigrationTaskType, payload: JSON.stringify(payload), }) @@ -186,7 +186,7 @@ export function StorageConfigTab() { }, }) const runMutation = useMutation({ - mutationFn: (executionID: string) => AdminService.retryTaskExecution(executionID), + mutationFn: (executionID: string) => services.adminTask.retryTaskExecution(executionID), onSuccess: () => { toast.success("存储迁移任务已重新下发") void queryClient.invalidateQueries({queryKey: ["admin", "storage-config"]}) diff --git a/frontend/app/(main)/admin/files/page.tsx b/frontend/app/(main)/admin/files/page.tsx index a8e3d376..a4937e34 100644 --- a/frontend/app/(main)/admin/files/page.tsx +++ b/frontend/app/(main)/admin/files/page.tsx @@ -5,9 +5,9 @@ import {motion} from "motion/react" import {FolderOpen} from "lucide-react" import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" -import {FileStats} from "@/components/common/admin/file-stats" -import {FileList} from "@/components/common/admin/file-list" -import {StorageConfigTab} from "@/components/common/admin/storage-config-tab" +import {FileStats} from "./components/file-stats" +import {FileList} from "./components/file-list" +import {StorageConfigTab} from "./components/storage-config-tab" export default function FilesPage() { const [activeTab, setActiveTab] = React.useState("stats") diff --git a/frontend/components/common/admin/access-analytics.tsx b/frontend/app/(main)/admin/logs/components/access-analytics.tsx similarity index 99% rename from frontend/components/common/admin/access-analytics.tsx rename to frontend/app/(main)/admin/logs/components/access-analytics.tsx index cb9f7e4c..d5c44d5c 100644 --- a/frontend/components/common/admin/access-analytics.tsx +++ b/frontend/app/(main)/admin/logs/components/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/admin" +import services from "@/lib/services" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import {EmptyStateWithBorder} from "@/components/layout/empty" @@ -77,7 +77,7 @@ export function AccessAnalytics() { try { setLoading(true) setError(null) - const data = await AdminService.getLogsAnalytics() + const data = await services.adminLog.getLogsAnalytics() // Formats the date labels for the X-axis representation const formattedTrend = (data.trend || []).map(item => ({ diff --git a/frontend/components/common/admin/access-logs.tsx b/frontend/app/(main)/admin/logs/components/access-logs.tsx similarity index 99% rename from frontend/components/common/admin/access-logs.tsx rename to frontend/app/(main)/admin/logs/components/access-logs.tsx index df20fdd1..99d0fb18 100644 --- a/frontend/components/common/admin/access-logs.tsx +++ b/frontend/app/(main)/admin/logs/components/access-logs.tsx @@ -22,7 +22,7 @@ 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/admin" +import services from "@/lib/services" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import {EmptyStateWithBorder} from "@/components/layout/empty" @@ -106,7 +106,7 @@ export function AccessLogs() { endISO = new Date(searchParams.end_time).toISOString() } - return AdminService.getAccessLogs({ + return services.adminLog.getAccessLogs({ page, page_size: PAGE_SIZE, username: searchParams.username || undefined, diff --git a/frontend/components/common/admin/app-logs.tsx b/frontend/app/(main)/admin/logs/components/app-logs.tsx similarity index 99% rename from frontend/components/common/admin/app-logs.tsx rename to frontend/app/(main)/admin/logs/components/app-logs.tsx index 2d59117f..27b84a21 100644 --- a/frontend/components/common/admin/app-logs.tsx +++ b/frontend/app/(main)/admin/logs/components/app-logs.tsx @@ -21,7 +21,7 @@ import {useVirtualizer} from "@tanstack/react-virtual" import {toast} from "sonner" import {ArrowDown, ChevronUp, Loader2} from "lucide-react" -import {AdminService} from "@/lib/services/admin" +import services from "@/lib/services" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import {Button} from "@/components/ui/button" @@ -156,7 +156,7 @@ export function AppLogs() { // ---- Data fetching --------------------------------------------------- const fetchLogs = useCallback(async (cursor: number = 0) => { try { - return await AdminService.getLogs(cursor) + return await services.adminLog.getLogs(cursor) } catch (err) { throw err instanceof Error ? err : new Error("获取日志失败") } diff --git a/frontend/app/(main)/admin/logs/page-client.tsx b/frontend/app/(main)/admin/logs/page-client.tsx index 8630b455..9b73deb1 100644 --- a/frontend/app/(main)/admin/logs/page-client.tsx +++ b/frontend/app/(main)/admin/logs/page-client.tsx @@ -10,17 +10,17 @@ const tabFallback = ( ) const AccessAnalytics = dynamic( - () => import("@/components/common/admin/access-analytics").then((mod) => mod.AccessAnalytics), + () => import("./components/access-analytics").then((mod) => mod.AccessAnalytics), { loading: () => tabFallback }, ) const AccessLogs = dynamic( - () => import("@/components/common/admin/access-logs").then((mod) => mod.AccessLogs), + () => import("./components/access-logs").then((mod) => mod.AccessLogs), { loading: () => tabFallback }, ) const AppLogs = dynamic( - () => import("@/components/common/admin/app-logs").then((mod) => mod.AppLogs), + () => import("./components/app-logs").then((mod) => mod.AppLogs), { loading: () => tabFallback }, ) diff --git a/frontend/components/common/admin/push/events-tab.tsx b/frontend/app/(main)/admin/push/components/events-tab.tsx similarity index 99% rename from frontend/components/common/admin/push/events-tab.tsx rename to frontend/app/(main)/admin/push/components/events-tab.tsx index 857ee964..f6b9f7d7 100644 --- a/frontend/components/common/admin/push/events-tab.tsx +++ b/frontend/app/(main)/admin/push/components/events-tab.tsx @@ -33,7 +33,7 @@ import {LoadingStateWithBorder} from "@/components/layout/loading" import {EmptyStateWithBorder} from "@/components/layout/empty" -import {AdminService} from "@/lib/services/admin" +import services from "@/lib/services" import {PushService} from "@/lib/services/push" import type {CreatePushEventRequest, PushEvent, UpdatePushEventRequest,} from "@/lib/services/push" @@ -67,7 +67,7 @@ export function EventsTab() { // --- 获取所有系统可调度任务类型 --- const taskTypesQuery = useQuery({ queryKey: ["admin", "task-types"], - queryFn: () => AdminService.getTaskTypes(), + queryFn: () => services.adminTask.getTaskTypes(), }) // --- 修改保存事件 Mutation --- diff --git a/frontend/components/common/admin/push/histories-tab.tsx b/frontend/app/(main)/admin/push/components/histories-tab.tsx similarity index 100% rename from frontend/components/common/admin/push/histories-tab.tsx rename to frontend/app/(main)/admin/push/components/histories-tab.tsx diff --git a/frontend/components/common/admin/push/settings-tab.tsx b/frontend/app/(main)/admin/push/components/settings-tab.tsx similarity index 100% rename from frontend/components/common/admin/push/settings-tab.tsx rename to frontend/app/(main)/admin/push/components/settings-tab.tsx diff --git a/frontend/app/(main)/admin/push/page.tsx b/frontend/app/(main)/admin/push/page.tsx index e12a1531..d0d43a6f 100644 --- a/frontend/app/(main)/admin/push/page.tsx +++ b/frontend/app/(main)/admin/push/page.tsx @@ -8,9 +8,9 @@ import {motion} from "motion/react" import {Bell, History, Layers, Settings} from "lucide-react" import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" -import {EventsTab} from "@/components/common/admin/push/events-tab" -import {HistoriesTab} from "@/components/common/admin/push/histories-tab" -import {SettingsTab} from "@/components/common/admin/push/settings-tab" +import {EventsTab} from "./components/events-tab" +import {HistoriesTab} from "./components/histories-tab" +import {SettingsTab} from "./components/settings-tab" export default function PushAdminPage() { const [activeTab, setActiveTab] = React.useState("events") diff --git a/frontend/components/common/admin/status.tsx b/frontend/app/(main)/admin/settings/components/system-status.tsx similarity index 99% rename from frontend/components/common/admin/status.tsx rename to frontend/app/(main)/admin/settings/components/system-status.tsx index b85896ce..9697ef53 100644 --- a/frontend/components/common/admin/status.tsx +++ b/frontend/app/(main)/admin/settings/components/system-status.tsx @@ -12,7 +12,7 @@ import {Badge} from "@/components/ui/badge" import {Progress} from "@/components/ui/progress" import {Skeleton} from "@/components/ui/skeleton" import type {SystemStatus} from "@/lib/services/admin" -import {AdminService} from "@/lib/services/admin" +import services from "@/lib/services" /** * 格式化数字,每3位加逗号 @@ -40,7 +40,7 @@ export function SystemStatusManager() { const fetchStatus = useCallback(async (isSilent = false) => { if (!isSilent) setWavelet(true) try { - const data = await AdminService.getSystemStatus() + const data = await services.adminStatus.getSystemStatus() // 检测变化的字段用于微动画效果 if (prevStatusRef.current) { diff --git a/frontend/app/(main)/admin/settings/page-client.tsx b/frontend/app/(main)/admin/settings/page-client.tsx index f09c8365..83bc7069 100644 --- a/frontend/app/(main)/admin/settings/page-client.tsx +++ b/frontend/app/(main)/admin/settings/page-client.tsx @@ -12,7 +12,7 @@ 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 services from "@/lib/services" import type {SystemConfig} from "@/lib/services/admin" const tabFallback = ( @@ -45,7 +45,7 @@ const InfoTab = dynamic( ) const SystemStatusManager = dynamic( - () => import("@/components/common/admin/status").then((mod) => mod.SystemStatusManager), + () => import("./components/system-status").then((mod) => mod.SystemStatusManager), { loading: () => tabFallback }, ) @@ -62,7 +62,7 @@ export function AdminSettingsPageClient() { const systemConfigsQuery = useQuery({ queryKey: ["admin", "system-configs"], - queryFn: () => AdminService.listSystemConfigs("system"), + queryFn: () => services.adminSystemConfig.listSystemConfigs("system"), enabled: !!user?.is_admin, }) diff --git a/frontend/components/common/admin/task-executions.tsx b/frontend/app/(main)/admin/tasks/components/task-executions.tsx similarity index 98% rename from frontend/components/common/admin/task-executions.tsx rename to frontend/app/(main)/admin/tasks/components/task-executions.tsx index aab06abd..5a25d0ad 100644 --- a/frontend/components/common/admin/task-executions.tsx +++ b/frontend/app/(main)/admin/tasks/components/task-executions.tsx @@ -6,7 +6,7 @@ import {toast} from "sonner" import {format} from "date-fns" import {Activity, ChevronLeft, ChevronRight, RefreshCw, RotateCcw} from "lucide-react" -import {AdminService} from "@/lib/services/admin" +import services from "@/lib/services" import type {TaskExecution, TaskExecutionStatus, TaskMeta} from "@/lib/services/admin" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" @@ -65,12 +65,12 @@ export function TaskExecutionsManager() { const taskTypesQuery = useQuery({ queryKey: ["admin", "task-types"], - queryFn: () => AdminService.getTaskTypes(), + queryFn: () => services.adminTask.getTaskTypes(), }) const executionsQuery = useQuery({ queryKey: ["admin", "task-executions", executionsPage, executionStatus, executionTaskType], - queryFn: () => AdminService.listTaskExecutions({ + queryFn: () => services.adminTask.listTaskExecutions({ page: executionsPage, page_size: PAGE_SIZE, status: executionStatus === "all" ? undefined : executionStatus, @@ -80,12 +80,12 @@ export function TaskExecutionsManager() { const detailQuery = useQuery({ queryKey: ["admin", "task-execution", selectedExecutionId], - queryFn: () => AdminService.getTaskExecution(selectedExecutionId!), + queryFn: () => services.adminTask.getTaskExecution(selectedExecutionId!), enabled: detailOpen && !!selectedExecutionId, }) const retryMutation = useMutation({ - mutationFn: (id: string) => AdminService.retryTaskExecution(id), + mutationFn: (id: string) => services.adminTask.retryTaskExecution(id), onSuccess: (taskID) => { toast.success("任务已重新下发", { description: `新任务 ID:${ taskID }`, diff --git a/frontend/components/common/admin/task-manager.tsx b/frontend/app/(main)/admin/tasks/components/task-manager.tsx similarity index 98% rename from frontend/components/common/admin/task-manager.tsx rename to frontend/app/(main)/admin/tasks/components/task-manager.tsx index b7053979..ccde41b9 100644 --- a/frontend/components/common/admin/task-manager.tsx +++ b/frontend/app/(main)/admin/tasks/components/task-manager.tsx @@ -11,7 +11,8 @@ 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/admin" +import type {DispatchTaskRequest, TaskMeta} from "@/lib/services/admin" +import services from "@/lib/services" import {buildTaskPayload} from "@/lib/task-param-utils" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" @@ -148,7 +149,7 @@ export function TaskManager() { try { setLoading(true) setError(null) - const data = await AdminService.getTaskTypes() + const data = await services.adminTask.getTaskTypes() setTaskTypes(data) } catch (err) { setError(err instanceof Error ? err : new Error('加载任务类型失败')) @@ -209,7 +210,7 @@ export function TaskManager() { params.payload = payload ?? undefined } - const taskID = await AdminService.dispatchTask(params) + const taskID = await services.adminTask.dispatchTask(params) toast.success('任务下发成功', { description: `已成功将任务 ${ targetTask?.name || selectedTaskType } 加入队列:${ taskID }` diff --git a/frontend/components/common/admin/task-schedules.tsx b/frontend/app/(main)/admin/tasks/components/task-schedules.tsx similarity index 97% rename from frontend/components/common/admin/task-schedules.tsx rename to frontend/app/(main)/admin/tasks/components/task-schedules.tsx index e21bf227..5573be0d 100644 --- a/frontend/components/common/admin/task-schedules.tsx +++ b/frontend/app/(main)/admin/tasks/components/task-schedules.tsx @@ -12,7 +12,8 @@ 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/admin" +import type {CreateScheduleRequest, Schedule, TaskMeta, UpdateScheduleRequest} from "@/lib/services/admin" +import services from "@/lib/services" import {buildTaskPayload} from "@/lib/task-param-utils" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" @@ -47,8 +48,8 @@ export function TaskSchedulesManager() { setLoading(true) setError(null) const [schedulesData, taskTypesData] = await Promise.all([ - AdminService.listSchedules(), - AdminService.getTaskTypes() + services.adminTask.listSchedules(), + services.adminTask.getTaskTypes() ]) setSchedules(schedulesData || []) setTaskTypes(taskTypesData || []) @@ -97,7 +98,7 @@ export function TaskSchedulesManager() { const handleToggleActive = async (schedule: Schedule) => { try { - const updated = await AdminService.updateSchedule(schedule.id, { + const updated = await services.adminTask.updateSchedule(schedule.id, { name: schedule.name, task_type: schedule.task_type, cron: schedule.cron, @@ -167,7 +168,7 @@ export function TaskSchedulesManager() { payload: payloadStr, is_active: isActive } - const updated = await AdminService.updateSchedule(editingSchedule.id, req) + const updated = await services.adminTask.updateSchedule(editingSchedule.id, req) setSchedules(prev => prev.map(s => s.id === editingSchedule.id ? updated : s)) toast.success("定时任务更新成功") } else { @@ -178,7 +179,7 @@ export function TaskSchedulesManager() { payload: payloadStr, is_active: isActive } - const created = await AdminService.createSchedule(req) + const created = await services.adminTask.createSchedule(req) setSchedules(prev => [created, ...prev]) toast.success("定时任务创建成功") } @@ -202,7 +203,7 @@ export function TaskSchedulesManager() { if (!deletingId) return try { setDeleteLoading(true) - await AdminService.deleteSchedule(deletingId) + await services.adminTask.deleteSchedule(deletingId) setSchedules(prev => prev.filter(s => s.id !== deletingId)) toast.success("定时任务删除成功") setDeleteOpen(false) diff --git a/frontend/app/(main)/admin/tasks/page.tsx b/frontend/app/(main)/admin/tasks/page.tsx index b18108b6..54f71f11 100644 --- a/frontend/app/(main)/admin/tasks/page.tsx +++ b/frontend/app/(main)/admin/tasks/page.tsx @@ -3,9 +3,9 @@ import {Suspense} from "react" import {useRouter, useSearchParams} from "next/navigation" import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" -import {TaskManager} from "@/components/common/admin/task-manager" -import {TaskSchedulesManager} from "@/components/common/admin/task-schedules" -import {TaskExecutionsManager} from "@/components/common/admin/task-executions" +import {TaskManager} from "./components/task-manager" +import {TaskSchedulesManager} from "./components/task-schedules" +import {TaskExecutionsManager} from "./components/task-executions" import {Spinner} from "@/components/ui/spinner" import {Layers} from "lucide-react" diff --git a/frontend/components/common/admin/create-user-modal.tsx b/frontend/app/(main)/admin/users/components/create-user-modal.tsx similarity index 100% rename from frontend/components/common/admin/create-user-modal.tsx rename to frontend/app/(main)/admin/users/components/create-user-modal.tsx diff --git a/frontend/components/common/admin/user-detail-sheet.tsx b/frontend/app/(main)/admin/users/components/user-detail-sheet.tsx similarity index 100% rename from frontend/components/common/admin/user-detail-sheet.tsx rename to frontend/app/(main)/admin/users/components/user-detail-sheet.tsx diff --git a/frontend/components/common/admin/user-filter-bar.tsx b/frontend/app/(main)/admin/users/components/user-filter-bar.tsx similarity index 100% rename from frontend/components/common/admin/user-filter-bar.tsx rename to frontend/app/(main)/admin/users/components/user-filter-bar.tsx diff --git a/frontend/app/(main)/admin/users/page.tsx b/frontend/app/(main)/admin/users/page.tsx index 8d43221b..ab29ce8d 100644 --- a/frontend/app/(main)/admin/users/page.tsx +++ b/frontend/app/(main)/admin/users/page.tsx @@ -25,10 +25,9 @@ import {EmptyStateWithBorder} from "@/components/layout/empty" import {LoadingStateWithBorder} from "@/components/layout/loading" import {ErrorInline} from "@/components/layout/error" import {useAdminUsers} from "@/contexts/admin-users-context" -import {CreateUserModal} from "@/components/common/admin/create-user-modal" - -import {UserFilterBar} from "@/components/common/admin/user-filter-bar" -import {UserDetailSheet} from "@/components/common/admin/user-detail-sheet" +import {CreateUserModal} from "./components/create-user-modal" +import {UserFilterBar} from "./components/user-filter-bar" +import {UserDetailSheet} from "./components/user-detail-sheet" export default function UsersPage() { const { diff --git a/frontend/components/common/settings/auth-source-modal.tsx b/frontend/components/common/settings/auth-source-modal.tsx index ac0ec6c6..764f1c89 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/admin" +import services from "@/lib/services" import type {AuthSource, AuthSourceRequest} from "@/lib/services/admin" const emptyForm: AuthSourceRequest = { @@ -63,9 +63,9 @@ export function AuthSourceModal({ setSaving(true) try { if (source) { - await AdminService.updateAuthSource(source.id, form) + await services.adminAuthSource.updateAuthSource(source.id, form) } else { - await AdminService.createAuthSource(form) + await services.adminAuthSource.createAuthSource(form) } await onChanged() toast.success("认证源已保存") diff --git a/frontend/components/common/settings/info-tab.tsx b/frontend/components/common/settings/info-tab.tsx index c47760ff..761a6dc6 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/admin" +import services from "@/lib/services" import { AlertDialog, AlertDialogAction, @@ -33,13 +33,13 @@ function InfoRow({ label, value }: { label: string; value: React.ReactNode }) { export function InfoTab() { const updateQuery = useQuery({ queryKey: ["admin", "update"], - queryFn: () => AdminService.getUpdateStatus(), + queryFn: () => services.adminStatus.getUpdateStatus(), refetchInterval: 30 * 60 * 1000, staleTime: 5 * 60 * 1000, }) const applyUpdateMutation = useMutation({ - mutationFn: () => AdminService.applyUpdate(), + mutationFn: () => services.adminStatus.applyUpdate(), onSuccess: () => { toast.success("升级包已校验完成,服务正在重启") }, diff --git a/frontend/components/common/settings/operation-tab.tsx b/frontend/components/common/settings/operation-tab.tsx index 536af9f0..b386454a 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/admin" +import services from "@/lib/services" import type {SystemConfig} from "@/lib/services/admin" import {TemplatesManager} from "./templates" import {toast} from "sonner" @@ -21,7 +21,7 @@ export function OperationTab({ configs, systemConfigsQuery }: OperationTabProps) const uploadTypesQuery = useQuery({ queryKey: ["admin", "upload-types"], - queryFn: () => AdminService.listUploadTypes(), + queryFn: () => services.adminSystemConfig.listUploadTypes(), }) const updateWhitelistMutation = useMutation({ @@ -30,7 +30,7 @@ export function OperationTab({ configs, systemConfigsQuery }: OperationTabProps) if (!config) { throw new Error("缺少配置项: file_access_whitelist") } - await AdminService.updateSystemConfig("file_access_whitelist", { + await services.adminSystemConfig.updateSystemConfig("file_access_whitelist", { value: newValue, description: config.description, }) diff --git a/frontend/components/common/settings/other-tab.tsx b/frontend/components/common/settings/other-tab.tsx index 700c2907..d1bf4dd9 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/admin" +import services from "@/lib/services" import type {SystemConfig} from "@/lib/services/admin" import {toast} from "sonner" @@ -90,7 +90,7 @@ export function OtherTab({ configs }: OtherTabProps) { mutationFn: async ({ path, enabled }: { path: string; enabled: boolean }) => { const newConfig = { ...menuDisplayConfig, [path]: enabled } const currentCfg = configs["menu_display_config"] - await AdminService.updateSystemConfig("menu_display_config", { + await services.adminSystemConfig.updateSystemConfig("menu_display_config", { value: JSON.stringify(newConfig), description: currentCfg?.description || "目录显示配置(JSON 字符串,格式为 {url: enabled})", }) diff --git a/frontend/components/common/settings/profile.tsx b/frontend/components/common/settings/profile.tsx index dc857c75..cb278128 100644 --- a/frontend/components/common/settings/profile.tsx +++ b/frontend/components/common/settings/profile.tsx @@ -47,7 +47,7 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/ import {Label} from "@/components/ui/label" import {AuthService} from "@/lib/services/auth" import type {ChangePasswordRequest, UpdateProfileRequest} from "@/lib/services/auth" -import {UploadService} from "@/lib/services/upload/upload.service" +import services from "@/lib/services" import {ImageCrop, ImageCropApply, ImageCropContent, ImageCropReset} from "@/components/ui/image-crop" import {toast} from "sonner" @@ -193,7 +193,7 @@ export function ProfileMain() { const handleCroppedImage = async (croppedBase64: string) => { try { - const res = await UploadService.uploadBase64Image(croppedBase64, "avatar", "avatar.png") + const res = await services.upload.uploadBase64Image(croppedBase64, "avatar", "avatar.png") setAvatarUrl(`/f/${res.id}`) setIsCropDialogOpen(false) setCropFile(null) diff --git a/frontend/components/common/settings/security-tab.tsx b/frontend/components/common/settings/security-tab.tsx index d38d4f88..e5fe031a 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/admin" +import services from "@/lib/services" import type {AuthSource, SystemConfig} from "@/lib/services/admin" import {toast} from "sonner" @@ -89,7 +89,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) { const authSourcesQuery = useQuery({ queryKey: ["auth", "sources"], - queryFn: () => AdminService.listAuthSources(), + queryFn: () => services.adminAuthSource.listAuthSources(), }) useEffect(() => { @@ -120,7 +120,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) { if (!config) { throw new Error("缺少配置项: login_session_ttl_hours") } - await AdminService.updateSystemConfig("login_session_ttl_hours", { + await services.adminSystemConfig.updateSystemConfig("login_session_ttl_hours", { value: value, description: config.description, }) @@ -159,7 +159,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) { if (!config) { throw new Error(`缺少配置项: ${key}`) } - await AdminService.updateSystemConfig(key, { + await services.adminSystemConfig.updateSystemConfig(key, { value: value ? "true" : "false", description: config.description, }) @@ -176,7 +176,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) { const toggleSourceMutation = useMutation({ mutationFn: async (source: AuthSource) => { - await AdminService.toggleAuthSource(source.id, { is_active: !source.is_active }) + await services.adminAuthSource.toggleAuthSource(source.id, { is_active: !source.is_active }) }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["auth", "sources"] }) @@ -190,7 +190,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) { const deleteSourceMutation = useMutation({ mutationFn: async (sourceId: string) => { - await AdminService.deleteAuthSource(sourceId) + await services.adminAuthSource.deleteAuthSource(sourceId) }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["auth", "sources"] }) @@ -219,7 +219,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) { for (const update of updates) { const currentCfg = configs[update.key] - await AdminService.updateSystemConfig(update.key, { + await services.adminSystemConfig.updateSystemConfig(update.key, { value: update.value, description: currentCfg?.description || "", }) diff --git a/frontend/components/common/settings/system-tab.tsx b/frontend/components/common/settings/system-tab.tsx index 391d9714..c276b7a7 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/admin" +import services from "@/lib/services" import type {SystemConfig} from "@/lib/services/admin" import {toast} from "sonner" @@ -58,7 +58,7 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) { if (!config) { throw new Error(`缺少配置项: ${key}`) } - await AdminService.updateSystemConfig(key, { + await services.adminSystemConfig.updateSystemConfig(key, { value: value ? "true" : "false", description: config.description, }) @@ -78,11 +78,11 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) { const currentAddrCfg = configs["server_address"] const currentSiteCfg = configs["site_name"] await Promise.all([ - AdminService.updateSystemConfig("server_address", { + services.adminSystemConfig.updateSystemConfig("server_address", { value: serverAddress, description: currentAddrCfg?.description || "服务器地址", }), - AdminService.updateSystemConfig("site_name", { + services.adminSystemConfig.updateSystemConfig("site_name", { value: siteName, description: currentSiteCfg?.description || "系统平台的展示名称", }), @@ -122,7 +122,7 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) { continue } } - await AdminService.updateSystemConfig(update.key, { + await services.adminSystemConfig.updateSystemConfig(update.key, { value: update.value, description: currentCfg?.description || "", }) @@ -148,7 +148,7 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) { setSmtpTestSuccess(null) setSmtpTestError("") - const res = await AdminService.testSMTP({ + const res = await services.adminSystemConfig.testSMTP({ smtp_host: smtpHost, smtp_port: parseInt(smtpPort, 10) || 587, smtp_username: smtpUsername, diff --git a/frontend/components/common/settings/templates.tsx b/frontend/components/common/settings/templates.tsx index 5d2ef7eb..85f92f44 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/admin" +import services from "@/lib/services" import type {Template} from "@/lib/services/admin/types" import {toast} from "sonner" @@ -29,12 +29,12 @@ export function TemplatesManager() { const templatesQuery = useQuery({ queryKey: ["admin", "templates"], - queryFn: () => AdminService.listTemplates(), + queryFn: () => services.adminTemplate.listTemplates(), }) const createTemplateMutation = useMutation({ mutationFn: async () => { - await AdminService.createTemplate({ + await services.adminTemplate.createTemplate({ key, name, type, @@ -55,7 +55,7 @@ export function TemplatesManager() { const updateTemplateMutation = useMutation({ mutationFn: async (key: string) => { - await AdminService.updateTemplate(key, { + await services.adminTemplate.updateTemplate(key, { name, type, subject, @@ -75,7 +75,7 @@ export function TemplatesManager() { const deleteTemplateMutation = useMutation({ mutationFn: async (key: string) => { - await AdminService.deleteTemplate(key) + await services.adminTemplate.deleteTemplate(key) }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["admin", "templates"] }) diff --git a/frontend/components/common/user/file-manager.tsx b/frontend/components/common/user/file-manager.tsx index c71d93a7..8fe47601 100644 --- a/frontend/components/common/user/file-manager.tsx +++ b/frontend/components/common/user/file-manager.tsx @@ -36,7 +36,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog" -import {formatFileSize, getFileUrl, UploadService} from "@/lib/services/upload/upload.service" +import services, {formatFileSize, getFileUrl} from "@/lib/services" import type {Upload as UploadRecord} from "@/lib/services/upload/types" /* ─── 工具函数 ─────────────────────────────────────────── */ @@ -83,7 +83,7 @@ export function UserFileManager() { // 我的文件列表查询 const { data, isPending } = useQuery({ queryKey: ["user-files", page, pageSize, debouncedKeyword], - queryFn: () => UploadService.listMyUploads(page, pageSize, debouncedKeyword || undefined), + queryFn: () => services.upload.listMyUploads(page, pageSize, debouncedKeyword || undefined), }) const files = data?.items ?? [] @@ -93,7 +93,7 @@ export function UserFileManager() { // 上传文件 Mutation const uploadMutation = useMutation({ mutationFn: async (file: File) => { - return UploadService.uploadFile(file, "generic", undefined, uploadAccessMode) + return services.upload.uploadFile(file, "generic", undefined, uploadAccessMode) }, onMutate: () => { setUploading(true) @@ -115,7 +115,7 @@ export function UserFileManager() { // 删除文件 Mutation const deleteMutation = useMutation({ - mutationFn: (id: string) => UploadService.deleteMyFile(id), + mutationFn: (id: string) => services.upload.deleteMyFile(id), onSuccess: () => { toast.success("文件已成功删除") void queryClient.invalidateQueries({ queryKey: ["user-files"] }) @@ -138,7 +138,7 @@ export function UserFileManager() { } const handleDownload = (file: UploadRecord) => { - const url = UploadService.getDownloadUrl(file.id) + const url = services.upload.getDownloadUrl(file.id) const a = document.createElement("a") a.href = url a.download = file.file_name diff --git a/frontend/contexts/admin-context.tsx b/frontend/contexts/admin-context.tsx index e54fc079..fccc1bbc 100644 --- a/frontend/contexts/admin-context.tsx +++ b/frontend/contexts/admin-context.tsx @@ -5,7 +5,7 @@ import {createContext, useCallback, useContext, useRef, useState} from "react" import {useQueryClient} from "@tanstack/react-query" import type {SystemConfig, UpdateSystemConfigRequest} from "@/lib/services/admin" -import {AdminService} from "@/lib/services/admin" +import services from "@/lib/services" 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 AdminService.listSystemConfigs(type) + const data = await services.adminSystemConfig.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 AdminService.updateSystemConfig(key, data) + await services.adminSystemConfig.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 c9e38954..d8a50223 100644 --- a/frontend/contexts/admin-users-context.tsx +++ b/frontend/contexts/admin-users-context.tsx @@ -3,7 +3,8 @@ 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/admin" +import type {AdminUser, CreateUserRequest, ListUsersRequest} from "@/lib/services/admin" +import services from "@/lib/services" /** 用户列表查询参数 */ export interface UserQueryParams { @@ -113,12 +114,12 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode }) // Current API doesn't support status filter in listUsers? // The previous implementation did client-side filtering. // Ideally backend supports it. If not, we fetch and filter? - // "AdminService.listUsers" in previous code only took page, page_size, username. + // "services.adminUser.listUsers" in previous code only took page, page_size, username. // So status filter was client side. // However, caching client-filtered result is tricky if we don't have all data. // But previous implementation fetched *paged* data then filtered? No, that would be wrong (filtering 20 items might leave 0). // Let's check previous implementation: - // "const data = await AdminService.listUsers(...) ... let filteredUsers = data.users ... if (statusFilter...) filtered..." + // "const data = await services.adminUser.listUsers(...) ... let filteredUsers = data.users ... if (statusFilter...) filtered..." // This means filtering happens ONLY on the current page of results! This is technically buggy if the user wants "all inactive users". // But preserving that behavior for now. @@ -129,7 +130,7 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode }) username: debouncedSearchUsername || undefined } - const response = await AdminService.listUsers(requestParams) + const response = await services.adminUser.listUsers(requestParams) if (requestId !== latestRequestIdRef.current) return @@ -180,7 +181,7 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode }) cacheRef.current = {} try { - await AdminService.updateUserStatus(user.id, { is_active: !user.is_active }) + await services.adminUser.updateUserStatus(user.id, { is_active: !user.is_active }) toast.success(`已${ !user.is_active ? '启用' : '禁用' }用户 ${ user.username }`) } catch { // Revert on error @@ -192,12 +193,12 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode }) } const getUserDetail = async (id: string) => { - return AdminService.getUser(id) + return services.adminUser.getUser(id) } const createUser = async (req: CreateUserRequest) => { try { - const newUser = await AdminService.createUser(req) + const newUser = await services.adminUser.createUser(req) setUsers(prev => [newUser, ...prev]) setTotal(prev => prev + 1) // Clear cache because data changed @@ -212,7 +213,7 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode }) const deleteUser = async (user: AdminUser) => { try { - await AdminService.deleteUser(user.id) + await services.adminUser.deleteUser(user.id) setUsers(prev => prev.filter(u => u.id !== user.id)) setTotal(prev => Math.max(0, prev - 1)) cacheRef.current = {} diff --git a/frontend/hooks/use-public-config.ts b/frontend/hooks/use-public-config.ts index 99507826..11e6b6e9 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/config' +import services from '@/lib/services' /** * 获取公共配置(使用 React Query 进行统一缓存与自动同步) @@ -12,7 +12,7 @@ import {ConfigService} from '@/lib/services/config' export function usePublicConfig() { const { data: config, isLoading: loading, error } = useQuery({ queryKey: ['public-config'], - queryFn: () => ConfigService.getPublicConfig(), + queryFn: () => services.config.getPublicConfig(), staleTime: 5 * 60 * 1000, // 5 分钟缓存 }) diff --git a/frontend/lib/services/admin/admin.service.ts b/frontend/lib/services/admin/admin.service.ts deleted file mode 100644 index 0a8e8d0b..00000000 --- a/frontend/lib/services/admin/admin.service.ts +++ /dev/null @@ -1,543 +0,0 @@ -import {BaseService} from '@/lib/services/core'; -import type { - AdminUser, - AppUpdateStatus, - AuthSource, - AuthSourceRequest, - CacheConfig, - CacheStatus, - CreateScheduleRequest, - CreateSystemConfigRequest, - CreateTemplateRequest, - CreateUserRequest, - DatabaseInfo, - DispatchTaskRequest, - ListTaskExecutionsRequest, - ListTaskExecutionsResponse, - ListUsersRequest, - ListUsersResponse, - Schedule, - SystemConfig, - SystemStatus, - TaskExecution, - TaskMeta, - TaskParamType, - TaskTypeResponse, - Template, - ToggleAuthSourceRequest, - UpdateScheduleRequest, - UpdateSystemConfigRequest, - UpdateTemplateRequest, - UpdateUserStatusRequest, -} from './types'; - -export type { AdminUser, CacheStatus, CacheConfig } from './types'; - -/** - * 管理员服务 - * - * @remarks - * 所有接口都需要管理员权限 - */ -export class AdminService extends BaseService { - protected static readonly basePath = '/api/v1/admin'; - - // ==================== 系统配置管理 ==================== - - /** - * 创建系统配置 - * @param request - 创建系统配置的请求参数 - * @returns void - * @throws {UnauthorizedError} 当未登录时 - * @throws {ForbiddenError} 当无管理员权限时 - * @throws {ValidationError} 当参数验证失败或配置键已存在时 - * - * @example - * ```typescript - * await AdminService.createSystemConfig({ - * key: 'app.version', - * value: '1.0.0', - * description: '应用版本号' - * }); - * ``` - */ - static async createSystemConfig( - request: CreateSystemConfigRequest, - ): Promise { - return this.post('/system-configs', request); - } - - /** - * 获取系统配置列表 - * @returns 系统配置列表 - * @throws {UnauthorizedError} 当未登录时 - * @throws {ForbiddenError} 当无管理员权限时 - * - * @example - * ```typescript - * const configs = await AdminService.listSystemConfigs(); - * console.log('系统配置数量:', configs.length); - * ``` - */ - static async listSystemConfigs(type?: 'system' | 'business'): Promise { - const query = type ? `?type=${type}` : ''; - return this.get(`/system-configs${query}`); - } - - /** - * 获取单个系统配置 - * @param key - 配置键 - * @returns 系统配置信息 - * @throws {UnauthorizedError} 当未登录时 - * @throws {ForbiddenError} 当无管理员权限时 - * @throws {NotFoundError} 当配置不存在时 - * - * @example - * ```typescript - * const config = await AdminService.getSystemConfig('app.version'); - * console.log('应用版本:', config.value); - * ``` - */ - static async getSystemConfig(key: string): Promise { - return this.get(`/system-configs/${ key }`); - } - - /** - * 更新系统配置 - * @param key - 配置键 - * @param request - 更新系统配置的请求参数 - * @returns void - * @throws {UnauthorizedError} 当未登录时 - * @throws {ForbiddenError} 当无管理员权限时 - * @throws {NotFoundError} 当配置不存在时 - * @throws {ValidationError} 当参数验证失败时 - * - * @example - * ```typescript - * await AdminService.updateSystemConfig('app.version', { - * value: '1.1.0', - * description: '更新到新版本' - * }); - * ``` - */ - static async updateSystemConfig( - key: string, - request: UpdateSystemConfigRequest, - ): Promise { - return this.put(`/system-configs/${ key }`, request); - } - - static async testSMTP(request: { - smtp_host: string; - smtp_port: number; - smtp_username: string; - smtp_password: string; - to: string; - }): Promise<{ success: boolean; log: string; error: string }> { - return this.post<{ success: boolean; log: string; error: string }>('/system-configs/smtp/test', request); - } - - // ==================== 应用更新 ==================== - - static async getUpdateStatus(): Promise { - return this.get('/update'); - } - - static async applyUpdate(): Promise { - return this.post('/update/apply'); - } - - /** - * 获取所有已存在的文件业务类型及默认内置类型列表 - * @returns 业务类型列表 - */ - static async listUploadTypes(): Promise { - return this.get('/uploads/types'); - } - - // ==================== 认证源管理 ==================== - - static async listAuthSources(): Promise { - return this.get('/auth-sources'); - } - - static async createAuthSource(request: AuthSourceRequest): Promise { - return this.post('/auth-sources', request); - } - - static async updateAuthSource(id: string, request: AuthSourceRequest): Promise { - return this.put(`/auth-sources/${ id }`, request); - } - - static async toggleAuthSource(id: string, request: ToggleAuthSourceRequest): Promise { - return this.put(`/auth-sources/${ id }/toggle`, request); - } - - static async deleteAuthSource(id: string): Promise { - return this.delete(`/auth-sources/${ id }`); - } - - - - // ==================== 任务管理 ==================== - - /** - * 获取支持的任务类型列表 - * @returns 任务类型列表 - * @throws {UnauthorizedError} 当未登录时 - * @throws {ForbiddenError} 当无管理员权限时 - * - * @example - * ```typescript - * const taskTypes = await AdminService.getTaskTypes(); - * console.log('可用任务类型:', taskTypes); - * ``` - */ - static async getTaskTypes(): Promise { - const response = await this.get('/tasks/types'); - // Adapt backend PascalCase to frontend snake_case - return response.map(item => ({ - type: item.Type || item.type || '', - asynq_task: item.AsynqTask || item.asynq_task || '', - name: item.Name || item.name || '', - description: item.Description || item.description || '', - supports_time: item.SupportsTime ?? item.supports_time ?? false, - max_retry: item.MaxRetry ?? item.max_retry ?? 0, - queue: item.Queue || item.queue || '', - params: (item.Params || item.params || []).map(p => ({ - name: p.Name || p.name || '', - label: p.Label || p.label || '', - // Cast to TaskParamType: backend only emits 'string' | 'text' | 'number'. - // Unknown future values fall through safely to the string branch. - type: (p.Type || p.type || 'string') as TaskParamType, - required: p.Required ?? p.required ?? false, - placeholder: p.Placeholder || p.placeholder || '', - description: p.Description || p.description || '', - })), - })); - } - - /** - * 下发任务 - * @param request - 下发任务请求参数 - * @returns void - * @throws {UnauthorizedError} 当未登录时 - * @throws {ForbiddenError} 当无管理员权限时 - * @throws {ValidationError} 当参数验证失败时 - * @remarks - * - 不同任务类型需要不同的参数 - * - order_sync 支持 start_time 和 end_time 参数 - * - user_gamification 需要 user_id 参数 - * - 其他任务无需额外参数 - */ - static async dispatchTask(request: DispatchTaskRequest): Promise { - return this.post('/tasks/dispatch', request); - } - - /** - * 查询任务执行记录列表 - */ - static async listTaskExecutions( - request: ListTaskExecutionsRequest = {}, - ): Promise { - return this.get( - '/tasks/executions', - request as unknown as Record, - ); - } - - /** - * 查询任务执行详情 - */ - static async getTaskExecution(id: string): Promise { - return this.get(`/tasks/executions/${ id }`); - } - - /** - * 重试失败任务 - */ - static async retryTaskExecution(id: string): Promise { - return this.post(`/tasks/executions/${ id }/retry`); - } - - // ==================== 定时任务管理 ==================== - - /** - * 获取定时任务列表 - */ - static async listSchedules(): Promise { - return this.get('/tasks/schedules'); - } - - /** - * 创建定时任务 - */ - static async createSchedule(request: CreateScheduleRequest): Promise { - return this.post('/tasks/schedules', request); - } - - /** - * 更新定时任务 - */ - static async updateSchedule(id: string, request: UpdateScheduleRequest): Promise { - return this.put(`/tasks/schedules/${ id }`, request); - } - - /** - * 删除定时任务 - */ - static async deleteSchedule(id: string): Promise { - return this.delete(`/tasks/schedules/${ id }`); - } - - // ==================== 用户管理 ==================== - - /** - * 获取用户列表 - * @param request - 查询参数 - * @returns 用户列表及总数 - * @throws {UnauthorizedError} 当未登录时 - * @throws {ForbiddenError} 当无管理员权限时 - * @throws {ValidationError} 当参数验证失败时 - * - * @example - * ```typescript - * const result = await AdminService.listUsers({ - * page: 1, - * page_size: 20, - * user_id: '10001', - * username: 'test' - * }); - * console.log('用户总数:', result.total); - * console.log('用户列表:', result.users); - * ``` - * - * @remarks - * - page 从 1 开始 - * - page_size 范围 1-100 - * - user_id 按用户 ID 精确搜索 - * - username 按用户名做前缀搜索 - */ - static async listUsers(request: ListUsersRequest): Promise { - return this.get('/users', request as unknown as Record); - } - - /** - * 获取用户详情 - * @param id - 用户 ID - * @returns 用户完整资料 - */ - static async getUser(id: string): Promise { - return this.get(`/users/${ id }`); - } - - /** - * 更新用户状态 - * @param id - 用户 ID - * @param request - 更新状态请求参数 - * @returns void - * @throws {UnauthorizedError} 当未登录时 - * @throws {ForbiddenError} 当无管理员权限或禁用管理员用户时 - * @throws {NotFoundError} 当用户不存在时 - * - * @example - * ```typescript - * // 禁用用户 - * await AdminService.updateUserStatus(123, { is_active: false }); - * - * // 启用用户 - * await AdminService.updateUserStatus(123, { is_active: true }); - * ``` - * - * @remarks - * - 不能禁用管理员用户 - */ - static async updateUserStatus( - id: string, - request: UpdateUserStatusRequest - ): Promise { - return this.put(`/users/${ id }/status`, request); - } - - /** - * 创建用户 - * @param request - 创建用户请求参数 - * @returns 创建成功的用户信息 - */ - static async createUser(request: CreateUserRequest): Promise { - return this.post('/users', request); - } - - /** - * 删除用户 - * @param id - 用户 ID - */ - static async deleteUser(id: string): Promise { - return this.delete(`/users/${ id }`); - } - - /** - * 获取系统状态 - * @returns 系统状态指标数据 - * @throws {UnauthorizedError} 当未登录时 - * @throws {ForbiddenError} 当无管理员权限时 - */ - static async getSystemStatus(): Promise { - return this.get('/status'); - } - - /** - * 获取数据库信息 - * @returns 数据库类型、名称、版本信息 - * @throws {UnauthorizedError} 当未登录时 - * @throws {ForbiddenError} 当无管理员权限时 - */ - static async getDatabaseInfo(): Promise { - return this.get('/db-info'); - } - - /** - * 导出数据库 - * SQLite 返回 .db 二进制,PostgreSQL 返回 pg_dump .sql 文本 - * @returns 包含文件 Blob 和建议文件名的对象 - * @throws {UnauthorizedError} 当未登录时 - * @throws {ForbiddenError} 当无管理员权限时 - */ - static async exportDatabase(): Promise<{ blob: Blob; filename: string }> { - const response = await import('axios').then(({ default: axios }) => - axios.get(this.getFullPath('/db-export'), { - withCredentials: true, - responseType: 'blob', - }) - ); - // 从 Content-Disposition 提取文件名 - const disposition = response.headers['content-disposition'] as string | undefined; - let filename = 'wavelet_export'; - if (disposition) { - const match = disposition.match(/filename="?([^";]+)"?/); - if (match) filename = match[1]; - } - return { blob: response.data, filename }; - } - - // ==================== 系统日志 ==================== - - /** - * 获取系统历史日志 - * @param cursor - 日志游标,0=获取最新,>0=获取更早 - * @param limit - 每页条数,默认 200 - */ - static async getLogs(cursor: number = 0, limit: number = 200): Promise<{ - lines: Array<{ index: number; data: string }>; - has_more: boolean; - next_cursor: number; - }> { - return this.get('/logs', { cursor, limit }); - } - - /** - * 获取 ClickHouse 访问日志列表 - */ - static async getAccessLogs(params: { - page: number; - page_size: number; - username?: string; - path?: string; - start_time?: string; - end_time?: string; - }): Promise<{ - total: number; - list: Array<{ - id: string; - user_id: string; - username: string; - nickname: string; - path: string; - method: string; - ip: string; - user_agent: string; - headers: string; - status: number; - latency: number; - created_at: string; - }>; - }> { - return this.get('/logs/access', params as Record); - } - - /** - * 获取 ClickHouse 访问日志图表聚合指标 - */ - static async getLogsAnalytics(): Promise<{ - trend: Array<{ date: string; count: number }>; - browsers: Array<{ browser: string; count: number }>; - top_users: Array<{ - user_id: string; - username: string; - nickname: string; - count: number; - }>; - }> { - return this.get('/logs/analytics'); - } - - // ==================== 模板管理 ==================== - - /** - * 获取模板列表 - */ - static async listTemplates(): Promise { - return this.get('/templates'); - } - - /** - * 获取单个模板 - */ - static async getTemplate(key: string): Promise