import { useEffect, useState } from "react"; import { Alert, Button, Card, Col, Form, Input, InputNumber, Modal, Popconfirm, Radio, Row, Select, Space, Table, Tabs, Tag, Typography, message, } from "antd"; import type { FormInstance } from "antd"; import { ClockCircleOutlined, CodeOutlined, CopyOutlined, DollarOutlined, FileTextOutlined, KeyOutlined, LinkOutlined, MobileOutlined, PlusOutlined, SafetyCertificateOutlined, SendOutlined, WalletOutlined, } from "@ant-design/icons"; import type { ColumnsType } from "antd/es/table"; import { merchantApi } from "../api"; import { formatDateTime } from "../utils/time"; import type { ApiClient, CallbackSubscription, FulfillmentOrder, MerchantMember, MerchantRole, MerchantProduct, PageResult, WalletAccount, WalletLedgerEntry, } from "../types"; import { defaultCallbackFormValues, eventOptions, money, orderStatusOptions, orderStatusMap, pageConfig, rolePermissionText, roleText, scopeLabel, } from "./merchantCenterUtils"; export function SecretBlock({ appKey, secret, }: { appKey?: string; secret: string; }) { return ( {appKey && ( App Key {appKey} )} Secret {secret} } onClick={() => navigator.clipboard .writeText(secret) .then(() => message.success("已复制")) } > 复制 Secret ); } export function ProductsTab({ loading, products, canManage, canEditPricing = false, filters, onToggleProduct, onDeleteProduct, onFilterChange, onLoadProducts, onUpdateProduct, }: { loading: boolean; products: PageResult; canManage: boolean; canEditPricing?: boolean; filters: { keyword: string; status: string }; onToggleProduct: (record: MerchantProduct) => void; onDeleteProduct: (record: MerchantProduct) => void; onFilterChange: (filters: { keyword: string; status: string }) => void; onLoadProducts: (page?: number, size?: number) => void; onUpdateProduct?: ( id: number, data: { display_name?: string; price_amount?: number; cost_amount?: number; stock?: number; status?: "active" | "inactive"; }, ) => Promise; }) { const [keyword, setKeyword] = useState(filters.keyword); const [editModalOpen, setEditModalOpen] = useState(false); const [editingProduct, setEditingProduct] = useState( null, ); const [editForm] = Form.useForm(); const [editSubmitting, setEditSubmitting] = useState(false); const [stockMode, setStockMode] = useState<"unlimited" | "custom">( "unlimited", ); useEffect(() => { setKeyword(filters.keyword); }, [filters.keyword]); const submitKeyword = () => onFilterChange({ ...filters, keyword: keyword.trim() }); const openEditModal = (record: MerchantProduct) => { setEditingProduct(record); const isUnlimited = record.stock < 0; setStockMode(isUnlimited ? "unlimited" : "custom"); editForm.setFieldsValue({ display_name: record.display_name || record.product?.name || "", price_amount: record.price_amount, cost_amount: record.cost_amount, stock_mode: isUnlimited ? "unlimited" : "custom", stock_count: isUnlimited ? 100 : record.stock, status: record.status, }); setEditModalOpen(true); }; const handleEditSubmit = async () => { if (!editingProduct || !onUpdateProduct) return; const values = await editForm.validateFields(); setEditSubmitting(true); try { const stock = values.stock_mode === "unlimited" ? -1 : Number(values.stock_count ?? 0); await onUpdateProduct(editingProduct.id, { display_name: values.display_name?.trim(), price_amount: Number(values.price_amount), cost_amount: Number(values.cost_amount), stock, status: values.status, }); setEditModalOpen(false); } finally { setEditSubmitting(false); } }; const productColumns: ColumnsType = [ { title: "商户 SKU", dataIndex: "sku", width: 180, ellipsis: true, render: (v) => ( {v} ), }, { title: "商品名称", dataIndex: "display_name", width: 220, ellipsis: true, render: (_, r) => ( {r.display_name || r.product?.name || "-"} {r.product?.category && ( {r.product.category} )} ), }, { title: "目录编码", dataIndex: ["product", "code"], width: 150, ellipsis: true, render: (_, r) => r.product?.code ? ( {r.product.code} ) : ( "-" ), }, { title: "对外售价", dataIndex: "price_amount", width: 120, render: (v) => ( {money(v)} ), }, { title: "成本价", dataIndex: "cost_amount", width: 120, render: (v) => {money(v)}, }, { title: "当前库存", dataIndex: "stock", width: 110, render: (v) => v < 0 ? ( 无限库存 ) : v === 0 ? ( 已缺货 ) : ( `${v} 件` ), }, { title: "状态", dataIndex: "status", width: 100, render: productStatusTag, }, { title: "操作", key: "action", width: 160, fixed: "right", render: (_, record) => canManage ? ( {canEditPricing && ( openEditModal(record)} > 编辑 )} onToggleProduct(record)} > {record.status === "active" ? "下架" : "上架"} {record.status === "inactive" && ( onDeleteProduct(record)} > 删除 )} ) : ( "-" ), }, ]; return ( 商品上架与售卖管理 可编辑商品名称、对外积分售价、成本价及库存模式;已下架商品可删除。 setKeyword(e.target.value)} onSearch={submitKeyword} style={{ width: 240 }} /> onFilterChange({ ...filters, status: status || "" }) } /> { setKeyword(""); onFilterChange({ keyword: "", status: "" }); }} > 重置 {/* 编辑商品售价与库存 Modal */} setEditModalOpen(false)} confirmLoading={editSubmitting} destroyOnClose width={560} > {editingProduct?.sku} setStockMode(e.target.value)}> 无限库存 (推荐) 指定有限库存 {stockMode === "custom" && ( )} 正常上架 (可售) 立即下架 (不可售) ); } export function OrdersTab({ loading, orders, orderSource, filters, canManage, onCreateManualOrder, onOrderSourceChange, onFilterChange, onLoadOrders, onCopyLink, onOpenLink, onRevokeLink, onRestoreLink, onCancelOrder, }: { loading: boolean; orders: PageResult; orderSource: "api" | "manual"; filters: { keyword: string; order_status: string }; canManage: boolean; onCreateManualOrder: () => void; onOrderSourceChange: (source: "api" | "manual") => void; onFilterChange: (filters: { keyword: string; order_status: string }) => void; onLoadOrders: (page?: number, size?: number) => void; onCopyLink: (orderNo: string) => void; onOpenLink: (orderNo: string) => void; onRevokeLink: (orderNo: string) => void; onRestoreLink: (orderNo: string) => void; onCancelOrder: (orderNo: string, reason: string) => Promise | void; }) { const [keyword, setKeyword] = useState(filters.keyword); const [cancelling, setCancelling] = useState(null); const [cancelReason, setCancelReason] = useState(""); const [cancelSubmitting, setCancelSubmitting] = useState(false); useEffect(() => { setKeyword(filters.keyword); }, [filters.keyword]); const submitKeyword = () => onFilterChange({ ...filters, keyword: keyword.trim() }); const submitCancel = async () => { if (!cancelling) return; if (!cancelReason.trim()) { message.warning("请填写取消原因"); return; } setCancelSubmitting(true); try { await onCancelOrder(cancelling.order_no, cancelReason.trim()); setCancelling(null); setCancelReason(""); } finally { setCancelSubmitting(false); } }; const orderColumns: ColumnsType = [ { title: "平台订单号", dataIndex: "order_no", width: 190, ellipsis: true, render: (v) => ( {v} ), }, { title: "商户单号", dataIndex: "client_order_no", width: 140, ellipsis: true, }, { title: "SKU", dataIndex: "product_sku", width: 130, ellipsis: true, render: (v) => ( {v} ), }, { title: "商品", dataIndex: "product_name", width: 220, ellipsis: true }, { title: "基础金额", dataIndex: "base_amount", width: 88, render: money }, { title: "手续费", dataIndex: "service_fee_amount", width: 82, render: money, }, { title: "扣款合计", dataIndex: "amount", width: 88, render: money }, { title: "状态", dataIndex: "order_status", width: 92, render: orderStatusTag, }, { title: "链接有效期", dataIndex: "delivery_link_expires_at", width: 140, render: (_, record) => { if (record.delivery_link_revoked_at) return 已作废; if (!record.delivery_link_expires_at) return 永久有效; return formatDateTime(record.delivery_link_expires_at); }, }, { title: "时间", dataIndex: "created_at", width: 150, render: formatDateTime, }, { title: "发货链接", key: "delivery_link", width: 160, fixed: "right", render: (_, record) => { // 非可发货状态不再展示链接作废等操作;已交付订单保留复制/打开,便于查看兑换结果。 if (record.order_status === "cancelled") { return 订单已取消; } if (record.order_status === "delivering") { return 发货中; } if (record.order_status === "delivered") { if (record.delivery_link_revoked_at) { return 已作废; } // expires_at 为空表示永久有效,不视为过期 const expired = !!record.delivery_link_expires_at && new Date(record.delivery_link_expires_at).getTime() < Date.now(); if (expired) { return 已过期; } return ( onCopyLink(record.order_no)} > 复制 onOpenLink(record.order_no)} > 打开 ); } if (record.delivery_link_revoked_at) { return ( onRestoreLink(record.order_no)} > 恢复链接 ); } return ( onCopyLink(record.order_no)} > 复制 onOpenLink(record.order_no)} > 打开 onRevokeLink(record.order_no)} > 链接作废 ); }, }, { title: "操作", key: "action", width: 110, fixed: "right", render: (_, record) => { if (!canManage) return "-"; if ( record.order_status === "paid" || record.order_status === "ship_failed" ) { return ( { setCancelling(record); setCancelReason(""); }} > 取消订单 ); } return "-"; }, }, ]; return ( onOrderSourceChange(key as "api" | "manual")} items={[ { key: "api", label: "API 订单" }, { key: "manual", label: "手动订单" }, ]} style={{ minWidth: 220 }} /> {canManage && ( } onClick={onCreateManualOrder} > 创建手动订单 )} setKeyword(event.target.value)} onSearch={submitKeyword} placeholder="搜索平台订单号或商户单号" allowClear onClear={() => onFilterChange({ ...filters, keyword: "" })} style={{ width: 300, maxWidth: "100%" }} /> {orderStatusOptions.map((option) => ( onFilterChange({ ...filters, order_status: option.value }) } style={{ minWidth: 76 }} > {option.label} ))} { setCancelling(null); setCancelReason(""); }} okText="确认取消并退款" okButtonProps={{ danger: true }} destroyOnClose > setCancelReason(e.target.value)} placeholder="请填写取消原因(必填)" /> ); } export function WalletTab({ loading, wallet, canFinance, ledger, form, onFilter, onResetFilter, onLoadLedger, }: { loading: boolean; wallet: WalletAccount | null; canFinance: boolean; ledger: PageResult; form: FormInstance; onFilter: () => void; onResetFilter: () => void; onLoadLedger: (page: number, size: number) => void; }) { const ledgerColumns: ColumnsType = [ { title: "流水号", dataIndex: "entry_no", width: 260, render: (v) => ( {v} ), }, { title: "类型", dataIndex: "type", width: 90, render: ledgerTypeTag }, { title: "变动积分", dataIndex: "amount", width: 130, render: moneyWithSign, }, { title: "结余积分", dataIndex: "balance_after", width: 130, render: (v) => ( {money(v)} ), }, { title: "关联单号", dataIndex: "reference_no", width: 200, ellipsis: true, render: (v) => v ? ( {v} ) : ( "-" ), }, { title: "备注说明", dataIndex: "note", width: 220, ellipsis: { showTitle: false }, render: (v) => ( {v || "-"} ), }, { title: "变动时间", dataIndex: "created_at", width: 170, render: formatDateTime, }, ]; return ( 可用余额 {(wallet?.available_balance ?? 0).toLocaleString("zh-CN")} 积分 充值/调账 仅平台管理员可操作 冻结余额 {(wallet?.frozen_balance ?? 0).toLocaleString("zh-CN")} 积分 提现/发货预扣 0 笔预扣 结算币种与状态 {wallet?.currency || "POINT"} 账户正常 计费方式 自动按单扣减 {canFinance && ( 入账 (Credit) 扣款 (Debit) 退款 (Refund) 调账 (Adjust) 重置 查询 )} {canFinance ? ( ) : ( 当前角色可查看余额,钱包流水仅财务或负责人可见。 )} ); } export function ApiKeysTab({ loading, clients, clientMax, isAdmin, canManage, form, onOpenCreate, onNavigateDocs, onNavigateDebug, onToggleClient, onDeleteClient, }: { loading: boolean; clients: ApiClient[]; clientMax: number; isAdmin: boolean; canManage: boolean; form: FormInstance; onOpenCreate: () => void; onNavigateDocs: () => void; onNavigateDebug: () => void; onToggleClient: (record: ApiClient) => void; onDeleteClient: (record: ApiClient) => void; }) { const apiClientColumns: ColumnsType = [ { title: "名称", dataIndex: "name", width: 140, ellipsis: true }, { title: "App Key", dataIndex: "app_key", width: 230, render: (v) => ( {v} ), }, { title: "签名", dataIndex: "signature_version", width: 80, render: (v) => {v}, }, { title: "权限", dataIndex: "scopes", width: 250, render: scopesTag }, { title: "状态", dataIndex: "status", width: 80, render: activeStatusTag }, { title: "最后使用", dataIndex: "last_used_at", width: 170, render: formatDateTime, }, { title: "操作", key: "action", width: 150, render: (_, record) => canManage ? ( onToggleClient(record)} > {record.status === "active" ? "禁用" : "启用"} onDeleteClient(record)} > 删除 ) : ( "-" ), }, ]; return ( API 密钥与开放接口鉴权 已配额 {clients.length} / {clientMax} } onClick={onNavigateDocs}> 对接文档 {isAdmin && ( } onClick={onNavigateDebug}> 接口调试 )} {canManage && ( } disabled={clients.length >= clientMax} onClick={() => { form.resetFields(); form.setFieldsValue({ signature_version: "v1", scopes: ["products:read", "orders:read", "orders:write"], }); onOpenCreate(); }} > 新增 API 密钥 )} } > 用于第三方分销系统或自建后台调用开放 API (商品查询、订单发货) 的 HMAC-SHA256 签名鉴权凭证。新生成的 App Secret 仅在创建成功时展示一次,请妥善保存。 ); } export function CallbacksTab({ canManage, callback, form, onSubmit, onRotateSecret, }: { canManage: boolean; callback: CallbackSubscription | null; form: FormInstance; onSubmit: () => void; onRotateSecret: () => void; }) { return ( Webhook 异步回调订阅设置 {callback?.status === "active" ? ( 推送已启用 ) : ( 推送已禁用 )} {" "} HMAC-SHA256 签名保护 } > 重试机制 Outbox 队列 + 指数退避 (最多 16 次) 推送信道签名 Header X-Signature (HMAC-SHA256) 推送超时标准 5000 ms 强制响应超时 } placeholder="https://your-domain.com/api/v1/webhook" allowClear /> {callback?.updated_at ? `上次保存:${formatDateTime(callback.updated_at)}` : "尚未保存回调配置"} {canManage && ( 重置 Secret )} {canManage && ( 保存回调配置 )} ); } export function MembersTab({ loading, members, roles, canManage, form, onOpenAdd, onOpenCreateRole, onEditRole, onEditMember, onToggleMemberStatus, onRemoveMember, onReload, currentUserID, }: { loading: boolean; members: MerchantMember[]; roles: MerchantRole[]; canManage: boolean; form: FormInstance; onOpenAdd: () => void; onOpenCreateRole: () => void; onEditRole: (role: MerchantRole) => void; onEditMember: (member: MerchantMember) => void; onToggleMemberStatus: (member: MerchantMember) => void; onRemoveMember: (member: MerchantMember) => void; onReload: () => void; currentUserID?: number; }) { const [phoneTarget, setPhoneTarget] = useState(null); const [phoneValue, setPhoneValue] = useState(""); const [phoneSaving, setPhoneSaving] = useState(false); // 负责人账号的手机号/设备只能由本人或平台管理员维护,避免成员改绑后接管账号。 const canEditSecurity = (member: MerchantMember) => canManage && (member.role !== "owner" || member.user_id === currentUserID); const submitPhone = async () => { if (!phoneTarget) return; const phone = phoneValue.trim(); if (!/^1[3-9]\d{9}$/.test(phone)) { message.warning("请输入正确的 11 位手机号"); return; } setPhoneSaving(true); try { await merchantApi.bindMemberPhone(phoneTarget.id, phone); message.success("手机号已更新"); setPhoneTarget(null); setPhoneValue(""); onReload(); } catch (error) { message.error(error instanceof Error ? error.message : "手机号绑定失败"); } finally { setPhoneSaving(false); } }; const resetDevice = async (member: MerchantMember) => { try { await merchantApi.resetMemberDevice(member.id); message.success("设备绑定已重置,该成员下次登录需重新绑定"); onReload(); } catch (error) { message.error(error instanceof Error ? error.message : "设备重置失败"); } }; const roleNames = new Map(roles.map((item) => [item.code, item.name])); const memberColumns: ColumnsType = [ { title: "用户", dataIndex: ["user", "username"], render: (_, r) => r.user?.username || `#${r.user_id}`, }, { title: "昵称", dataIndex: ["user", "nickname"], render: (_, r) => r.user?.nickname || "-", }, { title: "手机号", width: 170, render: (_, member) => { const phone = member.user?.phone_masked || ""; return ( {phone ? ( {phone} ) : ( 未绑定 )} {canEditSecurity(member) && ( { setPhoneTarget(member); setPhoneValue(""); }} > {phone ? "修改" : "绑定"} )} ); }, }, { title: "登录设备", width: 210, render: (_, member) => { const bound = Boolean(member.user?.device_bound); return ( {bound ? ( {member.user?.device_id_masked || "已绑定"} ) : ( 未绑定 )} {bound && canEditSecurity(member) && ( resetDevice(member)} > 重置 )} ); }, }, { title: "角色", dataIndex: "role", width: 140, render: (value) => ( {roleNames.get(value) || roleText(value)} ), }, { title: "默认", dataIndex: "is_default", width: 80, render: (v) => (v ? 默认 : "-"), }, { title: "状态", dataIndex: "status", width: 90, render: (v) => v === 1 ? 启用 : 禁用, }, { title: "操作", width: 210, render: (_, member) => { const protectedMember = member.role === "owner" || member.user_id === currentUserID; if (!canManage) return "-"; if (protectedMember) return 受保护; const action = member.status === 1 ? "停用" : "启用"; return ( onEditMember(member)} > 编辑 onToggleMemberStatus(member)} > {action} onRemoveMember(member)} okButtonProps={{ danger: true }} > 移除 ); }, }, ]; return ( 成员仅属于当前商户;角色和权限也在当前商户内独立配置。 {canManage && ( { form.resetFields(); onOpenCreateRole(); }} > 新建角色 } onClick={() => { form.resetFields(); onOpenAdd(); }} > 添加员工 )} role.id || role.code} size="small" pagination={false} dataSource={roles} columns={[ { title: "角色", dataIndex: "name", width: 160, render: (name, role) => ( {name} {role.code === "owner" && ( 固定 )} ), }, { title: "权限", dataIndex: "permissions", render: (permissions) => permissions ? permissions .split(",") .map((item: string) => ( {rolePermissionText(item)} )) : "-", }, { title: "操作", width: 90, render: (_, role) => role.code === "owner" ? ( "-" ) : ( onEditRole(role)} > 编辑 ), }, ]} /> setPhoneTarget(null)} onOk={() => void submitPhone()} confirmLoading={phoneSaving} okText="保存" cancelText="取消" > 绑定后该成员登录时需要短信验证码,并强制校验本机 DCAuth 设备。 setPhoneValue(event.target.value)} onPressEnter={() => void submitPhone()} placeholder="请输入 11 位手机号" maxLength={11} inputMode="numeric" prefix={} /> ); } function moneyWithSign(value?: number | null) { const amount = Number(value || 0); if (amount > 0) { return ( +{money(amount)} ); } if (amount < 0) { return ( {money(amount)} ); } return ( {money(amount)} ); } function productStatusTag(value: string) { return value === "active" ? ( 已上架 ) : ( 已下架 ); } function activeStatusTag(value: string) { return value === "active" ? ( 已启用 ) : ( 已禁用 ); } function orderStatusTag(value: string) { const item = orderStatusMap[value] || { color: "default", text: value }; const tagClassMap: Record = { paid: "status-tag--blue", delivering: "status-tag--cyan", delivered: "status-tag--green", ship_failed: "status-tag--red", cancelled: "status-tag--gray", }; const tagClass = tagClassMap[value] || "status-tag--gray"; return ( {item.text} ); } function ledgerTypeTag(value: string) { const map: Record = { credit: { color: "green", text: "入账" }, debit: { color: "red", text: "扣款" }, refund: { color: "purple", text: "退款" }, adjust: { color: "blue", text: "调整" }, }; const item = map[value] || { color: "default", text: value }; return {item.text}; } function scopesTag(value: string) { const scopes = (value || "").split(",").filter(Boolean); if (scopes.length === 0) { return "-"; } return ( {scopes.map((s) => ( {scopeLabel(s)} ))} ); }