精简同步按钮
This commit is contained in:
parent
1f78d88d10
commit
9a47af76ce
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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<string>({ path: 'medusaId' })
|
||||
const { value: seedId } = useField<string>({ path: 'seedId' })
|
||||
|
||||
const [orders, setOrders] = useState<Order[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div style={{ padding: '1rem', background: '#f5f5f5', borderRadius: '4px', marginBottom: '1rem' }}>
|
||||
<p style={{ margin: 0, color: '#666' }}>
|
||||
产品尚未同步到 Medusa,无法查看订单
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ padding: '1rem', background: '#f5f5f5', borderRadius: '4px', marginBottom: '1rem' }}>
|
||||
<p style={{ margin: 0 }}>加载订单中...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: '1rem', background: '#fee', borderRadius: '4px', marginBottom: '1rem', border: '1px solid #fcc' }}>
|
||||
<p style={{ margin: 0, color: '#c00' }}>加载失败: {error}</p>
|
||||
<button
|
||||
onClick={fetchOrders}
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
background: '#fff',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (orders.length === 0) {
|
||||
return (
|
||||
<div style={{ padding: '1rem', background: '#f5f5f5', borderRadius: '4px', marginBottom: '1rem' }}>
|
||||
<p style={{ margin: 0, color: '#666' }}>暂无订单</p>
|
||||
<button
|
||||
onClick={fetchOrders}
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '0.25rem 0.5rem',
|
||||
background: '#fff',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
{/* 统计信息 */}
|
||||
{stats && (
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
background: '#e3f2fd',
|
||||
borderRadius: '4px',
|
||||
marginBottom: '1rem',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: '1rem',
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#666', marginBottom: '0.25rem' }}>订单总数</div>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1976d2' }}>{stats.total}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#666', marginBottom: '0.25rem' }}>订单总额</div>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1976d2' }}>
|
||||
{formatCurrency(stats.totalAmount, orders[0]?.currency_code || 'CNY')}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||
<button
|
||||
onClick={fetchOrders}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
background: '#fff',
|
||||
border: '1px solid #1976d2',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
color: '#1976d2',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
🔄 刷新订单
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 订单列表 */}
|
||||
<div style={{
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#f5f5f5', borderBottom: '2px solid #e0e0e0' }}>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'left', fontSize: '0.875rem', fontWeight: 600 }}>订单号</th>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'left', fontSize: '0.875rem', fontWeight: 600 }}>客户</th>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'left', fontSize: '0.875rem', fontWeight: 600 }}>商品</th>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'right', fontSize: '0.875rem', fontWeight: 600 }}>金额</th>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'center', fontSize: '0.875rem', fontWeight: 600 }}>状态</th>
|
||||
<th style={{ padding: '0.75rem', textAlign: 'left', fontSize: '0.875rem', fontWeight: 600 }}>时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{orders.map((order, index) => (
|
||||
<tr
|
||||
key={order.id}
|
||||
style={{
|
||||
borderBottom: index < orders.length - 1 ? '1px solid #e0e0e0' : 'none',
|
||||
background: index % 2 === 0 ? '#fff' : '#fafafa',
|
||||
}}
|
||||
>
|
||||
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||||
<div style={{ fontWeight: 600 }}>#{order.display_id}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#999' }}>{order.id.slice(0, 8)}</div>
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||||
{order.email}
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||||
{order.items.map((item, i) => (
|
||||
<div key={i} style={{ marginBottom: i < order.items.length - 1 ? '0.25rem' : 0 }}>
|
||||
{item.title} × {item.quantity}
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem', fontSize: '0.875rem', textAlign: 'right', fontWeight: 600 }}>
|
||||
{formatCurrency(order.total, order.currency_code)}
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem', textAlign: 'center' }}>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
padding: '0.25rem 0.5rem',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
background: order.status === 'completed' ? '#e8f5e9' : '#fff3e0',
|
||||
color: order.status === 'completed' ? '#2e7d32' : '#f57c00',
|
||||
}}>
|
||||
{order.status}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||||
{formatDate(order.created_at)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<string>({ path: 'url' })
|
||||
const { value: thumbnail } = useField<string>({ path: 'thumbnail' })
|
||||
|
||||
const openLink = () => {
|
||||
if (url) {
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}
|
||||
|
||||
if (!url && !thumbnail) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '0.75rem',
|
||||
background: '#f7f9fb',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #e5e7eb',
|
||||
}}
|
||||
>
|
||||
{thumbnail && (
|
||||
<div style={{ marginBottom: '0.75rem' }}>
|
||||
<div style={{ fontSize: '0.75rem', color: '#666', marginBottom: '0.25rem' }}>
|
||||
预览:
|
||||
</div>
|
||||
<img
|
||||
src={thumbnail}
|
||||
alt="淘宝商品预览"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '200px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #e0e0e0',
|
||||
}}
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{url && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openLink}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
background: '#ff6700',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
width: 'fit-content',
|
||||
}}
|
||||
>
|
||||
🔗 打开淘宝链接
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div style={{ padding: '1rem', borderTop: '1px solid var(--theme-elevation-100)' }}>
|
||||
<h3 style={{ marginBottom: '1rem' }}>批量操作</h3>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.75rem', marginBottom: '1rem', flexWrap: 'wrap' }}>
|
||||
<Button onClick={() => handleBatchSync(false)} disabled={loading} buttonStyle="secondary">
|
||||
{loading ? '同步中...' : '同步选中商品'}
|
||||
</Button>
|
||||
<Button onClick={() => handleBatchSync(true)} disabled={loading} buttonStyle="secondary">
|
||||
{loading ? '强制更新中...' : '强制更新选中商品'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.75rem',
|
||||
backgroundColor: message.includes('失败') || message.includes('出错')
|
||||
? 'var(--theme-error-50)'
|
||||
: message.includes('请先选择')
|
||||
? 'var(--theme-warning-50)'
|
||||
: 'var(--theme-success-50)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{ marginTop: '1rem', fontSize: '0.875rem', color: 'var(--theme-elevation-400)' }}
|
||||
>
|
||||
<p style={{ marginBottom: '0.5rem' }}>
|
||||
• <strong>同步选中商品</strong>: 只更新选中商品的空字段
|
||||
</p>
|
||||
<p style={{ margin: 0 }}>
|
||||
• <strong>强制更新选中商品</strong>: 覆盖选中商品的所有字段
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<Button onClick={handleForceSync} disabled={loading} buttonStyle="secondary">
|
||||
{loading ? '同步中...' : '从 Medusa 强制更新'}
|
||||
<div style={{
|
||||
marginTop: '1rem',
|
||||
padding: '1rem',
|
||||
background: 'var(--theme-elevation-50)',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid var(--theme-elevation-100)',
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
marginBottom: '0.75rem',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
<span style={{ fontSize: '1.25rem' }}>🔄</span>
|
||||
从 Medusa 同步数据
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleForceSync}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
>
|
||||
⚡ {loading ? '同步中...' : '強制更新此商品'}
|
||||
</Button>
|
||||
{message && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
marginTop: '0.75rem',
|
||||
padding: '0.75rem',
|
||||
backgroundColor:
|
||||
message.includes('失败') || message.includes('出错')
|
||||
message.includes('❌')
|
||||
? 'var(--theme-error-50)'
|
||||
: 'var(--theme-success-50)',
|
||||
borderRadius: '4px',
|
||||
|
|
@ -70,6 +91,13 @@ export function ForceSyncButton() {
|
|||
{message}
|
||||
</div>
|
||||
)}
|
||||
<div style={{
|
||||
marginTop: '0.75rem',
|
||||
fontSize: '0.75rem',
|
||||
color: 'var(--theme-elevation-400)',
|
||||
}}>
|
||||
💡 此操作将从 Medusa 获取最新数据并覆盖当前商品信息
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div style={{ padding: '1rem', borderTop: '1px solid var(--theme-elevation-100)' }}>
|
||||
<h3 style={{ marginBottom: '1rem' }}>Medusa 商品同步</h3>
|
||||
|
||||
{showConfirmInput ? (
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '0.75rem',
|
||||
padding: '0.75rem',
|
||||
backgroundColor: 'var(--theme-warning-50)',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
margin: '0 0 0.5rem 0',
|
||||
fontWeight: 'bold',
|
||||
color: 'var(--theme-warning-900)',
|
||||
}}
|
||||
>
|
||||
⚠️ 危险操作
|
||||
</p>
|
||||
<p style={{ margin: '0 0 0.5rem 0', fontSize: '0.875rem' }}>
|
||||
这将强制更新所有已存在的商品,覆盖所有本地修改。
|
||||
</p>
|
||||
<p style={{ margin: 0, fontSize: '0.875rem' }}>
|
||||
请输入{' '}
|
||||
<code
|
||||
style={{
|
||||
padding: '0.125rem 0.25rem',
|
||||
backgroundColor: 'var(--theme-elevation-100)',
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
>
|
||||
FORCE_UPDATE_ALL
|
||||
</code>{' '}
|
||||
确认:
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={confirmText}
|
||||
onChange={(e) => 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}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<Button
|
||||
onClick={handleConfirmForceUpdate}
|
||||
disabled={loading || confirmText !== 'FORCE_UPDATE_ALL'}
|
||||
>
|
||||
{loading ? '更新中...' : '确认强制更新'}
|
||||
</Button>
|
||||
<Button onClick={handleCancelForceUpdate} disabled={loading} buttonStyle="secondary">
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', gap: '0.75rem', marginBottom: '1rem' }}>
|
||||
<Button onClick={handleSync} disabled={loading}>
|
||||
{loading ? '同步中...' : '同步新商品'}
|
||||
</Button>
|
||||
<Button onClick={handleForceUpdateAll} disabled={loading} buttonStyle="secondary">
|
||||
强制更新全部
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.75rem',
|
||||
backgroundColor:
|
||||
message.includes('失败') || message.includes('出错') || message.includes('不正确')
|
||||
? 'var(--theme-error-50)'
|
||||
: 'var(--theme-success-50)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showConfirmInput && (
|
||||
<div
|
||||
style={{ marginTop: '1rem', fontSize: '0.875rem', color: 'var(--theme-elevation-400)' }}
|
||||
>
|
||||
<p style={{ marginBottom: '0.5rem' }}>
|
||||
• <strong>同步新商品</strong>: 从 Medusa 导入尚未同步的商品,不会更新已存在的商品。
|
||||
</p>
|
||||
<p style={{ margin: 0 }}>
|
||||
• <strong>强制更新全部</strong>: 更新所有商品,覆盖本地修改(需要确认)。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div style={{ padding: '1rem', borderTop: '1px solid var(--theme-elevation-100)' }}>
|
||||
<h3 style={{ marginBottom: '1rem', fontSize: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
<span style={{ fontSize: '1.25rem' }}>🔄</span>
|
||||
Medusa 商品同步管理
|
||||
</h3>
|
||||
|
||||
{showForceAllConfirm ? (
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '0.75rem',
|
||||
padding: '0.75rem',
|
||||
backgroundColor: 'var(--theme-warning-50)',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
margin: '0 0 0.5rem 0',
|
||||
fontWeight: 'bold',
|
||||
color: 'var(--theme-warning-900)',
|
||||
}}
|
||||
>
|
||||
⚠️ 危险操作
|
||||
</p>
|
||||
<p style={{ margin: '0 0 0.5rem 0', fontSize: '0.875rem' }}>
|
||||
这将强制更新所有已存在的商品,覆盖所有本地修改。
|
||||
</p>
|
||||
<p style={{ margin: 0, fontSize: '0.875rem' }}>
|
||||
请输入{' '}
|
||||
<code
|
||||
style={{
|
||||
padding: '0.125rem 0.25rem',
|
||||
backgroundColor: 'var(--theme-elevation-100)',
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
>
|
||||
FORCE_UPDATE_ALL
|
||||
</code>{' '}
|
||||
确认:
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={confirmText}
|
||||
onChange={(e) => 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}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<Button
|
||||
onClick={handleForceUpdateAll}
|
||||
disabled={loading || confirmText !== 'FORCE_UPDATE_ALL'}
|
||||
>
|
||||
{loading ? '更新中...' : '✅ 确认强制更新'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowForceAllConfirm(false)
|
||||
setConfirmText('')
|
||||
setMessage('')
|
||||
}}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
>
|
||||
❌ 取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: '0.75rem',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
{/* 第一行:基础同步功能 */}
|
||||
<Button
|
||||
onClick={handleSyncNew}
|
||||
disabled={loading}
|
||||
buttonStyle="primary"
|
||||
>
|
||||
📥 {loading ? '同步中...' : '同步新商品'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => handleBatchSync(false)}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
>
|
||||
🔄 {loading ? '同步中...' : '同步选中商品'}
|
||||
</Button>
|
||||
|
||||
{/* 第二行:强制更新功能 */}
|
||||
<Button
|
||||
onClick={() => handleBatchSync(true)}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
>
|
||||
⚡ {loading ? '更新中...' : '强制更新选中'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowForceAllConfirm(true)
|
||||
setMessage('')
|
||||
setConfirmText('')
|
||||
}}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
>
|
||||
🔥 强制更新全部
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.75rem',
|
||||
backgroundColor: message.includes('❌')
|
||||
? 'var(--theme-error-50)'
|
||||
: message.includes('⚠️')
|
||||
? 'var(--theme-warning-50)'
|
||||
: 'var(--theme-success-50)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.875rem',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showForceAllConfirm && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '0.75rem',
|
||||
color: 'var(--theme-elevation-400)',
|
||||
lineHeight: '1.5',
|
||||
background: 'var(--theme-elevation-50)',
|
||||
padding: '0.75rem',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: '0.5rem', fontWeight: 600 }}>💡 功能说明:</div>
|
||||
<div style={{ display: 'grid', gap: '0.25rem' }}>
|
||||
<div>
|
||||
<strong>📥 同步新商品</strong>: 从 Medusa 导入尚未同步的商品
|
||||
</div>
|
||||
<div>
|
||||
<strong>🔄 同步选中商品</strong>: 只更新选中商品的空字段
|
||||
</div>
|
||||
<div>
|
||||
<strong>⚡ 强制更新选中</strong>: 覆盖选中商品的所有字段
|
||||
</div>
|
||||
<div>
|
||||
<strong>🔥 强制更新全部</strong>: 更新所有商品(需要确认)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<T extends boolean = true> {
|
|||
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<T extends boolean = true> {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue