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),
|
api.post<ApiResponse<any>>('/membership/access', payload).then(r => r.data),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消资源授权
|
* 取消资源授权
|
||||||
*/
|
*/
|
||||||
revokeResource: (payload: { userId: string; resourceType: 'agent' | 'knowledge'; resourceId: string }) =>
|
revokeResource: (payload: { userId: string; resourceType: 'agent' | 'knowledge'; resourceIds: string[] }) =>
|
||||||
api.delete<ApiResponse<any>>('/membership/access', { data: payload }).then(r => r.data),
|
api.delete<ApiResponse<any>>('/membership/access', { data: payload }).then(r => r.data),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export const TeamAPI = {
|
||||||
detail: (id: string) => api.get<Team>(`/teams/${id}`).then((r) => r.data),
|
detail: (id: string) => api.get<Team>(`/teams/${id}`).then((r) => r.data),
|
||||||
create: (name: string) => api.post<Team>('/teams', { name }).then((r) => r.data),
|
create: (name: string) => api.post<Team>('/teams', { name }).then((r) => r.data),
|
||||||
rename: (id: string, name: string) => api.put(`/teams/${id}`, { name }).then((r) => r.data),
|
rename: (id: string, name: string) => api.put(`/teams/${id}`, { name }).then((r) => r.data),
|
||||||
|
addMember: (id: string, phone: string) => api.post(`/teams/${id}/members`, { phone }).then((r) => r.data),
|
||||||
remove: (id: string) => api.delete(`/teams/${id}`).then((r) => r.data),
|
remove: (id: string) => api.delete(`/teams/${id}`).then((r) => r.data),
|
||||||
removeMember: (id: string, userId: string) => api.delete(`/teams/${id}/members/${userId}`).then((r) => r.data)
|
removeMember: (id: string, userId: string) => api.delete(`/teams/${id}/members/${userId}`).then((r) => r.data)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,14 @@ export default function ExternalToolEditor({ open, agentId, plugin, onClose, onS
|
||||||
onOk={handleSubmit}
|
onOk={handleSubmit}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
afterOpenChange={handleAfterOpenChange}
|
afterOpenChange={handleAfterOpenChange}
|
||||||
|
maskClosable={false}
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
maxHeight: '70vh',
|
||||||
|
overflowY: 'auto',
|
||||||
|
paddingRight: 12
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" requiredMark="optional" preserve={true}>
|
<Form form={form} layout="vertical" requiredMark="optional" preserve={true}>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ const NAV_GROUPS: Array<{
|
||||||
items: [
|
items: [
|
||||||
{ to: '/chat', icon: <MessageOutlined />, label: '聊天' },
|
{ to: '/chat', icon: <MessageOutlined />, label: '聊天' },
|
||||||
{ to: '/agents', icon: <RobotOutlined />, label: '我的智能体' },
|
{ to: '/agents', icon: <RobotOutlined />, label: '我的智能体' },
|
||||||
{ to: '/marketplace', icon: <CompassOutlined />, label: '智能体广场' }
|
// { to: '/marketplace', icon: <CompassOutlined />, label: '智能体广场' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,10 @@ export default function AgentListH5({ logic }: Props) {
|
||||||
type="primary"
|
type="primary"
|
||||||
size="middle"
|
size="middle"
|
||||||
icon={<CompassOutlined />}
|
icon={<CompassOutlined />}
|
||||||
onClick={() => navigate('/marketplace')}
|
onClick={() => navigate('/agents/new')}
|
||||||
className="h5-stats-marketplace-btn"
|
className="h5-stats-marketplace-btn"
|
||||||
>
|
>
|
||||||
前往智能体广场
|
创建智能体
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -69,8 +69,8 @@ export default function AgentListH5({ logic }: Props) {
|
||||||
{!loading && list.length === 0 ? (
|
{!loading && list.length === 0 ? (
|
||||||
<div className="empty-state h5-empty-state">
|
<div className="empty-state h5-empty-state">
|
||||||
<Empty description="你还没有任何智能体">
|
<Empty description="你还没有任何智能体">
|
||||||
<Button type="primary" onClick={() => navigate('/marketplace')} style={{ borderRadius: 8 }}>
|
<Button type="primary" onClick={() => navigate('/agents/new')} style={{ borderRadius: 8 }}>
|
||||||
前往广场创建
|
创建智能体
|
||||||
</Button>
|
</Button>
|
||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -126,14 +126,14 @@
|
||||||
|
|
||||||
.agent-card-desc-container {
|
.agent-card-desc-container {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding: 16px 16px 18px;
|
padding: 16px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: linear-gradient(180deg, rgba(248,250,252,0.9) 0%, rgba(255,255,255,0.95) 100%);
|
background: linear-gradient(180deg, rgba(248,250,252,0.9) 0%, rgba(255,255,255,0.95) 100%);
|
||||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-card-desc {
|
.agent-card-desc {
|
||||||
min-height: 66px;
|
min-height: 48px;
|
||||||
font-size: 13.5px;
|
font-size: 13.5px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,10 @@ export default function AgentListWeb({ logic }: Props) {
|
||||||
type="primary"
|
type="primary"
|
||||||
size="large"
|
size="large"
|
||||||
icon={<CompassOutlined />}
|
icon={<CompassOutlined />}
|
||||||
onClick={() => navigate('/marketplace')}
|
onClick={() => navigate('/agents/new')}
|
||||||
style={{ borderRadius: 14, height: 46, padding: '0 18px', fontWeight: 600 }}
|
style={{ borderRadius: 14, height: 46, padding: '0 18px', fontWeight: 600 }}
|
||||||
>
|
>
|
||||||
前往智能体广场
|
创建智能体
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -73,8 +73,8 @@ export default function AgentListWeb({ logic }: Props) {
|
||||||
{!loading && list.length === 0 ? (
|
{!loading && list.length === 0 ? (
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
<Empty description="你还没有任何智能体">
|
<Empty description="你还没有任何智能体">
|
||||||
<Button type="primary" onClick={() => navigate('/marketplace')} style={{ borderRadius: 10 }}>
|
<Button type="primary" onClick={() => navigate('/agents/new')} style={{ borderRadius: 10 }}>
|
||||||
前往广场创建
|
创建智能体
|
||||||
</Button>
|
</Button>
|
||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,8 @@ export default function AgentListWebBase({ logic, viewport }: AgentListWebVarian
|
||||||
把你的 AI 助手沉淀成一组可管理、可协作、可持续进化的能力单元。创建入口统一在智能体广场,这里负责查看、进入和运营它们。
|
把你的 AI 助手沉淀成一组可管理、可协作、可持续进化的能力单元。创建入口统一在智能体广场,这里负责查看、进入和运营它们。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button type="primary" size="large" icon={<CompassOutlined />} onClick={() => navigate('/marketplace')} className="agent-list-web-market-btn">
|
<Button type="primary" size="large" icon={<CompassOutlined />} onClick={() => navigate('/agents/new')} className="agent-list-web-market-btn">
|
||||||
前往智能体广场
|
创建智能体
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -53,8 +53,8 @@ export default function AgentListWebBase({ logic, viewport }: AgentListWebVarian
|
||||||
{!loading && list.length === 0 ? (
|
{!loading && list.length === 0 ? (
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
<Empty description="你还没有任何智能体">
|
<Empty description="你还没有任何智能体">
|
||||||
<Button type="primary" onClick={() => navigate('/marketplace')} className="agent-list-web-empty-btn">
|
<Button type="primary" onClick={() => navigate('/agents/new')} className="agent-list-web-empty-btn">
|
||||||
前往广场创建
|
创建智能体
|
||||||
</Button>
|
</Button>
|
||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -98,9 +98,9 @@ export function useProfileLogic() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAuthorizeAgent = async (userId: string, agentId: string, level: 'read' | 'write') => {
|
const handleAuthorizeAgent = async (userId: string, agentIds: string[], level: 'read' | 'write') => {
|
||||||
try {
|
try {
|
||||||
await MembershipAPI.authorizeResource({ userId, resourceType: 'agent', resourceId: agentId, level });
|
await MembershipAPI.authorizeResource({ userId, resourceType: 'agent', resourceIds: agentIds, level });
|
||||||
message.success('授权成功');
|
message.success('授权成功');
|
||||||
const mList = await MembershipAPI.listMembers();
|
const mList = await MembershipAPI.listMembers();
|
||||||
setMembers(mList);
|
setMembers(mList);
|
||||||
|
|
@ -109,9 +109,9 @@ export function useProfileLogic() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRevokeAgent = async (userId: string, agentId: string) => {
|
const handleRevokeAgent = async (userId: string, agentIds: string[]) => {
|
||||||
try {
|
try {
|
||||||
await MembershipAPI.revokeResource({ userId, resourceType: 'agent', resourceId: agentId });
|
await MembershipAPI.revokeResource({ userId, resourceType: 'agent', resourceIds: agentIds });
|
||||||
message.success('取消授权成功');
|
message.success('取消授权成功');
|
||||||
const mList = await MembershipAPI.listMembers();
|
const mList = await MembershipAPI.listMembers();
|
||||||
setMembers(mList);
|
setMembers(mList);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Modal, Input, Select, Space, Tag, InputNumber, Popconfirm, List, Card } from 'antd';
|
import { Button, Modal, Input, Select, Space, Tag, InputNumber, Popconfirm, List, Card, Checkbox, Divider } from 'antd';
|
||||||
import { UserAddOutlined, DeleteOutlined, KeyOutlined, DashboardOutlined } from '@ant-design/icons';
|
import { UserAddOutlined, DeleteOutlined, KeyOutlined, DashboardOutlined } from '@ant-design/icons';
|
||||||
import type { ProfileLogicOutput } from '../ProfileLogic';
|
import type { ProfileLogicOutput } from '../ProfileLogic';
|
||||||
import '../styles/profile.css';
|
import '../styles/profile.css';
|
||||||
|
|
@ -30,7 +30,7 @@ export default function ProfileH5({ logic }: Props) {
|
||||||
const [newChildId, setNewChildId] = useState('');
|
const [newChildId, setNewChildId] = useState('');
|
||||||
const [selectedMember, setSelectedMember] = useState<any>(null);
|
const [selectedMember, setSelectedMember] = useState<any>(null);
|
||||||
const [quotaAmount, setQuotaAmount] = useState<number>(100000);
|
const [quotaAmount, setQuotaAmount] = useState<number>(100000);
|
||||||
const [selectedAgentId, setSelectedAgentId] = useState<string>('');
|
const [selectedAgentIds, setSelectedAgentIds] = useState<string[]>([]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="profile-page profile-page-h5">
|
<div className="profile-page profile-page-h5">
|
||||||
|
|
@ -90,7 +90,14 @@ export default function ProfileH5({ logic }: Props) {
|
||||||
style={{ marginBottom: 12, borderRadius: 12 }}
|
style={{ marginBottom: 12, borderRadius: 12 }}
|
||||||
actions={[
|
actions={[
|
||||||
<DashboardOutlined key="quota" onClick={() => { setSelectedMember(record); setQuotaModalVisible(true); }} />,
|
<DashboardOutlined key="quota" onClick={() => { setSelectedMember(record); setQuotaModalVisible(true); }} />,
|
||||||
<KeyOutlined key="auth" onClick={() => { setSelectedMember(record); setAuthModalVisible(true); }} />,
|
<KeyOutlined
|
||||||
|
key="auth"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedMember(record);
|
||||||
|
setSelectedAgentIds(record.authorizedAgentIds || []);
|
||||||
|
setAuthModalVisible(true);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
<Popconfirm key="delete" title="确定移除吗?" onConfirm={() => handleRemoveChild(record.id)}>
|
<Popconfirm key="delete" title="确定移除吗?" onConfirm={() => handleRemoveChild(record.id)}>
|
||||||
<DeleteOutlined style={{ color: 'var(--color-error)' }} />
|
<DeleteOutlined style={{ color: 'var(--color-error)' }} />
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
|
|
@ -112,13 +119,23 @@ export default function ProfileH5({ logic }: Props) {
|
||||||
<Tag
|
<Tag
|
||||||
key={id}
|
key={id}
|
||||||
closable
|
closable
|
||||||
onClose={() => handleRevokeAgent(record.child_user_id, id)}
|
onClose={() => handleRevokeAgent(record.child_user_id, [id])}
|
||||||
style={{ borderRadius: 4, fontSize: 10, margin: '2px' }}
|
style={{ borderRadius: 4, fontSize: 10, margin: '2px', display: 'flex', width: 'fit-content' }}
|
||||||
>
|
>
|
||||||
{agent?.name || '未知'}
|
🤖 {agent?.name || '未知'}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{record.authorizedAgentIds?.length > 0 && (
|
||||||
|
<Popconfirm
|
||||||
|
title="一键取消全部授权?"
|
||||||
|
onConfirm={() => handleRevokeAgent(record.child_user_id, record.authorizedAgentIds)}
|
||||||
|
>
|
||||||
|
<Button type="link" size="small" danger style={{ fontSize: 10, padding: '0 2px', height: 'auto' }}>
|
||||||
|
全部移除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -163,12 +180,42 @@ export default function ProfileH5({ logic }: Props) {
|
||||||
<Modal
|
<Modal
|
||||||
title={`为 ${selectedMember?.child_name} 授权资源`}
|
title={`为 ${selectedMember?.child_name} 授权资源`}
|
||||||
open={authModalVisible}
|
open={authModalVisible}
|
||||||
onOk={() => { handleAuthorizeAgent(selectedMember.child_user_id, selectedAgentId, 'read'); setAuthModalVisible(false); setSelectedAgentId(''); }}
|
onOk={() => {
|
||||||
onCancel={() => setAuthModalVisible(false)}
|
handleAuthorizeAgent(selectedMember.child_user_id, selectedAgentIds, 'read');
|
||||||
|
setAuthModalVisible(false);
|
||||||
|
setSelectedAgentIds([]);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setAuthModalVisible(false);
|
||||||
|
setSelectedAgentIds([]);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Select style={{ width: '100%' }} placeholder="选择智能体" value={selectedAgentId} onChange={setSelectedAgentId}>
|
<div style={{ marginBottom: 12, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
{myAgents.map(a => <Select.Option key={a.id} value={a.id}>{a.name}</Select.Option>)}
|
<span style={{ fontSize: 13, fontWeight: 500 }}>选择智能体</span>
|
||||||
</Select>
|
<Checkbox
|
||||||
|
indeterminate={selectedAgentIds.length > 0 && selectedAgentIds.length < myAgents.length}
|
||||||
|
checked={selectedAgentIds.length === myAgents.length && myAgents.length > 0}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedAgentIds(e.target.checked ? myAgents.map(a => a.id) : []);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
全选
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
<div style={{ maxHeight: 300, overflowY: 'auto' }}>
|
||||||
|
<Checkbox.Group
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={selectedAgentIds}
|
||||||
|
onChange={(vals) => setSelectedAgentIds(vals as string[])}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||||
|
{myAgents.map(a => (
|
||||||
|
<Checkbox key={a.id} value={a.id}>{a.name}</Checkbox>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Checkbox.Group>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button, Table, Modal, Input, Select, Space, Tag, InputNumber, Popconfirm } from 'antd';
|
import { Button, Table, Modal, Input, Select, Space, Tag, InputNumber, Popconfirm, Checkbox, Divider } from 'antd';
|
||||||
import { UserAddOutlined, DeleteOutlined, KeyOutlined, DashboardOutlined } from '@ant-design/icons';
|
import { UserAddOutlined, DeleteOutlined, KeyOutlined, DashboardOutlined } from '@ant-design/icons';
|
||||||
import type { ProfileLogicOutput } from '../ProfileLogic';
|
import type { ProfileLogicOutput } from '../ProfileLogic';
|
||||||
import '../styles/profile.css';
|
import '../styles/profile.css';
|
||||||
|
|
@ -30,7 +30,7 @@ export default function ProfileWeb({ logic }: Props) {
|
||||||
const [newChildId, setNewChildId] = useState('');
|
const [newChildId, setNewChildId] = useState('');
|
||||||
const [selectedMember, setSelectedMember] = useState<any>(null);
|
const [selectedMember, setSelectedMember] = useState<any>(null);
|
||||||
const [quotaAmount, setQuotaAmount] = useState<number>(100000);
|
const [quotaAmount, setQuotaAmount] = useState<number>(100000);
|
||||||
const [selectedAgentId, setSelectedAgentId] = useState<string>('');
|
const [selectedAgentIds, setSelectedAgentIds] = useState<string[]>([]);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
|
|
@ -56,24 +56,35 @@ export default function ProfileWeb({ logic }: Props) {
|
||||||
title: '已授权资源',
|
title: '已授权资源',
|
||||||
key: 'resources',
|
key: 'resources',
|
||||||
render: (_: any, record: any) => (
|
render: (_: any, record: any) => (
|
||||||
<div className="resource-tags">
|
<div className="resource-tags-container">
|
||||||
|
<div className="resource-tags" style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 4 }}>
|
||||||
{record.authorizedAgentIds?.map((id: string) => {
|
{record.authorizedAgentIds?.map((id: string) => {
|
||||||
const agent = myAgents.find(a => a.id === id);
|
const agent = myAgents.find(a => a.id === id);
|
||||||
return (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
key={id}
|
key={id}
|
||||||
closable
|
closable
|
||||||
onClose={() => handleRevokeAgent(record.child_user_id, id)}
|
onClose={() => handleRevokeAgent(record.child_user_id, [id])}
|
||||||
style={{ borderRadius: 4, background: 'var(--color-fill-secondary)', border: 'none' }}
|
style={{ borderRadius: 4, background: 'var(--color-fill-secondary)', border: 'none', margin: 0 }}
|
||||||
>
|
>
|
||||||
🤖 {agent?.name || '未知智能体'}
|
🤖 {agent?.name || '未知智能体'}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{(!record.authorizedAgentIds || record.authorizedAgentIds.length === 0) && (
|
{(!record.authorizedAgentIds || record.authorizedAgentIds.length === 0) ? (
|
||||||
<span style={{ color: 'var(--color-text-tertiary)', fontSize: 12 }}>暂无授权</span>
|
<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>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -104,6 +115,7 @@ export default function ProfileWeb({ logic }: Props) {
|
||||||
icon={<KeyOutlined />}
|
icon={<KeyOutlined />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedMember(record);
|
setSelectedMember(record);
|
||||||
|
setSelectedAgentIds(record.authorizedAgentIds || []);
|
||||||
setAuthModalVisible(true);
|
setAuthModalVisible(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -233,24 +245,57 @@ export default function ProfileWeb({ logic }: Props) {
|
||||||
<Modal
|
<Modal
|
||||||
title={`为 ${selectedMember?.child_name} 授权资源`}
|
title={`为 ${selectedMember?.child_name} 授权资源`}
|
||||||
open={authModalVisible}
|
open={authModalVisible}
|
||||||
onOk={() => {
|
width={520}
|
||||||
handleAuthorizeAgent(selectedMember.child_user_id, selectedAgentId, 'read');
|
onOk={async () => {
|
||||||
|
// 计算新增的和移除的,但根据接口逻辑,通常是全量覆盖或者增量。
|
||||||
|
// 这里假设 handleAuthorizeAgent 是设置最终状态,或者后端支持 resourceIds 覆盖。
|
||||||
|
// 之前的 handleAuthorizeAgent 逻辑是直接调用 authorizeResource。
|
||||||
|
handleAuthorizeAgent(selectedMember.child_user_id, selectedAgentIds, 'read');
|
||||||
setAuthModalVisible(false);
|
setAuthModalVisible(false);
|
||||||
setSelectedAgentId('');
|
setSelectedAgentIds([]);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setAuthModalVisible(false);
|
||||||
|
setSelectedAgentIds([]);
|
||||||
}}
|
}}
|
||||||
onCancel={() => setAuthModalVisible(false)}
|
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: 8 }}>选择智能体:</div>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<Select
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||||
|
<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%' }}
|
style={{ width: '100%' }}
|
||||||
placeholder="请选择智能体"
|
value={selectedAgentIds}
|
||||||
value={selectedAgentId}
|
onChange={(vals) => setSelectedAgentIds(vals as string[])}
|
||||||
onChange={setSelectedAgentId}
|
|
||||||
>
|
>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||||||
{myAgents.map(a => (
|
{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>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTeamsPageLogic } from './TeamsPage/TeamsPageLogic';
|
import { useTeamsPageLogic } from './TeamsPage/TeamsPageLogic';
|
||||||
import TeamsPageWeb from './TeamsPage/components/TeamsPageWeb';
|
import TeamsPageWebBase from './TeamsPage/components/TeamsPageWebBase';
|
||||||
import TeamsPageH5 from './TeamsPage/components/TeamsPageH5';
|
import TeamsPageH5 from './TeamsPage/components/TeamsPageH5';
|
||||||
|
import { useDesktopViewport } from '../hooks/useDesktopViewport';
|
||||||
|
|
||||||
const isMobileDevice = () => {
|
const isMobileDevice = () => {
|
||||||
if (typeof window === 'undefined') return false;
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
@ -10,6 +11,7 @@ const isMobileDevice = () => {
|
||||||
|
|
||||||
export default function TeamsPage() {
|
export default function TeamsPage() {
|
||||||
const logic = useTeamsPageLogic();
|
const logic = useTeamsPageLogic();
|
||||||
|
const viewport = useDesktopViewport();
|
||||||
const [isMobile, setIsMobile] = useState(isMobileDevice());
|
const [isMobile, setIsMobile] = useState(isMobileDevice());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -20,5 +22,9 @@ export default function TeamsPage() {
|
||||||
return () => window.removeEventListener('resize', handleResize);
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return isMobile ? <TeamsPageH5 logic={logic} /> : <TeamsPageWeb logic={logic} />;
|
return isMobile ? (
|
||||||
|
<TeamsPageH5 logic={logic} />
|
||||||
|
) : (
|
||||||
|
<TeamsPageWebBase logic={logic} viewport={viewport} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ export function useTeamsPageLogic() {
|
||||||
const [list, setList] = useState<Team[]>([]);
|
const [list, setList] = useState<Team[]>([]);
|
||||||
const [active, setActive] = useState<Team | null>(null);
|
const [active, setActive] = useState<Team | null>(null);
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [inviteOpen, setInviteOpen] = useState(false);
|
const [addMemberOpen, setAddMemberOpen] = useState(false);
|
||||||
const [lastInviteCode, setLastInviteCode] = useState<string | null>(null);
|
const [renameOpen, setRenameOpen] = useState(false);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
const l = await TeamAPI.list();
|
const l = await TeamAPI.list();
|
||||||
|
|
@ -27,14 +27,22 @@ export function useTeamsPageLogic() {
|
||||||
setActive(await TeamAPI.detail(t.id));
|
setActive(await TeamAPI.detail(t.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInvite = async (v: any) => {
|
const handleRename = async (v: any) => {
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
const inv = await AuthAPI.createInvite({
|
await TeamAPI.rename(active.id, v.name);
|
||||||
teamId: active.id,
|
setRenameOpen(false);
|
||||||
phone: v.phone || undefined,
|
message.success('已更名');
|
||||||
ttlHours: Number(v.ttlHours) || 168,
|
await load();
|
||||||
});
|
setActive(await TeamAPI.detail(active.id));
|
||||||
setLastInviteCode(inv.code);
|
};
|
||||||
|
|
||||||
|
const handleAddMember = async (v: any) => {
|
||||||
|
if (!active) return;
|
||||||
|
await TeamAPI.addMember(active.id, v.phone);
|
||||||
|
setAddMemberOpen(false);
|
||||||
|
message.success('已添加');
|
||||||
|
await load();
|
||||||
|
setActive(await TeamAPI.detail(active.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
const handleDelete = async (id: string) => {
|
||||||
|
|
@ -44,10 +52,11 @@ export function useTeamsPageLogic() {
|
||||||
setActive(null);
|
setActive(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveMember = async (id: string) => {
|
const handleRemoveMember = async (userId: string) => {
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
await TeamAPI.removeMember(active.id, id);
|
await TeamAPI.removeMember(active.id, userId);
|
||||||
message.success('已移除');
|
message.success('已移除');
|
||||||
|
await load();
|
||||||
setActive(await TeamAPI.detail(active.id));
|
setActive(await TeamAPI.detail(active.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -55,17 +64,18 @@ export function useTeamsPageLogic() {
|
||||||
list,
|
list,
|
||||||
active,
|
active,
|
||||||
createOpen,
|
createOpen,
|
||||||
inviteOpen,
|
addMemberOpen,
|
||||||
lastInviteCode,
|
renameOpen,
|
||||||
load,
|
load,
|
||||||
handleCreate,
|
handleCreate,
|
||||||
handleInvite,
|
handleRename,
|
||||||
|
handleAddMember,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
handleRemoveMember,
|
handleRemoveMember,
|
||||||
setActive,
|
setActive,
|
||||||
setCreateOpen,
|
setCreateOpen,
|
||||||
setInviteOpen,
|
setAddMemberOpen,
|
||||||
setLastInviteCode,
|
setRenameOpen,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import {
|
import {
|
||||||
CopyOutlined,
|
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
MailOutlined,
|
EditOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
|
UserAddOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Card, Button, List, Tag, Space, Popconfirm, App as AntApp, Modal, Form, Input, Empty } from 'antd';
|
import { Card, Button, List, Tag, Space, Popconfirm, App as AntApp, Modal, Form, Input, Empty } from 'antd';
|
||||||
import { TeamAPI, type Team } from '../../../api';
|
import { TeamAPI, type Team } from '../../../api';
|
||||||
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||||
|
import '../styles/teams-page-h5.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logic: TeamsPageLogicOutput;
|
logic: TeamsPageLogicOutput;
|
||||||
|
|
@ -20,55 +21,26 @@ export default function TeamsPageH5({ logic }: Props) {
|
||||||
list,
|
list,
|
||||||
active,
|
active,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
|
handleRename,
|
||||||
handleRemoveMember,
|
handleRemoveMember,
|
||||||
|
handleAddMember,
|
||||||
setCreateOpen,
|
setCreateOpen,
|
||||||
|
setRenameOpen,
|
||||||
|
setAddMemberOpen,
|
||||||
} = logic;
|
} = logic;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="feature-cover-container">
|
<div className="feature-cover-container">
|
||||||
<div className="page-container h5-page-container" style={{ padding: '0 8px' }}>
|
<div className="page-container h5-page-container">
|
||||||
<div
|
<div className="teams-page-h5-hero">
|
||||||
style={{
|
<div className="teams-page-h5-hero-header">
|
||||||
borderRadius: 16,
|
|
||||||
padding: '20px 16px 18px',
|
|
||||||
background:
|
|
||||||
'linear-gradient(135deg, rgba(255,255,255,0.98) 0%, rgba(236,253,245,0.92) 42%, rgba(239,246,255,0.96) 100%)',
|
|
||||||
border: '1px solid rgba(8, 145, 178, 0.12)',
|
|
||||||
boxShadow: '0 10px 24px rgba(15, 23, 42, 0.06)',
|
|
||||||
marginBottom: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
gap: 12,
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
marginBottom: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ width: '100%' }}>
|
<div style={{ width: '100%' }}>
|
||||||
<div
|
<div className="teams-page-h5-badge">
|
||||||
style={{
|
<TeamOutlined className="teams-page-h5-member-avatar-icon" />
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 6,
|
|
||||||
padding: '4px 10px',
|
|
||||||
borderRadius: 999,
|
|
||||||
background: 'rgba(255,255,255,0.78)',
|
|
||||||
border: '1px solid rgba(8, 145, 178, 0.10)',
|
|
||||||
color: 'var(--color-text-secondary)',
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: 600,
|
|
||||||
marginBottom: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TeamOutlined style={{ color: 'var(--color-brand)', fontSize: 12 }} />
|
|
||||||
协作组织空间
|
协作组织空间
|
||||||
</div>
|
</div>
|
||||||
<h1 className="page-title h5-page-title" style={{ marginBottom: 8, fontSize: 22 }}>团队管理</h1>
|
<h1 className="page-title h5-page-title teams-page-h5-title">团队管理</h1>
|
||||||
<div className="page-subtitle h5-page-subtitle" style={{ marginTop: 0, fontSize: 13, lineHeight: 1.6 }}>
|
<div className="page-subtitle h5-page-subtitle teams-page-h5-subtitle">
|
||||||
团队不只是成员列表,更是共享智能体、协同运营和权限分工的组织单元。
|
团队不只是成员列表,更是共享智能体、协同运营和权限分工的组织单元。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -77,40 +49,23 @@ export default function TeamsPageH5({ logic }: Props) {
|
||||||
size="middle"
|
size="middle"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={() => setCreateOpen(true)}
|
onClick={() => setCreateOpen(true)}
|
||||||
style={{ borderRadius: 10, height: 40, padding: '0 14px', fontWeight: 600, width: '100%' }}
|
className="teams-page-h5-create-btn"
|
||||||
>
|
>
|
||||||
创建团队
|
创建团队
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10 }}>
|
<div className="teams-page-h5-stats-grid">
|
||||||
{[
|
{[
|
||||||
{ label: '团队数量', value: list.length, tone: 'rgba(8, 145, 178, 0.10)', color: 'var(--color-brand)' },
|
{ label: '团队数量', value: list.length, type: 'brand' },
|
||||||
{ label: '当前成员数', value: active?.members?.length ?? 0, tone: 'rgba(14, 165, 233, 0.10)', color: 'var(--color-info)' },
|
{ label: '当前成员数', value: active?.members?.length ?? 0, type: 'info' },
|
||||||
{ label: '共享智能体', value: active?.agentCount ?? 0, tone: 'rgba(34, 197, 94, 0.10)', color: 'var(--color-success)' },
|
{ label: '共享智能体', value: active?.agentCount ?? 0, type: 'success' },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<div
|
<div key={item.label} className="teams-page-h5-stat-card">
|
||||||
key={item.label}
|
<div className="teams-page-h5-stat-label">{item.label}</div>
|
||||||
style={{
|
<div className="teams-page-h5-stat-row">
|
||||||
borderRadius: 12,
|
<span className="teams-page-h5-stat-value">{item.value}</span>
|
||||||
padding: '12px 14px',
|
<span className={`teams-page-h5-stat-chip teams-page-h5-stat-chip-${item.type}`}>
|
||||||
background: 'rgba(255,255,255,0.72)',
|
|
||||||
border: '1px solid rgba(255,255,255,0.7)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)', marginBottom: 6 }}>{item.label}</div>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
|
|
||||||
<span style={{ fontSize: 22, fontWeight: 700, color: 'var(--color-text)' }}>{item.value}</span>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
borderRadius: 999,
|
|
||||||
padding: '3px 6px',
|
|
||||||
background: item.tone,
|
|
||||||
color: item.color,
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: 600,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
当前选中
|
当前选中
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -120,38 +75,21 @@ export default function TeamsPageH5({ logic }: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{list.length > 0 && (
|
{list.length > 0 && (
|
||||||
<div
|
<div className="teams-page-h5-list-panel">
|
||||||
style={{
|
<div className="teams-page-h5-list-head">
|
||||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.98) 0%, rgba(252,252,253,1) 100%)',
|
<div className="teams-page-h5-section-title">团队列表</div>
|
||||||
border: '1px solid var(--color-border)',
|
<div className="teams-page-h5-section-desc">点击选择一个团队</div>
|
||||||
borderRadius: 16,
|
|
||||||
padding: 12,
|
|
||||||
marginBottom: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ padding: '4px 6px 10px' }}>
|
|
||||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--color-text)', marginBottom: 4 }}>团队列表</div>
|
|
||||||
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)' }}>点击选择一个团队</div>
|
|
||||||
</div>
|
</div>
|
||||||
{list.map((item) => (
|
{list.map((item) => (
|
||||||
<div
|
<div
|
||||||
className={`nav-item ${active?.id === item.id ? 'active' : ''}`}
|
key={item.id}
|
||||||
|
className={`teams-page-h5-nav-item ${active?.id === item.id ? 'active' : ''}`}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
logic.setActive(await TeamAPI.detail(item.id));
|
logic.setActive(await TeamAPI.detail(item.id));
|
||||||
}}
|
}}
|
||||||
style={{
|
|
||||||
padding: '10px 12px',
|
|
||||||
borderRadius: 10,
|
|
||||||
cursor: 'pointer',
|
|
||||||
marginBottom: 6,
|
|
||||||
background: active?.id === item.id ? 'rgba(8, 145, 178, 0.10)' : 'transparent',
|
|
||||||
color: active?.id === item.id ? 'var(--color-brand)' : 'var(--color-text-secondary)',
|
|
||||||
fontWeight: active?.id === item.id ? 600 : 500,
|
|
||||||
border: active?.id === item.id ? '1px solid rgba(8, 145, 178, 0.16)' : '1px solid transparent',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div style={{ fontSize: 13, marginBottom: 3 }}>{item.name}</div>
|
<div className="teams-page-h5-nav-item-name">{item.name}</div>
|
||||||
<div style={{ fontSize: 11, color: active?.id === item.id ? 'var(--color-brand)' : 'var(--color-text-tertiary)' }}>
|
<div className="teams-page-h5-nav-item-count">
|
||||||
{item.agentCount ?? 0} 个智能体
|
{item.agentCount ?? 0} 个智能体
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -160,27 +98,26 @@ export default function TeamsPageH5({ logic }: Props) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{active ? (
|
{active ? (
|
||||||
<Card
|
<Card className="teams-page-h5-detail-card">
|
||||||
style={{ borderRadius: 16, boxShadow: '0 6px 16px rgba(15, 23, 42, 0.045)' }}
|
<div className="teams-page-h5-detail-head">
|
||||||
bodyStyle={{ padding: 16 }}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginBottom: 16 }}>
|
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', marginBottom: 4 }}>
|
<div className="teams-page-h5-active-title-row">
|
||||||
<span style={{ fontSize: 18, fontWeight: 700, color: 'var(--color-text)' }}>{active.name}</span>
|
<span className="teams-page-h5-active-title">{active.name}</span>
|
||||||
<Tag bordered={false} style={{ background: 'var(--color-brand-soft)', color: 'var(--color-brand)', borderRadius: 999, margin: 0, fontSize: 11 }}>{active.myRole}</Tag>
|
<Button
|
||||||
<Tag bordered={false} style={{ background: 'var(--color-surface-2)', color: 'var(--color-text-secondary)', borderRadius: 999, margin: 0, fontSize: 11 }}>{active.agentCount ?? 0} 智能体</Tag>
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => setRenameOpen(true)}
|
||||||
|
className="teams-page-h5-edit-btn"
|
||||||
|
/>
|
||||||
|
<Tag bordered={false} className="teams-page-h5-tag teams-page-h5-tag-brand">{active.myRole}</Tag>
|
||||||
|
<Tag bordered={false} className="teams-page-h5-tag teams-page-h5-tag-neutral">{active.agentCount ?? 0} 智能体</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-text-secondary)' }}>
|
<div className="teams-page-h5-section-desc">
|
||||||
管理成员权限、邀请新伙伴。
|
管理成员权限、邀请新伙伴。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{(active.myRole === 'owner' || active.myRole === 'admin') && (
|
|
||||||
<Button icon={<MailOutlined />} size="small" onClick={() => logic.setInviteOpen(true)} style={{ borderRadius: 8 }}>
|
|
||||||
生成邀请码
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{active.myRole === 'owner' && (
|
{active.myRole === 'owner' && (
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title="确定删除该团队?团队内的智能体会变成 owner 私有"
|
title="确定删除该团队?团队内的智能体会变成 owner 私有"
|
||||||
|
|
@ -189,7 +126,7 @@ export default function TeamsPageH5({ logic }: Props) {
|
||||||
message.success('已删除');
|
message.success('已删除');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button danger size="small" icon={<DeleteOutlined />} style={{ borderRadius: 8 }}>
|
<Button danger size="small" icon={<DeleteOutlined />} className="teams-page-web-soft-btn">
|
||||||
删除团队
|
删除团队
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
|
|
@ -197,29 +134,43 @@ export default function TeamsPageH5({ logic }: Props) {
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', gap: 10, marginBottom: 16 }}>
|
<div className="teams-page-h5-mini-grid">
|
||||||
<div style={{ borderRadius: 12, padding: '12px 14px', background: 'rgba(8, 145, 178, 0.06)', border: '1px solid rgba(8, 145, 178, 0.10)' }}>
|
<div className="teams-page-h5-mini-card teams-page-h5-mini-card-brand">
|
||||||
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)', marginBottom: 6 }}>成员规模</div>
|
<div className="teams-page-h5-mini-label">成员规模</div>
|
||||||
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--color-text)' }}>{active.members?.length || 0}</div>
|
<div className="teams-page-h5-mini-value">{active.members?.length || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ borderRadius: 12, padding: '12px 14px', background: 'rgba(34, 197, 94, 0.06)', border: '1px solid rgba(34, 197, 94, 0.10)' }}>
|
<div className="teams-page-h5-mini-card teams-page-h5-mini-card-success">
|
||||||
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)', marginBottom: 6 }}>共享智能体</div>
|
<div className="teams-page-h5-mini-label">共享智能体</div>
|
||||||
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--color-text)' }}>{active.agentCount ?? 0}</div>
|
<div className="teams-page-h5-mini-value">{active.agentCount ?? 0}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ borderRadius: 12, padding: '12px 14px', background: 'rgba(249, 115, 22, 0.06)', border: '1px solid rgba(249, 115, 22, 0.10)' }}>
|
<div className="teams-page-h5-mini-card teams-page-h5-mini-card-warning">
|
||||||
<div style={{ fontSize: 11, color: 'var(--color-text-secondary)', marginBottom: 6 }}>当前身份</div>
|
<div className="teams-page-h5-mini-label">当前身份</div>
|
||||||
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--color-text)', textTransform: 'capitalize' }}>{active.myRole}</div>
|
<div className="teams-page-h5-mini-value" style={{ textTransform: 'capitalize' }}>{active.myRole}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--color-text)', marginBottom: 10 }}>
|
<div className="teams-page-h5-members-title-row">
|
||||||
|
<div className="teams-page-h5-members-title">
|
||||||
成员 ({active.members?.length || 0})
|
成员 ({active.members?.length || 0})
|
||||||
</div>
|
</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
|
<List
|
||||||
dataSource={active.members || []}
|
dataSource={active.members || []}
|
||||||
renderItem={(m) => (
|
renderItem={(m) => (
|
||||||
<List.Item
|
<List.Item
|
||||||
style={{ padding: '10px 0' }}
|
className="teams-page-h5-member-item"
|
||||||
actions={
|
actions={
|
||||||
(active.myRole === 'owner' || active.myRole === 'admin') && m.role !== 'owner'
|
(active.myRole === 'owner' || active.myRole === 'admin') && m.role !== 'owner'
|
||||||
? [
|
? [
|
||||||
|
|
@ -230,7 +181,7 @@ export default function TeamsPageH5({ logic }: Props) {
|
||||||
await handleRemoveMember(m.id);
|
await handleRemoveMember(m.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button size="small" danger style={{ borderRadius: 6 }}>
|
<Button size="small" danger className="teams-page-web-soft-btn">
|
||||||
移除
|
移除
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>,
|
</Popconfirm>,
|
||||||
|
|
@ -240,52 +191,25 @@ export default function TeamsPageH5({ logic }: Props) {
|
||||||
>
|
>
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
avatar={
|
avatar={
|
||||||
<div
|
<div className="teams-page-h5-member-avatar">
|
||||||
style={{
|
|
||||||
width: 36,
|
|
||||||
height: 36,
|
|
||||||
borderRadius: 999,
|
|
||||||
background: 'rgba(8, 145, 178, 0.10)',
|
|
||||||
color: 'var(--color-brand)',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<UserOutlined />
|
<UserOutlined />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
<span style={{ fontWeight: 600, fontSize: 14 }}>{m.name}</span>
|
<span className="teams-page-h5-member-name">{m.name}</span>
|
||||||
<Tag
|
<Tag
|
||||||
bordered={false}
|
bordered={false}
|
||||||
style={{
|
className={`teams-page-h5-tag teams-page-h5-tag-role-${m.role}`}
|
||||||
background:
|
|
||||||
m.role === 'owner'
|
|
||||||
? 'var(--color-warning-soft)'
|
|
||||||
: m.role === 'admin'
|
|
||||||
? 'var(--color-info-soft)'
|
|
||||||
: 'var(--color-surface-2)',
|
|
||||||
color:
|
|
||||||
m.role === 'owner'
|
|
||||||
? 'var(--color-warning)'
|
|
||||||
: m.role === 'admin'
|
|
||||||
? 'var(--color-info)'
|
|
||||||
: 'var(--color-text-secondary)',
|
|
||||||
borderRadius: 999,
|
|
||||||
margin: 0,
|
|
||||||
fontSize: 10,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{m.role}
|
{m.role}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
description={
|
description={
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
<div className="teams-page-h5-member-desc">
|
||||||
<span style={{ fontSize: 12 }}>{m.email}</span>
|
<span className="teams-page-h5-member-email">{m.email}</span>
|
||||||
<span style={{ fontSize: 11, color: 'var(--color-text-tertiary)' }}>
|
<span className="teams-page-h5-member-time">
|
||||||
加入时间 {new Date(m.joinedAt).toLocaleDateString('zh-CN')}
|
加入时间 {new Date(m.joinedAt).toLocaleDateString('zh-CN')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -317,63 +241,27 @@ export default function TeamsPageH5({ logic }: Props) {
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal open={logic.renameOpen} title="重命名团队" onCancel={() => logic.setRenameOpen(false)} footer={null} width="95%" destroyOnHidden>
|
||||||
open={logic.inviteOpen}
|
<Form layout="vertical" onFinish={logic.handleRename} initialValues={{ name: active?.name }}>
|
||||||
title={`📨 邀请加入 ${active?.name}`}
|
<Form.Item name="name" label="新团队名称" rules={[{ required: true }]}>
|
||||||
onCancel={() => {
|
<Input placeholder="输入新的团队名称" autoFocus />
|
||||||
logic.setInviteOpen(false);
|
|
||||||
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>
|
</Form.Item>
|
||||||
<Button type="primary" htmlType="submit" block>
|
<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>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
|
||||||
<div className="feature-cover">
|
|
||||||
<Empty description="功能规划中,本期不支持" />
|
|
||||||
</div>
|
</div>
|
||||||
</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 TeamsPageWebHero from './TeamsPageWebHero';
|
||||||
import TeamsPageWebList from './TeamsPageWebList';
|
import TeamsPageWebList from './TeamsPageWebList';
|
||||||
import TeamsPageWebModals from './TeamsPageWebModals';
|
import TeamsPageWebModals from './TeamsPageWebModals';
|
||||||
|
import '../styles/teams-page-web.css';
|
||||||
|
|
||||||
export interface TeamsPageWebVariantProps {
|
export interface TeamsPageWebVariantProps {
|
||||||
logic: TeamsPageLogicOutput;
|
logic: TeamsPageLogicOutput;
|
||||||
|
|
@ -23,9 +24,6 @@ export default function TeamsPageWebBase({ logic, viewport }: TeamsPageWebVarian
|
||||||
</div>
|
</div>
|
||||||
<TeamsPageWebModals logic={logic} />
|
<TeamsPageWebModals logic={logic} />
|
||||||
</div>
|
</div>
|
||||||
<div className="feature-cover">
|
|
||||||
<Empty description="功能规划中,本期不支持" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { DeleteOutlined, MailOutlined, UserOutlined } from '@ant-design/icons';
|
import { DeleteOutlined, EditOutlined, UserAddOutlined, UserOutlined } from '@ant-design/icons';
|
||||||
import { Button, Card, Empty, List, Popconfirm, Space, Tag } from 'antd';
|
import { Button, Card, Empty, List, Popconfirm, Space, Tag } from 'antd';
|
||||||
import type { Team } from '../../../api';
|
import type { Team } from '../../../api';
|
||||||
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||||
|
|
@ -34,22 +34,26 @@ export default function TeamsPageWebDetail({ logic }: { logic: TeamsPageLogicOut
|
||||||
const canManage = active.myRole === 'owner' || active.myRole === 'admin';
|
const canManage = active.myRole === 'owner' || active.myRole === 'admin';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="teams-page-web-detail-card" styles={{ body: { padding: 22 } }}>
|
<Card className="teams-page-web-detail-card">
|
||||||
<div className="teams-page-web-detail-head">
|
<div className="teams-page-web-detail-head">
|
||||||
<div>
|
<div>
|
||||||
<div className="teams-page-web-active-title-row">
|
<div className="teams-page-web-active-title-row">
|
||||||
<span className="teams-page-web-active-title">{active.name}</span>
|
<span className="teams-page-web-active-title">{active.name}</span>
|
||||||
|
{canManage && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => logic.setRenameOpen(true)}
|
||||||
|
className="teams-page-web-edit-btn"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Tag bordered={false} className="teams-page-web-tag-brand">{active.myRole}</Tag>
|
<Tag bordered={false} className="teams-page-web-tag-brand">{active.myRole}</Tag>
|
||||||
<Tag bordered={false} className="teams-page-web-tag-neutral">{active.agentCount ?? 0} 智能体</Tag>
|
<Tag bordered={false} className="teams-page-web-tag-neutral">{active.agentCount ?? 0} 智能体</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div className="teams-page-web-section-desc">管理成员权限、邀请新伙伴,并协同维护团队共享的智能体资产。</div>
|
<div className="teams-page-web-section-desc">管理成员权限、邀请新伙伴,并协同维护团队共享的智能体资产。</div>
|
||||||
</div>
|
</div>
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{canManage && (
|
|
||||||
<Button icon={<MailOutlined />} onClick={() => logic.setInviteOpen(true)} className="teams-page-web-soft-btn">
|
|
||||||
生成邀请码
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{active.myRole === 'owner' && (
|
{active.myRole === 'owner' && (
|
||||||
<Popconfirm title="确定删除该团队?团队内的智能体会变成 owner 私有" onConfirm={() => handleDelete(active.id)}>
|
<Popconfirm title="确定删除该团队?团队内的智能体会变成 owner 私有" onConfirm={() => handleDelete(active.id)}>
|
||||||
<Button danger icon={<DeleteOutlined />} className="teams-page-web-soft-btn">
|
<Button danger icon={<DeleteOutlined />} className="teams-page-web-soft-btn">
|
||||||
|
|
@ -62,26 +66,45 @@ export default function TeamsPageWebDetail({ logic }: { logic: TeamsPageLogicOut
|
||||||
|
|
||||||
<TeamSummary active={active} />
|
<TeamSummary active={active} />
|
||||||
|
|
||||||
|
<div className="teams-page-web-members-title-row">
|
||||||
<div className="teams-page-web-members-title">成员 ({active.members?.length || 0})</div>
|
<div className="teams-page-web-members-title">成员 ({active.members?.length || 0})</div>
|
||||||
<List
|
{canManage && (
|
||||||
dataSource={active.members || []}
|
<Button
|
||||||
renderItem={(member) => (
|
type="primary"
|
||||||
<List.Item
|
size="small"
|
||||||
className="teams-page-web-member-item"
|
ghost
|
||||||
actions={canManage && member.role !== 'owner' ? [
|
icon={<UserAddOutlined />}
|
||||||
<Popconfirm key="kick" title="移除该成员?" onConfirm={() => handleRemoveMember(member.id)}>
|
onClick={() => logic.setAddMemberOpen(true)}
|
||||||
<Button size="small" danger className="teams-page-web-soft-btn">移除</Button>
|
|
||||||
</Popconfirm>,
|
|
||||||
] : []}
|
|
||||||
>
|
>
|
||||||
<List.Item.Meta
|
添加成员
|
||||||
avatar={<div className="teams-page-web-member-avatar"><UserOutlined /></div>}
|
</Button>
|
||||||
title={<Space wrap><span className="teams-page-web-member-name">{member.name}</span><Tag bordered={false} className={roleClass(member.role)}>{member.role}</Tag></Space>}
|
|
||||||
description={<div className="teams-page-web-member-desc"><span>{member.email}</span><span>加入时间 {new Date(member.joinedAt).toLocaleDateString('zh-CN')}</span></div>}
|
|
||||||
/>
|
|
||||||
</List.Item>
|
|
||||||
)}
|
)}
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
|
<div className="teams-page-web-member-list">
|
||||||
|
{(active.members || []).map((member) => (
|
||||||
|
<div className="teams-page-web-member-item" key={member.id}>
|
||||||
|
<div className="teams-page-web-member-info">
|
||||||
|
<div className="teams-page-web-member-avatar">
|
||||||
|
<UserOutlined />
|
||||||
|
</div>
|
||||||
|
<div className="teams-page-web-member-meta">
|
||||||
|
<div className="teams-page-web-member-name-row">
|
||||||
|
<span className="teams-page-web-member-name">{member.name}</span>
|
||||||
|
<Tag bordered={false} className={roleClass(member.role)}>{member.role}</Tag>
|
||||||
|
</div>
|
||||||
|
<div className="teams-page-web-member-email">{member.email}</div>
|
||||||
|
<div className="teams-page-web-member-time">加入时间 {new Date(member.joinedAt).toLocaleDateString('zh-CN')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{canManage && member.role !== 'owner' && (
|
||||||
|
<Popconfirm title="移除该成员?" onConfirm={() => handleRemoveMember(member.id)}>
|
||||||
|
<Button size="small" danger className="teams-page-web-soft-btn">移除</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||||
export default function TeamsPageWebHero({ logic }: { logic: TeamsPageLogicOutput }) {
|
export default function TeamsPageWebHero({ logic }: { logic: TeamsPageLogicOutput }) {
|
||||||
const { list, active, setCreateOpen } = logic;
|
const { list, active, setCreateOpen } = logic;
|
||||||
const stats = [
|
const stats = [
|
||||||
{ label: '团队数量', value: list.length, tone: 'rgba(8, 145, 178, 0.10)', color: 'var(--color-brand)' },
|
{ label: '团队数量', value: list.length, type: 'brand' },
|
||||||
{ label: '当前成员数', value: active?.members?.length ?? 0, tone: 'rgba(14, 165, 233, 0.10)', color: 'var(--color-info)' },
|
{ label: '当前成员数', value: active?.members?.length ?? 0, type: 'info' },
|
||||||
{ label: '共享智能体', value: active?.agentCount ?? 0, tone: 'rgba(34, 197, 94, 0.10)', color: 'var(--color-success)' },
|
{ label: '共享智能体', value: active?.agentCount ?? 0, type: 'success' },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -34,7 +34,7 @@ export default function TeamsPageWebHero({ logic }: { logic: TeamsPageLogicOutpu
|
||||||
<div className="teams-page-web-stat-label">{item.label}</div>
|
<div className="teams-page-web-stat-label">{item.label}</div>
|
||||||
<div className="teams-page-web-stat-row">
|
<div className="teams-page-web-stat-row">
|
||||||
<span className="teams-page-web-stat-value">{item.value}</span>
|
<span className="teams-page-web-stat-value">{item.value}</span>
|
||||||
<span className="teams-page-web-stat-chip" style={{ background: item.tone, color: item.color }}>
|
<span className={`teams-page-web-stat-chip teams-page-web-stat-chip-${item.type}`}>
|
||||||
当前选中
|
当前选中
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,19 +14,21 @@ export default function TeamsPageWebList({ logic }: { logic: TeamsPageLogicOutpu
|
||||||
{list.length === 0 ? (
|
{list.length === 0 ? (
|
||||||
<Empty description="还没有团队" />
|
<Empty description="还没有团队" />
|
||||||
) : (
|
) : (
|
||||||
<List
|
<div className="teams-page-web-list-items">
|
||||||
dataSource={list}
|
{list.map((item) => (
|
||||||
renderItem={(item) => (
|
|
||||||
<button
|
<button
|
||||||
|
key={item.id}
|
||||||
className={`teams-page-web-nav-item ${active?.id === item.id ? 'active' : ''}`}
|
className={`teams-page-web-nav-item ${active?.id === item.id ? 'active' : ''}`}
|
||||||
onClick={async () => setActive(await TeamAPI.detail(item.id))}
|
onClick={async () => setActive(await TeamAPI.detail(item.id))}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<span>{item.name}</span>
|
<div className="teams-page-web-nav-item-content">
|
||||||
<small>{item.agentCount ?? 0} 个智能体</small>
|
<span className="teams-page-web-nav-item-name">{item.name}</span>
|
||||||
|
<small className="teams-page-web-nav-item-count">{item.agentCount ?? 0} 个智能体</small>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
))}
|
||||||
/>
|
</div>
|
||||||
)}
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { CopyOutlined } from '@ant-design/icons';
|
|
||||||
import { App as AntApp, Button, Form, Input, Modal } from 'antd';
|
import { App as AntApp, Button, Form, Input, Modal } from 'antd';
|
||||||
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||||
|
|
||||||
|
|
@ -19,42 +18,26 @@ export default function TeamsPageWebModals({ logic }: { logic: TeamsPageLogicOut
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal open={logic.renameOpen} title="重命名团队" onCancel={() => logic.setRenameOpen(false)} footer={null} destroyOnHidden>
|
||||||
open={logic.inviteOpen}
|
<Form layout="vertical" onFinish={logic.handleRename} initialValues={{ name: active?.name }}>
|
||||||
title={`邀请加入 ${active?.name}`}
|
<Form.Item name="name" label="新团队名称" rules={[{ required: true }]}>
|
||||||
onCancel={() => {
|
<Input placeholder="输入新的团队名称" autoFocus />
|
||||||
logic.setInviteOpen(false);
|
|
||||||
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>
|
</Form.Item>
|
||||||
<Button type="primary" htmlType="submit" block>
|
<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>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
|
||||||
</Modal>
|
</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 {
|
.teams-page-web-mini-card {
|
||||||
border: 1px solid rgba(8, 145, 178, 0.1);
|
border: 1px solid rgba(8, 145, 178, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-mini-brand {
|
.teams-page-web-mini-brand {
|
||||||
|
|
@ -55,51 +57,83 @@
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-members-title {
|
.teams-page-web-detail-card .ant-card-body {
|
||||||
margin-bottom: 14px;
|
padding: 24px !important;
|
||||||
font-size: 16px;
|
}
|
||||||
|
|
||||||
|
.teams-page-web-members-title-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid var(--color-border-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-member-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-member-item {
|
.teams-page-web-member-item {
|
||||||
padding: 14px 0;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
border-bottom: 1px solid var(--color-border-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-member-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-member-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-member-avatar {
|
.teams-page-web-member-avatar {
|
||||||
width: 42px;
|
width: 44px;
|
||||||
height: 42px;
|
height: 44px;
|
||||||
display: grid;
|
display: flex;
|
||||||
place-items: center;
|
align-items: center;
|
||||||
border-radius: 999px;
|
justify-content: center;
|
||||||
background: rgba(8, 145, 178, 0.1);
|
border-radius: 12px;
|
||||||
|
background: var(--color-brand-soft);
|
||||||
color: var(--color-brand);
|
color: var(--color-brand);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-member-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-member-name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-member-name {
|
.teams-page-web-member-name {
|
||||||
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-member-desc {
|
.teams-page-web-member-email {
|
||||||
display: grid;
|
font-size: 13px;
|
||||||
gap: 4px;
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-member-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-empty-detail {
|
.teams-page-web-empty-detail {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-invite-label {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.teams-page-web-invite-code {
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.teams-page-web-copy-btn {
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.teams-page-web-invite-tip {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -44,20 +44,35 @@
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.teams-page-web-badge .anticon {
|
||||||
|
color: var(--color-brand);
|
||||||
|
}
|
||||||
|
|
||||||
.teams-page-web-title {
|
.teams-page-web-title {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 850;
|
||||||
|
color: var(--color-text);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-subtitle {
|
.teams-page-web-subtitle {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
font-size: 15px;
|
font-size: 14.5px;
|
||||||
line-height: 1.75;
|
line-height: 1.6;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
max-width: 580px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-create-btn,
|
.teams-page-web-create-btn {
|
||||||
.teams-page-web-soft-btn,
|
border-radius: 14px !important;
|
||||||
.teams-page-web-copy-btn {
|
height: 46px !important;
|
||||||
border-radius: 12px;
|
padding: 0 18px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-soft-btn {
|
||||||
|
border-radius: 12px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-stats-grid,
|
.teams-page-web-stats-grid,
|
||||||
|
|
@ -69,25 +84,19 @@
|
||||||
|
|
||||||
.teams-page-web-stats-grid {
|
.teams-page-web-stats-grid {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
margin-top: 20px;
|
margin-top: 24px;
|
||||||
}
|
gap: 16px; /* Optimized gap for stats grid */
|
||||||
|
|
||||||
.teams-page-web-stat-card,
|
|
||||||
.teams-page-web-mini-card {
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 16px 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-stat-card {
|
.teams-page-web-stat-card {
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 16px 18px;
|
||||||
background: rgba(255, 255, 255, 0.72);
|
background: rgba(255, 255, 255, 0.72);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.7);
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-stat-label,
|
.teams-page-web-stat-label,
|
||||||
.teams-page-web-section-desc,
|
.teams-page-web-section-desc {
|
||||||
.teams-page-web-mini-label,
|
|
||||||
.teams-page-web-member-desc,
|
|
||||||
.teams-page-web-invite-tip {
|
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
}
|
}
|
||||||
|
|
@ -102,14 +111,30 @@
|
||||||
.teams-page-web-stat-value {
|
.teams-page-web-stat-value {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-stat-chip {
|
.teams-page-web-stat-chip {
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 4px 8px;
|
padding: 4px 10px;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-stat-chip-brand {
|
||||||
|
background: var(--color-brand-soft);
|
||||||
|
color: var(--color-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-stat-chip-info {
|
||||||
|
background: var(--color-info-soft);
|
||||||
|
color: var(--color-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-stat-chip-success {
|
||||||
|
background: var(--color-success-soft);
|
||||||
|
color: var(--color-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-main-grid {
|
.teams-page-web-main-grid {
|
||||||
|
|
@ -144,29 +169,56 @@
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.teams-page-web-list-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.teams-page-web-nav-item {
|
.teams-page-web-nav-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
|
||||||
gap: 4px;
|
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 14px;
|
border-radius: 12px;
|
||||||
padding: 10px 12px;
|
padding: 10px 14px;
|
||||||
margin-bottom: 6px;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-nav-item:hover {
|
||||||
|
background: var(--color-surface-2);
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-nav-item.active {
|
.teams-page-web-nav-item.active {
|
||||||
background: rgba(8, 145, 178, 0.1);
|
background: var(--color-brand-soft);
|
||||||
border-color: rgba(8, 145, 178, 0.16);
|
border-color: rgba(8, 145, 178, 0.12);
|
||||||
color: var(--color-brand);
|
color: var(--color-brand);
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-nav-item small {
|
.teams-page-web-nav-item-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-nav-item-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-nav-item-count {
|
||||||
|
font-size: 12px;
|
||||||
color: var(--color-text-tertiary);
|
color: var(--color-text-tertiary);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.teams-page-web-nav-item.active .teams-page-web-nav-item-count {
|
||||||
|
color: var(--color-brand);
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.teams-page-web-detail-head {
|
.teams-page-web-detail-head {
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,6 @@
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.streaming-section {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.streaming-section-label {
|
.streaming-section-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-text-tertiary);
|
color: var(--color-text-tertiary);
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,14 @@ export default function ChatBody(props: {
|
||||||
|
|
||||||
const [streamingReasoningExpanded, setStreamingReasoningExpanded] = useState(true);
|
const [streamingReasoningExpanded, setStreamingReasoningExpanded] = useState(true);
|
||||||
|
|
||||||
// 当开始新的回答时,默认展开推理过程
|
// 当开始新的回答时,默认展开推理过程;当正式回答开始时,自动折叠推理过程
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (streaming.active && !streaming.answerText) {
|
if (streaming.active) {
|
||||||
|
if (!streaming.answerText) {
|
||||||
setStreamingReasoningExpanded(true);
|
setStreamingReasoningExpanded(true);
|
||||||
|
} else {
|
||||||
|
setStreamingReasoningExpanded(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [streaming.active, !!streaming.answerText]);
|
}, [streaming.active, !!streaming.answerText]);
|
||||||
|
|
||||||
|
|
@ -116,11 +120,15 @@ export default function ChatBody(props: {
|
||||||
onToggle={() => setStreamingReasoningExpanded(!streamingReasoningExpanded)}
|
onToggle={() => setStreamingReasoningExpanded(!streamingReasoningExpanded)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{streamingReasoningExpanded && <Divider style={{ margin: '8px 0' }} />}
|
{streaming.answerText && (
|
||||||
|
<>
|
||||||
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
<div className="streaming-section">
|
<div className="streaming-section">
|
||||||
<div className="streaming-section-label">正式回答</div>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(streaming.retrieved.length > 0 || streaming.toolCalls.length > 0) && (
|
{(streaming.retrieved.length > 0 || streaming.toolCalls.length > 0) && (
|
||||||
|
|
|
||||||
|
|
@ -1583,7 +1583,8 @@ span.mention {
|
||||||
}
|
}
|
||||||
|
|
||||||
.points-mall-banner-section {
|
.points-mall-banner-section {
|
||||||
display: grid;
|
/* display: grid; */
|
||||||
|
display: none;
|
||||||
grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue