first version

This commit is contained in:
liu
2026-07-23 20:41:25 +08:00
parent 00a0938a1b
commit 10cff754a4
211 changed files with 11577 additions and 842 deletions
+294
View File
@@ -0,0 +1,294 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Button,
Form,
message,
Modal,
Popconfirm,
Radio,
Select,
Space,
Tag,
Typography,
} from 'antd';
import XinTable from '@/components/XinTable';
import type {
XinTableColumn,
XinTableInstance,
XinTableProps,
} from '@/components/XinTable/typings.ts';
import type IMiniUser from '@/domain/iMiniUser.ts';
import { MINI_USER_STATUS_MAP, MINI_USER_TYPE_MAP } from '@/domain/iMiniUser.ts';
import type IStore from '@/domain/iStore.ts';
import type ISupplier from '@/domain/iSupplier.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
import { getSupplierOptions } from '@/api/customer/supplier.ts';
import { bindMiniUser, toggleMiniUserStatus } from '@/api/customer/miniUser.ts';
import AuthButton from '@/components/AuthButton';
const { Title, Text } = Typography;
interface BindFormValues {
type: number;
store_id?: number;
supplier_id?: number;
}
/**
* 小程序用户管理(用户由小程序登录自动生成,后台只做绑定与状态管理)
*/
const MiniUserPage: React.FC = () => {
const tableRef = useRef<XinTableInstance<IMiniUser>>(null);
const [stores, setStores] = useState<IStore[]>([]);
const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
// 绑定弹窗
const [bindOpen, setBindOpen] = useState(false);
const [bindTarget, setBindTarget] = useState<IMiniUser | null>(null);
const [bindLoading, setBindLoading] = useState(false);
const [bindForm] = Form.useForm<BindFormValues>();
useEffect(() => {
getStoreOptions().then((res) => setStores(res.data.data ?? []));
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
}, []);
const openBind = (record: IMiniUser) => {
setBindTarget(record);
bindForm.setFieldsValue({
type: record.type && record.type > 0 ? record.type : undefined,
store_id: record.store_id || undefined,
supplier_id: record.supplier_id || undefined,
});
setBindOpen(true);
};
const handleBind = async (values: BindFormValues) => {
if (!bindTarget?.id) {
return;
}
setBindLoading(true);
try {
await bindMiniUser(bindTarget.id, {
type: values.type,
store_id: values.type === 1 ? values.store_id : undefined,
supplier_id: values.type === 2 ? values.supplier_id : undefined,
});
message.success('绑定成功');
setBindOpen(false);
await tableRef.current?.reload();
} finally {
setBindLoading(false);
}
};
const handleToggleStatus = async (record: IMiniUser) => {
await toggleMiniUserStatus(record.id!, record.status === 1 ? 0 : 1);
message.success(record.status === 1 ? '已停用' : '已启用');
await tableRef.current?.reload();
};
const columns: XinTableColumn<IMiniUser>[] = [
{
title: 'ID',
dataIndex: 'id',
hideInSearch: true,
width: 70,
align: 'center',
},
{
title: '昵称',
dataIndex: 'nickname',
valueType: 'text',
hideInSearch: true,
render: (_, record) => (
<Space size={8}>
{record.avatar ? (
<img src={record.avatar} alt="" className="h-6 w-6 rounded-full" />
) : null}
<span>{record.nickname || '-'}</span>
</Space>
),
},
{
title: '手机号',
dataIndex: 'phone',
valueType: 'text',
hideInSearch: true,
render: (_, record) => record.phone || <Text type="secondary"></Text>,
},
{
title: '用户类型',
dataIndex: 'type',
valueType: 'select',
fieldProps: {
options: [
{ value: 0, label: '待绑定' },
{ value: 1, label: '门店' },
{ value: 2, label: '供应商' },
],
},
render: (_, record) => {
const item = MINI_USER_TYPE_MAP[record.type ?? 0];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
align: 'center',
},
{
title: '绑定主体',
dataIndex: 'bound_name',
hideInSearch: true,
render: (_, record) => {
if (record.type === 1) {
return <Tag color="blue">{record.store?.name ?? `门店#${record.store_id}`}</Tag>;
}
if (record.type === 2) {
return (
<Tag color="purple">{record.supplier?.name ?? `供应商#${record.supplier_id}`}</Tag>
);
}
return <Text type="secondary"></Text>;
},
},
{
title: '状态',
dataIndex: 'status',
valueType: 'select',
fieldProps: {
options: [
{ value: 1, label: '正常' },
{ value: 0, label: '停用' },
],
},
render: (_, record) => {
const item = MINI_USER_STATUS_MAP[record.status ?? 1];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
align: 'center',
},
{
title: '最后登录',
dataIndex: 'last_login_at',
hideInSearch: true,
align: 'center',
render: (_, record) => record.last_login_at ?? <Text type="secondary"></Text>,
},
];
const operateRender: XinTableProps<IMiniUser>['operateRender'] = (record) => [
<AuthButton key="bind" auth="customer.miniUser.bind">
<Button size="small" color="blue" variant="outlined" onClick={() => openBind(record)}>
</Button>
</AuthButton>,
<AuthButton key="status" auth="customer.miniUser.update">
<Popconfirm
title={record.status === 1 ? '确定停用该账号?' : '确定启用该账号?'}
description={record.status === 1 ? '停用后该用户将无法登录小程序' : undefined}
onConfirm={() => handleToggleStatus(record)}
>
<Button size="small" danger={record.status === 1}>
{record.status === 1 ? '停用' : '启用'}
</Button>
</Popconfirm>
</AuthButton>,
];
const tableProps: XinTableProps<IMiniUser> = {
api: '/customer/miniUser',
columns,
rowKey: 'id',
accessName: 'customer.miniUser',
tableRef,
operateRender,
// 无新增/编辑表单
formProps: false,
};
return (
<>
<div className="mb-5">
<Title level={3}></Title>
<Text type="secondary">
/
</Text>
</div>
<XinTable<IMiniUser> {...tableProps} />
<Modal
title={`绑定主体 · ${bindTarget?.nickname || ''}`}
open={bindOpen}
onCancel={() => setBindOpen(false)}
onOk={() => bindForm.submit()}
confirmLoading={bindLoading}
okText="确认绑定"
destroyOnHidden
>
<Form<BindFormValues>
form={bindForm}
layout="vertical"
onFinish={handleBind}
className="mt-4"
>
<Form.Item
label="用户类型"
name="type"
rules={[{ required: true, message: '请选择用户类型' }]}
>
<Radio.Group
options={[
{ label: '门店', value: 1 },
{ label: '供应商', value: 2 },
]}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.type !== cur.type}>
{({ getFieldValue }) => {
const type = getFieldValue('type');
if (type === 1) {
return (
<Form.Item
label="绑定门店"
name="store_id"
rules={[{ required: true, message: '请选择门店' }]}
>
<Select
showSearch
optionFilterProp="label"
placeholder="选择门店"
options={stores.map((s) => ({
label: `${s.name}${s.code}`,
value: s.id,
}))}
/>
</Form.Item>
);
}
if (type === 2) {
return (
<Form.Item
label="绑定供应商"
name="supplier_id"
rules={[{ required: true, message: '请选择供应商' }]}
>
<Select
showSearch
optionFilterProp="label"
placeholder="选择供应商"
options={suppliers.map((s) => ({ label: s.name, value: s.id }))}
/>
</Form.Item>
);
}
return null;
}}
</Form.Item>
</Form>
</Modal>
</>
);
};
export default MiniUserPage;