日志记录

This commit is contained in:
龟男日记\www 2026-02-12 02:50:18 +08:00
parent 07d1c2274b
commit b26fe1e117
12 changed files with 960 additions and 2 deletions

View File

@ -29,6 +29,7 @@ import { SyncMedusaButton as SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08 }
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
import { ForceSyncButton as ForceSyncButton_28396efe36d6238add95cf44109e281c } from '../../../components/sync/ForceSyncButton'
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 { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
@ -64,6 +65,7 @@ export const importMap = {
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
"/components/sync/ForceSyncButton#ForceSyncButton": ForceSyncButton_28396efe36d6238add95cf44109e281c,
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
}

View File

@ -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 },
)
}
}

View File

@ -1,4 +1,5 @@
import type { CollectionConfig } from 'payload'
import { logAfterChange, logAfterDelete } from '../hooks/logAction'
import {
BlocksFeature,
BoldFeature,
@ -220,5 +221,7 @@ export const Announcements: CollectionConfig = {
return data
},
],
afterChange: [logAfterChange],
afterDelete: [logAfterDelete],
},
}

View File

@ -1,4 +1,5 @@
import type { CollectionConfig } from 'payload'
import { logAfterChange, logAfterDelete } from '../hooks/logAction'
import {
BlocksFeature,
BoldFeature,
@ -330,5 +331,7 @@ export const Articles: CollectionConfig = {
return data
},
],
afterChange: [logAfterChange],
afterDelete: [logAfterDelete],
},
}

104
src/collections/Logs.ts Normal file
View File

@ -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,
}

View File

@ -1,4 +1,5 @@
import type { CollectionConfig } from 'payload'
import { logAfterChange, logAfterDelete } from '../hooks/logAction'
export const Media: CollectionConfig = {
slug: 'media',
@ -12,5 +13,9 @@ export const Media: CollectionConfig = {
required: true,
},
],
hooks: {
afterChange: [logAfterChange],
afterDelete: [logAfterDelete],
},
upload: true,
}

View File

@ -1,4 +1,5 @@
import type { CollectionConfig } from 'payload'
import { logAfterChange, logAfterDelete } from '../hooks/logAction'
import {
AlignFeature,
BlocksFeature,
@ -200,5 +201,9 @@ export const Products: CollectionConfig = {
],
},
],
hooks: {
afterChange: [logAfterChange],
afterDelete: [logAfterDelete],
},
timestamps: true,
}

View File

@ -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>
)
}

View File

@ -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,
},
},
],
}

107
src/hooks/logAction.ts Normal file
View File

@ -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
}

View File

@ -72,6 +72,7 @@ export interface Config {
products: Product;
announcements: Announcement;
articles: Article;
logs: Log;
'payload-kv': PayloadKv;
'payload-jobs': PayloadJob;
'payload-locked-documents': PayloadLockedDocument;
@ -85,6 +86,7 @@ export interface Config {
products: ProductsSelect<false> | ProductsSelect<true>;
announcements: AnnouncementsSelect<false> | AnnouncementsSelect<true>;
articles: ArticlesSelect<false> | ArticlesSelect<true>;
logs: LogsSelect<false> | LogsSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
@ -97,9 +99,11 @@ export interface Config {
fallbackLocale: null;
globals: {
'admin-settings': AdminSetting;
'logs-manager': LogsManager;
};
globalsSelect: {
'admin-settings': AdminSettingsSelect<false> | AdminSettingsSelect<true>;
'logs-manager': LogsManagerSelect<false> | LogsManagerSelect<true>;
};
locale: null;
user: User;
@ -386,6 +390,57 @@ export interface Article {
createdAt: string;
_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
* via the `definition` "payload-kv".
@ -521,6 +576,10 @@ export interface PayloadLockedDocument {
| ({
relationTo: 'articles';
value: number | Article;
} | null)
| ({
relationTo: 'logs';
value: number | Log;
} | null);
globalSlug?: string | null;
user: {
@ -664,6 +723,22 @@ export interface ArticlesSelect<T extends boolean = true> {
createdAt?: 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
* via the `definition` "payload-kv_select".
@ -747,6 +822,18 @@ export interface AdminSetting {
updatedAt?: 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
* via the `definition` "admin-settings_select".
@ -757,6 +844,16 @@ export interface AdminSettingsSelect<T extends boolean = true> {
createdAt?: 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
* via the `definition` "TaskSchedulePublish".

View File

@ -10,7 +10,9 @@ import { Media } from './collections/Media'
import { Products } from './collections/Products'
import { Announcements } from './collections/Announcements'
import { Articles } from './collections/Articles'
import { Logs } from './collections/Logs'
import { AdminSettings } from './globals/AdminSettings'
import { LogsManager } from './globals/LogsManager'
import { s3Storage } from '@payloadcms/storage-s3'
import { en } from '@payloadcms/translations/languages/en'
import { zh } from '@payloadcms/translations/languages/zh'
@ -42,8 +44,8 @@ export default buildConfig({
},
fallbackLanguage: 'zh',
},
collections: [Users, Media, Products, Announcements, Articles],
globals: [AdminSettings],
collections: [Users, Media, Products, Announcements, Articles, Logs],
globals: [AdminSettings, LogsManager],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '',
typescript: {