webadmin端

批量删除用户
模板上传图片数量限制改成30
小飞侠接口配置
This commit is contained in:
2026-07-06 18:04:02 +08:00
parent 4e18763968
commit f0b99ea9da
17 changed files with 1172 additions and 49 deletions
+208 -4
View File
@@ -1,10 +1,14 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
Alert,
Button,
Collapse,
Descriptions,
Drawer,
Form,
Input,
InputNumber,
Modal,
Select,
Space,
Table,
@@ -14,7 +18,20 @@ import {
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { request, type AdminOrderRow, type Paginated } from '../lib/api';
import { ORDER_STATUS_LABELS } from '../lib/constants';
import { DELIVERY_PROVIDER_LABELS, ORDER_STATUS_LABELS, fmtTime } from '../lib/constants';
type ShipDefaults = {
provider: string;
providerLabel: string;
fromName: string;
fromMobile: string;
fromAddress: string;
fromAddressDetail: string;
fromLng: number;
fromLat: number;
weight: number;
payMode: string;
};
type OrderDetail = AdminOrderRow & {
receiverAddress?: string;
@@ -45,12 +62,23 @@ type OrderDetail = AdminOrderRow & {
export default function OrdersPage() {
const [form] = Form.useForm();
const [shipForm] = Form.useForm();
const [data, setData] = useState<Paginated<AdminOrderRow> | null>(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [detail, setDetail] = useState<OrderDetail | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
const [batchDeleteOpen, setBatchDeleteOpen] = useState(false);
const [batchDeleting, setBatchDeleting] = useState(false);
const [shipDefaults, setShipDefaults] = useState<ShipDefaults | null>(null);
const [shipping, setShipping] = useState(false);
const selectedOrders = useMemo(
() => (data?.items ?? []).filter((row) => selectedRowKeys.includes(row.id)),
[data?.items, selectedRowKeys],
);
const load = useCallback(async () => {
setLoading(true);
@@ -71,10 +99,71 @@ export default function OrdersPage() {
void load();
}, [load]);
useEffect(() => {
void request<ShipDefaults>('/admin/orders/ship-defaults').then(setShipDefaults).catch(() => {});
}, []);
async function openDetail(id: string) {
const res = await request<OrderDetail>(`/admin/orders/${id}`);
setDetail(res);
setDrawerOpen(true);
const defaults = shipDefaults ?? await request<ShipDefaults>('/admin/orders/ship-defaults').catch(() => null);
if (defaults) {
setShipDefaults(defaults);
shipForm.setFieldsValue({
provider: defaults.provider,
weight: defaults.weight,
payMode: defaults.payMode,
fromName: defaults.fromName,
fromMobile: defaults.fromMobile,
fromAddress: defaults.fromAddress,
fromAddressDetail: defaults.fromAddressDetail,
fromLng: defaults.fromLng,
fromLat: defaults.fromLat,
});
}
}
async function submitShip() {
if (!detail) return;
const values = await shipForm.validateFields();
setShipping(true);
try {
const res = await request<OrderDetail>(`/admin/orders/${detail.id}/ship`, {
method: 'POST',
body: JSON.stringify(values),
});
message.success('发货成功');
setDetail(res);
void load();
} catch (e) {
message.error(e instanceof Error ? e.message : '发货失败');
} finally {
setShipping(false);
}
}
async function confirmBatchDelete() {
if (!selectedRowKeys.length) return;
setBatchDeleting(true);
try {
const res = await request<{ deleted: number; message: string }>('/admin/orders/batch-delete', {
method: 'POST',
body: JSON.stringify({ ids: selectedRowKeys }),
});
message.success(res.message || `已删除 ${res.deleted} 笔订单`);
setBatchDeleteOpen(false);
setSelectedRowKeys([]);
if (detail && selectedRowKeys.includes(detail.id)) {
setDrawerOpen(false);
setDetail(null);
}
void load();
} catch (e) {
message.error(e instanceof Error ? e.message : '批量删除失败');
} finally {
setBatchDeleting(false);
}
}
const columns: ColumnsType<AdminOrderRow> = [
@@ -124,7 +213,16 @@ export default function OrdersPage() {
return (
<div>
<Typography.Title level={4}></Typography.Title>
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title>
<Button
danger
disabled={!selectedRowKeys.length}
onClick={() => setBatchDeleteOpen(true)}
>
{selectedRowKeys.length ? ` (${selectedRowKeys.length})` : ''}
</Button>
</Space>
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={() => { setPage(1); void load(); }}>
<Form.Item name="orderNo" label="订单号">
<Input placeholder="DK..." allowClear />
@@ -149,6 +247,10 @@ export default function OrdersPage() {
columns={columns}
dataSource={data?.items ?? []}
scroll={{ x: 1200 }}
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys as string[]),
}}
pagination={{
current: page,
pageSize,
@@ -215,11 +317,75 @@ export default function OrdersPage() {
{detail.delivery && (
<Descriptions column={1} bordered size="small" title="配送" style={{ marginTop: 16 }}>
<Descriptions.Item label="provider">{detail.delivery.provider}</Descriptions.Item>
<Descriptions.Item label="快递公司">
{DELIVERY_PROVIDER_LABELS[detail.delivery.provider] || detail.delivery.provider}
</Descriptions.Item>
<Descriptions.Item label="运单号">{detail.delivery.trackingNo || '—'}</Descriptions.Item>
<Descriptions.Item label="三方单号">{detail.delivery.providerOrderNo || '—'}</Descriptions.Item>
</Descriptions>
)}
{['PENDING_SHIP', 'OUT_WAREHOUSE'].includes(detail.status) && !detail.delivery?.trackingNo && (
<div style={{ marginTop: 16 }}>
<Typography.Title level={5}></Typography.Title>
<Form form={shipForm} layout="vertical" size="small">
<Form.Item name="provider" label="快递公司" rules={[{ required: true }]}>
<Select
options={[{ value: 'XFX', label: '小飞侠' }]}
/>
</Form.Item>
<Form.Item name="weight" label="重量(kg)" rules={[{ required: true }]}>
<InputNumber min={0.01} step={0.5} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="payMode" label="付费方式" rules={[{ required: true }]}>
<Select
options={[
{ value: '1', label: '寄付' },
{ value: '2', label: '到付' },
]}
/>
</Form.Item>
<Form.Item name="remark" label="备注">
<Input.TextArea rows={2} placeholder="可选" />
</Form.Item>
<Collapse
ghost
items={[{
key: 'from',
label: '寄件信息(默认仓库,可修改)',
children: (
<>
<Form.Item name="fromName" label="寄件人" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="fromMobile" label="寄件手机" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="fromAddress" label="寄件区域" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="fromAddressDetail" label="寄件详细地址" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Space>
<Form.Item name="fromLng" label="经度">
<InputNumber step={0.001} />
</Form.Item>
<Form.Item name="fromLat" label="纬度">
<InputNumber step={0.001} />
</Form.Item>
</Space>
</>
),
}]}
/>
<Button type="primary" loading={shipping} onClick={() => void submitShip()}>
</Button>
</Form>
</div>
)}
{detail.statusLogs && detail.statusLogs.length > 0 && (
<>
<Typography.Title level={5} style={{ marginTop: 16 }}></Typography.Title>
@@ -239,6 +405,44 @@ export default function OrdersPage() {
</>
)}
</Drawer>
<Modal
title={`确认批量删除(${selectedOrders.length} 笔)`}
open={batchDeleteOpen}
okText="确认删除"
okButtonProps={{ danger: true, loading: batchDeleting }}
onOk={() => void confirmBatchDelete()}
onCancel={() => setBatchDeleteOpen(false)}
width={720}
destroyOnClose
>
<Alert
type="error"
showIcon
style={{ marginBottom: 16 }}
message="此操作不可恢复"
description="将删除所选订单及其配送单、权益券、核销记录等关联业务数据。订单状态流转等业务日志将保留。"
/>
<Table
size="small"
rowKey="id"
pagination={false}
scroll={{ x: 600, y: 280 }}
dataSource={selectedOrders}
columns={[
{ title: '订单号', dataIndex: 'orderNo', width: 170 },
{
title: '状态',
dataIndex: 'status',
width: 100,
render: (s) => <Tag>{ORDER_STATUS_LABELS[s] || s}</Tag>,
},
{ title: '实付', dataIndex: 'payAmount', width: 90, render: (v) => `¥${v}` },
{ title: '收货人', dataIndex: 'receiverName', width: 90 },
{ title: '下单时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
]}
/>
</Modal>
</div>
);
}