暂时完结

This commit is contained in:
龟男日记\www 2026-02-12 02:30:28 +08:00
parent bdee359c4c
commit 249423d73d
20 changed files with 2212 additions and 177 deletions

View File

@ -28,6 +28,7 @@ import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932
import { SyncMedusaButton as SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08 } from '../../../components/sync/SyncMedusaButton' import { SyncMedusaButton as SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08 } from '../../../components/sync/SyncMedusaButton'
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 { 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'
@ -62,6 +63,7 @@ export const importMap = {
"/components/sync/SyncMedusaButton#SyncMedusaButton": SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08, "/components/sync/SyncMedusaButton#SyncMedusaButton": SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08,
"/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,
"@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
} }

View File

@ -0,0 +1,151 @@
import { getPayload } from 'payload'
import config from '@payload-config'
import { NextResponse } from 'next/server'
/**
* Users Media
* GET /api/clear-data?confirm=true
*/
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url)
const confirm = searchParams.get('confirm')
// 安全检查:必须明确确认
if (confirm !== 'true') {
return NextResponse.json(
{
success: false,
error: '需要确认参数:?confirm=true',
message:
'此操作将删除 Products, Announcements, Articles 的所有数据(保留 Users 和 Media',
},
{ status: 400 },
)
}
const payload = await getPayload({ config })
const results = {
products: 0,
announcements: 0,
articles: 0,
errors: [] as string[],
}
// 清理 Products
try {
const deletedProducts = await payload.delete({
collection: 'products',
where: {},
})
results.products = deletedProducts.docs?.length || 0
console.log(`✅ 已清理 ${results.products} 个商品`)
} catch (error) {
const errorMsg = `清理 Products 失败: ${error instanceof Error ? error.message : '未知错误'}`
console.error('❌', errorMsg)
results.errors.push(errorMsg)
}
// 清理 Announcements
try {
const deletedAnnouncements = await payload.delete({
collection: 'announcements',
where: {},
})
results.announcements = deletedAnnouncements.docs?.length || 0
console.log(`✅ 已清理 ${results.announcements} 个公告`)
} catch (error) {
const errorMsg = `清理 Announcements 失败: ${error instanceof Error ? error.message : '未知错误'}`
console.error('❌', errorMsg)
results.errors.push(errorMsg)
}
// 清理 Articles
try {
const deletedArticles = await payload.delete({
collection: 'articles',
where: {},
})
results.articles = deletedArticles.docs?.length || 0
console.log(`✅ 已清理 ${results.articles} 个文章`)
} catch (error) {
const errorMsg = `清理 Articles 失败: ${error instanceof Error ? error.message : '未知错误'}`
console.error('❌', errorMsg)
results.errors.push(errorMsg)
}
return NextResponse.json({
success: true,
message: `数据清理完成!已删除 ${results.products} 个商品、${results.announcements} 个公告、${results.articles} 个文章`,
results,
})
} catch (error) {
console.error('Clear data error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 },
)
}
}
/**
* POST /api/clear-data
*
*/
export async function POST(request: Request) {
try {
const payload = await getPayload({ config })
// 可选:添加认证检查
// const { user } = await payload.auth({ headers: request.headers })
// if (!user?.roles?.includes('admin')) {
// return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
// }
const body = await request.json()
const { collections = ['products', 'announcements', 'articles'] } = body
const results: Record<string, number> = {}
const errors: string[] = []
for (const collection of collections) {
if (['users', 'media'].includes(collection)) {
errors.push(`跳过受保护的集合: ${collection}`)
continue
}
try {
const deleted = await payload.delete({
collection,
where: {},
})
results[collection] = deleted.docs?.length || 0
console.log(`✅ 已清理 ${results[collection]}${collection}`)
} catch (error) {
const errorMsg = `清理 ${collection} 失败: ${error instanceof Error ? error.message : '未知错误'}`
console.error('❌', errorMsg)
errors.push(errorMsg)
}
}
return NextResponse.json({
success: true,
message: '数据清理完成',
results,
errors: errors.length > 0 ? errors : undefined,
})
} catch (error) {
console.error('Clear data error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 },
)
}
}

View File

@ -0,0 +1,46 @@
import { getPayload } from 'payload'
import config from '@payload-config'
import { NextResponse } from 'next/server'
/**
*
* GET /api/fix-database
*/
export async function GET(request: Request) {
try {
const payload = await getPayload({ config })
const db = payload.db
console.log('🔧 开始修复数据库字段类型...')
// 修复 products 表的 thumbnail 字段
await db.execute({
raw: `
ALTER TABLE "products"
ALTER COLUMN "thumbnail" TYPE varchar
USING CASE
WHEN "thumbnail" IS NULL THEN NULL
ELSE "thumbnail"::varchar
END;
`,
})
console.log('✅ Products.thumbnail 字段已修复为 varchar 类型')
return NextResponse.json({
success: true,
message: '数据库字段类型修复成功!',
fixes: ['products.thumbnail: integer → varchar'],
})
} catch (error) {
console.error('❌ 修复数据库出错:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
message: '数据库修复失败,请查看控制台日志',
},
{ status: 500 },
)
}
}

View File

@ -119,7 +119,12 @@ async function syncSingleProductByMedusaId(payload: any, medusaId: string, force
} }
} catch (error) { } catch (error) {
console.error(`Error syncing product ${medusaId}:`, error) console.error(`Error syncing product ${medusaId}:`, error)
throw error return {
success: false,
action: 'error',
message: `同步商品 ${medusaId} 失败`,
error: error instanceof Error ? error.message : 'Unknown error',
}
} }
} }
@ -155,7 +160,12 @@ async function syncSingleProductByPayloadId(payload: any, payloadId: string, for
return await syncSingleProductByMedusaId(payload, product.medusaId, forceUpdate) return await syncSingleProductByMedusaId(payload, product.medusaId, forceUpdate)
} catch (error) { } catch (error) {
console.error(`Error syncing product by Payload ID ${payloadId}:`, error) console.error(`Error syncing product by Payload ID ${payloadId}:`, error)
throw error return {
success: false,
action: 'error',
message: `同步商品 ID ${payloadId} 失败`,
error: error instanceof Error ? error.message : 'Unknown error',
}
} }
} }

View File

