226 lines
6.1 KiB
TypeScript
226 lines
6.1 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
||
import {
|
||
Button,
|
||
Form,
|
||
message,
|
||
Modal,
|
||
Popconfirm,
|
||
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 } from '@/domain/iMiniUser.ts';
|
||
import type IStore from '@/domain/iStore.ts';
|
||
import { getStoreOptions } from '@/api/customer/store.ts';
|
||
import { bindMiniUser, toggleMiniUserStatus } from '@/api/customer/miniUser.ts';
|
||
import AuthButton from '@/components/AuthButton';
|
||
|
||
const { Title, Text } = Typography;
|
||
|
||
interface BindFormValues {
|
||
store_id: number;
|
||
}
|
||
|
||
/**
|
||
* 小程序用户管理(用户由小程序登录自动生成,后台只做绑定与状态管理)
|
||
*/
|
||
const MiniUserPage: React.FC = () => {
|
||
const tableRef = useRef<XinTableInstance<IMiniUser>>(null);
|
||
const [stores, setStores] = useState<IStore[]>([]);
|
||
|
||
// 绑定弹窗
|
||
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 ?? []));
|
||
}, []);
|
||
|
||
const openBind = (record: IMiniUser) => {
|
||
setBindTarget(record);
|
||
bindForm.setFieldsValue({
|
||
store_id: record.store_id || undefined,
|
||
});
|
||
setBindOpen(true);
|
||
};
|
||
|
||
const handleBind = async (values: BindFormValues) => {
|
||
if (!bindTarget?.id) {
|
||
return;
|
||
}
|
||
setBindLoading(true);
|
||
try {
|
||
await bindMiniUser(bindTarget.id, {
|
||
store_id: values.store_id,
|
||
});
|
||
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: 'store_id',
|
||
hideInSearch: true,
|
||
render: (_, record) => {
|
||
return <Tag color="blue">{record.store?.name ?? `门店#${record.store_id}`}</Tag>;
|
||
},
|
||
},
|
||
{
|
||
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>,
|
||
},
|
||
{
|
||
title: '注册时间',
|
||
dataIndex: 'created_at',
|
||
hideInSearch: true,
|
||
align: 'center',
|
||
},
|
||
];
|
||
|
||
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="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>
|
||
</Form>
|
||
</Modal>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default MiniUserPage;
|