feat: add team feature

main
yannyang 2026-07-29 21:25:11 +08:00
parent c7c580a388
commit 23d1df700c
26 changed files with 875 additions and 829 deletions

View File

@ -93,13 +93,13 @@ export const MembershipAPI = {
/** /**
* (/)访 * (/)访
*/ */
authorizeResource: (payload: { userId: string; resourceType: 'agent' | 'knowledge'; resourceId: string; level: 'read' | 'write' }) => authorizeResource: (payload: { userId: string; resourceType: 'agent' | 'knowledge'; resourceIds: string[]; level: 'read' | 'write' }) =>
api.post<ApiResponse<any>>('/membership/access', payload).then(r => r.data), api.post<ApiResponse<any>>('/membership/access', payload).then(r => r.data),
/** /**
* *
*/ */
revokeResource: (payload: { userId: string; resourceType: 'agent' | 'knowledge'; resourceId: string }) => revokeResource: (payload: { userId: string; resourceType: 'agent' | 'knowledge'; resourceIds: string[] }) =>
api.delete<ApiResponse<any>>('/membership/access', { data: payload }).then(r => r.data), api.delete<ApiResponse<any>>('/membership/access', { data: payload }).then(r => r.data),
/** /**

View File

@ -15,6 +15,7 @@ export const TeamAPI = {
detail: (id: string) => api.get<Team>(`/teams/${id}`).then((r) => r.data), detail: (id: string) => api.get<Team>(`/teams/${id}`).then((r) => r.data),
create: (name: string) => api.post<Team>('/teams', { name }).then((r) => r.data), create: (name: string) => api.post<Team>('/teams', { name }).then((r) => r.data),
rename: (id: string, name: string) => api.put(`/teams/${id}`, { name }).then((r) => r.data), rename: (id: string, name: string) => api.put(`/teams/${id}`, { name }).then((r) => r.data),
addMember: (id: string, phone: string) => api.post(`/teams/${id}/members`, { phone }).then((r) => r.data),
remove: (id: string) => api.delete(`/teams/${id}`).then((r) => r.data), remove: (id: string) => api.delete(`/teams/${id}`).then((r) => r.data),
removeMember: (id: string, userId: string) => api.delete(`/teams/${id}/members/${userId}`).then((r) => r.data) removeMember: (id: string, userId: string) => api.delete(`/teams/${id}/members/${userId}`).then((r) => r.data)
}; };

View File

@ -193,6 +193,14 @@ export default function ExternalToolEditor({ open, agentId, plugin, onClose, onS
onOk={handleSubmit} onOk={handleSubmit}
destroyOnHidden destroyOnHidden
afterOpenChange={handleAfterOpenChange} afterOpenChange={handleAfterOpenChange}
maskClosable={false}
styles={{
body: {
maxHeight: '70vh',
overflowY: 'auto',
paddingRight: 12
}
}}
> >
<Form form={form} layout="vertical" requiredMark="optional" preserve={true}> <Form form={form} layout="vertical" requiredMark="optional" preserve={true}>
<Tabs <Tabs

View File

@ -36,7 +36,7 @@ const NAV_GROUPS: Array<{
items: [ items: [
{ to: '/chat', icon: <MessageOutlined />, label: '聊天' }, { to: '/chat', icon: <MessageOutlined />, label: '聊天' },
{ to: '/agents', icon: <RobotOutlined />, label: '我的智能体' }, { to: '/agents', icon: <RobotOutlined />, label: '我的智能体' },
{ to: '/marketplace', icon: <CompassOutlined />, label: '智能体广场' } // { to: '/marketplace', icon: <CompassOutlined />, label: '智能体广场' }
] ]
}, },
{ {

View File

@ -38,10 +38,10 @@ export default function AgentListH5({ logic }: Props) {
type="primary" type="primary"
size="middle" size="middle"
icon={<CompassOutlined />} icon={<CompassOutlined />}
onClick={() => navigate('/marketplace')} onClick={() => navigate('/agents/new')}
className="h5-stats-marketplace-btn" className="h5-stats-marketplace-btn"
> >
广
</Button> </Button>
</div> </div>
@ -69,8 +69,8 @@ export default function AgentListH5({ logic }: Props) {
{!loading && list.length === 0 ? ( {!loading && list.length === 0 ? (
<div className="empty-state h5-empty-state"> <div className="empty-state h5-empty-state">
<Empty description="你还没有任何智能体"> <Empty description="你还没有任何智能体">
<Button type="primary" onClick={() => navigate('/marketplace')} style={{ borderRadius: 8 }}> <Button type="primary" onClick={() => navigate('/agents/new')} style={{ borderRadius: 8 }}>
广
</Button> </Button>
</Empty> </Empty>
</div> </div>

View File

@ -126,14 +126,14 @@
.agent-card-desc-container { .agent-card-desc-container {
margin-top: 16px; margin-top: 16px;
padding: 16px 16px 18px; padding: 16px;
border-radius: 16px; border-radius: 16px;
background: linear-gradient(180deg, rgba(248,250,252,0.9) 0%, rgba(255,255,255,0.95) 100%); background: linear-gradient(180deg, rgba(248,250,252,0.9) 0%, rgba(255,255,255,0.95) 100%);
border: 1px solid rgba(148, 163, 184, 0.14); border: 1px solid rgba(148, 163, 184, 0.14);
} }
.agent-card-desc { .agent-card-desc {
min-height: 66px; min-height: 48px;
font-size: 13.5px; font-size: 13.5px;
line-height: 1.7; line-height: 1.7;
display: -webkit-box; display: -webkit-box;

View File

@ -42,10 +42,10 @@ export default function AgentListWeb({ logic }: Props) {
type="primary" type="primary"
size="large" size="large"
icon={<CompassOutlined />} icon={<CompassOutlined />}
onClick={() => navigate('/marketplace')} onClick={() => navigate('/agents/new')}
style={{ borderRadius: 14, height: 46, padding: '0 18px', fontWeight: 600 }} style={{ borderRadius: 14, height: 46, padding: '0 18px', fontWeight: 600 }}
> >
广
</Button> </Button>
</div> </div>
@ -73,8 +73,8 @@ export default function AgentListWeb({ logic }: Props) {
{!loading && list.length === 0 ? ( {!loading && list.length === 0 ? (
<div className="empty-state"> <div className="empty-state">
<Empty description="你还没有任何智能体"> <Empty description="你还没有任何智能体">
<Button type="primary" onClick={() => navigate('/marketplace')} style={{ borderRadius: 10 }}> <Button type="primary" onClick={() => navigate('/agents/new')} style={{ borderRadius: 10 }}>
广
</Button> </Button>
</Empty> </Empty>
</div> </div>

View File

@ -30,8 +30,8 @@ export default function AgentListWebBase({ logic, viewport }: AgentListWebVarian
AI 广 AI 广
</div> </div>
</div> </div>
<Button type="primary" size="large" icon={<CompassOutlined />} onClick={() => navigate('/marketplace')} className="agent-list-web-market-btn"> <Button type="primary" size="large" icon={<CompassOutlined />} onClick={() => navigate('/agents/new')} className="agent-list-web-market-btn">
广
</Button> </Button>
</div> </div>
@ -53,8 +53,8 @@ export default function AgentListWebBase({ logic, viewport }: AgentListWebVarian
{!loading && list.length === 0 ? ( {!loading && list.length === 0 ? (
<div className="empty-state"> <div className="empty-state">
<Empty description="你还没有任何智能体"> <Empty description="你还没有任何智能体">
<Button type="primary" onClick={() => navigate('/marketplace')} className="agent-list-web-empty-btn"> <Button type="primary" onClick={() => navigate('/agents/new')} className="agent-list-web-empty-btn">
广
</Button> </Button>
</Empty> </Empty>
</div> </div>

View File

@ -98,9 +98,9 @@ export function useProfileLogic() {
} }
}; };
const handleAuthorizeAgent = async (userId: string, agentId: string, level: 'read' | 'write') => { const handleAuthorizeAgent = async (userId: string, agentIds: string[], level: 'read' | 'write') => {
try { try {
await MembershipAPI.authorizeResource({ userId, resourceType: 'agent', resourceId: agentId, level }); await MembershipAPI.authorizeResource({ userId, resourceType: 'agent', resourceIds: agentIds, level });
message.success('授权成功'); message.success('授权成功');
const mList = await MembershipAPI.listMembers(); const mList = await MembershipAPI.listMembers();
setMembers(mList); setMembers(mList);
@ -109,9 +109,9 @@ export function useProfileLogic() {
} }
}; };
const handleRevokeAgent = async (userId: string, agentId: string) => { const handleRevokeAgent = async (userId: string, agentIds: string[]) => {
try { try {
await MembershipAPI.revokeResource({ userId, resourceType: 'agent', resourceId: agentId }); await MembershipAPI.revokeResource({ userId, resourceType: 'agent', resourceIds: agentIds });
message.success('取消授权成功'); message.success('取消授权成功');
const mList = await MembershipAPI.listMembers(); const mList = await MembershipAPI.listMembers();
setMembers(mList); setMembers(mList);

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button, Modal, Input, Select, Space, Tag, InputNumber, Popconfirm, List, Card } from 'antd'; import { Button, Modal, Input, Select, Space, Tag, InputNumber, Popconfirm, List, Card, Checkbox, Divider } from 'antd';
import { UserAddOutlined, DeleteOutlined, KeyOutlined, DashboardOutlined } from '@ant-design/icons'; import { UserAddOutlined, DeleteOutlined, KeyOutlined, DashboardOutlined } from '@ant-design/icons';
import type { ProfileLogicOutput } from '../ProfileLogic'; import type { ProfileLogicOutput } from '../ProfileLogic';
import '../styles/profile.css'; import '../styles/profile.css';
@ -30,7 +30,7 @@ export default function ProfileH5({ logic }: Props) {
const [newChildId, setNewChildId] = useState(''); const [newChildId, setNewChildId] = useState('');
const [selectedMember, setSelectedMember] = useState<any>(null); const [selectedMember, setSelectedMember] = useState<any>(null);
const [quotaAmount, setQuotaAmount] = useState<number>(100000); const [quotaAmount, setQuotaAmount] = useState<number>(100000);
const [selectedAgentId, setSelectedAgentId] = useState<string>(''); const [selectedAgentIds, setSelectedAgentIds] = useState<string[]>([]);
return ( return (
<div className="profile-page profile-page-h5"> <div className="profile-page profile-page-h5">
@ -90,7 +90,14 @@ export default function ProfileH5({ logic }: Props) {
style={{ marginBottom: 12, borderRadius: 12 }} style={{ marginBottom: 12, borderRadius: 12 }}
actions={[ actions={[
<DashboardOutlined key="quota" onClick={() => { setSelectedMember(record); setQuotaModalVisible(true); }} />, <DashboardOutlined key="quota" onClick={() => { setSelectedMember(record); setQuotaModalVisible(true); }} />,
<KeyOutlined key="auth" onClick={() => { setSelectedMember(record); setAuthModalVisible(true); }} />, <KeyOutlined
key="auth"
onClick={() => {
setSelectedMember(record);
setSelectedAgentIds(record.authorizedAgentIds || []);
setAuthModalVisible(true);
}}
/>,
<Popconfirm key="delete" title="确定移除吗?" onConfirm={() => handleRemoveChild(record.id)}> <Popconfirm key="delete" title="确定移除吗?" onConfirm={() => handleRemoveChild(record.id)}>
<DeleteOutlined style={{ color: 'var(--color-error)' }} /> <DeleteOutlined style={{ color: 'var(--color-error)' }} />
</Popconfirm>, </Popconfirm>,
@ -112,13 +119,23 @@ export default function ProfileH5({ logic }: Props) {
<Tag <Tag
key={id} key={id}
closable closable
onClose={() => handleRevokeAgent(record.child_user_id, id)} onClose={() => handleRevokeAgent(record.child_user_id, [id])}
style={{ borderRadius: 4, fontSize: 10, margin: '2px' }} style={{ borderRadius: 4, fontSize: 10, margin: '2px', display: 'flex', width: 'fit-content' }}
> >
{agent?.name || '未知'} 🤖 {agent?.name || '未知'}
</Tag> </Tag>
); );
})} })}
{record.authorizedAgentIds?.length > 0 && (
<Popconfirm
title="一键取消全部授权?"
onConfirm={() => handleRevokeAgent(record.child_user_id, record.authorizedAgentIds)}
>
<Button type="link" size="small" danger style={{ fontSize: 10, padding: '0 2px', height: 'auto' }}>
</Button>
</Popconfirm>
)}
</div> </div>
</div> </div>
} }
@ -163,12 +180,42 @@ export default function ProfileH5({ logic }: Props) {
<Modal <Modal
title={`${selectedMember?.child_name} 授权资源`} title={`${selectedMember?.child_name} 授权资源`}
open={authModalVisible} open={authModalVisible}
onOk={() => { handleAuthorizeAgent(selectedMember.child_user_id, selectedAgentId, 'read'); setAuthModalVisible(false); setSelectedAgentId(''); }} onOk={() => {
onCancel={() => setAuthModalVisible(false)} handleAuthorizeAgent(selectedMember.child_user_id, selectedAgentIds, 'read');
setAuthModalVisible(false);
setSelectedAgentIds([]);
}}
onCancel={() => {
setAuthModalVisible(false);
setSelectedAgentIds([]);
}}
> >
<Select style={{ width: '100%' }} placeholder="选择智能体" value={selectedAgentId} onChange={setSelectedAgentId}> <div style={{ marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
{myAgents.map(a => <Select.Option key={a.id} value={a.id}>{a.name}</Select.Option>)} <span style={{ fontSize: 13, fontWeight: 500 }}></span>
</Select> <Checkbox
indeterminate={selectedAgentIds.length > 0 && selectedAgentIds.length < myAgents.length}
checked={selectedAgentIds.length === myAgents.length && myAgents.length > 0}
onChange={(e) => {
setSelectedAgentIds(e.target.checked ? myAgents.map(a => a.id) : []);
}}
>
</Checkbox>
</div>
<Divider style={{ margin: '8px 0' }} />
<div style={{ maxHeight: 300, overflowY: 'auto' }}>
<Checkbox.Group
style={{ width: '100%' }}
value={selectedAgentIds}
onChange={(vals) => setSelectedAgentIds(vals as string[])}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{myAgents.map(a => (
<Checkbox key={a.id} value={a.id}>{a.name}</Checkbox>
))}
</div>
</Checkbox.Group>
</div>
</Modal> </Modal>
</div> </div>
); );

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Button, Table, Modal, Input, Select, Space, Tag, InputNumber, Popconfirm } from 'antd'; import { Button, Table, Modal, Input, Select, Space, Tag, InputNumber, Popconfirm, Checkbox, Divider } from 'antd';
import { UserAddOutlined, DeleteOutlined, KeyOutlined, DashboardOutlined } from '@ant-design/icons'; import { UserAddOutlined, DeleteOutlined, KeyOutlined, DashboardOutlined } from '@ant-design/icons';
import type { ProfileLogicOutput } from '../ProfileLogic'; import type { ProfileLogicOutput } from '../ProfileLogic';
import '../styles/profile.css'; import '../styles/profile.css';
@ -30,7 +30,7 @@ export default function ProfileWeb({ logic }: Props) {
const [newChildId, setNewChildId] = useState(''); const [newChildId, setNewChildId] = useState('');
const [selectedMember, setSelectedMember] = useState<any>(null); const [selectedMember, setSelectedMember] = useState<any>(null);
const [quotaAmount, setQuotaAmount] = useState<number>(100000); const [quotaAmount, setQuotaAmount] = useState<number>(100000);
const [selectedAgentId, setSelectedAgentId] = useState<string>(''); const [selectedAgentIds, setSelectedAgentIds] = useState<string[]>([]);
const columns = [ const columns = [
{ {
@ -56,23 +56,34 @@ export default function ProfileWeb({ logic }: Props) {
title: '已授权资源', title: '已授权资源',
key: 'resources', key: 'resources',
render: (_: any, record: any) => ( render: (_: any, record: any) => (
<div className="resource-tags"> <div className="resource-tags-container">
{record.authorizedAgentIds?.map((id: string) => { <div className="resource-tags" style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 4 }}>
const agent = myAgents.find(a => a.id === id); {record.authorizedAgentIds?.map((id: string) => {
return ( const agent = myAgents.find(a => a.id === id);
<Tag return (
key={id} <Tag
closable key={id}
onClose={() => handleRevokeAgent(record.child_user_id, id)} closable
style={{ borderRadius: 4, background: 'var(--color-fill-secondary)', border: 'none' }} onClose={() => handleRevokeAgent(record.child_user_id, [id])}
style={{ borderRadius: 4, background: 'var(--color-fill-secondary)', border: 'none', margin: 0 }}
>
🤖 {agent?.name || '未知智能体'}
</Tag>
);
})}
{(!record.authorizedAgentIds || record.authorizedAgentIds.length === 0) ? (
<span style={{ color: 'var(--color-text-tertiary)', fontSize: 12 }}></span>
) : (
<Popconfirm
title="确定取消该成员的所有智能体授权吗?"
onConfirm={() => handleRevokeAgent(record.child_user_id, record.authorizedAgentIds)}
> >
🤖 {agent?.name || '未知智能体'} <Button type="link" size="small" danger style={{ padding: '0 4px', fontSize: 12 }}>
</Tag>
); </Button>
})} </Popconfirm>
{(!record.authorizedAgentIds || record.authorizedAgentIds.length === 0) && ( )}
<span style={{ color: 'var(--color-text-tertiary)', fontSize: 12 }}></span> </div>
)}
</div> </div>
), ),
}, },
@ -104,6 +115,7 @@ export default function ProfileWeb({ logic }: Props) {
icon={<KeyOutlined />} icon={<KeyOutlined />}
onClick={() => { onClick={() => {
setSelectedMember(record); setSelectedMember(record);
setSelectedAgentIds(record.authorizedAgentIds || []);
setAuthModalVisible(true); setAuthModalVisible(true);
}} }}
> >
@ -233,24 +245,57 @@ export default function ProfileWeb({ logic }: Props) {
<Modal <Modal
title={`${selectedMember?.child_name} 授权资源`} title={`${selectedMember?.child_name} 授权资源`}
open={authModalVisible} open={authModalVisible}
onOk={() => { width={520}
handleAuthorizeAgent(selectedMember.child_user_id, selectedAgentId, 'read'); onOk={async () => {
// 计算新增的和移除的,但根据接口逻辑,通常是全量覆盖或者增量。
// 这里假设 handleAuthorizeAgent 是设置最终状态,或者后端支持 resourceIds 覆盖。
// 之前的 handleAuthorizeAgent 逻辑是直接调用 authorizeResource。
handleAuthorizeAgent(selectedMember.child_user_id, selectedAgentIds, 'read');
setAuthModalVisible(false); setAuthModalVisible(false);
setSelectedAgentId(''); setSelectedAgentIds([]);
}}
onCancel={() => {
setAuthModalVisible(false);
setSelectedAgentIds([]);
}} }}
onCancel={() => setAuthModalVisible(false)}
> >
<div style={{ marginBottom: 8 }}></div> <div style={{ marginBottom: 16 }}>
<Select <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
style={{ width: '100%' }} <span style={{ fontWeight: 500 }}></span>
placeholder="请选择智能体" <Checkbox
value={selectedAgentId} indeterminate={selectedAgentIds.length > 0 && selectedAgentIds.length < myAgents.length}
onChange={setSelectedAgentId} checked={selectedAgentIds.length === myAgents.length && myAgents.length > 0}
> onChange={(e) => {
{myAgents.map(a => ( setSelectedAgentIds(e.target.checked ? myAgents.map(a => a.id) : []);
<Select.Option key={a.id} value={a.id}>{a.name}</Select.Option> }}
))} >
</Select>
</Checkbox>
</div>
<Divider style={{ margin: '0 0 12px 0' }} />
<div style={{ maxHeight: 300, overflowY: 'auto', padding: '4px' }}>
<Checkbox.Group
style={{ width: '100%' }}
value={selectedAgentIds}
onChange={(vals) => setSelectedAgentIds(vals as string[])}
>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
{myAgents.map(a => (
<div key={a.id} className="auth-checkbox-item">
<Checkbox value={a.id}>
<span title={a.name}>{a.name}</span>
</Checkbox>
</div>
))}
</div>
</Checkbox.Group>
{myAgents.length === 0 && (
<div style={{ textAlign: 'center', padding: '20px 0', color: 'var(--color-text-tertiary)' }}>
</div>
)}
</div>
</div>
</Modal> </Modal>
</div> </div>
); );

View File

@ -1,7 +1,8 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTeamsPageLogic } from './TeamsPage/TeamsPageLogic'; import { useTeamsPageLogic } from './TeamsPage/TeamsPageLogic';
import TeamsPageWeb from './TeamsPage/components/TeamsPageWeb'; import TeamsPageWebBase from './TeamsPage/components/TeamsPageWebBase';
import TeamsPageH5 from './TeamsPage/components/TeamsPageH5'; import TeamsPageH5 from './TeamsPage/components/TeamsPageH5';
import { useDesktopViewport } from '../hooks/useDesktopViewport';
const isMobileDevice = () => { const isMobileDevice = () => {
if (typeof window === 'undefined') return false; if (typeof window === 'undefined') return false;
@ -10,6 +11,7 @@ const isMobileDevice = () => {
export default function TeamsPage() { export default function TeamsPage() {
const logic = useTeamsPageLogic(); const logic = useTeamsPageLogic();
const viewport = useDesktopViewport();
const [isMobile, setIsMobile] = useState(isMobileDevice()); const [isMobile, setIsMobile] = useState(isMobileDevice());
useEffect(() => { useEffect(() => {
@ -20,5 +22,9 @@ export default function TeamsPage() {
return () => window.removeEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize);
}, []); }, []);
return isMobile ? <TeamsPageH5 logic={logic} /> : <TeamsPageWeb logic={logic} />; return isMobile ? (
<TeamsPageH5 logic={logic} />
) : (
<TeamsPageWebBase logic={logic} viewport={viewport} />
);
} }

View File

@ -6,8 +6,8 @@ export function useTeamsPageLogic() {
const [list, setList] = useState<Team[]>([]); const [list, setList] = useState<Team[]>([]);
const [active, setActive] = useState<Team | null>(null); const [active, setActive] = useState<Team | null>(null);
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [inviteOpen, setInviteOpen] = useState(false); const [addMemberOpen, setAddMemberOpen] = useState(false);
const [lastInviteCode, setLastInviteCode] = useState<string | null>(null); const [renameOpen, setRenameOpen] = useState(false);
const load = async () => { const load = async () => {
const l = await TeamAPI.list(); const l = await TeamAPI.list();
@ -27,14 +27,22 @@ export function useTeamsPageLogic() {
setActive(await TeamAPI.detail(t.id)); setActive(await TeamAPI.detail(t.id));
}; };
const handleInvite = async (v: any) => { const handleRename = async (v: any) => {
if (!active) return; if (!active) return;
const inv = await AuthAPI.createInvite({ await TeamAPI.rename(active.id, v.name);
teamId: active.id, setRenameOpen(false);
phone: v.phone || undefined, message.success('已更名');
ttlHours: Number(v.ttlHours) || 168, await load();
}); setActive(await TeamAPI.detail(active.id));
setLastInviteCode(inv.code); };
const handleAddMember = async (v: any) => {
if (!active) return;
await TeamAPI.addMember(active.id, v.phone);
setAddMemberOpen(false);
message.success('已添加');
await load();
setActive(await TeamAPI.detail(active.id));
}; };
const handleDelete = async (id: string) => { const handleDelete = async (id: string) => {
@ -44,10 +52,11 @@ export function useTeamsPageLogic() {
setActive(null); setActive(null);
}; };
const handleRemoveMember = async (id: string) => { const handleRemoveMember = async (userId: string) => {
if (!active) return; if (!active) return;
await TeamAPI.removeMember(active.id, id); await TeamAPI.removeMember(active.id, userId);
message.success('已移除'); message.success('已移除');
await load();
setActive(await TeamAPI.detail(active.id)); setActive(await TeamAPI.detail(active.id));
}; };
@ -55,17 +64,18 @@ export function useTeamsPageLogic() {
list, list,
active, active,
createOpen, createOpen,
inviteOpen, addMemberOpen,
lastInviteCode, renameOpen,
load, load,
handleCreate, handleCreate,
handleInvite, handleRename,
handleAddMember,
handleDelete, handleDelete,
handleRemoveMember, handleRemoveMember,
setActive, setActive,
setCreateOpen, setCreateOpen,
setInviteOpen, setAddMemberOpen,
setLastInviteCode, setRenameOpen,
}; };
} }

View File

@ -1,14 +1,15 @@
import { import {
CopyOutlined,
DeleteOutlined, DeleteOutlined,
MailOutlined, EditOutlined,
PlusOutlined, PlusOutlined,
TeamOutlined, TeamOutlined,
UserAddOutlined,
UserOutlined, UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Card, Button, List, Tag, Space, Popconfirm, App as AntApp, Modal, Form, Input, Empty } from 'antd'; import { Card, Button, List, Tag, Space, Popconfirm, App as AntApp, Modal, Form, Input, Empty } from 'antd';
import { TeamAPI, type Team } from '../../../api'; import { TeamAPI, type Team } from '../../../api';
import type { TeamsPageLogicOutput } from '../TeamsPageLogic'; import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
import '../styles/teams-page-h5.css';
interface Props { interface Props {
logic: TeamsPageLogicOutput; logic: TeamsPageLogicOutput;
@ -20,55 +21,26 @@ export default function TeamsPageH5({ logic }: Props) {
list, list,
active, active,
handleDelete, handleDelete,
handleRename,
handleRemoveMember, handleRemoveMember,
handleAddMember,
setCreateOpen, setCreateOpen,
setRenameOpen,
setAddMemberOpen,
} = logic; } = logic;
return ( return (
<div className="feature-cover-container"> <div className="feature-cover-container">
<div className="page-container h5-page-container" style={{ padding: '0 8px' }}> <div className="page-container h5-page-container">
<div <div className="teams-page-h5-hero">
style={{ <div className="teams-page-h5-hero-header">
borderRadius: 16,
padding: '20px 16px 18px',
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 10px 24px rgba(15, 23, 42, 0.06)',
marginBottom: 16,
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: 12,
flexWrap: 'wrap',
marginBottom: 16,
}}
>
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
<div <div className="teams-page-h5-badge">
style={{ <TeamOutlined className="teams-page-h5-member-avatar-icon" />
display: 'inline-flex',
alignItems: 'center',
gap: 6,
padding: '4px 10px',
borderRadius: 999,
background: 'rgba(255,255,255,0.78)',
border: '1px solid rgba(8, 145, 178, 0.10)',
color: 'var(--color-text-secondary)',
fontSize: 11,
fontWeight: 600,
marginBottom: 12,
}}
>
<TeamOutlined style={{ color: 'var(--color-brand)', fontSize: 12 }} />
</div> </div>
<h1 className="page-title h5-page-title" style={{ marginBottom: 8, fontSize: 22 }}></h1> <h1 className="page-title h5-page-title teams-page-h5-title"></h1>
<div className="page-subtitle h5-page-subtitle" style={{ marginTop: 0, fontSize: 13, lineHeight: 1.6 }}> <div className="page-subtitle h5-page-subtitle teams-page-h5-subtitle">
</div> </div>
</div> </div>
@ -77,40 +49,23 @@ export default function TeamsPageH5({ logic }: Props) {
size="middle" size="middle"
icon={<PlusOutlined />} icon={<PlusOutlined />}
onClick={() => setCreateOpen(true)} onClick={() => setCreateOpen(true)}
style={{ borderRadius: 10, height: 40, padding: '0 14px', fontWeight: 600, width: '100%' }} className="teams-page-h5-create-btn"
> >
</Button> </Button>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}> <div className="teams-page-h5-stats-grid">
{[ {[
{ label: '团队数量', value: list.length, tone: 'rgba(8, 145, 178, 0.10)', color: 'var(--color-brand)' }, { label: '团队数量', value: list.length, type: 'brand' },
{ label: '当前成员数', value: active?.members?.length ?? 0, tone: 'rgba(14, 165, 233, 0.10)', color: 'var(--color-info)' }, { label: '当前成员数', value: active?.members?.length ?? 0, type: 'info' },
{ label: '共享智能体', value: active?.agentCount ?? 0, tone: 'rgba(34, 197, 94, 0.10)', color: 'var(--color-success)' }, { label: '共享智能体', value: active?.agentCount ?? 0, type: 'success' },
].map((item) => ( ].map((item) => (
<div <div key={item.label} className="teams-page-h5-stat-card">
key={item.label} <div className="teams-page-h5-stat-label">{item.label}</div>
style={{ <div className="teams-page-h5-stat-row">
borderRadius: 12, <span className="teams-page-h5-stat-value">{item.value}</span>
padding: '12px 14px', <span className={`teams-page-h5-stat-chip teams-page-h5-stat-chip-${item.type}`}>
background: 'rgba(255,255,255,0.72)',
border: '1px solid rgba(255,255,255,0.7)',
}}
>
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)', marginBottom: 6 }}>{item.label}</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<span style={{ fontSize: 22, fontWeight: 700, color: 'var(--color-text)' }}>{item.value}</span>
<span
style={{
borderRadius: 999,
padding: '3px 6px',
background: item.tone,
color: item.color,
fontSize: 10,
fontWeight: 600,
}}
>
</span> </span>
</div> </div>
@ -120,38 +75,21 @@ export default function TeamsPageH5({ logic }: Props) {
</div> </div>
{list.length > 0 && ( {list.length > 0 && (
<div <div className="teams-page-h5-list-panel">
style={{ <div className="teams-page-h5-list-head">
background: 'linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(252,252,253,1) 100%)', <div className="teams-page-h5-section-title"></div>
border: '1px solid var(--color-border)', <div className="teams-page-h5-section-desc"></div>
borderRadius: 16,
padding: 12,
marginBottom: 16,
}}
>
<div style={{ padding: '4px 6px 10px' }}>
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--color-text)', marginBottom: 4 }}></div>
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)' }}></div>
</div> </div>
{list.map((item) => ( {list.map((item) => (
<div <div
className={`nav-item ${active?.id === item.id ? 'active' : ''}`} key={item.id}
className={`teams-page-h5-nav-item ${active?.id === item.id ? 'active' : ''}`}
onClick={async () => { onClick={async () => {
logic.setActive(await TeamAPI.detail(item.id)); logic.setActive(await TeamAPI.detail(item.id));
}} }}
style={{
padding: '10px 12px',
borderRadius: 10,
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: 13, marginBottom: 3 }}>{item.name}</div> <div className="teams-page-h5-nav-item-name">{item.name}</div>
<div style={{ fontSize: 11, color: active?.id === item.id ? 'var(--color-brand)' : 'var(--color-text-tertiary)' }}> <div className="teams-page-h5-nav-item-count">
{item.agentCount ?? 0} {item.agentCount ?? 0}
</div> </div>
</div> </div>
@ -160,27 +98,26 @@ export default function TeamsPageH5({ logic }: Props) {
)} )}
{active ? ( {active ? (
<Card <Card className="teams-page-h5-detail-card">
style={{ borderRadius: 16, boxShadow: '0 6px 16px rgba(15, 23, 42, 0.045)' }} <div className="teams-page-h5-detail-head">
bodyStyle={{ padding: 16 }}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginBottom: 16 }}>
<div> <div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', marginBottom: 4 }}> <div className="teams-page-h5-active-title-row">
<span style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text)' }}>{active.name}</span> <span className="teams-page-h5-active-title">{active.name}</span>
<Tag bordered={false} style={{ background: 'var(--color-brand-soft)', color: 'var(--color-brand)', borderRadius: 999, margin: 0, fontSize: 11 }}>{active.myRole}</Tag> <Button
<Tag bordered={false} style={{ background: 'var(--color-surface-2)', color: 'var(--color-text-secondary)', borderRadius: 999, margin: 0, fontSize: 11 }}>{active.agentCount ?? 0} </Tag> type="text"
size="small"
icon={<EditOutlined />}
onClick={() => setRenameOpen(true)}
className="teams-page-h5-edit-btn"
/>
<Tag bordered={false} className="teams-page-h5-tag teams-page-h5-tag-brand">{active.myRole}</Tag>
<Tag bordered={false} className="teams-page-h5-tag teams-page-h5-tag-neutral">{active.agentCount ?? 0} </Tag>
</div> </div>
<div style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}> <div className="teams-page-h5-section-desc">
</div> </div>
</div> </div>
<Space wrap> <Space wrap>
{(active.myRole === 'owner' || active.myRole === 'admin') && (
<Button icon={<MailOutlined />} size="small" onClick={() => logic.setInviteOpen(true)} style={{ borderRadius: 8 }}>
</Button>
)}
{active.myRole === 'owner' && ( {active.myRole === 'owner' && (
<Popconfirm <Popconfirm
title="确定删除该团队?团队内的智能体会变成 owner 私有" title="确定删除该团队?团队内的智能体会变成 owner 私有"
@ -189,7 +126,7 @@ export default function TeamsPageH5({ logic }: Props) {
message.success('已删除'); message.success('已删除');
}} }}
> >
<Button danger size="small" icon={<DeleteOutlined />} style={{ borderRadius: 8 }}> <Button danger size="small" icon={<DeleteOutlined />} className="teams-page-web-soft-btn">
</Button> </Button>
</Popconfirm> </Popconfirm>
@ -197,29 +134,43 @@ export default function TeamsPageH5({ logic }: Props) {
</Space> </Space>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10, marginBottom: 16 }}> <div className="teams-page-h5-mini-grid">
<div style={{ borderRadius: 12, padding: '12px 14px', background: 'rgba(8, 145, 178, 0.06)', border: '1px solid rgba(8, 145, 178, 0.10)' }}> <div className="teams-page-h5-mini-card teams-page-h5-mini-card-brand">
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)', marginBottom: 6 }}></div> <div className="teams-page-h5-mini-label"></div>
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--color-text)' }}>{active.members?.length || 0}</div> <div className="teams-page-h5-mini-value">{active.members?.length || 0}</div>
</div> </div>
<div style={{ borderRadius: 12, padding: '12px 14px', background: 'rgba(34, 197, 94, 0.06)', border: '1px solid rgba(34, 197, 94, 0.10)' }}> <div className="teams-page-h5-mini-card teams-page-h5-mini-card-success">
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)', marginBottom: 6 }}></div> <div className="teams-page-h5-mini-label"></div>
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--color-text)' }}>{active.agentCount ?? 0}</div> <div className="teams-page-h5-mini-value">{active.agentCount ?? 0}</div>
</div> </div>
<div style={{ borderRadius: 12, padding: '12px 14px', background: 'rgba(249, 115, 22, 0.06)', border: '1px solid rgba(249, 115, 22, 0.10)' }}> <div className="teams-page-h5-mini-card teams-page-h5-mini-card-warning">
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)', marginBottom: 6 }}></div> <div className="teams-page-h5-mini-label"></div>
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--color-text)', textTransform: 'capitalize' }}>{active.myRole}</div> <div className="teams-page-h5-mini-value" style={{ textTransform: 'capitalize' }}>{active.myRole}</div>
</div> </div>
</div> </div>
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--color-text)', marginBottom: 10 }}> <div className="teams-page-h5-members-title-row">
({active.members?.length || 0}) <div className="teams-page-h5-members-title">
({active.members?.length || 0})
</div>
{(active.myRole === 'owner' || active.myRole === 'admin') && (
<Button
type="primary"
size="small"
ghost
icon={<UserAddOutlined />}
onClick={() => setAddMemberOpen(true)}
className="teams-page-web-soft-btn"
>
</Button>
)}
</div> </div>
<List <List
dataSource={active.members || []} dataSource={active.members || []}
renderItem={(m) => ( renderItem={(m) => (
<List.Item <List.Item
style={{ padding: '10px 0' }} className="teams-page-h5-member-item"
actions={ actions={
(active.myRole === 'owner' || active.myRole === 'admin') && m.role !== 'owner' (active.myRole === 'owner' || active.myRole === 'admin') && m.role !== 'owner'
? [ ? [
@ -230,7 +181,7 @@ export default function TeamsPageH5({ logic }: Props) {
await handleRemoveMember(m.id); await handleRemoveMember(m.id);
}} }}
> >
<Button size="small" danger style={{ borderRadius: 6 }}> <Button size="small" danger className="teams-page-web-soft-btn">
</Button> </Button>
</Popconfirm>, </Popconfirm>,
@ -240,52 +191,25 @@ export default function TeamsPageH5({ logic }: Props) {
> >
<List.Item.Meta <List.Item.Meta
avatar={ avatar={
<div <div className="teams-page-h5-member-avatar">
style={{
width: 36,
height: 36,
borderRadius: 999,
background: 'rgba(8, 145, 178, 0.10)',
color: 'var(--color-brand)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<UserOutlined /> <UserOutlined />
</div> </div>
} }
title={ title={
<Space> <Space>
<span style={{ fontWeight: 600, fontSize: 14 }}>{m.name}</span> <span className="teams-page-h5-member-name">{m.name}</span>
<Tag <Tag
bordered={false} bordered={false}
style={{ className={`teams-page-h5-tag teams-page-h5-tag-role-${m.role}`}
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,
fontSize: 10,
}}
> >
{m.role} {m.role}
</Tag> </Tag>
</Space> </Space>
} }
description={ description={
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}> <div className="teams-page-h5-member-desc">
<span style={{ fontSize: 12 }}>{m.email}</span> <span className="teams-page-h5-member-email">{m.email}</span>
<span style={{ fontSize: 11, color: 'var(--color-text-tertiary)' }}> <span className="teams-page-h5-member-time">
{new Date(m.joinedAt).toLocaleDateString('zh-CN')} {new Date(m.joinedAt).toLocaleDateString('zh-CN')}
</span> </span>
</div> </div>
@ -317,63 +241,27 @@ export default function TeamsPageH5({ logic }: Props) {
</Form> </Form>
</Modal> </Modal>
<Modal <Modal open={logic.renameOpen} title="重命名团队" onCancel={() => logic.setRenameOpen(false)} footer={null} width="95%" destroyOnHidden>
open={logic.inviteOpen} <Form layout="vertical" onFinish={logic.handleRename} initialValues={{ name: active?.name }}>
title={`📨 邀请加入 ${active?.name}`} <Form.Item name="name" label="新团队名称" rules={[{ required: true }]}>
onCancel={() => { <Input placeholder="输入新的团队名称" autoFocus />
logic.setInviteOpen(false); </Form.Item>
logic.setLastInviteCode(null); <Button type="primary" htmlType="submit" block>
}}
width="95%" </Button>
footer={null} </Form>
destroyOnHidden </Modal>
>
{logic.lastInviteCode ? ( <Modal open={logic.addMemberOpen} title={`${active?.name} 添加成员`} onCancel={() => logic.setAddMemberOpen(false)} footer={null} width="95%" destroyOnHidden>
<div> <Form layout="vertical" onFinish={logic.handleAddMember}>
<div style={{ marginBottom: 12 }}></div> <Form.Item name="phone" label="成员手机号" rules={[{ required: true, message: '请输入手机号' }]}>
<Input.TextArea <Input placeholder="输入成员绑定的手机号" autoFocus />
value={logic.lastInviteCode} </Form.Item>
readOnly <Button type="primary" htmlType="submit" block>
autoSize
style={{ fontFamily: 'monospace', fontSize: 14 }} </Button>
/> </Form>
<Button
type="default"
icon={<CopyOutlined />}
style={{ marginTop: 12, borderRadius: 8 }}
onClick={() => {
navigator.clipboard?.writeText(logic.lastInviteCode || '').then(() => message.success('邀请码已复制'));
}}
>
</Button>
<div style={{ color: 'var(--color-text-secondary)', fontSize: 11, marginTop: 8 }}>
</div>
</div>
) : (
<Form layout="vertical" onFinish={logic.handleInvite}>
<Form.Item
name="phone"
label="限定手机号(可选)"
rules={[
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式' }
]}
>
<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> </Modal>
</div>
<div className="feature-cover">
<Empty description="功能规划中,本期不支持" />
</div> </div>
</div> </div>
); );

View File

@ -1,382 +0,0 @@
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 { TeamAPI, type Team } from '../../../api';
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
interface Props {
logic: TeamsPageLogicOutput;
}
export default function TeamsPageWeb({ logic }: Props) {
const { message } = AntApp.useApp();
const {
list,
active,
handleDelete,
handleRemoveMember,
setCreateOpen,
} = logic;
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)', marginBottom: 4 }}></div>
</div>
{list.length === 0 ? (
<Empty description="还没有团队" />
) : (
<List
dataSource={list}
renderItem={(item) => (
<div
className={`nav-item ${active?.id === item.id ? 'active' : ''}`}
onClick={async () => {
logic.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={() => logic.setInviteOpen(true)} style={{ borderRadius: 12 }}>
</Button>
)}
{active.myRole === 'owner' && (
<Popconfirm
title="确定删除该团队?团队内的智能体会变成 owner 私有"
onConfirm={async () => {
await handleDelete(active.id);
message.success('已删除');
}}
>
<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 handleRemoveMember(m.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={logic.createOpen}
title="新建团队"
onCancel={() => logic.setCreateOpen(false)}
footer={null}
destroyOnHidden
>
<Form layout="vertical" onFinish={logic.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={logic.inviteOpen}
title={`📨 邀请加入 ${active?.name}`}
onCancel={() => {
logic.setInviteOpen(false);
logic.setLastInviteCode(null);
}}
footer={null}
destroyOnHidden
>
{logic.lastInviteCode ? (
<div>
<div style={{ marginBottom: 12 }}></div>
<Input.TextArea
value={logic.lastInviteCode}
readOnly
autoSize
style={{ fontFamily: 'monospace', fontSize: 16 }}
/>
<Button
type="default"
icon={<CopyOutlined />}
style={{ marginTop: 12, borderRadius: 10 }}
onClick={() => {
navigator.clipboard?.writeText(logic.lastInviteCode || '').then(() => message.success('邀请码已复制'));
}}
>
</Button>
<div style={{ color: 'var(--color-text-secondary)', fontSize: 12, marginTop: 8 }}>
</div>
</div>
) : (
<Form layout="vertical" onFinish={logic.handleInvite}>
<Form.Item
name="phone"
label="限定手机号(可选)"
rules={[
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式' }
]}
>
<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>
);
}

View File

@ -6,6 +6,7 @@ import TeamsPageWebDetail from './TeamsPageWebDetail';
import TeamsPageWebHero from './TeamsPageWebHero'; import TeamsPageWebHero from './TeamsPageWebHero';
import TeamsPageWebList from './TeamsPageWebList'; import TeamsPageWebList from './TeamsPageWebList';
import TeamsPageWebModals from './TeamsPageWebModals'; import TeamsPageWebModals from './TeamsPageWebModals';
import '../styles/teams-page-web.css';
export interface TeamsPageWebVariantProps { export interface TeamsPageWebVariantProps {
logic: TeamsPageLogicOutput; logic: TeamsPageLogicOutput;
@ -23,9 +24,6 @@ export default function TeamsPageWebBase({ logic, viewport }: TeamsPageWebVarian
</div> </div>
<TeamsPageWebModals logic={logic} /> <TeamsPageWebModals logic={logic} />
</div> </div>
<div className="feature-cover">
<Empty description="功能规划中,本期不支持" />
</div>
</div> </div>
); );
} }

View File

@ -1,4 +1,4 @@
import { DeleteOutlined, MailOutlined, UserOutlined } from '@ant-design/icons'; import { DeleteOutlined, EditOutlined, UserAddOutlined, UserOutlined } from '@ant-design/icons';
import { Button, Card, Empty, List, Popconfirm, Space, Tag } from 'antd'; import { Button, Card, Empty, List, Popconfirm, Space, Tag } from 'antd';
import type { Team } from '../../../api'; import type { Team } from '../../../api';
import type { TeamsPageLogicOutput } from '../TeamsPageLogic'; import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
@ -34,22 +34,26 @@ export default function TeamsPageWebDetail({ logic }: { logic: TeamsPageLogicOut
const canManage = active.myRole === 'owner' || active.myRole === 'admin'; const canManage = active.myRole === 'owner' || active.myRole === 'admin';
return ( return (
<Card className="teams-page-web-detail-card" styles={{ body: { padding: 22 } }}> <Card className="teams-page-web-detail-card">
<div className="teams-page-web-detail-head"> <div className="teams-page-web-detail-head">
<div> <div>
<div className="teams-page-web-active-title-row"> <div className="teams-page-web-active-title-row">
<span className="teams-page-web-active-title">{active.name}</span> <span className="teams-page-web-active-title">{active.name}</span>
{canManage && (
<Button
type="text"
size="small"
icon={<EditOutlined />}
onClick={() => logic.setRenameOpen(true)}
className="teams-page-web-edit-btn"
/>
)}
<Tag bordered={false} className="teams-page-web-tag-brand">{active.myRole}</Tag> <Tag bordered={false} className="teams-page-web-tag-brand">{active.myRole}</Tag>
<Tag bordered={false} className="teams-page-web-tag-neutral">{active.agentCount ?? 0} </Tag> <Tag bordered={false} className="teams-page-web-tag-neutral">{active.agentCount ?? 0} </Tag>
</div> </div>
<div className="teams-page-web-section-desc"></div> <div className="teams-page-web-section-desc"></div>
</div> </div>
<Space wrap> <Space wrap>
{canManage && (
<Button icon={<MailOutlined />} onClick={() => logic.setInviteOpen(true)} className="teams-page-web-soft-btn">
</Button>
)}
{active.myRole === 'owner' && ( {active.myRole === 'owner' && (
<Popconfirm title="确定删除该团队?团队内的智能体会变成 owner 私有" onConfirm={() => handleDelete(active.id)}> <Popconfirm title="确定删除该团队?团队内的智能体会变成 owner 私有" onConfirm={() => handleDelete(active.id)}>
<Button danger icon={<DeleteOutlined />} className="teams-page-web-soft-btn"> <Button danger icon={<DeleteOutlined />} className="teams-page-web-soft-btn">
@ -62,26 +66,45 @@ export default function TeamsPageWebDetail({ logic }: { logic: TeamsPageLogicOut
<TeamSummary active={active} /> <TeamSummary active={active} />
<div className="teams-page-web-members-title"> ({active.members?.length || 0})</div> <div className="teams-page-web-members-title-row">
<List <div className="teams-page-web-members-title"> ({active.members?.length || 0})</div>
dataSource={active.members || []} {canManage && (
renderItem={(member) => ( <Button
<List.Item type="primary"
className="teams-page-web-member-item" size="small"
actions={canManage && member.role !== 'owner' ? [ ghost
<Popconfirm key="kick" title="移除该成员?" onConfirm={() => handleRemoveMember(member.id)}> icon={<UserAddOutlined />}
<Button size="small" danger className="teams-page-web-soft-btn"></Button> onClick={() => logic.setAddMemberOpen(true)}
</Popconfirm>,
] : []}
> >
<List.Item.Meta
avatar={<div className="teams-page-web-member-avatar"><UserOutlined /></div>} </Button>
title={<Space wrap><span className="teams-page-web-member-name">{member.name}</span><Tag bordered={false} className={roleClass(member.role)}>{member.role}</Tag></Space>}
description={<div className="teams-page-web-member-desc"><span>{member.email}</span><span> {new Date(member.joinedAt).toLocaleDateString('zh-CN')}</span></div>}
/>
</List.Item>
)} )}
/> </div>
<div className="teams-page-web-member-list">
{(active.members || []).map((member) => (
<div className="teams-page-web-member-item" key={member.id}>
<div className="teams-page-web-member-info">
<div className="teams-page-web-member-avatar">
<UserOutlined />
</div>
<div className="teams-page-web-member-meta">
<div className="teams-page-web-member-name-row">
<span className="teams-page-web-member-name">{member.name}</span>
<Tag bordered={false} className={roleClass(member.role)}>{member.role}</Tag>
</div>
<div className="teams-page-web-member-email">{member.email}</div>
<div className="teams-page-web-member-time"> {new Date(member.joinedAt).toLocaleDateString('zh-CN')}</div>
</div>
</div>
{canManage && member.role !== 'owner' && (
<Popconfirm title="移除该成员?" onConfirm={() => handleRemoveMember(member.id)}>
<Button size="small" danger className="teams-page-web-soft-btn"></Button>
</Popconfirm>
)}
</div>
))}
</div>
</Card> </Card>
); );
} }

View File

@ -5,9 +5,9 @@ import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
export default function TeamsPageWebHero({ logic }: { logic: TeamsPageLogicOutput }) { export default function TeamsPageWebHero({ logic }: { logic: TeamsPageLogicOutput }) {
const { list, active, setCreateOpen } = logic; const { list, active, setCreateOpen } = logic;
const stats = [ const stats = [
{ label: '团队数量', value: list.length, tone: 'rgba(8, 145, 178, 0.10)', color: 'var(--color-brand)' }, { label: '团队数量', value: list.length, type: 'brand' },
{ label: '当前成员数', value: active?.members?.length ?? 0, tone: 'rgba(14, 165, 233, 0.10)', color: 'var(--color-info)' }, { label: '当前成员数', value: active?.members?.length ?? 0, type: 'info' },
{ label: '共享智能体', value: active?.agentCount ?? 0, tone: 'rgba(34, 197, 94, 0.10)', color: 'var(--color-success)' }, { label: '共享智能体', value: active?.agentCount ?? 0, type: 'success' },
]; ];
return ( return (
@ -34,7 +34,7 @@ export default function TeamsPageWebHero({ logic }: { logic: TeamsPageLogicOutpu
<div className="teams-page-web-stat-label">{item.label}</div> <div className="teams-page-web-stat-label">{item.label}</div>
<div className="teams-page-web-stat-row"> <div className="teams-page-web-stat-row">
<span className="teams-page-web-stat-value">{item.value}</span> <span className="teams-page-web-stat-value">{item.value}</span>
<span className="teams-page-web-stat-chip" style={{ background: item.tone, color: item.color }}> <span className={`teams-page-web-stat-chip teams-page-web-stat-chip-${item.type}`}>
</span> </span>
</div> </div>

View File

@ -14,19 +14,21 @@ export default function TeamsPageWebList({ logic }: { logic: TeamsPageLogicOutpu
{list.length === 0 ? ( {list.length === 0 ? (
<Empty description="还没有团队" /> <Empty description="还没有团队" />
) : ( ) : (
<List <div className="teams-page-web-list-items">
dataSource={list} {list.map((item) => (
renderItem={(item) => (
<button <button
key={item.id}
className={`teams-page-web-nav-item ${active?.id === item.id ? 'active' : ''}`} className={`teams-page-web-nav-item ${active?.id === item.id ? 'active' : ''}`}
onClick={async () => setActive(await TeamAPI.detail(item.id))} onClick={async () => setActive(await TeamAPI.detail(item.id))}
type="button" type="button"
> >
<span>{item.name}</span> <div className="teams-page-web-nav-item-content">
<small>{item.agentCount ?? 0} </small> <span className="teams-page-web-nav-item-name">{item.name}</span>
<small className="teams-page-web-nav-item-count">{item.agentCount ?? 0} </small>
</div>
</button> </button>
)} ))}
/> </div>
)} )}
</aside> </aside>
); );

View File

@ -1,4 +1,3 @@
import { CopyOutlined } from '@ant-design/icons';
import { App as AntApp, Button, Form, Input, Modal } from 'antd'; import { App as AntApp, Button, Form, Input, Modal } from 'antd';
import type { TeamsPageLogicOutput } from '../TeamsPageLogic'; import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
@ -19,42 +18,26 @@ export default function TeamsPageWebModals({ logic }: { logic: TeamsPageLogicOut
</Form> </Form>
</Modal> </Modal>
<Modal <Modal open={logic.renameOpen} title="重命名团队" onCancel={() => logic.setRenameOpen(false)} footer={null} destroyOnHidden>
open={logic.inviteOpen} <Form layout="vertical" onFinish={logic.handleRename} initialValues={{ name: active?.name }}>
title={`邀请加入 ${active?.name}`} <Form.Item name="name" label="新团队名称" rules={[{ required: true }]}>
onCancel={() => { <Input placeholder="输入新的团队名称" autoFocus />
logic.setInviteOpen(false); </Form.Item>
logic.setLastInviteCode(null); <Button type="primary" htmlType="submit" block>
}}
footer={null} </Button>
destroyOnHidden </Form>
> </Modal>
{logic.lastInviteCode ? (
<div> <Modal open={logic.addMemberOpen} title={`${active?.name} 添加成员`} onCancel={() => logic.setAddMemberOpen(false)} footer={null} destroyOnHidden>
<div className="teams-page-web-invite-label"></div> <Form layout="vertical" onFinish={logic.handleAddMember}>
<Input.TextArea value={logic.lastInviteCode} readOnly autoSize className="teams-page-web-invite-code" /> <Form.Item name="phone" label="成员手机号" rules={[{ required: true, message: '请输入手机号' }]}>
<Button <Input placeholder="输入成员绑定的手机号" autoFocus />
icon={<CopyOutlined />} </Form.Item>
className="teams-page-web-copy-btn" <Button type="primary" htmlType="submit" block>
onClick={() => navigator.clipboard?.writeText(logic.lastInviteCode || '').then(() => message.success('邀请码已复制'))}
> </Button>
</Form>
</Button>
<div className="teams-page-web-invite-tip"></div>
</div>
) : (
<Form layout="vertical" onFinish={logic.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> </Modal>
</> </>
); );

View File

@ -0,0 +1,326 @@
.h5-page-container {
padding: 0 8px;
}
.teams-page-h5-hero {
border-radius: 16px;
padding: 20px 16px 18px;
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);
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06);
margin-bottom: 16px;
}
.teams-page-h5-hero-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.teams-page-h5-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 999px;
background: rgba(255,255,255,0.78);
border: 1px solid rgba(8, 145, 178, 0.10);
color: var(--color-text-secondary);
font-size: 11px;
font-weight: 600;
margin-bottom: 12px;
}
.teams-page-h5-title {
margin-bottom: 8px;
font-size: 22px;
}
.teams-page-h5-subtitle {
margin-top: 0;
font-size: 13px;
line-height: 1.6;
}
.teams-page-h5-create-btn {
border-radius: 10px;
height: 40px;
padding: 0 14px;
font-weight: 600;
width: 100%;
}
.teams-page-h5-stats-grid {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 10px;
}
.teams-page-h5-stat-card {
border-radius: 12px;
padding: 12px 14px;
background: rgba(255,255,255,0.72);
border: 1px solid rgba(255,255,255,0.7);
}
.teams-page-h5-stat-label {
font-size: 11px;
color: var(--color-text-secondary);
margin-bottom: 6px;
}
.teams-page-h5-stat-row {
display: flex;
align-items: baseline;
gap: 6px;
}
.teams-page-h5-stat-value {
font-size: 22px;
font-weight: 700;
color: var(--color-text);
}
.teams-page-h5-stat-chip {
border-radius: 999px;
padding: 3px 6px;
font-size: 10px;
font-weight: 600;
}
.teams-page-h5-stat-chip-brand {
background: var(--color-brand-soft);
color: var(--color-brand);
}
.teams-page-h5-stat-chip-info {
background: var(--color-info-soft);
color: var(--color-info);
}
.teams-page-h5-stat-chip-success {
background: var(--color-success-soft);
color: var(--color-success);
}
.teams-page-h5-list-panel {
background: linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(252,252,253,1) 100%);
border: 1px solid var(--color-border);
border-radius: 16px;
padding: 12px;
margin-bottom: 16px;
}
.teams-page-h5-list-head {
padding: 4px 6px 10px;
}
.teams-page-h5-section-title {
font-size: 14px;
font-weight: 700;
color: var(--color-text);
margin-bottom: 4px;
}
.teams-page-h5-section-desc {
font-size: 11px;
color: var(--color-text-secondary);
}
.teams-page-h5-nav-item {
padding: 10px 12px;
border-radius: 10px;
cursor: pointer;
margin-bottom: 6px;
background: transparent;
color: var(--color-text-secondary);
font-weight: 500;
border: 1px solid transparent;
}
.teams-page-h5-nav-item.active {
background: rgba(8, 145, 178, 0.10);
color: var(--color-brand);
font-weight: 600;
border: 1px solid rgba(8, 145, 178, 0.16);
}
.teams-page-h5-nav-item-name {
font-size: 13px;
margin-bottom: 3px;
}
.teams-page-h5-nav-item-count {
font-size: 11px;
color: var(--color-text-tertiary);
}
.teams-page-h5-nav-item.active .teams-page-h5-nav-item-count {
color: var(--color-brand);
}
.teams-page-h5-detail-card {
border-radius: 16px;
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.045);
}
.teams-page-web-soft-btn {
border-radius: 8px !important;
}
.teams-page-h5-detail-card .ant-card-body {
padding: 16px !important;
}
.teams-page-h5-detail-head {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.teams-page-h5-active-title-row {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
margin-bottom: 4px;
}
.teams-page-h5-active-title {
font-size: 18px;
font-weight: 700;
color: var(--color-text);
}
.teams-page-h5-edit-btn {
color: var(--color-text-tertiary);
padding: 0 4px;
}
.teams-page-h5-tag {
border-radius: 999px;
margin: 0;
font-size: 11px;
}
.teams-page-h5-tag-brand {
background: var(--color-brand-soft);
color: var(--color-brand);
}
.teams-page-h5-tag-neutral {
background: var(--color-surface-2);
color: var(--color-text-secondary);
}
.teams-page-h5-mini-grid {
display: grid;
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 10px;
margin-bottom: 16px;
}
.teams-page-h5-mini-card {
border-radius: 12px;
padding: 12px 14px;
border: 1px solid transparent;
}
.teams-page-h5-mini-card-brand {
background: rgba(8, 145, 178, 0.06);
border: 1px solid rgba(8, 145, 178, 0.10);
}
.teams-page-h5-mini-card-success {
background: rgba(34, 197, 94, 0.06);
border: 1px solid rgba(34, 197, 94, 0.10);
}
.teams-page-h5-mini-card-warning {
background: rgba(249, 115, 22, 0.06);
border: 1px solid rgba(249, 115, 22, 0.10);
}
.teams-page-h5-mini-label {
font-size: 11px;
color: var(--color-text-secondary);
margin-bottom: 6px;
}
.teams-page-h5-mini-value {
font-size: 20px;
font-weight: 700;
color: var(--color-text);
}
.teams-page-h5-members-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.teams-page-h5-members-title {
font-size: 14px;
font-weight: 700;
color: var(--color-text);
}
.teams-page-h5-member-item {
padding: 10px 0;
}
.teams-page-h5-member-avatar {
width: 36px;
height: 36px;
border-radius: 999px;
background: rgba(8, 145, 178, 0.10);
color: var(--color-brand);
display: flex;
align-items: center;
justify-content: center;
}
.teams-page-h5-member-avatar-icon {
color: var(--color-brand);
font-size: 12px;
}
.teams-page-h5-member-name {
font-weight: 600;
font-size: 14px;
}
.teams-page-h5-member-desc {
display: flex;
flex-direction: column;
gap: 4px;
}
.teams-page-h5-member-email {
font-size: 12px;
}
.teams-page-h5-member-time {
font-size: 11px;
color: var(--color-text-tertiary);
}
.teams-page-h5-tag-role-owner {
background: var(--color-warning-soft);
color: var(--color-warning);
}
.teams-page-h5-tag-role-admin {
background: var(--color-info-soft);
color: var(--color-info);
}
.teams-page-h5-tag-role-member {
background: var(--color-surface-2);
color: var(--color-text-secondary);
}

View File

@ -33,6 +33,8 @@
.teams-page-web-mini-card { .teams-page-web-mini-card {
border: 1px solid rgba(8, 145, 178, 0.1); border: 1px solid rgba(8, 145, 178, 0.1);
border-radius: 8px;
padding: 12px;
} }
.teams-page-web-mini-brand { .teams-page-web-mini-brand {
@ -55,51 +57,83 @@
text-transform: capitalize; text-transform: capitalize;
} }
.teams-page-web-members-title { .teams-page-web-detail-card .ant-card-body {
margin-bottom: 14px; padding: 24px !important;
font-size: 16px; }
.teams-page-web-members-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 24px;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-border-soft);
}
.teams-page-web-member-list {
display: flex;
flex-direction: column;
} }
.teams-page-web-member-item { .teams-page-web-member-item {
padding: 14px 0; display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid var(--color-border-soft);
}
.teams-page-web-member-item:last-child {
border-bottom: none;
}
.teams-page-web-member-info {
display: flex;
gap: 14px;
align-items: center;
} }
.teams-page-web-member-avatar { .teams-page-web-member-avatar {
width: 42px; width: 44px;
height: 42px; height: 44px;
display: grid; display: flex;
place-items: center; align-items: center;
border-radius: 999px; justify-content: center;
background: rgba(8, 145, 178, 0.1); border-radius: 12px;
background: var(--color-brand-soft);
color: var(--color-brand); color: var(--color-brand);
font-size: 20px;
}
.teams-page-web-member-meta {
display: flex;
flex-direction: column;
gap: 4px;
}
.teams-page-web-member-name-row {
display: flex;
align-items: center;
gap: 8px;
} }
.teams-page-web-member-name { .teams-page-web-member-name {
font-size: 15px;
font-weight: 700; font-weight: 700;
color: var(--color-text);
} }
.teams-page-web-member-desc { .teams-page-web-member-email {
display: grid; font-size: 13px;
gap: 4px; color: var(--color-text-secondary);
}
.teams-page-web-member-time {
font-size: 12px;
color: var(--color-text-tertiary);
} }
.teams-page-web-empty-detail { .teams-page-web-empty-detail {
align-self: center; align-self: center;
} }
.teams-page-web-invite-label {
margin-bottom: 12px;
}
.teams-page-web-invite-code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 16px;
}
.teams-page-web-copy-btn {
margin-top: 12px;
}
.teams-page-web-invite-tip {
margin-top: 8px;
}

View File

@ -44,20 +44,35 @@
font-weight: 700; font-weight: 700;
} }
.teams-page-web-badge .anticon {
color: var(--color-brand);
}
.teams-page-web-title { .teams-page-web-title {
margin-bottom: 10px; margin-bottom: 8px;
font-size: 28px;
font-weight: 850;
color: var(--color-text);
letter-spacing: -0.02em;
} }
.teams-page-web-subtitle { .teams-page-web-subtitle {
margin-top: 0; margin-top: 0;
font-size: 15px; font-size: 14.5px;
line-height: 1.75; line-height: 1.6;
color: var(--color-text-secondary);
max-width: 580px;
} }
.teams-page-web-create-btn, .teams-page-web-create-btn {
.teams-page-web-soft-btn, border-radius: 14px !important;
.teams-page-web-copy-btn { height: 46px !important;
border-radius: 12px; padding: 0 18px !important;
font-weight: 600 !important;
}
.teams-page-web-soft-btn {
border-radius: 12px !important;
} }
.teams-page-web-stats-grid, .teams-page-web-stats-grid,
@ -69,25 +84,19 @@
.teams-page-web-stats-grid { .teams-page-web-stats-grid {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 20px; margin-top: 24px;
} gap: 16px; /* Optimized gap for stats grid */
.teams-page-web-stat-card,
.teams-page-web-mini-card {
border-radius: 18px;
padding: 16px 18px;
} }
.teams-page-web-stat-card { .teams-page-web-stat-card {
background: rgba(255,255,255,0.72); border-radius: 18px;
border: 1px solid rgba(255,255,255,0.7); padding: 16px 18px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(255, 255, 255, 0.7);
} }
.teams-page-web-stat-label, .teams-page-web-stat-label,
.teams-page-web-section-desc, .teams-page-web-section-desc {
.teams-page-web-mini-label,
.teams-page-web-member-desc,
.teams-page-web-invite-tip {
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: 12.5px; font-size: 12.5px;
} }
@ -102,14 +111,30 @@
.teams-page-web-stat-value { .teams-page-web-stat-value {
color: var(--color-text); color: var(--color-text);
font-size: 30px; font-size: 30px;
font-weight: 800; font-weight: 700;
} }
.teams-page-web-stat-chip { .teams-page-web-stat-chip {
border-radius: 999px; border-radius: 999px;
padding: 4px 8px; padding: 4px 10px;
font-size: 12px; font-size: 11px;
font-weight: 700; font-weight: 700;
letter-spacing: 0.02em;
}
.teams-page-web-stat-chip-brand {
background: var(--color-brand-soft);
color: var(--color-brand);
}
.teams-page-web-stat-chip-info {
background: var(--color-info-soft);
color: var(--color-info);
}
.teams-page-web-stat-chip-success {
background: var(--color-success-soft);
color: var(--color-success);
} }
.teams-page-web-main-grid { .teams-page-web-main-grid {
@ -144,29 +169,56 @@
font-size: 15px; font-size: 15px;
} }
.teams-page-web-list-items {
display: flex;
flex-direction: column;
gap: 2px;
}
.teams-page-web-nav-item { .teams-page-web-nav-item {
width: 100%; width: 100%;
display: grid;
gap: 4px;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 14px; border-radius: 12px;
padding: 10px 12px; padding: 10px 14px;
margin-bottom: 6px;
background: transparent; background: transparent;
color: var(--color-text-secondary); color: var(--color-text-secondary);
cursor: pointer; cursor: pointer;
text-align: left; text-align: left;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.teams-page-web-nav-item:hover {
background: var(--color-surface-2);
color: var(--color-text);
} }
.teams-page-web-nav-item.active { .teams-page-web-nav-item.active {
background: rgba(8, 145, 178, 0.1); background: var(--color-brand-soft);
border-color: rgba(8, 145, 178, 0.16); border-color: rgba(8, 145, 178, 0.12);
color: var(--color-brand); color: var(--color-brand);
font-weight: 700;
} }
.teams-page-web-nav-item small { .teams-page-web-nav-item-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.teams-page-web-nav-item-name {
font-size: 14px;
font-weight: 600;
line-height: 1.4;
}
.teams-page-web-nav-item-count {
font-size: 12px;
color: var(--color-text-tertiary); color: var(--color-text-tertiary);
font-weight: 400;
}
.teams-page-web-nav-item.active .teams-page-web-nav-item-count {
color: var(--color-brand);
opacity: 0.8;
} }
.teams-page-web-detail-head { .teams-page-web-detail-head {

View File

@ -75,10 +75,6 @@
color: var(--color-text); color: var(--color-text);
} }
.streaming-section {
margin-bottom: 16px;
}
.streaming-section-label { .streaming-section-label {
font-size: 12px; font-size: 12px;
color: var(--color-text-tertiary); color: var(--color-text-tertiary);

View File

@ -29,10 +29,14 @@ export default function ChatBody(props: {
const [streamingReasoningExpanded, setStreamingReasoningExpanded] = useState(true); const [streamingReasoningExpanded, setStreamingReasoningExpanded] = useState(true);
// 当开始新的回答时,默认展开推理过程 // 当开始新的回答时,默认展开推理过程;当正式回答开始时,自动折叠推理过程
useEffect(() => { useEffect(() => {
if (streaming.active && !streaming.answerText) { if (streaming.active) {
setStreamingReasoningExpanded(true); if (!streaming.answerText) {
setStreamingReasoningExpanded(true);
} else {
setStreamingReasoningExpanded(false);
}
} }
}, [streaming.active, !!streaming.answerText]); }, [streaming.active, !!streaming.answerText]);
@ -116,11 +120,15 @@ export default function ChatBody(props: {
onToggle={() => setStreamingReasoningExpanded(!streamingReasoningExpanded)} onToggle={() => setStreamingReasoningExpanded(!streamingReasoningExpanded)}
/> />
</div> </div>
{streamingReasoningExpanded && <Divider style={{ margin: '8px 0' }} />} {streaming.answerText && (
<div className="streaming-section"> <>
<div className="streaming-section-label"></div> <Divider style={{ margin: '8px 0' }} />
{streaming.answerText ? <Markdown>{streaming.answerText + '▍'}</Markdown> : <span style={{ color: 'var(--color-text-tertiary)' }}></span>} <div className="streaming-section">
</div> <div className="streaming-section-label"></div>
<Markdown>{streaming.answerText + '▍'}</Markdown>
</div>
</>
)}
</div> </div>
</div> </div>
{(streaming.retrieved.length > 0 || streaming.toolCalls.length > 0) && ( {(streaming.retrieved.length > 0 || streaming.toolCalls.length > 0) && (

View File

@ -1583,7 +1583,8 @@ span.mention {
} }
.points-mall-banner-section { .points-mall-banner-section {
display: grid; /* display: grid; */
display: none;
grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
gap: 14px; gap: 14px;
margin-bottom: 12px; margin-bottom: 12px;