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 {
- return this.get(`/templates/${ key }`);
- }
-
- /**
- * 创建模板
- */
- static async createTemplate(request: CreateTemplateRequest): Promise {
- return this.post('/templates', request);
- }
-
- /**
- * 更新模板
- */
- static async updateTemplate(key: string, request: UpdateTemplateRequest): Promise {
- return this.put(`/templates/${ key }`, request);
- }
-
- /**
- * 删除模板
- */
- static async deleteTemplate(key: string): Promise {
- return this.delete(`/templates/${ key }`);
- }
-
- // ==================== 磁盘缓存管理 ====================
-
- /**
- * 获取磁盘缓存状态与当前统计数据
- */
- static async getCacheStatus(): Promise {
- return this.get('/cache/status');
- }
-
- /**
- * 更新磁盘缓存策略配置
- */
- static async updateCacheConfig(config: CacheConfig): Promise {
- return this.post('/cache/config', config);
- }
-
- /**
- * 一键清空所有磁盘缓存数据
- */
- static async clearCache(): Promise {
- return this.post('/cache/clear');
- }
-}
diff --git a/frontend/lib/services/admin/auth-source.service.ts b/frontend/lib/services/admin/auth-source.service.ts
new file mode 100644
index 00000000..ff38b019
--- /dev/null
+++ b/frontend/lib/services/admin/auth-source.service.ts
@@ -0,0 +1,26 @@
+import {BaseService} from '@/lib/services/core';
+import type {AuthSource, AuthSourceRequest, ToggleAuthSourceRequest} from './types';
+
+export class AdminAuthSourceService extends BaseService {
+ protected static readonly basePath = '/api/v1/admin';
+
+ 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}`);
+ }
+}
\ No newline at end of file
diff --git a/frontend/lib/services/admin/cache.service.ts b/frontend/lib/services/admin/cache.service.ts
new file mode 100644
index 00000000..57c265c6
--- /dev/null
+++ b/frontend/lib/services/admin/cache.service.ts
@@ -0,0 +1,18 @@
+import {BaseService} from '@/lib/services/core';
+import type {CacheConfig, CacheStatus} from './types';
+
+export class AdminCacheService extends BaseService {
+ protected static readonly basePath = '/api/v1/admin';
+
+ static async getCacheStatus(): Promise {
+ return this.get('/cache/status');
+ }
+
+ static async updateCacheConfig(config: CacheConfig): Promise {
+ return this.post('/cache/config', config);
+ }
+
+ static async clearCache(): Promise {
+ return this.post('/cache/clear');
+ }
+}
\ No newline at end of file
diff --git a/frontend/lib/services/admin/index.ts b/frontend/lib/services/admin/index.ts
index 4ababbc8..ce17d082 100644
--- a/frontend/lib/services/admin/index.ts
+++ b/frontend/lib/services/admin/index.ts
@@ -1,25 +1,12 @@
-/**
- * 管理员服务模块
- *
- * @description
- * 提供系统配置、用户与任务管理功能,包括:
- * - 系统配置管理(创建、查询、更新、删除)
- * - 异步任务配置及下发
- * - 用户账号状态管理
- *
- * @remarks
- * 所有接口都需要管理员权限
- *
- * @example
- * ```typescript
- * import { AdminService } from '@/lib/services';
- *
- * // 获取系统配置列表
- * const configs = await AdminService.listSystemConfigs();
- * ```
- */
+export { AdminSystemConfigService } from './system-config.service';
+export { AdminAuthSourceService } from './auth-source.service';
+export { AdminTaskService } from './task.service';
+export { AdminUserService } from './user.service';
+export { AdminStatusService } from './status.service';
+export { AdminLogService } from './log.service';
+export { AdminTemplateService } from './template.service';
+export { AdminCacheService } from './cache.service';
-export { AdminService } from './admin.service';
export type {
SystemConfig,
CreateSystemConfigRequest,
@@ -41,11 +28,16 @@ export type {
ListUsersResponse,
UpdateUserStatusRequest,
SystemStatus,
- DatabaseInfo,
AppUpdateStatus,
Schedule,
CreateScheduleRequest,
UpdateScheduleRequest,
CacheStatus,
CacheConfig,
-} from './types';
+ Template,
+ CreateTemplateRequest,
+ UpdateTemplateRequest,
+ StorageDriver,
+ StorageConfig,
+ ObjectStorageConfig,
+} from './types';
\ No newline at end of file
diff --git a/frontend/lib/services/admin/log.service.ts b/frontend/lib/services/admin/log.service.ts
new file mode 100644
index 00000000..ff46d949
--- /dev/null
+++ b/frontend/lib/services/admin/log.service.ts
@@ -0,0 +1,53 @@
+import {BaseService} from '@/lib/services/core';
+
+export class AdminLogService extends BaseService {
+ protected static readonly basePath = '/api/v1/admin';
+
+ 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 });
+ }
+
+ 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);
+ }
+
+ 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');
+ }
+}
\ No newline at end of file
diff --git a/frontend/lib/services/admin/status.service.ts b/frontend/lib/services/admin/status.service.ts
new file mode 100644
index 00000000..75640e5c
--- /dev/null
+++ b/frontend/lib/services/admin/status.service.ts
@@ -0,0 +1,18 @@
+import {BaseService} from '@/lib/services/core';
+import type {AppUpdateStatus, SystemStatus} from './types';
+
+export class AdminStatusService extends BaseService {
+ protected static readonly basePath = '/api/v1/admin';
+
+ static async getSystemStatus(): Promise {
+ return this.get('/status');
+ }
+
+ static async getUpdateStatus(): Promise {
+ return this.get('/update');
+ }
+
+ static async applyUpdate(): Promise {
+ return this.post('/update/apply');
+ }
+}
\ No newline at end of file
diff --git a/frontend/lib/services/admin/system-config.service.ts b/frontend/lib/services/admin/system-config.service.ts
new file mode 100644
index 00000000..c1874a99
--- /dev/null
+++ b/frontend/lib/services/admin/system-config.service.ts
@@ -0,0 +1,44 @@
+import {BaseService} from '@/lib/services/core';
+import type {
+ CreateSystemConfigRequest,
+ SystemConfig,
+ UpdateSystemConfigRequest,
+} from './types';
+
+export class AdminSystemConfigService extends BaseService {
+ protected static readonly basePath = '/api/v1/admin';
+
+ static async createSystemConfig(request: CreateSystemConfigRequest): Promise {
+ return this.post('/system-configs', request);
+ }
+
+ static async listSystemConfigs(type?: 'system' | 'business'): Promise {
+ const query = type ? `?type=${type}` : '';
+ return this.get(`/system-configs${query}`);
+ }
+
+ static async getSystemConfig(key: string): Promise {
+ return this.get(`/system-configs/${key}`);
+ }
+
+ 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 listUploadTypes(): Promise {
+ return this.get('/uploads/types');
+ }
+}
\ No newline at end of file
diff --git a/frontend/lib/services/admin/task.service.ts b/frontend/lib/services/admin/task.service.ts
new file mode 100644
index 00000000..bc457a1e
--- /dev/null
+++ b/frontend/lib/services/admin/task.service.ts
@@ -0,0 +1,75 @@
+import {BaseService} from '@/lib/services/core';
+import type {
+ CreateScheduleRequest,
+ DispatchTaskRequest,
+ ListTaskExecutionsRequest,
+ ListTaskExecutionsResponse,
+ Schedule,
+ TaskExecution,
+ TaskMeta,
+ TaskParamType,
+ TaskTypeResponse,
+ UpdateScheduleRequest,
+} from './types';
+
+export class AdminTaskService extends BaseService {
+ protected static readonly basePath = '/api/v1/admin';
+
+ static async getTaskTypes(): Promise {
+ const response = await this.get('/tasks/types');
+ 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 || '',
+ type: (p.Type || p.type || 'string') as TaskParamType,
+ required: p.Required ?? p.required ?? false,
+ placeholder: p.Placeholder || p.placeholder || '',
+ description: p.Description || p.description || '',
+ })),
+ }));
+ }
+
+ 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}`);
+ }
+}
\ No newline at end of file
diff --git a/frontend/lib/services/admin/template.service.ts b/frontend/lib/services/admin/template.service.ts
new file mode 100644
index 00000000..a6ec044f
--- /dev/null
+++ b/frontend/lib/services/admin/template.service.ts
@@ -0,0 +1,26 @@
+import {BaseService} from '@/lib/services/core';
+import type {CreateTemplateRequest, Template, UpdateTemplateRequest} from './types';
+
+export class AdminTemplateService extends BaseService {
+ protected static readonly basePath = '/api/v1/admin';
+
+ static async listTemplates(): Promise {
+ return this.get('/templates');
+ }
+
+ static async getTemplate(key: string): Promise {
+ return this.get(`/templates/${key}`);
+ }
+
+ static async createTemplate(request: CreateTemplateRequest): Promise {
+ return this.post('/templates', request);
+ }
+
+ static async updateTemplate(key: string, request: UpdateTemplateRequest): Promise {
+ return this.put(`/templates/${key}`, request);
+ }
+
+ static async deleteTemplate(key: string): Promise {
+ return this.delete(`/templates/${key}`);
+ }
+}
\ No newline at end of file
diff --git a/frontend/lib/services/admin/types.ts b/frontend/lib/services/admin/types.ts
index 7339390e..876780ec 100644
--- a/frontend/lib/services/admin/types.ts
+++ b/frontend/lib/services/admin/types.ts
@@ -456,18 +456,6 @@ export interface UpdateTemplateRequest {
description: string;
}
-/**
- * 数据库信息
- */
-export interface DatabaseInfo {
- /** 数据库类型:'sqlite' | 'postgres' */
- type: 'sqlite' | 'postgres';
- /** 数据库名称或文件路径 */
- name: string;
- /** 数据库版本字符串 */
- version: string;
-}
-
/**
* 应用更新状态
*/
diff --git a/frontend/lib/services/admin/user.service.ts b/frontend/lib/services/admin/user.service.ts
new file mode 100644
index 00000000..d34f6f5e
--- /dev/null
+++ b/frontend/lib/services/admin/user.service.ts
@@ -0,0 +1,32 @@
+import {BaseService} from '@/lib/services/core';
+import type {
+ AdminUser,
+ CreateUserRequest,
+ ListUsersRequest,
+ ListUsersResponse,
+ UpdateUserStatusRequest,
+} from './types';
+
+export class AdminUserService extends BaseService {
+ protected static readonly basePath = '/api/v1/admin';
+
+ static async listUsers(request: ListUsersRequest): Promise {
+ return this.get('/users', request as unknown as Record);
+ }
+
+ static async getUser(id: string): Promise {
+ return this.get(`/users/${id}`);
+ }
+
+ static async updateUserStatus(id: string, request: UpdateUserStatusRequest): Promise {
+ return this.put(`/users/${id}/status`, request);
+ }
+
+ static async createUser(request: CreateUserRequest): Promise {
+ return this.post('/users', request);
+ }
+
+ static async deleteUser(id: string): Promise {
+ return this.delete(`/users/${id}`);
+ }
+}
\ No newline at end of file
diff --git a/frontend/lib/services/db-manage/db-manage.service.ts b/frontend/lib/services/db-manage/db-manage.service.ts
index 8fb30d0c..7dc6ec3a 100644
--- a/frontend/lib/services/db-manage/db-manage.service.ts
+++ b/frontend/lib/services/db-manage/db-manage.service.ts
@@ -1,3 +1,6 @@
+import type {InternalAxiosRequestConfig} from 'axios';
+
+import apiClient from '@/lib/services/core/api-client';
import {BaseService} from '@/lib/services/core';
import type {DBOverview, ExecuteSQLResponse, TableDataResponse} from './types';
@@ -38,4 +41,22 @@ export class DbManageService extends BaseService {
static async executeSQL(sql: string): Promise {
return this.post('/query', { sql });
}
+
+ /**
+ * 导出数据库备份(SQLite .db / PostgreSQL .sql)
+ */
+ static async exportDatabase(): Promise<{ blob: Blob; filename: string }> {
+ const response = await apiClient.get('/api/v1/admin/db-export', {
+ withCredentials: true,
+ responseType: 'blob',
+ } as InternalAxiosRequestConfig);
+
+ 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 };
+ }
}
diff --git a/frontend/lib/services/index.ts b/frontend/lib/services/index.ts
index e113a929..72630107 100644
--- a/frontend/lib/services/index.ts
+++ b/frontend/lib/services/index.ts
@@ -1,58 +1,52 @@
/**
* 服务层统一入口
- * 提供所有业务服务的访问接口
*
* @example
* ```typescript
- * // 推荐:使用统一的 services 对象
* import services from '@/lib/services';
*
* const user = await services.auth.getUserInfo();
- * const systemConfig = await services.admin.listSystemConfigs();
- * ```
- *
- * @example
- * ```typescript
- * // 按需导入:直接导入特定服务
- * import { AuthService } from '@/lib/services';
- *
- * const user = await AuthService.getUserInfo();
+ * const configs = await services.adminSystemConfig.listSystemConfigs();
* ```
*/
+import {
+ AdminAuthSourceService,
+ AdminCacheService,
+ AdminLogService,
+ AdminStatusService,
+ AdminSystemConfigService,
+ AdminTaskService,
+ AdminTemplateService,
+ AdminUserService,
+} from './admin';
import {AuthService} from './auth';
-import {AdminService} from './admin';
-import {UserService} from './user';
import {ConfigService} from './config';
-import {UploadService} from './upload';
import {DbManageService} from './db-manage';
import {PushService} from './push';
+import {AdminUploadService, UploadService} from './upload';
+import {UserService} from './user';
-/**
- * 服务对象
- * 集中导出所有业务服务
- *
- * @description
- * 推荐使用此对象访问所有服务,保持代码风格统一
- */
const services = {
- /** 认证服务 */
auth: AuthService,
- /** 管理员服务 */
- admin: AdminService,
- /** 用户服务 */
+ adminSystemConfig: AdminSystemConfigService,
+ adminAuthSource: AdminAuthSourceService,
+ adminTask: AdminTaskService,
+ adminUser: AdminUserService,
+ adminStatus: AdminStatusService,
+ adminLog: AdminLogService,
+ adminTemplate: AdminTemplateService,
+ adminCache: AdminCacheService,
user: UserService,
- /** 配置服务 */
config: ConfigService,
- /** 上传服务 */
upload: UploadService,
- /** 数据库管理服务 */
+ adminUpload: AdminUploadService,
dbManage: DbManageService,
- /** 通知推送服务 */
push: PushService,
} as const;
export default services;
+export { services };
// ==================== 核心模块导出 ====================
@@ -86,20 +80,26 @@ export type {
// ==================== 业务服务导出 ====================
-// 认证服务
export { AuthService } from './auth';
export type { User, OAuthLoginUrlResponse, OAuthCallbackRequest, AuthSource, ExternalAccountBinding, ChangePasswordRequest, UpdateProfileRequest } from './auth';
-// 配置服务
export { ConfigService } from './config';
export type { PublicConfigResponse } from './config';
-// 管理员服务
-export { AdminService } from './admin';
+export {
+ AdminSystemConfigService,
+ AdminAuthSourceService,
+ AdminTaskService,
+ AdminUserService,
+ AdminStatusService,
+ AdminLogService,
+ AdminTemplateService,
+ AdminCacheService,
+} from './admin';
+
export type {
SystemConfig,
CreateSystemConfigRequest,
- CreateUserRequest,
UpdateSystemConfigRequest,
TaskMeta,
TaskParam,
@@ -113,28 +113,33 @@ export type {
ListUsersRequest,
ListUsersResponse,
UpdateUserStatusRequest,
+ CreateUserRequest,
SystemStatus,
- DatabaseInfo,
AppUpdateStatus,
Schedule,
CreateScheduleRequest,
UpdateScheduleRequest,
CacheStatus,
CacheConfig,
+ Template,
+ CreateTemplateRequest,
+ UpdateTemplateRequest,
+ AuthSource as AdminAuthSource,
+ AuthSourceRequest,
+ ToggleAuthSourceRequest,
+ StorageDriver,
+ StorageConfig,
+ ObjectStorageConfig,
} from './admin';
-// 用户服务
export { UserService } from './user';
export type { AccessToken, CreateTokenResponse } from './user';
-// 上传服务
-export { UploadService } from './upload';
-export type { UploadImageResponse } from './upload';
+export { UploadService, AdminUploadService, formatFileSize, getFileUrl } from './upload';
+export type { UploadImageResponse, Upload, ListUploadsResponse, FileStatsResponse, ImageQuality } from './upload';
-// 数据库管理服务
export { DbManageService } from './db-manage';
export type { DBOverview, TableDataResponse, ExecuteSQLResponse } from './db-manage';
-// 通知推送服务
export { PushService } from './push';
-export type { PushEvent, PushHistory, PushChannelConfig, ListPushHistoriesRequest, ListPushHistoriesResponse, UpdatePushEventRequest, TestPushRequest } from './push';
+export type { PushEvent, PushHistory, PushChannelConfig, ListPushHistoriesRequest, ListPushHistoriesResponse, UpdatePushEventRequest, TestPushRequest } from './push';
\ No newline at end of file
diff --git a/frontend/lib/services/upload/admin-upload.service.ts b/frontend/lib/services/upload/admin-upload.service.ts
new file mode 100644
index 00000000..5d2a9146
--- /dev/null
+++ b/frontend/lib/services/upload/admin-upload.service.ts
@@ -0,0 +1,40 @@
+import type {InternalAxiosRequestConfig} from 'axios';
+
+import {BaseService} from '../core/base.service';
+import type {FileStatsResponse, ListUploadsResponse} from './types';
+
+export class AdminUploadService extends BaseService {
+ protected static readonly basePath = '/api/v1/admin/uploads';
+
+ static async listUploads(
+ page = 1,
+ pageSize = 20,
+ keyword?: string,
+ type?: string,
+ extension?: string,
+ ): Promise {
+ const params: Record = { page, page_size: pageSize };
+ if (keyword) params.keyword = keyword;
+ if (type) params.type = type;
+ if (extension) params.extension = extension;
+ return this.get('', params);
+ }
+
+ static async getFileStats(): Promise {
+ return this.get('/stats');
+ }
+
+ static async deleteFile(id: string): Promise {
+ return this.delete(`/${id}`);
+ }
+
+ static getDownloadUrl(id: string): string {
+ return `${this.basePath}/download/${id}`;
+ }
+
+ static async batchDownload(ids: string[]): Promise {
+ return this.post('/download/batch', { ids }, {
+ responseType: 'blob',
+ } as InternalAxiosRequestConfig);
+ }
+}
\ No newline at end of file
diff --git a/frontend/lib/services/upload/index.ts b/frontend/lib/services/upload/index.ts
index 702b5e14..282e4c2e 100644
--- a/frontend/lib/services/upload/index.ts
+++ b/frontend/lib/services/upload/index.ts
@@ -1,2 +1,5 @@
export { UploadService } from './upload.service';
-export type { UploadImageResponse } from './types';
\ No newline at end of file
+export { AdminUploadService } from './admin-upload.service';
+export { formatFileSize, getFileUrl } from './utils';
+export type { ImageQuality } from './utils';
+export type { UploadImageResponse, Upload, ListUploadsResponse, FileStatsResponse } from './types';
\ No newline at end of file
diff --git a/frontend/lib/services/upload/upload.service.ts b/frontend/lib/services/upload/upload.service.ts
index b434bf78..f7381637 100644
--- a/frontend/lib/services/upload/upload.service.ts
+++ b/frontend/lib/services/upload/upload.service.ts
@@ -1,189 +1,78 @@
-import {BaseService} from '../core/base.service'
-import type {ListUploadsResponse, Upload, UploadImageResponse, FileStatsResponse} from './types'
-import type {InternalAxiosRequestConfig} from 'axios'
-import apiClient from '../core/api-client'
+import type {InternalAxiosRequestConfig} from 'axios';
-export type ImageQuality = 'low' | 'medium' | 'high' | 'origin'
+import {BaseService} from '../core/base.service';
+import type {ListUploadsResponse, Upload, UploadImageResponse} from './types';
-/**
- * 根据上传ID构造文件访问URL
- * @param id - 上传记录ID
- * @param quality - 图片质量
- * @returns 文件访问URL
- */
-export function getFileUrl(
- id: string | number | null | undefined,
- quality: ImageQuality = 'origin'
-): string | null {
- if (!id) return null
- if (quality === 'origin') return `/f/${id}`
- return `/f/${id}?quality=${quality}`
-}
-
-/**
- * 格式化文件大小
- */
-export function formatFileSize(bytes: number): string {
- if (bytes === 0) return '0 B'
- const k = 1024
- const sizes = ['B', 'KB', 'MB', 'GB']
- const i = Math.floor(Math.log(bytes) / Math.log(k))
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
-}
-
-/**
- * 上传服务
- * 处理文件上传相关的 API 请求
- */
export class UploadService extends BaseService {
- protected static readonly basePath = '/api/v1/admin/uploads'
+ protected static readonly basePath = '/api/v1/upload';
- /**
- * 通用文件上传
- * @param file - 文件对象
- * @param type - 业务分类(如 avatar、attachment、generic)
- * @param metadata - 可选额外 JSON 元数据
- */
static async uploadFile(
file: File,
type: string = 'generic',
metadata?: Record,
- accessMode?: number
+ accessMode?: number,
): Promise {
- const formData = new FormData()
- formData.append('file', file)
- formData.append('type', type)
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('type', type);
if (metadata) {
- formData.append('metadata', JSON.stringify(metadata))
+ formData.append('metadata', JSON.stringify(metadata));
}
if (accessMode !== undefined) {
- formData.append('access_mode', String(accessMode))
+ formData.append('access_mode', String(accessMode));
}
- const response = await apiClient.post<{ data: Upload }>('/api/v1/upload', formData, {
+ return this.post('', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
- } as InternalAxiosRequestConfig)
- return response.data.data
+ } as InternalAxiosRequestConfig);
}
- /**
- * 获取文件列表
- * @param page - 页码(1-based)
- * @param pageSize - 每页数量
- * @param keyword - 搜索关键词(文件名模糊)
- * @param type - 业务分类过滤
- * @param extension - 扩展名过滤
- */
- static async listUploads(
- page = 1,
- pageSize = 20,
- keyword?: string,
- type?: string,
- extension?: string
- ): Promise {
- const params: Record = { page, page_size: pageSize }
- if (keyword) params.keyword = keyword
- if (type) params.type = type
- if (extension) params.extension = extension
- return this.get('', params)
- }
-
- /**
- * 获取当前用户文件统计信息
- */
- static async getFileStats(): Promise {
- return this.get('/stats')
- }
-
- /**
- * 删除文件
- */
- static async deleteFile(id: string): Promise {
- return this.delete(`/${id}`)
- }
-
- /**
- * 获取单文件下载 URL(触发 attachment 下载)
- */
- static getDownloadUrl(id: string): string {
- return `/api/v1/admin/uploads/download/${id}`
- }
-
- /**
- * 批量 ZIP 打包下载
- * @param ids - 文件 ID 数组
- */
- static async batchDownload(ids: string[]): Promise {
- const response = await this.post('/download/batch', { ids }, {
- responseType: 'blob',
- } as InternalAxiosRequestConfig)
- return response
- }
-
- /**
- * 将 base64 图片转换为 Blob 并上传(兼容旧接口)
- */
static async uploadBase64Image(
base64: string,
type: string = 'generic',
filename: string = 'image.png',
- accessMode?: number
+ accessMode?: number,
): Promise {
- const response = await fetch(base64)
- const blob = await response.blob()
- const mimeType = base64.match(/data:([^;]+);/)?.[1] || 'image/png'
- const file = new File([blob], filename, { type: mimeType })
- const result = await this.uploadFile(file, type, undefined, accessMode)
- return { id: result.id }
+ const response = await fetch(base64);
+ const blob = await response.blob();
+ const mimeType = base64.match(/data:([^;]+);/)?.[1] || 'image/png';
+ const file = new File([blob], filename, { type: mimeType });
+ const result = await this.uploadFile(file, type, undefined, accessMode);
+ return { id: result.id };
}
- /**
- * 获取我的文件列表
- */
static async listMyUploads(
page = 1,
pageSize = 20,
keyword?: string,
type?: string,
- extension?: string
+ extension?: string,
): Promise {
- const params: Record = { page, page_size: pageSize }
- if (keyword) params.keyword = keyword
- if (type) params.type = type
- if (extension) params.extension = extension
- const response = await apiClient.get<{ data: ListUploadsResponse }>('/api/v1/upload/my', {
- params
- } as InternalAxiosRequestConfig)
- return response.data.data
+ const params: Record = { page, page_size: pageSize };
+ if (keyword) params.keyword = keyword;
+ if (type) params.type = type;
+ if (extension) params.extension = extension;
+ return this.get('/my', params);
}
-
- /**
- * 删除我的文件
- */
static async deleteMyFile(id: string): Promise {
- const response = await apiClient.delete<{ data: void }>(`/api/v1/upload/${id}`)
- return response.data.data
+ return this.delete(`/${id}`);
}
- /**
- * 更新我的文件
- */
static async updateMyFile(id: string, fileName: string, accessMode?: number): Promise {
- const response = await apiClient.put<{ data: Upload }>(`/api/v1/upload/${id}`, {
+ return this.put(`/${id}`, {
file_name: fileName,
- access_mode: accessMode
- })
- return response.data.data
+ access_mode: accessMode,
+ });
}
- /**
- * 我的文件批量 ZIP 打包下载
- */
static async batchDownloadMyFiles(ids: string[]): Promise {
- const response = await apiClient.post('/api/v1/upload/download/batch', { ids }, {
+ return this.post('/download/batch', { ids }, {
responseType: 'blob',
- } as InternalAxiosRequestConfig)
- return response.data
+ } as InternalAxiosRequestConfig);
}
-}
+
+ static getDownloadUrl(id: string): string {
+ return `${this.basePath}/download/${id}`;
+ }
+}
\ No newline at end of file
diff --git a/frontend/lib/services/upload/utils.ts b/frontend/lib/services/upload/utils.ts
new file mode 100644
index 00000000..3ff31517
--- /dev/null
+++ b/frontend/lib/services/upload/utils.ts
@@ -0,0 +1,18 @@
+export type ImageQuality = 'low' | 'medium' | 'high' | 'origin';
+
+export function getFileUrl(
+ id: string | number | null | undefined,
+ quality: ImageQuality = 'origin',
+): string | null {
+ if (!id) return null;
+ if (quality === 'origin') return `/f/${id}`;
+ return `/f/${id}?quality=${quality}`;
+}
+
+export function formatFileSize(bytes: number): string {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
+}
\ No newline at end of file