fix(admin): 列设置靠右并支持订单状态多选

HQ 列表主操作居右、列设置贴最右侧;订单筛选状态可多选,导出同步过滤。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-25 14:19:49 +08:00
parent f4da71e952
commit fed8ff3d3a
52 changed files with 566 additions and 437 deletions
+44
View File
@@ -74,6 +74,50 @@ body.admin-col-resizing * {
user-select: none !important; user-select: none !important;
} }
/* 列表页顶栏:标题左、主操作右、列设置最右 */
.admin-list-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.admin-list-header-left {
min-width: 0;
}
.admin-list-header-desc {
margin-top: 4px;
color: rgba(0, 0, 0, 0.45);
line-height: 1.5;
}
.admin-list-header-right {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-shrink: 0;
margin-left: auto;
}
.admin-list-settings-slot {
display: inline-flex;
margin-left: auto;
order: 99;
flex-shrink: 0;
}
.admin-list-settings-btn.ant-btn {
color: rgba(0, 0, 0, 0.55);
padding-inline: 8px;
}
.admin-list-settings-btn.ant-btn:hover {
color: rgba(0, 0, 0, 0.88);
}
/* 操作列保持可见 */ /* 操作列保持可见 */
.admin-layout .ant-table-cell:has(.ant-btn), .admin-layout .ant-table-cell:has(.ant-btn),
.ant-drawer .ant-table-cell:has(.ant-btn), .ant-drawer .ant-table-cell:has(.ant-btn),
@@ -0,0 +1,36 @@
import type { ReactNode } from 'react';
import { Space, Typography } from 'antd';
/** 列表页顶栏:标题在左,主操作在右,列设置永远最右 */
export function AdminListHeader({
title,
description,
actions,
settings,
}: {
title?: ReactNode;
description?: ReactNode;
actions?: ReactNode;
settings?: ReactNode;
}) {
return (
<div className="admin-list-header">
<div className="admin-list-header-left">
{title == null || title === '' ? null : typeof title === 'string' || typeof title === 'number' ? (
<Typography.Title level={4} style={{ margin: 0 }}>
{title}
</Typography.Title>
) : (
title
)}
{description ? (
<div className="admin-list-header-desc">{description}</div>
) : null}
</div>
<div className="admin-list-header-right">
{actions ? <Space wrap>{actions}</Space> : null}
{settings}
</div>
</div>
);
}
@@ -153,9 +153,11 @@ export function useAdminListColumns<T>(
}, [configured, serialCol, pref?.widths, localWidths, persistWidth]); }, [configured, serialCol, pref?.widths, localWidths, persistWidth]);
const settingsButton = ( const settingsButton = (
<Button icon={<SettingOutlined />} onClick={() => setOpen(true)}> <span className="admin-list-settings-slot">
<Button type="text" icon={<SettingOutlined />} className="admin-list-settings-btn" onClick={() => setOpen(true)}>
</Button>
</Button>
</span>
); );
const settingsModal = ( const settingsModal = (
@@ -23,6 +23,7 @@ import { COUPON_STATUS_LABELS, DELIVERY_TYPE_LABELS, fmtTime } from '../lib/cons
import RedeemRecordDetailDescriptions from '../components/RedeemRecordDetailDescriptions'; import RedeemRecordDetailDescriptions from '../components/RedeemRecordDetailDescriptions';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
type CouponOrder = { type CouponOrder = {
@@ -248,17 +249,15 @@ export default function BenefitCouponsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}> title="好客权益券"
settings={settingsButton}
</Typography.Title> actions={
<Space>
{settingsButton}
<Button type="primary" onClick={() => setGrantOpen(true)}> <Button type="primary" onClick={() => setGrantOpen(true)}>
</Button> </Button>
</Space> }
</div> />
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}> <Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
<Form.Item name="couponNo" label="券号"> <Form.Item name="couponNo" label="券号">
@@ -4,6 +4,7 @@ import type { ColumnsType } from 'antd/es/table';
import { LEDGER_TYPE_LABELS, fmtTime } from '../lib/constants'; import { LEDGER_TYPE_LABELS, fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
type Row = { type Row = {
@@ -41,8 +42,7 @@ export default function BenefitLedgersPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4}></Typography.Title> <AdminListHeader title="权益流水" settings={settingsButton} />
{settingsButton}
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}> <Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
<Form.Item name="userId" label="用户ID"><Input allowClear /></Form.Item> <Form.Item name="userId" label="用户ID"><Input allowClear /></Form.Item>
<Form.Item name="couponId" label="券ID"><Input allowClear /></Form.Item> <Form.Item name="couponId" label="券ID"><Input allowClear /></Form.Item>
+6 -5
View File
@@ -25,6 +25,7 @@ import { useAdminList } from '../lib/useAdminList';
import CityPartnersPanel from '../components/CityPartnersPanel'; import CityPartnersPanel from '../components/CityPartnersPanel';
import ChinaProvinceCityCascader from '../components/ChinaProvinceCityCascader'; import ChinaProvinceCityCascader from '../components/ChinaProvinceCityCascader';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -326,11 +327,11 @@ export default function CitiesPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> title="城市"
{settingsButton} settings={settingsButton}
<Button type="primary" onClick={openCreateModal}></Button> actions={<Button type="primary" onClick={openCreateModal}></Button>}
</Space> />
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}> <Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
<Form.Item name="name" label="城市"><Input allowClear /></Form.Item> <Form.Item name="name" label="城市"><Input allowClear /></Form.Item>
<Form.Item name="code" label="编码"><Input allowClear /></Form.Item> <Form.Item name="code" label="编码"><Input allowClear /></Form.Item>
+24 -23
View File
@@ -37,6 +37,7 @@ import { useAdminList } from '../lib/useAdminList';
import CityDistrictMultiSelect from '../components/CityDistrictMultiSelect'; import CityDistrictMultiSelect from '../components/CityDistrictMultiSelect';
import PartnerSubAccountList, { type PartnerSubAccountRow } from '../components/PartnerSubAccountList'; import PartnerSubAccountList, { type PartnerSubAccountRow } from '../components/PartnerSubAccountList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -350,29 +351,29 @@ export default function CityPartnersPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}> title="城市合伙人"
settings={settingsButton}
</Typography.Title> actions={
{settingsButton} <Button
<Button type="primary"
type="primary" onClick={() => {
onClick={() => { createForm.resetFields();
createForm.resetFields(); createForm.setFieldsValue({
createForm.setFieldsValue({ orderCommissionRate: 0,
orderCommissionRate: 0, redeemCommissionRate: 3,
redeemCommissionRate: 3, scopeType: CityPartnerScopeType.CITY_WIDE,
scopeType: CityPartnerScopeType.CITY_WIDE, });
}); setCreateScopeType(CityPartnerScopeType.CITY_WIDE);
setCreateScopeType(CityPartnerScopeType.CITY_WIDE); setCreateMaxRate(0.05);
setCreateMaxRate(0.05); setCreateCityCode(undefined);
setCreateCityCode(undefined); setCreateOpen(true);
setCreateOpen(true); }}
}} >
>
</Button>
</Button> }
</Space> />
<Alert <Alert
type="info" type="info"
+24 -22
View File
@@ -14,7 +14,6 @@ import {
Switch, Switch,
Table, Table,
Tag, Tag,
Typography,
message, message,
} from 'antd'; } from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
@@ -31,6 +30,7 @@ import { request, type Paginated } from '../lib/api';
import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants'; import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -316,27 +316,29 @@ export default function CityWarehousesPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> title="仓库"
{settingsButton} settings={settingsButton}
<Button actions={
type="primary" <Button
onClick={() => { type="primary"
createForm.resetFields(); onClick={() => {
setCreateManagerType(WarehouseManagerType.HQ); createForm.resetFields();
setCreateCityId(undefined); setCreateManagerType(WarehouseManagerType.HQ);
setCreateFulfillmentMode(WarehouseFulfillmentMode.MANUAL); setCreateCityId(undefined);
createForm.setFieldsValue({ setCreateFulfillmentMode(WarehouseFulfillmentMode.MANUAL);
managerType: WarehouseManagerType.HQ, createForm.setFieldsValue({
status: WarehouseStatus.ACTIVE, managerType: WarehouseManagerType.HQ,
fulfillmentMode: WarehouseFulfillmentMode.MANUAL, status: WarehouseStatus.ACTIVE,
}); fulfillmentMode: WarehouseFulfillmentMode.MANUAL,
setCreateOpen(true); });
}} setCreateOpen(true);
> }}
>
</Button>
</Space> </Button>
}
/>
<Alert <Alert
type="info" type="info"
+13 -2
View File
@@ -8,6 +8,7 @@ import { request } from '../lib/api';
import { ORDER_STATUS_LABELS, fmtTime } from '../lib/constants'; import { ORDER_STATUS_LABELS, fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -18,6 +19,8 @@ type Row = {
trackingNo: string | null; trackingNo: string | null;
providerOrderNo: string | null; providerOrderNo: string | null;
updatedAt: string; updatedAt: string;
/** 当次应付物流费 */
logisticsFee?: number | null;
order?: { order?: {
id: string; id: string;
orderNo: string; orderNo: string;
@@ -82,6 +85,12 @@ export default function DeliveriesPage() {
}, },
{ title: 'provider', dataIndex: 'provider', width: 90 }, { title: 'provider', dataIndex: 'provider', width: 90 },
{ title: '运单号', dataIndex: 'trackingNo', width: 140, render: (v) => v || '—' }, { title: '运单号', dataIndex: 'trackingNo', width: 140, render: (v) => v || '—' },
{
title: '运费',
dataIndex: 'logisticsFee',
width: 90,
render: (v: number | null | undefined) => (v == null ? '—' : `¥${Number(v).toFixed(2)}`),
},
{ title: '第三方单号', dataIndex: 'providerOrderNo', width: 140, render: (v) => v || '—' }, { title: '第三方单号', dataIndex: 'providerOrderNo', width: 140, render: (v) => v || '—' },
{ title: '订单状态', dataIndex: ['order', 'status'], width: 100, render: (s) => ORDER_STATUS_LABELS[s] || s }, { title: '订单状态', dataIndex: ['order', 'status'], width: 100, render: (s) => ORDER_STATUS_LABELS[s] || s },
{ title: '收货人', dataIndex: ['order', 'receiverName'], width: 90 }, { title: '收货人', dataIndex: ['order', 'receiverName'], width: 90 },
@@ -107,8 +116,7 @@ export default function DeliveriesPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4}>/</Typography.Title> <AdminListHeader title="快递/配送单" settings={settingsButton} />
{settingsButton}
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}> <Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
<Form.Item name="orderNo" label="订单号"><Input allowClear /></Form.Item> <Form.Item name="orderNo" label="订单号"><Input allowClear /></Form.Item>
<Form.Item name="provider" label="provider"><Input allowClear placeholder="MOCK" /></Form.Item> <Form.Item name="provider" label="provider"><Input allowClear placeholder="MOCK" /></Form.Item>
@@ -136,6 +144,9 @@ export default function DeliveriesPage() {
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}> <Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
<Descriptions.Item label="订单">{detail.order?.orderNo}</Descriptions.Item> <Descriptions.Item label="订单">{detail.order?.orderNo}</Descriptions.Item>
<Descriptions.Item label="收货">{detail.order?.receiverName} {detail.order?.receiverPhone}</Descriptions.Item> <Descriptions.Item label="收货">{detail.order?.receiverName} {detail.order?.receiverPhone}</Descriptions.Item>
<Descriptions.Item label="运费">
{detail.logisticsFee == null ? '—' : `¥${Number(detail.logisticsFee).toFixed(2)}`}
</Descriptions.Item>
</Descriptions> </Descriptions>
<Form form={editForm} layout="vertical"> <Form form={editForm} layout="vertical">
<Form.Item name="provider" label="provider" rules={[{ required: true }]}><Input /></Form.Item> <Form.Item name="provider" label="provider" rules={[{ required: true }]}><Input /></Form.Item>
+35 -34
View File
@@ -31,6 +31,7 @@ import { downloadBase64File } from '../lib/exportExcel';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import OssUpload from '../components/OssUpload'; import OssUpload from '../components/OssUpload';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -309,40 +310,40 @@ export default function DevPlanTasksPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}> title="开发计划 · 任务列表"
· settings={settingsButton}
</Typography.Title> actions={
{settingsButton} <>
<Space wrap> <Select
<Select value={exportFormat}
value={exportFormat} style={{ width: 120 }}
style={{ width: 120 }} onChange={setExportFormat}
onChange={setExportFormat} options={[
options={[ { value: 'markdown', label: 'Markdown' },
{ value: 'markdown', label: 'Markdown' }, { value: 'docx', label: 'Word' },
{ value: 'docx', label: 'Word' }, { value: 'xlsx', label: 'Excel' },
{ value: 'xlsx', label: 'Excel' }, { value: 'pdf', label: 'PDF' },
{ value: 'pdf', label: 'PDF' }, ]}
]} />
/> <Button loading={exporting} disabled={!selectedRowKeys.length} onClick={() => void exportTasks('selected')}>
<Button loading={exporting} disabled={!selectedRowKeys.length} onClick={() => void exportTasks('selected')}>
</Button>
</Button> <Button loading={exporting} onClick={() => void exportTasks('filter')}>
<Button loading={exporting} onClick={() => void exportTasks('filter')}>
</Button>
</Button> <Button disabled={!selectedRowKeys.length} onClick={openBatchEdit}>
<Button disabled={!selectedRowKeys.length} onClick={openBatchEdit}>
</Button>
</Button> <Button disabled={!selectedRowKeys.length} onClick={openDispatch}>
<Button disabled={!selectedRowKeys.length} onClick={openDispatch}>
</Button>
</Button> <Button type="primary" onClick={openCreate}>
<Button type="primary" onClick={openCreate}>
</Button>
</Button> </>
</Space> }
</div> />
<Form <Form
layout="inline" layout="inline"
@@ -9,7 +9,6 @@ import {
Space, Space,
Table, Table,
Tag, Tag,
Typography,
message, message,
} from 'antd'; } from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
@@ -23,6 +22,7 @@ import { request } from '../lib/api';
import { fmtTime } from '../lib/constants'; import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -168,15 +168,15 @@ export default function DevPlanVersionsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}> title="开发计划 · 版本列表"
· settings={settingsButton}
</Typography.Title> actions={
{settingsButton} <Button type="primary" onClick={openCreate}>
<Button type="primary" onClick={openCreate}>
</Button>
</Button> }
</div> />
<Form <Form
layout="inline" layout="inline"
@@ -11,7 +11,6 @@ import {
Space, Space,
Table, Table,
Tag, Tag,
Typography,
message, message,
} from 'antd'; } from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
@@ -29,6 +28,7 @@ import {
import { request } from '../lib/api'; import { request } from '../lib/api';
import { fmtTime } from '../lib/constants'; import { fmtTime } from '../lib/constants';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -247,20 +247,16 @@ export default function FulfillmentProvidersPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<div> title="仓配管理"
<Typography.Title level={4} style={{ margin: 0 }}> description="注册承运商接口凭证,并配置物流对账用的银行账户、结算方式与计价标准"
settings={settingsButton}
</Typography.Title> actions={
{settingsButton} <Button type="primary" onClick={openCreate}>
<Typography.Text type="secondary">
</Button>
</Typography.Text> }
</div> />
<Button type="primary" onClick={openCreate}>
</Button>
</Space>
<Alert <Alert
type="info" type="info"
+19 -16
View File
@@ -8,6 +8,7 @@ import { request, type HqProfile, type Paginated } from '../lib/api';
import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants'; import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -145,22 +146,24 @@ export default function HqAccountsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}>HQ </Typography.Title> title="HQ 账户"
{settingsButton} settings={settingsButton}
{isSuperAdmin && ( actions={
<Button isSuperAdmin ? (
type="primary" <Button
onClick={() => { type="primary"
createForm.resetFields(); onClick={() => {
createForm.setFieldsValue({ credentialType: 'phone', adminRole: 'OPS', cityIds: [] }); createForm.resetFields();
setCreateOpen(true); createForm.setFieldsValue({ credentialType: 'phone', adminRole: 'OPS', cityIds: [] });
}} setCreateOpen(true);
> }}
>
</Button>
)} </Button>
</Space> ) : null
}
/>
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}> <Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
<Form.Item name="phone" label="手机"><Input allowClear /></Form.Item> <Form.Item name="phone" label="手机"><Input allowClear /></Form.Item>
<Form.Item name="adminRole" label="角色"> <Form.Item name="adminRole" label="角色">
+6 -5
View File
@@ -10,6 +10,7 @@ import { fmtTime } from '../lib/constants';
import { HQ_OPERATION_ACTION_OPTIONS, resolveHqOperationLabel } from '../lib/hq-log'; import { HQ_OPERATION_ACTION_OPTIONS, resolveHqOperationLabel } from '../lib/hq-log';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -109,11 +110,11 @@ export default function HqLogsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4}>HQ </Typography.Title> <AdminListHeader
{settingsButton} title="HQ 操作日志"
<Typography.Paragraph type="secondary"> settings={settingsButton}
description="记录总部后台写操作(开城、订单、用户、权限、合伙人等),仅追加不删除。"
</Typography.Paragraph> />
<Form <Form
form={form} form={form}
+20 -27
View File
@@ -11,7 +11,6 @@ import {
Space, Space,
Table, Table,
Tag, Tag,
Typography,
Upload, Upload,
message, message,
} from 'antd'; } from 'antd';
@@ -31,6 +30,7 @@ import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { uploadFileToOss } from '../lib/upload'; import { uploadFileToOss } from '../lib/upload';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -241,32 +241,25 @@ export default function InvoicesPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<div <AdminListHeader
style={{ title="发票管理"
display: 'flex', settings={settingsButton}
justifyContent: 'space-between', actions={
alignItems: 'center', <Button
marginBottom: 16, type="primary"
}} onClick={() => {
> createForm.setFieldsValue({
<Typography.Title level={4} style={{ margin: 0 }}> titleType: 'PERSONAL',
invoiceKind: 'NORMAL',
</Typography.Title> invoiceCategory: 'LIQUOR',
{settingsButton} });
<Button setCreateOpen(true);
type="primary" }}
onClick={() => { >
createForm.setFieldsValue({
titleType: 'PERSONAL', </Button>
invoiceKind: 'NORMAL', }
invoiceCategory: 'LIQUOR', />
});
setCreateOpen(true);
}}
>
</Button>
</div>
<Form <Form
form={filterForm} form={filterForm}
layout="inline" layout="inline"
@@ -26,6 +26,7 @@ import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { uploadFileToOss } from '../lib/upload'; import { uploadFileToOss } from '../lib/upload';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -264,15 +265,11 @@ export default function KnowledgeBasesPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16 }} wrap> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}> title="知识库"
settings={settingsButton}
</Typography.Title> description="支持粘贴文本或上传 .txt/.md;可绑定到企微机器人供 AI 检索"
{settingsButton} />
<Typography.Text type="secondary">
.txt/.md AI
</Typography.Text>
</Space>
<Form <Form
form={filterForm} form={filterForm}
+6 -9
View File
@@ -26,6 +26,7 @@ import { request } from '../lib/api';
import { fmtTime } from '../lib/constants'; import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -271,15 +272,11 @@ export default function LlmConfigsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16 }} wrap> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}> title="语言模型配置"
settings={settingsButton}
</Typography.Title> description="非超管仅可见自己创建的配置,且创建后只能改是否生效"
{settingsButton} />
<Typography.Text type="secondary">
</Typography.Text>
</Space>
<Form <Form
form={filterForm} form={filterForm}
@@ -30,6 +30,7 @@ import { downloadExcelCsv } from '../lib/exportExcel';
import { request } from '../lib/api'; import { request } from '../lib/api';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
type BillRow = { type BillRow = {
@@ -405,15 +406,11 @@ export default function LogisticsBillsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space direction="vertical" size={0} style={{ marginBottom: 16 }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}> title="物流对账"
settings={settingsButton}
</Typography.Title> description="按快递承运商汇总月度物流费;计价与银行账户在「仓配管理」配置。前期充值扣款,后期可切挂账月结。"
{settingsButton} />
<Typography.Text type="secondary">
</Typography.Text>
</Space>
<Tabs <Tabs
items={[ items={[
+28 -16
View File
@@ -27,6 +27,7 @@ import dayjs, { type Dayjs } from 'dayjs';
import { ORDER_TYPE_LABELS } from '@dukang/shared-types'; import { ORDER_TYPE_LABELS } from '@dukang/shared-types';
import { request, type AdminOrderItem, type AdminOrderRow, type HqProfile, type Paginated } from '../lib/api'; import { request, type AdminOrderItem, type AdminOrderRow, type HqProfile, type Paginated } from '../lib/api';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
import { downloadBase64File } from '../lib/exportExcel'; import { downloadBase64File } from '../lib/exportExcel';
import { import {
@@ -144,7 +145,7 @@ type OrderExportFormat = 'xlsx' | 'pdf';
type OrderExportFilters = { type OrderExportFilters = {
orderNo?: string; orderNo?: string;
status?: string; status?: string | string[];
orderType?: string; orderType?: string;
cityId?: string; cityId?: string;
receiverPhone?: string; receiverPhone?: string;
@@ -219,6 +220,12 @@ function formatBenefitBrief(row: AdminOrderRow) {
return '—'; return '—';
} }
function selectedStatuses(status: unknown): string[] {
if (Array.isArray(status)) return status.filter((s): s is string => Boolean(s));
if (typeof status === 'string' && status) return [status];
return [];
}
function buildExportPayload( function buildExportPayload(
scope: OrderExportScope, scope: OrderExportScope,
format: OrderExportFormat, format: OrderExportFormat,
@@ -231,7 +238,8 @@ function buildExportPayload(
return payload; return payload;
} }
if (filters.orderNo) payload.orderNo = filters.orderNo; if (filters.orderNo) payload.orderNo = filters.orderNo;
if (filters.status) payload.status = filters.status; const statuses = selectedStatuses(filters.status);
if (statuses.length) payload.status = statuses;
if (filters.orderType) payload.orderType = filters.orderType; if (filters.orderType) payload.orderType = filters.orderType;
if (filters.cityId) payload.cityId = filters.cityId; if (filters.cityId) payload.cityId = filters.cityId;
if (filters.receiverPhone) payload.receiverPhone = filters.receiverPhone; if (filters.receiverPhone) payload.receiverPhone = filters.receiverPhone;
@@ -357,7 +365,7 @@ export default function OrdersPage() {
const qs = new URLSearchParams({ page: String(page), pageSize: String(pageSize) }); const qs = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
if (initialOrderNo) qs.set('orderNo', initialOrderNo); if (initialOrderNo) qs.set('orderNo', initialOrderNo);
if (values.orderNo) qs.set('orderNo', values.orderNo); if (values.orderNo) qs.set('orderNo', values.orderNo);
if (values.status) qs.set('status', values.status); for (const status of selectedStatuses(values.status)) qs.append('status', status);
if (values.orderType) qs.set('orderType', values.orderType); if (values.orderType) qs.set('orderType', values.orderType);
if (values.cityId) qs.set('cityId', values.cityId); if (values.cityId) qs.set('cityId', values.cityId);
if (values.receiverPhone) qs.set('receiverPhone', values.receiverPhone); if (values.receiverPhone) qs.set('receiverPhone', values.receiverPhone);
@@ -733,18 +741,20 @@ export default function OrdersPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 20, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> title="订单监控"
<Space size={12}> settings={settingsButton}
{settingsButton} actions={
<Button onClick={() => window.open('/orders/big-screen', 'dukang-big-screen')}></Button> <>
{canProxyOrder ? ( <Button onClick={() => window.open('/orders/big-screen', 'dukang-big-screen')}></Button>
<Button type="primary" onClick={() => setProxyOpen(true)}> {canProxyOrder ? (
<Button type="primary" onClick={() => setProxyOpen(true)}>
</Button>
) : null} </Button>
</Space> ) : null}
</Space> </>
}
/>
{!canDeleteOrders && profile ? ( {!canDeleteOrders && profile ? (
<Alert <Alert
@@ -782,10 +792,12 @@ export default function OrdersPage() {
</Space> </Space>
</Form.Item> </Form.Item>
</Col> </Col>
<Col xs={12} sm={6} md={5} lg={3}> <Col xs={24} sm={12} md={8} lg={6}>
<Form.Item name="status" label="状态" style={{ marginBottom: 12 }}> <Form.Item name="status" label="状态" style={{ marginBottom: 12 }}>
<Select <Select
mode="multiple"
allowClear allowClear
maxTagCount="responsive"
placeholder="全部" placeholder="全部"
options={Object.entries(ORDER_STATUS_LABELS).map(([value, label]) => ({ value, label }))} options={Object.entries(ORDER_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
/> />
@@ -8,6 +8,7 @@ import { request, type Paginated } from '../lib/api';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, ORDER_STATUS_LABELS, PARTNER_BILL_STATUS_LABELS, fmtTime } from '../lib/constants'; import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, ORDER_STATUS_LABELS, PARTNER_BILL_STATUS_LABELS, fmtTime } from '../lib/constants';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
type PartnerOption = { id: string; companyName: string }; type PartnerOption = { id: string; companyName: string };
@@ -261,15 +262,11 @@ export default function PartnerAccountsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<div> title="合伙人子账号"
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> settings={settingsButton}
{settingsButton} description="主账号在「开城合伙人」创建;此处仅管理子账号树与权限"
<Typography.Text type="secondary"> />
</Typography.Text>
</div>
</Space>
<Form <Form
form={form} form={form}
layout="inline" layout="inline"
@@ -23,6 +23,7 @@ import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
import { downloadExcelCsv } from '../lib/exportExcel'; import { downloadExcelCsv } from '../lib/exportExcel';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -349,15 +350,11 @@ export default function PartnerBillsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space direction="vertical" size={0} style={{ marginBottom: 16 }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}> title="合伙人账单"
settings={settingsButton}
</Typography.Title> description="每月 1 日 8:00 自动生成上月账单(待审核)→ 发送合伙人确认 → 未打款 → 已打款"
{settingsButton} />
<Typography.Text type="secondary">
1 8:00
</Typography.Text>
</Space>
{summary && ( {summary && (
<Card size="small" style={{ marginBottom: 16 }}> <Card size="small" style={{ marginBottom: 16 }}>
+2 -4
View File
@@ -15,6 +15,7 @@ import { AdminCellLine } from '../components/AdminCellLine';
import { fmtTime } from '../lib/constants'; import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -156,10 +157,7 @@ export default function PartnerLogsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4} style={{ marginBottom: 16 }}> <AdminListHeader title="合伙人日志" settings={settingsButton} />
</Typography.Title>
{settingsButton}
<Segmented <Segmented
options={PARTNER_LOG_CATEGORY_OPTIONS.map((o) => ({ value: o.value, label: o.label }))} options={PARTNER_LOG_CATEGORY_OPTIONS.map((o) => ({ value: o.value, label: o.label }))}
value={category} value={category}
+10 -7
View File
@@ -28,6 +28,7 @@ import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import CityDistrictMultiSelect from '../components/CityDistrictMultiSelect'; import CityDistrictMultiSelect from '../components/CityDistrictMultiSelect';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -218,13 +219,15 @@ export default function PartnersPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> title="开城合伙人"
{settingsButton} settings={settingsButton}
<Button type="primary" onClick={() => { setCreateOpen(true); createForm.resetFields(); setCreateScopeType(CityPartnerScopeType.CITY_WIDE); }}> actions={
<Button type="primary" onClick={() => { setCreateOpen(true); createForm.resetFields(); setCreateScopeType(CityPartnerScopeType.CITY_WIDE); }}>
</Button>
</Space> </Button>
}
/>
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}> <Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
<Form.Item name="companyName" label="公司"><Input allowClear /></Form.Item> <Form.Item name="companyName" label="公司"><Input allowClear /></Form.Item>
<Form.Item name="contactPhone" label="电话"><Input allowClear /></Form.Item> <Form.Item name="contactPhone" label="电话"><Input allowClear /></Form.Item>
@@ -24,6 +24,7 @@ import { request } from '../lib/api';
import { fmtTime } from '../lib/constants'; import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -146,8 +147,7 @@ export default function PendingRedeemPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4}></Typography.Title> <AdminListHeader title="待处理核销(弱网兜底)" settings={settingsButton} />
{settingsButton}
<Form <Form
form={form} form={form}
layout="inline" layout="inline"
+6 -5
View File
@@ -15,6 +15,7 @@ import ProductDetailTemplatePicker from '../components/ProductDetailTemplatePick
import ProductSpecsEditor from '../components/ProductSpecsEditor'; import ProductSpecsEditor from '../components/ProductSpecsEditor';
import type { FormInstance } from 'antd/es/form'; import type { FormInstance } from 'antd/es/form';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -510,11 +511,11 @@ export default function ProductsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> title="商品管理"
{settingsButton} settings={settingsButton}
<Button type="primary" onClick={() => setCreateOpen(true)}></Button> actions={<Button type="primary" onClick={() => setCreateOpen(true)}></Button>}
</Space> />
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}> <Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
<Form.Item name="name" label="名称"> <Form.Item name="name" label="名称">
<Input allowClear placeholder="名称 / SKU / 69 码" style={{ width: 200 }} /> <Input allowClear placeholder="名称 / SKU / 69 码" style={{ width: 200 }} />
+6 -5
View File
@@ -15,6 +15,7 @@ import { request } from '../lib/api';
import { fmtTime } from '../lib/constants'; import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -175,11 +176,11 @@ export default function PromoCodesPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}>广</Typography.Title> title="推广码管理"
{settingsButton} settings={settingsButton}
<Button type="primary" onClick={() => setCreateOpen(true)}>广</Button> actions={<Button type="primary" onClick={() => setCreateOpen(true)}>广</Button>}
</div> />
<Form <Form
form={filterForm} form={filterForm}
@@ -8,6 +8,7 @@ import { request } from '../lib/api';
import { fmtTime } from '../lib/constants'; import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -141,8 +142,7 @@ export default function RedeemRecordsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4}></Typography.Title> <AdminListHeader title="核销记录" settings={settingsButton} />
{settingsButton}
<Form <Form
form={form} form={form}
layout="inline" layout="inline"
+6 -9
View File
@@ -13,6 +13,7 @@ import {
} from '../lib/constants'; } from '../lib/constants';
import OssUpload from '../components/OssUpload'; import OssUpload from '../components/OssUpload';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
type Row = { type Row = {
@@ -121,15 +122,11 @@ export default function ResourcesPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}> title="OSS 资源库"
OSS settings={settingsButton}
</Typography.Title> actions={<Button type="primary" onClick={() => setCreateOpen(true)}></Button>}
{settingsButton} />
<Button type="primary" onClick={() => setCreateOpen(true)}>
</Button>
</Space>
<Form <Form
form={form} form={form}
layout="inline" layout="inline"
+17 -18
View File
@@ -7,6 +7,7 @@ import { request, type Paginated } from '../lib/api';
import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, STORE_STATUS_LABELS, fmtTime } from '../lib/constants'; import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, STORE_STATUS_LABELS, fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -153,24 +154,22 @@ export default function StoreAccountsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<Space direction="vertical" size={0}> title="门店账户"
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> settings={settingsButton}
{settingsButton} description="主账号可绑定多家门店;收款信息挂在主账号;「新建账户」用于补录无主账号门店"
<Typography.Text type="secondary"> actions={
<Button
</Typography.Text> type="primary"
</Space> onClick={() => {
<Button void loadStores();
type="primary" setCreateOpen(true);
onClick={() => { }}
void loadStores(); >
setCreateOpen(true);
}} </Button>
> }
/>
</Button>
</Space>
<Form <Form
form={form} form={form}
layout="inline" layout="inline"
+18 -15
View File
@@ -31,6 +31,7 @@ import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
import { downloadExcelCsv } from '../lib/exportExcel'; import { downloadExcelCsv } from '../lib/exportExcel';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -381,21 +382,23 @@ export default function StoreBillsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space direction="vertical" size={0} style={{ marginBottom: 16 }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}> title="门店账单"
settings={settingsButton}
</Typography.Title> description={
{settingsButton} <>
<Typography.Text type="secondary"> T+1 沿
T+1 沿 = T+1 =
= T+1 = {overdueSummary && overdueSummary.overdueCount > 0 ? (
</Typography.Text> <div>
{overdueSummary && overdueSummary.overdueCount > 0 ? ( <Typography.Text type="danger">
<Typography.Text type="danger"> {overdueSummary.overdueCount} {overdueSummary.pendingCount}
{overdueSummary.overdueCount} {overdueSummary.pendingCount} </Typography.Text>
</Typography.Text> </div>
) : null} ) : null}
</Space> </>
}
/>
{summary && ( {summary && (
<Card size="small" style={{ marginBottom: 16 }}> <Card size="small" style={{ marginBottom: 16 }}>
@@ -5,6 +5,7 @@ import {
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { request, type HqProfile } from '../lib/api'; import { request, type HqProfile } from '../lib/api';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -179,27 +180,27 @@ export default function StoreCategoriesPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<Space direction="vertical" size={0}> title="门店分类"
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> settings={settingsButton}
{settingsButton} description="两级分类:一级(餐饮/住宿/娱乐)→ 二级业态,供合伙人开店选择"
<Typography.Text type="secondary">/宿/ </Typography.Text> actions={
</Space> <>
<Space> {canDelete ? (
{canDelete ? ( <Button
<Button onClick={async () => {
onClick={async () => { await request('/admin/store-categories/ensure-defaults', { method: 'POST' });
await request('/admin/store-categories/ensure-defaults', { method: 'POST' }); message.success('已同步默认分类');
message.success('已同步默认分类'); void reload();
void reload(); }}
}} >
>
</Button>
</Button> ) : null}
) : null} <Button type="primary" onClick={() => openCreate()}></Button>
<Button type="primary" onClick={() => openCreate()}></Button> </>
</Space> }
</Space> />
<Table <Table
rowKey="id" rowKey="id"
+6 -5
View File
@@ -13,6 +13,7 @@ import { AdminCellLine } from '../components/AdminCellLine';
import { fmtTime } from '../lib/constants'; import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -151,11 +152,11 @@ export default function StoreLogsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4}></Typography.Title> <AdminListHeader
{settingsButton} title="商户日志"
<Typography.Paragraph type="secondary" style={{ marginTop: -8 }}> settings={settingsButton}
/ description="门店登录、微信授权、核销、打款与营业状态等操作记录;历史核销/打款数据来自业务表归档。"
</Typography.Paragraph> />
<Segmented <Segmented
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
options={STORE_LOG_CATEGORY_OPTIONS.map((o) => ({ value: o.value, label: o.label }))} options={STORE_LOG_CATEGORY_OPTIONS.map((o) => ({ value: o.value, label: o.label }))}
+6 -5
View File
@@ -8,6 +8,7 @@ import { ADMIN_OPTIONS_PAGE_SIZE, MEDIA_TYPE_LABELS, fmtTime } from '../lib/cons
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import OssUpload from '../components/OssUpload'; import OssUpload from '../components/OssUpload';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -96,11 +97,11 @@ export default function StoreMediaPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> title="门店资源"
{settingsButton} settings={settingsButton}
<Button type="primary" onClick={() => { void loadStores(); setCreateOpen(true); }}></Button> actions={<Button type="primary" onClick={() => { void loadStores(); setCreateOpen(true); }}></Button>}
</Space> />
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}> <Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
<Form.Item name="storeId" label="门店ID"><Input allowClear /></Form.Item> <Form.Item name="storeId" label="门店ID"><Input allowClear /></Form.Item>
<Form.Item name="mediaType" label="类型"> <Form.Item name="mediaType" label="类型">
@@ -33,6 +33,7 @@ import { notifyPackageAuditChanged } from '../lib/admin-events';
import { fmtTime } from '../lib/constants'; import { fmtTime } from '../lib/constants';
import StorePackageAuditPanel from '../components/StorePackageAuditPanel'; import StorePackageAuditPanel from '../components/StorePackageAuditPanel';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
const HQ_PACKAGE_STATUS_LABELS: Record<StorePackageChangeStatus, string> = { const HQ_PACKAGE_STATUS_LABELS: Record<StorePackageChangeStatus, string> = {
@@ -545,8 +546,7 @@ export default function StorePackageAuditsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4}></Typography.Title> <AdminListHeader title="审核通知" settings={settingsButton} />
{settingsButton}
<Tabs <Tabs
activeKey={activeTab} activeKey={activeTab}
onChange={setActiveTab} onChange={setActiveTab}
@@ -6,6 +6,7 @@ import { fmtTime, ADMIN_OPTIONS_PAGE_SIZE } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { request, type Paginated } from '../lib/api'; import { request, type Paginated } from '../lib/api';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
type Row = { type Row = {
@@ -69,10 +70,7 @@ export default function StoreRatingsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4} style={{ marginTop: 0 }}> <AdminListHeader title="门店评价" settings={settingsButton} />
</Typography.Title>
{settingsButton}
<Form <Form
form={form} form={form}
layout="inline" layout="inline"
@@ -21,6 +21,7 @@ import { request, type Paginated } from '../lib/api';
import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants'; import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -237,10 +238,7 @@ export default function StoreWithdrawalsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4} style={{ marginTop: 0 }}> <AdminListHeader title="门店提现审" settings={settingsButton} />
</Typography.Title>
{settingsButton}
{overdueSummary ? ( {overdueSummary ? (
<Typography.Paragraph type="secondary"> <Typography.Paragraph type="secondary">
{overdueSummary.pendingCount} {overdueSummary.pendingCount}
+8 -9
View File
@@ -40,6 +40,7 @@ import {
} from '../lib/storeCreate'; } from '../lib/storeCreate';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
import { resolveRegionBinding } from '../lib/china-region'; import { resolveRegionBinding } from '../lib/china-region';
import ChinaRegionCascader from '../components/ChinaRegionCascader'; import ChinaRegionCascader from '../components/ChinaRegionCascader';
@@ -873,16 +874,14 @@ export default function StoresPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<Space direction="vertical" size={0}> title="门店"
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> settings={settingsButton}
<Typography.Text type="secondary"> {data?.total ?? 0} </Typography.Text> description={`${data?.total ?? 0} 家门店(含合伙人录入)`}
</Space> actions={
<Space>
{settingsButton}
<Button type="primary" onClick={openCreateModal}></Button> <Button type="primary" onClick={openCreateModal}></Button>
</Space> }
</Space> />
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}> <Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
<Form.Item name="name" label="名称"><Input allowClear /></Form.Item> <Form.Item name="name" label="名称"><Input allowClear /></Form.Item>
<Form.Item name="phone" label="电话"><Input allowClear /></Form.Item> <Form.Item name="phone" label="电话"><Input allowClear /></Form.Item>
@@ -21,6 +21,7 @@ import type { ColumnsType } from 'antd/es/table';
import { request, type Paginated } from '../lib/api'; import { request, type Paginated } from '../lib/api';
import { fmtTime } from '../lib/constants'; import { fmtTime } from '../lib/constants';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
const MOCK_KEYS = ['MOCK_SMS', 'MOCK_WECHAT', 'MOCK_PAY'] as const; const MOCK_KEYS = ['MOCK_SMS', 'MOCK_WECHAT', 'MOCK_PAY'] as const;
@@ -410,10 +411,7 @@ export default function TestWhitelistPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4} style={{ marginTop: 0 }}> <AdminListHeader title="白名单管理" settings={settingsButton} />
</Typography.Title>
{settingsButton}
<Card size="small" loading={mockLoading} style={{ marginBottom: 16 }}> <Card size="small" loading={mockLoading} style={{ marginBottom: 16 }}>
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}> <Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
@@ -5,6 +5,7 @@ import { request } from '../lib/api';
import { fmtTime } from '../lib/constants'; import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -137,8 +138,7 @@ export default function ThirdPartyLogsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4}></Typography.Title> <AdminListHeader title="第三方日志" settings={settingsButton} />
{settingsButton}
<Form <Form
layout="inline" layout="inline"
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
+2 -2
View File
@@ -8,6 +8,7 @@ import { AdminCellLine } from '../components/AdminCellLine';
import { fmtTime } from '../lib/constants'; import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -120,8 +121,7 @@ export default function UserLogsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4}></Typography.Title> <AdminListHeader title="用户日志" settings={settingsButton} />
{settingsButton}
<Segmented <Segmented
style={{ marginBottom: 16 }} style={{ marginBottom: 16 }}
options={USER_LOG_CATEGORY_OPTIONS.map((o) => ({ value: o.value, label: o.label }))} options={USER_LOG_CATEGORY_OPTIONS.map((o) => ({ value: o.value, label: o.label }))}
+16 -15
View File
@@ -20,6 +20,7 @@ import type { ColumnsType } from 'antd/es/table';
import { USER_SOURCE_TYPE_LABELS, resolveUserLogCategory, type UserSourceType } from '@dukang/shared-types'; import { USER_SOURCE_TYPE_LABELS, resolveUserLogCategory, type UserSourceType } from '@dukang/shared-types';
import { request, type AdminUserRow, type HqProfile, type Paginated } from '../lib/api'; import { request, type AdminUserRow, type HqProfile, type Paginated } from '../lib/api';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
import { ORDER_STATUS_LABELS, fmtTime, maskPhone } from '../lib/constants'; import { ORDER_STATUS_LABELS, fmtTime, maskPhone } from '../lib/constants';
@@ -481,21 +482,21 @@ export default function UsersPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}> <AdminListHeader
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title> title="用户监控"
<Space> settings={settingsButton}
{settingsButton} actions={
{canDeleteUsers ? ( canDeleteUsers ? (
<Button <Button
danger danger
disabled={!selectedRowKeys.length} disabled={!selectedRowKeys.length}
onClick={() => void openBatchDeleteModal()} onClick={() => void openBatchDeleteModal()}
> >
{selectedRowKeys.length ? ` (${selectedRowKeys.length})` : ''} {selectedRowKeys.length ? ` (${selectedRowKeys.length})` : ''}
</Button> </Button>
) : null} ) : null
</Space> }
</Space> />
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={() => { setPage(1); void load(); }}> <Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={() => { setPage(1); void load(); }}>
<Form.Item name="phone" label="手机号"> <Form.Item name="phone" label="手机号">
<Input placeholder="模糊搜索" allowClear /> <Input placeholder="模糊搜索" allowClear />
@@ -8,6 +8,7 @@ import { AdminCellLine } from '../components/AdminCellLine';
import { fmtTime, maskPhone } from '../lib/constants'; import { fmtTime, maskPhone } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -210,11 +211,11 @@ export default function WechatBindingsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4}></Typography.Title> <AdminListHeader
{settingsButton} title="微信绑定总览"
<Typography.Paragraph type="secondary"> settings={settingsButton}
unionId C HQ unionId description="按 unionId 聚合展示已绑定微信的 C 端用户、门店账号、合伙人账号与 HQ 账号;无 unionId 时按单账号分组。"
</Typography.Paragraph> />
<Form <Form
form={form} form={form}
@@ -6,6 +6,7 @@ import { request } from '../lib/api';
import { fmtTime } from '../lib/constants'; import { fmtTime } from '../lib/constants';
import type { WecomBotLogDto } from '@dukang/shared-types'; import type { WecomBotLogDto } from '@dukang/shared-types';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -64,11 +65,11 @@ export default function WecomBotLogsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<Typography.Title level={4}></Typography.Title> <AdminListHeader
{settingsButton} title="企微机器人日志"
<Typography.Paragraph type="secondary"> settings={settingsButton}
description="记录机器人在企微内的查询与审批操作,含权限点、耗时与成败。"
</Typography.Paragraph> />
<Form <Form
form={form} form={form}
@@ -35,6 +35,7 @@ import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import OssUpload from '../components/OssUpload'; import OssUpload from '../components/OssUpload';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -272,9 +273,15 @@ function PushRoutesTab() {
return ( return (
<> <>
{settingsModal} {settingsModal}
<Typography.Paragraph type="secondary"> <AdminListHeader
Webhook / .env Webhook URL settings={settingsButton}
</Typography.Paragraph> description="配置群机器人 Webhook:按推送条件订阅业务通知 / 告警。运行时不再读取 .env 中的 Webhook URL。"
actions={
<Button type="primary" onClick={openCreate}>
</Button>
}
/>
<Form <Form
form={filterForm} form={filterForm}
@@ -309,10 +316,6 @@ function PushRoutesTab() {
> >
</Button> </Button>
<Button type="primary" onClick={openCreate}>
</Button>
{settingsButton}
</Space> </Space>
</Form.Item> </Form.Item>
</Form> </Form>
+13 -25
View File
@@ -27,6 +27,7 @@ import { downloadExcelCsv } from '../lib/exportExcel';
import { request, type HqProfile } from '../lib/api'; import { request, type HqProfile } from '../lib/api';
import { useAdminList } from '../lib/useAdminList'; import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns'; import { useAdminListColumns } from '../lib/useAdminListColumns';
import { AdminListHeader } from '../components/AdminListHeader';
import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
@@ -305,31 +306,18 @@ export default function WineryBillsPage() {
return ( return (
<div> <div>
{settingsModal} {settingsModal}
<div <AdminListHeader
style={{ title="酒厂对账单"
display: 'flex', settings={settingsButton}
justifyContent: 'space-between', description={`T+3:每日 8:00 汇总 3 天前(自然日)已完成的同城/跨城订单(实付 × ${ratePct}%);未打款红色、已打款绿色、应付为 0 无需打款(灰),可展开订单明细`}
alignItems: 'flex-start', actions={
marginBottom: 16, canEditWineryBank ? (
gap: 16, <Button type="default" onClick={() => void openBankModal()}>
}}
> </Button>
<Space direction="vertical" size={0}> ) : null
<Typography.Title level={4} style={{ margin: 0 }}> }
/>
</Typography.Title>
{settingsButton}
<Typography.Text type="secondary">
T+3 8:00 3 / × {ratePct}%绿 0
</Typography.Text>
</Space>
{canEditWineryBank ? (
<Button type="default" onClick={() => void openBankModal()}>
</Button>
) : null}
</div>
{summary && ( {summary && (
<Card size="small" style={{ marginBottom: 16 }}> <Card size="small" style={{ marginBottom: 16 }}>
@@ -11,6 +11,7 @@ import { useAdminList } from '../../lib/useAdminList';
import type { PromoCodeDetailContext } from './PromoCodeDetailLayout'; import type { PromoCodeDetailContext } from './PromoCodeDetailLayout';
import { useAdminListColumns } from '../../lib/useAdminListColumns'; import { useAdminListColumns } from '../../lib/useAdminListColumns';
import { AdminPrimaryLink } from '../../components/AdminPrimaryLink'; import { AdminPrimaryLink } from '../../components/AdminPrimaryLink';
import { AdminListHeader } from '../../components/AdminListHeader';
export default function PromoCodeUsersPage() { export default function PromoCodeUsersPage() {
@@ -84,20 +85,24 @@ export default function PromoCodeUsersPage() {
const { columns, settingsButton, settingsModal } = useAdminListColumns('promo-code-users', baseColumns, { page, pageSize }); const { columns, settingsButton, settingsModal } = useAdminListColumns('promo-code-users', baseColumns, { page, pageSize });
return ( return (
<Table <>
rowKey="id" {settingsModal}
className="admin-table-nowrap" <AdminListHeader settings={settingsButton} />
loading={loading} <Table
columns={columns} rowKey="id"
dataSource={data?.items ?? []} className="admin-table-nowrap"
scroll={{ x: 'max-content' }} loading={loading}
pagination={{ columns={columns}
current: page, dataSource={data?.items ?? []}
pageSize, scroll={{ x: 'max-content' }}
total: data?.total ?? 0, pagination={{
showSizeChanger: true, current: page,
onChange: (p, ps) => { setPage(p); setPageSize(ps); }, pageSize,
}} total: data?.total ?? 0,
/> showSizeChanger: true,
onChange: (p, ps) => { setPage(p); setPageSize(ps); },
}}
/>
</>
); );
} }
+1 -1
View File
@@ -84,7 +84,7 @@
**HQ 订单导出**`admin-web` 订单监控): **HQ 订单导出**`admin-web` 订单监控):
- 支持按筛选(下单日期、状态、类型、城市、配送、收货手机等)或勾选订单导出 - 支持按筛选(下单日期、状态可多选、类型、城市、配送、收货手机等)或勾选订单导出
- 格式:Excel`.xlsx`/ PDF`.pdf` - 格式:Excel`.xlsx`/ PDF`.pdf`
- 单次上限 5000 条;按筛选且未指定日期时默认近 30 天 - 单次上限 5000 条;按筛选且未指定日期时默认近 30 天
- 权限:`orders`;操作审计 `ORDER_EXPORT` - 权限:`orders`;操作审计 `ORDER_EXPORT`
+1 -1
View File
@@ -67,7 +67,7 @@
| 日期 | 说明 | | 日期 | 说明 |
|------|------| |------|------|
| 2026-08-25 | v3.5.9:门店列表累计核销好客权益;HQ 表格去省略号;主列表序号 + 列设置/列宽存 HQ 账号;用户列表备注(不改昵称)、手机号不脱敏 | | 2026-08-25 | v3.5.9:门店列表累计核销好客权益;HQ 表格去省略号;主列表序号 + 列设置/列宽存 HQ 账号;用户列表备注(不改昵称)、手机号不脱敏;订单列表状态可多选 |
| 2026-08-24 | v3.5.8HQ 单账号追加/撤销权限;运营改客服;城市门店服务与城市范围;分类删除权限与概览按权限/城市裁剪 | | 2026-08-24 | v3.5.8HQ 单账号追加/撤销权限;运营改客服;城市门店服务与城市范围;分类删除权限与概览按权限/城市裁剪 |
| 2026-08-23 | v3.5.7:门店核销记录时间显示秒;首页「今日到账金额」→「今日核销金额」 | | 2026-08-23 | v3.5.7:门店核销记录时间显示秒;首页「今日到账金额」→「今日核销金额」 |
| 2026-08-23 | v3.5.6:开发计划任务导出;核销/财务银行信息与打款凭证;工单图片入任务;运营去调试;门店审核全屏 | | 2026-08-23 | v3.5.6:开发计划任务导出;核销/财务银行信息与打款凭证;工单图片入任务;运营去调试;门店审核全屏 |
+7
View File
@@ -120,6 +120,7 @@
- [ ] 用户列表昵称只读;双击「备注」可改,离开编辑后 `user_user.hq_remark` 已更新;C 端看不到该字段 - [ ] 用户列表昵称只读;双击「备注」可改,离开编辑后 `user_user.hq_remark` 已更新;C 端看不到该字段
- [ ] 用户列表手机号完整可见(详情仍脱敏) - [ ] 用户列表手机号完整可见(详情仍脱敏)
- [ ] 权益券列表:用户编号、订单号可点进对应用户/订单详情;来源含商品名、规格、数量、配送方式、实付金额 - [ ] 权益券列表:用户编号、订单号可点进对应用户/订单详情;来源含商品名、规格、数量、配送方式、实付金额
- [ ] 订单列表「状态」可多选;导出按所选状态过滤;不选即全部
--- ---
@@ -128,3 +129,9 @@
- **用户**、**订单**列(及券详情)用主列下划线,分别打开用户详情抽屉、订单详情抽屉。 - **用户**、**订单**列(及券详情)用主列下划线,分别打开用户详情抽屉、订单详情抽屉。
- **来源**:关联订单时拼 `商品名 / 规格 / 数量(瓶或箱) / 配送方式 / ¥实付`;无订单仍用 `sourceProduct`(如总部手动发放)。 - **来源**:关联订单时拼 `商品名 / 规格 / 数量(瓶或箱) / 配送方式 / ¥实付`;无订单仍用 `sourceProduct`(如总部手动发放)。
- 列表接口 `order` 增补 `productName``productSpec``quantity``saleUnit``deliveryType``payAmount` - 列表接口 `order` 增补 `productName``productSpec``quantity``saleUnit``deliveryType``payAmount`
---
## 11. 订单列表状态多选
`GET /admin/orders``POST /admin/orders/export``status` 支持多值(重复 query、逗号串、JSON 数组均可)。列表筛选为多选;空 = 全部。单值旧链接仍可用。
+15
View File
@@ -3,6 +3,21 @@ export interface AdminListQuery {
pageSize?: number; pageSize?: number;
} }
/** HQ 订单列表 / 导出筛选。`status` 可多选(单值仍兼容) */
export interface AdminOrdersListQuery extends AdminListQuery {
orderNo?: string;
status?: string | string[];
orderType?: string;
userId?: string;
cityId?: string;
receiverPhone?: string;
fulfillmentHold?: string | boolean;
createdFrom?: string;
createdTo?: string;
excludeTest?: boolean;
deliveryType?: string;
}
export interface UpdateAdminUserRequest { export interface UpdateAdminUserRequest {
hqRemark: string; hqRemark: string;
} }
@@ -155,7 +155,9 @@ export class AdminOrdersService {
const where: Prisma.OrderWhereInput = {}; const where: Prisma.OrderWhereInput = {};
if (query.orderNo) where.orderNo = { contains: query.orderNo }; if (query.orderNo) where.orderNo = { contains: query.orderNo };
if (query.status) where.status = query.status as Prisma.EnumOrderStatusFilter['equals']; if (query.status?.length) {
where.status = { in: query.status as Prisma.EnumOrderStatusFilter['in'] };
}
if (query.orderType) where.orderType = query.orderType as Prisma.EnumOrderTypeFilter['equals']; if (query.orderType) where.orderType = query.orderType as Prisma.EnumOrderTypeFilter['equals'];
if (query.userId) where.userId = BigInt(query.userId); if (query.userId) where.userId = BigInt(query.userId);
if (query.cityId) where.cityId = BigInt(query.cityId); if (query.cityId) where.cityId = BigInt(query.cityId);
@@ -1,6 +1,9 @@
import { OrderStatus } from '@dukang/shared-types';
import { Type, Transform } from 'class-transformer'; import { Type, Transform } from 'class-transformer';
import { IsArray, IsBoolean, IsIn, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; import { IsArray, IsBoolean, IsIn, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
const ORDER_STATUS_VALUES = Object.values(OrderStatus);
function toOptionalBoolean(value: unknown): boolean | undefined { function toOptionalBoolean(value: unknown): boolean | undefined {
if (value === undefined || value === null || value === '') return undefined; if (value === undefined || value === null || value === '') return undefined;
if (value === true || value === 'true' || value === '1' || value === 1) return true; if (value === true || value === 'true' || value === '1' || value === 1) return true;
@@ -8,6 +11,19 @@ function toOptionalBoolean(value: unknown): boolean | undefined {
return undefined; return undefined;
} }
/** Query/body:单值、逗号串、重复 key、数组均可 → string[] */
function toOptionalStringList(value: unknown): string[] | undefined {
if (value === undefined || value === null || value === '') return undefined;
const raw = Array.isArray(value) ? value : [value];
const items = [...new Set(
raw
.flatMap((v) => String(v).split(','))
.map((s) => s.trim())
.filter(Boolean),
)];
return items.length ? items : undefined;
}
export class PaginationQueryDto { export class PaginationQueryDto {
@IsOptional() @IsOptional()
@Type(() => Number) @Type(() => Number)
@@ -57,9 +73,12 @@ export class AdminOrdersQueryDto extends PaginationQueryDto {
@IsString() @IsString()
orderNo?: string; orderNo?: string;
/** 单值或多项:`?status=PENDING_PAY` / `?status=A&status=B` / `?status=A,B` */
@IsOptional() @IsOptional()
@IsString() @Transform(({ value }) => toOptionalStringList(value))
status?: string; @IsArray()
@IsIn(ORDER_STATUS_VALUES, { each: true })
status?: string[];
@IsOptional() @IsOptional()
@IsIn(['NORMAL', 'PROXY']) @IsIn(['NORMAL', 'PROXY'])
@@ -117,8 +136,10 @@ export class AdminOrdersExportDto {
orderNo?: string; orderNo?: string;
@IsOptional() @IsOptional()
@IsString() @Transform(({ value }) => toOptionalStringList(value))
status?: string; @IsArray()
@IsIn(ORDER_STATUS_VALUES, { each: true })
status?: string[];
@IsOptional() @IsOptional()
@IsIn(['NORMAL', 'PROXY', 'RESHIPMENT']) @IsIn(['NORMAL', 'PROXY', 'RESHIPMENT'])