@ -0,0 +1,77 @@
import { getPayload } from 'payload'
import config from '@payload-config'
import { NextRequest } from 'next/server'
/**
* API
* POST /api/upload-media
*/
export async function POST(req: NextRequest) {
try {
const payload = await getPayload({ config })
// 检查用户认证
const { user } = await payload.auth({ headers: req.headers })
if (!user) {
return Response.json({ error: '未授权,请先登录' }, { status: 401 })
}
// 获取 FormData
const formData = await req.formData()
const file = formData.get('file') as File
const alt = formData.get('alt') as string
if (!file) {
return Response.json({ error: '请选择要上传的文件' }, { status: 400 })
}
// 验证文件类型
if (!file.type.startsWith('image/')) {
return Response.json({ error: '只能上传图片文件' }, { status: 400 })
}
// 验证文件大小 (最大 10MB)
const maxSize = 10 * 1024 * 1024
if (file.size > maxSize) {
return Response.json({ error: '文件大小不能超过 10MB' }, { status: 400 })
}
// 将 File 转换为 Buffer
const arrayBuffer = await file.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
// 上传到 Media collection
const media = await payload.create({
collection: 'media',
data: {
alt: alt || file.name,
},
file: {
data: buffer,
name: file.name,
mimetype: file.type,
size: file.size,
},
user,
})
return Response.json({
success: true,
doc: {
id: media.id,
url: media.url,
filename: media.filename,
alt: media.alt,
},
})
} catch (error) {
console.error('Media upload error:', error)
return Response.json(
{
error: '上传失败',
message: error instanceof Error ? error.message : '未知错误',
},
{ status: 500 },
)
}
}

View File

@ -0,0 +1,224 @@
import type { CollectionConfig } from 'payload'
import {
BlocksFeature,
BoldFeature,
HeadingFeature,
InlineCodeFeature,
ItalicFeature,
lexicalEditor,
LinkFeature,
OrderedListFeature,
ParagraphFeature,
UnorderedListFeature,
FixedToolbarFeature,
InlineToolbarFeature,
HorizontalRuleFeature,
BlockquoteFeature,
AlignFeature,
} from '@payloadcms/richtext-lexical'
export const Announcements: CollectionConfig = {
slug: 'announcements',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'type', 'status', 'publishedAt', 'updatedAt'],
description: '管理系统公告和通知',
pagination: {
defaultLimit: 25,
},
},
access: {
read: ({ req: { user } }) => {
// 公开访问已发布的公告
if (!user) {
return {
status: { equals: 'published' },
}
}
// 认证用户可以查看所有
return true
},
create: ({ req: { user } }) => {
// 所有已认证用户都可以创建
return Boolean(user)
},
update: ({ req: { user } }) => {
// 所有已认证用户都可以更新
return Boolean(user)
},
delete: ({ req: { user } }) => {
// 只有 admin 可以删除
if (!user) return false
return user.roles?.includes('admin') || false
},
},
fields: [
{
type: 'row',
fields: [
{
name: 'title',
type: 'text',
required: true,
admin: {
description: '公告标题',
width: '70%',
},
},
{
name: 'type',
type: 'select',
required: true,
defaultValue: 'info',
options: [
{
label: '信息',
value: 'info',
},
{
label: '警告',
value: 'warning',
},
{
label: '重要',
value: 'important',
},
{
label: '紧急',
value: 'urgent',
},
],
admin: {
description: '公告类型',
width: '30%',
},
},
],
},
{
type: 'row',
fields: [
{
name: 'status',
type: 'select',
required: true,
defaultValue: 'draft',
options: [
{
label: '草稿',
value: 'draft',
},
{
label: '已发布',
value: 'published',
},
{
label: '已归档',
value: 'archived',
},
],
admin: {
description: '发布状态',
width: '50%',
},
},
{
name: 'priority',
type: 'number',
defaultValue: 0,
admin: {
description: '优先级(数字越大越靠前)',
width: '50%',
},
},
],
},
{
name: 'summary',
type: 'textarea',
admin: {
description: '公告摘要(显示在列表页)',
},
},
{
name: 'content',
type: 'richText',
required: true,
admin: {
description: '公告详细内容',
},
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
BoldFeature(),
ItalicFeature(),
LinkFeature({}),
OrderedListFeature(),
UnorderedListFeature(),
BlockquoteFeature(),
AlignFeature(),
InlineCodeFeature(),
FixedToolbarFeature(),
InlineToolbarFeature(),
HorizontalRuleFeature(),
],
}),
},
{
type: 'row',
fields: [
{
name: 'publishedAt',
type: 'date',
admin: {
description: '发布时间',
width: '50%',
date: {
displayFormat: 'yyyy-MM-dd HH:mm',
},
},
},
{
name: 'expiresAt',
type: 'date',
admin: {
description: '过期时间(可选)',
width: '50%',
date: {
displayFormat: 'yyyy-MM-dd HH:mm',
},
},
},
],
},
{
name: 'showOnHomepage',
type: 'checkbox',
defaultValue: false,
admin: {
description: '在首页显示此公告',
},
},
{
name: 'author',
type: 'relationship',
relationTo: 'users',
admin: {
description: '发布者',
},
},
],
timestamps: true,
hooks: {
beforeChange: [
({ data, operation }) => {
// 自动设置发布时间
if (operation === 'create' && data.status === 'published' && !data.publishedAt) {
data.publishedAt = new Date()
}
return data
},
],
},
}

334
src/collections/Articles.ts Normal file
View File

