Files
xin-procurement/web/pages/customer/mini-user.tsx
T
2026-08-10 08:55:22 +08:00

226 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;