按钮精简
This commit is contained in:
parent
c84eef485b
commit
6e75c34faf
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<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>
|
||||
订单计数管理
|
||||
</h3>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginBottom: '0.5rem' }}>
|
||||
<Button
|
||||
onClick={handleRefreshSelected}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
size="small"
|
||||
>
|
||||
{loading ? '刷新中...' : '刷新选中商品'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleRefreshAll}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
size="small"
|
||||
>
|
||||
{loading ? '刷新中...' : '刷新全部订单计数'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.75rem',
|
||||
marginTop: '0.5rem',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: message.startsWith('✅')
|
||||
? 'var(--theme-success-50)'
|
||||
: message.startsWith('⚠️')
|
||||
? 'var(--theme-warning-50)'
|
||||
: 'var(--theme-error-50)',
|
||||
color: message.startsWith('✅')
|
||||
? 'var(--theme-success-900)'
|
||||
: message.startsWith('⚠️')
|
||||
? 'var(--theme-warning-900)'
|
||||
: 'var(--theme-error-900)',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '1rem', fontSize: '0.875rem', color: 'var(--theme-elevation-500)' }}>
|
||||
<p style={{ margin: '0.25rem 0' }}>
|
||||
💡 <strong>说明:</strong>订单计数 = 真实订单计数 + Fake计数
|
||||
</p>
|
||||
<p style={{ margin: '0.25rem 0' }}>
|
||||
• <strong>真实订单计数</strong>:从 Medusa 订单系统同步,只读
|
||||
</p>
|
||||
<p style={{ margin: '0.25rem 0' }}>
|
||||
• <strong>Fake计数</strong>:可手动编辑,用于调整显示的进度
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<string | null>(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 (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{/* 更新(非强制) */}
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => run(false)}
|
||||
style={{
|
||||
padding: '0.4rem 0.85rem',
|
||||
background: busy ? '#9ca3af' : '#10b981',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: busy ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{loadingNormal ? '更新中…' : '🔄 更新全部淘宝'}
|
||||
</button>
|
||||
|
||||
{/* 强制更新(二次确认) */}
|
||||
{!confirmForce ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => setConfirmForce(true)}
|
||||
style={{
|
||||
padding: '0.4rem 0.85rem',
|
||||
background: busy ? '#9ca3af' : '#ef4444',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: busy ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
⚡ 强制更新全部淘宝
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<span style={{ fontSize: '0.78rem', color: '#dc2626', fontWeight: 600 }}>
|
||||
确认覆盖所有字段?
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => run(true)}
|
||||
style={{
|
||||
padding: '0.35rem 0.75rem',
|
||||
background: '#dc2626',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{loadingForce ? '更新中…' : '确认'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfirmForce(false)}
|
||||
style={{
|
||||
padding: '0.35rem 0.75rem',
|
||||
background: 'transparent',
|
||||
color: 'var(--theme-elevation-600)',
|
||||
border: '1px solid var(--theme-elevation-200)',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.78rem',
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: '0.78rem',
|
||||
color: message.startsWith('✅') ? '#16a34a' : '#dc2626',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<string | null>(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 (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
padding: '0.75rem 1rem',
|
||||
marginBottom: '1rem',
|
||||
background: 'var(--theme-elevation-50)',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid var(--theme-elevation-150)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--theme-elevation-600)' }}>
|
||||
淘宝自动解析
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
{/* 更新(非强制) */}
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => run(false)}
|
||||
style={{
|
||||
padding: '0.4rem 0.9rem',
|
||||
background: busy ? '#9ca3af' : '#3b82f6',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: busy ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{loadingNormal ? '解析中…' : '🔄 更新淘宝信息'}
|
||||
</button>
|
||||
|
||||
{/* 强制全量更新 */}
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => run(true)}
|
||||
style={{
|
||||
padding: '0.4rem 0.9rem',
|
||||
background: busy ? '#9ca3af' : '#f97316',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: busy ? 'not-allowed' : 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 500,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{loadingForce ? '解析中…' : '⚡ 强制更新淘宝信息'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 说明文字 */}
|
||||
<div style={{ fontSize: '0.73rem', color: 'var(--theme-elevation-450)', lineHeight: 1.5 }}>
|
||||
<strong>🔄 更新</strong>:仅填充空白字段(标题、封面、价格) 
|
||||
<strong>⚡ 强制更新</strong>:覆盖已有字段
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.4rem 0.75rem',
|
||||
borderRadius: '4px',
|
||||
background: message.startsWith('✅') ? 'var(--theme-success-50)' : 'var(--theme-error-50)',
|
||||
color: message.startsWith('✅') ? 'var(--theme-success-750)' : 'var(--theme-error-750)',
|
||||
fontSize: '0.8rem',
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<p
|
||||
style={{
|
||||
margin: '0.25rem 0 0',
|
||||
padding: '0.2rem 0.5rem',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.78rem',
|
||||
lineHeight: 1.4,
|
||||
background: isErr
|
||||
? 'var(--theme-error-50)'
|
||||
: isWarn
|
||||
? 'var(--theme-warning-50)'
|
||||
: 'var(--theme-success-50)',
|
||||
color: isErr
|
||||
? 'var(--theme-error-750)'
|
||||
: isWarn
|
||||
? 'var(--theme-warning-750)'
|
||||
: 'var(--theme-success-750)',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
function Divider() {
|
||||
return <div style={{ borderTop: '1px solid var(--theme-elevation-100)' }} />
|
||||
}
|
||||
|
||||
function SectionLabel({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<p style={{
|
||||
margin: '0 0 0.25rem',
|
||||
fontSize: '0.72rem',
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
color: 'var(--theme-elevation-500)',
|
||||
}}>
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 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 batchSync = async (force: boolean) => {
|
||||
const queryParams = getQueryParams()
|
||||
let selectedIds: string[] = []
|
||||
|
||||
let ids: string[] = []
|
||||
if (queryParams && typeof queryParams === 'object') {
|
||||
const whereCondition = (queryParams as any).where
|
||||
if (whereCondition?.id?.in) {
|
||||
selectedIds = whereCondition.id.in
|
||||
const where = (queryParams as any).where
|
||||
if (where?.id?.in) ids = where.id.in
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectedIds || selectedIds.length === 0) {
|
||||
setMessage('⚠️ 请先勾选要同步的商品(使用列表左侧的复选框)')
|
||||
if (!ids.length) {
|
||||
setMsg('⚠️ 请先勾选要同步的商品(列表左侧复选框)')
|
||||
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 handleForceUpdateAll = async () => {
|
||||
if (confirmText !== 'FORCE_UPDATE_ALL') {
|
||||
setMessage('❌ 确认字符不正确,请输入: FORCE_UPDATE_ALL')
|
||||
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 response.json()
|
||||
|
||||
if (data.success) {
|
||||
setMessage('✅ ' + (data.message || '强制更新成功!'))
|
||||
setTimeout(() => window.location.reload(), 1500)
|
||||
} else {
|
||||
setMessage('❌ 同步失败: ' + (data.error || data.message || '未知错误'))
|
||||
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) }
|
||||
}
|
||||
} 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 (
|
||||
<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',
|
||||
}}
|
||||
<div>
|
||||
<SectionLabel>🔄 Medusa 商品同步</SectionLabel>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<Button onClick={syncNew} disabled={busy} buttonStyle="primary" size="small">
|
||||
{loadingNew ? '同步中…' : '📥 同步新商品'}
|
||||
</Button>
|
||||
<Button onClick={() => batchSync(false)} disabled={busy} buttonStyle="secondary" size="small">
|
||||
{loadingBatch ? '同步中…' : '🔄 同步选中'}
|
||||
</Button>
|
||||
<Button onClick={() => batchSync(true)} disabled={busy} buttonStyle="secondary" size="small">
|
||||
{loadingForceBatch ? '更新中…' : '⚡ 强制更新选中'}
|
||||
</Button>
|
||||
{!showForceAll ? (
|
||||
<Button
|
||||
onClick={() => { setShowForceAll(true); setMsg(''); setConfirmText('') }}
|
||||
disabled={busy}
|
||||
buttonStyle="secondary"
|
||||
size="small"
|
||||
>
|
||||
<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>
|
||||
🔥 强制更新全部
|
||||
</Button>
|
||||
) : (
|
||||
<span style={{ display: 'inline-flex', gap: '0.4rem', alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={confirmText}
|
||||
onChange={(e) => setConfirmText(e.target.value)}
|
||||
placeholder="输入 FORCE_UPDATE_ALL"
|
||||
placeholder="输入 FORCE_UPDATE_ALL 确认"
|
||||
disabled={loadingForceAll}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.5rem',
|
||||
marginBottom: '0.75rem',
|
||||
padding: '0.3rem 0.5rem',
|
||||
border: '1px solid var(--theme-elevation-400)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.875rem',
|
||||
fontSize: '0.78rem',
|
||||
width: '200px',
|
||||
}}
|
||||
disabled={loading}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<Button
|
||||
onClick={handleForceUpdateAll}
|
||||
disabled={loading || confirmText !== 'FORCE_UPDATE_ALL'}
|
||||
onClick={forceAll}
|
||||
disabled={loadingForceAll || confirmText !== 'FORCE_UPDATE_ALL'}
|
||||
size="small"
|
||||
>
|
||||
{loading ? '更新中...' : '✅ 确认强制更新'}
|
||||
{loadingForceAll ? '更新中…' : '确认'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowForceAllConfirm(false)
|
||||
setConfirmText('')
|
||||
setMessage('')
|
||||
}}
|
||||
disabled={loading}
|
||||
onClick={() => { setShowForceAll(false); setConfirmText('') }}
|
||||
disabled={loadingForceAll}
|
||||
buttonStyle="secondary"
|
||||
size="small"
|
||||
>
|
||||
❌ 取消
|
||||
取消
|
||||
</Button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Msg text={msg} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Section: Taobao sync ──────────────────────────────────────────────────────
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<SectionLabel>🛍️ 淘宝信息同步</SectionLabel>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<Button onClick={() => run(false)} disabled={busy} buttonStyle="secondary" size="small">
|
||||
{loadingNormal ? '更新中…' : '🔄 更新全部淘宝'}
|
||||
</Button>
|
||||
{!confirmForce ? (
|
||||
<Button onClick={() => setConfirmForce(true)} disabled={busy} buttonStyle="secondary" size="small">
|
||||
⚡ 强制更新全部淘宝
|
||||
</Button>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: '0.75rem',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
{/* 第一行:基础同步功能 */}
|
||||
<Button
|
||||
onClick={handleSyncNew}
|
||||
disabled={loading}
|
||||
buttonStyle="primary"
|
||||
>
|
||||
📥 {loading ? '同步中...' : '同步新商品'}
|
||||
<span style={{ display: 'inline-flex', gap: '0.4rem', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '0.78rem', color: 'var(--theme-error-750)', fontWeight: 600 }}>
|
||||
确认覆盖所有字段?
|
||||
</span>
|
||||
<Button onClick={() => run(true)} disabled={busy} size="small">
|
||||
{loadingForce ? '更新中…' : '确认'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => handleBatchSync(false)}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
>
|
||||
🔄 {loading ? '同步中...' : '同步选中商品'}
|
||||
<Button onClick={() => setConfirmForce(false)} disabled={busy} buttonStyle="secondary" size="small">
|
||||
取消
|
||||
</Button>
|
||||
|
||||
{/* 第二行:强制更新功能 */}
|
||||
<Button
|
||||
onClick={() => handleBatchSync(true)}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
>
|
||||
⚡ {loading ? '更新中...' : '强制更新选中'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowForceAllConfirm(true)
|
||||
setMessage('')
|
||||
setConfirmText('')
|
||||
}}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
>
|
||||
🔥 强制更新全部
|
||||
</Button>
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{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>
|
||||
<Msg text={msg} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Section: Preorder management ──────────────────────────────────────────────
|
||||
|
||||
function PreorderSection() {
|
||||
const { getQueryParams, toggleAll } = useSelection()
|
||||
const router = useRouter()
|
||||
|
||||
const [hcLoading, setHcLoading] = useState(false)
|
||||
const [hcError, setHcError] = useState<string | null>(null)
|
||||
const [hcResult, setHcResult] = useState<HealthCheckResult | null>(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<string, string>)[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 (
|
||||
<div>
|
||||
<SectionLabel>📦 预购管理</SectionLabel>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<Button onClick={runHealthCheck} disabled={hcLoading} buttonStyle="secondary" size="small">
|
||||
{hcLoading ? '检查中…' : '🏥 健康检查'}
|
||||
</Button>
|
||||
<Button onClick={refreshSelected} disabled={rcLoading} buttonStyle="secondary" size="small">
|
||||
{rcLoading ? '刷新中…' : '📊 刷新选中计数'}
|
||||
</Button>
|
||||
<Button onClick={refreshAll} disabled={rcLoading} buttonStyle="secondary" size="small">
|
||||
{rcLoading ? '刷新中…' : '📊 刷新全部计数'}
|
||||
</Button>
|
||||
</div>
|
||||
{hcError && <Msg text={`❌ ${hcError}`} />}
|
||||
<Msg text={rcMsg} />
|
||||
|
||||
{hcOpen && hcResult && (
|
||||
<Modal slug="preorder-health-check-modal" onClose={() => setHcOpen(false)}>
|
||||
<div style={{ padding: '2rem', maxWidth: '900px' }}>
|
||||
<h2 style={{ marginBottom: '1.5rem', fontSize: '1.25rem', fontWeight: 700 }}>
|
||||
预购产品健康检查
|
||||
</h2>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: '0.75rem', marginBottom: '1.25rem' }}>
|
||||
{([
|
||||
{ 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 }) => (
|
||||
<div key={label} style={{ padding: '0.75rem', background: bg, border: `1px solid ${border}`, borderRadius: '6px' }}>
|
||||
<p style={{ margin: '0 0 0.2rem', fontSize: '0.75rem', color, fontWeight: 500 }}>{label}</p>
|
||||
<p style={{ margin: 0, fontSize: '1.75rem', fontWeight: 700, color }}>{value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p style={{ fontSize: '0.8rem', color: '#6B7280', marginBottom: '1rem' }}>
|
||||
检查时间: {new Date(hcResult.timestamp).toLocaleString('zh-CN')}
|
||||
</p>
|
||||
<div style={{ maxHeight: '480px', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
{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 (
|
||||
<div key={p.id} style={{ border: `1px solid ${borderColor}`, background: bgColor, borderRadius: '6px', padding: '0.75rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '1rem' }}>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', marginBottom: '0.25rem' }}>
|
||||
<span>{severityIcon(p.severity)}</span>
|
||||
<strong style={{ fontSize: '0.9rem' }}>{p.title}</strong>
|
||||
<span style={{
|
||||
padding: '0.1rem 0.45rem', fontSize: '0.7rem', borderRadius: '999px',
|
||||
background: p.status === 'published' ? '#D1FAE5' : '#F3F4F6',
|
||||
color: p.status === 'published' ? '#065F46' : '#374151',
|
||||
}}>{p.status}</span>
|
||||
</div>
|
||||
<p style={{ margin: 0, fontSize: '0.78rem', color: '#4B5563' }}>Medusa ID: {p.medusaId}</p>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right', fontSize: '0.78rem', color: '#4B5563', flexShrink: 0 }}>
|
||||
<p style={{ margin: 0 }}>进度 {p.stats.completionPercentage}%</p>
|
||||
<p style={{ margin: 0 }}>{p.stats.totalDisplayCount} / {p.stats.fundingGoal}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '1rem', fontSize: '0.78rem', color: '#4B5563', marginTop: '0.4rem' }}>
|
||||
<span><strong>开始:</strong> {fmtDate(p.dates.preorderStartDate)}</span>
|
||||
<span><strong>结束:</strong> {fmtDate(p.dates.preorderEndDate)}</span>
|
||||
</div>
|
||||
{p.issues.length > 0 && (
|
||||
<ul style={{ margin: '0.5rem 0 0', paddingLeft: '1.25rem', fontSize: '0.78rem' }}>
|
||||
{p.issues.map((issue, i) => <li key={i}>{issue}</li>)}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{!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>: 更新所有商品(需要确认)
|
||||
)
|
||||
})}
|
||||
{hcResult.products.length === 0 && (
|
||||
<p style={{ textAlign: 'center', padding: '2rem', color: '#6B7280' }}>没有找到预购产品</p>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ marginTop: '1.25rem', textAlign: 'right' }}>
|
||||
<Button onClick={() => setHcOpen(false)} buttonStyle="primary">关闭</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<div style={{
|
||||
margin: '0.25rem 0 0.5rem',
|
||||
padding: '0.5rem 0.75rem',
|
||||
background: 'var(--theme-elevation-50)',
|
||||
border: '1px solid var(--theme-elevation-150)',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.5rem',
|
||||
}}>
|
||||
<MedusaSyncSection collection={collection} />
|
||||
<Divider />
|
||||
<TaobaoSyncSection />
|
||||
{isPreorder && (
|
||||
<>
|
||||
<Divider />
|
||||
<PreorderSection />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<any>(null)
|
||||
|
||||
const handleReset = async (mode: 'full' | 'medusa-only') => {
|
||||
const confirmMsg = mode === 'medusa-only'
|
||||
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 (
|
||||
<div className={className}>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', marginBottom: '1rem' }}>
|
||||
<Button
|
||||
onClick={handleResetData}
|
||||
buttonStyle="error"
|
||||
disabled={loading !== null}
|
||||
size="medium"
|
||||
>
|
||||
<div>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<Button onClick={() => handle('full')} buttonStyle="error" disabled={busy} size="small">
|
||||
{loading === 'full' ? '🔄 重置中...' : '🗑️ 重置所有数据'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleResetMedusaOnly}
|
||||
onClick={() => handle('medusa-only')}
|
||||
buttonStyle="secondary"
|
||||
disabled={loading !== null}
|
||||
size="medium"
|
||||
disabled={busy}
|
||||
size="small"
|
||||
>
|
||||
{loading === 'medusa-only' ? '🔄 重置中...' : '🔄 仅重置 Medusa'}
|
||||
</Button>
|
||||
|
|
@ -83,33 +67,37 @@ export function ResetDataButton({ className }: Props) {
|
|||
{message && (
|
||||
<div
|
||||
style={{
|
||||
padding: '1rem',
|
||||
backgroundColor: message.includes('✅') ? '#d4edda' : message.includes('❌') ? '#f8d7da' : '#d1ecf1',
|
||||
border: `1px solid ${message.includes('✅') ? '#c3e6cb' : message.includes('❌') ? '#f5c6cb' : '#bee5eb'}`,
|
||||
marginTop: '0.75rem',
|
||||
padding: '0.75rem',
|
||||
borderRadius: '4px',
|
||||
marginTop: '1rem',
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontSize: '0.9rem',
|
||||
fontSize: '0.85rem',
|
||||
backgroundColor: message.includes('✅')
|
||||
? '#d4edda'
|
||||
: message.includes('❌')
|
||||
? '#f8d7da'
|
||||
: '#d1ecf1',
|
||||
border: `1px solid ${message.includes('✅') ? '#c3e6cb' : message.includes('❌') ? '#f5c6cb' : '#bee5eb'}`,
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{details && details.steps && (
|
||||
{details?.steps && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '1rem',
|
||||
padding: '1rem',
|
||||
backgroundColor: '#f8f9fa',
|
||||
marginTop: '0.75rem',
|
||||
padding: '0.75rem',
|
||||
background: '#f8f9fa',
|
||||
border: '1px solid #dee2e6',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.85rem',
|
||||
fontSize: '0.82rem',
|
||||
}}
|
||||
>
|
||||
<h4 style={{ marginTop: 0, marginBottom: '0.5rem' }}>详细信息:</h4>
|
||||
{details.steps.map((step: any, index: number) => (
|
||||
<div key={index} style={{ marginBottom: '0.5rem' }}>
|
||||
<h4 style={{ margin: '0 0 0.5rem' }}>详细信息:</h4>
|
||||
{details.steps.map((step: any, i: number) => (
|
||||
<div key={i} style={{ marginBottom: '0.4rem' }}>
|
||||
<strong>
|
||||
[{step.step}/3] {step.name}:{' '}
|
||||
</strong>
|
||||
|
|
@ -117,9 +105,7 @@ export function ResetDataButton({ className }: Props) {
|
|||
{step.skipped ? '⏭️ 跳过' : step.success ? '✅ 成功' : '❌ 失败'}
|
||||
</span>
|
||||
{step.deleted !== undefined && (
|
||||
<span style={{ marginLeft: '0.5rem' }}>
|
||||
(删除 {step.deleted} 条记录)
|
||||
</span>
|
||||
<span style={{ marginLeft: '0.4rem' }}>(删除 {step.deleted} 条记录)</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -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 (
|
||||
<div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
onClick={() => handle(false)}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
size="small"
|
||||
>
|
||||
🔄 {loading ? '同步中...' : '同步选中商品'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handle(true)}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
size="small"
|
||||
>
|
||||
⚡ {loading ? '更新中...' : '强制更新选中'}
|
||||
</Button>
|
||||
</div>
|
||||
{message && <StatusMsg text={message} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusMsg({ text }: { text: string }) {
|
||||
const isError = text.startsWith('❌')
|
||||
const isWarn = text.startsWith('⚠️')
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '0.4rem 0.6rem',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.8rem',
|
||||
background: isError
|
||||
? 'var(--theme-error-50)'
|
||||
: isWarn
|
||||
? 'var(--theme-warning-50)'
|
||||
: 'var(--theme-success-50)',
|
||||
color: isError
|
||||
? 'var(--theme-error-750)'
|
||||
: isWarn
|
||||
? 'var(--theme-warning-750)'
|
||||
: 'var(--theme-success-750)',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '0.75rem',
|
||||
padding: '0.75rem',
|
||||
background: 'var(--theme-warning-50)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
>
|
||||
<p style={{ margin: '0 0 0.4rem', fontWeight: 700, color: 'var(--theme-warning-900)' }}>
|
||||
⚠️ 危险操作
|
||||
</p>
|
||||
<p style={{ margin: '0 0 0.4rem' }}>这将强制更新所有商品,覆盖所有本地修改。</p>
|
||||
<p style={{ margin: 0 }}>
|
||||
请输入{' '}
|
||||
<code
|
||||
style={{
|
||||
padding: '0.1rem 0.3rem',
|
||||
background: '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"
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<Button
|
||||
onClick={handle}
|
||||
disabled={loading || confirmText !== 'FORCE_UPDATE_ALL'}
|
||||
size="small"
|
||||
>
|
||||
{loading ? '更新中...' : '✅ 确认强制更新'}
|
||||
</Button>
|
||||
<Button onClick={cancel} disabled={loading} buttonStyle="secondary" size="small">
|
||||
❌ 取消
|
||||
</Button>
|
||||
</div>
|
||||
{message && <StatusMsg text={message} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowConfirm(true)
|
||||
setMessage('')
|
||||
setConfirmText('')
|
||||
}}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
size="small"
|
||||
>
|
||||
🔥 强制更新全部
|
||||
</Button>
|
||||
{message && <StatusMsg text={message} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusMsg({ text }: { text: string }) {
|
||||
const isError = text.startsWith('❌')
|
||||
const isWarn = text.startsWith('⚠️')
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '0.4rem 0.6rem',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.8rem',
|
||||
background: isError
|
||||
? 'var(--theme-error-50)'
|
||||
: isWarn
|
||||
? 'var(--theme-warning-50)'
|
||||
: 'var(--theme-success-50)',
|
||||
color: isError
|
||||
? 'var(--theme-error-750)'
|
||||
: isWarn
|
||||
? 'var(--theme-warning-750)'
|
||||
: 'var(--theme-success-750)',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div>
|
||||
<Button onClick={handle} disabled={loading} buttonStyle="primary" size="small">
|
||||
📥 {loading ? '同步中...' : '同步新商品'}
|
||||
</Button>
|
||||
{message && <StatusMsg text={message} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusMsg({ text }: { text: string }) {
|
||||
const isError = text.startsWith('❌')
|
||||
const isWarn = text.startsWith('⚠️')
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '0.4rem 0.6rem',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.8rem',
|
||||
background: isError
|
||||
? 'var(--theme-error-50)'
|
||||
: isWarn
|
||||
? 'var(--theme-warning-50)'
|
||||
: 'var(--theme-success-50)',
|
||||
color: isError
|
||||
? 'var(--theme-error-750)'
|
||||
: isWarn
|
||||
? 'var(--theme-warning-750)'
|
||||
: 'var(--theme-success-750)',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<HealthCheckResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||
<Button onClick={run} disabled={loading} buttonStyle="secondary" size="small">
|
||||
{loading ? '检查中...' : '🏥 健康检查'}
|
||||
</Button>
|
||||
{error && <span style={{ fontSize: '0.8rem', color: 'var(--theme-error-750)' }}>❌ {error}</span>}
|
||||
</div>
|
||||
|
||||
{isOpen && result && (
|
||||
<Modal slug="preorder-health-check-modal" onClose={() => setIsOpen(false)}>
|
||||
<div style={{ padding: '2rem', maxWidth: '900px' }}>
|
||||
<h2 style={{ marginBottom: '1.5rem', fontSize: '1.5rem', fontWeight: 'bold' }}>
|
||||
预购产品健康检查
|
||||
</h2>
|
||||
|
||||
{/* 概览 */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
gap: '1rem',
|
||||
marginBottom: '1.5rem',
|
||||
}}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ 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 }) => (
|
||||
<div
|
||||
key={label}
|
||||
style={{ padding: '1rem', backgroundColor: bg, borderRadius: '0.5rem', border: `1px solid ${border}` }}
|
||||
>
|
||||
<p style={{ fontSize: '0.875rem', color: text, fontWeight: '500', marginBottom: '0.25rem' }}>{label}</p>
|
||||
<p style={{ fontSize: '2rem', fontWeight: 'bold', color: bold }}>{value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: '0.875rem', color: '#6B7280', marginBottom: '1.5rem' }}>
|
||||
检查时间: {new Date(result.timestamp).toLocaleString('zh-CN')}
|
||||
</p>
|
||||
|
||||
{/* 产品列表 */}
|
||||
<div style={{ maxHeight: '500px', overflowY: 'auto' }}>
|
||||
{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 (
|
||||
<div
|
||||
key={product.id}
|
||||
style={{
|
||||
border: `1px solid ${borderColor}`,
|
||||
backgroundColor: bgColor,
|
||||
borderRadius: '0.5rem',
|
||||
padding: '1rem',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.75rem' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
|
||||
<span style={{ fontSize: '1.5rem' }}>{severityIcon(product.severity)}</span>
|
||||
<h3 style={{ fontSize: '1.125rem', fontWeight: '600' }}>{product.title}</h3>
|
||||
<span
|
||||
style={{
|
||||
padding: '0.25rem 0.5rem',
|
||||
fontSize: '0.75rem',
|
||||
borderRadius: '9999px',
|
||||
backgroundColor: product.status === 'published' ? '#D1FAE5' : '#F3F4F6',
|
||||
color: product.status === 'published' ? '#065F46' : '#374151',
|
||||
}}
|
||||
>
|
||||
{product.status}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#4B5563' }}>
|
||||
<p>Medusa ID: {product.medusaId}</p>
|
||||
{product.seedId && <p>Seed ID: {product.seedId}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right', fontSize: '0.875rem', color: '#4B5563' }}>
|
||||
<p>进度: {product.stats.completionPercentage}%</p>
|
||||
<p>
|
||||
{product.stats.totalDisplayCount} / {product.stats.fundingGoal}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
fontSize: '0.875rem',
|
||||
color: '#4B5563',
|
||||
marginBottom: '0.75rem',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<span style={{ fontWeight: '500' }}>开始:</span>{' '}
|
||||
{fmtDate(product.dates.preorderStartDate)}
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ fontWeight: '500' }}>结束:</span>{' '}
|
||||
{fmtDate(product.dates.preorderEndDate)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{product.issues.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.75rem',
|
||||
paddingTop: '0.75rem',
|
||||
borderTop: '1px solid #D1D5DB',
|
||||
}}
|
||||
>
|
||||
<p style={{ fontSize: '0.875rem', fontWeight: '500', marginBottom: '0.5rem' }}>问题:</p>
|
||||
<ul style={{ fontSize: '0.875rem', paddingLeft: '1rem' }}>
|
||||
{product.issues.map((issue, i) => (
|
||||
<li key={i} style={{ marginBottom: '0.25rem' }}>
|
||||
{issue}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{result.products.length === 0 && (
|
||||
<p style={{ textAlign: 'center', padding: '3rem', color: '#6B7280' }}>
|
||||
没有找到预购产品
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '1.5rem', textAlign: 'right' }}>
|
||||
<Button onClick={() => setIsOpen(false)} buttonStyle="primary">
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<Button onClick={handleSelected} disabled={loading} buttonStyle="secondary" size="small">
|
||||
{loading ? '刷新中...' : '刷新选中订单计数'}
|
||||
</Button>
|
||||
<Button onClick={handleAll} disabled={loading} buttonStyle="secondary" size="small">
|
||||
{loading ? '刷新中...' : '刷新全部订单计数'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{message && <StatusMsg text={message} />}
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.75rem',
|
||||
fontSize: '0.78rem',
|
||||
color: 'var(--theme-elevation-500)',
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
<p style={{ margin: '0.2rem 0' }}>
|
||||
💡 <strong>说明:</strong>订单计数 = 真实订单计数 + Fake计数
|
||||
</p>
|
||||
<p style={{ margin: '0.2rem 0' }}>
|
||||
• <strong>真实订单计数</strong>:从 Medusa 订单系统同步,只读
|
||||
</p>
|
||||
<p style={{ margin: '0.2rem 0' }}>
|
||||
• <strong>Fake计数</strong>:可手动编辑,用于调整显示的进度
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusMsg({ text }: { text: string }) {
|
||||
const isError = text.startsWith('❌')
|
||||
const isWarn = text.startsWith('⚠️')
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '0.4rem 0.6rem',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.8rem',
|
||||
background: isError
|
||||
? 'var(--theme-error-50)'
|
||||
: isWarn
|
||||
? 'var(--theme-warning-50)'
|
||||
: 'var(--theme-success-50)',
|
||||
color: isError
|
||||
? 'var(--theme-error-750)'
|
||||
: isWarn
|
||||
? 'var(--theme-warning-750)'
|
||||
: 'var(--theme-success-750)',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<string | null>(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 (
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => run(false)}
|
||||
style={btnStyle(busy, '#10b981')}
|
||||
>
|
||||
{loadingNormal ? '更新中…' : '🔄 更新全部淘宝'}
|
||||
</button>
|
||||
|
||||
{!confirmForce ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => setConfirmForce(true)}
|
||||
style={btnStyle(busy, '#ef4444')}
|
||||
>
|
||||
⚡ 强制更新全部淘宝
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<span style={{ fontSize: '0.78rem', color: '#dc2626', fontWeight: 600 }}>
|
||||
确认覆盖所有字段?
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => run(true)}
|
||||
style={btnStyle(busy, '#dc2626')}
|
||||
>
|
||||
{loadingForce ? '更新中…' : '确认'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConfirmForce(false)}
|
||||
style={{
|
||||
padding: '0.35rem 0.75rem',
|
||||
background: 'transparent',
|
||||
color: 'var(--theme-elevation-600)',
|
||||
border: '1px solid var(--theme-elevation-200)',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.78rem',
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{message && <StatusMsg text={message} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '0.4rem 0.6rem',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.8rem',
|
||||
background: isError ? 'var(--theme-error-50)' : 'var(--theme-success-50)',
|
||||
color: isError ? 'var(--theme-error-750)' : 'var(--theme-success-750)',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<string | null>(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 (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 600,
|
||||
color: 'var(--theme-elevation-600)',
|
||||
marginBottom: '0.5rem',
|
||||
}}
|
||||
>
|
||||
淘宝自动解析
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => run(false)}
|
||||
style={btnStyle(busy, '#3b82f6')}
|
||||
>
|
||||
{loadingNormal ? '解析中…' : '🔄 更新淘宝信息'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => run(true)}
|
||||
style={btnStyle(busy, '#f97316')}
|
||||
>
|
||||
{loadingForce ? '解析中…' : '⚡ 强制更新淘宝信息'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
fontSize: '0.73rem',
|
||||
color: 'var(--theme-elevation-450)',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
<strong>🔄 更新</strong>:仅填充空白字段(标题、封面、价格) 
|
||||
<strong>⚡ 强制更新</strong>:覆盖已有字段
|
||||
</div>
|
||||
|
||||
{message && <StatusMsg text={message} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '0.4rem 0.75rem',
|
||||
borderRadius: '4px',
|
||||
fontSize: '0.8rem',
|
||||
background: isError ? 'var(--theme-error-50)' : 'var(--theme-success-50)',
|
||||
color: isError ? 'var(--theme-error-750)' : 'var(--theme-success-750)',
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 数据
|
||||
</p>
|
||||
|
||||
<ResetDataButton />
|
||||
<ResetData />
|
||||
</div>
|
||||
|
||||
{/* 清理数据库 */}
|
||||
|
|
|
|||
|
|
@ -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<HealthCheckResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<>
|
||||
<div style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem' }}>
|
||||
<Button
|
||||
onClick={runHealthCheck}
|
||||
disabled={loading}
|
||||
buttonStyle="secondary"
|
||||
icon="health"
|
||||
>
|
||||
{loading ? '检查中...' : '🏥 健康检查'}
|
||||
</Button>
|
||||
{error && (
|
||||
<span style={{ color: 'red', marginLeft: '1rem', alignSelf: 'center' }}>
|
||||
错误: {error}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isOpen && result && (
|
||||
<Modal slug="health-check-modal" onClose={() => setIsOpen(false)}>
|
||||
<div style={{ padding: '2rem', maxWidth: '900px' }}>
|
||||
<h2 style={{ marginBottom: '1.5rem', fontSize: '1.5rem', fontWeight: 'bold' }}>
|
||||
预购产品健康检查
|
||||
</h2>
|
||||
|
||||
{/* 概览统计 */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
gap: '1rem',
|
||||
marginBottom: '1.5rem'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
backgroundColor: '#EFF6FF',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid #BFDBFE'
|
||||
}}>
|
||||
<p style={{ fontSize: '0.875rem', color: '#2563EB', fontWeight: '500', marginBottom: '0.25rem' }}>总数</p>
|
||||
<p style={{ fontSize: '2rem', fontWeight: 'bold', color: '#1E40AF' }}>{result.summary.total}</p>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
backgroundColor: '#F0FDF4',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid #BBF7D0'
|
||||
}}>
|
||||
<p style={{ fontSize: '0.875rem', color: '#16A34A', fontWeight: '500', marginBottom: '0.25rem' }}>健康</p>
|
||||
<p style={{ fontSize: '2rem', fontWeight: 'bold', color: '#15803D' }}>{result.summary.healthy}</p>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
backgroundColor: '#FEFCE8',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid #FDE047'
|
||||
}}>
|
||||
<p style={{ fontSize: '0.875rem', color: '#CA8A04', fontWeight: '500', marginBottom: '0.25rem' }}>警告</p>
|
||||
<p style={{ fontSize: '2rem', fontWeight: 'bold', color: '#A16207' }}>{result.summary.warnings}</p>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '1rem',
|
||||
backgroundColor: '#FEF2F2',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid #FECACA'
|
||||
}}>
|
||||
<p style={{ fontSize: '0.875rem', color: '#DC2626', fontWeight: '500', marginBottom: '0.25rem' }}>错误</p>
|
||||
<p style={{ fontSize: '2rem', fontWeight: 'bold', color: '#B91C1C' }}>{result.summary.errors}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: '0.875rem', color: '#6B7280', marginBottom: '1.5rem' }}>
|
||||
检查时间: {new Date(result.timestamp).toLocaleString('zh-CN')}
|
||||
</p>
|
||||
|
||||
{/* 产品列表 */}
|
||||
<div style={{ maxHeight: '500px', overflowY: 'auto' }}>
|
||||
{result.products.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
style={{
|
||||
border: `1px solid ${
|
||||
product.severity === 'error'
|
||||
? '#FCA5A5'
|
||||
: product.severity === 'warning'
|
||||
? '#FCD34D'
|
||||
: '#86EFAC'
|
||||
}`,
|
||||
backgroundColor: product.severity === 'error'
|
||||
? '#FEF2F2'
|
||||
: product.severity === 'warning'
|
||||
? '#FEFCE8'
|
||||
: '#F0FDF4',
|
||||
borderRadius: '0.5rem',
|
||||
padding: '1rem',
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.75rem' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.5rem' }}>
|
||||
<span style={{ fontSize: '1.5rem' }}>{getSeverityIcon(product.severity)}</span>
|
||||
<h3 style={{ fontSize: '1.125rem', fontWeight: '600' }}>{product.title}</h3>
|
||||
<span
|
||||
style={{
|
||||
padding: '0.25rem 0.5rem',
|
||||
fontSize: '0.75rem',
|
||||
borderRadius: '9999px',
|
||||
backgroundColor: product.status === 'published' ? '#D1FAE5' : '#F3F4F6',
|
||||
color: product.status === 'published' ? '#065F46' : '#374151',
|
||||
}}
|
||||
>
|
||||
{product.status}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.875rem', color: '#4B5563' }}>
|
||||
<p>Medusa ID: {product.medusaId}</p>
|
||||
{product.seedId && <p>Seed ID: {product.seedId}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right', fontSize: '0.875rem', color: '#4B5563' }}>
|
||||
<p>进度: {product.stats.completionPercentage}%</p>
|
||||
<p>
|
||||
{product.stats.totalDisplayCount} / {product.stats.fundingGoal}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '1rem', fontSize: '0.875rem', color: '#4B5563', marginBottom: '0.75rem' }}>
|
||||
<div>
|
||||
<span style={{ fontWeight: '500' }}>开始:</span>{' '}
|
||||
{formatDate(product.dates.preorderStartDate)}
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ fontWeight: '500' }}>结束:</span>{' '}
|
||||
{formatDate(product.dates.preorderEndDate)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{product.issues.length > 0 && (
|
||||
<div style={{ marginTop: '0.75rem', paddingTop: '0.75rem', borderTop: '1px solid #D1D5DB' }}>
|
||||
<p style={{ fontSize: '0.875rem', fontWeight: '500', marginBottom: '0.5rem' }}>问题:</p>
|
||||
<ul style={{ fontSize: '0.875rem', paddingLeft: '1rem' }}>
|
||||
{product.issues.map((issue, idx) => (
|
||||
<li key={idx} style={{ marginBottom: '0.25rem' }}>
|
||||
{issue}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{result.products.length === 0 && (
|
||||
<p style={{ textAlign: 'center', padding: '3rem', color: '#6B7280' }}>
|
||||
没有找到预购产品
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '1.5rem', textAlign: 'right' }}>
|
||||
<Button onClick={() => setIsOpen(false)} buttonStyle="primary">
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue