订单查看
This commit is contained in:
parent
4fb29d9cb7
commit
1f03387619
|
|
@ -22,6 +22,7 @@ import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0
|
||||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField'
|
import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField'
|
||||||
|
import { ProductOrdersField as ProductOrdersField_d2dd8a14d02b2830d96686a022683d02 } from '../../../components/fields/ProductOrdersField'
|
||||||
import { TaobaoProductSync as TaobaoProductSync_c920a85a41a3caf5464668c331ea204a } from '../../../components/sync/taobao/TaobaoProductSync'
|
import { TaobaoProductSync as TaobaoProductSync_c920a85a41a3caf5464668c331ea204a } from '../../../components/sync/taobao/TaobaoProductSync'
|
||||||
import { TaobaoFetchButton as TaobaoFetchButton_6da2c7669760b5ece28f442df13318c7 } from '../../../components/fields/TaobaoFetchButton'
|
import { TaobaoFetchButton as TaobaoFetchButton_6da2c7669760b5ece28f442df13318c7 } from '../../../components/fields/TaobaoFetchButton'
|
||||||
import { TaobaoLinkPreview as TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959 } from '../../../components/fields/TaobaoLinkPreview'
|
import { TaobaoLinkPreview as TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959 } from '../../../components/fields/TaobaoLinkPreview'
|
||||||
|
|
@ -29,7 +30,6 @@ import { UnifiedSyncButton as UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523
|
||||||
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
|
import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler'
|
||||||
import { PreorderProgressCell as PreorderProgressCell_67df47753573233f0c83480de687f13b } from '../../../components/cells/PreorderProgressCell'
|
import { PreorderProgressCell as PreorderProgressCell_67df47753573233f0c83480de687f13b } from '../../../components/cells/PreorderProgressCell'
|
||||||
import { RefreshOrderCountField as RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996 } from '../../../components/fields/RefreshOrderCountField'
|
import { RefreshOrderCountField as RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996 } from '../../../components/fields/RefreshOrderCountField'
|
||||||
import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f } from '../../../components/fields/PreorderOrdersField'
|
|
||||||
import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler'
|
import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler'
|
||||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
|
|
@ -70,6 +70,7 @@ export const importMap = {
|
||||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426,
|
"/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426,
|
||||||
|
"/components/fields/ProductOrdersField#ProductOrdersField": ProductOrdersField_d2dd8a14d02b2830d96686a022683d02,
|
||||||
"/components/sync/taobao/TaobaoProductSync#TaobaoProductSync": TaobaoProductSync_c920a85a41a3caf5464668c331ea204a,
|
"/components/sync/taobao/TaobaoProductSync#TaobaoProductSync": TaobaoProductSync_c920a85a41a3caf5464668c331ea204a,
|
||||||
"/components/fields/TaobaoFetchButton#TaobaoFetchButton": TaobaoFetchButton_6da2c7669760b5ece28f442df13318c7,
|
"/components/fields/TaobaoFetchButton#TaobaoFetchButton": TaobaoFetchButton_6da2c7669760b5ece28f442df13318c7,
|
||||||
"/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959,
|
"/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959,
|
||||||
|
|
@ -77,7 +78,6 @@ export const importMap = {
|
||||||
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
|
"/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2,
|
||||||
"/components/cells/PreorderProgressCell#PreorderProgressCell": PreorderProgressCell_67df47753573233f0c83480de687f13b,
|
"/components/cells/PreorderProgressCell#PreorderProgressCell": PreorderProgressCell_67df47753573233f0c83480de687f13b,
|
||||||
"/components/fields/RefreshOrderCountField#RefreshOrderCountField": RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996,
|
"/components/fields/RefreshOrderCountField#RefreshOrderCountField": RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996,
|
||||||
"/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f,
|
|
||||||
"/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5,
|
"/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5,
|
||||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { getPayload } from 'payload'
|
import { getPayload } from 'payload'
|
||||||
import config from '@payload-config'
|
import config from '@payload-config'
|
||||||
|
|
||||||
const MEDUSA_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY || ''
|
const MEDUSA_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
||||||
|
const PAYLOAD_API_KEY = process.env.PAYLOAD_API_KEY || ''
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取预购产品的订单列表(从 Medusa 获取)
|
* 获取预购产品的订单列表(调用 Medusa /hooks/preorder-orders)
|
||||||
* GET /api/preorders/:id/orders
|
* GET /api/preorders/:id/orders
|
||||||
*/
|
*/
|
||||||
export async function GET(
|
export async function GET(
|
||||||
|
|
@ -16,16 +17,16 @@ export async function GET(
|
||||||
const payload = await getPayload({ config })
|
const payload = await getPayload({ config })
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
|
|
||||||
// 获取预购产品
|
// 获取预购产品(先尝试 Payload ID,再尝试 medusaId / seedId)
|
||||||
let product: any = null
|
let product: any = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
product = await payload.findByID({
|
product = await payload.findByID({
|
||||||
collection: 'preorder-products',
|
collection: 'preorder-products',
|
||||||
id,
|
id,
|
||||||
depth: 2,
|
depth: 0,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch {
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'preorder-products',
|
collection: 'preorder-products',
|
||||||
where: {
|
where: {
|
||||||
|
|
@ -35,89 +36,49 @@ export async function GET(
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
limit: 1,
|
limit: 1,
|
||||||
depth: 2,
|
depth: 0,
|
||||||
})
|
})
|
||||||
|
if (result.docs.length > 0) product = result.docs[0]
|
||||||
if (result.docs.length > 0) {
|
|
||||||
product = result.docs[0]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
return NextResponse.json(
|
return NextResponse.json({ error: 'Preorder product not found' }, { status: 404 })
|
||||||
{ error: 'Preorder product not found' },
|
|
||||||
{ status: 404 }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!product.medusaId) {
|
// 构建查询参数:优先 medusaId,其次 seedId
|
||||||
|
const queryParam = product.medusaId
|
||||||
|
? `product_id=${encodeURIComponent(product.medusaId)}`
|
||||||
|
: product.seedId
|
||||||
|
? `seed_id=${encodeURIComponent(product.seedId)}`
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (!queryParam) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Product has no Medusa ID, cannot fetch orders' },
|
{ error: 'Product has no Medusa ID or seed ID, cannot fetch orders' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 Medusa 获取订单数据
|
// 调用 Medusa 内部 hook(使用 x-payload-api-key,无需 admin JWT)
|
||||||
const medusaUrl = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
const medusaResponse = await fetch(
|
||||||
const medusaResponse = await fetch(`${medusaUrl}/admin/orders?limit=500`, {
|
`${MEDUSA_URL}/hooks/preorder-orders?${queryParam}`,
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-publishable-api-key': MEDUSA_PUBLISHABLE_KEY,
|
'x-payload-api-key': PAYLOAD_API_KEY,
|
||||||
},
|
},
|
||||||
})
|
|
||||||
|
|
||||||
if (!medusaResponse.ok) {
|
|
||||||
throw new Error('Failed to fetch orders from Medusa')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { orders } = await medusaResponse.json()
|
|
||||||
|
|
||||||
// 筛选包含此产品的订单
|
|
||||||
const productOrders = (orders || [])
|
|
||||||
.filter((order: any) => {
|
|
||||||
return order?.items?.some((item: any) => item.product_id === product.medusaId)
|
|
||||||
})
|
|
||||||
.map((order: any) => {
|
|
||||||
// 提取此产品的订单项
|
|
||||||
const items = order.items.filter((item: any) => item.product_id === product.medusaId)
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: order.id,
|
|
||||||
display_id: order.display_id,
|
|
||||||
status: order.status,
|
|
||||||
payment_status: order.payment_status,
|
|
||||||
fulfillment_status: order.fulfillment_status,
|
|
||||||
customer_id: order.customer_id,
|
|
||||||
email: order.email,
|
|
||||||
total: order.total,
|
|
||||||
currency_code: order.currency_code,
|
|
||||||
created_at: order.created_at,
|
|
||||||
updated_at: order.updated_at,
|
|
||||||
items: items.map((item: any) => ({
|
|
||||||
id: item.id,
|
|
||||||
variant_id: item.variant_id,
|
|
||||||
title: item.title,
|
|
||||||
quantity: item.quantity,
|
|
||||||
unit_price: item.unit_price,
|
|
||||||
total: item.total,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 按创建时间倒序排序
|
|
||||||
productOrders.sort((a: any, b: any) =>
|
|
||||||
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return NextResponse.json({
|
if (!medusaResponse.ok) {
|
||||||
orders: productOrders,
|
const errBody = await medusaResponse.json().catch(() => ({}))
|
||||||
count: productOrders.length,
|
console.error('[Payload Preorder Orders API] Medusa error:', errBody)
|
||||||
product: {
|
throw new Error((errBody as any).message || `Medusa responded ${medusaResponse.status}`)
|
||||||
id: product.id,
|
}
|
||||||
title: product.title,
|
|
||||||
medusa_id: product.medusaId,
|
// 直接透传 Medusa 的响应
|
||||||
},
|
const data = await medusaResponse.json()
|
||||||
})
|
return NextResponse.json(data)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Payload Preorder Orders API] Error:', error?.message || error)
|
console.error('[Payload Preorder Orders API] Error:', error?.message || error)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
|
||||||
|
const MEDUSA_URL = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000'
|
||||||
|
const PAYLOAD_API_KEY = process.env.PAYLOAD_API_KEY || ''
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取产品的订单列表(通用,适用于 products 和 preorder-products)
|
||||||
|
* GET /api/products/:id/orders
|
||||||
|
*
|
||||||
|
* :id 可以是:
|
||||||
|
* - Payload 文档 ID
|
||||||
|
* - Medusa 产品 ID (medusaId)
|
||||||
|
* - Seed ID (seedId)
|
||||||
|
*/
|
||||||
|
export async function GET(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
const { id } = await params
|
||||||
|
|
||||||
|
// 依次在两个集合中查找产品
|
||||||
|
let product: any = null
|
||||||
|
const collections = ['products', 'preorder-products'] as const
|
||||||
|
|
||||||
|
for (const collection of collections) {
|
||||||
|
try {
|
||||||
|
product = await payload.findByID({ collection, id, depth: 0 })
|
||||||
|
if (product) break
|
||||||
|
} catch {
|
||||||
|
// 不是 Payload ID,通过 medusaId / seedId 再试
|
||||||
|
const result = await payload.find({
|
||||||
|
collection,
|
||||||
|
where: { or: [{ medusaId: { equals: id } }, { seedId: { equals: id } }] },
|
||||||
|
limit: 1,
|
||||||
|
depth: 0,
|
||||||
|
})
|
||||||
|
if (result.docs.length > 0) {
|
||||||
|
product = result.docs[0]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
return NextResponse.json({ error: 'Product not found' }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParam = product.medusaId
|
||||||
|
? `product_id=${encodeURIComponent(product.medusaId)}`
|
||||||
|
: product.seedId
|
||||||
|
? `seed_id=${encodeURIComponent(product.seedId)}`
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (!queryParam) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Product has no Medusa ID or seed ID, cannot fetch orders' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const medusaResponse = await fetch(
|
||||||
|
`${MEDUSA_URL}/hooks/preorder-orders?${queryParam}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-payload-api-key': PAYLOAD_API_KEY,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!medusaResponse.ok) {
|
||||||
|
const errBody = await medusaResponse.json().catch(() => ({}))
|
||||||
|
console.error('[Products Orders API] Medusa error:', errBody)
|
||||||
|
throw new Error((errBody as any).message || `Medusa responded ${medusaResponse.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await medusaResponse.json()
|
||||||
|
|
||||||
|
// 在服务端注入 Medusa 后台订单链接(避免客户端暴露内部 URL)
|
||||||
|
const medusaAdminBase = MEDUSA_URL
|
||||||
|
if (Array.isArray(data.orders)) {
|
||||||
|
data.orders = data.orders.map((order: any) => ({
|
||||||
|
...order,
|
||||||
|
medusa_url: `${medusaAdminBase}/app/orders/${order.id}`,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(data)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Products Orders API] Error:', error?.message || error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Failed to fetch orders', message: error?.message },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -295,6 +295,9 @@ export const RelatedProductsField: Field = {
|
||||||
// TaobaoLinksField 已移至独立文件,包含自动解析功能
|
// TaobaoLinksField 已移至独立文件,包含自动解析功能
|
||||||
export { TaobaoLinksField } from './TaobaoLinksField'
|
export { TaobaoLinksField } from './TaobaoLinksField'
|
||||||
|
|
||||||
|
// OrdersTab 定义于 collections/project/OrdersTab.ts,此处统一导出供各集合使用
|
||||||
|
export { OrdersTab } from '../project/OrdersTab'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 项目状态 Tab
|
* 项目状态 Tab
|
||||||
* 内嵌数组,可在产品表单中直接添加和编辑,无需跳转新窗口
|
* 内嵌数组,可在产品表单中直接添加和编辑,无需跳转新窗口
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload'
|
||||||
import { logAfterChange, logAfterDelete } from '../../hooks/logAction'
|
import { logAfterChange, logAfterDelete } from '../../hooks/logAction'
|
||||||
import { cacheAfterChange, cacheAfterDelete } from '../../hooks/cacheInvalidation'
|
import { cacheAfterChange, cacheAfterDelete } from '../../hooks/cacheInvalidation'
|
||||||
import { ProductBaseFields, RelatedProductsField, TaobaoLinksField, MedusaAttributesTab, ProjectStatusesTab, PrecautionsTab } from '../base/ProductBase'
|
import { ProductBaseFields, RelatedProductsField, TaobaoLinksField, MedusaAttributesTab, ProjectStatusesTab, PrecautionsTab, OrdersTab } from '../base/ProductBase'
|
||||||
import {
|
import {
|
||||||
AlignFeature,
|
AlignFeature,
|
||||||
BlocksFeature,
|
BlocksFeature,
|
||||||
|
|
@ -225,20 +225,7 @@ export const PreorderProducts: CollectionConfig = {
|
||||||
label: '🔗 相关商品',
|
label: '🔗 相关商品',
|
||||||
fields: [RelatedProductsField],
|
fields: [RelatedProductsField],
|
||||||
},
|
},
|
||||||
{
|
OrdersTab,
|
||||||
label: '📦 订单信息',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'ordersDisplay',
|
|
||||||
type: 'ui',
|
|
||||||
admin: {
|
|
||||||
components: {
|
|
||||||
Field: '/components/fields/PreorderOrdersField#PreorderOrdersField',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
MedusaAttributesTab,
|
MedusaAttributesTab,
|
||||||
ProjectStatusesTab,
|
ProjectStatusesTab,
|
||||||
PrecautionsTab,
|
PrecautionsTab,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload'
|
||||||
import { logAfterChange, logAfterDelete } from '../../hooks/logAction'
|
import { logAfterChange, logAfterDelete } from '../../hooks/logAction'
|
||||||
import { cacheAfterChange, cacheAfterDelete } from '../../hooks/cacheInvalidation'
|
import { cacheAfterChange, cacheAfterDelete } from '../../hooks/cacheInvalidation'
|
||||||
import { ProductBaseFields, RelatedProductsField, MedusaAttributesTab, ProjectStatusesTab, PrecautionsTab } from '../base/ProductBase'
|
import { ProductBaseFields, RelatedProductsField, MedusaAttributesTab, ProjectStatusesTab, PrecautionsTab, OrdersTab } from '../base/ProductBase'
|
||||||
import { TaobaoLinksField } from '../base/TaobaoLinksField'
|
import { TaobaoLinksField } from '../base/TaobaoLinksField'
|
||||||
import {
|
import {
|
||||||
AlignFeature,
|
AlignFeature,
|
||||||
|
|
@ -117,6 +117,7 @@ export const Products: CollectionConfig = {
|
||||||
label: '🔗 关联信息',
|
label: '🔗 关联信息',
|
||||||
fields: [RelatedProductsField],
|
fields: [RelatedProductsField],
|
||||||
},
|
},
|
||||||
|
OrdersTab,
|
||||||
MedusaAttributesTab,
|
MedusaAttributesTab,
|
||||||
ProjectStatusesTab,
|
ProjectStatusesTab,
|
||||||
PrecautionsTab,
|
PrecautionsTab,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { Tab } from 'payload'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单信息 Tab(通用)
|
||||||
|
* 通过 /api/products/:id/orders 从 Medusa 拉取该产品的订单数据
|
||||||
|
* Products 和 PreorderProducts 均可使用
|
||||||
|
*/
|
||||||
|
export const OrdersTab: Tab = {
|
||||||
|
label: '📦 订单信息',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'ordersDisplay',
|
||||||
|
type: 'ui',
|
||||||
|
admin: {
|
||||||
|
components: {
|
||||||
|
Field: '/components/fields/ProductOrdersField#ProductOrdersField',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
@ -3,20 +3,37 @@
|
||||||
import { useField, useFormFields } from '@payloadcms/ui'
|
import { useField, useFormFields } from '@payloadcms/ui'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
interface PreorderItem {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
variant_id: string
|
||||||
|
variant_sku?: string
|
||||||
|
variant_title?: string
|
||||||
|
quantity: number
|
||||||
|
unit_price: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
interface Order {
|
interface Order {
|
||||||
id: string
|
id: string
|
||||||
display_id: number
|
display_id: number
|
||||||
status: string
|
status: string
|
||||||
payment_status: string
|
payment_status: string
|
||||||
|
fulfillment_status: string
|
||||||
email: string
|
email: string
|
||||||
total: number
|
total: number
|
||||||
|
preorder_amount: number
|
||||||
|
preorder_quantity: number
|
||||||
currency_code: string
|
currency_code: string
|
||||||
created_at: string
|
created_at: string
|
||||||
items: Array<{
|
preorder_items: PreorderItem[]
|
||||||
title: string
|
}
|
||||||
quantity: number
|
|
||||||
unit_price: number
|
interface Statistics {
|
||||||
}>
|
total_orders: number
|
||||||
|
total_quantity: number
|
||||||
|
total_amount: number
|
||||||
|
status_breakdown: Record<string, number>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PreorderOrdersField: React.FC = () => {
|
export const PreorderOrdersField: React.FC = () => {
|
||||||
|
|
@ -26,7 +43,7 @@ export const PreorderOrdersField: React.FC = () => {
|
||||||
const [orders, setOrders] = useState<Order[]>([])
|
const [orders, setOrders] = useState<Order[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [stats, setStats] = useState<{ total: number; totalAmount: number } | null>(null)
|
const [stats, setStats] = useState<Statistics | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (medusaId || seedId) {
|
if (medusaId || seedId) {
|
||||||
|
|
@ -50,13 +67,7 @@ export const PreorderOrdersField: React.FC = () => {
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setOrders(data.orders || [])
|
setOrders(data.orders || [])
|
||||||
|
setStats(data.statistics ?? null)
|
||||||
// 计算统计数据
|
|
||||||
const totalAmount = data.orders?.reduce((sum: number, order: Order) => sum + order.total, 0) || 0
|
|
||||||
setStats({
|
|
||||||
total: data.count || 0,
|
|
||||||
totalAmount,
|
|
||||||
})
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Failed to fetch orders:', err)
|
console.error('Failed to fetch orders:', err)
|
||||||
setError(err.message || 'Failed to load orders')
|
setError(err.message || 'Failed to load orders')
|
||||||
|
|
@ -152,17 +163,21 @@ export const PreorderOrdersField: React.FC = () => {
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
marginBottom: '1rem',
|
marginBottom: '1rem',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))',
|
||||||
gap: '1rem',
|
gap: '1rem',
|
||||||
}}>
|
}}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '0.875rem', color: '#666', marginBottom: '0.25rem' }}>订单总数</div>
|
<div style={{ fontSize: '0.875rem', color: '#666', marginBottom: '0.25rem' }}>订单总数</div>
|
||||||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1976d2' }}>{stats.total}</div>
|
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1976d2' }}>{stats.total_orders}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '0.875rem', color: '#666', marginBottom: '0.25rem' }}>订单总额</div>
|
<div style={{ fontSize: '0.875rem', color: '#666', marginBottom: '0.25rem' }}>预购数量</div>
|
||||||
|
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1976d2' }}>{stats.total_quantity}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: '0.875rem', color: '#666', marginBottom: '0.25rem' }}>预购金额</div>
|
||||||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1976d2' }}>
|
<div style={{ fontSize: '1.5rem', fontWeight: 'bold', color: '#1976d2' }}>
|
||||||
{formatCurrency(stats.totalAmount, orders[0]?.currency_code || 'CNY')}
|
{formatCurrency(stats.total_amount, orders[0]?.currency_code || 'CNY')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'flex-end' }}>
|
<div style={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||||
|
|
@ -218,9 +233,9 @@ export const PreorderOrdersField: React.FC = () => {
|
||||||
{order.email}
|
{order.email}
|
||||||
</td>
|
</td>
|
||||||
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
<td style={{ padding: '0.75rem', fontSize: '0.875rem' }}>
|
||||||
{order.items.map((item, i) => (
|
{(order.preorder_items || []).map((item, i) => (
|
||||||
<div key={i} style={{ marginBottom: i < order.items.length - 1 ? '0.25rem' : 0 }}>
|
<div key={i} style={{ marginBottom: i < order.preorder_items.length - 1 ? '0.25rem' : 0 }}>
|
||||||
{item.title} × {item.quantity}
|
{item.variant_title ? `${item.title} · ${item.variant_title}` : item.title} × {item.quantity}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,360 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Button, useField } from '@payloadcms/ui'
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
interface OrderItem {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
variant_id: string
|
||||||
|
variant_sku?: string
|
||||||
|
variant_title?: string
|
||||||
|
quantity: number
|
||||||
|
unit_price: number
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Order {
|
||||||
|
id: string
|
||||||
|
display_id: number
|
||||||
|
status: string
|
||||||
|
payment_status: string
|
||||||
|
fulfillment_status: string
|
||||||
|
email: string
|
||||||
|
total: number
|
||||||
|
preorder_amount: number
|
||||||
|
preorder_quantity: number
|
||||||
|
currency_code: string
|
||||||
|
created_at: string
|
||||||
|
preorder_items: OrderItem[]
|
||||||
|
medusa_url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Statistics {
|
||||||
|
total_orders: number
|
||||||
|
total_quantity: number
|
||||||
|
total_amount: number
|
||||||
|
status_breakdown: Record<string, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_COLORS: Record<string, { bg: string; text: string }> = {
|
||||||
|
completed: { bg: 'var(--theme-success-50)', text: 'var(--theme-success-900)' },
|
||||||
|
pending: { bg: 'var(--theme-warning-50)', text: 'var(--theme-warning-900)' },
|
||||||
|
cancelled: { bg: 'var(--theme-error-50)', text: 'var(--theme-error-900)' },
|
||||||
|
processing: { bg: 'var(--theme-elevation-100)', text: 'var(--theme-text)' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusColor = (status: string) =>
|
||||||
|
STATUS_COLORS[status] ?? STATUS_COLORS.processing
|
||||||
|
|
||||||
|
// ─── shared styles ──────────────────────────────────────────────────────────
|
||||||
|
const card: React.CSSProperties = {
|
||||||
|
padding: '1rem',
|
||||||
|
border: '1px solid var(--theme-elevation-150)',
|
||||||
|
borderRadius: 'var(--style-radius-m)',
|
||||||
|
backgroundColor: 'var(--theme-elevation-50)',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
}
|
||||||
|
|
||||||
|
const TH: React.CSSProperties = {
|
||||||
|
padding: '0.6rem 0.75rem',
|
||||||
|
textAlign: 'left',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--theme-elevation-600)',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.04em',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}
|
||||||
|
|
||||||
|
const TD: React.CSSProperties = {
|
||||||
|
padding: '0.6rem 0.75rem',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── sub-component ──────────────────────────────────────────────────────────
|
||||||
|
function StatCard({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
padding: '0.75rem 1rem',
|
||||||
|
border: '1px solid var(--theme-elevation-150)',
|
||||||
|
borderRadius: 'var(--style-radius-m)',
|
||||||
|
backgroundColor: 'var(--theme-elevation-50)',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
color: 'var(--theme-elevation-500)',
|
||||||
|
marginBottom: '0.25rem',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.04em',
|
||||||
|
}}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '1.375rem', fontWeight: 700, color: 'var(--theme-text)' }}>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── main component ─────────────────────────────────────────────────────────
|
||||||
|
/**
|
||||||
|
* 通用产品订单展示组件
|
||||||
|
* 适用于 Products 和 PreorderProducts
|
||||||
|
* 通过 /api/products/:id/orders 获取数据,点击「查看」跳转 Medusa 后台订单页
|
||||||
|
*/
|
||||||
|
export const ProductOrdersField: React.FC = () => {
|
||||||
|
const { value: medusaId } = useField<string>({ path: 'medusaId' })
|
||||||
|
const { value: seedId } = useField<string>({ path: 'seedId' })
|
||||||
|
|
||||||
|
const [orders, setOrders] = useState<Order[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [stats, setStats] = useState<Statistics | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (medusaId || seedId) fetchOrders()
|
||||||
|
}, [medusaId, seedId])
|
||||||
|
|
||||||
|
const fetchOrders = async () => {
|
||||||
|
const productId = seedId || medusaId
|
||||||
|
if (!productId) return
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/products/${productId}/orders`)
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => ({}))
|
||||||
|
throw new Error((body as any).message || 'Failed to fetch orders')
|
||||||
|
}
|
||||||
|
const data = await res.json()
|
||||||
|
setOrders(data.orders || [])
|
||||||
|
setStats(data.statistics ?? null)
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || 'Failed to load orders')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmt = (amount: number, currency: string) =>
|
||||||
|
new Intl.NumberFormat('zh-CN', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: currency.toUpperCase(),
|
||||||
|
}).format(amount / 100)
|
||||||
|
|
||||||
|
const fmtDate = (d: string) =>
|
||||||
|
new Date(d).toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||||
|
hour: '2-digit', minute: '2-digit',
|
||||||
|
})
|
||||||
|
|
||||||
|
// ─── state views ─────────────────────────────────────────────────────────
|
||||||
|
if (!medusaId && !seedId) {
|
||||||
|
return (
|
||||||
|
<div style={card}>
|
||||||
|
<p style={{ margin: 0, color: 'var(--theme-elevation-500)', fontSize: '0.875rem' }}>
|
||||||
|
产品尚未同步到 Medusa,无法查看订单
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={card}>
|
||||||
|
<p style={{ margin: 0, fontSize: '0.875rem', color: 'var(--theme-elevation-500)' }}>
|
||||||
|
加载订单中…
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div style={{ ...card, backgroundColor: 'var(--theme-error-50)', borderColor: 'var(--theme-error-300)' }}>
|
||||||
|
<p style={{ margin: '0 0 0.75rem', color: 'var(--theme-error-900)', fontSize: '0.875rem' }}>
|
||||||
|
加载失败:{error}
|
||||||
|
</p>
|
||||||
|
<Button buttonStyle="secondary" size="small" onClick={fetchOrders}>重试</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orders.length === 0) {
|
||||||
|
return (
|
||||||
|
<div style={card}>
|
||||||
|
<p style={{ margin: '0 0 0.75rem', color: 'var(--theme-elevation-500)', fontSize: '0.875rem' }}>
|
||||||
|
暂无订单
|
||||||
|
</p>
|
||||||
|
<Button buttonStyle="secondary" size="small" onClick={fetchOrders}>刷新</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── main view ──────────────────────────────────────────────────────────
|
||||||
|
const baseCurrency = orders[0]?.currency_code || 'CNY'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: '1.5rem' }}>
|
||||||
|
|
||||||
|
{/* 统计栏 */}
|
||||||
|
{stats && (
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))',
|
||||||
|
gap: '0.75rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
}}>
|
||||||
|
<StatCard label="订单总数" value={String(stats.total_orders)} />
|
||||||
|
<StatCard label="下单数量" value={String(stats.total_quantity)} />
|
||||||
|
<StatCard label="订单金额" value={fmt(stats.total_amount, baseCurrency)} />
|
||||||
|
<div style={{
|
||||||
|
padding: '0.75rem 1rem',
|
||||||
|
border: '1px solid var(--theme-elevation-150)',
|
||||||
|
borderRadius: 'var(--style-radius-m)',
|
||||||
|
backgroundColor: 'var(--theme-elevation-50)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: '0.75rem', color: 'var(--theme-elevation-500)', marginBottom: '0.5rem', textTransform: 'uppercase', letterSpacing: '0.04em' }}>
|
||||||
|
操作
|
||||||
|
</div>
|
||||||
|
<Button buttonStyle="primary" onClick={fetchOrders} disabled={loading}>
|
||||||
|
🔄 刷新订单
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 订单表格 */}
|
||||||
|
<div style={{
|
||||||
|
border: '1px solid var(--theme-elevation-150)',
|
||||||
|
borderRadius: 'var(--style-radius-m)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8125rem' }}>
|
||||||
|
<thead>
|
||||||
|
<tr style={{
|
||||||
|
background: 'var(--theme-elevation-50)',
|
||||||
|
borderBottom: '1px solid var(--theme-elevation-150)',
|
||||||
|
}}>
|
||||||
|
{['订单号', '客户', '商品', '金额', '状态', '时间', ''].map(h => (
|
||||||
|
<th key={h} style={TH}>{h}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{orders.map((order, idx) => {
|
||||||
|
const sc = statusColor(order.status)
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={order.id}
|
||||||
|
style={{
|
||||||
|
borderBottom: idx < orders.length - 1
|
||||||
|
? '1px solid var(--theme-elevation-100)'
|
||||||
|
: 'none',
|
||||||
|
background: idx % 2 === 0
|
||||||
|
? 'var(--theme-bg)'
|
||||||
|
: 'var(--theme-elevation-50)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 订单号 */}
|
||||||
|
<td style={TD}>
|
||||||
|
<span style={{ fontWeight: 600, color: 'var(--theme-text)' }}>
|
||||||
|
#{order.display_id}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<span style={{ fontSize: '0.7rem', color: 'var(--theme-elevation-400)' }}>
|
||||||
|
{order.id.slice(0, 8)}…
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* 客户 */}
|
||||||
|
<td style={TD}>
|
||||||
|
<span style={{ color: 'var(--theme-elevation-700)' }}>{order.email}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* 商品 */}
|
||||||
|
<td style={TD}>
|
||||||
|
{(order.preorder_items || []).map((item, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
style={{ marginBottom: i < order.preorder_items.length - 1 ? '0.2rem' : 0 }}
|
||||||
|
>
|
||||||
|
<span style={{ color: 'var(--theme-text)' }}>
|
||||||
|
{item.variant_title
|
||||||
|
? `${item.title} · ${item.variant_title}`
|
||||||
|
: item.title}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: 'var(--theme-elevation-400)', marginLeft: '0.25rem' }}>
|
||||||
|
× {item.quantity}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* 金额 */}
|
||||||
|
<td style={{ ...TD, textAlign: 'right', fontWeight: 600, whiteSpace: 'nowrap' }}>
|
||||||
|
{fmt(order.total, order.currency_code)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* 状态 */}
|
||||||
|
<td style={{ ...TD, textAlign: 'center' }}>
|
||||||
|
<span style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '0.2rem 0.6rem',
|
||||||
|
borderRadius: '999px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
backgroundColor: sc.bg,
|
||||||
|
color: sc.text,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}>
|
||||||
|
{order.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* 时间 */}
|
||||||
|
<td style={{ ...TD, whiteSpace: 'nowrap', color: 'var(--theme-elevation-500)' }}>
|
||||||
|
{fmtDate(order.created_at)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{/* 跳转 Medusa */}
|
||||||
|
<td style={{ ...TD, textAlign: 'center' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => window.open(order.medusa_url, '_blank', 'noopener,noreferrer')}
|
||||||
|
style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.25rem',
|
||||||
|
padding: '0.5rem 0.875rem',
|
||||||
|
borderRadius: 'var(--style-radius-s)',
|
||||||
|
border: '1px solid var(--theme-elevation-300)',
|
||||||
|
backgroundColor: 'var(--theme-elevation-0, var(--theme-bg))',
|
||||||
|
color: 'var(--theme-text)',
|
||||||
|
fontSize: '0.8125rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
cursor: 'pointer',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
onMouseEnter={e => (e.currentTarget.style.backgroundColor = 'var(--theme-elevation-100)')}
|
||||||
|
onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'var(--theme-elevation-0, var(--theme-bg))')}
|
||||||
|
>
|
||||||
|
查看
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style={{ flexShrink: 0, opacity: 0.6 }}>
|
||||||
|
<path d="M1 11L11 1M11 1H4M11 1V8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix precautions ID type mismatch.
|
||||||
|
*
|
||||||
|
* The `precautions` table was created by a manual migration with `serial`
|
||||||
|
* (integer) PKs. Payload v3 / @payloadcms/db-postgres defaults to `text`
|
||||||
|
* (varchar) IDs. On dev-mode startup, Payload's schema-push tries to ALTER
|
||||||
|
* the `precautions_id` relationship columns to varchar, but the FK constraint
|
||||||
|
* pointing to an integer PK blocks it.
|
||||||
|
*
|
||||||
|
* Fix: drop the precautions table and all `precautions_id` rels columns so
|
||||||
|
* Payload's dev-mode push can recreate everything with the correct types.
|
||||||
|
*/
|
||||||
|
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
-- Drop FK constraints first (payload_locked_documents_rels)
|
||||||
|
ALTER TABLE "payload_locked_documents_rels"
|
||||||
|
DROP CONSTRAINT IF EXISTS "payload_locked_documents_rels_precautions_fk";
|
||||||
|
|
||||||
|
-- Drop the precautions_id column from payload_locked_documents_rels
|
||||||
|
ALTER TABLE "payload_locked_documents_rels"
|
||||||
|
DROP COLUMN IF EXISTS "precautions_id";
|
||||||
|
|
||||||
|
-- Drop from products_rels (sharedPrecautions relationship)
|
||||||
|
ALTER TABLE "products_rels"
|
||||||
|
DROP CONSTRAINT IF EXISTS "products_rels_precautions_fk";
|
||||||
|
ALTER TABLE "products_rels"
|
||||||
|
DROP COLUMN IF EXISTS "precautions_id";
|
||||||
|
|
||||||
|
-- Drop from preorder_products_rels (sharedPrecautions relationship)
|
||||||
|
ALTER TABLE "preorder_products_rels"
|
||||||
|
DROP CONSTRAINT IF EXISTS "preorder_products_rels_precautions_fk";
|
||||||
|
ALTER TABLE "preorder_products_rels"
|
||||||
|
DROP COLUMN IF EXISTS "precautions_id";
|
||||||
|
|
||||||
|
-- Drop the precautions table itself (Payload will recreate with varchar PK)
|
||||||
|
DROP TABLE IF EXISTS "precautions" CASCADE;
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||||
|
// Recreating the integer-based schema is not worth reverting to since
|
||||||
|
// the whole point is to migrate to the correct varchar-based schema.
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import * as migration_baseline from './baseline'
|
import * as migration_baseline from './baseline'
|
||||||
|
import * as migration_fix_precautions_id_type from './fix_precautions_id_type'
|
||||||
|
|
||||||
export const migrations = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
|
|
@ -6,5 +7,10 @@ export const migrations = [
|
||||||
down: migration_baseline.down,
|
down: migration_baseline.down,
|
||||||
name: 'baseline',
|
name: 'baseline',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
up: migration_fix_precautions_id_type.up,
|
||||||
|
down: migration_fix_precautions_id_type.down,
|
||||||
|
name: 'fix_precautions_id_type',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue