295 lines
8.8 KiB
TypeScript
295 lines
8.8 KiB
TypeScript
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;
|