日志记录
This commit is contained in:
parent
07d1c2274b
commit
b26fe1e117
|
|
@ -29,6 +29,7 @@ import { SyncMedusaButton as SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08 }
|
||||||
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
|
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
|
||||||
import { ForceSyncButton as ForceSyncButton_28396efe36d6238add95cf44109e281c } from '../../../components/sync/ForceSyncButton'
|
import { ForceSyncButton as ForceSyncButton_28396efe36d6238add95cf44109e281c } from '../../../components/sync/ForceSyncButton'
|
||||||
import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel'
|
import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel'
|
||||||
|
import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView'
|
||||||
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
||||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||||
|
|
||||||
|
|
@ -64,6 +65,7 @@ export const importMap = {
|
||||||
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
|
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
|
||||||
"/components/sync/ForceSyncButton#ForceSyncButton": ForceSyncButton_28396efe36d6238add95cf44109e281c,
|
"/components/sync/ForceSyncButton#ForceSyncButton": ForceSyncButton_28396efe36d6238add95cf44109e281c,
|
||||||
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
|
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
|
||||||
|
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
|
||||||
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
||||||
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除日志 API
|
||||||
|
* DELETE /api/delete-logs?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD
|
||||||
|
*/
|
||||||
|
export async function DELETE(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
// 检查用户权限
|
||||||
|
const { user } = await payload.auth({ headers: req.headers })
|
||||||
|
if (!user || !user.roles?.includes('admin')) {
|
||||||
|
return Response.json({ error: '需要管理员权限' }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日期参数
|
||||||
|
const { searchParams } = new URL(req.url)
|
||||||
|
const startDate = searchParams.get('startDate')
|
||||||
|
const endDate = searchParams.get('endDate')
|
||||||
|
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
return Response.json({ error: '请提供开始和结束日期' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证日期格式
|
||||||
|
const start = new Date(startDate)
|
||||||
|
const end = new Date(endDate)
|
||||||
|
|
||||||
|
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
|
||||||
|
return Response.json({ error: '无效的日期格式' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置时间范围
|
||||||
|
start.setHours(0, 0, 0, 0)
|
||||||
|
end.setHours(23, 59, 59, 999)
|
||||||
|
|
||||||
|
// 查询要删除的日志
|
||||||
|
const logsToDelete = await payload.find({
|
||||||
|
collection: 'logs',
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
createdAt: {
|
||||||
|
greater_than_equal: start.toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: {
|
||||||
|
less_than_equal: end.toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
limit: 10000, // 限制一次删除数量
|
||||||
|
})
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
|
let deletedCount = 0
|
||||||
|
for (const log of logsToDelete.docs) {
|
||||||
|
try {
|
||||||
|
await payload.delete({
|
||||||
|
collection: 'logs',
|
||||||
|
id: log.id,
|
||||||
|
context: { skipHooks: true }, // 跳过钩子
|
||||||
|
})
|
||||||
|
deletedCount++
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to delete log ${log.id}:`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
message: `成功删除 ${deletedCount} 条日志记录`,
|
||||||
|
deletedCount,
|
||||||
|
totalFound: logsToDelete.totalDocs,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete logs error:', error)
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: '删除失败',
|
||||||
|
message: error instanceof Error ? error.message : '未知错误',
|
||||||
|
},
|
||||||
|
{ status: 500 },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload'
|
||||||
|
import { logAfterChange, logAfterDelete } from '../hooks/logAction'
|
||||||
import {
|
import {
|
||||||
BlocksFeature,
|
BlocksFeature,
|
||||||
BoldFeature,
|
BoldFeature,
|
||||||
|
|
@ -220,5 +221,7 @@ export const Announcements: CollectionConfig = {
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
afterChange: [logAfterChange],
|
||||||
|
afterDelete: [logAfterDelete],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload'
|
||||||
|
import { logAfterChange, logAfterDelete } from '../hooks/logAction'
|
||||||
import {
|
import {
|
||||||
BlocksFeature,
|
BlocksFeature,
|
||||||
BoldFeature,
|
BoldFeature,
|
||||||
|
|
@ -330,5 +331,7 @@ export const Articles: CollectionConfig = {
|
||||||
return data
|
return data
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
afterChange: [logAfterChange],
|
||||||
|
afterDelete: [logAfterDelete],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
import type { CollectionConfig } from 'payload'
|
||||||
|
|
||||||
|
export const Logs: CollectionConfig = {
|
||||||
|
slug: 'logs',
|
||||||
|
admin: {
|
||||||
|
useAsTitle: 'action',
|
||||||
|
defaultColumns: ['action', 'collection', 'user', 'createdAt'],
|
||||||
|
description: '系统操作日志记录',
|
||||||
|
group: '系统',
|
||||||
|
pagination: {
|
||||||
|
defaultLimit: 50,
|
||||||
|
},
|
||||||
|
listSearchableFields: ['action', 'collection', 'documentId'],
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
// 只有 admin 和 editor 可以查看日志
|
||||||
|
read: ({ req: { user } }) => {
|
||||||
|
if (!user) return false
|
||||||
|
return user.roles?.includes('admin') || user.roles?.includes('editor') || false
|
||||||
|
},
|
||||||
|
// 禁止手动创建、更新日志(只能通过系统钩子自动创建)
|
||||||
|
create: () => false,
|
||||||
|
update: () => false,
|
||||||
|
// 只有 admin 可以删除
|
||||||
|
delete: ({ req: { user } }) => {
|
||||||
|
if (!user) return false
|
||||||
|
return user.roles?.includes('admin') || false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'action',
|
||||||
|
type: 'select',
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{ label: '创建', value: 'create' },
|
||||||
|
{ label: '更新', value: 'update' },
|
||||||
|
{ label: '删除', value: 'delete' },
|
||||||
|
{ label: '同步', value: 'sync' },
|
||||||
|
{ label: '登录', value: 'login' },
|
||||||
|
{ label: '登出', value: 'logout' },
|
||||||
|
],
|
||||||
|
admin: {
|
||||||
|
description: '操作类型',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'collection',
|
||||||
|
type: 'text',
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
admin: {
|
||||||
|
description: '操作的集合名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'documentId',
|
||||||
|
type: 'text',
|
||||||
|
index: true,
|
||||||
|
admin: {
|
||||||
|
description: '文档 ID',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'documentTitle',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
description: '文档标题',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'user',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: 'users',
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
admin: {
|
||||||
|
description: '操作用户',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'changes',
|
||||||
|
type: 'json',
|
||||||
|
admin: {
|
||||||
|
description: '变更内容(JSON 格式)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ip',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
description: 'IP 地址',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'userAgent',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
description: '浏览器信息',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload'
|
||||||
|
import { logAfterChange, logAfterDelete } from '../hooks/logAction'
|
||||||
|
|
||||||
export const Media: CollectionConfig = {
|
export const Media: CollectionConfig = {
|
||||||
slug: 'media',
|
slug: 'media',
|
||||||
|
|
@ -12,5 +13,9 @@ export const Media: CollectionConfig = {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
hooks: {
|
||||||
|
afterChange: [logAfterChange],
|
||||||
|
afterDelete: [logAfterDelete],
|
||||||
|
},
|
||||||
upload: true,
|
upload: true,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload'
|
||||||
|
import { logAfterChange, logAfterDelete } from '../hooks/logAction'
|
||||||
import {
|
import {
|
||||||
AlignFeature,
|
AlignFeature,
|
||||||
BlocksFeature,
|
BlocksFeature,
|
||||||
|
|
@ -200,5 +201,9 @@ export const Products: CollectionConfig = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
hooks: {
|
||||||
|
afterChange: [logAfterChange],
|
||||||
|
afterDelete: [logAfterDelete],
|
||||||
|
},
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,500 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { Button, useAuth } from '@payloadcms/ui'
|
||||||
|
|
||||||
|
interface Log {
|
||||||
|
id: string
|
||||||
|
action: string
|
||||||
|
collection: string
|
||||||
|
documentId?: string
|
||||||
|
documentTitle?: string
|
||||||
|
user: {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
createdAt: string
|
||||||
|
ip?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LogsManagerView() {
|
||||||
|
const { user } = useAuth()
|
||||||
|
const [logs, setLogs] = useState<Log[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [totalDocs, setTotalDocs] = useState(0)
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [limit] = useState(50)
|
||||||
|
|
||||||
|
// 筛选条件
|
||||||
|
const [days, setDays] = useState(7) // 默认显示最近7天
|
||||||
|
const [selectedCollection, setSelectedCollection] = useState('all')
|
||||||
|
const [selectedAction, setSelectedAction] = useState('all')
|
||||||
|
|
||||||
|
// 删除相关
|
||||||
|
const [deleteStartDate, setDeleteStartDate] = useState('')
|
||||||
|
const [deleteEndDate, setDeleteEndDate] = useState('')
|
||||||
|
const [deleteLoading, setDeleteLoading] = useState(false)
|
||||||
|
const [deleteMessage, setDeleteMessage] = useState('')
|
||||||
|
|
||||||
|
const isAdmin = user?.roles?.includes('admin')
|
||||||
|
|
||||||
|
// 加载日志
|
||||||
|
const loadLogs = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
// 计算日期范围
|
||||||
|
const endDate = new Date()
|
||||||
|
const startDate = new Date()
|
||||||
|
startDate.setDate(startDate.getDate() - days)
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
const whereConditions: any[] = [
|
||||||
|
{
|
||||||
|
createdAt: {
|
||||||
|
greater_than_equal: startDate.toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
createdAt: {
|
||||||
|
less_than_equal: endDate.toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (selectedCollection !== 'all') {
|
||||||
|
whereConditions.push({
|
||||||
|
collection: {
|
||||||
|
equals: selectedCollection,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedAction !== 'all') {
|
||||||
|
whereConditions.push({
|
||||||
|
action: {
|
||||||
|
equals: selectedAction,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
depth: '1',
|
||||||
|
limit: limit.toString(),
|
||||||
|
page: page.toString(),
|
||||||
|
sort: '-createdAt',
|
||||||
|
where: JSON.stringify({
|
||||||
|
and: whereConditions,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(`/admin/api/logs?${query}`)
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
setLogs(data.docs || [])
|
||||||
|
setTotalDocs(data.totalDocs || 0)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load logs:', error)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除指定日期范围的日志
|
||||||
|
const handleDeleteLogs = async () => {
|
||||||
|
if (!deleteStartDate || !deleteEndDate) {
|
||||||
|
setDeleteMessage('请选择开始和结束日期')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm(`确定删除 ${deleteStartDate} 至 ${deleteEndDate} 的所有日志吗?此操作不可撤销!`)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeleteLoading(true)
|
||||||
|
setDeleteMessage('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/delete-logs?startDate=${deleteStartDate}&endDate=${deleteEndDate}`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setDeleteMessage(data.message)
|
||||||
|
// 重新加载日志
|
||||||
|
loadLogs()
|
||||||
|
// 清空日期选择
|
||||||
|
setDeleteStartDate('')
|
||||||
|
setDeleteEndDate('')
|
||||||
|
} else {
|
||||||
|
setDeleteMessage(`删除失败: ${data.error}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setDeleteMessage(`删除出错: ${error instanceof Error ? error.message : '未知错误'}`)
|
||||||
|
} finally {
|
||||||
|
setDeleteLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快速删除按钮
|
||||||
|
const handleQuickDelete = (daysAgo: number) => {
|
||||||
|
const end = new Date()
|
||||||
|
const start = new Date()
|
||||||
|
start.setDate(start.getDate() - daysAgo)
|
||||||
|
|
||||||
|
setDeleteStartDate(start.toISOString().split('T')[0])
|
||||||
|
setDeleteEndDate(end.toISOString().split('T')[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadLogs()
|
||||||
|
}, [days, selectedCollection, selectedAction, page])
|
||||||
|
|
||||||
|
const actionLabels: Record<string, string> = {
|
||||||
|
create: '创建',
|
||||||
|
update: '更新',
|
||||||
|
delete: '删除',
|
||||||
|
sync: '同步',
|
||||||
|
login: '登录',
|
||||||
|
logout: '登出',
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionColors: Record<string, string> = {
|
||||||
|
create: '#10b981',
|
||||||
|
update: '#3b82f6',
|
||||||
|
delete: '#ef4444',
|
||||||
|
sync: '#8b5cf6',
|
||||||
|
login: '#14b8a6',
|
||||||
|
logout: '#6b7280',
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem', maxWidth: '1400px', margin: '0 auto' }}>
|
||||||
|
<h1 style={{ marginBottom: '2rem', fontSize: '2rem', fontWeight: 'bold' }}>
|
||||||
|
📋 操作日志管理
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* 筛选区域 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--theme-elevation-50)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '1.5rem',
|
||||||
|
marginBottom: '1.5rem',
|
||||||
|
border: '1px solid var(--theme-elevation-100)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 style={{ marginBottom: '1rem', fontSize: '1.25rem', fontWeight: '600' }}>
|
||||||
|
🔍 筛选条件
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>
|
||||||
|
时间范围
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={days}
|
||||||
|
onChange={(e) => setDays(Number(e.target.value))}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '0.5rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--theme-elevation-400)',
|
||||||
|
backgroundColor: 'var(--theme-elevation-0)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value={1}>最近 1 天</option>
|
||||||
|
<option value={3}>最近 3 天</option>
|
||||||
|
<option value={7}>最近 7 天</option>
|
||||||
|
<option value={15}>最近 15 天</option>
|
||||||
|
<option value={30}>最近 30 天</option>
|
||||||
|
<option value={90}>最近 90 天</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>
|
||||||
|
集合
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedCollection}
|
||||||
|
onChange={(e) => setSelectedCollection(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '0.5rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--theme-elevation-400)',
|
||||||
|
backgroundColor: 'var(--theme-elevation-0)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="all">全部</option>
|
||||||
|
<option value="products">商品</option>
|
||||||
|
<option value="announcements">公告</option>
|
||||||
|
<option value="articles">文章</option>
|
||||||
|
<option value="media">媒体文件</option>
|
||||||
|
<option value="users">用户</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>
|
||||||
|
操作类型
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedAction}
|
||||||
|
onChange={(e) => setSelectedAction(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '0.5rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--theme-elevation-400)',
|
||||||
|
backgroundColor: 'var(--theme-elevation-0)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="all">全部</option>
|
||||||
|
<option value="create">创建</option>
|
||||||
|
<option value="update">更新</option>
|
||||||
|
<option value="delete">删除</option>
|
||||||
|
<option value="sync">同步</option>
|
||||||
|
<option value="login">登录</option>
|
||||||
|
<option value="logout">登出</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem' }}>
|
||||||
|
<Button onClick={loadLogs} disabled={loading}>
|
||||||
|
{loading ? '加载中...' : '刷新'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 删除日志区域 (仅管理员) */}
|
||||||
|
{isAdmin && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--theme-elevation-50)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '1.5rem',
|
||||||
|
marginBottom: '1.5rem',
|
||||||
|
border: '1px solid var(--theme-error-500)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 style={{ marginBottom: '1rem', fontSize: '1.25rem', fontWeight: '600' }}>
|
||||||
|
🗑️ 删除日志(管理员)
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
|
<p style={{ marginBottom: '0.5rem' }}>快速选择:</p>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => handleQuickDelete(7)}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--theme-elevation-400)',
|
||||||
|
backgroundColor: 'var(--theme-elevation-0)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
7天前
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleQuickDelete(30)}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--theme-elevation-400)',
|
||||||
|
backgroundColor: 'var(--theme-elevation-0)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
30天前
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleQuickDelete(90)}
|
||||||
|
style={{
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--theme-elevation-400)',
|
||||||
|
backgroundColor: 'var(--theme-elevation-0)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
90天前
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: '1rem', alignItems: 'end' }}>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>
|
||||||
|
开始日期
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={deleteStartDate}
|
||||||
|
onChange={(e) => setDeleteStartDate(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '0.5rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--theme-elevation-400)',
|
||||||
|
backgroundColor: 'var(--theme-elevation-0)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: '500' }}>
|
||||||
|
结束日期
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={deleteEndDate}
|
||||||
|
onChange={(e) => setDeleteEndDate(e.target.value)}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '0.5rem',
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: '1px solid var(--theme-elevation-400)',
|
||||||
|
backgroundColor: 'var(--theme-elevation-0)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button onClick={handleDeleteLogs} disabled={deleteLoading} buttonStyle="error">
|
||||||
|
{deleteLoading ? '删除中...' : '删除日志'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{deleteMessage && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: '1rem',
|
||||||
|
padding: '1rem',
|
||||||
|
backgroundColor:
|
||||||
|
deleteMessage.includes('失败') || deleteMessage.includes('出错')
|
||||||
|
? 'var(--theme-error-50)'
|
||||||
|
: 'var(--theme-success-50)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
border: `1px solid ${
|
||||||
|
deleteMessage.includes('失败') || deleteMessage.includes('出错')
|
||||||
|
? 'var(--theme-error-500)'
|
||||||
|
: 'var(--theme-success-500)'
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{deleteMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 日志列表 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--theme-elevation-50)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '1.5rem',
|
||||||
|
border: '1px solid var(--theme-elevation-100)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
|
||||||
|
<h2 style={{ fontSize: '1.25rem', fontWeight: '600' }}>
|
||||||
|
📊 日志记录(共 {totalDocs} 条)
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '2rem' }}>加载中...</div>
|
||||||
|
) : logs.length === 0 ? (
|
||||||
|
<div style={{ textAlign: 'center', padding: '2rem', color: 'var(--theme-elevation-600)' }}>
|
||||||
|
没有找到日志记录
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div style={{ overflowX: 'auto' }}>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{ borderBottom: '2px solid var(--theme-elevation-200)' }}>
|
||||||
|
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>时间</th>
|
||||||
|
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>操作</th>
|
||||||
|
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>集合</th>
|
||||||
|
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>文档</th>
|
||||||
|
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>用户</th>
|
||||||
|
<th style={{ padding: '0.75rem', textAlign: 'left', fontWeight: '600' }}>IP</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{logs.map((log) => (
|
||||||
|
<tr
|
||||||
|
key={log.id}
|
||||||
|
style={{
|
||||||
|
borderBottom: '1px solid var(--theme-elevation-150)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||||||
|
{new Date(log.createdAt).toLocaleString('zh-CN')}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.75rem' }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '0.25rem 0.75rem',
|
||||||
|
borderRadius: '9999px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
backgroundColor: actionColors[log.action] + '20',
|
||||||
|
color: actionColors[log.action],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{actionLabels[log.action] || log.action}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>{log.collection}</td>
|
||||||
|
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||||||
|
{log.documentTitle || log.documentId || '-'}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||||||
|
{typeof log.user === 'object' ? log.user.email : log.user}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>{log.ip || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{totalDocs > limit && (
|
||||||
|
<div style={{ marginTop: '1rem', display: 'flex', justifyContent: 'center', gap: '0.5rem' }}>
|
||||||
|
<Button onClick={() => setPage(Math.max(1, page - 1))} disabled={page === 1}>
|
||||||
|
上一页
|
||||||
|
</Button>
|
||||||
|
<span style={{ padding: '0.5rem 1rem', alignItems: 'center', display: 'flex' }}>
|
||||||
|
第 {page} / {Math.ceil(totalDocs / limit)} 页
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
onClick={() => setPage(page + 1)}
|
||||||
|
disabled={page >= Math.ceil(totalDocs / limit)}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import type { GlobalConfig } from 'payload'
|
||||||
|
|
||||||
|
export const LogsManager: GlobalConfig = {
|
||||||
|
slug: 'logs-manager',
|
||||||
|
access: {
|
||||||
|
read: ({ req: { user } }) => {
|
||||||
|
// admin 和 editor 可以访问
|
||||||
|
if (!user) return false
|
||||||
|
return user.roles?.includes('admin') || user.roles?.includes('editor') || false
|
||||||
|
},
|
||||||
|
update: ({ req: { user } }) => {
|
||||||
|
// 只有 admin 可以删除日志
|
||||||
|
if (!user) return false
|
||||||
|
return user.roles?.includes('admin') || false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
group: '系统',
|
||||||
|
description: '日志查看和管理工具',
|
||||||
|
components: {
|
||||||
|
views: {
|
||||||
|
edit: {
|
||||||
|
default: {
|
||||||
|
Component: '/components/views/LogsManagerView',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'placeholder',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook } from 'payload'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录操作日志的通用函数
|
||||||
|
*/
|
||||||
|
export async function logAction({
|
||||||
|
req,
|
||||||
|
action,
|
||||||
|
collection,
|
||||||
|
documentId,
|
||||||
|
documentTitle,
|
||||||
|
changes,
|
||||||
|
}: {
|
||||||
|
req: any
|
||||||
|
action: 'create' | 'update' | 'delete' | 'sync' | 'login' | 'logout'
|
||||||
|
collection: string
|
||||||
|
documentId?: string
|
||||||
|
documentTitle?: string
|
||||||
|
changes?: any
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const { payload, user } = req
|
||||||
|
|
||||||
|
if (!user) return // 无用户信息则不记录
|
||||||
|
|
||||||
|
// 获取 IP 地址
|
||||||
|
const ip =
|
||||||
|
req.headers?.['x-forwarded-for'] ||
|
||||||
|
req.headers?.['x-real-ip'] ||
|
||||||
|
req.ip ||
|
||||||
|
req.connection?.remoteAddress ||
|
||||||
|
'unknown'
|
||||||
|
|
||||||
|
// 获取 User Agent
|
||||||
|
const userAgent = req.headers?.['user-agent'] || 'unknown'
|
||||||
|
|
||||||
|
// 创建日志记录
|
||||||
|
await payload.create({
|
||||||
|
collection: 'logs',
|
||||||
|
data: {
|
||||||
|
action,
|
||||||
|
collection,
|
||||||
|
documentId: documentId?.toString(),
|
||||||
|
documentTitle,
|
||||||
|
user: user.id,
|
||||||
|
changes,
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
},
|
||||||
|
// 不触发 hooks,避免递归
|
||||||
|
context: { skipHooks: true },
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// 静默失败,避免影响主要操作
|
||||||
|
console.error('[Log Hook Error]:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* afterChange 钩子:记录创建和更新操作
|
||||||
|
*/
|
||||||
|
export const logAfterChange: CollectionAfterChangeHook = async ({
|
||||||
|
doc,
|
||||||
|
req,
|
||||||
|
operation,
|
||||||
|
collection,
|
||||||
|
}) => {
|
||||||
|
// 跳过日志自身的操作,避免递归
|
||||||
|
if (req.context?.skipHooks) return doc
|
||||||
|
|
||||||
|
const collectionSlug = collection?.slug as string
|
||||||
|
|
||||||
|
// 不记录 logs 和 users-sessions 自身
|
||||||
|
if (collectionSlug === 'logs' || collectionSlug === 'users-sessions') return doc
|
||||||
|
|
||||||
|
await logAction({
|
||||||
|
req,
|
||||||
|
action: operation === 'create' ? 'create' : 'update',
|
||||||
|
collection: collectionSlug,
|
||||||
|
documentId: doc.id,
|
||||||
|
documentTitle: doc.title || doc.name || doc.email || doc.alt || `ID: ${doc.id}`,
|
||||||
|
changes: operation === 'update' ? { updatedFields: Object.keys(doc) } : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* afterDelete 钩子:记录删除操作
|
||||||
|
*/
|
||||||
|
export const logAfterDelete: CollectionAfterDeleteHook = async ({ doc, req, collection }) => {
|
||||||
|
if (req.context?.skipHooks) return doc
|
||||||
|
|
||||||
|
const collectionSlug = collection?.slug as string
|
||||||
|
|
||||||
|
if (collectionSlug === 'logs' || collectionSlug === 'users-sessions') return doc
|
||||||
|
|
||||||
|
await logAction({
|
||||||
|
req,
|
||||||
|
action: 'delete',
|
||||||
|
collection: collectionSlug,
|
||||||
|
documentId: doc.id,
|
||||||
|
documentTitle: doc.title || doc.name || doc.email || doc.alt || `ID: ${doc.id}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
@ -72,6 +72,7 @@ export interface Config {
|
||||||
products: Product;
|
products: Product;
|
||||||
announcements: Announcement;
|
announcements: Announcement;
|
||||||
articles: Article;
|
articles: Article;
|
||||||
|
logs: Log;
|
||||||
'payload-kv': PayloadKv;
|
'payload-kv': PayloadKv;
|
||||||
'payload-jobs': PayloadJob;
|
'payload-jobs': PayloadJob;
|
||||||
'payload-locked-documents': PayloadLockedDocument;
|
'payload-locked-documents': PayloadLockedDocument;
|
||||||
|
|
@ -85,6 +86,7 @@ export interface Config {
|
||||||
products: ProductsSelect<false> | ProductsSelect<true>;
|
products: ProductsSelect<false> | ProductsSelect<true>;
|
||||||
announcements: AnnouncementsSelect<false> | AnnouncementsSelect<true>;
|
announcements: AnnouncementsSelect<false> | AnnouncementsSelect<true>;
|
||||||
articles: ArticlesSelect<false> | ArticlesSelect<true>;
|
articles: ArticlesSelect<false> | ArticlesSelect<true>;
|
||||||
|
logs: LogsSelect<false> | LogsSelect<true>;
|
||||||
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||||
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
|
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
|
||||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||||
|
|
@ -97,9 +99,11 @@ export interface Config {
|
||||||
fallbackLocale: null;
|
fallbackLocale: null;
|
||||||
globals: {
|
globals: {
|
||||||
'admin-settings': AdminSetting;
|
'admin-settings': AdminSetting;
|
||||||
|
'logs-manager': LogsManager;
|
||||||
};
|
};
|
||||||
globalsSelect: {
|
globalsSelect: {
|
||||||
'admin-settings': AdminSettingsSelect<false> | AdminSettingsSelect<true>;
|
'admin-settings': AdminSettingsSelect<false> | AdminSettingsSelect<true>;
|
||||||
|
'logs-manager': LogsManagerSelect<false> | LogsManagerSelect<true>;
|
||||||
};
|
};
|
||||||
locale: null;
|
locale: null;
|
||||||
user: User;
|
user: User;
|
||||||
|
|
@ -386,6 +390,57 @@ export interface Article {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
_status?: ('draft' | 'published') | null;
|
_status?: ('draft' | 'published') | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 系统操作日志记录
|
||||||
|
*
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "logs".
|
||||||
|
*/
|
||||||
|
export interface Log {
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* 操作类型
|
||||||
|
*/
|
||||||
|
action: 'create' | 'update' | 'delete' | 'sync' | 'login' | 'logout';
|
||||||
|
/**
|
||||||
|
* 操作的集合名称
|
||||||
|
*/
|
||||||
|
collection: string;
|
||||||
|
/**
|
||||||
|
* 文档 ID
|
||||||
|
*/
|
||||||
|
documentId?: string | null;
|
||||||
|
/**
|
||||||
|
* 文档标题
|
||||||
|
*/
|
||||||
|
documentTitle?: string | null;
|
||||||
|
/**
|
||||||
|
* 操作用户
|
||||||
|
*/
|
||||||
|
user: number | User;
|
||||||
|
/**
|
||||||
|
* 变更内容(JSON 格式)
|
||||||
|
*/
|
||||||
|
changes?:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
/**
|
||||||
|
* IP 地址
|
||||||
|
*/
|
||||||
|
ip?: string | null;
|
||||||
|
/**
|
||||||
|
* 浏览器信息
|
||||||
|
*/
|
||||||
|
userAgent?: string | null;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-kv".
|
* via the `definition` "payload-kv".
|
||||||
|
|
@ -521,6 +576,10 @@ export interface PayloadLockedDocument {
|
||||||
| ({
|
| ({
|
||||||
relationTo: 'articles';
|
relationTo: 'articles';
|
||||||
value: number | Article;
|
value: number | Article;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'logs';
|
||||||
|
value: number | Log;
|
||||||
} | null);
|
} | null);
|
||||||
globalSlug?: string | null;
|
globalSlug?: string | null;
|
||||||
user: {
|
user: {
|
||||||
|
|
@ -664,6 +723,22 @@ export interface ArticlesSelect<T extends boolean = true> {
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
_status?: T;
|
_status?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "logs_select".
|
||||||
|
*/
|
||||||
|
export interface LogsSelect<T extends boolean = true> {
|
||||||
|
action?: T;
|
||||||
|
collection?: T;
|
||||||
|
documentId?: T;
|
||||||
|
documentTitle?: T;
|
||||||
|
user?: T;
|
||||||
|
changes?: T;
|
||||||
|
ip?: T;
|
||||||
|
userAgent?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-kv_select".
|
* via the `definition` "payload-kv_select".
|
||||||
|
|
@ -747,6 +822,18 @@ export interface AdminSetting {
|
||||||
updatedAt?: string | null;
|
updatedAt?: string | null;
|
||||||
createdAt?: string | null;
|
createdAt?: string | null;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 日志查看和管理工具
|
||||||
|
*
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "logs-manager".
|
||||||
|
*/
|
||||||
|
export interface LogsManager {
|
||||||
|
id: number;
|
||||||
|
placeholder?: string | null;
|
||||||
|
updatedAt?: string | null;
|
||||||
|
createdAt?: string | null;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "admin-settings_select".
|
* via the `definition` "admin-settings_select".
|
||||||
|
|
@ -757,6 +844,16 @@ export interface AdminSettingsSelect<T extends boolean = true> {
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
globalType?: T;
|
globalType?: T;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "logs-manager_select".
|
||||||
|
*/
|
||||||
|
export interface LogsManagerSelect<T extends boolean = true> {
|
||||||
|
placeholder?: T;
|
||||||
|
updatedAt?: T;
|
||||||
|
createdAt?: T;
|
||||||
|
globalType?: T;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "TaskSchedulePublish".
|
* via the `definition` "TaskSchedulePublish".
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ import { Media } from './collections/Media'
|
||||||
import { Products } from './collections/Products'
|
import { Products } from './collections/Products'
|
||||||
import { Announcements } from './collections/Announcements'
|
import { Announcements } from './collections/Announcements'
|
||||||
import { Articles } from './collections/Articles'
|
import { Articles } from './collections/Articles'
|
||||||
|
import { Logs } from './collections/Logs'
|
||||||
import { AdminSettings } from './globals/AdminSettings'
|
import { AdminSettings } from './globals/AdminSettings'
|
||||||
|
import { LogsManager } from './globals/LogsManager'
|
||||||
import { s3Storage } from '@payloadcms/storage-s3'
|
import { s3Storage } from '@payloadcms/storage-s3'
|
||||||
import { en } from '@payloadcms/translations/languages/en'
|
import { en } from '@payloadcms/translations/languages/en'
|
||||||
import { zh } from '@payloadcms/translations/languages/zh'
|
import { zh } from '@payloadcms/translations/languages/zh'
|
||||||
|
|
@ -42,8 +44,8 @@ export default buildConfig({
|
||||||
},
|
},
|
||||||
fallbackLanguage: 'zh',
|
fallbackLanguage: 'zh',
|
||||||
},
|
},
|
||||||
collections: [Users, Media, Products, Announcements, Articles],
|
collections: [Users, Media, Products, Announcements, Articles, Logs],
|
||||||
globals: [AdminSettings],
|
globals: [AdminSettings, LogsManager],
|
||||||
editor: lexicalEditor(),
|
editor: lexicalEditor(),
|
||||||
secret: process.env.PAYLOAD_SECRET || '',
|
secret: process.env.PAYLOAD_SECRET || '',
|
||||||
typescript: {
|
typescript: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue