diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index d156704..bc2f82a 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -25,11 +25,12 @@ import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997e import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField' -import { SyncMedusaButton as SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08 } from '../../../components/sync/SyncMedusaButton' -import { BatchSyncButton as BatchSyncButton_c62499057175f17acbe529b96de3aeb8 } from '../../../components/sync/BatchSyncButton' +import { UnifiedSyncButton as UnifiedSyncButton_8f3d4a2e1b5c6d7a9e0f1a2b3c4d5e6f } from '../../../components/sync/UnifiedSyncButton' import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler' import { ForceSyncButton as ForceSyncButton_28396efe36d6238add95cf44109e281c } from '../../../components/sync/ForceSyncButton' +import { TaobaoLinkPreview as TaobaoLinkPreview_7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b } from '../../../components/fields/TaobaoLinkPreview' import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f } from '../../../components/fields/PreorderOrdersField' 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' @@ -63,11 +64,12 @@ export const importMap = { "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426, - "/components/sync/SyncMedusaButton#SyncMedusaButton": SyncMedusaButton_31e6578e170fdd0bad7013c8202d6e08, - "/components/sync/BatchSyncButton#BatchSyncButton": BatchSyncButton_c62499057175f17acbe529b96de3aeb8, + "/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_8f3d4a2e1b5c6d7a9e0f1a2b3c4d5e6f, "/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2, "/components/sync/ForceSyncButton#ForceSyncButton": ForceSyncButton_28396efe36d6238add95cf44109e281c, + "/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b, "@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f, "/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4, "/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f, "@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24, diff --git a/src/app/api/preorders/[id]/route.ts b/src/app/api/preorders/[id]/route.ts index a1501b0..93c9343 100644 --- a/src/app/api/preorders/[id]/route.ts +++ b/src/app/api/preorders/[id]/route.ts @@ -99,11 +99,14 @@ export async function GET( seed_id: product.seedId || product.medusaId, medusa_id: product.medusaId, - // 预购元数据 + // 预购元数据(从 Payload 管理) is_preorder: true, preorder_type: product.preorderType || 'standard', estimated_ship_date: product.estimatedShipDate || null, + preorder_end_date: product.preorderEndDate || null, funding_goal: fundingGoal, + min_order_quantity: product.minOrderQuantity || 1, + max_order_quantity: product.maxOrderQuantity || 0, // 统计数据 current_orders: totalOrders, @@ -126,6 +129,7 @@ export async function GET( // 时间戳 created_at: product.createdAt, updated_at: product.updatedAt, + last_synced_at: product.lastSyncedAt, }, }) } catch (error: any) { @@ -149,8 +153,11 @@ export async function GET( * - decrement?: number - 减少订单数 * * - estimated_ship_date?: string - 更新预估发货日期 - * - funding_goal?: number - 更新目标金额 + * - preorder_end_date?: string - 更新预购结束日期 + * - funding_goal?: number - 更新众筹目标 * - preorder_type?: string - 更新预购类型 + * - min_order_quantity?: number - 最小起订量 + * - max_order_quantity?: number - 最大购买数量 */ export async function PATCH( req: NextRequest, @@ -168,8 +175,11 @@ export async function PATCH( increment, decrement, estimated_ship_date, + preorder_end_date, funding_goal, preorder_type, + min_order_quantity, + max_order_quantity, } = body // 获取产品 @@ -272,18 +282,27 @@ export async function PATCH( }) } - // 模式2: 更新产品级别元数据 + // 模式2: 更新产品级别预购元数据(在 Payload 中管理) const updateData: any = {} if (estimated_ship_date !== undefined) { updateData.estimatedShipDate = estimated_ship_date } + if (preorder_end_date !== undefined) { + updateData.preorderEndDate = preorder_end_date + } if (funding_goal !== undefined) { updateData.fundingGoal = String(funding_goal) } if (preorder_type !== undefined) { updateData.preorderType = preorder_type } + if (min_order_quantity !== undefined) { + updateData.minOrderQuantity = min_order_quantity + } + if (max_order_quantity !== undefined) { + updateData.maxOrderQuantity = max_order_quantity + } if (Object.keys(updateData).length > 0) { await payload.update({ @@ -296,6 +315,7 @@ export async function PATCH( return NextResponse.json({ success: true, message: 'Preorder product updated successfully', + updated_fields: Object.keys(updateData), }) } catch (error: any) { console.error('[Payload Preorder Update API] Error:', error?.message || error) diff --git a/src/collections/PreorderProducts.ts b/src/collections/PreorderProducts.ts index 41e69ba..203c22d 100644 --- a/src/collections/PreorderProducts.ts +++ b/src/collections/PreorderProducts.ts @@ -38,8 +38,7 @@ export const PreorderProducts: CollectionConfig = { PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton', }, beforeListTable: [ - '/components/sync/SyncMedusaButton#SyncMedusaButton', - '/components/sync/BatchSyncButton#BatchSyncButton', + '/components/sync/UnifiedSyncButton#UnifiedSyncButton', '/components/list/ProductGridStyler', ], }, @@ -52,7 +51,7 @@ export const PreorderProducts: CollectionConfig = { type: 'tabs', tabs: [ { - label: '基本信息', + label: 'ℹ️ 基本信息', fields: [ { type: 'row', @@ -134,7 +133,84 @@ export const PreorderProducts: CollectionConfig = { ], }, { - label: '商品描述', + label: '⚙️ 预购设置', + fields: [ + { + type: 'row', + fields: [ + { + name: 'preorderType', + type: 'select', + required: true, + defaultValue: 'standard', + options: [ + { label: '标准预购', value: 'standard' }, + { label: '众筹预购', value: 'crowdfunding' }, + { label: '限量预购', value: 'limited' }, + ], + admin: { + description: '预购类型', + width: '50%', + }, + }, + { + name: 'fundingGoal', + type: 'number', + required: true, + defaultValue: 0, + admin: { + description: '众筹目标数量(0 表示以变体 max_orders 总和为准)', + width: '50%', + }, + }, + ], + }, + { + name: 'estimatedShipDate', + type: 'date', + admin: { + description: '预计发货日期', + date: { + displayFormat: 'yyyy-MM-dd', + }, + }, + }, + { + name: 'preorderEndDate', + type: 'date', + admin: { + description: '预购结束日期(可选)', + date: { + displayFormat: 'yyyy-MM-dd HH:mm', + }, + }, + }, + { + type: 'row', + fields: [ + { + name: 'minOrderQuantity', + type: 'number', + defaultValue: 1, + admin: { + description: '最小起订量', + width: '50%', + }, + }, + { + name: 'maxOrderQuantity', + type: 'number', + admin: { + description: '最大购买数量(0 表示不限制)', + width: '50%', + }, + }, + ], + }, + ], + }, + { + label: '📝 商品描述', fields: [ { name: 'description', @@ -194,7 +270,7 @@ export const PreorderProducts: CollectionConfig = { ], }, { - label: '相关商品', + label: '🔗 相关商品', fields: [ { name: 'relatedProducts', @@ -210,6 +286,84 @@ export const PreorderProducts: CollectionConfig = { }, ], }, + { + label: '📦 订单信息', + fields: [ + { + name: 'ordersDisplay', + type: 'ui', + admin: { + components: { + Field: '/components/fields/PreorderOrdersField#PreorderOrdersField', + }, + }, + }, + ], + }, + { + label: '🛒 淘宝链接', + fields: [ + { + name: 'taobaoLinks', + type: 'array', + label: '淘宝采购链接列表', + admin: { + description: '💡 管理淘宝采购链接(仅后台显示,不通过 API 暴露)', + initCollapsed: false, + }, + access: { + read: ({ req: { user } }) => !!user, + update: ({ req: { user } }) => !!user, + }, + fields: [ + { + name: 'url', + type: 'text', + label: '🔗 淘宝链接', + required: true, + admin: { + placeholder: 'https://item.taobao.com/...', + }, + }, + { + name: 'title', + type: 'text', + label: '📝 标题', + admin: { + placeholder: '链接标题或商品名称', + }, + }, + { + name: 'thumbnail', + type: 'text', + label: '🖼️ 缩略图 URL', + admin: { + placeholder: 'https://...', + description: '淘宝商品图片地址', + }, + }, + { + name: 'note', + type: 'textarea', + label: '📄 备注', + admin: { + placeholder: '其他备注信息...', + rows: 3, + }, + }, + { + type: 'ui', + name: 'linkPreview', + admin: { + components: { + Field: '/components/fields/TaobaoLinkPreview#TaobaoLinkPreview', + }, + }, + }, + ], + }, + ], + }, ], }, ], diff --git a/src/collections/Products.ts b/src/collections/Products.ts index 6da188b..54a0f82 100644 --- a/src/collections/Products.ts +++ b/src/collections/Products.ts @@ -38,8 +38,7 @@ export const Products: CollectionConfig = { PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton', }, beforeListTable: [ - '/components/sync/SyncMedusaButton#SyncMedusaButton', - '/components/sync/BatchSyncButton#BatchSyncButton', + '/components/sync/UnifiedSyncButton#UnifiedSyncButton', '/components/list/ProductGridStyler', ], }, @@ -52,7 +51,7 @@ export const Products: CollectionConfig = { type: 'tabs', tabs: [ { - label: '基本信息', + label: 'ℹ️ 基本信息', fields: [ { type: 'row', @@ -142,7 +141,7 @@ export const Products: CollectionConfig = { ], }, { - label: '商品详情', + label: '📄 商品详情', fields: [ { name: 'content', @@ -194,7 +193,7 @@ export const Products: CollectionConfig = { ], }, { - label: '关联信息', + label: '🔗 关联信息', fields: [ { name: 'relatedProducts', @@ -210,6 +209,70 @@ export const Products: CollectionConfig = { }, ], }, + { + label: '🛒 淘宝链接', + fields: [ + { + name: 'taobaoLinks', + type: 'array', + label: '淘宝采购链接列表', + admin: { + description: '💡 管理淘宝采购链接(仅后台显示,不通过 API 暴露)', + initCollapsed: false, + }, + access: { + read: ({ req: { user } }) => !!user, + update: ({ req: { user } }) => !!user, + }, + fields: [ + { + name: 'url', + type: 'text', + label: '🔗 淘宝链接', + required: true, + admin: { + placeholder: 'https://item.taobao.com/...', + }, + }, + { + name: 'title', + type: 'text', + label: '📝 标题', + admin: { + placeholder: '链接标题或商品名称', + }, + }, + { + name: 'thumbnail', + type: 'text', + label: '🖼️ 缩略图 URL', + admin: { + placeholder: 'https://...', + description: '淘宝商品图片地址', + }, + }, + { + name: 'note', + type: 'textarea', + label: '📄 备注', + admin: { + placeholder: '其他备注信息...', + rows: 3, + }, + }, + { + type: 'ui', + name: 'linkPreview', + admin: { + components: { + Field: '/components/fields/TaobaoLinkPreview#TaobaoLinkPreview', + }, + }, + }, + ], + }, + ], + }, ], }, ], diff --git a/src/components/fields/PreorderOrdersField.tsx b/src/components/fields/PreorderOrdersField.tsx new file mode 100644 index 0000000..895cb1d --- /dev/null +++ b/src/components/fields/PreorderOrdersField.tsx @@ -0,0 +1,253 @@ +'use client' + +import { useField, useFormFields } from '@payloadcms/ui' +import React, { useEffect, useState } from 'react' + +interface Order { + id: string + display_id: number + status: string + payment_status: string + email: string + total: number + currency_code: string + created_at: string + items: Array<{ + title: string + quantity: number + unit_price: number + }> +} + +export const PreorderOrdersField: React.FC = () => { + const { value: medusaId } = useField({ path: 'medusaId' }) + const { value: seedId } = useField({ path: 'seedId' }) + + const [orders, setOrders] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [stats, setStats] = useState<{ total: number; totalAmount: number } | null>(null) + + useEffect(() => { + if (medusaId || seedId) { + fetchOrders() + } + }, [medusaId, seedId]) + + const fetchOrders = async () => { + const productId = seedId || medusaId + if (!productId) return + + try { + setLoading(true) + setError(null) + + const response = await fetch(`/api/preorders/${productId}/orders`) + + if (!response.ok) { + throw new Error('Failed to fetch orders') + } + + const data = await response.json() + setOrders(data.orders || []) + + // 计算统计数据 + const totalAmount = data.orders?.reduce((sum: number, order: Order) => sum + order.total, 0) || 0 + setStats({ + total: data.count || 0, + totalAmount, + }) + } catch (err: any) { + console.error('Failed to fetch orders:', err) + setError(err.message || 'Failed to load orders') + } finally { + setLoading(false) + } + } + + const formatCurrency = (amount: number, currency: string) => { + return new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency: currency.toUpperCase(), + }).format(amount / 100) + } + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) + } + + if (!medusaId && !seedId) { + return ( +
+

+ 产品尚未同步到 Medusa,无法查看订单 +

+
+ ) + } + + if (loading) { + return ( +
+

加载订单中...

+
+ ) + } + + if (error) { + return ( +
+

加载失败: {error}

+ +
+ ) + } + + if (orders.length === 0) { + return ( +
+

暂无订单

+ +
+ ) + } + + return ( +
+ {/* 统计信息 */} + {stats && ( +
+
+
订单总数
+
{stats.total}
+
+
+
订单总额
+
+ {formatCurrency(stats.totalAmount, orders[0]?.currency_code || 'CNY')} +
+
+
+ +
+
+ )} + + {/* 订单列表 */} +
+ + + + + + + + + + + + + {orders.map((order, index) => ( + + + + + + + + + ))} + +
订单号客户商品金额状态时间
+
#{order.display_id}
+
{order.id.slice(0, 8)}
+
+ {order.email} + + {order.items.map((item, i) => ( +
+ {item.title} × {item.quantity} +
+ ))} +
+ {formatCurrency(order.total, order.currency_code)} + + + {order.status} + + + {formatDate(order.created_at)} +
+
+
+ ) +} diff --git a/src/components/fields/TaobaoLinkPreview.tsx b/src/components/fields/TaobaoLinkPreview.tsx new file mode 100644 index 0000000..118ac40 --- /dev/null +++ b/src/components/fields/TaobaoLinkPreview.tsx @@ -0,0 +1,79 @@ +'use client' + +import { useField, useFormFields } from '@payloadcms/ui' +import React from 'react' + +/** + * 淘宝链接预览组件 + * 显示在每个淘宝链接数组项中 + */ +export const TaobaoLinkPreview: React.FC = () => { + const { value: url } = useField({ path: 'url' }) + const { value: thumbnail } = useField({ path: 'thumbnail' }) + + const openLink = () => { + if (url) { + window.open(url, '_blank', 'noopener,noreferrer') + } + } + + if (!url && !thumbnail) { + return null + } + + return ( +
+ {thumbnail && ( +
+
+ 预览: +
+ 淘宝商品预览 { + (e.target as HTMLImageElement).style.display = 'none' + }} + /> +
+ )} + + {url && ( + + )} +
+ ) +} diff --git a/src/components/sync/BatchSyncButton.tsx b/src/components/sync/BatchSyncButton.tsx deleted file mode 100644 index 005fa44..0000000 --- a/src/components/sync/BatchSyncButton.tsx +++ /dev/null @@ -1,128 +0,0 @@ -'use client' -import { useState } from 'react' -import { Button, useSelection } from '@payloadcms/ui' -import { useRouter } from 'next/navigation' - -/** - * 批量同步按钮组件 - * 用于同步选中的产品到 Medusa - */ -export function BatchSyncButton() { - const { getQueryParams, selectAll, toggleAll } = useSelection() - const [loading, setLoading] = useState(false) - const [message, setMessage] = useState('') - const router = useRouter() - - // 获取当前页面的 collection slug - const pathname = typeof window !== 'undefined' ? window.location.pathname : '' - const collectionSlug = pathname.includes('preorder-products') - ? 'preorder-products' - : 'products' - - const handleBatchSync = async (forceUpdate: boolean = false) => { - try { - const queryParams = getQueryParams() - - // 尝试从不同的位置获取选中的 IDs - let selectedIds: string[] = [] - - if (queryParams && typeof queryParams === 'object') { - // 尝试从 where 条件中获取 - const whereCondition = (queryParams as any).where - if (whereCondition?.id?.in) { - selectedIds = whereCondition.id.in - } - } - - if (!selectedIds || selectedIds.length === 0) { - setMessage('请先勾选要同步的商品(使用列表左侧的复选框)') - return - } - - if ( - forceUpdate && - !confirm(`确定要强制更新选中的 ${selectedIds.length} 个商品吗?这将覆盖本地修改。`) - ) { - return - } - - setLoading(true) - setMessage('') - - const response = await fetch('/api/batch-sync-medusa', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - ids: selectedIds, - collection: collectionSlug, - forceUpdate, - }), - }) - - const data = await response.json() - - if (data.success) { - setMessage(data.message || '批量同步成功!') - // 清除选择 - if (toggleAll) { - toggleAll() - } - // 刷新页面 - setTimeout(() => { - router.refresh() - }, 1500) - } else { - setMessage(`批量同步失败: ${data.error || '未知错误'}`) - } - } catch (error) { - setMessage(`批量同步出错: ${error instanceof Error ? error.message : '未知错误'}`) - } finally { - setLoading(false) - } - } - - return ( -
-

批量操作

- -
- - -
- - {message && ( -
- {message} -
- )} - -
-

- • 同步选中商品: 只更新选中商品的空字段 -

-

- • 强制更新选中商品: 覆盖选中商品的所有字段 -

-
-
- ) -} diff --git a/src/components/sync/ForceSyncButton.tsx b/src/components/sync/ForceSyncButton.tsx index 49e8eaf..2d93a50 100644 --- a/src/components/sync/ForceSyncButton.tsx +++ b/src/components/sync/ForceSyncButton.tsx @@ -14,7 +14,7 @@ export function ForceSyncButton() { const handleForceSync = async () => { if (!id) { - setMessage('无法获取商品 ID') + setMessage('❌ 无法获取商品 ID') return } @@ -36,31 +36,52 @@ export function ForceSyncButton() { const data = await response.json() if (data.success) { - setMessage(data.message || '强制同步成功!') + setMessage('✅ ' + (data.message || '强制同步成功!')) // 刷新页面显示更新后的数据 setTimeout(() => window.location.reload(), 1500) } else { - setMessage(`同步失败: ${data.error || data.message}`) + setMessage(`❌ 同步失败: ${data.error || data.message}`) } } catch (error) { - setMessage(`同步出错: ${error instanceof Error ? error.message : '未知错误'}`) + setMessage(`❌ 同步出错: ${error instanceof Error ? error.message : '未知错误'}`) } finally { setLoading(false) } } return ( -
- {message && (
)} +
+ 💡 此操作将从 Medusa 获取最新数据并覆盖当前商品信息 +
) } diff --git a/src/components/sync/SyncMedusaButton.tsx b/src/components/sync/SyncMedusaButton.tsx deleted file mode 100644 index 8d6bdeb..0000000 --- a/src/components/sync/SyncMedusaButton.tsx +++ /dev/null @@ -1,187 +0,0 @@ -'use client' -import { useState } from 'react' -import { Button } from '@payloadcms/ui' - -export function SyncMedusaButton() { - const [loading, setLoading] = useState(false) - const [message, setMessage] = useState('') - const [showConfirmInput, setShowConfirmInput] = useState(false) - const [confirmText, setConfirmText] = useState('') - - const handleSync = async () => { - setLoading(true) - setMessage('') - - try { - const response = await fetch('/api/sync-medusa?forceUpdate=false', { - method: 'GET', - }) - - const data = await response.json() - - if (data.success) { - setMessage(data.message || '同步成功!') - // 刷新页面显示新商品 - setTimeout(() => window.location.reload(), 1500) - } else { - setMessage(`同步失败: ${data.error || data.message || '未知错误'}`) - } - } catch (error) { - setMessage(`同步出错: ${error instanceof Error ? error.message : '未知错误'}`) - } finally { - setLoading(false) - } - } - - const handleForceUpdateAll = () => { - setShowConfirmInput(true) - setMessage('') - setConfirmText('') - } - - const handleConfirmForceUpdate = async () => { - if (confirmText !== 'FORCE_UPDATE_ALL') { - setMessage('确认字符不正确,请输入: FORCE_UPDATE_ALL') - return - } - - setLoading(true) - setMessage('') - setShowConfirmInput(false) - - try { - const response = await fetch('/api/sync-medusa?forceUpdate=true', { - method: 'GET', - }) - - const data = await response.json() - - if (data.success) { - setMessage(data.message || '强制更新成功!') - setTimeout(() => window.location.reload(), 1500) - } else { - setMessage(`同步失败: ${data.error || data.message || '未知错误'}`) - } - } catch (error) { - setMessage(`同步出错: ${error instanceof Error ? error.message : '未知错误'}`) - } finally { - setLoading(false) - setConfirmText('') - } - } - - const handleCancelForceUpdate = () => { - setShowConfirmInput(false) - setConfirmText('') - setMessage('') - } - - return ( -
-

Medusa 商品同步

- - {showConfirmInput ? ( -
-
-

- ⚠️ 危险操作 -

-

- 这将强制更新所有已存在的商品,覆盖所有本地修改。 -

-

- 请输入{' '} - - FORCE_UPDATE_ALL - {' '} - 确认: -

-
- setConfirmText(e.target.value)} - placeholder="输入 FORCE_UPDATE_ALL" - style={{ - width: '100%', - padding: '0.5rem', - marginBottom: '0.75rem', - border: '1px solid var(--theme-elevation-400)', - borderRadius: '4px', - fontSize: '0.875rem', - }} - disabled={loading} - /> -
- - -
-
- ) : ( -
- - -
- )} - - {message && ( -
- {message} -
- )} - - {!showConfirmInput && ( -
-

- • 同步新商品: 从 Medusa 导入尚未同步的商品,不会更新已存在的商品。 -

-

- • 强制更新全部: 更新所有商品,覆盖本地修改(需要确认)。 -

-
- )} -
- ) -} diff --git a/src/components/sync/UnifiedSyncButton.tsx b/src/components/sync/UnifiedSyncButton.tsx new file mode 100644 index 0000000..387ad9f --- /dev/null +++ b/src/components/sync/UnifiedSyncButton.tsx @@ -0,0 +1,316 @@ +'use client' +import { useState } from 'react' +import { Button, useSelection } from '@payloadcms/ui' +import { useRouter } from 'next/navigation' + +/** + * 统一的同步按钮组件 + * 整合所有同步功能,布局更紧凑 + */ +export function UnifiedSyncButton() { + const { getQueryParams, toggleAll } = useSelection() + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + const [showForceAllConfirm, setShowForceAllConfirm] = useState(false) + const [confirmText, setConfirmText] = useState('') + const router = useRouter() + + // 获取当前页面的 collection slug + const pathname = typeof window !== 'undefined' ? window.location.pathname : '' + const collectionSlug = pathname.includes('preorder-products') + ? 'preorder-products' + : 'products' + + // 同步新商品 + const handleSyncNew = async () => { + setLoading(true) + setMessage('') + + try { + const response = await fetch('/api/sync-medusa?forceUpdate=false', { + method: 'GET', + }) + + const data = await response.json() + + if (data.success) { + setMessage('✅ ' + (data.message || '同步成功!')) + setTimeout(() => window.location.reload(), 1500) + } else { + setMessage('❌ 同步失败: ' + (data.error || data.message || '未知错误')) + } + } catch (error) { + setMessage('❌ 同步出错: ' + (error instanceof Error ? error.message : '未知错误')) + } finally { + setLoading(false) + } + } + + // 批量同步选中 + const handleBatchSync = async (forceUpdate: boolean = false) => { + try { + const queryParams = getQueryParams() + let selectedIds: string[] = [] + + if (queryParams && typeof queryParams === 'object') { + const whereCondition = (queryParams as any).where + if (whereCondition?.id?.in) { + selectedIds = whereCondition.id.in + } + } + + if (!selectedIds || selectedIds.length === 0) { + setMessage('⚠️ 请先勾选要同步的商品(使用列表左侧的复选框)') + return + } + + if ( + forceUpdate && + !confirm(`确定要强制更新选中的 ${selectedIds.length} 个商品吗?这将覆盖本地修改。`) + ) { + return + } + + setLoading(true) + setMessage('') + + const response = await fetch('/api/batch-sync-medusa', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ids: selectedIds, + collection: collectionSlug, + forceUpdate, + }), + }) + + const data = await response.json() + + if (data.success) { + setMessage('✅ ' + (data.message || '批量同步成功!')) + if (toggleAll) { + toggleAll() + } + setTimeout(() => { + router.refresh() + }, 1500) + } else { + setMessage('❌ 批量同步失败: ' + (data.error || '未知错误')) + } + } catch (error) { + setMessage('❌ 批量同步出错: ' + (error instanceof Error ? error.message : '未知错误')) + } finally { + setLoading(false) + } + } + + // 强制更新全部 + const handleForceUpdateAll = async () => { + if (confirmText !== 'FORCE_UPDATE_ALL') { + setMessage('❌ 确认字符不正确,请输入: FORCE_UPDATE_ALL') + return + } + + setLoading(true) + setMessage('') + setShowForceAllConfirm(false) + + try { + const response = await fetch('/api/sync-medusa?forceUpdate=true', { + method: 'GET', + }) + + const data = await response.json() + + if (data.success) { + setMessage('✅ ' + (data.message || '强制更新成功!')) + setTimeout(() => window.location.reload(), 1500) + } else { + setMessage('❌ 同步失败: ' + (data.error || data.message || '未知错误')) + } + } catch (error) { + setMessage('❌ 同步出错: ' + (error instanceof Error ? error.message : '未知错误')) + } finally { + setLoading(false) + setConfirmText('') + } + } + + return ( +
+

+ 🔄 + Medusa 商品同步管理 +

+ + {showForceAllConfirm ? ( +
+
+

+ ⚠️ 危险操作 +

+

+ 这将强制更新所有已存在的商品,覆盖所有本地修改。 +

+

+ 请输入{' '} + + FORCE_UPDATE_ALL + {' '} + 确认: +

+
+ setConfirmText(e.target.value)} + placeholder="输入 FORCE_UPDATE_ALL" + style={{ + width: '100%', + padding: '0.5rem', + marginBottom: '0.75rem', + border: '1px solid var(--theme-elevation-400)', + borderRadius: '4px', + fontSize: '0.875rem', + }} + disabled={loading} + /> +
+ + +
+
+ ) : ( +
+ {/* 第一行:基础同步功能 */} + + + + + {/* 第二行:强制更新功能 */} + + + +
+ )} + + {message && ( +
+ {message} +
+ )} + + {!showForceAllConfirm && ( +
+
💡 功能说明:
+
+
+ 📥 同步新商品: 从 Medusa 导入尚未同步的商品 +
+
+ 🔄 同步选中商品: 只更新选中商品的空字段 +
+
+ ⚡ 强制更新选中: 覆盖选中商品的所有字段 +
+
+ 🔥 强制更新全部: 更新所有商品(需要确认) +
+
+
+ )} +
+ ) +} diff --git a/src/payload-types.ts b/src/payload-types.ts index 937aae2..c7066c3 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -255,6 +255,21 @@ export interface Product { } )[] | null; + /** + * 💡 管理淘宝采购链接(仅后台显示,不通过 API 暴露) + */ + taobaoLinks?: + | { + url: string; + title?: string | null; + /** + * 淘宝商品图片地址 + */ + thumbnail?: string | null; + note?: string | null; + id?: string | null; + }[] + | null; updatedAt: string; createdAt: string; } @@ -294,6 +309,30 @@ export interface PreorderProduct { * 上次同步时间 */ lastSyncedAt?: string | null; + /** + * 预购类型 + */ + preorderType: 'standard' | 'crowdfunding' | 'limited'; + /** + * 众筹目标数量(0 表示以变体 max_orders 总和为准) + */ + fundingGoal: number; + /** + * 预计发货日期 + */ + estimatedShipDate?: string | null; + /** + * 预购结束日期(可选) + */ + preorderEndDate?: string | null; + /** + * 最小起订量 + */ + minOrderQuantity?: number | null; + /** + * 最大购买数量(0 表示不限制) + */ + maxOrderQuantity?: number | null; /** * 预售商品的详细描述(支持富文本编辑) */ @@ -327,6 +366,21 @@ export interface PreorderProduct { } )[] | null; + /** + * 💡 管理淘宝采购链接(仅后台显示,不通过 API 暴露) + */ + taobaoLinks?: + | { + url: string; + title?: string | null; + /** + * 淘宝商品图片地址 + */ + thumbnail?: string | null; + note?: string | null; + id?: string | null; + }[] + | null; updatedAt: string; createdAt: string; } @@ -777,6 +831,15 @@ export interface ProductsSelect { lastSyncedAt?: T; content?: T; relatedProducts?: T; + taobaoLinks?: + | T + | { + url?: T; + title?: T; + thumbnail?: T; + note?: T; + id?: T; + }; updatedAt?: T; createdAt?: T; } @@ -792,8 +855,23 @@ export interface PreorderProductsSelect { handle?: T; thumbnail?: T; lastSyncedAt?: T; + preorderType?: T; + fundingGoal?: T; + estimatedShipDate?: T; + preorderEndDate?: T; + minOrderQuantity?: T; + maxOrderQuantity?: T; description?: T; relatedProducts?: T; + taobaoLinks?: + | T + | { + url?: T; + title?: T; + thumbnail?: T; + note?: T; + id?: T; + }; updatedAt?: T; createdAt?: T; }