feat(r3-order-pay): 结算与订单支付流程、支付倒计时/商品图组件,接口地址改为构建期注入

- 新增 PaymentCountdown、ProductImage 组件
- 完善 checkout/orders/orderDetail 订单支付链路与地址、商品、设计数据
- request 接口地址改为构建期注入并保留真实网络错误,默认兜底线上地址
- 同步重新构建 dist 产物
This commit is contained in:
fenjoyoung
2026-09-13 14:50:39 +08:00
parent a13e380009
commit 8fdf0b1b4f
50 changed files with 895 additions and 777 deletions
+40 -99
View File
@@ -1,119 +1,60 @@
/**
* R2 设计清单接口(api-contract-v1 §5)。
*
* 映射约定(阶段0 决策,见 docs/r2-workflow.md):
* - 一条前端 DesignItem = 一条后端 DesignList 记录,items 固定 1 个元素;
* - title 由 productName 承担(后端必填);
* - productIcon 不入库(后端 forbidNonWhitelisted 会拒绝),页面按 productId 兜底推导;
* - 状态映射:DRAFT↔undesigned、SUBMITTED↔designing、PROCESSING↔processing、DONE↔ordered
* - 前端只允许提交 designing(→SUBMITTED),PROCESSING/DONE 由 R3 驱动。
*/
import http from '../request'
import type { DesignDataV1, DesignItem } from '../../types'
import { getProductIconImg } from '../productConfig'
/** 后端 DesignListStatus → 前端状态码(契约 §5 映射表) */
const SERVER_STATUS_MAP: Record<string, DesignItem['status']> = {
DRAFT: 'undesigned',
SUBMITTED: 'designing',
PROCESSING: 'processing',
DONE: 'ordered',
type ServerEntry = { productId: string; productName: string; unitPrice: number; count: number; designData?: DesignDataV1 }
type ServerList = { id: string; title: string; items: ServerEntry[]; status: 'DRAFT' | 'SUBMITTED' | 'PROCESSING' | 'DONE'; orderId?: string; createdAt: string }
const statusToClient = (item: ServerList): DesignItem['status'] => {
if (item.orderId || item.status === 'DONE') return 'ordered'
if (item.status === 'PROCESSING') return 'processing'
if (item.status === 'DRAFT') return 'undesigned'
return 'designing'
}
/** 清单条目(后端 items[] 固定 1 个元素,契约 §5) */
export interface DesignListEntryPayload {
productId: string
productName: string
unitPrice: number
count: number
designData?: DesignDataV1
}
const statusToServer = (status: DesignItem['status']) =>
status === 'undesigned' ? 'DRAFT' : status === 'designing' ? 'SUBMITTED' : status === 'processing' ? 'PROCESSING' : 'DONE'
/** 后端 DesignList 记录结构 */
interface ServerDesignList {
id: string
userId?: string
title: string
items: (Partial<DesignListEntryPayload> & Record<string, unknown>)[]
status: string
createdAt: string
updatedAt?: string
}
/** ServerDesignList → 前端 DesignItem */
function toDesignItem(rec: ServerDesignList): DesignItem {
const entry = rec.items && rec.items[0]
const toClient = (list: ServerList): DesignItem => {
const item = list.items[0]
return {
id: rec.id,
productId: entry?.productId ?? '',
productName: entry?.productName || rec.title,
// productIcon 有意不返回:服务端不存储,页面按 PRODUCT_ICON_MAP[productId] 兜底
unitPrice: entry?.unitPrice ?? 0,
count: entry?.count ?? 1,
status: SERVER_STATUS_MAP[rec.status] ?? 'undesigned',
designData: entry?.designData,
createdAt: (rec.createdAt || '').slice(0, 10),
id: list.id,
productId: item.productId,
productName: item.productName,
productIcon: getProductIconImg({ id: item.productId }),
unitPrice: Number(item.unitPrice),
count: item.count,
status: statusToClient(list),
designData: item.designData,
orderId: list.orderId,
createdAt: list.createdAt,
}
}
/** 我的设计清单(createdAt 倒序) */
export async function fetchDesignList(): Promise<DesignItem[]> {
const list = await http.get<ServerDesignList[]>('/api/design-list')
return (list || []).map(toDesignItem)
const rows = await http.get<ServerList[]>('/api/design-list')
return rows.filter(row => row.items?.length > 0).map(toClient)
}
/** 创建清单(一条设计一条清单;后端初始状态 DRAFT/undesigned */
export async function createDesign(entry: DesignListEntryPayload): Promise<DesignItem> {
const rec = await http.post<ServerDesignList>('/api/design-list', {
title: entry.productName,
items: [
{
productId: entry.productId,
productName: entry.productName,
unitPrice: entry.unitPrice,
count: entry.count,
...(entry.designData !== undefined ? { designData: entry.designData } : {}),
},
],
})
return toDesignItem(rec)
export async function fetchDesign(id: string): Promise<DesignItem> {
return toClient(await http.get<ServerList>(`/api/design-list/${id}`))
}
export interface UpdateDesignPayload {
/** 新标题(前端一般不传) */
title?: string
/** 状态迁移:前端只允许 designing(→SUBMITTEDDIY 保存设计时) */
status?: 'designing'
/** 条目整体替换(items 是全量覆盖,修改 designData 时必须带全 4 个基本字段) */
item?: DesignListEntryPayload
export async function createDesign(item: Omit<DesignItem, 'id' | 'createdAt' | 'status' | 'productIcon'>): Promise<DesignItem> {
return toClient(await http.post<ServerList>('/api/design-list', { title: item.productName, items: [{ productId: item.productId, productName: item.productName, unitPrice: item.unitPrice, count: item.count, designData: item.designData }] }))
}
/** 更新本人清单(部分更新:只传显式给出的字段) */
export async function updateDesign(id: string, payload: UpdateDesignPayload): Promise<DesignItem> {
const body: Record<string, unknown> = {}
if (payload.title !== undefined) body.title = payload.title
if (payload.status !== undefined) body.status = 'SUBMITTED'
if (payload.item !== undefined) {
body.items = [
{
productId: payload.item.productId,
productName: payload.item.productName,
unitPrice: payload.item.unitPrice,
count: payload.item.count,
...(payload.item.designData !== undefined ? { designData: payload.item.designData } : {}),
},
]
export async function updateDesign(id: string, patch: Partial<DesignItem>): Promise<DesignItem> {
const current = await fetchDesign(id)
const merged = { ...current, ...patch }
const data: Record<string, unknown> = {}
if (patch.designData !== undefined || patch.productId !== undefined || patch.productName !== undefined || patch.unitPrice !== undefined || patch.count !== undefined) {
data.items = [{ productId: merged.productId, productName: merged.productName, unitPrice: merged.unitPrice, count: merged.count, designData: merged.designData }]
}
const rec = await http.patch<ServerDesignList>(`/api/design-list/${id}`, body)
return toDesignItem(rec)
if (patch.productName !== undefined) data.title = patch.productName
if (patch.status !== undefined) data.status = statusToServer(patch.status)
return toClient(await http.patch<ServerList>(`/api/design-list/${id}`, data))
}
/** 删除本人清单 */
export async function deleteDesign(id: string): Promise<void> {
await http.del(`/api/design-list/${id}`)
}
/** 批量删除(后端只删本人条目,返回实际删除数;部分 id 无效不报错) */
export async function deleteDesigns(ids: string[]): Promise<number> {
const res = await http.post<{ deleted: number }>('/api/design-list/batch-delete', { ids })
return res?.deleted ?? 0
}
export async function deleteDesign(id: string): Promise<void> { await http.del(`/api/design-list/${id}`) }
export async function deleteDesigns(ids: string[]): Promise<void> { await http.post('/api/design-list/batch-delete', { ids }) }