feat: audit diff, mock SMS 999888, env photos, proxy pay lock, mini-user UX
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Card, Form, Input, Tabs, message, Typography } from 'antd';
|
||||
import { MOCK_SMS_FIXED_CODE } from '@dukang/shared-types';
|
||||
import { saveAuth, request } from '../lib/api';
|
||||
|
||||
type LoginResult = { accessToken: string; refreshToken: string };
|
||||
@@ -117,14 +118,14 @@ export default function LoginPage() {
|
||||
form={smsForm}
|
||||
layout="vertical"
|
||||
onFinish={onSmsFinish}
|
||||
initialValues={{ phone: '13600000001', code: '123456' }}
|
||||
initialValues={{ phone: '13600000001', code: MOCK_SMS_FIXED_CODE }}
|
||||
>
|
||||
<Form.Item name="phone" label="手机号" rules={[{ required: true, message: '请输入手机号' }]}>
|
||||
<Input placeholder="13600000001" maxLength={11} />
|
||||
</Form.Item>
|
||||
<Form.Item name="code" label="验证码" rules={[{ required: true, message: '请输入验证码' }]}>
|
||||
<Input
|
||||
placeholder="123456"
|
||||
placeholder={MOCK_SMS_FIXED_CODE}
|
||||
addonAfter={
|
||||
<Button type="link" size="small" disabled={codeCooldown > 0} onClick={() => void sendCode()}>
|
||||
{codeCooldown > 0 ? `${codeCooldown}s` : '获取验证码'}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import {
|
||||
Button, Card, Col, Form, Input, InputNumber, Row, Space, Tabs, Typography, message,
|
||||
} from 'antd';
|
||||
import { MOCK_SMS_FIXED_CODE } from '@dukang/shared-types';
|
||||
import { request } from '../lib/api';
|
||||
|
||||
type ApiResult = Record<string, unknown>;
|
||||
@@ -255,7 +256,7 @@ export default function RedeemDebugPage() {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="code" label="验证码" rules={[{ required: true }]}>
|
||||
<Input placeholder="Mock 默认 123456" />
|
||||
<Input placeholder={`Mock 默认 ${MOCK_SMS_FIXED_CODE}`} />
|
||||
</Form.Item>
|
||||
<Button
|
||||
loading={loading}
|
||||
@@ -335,7 +336,7 @@ export default function RedeemDebugPage() {
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="code" label="核销确认验证码" rules={[{ required: true }]}>
|
||||
<Input placeholder="Mock 默认 123456" />
|
||||
<Input placeholder={`Mock 默认 ${MOCK_SMS_FIXED_CODE}`} />
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
|
||||
@@ -1,11 +1,68 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, Input, Modal, Space, Table, Tag, Typography, message } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
Input,
|
||||
Modal,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { StorePackageChangeRequestDto } from '@dukang/shared-types';
|
||||
import type {
|
||||
StorePackageAuditDetailDto,
|
||||
StorePackageChangeRequestDto,
|
||||
StorePackageItemDto,
|
||||
StorePackageViewDto,
|
||||
} from '@dukang/shared-types';
|
||||
import { STORE_PACKAGE_CHANGE_STATUS_LABELS } from '@dukang/shared-types';
|
||||
import { request, type Paginated } from '../lib/api';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
|
||||
function packageKey(pkg: StorePackageItemDto | StorePackageViewDto, index: number) {
|
||||
const name = String(pkg.name ?? '').trim();
|
||||
return name ? `name:${name}` : `idx:${index}`;
|
||||
}
|
||||
|
||||
function diffPackages(live: StorePackageViewDto[], proposed: StorePackageItemDto[]) {
|
||||
const liveMap = new Map(live.map((p, i) => [packageKey(p, i), p]));
|
||||
const proposedMap = new Map(proposed.map((p, i) => [packageKey(p, i), p]));
|
||||
const keys = new Set([...liveMap.keys(), ...proposedMap.keys()]);
|
||||
const rows: Array<{
|
||||
key: string;
|
||||
change: 'added' | 'removed' | 'changed' | 'unchanged';
|
||||
live?: StorePackageViewDto;
|
||||
proposed?: StorePackageItemDto;
|
||||
}> = [];
|
||||
|
||||
for (const key of keys) {
|
||||
const l = liveMap.get(key);
|
||||
const p = proposedMap.get(key);
|
||||
if (l && !p) {
|
||||
rows.push({ key, change: 'removed', live: l });
|
||||
} else if (!l && p) {
|
||||
rows.push({ key, change: 'added', proposed: p });
|
||||
} else if (l && p) {
|
||||
const changed =
|
||||
l.price !== p.price ||
|
||||
l.dishes !== p.dishes ||
|
||||
(l.usableTime ?? '') !== (p.usableTime ?? '') ||
|
||||
(l.otherNotes ?? '') !== (p.otherNotes ?? '');
|
||||
rows.push({ key, change: changed ? 'changed' : 'unchanged', live: l, proposed: p });
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
const CHANGE_LABELS = {
|
||||
added: { text: '新增', color: 'green' },
|
||||
removed: { text: '删除', color: 'red' },
|
||||
changed: { text: '变更', color: 'orange' },
|
||||
unchanged: { text: '未变', color: 'default' },
|
||||
} as const;
|
||||
|
||||
export default function StorePackageAuditsPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [items, setItems] = useState<StorePackageChangeRequestDto[]>([]);
|
||||
@@ -15,6 +72,9 @@ export default function StorePackageAuditsPage() {
|
||||
const [rejectOpen, setRejectOpen] = useState(false);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [detail, setDetail] = useState<StorePackageAuditDetailDto | null>(null);
|
||||
|
||||
async function reload(nextPage = page, nextStatus = status) {
|
||||
setLoading(true);
|
||||
@@ -41,6 +101,21 @@ export default function StorePackageAuditsPage() {
|
||||
void reload(1, status);
|
||||
}, [status]);
|
||||
|
||||
async function openDetail(id: string) {
|
||||
setDetailOpen(true);
|
||||
setDetailLoading(true);
|
||||
setDetail(null);
|
||||
try {
|
||||
const data = await request<StorePackageAuditDetailDto>(`/admin/store-package-audits/${id}`);
|
||||
setDetail(data);
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载详情失败');
|
||||
setDetailOpen(false);
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function audit(id: string, action: 'APPROVE' | 'REJECT', reason?: string) {
|
||||
try {
|
||||
await request(`/admin/store-package-audits/${id}/audit`, {
|
||||
@@ -50,12 +125,63 @@ export default function StorePackageAuditsPage() {
|
||||
),
|
||||
});
|
||||
message.success(action === 'APPROVE' ? '已通过' : '已驳回');
|
||||
setDetailOpen(false);
|
||||
void reload(page, status);
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '操作失败');
|
||||
}
|
||||
}
|
||||
|
||||
const diffRows = detail ? diffPackages(detail.livePackages ?? [], detail.packages ?? []) : [];
|
||||
|
||||
const diffColumns: ColumnsType<(typeof diffRows)[number]> = [
|
||||
{
|
||||
title: '变更',
|
||||
dataIndex: 'change',
|
||||
width: 72,
|
||||
render: (v: keyof typeof CHANGE_LABELS) => {
|
||||
const meta = CHANGE_LABELS[v];
|
||||
return <Tag color={meta.color}>{meta.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '当前线上',
|
||||
render: (_, row) =>
|
||||
row.live ? (
|
||||
<div>
|
||||
<div><strong>{row.live.name}</strong> · ¥{row.live.price}</div>
|
||||
<Typography.Text type="secondary">{row.live.dishes}</Typography.Text>
|
||||
{row.live.usableTime ? (
|
||||
<div><Typography.Text type="secondary">可用:{row.live.usableTime}</Typography.Text></div>
|
||||
) : null}
|
||||
{row.live.otherNotes ? (
|
||||
<div><Typography.Text type="secondary">备注:{row.live.otherNotes}</Typography.Text></div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
'—'
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '申请变更',
|
||||
render: (_, row) =>
|
||||
row.proposed ? (
|
||||
<div>
|
||||
<div><strong>{row.proposed.name}</strong> · ¥{row.proposed.price}</div>
|
||||
<Typography.Text type="secondary">{row.proposed.dishes}</Typography.Text>
|
||||
{row.proposed.usableTime ? (
|
||||
<div><Typography.Text type="secondary">可用:{row.proposed.usableTime}</Typography.Text></div>
|
||||
) : null}
|
||||
{row.proposed.otherNotes ? (
|
||||
<div><Typography.Text type="secondary">备注:{row.proposed.otherNotes}</Typography.Text></div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
'—'
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const columns: ColumnsType<StorePackageChangeRequestDto> = [
|
||||
{ title: '门店', dataIndex: 'storeName', render: (_, row) => row.storeName || row.storeId },
|
||||
{
|
||||
@@ -76,27 +202,33 @@ export default function StorePackageAuditsPage() {
|
||||
{ title: '提交时间', dataIndex: 'createdAt', render: (v) => fmtTime(String(v)) },
|
||||
{
|
||||
title: '操作',
|
||||
render: (_, row) =>
|
||||
row.status === 'PENDING' ? (
|
||||
<Space>
|
||||
<Button type="link" onClick={() => void audit(row.id, 'APPROVE')}>
|
||||
通过
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
onClick={() => {
|
||||
setActiveId(row.id);
|
||||
setRejectReason('');
|
||||
setRejectOpen(true);
|
||||
}}
|
||||
>
|
||||
驳回
|
||||
</Button>
|
||||
</Space>
|
||||
) : (
|
||||
row.rejectReason || '—'
|
||||
),
|
||||
render: (_, row) => (
|
||||
<Space>
|
||||
<Button type="link" onClick={() => void openDetail(row.id)}>
|
||||
查看变更
|
||||
</Button>
|
||||
{row.status === 'PENDING' ? (
|
||||
<>
|
||||
<Button type="link" onClick={() => void audit(row.id, 'APPROVE')}>
|
||||
通过
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
onClick={() => {
|
||||
setActiveId(row.id);
|
||||
setRejectReason('');
|
||||
setRejectOpen(true);
|
||||
}}
|
||||
>
|
||||
驳回
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
row.rejectReason || null
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -122,6 +254,57 @@ export default function StorePackageAuditsPage() {
|
||||
onChange: (p) => void reload(p, status),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Drawer
|
||||
title={detail ? `${detail.storeName || detail.storeId} · 套餐变更` : '套餐变更详情'}
|
||||
width={720}
|
||||
open={detailOpen}
|
||||
onClose={() => setDetailOpen(false)}
|
||||
extra={
|
||||
detail?.status === 'PENDING' ? (
|
||||
<Space>
|
||||
<Button onClick={() => void audit(detail.id, 'APPROVE')}>通过</Button>
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
setActiveId(detail.id);
|
||||
setRejectReason('');
|
||||
setRejectOpen(true);
|
||||
}}
|
||||
>
|
||||
驳回
|
||||
</Button>
|
||||
</Space>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
{detailLoading ? (
|
||||
<Typography.Text type="secondary">加载中…</Typography.Text>
|
||||
) : detail ? (
|
||||
<>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Tag>{STORE_PACKAGE_CHANGE_STATUS_LABELS[detail.status]}</Tag>
|
||||
<Typography.Text type="secondary">
|
||||
提交方:{detail.submitterType === 'PARTNER' ? '合伙人' : '门店'} · {fmtTime(detail.createdAt)}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
{detail.rejectReason ? (
|
||||
<Typography.Paragraph type="danger">驳回原因:{detail.rejectReason}</Typography.Paragraph>
|
||||
) : null}
|
||||
<Typography.Paragraph type="secondary">
|
||||
线上 {detail.livePackages?.length ?? 0} 条 → 申请 {detail.packages?.length ?? 0} 条
|
||||
</Typography.Paragraph>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey="key"
|
||||
columns={diffColumns}
|
||||
dataSource={diffRows}
|
||||
pagination={false}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
title="驳回套餐变更"
|
||||
open={rejectOpen}
|
||||
|
||||
@@ -1404,14 +1404,20 @@ export default function StoresPage() {
|
||||
<OssUpload bizType="STORE_TITLE" mediaType="IMAGE" />
|
||||
</Form.Item>
|
||||
<Typography.Text strong>环境照片</Typography.Text>
|
||||
<Typography.Paragraph type="secondary" style={{ marginTop: 4 }}>
|
||||
至少 3 张,可继续添加
|
||||
</Typography.Paragraph>
|
||||
<Form.List name="envPhotoUrls">
|
||||
{(fields) => (
|
||||
{(fields, { add }) => (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key} name={field.name} label={`环境图 ${index + 1}`}>
|
||||
<OssUpload bizType="STORE_ENV" mediaType="IMAGE" />
|
||||
</Form.Item>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add('')} block>
|
||||
添加环境照片
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
Reference in New Issue
Block a user