修复Docker构建:添加被gitignore忽略的logs页面文件,修复前端.gitignore的logs规则

This commit is contained in:
Admin
2026-04-29 22:31:52 +08:00
parent 51c34410c8
commit cc20f2c74f
8 changed files with 881 additions and 1 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# Logs
logs
/logs
*.log
npm-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>
+231
View File
@@ -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>