@ -0,0 +1,334 @@
import type { CollectionConfig } from 'payload'
import {
BlocksFeature,
BoldFeature,
HeadingFeature,
InlineCodeFeature,
ItalicFeature,
lexicalEditor,
LinkFeature,
OrderedListFeature,
ParagraphFeature,
UnorderedListFeature,
FixedToolbarFeature,
InlineToolbarFeature,
HorizontalRuleFeature,
BlockquoteFeature,
AlignFeature,
ChecklistFeature,
IndentFeature,
UploadFeature,
} from '@payloadcms/richtext-lexical'
export const Articles: CollectionConfig = {
slug: 'articles',
admin: {
useAsTitle: 'title',
defaultColumns: ['featuredImage', 'title', 'category', 'status', 'publishedAt', 'updatedAt'],
description: '管理文章内容',
listSearchableFields: ['title', 'slug', 'excerpt'],
pagination: {
defaultLimit: 25,
},
},
access: {
read: ({ req: { user } }) => {
// 公开访问已发布的文章
if (!user) {
return {
status: { equals: 'published' },
}
}
// 认证用户可以查看所有
return true
},
create: ({ req: { user } }) => {
// 所有已认证用户都可以创建
return Boolean(user)
},
update: ({ req: { user } }) => {
// 所有已认证用户都可以更新
return Boolean(user)
},
delete: ({ req: { user } }) => {
// 只有 admin 可以删除
if (!user) return false
return user.roles?.includes('admin') || false
},
},
versions: {
drafts: {
autosave: true,
schedulePublish: true,
},
maxPerDoc: 50,
},
fields: [
{
type: 'row',
fields: [
{
name: 'title',
type: 'text',
required: true,
admin: {
description: '文章标题',
width: '70%',
},
},
{
name: 'status',
type: 'select',
required: true,
defaultValue: 'draft',
options: [
{
label: '草稿',
value: 'draft',
},
{
label: '已发布',
value: 'published',
},
],
admin: {
description: '发布状态',
width: '30%',
},
},
],
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
index: true,
admin: {
description: '文章 URL 路径(用于 SEO 友好的 URL',
},
},
{
name: 'featuredImage',
type: 'upload',
relationTo: 'media',
admin: {
description: '文章特色图片(封面)',
},
},
{
name: 'excerpt',
type: 'textarea',
admin: {
description: '文章摘要(显示在列表页和 SEO',
},
},
{
name: 'content',
type: 'richText',
required: true,
admin: {
description: '文章详细内容',
},
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
BoldFeature(),
ItalicFeature(),
LinkFeature({
enabledCollections: ['articles', 'products'],
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document.',
},
},
],
}),
OrderedListFeature(),
UnorderedListFeature(),
ChecklistFeature(),
BlockquoteFeature(),
AlignFeature(),
IndentFeature(),
InlineCodeFeature(),
UploadFeature({
collections: {
media: {
fields: [
{
name: 'caption',
type: 'richText',
label: '图片说明',
editor: lexicalEditor(),
},
],
},
},
}),
FixedToolbarFeature(),
InlineToolbarFeature(),
HorizontalRuleFeature(),
],
}),
},
{
type: 'row',
fields: [
{
name: 'category',
type: 'select',
required: true,
options: [
{
label: '新闻',
value: 'news',
},
{
label: '教程',
value: 'tutorial',
},
{
label: '技术',
value: 'tech',
},
{
label: '产品评测',
value: 'review',
},
{
label: '行业动态',
value: 'industry',
},
{
label: '其他',
value: 'other',
},
],
admin: {
description: '文章分类',
width: '50%',
},
},
{
name: 'featured',
type: 'checkbox',
defaultValue: false,
admin: {
description: '标记为推荐文章',
width: '50%',
},
},
],
},
{
name: 'tags',
type: 'text',
hasMany: true,
admin: {
description: '文章标签(用于搜索和分类)',
},
},
{
type: 'row',
fields: [
{
name: 'author',
type: 'relationship',
relationTo: 'users',
required: true,
admin: {
description: '文章作者',
width: '50%',
},
},
{
name: 'publishedAt',
type: 'date',
admin: {
description: '发布时间',
width: '50%',
date: {
displayFormat: 'yyyy-MM-dd HH:mm',
},
},
},
],
},
{
name: 'relatedArticles',
type: 'relationship',
relationTo: 'articles',
hasMany: true,
admin: {
description: '相关文章',
},
},
{
name: 'relatedProducts',
type: 'relationship',
relationTo: 'products',
hasMany: true,
admin: {
description: '相关商品',
},
},
{
type: 'collapsible',
label: 'SEO 设置',
fields: [
{
name: 'metaTitle',
type: 'text',
admin: {
description: 'SEO 标题(留空则使用文章标题)',
},
},
{
name: 'metaDescription',
type: 'textarea',
admin: {
description: 'SEO 描述(留空则使用摘要)',
},
},
{
name: 'metaKeywords',
type: 'text',
hasMany: true,
admin: {
description: 'SEO 关键词',
},
},
],
},
],
timestamps: true,
hooks: {
beforeChange: [
({ data, operation }) => {
// 自动生成 slug如果未提供
if (operation === 'create' && !data.slug && data.title) {
data.slug = data.title
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
.replace(/(^-|-$)/g, '')
}
// 自动设置发布时间
if (operation === 'create' && data.status === 'published' && !data.publishedAt) {
data.publishedAt = new Date()
}
return data
},
],
},
}

View File

@ -46,144 +46,158 @@ export const Products: CollectionConfig = {
}, },
fields: [ fields: [
{ {
type: 'row', type: 'tabs',
fields: [ tabs: [
{ {
name: 'medusaId', label: '基本信息',
type: 'text', fields: [
required: true,
unique: true,
index: true,
admin: {
description: 'Medusa 商品 ID',
readOnly: true,
width: '80%',
},
},
{
name: 'status',
type: 'select',
required: true,
defaultValue: 'draft',
options: [
{ {
label: '草稿', type: 'row',
value: 'draft', fields: [
{
name: 'medusaId',
type: 'text',
required: true,
unique: true,
index: true,
admin: {
description: 'Medusa 商品 ID',
readOnly: true,
width: '60%',
},
},
{
name: 'status',
type: 'select',
required: true,
defaultValue: 'draft',
options: [
{
label: '草稿',
value: 'draft',
},
{
label: '已发布',
value: 'published',
},
],
admin: {
description: '商品详情状态',
width: '40%',
},
},
],
}, },
{ {
label: '已发布', name: 'title',
value: 'published', type: 'text',
required: true,
admin: {
description: '商品标题(从 Medusa 同步)',
readOnly: true,
},
}, },
], {
admin: { name: 'handle',
description: '商品详情状态', type: 'text',
width: '20%', admin: {
}, hidden: true, // 隐藏字段,但保留用于搜索
}, },
],
},
{
type: 'row',
fields: [
{
name: 'title',
type: 'text',
required: true,
admin: {
description: '商品标题(从 Medusa 同步)',
readOnly: true,
width: '80%',
},
},
{
name: 'thumbnail',
type: 'text',
admin: {
description: '商品封面 URL从 Medusa 同步)',
readOnly: true,
width: '20%',
components: {
Cell: '/components/cells/ThumbnailCell#ThumbnailCell',
Field: '/components/fields/ThumbnailField#ThumbnailField',
}, },
}, {
}, name: 'thumbnail',
], type: 'text',
}, admin: {
{ description: '商品封面 URL支持上传或输入 URL',
name: 'content', components: {
type: 'richText', Cell: '/components/cells/ThumbnailCell#ThumbnailCell',
admin: { Field: '/components/fields/ThumbnailField#ThumbnailField',
description: '商品详细内容',
},
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
LinkFeature({
enabledCollections: ['products'],
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
}, },
}, },
], },
}), {
UploadFeature({ name: 'lastSyncedAt',
collections: { type: 'date',
media: { admin: {
fields: [ description: '最后同步时间',
{ readOnly: true,
name: 'caption', date: {
type: 'richText', displayFormat: 'yyyy-MM-dd HH:mm:ss',
label: '图片说明', },
editor: lexicalEditor(),
},
],
}, },
}, },
}), ],
FixedToolbarFeature(),
InlineToolbarFeature(),
HorizontalRuleFeature(),
],
}),
},
{
name: 'handle',
type: 'text',
admin: {
description: '商品 URL slug从 Medusa 同步)',
readOnly: true,
},
},
{
name: 'relatedProducts',
type: 'relationship',
relationTo: 'products',
hasMany: true,
admin: {
description: '相关商品,支持搜索联想',
components: {
Field: '/components/fields/RelatedProductsField#RelatedProductsField',
}, },
}, {
}, label: '商品详情',
{ fields: [
name: 'lastSyncedAt', {
type: 'date', name: 'content',
admin: { type: 'richText',
description: '最后同步时间', admin: {
readOnly: true, description: '商品详细内容(支持图文混排)',
}, },
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
LinkFeature({
enabledCollections: ['products'],
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'rel',
label: 'Rel Attribute',
type: 'select',
hasMany: true,
options: ['noopener', 'noreferrer', 'nofollow'],
admin: {
description:
'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.',
},
},
],
}),
UploadFeature({
collections: {
media: {
fields: [
{
name: 'caption',
type: 'richText',
label: '图片说明',
editor: lexicalEditor(),
},
],
},
},
}),
FixedToolbarFeature(),
InlineToolbarFeature(),
HorizontalRuleFeature(),
],
}),
},
],
},
{
label: '关联信息',
fields: [
{
name: 'relatedProducts',
type: 'relationship',
relationTo: 'products',
hasMany: true,
admin: {
description: '相关商品,支持搜索联想',
components: {
Field: '/components/fields/RelatedProductsField#RelatedProductsField',
},
},
},
],
},
],
}, },
], ],
timestamps: true, timestamps: true,

