From 6e75c34fafa43db2661984ced10cf78b5589da29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=9F=E7=94=B7=E6=97=A5=E8=AE=B0=5Cwww?= Date: Mon, 23 Feb 2026 21:23:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E9=92=AE=E7=B2=BE=E7=AE=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(payload)/admin/importMap.js | 8 +- src/collections/PreorderProducts.ts | 4 +- src/collections/Products.ts | 3 +- .../sync/RefreshOrderCountButton.tsx | 167 ---- src/components/sync/TaobaoSyncAllButton.tsx | 146 ---- src/components/sync/TaobaoSyncButtons.tsx | 135 ---- src/components/sync/UnifiedSyncButton.tsx | 731 +++++++++++------- .../ResetData.tsx} | 90 +-- .../sync/medusa/BatchSyncProducts.tsx | 119 +++ src/components/sync/medusa/ForceUpdateAll.tsx | 152 ++++ .../sync/medusa/SyncNewProducts.tsx | 67 ++ src/components/sync/preorder/HealthCheck.tsx | 224 ++++++ .../sync/preorder/RefreshOrderCounts.tsx | 136 ++++ src/components/sync/taobao/TaobaoAllSync.tsx | 123 +++ .../sync/taobao/TaobaoProductSync.tsx | 121 +++ src/components/views/AdminPanel.tsx | 4 +- .../views/PreorderHealthCheckButton.tsx | 260 ------- 17 files changed, 1431 insertions(+), 1059 deletions(-) delete mode 100644 src/components/sync/RefreshOrderCountButton.tsx delete mode 100644 src/components/sync/TaobaoSyncAllButton.tsx delete mode 100644 src/components/sync/TaobaoSyncButtons.tsx rename src/components/sync/{ResetDataButton.tsx => admin/ResetData.tsx} (50%) create mode 100644 src/components/sync/medusa/BatchSyncProducts.tsx create mode 100644 src/components/sync/medusa/ForceUpdateAll.tsx create mode 100644 src/components/sync/medusa/SyncNewProducts.tsx create mode 100644 src/components/sync/preorder/HealthCheck.tsx create mode 100644 src/components/sync/preorder/RefreshOrderCounts.tsx create mode 100644 src/components/sync/taobao/TaobaoAllSync.tsx create mode 100644 src/components/sync/taobao/TaobaoProductSync.tsx delete mode 100644 src/components/views/PreorderHealthCheckButton.tsx 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 }) => ( +
+

{label}

+

{value}

+
+ ))}
-
- 🔄 同步选中商品: 只更新选中商品的空字段 +

