379 lines
14 KiB
TypeScript
379 lines
14 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
|
import { Button, Card, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd';
|
|
import { DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, SettingOutlined, TeamOutlined, UserOutlined } from '@ant-design/icons';
|
|
import { deleteAdminTeam, getAdminTeams, getTeamMembersForAdmin, saveAdminTeam, setTeamManager } from '../api';
|
|
import type { AdminTeam, AdminUser } from '../types';
|
|
import { formatDate } from '../utils/formatDate';
|
|
|
|
const statusOptions = [
|
|
{ value: '', label: '全部状态' },
|
|
{ value: 'active', label: '启用' },
|
|
{ value: 'disabled', label: '禁用' },
|
|
];
|
|
|
|
const editStatusOptions = [
|
|
{ value: 'active', label: '启用' },
|
|
{ value: 'disabled', label: '禁用' },
|
|
];
|
|
|
|
function statusLabel(status?: string): string {
|
|
if (status === 'active') return '启用';
|
|
if (status === 'disabled') return '禁用';
|
|
return status || '-';
|
|
}
|
|
|
|
const AdminTeams: React.FC = () => {
|
|
const [items, setItems] = useState<AdminTeam[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [saving, setSaving] = useState(false);
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(20);
|
|
const [total, setTotal] = useState(0);
|
|
const [keyword, setKeyword] = useState('');
|
|
const [status, setStatus] = useState('');
|
|
const [modal, setModal] = useState<{ open: boolean; item: AdminTeam | null }>({ open: false, item: null });
|
|
const [form] = Form.useForm();
|
|
|
|
// 管理人弹窗状态
|
|
const [managerModal, setManagerModal] = useState<{ open: boolean; team: AdminTeam | null }>({ open: false, team: null });
|
|
const [managerMembers, setManagerMembers] = useState<AdminUser[]>([]);
|
|
const [managerLoading, setManagerLoading] = useState(false);
|
|
const [selectedManagerId, setSelectedManagerId] = useState<string | null>(null);
|
|
|
|
// 查看成员弹窗状态
|
|
const [membersModal, setMembersModal] = useState<{ open: boolean; team: AdminTeam | null }>({ open: false, team: null });
|
|
const [teamMembers, setTeamMembers] = useState<AdminUser[]>([]);
|
|
const [membersLoading, setMembersLoading] = useState(false);
|
|
|
|
const query = useMemo(() => ({
|
|
page,
|
|
pageSize,
|
|
keyword: keyword || undefined,
|
|
status: status || undefined,
|
|
}), [page, pageSize, keyword, status]);
|
|
|
|
const load = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await getAdminTeams(query);
|
|
setItems(res.items || []);
|
|
setTotal(res.total || 0);
|
|
} catch (e: any) {
|
|
message.error(e?.message || '加载团队列表失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => { load(); }, [query]);
|
|
|
|
const openCreate = () => {
|
|
form.resetFields();
|
|
form.setFieldsValue({ status: 'active', sortOrder: 0 });
|
|
setModal({ open: true, item: null });
|
|
};
|
|
|
|
const openEdit = (item: AdminTeam) => {
|
|
form.setFieldsValue({
|
|
name: item.name,
|
|
code: item.code || '',
|
|
description: item.description || '',
|
|
status: item.status || 'active',
|
|
sortOrder: item.sortOrder || 0,
|
|
});
|
|
setModal({ open: true, item });
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
const values = await form.validateFields();
|
|
setSaving(true);
|
|
await saveAdminTeam({
|
|
id: modal.item?.id,
|
|
name: values.name,
|
|
code: values.code || null,
|
|
description: values.description || null,
|
|
status: values.status || 'active',
|
|
sort_order: values.sortOrder || 0,
|
|
});
|
|
message.success(modal.item ? '团队已更新' : '团队已创建');
|
|
setModal({ open: false, item: null });
|
|
form.resetFields();
|
|
load();
|
|
} catch (e: any) {
|
|
if (e?.errorFields) return;
|
|
message.error(e?.message || '保存失败');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (item: AdminTeam) => {
|
|
try {
|
|
await deleteAdminTeam(item.id);
|
|
message.success('团队已删除');
|
|
load();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '删除失败');
|
|
}
|
|
};
|
|
|
|
const loadTeamMembers = async (teamId: string): Promise<AdminUser[]> => {
|
|
try {
|
|
const res = await getTeamMembersForAdmin(teamId);
|
|
return res.items || [];
|
|
} catch (e: any) {
|
|
message.error(e?.message || '加载成员失败');
|
|
return [];
|
|
}
|
|
};
|
|
|
|
const openManagerModal = async (team: AdminTeam) => {
|
|
setManagerModal({ open: true, team });
|
|
setSelectedManagerId(team.managerId || null);
|
|
setManagerLoading(true);
|
|
const data = await loadTeamMembers(team.id);
|
|
setManagerMembers(data);
|
|
setManagerLoading(false);
|
|
};
|
|
|
|
const openMembersModal = async (team: AdminTeam) => {
|
|
setMembersModal({ open: true, team });
|
|
setMembersLoading(true);
|
|
const data = await loadTeamMembers(team.id);
|
|
setTeamMembers(data);
|
|
setMembersLoading(false);
|
|
};
|
|
|
|
const handleSetManager = async () => {
|
|
if (!managerModal.team) return;
|
|
try {
|
|
setManagerLoading(true);
|
|
await setTeamManager(managerModal.team.id, selectedManagerId);
|
|
message.success(selectedManagerId ? '已设置管理人' : '已取消管理人');
|
|
setManagerModal({ open: false, team: null });
|
|
load();
|
|
} catch (e: any) {
|
|
message.error(e?.message || '设置失败');
|
|
} finally {
|
|
setManagerLoading(false);
|
|
}
|
|
};
|
|
|
|
const columns = [
|
|
{
|
|
title: '团队名称',
|
|
dataIndex: 'name',
|
|
width: 220,
|
|
render: (v: string, r: AdminTeam) => (
|
|
<Space>
|
|
<div style={{ width: 32, height: 32, borderRadius: 8, background: 'rgba(99,102,241,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#6366f1' }}>
|
|
<TeamOutlined />
|
|
</div>
|
|
<div>
|
|
<Typography.Text strong>{v}</Typography.Text>
|
|
<div style={{ fontSize: 12, color: '#94a3b8' }}>{r.code || '-'}</div>
|
|
</div>
|
|
</Space>
|
|
),
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
width: 100,
|
|
render: (v: string) => <Tag color={v === 'active' ? 'green' : 'default'}>{statusLabel(v)}</Tag>,
|
|
},
|
|
{
|
|
title: '成员数',
|
|
dataIndex: 'memberCount',
|
|
width: 100,
|
|
render: (v: number) => <Typography.Text strong>{Number(v || 0).toLocaleString()}</Typography.Text>,
|
|
},
|
|
{
|
|
title: '管理人',
|
|
key: 'manager',
|
|
width: 140,
|
|
render: (_: any, r: AdminTeam) => (
|
|
<Typography.Text style={{ fontSize: 13 }}>
|
|
{r.managerName || <span style={{ color: '#94a3b8' }}>未设置</span>}
|
|
</Typography.Text>
|
|
),
|
|
},
|
|
{
|
|
title: '排序',
|
|
dataIndex: 'sortOrder',
|
|
width: 90,
|
|
render: (v: number) => v ?? 0,
|
|
},
|
|
{
|
|
title: '备注',
|
|
dataIndex: 'description',
|
|
ellipsis: true,
|
|
render: (v: string) => v || '-',
|
|
},
|
|
{
|
|
title: '创建时间',
|
|
dataIndex: 'createdAt',
|
|
width: 160,
|
|
render: (v: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{formatDate(v)}</Typography.Text>,
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'action',
|
|
width: 230,
|
|
fixed: 'right' as const,
|
|
render: (_: any, r: AdminTeam) => (
|
|
<Space size={4}>
|
|
<Button type="link" size="small" icon={<UserOutlined />} onClick={() => openMembersModal(r)}>成员</Button>
|
|
<Button type="link" size="small" icon={<SettingOutlined />} onClick={() => openManagerModal(r)}>管理人</Button>
|
|
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openEdit(r)}>编辑</Button>
|
|
<Popconfirm
|
|
title="确定删除该团队?"
|
|
description={r.memberCount > 0 ? '该团队下仍有成员,后端会拒绝删除。' : '删除后团队不再出现在设置下拉中。'}
|
|
onConfirm={() => handleDelete(r)}
|
|
>
|
|
<Button type="link" size="small" danger icon={<DeleteOutlined />}>删除</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 12 }}>
|
|
<Space wrap>
|
|
<Input
|
|
placeholder="搜索团队名称/编码/备注"
|
|
prefix={<SearchOutlined style={{ color: '#94a3b8' }} />}
|
|
value={keyword}
|
|
onChange={(e) => { setPage(1); setKeyword(e.target.value); }}
|
|
onPressEnter={load}
|
|
style={{ width: 260 }}
|
|
allowClear
|
|
/>
|
|
<Select value={status} onChange={(v) => { setPage(1); setStatus(v); }} style={{ width: 130 }} options={statusOptions} />
|
|
<Button icon={<ReloadOutlined />} onClick={load}>刷新</Button>
|
|
</Space>
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>新增团队</Button>
|
|
</div>
|
|
|
|
<Table
|
|
columns={columns}
|
|
dataSource={items}
|
|
rowKey="id"
|
|
loading={loading}
|
|
pagination={{
|
|
current: page,
|
|
pageSize,
|
|
total,
|
|
onChange: (p, ps) => { setPage(p); setPageSize(ps); },
|
|
showSizeChanger: true,
|
|
showTotal: (t) => `共 ${t} 个团队`,
|
|
}}
|
|
scroll={{ x: 1100 }}
|
|
/>
|
|
</Card>
|
|
|
|
<Modal
|
|
title={<Space><TeamOutlined />{modal.item ? '编辑团队' : '新增团队'}</Space>}
|
|
open={modal.open}
|
|
confirmLoading={saving}
|
|
onOk={handleSave}
|
|
onCancel={() => { setModal({ open: false, item: null }); form.resetFields(); }}
|
|
okText="保存"
|
|
cancelText="取消"
|
|
width={520}
|
|
>
|
|
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
|
<Form.Item name="name" label="团队名称" rules={[{ required: true, message: '请输入团队名称' }]}>
|
|
<Input maxLength={128} placeholder="请输入团队名称" size="large" />
|
|
</Form.Item>
|
|
<Form.Item name="code" label="团队编码" extra="选填,用于内部标识或后续外部系统对接。">
|
|
<Input maxLength={64} placeholder="例如 sales_a" size="large" />
|
|
</Form.Item>
|
|
<Form.Item name="status" label="状态" rules={[{ required: true, message: '请选择状态' }]}>
|
|
<Select size="large" options={editStatusOptions} />
|
|
</Form.Item>
|
|
<Form.Item name="sortOrder" label="排序" initialValue={0}>
|
|
<InputNumber min={0} max={999999} precision={0} style={{ width: '100%' }} size="large" />
|
|
</Form.Item>
|
|
<Form.Item name="description" label="备注">
|
|
<Input.TextArea rows={3} maxLength={512} placeholder="选填" />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
|
|
{/* 设置管理人弹窗 */}
|
|
<Modal
|
|
title={<Space><UserOutlined />设置团队管理人</Space>}
|
|
open={managerModal.open}
|
|
confirmLoading={managerLoading}
|
|
onOk={handleSetManager}
|
|
onCancel={() => { setManagerModal({ open: false, team: null }); setSelectedManagerId(null); }}
|
|
okText="保存"
|
|
cancelText="取消"
|
|
width={480}
|
|
>
|
|
<div style={{ marginTop: 16 }}>
|
|
<Typography.Text style={{ fontSize: 13, color: '#64748b', display: 'block', marginBottom: 8 }}>
|
|
团队:{managerModal.team?.name || '-'}
|
|
</Typography.Text>
|
|
<Typography.Text style={{ fontSize: 13, color: '#64748b', display: 'block', marginBottom: 12 }}>
|
|
选择该团队的一名成员作为管理人。管理人可在前台管理团队成员和分配积分。
|
|
</Typography.Text>
|
|
<Select
|
|
style={{ width: '100%' }}
|
|
placeholder="选择管理人(可清空取消)"
|
|
value={selectedManagerId}
|
|
onChange={(v) => setSelectedManagerId(v || null)}
|
|
allowClear
|
|
loading={managerLoading}
|
|
optionFilterProp="label"
|
|
options={managerMembers.map((m) => ({
|
|
value: m.id,
|
|
label: `${m.username}${m.phone ? ` (${m.phone})` : ''}`,
|
|
}))}
|
|
showSearch
|
|
/>
|
|
</div>
|
|
</Modal>
|
|
|
|
{/* 查看成员弹窗 */}
|
|
<Modal
|
|
title={<Space><UserOutlined />团队成员 - {membersModal.team?.name}</Space>}
|
|
open={membersModal.open}
|
|
onCancel={() => setMembersModal({ open: false, team: null })}
|
|
footer={null}
|
|
width={600}
|
|
>
|
|
<div style={{ marginTop: 8 }}>
|
|
{membersModal.team && (
|
|
<Table
|
|
size="small"
|
|
rowKey="id"
|
|
loading={membersLoading}
|
|
dataSource={teamMembers}
|
|
pagination={false}
|
|
scroll={{ y: 400 }}
|
|
columns={[
|
|
{ title: '用户名', dataIndex: 'username', width: 140, render: (v: string) => <Typography.Text strong>{v}</Typography.Text> },
|
|
{ title: '手机号', dataIndex: 'phone', width: 130, render: (v: string) => v || '-' },
|
|
{ title: '积分', dataIndex: 'credits', width: 100, render: (v: number) => <Typography.Text style={{ color: '#6366f1' }}>{(v ?? 0).toFixed(2)}</Typography.Text> },
|
|
{
|
|
title: '状态', dataIndex: 'isActive', width: 80,
|
|
render: (v: boolean) => <Tag color={v ? 'green' : 'red'}>{v ? '启用' : '禁用'}</Tag>,
|
|
},
|
|
]}
|
|
locale={{ emptyText: '该团队暂无成员' }}
|
|
/>
|
|
)}
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AdminTeams;
|