View File

@ -8,6 +8,104 @@ export const Users: CollectionConfig = {
auth: true, auth: true,
fields: [ fields: [
// Email added by default // Email added by default
// Add more fields as needed {
name: 'roles',
type: 'select',
hasMany: true,
options: ['admin', 'editor', 'user'],
defaultValue: ['user'],
required: true,
saveToJWT: true, // Include in JWT for fast access checks
access: {
update: ({ req: { user } }) => {
// Only admins can update roles
if (!user) return false
return user.roles?.includes('admin') || false
},
},
},
], ],
hooks: {
beforeChange: [
async ({ data, req, operation }) => {
// 如果是创建操作,检查是否为第一个用户
if (operation === 'create') {
const { totalDocs } = await req.payload.count({
collection: 'users',
})
// 如果这是第一个用户,自动设置为 admin
if (totalDocs === 0) {
data.roles = ['admin']
console.log('🎉 第一个用户注册,自动设置为管理员')
}
}
// 如果是更新操作,检查是否为唯一用户且 roles 为空
if (operation === 'update') {
const { totalDocs } = await req.payload.count({
collection: 'users',
})
// 如果只有一个用户且 roles 为空或只有 user自动升级为 admin
if (
totalDocs === 1 &&
(!data.roles ||
data.roles.length === 0 ||
(data.roles.length === 1 && data.roles[0] === 'user'))
) {
data.roles = ['admin']
console.log('🔧 当前是唯一用户,自动升级为管理员')
}
}
return data
},
],
afterRead: [
async ({ doc, req, context }) => {
// 跳过已标记为处理过的请求
if (context?.skipAutoAdmin) return doc
// 检查是否为唯一用户且 roles 为空或不正确
if (
!doc.roles ||
doc.roles.length === 0 ||
(doc.roles.length === 1 && doc.roles[0] === 'user')
) {
const { totalDocs } = await req.payload.count({
collection: 'users',
})
// 如果只有一个用户,自动更新为 admin
if (totalDocs === 1) {
console.log('🔄 检测到唯一用户权限异常,正在修复...')
try {
// 使用 overrideAccess 绕过权限检查,标记 context 避免循环
await req.payload.update({
collection: 'users',
id: doc.id,
data: {
roles: ['admin'],
},
context: {
skipAutoAdmin: true,
},
overrideAccess: true,
})
// 更新当前文档的 roles
doc.roles = ['admin']
console.log('✅ 唯一用户权限已修复为管理员')
} catch (error) {
console.error('❌ 更新用户权限失败:', error)
}
}
}
return doc
},
],
},
} }

View File

