精简api

This commit is contained in:
龟男日记\www 2026-02-21 03:04:38 +08:00
parent 41f3eb5adf
commit b9cb60e3d0
12 changed files with 533 additions and 329 deletions

View File

@ -35,6 +35,7 @@ import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228
import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler' import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler'
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 { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView'
import { RestoreRecommendationsSeedButton as RestoreRecommendationsSeedButton_ebef550e255346daa9e9f2a11698b0da } from '../../../components/seed/RestoreRecommendationsSeedButton'
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'
@ -76,6 +77,7 @@ export const importMap = {
"/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5, "/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5,
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4, "/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f, "/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
"/components/seed/RestoreRecommendationsSeedButton#RestoreRecommendationsSeedButton": RestoreRecommendationsSeedButton_ebef550e255346daa9e9f2a11698b0da,
"@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
} }

109
src/app/api/admin/route.ts Normal file
View File

@ -0,0 +1,109 @@
import { getPayload } from 'payload'
import config from '@payload-config'
import { NextRequest, NextResponse } from 'next/server'
/**
* Admin Management API
* Combined endpoint for data clearing, stats, and diagnostics
*/
/**
* GET /api/admin?action=stats
* Get Payload collection statistics
*/
async function getStats() {
const payload = await getPayload({ config })
const [products, preorderProducts] = await Promise.all([
payload.find({ collection: 'products', limit: 0 }),
payload.find({ collection: 'preorder-products', limit: 0 }),
])
return {
products: products.totalDocs,
preorderProducts: preorderProducts.totalDocs,
total: products.totalDocs + preorderProducts.totalDocs,
}
}
/**
* DELETE /api/admin
* Clear Payload data (preserves Users and Media)
* Query params: ?collections=products,preorderProducts,announcements,articles
*/
async function clearData(searchParams: URLSearchParams) {
const payload = await getPayload({ config })
const collectionsParam = searchParams.get('collections')
const collections = collectionsParam
? collectionsParam.split(',')
: ['products', 'preorder-products', 'announcements', 'articles']
const results: Record<string, number> = {}
const errors: string[] = []
for (const collection of collections) {
// Protect critical collections
if (['users', 'media'].includes(collection)) {
errors.push(`Skipped protected collection: ${collection}`)
continue
}
try {
const deleted = await payload.delete({
collection: collection as any,
where: {},
})
results[collection] = deleted.docs?.length || 0
console.log(`✅ Cleared ${results[collection]} documents from ${collection}`)
} catch (error) {
const errorMsg = `Failed to clear ${collection}: ${error instanceof Error ? error.message : 'Unknown error'}`
console.error('❌', errorMsg)
errors.push(errorMsg)
}
}
return {
success: true,
message: 'Data cleared successfully',
results,
errors: errors.length > 0 ? errors : undefined,
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
if (action === 'stats') {
const stats = await getStats()
return NextResponse.json({ success: true, ...stats })
}
return NextResponse.json({
success: false,
error: 'Invalid action. Valid actions: stats',
}, { status: 400 })
} catch (error) {
console.error('[admin] GET error:', error)
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}, { status: 500 })
}
}
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const result = await clearData(searchParams)
return NextResponse.json(result)
} catch (error) {
console.error('[admin] DELETE error:', error)
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}, { status: 500 })
}
}

View File

