diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx index 44a0851..694f46e 100644 --- a/apps/admin-web/src/App.tsx +++ b/apps/admin-web/src/App.tsx @@ -18,6 +18,10 @@ import CitiesPage from './pages/CitiesPage'; import CityPartnersPage from './pages/CityPartnersPage'; import CityWarehousesPage from './pages/CityWarehousesPage'; import StoreMediaPage from './pages/StoreMediaPage'; +import PromoCodesPage from './pages/PromoCodesPage'; +import PromoCodeDetailLayout from './pages/promo/PromoCodeDetailLayout'; +import PromoCodeDetailPage from './pages/promo/PromoCodeDetailPage'; +import PromoCodeUsersPage from './pages/promo/PromoCodeUsersPage'; import ProductsPage from './pages/ProductsPage'; import ProductDetailTemplatesPage from './pages/ProductDetailTemplatesPage'; import ResourcesPage from './pages/ResourcesPage'; @@ -52,6 +56,11 @@ export default function App() { } /> } /> } /> + } /> + }> + } /> + } /> + } /> } /> } /> diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx index 390e6a3..2915d2f 100644 --- a/apps/admin-web/src/layouts/AdminLayout.tsx +++ b/apps/admin-web/src/layouts/AdminLayout.tsx @@ -35,6 +35,7 @@ const MENU_ITEMS: MenuProps['items'] = [ ], }, { key: '/orders', icon: , label: '订单' }, + { key: '/promo-codes', icon: , label: '推广码' }, { key: 'stores-group', icon: , @@ -118,7 +119,9 @@ export default function AdminLayout() { navigate('/login'); } - const selectedKey = location.pathname; + const selectedKey = location.pathname.startsWith('/promo-codes') + ? '/promo-codes' + : location.pathname; return ( diff --git a/apps/admin-web/src/lib/api.ts b/apps/admin-web/src/lib/api.ts index 658e8f3..f327cca 100644 --- a/apps/admin-web/src/lib/api.ts +++ b/apps/admin-web/src/lib/api.ts @@ -83,6 +83,9 @@ export type AdminUserRow = { wechatVerified: boolean; nickname: string | null; status: number; + sourceType: string; + sourceRefId: string | null; + sourceLabel: string | null; createdAt: string; orderCount: number; }; diff --git a/apps/admin-web/src/pages/PromoCodesPage.tsx b/apps/admin-web/src/pages/PromoCodesPage.tsx new file mode 100644 index 0000000..e0cfa53 --- /dev/null +++ b/apps/admin-web/src/pages/PromoCodesPage.tsx @@ -0,0 +1,236 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Button, Form, Input, Modal, Select, Space, Table, Tag, Typography, message, +} from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { + PROMO_CODE_SCENE_LABELS, + PROMO_CODE_STATUS_LABELS, + promoConversion, + type PromoCodeItem, + type PromoCodeScene, +} from '@dukang/shared-types'; +import { request } from '../lib/api'; +import { fmtTime } from '../lib/constants'; +import { useAdminList } from '../lib/useAdminList'; + +type Row = PromoCodeItem; + +type SceneOption = { value: PromoCodeScene; label: string }; + +async function downloadQrcode(url: string, filename: string) { + try { + const res = await fetch(url); + const blob = await res.blob(); + const objectUrl = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = objectUrl; + a.download = filename; + a.click(); + URL.revokeObjectURL(objectUrl); + } catch { + window.open(url, '_blank'); + } +} + +export default function PromoCodesPage() { + const navigate = useNavigate(); + const [filterForm] = Form.useForm(); + const [createForm] = Form.useForm(); + const [filters, setFilters] = useState>({}); + const [scenes, setScenes] = useState( + Object.entries(PROMO_CODE_SCENE_LABELS).map(([value, label]) => ({ + value: value as PromoCodeScene, + label, + })), + ); + const [createOpen, setCreateOpen] = useState(false); + const [creating, setCreating] = useState(false); + + const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( + '/admin/promo-codes', + () => { + const qs = new URLSearchParams(); + if (filters.name) qs.set('name', filters.name); + if (filters.code) qs.set('code', filters.code); + if (filters.status) qs.set('status', filters.status); + if (filters.scene) qs.set('scene', filters.scene); + return qs; + }, + [filters], + ); + + async function loadScenes() { + try { + const list = await request('/admin/promo-codes/scenes'); + if (list.length) setScenes(list); + } catch { + /* 使用本地默认场景 */ + } + } + + useEffect(() => { + void loadScenes(); + }, []); + + const columns: ColumnsType = [ + { title: '名称', dataIndex: 'name', width: 160, ellipsis: true }, + { title: '码值', dataIndex: 'code', width: 110 }, + { + title: '场景', + dataIndex: 'scene', + width: 110, + render: (s: PromoCodeScene) => PROMO_CODE_SCENE_LABELS[s] || s, + }, + { + title: '状态', + dataIndex: 'status', + width: 90, + render: (s) => ( + + {PROMO_CODE_STATUS_LABELS[s as keyof typeof PROMO_CODE_STATUS_LABELS] || s} + + ), + }, + { title: '扫码', dataIndex: 'scanCount', width: 70 }, + { title: '订单', dataIndex: 'orderCount', width: 70 }, + { + title: '转化率', + width: 90, + render: (_, row) => promoConversion(row.scanCount, row.orderCount), + }, + { + title: '渠道负责人', + width: 120, + render: (_, row) => row.ownerUser?.userNo || row.ownerUser?.phone || '—', + }, + { title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime }, + { + title: '操作', + width: 220, + fixed: 'right', + render: (_, row) => ( + + + + + + ), + }, + ]; + + async function handleCreate(values: Record) { + setCreating(true); + try { + const created = await request('/admin/promo-codes', { + method: 'POST', + body: JSON.stringify({ + name: values.name, + code: values.code?.trim() || undefined, + scene: values.scene, + ownerUserId: values.ownerUserId?.trim() || undefined, + remark: values.remark?.trim() || undefined, + }), + }); + message.success('推广码已创建'); + setCreateOpen(false); + createForm.resetFields(); + void reload(); + navigate(`/promo-codes/${created.id}`); + } catch (e) { + message.error(e instanceof Error ? e.message : '创建失败'); + } finally { + setCreating(false); + } + } + + return ( +
+
+ 推广码管理 + +
+ +
{ setFilters(v); setPage(1); }} + > + + + + ({ value, label }))} + /> + + +
+ + { setPage(p); setPageSize(ps); }, + }} + /> + + setCreateOpen(false)} + footer={null} + destroyOnClose + > +
+ + + + + + + + + + + + + + +
+ + ); +} diff --git a/apps/admin-web/src/pages/UsersPage.tsx b/apps/admin-web/src/pages/UsersPage.tsx index 694a078..c3f1f32 100644 --- a/apps/admin-web/src/pages/UsersPage.tsx +++ b/apps/admin-web/src/pages/UsersPage.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Alert, Button, @@ -17,6 +17,7 @@ import { message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; +import { USER_SOURCE_TYPE_LABELS, type UserSourceType } from '@dukang/shared-types'; import { request, type AdminUserRow, type Paginated } from '../lib/api'; import { ORDER_STATUS_LABELS, fmtTime } from '../lib/constants'; @@ -33,6 +34,7 @@ type UserDetail = AdminUserRow & { wxUnionId?: string | null; cityPref?: Record | null; mergedInto?: { id: string; userNo: string; phone: string | null; nickname: string | null } | null; + sourcePromo?: { id: string; code: string; name: string } | null; orders?: UserOrderRow[]; mergedFromCount?: number; addressCount?: number; @@ -62,6 +64,7 @@ type BatchDeletePreview = { export default function UsersPage() { const navigate = useNavigate(); + const location = useLocation(); const [form] = Form.useForm(); const [data, setData] = useState | null>(null); const [loading, setLoading] = useState(false); @@ -105,6 +108,14 @@ export default function UsersPage() { void load(); }, [load]); + useEffect(() => { + const openUserId = (location.state as { openUserId?: string } | null)?.openUserId; + if (openUserId) { + void openDetail(openUserId); + navigate(location.pathname, { replace: true, state: null }); + } + }, [location.state, location.pathname, navigate]); + async function openDetail(id: string) { const res = await request(`/admin/users/${id}`); setDetail(res); @@ -225,6 +236,40 @@ export default function UsersPage() { width: 100, render: (v) => (v ? 微信已验证 : 未绑定), }, + { + title: '来源类型', + dataIndex: 'sourceType', + width: 100, + render: (v: string) => ( + + {USER_SOURCE_TYPE_LABELS[v as UserSourceType] || v} + + ), + }, + { + title: '来源 ID', + dataIndex: 'sourceRefId', + width: 100, + ellipsis: true, + render: (v, row) => { + if (!v) return '—'; + if (row.sourceType === 'PROMO_CODE') { + return ( + e.stopPropagation()}> + {v} + + ); + } + return v; + }, + }, + { + title: '来源标签', + dataIndex: 'sourceLabel', + width: 120, + ellipsis: true, + render: (v) => v || '—', + }, { title: 'deviceKey', dataIndex: 'deviceKey', @@ -307,7 +352,7 @@ export default function UsersPage() { loading={loading} columns={columns} dataSource={data?.items ?? []} - scroll={{ x: 1200 }} + scroll={{ x: 1500 }} rowSelection={{ selectedRowKeys, preserveSelectedRowKeys: true, @@ -347,6 +392,28 @@ export default function UsersPage() { {detail.wechatVerified ? 微信已验证 : 未绑定} + + + {USER_SOURCE_TYPE_LABELS[detail.sourceType as UserSourceType] || detail.sourceType} + + + + {detail.sourceRefId ? ( + detail.sourceType === 'PROMO_CODE' ? ( + {detail.sourceRefId} + ) : ( + detail.sourceRefId + ) + ) : '—'} + + {detail.sourceLabel || '—'} + {detail.sourcePromo && ( + + + {detail.sourcePromo.name}({detail.sourcePromo.code}) + + + )} {detail.wxOpenId || '—'} {detail.wxUnionId || '—'} {detail.deviceKey || '—'} diff --git a/apps/admin-web/src/pages/promo/PromoCodeDetailLayout.tsx b/apps/admin-web/src/pages/promo/PromoCodeDetailLayout.tsx new file mode 100644 index 0000000..1dbb675 --- /dev/null +++ b/apps/admin-web/src/pages/promo/PromoCodeDetailLayout.tsx @@ -0,0 +1,108 @@ +import { useEffect, useState } from 'react'; +import { Link, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom'; +import { Breadcrumb, Button, Space, Spin, Tabs, Tag, Typography } from 'antd'; +import { ArrowLeftOutlined } from '@ant-design/icons'; +import { + PROMO_CODE_STATUS_LABELS, + type PromoCodeItem, + type PromoCodeStats, +} from '@dukang/shared-types'; +import { request } from '../../lib/api'; + +export type PromoCodeDetailContext = { + detail: PromoCodeItem & { stats?: PromoCodeStats }; + reload: () => Promise; +}; + +export default function PromoCodeDetailLayout() { + const { id = '' } = useParams(); + const navigate = useNavigate(); + const location = useLocation(); + const [detail, setDetail] = useState<(PromoCodeItem & { stats?: PromoCodeStats }) | null>(null); + const [loading, setLoading] = useState(true); + + async function loadDetail() { + setLoading(true); + try { + const res = await request(`/admin/promo-codes/${id}`); + setDetail(res); + } catch { + setDetail(null); + } finally { + setLoading(false); + } + } + + useEffect(() => { + void loadDetail(); + }, [id]); + + const tabKey = location.pathname.endsWith('/users') ? 'users' : 'overview'; + + if (loading) { + return ; + } + + if (!detail) { + return ( +
+ 推广码不存在或加载失败 +
+ +
+
+ ); + } + + return ( +
+ 推广码管理 }, + { title: detail.name }, + ]} + /> + +
+ + +
+ + { + if (key === 'users') navigate(`/promo-codes/${id}/users`); + else navigate(`/promo-codes/${id}`); + }} + items={[ + { key: 'overview', label: '概览' }, + { + key: 'users', + label: `关联用户${detail.stats?.sourceMarkedCount != null ? ` (${detail.stats.sourceMarkedCount})` : ''}`, + }, + ]} + style={{ marginBottom: 16 }} + /> + + +
+ ); +} diff --git a/apps/admin-web/src/pages/promo/PromoCodeDetailPage.tsx b/apps/admin-web/src/pages/promo/PromoCodeDetailPage.tsx new file mode 100644 index 0000000..079330e --- /dev/null +++ b/apps/admin-web/src/pages/promo/PromoCodeDetailPage.tsx @@ -0,0 +1,226 @@ +import { useState } from 'react'; +import { useOutletContext } from 'react-router-dom'; +import { + Button, + Card, + Col, + Descriptions, + Form, + Input, + Modal, + Popconfirm, + Row, + Select, + Space, + Statistic, + Typography, + message, +} from 'antd'; +import { + PROMO_CODE_SCENE_LABELS, + PROMO_CODE_STATUS_LABELS, + promoConversion, +} from '@dukang/shared-types'; +import { request } from '../../lib/api'; +import { fmtTime } from '../../lib/constants'; +import type { PromoCodeDetailContext } from './PromoCodeDetailLayout'; + +async function downloadQrcode(url: string, filename: string) { + try { + const res = await fetch(url); + const blob = await res.blob(); + const objectUrl = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = objectUrl; + a.download = filename; + a.click(); + URL.revokeObjectURL(objectUrl); + } catch { + window.open(url, '_blank'); + } +} + +export default function PromoCodeDetailPage() { + const { detail, reload } = useOutletContext(); + const [editForm] = Form.useForm(); + const [editOpen, setEditOpen] = useState(false); + const [saving, setSaving] = useState(false); + + const stats = detail.stats; + + async function handleEdit(values: Record) { + setSaving(true); + try { + await request(`/admin/promo-codes/${detail.id}`, { + method: 'PUT', + body: JSON.stringify({ + name: values.name, + scene: values.scene, + ownerUserId: values.ownerUserId?.trim() || null, + remark: values.remark?.trim() || null, + }), + }); + message.success('已保存'); + setEditOpen(false); + await reload(); + } catch (e) { + message.error(e instanceof Error ? e.message : '保存失败'); + } finally { + setSaving(false); + } + } + + return ( + <> + +
+ + {detail.qrcodeUrl ? ( +
+ 推广二维码 + + + +
+ ) : ( + 暂无二维码 + )} +
+ + + + + + { + await request(`/admin/promo-codes/${detail.id}/status`, { + method: 'PUT', + body: JSON.stringify({ + status: detail.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE', + }), + }); + message.success('状态已更新'); + await reload(); + }} + > + + + + )} + > + + {detail.name} + {detail.code} + + {PROMO_CODE_SCENE_LABELS[detail.scene] || detail.scene} + + + {PROMO_CODE_STATUS_LABELS[detail.status] || detail.status} + + + {detail.qrcodeId} + + + {detail.landingUrl} + + + {detail.qrcodeUrl ? ( + + {detail.qrcodeUrl} + + ) : '—'} + + + {detail.ownerUser?.userNo || detail.ownerUser?.phone || '—'} + + {detail.remark || '—'} + {fmtTime(detail.createdAt)} + {fmtTime(detail.updatedAt)} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setEditOpen(false)} + footer={null} + destroyOnClose + > +
+ + + + + + + + + + + +
+ + ); +} diff --git a/apps/admin-web/src/pages/promo/PromoCodeUsersPage.tsx b/apps/admin-web/src/pages/promo/PromoCodeUsersPage.tsx new file mode 100644 index 0000000..4860bcc --- /dev/null +++ b/apps/admin-web/src/pages/promo/PromoCodeUsersPage.tsx @@ -0,0 +1,89 @@ +import { useNavigate, useOutletContext, useParams } from 'react-router-dom'; +import { Button, Space, Table, Tag } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { + USER_SOURCE_TYPE_LABELS, + type PromoCodeAttributedUser, + type UserSourceType, +} from '@dukang/shared-types'; +import { fmtTime } from '../../lib/constants'; +import { useAdminList } from '../../lib/useAdminList'; +import type { PromoCodeDetailContext } from './PromoCodeDetailLayout'; + +export default function PromoCodeUsersPage() { + const { id = '' } = useParams(); + const navigate = useNavigate(); + const { detail } = useOutletContext(); + + const { data, loading, page, pageSize, setPage, setPageSize } = useAdminList( + `/admin/promo-codes/${id}/users`, + () => new URLSearchParams(), + [id], + ); + + const columns: ColumnsType = [ + { title: '用户编号', dataIndex: 'userNo', width: 120 }, + { title: '昵称', dataIndex: 'nickname', width: 100, render: (v) => v || '—' }, + { title: '手机', dataIndex: 'phone', width: 120, render: (v) => v || '—' }, + { + title: '验手机', + dataIndex: 'phoneVerifiedAt', + width: 90, + render: (v) => (v ? 已验证 : 访客), + }, + { + title: '来源类型', + dataIndex: 'sourceType', + width: 100, + render: (v: string) => ( + + {USER_SOURCE_TYPE_LABELS[v as UserSourceType] || v} + + ), + }, + { + title: '来源 ID', + dataIndex: 'sourceRefId', + width: 100, + render: (v) => (v === detail.id ? 本码 : v || '—'), + }, + { + title: '首次触达', + dataIndex: 'firstTouchAt', + width: 160, + render: (v) => (v ? fmtTime(v) : '—'), + }, + { title: '订单数', dataIndex: 'orderCount', width: 80 }, + { title: '注册时间', dataIndex: 'createdAt', width: 160, render: fmtTime }, + { + title: '操作', + width: 100, + fixed: 'right', + render: (_, row) => ( + + + + ), + }, + ]; + + return ( +
{ setPage(p); setPageSize(ps); }, + }} + /> + ); +} diff --git a/apps/h5-partner/src/PartnerAppRoutes.tsx b/apps/h5-partner/src/PartnerAppRoutes.tsx index 13671c9..d673bc9 100644 --- a/apps/h5-partner/src/PartnerAppRoutes.tsx +++ b/apps/h5-partner/src/PartnerAppRoutes.tsx @@ -14,7 +14,7 @@ import BillsPage from './pages/BillsPage'; import SettlementPage from './pages/SettlementPage'; import ReshipPage from './pages/ReshipPage'; import WeeklyReportPage from './pages/WeeklyReportPage'; -import LeaderboardPage from './pages/LeaderboardPage'; +import ProxyOrderPage from './pages/ProxyOrderPage'; import StaffListPage from './pages/StaffListPage'; import StaffCreatePage from './pages/StaffCreatePage'; @@ -32,6 +32,7 @@ function PrimaryRoutes() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/h5-partner/src/pages/HomePage.tsx b/apps/h5-partner/src/pages/HomePage.tsx index 2a823e5..2fb08b3 100644 --- a/apps/h5-partner/src/pages/HomePage.tsx +++ b/apps/h5-partner/src/pages/HomePage.tsx @@ -100,6 +100,12 @@ export default function HomePage() {
+ +
+ shopping_cart_checkout +
+ 代下单 +
add_business diff --git a/apps/h5-partner/src/pages/ProxyOrderPage.tsx b/apps/h5-partner/src/pages/ProxyOrderPage.tsx new file mode 100644 index 0000000..338cb4f --- /dev/null +++ b/apps/h5-partner/src/pages/ProxyOrderPage.tsx @@ -0,0 +1,330 @@ +import { useEffect, useMemo, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import PageHeader from '@dukang/shared-ui/PageHeader'; +import ChinaRegionPicker from '../components/ChinaRegionPicker'; +import { request } from '../lib/api'; +import { formatRegionLabel, parseRegionCodes } from '../lib/china-region'; +import { toastError, toastSuccess } from '../lib/toast'; +import type { + PartnerProxyOrderCreateRequest, + PartnerProxyOrderOptions, + PartnerProxyOrderPreviewResult, +} from '@dukang/shared-types'; + +function fmtMoney(n: number) { + return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +export default function ProxyOrderPage() { + const navigate = useNavigate(); + const [options, setOptions] = useState(null); + const [loadingOptions, setLoadingOptions] = useState(true); + const [phone, setPhone] = useState(''); + const [smsCode, setSmsCode] = useState(''); + const [receiverName, setReceiverName] = useState(''); + const [regionCodes, setRegionCodes] = useState([]); + const [addressDetail, setAddressDetail] = useState(''); + const [productId, setProductId] = useState(''); + const [quantity, setQuantity] = useState(2); + const [promoCodeId, setPromoCodeId] = useState(''); + const [preview, setPreview] = useState(null); + const [previewLoading, setPreviewLoading] = useState(false); + const [smsCooldown, setSmsCooldown] = useState(0); + const [submitting, setSubmitting] = useState(false); + const [msg, setMsg] = useState(''); + + const region = useMemo(() => parseRegionCodes(regionCodes), [regionCodes]); + const regionLabel = region ? formatRegionLabel(region) : ''; + + useEffect(() => { + request('PARTNER_H5', '/partner/proxy-orders/options') + .then((data) => { + setOptions(data); + if (data.products[0]) setProductId(data.products[0].id); + }) + .catch((e) => toastError(e instanceof Error ? e.message : '加载失败')) + .finally(() => setLoadingOptions(false)); + }, []); + + useEffect(() => { + if (!productId || quantity < 1) { + setPreview(null); + return; + } + const timer = setTimeout(() => { + setPreviewLoading(true); + request('PARTNER_H5', '/partner/proxy-orders/preview', { + method: 'POST', + body: JSON.stringify({ + productId, + quantity, + receiverCity: region.city || undefined, + receiverDistrict: region.district || undefined, + }), + silent: true, + }) + .then(setPreview) + .catch(() => setPreview(null)) + .finally(() => setPreviewLoading(false)); + }, 300); + return () => clearTimeout(timer); + }, [productId, quantity, region.city, region.district]); + + async function sendSms() { + setMsg(''); + if (!/^1\d{10}$/.test(phone.trim())) { + setMsg('请输入有效手机号'); + return; + } + try { + const res = await request<{ maskedPhone: string }>('PARTNER_H5', '/partner/proxy-orders/send-sms', { + method: 'POST', + body: JSON.stringify({ phone: phone.trim() }), + silent: true, + }); + setMsg(`验证码已发送至 ${res.maskedPhone}`); + setSmsCooldown(60); + const timer = setInterval(() => { + setSmsCooldown((s) => { + if (s <= 1) { + clearInterval(timer); + return 0; + } + return s - 1; + }); + }, 1000); + } catch (e) { + setMsg(e instanceof Error ? e.message : '发送失败'); + } + } + + async function submit() { + setMsg(''); + if (!/^1\d{10}$/.test(phone.trim())) { + setMsg('请输入有效手机号'); + return; + } + if (!smsCode.trim()) { + setMsg('请输入验证码'); + return; + } + if (!region?.province || !region.city || !region.district) { + setMsg('请选择省市区'); + return; + } + if (!addressDetail.trim()) { + setMsg('请填写详细地址'); + return; + } + if (!productId) { + setMsg('请选择商品'); + return; + } + + const payload: PartnerProxyOrderCreateRequest = { + phone: phone.trim(), + smsCode: smsCode.trim(), + receiverName: receiverName.trim() || undefined, + province: region.province, + city: region.city, + district: region.district, + addressDetail: addressDetail.trim(), + productId, + quantity, + promoCodeId: promoCodeId || undefined, + }; + + setSubmitting(true); + try { + const order = await request<{ id: string; orderNo: string }>('PARTNER_H5', '/partner/proxy-orders', { + method: 'POST', + body: JSON.stringify(payload), + silent: true, + }); + toastSuccess(`代下单成功:${order.orderNo}`); + navigate(`/orders/${order.id}`); + } catch (e) { + setMsg(e instanceof Error ? e.message : '下单失败'); + } finally { + setSubmitting(false); + } + } + + const selectedProduct = options?.products.find((p) => p.id === productId); + + return ( +
+ navigate(-1)} /> + +
+ {loadingOptions ? ( +

加载商品与推广码…

+ ) : ( + <> +
+ +
+
+ setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))} + /> +
+ +
+
+ +
+ +
+ setSmsCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + /> +
+
+ +
+ +
+ setReceiverName(e.target.value)} + /> +
+
+ +
+ + + {regionLabel &&

{regionLabel}

} +
+ +
+ +
+