@ -1,45 +1,225 @@
'use client' 'use client'
import { useFormFields } from '@payloadcms/ui' import React, { useState, useCallback } from 'react'
import { useField, Button } from '@payloadcms/ui'
import type { TextFieldClientComponent } from 'payload' import type { TextFieldClientComponent } from 'payload'
// 只显示缩略图封面 /**
export const ThumbnailField: TextFieldClientComponent = ({ path }) => { * Thumbnail
const fields = useFormFields(([fields]) => fields) * -
const thumbnail = fields.thumbnail?.value * - Media collection
* - URL
*/
export const ThumbnailField: TextFieldClientComponent = (props) => {
const { path, field } = props
const { value, setValue } = useField<string>({ path })
const [uploading, setUploading] = useState(false)
const [uploadError, setUploadError] = useState('')
const isImage = typeof thumbnail === 'string' && thumbnail.match(/^https?:\/\/.+/) const label = typeof field.label === 'string' ? field.label : '商品封面'
const required = field.required || false
// 处理文件上传
const handleFileUpload = useCallback(
async (file: File) => {
if (!file) return
setUploading(true)
setUploadError('')
try {
// 创建 FormData
const formData = new FormData()
formData.append('file', file)
formData.append('alt', file.name)
// 上传到自定义 API
const response = await fetch('/api/upload-media', {
method: 'POST',
body: formData,
credentials: 'include',
})
if (!response.ok) {
const errorText = await response.text()
let errorMsg = `上传失败 (${response.status})`
try {
const errorData = JSON.parse(errorText)
errorMsg = errorData.message || errorData.error || errorMsg
} catch {
errorMsg = errorText || errorMsg
}
throw new Error(errorMsg)
}
const data = await response.json()
// 获取上传后的图片 URL
if (data.doc?.url) {
setValue(data.doc.url)
setUploadError('') // 清除之前的错误
} else {
throw new Error('服务器返回数据中没有图片 URL')
}
} catch (error) {
console.error('Upload error:', error)
setUploadError(error instanceof Error ? error.message : '上传失败,请重试')
} finally {
setUploading(false)
}
},
[setValue],
)
// 处理 URL 输入
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
}
// 清除图片
const handleClear = () => {
setValue('')
setUploadError('')
}
return ( return (
<div> <div style={{ marginBottom: '1rem' }}>
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}></label> <div style={{ marginBottom: '0.5rem' }}>
<div <label
style={{ style={{
width: '100%', display: 'block',
maxWidth: 300, fontWeight: '600',
height: 200, fontSize: '0.875rem',
display: 'flex', marginBottom: '0.5rem',
alignItems: 'center', }}
justifyContent: 'center', >
border: '1px solid #eee', {label || '商品封面'}
borderRadius: 8, {required && <span style={{ color: 'var(--theme-error-500)' }}> *</span>}
background: '#fafbfc', </label>
overflow: 'hidden', </div>
}}
> {/* 图片预览 */}
{isImage ? ( {value && (
<div
style={{
marginBottom: '1rem',
padding: '1rem',
backgroundColor: 'var(--theme-elevation-50)',
borderRadius: '4px',
border: '1px solid var(--theme-elevation-100)',
}}
>
<img <img
src={thumbnail} src={value}
alt="商品封面" alt="商品封面"
style={{ style={{
width: '100%', maxWidth: '100%',
height: '100%', maxHeight: '300px',
objectFit: 'contain', objectFit: 'contain',
display: 'block',
margin: '0 auto',
}}
onError={(e) => {
e.currentTarget.style.display = 'none'
}} }}
/> />
) : ( </div>
<span style={{ color: '#bbb' }}></span> )}
{/* URL 输入框 */}
<div style={{ marginBottom: '0.75rem' }}>
<input
type="text"
value={value || ''}
onChange={handleUrlChange}
placeholder="输入图片 URL 或上传图片"
style={{
width: '100%',
padding: '0.5rem',
fontSize: '0.875rem',
border: '1px solid var(--theme-elevation-400)',
borderRadius: '4px',
backgroundColor: 'var(--theme-elevation-0)',
color: 'var(--theme-text)',
}}
/>
</div>
{/* 文件上传按钮 */}
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap' }}>
<label
style={{
display: 'inline-block',
padding: '0.5rem 1rem',
fontSize: '0.875rem',
fontWeight: '500',
color: 'var(--theme-elevation-0)',
backgroundColor: 'var(--theme-elevation-800)',
border: 'none',
borderRadius: '4px',
cursor: uploading ? 'not-allowed' : 'pointer',
opacity: uploading ? 0.6 : 1,
}}
>
{uploading ? '上传中...' : '上传图片'}
<input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) {
handleFileUpload(file)
}
}}
disabled={uploading}
style={{ display: 'none' }}
/>
</label>
{value && (
<button
type="button"
onClick={handleClear}
disabled={uploading}
style={{
padding: '0.5rem 1rem',
fontSize: '0.875rem',
fontWeight: '500',
color: 'var(--theme-error-500)',
backgroundColor: 'transparent',
border: '1px solid var(--theme-error-500)',
borderRadius: '4px',
cursor: uploading ? 'not-allowed' : 'pointer',
opacity: uploading ? 0.6 : 1,
}}
>
</button>
)} )}
</div> </div>
{/* 错误提示 */}
{uploadError && (
<div
style={{
marginTop: '0.5rem',
padding: '0.5rem',
backgroundColor: 'var(--theme-error-50)',
color: 'var(--theme-error-700)',
fontSize: '0.75rem',
borderRadius: '4px',
border: '1px solid var(--theme-error-500)',
}}
>
{uploadError}
</div>
)}
{/* 说明文本 */}
<div
style={{ marginTop: '0.5rem', fontSize: '0.75rem', color: 'var(--theme-elevation-600)' }}
>
URL
</div>
</div> </div>
) )
} }

View File

@ -0,0 +1,45 @@
'use client'
import React from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
/**
*
*/
export function AdminPanelNavLink() {
const pathname = usePathname()
const isActive = pathname === '/admin/admin-panel'
return (
<Link
href="/admin/admin-panel"
style={{
display: 'block',
padding: '0.75rem 1rem',
textDecoration: 'none',
color: isActive ? 'var(--theme-text)' : 'var(--theme-elevation-800)',
backgroundColor: isActive ? 'var(--theme-elevation-100)' : 'transparent',
borderRadius: '4px',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'var(--theme-elevation-50)'
}
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'transparent'
}
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ fontSize: '1.25rem' }}>🛠</span>
<span style={{ fontSize: '0.875rem', fontWeight: isActive ? '600' : '400' }}>
</span>
</div>
</Link>
)
}

View File

@ -0,0 +1,107 @@
'use client'
import { useState } from 'react'
import { Button } from '@payloadcms/ui'
/**
*
* Products, Announcements, Articles
*/
export function ClearDataButton() {
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('')
const [showConfirm, setShowConfirm] = useState(false)
const handleClearData = () => {
setShowConfirm(true)
setMessage('')
}
const handleConfirm = async () => {
setLoading(true)
setMessage('')
setShowConfirm(false)
try {
const response = await fetch('/api/clear-data?confirm=true', {
method: 'GET',
})
const data = await response.json()
if (data.success) {
setMessage(data.message || '数据清理成功!')
setTimeout(() => window.location.reload(), 2000)
} else {
setMessage(`清理失败: ${data.error}`)
}
} catch (error) {
setMessage(`清理出错: ${error instanceof Error ? error.message : '未知错误'}`)
} finally {
setLoading(false)
}
}
const handleCancel = () => {
setShowConfirm(false)
setMessage('')
}
return (
<div style={{ padding: '1rem', borderBottom: '1px solid var(--theme-elevation-100)' }}>
<h3 style={{ marginBottom: '0.5rem', fontSize: '1rem' }}>🗑 </h3>
<p style={{ marginBottom: '1rem', fontSize: '0.875rem', color: 'var(--theme-elevation-500)' }}>
</p>
{showConfirm ? (
<div>
<div
style={{
marginBottom: '1rem',
padding: '0.75rem',
backgroundColor: 'var(--theme-error-50)',
borderRadius: '4px',
border: '1px solid var(--theme-error-500)',
}}
>
<p style={{ margin: '0', fontWeight: 'bold', color: 'var(--theme-error-700)' }}>
</p>
<p style={{ margin: '0.5rem 0 0 0', fontSize: '0.875rem' }}>
</p>
</div>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<Button onClick={handleConfirm} disabled={loading} buttonStyle="error">
{loading ? '清理中...' : '确认清理'}
</Button>
<Button onClick={handleCancel} disabled={loading} buttonStyle="secondary">
</Button>
</div>
</div>
) : (
<Button onClick={handleClearData} disabled={loading} buttonStyle="error">
</Button>
)}
{message && (
<div
style={{
marginTop: '0.75rem',
padding: '0.75rem',
backgroundColor: message.includes('失败') || message.includes('出错')
? 'var(--theme-error-50)'
: 'var(--theme-success-50)',
borderRadius: '4px',
fontSize: '0.875rem',
}}
>
{message}
</div>
)}
</div>
)
}

