v4.0.18版本提交
This commit is contained in:
@@ -36,6 +36,7 @@ import StoreBillsPage from './pages/StoreBillsPage';
|
||||
import PartnerBillsPage from './pages/PartnerBillsPage';
|
||||
import WineryBillsPage from './pages/WineryBillsPage';
|
||||
import LogisticsBillsPage from './pages/LogisticsBillsPage';
|
||||
import BankAccountsPage from './pages/BankAccountsPage';
|
||||
import TicketsPage from './pages/TicketsPage';
|
||||
import SupportTicketsPage from './pages/SupportTicketsPage';
|
||||
import InvoicesPage from './pages/InvoicesPage';
|
||||
@@ -133,6 +134,7 @@ export default function App() {
|
||||
<Route path="/finance/partner-bills" element={<PartnerBillsPage />} />
|
||||
<Route path="/finance/winery-bills" element={<WineryBillsPage />} />
|
||||
<Route path="/finance/logistics-bills" element={<LogisticsBillsPage />} />
|
||||
<Route path="/finance/bank-accounts" element={<BankAccountsPage />} />
|
||||
<Route path="/store-bills" element={<Navigate to="/finance/store-bills" replace />} />
|
||||
<Route path="/store-payouts" element={<Navigate to="/finance/store-bills" replace />} />
|
||||
<Route
|
||||
|
||||
@@ -86,6 +86,7 @@ const MENU_ITEMS: MenuProps['items'] = [
|
||||
{ key: '/finance/partner-bills', label: '合伙人账单' },
|
||||
{ key: '/finance/winery-bills', label: '酒厂账单' },
|
||||
{ key: '/finance/logistics-bills', label: '物流对账' },
|
||||
{ key: '/finance/bank-accounts', label: '银行账户' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -211,6 +212,7 @@ function menuAllowed(key: string, permissionKeys: string[]): boolean {
|
||||
'/finance/partner-bills': 'finance',
|
||||
'/finance/winery-bills': 'finance',
|
||||
'/finance/logistics-bills': 'finance',
|
||||
'/finance/bank-accounts': 'finance',
|
||||
'benefit-group': 'benefit',
|
||||
'/benefit/coupons': 'benefit',
|
||||
'/benefit/ledgers': 'benefit',
|
||||
|
||||
@@ -28,6 +28,8 @@ export const HQ_OPERATION_ACTION_OPTIONS = [
|
||||
{ value: 'STORE_AUDIT', label: '门店审核' },
|
||||
{ value: 'STORE_ACCOUNT_CREATE', label: '新增门店账户' },
|
||||
{ value: 'STORE_ACCOUNT_UPDATE', label: '编辑门店账户' },
|
||||
{ value: 'STORE_ACCOUNT_STAFF_CREATE', label: '新增门店子账号' },
|
||||
{ value: 'STORE_ACCOUNT_STAFF_UPDATE', label: '编辑门店子账号' },
|
||||
{ value: 'STORE_ACCOUNT_STAFF_DELETE', label: '删除门店子账号' },
|
||||
{ value: 'STORE_CATEGORY_CREATE', label: '新增门店分类' },
|
||||
{ value: 'STORE_CATEGORY_UPDATE', label: '编辑门店分类' },
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
message,
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import {
|
||||
FINANCE_BANK_ACCOUNT_TYPE_LABELS,
|
||||
FINANCE_BANK_ACCOUNT_TYPES,
|
||||
type FinanceBankAccountDto,
|
||||
type FinanceBankAccountType,
|
||||
} from '@dukang/shared-types';
|
||||
import { request, type Paginated } from '../lib/api';
|
||||
import { ADMIN_OPTIONS_PAGE_SIZE } from '../lib/constants';
|
||||
import { downloadBase64File } from '../lib/exportExcel';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
import { useAdminListColumns } from '../lib/useAdminListColumns';
|
||||
import { AdminListHeader } from '../components/AdminListHeader';
|
||||
|
||||
type CityOption = { id: string; name: string };
|
||||
|
||||
type ExportResult = {
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
contentBase64: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
type OtherForm = {
|
||||
name?: string;
|
||||
bankAccountName: string;
|
||||
bankAccountNo: string;
|
||||
bankBranch?: string;
|
||||
remark?: string;
|
||||
};
|
||||
|
||||
const TYPE_COLORS: Record<FinanceBankAccountType, string> = {
|
||||
STORE: 'blue',
|
||||
WINERY: 'gold',
|
||||
PARTNER: 'purple',
|
||||
LOGISTICS: 'cyan',
|
||||
OTHER: 'default',
|
||||
};
|
||||
|
||||
function otherNumericId(id: string) {
|
||||
return id.startsWith('OTHER:') ? id.slice('OTHER:'.length) : id;
|
||||
}
|
||||
|
||||
export default function BankAccountsPage() {
|
||||
const [filterForm] = Form.useForm<{
|
||||
type?: FinanceBankAccountType;
|
||||
cityId?: string;
|
||||
keyword?: string;
|
||||
}>();
|
||||
const [otherForm] = Form.useForm<OtherForm>();
|
||||
const [remarkForm] = Form.useForm<{ remark?: string }>();
|
||||
const [filters, setFilters] = useState({ type: '', cityId: '', keyword: '' });
|
||||
const [cities, setCities] = useState<CityOption[]>([]);
|
||||
const [otherOpen, setOtherOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<FinanceBankAccountDto | null>(null);
|
||||
const [remarkTarget, setRemarkTarget] = useState<FinanceBankAccountDto | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [exporting, setExporting] = useState<'xlsx' | 'pdf' | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void request<Paginated<CityOption>>(`/admin/cities?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
||||
.then((res) => setCities(res.items ?? []))
|
||||
.catch(() => setCities([]));
|
||||
}, []);
|
||||
|
||||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<FinanceBankAccountDto>(
|
||||
'/admin/finance/bank-accounts',
|
||||
() => {
|
||||
const qs = new URLSearchParams();
|
||||
if (filters.type) qs.set('type', filters.type);
|
||||
if (filters.cityId) qs.set('cityId', filters.cityId);
|
||||
if (filters.keyword) qs.set('keyword', filters.keyword);
|
||||
return qs;
|
||||
},
|
||||
[filters.type, filters.cityId, filters.keyword],
|
||||
);
|
||||
|
||||
function openCreate() {
|
||||
setEditing(null);
|
||||
otherForm.resetFields();
|
||||
setOtherOpen(true);
|
||||
}
|
||||
|
||||
function openEdit(row: FinanceBankAccountDto) {
|
||||
setEditing(row);
|
||||
otherForm.setFieldsValue({
|
||||
name: row.ownerName === row.bankAccountName ? undefined : row.ownerName,
|
||||
bankAccountName: row.bankAccountName,
|
||||
bankAccountNo: row.bankAccountNo,
|
||||
bankBranch: row.bankBranch ?? undefined,
|
||||
remark: row.remark ?? undefined,
|
||||
});
|
||||
setOtherOpen(true);
|
||||
}
|
||||
|
||||
function openRemark(row: FinanceBankAccountDto) {
|
||||
setRemarkTarget(row);
|
||||
remarkForm.setFieldsValue({ remark: row.remark ?? undefined });
|
||||
}
|
||||
|
||||
async function saveOther() {
|
||||
const values = await otherForm.validateFields();
|
||||
setSaving(true);
|
||||
try {
|
||||
if (editing) {
|
||||
await request(`/admin/finance/bank-accounts/other/${otherNumericId(editing.id)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
message.success('已保存');
|
||||
} else {
|
||||
await request('/admin/finance/bank-accounts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
message.success('已新增');
|
||||
}
|
||||
setOtherOpen(false);
|
||||
await reload();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRemark() {
|
||||
if (!remarkTarget) return;
|
||||
const values = await remarkForm.validateFields();
|
||||
setSaving(true);
|
||||
try {
|
||||
await request(`/admin/finance/bank-accounts/${encodeURIComponent(remarkTarget.id)}/remark`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ remark: values.remark ?? '' }),
|
||||
});
|
||||
message.success('备注已保存');
|
||||
setRemarkTarget(null);
|
||||
await reload();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeOther(row: FinanceBankAccountDto) {
|
||||
try {
|
||||
await request(`/admin/finance/bank-accounts/other/${otherNumericId(row.id)}`, { method: 'DELETE' });
|
||||
message.success('已删除');
|
||||
await reload();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function exportFile(format: 'xlsx' | 'pdf') {
|
||||
setExporting(format);
|
||||
try {
|
||||
const qs = new URLSearchParams();
|
||||
qs.set('format', format);
|
||||
if (filters.type) qs.set('type', filters.type);
|
||||
if (filters.cityId) qs.set('cityId', filters.cityId);
|
||||
if (filters.keyword) qs.set('keyword', filters.keyword);
|
||||
const result = await request<ExportResult>(`/admin/finance/bank-accounts/export?${qs}`);
|
||||
downloadBase64File(result.contentBase64, result.filename, result.mimeType);
|
||||
message.success(`已导出 ${result.count} 条`);
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '导出失败');
|
||||
} finally {
|
||||
setExporting(null);
|
||||
}
|
||||
}
|
||||
|
||||
const typeOptions = useMemo(
|
||||
() => FINANCE_BANK_ACCOUNT_TYPES.map((value) => ({ value, label: FINANCE_BANK_ACCOUNT_TYPE_LABELS[value] })),
|
||||
[],
|
||||
);
|
||||
|
||||
const baseColumns: ColumnsType<FinanceBankAccountDto> = [
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
width: 88,
|
||||
render: (type: FinanceBankAccountType) => (
|
||||
<Tag color={TYPE_COLORS[type]}>{FINANCE_BANK_ACCOUNT_TYPE_LABELS[type]}</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '归属', dataIndex: 'ownerName', width: 180 },
|
||||
{ title: '城市', dataIndex: 'cityName', width: 100, render: (v?: string | null) => v || '—' },
|
||||
{ title: '户名', dataIndex: 'bankAccountName', width: 140 },
|
||||
{ title: '银行账号', dataIndex: 'bankAccountNo', width: 180 },
|
||||
{ title: '开户行', dataIndex: 'bankBranch', width: 180, render: (v?: string | null) => v || '—' },
|
||||
{
|
||||
title: '默认',
|
||||
dataIndex: 'isDefault',
|
||||
width: 72,
|
||||
render: (v: boolean | undefined, row) => (row.type === 'STORE' ? (v ? '是' : '否') : '—'),
|
||||
},
|
||||
{ title: '备注', dataIndex: 'remark', width: 200, render: (v?: string | null) => v || '—' },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
render: (_, row) => (
|
||||
<Space size={0}>
|
||||
<Button type="link" size="small" onClick={() => openRemark(row)}>
|
||||
备注
|
||||
</Button>
|
||||
{row.editable ? (
|
||||
<>
|
||||
<Button type="link" size="small" onClick={() => openEdit(row)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={`确认删除账户「${row.ownerName}」?`}
|
||||
okText="删除"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => void removeOther(row)}
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const { columns, settingsButton, settingsModal } = useAdminListColumns('finance-bank-accounts', baseColumns, {
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{settingsModal}
|
||||
<AdminListHeader
|
||||
title="银行账户"
|
||||
description="汇总门店、酒厂、合伙人、物流的有效收款账户;可登记不挂门店的其他账户。打款仍以各业务源账户为准。"
|
||||
settings={settingsButton}
|
||||
actions={
|
||||
<Button type="primary" onClick={openCreate}>
|
||||
新增账户
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
<Form
|
||||
form={filterForm}
|
||||
layout="inline"
|
||||
style={{ marginBottom: 16 }}
|
||||
onFinish={(v) => {
|
||||
setFilters({
|
||||
type: v.type || '',
|
||||
cityId: v.cityId || '',
|
||||
keyword: v.keyword?.trim() || '',
|
||||
});
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
<Form.Item name="type" label="类型">
|
||||
<Select allowClear style={{ width: 140 }} options={typeOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="cityId" label="城市">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
style={{ width: 160 }}
|
||||
options={cities.map((c) => ({ value: c.id, label: c.name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="keyword" label="查找">
|
||||
<Input allowClear style={{ width: 220 }} placeholder="户名 / 账号 / 开户行 / 归属 / 备注" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
查询
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
filterForm.resetFields();
|
||||
setFilters({ type: '', cityId: '', keyword: '' });
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button loading={exporting === 'xlsx'} onClick={() => void exportFile('xlsx')}>
|
||||
导出 Excel
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button loading={exporting === 'pdf'} onClick={() => void exportFile('pdf')}>
|
||||
导出 PDF
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
className="admin-table-nowrap"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
scroll={{ x: 'max-content' }}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total: data?.total ?? 0,
|
||||
showSizeChanger: true,
|
||||
onChange: (p, ps) => {
|
||||
setPage(p);
|
||||
setPageSize(ps);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editing ? '编辑账户' : '新增账户'}
|
||||
open={otherOpen}
|
||||
onCancel={() => setOtherOpen(false)}
|
||||
onOk={() => void saveOther()}
|
||||
confirmLoading={saving}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={otherForm} layout="vertical">
|
||||
<Form.Item name="name" label="归属名称">
|
||||
<Input placeholder="选填,如供应商或内部账户名称" maxLength={128} />
|
||||
</Form.Item>
|
||||
<Form.Item name="bankAccountName" label="户名" rules={[{ required: true, message: '请填写户名' }]}>
|
||||
<Input maxLength={64} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="bankAccountNo"
|
||||
label="银行账号"
|
||||
rules={[
|
||||
{ required: true, message: '请填写银行账号' },
|
||||
{ pattern: /^\d{8,32}$/, message: '银行账号须为 8~32 位数字' },
|
||||
]}
|
||||
>
|
||||
<Input maxLength={32} />
|
||||
</Form.Item>
|
||||
<Form.Item name="bankBranch" label="开户行">
|
||||
<Input maxLength={128} />
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={3} maxLength={256} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="编辑备注"
|
||||
open={!!remarkTarget}
|
||||
onCancel={() => setRemarkTarget(null)}
|
||||
onOk={() => void saveRemark()}
|
||||
confirmLoading={saving}
|
||||
destroyOnClose
|
||||
>
|
||||
<Form form={remarkForm} layout="vertical">
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={4} maxLength={256} placeholder="仅财务目录可见,不改写来源账户" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -246,7 +246,7 @@ export default function HqPermissionsPage() {
|
||||
按角色配置基础权限;按用户可追加或撤销。最终生效权限 =(角色权限 ∪ 追加)− 撤销。
|
||||
超级管理员默认拥有除「危险操作」外的全部权限;删除用户/订单/城市、修改用户关联合伙人需在「按用户分配」或「按角色分配」中单独勾选(默认均无)。
|
||||
运营/财务默认可删除门店分类;城市门店服务可新增分类,不可删除。
|
||||
「系统设置」已拆分为各配置分组;「财务」对应门店/合伙人/酒厂账单。
|
||||
「系统设置」已拆分为各配置分组;「财务」对应门店/合伙人/酒厂账单、物流对账与银行账户。
|
||||
</Typography.Paragraph>
|
||||
|
||||
<Tabs
|
||||
|
||||
@@ -3,6 +3,13 @@ import {
|
||||
Button, Checkbox, Descriptions, Drawer, Form, Input, Modal, Popconfirm, Select, Space, Table, Tag, Typography, message,
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import {
|
||||
STORE_STAFF_DEFAULT_PERMISSIONS,
|
||||
STORE_STAFF_PERMISSION_LABELS,
|
||||
STORE_STAFF_ROLE_LABELS,
|
||||
StoreStaffRole,
|
||||
type AdminStoreStaffItem,
|
||||
} from '@dukang/shared-types';
|
||||
import { request, type Paginated } from '../lib/api';
|
||||
import { ACCOUNT_STATUS_LABELS, ADMIN_OPTIONS_PAGE_SIZE, STORE_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||||
import { useAdminList } from '../lib/useAdminList';
|
||||
@@ -10,7 +17,6 @@ import { useAdminListColumns } from '../lib/useAdminListColumns';
|
||||
import { AdminListHeader } from '../components/AdminListHeader';
|
||||
import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
|
||||
|
||||
|
||||
type StoreBrief = { id: string; name: string; status: string; cityName?: string };
|
||||
|
||||
type Row = {
|
||||
@@ -27,14 +33,22 @@ type Row = {
|
||||
bankBranch?: string | null;
|
||||
store?: StoreBrief | null;
|
||||
stores?: StoreBrief[];
|
||||
staff?: Array<{ id: string; name: string; phone: string; status: string; storeIds?: string[] }>;
|
||||
staff?: AdminStoreStaffItem[];
|
||||
};
|
||||
|
||||
type StoreOption = { id: string; name: string };
|
||||
|
||||
const STAFF_ROLE_OPTIONS = Object.entries(STORE_STAFF_ROLE_LABELS).map(([value, label]) => ({ value, label }));
|
||||
const STAFF_PERMISSION_OPTIONS = STORE_STAFF_DEFAULT_PERMISSIONS.map((value) => ({
|
||||
value,
|
||||
label: STORE_STAFF_PERMISSION_LABELS[value],
|
||||
}));
|
||||
|
||||
export default function StoreAccountsPage() {
|
||||
const [form] = Form.useForm();
|
||||
const [createForm] = Form.useForm();
|
||||
const [staffForm] = Form.useForm();
|
||||
const [staffEditForm] = Form.useForm();
|
||||
const [filters, setFilters] = useState<Record<string, string | boolean>>({});
|
||||
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
||||
'/admin/store-accounts',
|
||||
@@ -50,9 +64,14 @@ export default function StoreAccountsPage() {
|
||||
const [detail, setDetail] = useState<Row | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [staffOpen, setStaffOpen] = useState(false);
|
||||
const [staffEditOpen, setStaffEditOpen] = useState(false);
|
||||
const [editingStaffId, setEditingStaffId] = useState<string | null>(null);
|
||||
const [stores, setStores] = useState<StoreOption[]>([]);
|
||||
const [deletingStaffId, setDeletingStaffId] = useState<string | null>(null);
|
||||
|
||||
const parentStoreOptions = (detail?.stores ?? []).map((s) => ({ value: s.id, label: s.name }));
|
||||
|
||||
async function loadStores() {
|
||||
const res = await request<Paginated<StoreOption>>(`/admin/stores?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`);
|
||||
setStores(res.items);
|
||||
@@ -64,6 +83,34 @@ export default function StoreAccountsPage() {
|
||||
void reload();
|
||||
}
|
||||
|
||||
function openAddStaff() {
|
||||
if (!detail) return;
|
||||
if (!detail.stores?.length) {
|
||||
message.warning('请先为该主账号绑定门店');
|
||||
return;
|
||||
}
|
||||
staffForm.resetFields();
|
||||
staffForm.setFieldsValue({
|
||||
staffRole: StoreStaffRole.CASHIER,
|
||||
permissions: [...STORE_STAFF_DEFAULT_PERMISSIONS],
|
||||
storeIds: detail.stores.map((s) => s.id),
|
||||
});
|
||||
setStaffOpen(true);
|
||||
}
|
||||
|
||||
function openEditStaff(staff: AdminStoreStaffItem) {
|
||||
setEditingStaffId(staff.id);
|
||||
staffEditForm.setFieldsValue({
|
||||
name: staff.name,
|
||||
phone: staff.phone,
|
||||
staffRole: staff.staffRole ?? StoreStaffRole.CASHIER,
|
||||
status: staff.status,
|
||||
permissions: staff.permissions?.length ? staff.permissions : [...STORE_STAFF_DEFAULT_PERMISSIONS],
|
||||
storeIds: staff.storeIds?.length ? staff.storeIds : (staff.stores ?? []).map((s) => s.id),
|
||||
});
|
||||
setStaffEditOpen(true);
|
||||
}
|
||||
|
||||
async function deleteStaff(staffId: string) {
|
||||
if (!detail) return;
|
||||
setDeletingStaffId(staffId);
|
||||
@@ -151,13 +198,58 @@ export default function StoreAccountsPage() {
|
||||
|
||||
const { columns, settingsButton, settingsModal } = useAdminListColumns('store-accounts', baseColumns, { page, pageSize });
|
||||
|
||||
const staffColumns: ColumnsType<AdminStoreStaffItem> = [
|
||||
{ title: '姓名', dataIndex: 'name', width: 90 },
|
||||
{ title: '手机', dataIndex: 'phone', width: 120 },
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'staffRole',
|
||||
width: 80,
|
||||
render: (role) => STORE_STAFF_ROLE_LABELS[role as StoreStaffRole] || role || '—',
|
||||
},
|
||||
{
|
||||
title: '门店',
|
||||
render: (_, staff) =>
|
||||
staff.stores?.length ? staff.stores.map((s) => s.name).join('、') : '—',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 70,
|
||||
render: (s) => <Tag>{ACCOUNT_STATUS_LABELS[s] || s}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 120,
|
||||
render: (_, staff) => (
|
||||
<Space size={0}>
|
||||
<Button type="link" size="small" onClick={() => openEditStaff(staff)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确认删除该子账号?"
|
||||
description={`${staff.name}(${staff.phone})删除后将无法登录门店端`}
|
||||
okText="删除"
|
||||
okButtonProps={{ danger: true, loading: deletingStaffId === staff.id }}
|
||||
cancelText="取消"
|
||||
onConfirm={() => void deleteStaff(staff.id)}
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{settingsModal}
|
||||
<AdminListHeader
|
||||
title="门店账户"
|
||||
settings={settingsButton}
|
||||
description="主账号可绑定多家门店;收款信息挂在主账号;「新建账户」用于补录无主账号门店"
|
||||
description="主账号可绑定多家门店;收款信息挂在主账号;详情内可管理店员子账号"
|
||||
actions={
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -212,13 +304,13 @@ export default function StoreAccountsPage() {
|
||||
/>
|
||||
<Drawer
|
||||
title="门店主账号"
|
||||
width={520}
|
||||
width={640}
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
extra={
|
||||
detail && (
|
||||
<Select
|
||||
defaultValue={detail.status}
|
||||
value={detail.status}
|
||||
style={{ width: 100 }}
|
||||
options={Object.entries(ACCOUNT_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
|
||||
onChange={async (status) => {
|
||||
@@ -227,7 +319,7 @@ export default function StoreAccountsPage() {
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
message.success('已更新');
|
||||
void reload();
|
||||
await refreshDetail(detail.id);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@@ -251,48 +343,22 @@ export default function StoreAccountsPage() {
|
||||
{!detail.stores?.length ? '—' : null}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{detail.staff?.length ? (
|
||||
<>
|
||||
<Typography.Title level={5} style={{ marginTop: 24 }}>子账号</Typography.Title>
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
dataSource={detail.staff}
|
||||
columns={[
|
||||
{ title: '姓名', dataIndex: 'name' },
|
||||
{ title: '手机', dataIndex: 'phone' },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
render: (s) => <Tag>{ACCOUNT_STATUS_LABELS[s] || s}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
render: (_, staff) => (
|
||||
<Popconfirm
|
||||
title="确认删除该子账号?"
|
||||
description={`${staff.name}(${staff.phone})删除后将无法登录门店端`}
|
||||
okText="删除"
|
||||
okButtonProps={{ danger: true, loading: deletingStaffId === staff.id }}
|
||||
cancelText="取消"
|
||||
onConfirm={() => void deleteStaff(staff.id)}
|
||||
>
|
||||
<Button type="link" size="small" danger>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Typography.Paragraph type="secondary" style={{ marginTop: 24, marginBottom: 0 }}>
|
||||
暂无子账号
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 24, marginBottom: 8 }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
子账号({detail.staff?.length ?? 0})· 仅主账号可添加,不可多级
|
||||
</Typography.Text>
|
||||
<Button size="small" type="primary" onClick={openAddStaff}>
|
||||
添加子账号
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={false}
|
||||
dataSource={detail.staff ?? []}
|
||||
columns={staffColumns}
|
||||
locale={{ emptyText: '暂无子账号' }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
@@ -321,6 +387,87 @@ export default function StoreAccountsPage() {
|
||||
<Form.Item name="phone" label="手机" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={detail ? `添加子账号 · ${detail.name}` : '添加子账号'}
|
||||
open={staffOpen}
|
||||
onCancel={() => setStaffOpen(false)}
|
||||
onOk={async () => {
|
||||
if (!detail) return;
|
||||
const v = await staffForm.validateFields();
|
||||
await request(`/admin/store-accounts/${detail.id}/staff`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(v),
|
||||
});
|
||||
message.success('子账号已创建');
|
||||
setStaffOpen(false);
|
||||
await refreshDetail(detail.id);
|
||||
}}
|
||||
>
|
||||
<Form form={staffForm} layout="vertical">
|
||||
<Form.Item name="name" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item
|
||||
name="phone"
|
||||
label="登录手机"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' },
|
||||
]}
|
||||
>
|
||||
<Input maxLength={11} />
|
||||
</Form.Item>
|
||||
<Form.Item name="staffRole" label="角色" rules={[{ required: true }]}>
|
||||
<Select options={STAFF_ROLE_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item name="storeIds" label="可管门店" rules={[{ required: true, type: 'array', min: 1, message: '请至少绑定一家门店' }]}>
|
||||
<Select mode="multiple" options={parentStoreOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="permissions" label="权限">
|
||||
<Checkbox.Group options={STAFF_PERMISSION_OPTIONS} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Modal
|
||||
title="编辑子账号"
|
||||
open={staffEditOpen}
|
||||
onCancel={() => setStaffEditOpen(false)}
|
||||
onOk={async () => {
|
||||
if (!detail || !editingStaffId) return;
|
||||
const v = await staffEditForm.validateFields();
|
||||
await request(`/admin/store-accounts/${detail.id}/staff/${editingStaffId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(v),
|
||||
});
|
||||
message.success('子账号已更新');
|
||||
setStaffEditOpen(false);
|
||||
await refreshDetail(detail.id);
|
||||
}}
|
||||
>
|
||||
<Form form={staffEditForm} layout="vertical">
|
||||
<Form.Item name="name" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
|
||||
<Form.Item
|
||||
name="phone"
|
||||
label="登录手机"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' },
|
||||
]}
|
||||
>
|
||||
<Input maxLength={11} />
|
||||
</Form.Item>
|
||||
<Form.Item name="staffRole" label="角色" rules={[{ required: true }]}>
|
||||
<Select options={STAFF_ROLE_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item name="status" label="状态" rules={[{ required: true }]}>
|
||||
<Select options={Object.entries(ACCOUNT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="storeIds" label="可管门店" rules={[{ required: true, type: 'array', min: 1, message: '请至少绑定一家门店' }]}>
|
||||
<Select mode="multiple" options={parentStoreOptions} />
|
||||
</Form.Item>
|
||||
<Form.Item name="permissions" label="权限">
|
||||
<Checkbox.Group options={STAFF_PERMISSION_OPTIONS} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user