Compare commits
No commits in common. "35928a6144255e7daa2e51ac319e19317e826e17" and "9a47af76cef1ca9e39881ebda3b82b37bf04f02a" have entirely different histories.
35928a6144
...
9a47af76ce
16
.env.example
16
.env.example
|
|
@ -4,16 +4,14 @@ DATABASE_URL=postgresql://user:password@localhost:5432/database
|
|||
# Payload
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_URL=redis://localhost:6379
|
||||
# Redis 配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# API Keys (统一使用 PAYLOAD_API_KEY)
|
||||
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
|
||||
# Store API Key(用于访问 hero-slider 和 product-recommendations 接口)
|
||||
STORE_API_KEY=your-store-api-key-here
|
||||
|
||||
# Cloudflare R2 配置
|
||||
CLOUDFLARE_R2_BUCKET=your-bucket
|
||||
|
|
|
|||
|
|
@ -25,14 +25,12 @@ import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997e
|
|||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField'
|
||||
import { TaobaoLinkPreview as TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959 } from '../../../components/fields/TaobaoLinkPreview'
|
||||
import { UnifiedSyncButton as UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523 } from '../../../components/sync/UnifiedSyncButton'
|
||||
import { UnifiedSyncButton as UnifiedSyncButton_8f3d4a2e1b5c6d7a9e0f1a2b3c4d5e6f } from '../../../components/sync/UnifiedSyncButton'
|
||||
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 { ForceSyncButton as ForceSyncButton_28396efe36d6238add95cf44109e281c } from '../../../components/sync/ForceSyncButton'
|
||||
import { TaobaoLinkPreview as TaobaoLinkPreview_7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b } from '../../../components/fields/TaobaoLinkPreview'
|
||||
import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f } from '../../../components/fields/PreorderOrdersField'
|
||||
import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler'
|
||||
import { default as default_767734c8b7b095ea28d54c32abcf46e4 } from '../../../components/views/AdminPanel'
|
||||
import { default as default_a766ef013722c08f9bb937940272cb5f } from '../../../components/views/LogsManagerView'
|
||||
import { S3ClientUploadHandler as S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24 } from '@payloadcms/storage-s3/client'
|
||||
|
|
@ -66,14 +64,12 @@ export const importMap = {
|
|||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426,
|
||||
"/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959,
|
||||
"/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523,
|
||||
"/components/sync/UnifiedSyncButton#UnifiedSyncButton": UnifiedSyncButton_8f3d4a2e1b5c6d7a9e0f1a2b3c4d5e6f,
|
||||
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
|
||||
"/components/cells/PreorderProgressCell#PreorderProgressCell": PreorderProgressCell_67df47753573233f0c83480de687f13b,
|
||||
"/components/fields/RefreshOrderCountField#RefreshOrderCountField": RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996,
|
||||
"/components/sync/ForceSyncButton#ForceSyncButton": ForceSyncButton_28396efe36d6238add95cf44109e281c,
|
||||
"/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b,
|
||||
"@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f,
|
||||
"/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5,
|
||||
"/components/views/AdminPanel#default": default_767734c8b7b095ea28d54c32abcf46e4,
|
||||
"/components/views/LogsManagerView#default": default_a766ef013722c08f9bb937940272cb5f,
|
||||
"@payloadcms/storage-s3/client#S3ClientUploadHandler": S3ClientUploadHandler_f97aa6c64367fa259c5bc0567239ef24,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ export async function GET(request: Request) {
|
|||
|
||||
const results = {
|
||||
products: 0,
|
||||
preorderProducts: 0,
|
||||
announcements: 0,
|
||||
articles: 0,
|
||||
errors: [] as string[],
|
||||
|
|
@ -48,20 +47,6 @@ export async function GET(request: Request) {
|
|||
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
|
||||
try {
|
||||
const deletedAnnouncements = await payload.delete({
|
||||
|
|
@ -92,7 +77,7 @@ export async function GET(request: Request) {
|
|||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `数据清理完成!已删除 ${results.products} 个商品、${results.preorderProducts} 个预购商品、${results.announcements} 个公告、${results.articles} 个文章`,
|
||||
message: `数据清理完成!已删除 ${results.products} 个商品、${results.announcements} 个公告、${results.articles} 个文章`,
|
||||
results,
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
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,6 +89,7 @@ export async function GET(
|
|||
preorder: {
|
||||
id: product.id,
|
||||
title: product.title,
|
||||
handle: product.handle,
|
||||
description: product.description,
|
||||
status: product._status,
|
||||
thumbnail: product.thumbnail,
|
||||
|
|
@ -101,13 +102,11 @@ export async function GET(
|
|||
// 预购元数据(从 Payload 管理)
|
||||
is_preorder: true,
|
||||
preorder_type: product.preorderType || 'standard',
|
||||
estimated_ship_date: product.estimatedShipDate || null,
|
||||
preorder_end_date: product.preorderEndDate || null,
|
||||
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),
|
||||
min_order_quantity: product.minOrderQuantity || 1,
|
||||
max_order_quantity: product.maxOrderQuantity || 0,
|
||||
|
||||
// 统计数据
|
||||
current_orders: totalOrders,
|
||||
|
|
@ -153,10 +152,12 @@ export async function GET(
|
|||
* - increment?: number - 增加订单数
|
||||
* - decrement?: number - 减少订单数
|
||||
*
|
||||
* - estimated_ship_date?: string - 更新预估发货日期
|
||||
* - preorder_end_date?: string - 更新预购结束日期
|
||||
* - funding_goal?: number - 更新众筹目标
|
||||
* - preorder_type?: string - 更新预购类型
|
||||
* - fake_order_count?: number - 更新 Fake 订单计数
|
||||
* - min_order_quantity?: number - 最小起订量
|
||||
* - max_order_quantity?: number - 最大购买数量
|
||||
*/
|
||||
export async function PATCH(
|
||||
req: NextRequest,
|
||||
|
|
@ -173,10 +174,12 @@ export async function PATCH(
|
|||
max_orders,
|
||||
increment,
|
||||
decrement,
|
||||
estimated_ship_date,
|
||||
preorder_end_date,
|
||||
funding_goal,
|
||||
preorder_type,
|
||||
fake_order_count,
|
||||
min_order_quantity,
|
||||
max_order_quantity,
|
||||
} = body
|
||||
|
||||
// 获取产品
|
||||
|
|
@ -282,6 +285,9 @@ export async function PATCH(
|
|||
// 模式2: 更新产品级别预购元数据(在 Payload 中管理)
|
||||
const updateData: any = {}
|
||||
|
||||
if (estimated_ship_date !== undefined) {
|
||||
updateData.estimatedShipDate = estimated_ship_date
|
||||
}
|
||||
if (preorder_end_date !== undefined) {
|
||||
updateData.preorderEndDate = preorder_end_date
|
||||
}
|
||||
|
|
@ -291,8 +297,11 @@ export async function PATCH(
|
|||
if (preorder_type !== undefined) {
|
||||
updateData.preorderType = preorder_type
|
||||
}
|
||||
if (fake_order_count !== undefined) {
|
||||
updateData.fakeOrderCount = Math.max(0, fake_order_count)
|
||||
if (min_order_quantity !== undefined) {
|
||||
updateData.minOrderQuantity = min_order_quantity
|
||||
}
|
||||
if (max_order_quantity !== undefined) {
|
||||
updateData.maxOrderQuantity = max_order_quantity
|
||||
}
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export async function GET(req: NextRequest) {
|
|||
return {
|
||||
id: product.id,
|
||||
title: product.title,
|
||||
handle: product.handle,
|
||||
status: product._status,
|
||||
thumbnail: product.thumbnail,
|
||||
description: product.description,
|
||||
|
|
@ -86,13 +87,9 @@ export async function GET(req: NextRequest) {
|
|||
|
||||
// 预购元数据
|
||||
preorder_type: product.preorderType || 'standard',
|
||||
estimated_ship_date: product.estimatedShipDate || null,
|
||||
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,
|
||||
total_max_orders: totalMaxOrders,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export async function GET(req: NextRequest) {
|
|||
try {
|
||||
// 验证 API Key
|
||||
const apiKey = req.headers.get('x-store-api-key')
|
||||
const validApiKey = process.env.PAYLOAD_API_KEY
|
||||
const validApiKey = process.env.STORE_API_KEY
|
||||
|
||||
if (!apiKey || !validApiKey || apiKey !== validApiKey) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export async function GET(req: NextRequest) {
|
|||
try {
|
||||
// 验证 API Key
|
||||
const apiKey = req.headers.get('x-store-api-key')
|
||||
const validApiKey = process.env.PAYLOAD_API_KEY
|
||||
const validApiKey = process.env.STORE_API_KEY
|
||||
|
||||
if (!apiKey || !validApiKey || apiKey !== validApiKey) {
|
||||
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
|
||||
|
|
|
|||
|
|
@ -1,163 +0,0 @@
|
|||
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 {
|
||||
// 可选的 API Key 验证
|
||||
const authHeader = request.headers.get('authorization')
|
||||
const payloadApiKey = process.env.PAYLOAD_API_KEY
|
||||
const storeApiKey = process.env.STORE_API_KEY
|
||||
|
||||
// 如果配置了 PAYLOAD_API_KEY,则验证请求
|
||||
if (payloadApiKey && authHeader) {
|
||||
// 如果配置了 STORE_API_KEY,则验证请求
|
||||
if (storeApiKey && authHeader) {
|
||||
const token = authHeader.replace('Bearer ', '')
|
||||
if (token !== payloadApiKey) {
|
||||
if (token !== storeApiKey) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
|
|
|
|||
|
|
@ -27,24 +27,24 @@ export const PreorderProducts: CollectionConfig = {
|
|||
slug: 'preorder-products',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['thumbnail', 'title', 'medusaId', 'progress', 'status', 'updatedAt'],
|
||||
defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'],
|
||||
description: '管理预售商品的详细内容和描述',
|
||||
listSearchableFields: ['title', 'medusaId'],
|
||||
listSearchableFields: ['title', 'medusaId', 'handle'],
|
||||
pagination: {
|
||||
defaultLimit: 25,
|
||||
},
|
||||
components: {
|
||||
edit: {
|
||||
PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton',
|
||||
},
|
||||
beforeListTable: [
|
||||
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
|
||||
'/components/list/PreorderProductGridStyler#PreorderProductGridStyler',
|
||||
'/components/list/ProductGridStyler',
|
||||
],
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: () => true, // 公开可读
|
||||
create: ({ req: { user } }) => !!user, // 登录用户可创建
|
||||
update: ({ req: { user } }) => !!user, // 登录用户可更新
|
||||
delete: ({ req: { user } }) => !!user, // 登录用户可删除
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
|
|
@ -103,15 +103,20 @@ export const PreorderProducts: CollectionConfig = {
|
|||
readOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'handle',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: '商品 URL handle(从 Medusa 同步)',
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'thumbnail',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: '商品封面 URL(支持上传或输入 URL)',
|
||||
components: {
|
||||
Cell: '/components/cells/ThumbnailCell#ThumbnailCell',
|
||||
Field: '/components/fields/ThumbnailField#ThumbnailField',
|
||||
},
|
||||
description: '商品缩略图 URL(从 Medusa 同步)',
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -125,15 +130,6 @@ export const PreorderProducts: CollectionConfig = {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'progress',
|
||||
type: 'ui',
|
||||
admin: {
|
||||
components: {
|
||||
Cell: '/components/cells/PreorderProgressCell#PreorderProgressCell',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -169,6 +165,16 @@ export const PreorderProducts: CollectionConfig = {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'estimatedShipDate',
|
||||
type: 'date',
|
||||
admin: {
|
||||
description: '预计发货日期',
|
||||
date: {
|
||||
displayFormat: 'yyyy-MM-dd',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'preorderEndDate',
|
||||
type: 'date',
|
||||
|
|
@ -179,34 +185,23 @@ export const PreorderProducts: CollectionConfig = {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ui',
|
||||
name: 'refreshOrderCount',
|
||||
admin: {
|
||||
components: {
|
||||
Field: '/components/fields/RefreshOrderCountField#RefreshOrderCountField',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'orderCount',
|
||||
name: 'minOrderQuantity',
|
||||
type: 'number',
|
||||
defaultValue: 0,
|
||||
defaultValue: 1,
|
||||
admin: {
|
||||
description: '真实订单计数(从 Medusa 同步)',
|
||||
readOnly: true,
|
||||
description: '最小起订量',
|
||||
width: '50%',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'fakeOrderCount',
|
||||
name: 'maxOrderQuantity',
|
||||
type: 'number',
|
||||
defaultValue: 0,
|
||||
admin: {
|
||||
description: 'Fake 订单计数(手动设置)',
|
||||
description: '最大购买数量(0 表示不限制)',
|
||||
width: '50%',
|
||||
},
|
||||
},
|
||||
|
|
@ -288,17 +283,6 @@ export const PreorderProducts: CollectionConfig = {
|
|||
Field: '/components/fields/RelatedProductsField#RelatedProductsField',
|
||||
},
|
||||
},
|
||||
filterOptions: ({ relationTo, data }) => {
|
||||
// 过滤掉当前商品本身,避免自引用
|
||||
if (data?.id) {
|
||||
return {
|
||||
id: {
|
||||
not_equals: data.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -29,11 +29,14 @@ export const Products: CollectionConfig = {
|
|||
useAsTitle: 'title',
|
||||
defaultColumns: ['thumbnail', 'title', 'medusaId', 'status', 'updatedAt'],
|
||||
description: '管理 Medusa 商品的详细内容和描述',
|
||||
listSearchableFields: ['title', 'medusaId'],
|
||||
listSearchableFields: ['title', 'medusaId', 'handle'],
|
||||
pagination: {
|
||||
defaultLimit: 25,
|
||||
},
|
||||
components: {
|
||||
edit: {
|
||||
PreviewButton: '/components/sync/ForceSyncButton#ForceSyncButton',
|
||||
},
|
||||
beforeListTable: [
|
||||
'/components/sync/UnifiedSyncButton#UnifiedSyncButton',
|
||||
'/components/list/ProductGridStyler',
|
||||
|
|
@ -106,6 +109,13 @@ export const Products: CollectionConfig = {
|
|||
readOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'handle',
|
||||
type: 'text',
|
||||
admin: {
|
||||
hidden: true, // 隐藏字段,但保留用于搜索
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'thumbnail',
|
||||
type: 'text',
|
||||
|
|
@ -196,17 +206,6 @@ export const Products: CollectionConfig = {
|
|||
Field: '/components/fields/RelatedProductsField#RelatedProductsField',
|
||||
},
|
||||
},
|
||||
filterOptions: ({ relationTo, data }) => {
|
||||
// 过滤掉当前商品本身,避免自引用
|
||||
if (data?.id) {
|
||||
return {
|
||||
id: {
|
||||
not_equals: data.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
'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,21 +1,19 @@
|
|||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
export const ThumbnailCell = (props: any) => {
|
||||
const pathname = usePathname()
|
||||
|
||||
// 从 URL 路径中提取 collection slug
|
||||
const collectionSlug = pathname?.includes('/preorder-products')
|
||||
? 'preorder-products'
|
||||
: 'products'
|
||||
console.log('=== ThumbnailCell All Props ===', props)
|
||||
console.log('Props keys:', Object.keys(props))
|
||||
|
||||
// 尝试从不同的 props 路径获取值
|
||||
const value = props.value || props.cellData || props.data
|
||||
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 editUrl = `/admin/collections/${collectionSlug}/${rowData?.id || ''}`
|
||||
const editUrl = `/admin/collections/products/${rowData?.id || ''}`
|
||||
|
||||
return (
|
||||
<Link
|
||||
|
|
|
|||
|
|
@ -1,130 +0,0 @@
|
|||
'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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
'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 // 这是一个纯样式组件,不渲染任何内容
|
||||
}
|
||||
|
|
@ -1,240 +0,0 @@
|
|||
// 预购商品网格视图样式
|
||||
// 将表格转换为卡片网格,显示预购进度
|
||||
|
||||
.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,8 +1,7 @@
|
|||
// 这是为了覆盖 Payload 默认表格样式的 SCSS
|
||||
// 我们使用 CSS Grid 强制改变表格布局,从而实现 Grid 视图,同时保留 Payload 所有原生功能
|
||||
|
||||
.collection-list.collection-list--products,
|
||||
.collection-list.collection-list--preorder-products {
|
||||
.collection-list.collection-list--products {
|
||||
table {
|
||||
display: block !important;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
'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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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/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'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Button, useSelection } from '@payloadcms/ui'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
|
|
@ -13,19 +13,13 @@ export function UnifiedSyncButton() {
|
|||
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)
|
||||
}
|
||||
}, [])
|
||||
// 获取当前页面的 collection slug
|
||||
const pathname = typeof window !== 'undefined' ? window.location.pathname : ''
|
||||
const collectionSlug = pathname.includes('preorder-products')
|
||||
? 'preorder-products'
|
||||
: 'products'
|
||||
|
||||
// 同步新商品
|
||||
const handleSyncNew = async () => {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@
|
|||
|
||||
const MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
||||
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 {
|
||||
id: string
|
||||
|
|
@ -49,28 +47,25 @@ interface MedusaResponse<T> {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取所有 Medusa 商品(使用 admin API 以获取 metadata)
|
||||
* 获取所有 Medusa 商品
|
||||
*/
|
||||
export async function getAllMedusaProducts(): Promise<MedusaProduct[]> {
|
||||
try {
|
||||
const response = await fetch(`${MEDUSA_BACKEND_URL}/store/products-sync`, {
|
||||
const response = await fetch(`${MEDUSA_BACKEND_URL}/store/products`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY,
|
||||
'x-payload-api-key': PAYLOAD_API_KEY,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error(`[getAllMedusaProducts] Error response:`, errorText)
|
||||
throw new Error(`Failed to fetch products: ${response.status} ${response.statusText} - ${errorText}`)
|
||||
throw new Error(`Failed to fetch products: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data: MedusaResponse<MedusaProduct> = await response.json()
|
||||
return data.products || []
|
||||
} catch (error) {
|
||||
console.error('[getAllMedusaProducts] Error fetching Medusa products:', error)
|
||||
console.error('Error fetching Medusa products:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
|
@ -104,22 +99,30 @@ export async function getMedusaProduct(productId: string): Promise<MedusaProduct
|
|||
|
||||
/**
|
||||
* 分页获取 Medusa 商品
|
||||
* 注意:由于使用自定义同步端点,实际会获取所有产品然后在内存中分页
|
||||
*/
|
||||
export async function getMedusaProductsPaginated(
|
||||
offset: number = 0,
|
||||
limit: number = 100,
|
||||
): Promise<{ products: MedusaProduct[]; count: number }> {
|
||||
try {
|
||||
// 获取所有产品
|
||||
const allProducts = await getAllMedusaProducts()
|
||||
const response = await fetch(
|
||||
`${MEDUSA_BACKEND_URL}/store/products?offset=${offset}&limit=${limit}`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// 在内存中分页
|
||||
const products = allProducts.slice(offset, offset + limit)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch products: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data: MedusaResponse<MedusaProduct> = await response.json()
|
||||
return {
|
||||
products,
|
||||
count: allProducts.length,
|
||||
products: data.products || [],
|
||||
count: data.count || 0,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching Medusa products (paginated):', error)
|
||||
|
|
@ -175,21 +178,12 @@ export async function uploadImageFromUrl(imageUrl: string, payload: any): Promis
|
|||
|
||||
/**
|
||||
* 判断产品是否为预售商品
|
||||
* 检查 metadata.is_preorder 和 metadata.preorder (支持布尔值和字符串)
|
||||
* 检查 metadata.is_preorder (支持布尔值和字符串)
|
||||
*/
|
||||
export function isPreorderProduct(product: MedusaProduct): boolean {
|
||||
const isPreorderValue = product.metadata?.is_preorder
|
||||
const preorderValue = product.metadata?.preorder
|
||||
|
||||
// 尝试两个字段,支持多种类型
|
||||
const value = isPreorderValue !== undefined ? isPreorderValue : preorderValue
|
||||
|
||||
// 支持布尔值 true、字符串 "true"、数字 1、字符串 "1"
|
||||
if (value === true || value === 'true' || value === '1' || value === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
const isPreorder = product.metadata?.is_preorder
|
||||
// 支持布尔值 true 或字符串 "true"
|
||||
return isPreorder === true || isPreorder === 'true'
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ export interface Product {
|
|||
* 商品标题(从 Medusa 同步)
|
||||
*/
|
||||
title: string;
|
||||
handle?: string | null;
|
||||
/**
|
||||
* 商品封面 URL(支持上传或输入 URL)
|
||||
*/
|
||||
|
|
@ -297,7 +298,11 @@ export interface PreorderProduct {
|
|||
*/
|
||||
title: string;
|
||||
/**
|
||||
* 商品封面 URL(支持上传或输入 URL)
|
||||
* 商品 URL handle(从 Medusa 同步)
|
||||
*/
|
||||
handle?: string | null;
|
||||
/**
|
||||
* 商品缩略图 URL(从 Medusa 同步)
|
||||
*/
|
||||
thumbnail?: string | null;
|
||||
/**
|
||||
|
|
@ -312,18 +317,22 @@ export interface PreorderProduct {
|
|||
* 众筹目标数量(0 表示以变体 max_orders 总和为准)
|
||||
*/
|
||||
fundingGoal: number;
|
||||
/**
|
||||
* 预计发货日期
|
||||
*/
|
||||
estimatedShipDate?: string | null;
|
||||
/**
|
||||
* 预购结束日期(可选)
|
||||
*/
|
||||
preorderEndDate?: string | null;
|
||||
/**
|
||||
* 真实订单计数(从 Medusa 同步)
|
||||
* 最小起订量
|
||||
*/
|
||||
orderCount?: number | null;
|
||||
minOrderQuantity?: number | null;
|
||||
/**
|
||||
* Fake 订单计数(手动设置)
|
||||
* 最大购买数量(0 表示不限制)
|
||||
*/
|
||||
fakeOrderCount?: number | null;
|
||||
maxOrderQuantity?: number | null;
|
||||
/**
|
||||
* 预售商品的详细描述(支持富文本编辑)
|
||||
*/
|
||||
|
|
@ -817,6 +826,7 @@ export interface ProductsSelect<T extends boolean = true> {
|
|||
status?: T;
|
||||
seedId?: T;
|
||||
title?: T;
|
||||
handle?: T;
|
||||
thumbnail?: T;
|
||||
lastSyncedAt?: T;
|
||||
content?: T;
|
||||
|
|
@ -842,13 +852,15 @@ export interface PreorderProductsSelect<T extends boolean = true> {
|
|||
status?: T;
|
||||
seedId?: T;
|
||||
title?: T;
|
||||
handle?: T;
|
||||
thumbnail?: T;
|
||||
lastSyncedAt?: T;
|
||||
preorderType?: T;
|
||||
fundingGoal?: T;
|
||||
estimatedShipDate?: T;
|
||||
preorderEndDate?: T;
|
||||
orderCount?: T;
|
||||
fakeOrderCount?: T;
|
||||
minOrderQuantity?: T;
|
||||
maxOrderQuantity?: T;
|
||||
description?: T;
|
||||
relatedProducts?: T;
|
||||
taobaoLinks?:
|
||||
|
|
|
|||
Loading…
Reference in New Issue