401 lines
17 KiB
TypeScript
401 lines
17 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import {
|
||
CopyOutlined,
|
||
DeleteOutlined,
|
||
MailOutlined,
|
||
PlusOutlined,
|
||
TeamOutlined,
|
||
UserOutlined
|
||
} from '@ant-design/icons';
|
||
import { Card, Button, List, Tag, Space, Popconfirm, App as AntApp, Modal, Form, Input, Empty } from 'antd';
|
||
import { AuthAPI, Team, TeamAPI } from '../api';
|
||
|
||
export default function TeamsPage() {
|
||
const { message } = AntApp.useApp();
|
||
const [list, setList] = useState<Team[]>([]);
|
||
const [active, setActive] = useState<Team | null>(null);
|
||
const [createOpen, setCreateOpen] = useState(false);
|
||
const [inviteOpen, setInviteOpen] = useState(false);
|
||
const [lastInviteCode, setLastInviteCode] = useState<string | null>(null);
|
||
|
||
const load = async () => {
|
||
const l = await TeamAPI.list();
|
||
setList(l);
|
||
if (l.length && !active) setActive(await TeamAPI.detail(l[0].id));
|
||
};
|
||
|
||
useEffect(() => {
|
||
load();
|
||
}, []);
|
||
|
||
const handleCreate = async (v: any) => {
|
||
const t = await TeamAPI.create(v.name);
|
||
setCreateOpen(false);
|
||
message.success('已创建');
|
||
await load();
|
||
setActive(await TeamAPI.detail(t.id));
|
||
};
|
||
|
||
const handleInvite = async (v: any) => {
|
||
if (!active) return;
|
||
const inv = await AuthAPI.createInvite({
|
||
teamId: active.id,
|
||
email: v.email || undefined,
|
||
ttlHours: Number(v.ttlHours) || 168
|
||
});
|
||
setLastInviteCode(inv.code);
|
||
};
|
||
|
||
return (
|
||
<div className="feature-cover-container">
|
||
<div className="page-container" style={{ maxWidth: 1080 }}>
|
||
<div
|
||
style={{
|
||
borderRadius: 24,
|
||
padding: '30px 30px 26px',
|
||
background:
|
||
'linear-gradient(135deg, rgba(255,255,255,0.98) 0%, rgba(236,253,245,0.92) 42%, rgba(239,246,255,0.96) 100%)',
|
||
border: '1px solid rgba(8, 145, 178, 0.12)',
|
||
boxShadow: '0 20px 48px rgba(15, 23, 42, 0.06)',
|
||
marginBottom: 24
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'flex-start',
|
||
gap: 20,
|
||
flexWrap: 'wrap',
|
||
marginBottom: 20
|
||
}}
|
||
>
|
||
<div style={{ maxWidth: 620 }}>
|
||
<div
|
||
style={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
padding: '6px 12px',
|
||
borderRadius: 999,
|
||
background: 'rgba(255,255,255,0.78)',
|
||
border: '1px solid rgba(8, 145, 178, 0.10)',
|
||
color: 'var(--color-text-secondary)',
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
marginBottom: 16
|
||
}}
|
||
>
|
||
<TeamOutlined style={{ color: 'var(--color-brand)' }} />
|
||
协作组织空间
|
||
</div>
|
||
<h1 className="page-title" style={{ marginBottom: 10 }}>团队管理</h1>
|
||
<div className="page-subtitle" style={{ marginTop: 0, fontSize: 15, lineHeight: 1.75 }}>
|
||
团队不只是成员列表,更是共享智能体、协同运营和权限分工的组织单元。这里统一查看团队、成员和邀请状态。
|
||
</div>
|
||
</div>
|
||
<Button type="primary" size="large" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)} style={{ borderRadius: 14, height: 46, padding: '0 18px', fontWeight: 600 }}>
|
||
创建团队
|
||
</Button>
|
||
</div>
|
||
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 14 }}>
|
||
{[
|
||
{ label: '团队数量', value: list.length, tone: 'rgba(8, 145, 178, 0.10)', color: 'var(--color-brand)' },
|
||
{ label: '当前成员数', value: active?.members?.length ?? 0, tone: 'rgba(14, 165, 233, 0.10)', color: 'var(--color-info)' },
|
||
{ label: '共享智能体', value: active?.agentCount ?? 0, tone: 'rgba(34, 197, 94, 0.10)', color: 'var(--color-success)' }
|
||
].map((item) => (
|
||
<div
|
||
key={item.label}
|
||
style={{
|
||
borderRadius: 18,
|
||
padding: '16px 18px',
|
||
background: 'rgba(255,255,255,0.72)',
|
||
border: '1px solid rgba(255,255,255,0.7)'
|
||
}}
|
||
>
|
||
<div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)', marginBottom: 10 }}>{item.label}</div>
|
||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
|
||
<span style={{ fontSize: 30, fontWeight: 700, color: 'var(--color-text)' }}>{item.value}</span>
|
||
<span
|
||
style={{
|
||
borderRadius: 999,
|
||
padding: '4px 8px',
|
||
background: item.tone,
|
||
color: item.color,
|
||
fontSize: 12,
|
||
fontWeight: 600
|
||
}}
|
||
>
|
||
当前选中
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: 22, alignItems: 'stretch' }}>
|
||
<div
|
||
style={{
|
||
width: 260,
|
||
flexShrink: 0,
|
||
background: 'linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(252,252,253,1) 100%)',
|
||
border: '1px solid var(--color-border)',
|
||
borderRadius: 22,
|
||
padding: 14,
|
||
boxShadow: '0 12px 28px rgba(15, 23, 42, 0.045)'
|
||
}}
|
||
>
|
||
<div style={{ padding: '8px 10px 14px' }}>
|
||
<div style={{ fontSize: 15, fontWeight: 700, color: 'var(--color-text)', marginBottom: 4 }}>团队列表</div>
|
||
<div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)' }}>选择一个团队查看成员与邀请</div>
|
||
</div>
|
||
{list.length === 0 ? (
|
||
<Empty description="还没有团队" />
|
||
) : (
|
||
<List
|
||
dataSource={list}
|
||
renderItem={(item) => (
|
||
<div
|
||
className={`nav-item ${active?.id === item.id ? 'active' : ''}`}
|
||
onClick={async () => setActive(await TeamAPI.detail(item.id))}
|
||
style={{
|
||
padding: '10px 12px',
|
||
borderRadius: 14,
|
||
cursor: 'pointer',
|
||
marginBottom: 6,
|
||
background: active?.id === item.id ? 'rgba(8, 145, 178, 0.10)' : 'transparent',
|
||
color: active?.id === item.id ? 'var(--color-brand)' : 'var(--color-text-secondary)',
|
||
fontWeight: active?.id === item.id ? 600 : 500,
|
||
border: active?.id === item.id ? '1px solid rgba(8, 145, 178, 0.16)' : '1px solid transparent'
|
||
}}
|
||
>
|
||
<div style={{ fontSize: 14, marginBottom: 4 }}>{item.name}</div>
|
||
<div style={{ fontSize: 12, color: active?.id === item.id ? 'var(--color-brand)' : 'var(--color-text-tertiary)' }}>
|
||
{item.agentCount ?? 0} 个智能体
|
||
</div>
|
||
</div>
|
||
)}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
{active ? (
|
||
<Card
|
||
style={{ borderRadius: 22, boxShadow: '0 12px 28px rgba(15, 23, 42, 0.045)' }}
|
||
bodyStyle={{ padding: 22 }}
|
||
>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16, flexWrap: 'wrap', marginBottom: 20 }}>
|
||
<div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 8 }}>
|
||
<span style={{ fontSize: 22, fontWeight: 700, color: 'var(--color-text)' }}>{active.name}</span>
|
||
<Tag bordered={false} style={{ background: 'var(--color-brand-soft)', color: 'var(--color-brand)', borderRadius: 999, margin: 0 }}>{active.myRole}</Tag>
|
||
<Tag bordered={false} style={{ background: 'var(--color-surface-2)', color: 'var(--color-text-secondary)', borderRadius: 999, margin: 0 }}>{active.agentCount ?? 0} 智能体</Tag>
|
||
</div>
|
||
<div style={{ fontSize: 13.5, color: 'var(--color-text-secondary)' }}>
|
||
管理成员权限、邀请新伙伴,并协同维护团队共享的智能体资产。
|
||
</div>
|
||
</div>
|
||
<Space>
|
||
{(active.myRole === 'owner' || active.myRole === 'admin') && (
|
||
<Button icon={<MailOutlined />} onClick={() => setInviteOpen(true)} style={{ borderRadius: 12 }}>
|
||
生成邀请码
|
||
</Button>
|
||
)}
|
||
{active.myRole === 'owner' && (
|
||
<Popconfirm
|
||
title="确定删除该团队?团队内的智能体会变成 owner 私有"
|
||
onConfirm={async () => {
|
||
await TeamAPI.remove(active.id);
|
||
message.success('已删除');
|
||
setActive(null);
|
||
load();
|
||
}}
|
||
>
|
||
<Button danger icon={<DeleteOutlined />} style={{ borderRadius: 12 }}>
|
||
删除团队
|
||
</Button>
|
||
</Popconfirm>
|
||
)}
|
||
</Space>
|
||
</div>
|
||
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 12, marginBottom: 20 }}>
|
||
<div style={{ borderRadius: 16, padding: '14px 16px', background: 'rgba(8, 145, 178, 0.06)', border: '1px solid rgba(8, 145, 178, 0.10)' }}>
|
||
<div style={{ fontSize: 12, color: 'var(--color-text-secondary)', marginBottom: 8 }}>成员规模</div>
|
||
<div style={{ fontSize: 24, fontWeight: 700, color: 'var(--color-text)' }}>{active.members?.length || 0}</div>
|
||
</div>
|
||
<div style={{ borderRadius: 16, padding: '14px 16px', background: 'rgba(34, 197, 94, 0.06)', border: '1px solid rgba(34, 197, 94, 0.10)' }}>
|
||
<div style={{ fontSize: 12, color: 'var(--color-text-secondary)', marginBottom: 8 }}>共享智能体</div>
|
||
<div style={{ fontSize: 24, fontWeight: 700, color: 'var(--color-text)' }}>{active.agentCount ?? 0}</div>
|
||
</div>
|
||
<div style={{ borderRadius: 16, padding: '14px 16px', background: 'rgba(249, 115, 22, 0.06)', border: '1px solid rgba(249, 115, 22, 0.10)' }}>
|
||
<div style={{ fontSize: 12, color: 'var(--color-text-secondary)', marginBottom: 8 }}>当前身份</div>
|
||
<div style={{ fontSize: 24, fontWeight: 700, color: 'var(--color-text)', textTransform: 'capitalize' }}>{active.myRole}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ fontSize: 16, fontWeight: 700, color: 'var(--color-text)', marginBottom: 14 }}>
|
||
成员 ({active.members?.length || 0})
|
||
</div>
|
||
<List
|
||
dataSource={active.members || []}
|
||
renderItem={(m) => (
|
||
<List.Item
|
||
style={{ padding: '14px 0' }}
|
||
actions={
|
||
(active.myRole === 'owner' || active.myRole === 'admin') && m.role !== 'owner'
|
||
? [
|
||
<Popconfirm
|
||
key="kick"
|
||
title="移除该成员?"
|
||
onConfirm={async () => {
|
||
await TeamAPI.removeMember(active.id, m.id);
|
||
message.success('已移除');
|
||
setActive(await TeamAPI.detail(active.id));
|
||
}}
|
||
>
|
||
<Button size="small" danger style={{ borderRadius: 10 }}>
|
||
移除
|
||
</Button>
|
||
</Popconfirm>
|
||
]
|
||
: []
|
||
}
|
||
>
|
||
<List.Item.Meta
|
||
avatar={
|
||
<div
|
||
style={{
|
||
width: 42,
|
||
height: 42,
|
||
borderRadius: 999,
|
||
background: 'rgba(8, 145, 178, 0.10)',
|
||
color: 'var(--color-brand)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center'
|
||
}}
|
||
>
|
||
<UserOutlined />
|
||
</div>
|
||
}
|
||
title={
|
||
<Space>
|
||
<span style={{ fontWeight: 600 }}>{m.name}</span>
|
||
<Tag
|
||
bordered={false}
|
||
style={{
|
||
background:
|
||
m.role === 'owner'
|
||
? 'var(--color-warning-soft)'
|
||
: m.role === 'admin'
|
||
? 'var(--color-info-soft)'
|
||
: 'var(--color-surface-2)',
|
||
color:
|
||
m.role === 'owner'
|
||
? 'var(--color-warning)'
|
||
: m.role === 'admin'
|
||
? 'var(--color-info)'
|
||
: 'var(--color-text-secondary)',
|
||
borderRadius: 999,
|
||
margin: 0
|
||
}}
|
||
>
|
||
{m.role}
|
||
</Tag>
|
||
</Space>
|
||
}
|
||
description={
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||
<span>{m.email}</span>
|
||
<span style={{ fontSize: 12, color: 'var(--color-text-tertiary)' }}>
|
||
加入时间 {new Date(m.joinedAt).toLocaleDateString('zh-CN')}
|
||
</span>
|
||
</div>
|
||
}
|
||
/>
|
||
</List.Item>
|
||
)}
|
||
/>
|
||
</Card>
|
||
) : (
|
||
<Empty description="选择或创建一个团队" />
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<Modal
|
||
open={createOpen}
|
||
title="新建团队"
|
||
onCancel={() => setCreateOpen(false)}
|
||
footer={null}
|
||
destroyOnHidden
|
||
>
|
||
<Form layout="vertical" onFinish={handleCreate}>
|
||
<Form.Item name="name" label="团队名称" rules={[{ required: true }]}>
|
||
<Input placeholder="如:AI 实验小组" autoFocus />
|
||
</Form.Item>
|
||
<Button type="primary" htmlType="submit" block>
|
||
创建
|
||
</Button>
|
||
</Form>
|
||
</Modal>
|
||
|
||
<Modal
|
||
open={inviteOpen}
|
||
title={`📨 邀请加入 ${active?.name}`}
|
||
onCancel={() => {
|
||
setInviteOpen(false);
|
||
setLastInviteCode(null);
|
||
}}
|
||
footer={null}
|
||
destroyOnHidden
|
||
>
|
||
{lastInviteCode ? (
|
||
<div>
|
||
<div style={{ marginBottom: 12 }}>邀请码生成成功,请发给受邀者:</div>
|
||
<Input.TextArea
|
||
value={lastInviteCode}
|
||
readOnly
|
||
autoSize
|
||
style={{ fontFamily: 'monospace', fontSize: 16 }}
|
||
/>
|
||
<Button
|
||
type="default"
|
||
icon={<CopyOutlined />}
|
||
style={{ marginTop: 12, borderRadius: 10 }}
|
||
onClick={() => {
|
||
navigator.clipboard?.writeText(lastInviteCode).then(() => message.success('邀请码已复制'));
|
||
}}
|
||
>
|
||
复制邀请码
|
||
</Button>
|
||
<div style={{ color: 'var(--color-text-secondary)', fontSize: 12, marginTop: 8 }}>
|
||
受邀者在注册页填入此邀请码即可加入团队
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<Form layout="vertical" onFinish={handleInvite}>
|
||
<Form.Item name="email" label="限定邮箱(可选)">
|
||
<Input placeholder="只允许该邮箱使用此邀请码" />
|
||
</Form.Item>
|
||
<Form.Item name="ttlHours" label="有效期(小时)" initialValue={168}>
|
||
<Input type="number" placeholder="168 = 7 天" />
|
||
</Form.Item>
|
||
<Button type="primary" htmlType="submit" block>
|
||
生成邀请码
|
||
</Button>
|
||
</Form>
|
||
)}
|
||
</Modal>
|
||
</div>
|
||
<div className="feature-cover">
|
||
<Empty description="功能规划中,本期不支持" />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|