View File

@ -24,7 +24,7 @@ export function SyncMedusaButton() {
// 刷新页面显示新商品 // 刷新页面显示新商品
setTimeout(() => window.location.reload(), 1500) setTimeout(() => window.location.reload(), 1500)
} else { } else {
setMessage(`同步失败: ${data.error}`) setMessage(`同步失败: ${data.error || data.message || '未知错误'}`)
} }
} catch (error) { } catch (error) {
setMessage(`同步出错: ${error instanceof Error ? error.message : '未知错误'}`) setMessage(`同步出错: ${error instanceof Error ? error.message : '未知错误'}`)
@ -60,7 +60,7 @@ export function SyncMedusaButton() {
setMessage(data.message || '强制更新成功!') setMessage(data.message || '强制更新成功!')
setTimeout(() => window.location.reload(), 1500) setTimeout(() => window.location.reload(), 1500)
} else { } else {
setMessage(`同步失败: ${data.error}`) setMessage(`同步失败: ${data.error || data.message || '未知错误'}`)
} }
} catch (error) { } catch (error) {
setMessage(`同步出错: ${error instanceof Error ? error.message : '未知错误'}`) setMessage(`同步出错: ${error instanceof Error ? error.message : '未知错误'}`)

View File

@ -0,0 +1,268 @@
'use client'
import React, { useState } from 'react'
import { Button } from '@payloadcms/ui'
/**
*
*
*/
export default function AdminPanel() {
const [clearLoading, setClearLoading] = useState(false)
const [clearMessage, setClearMessage] = useState('')
const [showClearConfirm, setShowClearConfirm] = useState(false)
const [fixLoading, setFixLoading] = useState(false)
const [fixMessage, setFixMessage] = useState('')
const handleClearData = () => {
setShowClearConfirm(true)
setClearMessage('')
}
const handleConfirmClear = async () => {
setClearLoading(true)
setClearMessage('')
setShowClearConfirm(false)
try {
const response = await fetch('/api/clear-data?confirm=true', {
method: 'GET',
})
const data = await response.json()
if (data.success) {
setClearMessage(data.message || '数据清理成功!')
} else {
setClearMessage(`清理失败: ${data.error}`)
}
} catch (error) {
setClearMessage(`清理出错: ${error instanceof Error ? error.message : '未知错误'}`)
} finally {
setClearLoading(false)
}
}
const handleCancelClear = () => {
setShowClearConfirm(false)
setClearMessage('')
}
const handleFixDatabase = async () => {
if (!confirm('确定要修复数据库字段类型吗?这会修改 products 表的 thumbnail 字段。')) {
return
}
setFixLoading(true)
setFixMessage('')
try {
const response = await fetch('/api/fix-database', {
method: 'GET',
})
const data = await response.json()
if (data.success) {
setFixMessage(data.message || '数据库修复成功!')
} else {
setFixMessage(`修复失败: ${data.error}`)
}
} catch (error) {
setFixMessage(`修复出错: ${error instanceof Error ? error.message : '未知错误'}`)
} finally {
setFixLoading(false)
}
}
return (
<div style={{ padding: '2rem', maxWidth: '1200px', 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={{
backgroundColor: 'var(--theme-elevation-0)',
borderRadius: '6px',
padding: '1.5rem',
border: '1px solid var(--theme-elevation-150)',
}}
>
<h3 style={{ marginBottom: '0.5rem', fontSize: '1rem', fontWeight: '600' }}>
🗑
</h3>
<p
style={{
marginBottom: '1rem',
fontSize: '0.875rem',
color: 'var(--theme-elevation-600)',
}}
>
</p>
{showClearConfirm ? (
<div>
<div
style={{
marginBottom: '1rem',
padding: '1rem',
backgroundColor: 'var(--theme-error-50)',
borderRadius: '6px',
border: '1px solid var(--theme-error-500)',
}}
>
<p
style={{
margin: '0 0 0.5rem 0',
fontWeight: 'bold',
color: 'var(--theme-error-700)',
}}
>
</p>
<p style={{ margin: '0', fontSize: '0.875rem', color: 'var(--theme-error-600)' }}>
</p>
</div>
<div style={{ display: 'flex', gap: '0.75rem' }}>
<Button onClick={handleConfirmClear} disabled={clearLoading} buttonStyle="error">
{clearLoading ? '清理中...' : '确认清理'}
</Button>
<Button onClick={handleCancelClear} disabled={clearLoading} buttonStyle="secondary">
</Button>
</div>
</div>
) : (
<Button onClick={handleClearData} disabled={clearLoading} buttonStyle="error">
</Button>
)}
{clearMessage && (
<div
style={{
marginTop: '1rem',
padding: '1rem',
backgroundColor:
clearMessage.includes('失败') || clearMessage.includes('出错')
? 'var(--theme-error-50)'
: 'var(--theme-success-50)',
borderRadius: '6px',
fontSize: '0.875rem',
border: `1px solid ${
clearMessage.includes('失败') || clearMessage.includes('出错')
? 'var(--theme-error-500)'
: 'var(--theme-success-500)'
}`,
}}
>
{clearMessage}
</div>
)}
</div>
{/* 修复数据库区域 */}
<div
style={{
backgroundColor: 'var(--theme-elevation-0)',
borderRadius: '6px',
padding: '1.5rem',
marginTop: '1rem',
border: '1px solid var(--theme-elevation-150)',
}}
>
<h3 style={{ marginBottom: '0.5rem', fontSize: '1rem', fontWeight: '600' }}>
🔧
</h3>
<p
style={{
marginBottom: '1rem',
fontSize: '0.875rem',
color: 'var(--theme-elevation-600)',
}}
>
products.thumbnail integer varchar
</p>
<Button onClick={handleFixDatabase} disabled={fixLoading} buttonStyle="primary">
{fixLoading ? '修复中...' : '修复字段类型'}
</Button>
{fixMessage && (
<div
style={{
marginTop: '1rem',
padding: '1rem',
backgroundColor:
fixMessage.includes('失败') || fixMessage.includes('出错')
? 'var(--theme-error-50)'
: 'var(--theme-success-50)',
borderRadius: '6px',
fontSize: '0.875rem',
border: `1px solid ${
fixMessage.includes('失败') || fixMessage.includes('出错')
? 'var(--theme-error-500)'
: 'var(--theme-success-500)'
}`,
}}
>
{fixMessage}
</div>
)}
</div>
</div>
{/* 系统信息区域 */}
<div
style={{
backgroundColor: 'var(--theme-elevation-50)',
borderRadius: '8px',
padding: '1.5rem',
border: '1px solid var(--theme-elevation-100)',
}}
>
<h2 style={{ marginBottom: '1rem', fontSize: '1.25rem', fontWeight: '600' }}>
</h2>
<div
style={{
backgroundColor: 'var(--theme-elevation-0)',
borderRadius: '6px',
padding: '1rem',
border: '1px solid var(--theme-elevation-150)',
}}
>
<div style={{ marginBottom: '0.5rem' }}>
<strong>Payload CMS:</strong> v3.75.0
</div>
<div style={{ marginBottom: '0.5rem' }}>
<strong>:</strong> PostgreSQL
</div>
<div style={{ marginBottom: '0.5rem' }}>
<strong>:</strong> Cloudflare R2 (S3 API)
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,41 @@
import type { GlobalConfig } from 'payload'
export const AdminSettings: GlobalConfig = {
slug: 'admin-settings',
access: {
read: ({ req: { user } }) => {
// 只有 admin 可以访问
if (!user) return false
return user.roles?.includes('admin') || 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/AdminPanel',
},
},
},
},
},
fields: [
{
name: 'title',
type: 'text',
defaultValue: '管理员设置',
admin: {
readOnly: true,
hidden: true,
},
},
],
}

View File

@ -194,3 +194,41 @@ export function transformMedusaProductToPayload(product: MedusaProduct) {
lastSyncedAt: new Date().toISOString(), lastSyncedAt: new Date().toISOString(),
} }
} }
/**
* Medusa Payload
* Media Media ID
*/
export async function transformMedusaProductToPayloadAsync(
product: MedusaProduct,
payload: any,
): Promise<{
medusaId: string
title: string
handle: string
thumbnail: string | null
status: string
lastSyncedAt: string
}> {
// 优先使用 thumbnail如果没有则使用第一张图片的 URL
let thumbnailUrl = product.thumbnail
if (!thumbnailUrl && product.images && product.images.length > 0) {
thumbnailUrl = product.images[0].url
}
// 上传图片到 Media 集合
let thumbnailId: string | null = null
if (thumbnailUrl) {
thumbnailId = await uploadImageFromUrl(thumbnailUrl, payload)
}
return {
medusaId: product.id,
title: product.title,
handle: product.handle,
thumbnail: thumbnailId,
status: 'draft',
lastSyncedAt: new Date().toISOString(),
}
}

