From cc20f2c74fb3286e7daf4515e7151b6c4a1e3f40 Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 29 Apr 2026 22:31:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DDocker=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=EF=BC=9A=E6=B7=BB=E5=8A=A0=E8=A2=ABgitignore=E5=BF=BD=E7=95=A5?= =?UTF-8?q?=E7=9A=84logs=E9=A1=B5=E9=9D=A2=E6=96=87=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=89=8D=E7=AB=AF.gitignore=E7=9A=84logs?= =?UTF-8?q?=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.gitignore | 2 +- .../pages/admin/logs/components/columns.ts | 346 ++++++++++++++++++ .../logs/components/data-table-toolbar.vue | 43 +++ .../admin/logs/components/data-table.vue | 93 +++++ frontend/src/pages/admin/logs/data/schema.ts | 37 ++ frontend/src/pages/admin/logs/index.vue | 231 ++++++++++++ .../developer/logs/components/data-table.vue | 93 +++++ .../src/pages/developer/logs/data/schema.ts | 37 ++ 8 files changed, 881 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/admin/logs/components/columns.ts create mode 100644 frontend/src/pages/admin/logs/components/data-table-toolbar.vue create mode 100644 frontend/src/pages/admin/logs/components/data-table.vue create mode 100644 frontend/src/pages/admin/logs/data/schema.ts create mode 100644 frontend/src/pages/admin/logs/index.vue create mode 100644 frontend/src/pages/developer/logs/components/data-table.vue create mode 100644 frontend/src/pages/developer/logs/data/schema.ts diff --git a/frontend/.gitignore b/frontend/.gitignore index e272c67..d66e075 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,5 +1,5 @@ # Logs -logs +/logs *.log npm-debug.log* yarn-debug.log* diff --git a/frontend/src/pages/admin/logs/components/columns.ts b/frontend/src/pages/admin/logs/components/columns.ts new file mode 100644 index 0000000..c1b653e --- /dev/null +++ b/frontend/src/pages/admin/logs/components/columns.ts @@ -0,0 +1,346 @@ +import type { ColumnDef } from '@tanstack/vue-table' +import { h } from 'vue' +import type { Composer } from 'vue-i18n' + +import Badge from '@/components/ui/badge/Badge.vue' + +import type { Log } from '@/pages/developer/logs/data/schema' + +function translateDetails(t: Composer['t'], details: string): string { + if (!details) return '-' + + const patterns: Array<{ + pattern: RegExp + template: string + extract: (match: RegExpMatchArray) => Record + }> = [ + { + pattern: /^用户注册: (.+)$/, + template: 'developer.logs.details.register', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^注册失败: 用户已存在 - (.+)$/, + template: 'developer.logs.details.register_failed_exists', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^用户登录: (.+)$/, + template: 'developer.logs.details.login', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^登录失败: 用户不存在 - (.+)$/, + template: 'developer.logs.details.login_failed_not_found', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^登录失败: 密码错误 - (.+)$/, + template: 'developer.logs.details.login_failed_password', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^登录失败: 余额不足 - (.+)$/, + template: 'developer.logs.details.login_failed_balance', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^登录失败: 设备绑定数量已达上限 - (.+)$/, + template: 'developer.logs.details.login_failed_device_limit', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^登录失败: 多开数量已达上限 - (.+)$/, + template: 'developer.logs.details.login_failed_multi_limit', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^用户充值: (.+), 卡密: (.+), 金额: ([\d.]+)$/, + template: 'developer.logs.details.recharge', + extract: (m) => ({ username: m[1], card: m[2], amount: m[3] }), + }, + { + pattern: /^充值失败: 用户不存在 - (.+)$/, + template: 'developer.logs.details.recharge_failed_user_not_found', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^充值失败: 用户已是永久会员 - (.+)$/, + template: 'developer.logs.details.recharge_failed_permanent', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^充值失败: 卡密不存在 - (.+)$/, + template: 'developer.logs.details.recharge_failed_card_not_found', + extract: (m) => ({ card: m[1] }), + }, + { + pattern: /^充值失败: 卡密已使用 - (.+)$/, + template: 'developer.logs.details.recharge_failed_card_used', + extract: (m) => ({ card: m[1] }), + }, + { + pattern: /^用户解绑设备: (.+)$/, + template: 'developer.logs.details.unbind_device', + extract: (m) => ({ device: m[1] }), + }, + { + pattern: /^扩展API充值: 用户(.+), 类型:(.+), 数量:(\d+)$/, + template: 'developer.logs.details.extension_recharge', + extract: (m) => ({ username: m[1], type: m[2], amount: m[3] }), + }, + { + pattern: /^扩展API充值失败: 用户不存在 - (.+)$/, + template: 'developer.logs.details.extension_recharge_failed_user_not_found', + extract: (m) => ({ userId: m[1] }), + }, + { + pattern: /^扩展API充值失败: 用户已是永久会员 - (.+)$/, + template: 'developer.logs.details.extension_recharge_failed_permanent', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^扩展API充值失败: 类型无效 - (.+)$/, + template: 'developer.logs.details.extension_recharge_failed_type', + extract: (m) => ({ type: m[1] }), + }, + { + pattern: /^扩展API充值失败: 保存失败 - (.+)$/, + template: 'developer.logs.details.extension_recharge_failed_save', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^扩展API扣费: 用户(.+), 类型:(.+), 数量:(\d+)$/, + template: 'developer.logs.details.extension_deduct', + extract: (m) => ({ username: m[1], type: m[2], amount: m[3] }), + }, + { + pattern: /^扩展API扣费失败: 用户不存在 - (.+)$/, + template: 'developer.logs.details.extension_deduct_failed_user_not_found', + extract: (m) => ({ userId: m[1] }), + }, + { + pattern: /^扩展API扣费失败: 用户已是永久会员 - (.+)$/, + template: 'developer.logs.details.extension_deduct_failed_permanent', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^扩展API扣费失败: 用户订阅已过期 - (.+)$/, + template: 'developer.logs.details.extension_deduct_failed_expired', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^扩展API扣费失败: 余额不足 - (.+)$/, + template: 'developer.logs.details.extension_deduct_failed_balance', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^扩展API扣费失败: 类型无效 - (.+)$/, + template: 'developer.logs.details.extension_deduct_failed_type', + extract: (m) => ({ type: m[1] }), + }, + { + pattern: /^扩展API扣费失败: 保存失败 - (.+)$/, + template: 'developer.logs.details.extension_deduct_failed_save', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^创建应用: (.+)$/, + template: 'developer.logs.details.create_application', + extract: (m) => ({ name: m[1] }), + }, + { + pattern: /^更新应用: (.+)$/, + template: 'developer.logs.details.update_application', + extract: (m) => ({ name: m[1] }), + }, + { + pattern: /^删除应用: (.+)$/, + template: 'developer.logs.details.delete_application', + extract: (m) => ({ name: m[1] }), + }, + { + pattern: /^创建用户: (.+) \(应用: (.+)\)$/, + template: 'developer.logs.details.create_user', + extract: (m) => ({ username: m[1], app: m[2] }), + }, + { + pattern: /^更新用户: (.+)$/, + template: 'developer.logs.details.update_user', + extract: (m) => ({ username: m[1] }), + }, + { + pattern: /^删除用户: (.+) \(应用: (.+)\)$/, + template: 'developer.logs.details.delete_user', + extract: (m) => ({ username: m[1], app: m[2] }), + }, + { + pattern: /^解绑设备: (.+) \(应用: (.+)\)$/, + template: 'developer.logs.details.unbind_device_app', + extract: (m) => ({ device: m[1], app: m[2] }), + }, + { + pattern: /^批量解绑设备: (\d+)个$/, + template: 'developer.logs.details.batch_unbind', + extract: (m) => ({ count: m[1] }), + }, + { + pattern: /^创建工单: (.+)$/, + template: 'developer.logs.details.create_ticket', + extract: (m) => ({ title: m[1] }), + }, + { + pattern: /^删除工单: (.+)$/, + template: 'developer.logs.details.delete_ticket', + extract: (m) => ({ title: m[1] }), + }, + { + pattern: /^创建Webhook: (.+)$/, + template: 'developer.logs.details.create_webhook', + extract: (m) => ({ name: m[1] }), + }, + { + pattern: /^删除Webhook: (.+)$/, + template: 'developer.logs.details.delete_webhook', + extract: (m) => ({ name: m[1] }), + }, + { + pattern: /^创建云端常量: (.+)$/, + template: 'developer.logs.details.create_cloud_constant', + extract: (m) => ({ key: m[1] }), + }, + { + pattern: /^更新云端常量: (.+)$/, + template: 'developer.logs.details.update_cloud_constant', + extract: (m) => ({ key: m[1] }), + }, + { + pattern: /^删除云端常量: (.+)$/, + template: 'developer.logs.details.delete_cloud_constant', + extract: (m) => ({ key: m[1] }), + }, + { + pattern: /^创建卡密类型: (.+)$/, + template: 'developer.logs.details.create_card_type', + extract: (m) => ({ name: m[1] }), + }, + { + pattern: /^更新卡密类型: (.+)$/, + template: 'developer.logs.details.update_card_type', + extract: (m) => ({ name: m[1] }), + }, + { + pattern: /^删除卡密类型: (.+)$/, + template: 'developer.logs.details.delete_card_type', + extract: (m) => ({ name: m[1] }), + }, + ] + + for (const { pattern, template, extract } of patterns) { + const match = details.match(pattern) + if (match) { + const params = extract(match) + return t(template, params) + } + } + + return details +} + +export function getColumns(t: Composer['t']): ColumnDef[] { + return [ + { + accessorKey: 'log_type', + header: () => t('developer.logs.columns.type'), + cell: ({ row }) => { + const type = row.getValue('log_type') as string + const typeMap: Record = { + operation: { text: t('developer.logs.types.operation'), variant: 'default' }, + verification: { text: t('developer.logs.types.verification'), variant: 'secondary' }, + exception: { text: t('developer.logs.types.exception'), variant: 'destructive' }, + } + const { text, variant } = typeMap[type] || { text: type, variant: 'default' } + return h(Badge, { variant }, () => text) + }, + }, + { + accessorKey: 'action', + header: () => t('developer.logs.columns.action'), + cell: ({ row }) => { + const action = row.getValue('action') as string + if (!action) return '-' + const actionKey = `developer.logs.actions.${action}` + const translated = t(actionKey) + return translated === actionKey ? action : translated + }, + }, + { + accessorKey: 'details', + header: () => t('developer.logs.columns.detail'), + cell: ({ row }) => { + const details = row.getValue('details') as string + if (!details) return '-' + const translated = translateDetails(t, details) + return h('div', { class: 'max-w-xs truncate', title: details }, translated) + }, + }, + { + accessorKey: 'status', + header: () => t('developer.logs.columns.status'), + cell: ({ row }) => { + const status = row.getValue('status') as string + const statusMap: Record = { + success: { text: t('developer.success'), variant: 'default' }, + failed: { text: t('developer.failed'), variant: 'destructive' }, + } + const { text, variant } = statusMap[status] || { text: status, variant: 'default' } + return h(Badge, { variant }, () => text) + }, + }, + { + accessorKey: 'ip_address', + header: () => t('developer.logs.columns.ip'), + cell: ({ row }) => { + const ip = row.getValue('ip_address') as string + return h('code', { class: 'text-xs font-mono' }, ip || '-') + }, + }, + { + accessorKey: 'application.name', + header: () => t('developer.logs.columns.application'), + cell: ({ row }) => { + const appName = row.original.application?.name + return appName ? h(Badge, { variant: 'outline' }, () => appName) : '-' + }, + }, + { + accessorKey: 'user.username', + header: () => t('developer.logs.columns.user'), + cell: ({ row }) => { + const username = row.original.user?.username || row.original.app_user?.username + return username || '-' + }, + }, + { + accessorKey: 'created_at', + header: () => t('developer.logs.columns.time'), + cell: ({ row }) => { + const createdAt = row.getValue('created_at') + if (!createdAt) return '-' + try { + const date = new Date(createdAt as string) + if (isNaN(date.getTime())) return '-' + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) + } catch { + return '-' + } + }, + }, + ] +} diff --git a/frontend/src/pages/admin/logs/components/data-table-toolbar.vue b/frontend/src/pages/admin/logs/components/data-table-toolbar.vue new file mode 100644 index 0000000..749b5ae --- /dev/null +++ b/frontend/src/pages/admin/logs/components/data-table-toolbar.vue @@ -0,0 +1,43 @@ + + + diff --git a/frontend/src/pages/admin/logs/components/data-table.vue b/frontend/src/pages/admin/logs/components/data-table.vue new file mode 100644 index 0000000..9cb4f7a --- /dev/null +++ b/frontend/src/pages/admin/logs/components/data-table.vue @@ -0,0 +1,93 @@ + + + diff --git a/frontend/src/pages/admin/logs/data/schema.ts b/frontend/src/pages/admin/logs/data/schema.ts new file mode 100644 index 0000000..858d83e --- /dev/null +++ b/frontend/src/pages/admin/logs/data/schema.ts @@ -0,0 +1,37 @@ +import { z } from 'zod' + +export const logTypeSchema = z.enum(['operation', 'verification', 'exception']) +export const logStatusSchema = z.enum(['success', 'failed']) + +export const logSchema = z.object({ + id: z.union([z.string(), z.number()]), + log_type: logTypeSchema, + action: z.string(), + resource: z.string().optional().nullable(), + resource_id: z.union([z.string(), z.number()]).optional().nullable(), + details: z.string(), + status: logStatusSchema, + ip_address: z.string(), + user_agent: z.string().optional().nullable(), + device_id: z.string().optional().nullable(), + level: z.string().optional().nullable(), + error_message: z.string().optional().nullable(), + application_id: z.union([z.string(), z.number()]).optional().nullable(), + user_id: z.union([z.string(), z.number()]).optional().nullable(), + app_user_id: z.union([z.string(), z.number()]).optional().nullable(), + created_at: z.string(), + application: z.object({ + id: z.union([z.string(), z.number()]), + name: z.string(), + }).optional().nullable(), + user: z.object({ + id: z.union([z.string(), z.number()]), + username: z.string(), + }).optional().nullable(), + app_user: z.object({ + id: z.union([z.string(), z.number()]), + username: z.string(), + }).optional().nullable(), +}) + +export type Log = z.infer diff --git a/frontend/src/pages/admin/logs/index.vue b/frontend/src/pages/admin/logs/index.vue new file mode 100644 index 0000000..3ecd4ca --- /dev/null +++ b/frontend/src/pages/admin/logs/index.vue @@ -0,0 +1,231 @@ + + + diff --git a/frontend/src/pages/developer/logs/components/data-table.vue b/frontend/src/pages/developer/logs/components/data-table.vue new file mode 100644 index 0000000..00a9e52 --- /dev/null +++ b/frontend/src/pages/developer/logs/components/data-table.vue @@ -0,0 +1,93 @@ + + + diff --git a/frontend/src/pages/developer/logs/data/schema.ts b/frontend/src/pages/developer/logs/data/schema.ts new file mode 100644 index 0000000..858d83e --- /dev/null +++ b/frontend/src/pages/developer/logs/data/schema.ts @@ -0,0 +1,37 @@ +import { z } from 'zod' + +export const logTypeSchema = z.enum(['operation', 'verification', 'exception']) +export const logStatusSchema = z.enum(['success', 'failed']) + +export const logSchema = z.object({ + id: z.union([z.string(), z.number()]), + log_type: logTypeSchema, + action: z.string(), + resource: z.string().optional().nullable(), + resource_id: z.union([z.string(), z.number()]).optional().nullable(), + details: z.string(), + status: logStatusSchema, + ip_address: z.string(), + user_agent: z.string().optional().nullable(), + device_id: z.string().optional().nullable(), + level: z.string().optional().nullable(), + error_message: z.string().optional().nullable(), + application_id: z.union([z.string(), z.number()]).optional().nullable(), + user_id: z.union([z.string(), z.number()]).optional().nullable(), + app_user_id: z.union([z.string(), z.number()]).optional().nullable(), + created_at: z.string(), + application: z.object({ + id: z.union([z.string(), z.number()]), + name: z.string(), + }).optional().nullable(), + user: z.object({ + id: z.union([z.string(), z.number()]), + username: z.string(), + }).optional().nullable(), + app_user: z.object({ + id: z.union([z.string(), z.number()]), + username: z.string(), + }).optional().nullable(), +}) + +export type Log = z.infer