@ -1,11 +1,12 @@
import { getPayload } from 'payload' import { getPayload } from 'payload'
import config from '@payload-config' import config from '@payload-config'
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { getAllMedusaProducts } from '@/lib/medusa'
/** /**
* Medusa * Batch Sync Selected Products
* POST /api/batch-sync-medusa * POST /api/batch-sync-medusa
* Body: { ids: string[], collection: 'products' | 'preorder-products', forceUpdate: boolean } * Body: { ids: string[], collection: 'products' | 'preorder-products', forceUpdate?: boolean }
*/ */
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
@ -14,83 +15,87 @@ export async function POST(request: Request) {
if (!ids || !Array.isArray(ids) || ids.length === 0) { if (!ids || !Array.isArray(ids) || ids.length === 0) {
return NextResponse.json( return NextResponse.json(
{ { success: false, error: 'No product IDs provided' },
success: false,
error: 'No product IDs provided',
},
{ status: 400 }, { status: 400 },
) )
} }
if (!collection || !['products', 'preorder-products'].includes(collection)) { if (!collection || !['products', 'preorder-products'].includes(collection)) {
return NextResponse.json( return NextResponse.json(
{ { success: false, error: 'Invalid collection' },
success: false,
error: 'Invalid collection',
},
{ status: 400 }, { status: 400 },
) )
} }
const payload = await getPayload({ config }) const payload = await getPayload({ config })
// Get all Medusa products once
const medusaProducts = await getAllMedusaProducts()
const medusaProductMap = new Map(medusaProducts.map(p => [p.id, p]))
const results = { const results = {
total: ids.length, total: ids.length,
success: 0, success: 0,
failed: 0, failed: 0,
skipped: 0,
details: [] as any[], details: [] as any[],
} }
// 逐个同步选中的产品 // Sync each selected product
for (const id of ids) { for (const id of ids) {
try { try {
// 获取产品信息
const product = await payload.findByID({ const product = await payload.findByID({
collection: collection as 'products' | 'preorder-products', collection: collection as 'products' | 'preorder-products',
id, id,
}) })
if (!product || !product.medusaId) { if (!product || !product.medusaId) {
results.failed++ results.skipped++
results.details.push({ results.details.push({
id, id,
title: product?.title || 'Unknown', title: product?.title || 'Unknown',
status: 'failed', status: 'skipped',
error: 'No Medusa ID', reason: 'No Medusa ID',
}) })
continue continue
} }
// 调用单个产品同步 API const medusaProduct = medusaProductMap.get(product.medusaId)
const syncResponse = await fetch(
`${request.url.replace('/api/batch-sync-medusa', '/api/sync-medusa')}?medusaId=${product.medusaId}&forceUpdate=${forceUpdate}`,
{
method: 'GET',
headers: request.headers,
},
)
const syncResult = await syncResponse.json() if (!medusaProduct) {
results.failed++
results.details.push({
id,
medusaId: product.medusaId,
title: product.title,
status: 'failed',
error: 'Product not found in Medusa',
})
continue
}
// Update basic fields from Medusa
const updateData: any = {
lastSyncedAt: new Date().toISOString(),
}
if (forceUpdate || !product.title) updateData.title = medusaProduct.title
if (forceUpdate || !product.handle) updateData.handle = medusaProduct.handle
if (forceUpdate || !product.thumbnail) updateData.thumbnail = medusaProduct.thumbnail?.url
await payload.update({
collection: collection as 'products' | 'preorder-products',
id,
data: updateData,
})
if (syncResult.success) {
results.success++ results.success++
results.details.push({ results.details.push({
id, id,
medusaId: product.medusaId, medusaId: product.medusaId,
title: product.title, title: product.title,
status: 'success', status: 'success',
action: syncResult.action,
}) })
} else {
results.failed++
results.details.push({
id,
medusaId: product.medusaId,
title: product.title,
status: 'failed',
error: syncResult.error || syncResult.message,
})
}
} catch (error) { } catch (error) {
results.failed++ results.failed++
results.details.push({ results.details.push({
@ -103,11 +108,11 @@ export async function POST(request: Request) {
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
message: `批量同步完成: ${results.success} 成功, ${results.failed} 失败`, message: `Batch sync completed: ${results.success} success, ${results.failed} failed, ${results.skipped} skipped`,
results, results,
}) })
} catch (error) { } catch (error) {
console.error('Batch sync error:', error) console.error('[batch-sync-medusa] Error:', error)
return NextResponse.json( return NextResponse.json(
{ {
success: false, success: false,

View File

@ -1,166 +0,0 @@
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,
preorderProducts: 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)
}
// 清理 Preorder Products
try {
const deletedPreorderProducts = await payload.delete({
collection: 'preorder-products',
where: {},
})
results.preorderProducts = deletedPreorderProducts.docs?.length || 0
console.log(`✅ 已清理 ${results.preorderProducts} 个预购商品`)
} catch (error) {
const errorMsg = `清理 Preorder 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.preorderProducts} 个预购商品、${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

@ -1,50 +0,0 @@
import { getPayload } from 'payload'
import config from '@payload-config'
import { NextResponse } from 'next/server'
export async function POST() {
try {
const payload = await getPayload({ config })
// Delete all products
const products = await payload.find({
collection: 'products',
limit: 1000,
})
for (const product of products.docs) {
await payload.delete({
collection: 'products',
id: product.id,
})
}
// Delete all preorder products
const preorderProducts = await payload.find({
collection: 'preorder-products',
limit: 1000,
})
for (const product of preorderProducts.docs) {
await payload.delete({
collection: 'preorder-products',
id: product.id,
})
}
return NextResponse.json({
success: true,
message: `Cleared ${products.docs.length} products and ${preorderProducts.docs.length} preorder-products`,
deleted: {
products: products.docs.length,
preorderProducts: preorderProducts.docs.length,
},
})
} catch (error) {
console.error('[clear-payload-collections] Error:', error)
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : String(error),
}, { status: 500 })
}
}

View File

@ -1,42 +0,0 @@
import { getAllMedusaProducts } from '@/lib/medusa'
import { NextResponse } from 'next/server'
/**
* API - Medusa metadata
* GET /api/diagnose-products
*/
export async function GET() {
try {
const products = await getAllMedusaProducts()
const diagnosis = products.map((product) => ({
id: product.id,
title: product.title,
handle: product.handle,
metadata: product.metadata,
is_preorder_value: product.metadata?.is_preorder,
is_preorder_type: typeof product.metadata?.is_preorder,
should_be_in_collection:
product.metadata?.is_preorder === true || product.metadata?.is_preorder === 'true'
? 'preorder-products'
: 'products',
}))
return NextResponse.json({
success: true,
total: products.length,
preorder_count: diagnosis.filter((d) => d.should_be_in_collection === 'preorder-products')
.length,
regular_count: diagnosis.filter((d) => d.should_be_in_collection === 'products').length,
products: diagnosis,
})
} catch (error) {
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 },
)
}
}

View File

@ -1,32 +0,0 @@
import { getPayload } from 'payload'
import config from '@payload-config'
import { NextResponse } from 'next/server'
export async function GET() {
try {
const payload = await getPayload({ config })
const products = await payload.find({
collection: 'products',
limit: 0, // Just get count
})
const preorderProducts = await payload.find({
collection: 'preorder-products',
limit: 0, // Just get count
})
return NextResponse.json({
success: true,
products: products.totalDocs,
preorderProducts: preorderProducts.totalDocs,
total: products.totalDocs + preorderProducts.totalDocs,
})
} catch (error) {
console.error('[payload-stats] Error:', error)
return NextResponse.json({
success: false,
error: error instanceof Error ? error.message : String(error),
}, { status: 500 })
}
}

View File

@ -0,0 +1,52 @@
import { NextRequest, NextResponse } from 'next/server'
import { getPayload } from 'payload'
import config from '@payload-config'
/**
* API Route: Restore Product Recommendations from Seed
* POST /api/restore-recommendations-seed
*
* This server-side route uses Payload's local API to update the global config
* which requires proper authentication context that client-side fetch doesn't have.
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { enabled, lists } = body
if (!lists || !Array.isArray(lists)) {
return NextResponse.json(
{ success: false, error: 'Invalid lists data' },
{ status: 400 }
)
}
// Get Payload instance with proper context
const payload = await getPayload({ config })
// Update the global using Payload's local API
const result = await payload.updateGlobal({
slug: 'product-recommendations',
data: {
enabled: enabled ?? true,
lists: lists,
},
})
return NextResponse.json({
success: true,
message: `Successfully restored ${lists.length} recommendation list(s)`,
data: result,
})
} catch (error) {
console.error('Error restoring recommendations seed:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error occurred',
},
{ status: 500 }
)
}
}

View File

@ -0,0 +1,254 @@
'use client'
import { useState } from 'react'
import { Button } from '@payloadcms/ui'
import { AVAILABLE_SEEDS, type SeedKey } from './data/20260221-product-recommendations-seeds'
interface Props {
className?: string
}
/**
* Restore Recommendations Seed Button
* Quick restore predefined product recommendation configurations
*/
export function RestoreRecommendationsSeedButton({ className }: Props) {
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState('')
const [showSeedSelector, setShowSeedSelector] = useState(false)
/**
* Find products by seed IDs
* Returns polymorphic relationship format: { relationTo, value }
*/
const findProductsBySeedIds = async (
seedIds: string[],
): Promise<Array<{ relationTo: string; value: string }>> => {
const products: Array<{ relationTo: string; value: string }> = []
for (const seedId of seedIds) {
try {
// Try products collection first
const productsResponse = await fetch(
`/api/products?where[seedId][equals]=${seedId}&limit=1`,
)
const productsData = await productsResponse.json()
if (productsData.docs && productsData.docs.length > 0) {
products.push({
relationTo: 'products',
value: productsData.docs[0].id,
})
continue
}
// Try preorder-products if not found
const preorderResponse = await fetch(
`/api/preorder-products?where[seedId][equals]=${seedId}&limit=1`,
)
const preorderData = await preorderResponse.json()
if (preorderData.docs && preorderData.docs.length > 0) {
products.push({
relationTo: 'preorder-products',
value: preorderData.docs[0].id,
})
} else {
console.warn(`Product not found for seedId: ${seedId}`)
}
} catch (error) {
console.error(`Error finding product ${seedId}:`, error)
}
}
return products
}
/**
* Restore recommendation list from seed
*/
const handleRestoreSeed = async (seedKey: SeedKey) => {
const seed = AVAILABLE_SEEDS[seedKey]
if (!confirm(
`Restore recommendation list configuration?\n\n` +
`Will create:\n${seed.lists.map(list => `${list.title} (${list.productSeedIds.length} products)`).join('\n')}\n\n` +
`Current configuration will be overwritten!`
)) {
return
}
setLoading(true)
setMessage('🔄 Finding products...')
try {
// Find all product IDs in polymorphic relationship format
const listsWithProductIds = await Promise.all(
seed.lists.map(async (list) => {
const products = await findProductsBySeedIds(list.productSeedIds)
return {
title: list.title,
subtitle: list.subtitle || '',
products: products,
}
}),
)
// Filter out lists with no products found
const validLists = listsWithProductIds.filter((list) => list.products.length > 0)
if (validLists.length === 0) {
setMessage('❌ No matching products found. Please run seed script first.')
setLoading(false)
return
}
setMessage('💾 Updating configuration...')
// Update product-recommendations global via server-side API
const updateResponse = await fetch('/api/restore-recommendations-seed', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
enabled: seed.enabled,
lists: validLists,
}),
})
const result = await updateResponse.json()
if (!result.success) {
throw new Error(result.error || 'Update failed')
}
setMessage(`✅ Successfully restored ${validLists.length} recommendation list(s)!`)
setShowSeedSelector(false)
// Refresh page after 2 seconds
setTimeout(() => {
window.location.reload()
}, 2000)
} catch (error) {
console.error('Failed to restore seed configuration:', error)
setMessage('❌ Restore failed: ' + (error instanceof Error ? error.message : 'Unknown error'))
} finally {
setLoading(false)
}
}
return (
<div className={className}>
<div style={{ marginBottom: '1rem' }}>
<Button
onClick={() => setShowSeedSelector(!showSeedSelector)}
buttonStyle="pill"
size="small"
disabled={loading}
>
🌱 {showSeedSelector ? 'Hide' : 'Restore from Seed'}
</Button>
</div>
{showSeedSelector && (
<div
style={{
padding: '1.5rem',
backgroundColor: 'var(--theme-elevation-50)',
borderRadius: '4px',
border: '1px solid var(--theme-elevation-150)',
marginBottom: '1rem',
}}
>
<h4 style={{ marginTop: 0, marginBottom: '1rem', fontSize: '0.9rem', fontWeight: 600 }}>
📦 Available Seed Configurations
</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{Object.entries(AVAILABLE_SEEDS).map(([key, seed]) => (
<div
key={key}
style={{
padding: '1rem',
backgroundColor: 'var(--theme-elevation-0)',
borderRadius: '4px',
border: '1px solid var(--theme-elevation-100)',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: '0.5rem',
}}
>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, marginBottom: '0.25rem' }}>
{key === 'batch02' ? 'Batch 02 Recommendations' : key}
</div>
<div style={{ fontSize: '0.85rem', color: 'var(--theme-elevation-500)' }}>
{seed.lists.length} list(s), {' '}
{seed.lists.reduce((sum, list) => sum + list.productSeedIds.length, 0)} product(s) total
</div>
</div>
<Button
onClick={() => handleRestoreSeed(key as SeedKey)}
buttonStyle="primary"
size="small"
disabled={loading}
>
Restore
</Button>
</div>
<div style={{ marginTop: '0.75rem', paddingTop: '0.75rem', borderTop: '1px solid var(--theme-elevation-100)' }}>
{seed.lists.map((list, index) => (
<div
key={index}
style={{
fontSize: '0.85rem',
marginBottom: '0.5rem',
paddingLeft: '1rem',
}}
>
<div style={{ fontWeight: 500, color: 'var(--theme-elevation-700)' }}>
{list.title}
</div>
<div style={{ color: 'var(--theme-elevation-500)', fontSize: '0.8rem' }}>
{list.subtitle || `${list.productSeedIds.length} product(s)`}
</div>
</div>
))}
</div>
</div>
))}
</div>
{message && (
<div
style={{
marginTop: '1rem',
padding: '0.75rem',
backgroundColor: message.includes('❌')
? 'var(--theme-error-100)'
: message.includes('✅')
? 'var(--theme-success-100)'
: 'var(--theme-info-100)',
color: message.includes('❌')
? 'var(--theme-error-900)'
: message.includes('✅')
? 'var(--theme-success-900)'
: 'var(--theme-info-900)',
borderRadius: '4px',
fontSize: '0.9rem',
}}
>
{message}
</div>
)}
</div>
)}
</div>
)
}

View File

@ -0,0 +1,55 @@
/**
* Product Recommendations Seed Data
* Predefined recommendation lists for quick restoration
* Date: 2026-02-21
*/
export interface RecommendationListSeed {
title: string
subtitle?: string
productSeedIds: string[]
}
export interface RecommendationsSeed {
enabled: boolean
lists: RecommendationListSeed[]
}
/**
* Batch 02 Recommendations
* - PreGame: Preorder games collection (4 games)
* - PreMod: Preorder modification collection (2 metal shells + 1 custom console)
*/
export const BATCH_02_RECOMMENDATIONS: RecommendationsSeed = {
enabled: true,
lists: [
{
title: 'PreGame - Preorder Games',
subtitle: 'Selected indie games, now available for preorder! Support indie developers and get early access to new releases.',
productSeedIds: [
'game-urcicus', // Urcicus - GBA Game
'game-mikoto-nikki', // Mikoto Nikki - GBA Game
'game-passaway', // Passaway - GB Game
'game-judys-adventure-dx', // Judys Adventure DX - GBA Game
],
},
{
title: 'PreMod - Preorder Modifications',
subtitle: 'Premium custom shells and consoles, full metal construction, artisan craftsmanship. Limited preorder available!',
productSeedIds: [
'shell-gba-sp-metal-unhinged', // Metal Shell - GBA SP (Unhinged Mod)
'shell-gba-metal', // Metal Shell - GBA
'console-retro-tetra', // Retro Tetra Console
],
},
],
}
/**
* All available seed configurations
*/
export const AVAILABLE_SEEDS = {
batch02: BATCH_02_RECOMMENDATIONS,
}
export type SeedKey = keyof typeof AVAILABLE_SEEDS

View File

@ -0,0 +1,3 @@
export { RestoreRecommendationsSeedButton } from './RestoreRecommendationsSeedButton'
export { AVAILABLE_SEEDS, BATCH_02_RECOMMENDATIONS } from './data/20260221-product-recommendations-seeds'
export type { RecommendationListSeed, RecommendationsSeed, SeedKey } from './data/20260221-product-recommendations-seeds'

View File

@ -26,6 +26,20 @@ export const ProductRecommendations: GlobalConfig = {
}, },
}, },
fields: [ fields: [
{
name: 'seedActions',
type: 'ui',
label: {
en: 'Quick Actions',
zh: '快捷操作',
},
admin: {
position: 'sidebar',
components: {
Field: '/components/seed/RestoreRecommendationsSeedButton#RestoreRecommendationsSeedButton',
},
},
},
{ {
name: 'enabled', name: 'enabled',
type: 'checkbox', type: 'checkbox',