View File

@ -1,4 +1,6 @@
import * as migration_20260208_171142 from './20260208_171142' import * as migration_20260208_171142 from './20260208_171142'
import * as migration_20260211_180407 from './20260211_180407'
import * as migration_20260211_180500_fix_thumbnail from './20260211_180500_fix_thumbnail'
export const migrations = [ export const migrations = [
{ {
@ -6,4 +8,14 @@ export const migrations = [
down: migration_20260208_171142.down, down: migration_20260208_171142.down,
name: '20260208_171142', name: '20260208_171142',
}, },
{
up: migration_20260211_180407.up,
down: migration_20260211_180407.down,
name: '20260211_180407',
},
{
up: migration_20260211_180500_fix_thumbnail.up,
down: migration_20260211_180500_fix_thumbnail.down,
name: '20260211_180500_fix_thumbnail',
},
] ]

View File

@ -70,7 +70,10 @@ export interface Config {
users: User; users: User;
media: Media; media: Media;
products: Product; products: Product;
announcements: Announcement;
articles: Article;
'payload-kv': PayloadKv; 'payload-kv': PayloadKv;
'payload-jobs': PayloadJob;
'payload-locked-documents': PayloadLockedDocument; 'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference; 'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration; 'payload-migrations': PayloadMigration;
@ -80,7 +83,10 @@ export interface Config {
users: UsersSelect<false> | UsersSelect<true>; users: UsersSelect<false> | UsersSelect<true>;
media: MediaSelect<false> | MediaSelect<true>; media: MediaSelect<false> | MediaSelect<true>;
products: ProductsSelect<false> | ProductsSelect<true>; products: ProductsSelect<false> | ProductsSelect<true>;
announcements: AnnouncementsSelect<false> | AnnouncementsSelect<true>;
articles: ArticlesSelect<false> | ArticlesSelect<true>;
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>; 'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
'payload-jobs': PayloadJobsSelect<false> | PayloadJobsSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>; 'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>; 'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>; 'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
@ -89,12 +95,22 @@ export interface Config {
defaultIDType: number; defaultIDType: number;
}; };
fallbackLocale: null; fallbackLocale: null;
globals: {}; globals: {
globalsSelect: {}; 'admin-settings': AdminSetting;
};
globalsSelect: {
'admin-settings': AdminSettingsSelect<false> | AdminSettingsSelect<true>;
};
locale: null; locale: null;
user: User; user: User;
jobs: { jobs: {
tasks: unknown; tasks: {
schedulePublish: TaskSchedulePublish;
inline: {
input: unknown;
output: unknown;
};
};
workflows: unknown; workflows: unknown;
}; };
} }
@ -122,6 +138,7 @@ export interface UserAuthOperations {
*/ */
export interface User { export interface User {
id: number; id: number;
roles: ('admin' | 'editor' | 'user')[];
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
email: string; email: string;
@ -180,12 +197,17 @@ export interface Product {
* Medusa * Medusa
*/ */
title: string; title: string;
handle?: string | null;
/** /**
* URLMedusa * URL URL
*/ */
thumbnail?: string | null; thumbnail?: string | null;
/** /**
* *
*/
lastSyncedAt?: string | null;
/**
*
*/ */
content?: { content?: {
root: { root: {
@ -202,21 +224,168 @@ export interface Product {
}; };
[k: string]: unknown; [k: string]: unknown;
} | null; } | null;
/**
* URL slug Medusa
*/
handle?: string | null;
/** /**
* *
*/ */
relatedProducts?: (number | Product)[] | null; relatedProducts?: (number | Product)[] | null;
/**
*
*/
lastSyncedAt?: string | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
*
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "announcements".
*/
export interface Announcement {
id: number;
/**
*
*/
title: string;
/**
*
*/
type: 'info' | 'warning' | 'important' | 'urgent';
/**
*
*/
status: 'draft' | 'published' | 'archived';
/**
*
*/
priority?: number | null;
/**
*
*/
summary?: string | null;
/**
*
*/
content: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
/**
*
*/
publishedAt?: string | null;
/**
*
*/
expiresAt?: string | null;
/**
*
*/
showOnHomepage?: boolean | null;
/**
*
*/
author?: (number | null) | User;
updatedAt: string;
createdAt: string;
}
/**
*
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "articles".
*/
export interface Article {
id: number;
/**
*
*/
title: string;
/**
*
*/
status: 'draft' | 'published';
/**
* URL SEO URL
*/
slug: string;
/**
*
*/
featuredImage?: (number | null) | Media;
/**
* SEO
*/
excerpt?: string | null;
/**
*
*/
content: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
};
/**
*
*/
category: 'news' | 'tutorial' | 'tech' | 'review' | 'industry' | 'other';
/**
*
*/
featured?: boolean | null;
/**
*
*/
tags?: string[] | null;
/**
*
*/
author: number | User;
/**
*
*/
publishedAt?: string | null;
/**
*
*/
relatedArticles?: (number | Article)[] | null;
/**
*
*/
relatedProducts?: (number | Product)[] | null;
/**
* SEO 使
*/
metaTitle?: string | null;
/**
* SEO 使
*/
metaDescription?: string | null;
/**
* SEO
*/
metaKeywords?: string[] | null;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/** /**
* 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".
@ -234,6 +403,98 @@ export interface PayloadKv {
| boolean | boolean
| null; | null;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-jobs".
*/
export interface PayloadJob {
id: number;
/**
* Input data provided to the job
*/
input?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
taskStatus?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
completedAt?: string | null;
totalTried?: number | null;
/**
* If hasError is true this job will not be retried
*/
hasError?: boolean | null;
/**
* If hasError is true, this is the error that caused it
*/
error?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
/**
* Task execution log
*/
log?:
| {
executedAt: string;
completedAt: string;
taskSlug: 'inline' | 'schedulePublish';
taskID: string;
input?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
output?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
state: 'failed' | 'succeeded';
error?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
id?: string | null;
}[]
| null;
taskSlug?: ('inline' | 'schedulePublish') | null;
queue?: string | null;
waitUntil?: string | null;
processing?: boolean | 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-locked-documents". * via the `definition` "payload-locked-documents".
@ -252,6 +513,14 @@ export interface PayloadLockedDocument {
| ({ | ({
relationTo: 'products'; relationTo: 'products';
value: number | Product; value: number | Product;
} | null)
| ({
relationTo: 'announcements';
value: number | Announcement;
} | null)
| ({
relationTo: 'articles';
value: number | Article;
} | null); } | null);
globalSlug?: string | null; globalSlug?: string | null;
user: { user: {
@ -300,6 +569,7 @@ export interface PayloadMigration {
* via the `definition` "users_select". * via the `definition` "users_select".
*/ */
export interface UsersSelect<T extends boolean = true> { export interface UsersSelect<T extends boolean = true> {
roles?: T;
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
email?: T; email?: T;
@ -343,14 +613,57 @@ export interface ProductsSelect<T extends boolean = true> {
medusaId?: T; medusaId?: T;
status?: T; status?: T;
title?: T; title?: T;
thumbnail?: T;
content?: T;
handle?: T; handle?: T;
relatedProducts?: T; thumbnail?: T;
lastSyncedAt?: T; lastSyncedAt?: T;
content?: T;
relatedProducts?: T;
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "announcements_select".
*/
export interface AnnouncementsSelect<T extends boolean = true> {
title?: T;
type?: T;
status?: T;
priority?: T;
summary?: T;
content?: T;
publishedAt?: T;
expiresAt?: T;
showOnHomepage?: T;
author?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "articles_select".
*/
export interface ArticlesSelect<T extends boolean = true> {
title?: T;
status?: T;
slug?: T;
featuredImage?: T;
excerpt?: T;
content?: T;
category?: T;
featured?: T;
tags?: T;
author?: T;
publishedAt?: T;
relatedArticles?: T;
relatedProducts?: T;
metaTitle?: T;
metaDescription?: T;
metaKeywords?: T;
updatedAt?: T;
createdAt?: T;
_status?: 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".
@ -359,6 +672,37 @@ export interface PayloadKvSelect<T extends boolean = true> {
key?: T; key?: T;
data?: T; data?: T;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-jobs_select".
*/
export interface PayloadJobsSelect<T extends boolean = true> {
input?: T;
taskStatus?: T;
completedAt?: T;
totalTried?: T;
hasError?: T;
error?: T;
log?:
| T
| {
executedAt?: T;
completedAt?: T;
taskSlug?: T;
taskID?: T;
input?: T;
output?: T;
state?: T;
error?: T;
id?: T;
};
taskSlug?: T;
queue?: T;
waitUntil?: T;
processing?: 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-locked-documents_select". * via the `definition` "payload-locked-documents_select".
@ -391,6 +735,45 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
} }
/**
* -
*
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "admin-settings".
*/
export interface AdminSetting {
id: number;
title?: string | null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "admin-settings_select".
*/
export interface AdminSettingsSelect<T extends boolean = true> {
title?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "TaskSchedulePublish".
*/
export interface TaskSchedulePublish {
input: {
type?: ('publish' | 'unpublish') | null;
locale?: string | null;
doc?: {
relationTo: 'articles';
value: number | Article;
} | null;
global?: string | null;
user?: (number | null) | User;
};
output?: unknown;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth". * via the `definition` "auth".

View File

@ -8,6 +8,9 @@ import sharp from 'sharp'
import { Users } from './collections/Users' import { Users } from './collections/Users'
import { Media } from './collections/Media' import { Media } from './collections/Media'
import { Products } from './collections/Products' import { Products } from './collections/Products'
import { Announcements } from './collections/Announcements'
import { Articles } from './collections/Articles'
import { AdminSettings } from './globals/AdminSettings'
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'
@ -39,7 +42,8 @@ export default buildConfig({
}, },
fallbackLanguage: 'zh', fallbackLanguage: 'zh',
}, },
collections: [Users, Media, Products], collections: [Users, Media, Products, Announcements, Articles],
globals: [AdminSettings],
editor: lexicalEditor(), editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || '', secret: process.env.PAYLOAD_SECRET || '',
typescript: { typescript: {

View File

@ -4,6 +4,7 @@ import config from '../../src/payload.config.js'
export const testUser = { export const testUser = {
email: 'dev@payloadcms.com', email: 'dev@payloadcms.com',
password: 'test', password: 'test',
roles: ['admin'] as ('admin' | 'editor' | 'user')[],
} }
/** /**