Files
order_site/apps/frontend/src/pages/admin/AdminDevMockPage.tsx
T
yml2213 827baf4ab2 优化领取页第一步文案示意与 Mock 表单
- 游戏编号文案与红色提示,底部增加编号位置示意图
- 压缩商品信息卡片,示意图移至最下方
- 开发 Mock 在待填 UID 步骤下 UID 改为可选
2026-07-10 18:14:10 +08:00

431 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { BugOutlined, CopyOutlined, LinkOutlined } from '@ant-design/icons'
import {
Alert,
App,
Button,
Card,
Col,
Divider,
Form,
Input,
Result,
Row,
Select,
Space,
Typography,
} from 'antd'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { Link } from 'react-router'
import PageHeader from '@/components/admin/PageHeader'
import {
buildOpen91QueryDevMock,
createFeifeiDevMock,
createLewanDevMock,
fetchDevMockStatus,
type DevMockCreateResult,
type DevMockOpen91QueryResult,
} from '@/services/admin'
export default function AdminDevMockPage() {
const { message } = App.useApp()
const [lastCreate, setLastCreate] = useState<DevMockCreateResult | null>(null)
const [lastQuery, setLastQuery] = useState<DevMockOpen91QueryResult | null>(null)
const statusQuery = useQuery({
queryKey: ['admin-dev-mock-status'],
queryFn: () => fetchDevMockStatus(),
retry: false,
})
const status = statusQuery.data?.data
const enabled = Boolean(status?.enabled)
const lewanMutation = useMutation({
mutationFn: createLewanDevMock,
onSuccess: (response) => {
setLastCreate(response.data)
message.success('lewan mock 已生成')
},
onError: (error) => {
message.error(error instanceof Error ? error.message : '生成失败')
},
})
const feifeiMutation = useMutation({
mutationFn: createFeifeiDevMock,
onSuccess: (response) => {
setLastCreate(response.data)
message.success('feifei mock 已生成')
},
onError: (error) => {
message.error(error instanceof Error ? error.message : '生成失败')
},
})
const open91Mutation = useMutation({
mutationFn: buildOpen91QueryDevMock,
onSuccess: (response) => {
setLastQuery(response.data)
message.success('91 查单请求已生成')
},
onError: (error) => {
message.error(error instanceof Error ? error.message : '生成失败')
},
})
const defaultOrderNo = useMemo(
() => lastCreate?.orderNo || '',
[lastCreate?.orderNo],
)
if (statusQuery.isLoading) {
return (
<section className="page-stack">
<PageHeader title="开发 Mock" description="加载中…" />
</section>
)
}
if (statusQuery.error || !enabled) {
return (
<section className="page-stack">
<PageHeader title="开发 Mock" description="仅开发环境可用。" />
<Result
status="403"
title="当前环境未开启开发 Mock"
subTitle={
statusQuery.error instanceof Error
? statusQuery.error.message
: '生产环境默认关闭。本地开发请使用 docker-compose.dev 或设置 ENABLE_DEV_MOCK=1。'
}
/>
</section>
)
}
return (
<section className="page-stack">
<PageHeader
title="开发 Mock"
description="一键造单、测领取页与 91 查单,无需真实 91 / 快手 / feifei 环境。"
extra={
<Typography.Text type="secondary">
NODE_ENV={status?.nodeEnv || '-'}
</Typography.Text>
}
/>
<Alert
type="info"
showIcon
icon={<BugOutlined />}
message="Mock 会写入开发库真实订单/任务,但 lewan 兑换走 mock 成功路径,feifei 不调用真实平台下单。"
description="建议 UID 与默认角色一致:lewan 默认 10001feifei 默认 166909256。"
/>
<Row gutter={[16, 16]}>
<Col xs={24} xl={12}>
<Card title="1. lewan 领取(强制 UID" bordered={false}>
<LewanMockForm
defaultUid={status?.defaultUid || '10001'}
loading={lewanMutation.isPending}
onSubmit={(values) => lewanMutation.mutate(values)}
/>
</Card>
</Col>
<Col xs={24} xl={12}>
<Card title="2. feifei 领取(拼 UID 到 H5" bordered={false}>
<FeifeiMockForm
loading={feifeiMutation.isPending}
onSubmit={(values) => feifeiMutation.mutate(values)}
/>
</Card>
</Col>
<Col xs={24}>
<Card title="3. 模拟 91 查单(open-91/query" bordered={false}>
<Form
layout="inline"
initialValues={{ orderNo: defaultOrderNo, execute: '1' }}
key={defaultOrderNo}
onFinish={(values) =>
open91Mutation.mutate({
orderNo: values.orderNo,
execute: values.execute === '1',
})
}
>
<Form.Item
label="orderNo"
name="orderNo"
rules={[{ required: true, message: '填写上方生成的订单号' }]}
>
<Input style={{ width: 280 }} placeholder="MOCK91…" />
</Form.Item>
<Form.Item label="执行" name="execute">
<Select
style={{ width: 180 }}
options={[
{ value: '1', label: '生成并请求本机' },
{ value: '0', label: '仅生成请求体' },
]}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={open91Mutation.isPending}>
模拟 91 查单
</Button>
</Form.Item>
</Form>
<Typography.Paragraph type="secondary" style={{ marginTop: 12, marginBottom: 0 }}>
对应日志:POST /api/v1/open/91/orders/query。需配置 KAQUAN91_SECRET32 位)。
</Typography.Paragraph>
</Card>
</Col>
</Row>
{lastCreate ? (
<Card
title="最近生成结果"
extra={
<Space>
<Link to={`/admin/orders/${lastCreate.orderId}`}>订单 #{lastCreate.orderId}</Link>
<Link to={`/admin/tasks/${lastCreate.taskId}`}>任务 #{lastCreate.taskId}</Link>
</Space>
}
>
<Row gutter={[16, 12]}>
<Col xs={24} md={12}>
<Typography.Text type="secondary">平台</Typography.Text>
<div>
<Typography.Text strong>{lastCreate.platform}</Typography.Text>
</div>
</Col>
<Col xs={24} md={12}>
<Typography.Text type="secondary">步骤 / UID</Typography.Text>
<div>
<Typography.Text strong>
{lastCreate.step} / {lastCreate.expectedUid}
</Typography.Text>
</div>
</Col>
<Col xs={24} md={12}>
<Typography.Text type="secondary">91 订单号</Typography.Text>
<div>
<Typography.Text copyable>{lastCreate.orderNo}</Typography.Text>
</div>
</Col>
<Col xs={24} md={12}>
<Typography.Text type="secondary">商品</Typography.Text>
<div>{lastCreate.productName}</div>
</Col>
<Col span={24}>
<Typography.Text type="secondary">领取链接</Typography.Text>
<div>
<Typography.Link href={lastCreate.claimUrl} target="_blank" rel="noreferrer">
<LinkOutlined /> {lastCreate.claimUrl}
</Typography.Link>
</div>
<Space style={{ marginTop: 8 }} wrap>
<Button
icon={<CopyOutlined />}
onClick={async () => {
await navigator.clipboard.writeText(lastCreate.claimUrl)
message.success('已复制领取链接')
}}
>
复制链接
</Button>
<Button type="primary" href={lastCreate.claimUrl} target="_blank">
打开领取页
</Button>
</Space>
</Col>
<Col span={24}>
<Alert
type="success"
showIcon
message="操作提示"
description={
<ul style={{ margin: 0, paddingLeft: 18 }}>
{lastCreate.tips.map((tip) => (
<li key={tip}>{tip}</li>
))}
<li>{lastCreate.open91QueryHint}</li>
</ul>
}
/>
</Col>
</Row>
</Card>
) : null}
{lastQuery ? (
<Card title="91 查单结果">
<Typography.Paragraph>
<Typography.Text type="secondary">Endpoint</Typography.Text>
<Typography.Text code copyable>
{lastQuery.endpoint}
</Typography.Text>
</Typography.Paragraph>
<Typography.Paragraph>
<Typography.Text type="secondary">Request</Typography.Text>
</Typography.Paragraph>
<pre className="dev-mock-pre">{JSON.stringify(lastQuery.requestBody, null, 2)}</pre>
<Typography.Paragraph>
<Typography.Text type="secondary">curl</Typography.Text>
<Typography.Text code copyable>
{lastQuery.curl}
</Typography.Text>
</Typography.Paragraph>
{lastQuery.response ? (
<>
<Divider />
<Typography.Paragraph>
<Typography.Text type="secondary">Response</Typography.Text>
</Typography.Paragraph>
<pre className="dev-mock-pre">{JSON.stringify(lastQuery.response, null, 2)}</pre>
</>
) : null}
</Card>
) : null}
</section>
)
}
function LewanMockForm({
defaultUid,
loading,
onSubmit,
}: {
defaultUid: string
loading: boolean
onSubmit: (values: Record<string, unknown>) => void
}) {
const [form] = Form.useForm()
const step = Form.useWatch('step', form)
const uidRequired = step !== 'uid'
return (
<Form
form={form}
layout="vertical"
initialValues={{
step: 'uid',
uid: defaultUid || '10001',
productNo: '套餐_1----3676797936',
}}
onFinish={onSubmit}
>
<Form.Item
label="页面步骤"
name="step"
tooltip="uid=只到填编号第一步;binding=已预填编号且角色匹配;confirm/result=后续步骤"
>
<Select
options={[
{ value: 'uid', label: 'uid · 待填 UID(第一步)' },
{ value: 'binding', label: 'binding · 绑定且 UID 已匹配(推荐)' },
{ value: 'confirm', label: 'confirm · 待确认兑换' },
{ value: 'result', label: 'result · 已完成' },
]}
/>
</Form.Item>
<Form.Item
label="游戏 UID"
name="uid"
required={uidRequired}
rules={
uidRequired
? [{ required: true, whitespace: true, message: '后续步骤请填写游戏 UID' }]
: []
}
extra={
step === 'uid'
? '「待填 UID」步骤可不填,打开领取页会停留在第一步让用户填写'
: '后续步骤用于模拟已提交的编号,建议与默认角色一致(如 10001)'
}
>
<Input allowClear placeholder={defaultUid || '10001'} />
</Form.Item>
<Form.Item label="91 商品 productNo" name="productNo">
<Input placeholder="套餐_1----3676797936" />
</Form.Item>
<Form.Item label="订单号 orderNo(可空自动生成)" name="orderNo">
<Input placeholder="留空自动 MOCK91…" />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading} block>
生成 lewan Mock
</Button>
</Form>
)
}
function FeifeiMockForm({
loading,
onSubmit,
}: {
loading: boolean
onSubmit: (values: Record<string, unknown>) => void
}) {
const [form] = Form.useForm()
const step = Form.useWatch('step', form)
const uidRequired = step !== 'uid'
return (
<Form
form={form}
layout="vertical"
initialValues={{
step: 'uid',
uid: '166909256',
productName: '套装-浪漫天命',
}}
onFinish={onSubmit}
>
<Form.Item label="页面步骤" name="step">
<Select
options={[
{ value: 'uid', label: 'uid · 待填 UID(第一步)' },
{ value: 'ready', label: 'ready · 已填 UID,可打开 H5(推荐)' },
{ value: 'completed', label: 'completed · 模拟成功' },
{ value: 'failed', label: 'failed · 模拟失败' },
]}
/>
</Form.Item>
<Form.Item
label="游戏 UID"
name="uid"
required={uidRequired}
rules={
uidRequired
? [{ required: true, whitespace: true, message: '后续步骤请填写游戏 UID' }]
: []
}
extra={
step === 'uid'
? '「待填 UID」步骤可不填,打开领取页会停留在第一步让用户填写'
: '后续步骤用于模拟已提交的编号'
}
>
<Input allowClear placeholder="166909256" />
</Form.Item>
<Form.Item label="商品名" name="productName">
<Input />
</Form.Item>
<Form.Item label="H5 链接(可空用 mock 地址)" name="h5Url">
<Input placeholder="http://skin-exchange.../h5/bind?code=..." />
</Form.Item>
<Form.Item label="订单号 orderNo(可空)" name="orderNo">
<Input placeholder="留空自动 MOCKFF…" />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading} block>
生成 feifei Mock
</Button>
</Form>
)
}