Compare commits
2 Commits
9a47af76ce
...
35928a6144
| Author | SHA1 | Date |
|---|---|---|
|
|
35928a6144 | |
|
|
efb3f2c727 |
16
.env.example
16
.env.example
|
|
@ -4,14 +4,16 @@ DATABASE_URL=postgresql://user:password@localhost:5432/database
|
||||||
# Payload
|
# Payload
|
||||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||||
|
|
||||||
# Redis 配置
|
# Redis Configuration
|
||||||
REDIS_HOST=localhost
|
REDIS_URL=redis://localhost:6379
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=
|
|
||||||
REDIS_DB=0
|
|
||||||
|
|
||||||
# Store API Key(用于访问 hero-slider 和 product-recommendations 接口)
|
# API Keys (统一使用 PAYLOAD_API_KEY)
|
||||||
STORE_API_KEY=your-store-api-key-here
|
PAYLOAD_API_KEY=your-payload-api-key-here
|
||||||
|
|
||||||
|
# Medusa 配置
|
||||||
|
MEDUSA_BACKEND_URL=http://localhost:9000
|
||||||
|
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=your-publishable-key-here
|
||||||
|
PAYLOAD_API_KEY=your-payload-api-key-here
|
||||||
|
|
||||||
# Cloudflare R2 配置
|
# Cloudflare R2 配置
|
||||||
CLOUDFLARE_R2_BUCKET=your-bucket
|
CLOUDFLARE_R2_BUCKET=your-bucket
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,14 @@ import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997e
|
||||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField'
|
import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField'
|
||||||
import { UnifiedSyncButton as UnifiedSyncButton_8f3d4a2e1b5c6d7a9e0f1a2b3c4d5e6f } from '../../../components/sync/UnifiedSyncButton'
|
import { TaobaoLinkPreview as TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959 } from '../../../components/fields/TaobaoLinkPreview'
|
||||||
|
import { UnifiedSyncButton as UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523 } from '../../../components/sync/UnifiedSyncButton'
|
||||||
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
|
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
|
||||||
import { ForceSyncButton as ForceSyncButton_28396efe36d6238add95cf44109e281c } from '../../../components/sync/ForceSyncButton'
|
import { PreorderProgressCell as PreorderProgressCell_67df47753573233f0c83480de687f13b } from '../../../components/cells/PreorderProgressCell'
|
||||||
import { TaobaoLinkPreview as TaobaoLinkPreview_7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b } from '../../../components/fields/TaobaoLinkPreview'
|
import { RefreshOrderCountField as RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996 } from '../../../components/fields/RefreshOrderCountField'
|
||||||
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f } from '../../../components/fields/PreorderOrdersField'
|
import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f } from '../../../components/fields/PreorderOrdersField'
|
||||||
|
import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler'
|
||||||
import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel'
|
import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel'
|
||||||
import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView'
|
import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView'
|
||||||
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
||||||
|
|
@ -64,12 +66,14 @@ export const importMap = {
|
||||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426,
|
"/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426,
|
||||||
"/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_8f3d4a2e1b5c6d7a9e0f1a2b3c4d5e6f,
|
"/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959,
|
||||||
|
"/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523,
|
||||||
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
|
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
|
||||||
"/components/sync/ForceSyncButton#ForceSyncButton": ForceSyncButton_28396efe36d6238add95cf44109e281c,
|
"/components/cells/PreorderProgressCell#PreorderProgressCell": PreorderProgressCell_67df47753573233f0c83480de687f13b,
|
||||||
"/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b,
|
"/components/fields/RefreshOrderCountField#RefreshOrderCountField": RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996,
|
||||||
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f,
|
"/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f,
|
||||||
|
"/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5,
|
||||||
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
|
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
|
||||||
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
|
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
|
||||||
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export async function GET(request: Request) {
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
products: 0,
|
products: 0,
|
||||||
|
preorderProducts: 0,
|
||||||
announcements: 0,
|
announcements: 0,
|
||||||
articles: 0,
|
articles: 0,
|
||||||
errors: [] as string[],
|
errors: [] as string[],
|
||||||
|
|
@ -47,6 +48,20 @@ export async function GET(request: Request) {
|
||||||
results.errors.push(errorMsg)
|
results.errors.push(errorMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理 Preorder Products
|
||||||
|
try {
|
||||||
|
const deletedPreorderProducts = await payload.delete({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
where: {},
|
||||||
|
})
|
||||||
|
results.preorderProducts = deletedPreorderProducts.docs?.length || 0
|
||||||
|
console.log(`✅ 已清理 ${results.preorderProducts} 个预购商品`)
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = `清理 Preorder Products 失败: ${error instanceof Error ? error.message : '未知错误'}`
|
||||||
|
console.error('❌', errorMsg)
|
||||||
|
results.errors.push(errorMsg)
|
||||||
|
}
|
||||||
|
|
||||||
// 清理 Announcements
|
// 清理 Announcements
|
||||||
try {
|
try {
|
||||||
const deletedAnnouncements = await payload.delete({
|
const deletedAnnouncements = await payload.delete({
|
||||||
|
|
@ -77,7 +92,7 @@ export async function GET(request: Request) {
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `数据清理完成!已删除 ${results.products} 个商品、${results.announcements} 个公告、${results.articles} 个文章`,
|
message: `数据清理完成!已删除 ${results.products} 个商品、${results.preorderProducts} 个预购商品、${results.announcements} 个公告、${results.articles} 个文章`,
|
||||||
results,
|
results,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
// Delete all products
|
||||||
|
const products = await payload.find({
|
||||||
|
collection: 'products',
|
||||||
|
limit: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const product of products.docs) {
|
||||||
|
await payload.delete({
|
||||||
|
collection: 'products',
|
||||||
|
id: product.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all preorder products
|
||||||
|
const preorderProducts = await payload.find({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
limit: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const product of preorderProducts.docs) {
|
||||||
|
await payload.delete({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
id: product.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `Cleared ${products.docs.length} products and ${preorderProducts.docs.length} preorder-products`,
|
||||||
|
deleted: {
|
||||||
|
products: products.docs.length,
|
||||||
|
preorderProducts: preorderProducts.docs.length,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[clear-payload-collections] Error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
const products = await payload.find({
|
||||||
|
collection: 'products',
|
||||||
|
limit: 0, // Just get count
|
||||||
|
})
|
||||||
|
|
||||||
|
const preorderProducts = await payload.find({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
limit: 0, // Just get count
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
products: products.totalDocs,
|
||||||
|
preorderProducts: preorderProducts.totalDocs,
|
||||||
|
total: products.totalDocs + preorderProducts.totalDocs,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[payload-stats] Error:', error)
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
}, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -89,7 +89,6 @@ export async function GET(
|
||||||
preorder: {
|
preorder: {
|
||||||
id: product.id,
|
id: product.id,
|
||||||
title: product.title,
|
title: product.title,
|
||||||
handle: product.handle,
|
|
||||||
description: product.description,
|
description: product.description,
|
||||||
status: product._status,
|
status: product._status,
|
||||||
thumbnail: product.thumbnail,
|
thumbnail: product.thumbnail,
|
||||||
|
|
@ -102,11 +101,13 @@ export async function GET(
|
||||||
// 预购元数据(从 Payload 管理)
|
// 预购元数据(从 Payload 管理)
|
||||||
is_preorder: true,
|
is_preorder: true,
|
||||||
preorder_type: product.preorderType || 'standard',
|
preorder_type: product.preorderType || 'standard',
|
||||||
estimated_ship_date: product.estimatedShipDate || null,
|
|
||||||
preorder_end_date: product.preorderEndDate || null,
|
preorder_end_date: product.preorderEndDate || null,
|
||||||
funding_goal: fundingGoal,
|
funding_goal: fundingGoal,
|
||||||
min_order_quantity: product.minOrderQuantity || 1,
|
|
||||||
max_order_quantity: product.maxOrderQuantity || 0,
|
// 订单计数
|
||||||
|
order_count: parseInt(product.orderCount || '0', 10) || 0,
|
||||||
|
fake_order_count: parseInt(product.fakeOrderCount || '0', 10) || 0,
|
||||||
|
total_display_count: (parseInt(product.orderCount || '0', 10) || 0) + (parseInt(product.fakeOrderCount || '0', 10) || 0),
|
||||||
|
|
||||||
// 统计数据
|
// 统计数据
|
||||||
current_orders: totalOrders,
|
current_orders: totalOrders,
|
||||||
|
|
@ -152,12 +153,10 @@ export async function GET(
|
||||||
* - increment?: number - 增加订单数
|
* - increment?: number - 增加订单数
|
||||||
* - decrement?: number - 减少订单数
|
* - decrement?: number - 减少订单数
|
||||||
*
|
*
|
||||||
* - estimated_ship_date?: string - 更新预估发货日期
|
|
||||||
* - preorder_end_date?: string - 更新预购结束日期
|
* - preorder_end_date?: string - 更新预购结束日期
|
||||||
* - funding_goal?: number - 更新众筹目标
|
* - funding_goal?: number - 更新众筹目标
|
||||||
* - preorder_type?: string - 更新预购类型
|
* - preorder_type?: string - 更新预购类型
|
||||||
* - min_order_quantity?: number - 最小起订量
|
* - fake_order_count?: number - 更新 Fake 订单计数
|
||||||
* - max_order_quantity?: number - 最大购买数量
|
|
||||||
*/
|
*/
|
||||||
export async function PATCH(
|
export async function PATCH(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
|
|
@ -174,12 +173,10 @@ export async function PATCH(
|
||||||
max_orders,
|
max_orders,
|
||||||
increment,
|
increment,
|
||||||
decrement,
|
decrement,
|
||||||
estimated_ship_date,
|
|
||||||
preorder_end_date,
|
preorder_end_date,
|
||||||
funding_goal,
|
funding_goal,
|
||||||
preorder_type,
|
preorder_type,
|
||||||
min_order_quantity,
|
fake_order_count,
|
||||||
max_order_quantity,
|
|
||||||
} = body
|
} = body
|
||||||
|
|
||||||
// 获取产品
|
// 获取产品
|
||||||
|
|
@ -285,9 +282,6 @@ export async function PATCH(
|
||||||
// 模式2: 更新产品级别预购元数据(在 Payload 中管理)
|
// 模式2: 更新产品级别预购元数据(在 Payload 中管理)
|
||||||
const updateData: any = {}
|
const updateData: any = {}
|
||||||
|
|
||||||
if (estimated_ship_date !== undefined) {
|
|
||||||
updateData.estimatedShipDate = estimated_ship_date
|
|
||||||
}
|
|
||||||
if (preorder_end_date !== undefined) {
|
if (preorder_end_date !== undefined) {
|
||||||
updateData.preorderEndDate = preorder_end_date
|
updateData.preorderEndDate = preorder_end_date
|
||||||
}
|
}
|
||||||
|
|
@ -297,11 +291,8 @@ export async function PATCH(
|
||||||
if (preorder_type !== undefined) {
|
if (preorder_type !== undefined) {
|
||||||
updateData.preorderType = preorder_type
|
updateData.preorderType = preorder_type
|
||||||
}
|
}
|
||||||
if (min_order_quantity !== undefined) {
|
if (fake_order_count !== undefined) {
|
||||||
updateData.minOrderQuantity = min_order_quantity
|
updateData.fakeOrderCount = Math.max(0, fake_order_count)
|
||||||
}
|
|
||||||
if (max_order_quantity !== undefined) {
|
|
||||||
updateData.maxOrderQuantity = max_order_quantity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(updateData).length > 0) {
|
if (Object.keys(updateData).length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,6 @@ export async function GET(req: NextRequest) {
|
||||||
return {
|
return {
|
||||||
id: product.id,
|
id: product.id,
|
||||||
title: product.title,
|
title: product.title,
|
||||||
handle: product.handle,
|
|
||||||
status: product._status,
|
status: product._status,
|
||||||
thumbnail: product.thumbnail,
|
thumbnail: product.thumbnail,
|
||||||
description: product.description,
|
description: product.description,
|
||||||
|
|
@ -87,9 +86,13 @@ export async function GET(req: NextRequest) {
|
||||||
|
|
||||||
// 预购元数据
|
// 预购元数据
|
||||||
preorder_type: product.preorderType || 'standard',
|
preorder_type: product.preorderType || 'standard',
|
||||||
estimated_ship_date: product.estimatedShipDate || null,
|
|
||||||
funding_goal: fundingGoal,
|
funding_goal: fundingGoal,
|
||||||
|
|
||||||
|
// 订单计数
|
||||||
|
order_count: parseInt(product.orderCount || '0', 10) || 0,
|
||||||
|
fake_order_count: parseInt(product.fakeOrderCount || '0', 10) || 0,
|
||||||
|
total_display_count: (parseInt(product.orderCount || '0', 10) || 0) + (parseInt(product.fakeOrderCount || '0', 10) || 0),
|
||||||
|
|
||||||
// 统计数据
|
// 统计数据
|
||||||
current_orders: totalOrders,
|
current_orders: totalOrders,
|
||||||
total_max_orders: totalMaxOrders,
|
total_max_orders: totalMaxOrders,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// 验证 API Key
|
// 验证 API Key
|
||||||
const apiKey = req.headers.get('x-store-api-key')
|
const apiKey = req.headers.get('x-store-api-key')
|
||||||
const validApiKey = process.env.STORE_API_KEY
|
const validApiKey = process.env.PAYLOAD_API_KEY
|
||||||
|
|
||||||
if (!apiKey || !validApiKey || apiKey !== validApiKey) {
|
if (!apiKey || !validApiKey || apiKey !== validApiKey) {
|
||||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// 验证 API Key
|
// 验证 API Key
|
||||||
const apiKey = req.headers.get('x-store-api-key')
|
const apiKey = req.headers.get('x-store-api-key')
|
||||||
const validApiKey = process.env.STORE_API_KEY
|
const validApiKey = process.env.PAYLOAD_API_KEY
|
||||||
|
|
||||||
if (!apiKey || !validApiKey || apiKey !== validApiKey) {
|
if (!apiKey || !validApiKey || apiKey !== validApiKey) {
|
||||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import payload from 'payload'
|
||||||
|
|
||||||
|
const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
||||||
|
const MEDUSA_ADMIN_API_KEY = process.env.MEDUSA_ADMIN_API_KEY || ''
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新预购商品的订单计数
|
||||||
|
* POST /api/refresh-order-counts
|
||||||
|
*
|
||||||
|
* Body:
|
||||||
|
* - productIds?: string[] - 要刷新的商品 ID 列表(Payload ID)
|
||||||
|
* - refreshAll?: boolean - 是否刷新所有预购商品
|
||||||
|
*/
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await req.json()
|
||||||
|
const { productIds, refreshAll } = body
|
||||||
|
|
||||||
|
if (!productIds && !refreshAll) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '请提供 productIds 或设置 refreshAll' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let products: any[] = []
|
||||||
|
|
||||||
|
if (refreshAll) {
|
||||||
|
// 获取所有预购商品
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
limit: 1000,
|
||||||
|
where: {
|
||||||
|
medusaId: {
|
||||||
|
exists: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
products = result.docs
|
||||||
|
} else {
|
||||||
|
// 获取指定的商品
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
limit: productIds.length,
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: productIds,
|
||||||
|
},
|
||||||
|
medusaId: {
|
||||||
|
exists: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
products = result.docs
|
||||||
|
}
|
||||||
|
|
||||||
|
if (products.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '没有找到要刷新的商品' },
|
||||||
|
{ status: 404 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计更新结果
|
||||||
|
let successCount = 0
|
||||||
|
let failCount = 0
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
// 为每个商品刷新订单计数
|
||||||
|
for (const product of products) {
|
||||||
|
try {
|
||||||
|
const orderCount = await fetchProductOrderCount(product.medusaId)
|
||||||
|
|
||||||
|
await payload.update({
|
||||||
|
collection: 'preorder-products',
|
||||||
|
id: product.id,
|
||||||
|
data: {
|
||||||
|
orderCount,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
successCount++
|
||||||
|
} catch (error) {
|
||||||
|
failCount++
|
||||||
|
const errorMsg = `商品 ${product.title} (${product.medusaId}): ${
|
||||||
|
error instanceof Error ? error.message : '未知错误'
|
||||||
|
}`
|
||||||
|
errors.push(errorMsg)
|
||||||
|
console.error('刷新订单计数失败:', errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `刷新完成!成功: ${successCount},失败: ${failCount}`,
|
||||||
|
details: {
|
||||||
|
total: products.length,
|
||||||
|
success: successCount,
|
||||||
|
failed: failCount,
|
||||||
|
errors: errors.length > 0 ? errors : undefined,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新订单计数 API 错误:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : '未知错误',
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Medusa 获取指定商品的订单数量
|
||||||
|
*/
|
||||||
|
async function fetchProductOrderCount(productId: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
// 使用 Medusa Admin API 查询订单
|
||||||
|
// 注意:这里需要使用 Query API 或者 Admin Orders API
|
||||||
|
// 查询所有包含该商品的已完成订单
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${MEDUSA_BACKEND_URL}/admin/orders?fields=id,items&items[product_id]=${productId}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-publishable-api-key': process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || '',
|
||||||
|
...(MEDUSA_ADMIN_API_KEY && { 'Authorization': `Bearer ${MEDUSA_ADMIN_API_KEY}` }),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Medusa API 返回错误: ${response.status} ${response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
// 计算订单中该商品的总数量
|
||||||
|
let totalQuantity = 0
|
||||||
|
|
||||||
|
if (data.orders && Array.isArray(data.orders)) {
|
||||||
|
for (const order of data.orders) {
|
||||||
|
if (order.items && Array.isArray(order.items)) {
|
||||||
|
for (const item of order.items) {
|
||||||
|
if (item.product_id === productId) {
|
||||||
|
totalQuantity += item.quantity || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalQuantity
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`获取商品 ${productId} 订单数量失败:`, error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -107,12 +107,12 @@ export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
// 可选的 API Key 验证
|
// 可选的 API Key 验证
|
||||||
const authHeader = request.headers.get('authorization')
|
const authHeader = request.headers.get('authorization')
|
||||||
const storeApiKey = process.env.STORE_API_KEY
|
const payloadApiKey = process.env.PAYLOAD_API_KEY
|
||||||
|
|
||||||
// 如果配置了 STORE_API_KEY,则验证请求
|
// 如果配置了 PAYLOAD_API_KEY,则验证请求
|
||||||
if (storeApiKey && authHeader) {
|
if (payloadApiKey && authHeader) {
|
||||||
const token = authHeader.replace('Bearer ', '')
|
const token = authHeader.replace('Bearer ', '')
|
||||||
if (token !== storeApiKey) {
|
if (token !== payloadApiKey) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
|
|
|
||||||
|
|
@ -27,24 +27,24 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
slug: 'preorder-products',
|
slug: 'preorder-products',
|
||||||
admin: {
|
admin: {
|
||||||
useAsTitle: 'title',
|
useAsTitle: 'title',
|
||||||
defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'],
|
defaultColumns: ['thumbnail', 'title', 'medusaId', 'progress', 'status', 'updatedAt'],
|
||||||
description: '管理预售商品的详细内容和描述',
|
description: '管理预售商品的详细内容和描述',
|
||||||
listSearchableFields: ['title', 'medusaId', 'handle'],
|
listSearchableFields: ['title', 'medusaId'],
|
||||||
pagination: {
|
pagination: {
|
||||||
defaultLimit: 25,
|
defaultLimit: 25,
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
edit: {
|
|
||||||
PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton',
|
|
||||||
},
|
|
||||||
beforeListTable: [
|
beforeListTable: [
|
||||||
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
|
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
|
||||||
'/components/list/ProductGridStyler',
|
'/components/list/PreorderProductGridStyler#PreorderProductGridStyler',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
read: () => true,
|
read: () => true, // 公开可读
|
||||||
|
create: ({ req: { user } }) => !!user, // 登录用户可创建
|
||||||
|
update: ({ req: { user } }) => !!user, // 登录用户可更新
|
||||||
|
delete: ({ req: { user } }) => !!user, // 登录用户可删除
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|
@ -103,20 +103,15 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'handle',
|
|
||||||
type: 'text',
|
|
||||||
admin: {
|
|
||||||
description: '商品 URL handle(从 Medusa 同步)',
|
|
||||||
readOnly: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'thumbnail',
|
name: 'thumbnail',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
admin: {
|
admin: {
|
||||||
description: '商品缩略图 URL(从 Medusa 同步)',
|
description: '商品封面 URL(支持上传或输入 URL)',
|
||||||
readOnly: true,
|
components: {
|
||||||
|
Cell: '/components/cells/ThumbnailCell#ThumbnailCell',
|
||||||
|
Field: '/components/fields/ThumbnailField#ThumbnailField',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -130,6 +125,15 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'progress',
|
||||||
|
type: 'ui',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
Cell: '/components/cells/PreorderProgressCell#PreorderProgressCell',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -165,16 +169,6 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'estimatedShipDate',
|
|
||||||
type: 'date',
|
|
||||||
admin: {
|
|
||||||
description: '预计发货日期',
|
|
||||||
date: {
|
|
||||||
displayFormat: 'yyyy-MM-dd',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'preorderEndDate',
|
name: 'preorderEndDate',
|
||||||
type: 'date',
|
type: 'date',
|
||||||
|
|
@ -185,23 +179,34 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'ui',
|
||||||
|
name: 'refreshOrderCount',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
Field: '/components/fields/RefreshOrderCountField#RefreshOrderCountField',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'row',
|
type: 'row',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'minOrderQuantity',
|
name: 'orderCount',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
defaultValue: 1,
|
defaultValue: 0,
|
||||||
admin: {
|
admin: {
|
||||||
description: '最小起订量',
|
description: '真实订单计数(从 Medusa 同步)',
|
||||||
|
readOnly: true,
|
||||||
width: '50%',
|
width: '50%',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'maxOrderQuantity',
|
name: 'fakeOrderCount',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
defaultValue: 0,
|
||||||
admin: {
|
admin: {
|
||||||
description: '最大购买数量(0 表示不限制)',
|
description: 'Fake 订单计数(手动设置)',
|
||||||
width: '50%',
|
width: '50%',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -283,6 +288,17 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
Field: '/components/fields/RelatedProductsField#RelatedProductsField',
|
Field: '/components/fields/RelatedProductsField#RelatedProductsField',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
filterOptions: ({ relationTo, data }) => {
|
||||||
|
// 过滤掉当前商品本身,避免自引用
|
||||||
|
if (data?.id) {
|
||||||
|
return {
|
||||||
|
id: {
|
||||||
|
not_equals: data.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,11 @@ export const Products: CollectionConfig = {
|
||||||
useAsTitle: 'title',
|
useAsTitle: 'title',
|
||||||
defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'],
|
defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'],
|
||||||
description: '管理 Medusa 商品的详细内容和描述',
|
description: '管理 Medusa 商品的详细内容和描述',
|
||||||
listSearchableFields: ['title', 'medusaId', 'handle'],
|
listSearchableFields: ['title', 'medusaId'],
|
||||||
pagination: {
|
pagination: {
|
||||||
defaultLimit: 25,
|
defaultLimit: 25,
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
edit: {
|
|
||||||
PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton',
|
|
||||||
},
|
|
||||||
beforeListTable: [
|
beforeListTable: [
|
||||||
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
|
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
|
||||||
'/components/list/ProductGridStyler',
|
'/components/list/ProductGridStyler',
|
||||||
|
|
@ -109,13 +106,6 @@ export const Products: CollectionConfig = {
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'handle',
|
|
||||||
type: 'text',
|
|
||||||
admin: {
|
|
||||||
hidden: true, // 隐藏字段,但保留用于搜索
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'thumbnail',
|
name: 'thumbnail',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|
@ -206,6 +196,17 @@ export const Products: CollectionConfig = {
|
||||||
Field: '/components/fields/RelatedProductsField#RelatedProductsField',
|
Field: '/components/fields/RelatedProductsField#RelatedProductsField',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
filterOptions: ({ relationTo, data }) => {
|
||||||
|
// 过滤掉当前商品本身,避免自引用
|
||||||
|
if (data?.id) {
|
||||||
|
return {
|
||||||
|
id: {
|
||||||
|
not_equals: data.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预购进度单元格组件
|
||||||
|
* 在列表网格视图中显示订单计数和进度
|
||||||
|
*/
|
||||||
|
export function PreorderProgressCell({ rowData }: any) {
|
||||||
|
const orderCount = parseInt(rowData?.orderCount || '0', 10) || 0
|
||||||
|
const fakeOrderCount = parseInt(rowData?.fakeOrderCount || '0', 10) || 0
|
||||||
|
const fundingGoal = parseInt(rowData?.fundingGoal || '0', 10) || 100
|
||||||
|
|
||||||
|
const totalCount = orderCount + fakeOrderCount
|
||||||
|
const percentage = fundingGoal > 0 ? Math.min(Math.round((totalCount / fundingGoal) * 100), 100) : 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="preorder-progress-info">
|
||||||
|
<div className="progress-label">
|
||||||
|
<span>预购进度</span>
|
||||||
|
<span className="progress-count">
|
||||||
|
{totalCount} / {fundingGoal}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="progress-bar">
|
||||||
|
<div
|
||||||
|
className="progress-fill"
|
||||||
|
style={{ width: `${percentage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="progress-stats">
|
||||||
|
<div className="stat-item">
|
||||||
|
<span className="stat-label">真实</span>
|
||||||
|
<span className="stat-value">{orderCount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="stat-item">
|
||||||
|
<span className="stat-label">Fake</span>
|
||||||
|
<span className="stat-value">{fakeOrderCount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="stat-item">
|
||||||
|
<span className="stat-label">完成度</span>
|
||||||
|
<span className="stat-value">{percentage}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,21 @@
|
||||||
'use client'
|
'use client'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
|
||||||
export const ThumbnailCell = (props: any) => {
|
export const ThumbnailCell = (props: any) => {
|
||||||
console.log('=== ThumbnailCell All Props ===', props)
|
const pathname = usePathname()
|
||||||
console.log('Props keys:', Object.keys(props))
|
|
||||||
|
// 从 URL 路径中提取 collection slug
|
||||||
|
const collectionSlug = pathname?.includes('/preorder-products')
|
||||||
|
? 'preorder-products'
|
||||||
|
: 'products'
|
||||||
|
|
||||||
// 尝试从不同的 props 路径获取值
|
// 尝试从不同的 props 路径获取值
|
||||||
const value = props.value || props.cellData || props.data
|
const value = props.value || props.cellData || props.data
|
||||||
const rowData = props.rowData || props.row
|
const rowData = props.rowData || props.row
|
||||||
|
|
||||||
console.log('Extracted value:', value)
|
|
||||||
console.log('Extracted rowData:', rowData)
|
|
||||||
|
|
||||||
const isImage = typeof value === 'string' && value.match(/^https?:\/\/.+/)
|
const isImage = typeof value === 'string' && value.match(/^https?:\/\/.+/)
|
||||||
const editUrl = `/admin/collections/products/${rowData?.id || ''}`
|
const editUrl = `/admin/collections/${collectionSlug}/${rowData?.id || ''}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Button, useDocumentInfo } from '@payloadcms/ui'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑页面内的订单计数刷新组件
|
||||||
|
* 只针对当前编辑的预购商品刷新订单数据
|
||||||
|
*/
|
||||||
|
export function RefreshOrderCountField() {
|
||||||
|
const { id } = useDocumentInfo()
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
if (!id) {
|
||||||
|
setMessage('⚠️ 无法获取商品 ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setMessage('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/refresh-order-counts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
productIds: [id],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage(`✅ ${data.message || '订单计数刷新成功!'}`)
|
||||||
|
// 刷新页面数据
|
||||||
|
setTimeout(() => {
|
||||||
|
router.refresh()
|
||||||
|
// 重新加载页面以更新显示
|
||||||
|
window.location.reload()
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
setMessage(`❌ 刷新失败: ${data.error || '未知错误'}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setMessage('❌ 刷新出错: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '1rem',
|
||||||
|
border: '1px solid var(--theme-elevation-150)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: 'var(--theme-elevation-50)',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: '0.75rem' }}>
|
||||||
|
<h4 style={{ margin: '0 0 0.5rem 0', fontSize: '0.875rem', fontWeight: 600 }}>
|
||||||
|
📊 订单计数同步
|
||||||
|
</h4>
|
||||||
|
<p style={{ margin: 0, fontSize: '0.8125rem', color: 'var(--theme-elevation-600)' }}>
|
||||||
|
从 Medusa 订单系统同步真实订单计数数据
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={loading}
|
||||||
|
buttonStyle="primary"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{loading ? '同步中...' : '🔄 刷新订单计数'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{message && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '0.75rem',
|
||||||
|
marginTop: '0.75rem',
|
||||||
|
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.8125rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: '0.75rem',
|
||||||
|
padding: '0.75rem',
|
||||||
|
backgroundColor: 'var(--theme-elevation-100)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '0.8125rem',
|
||||||
|
color: 'var(--theme-elevation-600)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p style={{ margin: '0.25rem 0', fontWeight: 600 }}>💡 说明:</p>
|
||||||
|
<p style={{ margin: '0.25rem 0' }}>
|
||||||
|
• <strong>真实订单计数</strong>:从 Medusa 自动同步,只读
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: '0.25rem 0' }}>
|
||||||
|
• <strong>Fake计数</strong>:上方可手动编辑
|
||||||
|
</p>
|
||||||
|
<p style={{ margin: '0.25rem 0' }}>
|
||||||
|
• <strong>显示进度</strong> = 真实订单 + Fake计数
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
'use client'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import './preorder-product-grid-styler.scss'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预购商品网格样式组件
|
||||||
|
* 将列表转换为卡片网格,显示预购进度
|
||||||
|
*/
|
||||||
|
export function PreorderProductGridStyler() {
|
||||||
|
useEffect(() => {
|
||||||
|
// 组件加载时添加样式类
|
||||||
|
const table = document.querySelector('.collection-list--preorder-products')
|
||||||
|
if (table) {
|
||||||
|
table.classList.add('preorder-grid-view')
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const table = document.querySelector('.collection-list--preorder-products')
|
||||||
|
if (table) {
|
||||||
|
table.classList.remove('preorder-grid-view')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return null // 这是一个纯样式组件,不渲染任何内容
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
// 预购商品网格视图样式
|
||||||
|
// 将表格转换为卡片网格,显示预购进度
|
||||||
|
|
||||||
|
.collection-list.collection-list--preorder-products.preorder-grid-view {
|
||||||
|
// 隐藏表头
|
||||||
|
thead {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主体使用 Grid 布局
|
||||||
|
tbody {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每个 tr 变成卡片
|
||||||
|
tr {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid var(--theme-elevation-150);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--theme-elevation-0);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
border-color: var(--theme-elevation-300);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选中状态
|
||||||
|
&.row-selected {
|
||||||
|
border-color: var(--theme-success-500);
|
||||||
|
background: var(--theme-success-50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有单元格
|
||||||
|
td {
|
||||||
|
border: none !important;
|
||||||
|
padding: 0.25rem 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: none !important;
|
||||||
|
|
||||||
|
// 隐藏不需要的列
|
||||||
|
&:not([class*='thumbnail']):not([class*='title']):not([class*='medusaId']):not([class*='status']):not([class*='updatedAt']) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缩略图
|
||||||
|
td[class*='thumbnail'] {
|
||||||
|
order: -1;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--theme-elevation-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有图片,显示占位符
|
||||||
|
&:empty::before {
|
||||||
|
content: '📦';
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
font-size: 4rem;
|
||||||
|
background: var(--theme-elevation-100);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--theme-elevation-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
td[class*='title'] {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--theme-elevation-1000);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
// 限制两行,超出省略
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Medusa ID
|
||||||
|
td[class*='medusaId'] {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--theme-elevation-500);
|
||||||
|
font-family: monospace;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '🆔 ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态标签
|
||||||
|
td[class*='status'] {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.pill--published {
|
||||||
|
background: var(--theme-success-100);
|
||||||
|
color: var(--theme-success-900);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pill--draft {
|
||||||
|
background: var(--theme-warning-100);
|
||||||
|
color: var(--theme-warning-900);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新时间
|
||||||
|
td[class*='updatedAt'] {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--theme-elevation-500);
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 0.75rem !important;
|
||||||
|
border-top: 1px solid var(--theme-elevation-100);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '🕐 ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复选框单元格
|
||||||
|
td:first-child {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
width: auto !important;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid var(--theme-elevation-400);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
background: var(--theme-success-500);
|
||||||
|
border-color: var(--theme-success-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作按钮单元格
|
||||||
|
td:last-child {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预购进度信息(需要通过自定义 Cell 组件添加)
|
||||||
|
.preorder-progress-info {
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--theme-elevation-50);
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--theme-elevation-150);
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--theme-elevation-600);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
.progress-count {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--theme-elevation-900);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: var(--theme-elevation-150);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--theme-success-500), var(--theme-success-600));
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
color: var(--theme-elevation-500);
|
||||||
|
margin-bottom: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--theme-elevation-900);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// 这是为了覆盖 Payload 默认表格样式的 SCSS
|
// 这是为了覆盖 Payload 默认表格样式的 SCSS
|
||||||
// 我们使用 CSS Grid 强制改变表格布局,从而实现 Grid 视图,同时保留 Payload 所有原生功能
|
// 我们使用 CSS Grid 强制改变表格布局,从而实现 Grid 视图,同时保留 Payload 所有原生功能
|
||||||
|
|
||||||
.collection-list.collection-list--products {
|
.collection-list.collection-list--products,
|
||||||
|
.collection-list.collection-list--preorder-products {
|
||||||
table {
|
table {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
'use client'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { Button } from '@payloadcms/ui'
|
|
||||||
import { useDocumentInfo } from '@payloadcms/ui'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 单个商品强制同步按钮
|
|
||||||
* 用于在商品编辑页面强制更新该商品的信息
|
|
||||||
*/
|
|
||||||
export function ForceSyncButton() {
|
|
||||||
const { id, collectionSlug } = useDocumentInfo()
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [message, setMessage] = useState('')
|
|
||||||
|
|
||||||
const handleForceSync = async () => {
|
|
||||||
if (!id) {
|
|
||||||
setMessage('❌ 无法获取商品 ID')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirm('确定要从 Medusa 强制更新此商品吗?这将覆盖当前的商品信息。')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
setMessage('')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`/api/sync-medusa?payloadId=${id}&collection=${collectionSlug}&forceUpdate=true`,
|
|
||||||
{
|
|
||||||
method: 'GET',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
setMessage('✅ ' + (data.message || '强制同步成功!'))
|
|
||||||
// 刷新页面显示更新后的数据
|
|
||||||
setTimeout(() => window.location.reload(), 1500)
|
|
||||||
} else {
|
|
||||||
setMessage(`❌ 同步失败: ${data.error || data.message}`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setMessage(`❌ 同步出错: ${error instanceof Error ? error.message : '未知错误'}`)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
marginTop: '1rem',
|
|
||||||
padding: '1rem',
|
|
||||||
background: 'var(--theme-elevation-50)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
border: '1px solid var(--theme-elevation-100)',
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '0.5rem',
|
|
||||||
marginBottom: '0.75rem',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: '1.25rem' }}>🔄</span>
|
|
||||||
从 Medusa 同步数据
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={handleForceSync}
|
|
||||||
disabled={loading}
|
|
||||||
buttonStyle="secondary"
|
|
||||||
>
|
|
||||||
⚡ {loading ? '同步中...' : '強制更新此商品'}
|
|
||||||
</Button>
|
|
||||||
{message && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
marginTop: '0.75rem',
|
|
||||||
padding: '0.75rem',
|
|
||||||
backgroundColor:
|
|
||||||
message.includes('❌')
|
|
||||||
? 'var(--theme-error-50)'
|
|
||||||
: 'var(--theme-success-50)',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div style={{
|
|
||||||
marginTop: '0.75rem',
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
color: 'var(--theme-elevation-400)',
|
|
||||||
}}>
|
|
||||||
💡 此操作将从 Medusa 获取最新数据并覆盖当前商品信息
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
'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/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/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,5 +1,5 @@
|
||||||
'use client'
|
'use client'
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Button, useSelection } from '@payloadcms/ui'
|
import { Button, useSelection } from '@payloadcms/ui'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
|
|
@ -13,13 +13,19 @@ export function UnifiedSyncButton() {
|
||||||
const [message, setMessage] = useState('')
|
const [message, setMessage] = useState('')
|
||||||
const [showForceAllConfirm, setShowForceAllConfirm] = useState(false)
|
const [showForceAllConfirm, setShowForceAllConfirm] = useState(false)
|
||||||
const [confirmText, setConfirmText] = useState('')
|
const [confirmText, setConfirmText] = useState('')
|
||||||
|
const [collectionSlug, setCollectionSlug] = useState('products')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// 获取当前页面的 collection slug
|
// 在客户端确定 collection slug,避免 hydration 错误
|
||||||
const pathname = typeof window !== 'undefined' ? window.location.pathname : ''
|
useEffect(() => {
|
||||||
const collectionSlug = pathname.includes('preorder-products')
|
if (typeof window !== 'undefined') {
|
||||||
? 'preorder-products'
|
const pathname = window.location.pathname
|
||||||
: 'products'
|
const slug = pathname.includes('preorder-products')
|
||||||
|
? 'preorder-products'
|
||||||
|
: 'products'
|
||||||
|
setCollectionSlug(slug)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// 同步新商品
|
// 同步新商品
|
||||||
const handleSyncNew = async () => {
|
const handleSyncNew = async () => {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
||||||
const MEDUSA_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || ''
|
const MEDUSA_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || ''
|
||||||
|
// Payload API key - 用于同步产品数据(包括 metadata)
|
||||||
|
const PAYLOAD_API_KEY = process.env.PAYLOAD_API_KEY || ''
|
||||||
|
|
||||||
interface MedusaProduct {
|
interface MedusaProduct {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -47,25 +49,28 @@ interface MedusaResponse<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有 Medusa 商品
|
* 获取所有 Medusa 商品(使用 admin API 以获取 metadata)
|
||||||
*/
|
*/
|
||||||
export async function getAllMedusaProducts(): Promise<MedusaProduct[]> {
|
export async function getAllMedusaProducts(): Promise<MedusaProduct[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${MEDUSA_BACKEND_URL}/store/products`, {
|
const response = await fetch(`${MEDUSA_BACKEND_URL}/store/products-sync`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY,
|
'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY,
|
||||||
|
'x-payload-api-key': PAYLOAD_API_KEY,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch products: ${response.statusText}`)
|
const errorText = await response.text()
|
||||||
|
console.error(`[getAllMedusaProducts] Error response:`, errorText)
|
||||||
|
throw new Error(`Failed to fetch products: ${response.status} ${response.statusText} - ${errorText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: MedusaResponse<MedusaProduct> = await response.json()
|
const data: MedusaResponse<MedusaProduct> = await response.json()
|
||||||
return data.products || []
|
return data.products || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching Medusa products:', error)
|
console.error('[getAllMedusaProducts] Error fetching Medusa products:', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,30 +104,22 @@ export async function getMedusaProduct(productId: string): Promise<MedusaProduct
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页获取 Medusa 商品
|
* 分页获取 Medusa 商品
|
||||||
|
* 注意:由于使用自定义同步端点,实际会获取所有产品然后在内存中分页
|
||||||
*/
|
*/
|
||||||
export async function getMedusaProductsPaginated(
|
export async function getMedusaProductsPaginated(
|
||||||
offset: number = 0,
|
offset: number = 0,
|
||||||
limit: number = 100,
|
limit: number = 100,
|
||||||
): Promise<{ products: MedusaProduct[]; count: number }> {
|
): Promise<{ products: MedusaProduct[]; count: number }> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
// 获取所有产品
|
||||||
`${MEDUSA_BACKEND_URL}/store/products?offset=${offset}&limit=${limit}`,
|
const allProducts = await getAllMedusaProducts()
|
||||||
{
|
|
||||||
headers: {
|
// 在内存中分页
|
||||||
'Content-Type': 'application/json',
|
const products = allProducts.slice(offset, offset + limit)
|
||||||
'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch products: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: MedusaResponse<MedusaProduct> = await response.json()
|
|
||||||
return {
|
return {
|
||||||
products: data.products || [],
|
products,
|
||||||
count: data.count || 0,
|
count: allProducts.length,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching Medusa products (paginated):', error)
|
console.error('Error fetching Medusa products (paginated):', error)
|
||||||
|
|
@ -178,12 +175,21 @@ export async function uploadImageFromUrl(imageUrl: string, payload: any): Promis
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 判断产品是否为预售商品
|
* 判断产品是否为预售商品
|
||||||
* 检查 metadata.is_preorder (支持布尔值和字符串)
|
* 检查 metadata.is_preorder 和 metadata.preorder (支持布尔值和字符串)
|
||||||
*/
|
*/
|
||||||
export function isPreorderProduct(product: MedusaProduct): boolean {
|
export function isPreorderProduct(product: MedusaProduct): boolean {
|
||||||
const isPreorder = product.metadata?.is_preorder
|
const isPreorderValue = product.metadata?.is_preorder
|
||||||
// 支持布尔值 true 或字符串 "true"
|
const preorderValue = product.metadata?.preorder
|
||||||
return isPreorder === true || isPreorder === 'true'
|
|
||||||
|
// 尝试两个字段,支持多种类型
|
||||||
|
const value = isPreorderValue !== undefined ? isPreorderValue : preorderValue
|
||||||
|
|
||||||
|
// 支持布尔值 true、字符串 "true"、数字 1、字符串 "1"
|
||||||
|
if (value === true || value === 'true' || value === '1' || value === 1) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,6 @@ export interface Product {
|
||||||
* 商品标题(从 Medusa 同步)
|
* 商品标题(从 Medusa 同步)
|
||||||
*/
|
*/
|
||||||
title: string;
|
title: string;
|
||||||
handle?: string | null;
|
|
||||||
/**
|
/**
|
||||||
* 商品封面 URL(支持上传或输入 URL)
|
* 商品封面 URL(支持上传或输入 URL)
|
||||||
*/
|
*/
|
||||||
|
|
@ -298,11 +297,7 @@ export interface PreorderProduct {
|
||||||
*/
|
*/
|
||||||
title: string;
|
title: string;
|
||||||
/**
|
/**
|
||||||
* 商品 URL handle(从 Medusa 同步)
|
* 商品封面 URL(支持上传或输入 URL)
|
||||||
*/
|
|
||||||
handle?: string | null;
|
|
||||||
/**
|
|
||||||
* 商品缩略图 URL(从 Medusa 同步)
|
|
||||||
*/
|
*/
|
||||||
thumbnail?: string | null;
|
thumbnail?: string | null;
|
||||||
/**
|
/**
|
||||||
|
|
@ -317,22 +312,18 @@ export interface PreorderProduct {
|
||||||
* 众筹目标数量(0 表示以变体 max_orders 总和为准)
|
* 众筹目标数量(0 表示以变体 max_orders 总和为准)
|
||||||
*/
|
*/
|
||||||
fundingGoal: number;
|
fundingGoal: number;
|
||||||
/**
|
|
||||||
* 预计发货日期
|
|
||||||
*/
|
|
||||||
estimatedShipDate?: string | null;
|
|
||||||
/**
|
/**
|
||||||
* 预购结束日期(可选)
|
* 预购结束日期(可选)
|
||||||
*/
|
*/
|
||||||
preorderEndDate?: string | null;
|
preorderEndDate?: string | null;
|
||||||
/**
|
/**
|
||||||
* 最小起订量
|
* 真实订单计数(从 Medusa 同步)
|
||||||
*/
|
*/
|
||||||
minOrderQuantity?: number | null;
|
orderCount?: number | null;
|
||||||
/**
|
/**
|
||||||
* 最大购买数量(0 表示不限制)
|
* Fake 订单计数(手动设置)
|
||||||
*/
|
*/
|
||||||
maxOrderQuantity?: number | null;
|
fakeOrderCount?: number | null;
|
||||||
/**
|
/**
|
||||||
* 预售商品的详细描述(支持富文本编辑)
|
* 预售商品的详细描述(支持富文本编辑)
|
||||||
*/
|
*/
|
||||||
|
|
@ -826,7 +817,6 @@ export interface ProductsSelect<T extends boolean = true> {
|
||||||
status?: T;
|
status?: T;
|
||||||
seedId?: T;
|
seedId?: T;
|
||||||
title?: T;
|
title?: T;
|
||||||
handle?: T;
|
|
||||||
thumbnail?: T;
|
thumbnail?: T;
|
||||||
lastSyncedAt?: T;
|
lastSyncedAt?: T;
|
||||||
content?: T;
|
content?: T;
|
||||||
|
|
@ -852,15 +842,13 @@ export interface PreorderProductsSelect<T extends boolean = true> {
|
||||||
status?: T;
|
status?: T;
|
||||||
seedId?: T;
|
seedId?: T;
|
||||||
title?: T;
|
title?: T;
|
||||||
handle?: T;
|
|
||||||
thumbnail?: T;
|
thumbnail?: T;
|
||||||
lastSyncedAt?: T;
|
lastSyncedAt?: T;
|
||||||
preorderType?: T;
|
preorderType?: T;
|
||||||
fundingGoal?: T;
|
fundingGoal?: T;
|
||||||
estimatedShipDate?: T;
|
|
||||||
preorderEndDate?: T;
|
preorderEndDate?: T;
|
||||||
minOrderQuantity?: T;
|
orderCount?: T;
|
||||||
maxOrderQuantity?: T;
|
fakeOrderCount?: T;
|
||||||
description?: T;
|
description?: T;
|
||||||
relatedProducts?: T;
|
relatedProducts?: T;
|
||||||
taobaoLinks?:
|
taobaoLinks?:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue