Files
dukang/apps/admin-web/src/pages/promo/PromoCodeDetailPage.tsx
T
2026-07-23 00:43:49 +08:00

235 lines
8.1 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 { 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<PromoCodeDetailContext>();
const [editForm] = Form.useForm();
const [editOpen, setEditOpen] = useState(false);
const [saving, setSaving] = useState(false);
const stats = detail.stats;
async function handleEdit(values: Record<string, string>) {
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 (
<>
<Row gutter={[16, 16]}>
<Col xs={24} lg={8}>
<Card title="小程序码" size="small">
{detail.qrcodeUrl ? (
<div style={{ textAlign: 'center' }}>
<img
src={detail.qrcodeUrl}
alt="推广小程序码"
style={{ width: 200, height: 200, marginBottom: 12 }}
/>
<Typography.Paragraph type="secondary" style={{ marginBottom: 12, fontSize: 12 }}>
微信扫码进入小程序(scene=活动 ID {detail.id}
</Typography.Paragraph>
<Space direction="vertical" style={{ width: '100%' }}>
<Button
block
onClick={() => void downloadQrcode(detail.qrcodeUrl!, `${detail.code}-wxacode.png`)}
>
下载小程序码
</Button>
</Space>
</div>
) : (
<Typography.Text type="secondary">暂无小程序码</Typography.Text>
)}
</Card>
</Col>
<Col xs={24} lg={16}>
<Card
title="基础信息"
size="small"
extra={(
<Space>
<Button size="small" onClick={() => {
editForm.setFieldsValue({
name: detail.name,
scene: detail.scene,
remark: detail.remark,
ownerUserId: detail.ownerUser?.id,
});
setEditOpen(true);
}}
>
编辑
</Button>
<Popconfirm
title={detail.status === 'ACTIVE' ? '确认关闭此推广码?关闭后不可再归因' : '确认重新启用?'}
onConfirm={async () => {
await request(`/admin/promo-codes/${detail.id}/status`, {
method: 'PUT',
body: JSON.stringify({
status: detail.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE',
}),
});
message.success('状态已更新');
await reload();
}}
>
<Button size="small" danger={detail.status === 'ACTIVE'}>
{detail.status === 'ACTIVE' ? '关闭' : '启用'}
</Button>
</Popconfirm>
</Space>
)}
>
<Descriptions column={{ xs: 1, sm: 2 }} bordered size="small">
<Descriptions.Item label="名称">{detail.name}</Descriptions.Item>
<Descriptions.Item label="码值">{detail.code}</Descriptions.Item>
<Descriptions.Item label="场景">
{PROMO_CODE_SCENE_LABELS[detail.scene] || detail.scene}
</Descriptions.Item>
<Descriptions.Item label="状态">
{PROMO_CODE_STATUS_LABELS[detail.status] || detail.status}
</Descriptions.Item>
<Descriptions.Item label="活动 ID" span={2}>
<Typography.Text copyable={{ text: String(detail.id) }}>
{detail.id}(小程序码 scene
</Typography.Text>
</Descriptions.Item>
<Descriptions.Item label="二维码 ID" span={2}>
<Typography.Text copyable={{ text: detail.qrcodeId }}>{detail.qrcodeId}</Typography.Text>
</Descriptions.Item>
<Descriptions.Item label="小程序码 OSS" span={2}>
{detail.qrcodeUrl ? (
<Typography.Text copyable={{ text: detail.qrcodeUrl }} ellipsis>
{detail.qrcodeUrl}
</Typography.Text>
) : '—'}
</Descriptions.Item>
<Descriptions.Item label="渠道负责人">
{detail.ownerUser?.userNo || detail.ownerUser?.phone || '—'}
</Descriptions.Item>
<Descriptions.Item label="备注">{detail.remark || '—'}</Descriptions.Item>
<Descriptions.Item label="创建时间">{fmtTime(detail.createdAt)}</Descriptions.Item>
<Descriptions.Item label="更新时间">{fmtTime(detail.updatedAt)}</Descriptions.Item>
</Descriptions>
</Card>
</Col>
</Row>
<Row gutter={16} style={{ marginTop: 16 }}>
<Col xs={12} sm={6}>
<Card size="small">
<Statistic title="扫码进入次数" value={stats?.scanCount ?? detail.scanCount} />
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small">
<Statistic
title="扫码注册用户数"
value={stats?.registerCount ?? stats?.sourceMarkedCount ?? 0}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small">
<Statistic title="订单数" value={stats?.orderCount ?? detail.orderCount} />
</Card>
</Col>
<Col xs={12} sm={6}>
<Card size="small">
<Statistic
title="转化率"
value={promoConversion(stats?.scanCount ?? detail.scanCount, stats?.orderCount ?? detail.orderCount)}
/>
</Card>
</Col>
</Row>
<Modal
title="编辑推广码"
open={editOpen}
onCancel={() => setEditOpen(false)}
footer={null}
destroyOnClose
>
<Form form={editForm} layout="vertical" onFinish={handleEdit}>
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="scene" label="场景" rules={[{ required: true }]}>
<Select
options={Object.entries(PROMO_CODE_SCENE_LABELS).map(([value, label]) => ({ value, label }))}
/>
</Form.Item>
<Form.Item name="ownerUserId" label="关联用户 ID">
<Input placeholder="留空表示解除关联" />
</Form.Item>
<Form.Item name="remark" label="备注">
<Input.TextArea rows={2} />
</Form.Item>
<Button type="primary" htmlType="submit" loading={saving} block>
保存
</Button>
</Form>
</Modal>
</>
);
}