merge main
This commit is contained in:
Vendored
+7
-7
File diff suppressed because one or more lines are too long
Vendored
+36
-36
@@ -1,37 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>后台管理</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName + ' - 管理后台';
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-OStxKvM2.js"></script>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>后台管理</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName + ' - 管理后台';
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-BaN8_2WC.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -181,6 +181,19 @@ export async function deleteAdminTeam(id: string): Promise<void> {
|
||||
await api.delete(`/admin/teams/${id}`);
|
||||
}
|
||||
|
||||
export async function setTeamManager(teamId: string, userId: string | null): Promise<AdminTeam> {
|
||||
return api.put(`/admin/teams/${teamId}/manager`, { user_id: userId });
|
||||
}
|
||||
|
||||
export async function getTeamMembersForAdmin(teamId: string, page = 1, pageSize = 50): Promise<{ items: AdminUser[]; total: number }> {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', String(page));
|
||||
params.set('page_size', String(pageSize));
|
||||
params.set('user_type', 'frontend');
|
||||
params.set('team_id', teamId);
|
||||
return api.get(`/admin/users?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function adjustCredits(userId: string, amount: number, description: string): Promise<void> {
|
||||
await api.post(`/admin/users/${userId}/credits`, { amount, description });
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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, TeamOutlined } from '@ant-design/icons';
|
||||
import { deleteAdminTeam, getAdminTeams, saveAdminTeam } from '../api';
|
||||
import type { AdminTeam } from '../types';
|
||||
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 = [
|
||||
@@ -34,6 +34,17 @@ const AdminTeams: React.FC = () => {
|
||||
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,
|
||||
@@ -107,6 +118,48 @@ const AdminTeams: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
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: '团队名称',
|
||||
@@ -136,6 +189,16 @@ const AdminTeams: React.FC = () => {
|
||||
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',
|
||||
@@ -157,10 +220,12 @@ const AdminTeams: React.FC = () => {
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 160,
|
||||
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="确定删除该团队?"
|
||||
@@ -239,6 +304,73 @@ const AdminTeams: React.FC = () => {
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -147,6 +147,8 @@ export interface AdminTeam {
|
||||
memberCount: number;
|
||||
createdAt: string;
|
||||
updatedAt?: string | null;
|
||||
managerId?: string | null;
|
||||
managerName?: string | null;
|
||||
}
|
||||
|
||||
export interface AdminTeamOption {
|
||||
|
||||
Reference in New Issue
Block a user