feat: add team feature
parent
c7c580a388
commit
23d1df700c
|
|
@ -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),
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: '智能体广场' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,23 +56,34 @@ export default function ProfileWeb({ logic }: Props) {
|
|||
title: '已授权资源',
|
||||
key: 'resources',
|
||||
render: (_: any, record: any) => (
|
||||
<div className="resource-tags">
|
||||
{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' }}
|
||||
<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', 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 || '未知智能体'}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
{(!record.authorizedAgentIds || record.authorizedAgentIds.length === 0) && (
|
||||
<span style={{ color: 'var(--color-text-tertiary)', fontSize: 12 }}>暂无授权</span>
|
||||
)}
|
||||
<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
|
||||
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: 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%' }}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }}>
|
||||
成员 ({active.members?.length || 0})
|
||||
<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 天" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" block>
|
||||
生成邀请码
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
<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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">成员 ({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>,
|
||||
] : []}
|
||||
<div className="teams-page-web-members-title-row">
|
||||
<div className="teams-page-web-members-title">成员 ({active.members?.length || 0})</div>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 天" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" block>
|
||||
生成邀请码
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
background: rgba(255,255,255,0.72);
|
||||
border: 1px solid rgba(255,255,255,0.7);
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -75,10 +75,6 @@
|
|||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.streaming-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.streaming-section-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-tertiary);
|
||||
|
|
|
|||
|
|
@ -29,10 +29,14 @@ export default function ChatBody(props: {
|
|||
|
||||
const [streamingReasoningExpanded, setStreamingReasoningExpanded] = useState(true);
|
||||
|
||||
// 当开始新的回答时,默认展开推理过程
|
||||
// 当开始新的回答时,默认展开推理过程;当正式回答开始时,自动折叠推理过程
|
||||
useEffect(() => {
|
||||
if (streaming.active && !streaming.answerText) {
|
||||
setStreamingReasoningExpanded(true);
|
||||
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' }} />}
|
||||
<div className="streaming-section">
|
||||
<div className="streaming-section-label">正式回答</div>
|
||||
{streaming.answerText ? <Markdown>{streaming.answerText + '▍'}</Markdown> : <span style={{ color: 'var(--color-text-tertiary)' }}>等待输出…</span>}
|
||||
</div>
|
||||
{streaming.answerText && (
|
||||
<>
|
||||
<Divider style={{ margin: '8px 0' }} />
|
||||
<div className="streaming-section">
|
||||
<div className="streaming-section-label">正式回答</div>
|
||||
<Markdown>{streaming.answerText + '▍'}</Markdown>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{(streaming.retrieved.length > 0 || streaming.toolCalls.length > 0) && (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue