From 1f03387619a389efd94c9586877a51277b892dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BE=9F=E7=94=B7=E6=97=A5=E8=AE=B0=5Cwww?= Date: Wed, 25 Feb 2026 21:57:32 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A2=E5=8D=95=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(payload)/admin/importMap.js | 4 +- src/app/api/preorders/[id]/orders/route.ts | 115 ++---- src/app/api/products/[id]/orders/route.ts | 100 +++++ src/collections/base/ProductBase.ts | 3 + src/collections/products/PreorderProducts.ts | 17 +- src/collections/products/Products.ts | 3 +- src/collections/project/OrdersTab.ts | 21 + src/components/fields/PreorderOrdersField.tsx | 55 ++- src/components/fields/ProductOrdersField.tsx | 360 ++++++++++++++++++ src/migrations/fix_precautions_id_type.ts | 45 +++ src/migrations/index.ts | 6 + 11 files changed, 614 insertions(+), 115 deletions(-) create mode 100644 src/app/api/products/[id]/orders/route.ts create mode 100644 src/collections/project/OrdersTab.ts create mode 100644 src/components/fields/ProductOrdersField.tsx create mode 100644 src/migrations/fix_precautions_id_type.ts diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index 8677edd..f6ae200 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -22,6 +22,7 @@ import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0 import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { RelatedProductsField as RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426 } from '../../../components/fields/RelatedProductsField' +import { ProductOrdersField as ProductOrdersField_d2dd8a14d02b2830d96686a022683d02 } from '../../../components/fields/ProductOrdersField' import { TaobaoProductSync as TaobaoProductSync_c920a85a41a3caf5464668c331ea204a } from '../../../components/sync/taobao/TaobaoProductSync' import { TaobaoFetchButton as TaobaoFetchButton_6da2c7669760b5ece28f442df13318c7 } from '../../../components/fields/TaobaoFetchButton' import { TaobaoLinkPreview as TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959 } from '../../../components/fields/TaobaoLinkPreview' @@ -29,7 +30,6 @@ import { UnifiedSyncButton as UnifiedSyncButton_fc99b3f144909da232f9fd4ff7269523 import { default as default_c2e3814fe427263135b1f5931c37f6f2 } from '../../../components/list/ProductGridStyler' import { PreorderProgressCell as PreorderProgressCell_67df47753573233f0c83480de687f13b } from '../../../components/cells/PreorderProgressCell' import { RefreshOrderCountField as RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996 } from '../../../components/fields/RefreshOrderCountField' -import { PreorderOrdersField as PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f } from '../../../components/fields/PreorderOrdersField' import { PreorderProductGridStyler as PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5 } from '../../../components/list/PreorderProductGridStyler' import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' @@ -70,6 +70,7 @@ export const importMap = { "@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "/components/fields/RelatedProductsField#RelatedProductsField": RelatedProductsField_f3e26ca26ab1ef52a2ee0f6932180426, + "/components/fields/ProductOrdersField#ProductOrdersField": ProductOrdersField_d2dd8a14d02b2830d96686a022683d02, "/components/sync/taobao/TaobaoProductSync#TaobaoProductSync": TaobaoProductSync_c920a85a41a3caf5464668c331ea204a, "/components/fields/TaobaoFetchButton#TaobaoFetchButton": TaobaoFetchButton_6da2c7669760b5ece28f442df13318c7, "/components/fields/TaobaoLinkPreview#TaobaoLinkPreview": TaobaoLinkPreview_44c9439e828c0463191af62d21ad4959, @@ -77,7 +78,6 @@ export const importMap = { "/components/list/ProductGridStyler#default": default_c2e3814fe427263135b1f5931c37f6f2, "/components/cells/PreorderProgressCell#PreorderProgressCell": PreorderProgressCell_67df47753573233f0c83480de687f13b, "/components/fields/RefreshOrderCountField#RefreshOrderCountField": RefreshOrderCountField_ef327f0ad449eac595b5e301044c0996, - "/components/fields/PreorderOrdersField#PreorderOrdersField": PreorderOrdersField_a4aa1b8cbd6dec364a834b059228f43f, "/components/list/PreorderProductGridStyler#PreorderProductGridStyler": PreorderProductGridStyler_e7f6f7c2233fc58ae87e992227bb80c5, "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, diff --git a/src/app/api/preorders/[id]/orders/route.ts b/src/app/api/preorders/[id]/orders/route.ts index 80d96c5..57a8ba3 100644 --- a/src/app/api/preorders/[id]/orders/route.ts +++ b/src/app/api/preorders/[id]/orders/route.ts @@ -2,10 +2,11 @@ import { NextRequest, NextResponse } from 'next/server' import { getPayload } from 'payload' 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 */ export async function GET( @@ -16,108 +17,68 @@ export async function GET( const payload = await getPayload({ config }) const { id } = await params - // 获取预购产品 + // 获取预购产品(先尝试 Payload ID,再尝试 medusaId / seedId) let product: any = null - + try { product = await payload.findByID({ collection: 'preorder-products', id, - depth: 2, + depth: 0, }) - } catch (err) { + } catch { const result = await payload.find({ collection: 'preorder-products', where: { or: [ { medusaId: { equals: id } }, - { seedId: { equals: id } }, + { seedId: { equals: id } }, ], }, 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) { - return NextResponse.json( - { error: 'Preorder product not found' }, - { status: 404 } - ) + return NextResponse.json({ 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( - { error: 'Product has no Medusa ID, cannot fetch orders' }, + { error: 'Product has no Medusa ID or seed ID, cannot fetch orders' }, { status: 400 } ) } - // 从 Medusa 获取订单数据 - const medusaUrl = process.env.MEDUSA_BACKEND_URL || 'http://localhost:9000' - const medusaResponse = await fetch(`${medusaUrl}/admin/orders?limit=500`, { - headers: { - 'Content-Type': 'application/json', - 'x-publishable-api-key': MEDUSA_PUBLISHABLE_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() + // 调用 Medusa 内部 hook(使用 x-payload-api-key,无需 admin JWT) + const medusaResponse = await fetch( + `${MEDUSA_URL}/hooks/preorder-orders?${queryParam}`, + { + headers: { + 'Content-Type': 'application/json', + 'x-payload-api-key': PAYLOAD_API_KEY, + }, + } ) - return NextResponse.json({ - orders: productOrders, - count: productOrders.length, - product: { - id: product.id, - title: product.title, - medusa_id: product.medusaId, - }, - }) + if (!medusaResponse.ok) { + const errBody = await medusaResponse.json().catch(() => ({})) + console.error('[Payload Preorder Orders API] Medusa error:', errBody) + throw new Error((errBody as any).message || `Medusa responded ${medusaResponse.status}`) + } + + // 直接透传 Medusa 的响应 + const data = await medusaResponse.json() + return NextResponse.json(data) } catch (error: any) { console.error('[Payload Preorder Orders API] Error:', error?.message || error) return NextResponse.json( diff --git a/src/app/api/products/[id]/orders/route.ts b/src/app/api/products/[id]/orders/route.ts new file mode 100644 index 0000000..7f87b55 --- /dev/null +++ b/src/app/api/products/[id]/orders/route.ts @@ -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 } + ) + } +} diff --git a/src/collections/base/ProductBase.ts b/src/collections/base/ProductBase.ts index 7ba02f3..c98ed3e 100644 --- a/src/collections/base/ProductBase.ts +++ b/src/collections/base/ProductBase.ts @@ -295,6 +295,9 @@ export const RelatedProductsField: Field = { // TaobaoLinksField 已移至独立文件,包含自动解析功能 export { TaobaoLinksField } from './TaobaoLinksField' +// OrdersTab 定义于 collections/project/OrdersTab.ts,此处统一导出供各集合使用 +export { OrdersTab } from '../project/OrdersTab' + /** * 项目状态 Tab * 内嵌数组,可在产品表单中直接添加和编辑,无需跳转新窗口 diff --git a/src/collections/products/PreorderProducts.ts b/src/collections/products/PreorderProducts.ts index 4430ce6..f910358 100644 --- a/src/collections/products/PreorderProducts.ts +++ b/src/collections/products/PreorderProducts.ts @@ -1,7 +1,7 @@ import type { CollectionConfig } from 'payload' import { logAfterChange, logAfterDelete } from '../../hooks/logAction' 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 { AlignFeature, BlocksFeature, @@ -225,20 +225,7 @@ export const PreorderProducts: CollectionConfig = { label: '🔗 相关商品', fields: [RelatedProductsField], }, - { - label: '📦 订单信息', - fields: [ - { - name: 'ordersDisplay', - type: 'ui', - admin: { - components: { - Field: '/components/fields/PreorderOrdersField#PreorderOrdersField', - }, - }, - }, - ], - }, + OrdersTab, MedusaAttributesTab, ProjectStatusesTab, PrecautionsTab, diff --git a/src/collections/products/Products.ts b/src/collections/products/Products.ts index 820a9e6..3ae2219 100644 --- a/src/collections/products/Products.ts +++ b/src/collections/products/Products.ts @@ -1,7 +1,7 @@ import type { CollectionConfig } from 'payload' import { logAfterChange, logAfterDelete } from '../../hooks/logAction' 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 { AlignFeature, @@ -117,6 +117,7 @@ export const Products: CollectionConfig = { label: '🔗 关联信息', fields: [RelatedProductsField], }, + OrdersTab, MedusaAttributesTab, ProjectStatusesTab, PrecautionsTab, diff --git a/src/collections/project/OrdersTab.ts b/src/collections/project/OrdersTab.ts new file mode 100644 index 0000000..41789b7 --- /dev/null +++ b/src/collections/project/OrdersTab.ts @@ -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', + }, + }, + }, + ], +} diff --git a/src/components/fields/PreorderOrdersField.tsx b/src/components/fields/PreorderOrdersField.tsx index 895cb1d..e2e3774 100644 --- a/src/components/fields/PreorderOrdersField.tsx +++ b/src/components/fields/PreorderOrdersField.tsx @@ -3,20 +3,37 @@ import { useField, useFormFields } from '@payloadcms/ui' 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 { 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 - items: Array<{ - title: string - quantity: number - unit_price: number - }> + preorder_items: PreorderItem[] +} + +interface Statistics { + total_orders: number + total_quantity: number + total_amount: number + status_breakdown: Record } export const PreorderOrdersField: React.FC = () => { @@ -26,7 +43,7 @@ export const PreorderOrdersField: React.FC = () => { const [orders, setOrders] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) - const [stats, setStats] = useState<{ total: number; totalAmount: number } | null>(null) + const [stats, setStats] = useState(null) useEffect(() => { if (medusaId || seedId) { @@ -50,13 +67,7 @@ export const PreorderOrdersField: React.FC = () => { const data = await response.json() setOrders(data.orders || []) - - // 计算统计数据 - const totalAmount = data.orders?.reduce((sum: number, order: Order) => sum + order.total, 0) || 0 - setStats({ - total: data.count || 0, - totalAmount, - }) + setStats(data.statistics ?? null) } catch (err: any) { console.error('Failed to fetch orders:', err) setError(err.message || 'Failed to load orders') @@ -152,17 +163,21 @@ export const PreorderOrdersField: React.FC = () => { borderRadius: '4px', marginBottom: '1rem', display: 'grid', - gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', + gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: '1rem', }}>
订单总数
-
{stats.total}
+
{stats.total_orders}
-
订单总额
+
预购数量
+
{stats.total_quantity}
+
+
+
预购金额
- {formatCurrency(stats.totalAmount, orders[0]?.currency_code || 'CNY')} + {formatCurrency(stats.total_amount, orders[0]?.currency_code || 'CNY')}
@@ -218,9 +233,9 @@ export const PreorderOrdersField: React.FC = () => { {order.email} - {order.items.map((item, i) => ( -
- {item.title} × {item.quantity} + {(order.preorder_items || []).map((item, i) => ( +
+ {item.variant_title ? `${item.title} · ${item.variant_title}` : item.title} × {item.quantity}
))} diff --git a/src/components/fields/ProductOrdersField.tsx b/src/components/fields/ProductOrdersField.tsx new file mode 100644 index 0000000..14cf9da --- /dev/null +++ b/src/components/fields/ProductOrdersField.tsx @@ -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 +} + +const STATUS_COLORS: Record = { + 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 ( +
+
+ {label} +
+
+ {value} +
+
+ ) +} + +// ─── main component ───────────────────────────────────────────────────────── +/** + * 通用产品订单展示组件 + * 适用于 Products 和 PreorderProducts + * 通过 /api/products/:id/orders 获取数据,点击「查看」跳转 Medusa 后台订单页 + */ +export const ProductOrdersField: React.FC = () => { + const { value: medusaId } = useField({ path: 'medusaId' }) + const { value: seedId } = useField({ path: 'seedId' }) + + const [orders, setOrders] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [stats, setStats] = useState(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 ( +
+

+ 产品尚未同步到 Medusa,无法查看订单 +

+
+ ) + } + + if (loading) { + return ( +
+

+ 加载订单中… +

+
+ ) + } + + if (error) { + return ( +
+

+ 加载失败:{error} +

+ +
+ ) + } + + if (orders.length === 0) { + return ( +
+

+ 暂无订单 +

+ +
+ ) + } + + // ─── main view ────────────────────────────────────────────────────────── + const baseCurrency = orders[0]?.currency_code || 'CNY' + + return ( +
+ + {/* 统计栏 */} + {stats && ( +
+ + + +
+
+ 操作 +
+ +
+
+ )} + + {/* 订单表格 */} +
+ + + + {['订单号', '客户', '商品', '金额', '状态', '时间', ''].map(h => ( + + ))} + + + + {orders.map((order, idx) => { + const sc = statusColor(order.status) + return ( + + {/* 订单号 */} + + + {/* 客户 */} + + + {/* 商品 */} + + + {/* 金额 */} + + + {/* 状态 */} + + + {/* 时间 */} + + + {/* 跳转 Medusa */} + + + ) + })} + +
{h}
+ + #{order.display_id} + +
+ + {order.id.slice(0, 8)}… + +
+ {order.email} + + {(order.preorder_items || []).map((item, i) => ( +
+ + {item.variant_title + ? `${item.title} · ${item.variant_title}` + : item.title} + + + × {item.quantity} + +
+ ))} +
+ {fmt(order.total, order.currency_code)} + + + {order.status} + + + {fmtDate(order.created_at)} + + +
+
+
+ ) +} diff --git a/src/migrations/fix_precautions_id_type.ts b/src/migrations/fix_precautions_id_type.ts new file mode 100644 index 0000000..184e6f0 --- /dev/null +++ b/src/migrations/fix_precautions_id_type.ts @@ -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 { + 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 { + // Recreating the integer-based schema is not worth reverting to since + // the whole point is to migrate to the correct varchar-based schema. +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 78d325a..d63efae 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -1,4 +1,5 @@ import * as migration_baseline from './baseline' +import * as migration_fix_precautions_id_type from './fix_precautions_id_type' export const migrations = [ { @@ -6,5 +7,10 @@ export const migrations = [ down: migration_baseline.down, name: 'baseline', }, + { + up: migration_fix_precautions_id_type.up, + down: migration_fix_precautions_id_type.down, + name: 'fix_precautions_id_type', + }, ]