diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js
index 1a05ba9..ad479b2 100644
--- a/src/app/(payload)/admin/importMap.js
+++ b/src/app/(payload)/admin/importMap.js
@@ -22,16 +22,14 @@ import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField'
-import { TaobaoSyncButtons as TaobaoSyncButtons_1287e89ff664e3153e7b1d531ac3c868 } from '../../../components/sync/TaobaoSyncButtons'
+import { TaobaoProductSync as TaobaoProductSync_c920a85a41a3caf5464668c331ea204a } from '../../../components/sync/taobao/TaobaoProductSync'
import { TaobaoFetchButton as TaobaoFetchButton_6da2c7669760b5ece28f442df13318c7 } from '../../../components/fields/TaobaoFetchButton'
import { TaobaoLinkPreview as TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959 } from '../../../components/fields/TaobaoLinkPreview'
import { UnifiedSyncButton as UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523 } from '../../../components/sync/UnifiedSyncButton'
-import { TaobaoSyncAllButton as TaobaoSyncAllButton_e831fa632dca24f7a1678e011885f4da } from '../../../components/sync/TaobaoSyncAllButton'
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
import { PreorderProgressCell as PreorderProgressCell_67df47753573233f0c83480de687f13b } from '../../../components/cells/PreorderProgressCell'
import { RefreshOrderCountField as RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996 } from '../../../components/fields/RefreshOrderCountField'
import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f } from '../../../components/fields/PreorderOrdersField'
-import { PreorderHealthCheckButton as PreorderHealthCheckButton_5c0756e0fa67593931ce171329b92892 } from '../../../components/views/PreorderHealthCheckButton'
import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler'
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
@@ -68,16 +66,14 @@ export const importMap = {
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426,
- "/components/sync/TaobaoSyncButtons#TaobaoSyncButtons": TaobaoSyncButtons_1287e89ff664e3153e7b1d531ac3c868,
+ "/components/sync/taobao/TaobaoProductSync#TaobaoProductSync": TaobaoProductSync_c920a85a41a3caf5464668c331ea204a,
"/components/fields/TaobaoFetchButton#TaobaoFetchButton": TaobaoFetchButton_6da2c7669760b5ece28f442df13318c7,
"/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959,
"/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523,
- "/components/sync/TaobaoSyncAllButton#TaobaoSyncAllButton": TaobaoSyncAllButton_e831fa632dca24f7a1678e011885f4da,
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
"/components/cells/PreorderProgressCell#PreorderProgressCell": PreorderProgressCell_67df47753573233f0c83480de687f13b,
"/components/fields/RefreshOrderCountField#RefreshOrderCountField": RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996,
"/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f,
- "/components/views/PreorderHealthCheckButton#PreorderHealthCheckButton": PreorderHealthCheckButton_5c0756e0fa67593931ce171329b92892,
"/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5,
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
diff --git a/src/collections/PreorderProducts.ts b/src/collections/PreorderProducts.ts
index 15f0c63..1249d58 100644
--- a/src/collections/PreorderProducts.ts
+++ b/src/collections/PreorderProducts.ts
@@ -37,8 +37,6 @@ export const PreorderProducts: CollectionConfig = {
components: {
beforeListTable: [
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
- '/components/views/PreorderHealthCheckButton#PreorderHealthCheckButton',
- '/components/sync/TaobaoSyncAllButton#TaobaoSyncAllButton',
'/components/list/PreorderProductGridStyler#PreorderProductGridStyler',
],
},
@@ -250,7 +248,7 @@ export const PreorderProducts: CollectionConfig = {
name: 'taobaoSyncButtons',
admin: {
components: {
- Field: '/components/sync/TaobaoSyncButtons#TaobaoSyncButtons',
+ Field: '/components/sync/taobao/TaobaoProductSync#TaobaoProductSync',
},
},
},
diff --git a/src/collections/Products.ts b/src/collections/Products.ts
index ecd7e42..0feafd9 100644
--- a/src/collections/Products.ts
+++ b/src/collections/Products.ts
@@ -38,7 +38,6 @@ export const Products: CollectionConfig = {
components: {
beforeListTable: [
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
- '/components/sync/TaobaoSyncAllButton#TaobaoSyncAllButton',
'/components/list/ProductGridStyler',
],
},
@@ -127,7 +126,7 @@ export const Products: CollectionConfig = {
name: 'taobaoSyncButtons',
admin: {
components: {
- Field: '/components/sync/TaobaoSyncButtons#TaobaoSyncButtons',
+ Field: '/components/sync/taobao/TaobaoProductSync#TaobaoProductSync',
},
},
},
diff --git a/src/components/sync/RefreshOrderCountButton.tsx b/src/components/sync/RefreshOrderCountButton.tsx
deleted file mode 100644
index 431e215..0000000
--- a/src/components/sync/RefreshOrderCountButton.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-'use client'
-import { useState } from 'react'
-import { Button, useSelection } from '@payloadcms/ui'
-import { useRouter } from 'next/navigation'
-
-/**
- * 刷新预购商品订单计数按钮
- * 从 Medusa 拉取最新订单数据并更新 orderCount
- */
-export function RefreshOrderCountButton() {
- const { getQueryParams, toggleAll } = useSelection()
- const [loading, setLoading] = useState(false)
- const [message, setMessage] = useState('')
- const router = useRouter()
-
- // 刷新所有商品的订单计数
- const handleRefreshAll = async () => {
- if (!confirm('确定要刷新所有预购商品的订单计数吗?')) {
- return
- }
-
- setLoading(true)
- setMessage('')
-
- try {
- const response = await fetch('/api/preorders/refresh-order-counts', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- refreshAll: true,
- }),
- })
-
- const data = await response.json()
-
- if (data.success) {
- setMessage(`✅ ${data.message || '订单计数刷新成功!'}`)
- setTimeout(() => {
- router.refresh()
- }, 1500)
- } else {
- setMessage(`❌ 刷新失败: ${data.error || '未知错误'}`)
- }
- } catch (error) {
- setMessage('❌ 刷新出错: ' + (error instanceof Error ? error.message : '未知错误'))
- } finally {
- setLoading(false)
- }
- }
-
- // 刷新选中商品的订单计数
- const handleRefreshSelected = async () => {
- 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
- }
-
- setLoading(true)
- setMessage('')
-
- const response = await fetch('/api/preorders/refresh-order-counts', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- productIds: selectedIds,
- }),
- })
-
- 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}
-
- )}
-
-
-
- 💡 说明:订单计数 = 真实订单计数 + Fake计数
-
-
- • 真实订单计数:从 Medusa 订单系统同步,只读
-
-
- • Fake计数:可手动编辑,用于调整显示的进度
-
-
-
- )
-}
diff --git a/src/components/sync/TaobaoSyncAllButton.tsx b/src/components/sync/TaobaoSyncAllButton.tsx
deleted file mode 100644
index 387b270..0000000
--- a/src/components/sync/TaobaoSyncAllButton.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-'use client'
-
-import React, { useState } from 'react'
-
-/**
- * 列表页 — 淘宝全量同步按钮
- *
- * 加入 beforeListTable 后显示两个按钮:
- * 🔄 更新全部淘宝信息 → 仅填充空字段 (force=false)
- * ⚡ 强制更新全部淘宝信息 → 覆盖所有字段 (force=true,二次确认)
- *
- * 依赖 API:POST /api/admin/taobao/sync-all
- */
-export const TaobaoSyncAllButton: React.FC = () => {
- const [loadingNormal, setLoadingNormal] = useState(false)
- const [loadingForce, setLoadingForce] = useState(false)
- const [confirmForce, setConfirmForce] = useState(false)
- const [message, setMessage] = useState(null)
-
- const run = async (force: boolean) => {
- const setLoading = force ? setLoadingForce : setLoadingNormal
- setLoading(true)
- setMessage(null)
- setConfirmForce(false)
-
- try {
- const res = await fetch('/api/admin/taobao/sync-all', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ force }),
- })
- const data = await res.json()
-
- if (!data.success) throw new Error(data.error || '请求失败')
- setMessage(`✅ ${data.message}`)
- } catch (err: any) {
- setMessage(`❌ ${err?.message ?? '未知错误'}`)
- } finally {
- setLoading(false)
- }
- }
-
- const busy = loadingNormal || loadingForce
-
- return (
-
- {/* 更新(非强制) */}
-
-
- {/* 强制更新(二次确认) */}
- {!confirmForce ? (
-
- ) : (
- <>
-
- 确认覆盖所有字段?
-
-
-
- >
- )}
-
- {message && (
-
- {message}
-
- )}
-
- )
-}
diff --git a/src/components/sync/TaobaoSyncButtons.tsx b/src/components/sync/TaobaoSyncButtons.tsx
deleted file mode 100644
index d076978..0000000
--- a/src/components/sync/TaobaoSyncButtons.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-'use client'
-
-import React, { useState, useEffect } from 'react'
-import { useDocumentInfo } from '@payloadcms/ui'
-
-/**
- * 产品编辑页 — 淘宝信息同步按钮
- *
- * 放置在淘宝链接 Tab 顶部(UI 字段),显示两个操作按钮:
- * 🔄 更新淘宝信息 → 仅填充空字段 (force=false)
- * ⚡ 强制更新淘宝信息 → 覆盖所有字段 (force=true)
- *
- * 依赖 API:POST /api/admin/taobao/sync-product
- */
-export const TaobaoSyncButtons: React.FC = () => {
- const { id, collectionSlug } = useDocumentInfo()
-
- const [loadingNormal, setLoadingNormal] = useState(false)
- const [loadingForce, setLoadingForce] = useState(false)
- const [message, setMessage] = useState(null)
-
- if (!id) return null // 新建文档时不显示
-
- const isValidCollection =
- collectionSlug === 'products' || collectionSlug === 'preorder-products'
- if (!isValidCollection) return null
-
- const run = async (force: boolean) => {
- const setLoading = force ? setLoadingForce : setLoadingNormal
- setLoading(true)
- setMessage(null)
-
- try {
- const res = await fetch('/api/admin/taobao/sync-product', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ productId: id, collection: collectionSlug, force }),
- })
- const data = await res.json()
-
- if (!data.success) throw new Error(data.error || '请求失败')
- setMessage(`✅ ${data.message || '完成'}`)
-
- // 刷新页面以显示更新后的字段值
- setTimeout(() => window.location.reload(), 1200)
- } catch (err: any) {
- setMessage(`❌ ${err?.message ?? '未知错误'}`)
- } finally {
- setLoading(false)
- }
- }
-
- const busy = loadingNormal || loadingForce
-
- return (
-
-
- 淘宝自动解析
-
-
-
- {/* 更新(非强制) */}
-
-
- {/* 强制全量更新 */}
-
-
-
- {/* 说明文字 */}
-
- 🔄 更新:仅填充空白字段(标题、封面、价格)
- ⚡ 强制更新:覆盖已有字段
-
-
- {message && (
-
- {message}
-
- )}
-
- )
-}
diff --git a/src/components/sync/UnifiedSyncButton.tsx b/src/components/sync/UnifiedSyncButton.tsx
index 9706593..da28c00 100644
--- a/src/components/sync/UnifiedSyncButton.tsx
+++ b/src/components/sync/UnifiedSyncButton.tsx
@@ -1,321 +1,480 @@
'use client'
-import { useState, useEffect } from 'react'
-import { Button, useSelection } from '@payloadcms/ui'
+import React, { useEffect, useState } from 'react'
+import { Button, Modal, useSelection } from '@payloadcms/ui'
import { useRouter } from 'next/navigation'
-/**
- * 统一的同步按钮组件
- * 整合所有同步功能,布局更紧凑
- */
-export function UnifiedSyncButton() {
+// ── Types ─────────────────────────────────────────────────────────────────────
+
+interface HealthCheckResult {
+ success: boolean
+ timestamp: string
+ summary: { total: number; healthy: number; warnings: number; errors: number }
+ products: Array<{
+ id: string
+ title: string
+ medusaId: string
+ seedId: string
+ status: string
+ severity: 'healthy' | 'warning' | 'error'
+ issues: string[]
+ stats: {
+ orderCount: number
+ fakeOrderCount: number
+ totalDisplayCount: number
+ fundingGoal: number
+ completionPercentage: number
+ }
+ dates: { preorderStartDate: string | null; preorderEndDate: string | null }
+ }>
+ issues: string[]
+}
+
+// ── Shared helpers ────────────────────────────────────────────────────────────
+
+function Msg({ text }: { text: string }) {
+ if (!text) return null
+ const isErr = text.startsWith('❌')
+ const isWarn = text.startsWith('⚠️')
+ return (
+
+ {text}
+
+ )
+}
+
+function Divider() {
+ return
+}
+
+function SectionLabel({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
+
+// ── Section: Medusa sync ──────────────────────────────────────────────────────
+
+function MedusaSyncSection({ collection }: { collection: string }) {
const { getQueryParams, toggleAll } = useSelection()
- const [loading, setLoading] = useState(false)
- const [message, setMessage] = useState('')
- const [showForceAllConfirm, setShowForceAllConfirm] = useState(false)
- const [confirmText, setConfirmText] = useState('')
- const [collectionSlug, setCollectionSlug] = useState('products')
const router = useRouter()
- // 在客户端确定 collection slug,避免 hydration 错误
- useEffect(() => {
- if (typeof window !== 'undefined') {
- const pathname = window.location.pathname
- const slug = pathname.includes('preorder-products')
- ? 'preorder-products'
- : 'products'
- setCollectionSlug(slug)
- }
- }, [])
+ const [loadingNew, setLoadingNew] = useState(false)
+ const [loadingBatch, setLoadingBatch] = useState(false)
+ const [loadingForceBatch, setLoadingForceBatch] = useState(false)
+ const [showForceAll, setShowForceAll] = useState(false)
+ const [loadingForceAll, setLoadingForceAll] = useState(false)
+ const [confirmText, setConfirmText] = useState('')
+ const [msg, setMsg] = useState('')
- // 同步新商品
- const handleSyncNew = async () => {
- setLoading(true)
- setMessage('')
+ const busy = loadingNew || loadingBatch || loadingForceBatch || loadingForceAll
+ const syncNew = async () => {
+ setLoadingNew(true); setMsg('')
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 res = await fetch('/api/sync/medusa?forceUpdate=false')
+ const data = await res.json()
+ setMsg(data.success
+ ? `✅ ${data.message || '同步成功'}`
+ : `❌ ${data.error || data.message || '同步失败'}`)
+ if (data.success) setTimeout(() => window.location.reload(), 1500)
+ } catch (e: any) { setMsg(`❌ ${e?.message ?? '未知错误'}`) }
+ finally { setLoadingNew(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/admin/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 batchSync = async (force: boolean) => {
+ const queryParams = getQueryParams()
+ let ids: string[] = []
+ if (queryParams && typeof queryParams === 'object') {
+ const where = (queryParams as any).where
+ if (where?.id?.in) ids = where.id.in
}
- }
-
- // 强制更新全部
- const handleForceUpdateAll = async () => {
- if (confirmText !== 'FORCE_UPDATE_ALL') {
- setMessage('❌ 确认字符不正确,请输入: FORCE_UPDATE_ALL')
+ if (!ids.length) {
+ setMsg('⚠️ 请先勾选要同步的商品(列表左侧复选框)')
return
}
-
- setLoading(true)
- setMessage('')
- setShowForceAllConfirm(false)
-
+ if (force && !confirm(`确定强制更新选中的 ${ids.length} 个商品?这将覆盖本地修改。`)) return
+ const setL = force ? setLoadingForceBatch : setLoadingBatch
+ setL(true); setMsg('')
try {
- const response = await fetch('/api/sync/medusa?forceUpdate=true', {
- method: 'GET',
+ const res = await fetch('/api/admin/batch-sync-medusa', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ids, collection, forceUpdate: force }),
})
+ const data = await res.json()
+ setMsg(data.success
+ ? `✅ ${data.message || '批量同步成功'}`
+ : `❌ ${data.error || '失败'}`)
+ if (data.success) { toggleAll?.(); setTimeout(() => router.refresh(), 1500) }
+ } catch (e: any) { setMsg(`❌ ${e?.message ?? '未知错误'}`) }
+ finally { setL(false) }
+ }
- 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 forceAll = async () => {
+ if (confirmText !== 'FORCE_UPDATE_ALL') {
+ setMsg('❌ 请输入: FORCE_UPDATE_ALL')
+ return
}
+ setLoadingForceAll(true); setMsg(''); setShowForceAll(false)
+ try {
+ const res = await fetch('/api/sync/medusa?forceUpdate=true')
+ const data = await res.json()
+ setMsg(data.success
+ ? `✅ ${data.message || '强制更新成功'}`
+ : `❌ ${data.error || data.message || '失败'}`)
+ if (data.success) setTimeout(() => window.location.reload(), 1500)
+ } catch (e: any) { setMsg(`❌ ${e?.message ?? '未知错误'}`) }
+ finally { setLoadingForceAll(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}
- />
-
-
-
-
-
- ) : (
-
- {/* 第一行:基础同步功能 */}
+
+
🔄 Medusa 商品同步
+
+
+
+
+ {!showForceAll ? (
-
-
-
- {/* 第二行:强制更新功能 */}
-
-
-
-
- )}
+ ) : (
+
+ setConfirmText(e.target.value)}
+ placeholder="输入 FORCE_UPDATE_ALL 确认"
+ disabled={loadingForceAll}
+ style={{
+ padding: '0.3rem 0.5rem',
+ border: '1px solid var(--theme-elevation-400)',
+ borderRadius: '4px',
+ fontSize: '0.78rem',
+ width: '200px',
+ }}
+ />
+
+
+
+ )}
+
+
+
+ )
+}
- {message && (
-
- {message}
-
- )}
+// ── Section: Taobao sync ──────────────────────────────────────────────────────
- {!showForceAllConfirm && (
-
-
💡 功能说明:
-
-
-
📥 同步新商品: 从 Medusa 导入尚未同步的商品
+function TaobaoSyncSection() {
+ const [loadingNormal, setLoadingNormal] = useState(false)
+ const [loadingForce, setLoadingForce] = useState(false)
+ const [confirmForce, setConfirmForce] = useState(false)
+ const [msg, setMsg] = useState('')
+ const busy = loadingNormal || loadingForce
+
+ const run = async (force: boolean) => {
+ const setL = force ? setLoadingForce : setLoadingNormal
+ setL(true); setMsg(''); setConfirmForce(false)
+ try {
+ const res = await fetch('/api/admin/taobao/sync-all', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ force }),
+ })
+ const data = await res.json()
+ if (!data.success) throw new Error(data.error || '请求失败')
+ setMsg(`✅ ${data.message}`)
+ } catch (e: any) { setMsg(`❌ ${e?.message ?? '未知错误'}`) }
+ finally { setL(false) }
+ }
+
+ return (
+
+
🛍️ 淘宝信息同步
+
+
+ {!confirmForce ? (
+
+ ) : (
+
+
+ 确认覆盖所有字段?
+
+
+
+
+ )}
+
+
+
+ )
+}
+
+// ── Section: Preorder management ──────────────────────────────────────────────
+
+function PreorderSection() {
+ const { getQueryParams, toggleAll } = useSelection()
+ const router = useRouter()
+
+ const [hcLoading, setHcLoading] = useState(false)
+ const [hcError, setHcError] = useState
(null)
+ const [hcResult, setHcResult] = useState(null)
+ const [hcOpen, setHcOpen] = useState(false)
+ const [rcLoading, setRcLoading] = useState(false)
+ const [rcMsg, setRcMsg] = useState('')
+
+ const runHealthCheck = async () => {
+ setHcLoading(true); setHcError(null)
+ try {
+ const res = await fetch('/api/preorders/health-check')
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
+ const data = await res.json()
+ setHcResult(data); setHcOpen(true)
+ } catch (e: any) { setHcError(e.message || '健康检查失败') }
+ finally { setHcLoading(false) }
+ }
+
+ const refreshSelected = async () => {
+ const queryParams = getQueryParams()
+ let ids: string[] = []
+ if (queryParams && typeof queryParams === 'object') {
+ const where = (queryParams as any).where
+ if (where?.id?.in) ids = where.id.in
+ }
+ if (!ids.length) {
+ setRcMsg('⚠️ 请先勾选要刷新的商品(列表左侧复选框)')
+ return
+ }
+ setRcLoading(true); setRcMsg('')
+ try {
+ const res = await fetch('/api/preorders/refresh-order-counts', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ productIds: ids }),
+ })
+ const data = await res.json()
+ setRcMsg(data.success
+ ? `✅ ${data.message || '刷新成功'}`
+ : `❌ ${data.error || '失败'}`)
+ if (data.success) { toggleAll?.(); setTimeout(() => router.refresh(), 1500) }
+ } catch (e: any) { setRcMsg(`❌ ${e?.message ?? '未知错误'}`) }
+ finally { setRcLoading(false) }
+ }
+
+ const refreshAll = async () => {
+ if (!confirm('确定要刷新所有预购商品的订单计数吗?')) return
+ setRcLoading(true); setRcMsg('')
+ try {
+ const res = await fetch('/api/preorders/refresh-order-counts', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ refreshAll: true }),
+ })
+ const data = await res.json()
+ setRcMsg(data.success
+ ? `✅ ${data.message || '刷新成功'}`
+ : `❌ ${data.error || '失败'}`)
+ if (data.success) setTimeout(() => router.refresh(), 1500)
+ } catch (e: any) { setRcMsg(`❌ ${e?.message ?? '未知错误'}`) }
+ finally { setRcLoading(false) }
+ }
+
+ const severityIcon = (s: string) =>
+ ({ error: '❌', warning: '⚠️', healthy: '✅' } as Record)[s] ?? 'ℹ️'
+
+ const fmtDate = (d: string | null) => {
+ if (!d) return 'N/A'
+ try {
+ return new Date(d).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' })
+ } catch { return d }
+ }
+
+ return (
+
+
📦 预购管理
+
+
+
+
+
+ {hcError &&
}
+
+
+ {hcOpen && hcResult && (
+
setHcOpen(false)}>
+
+
+ 预购产品健康检查
+
+
+ {([
+ { label: '总数', value: hcResult.summary.total, bg: '#EFF6FF', border: '#BFDBFE', color: '#1E40AF' },
+ { label: '健康', value: hcResult.summary.healthy, bg: '#F0FDF4', border: '#BBF7D0', color: '#15803D' },
+ { label: '警告', value: hcResult.summary.warnings, bg: '#FEFCE8', border: '#FDE047', color: '#A16207' },
+ { label: '错误', value: hcResult.summary.errors, bg: '#FEF2F2', border: '#FECACA', color: '#B91C1C' },
+ ] as const).map(({ label, value, bg, border, color }) => (
+
+ ))}
-
-
🔄 同步选中商品: 只更新选中商品的空字段
+
+ 检查时间: {new Date(hcResult.timestamp).toLocaleString('zh-CN')}
+
+
+ {hcResult.products.map((p) => {
+ const borderColor = p.severity === 'error' ? '#FCA5A5' : p.severity === 'warning' ? '#FCD34D' : '#86EFAC'
+ const bgColor = p.severity === 'error' ? '#FEF2F2' : p.severity === 'warning' ? '#FEFCE8' : '#F0FDF4'
+ return (
+
+
+
+
+ {severityIcon(p.severity)}
+ {p.title}
+ {p.status}
+
+
Medusa ID: {p.medusaId}
+
+
+
进度 {p.stats.completionPercentage}%
+
{p.stats.totalDisplayCount} / {p.stats.fundingGoal}
+
+
+
+ 开始: {fmtDate(p.dates.preorderStartDate)}
+ 结束: {fmtDate(p.dates.preorderEndDate)}
+
+ {p.issues.length > 0 && (
+
+ {p.issues.map((issue, i) => - {issue}
)}
+
+ )}
+
+ )
+ })}
+ {hcResult.products.length === 0 && (
+
没有找到预购产品
+ )}
-
- ⚡ 强制更新选中: 覆盖选中商品的所有字段
-
-
-
🔥 强制更新全部: 更新所有商品(需要确认)
+
+
-
+
+ )}
+
+ )
+}
+
+// ── Root export ───────────────────────────────────────────────────────────────
+
+/**
+ * 统一同步面板 — 列表页 beforeListTable
+ * 包含:Medusa 商品同步 / 淡宝信息同步 / 预购管理(仅 preorder-products)
+ */
+export function UnifiedSyncButton() {
+ const [isPreorder, setIsPreorder] = useState(false)
+ const [collection, setCollection] = useState('products')
+
+ useEffect(() => {
+ if (typeof window !== 'undefined') {
+ const isP = window.location.pathname.includes('preorder-products')
+ setIsPreorder(isP)
+ setCollection(isP ? 'preorder-products' : 'products')
+ }
+ }, [])
+
+ return (
+
+
+
+
+ {isPreorder && (
+ <>
+
+
+ >
)}
)
diff --git a/src/components/sync/ResetDataButton.tsx b/src/components/sync/admin/ResetData.tsx
similarity index 50%
rename from src/components/sync/ResetDataButton.tsx
rename to src/components/sync/admin/ResetData.tsx
index 4e585b8..19ecc95 100644
--- a/src/components/sync/ResetDataButton.tsx
+++ b/src/components/sync/admin/ResetData.tsx
@@ -2,25 +2,20 @@
import { useState } from 'react'
import { Button } from '@payloadcms/ui'
-interface Props {
- className?: string
-}
-
/**
- * Reset Data Button
- * 一键重置所有数据:清理 Payload + 清理 Medusa + Seed Medusa
- * 或仅重置 Medusa:清理 Medusa + Seed Medusa(不动 Payload)
+ * 数据重置按钮(全量 / 仅 Medusa)
+ * API: POST /api/admin/reset-data
*/
-export function ResetDataButton({ className }: Props) {
+export function ResetData() {
const [loading, setLoading] = useState<'full' | 'medusa-only' | null>(null)
const [message, setMessage] = useState('')
const [details, setDetails] = useState(null)
- const handleReset = async (mode: 'full' | 'medusa-only') => {
- const confirmMsg = mode === 'medusa-only'
- ? '⚠️ 重置 Medusa 数据\n\n此操作将:\n1. 清理所有 Medusa 数据\n2. 重新导入 Medusa seed 数据\n\nPayload CMS 数据不受影响。\n\n⚠️ 此操作不可撤销!确认继续吗?'
- : '⚠️ 危险操作:重置所有数据\n\n此操作将:\n1. 清理所有 Payload CMS 数据(保留用户)\n2. 清理所有 Medusa 数据\n3. 重新导入 Medusa seed 数据\n\n⚠️ 此操作不可撤销!确认要继续吗?'
-
+ const handle = async (mode: 'full' | 'medusa-only') => {
+ const confirmMsg =
+ mode === 'medusa-only'
+ ? '⚠️ 重置 Medusa 数据\n\n此操作将:\n1. 清理所有 Medusa 数据\n2. 重新导入 Medusa seed 数据\n\nPayload CMS 数据不受影响。\n\n⚠️ 此操作不可撤销!确认继续吗?'
+ : '⚠️ 危险操作:重置所有数据\n\n此操作将:\n1. 清理所有 Payload CMS 数据(保留用户)\n2. 清理所有 Medusa 数据\n3. 重新导入 Medusa seed 数据\n\n⚠️ 此操作不可撤销!确认要继续吗?'
if (!confirm(confirmMsg)) return
setLoading(mode)
@@ -28,53 +23,42 @@ export function ResetDataButton({ className }: Props) {
setDetails(null)
try {
- const response = await fetch('/api/admin/reset-data', {
+ const res = await fetch('/api/admin/reset-data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode }),
})
-
- const result = await response.json()
-
+ const result = await res.json()
if (!result.success) {
- // 优先显示顶级 error,否则找第一个失败步骤的错误
const stepError = result.steps?.find((s: any) => !s.success && !s.skipped && s.error)?.error
throw new Error(result.error || stepError || 'Reset failed')
}
-
setDetails(result)
setMessage(
mode === 'medusa-only'
? '✅ Medusa 数据重置完成!\n\n下一步:\n1. 同步 Medusa 商品到 Payload CMS'
- : '✅ 数据重置完成!\n\n下一步:\n1. 同步 Medusa 商品到 Payload CMS\n2. 设置 ProductRecommendations\n3. 配置 PreorderProducts 的预购设置'
+ : '✅ 数据重置完成!\n\n下一步:\n1. 同步 Medusa 商品到 Payload CMS\n2. 设置 ProductRecommendations\n3. 配置 PreorderProducts 的预购设置',
)
- } catch (error) {
- console.error('数据重置失败:', error)
- setMessage('❌ 重置失败: ' + (error instanceof Error ? error.message : 'Unknown error'))
+ } catch (err) {
+ setMessage('❌ 重置失败: ' + (err instanceof Error ? err.message : 'Unknown error'))
} finally {
setLoading(null)
}
}
- const handleResetData = () => handleReset('full')
- const handleResetMedusaOnly = () => handleReset('medusa-only')
+ const busy = loading !== null
return (
-
-
-