+ 检查时间: {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 ( -
-
- @@ -83,33 +67,37 @@ export function ResetDataButton({ className }: Props) { {message && (
{message}
)} - {details && details.steps && ( + {details?.steps && (
-

详细信息:

- {details.steps.map((step: any, index: number) => ( -
+

详细信息:

+ {details.steps.map((step: any, i: number) => ( +
[{step.step}/3] {step.name}:{' '} @@ -117,9 +105,7 @@ export function ResetDataButton({ className }: Props) { {step.skipped ? '⏭️ 跳过' : step.success ? '✅ 成功' : '❌ 失败'} {step.deleted !== undefined && ( - - (删除 {step.deleted} 条记录) - + (删除 {step.deleted} 条记录) )}
))} diff --git a/src/components/sync/medusa/BatchSyncProducts.tsx b/src/components/sync/medusa/BatchSyncProducts.tsx new file mode 100644 index 0000000..899bbc3 --- /dev/null +++ b/src/components/sync/medusa/BatchSyncProducts.tsx @@ -0,0 +1,119 @@ +'use client' +import { useState, useEffect } from 'react' +import { Button, useSelection } from '@payloadcms/ui' +import { useRouter } from 'next/navigation' + +/** + * 批量同步选中商品(普通 / 强制覆盖) + * API: POST /api/admin/batch-sync-medusa + */ +export function BatchSyncProducts() { + const { getQueryParams, toggleAll } = useSelection() + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + const [collectionSlug, setCollectionSlug] = useState('products') + const router = useRouter() + + useEffect(() => { + if (typeof window !== 'undefined') { + setCollectionSlug( + window.location.pathname.includes('preorder-products') + ? 'preorder-products' + : 'products', + ) + } + }, []) + + const handle = async (forceUpdate: boolean) => { + const queryParams = getQueryParams() + let selectedIds: string[] = [] + if (queryParams && typeof queryParams === 'object') { + const where = (queryParams as any).where + if (where?.id?.in) selectedIds = where.id.in + } + + if (!selectedIds.length) { + setMessage('⚠️ 请先勾选要同步的商品(使用列表左侧的复选框)') + return + } + + if ( + forceUpdate && + !confirm(`确定要强制更新选中的 ${selectedIds.length} 个商品吗?这将覆盖本地修改。`) + ) + return + + setLoading(true) + setMessage('') + try { + const res = 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 res.json() + if (data.success) { + setMessage('✅ ' + (data.message || '批量同步成功!')) + toggleAll?.() + setTimeout(() => router.refresh(), 1500) + } else { + setMessage('❌ ' + (data.error || '批量同步失败')) + } + } catch (err) { + setMessage('❌ ' + (err instanceof Error ? err.message : '未知错误')) + } finally { + setLoading(false) + } + } + + return ( +
+
+ + +
+ {message && } +
+ ) +} + +function StatusMsg({ text }: { text: string }) { + const isError = text.startsWith('❌') + const isWarn = text.startsWith('⚠️') + return ( +
+ {text} +
+ ) +} diff --git a/src/components/sync/medusa/ForceUpdateAll.tsx b/src/components/sync/medusa/ForceUpdateAll.tsx new file mode 100644 index 0000000..efc3070 --- /dev/null +++ b/src/components/sync/medusa/ForceUpdateAll.tsx @@ -0,0 +1,152 @@ +'use client' +import { useState } from 'react' +import { Button } from '@payloadcms/ui' + +/** + * 强制全量更新所有 Medusa 商品(需输入确认码) + * API: GET /api/sync/medusa?forceUpdate=true + */ +export function ForceUpdateAll() { + const [showConfirm, setShowConfirm] = useState(false) + const [confirmText, setConfirmText] = useState('') + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + + const handle = async () => { + if (confirmText !== 'FORCE_UPDATE_ALL') { + setMessage('❌ 确认字符不正确,请输入: FORCE_UPDATE_ALL') + return + } + setLoading(true) + setMessage('') + setShowConfirm(false) + try { + const res = await fetch('/api/sync/medusa?forceUpdate=true') + const data = await res.json() + if (data.success) { + setMessage('✅ ' + (data.message || '强制更新成功!')) + setTimeout(() => window.location.reload(), 1500) + } else { + setMessage('❌ ' + (data.error || data.message || '更新失败')) + } + } catch (err) { + setMessage('❌ ' + (err instanceof Error ? err.message : '未知错误')) + } finally { + setLoading(false) + setConfirmText('') + } + } + + const cancel = () => { + setShowConfirm(false) + setConfirmText('') + setMessage('') + } + + if (showConfirm) { + return ( +
+
+

+ ⚠️ 危险操作 +

+

这将强制更新所有商品,覆盖所有本地修改。

+

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

+
+ setConfirmText(e.target.value)} + placeholder="输入 FORCE_UPDATE_ALL" + disabled={loading} + style={{ + width: '100%', + padding: '0.4rem 0.5rem', + marginBottom: '0.5rem', + border: '1px solid var(--theme-elevation-400)', + borderRadius: '4px', + fontSize: '0.875rem', + }} + /> +
+ + +
+ {message && } +
+ ) + } + + return ( +
+ + {message && } +
+ ) +} + +function StatusMsg({ text }: { text: string }) { + const isError = text.startsWith('❌') + const isWarn = text.startsWith('⚠️') + return ( +
+ {text} +
+ ) +} diff --git a/src/components/sync/medusa/SyncNewProducts.tsx b/src/components/sync/medusa/SyncNewProducts.tsx new file mode 100644 index 0000000..cee00a1 --- /dev/null +++ b/src/components/sync/medusa/SyncNewProducts.tsx @@ -0,0 +1,67 @@ +'use client' +import { useState } from 'react' +import { Button } from '@payloadcms/ui' + +/** + * 从 Medusa 同步尚未导入的新商品 + * API: GET /api/sync/medusa?forceUpdate=false + */ +export function SyncNewProducts() { + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + + const handle = async () => { + setLoading(true) + setMessage('') + try { + const res = await fetch('/api/sync/medusa?forceUpdate=false') + const data = await res.json() + if (data.success) { + setMessage('✅ ' + (data.message || '同步成功!')) + setTimeout(() => window.location.reload(), 1500) + } else { + setMessage('❌ ' + (data.error || data.message || '同步失败')) + } + } catch (err) { + setMessage('❌ ' + (err instanceof Error ? err.message : '未知错误')) + } finally { + setLoading(false) + } + } + + return ( +
+ + {message && } +
+ ) +} + +function StatusMsg({ text }: { text: string }) { + const isError = text.startsWith('❌') + const isWarn = text.startsWith('⚠️') + return ( +
+ {text} +
+ ) +} diff --git a/src/components/sync/preorder/HealthCheck.tsx b/src/components/sync/preorder/HealthCheck.tsx new file mode 100644 index 0000000..4580960 --- /dev/null +++ b/src/components/sync/preorder/HealthCheck.tsx @@ -0,0 +1,224 @@ +'use client' +import React, { useState } from 'react' +import { Button, Modal } from '@payloadcms/ui' + +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[] +} + +/** + * 预购产品健康检查按钮(含结果弹窗) + * API: GET /api/preorders/health-check + */ +export function HealthCheck() { + const [isOpen, setIsOpen] = useState(false) + const [loading, setLoading] = useState(false) + const [result, setResult] = useState(null) + const [error, setError] = useState(null) + + const run = async () => { + setLoading(true) + setError(null) + try { + const res = await fetch('/api/preorders/health-check') + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data = await res.json() + setResult(data) + setIsOpen(true) + } catch (err: any) { + setError(err.message || '健康检查失败') + } finally { + setLoading(false) + } + } + + const severityIcon = (s: string) => + ({ error: '❌', warning: '⚠️', healthy: '✅' })[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 ( + <> +
+ + {error && ❌ {error}} +
+ + {isOpen && result && ( + setIsOpen(false)}> +
+

+ 预购产品健康检查 +

+ + {/* 概览 */} +
+ {( + [ + { label: '总数', value: result.summary.total, bg: '#EFF6FF', border: '#BFDBFE', text: '#2563EB', bold: '#1E40AF' }, + { label: '健康', value: result.summary.healthy, bg: '#F0FDF4', border: '#BBF7D0', text: '#16A34A', bold: '#15803D' }, + { label: '警告', value: result.summary.warnings, bg: '#FEFCE8', border: '#FDE047', text: '#CA8A04', bold: '#A16207' }, + { label: '错误', value: result.summary.errors, bg: '#FEF2F2', border: '#FECACA', text: '#DC2626', bold: '#B91C1C' }, + ] as const + ).map(({ label, value, bg, border, text, bold }) => ( +
+

{label}

+

{value}

+
+ ))} +
+ +

+ 检查时间: {new Date(result.timestamp).toLocaleString('zh-CN')} +

+ + {/* 产品列表 */} +
+ {result.products.map((product) => { + const borderColor = + product.severity === 'error' ? '#FCA5A5' : product.severity === 'warning' ? '#FCD34D' : '#86EFAC' + const bgColor = + product.severity === 'error' ? '#FEF2F2' : product.severity === 'warning' ? '#FEFCE8' : '#F0FDF4' + return ( +
+
+
+
+ {severityIcon(product.severity)} +

{product.title}

+ + {product.status} + +
+
+

Medusa ID: {product.medusaId}

+ {product.seedId &&

Seed ID: {product.seedId}

} +
+
+
+

进度: {product.stats.completionPercentage}%

+

+ {product.stats.totalDisplayCount} / {product.stats.fundingGoal} +

+
+
+ +
+
+ 开始:{' '} + {fmtDate(product.dates.preorderStartDate)} +
+
+ 结束:{' '} + {fmtDate(product.dates.preorderEndDate)} +
+
+ + {product.issues.length > 0 && ( +
+

问题:

+
    + {product.issues.map((issue, i) => ( +
  • + {issue} +
  • + ))} +
+
+ )} +
+ ) + })} + + {result.products.length === 0 && ( +

+ 没有找到预购产品 +

+ )} +
+ +
+ +
+
+
+ )} + + ) +} diff --git a/src/components/sync/preorder/RefreshOrderCounts.tsx b/src/components/sync/preorder/RefreshOrderCounts.tsx new file mode 100644 index 0000000..e420f9c --- /dev/null +++ b/src/components/sync/preorder/RefreshOrderCounts.tsx @@ -0,0 +1,136 @@ +'use client' +import { useState } from 'react' +import { Button, useSelection } from '@payloadcms/ui' +import { useRouter } from 'next/navigation' + +/** + * 刷新预购商品订单计数(选中 / 全部) + * API: POST /api/preorders/refresh-order-counts + */ +export function RefreshOrderCounts() { + const { getQueryParams, toggleAll } = useSelection() + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + const router = useRouter() + + const handleAll = async () => { + if (!confirm('确定要刷新所有预购商品的订单计数吗?')) return + setLoading(true) + setMessage('') + 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() + if (data.success) { + setMessage(`✅ ${data.message || '订单计数刷新成功!'}`) + setTimeout(() => router.refresh(), 1500) + } else { + setMessage(`❌ ${data.error || '刷新失败'}`) + } + } catch (err) { + setMessage('❌ ' + (err instanceof Error ? err.message : '未知错误')) + } finally { + setLoading(false) + } + } + + const handleSelected = async () => { + const queryParams = getQueryParams() + let selectedIds: string[] = [] + if (queryParams && typeof queryParams === 'object') { + const where = (queryParams as any).where + if (where?.id?.in) selectedIds = where.id.in + } + + if (!selectedIds.length) { + setMessage('⚠️ 请先勾选要刷新的商品(使用列表左侧的复选框)') + return + } + + setLoading(true) + setMessage('') + try { + const res = await fetch('/api/preorders/refresh-order-counts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ productIds: selectedIds }), + }) + const data = await res.json() + if (data.success) { + setMessage(`✅ ${data.message || '订单计数刷新成功!'}`) + toggleAll?.() + setTimeout(() => router.refresh(), 1500) + } else { + setMessage(`❌ ${data.error || '刷新失败'}`) + } + } catch (err) { + setMessage('❌ ' + (err instanceof Error ? err.message : '未知错误')) + } finally { + setLoading(false) + } + } + + return ( +
+
+ + +
+ + {message && } + +
+

+ 💡 说明:订单计数 = 真实订单计数 + Fake计数 +

+

+ • 真实订单计数:从 Medusa 订单系统同步,只读 +

+

+ • Fake计数:可手动编辑,用于调整显示的进度 +

+
+
+ ) +} + +function StatusMsg({ text }: { text: string }) { + const isError = text.startsWith('❌') + const isWarn = text.startsWith('⚠️') + return ( +
+ {text} +
+ ) +} diff --git a/src/components/sync/taobao/TaobaoAllSync.tsx b/src/components/sync/taobao/TaobaoAllSync.tsx new file mode 100644 index 0000000..ff0e5f9 --- /dev/null +++ b/src/components/sync/taobao/TaobaoAllSync.tsx @@ -0,0 +1,123 @@ +'use client' +import { useState } from 'react' + +/** + * 列表页 — 淘宝全量同步按钮(仅填充空字段 / 强制覆盖所有字段) + * API: POST /api/admin/taobao/sync-all + */ +export function TaobaoAllSync() { + const [loadingNormal, setLoadingNormal] = useState(false) + const [loadingForce, setLoadingForce] = useState(false) + const [confirmForce, setConfirmForce] = useState(false) + const [message, setMessage] = useState(null) + + const busy = loadingNormal || loadingForce + + 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) + } + } + + return ( +
+
+ + + {!confirmForce ? ( + + ) : ( + <> + + 确认覆盖所有字段? + + + + + )} +
+ + {message && } +
+ ) +} + +const btnStyle = (busy: boolean, color: string): React.CSSProperties => ({ + padding: '0.4rem 0.85rem', + background: busy ? '#9ca3af' : color, + color: '#fff', + border: 'none', + borderRadius: '4px', + cursor: busy ? 'not-allowed' : 'pointer', + fontSize: '0.78rem', + fontWeight: 500, + whiteSpace: 'nowrap', +}) + +function StatusMsg({ text }: { text: string }) { + const isError = text.startsWith('❌') + return ( +
+ {text} +
+ ) +} diff --git a/src/components/sync/taobao/TaobaoProductSync.tsx b/src/components/sync/taobao/TaobaoProductSync.tsx new file mode 100644 index 0000000..2a7d206 --- /dev/null +++ b/src/components/sync/taobao/TaobaoProductSync.tsx @@ -0,0 +1,121 @@ +'use client' +import { useState } from 'react' +import { useDocumentInfo } from '@payloadcms/ui' + +/** + * 产品编辑页 — 淘宝信息同步按钮(仅填充空字段 / 强制覆盖) + * API: POST /api/admin/taobao/sync-product + * + * 使用 useDocumentInfo,只能在 Product / PreorderProduct 编辑页使用。 + */ +export function TaobaoProductSync() { + const { id, collectionSlug } = useDocumentInfo() + const [loadingNormal, setLoadingNormal] = useState(false) + const [loadingForce, setLoadingForce] = useState(false) + const [message, setMessage] = useState(null) + + if (!id) return null + const isValid = collectionSlug === 'products' || collectionSlug === 'preorder-products' + if (!isValid) return null + + const busy = loadingNormal || loadingForce + + 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) + } + } + + return ( +
+
+ 淘宝自动解析 +
+ +
+ + +
+ +
+ 🔄 更新:仅填充空白字段(标题、封面、价格)  + ⚡ 强制更新:覆盖已有字段 +
+ + {message && } +
+ ) +} + +const btnStyle = (busy: boolean, color: string): React.CSSProperties => ({ + padding: '0.4rem 0.9rem', + background: busy ? '#9ca3af' : color, + color: '#fff', + border: 'none', + borderRadius: '4px', + cursor: busy ? 'not-allowed' : 'pointer', + fontSize: '0.8rem', + fontWeight: 500, + whiteSpace: 'nowrap', +}) + +function StatusMsg({ text }: { text: string }) { + const isError = text.startsWith('❌') + return ( +
+ {text} +
+ ) +} diff --git a/src/components/views/AdminPanel.tsx b/src/components/views/AdminPanel.tsx index 5f333b6..cf71ffe 100644 --- a/src/components/views/AdminPanel.tsx +++ b/src/components/views/AdminPanel.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react' import { Button } from '@payloadcms/ui' -import { ResetDataButton } from '../sync/ResetDataButton' +import { ResetData } from '../sync/admin/ResetData' /** * 管理员控制面板 @@ -168,7 +168,7 @@ export default function AdminPanel() { 一键重置所有数据:清理 Payload CMS → 清理 Medusa → 重新导入 Medusa seed 数据

- +
{/* 清理数据库 */} diff --git a/src/components/views/PreorderHealthCheckButton.tsx b/src/components/views/PreorderHealthCheckButton.tsx deleted file mode 100644 index 97368a8..0000000 --- a/src/components/views/PreorderHealthCheckButton.tsx +++ /dev/null @@ -1,260 +0,0 @@ -'use client' - -import React, { useState } from 'react' -import { Button, Modal } from '@payloadcms/ui' - -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[] -} - -export const PreorderHealthCheckButton: React.FC = () => { - const [isOpen, setIsOpen] = useState(false) - const [loading, setLoading] = useState(false) - const [result, setResult] = useState(null) - const [error, setError] = useState(null) - - const runHealthCheck = async () => { - setLoading(true) - setError(null) - - try { - const response = await fetch('/api/preorders/health-check') - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`) - } - const data = await response.json() - setResult(data) - setIsOpen(true) - } catch (err: any) { - setError(err.message || 'Failed to run health check') - console.error('Health check error:', err) - } finally { - setLoading(false) - } - } - - const getSeverityIcon = (severity: string) => { - switch (severity) { - case 'error': - return '❌' - case 'warning': - return '⚠️' - case 'healthy': - return '✅' - default: - return 'ℹ️' - } - } - - const formatDate = (dateString: string | null) => { - if (!dateString) return 'N/A' - try { - return new Date(dateString).toLocaleDateString('zh-CN', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - }) - } catch { - return dateString - } - } - - return ( - <> -
- - {error && ( - - 错误: {error} - - )} -
- - {isOpen && result && ( - setIsOpen(false)}> -
-

- 预购产品健康检查 -

- - {/* 概览统计 */} -
-
-

总数

-

{result.summary.total}

-
-
-

健康

-

{result.summary.healthy}

-
-
-

警告

-

{result.summary.warnings}

-
-
-

错误

-

{result.summary.errors}

-
-
- -

- 检查时间: {new Date(result.timestamp).toLocaleString('zh-CN')} -

- - {/* 产品列表 */} -
- {result.products.map((product) => ( -
-
-
-
- {getSeverityIcon(product.severity)} -

{product.title}

- - {product.status} - -
-
-

Medusa ID: {product.medusaId}

- {product.seedId &&

Seed ID: {product.seedId}

} -
-
-
-

进度: {product.stats.completionPercentage}%

-

- {product.stats.totalDisplayCount} / {product.stats.fundingGoal} -

-
-
- -
-
- 开始:{' '} - {formatDate(product.dates.preorderStartDate)} -
-
- 结束:{' '} - {formatDate(product.dates.preorderEndDate)} -
-
- - {product.issues.length > 0 && ( -
-

问题:

-
    - {product.issues.map((issue, idx) => ( -
  • - {issue} -
  • - ))} -
-
- )} -
- ))} -
- - {result.products.length === 0 && ( -

- 没有找到预购产品 -

- )} - -
- -
-
-
- )} - - ) -}