修复Docker构建:添加被gitignore忽略的logs页面文件,修复前端.gitignore的logs规则
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
/logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
|
|||||||
@@ -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<string, string>
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
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<Log>[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: 'log_type',
|
||||||
|
header: () => t('developer.logs.columns.type'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const type = row.getValue('log_type') as string
|
||||||
|
const typeMap: Record<string, { text: string, variant: any }> = {
|
||||||
|
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<string, { text: string, variant: any }> = {
|
||||||
|
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 '-'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Table } from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
import { X } from 'lucide-vue-next'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
|
||||||
|
import type { Log } from '@/pages/admin/logs/data/schema'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
interface DataTableToolbarProps {
|
||||||
|
table: Table<Log>
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<DataTableToolbarProps>()
|
||||||
|
|
||||||
|
const isFiltered = computed(() => props.table.getState().columnFilters.length > 0)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center flex-1 space-x-2">
|
||||||
|
<Input
|
||||||
|
:placeholder="t('developer.logs.searchPlaceholder')"
|
||||||
|
:model-value="(table.getColumn('action')?.getFilterValue() as string) ?? ''"
|
||||||
|
class="h-8 w-[150px] lg:w-[250px]"
|
||||||
|
@input="table.getColumn('action')?.setFilterValue($event.target.value)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
v-if="isFiltered"
|
||||||
|
variant="ghost"
|
||||||
|
class="h-8 px-2 lg:px-3"
|
||||||
|
@click="table.resetColumnFilters()"
|
||||||
|
>
|
||||||
|
{{ t('common.reset') }}
|
||||||
|
<X class="size-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import type { ColumnDef } from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
import DataTable from '@/components/data-table/data-table.vue'
|
||||||
|
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
||||||
|
import { SelectColumn } from '@/components/data-table/table-columns'
|
||||||
|
import DataTableViewOptions from '@/components/data-table/view-options.vue'
|
||||||
|
|
||||||
|
import type { Log } from '@/pages/admin/logs/data/schema'
|
||||||
|
|
||||||
|
import { getColumns } from '@/pages/admin/logs/components/columns'
|
||||||
|
import DataTableToolbar from '@/pages/admin/logs/components/data-table-toolbar.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
loading?: boolean
|
||||||
|
data: Log[]
|
||||||
|
serverPage?: number
|
||||||
|
serverPageSize?: number
|
||||||
|
serverTotal?: number
|
||||||
|
serverTotalPages?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
refresh: []
|
||||||
|
'page-change': [page: number]
|
||||||
|
'page-size-change': [pageSize: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const columns = computed(() => [
|
||||||
|
SelectColumn as ColumnDef<Log>,
|
||||||
|
...getColumns(t),
|
||||||
|
])
|
||||||
|
|
||||||
|
const table = generateVueTable<Log>({
|
||||||
|
get data() { return props.data },
|
||||||
|
get loading() { return props.loading },
|
||||||
|
columns: columns.value,
|
||||||
|
}, columns.value)
|
||||||
|
|
||||||
|
const columnLabels: Record<string, string> = {
|
||||||
|
'select': 'developer.logs.select',
|
||||||
|
'type': 'developer.logs.columns.type',
|
||||||
|
'action': 'developer.logs.columns.action',
|
||||||
|
'details': 'developer.logs.columns.detail',
|
||||||
|
'status': 'developer.logs.columns.status',
|
||||||
|
'ip': 'developer.logs.columns.ip',
|
||||||
|
'application_name': 'developer.logs.columns.application',
|
||||||
|
'user_username': 'developer.logs.columns.user',
|
||||||
|
'created_at': 'developer.logs.columns.time',
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverPagination = computed(() => {
|
||||||
|
if (props.serverPage && props.serverPageSize && props.serverTotal) {
|
||||||
|
return {
|
||||||
|
page: props.serverPage,
|
||||||
|
pageSize: props.serverPageSize,
|
||||||
|
total: props.serverTotal,
|
||||||
|
onPageChange: (page: number) => emit('page-change', page),
|
||||||
|
onPageSizeChange: (pageSize: number) => emit('page-size-change', pageSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
table,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DataTable :columns="columns" :data :loading :table :server-pagination @refresh="emit('refresh')">
|
||||||
|
<template #toolbar>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<DataTableToolbar :table />
|
||||||
|
<slot name="filters" />
|
||||||
|
</div>
|
||||||
|
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</template>
|
||||||
@@ -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<typeof logSchema>
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue'
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { BasicPage } from '@/components/global-layout'
|
||||||
|
import SingleFilter from '@/components/data-table/single-filter.vue'
|
||||||
|
import DateRangeFilter from '@/components/data-table/date-range-filter.vue'
|
||||||
|
import api from '@/services/api'
|
||||||
|
|
||||||
|
import type { Log } from '@/pages/admin/logs/data/schema'
|
||||||
|
|
||||||
|
import DataTable from '@/pages/admin/logs/components/data-table.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
interface Application {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogsResponse {
|
||||||
|
logs: Log[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
page_size: number
|
||||||
|
total_page: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const logs = ref<Log[]>([])
|
||||||
|
const applications = ref<Application[]>([])
|
||||||
|
|
||||||
|
const appFilter = ref<string>('all')
|
||||||
|
const typeFilter = ref<string>('all')
|
||||||
|
const statusFilter = ref<string>('all')
|
||||||
|
const startDate = ref<string>('')
|
||||||
|
const endDate = ref<string>('')
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
const total = ref(0)
|
||||||
|
const totalPages = ref(0)
|
||||||
|
|
||||||
|
const applicationOptions = computed(() => {
|
||||||
|
return applications.value.map(app => ({
|
||||||
|
label: app.name,
|
||||||
|
value: String(app.id),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const typeOptions = computed(() => [
|
||||||
|
{ label: t('developer.logs.types.all'), value: 'all' },
|
||||||
|
{ label: t('developer.logs.types.operation'), value: 'operation' },
|
||||||
|
{ label: t('developer.logs.types.verification'), value: 'verification' },
|
||||||
|
{ label: t('developer.logs.types.exception'), value: 'exception' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const statusOptions = computed(() => [
|
||||||
|
{ label: t('developer.logs.statuses.all'), value: 'all' },
|
||||||
|
{ label: t('developer.success'), value: 'success' },
|
||||||
|
{ label: t('developer.failed'), value: 'failed' },
|
||||||
|
])
|
||||||
|
|
||||||
|
async function fetchApplications() {
|
||||||
|
try {
|
||||||
|
const data = await api.get<{ applications: Application[] }>('/dev/applications')
|
||||||
|
applications.value = data?.applications || []
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('获取应用列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLogs() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('page', String(currentPage.value))
|
||||||
|
params.append('page_size', String(pageSize.value))
|
||||||
|
|
||||||
|
if (appFilter.value && appFilter.value !== 'all') {
|
||||||
|
params.append('application_id', appFilter.value)
|
||||||
|
}
|
||||||
|
if (typeFilter.value && typeFilter.value !== 'all') {
|
||||||
|
params.append('type', typeFilter.value)
|
||||||
|
}
|
||||||
|
if (statusFilter.value && statusFilter.value !== 'all') {
|
||||||
|
params.append('status', statusFilter.value)
|
||||||
|
}
|
||||||
|
if (startDate.value) {
|
||||||
|
params.append('start_date', startDate.value)
|
||||||
|
}
|
||||||
|
if (endDate.value) {
|
||||||
|
params.append('end_date', endDate.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await api.get<LogsResponse>(`/dev/logs?${params.toString()}`)
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
logs.value = data.logs || []
|
||||||
|
total.value = data.total || 0
|
||||||
|
currentPage.value = data.page || 1
|
||||||
|
totalPages.value = data.total_page || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('获取日志记录失败:', error)
|
||||||
|
logs.value = []
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([appFilter, typeFilter, statusFilter, startDate, endDate], () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchLogs()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchApplications()
|
||||||
|
fetchLogs()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BasicPage
|
||||||
|
:title="t('developer.logs.title')"
|
||||||
|
:description="t('developer.logs.description')"
|
||||||
|
sticky
|
||||||
|
>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<UiCard>
|
||||||
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
|
<UiCardTitle class="text-sm font-medium">
|
||||||
|
{{ t('developer.logs.totalLogs') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
<Icon icon="lucide:file-text" class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ total }}
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard>
|
||||||
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
|
<UiCardTitle class="text-sm font-medium">
|
||||||
|
{{ t('developer.logs.successCount') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
<Icon icon="lucide:check-circle" class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold text-green-600">
|
||||||
|
{{ logs.filter(log => log.status === 'success').length }}
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard>
|
||||||
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
|
<UiCardTitle class="text-sm font-medium">
|
||||||
|
{{ t('developer.logs.failedCount') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
<Icon icon="lucide:x-circle" class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold text-red-600">
|
||||||
|
{{ logs.filter(log => log.status === 'failed').length }}
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
|
|
||||||
|
<UiCard>
|
||||||
|
<UiCardHeader class="flex flex-row items-center justify-between pb-2 space-y-0">
|
||||||
|
<UiCardTitle class="text-sm font-medium">
|
||||||
|
{{ t('developer.logs.operationCount') }}
|
||||||
|
</UiCardTitle>
|
||||||
|
<Icon icon="lucide:activity" class="size-4 text-muted-foreground" />
|
||||||
|
</UiCardHeader>
|
||||||
|
<UiCardContent>
|
||||||
|
<div class="text-2xl font-bold">
|
||||||
|
{{ logs.filter(log => log.log_type === 'operation').length }}
|
||||||
|
</div>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UiCard>
|
||||||
|
<UiCardContent class="p-6">
|
||||||
|
<DataTable
|
||||||
|
ref="tableRef"
|
||||||
|
:loading
|
||||||
|
:data="logs"
|
||||||
|
:server-page="currentPage"
|
||||||
|
:server-page-size="pageSize"
|
||||||
|
:server-total="total"
|
||||||
|
:server-total-pages="totalPages"
|
||||||
|
@refresh="fetchLogs"
|
||||||
|
@page-change="currentPage = $event; fetchLogs()"
|
||||||
|
@page-size-change="pageSize = $event; currentPage = 1; fetchLogs()"
|
||||||
|
>
|
||||||
|
<template #filters>
|
||||||
|
<SingleFilter
|
||||||
|
v-model="appFilter"
|
||||||
|
:title="t('developer.logs.columns.application')"
|
||||||
|
:options="[{ label: t('developer.logs.allApplications'), value: 'all' }, ...applicationOptions]"
|
||||||
|
/>
|
||||||
|
<SingleFilter
|
||||||
|
v-model="typeFilter"
|
||||||
|
:title="t('developer.logs.columns.type')"
|
||||||
|
:options="typeOptions"
|
||||||
|
/>
|
||||||
|
<SingleFilter
|
||||||
|
v-model="statusFilter"
|
||||||
|
:title="t('developer.logs.columns.status')"
|
||||||
|
:options="statusOptions"
|
||||||
|
/>
|
||||||
|
<DateRangeFilter
|
||||||
|
v-model:start-model-value="startDate"
|
||||||
|
v-model:end-model-value="endDate"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</UiCardContent>
|
||||||
|
</UiCard>
|
||||||
|
</div>
|
||||||
|
</BasicPage>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import type { ColumnDef } from '@tanstack/vue-table'
|
||||||
|
|
||||||
|
import DataTable from '@/components/data-table/data-table.vue'
|
||||||
|
import { generateVueTable } from '@/components/data-table/use-generate-vue-table'
|
||||||
|
import { SelectColumn } from '@/components/data-table/table-columns'
|
||||||
|
import DataTableViewOptions from '@/components/data-table/view-options.vue'
|
||||||
|
|
||||||
|
import type { Log } from '@/pages/developer/logs/data/schema'
|
||||||
|
|
||||||
|
import { getColumns } from '@/pages/developer/logs/components/columns'
|
||||||
|
import DataTableToolbar from '@/pages/developer/logs/components/data-table-toolbar.vue'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
loading?: boolean
|
||||||
|
data: Log[]
|
||||||
|
serverPage?: number
|
||||||
|
serverPageSize?: number
|
||||||
|
serverTotal?: number
|
||||||
|
serverTotalPages?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
refresh: []
|
||||||
|
'page-change': [page: number]
|
||||||
|
'page-size-change': [pageSize: number]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const columns = computed(() => [
|
||||||
|
SelectColumn as ColumnDef<Log>,
|
||||||
|
...getColumns(t),
|
||||||
|
])
|
||||||
|
|
||||||
|
const table = generateVueTable<Log>({
|
||||||
|
get data() { return props.data },
|
||||||
|
get loading() { return props.loading },
|
||||||
|
columns: columns.value,
|
||||||
|
}, columns.value)
|
||||||
|
|
||||||
|
const columnLabels: Record<string, string> = {
|
||||||
|
'select': 'developer.logs.select',
|
||||||
|
'type': 'developer.logs.columns.type',
|
||||||
|
'action': 'developer.logs.columns.action',
|
||||||
|
'details': 'developer.logs.columns.detail',
|
||||||
|
'status': 'developer.logs.columns.status',
|
||||||
|
'ip': 'developer.logs.columns.ip',
|
||||||
|
'application_name': 'developer.logs.columns.application',
|
||||||
|
'user_username': 'developer.logs.columns.user',
|
||||||
|
'created_at': 'developer.logs.columns.time',
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverPagination = computed(() => {
|
||||||
|
if (props.serverPage && props.serverPageSize && props.serverTotal) {
|
||||||
|
return {
|
||||||
|
page: props.serverPage,
|
||||||
|
pageSize: props.serverPageSize,
|
||||||
|
total: props.serverTotal,
|
||||||
|
onPageChange: (page: number) => emit('page-change', page),
|
||||||
|
onPageSizeChange: (pageSize: number) => emit('page-size-change', pageSize),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
table,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DataTable :columns="columns" :data :loading :table :server-pagination @refresh="emit('refresh')">
|
||||||
|
<template #toolbar>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<slot name="actions" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<DataTableToolbar :table />
|
||||||
|
<slot name="filters" />
|
||||||
|
</div>
|
||||||
|
<DataTableViewOptions :table="table" :column-labels="columnLabels" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DataTable>
|
||||||
|
</template>
|
||||||
@@ -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<typeof logSchema>
|
||||||
Reference in New Issue
Block a user