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),
/**
*
*/
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),
/**

View File

@ -15,6 +15,7 @@ export const TeamAPI = {
detail: (id: string) => api.get<Team>(`/teams/${id}`).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),
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),
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}
destroyOnHidden
afterOpenChange={handleAfterOpenChange}
maskClosable={false}
styles={{
body: {
maxHeight: '70vh',
overflowY: 'auto',
paddingRight: 12
}
}}
>
<Form form={form} layout="vertical" requiredMark="optional" preserve={true}>
<Tabs

View File

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

View File

@ -126,14 +126,14 @@
.agent-card-desc-container {
margin-top: 16px;
padding: 16px 16px 18px;
padding: 16px;
border-radius: 16px;
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);
}
.agent-card-desc {
min-height: 66px;
min-height: 48px;
font-size: 13.5px;
line-height: 1.7;
display: -webkit-box;

View File

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

View File

@ -30,8 +30,8 @@ export default function AgentListWebBase({ logic, viewport }: AgentListWebVarian
AI 广
</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>
</div>
@ -53,8 +53,8 @@ export default function AgentListWebBase({ logic, viewport }: AgentListWebVarian
{!loading && list.length === 0 ? (
<div className="empty-state">
<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>
</Empty>
</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 {
await MembershipAPI.authorizeResource({ userId, resourceType: 'agent', resourceId: agentId, level });
await MembershipAPI.authorizeResource({ userId, resourceType: 'agent', resourceIds: agentIds, level });
message.success('授权成功');
const mList = await MembershipAPI.listMembers();
setMembers(mList);
@ -109,9 +109,9 @@ export function useProfileLogic() {
}
};
const handleRevokeAgent = async (userId: string, agentId: string) => {
const handleRevokeAgent = async (userId: string, agentIds: string[]) => {
try {
await MembershipAPI.revokeResource({ userId, resourceType: 'agent', resourceId: agentId });
await MembershipAPI.revokeResource({ userId, resourceType: 'agent', resourceIds: agentIds });
message.success('取消授权成功');
const mList = await MembershipAPI.listMembers();
setMembers(mList);

View File

@ -1,5 +1,5 @@
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 type { ProfileLogicOutput } from '../ProfileLogic';
import '../styles/profile.css';
@ -30,7 +30,7 @@ export default function ProfileH5({ logic }: Props) {
const [newChildId, setNewChildId] = useState('');
const [selectedMember, setSelectedMember] = useState<any>(null);
const [quotaAmount, setQuotaAmount] = useState<number>(100000);
const [selectedAgentId, setSelectedAgentId] = useState<string>('');
const [selectedAgentIds, setSelectedAgentIds] = useState<string[]>([]);
return (
<div className="profile-page profile-page-h5">
@ -90,7 +90,14 @@ export default function ProfileH5({ logic }: Props) {
style={{ marginBottom: 12, borderRadius: 12 }}
actions={[
<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)}>
<DeleteOutlined style={{ color: 'var(--color-error)' }} />
</Popconfirm>,
@ -112,13 +119,23 @@ export default function ProfileH5({ logic }: Props) {
<Tag
key={id}
closable
onClose={() => handleRevokeAgent(record.child_user_id, id)}
style={{ borderRadius: 4, fontSize: 10, margin: '2px' }}
onClose={() => handleRevokeAgent(record.child_user_id, [id])}
style={{ borderRadius: 4, fontSize: 10, margin: '2px', display: 'flex', width: 'fit-content' }}
>
{agent?.name || '未知'}
🤖 {agent?.name || '未知'}
</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>
}
@ -163,12 +180,42 @@ export default function ProfileH5({ logic }: Props) {
<Modal
title={`${selectedMember?.child_name} 授权资源`}
open={authModalVisible}
onOk={() => { handleAuthorizeAgent(selectedMember.child_user_id, selectedAgentId, 'read'); setAuthModalVisible(false); setSelectedAgentId(''); }}
onCancel={() => setAuthModalVisible(false)}
onOk={() => {
handleAuthorizeAgent(selectedMember.child_user_id, selectedAgentIds, 'read');
setAuthModalVisible(false);
setSelectedAgentIds([]);
}}
onCancel={() => {
setAuthModalVisible(false);
setSelectedAgentIds([]);
}}
>
<Select style={{ width: '100%' }} placeholder="选择智能体" value={selectedAgentId} onChange={setSelectedAgentId}>
{myAgents.map(a => <Select.Option key={a.id} value={a.id}>{a.name}</Select.Option>)}
</Select>
<div style={{ marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontSize: 13, fontWeight: 500 }}></span>
<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>
</div>
);

View File

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

View File

@ -1,7 +1,8 @@
import { useEffect, useState } from 'react';
import { useTeamsPageLogic } from './TeamsPage/TeamsPageLogic';
import TeamsPageWeb from './TeamsPage/components/TeamsPageWeb';
import TeamsPageWebBase from './TeamsPage/components/TeamsPageWebBase';
import TeamsPageH5 from './TeamsPage/components/TeamsPageH5';
import { useDesktopViewport } from '../hooks/useDesktopViewport';
const isMobileDevice = () => {
if (typeof window === 'undefined') return false;
@ -10,6 +11,7 @@ const isMobileDevice = () => {
export default function TeamsPage() {
const logic = useTeamsPageLogic();
const viewport = useDesktopViewport();
const [isMobile, setIsMobile] = useState(isMobileDevice());
useEffect(() => {
@ -20,5 +22,9 @@ export default function TeamsPage() {
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 [active, setActive] = useState<Team | null>(null);
const [createOpen, setCreateOpen] = useState(false);
const [inviteOpen, setInviteOpen] = useState(false);
const [lastInviteCode, setLastInviteCode] = useState<string | null>(null);
const [addMemberOpen, setAddMemberOpen] = useState(false);
const [renameOpen, setRenameOpen] = useState(false);
const load = async () => {
const l = await TeamAPI.list();
@ -27,14 +27,22 @@ export function useTeamsPageLogic() {
setActive(await TeamAPI.detail(t.id));
};
const handleInvite = async (v: any) => {
const handleRename = async (v: any) => {
if (!active) return;
const inv = await AuthAPI.createInvite({
teamId: active.id,
phone: v.phone || undefined,
ttlHours: Number(v.ttlHours) || 168,
});
setLastInviteCode(inv.code);
await TeamAPI.rename(active.id, v.name);
setRenameOpen(false);
message.success('已更名');
await load();
setActive(await TeamAPI.detail(active.id));
};
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) => {
@ -44,10 +52,11 @@ export function useTeamsPageLogic() {
setActive(null);
};
const handleRemoveMember = async (id: string) => {
const handleRemoveMember = async (userId: string) => {
if (!active) return;
await TeamAPI.removeMember(active.id, id);
await TeamAPI.removeMember(active.id, userId);
message.success('已移除');
await load();
setActive(await TeamAPI.detail(active.id));
};
@ -55,17 +64,18 @@ export function useTeamsPageLogic() {
list,
active,
createOpen,
inviteOpen,
lastInviteCode,
addMemberOpen,
renameOpen,
load,
handleCreate,
handleInvite,
handleRename,
handleAddMember,
handleDelete,
handleRemoveMember,
setActive,
setCreateOpen,
setInviteOpen,
setLastInviteCode,
setAddMemberOpen,
setRenameOpen,
};
}

View File

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

View File

@ -5,9 +5,9 @@ import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
export default function TeamsPageWebHero({ logic }: { logic: TeamsPageLogicOutput }) {
const { list, active, setCreateOpen } = logic;
const stats = [
{ 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)' },
{ label: '团队数量', value: list.length, type: 'brand' },
{ label: '当前成员数', value: active?.members?.length ?? 0, type: 'info' },
{ label: '共享智能体', value: active?.agentCount ?? 0, type: 'success' },
];
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-row">
<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>
</div>

View File

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

View File

@ -1,4 +1,3 @@
import { CopyOutlined } from '@ant-design/icons';
import { App as AntApp, Button, Form, Input, Modal } from 'antd';
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
@ -19,42 +18,26 @@ export default function TeamsPageWebModals({ logic }: { logic: TeamsPageLogicOut
</Form>
</Modal>
<Modal
open={logic.inviteOpen}
title={`邀请加入 ${active?.name}`}
onCancel={() => {
logic.setInviteOpen(false);
logic.setLastInviteCode(null);
}}
footer={null}
destroyOnHidden
>
{logic.lastInviteCode ? (
<div>
<div className="teams-page-web-invite-label"></div>
<Input.TextArea value={logic.lastInviteCode} readOnly autoSize className="teams-page-web-invite-code" />
<Button
icon={<CopyOutlined />}
className="teams-page-web-copy-btn"
onClick={() => navigator.clipboard?.writeText(logic.lastInviteCode || '').then(() => message.success('邀请码已复制'))}
>
</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 天" />
<Modal open={logic.renameOpen} title="重命名团队" onCancel={() => logic.setRenameOpen(false)} footer={null} destroyOnHidden>
<Form layout="vertical" onFinish={logic.handleRename} initialValues={{ name: active?.name }}>
<Form.Item name="name" label="新团队名称" rules={[{ required: true }]}>
<Input placeholder="输入新的团队名称" autoFocus />
</Form.Item>
<Button type="primary" htmlType="submit" block>
</Button>
</Form>
</Modal>
<Modal open={logic.addMemberOpen} title={`${active?.name} 添加成员`} onCancel={() => logic.setAddMemberOpen(false)} footer={null} destroyOnHidden>
<Form layout="vertical" onFinish={logic.handleAddMember}>
<Form.Item name="phone" label="成员手机号" rules={[{ required: true, message: '请输入手机号' }]}>
<Input placeholder="输入成员绑定的手机号" autoFocus />
</Form.Item>
<Button type="primary" htmlType="submit" block>
</Button>
</Form>
)}
</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 {
border: 1px solid rgba(8, 145, 178, 0.1);
border-radius: 8px;
padding: 12px;
}
.teams-page-web-mini-brand {
@ -55,51 +57,83 @@
text-transform: capitalize;
}
.teams-page-web-members-title {
margin-bottom: 14px;
font-size: 16px;
.teams-page-web-detail-card .ant-card-body {
padding: 24px !important;
}
.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 {
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 {
width: 42px;
height: 42px;
display: grid;
place-items: center;
border-radius: 999px;
background: rgba(8, 145, 178, 0.1);
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
background: var(--color-brand-soft);
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 {
font-size: 15px;
font-weight: 700;
color: var(--color-text);
}
.teams-page-web-member-desc {
display: grid;
gap: 4px;
.teams-page-web-member-email {
font-size: 13px;
color: var(--color-text-secondary);
}
.teams-page-web-member-time {
font-size: 12px;
color: var(--color-text-tertiary);
}
.teams-page-web-empty-detail {
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;
}
.teams-page-web-badge .anticon {
color: var(--color-brand);
}
.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 {
margin-top: 0;
font-size: 15px;
line-height: 1.75;
font-size: 14.5px;
line-height: 1.6;
color: var(--color-text-secondary);
max-width: 580px;
}
.teams-page-web-create-btn,
.teams-page-web-soft-btn,
.teams-page-web-copy-btn {
border-radius: 12px;
.teams-page-web-create-btn {
border-radius: 14px !important;
height: 46px !important;
padding: 0 18px !important;
font-weight: 600 !important;
}
.teams-page-web-soft-btn {
border-radius: 12px !important;
}
.teams-page-web-stats-grid,
@ -69,25 +84,19 @@
.teams-page-web-stats-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 20px;
}
.teams-page-web-stat-card,
.teams-page-web-mini-card {
border-radius: 18px;
padding: 16px 18px;
margin-top: 24px;
gap: 16px; /* Optimized gap for stats grid */
}
.teams-page-web-stat-card {
border-radius: 18px;
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-section-desc,
.teams-page-web-mini-label,
.teams-page-web-member-desc,
.teams-page-web-invite-tip {
.teams-page-web-section-desc {
color: var(--color-text-secondary);
font-size: 12.5px;
}
@ -102,14 +111,30 @@
.teams-page-web-stat-value {
color: var(--color-text);
font-size: 30px;
font-weight: 800;
font-weight: 700;
}
.teams-page-web-stat-chip {
border-radius: 999px;
padding: 4px 8px;
font-size: 12px;
padding: 4px 10px;
font-size: 11px;
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 {
@ -144,29 +169,56 @@
font-size: 15px;
}
.teams-page-web-list-items {
display: flex;
flex-direction: column;
gap: 2px;
}
.teams-page-web-nav-item {
width: 100%;
display: grid;
gap: 4px;
border: 1px solid transparent;
border-radius: 14px;
padding: 10px 12px;
margin-bottom: 6px;
border-radius: 12px;
padding: 10px 14px;
background: transparent;
color: var(--color-text-secondary);
cursor: pointer;
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 {
background: rgba(8, 145, 178, 0.1);
border-color: rgba(8, 145, 178, 0.16);
background: var(--color-brand-soft);
border-color: rgba(8, 145, 178, 0.12);
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);
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 {

View File

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

View File

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

View File

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