Compare commits
2 Commits
eea532371b
...
b7c4159f1a
| Author | SHA1 | Date |
|---|---|---|
|
|
b7c4159f1a | |
|
|
8cf48b775b |
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
## 核心规则
|
||||
|
||||
1. **提交每次修改** — 每次任务执行后默认提交修改代码到远程仓库,或者用户说“提交代码”,执行git commit 并且要把commit 信息展示出来。默认是全部本地修改都提交,包括不是本次修改的也提交, 不需要提问用户确认。commit 信息你需要总结本次修改内容,不能是代码。例如:“修复登录页面的错误提示”.最后一定要执行 git push 推送到远程仓库。默认直接提交当前分支,包括main分支。
|
||||
2. **方案性内容** - 当用户需求,如果是方案性内容,你首先要评估,有没有更好的方案,而不是执行。更好的方案,你可以搜索类似阿里、字节等大厂的方案来参考并给出建议。
|
||||
3. **代码修改** - 任何时候,当存在字段格式不对,变量名不对,表使用不对等,禁止做兼容修改,必须按唯一性修改。比如约定字段是string,正确:只能传string;错误:可以传int。 比如约定字段名是data,正确:只能传data;错误:可以传sourceData或者data。
|
||||
4. **语言** - 永远使用中文跟用户沟通,专有名词可以用英文。
|
||||
5. **文档** - 在用户没有明确要求先提供文档时,禁止创建文档。避免生产垃圾文件。用户明确要求提供文档时,才创建文档。
|
||||
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ export const AuthAPI = {
|
|||
me: () => api.get<AuthUser>('/auth/me').then((r) => r.data),
|
||||
verify: async (phone: string, password: string) => {
|
||||
try {
|
||||
const res = await axios.post(`${API_BASE_URL}/urser`, { phone, password }, { timeout: 3000 });
|
||||
const res = await axios.post(`${API_BASE_URL}urser`, { phone, password }, { timeout: 3000 });
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
console.warn('Backend /urser not available, fallback to mock true', e);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import axios from 'axios';
|
||||
import { clearUserStorage } from '../utils/storage';
|
||||
|
||||
export const API_BASE_URL = import.meta.env.DEV ? '/api' : 'https://tianchaoai.cc/aura/v1';
|
||||
export const API_BASE_URL = 'https://www.tianchaoai.cc/api/v1/';
|
||||
const APP_BASE = (import.meta.env.BASE_URL || '/').replace(/\/$/, '');
|
||||
export const withAppBase = (path: string) => `${APP_BASE}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
export const withApiBase = (path: string) => `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`;
|
||||
export const withApiBase = (path: string) => `${API_BASE_URL}${path.replace(/^\//, '')}`;
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export async function streamChat(
|
|||
modelId?: string,
|
||||
imageUrls?: string[]
|
||||
) {
|
||||
const resp = await fetch(`${API_BASE_URL}/rooms/${roomId}/messages/stream`, {
|
||||
const resp = await fetch(`${API_BASE_URL}rooms/${roomId}/messages/stream`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
||||
body: JSON.stringify({
|
||||
|
|
@ -55,7 +55,7 @@ export async function regenerateMessage(
|
|||
overrides?: ModelOverrides,
|
||||
attachmentsText?: string
|
||||
) {
|
||||
const resp = await fetch(`${API_BASE_URL}/chat/${agentId}/messages/${messageId}/regenerate`, {
|
||||
const resp = await fetch(`${API_BASE_URL}chat/${agentId}/messages/${messageId}/regenerate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
||||
body: JSON.stringify({ overrides, attachmentsText }),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Button, Form, Input, Select, Space } from 'antd';
|
||||
import type { McpServer } from '../../../api';
|
||||
|
||||
interface Props {
|
||||
initial: McpServer | null;
|
||||
onSubmit: (values: any) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function McpPanelForm({ initial, onSubmit, onCancel }: Props) {
|
||||
const [form] = Form.useForm();
|
||||
const [transport, setTransport] = useState<'stdio' | 'sse' | 'http'>(initial?.transport ?? 'stdio');
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
name: initial?.name ?? '',
|
||||
transport: initial?.transport ?? 'stdio',
|
||||
command: initial?.command ?? '',
|
||||
argsText: (initial?.args ?? []).join('\n'),
|
||||
envText: initial?.env ? JSON.stringify(initial.env, null, 2) : '',
|
||||
url: initial?.url ?? '',
|
||||
});
|
||||
setTransport(initial?.transport ?? 'stdio');
|
||||
}, [initial, form]);
|
||||
|
||||
return (
|
||||
<Form form={form} layout="vertical" onFinish={onSubmit}>
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Input placeholder="例如 filesystem / playwright" />
|
||||
</Form.Item>
|
||||
<Form.Item name="transport" label="Transport" rules={[{ required: true, message: '请选择 Transport' }]}>
|
||||
<Select
|
||||
value={transport}
|
||||
onChange={(value) => {
|
||||
setTransport(value as any);
|
||||
form.setFieldValue('transport', value);
|
||||
}}
|
||||
options={[
|
||||
{ value: 'stdio', label: 'stdio (本机进程)' },
|
||||
{ value: 'sse', label: 'SSE (远程)' },
|
||||
{ value: 'http', label: 'HTTP (Streamable)' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{transport === 'stdio' ? (
|
||||
<>
|
||||
<Form.Item name="command" label="Command"><Input placeholder="npx" /></Form.Item>
|
||||
<Form.Item name="argsText" label="Args (每行一个)"><Input.TextArea placeholder={'-y\n@modelcontextprotocol/server-filesystem\n/path/to/dir'} autoSize={{ minRows: 3, maxRows: 8 }} className="mcp-panel-web-mono-input" /></Form.Item>
|
||||
<Form.Item name="envText" label="Env (JSON 对象)"><Input.TextArea placeholder={'{ "API_KEY": "xxx" }'} autoSize={{ minRows: 2, maxRows: 8 }} className="mcp-panel-web-mono-input" /></Form.Item>
|
||||
</>
|
||||
) : (
|
||||
<Form.Item name="url" label="URL" rules={[{ required: true, message: '请输入 URL' }]}><Input placeholder="https://example.com/mcp" /></Form.Item>
|
||||
)}
|
||||
|
||||
<Space className="mcp-panel-web-form-actions">
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" htmlType="submit">保存</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import { App as AntApp, Alert, Button, Card, List, Modal, Popconfirm, Space, Tag, Tooltip } from 'antd';
|
||||
import type { DesktopViewport } from '../../../hooks/useDesktopViewport';
|
||||
import { desktopViewportClass } from '../../../hooks/useDesktopViewport';
|
||||
import type { McpPanelLogicOutput } from '../McpPanelLogic';
|
||||
import McpPanelForm from './McpPanelForm';
|
||||
|
||||
export interface McpPanelWebVariantProps {
|
||||
agentId: string;
|
||||
logic: McpPanelLogicOutput;
|
||||
viewport: DesktopViewport;
|
||||
}
|
||||
|
||||
export default function McpPanelWebBase({ logic, viewport }: McpPanelWebVariantProps) {
|
||||
const { message } = AntApp.useApp();
|
||||
const { servers, statusList, statusLoading, createOpen, importOpen, importJson, PRESETS } = logic;
|
||||
const { handleImport, handleDelete, openCreate, openEdit, closeCreate, setImportOpen, setImportJson, refreshStatus } = logic;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`mcp-panel-web ${desktopViewportClass(viewport)}`}
|
||||
title={<Space wrap><span>MCP Servers</span><Tag>{servers.length} 个</Tag></Space>}
|
||||
extra={
|
||||
<Space wrap className="mcp-panel-web-actions">
|
||||
<Button onClick={refreshStatus} loading={statusLoading}>刷新连接状态</Button>
|
||||
<Button onClick={() => setImportOpen(true)}>导入 JSON</Button>
|
||||
<Button type="primary" onClick={openCreate}>新增</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Alert className="mcp-panel-web-alert" type="info" showIcon message="MCP (Model Context Protocol) 让智能体能调用外部工具。本机首次连接 stdio 类型会启动子进程,可能需要数秒。" />
|
||||
|
||||
<List
|
||||
dataSource={servers}
|
||||
locale={{ emptyText: '尚未配置 MCP Server' }}
|
||||
renderItem={(server) => {
|
||||
const status = statusList.find((item) => item.id === server.id);
|
||||
return (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button key="edit" size="small" onClick={() => openEdit(server)}>编辑</Button>,
|
||||
<Popconfirm key="del" title="确认删除该 MCP Server?" onConfirm={async () => { await handleDelete(server.id); message.success('已删除'); }}>
|
||||
<Button danger size="small">删除</Button>
|
||||
</Popconfirm>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<Space wrap>
|
||||
<Tag color={server.transport === 'stdio' ? 'blue' : 'green'}>{server.transport}</Tag>
|
||||
<span>{server.name}</span>
|
||||
{!server.enabled && <Tag>已停用</Tag>}
|
||||
{status?.error && <Tooltip title={status.error}><Tag color="error">连接失败</Tag></Tooltip>}
|
||||
{status && !status.error && <Tag color="success">{status.toolCount} 工具就绪</Tag>}
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<Space direction="vertical" size={2} className="mcp-panel-web-server-desc">
|
||||
<code className="mcp-panel-web-command">{server.transport === 'stdio' ? `${server.command} ${server.args.join(' ')}` : server.url}</code>
|
||||
{status?.tools?.length ? (
|
||||
<Space wrap size={4}>
|
||||
{status.tools.slice(0, 8).map((tool) => <Tooltip key={tool.name} title={tool.description}><Tag color="purple" className="mcp-panel-web-tool-tag">{tool.name}</Tag></Tooltip>)}
|
||||
{status.tools.length > 8 && <Tag>+{status.tools.length - 8}</Tag>}
|
||||
</Space>
|
||||
) : null}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal open={createOpen} title={logic.editing ? '编辑 MCP Server' : '新增 MCP Server'} width={680} onCancel={closeCreate} footer={null} destroyOnHidden>
|
||||
<McpPanelForm
|
||||
initial={logic.editing}
|
||||
onSubmit={async (values) => {
|
||||
const result = await logic.handleSave(values);
|
||||
result.success ? message.success(logic.editing ? '已更新' : '已创建') : message.error(result.error);
|
||||
}}
|
||||
onCancel={closeCreate}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal open={importOpen} title="导入 mcpServers JSON" width={760} onCancel={() => setImportOpen(false)} onOk={async () => {
|
||||
const result = await handleImport();
|
||||
result.success ? message.success(`已导入 ${result.imported} 个 MCP Server`) : message.error(result.error);
|
||||
}} okText="导入" cancelText="取消">
|
||||
<Alert type="info" showIcon message="兼容 Claude Desktop / Cursor 的 mcpServers 配置格式。" className="mcp-panel-web-alert" />
|
||||
<Space className="mcp-panel-web-presets" wrap>
|
||||
{PRESETS.map((preset) => <Tooltip key={preset.label} title={preset.description}><Button size="small" onClick={() => setImportJson(JSON.stringify(preset.config, null, 2))}>{preset.label}</Button></Tooltip>)}
|
||||
</Space>
|
||||
<textarea value={importJson} onChange={(e) => setImportJson(e.target.value)} placeholder={'{\\n "mcpServers": {}\\n}'} className="mcp-panel-web-import-textarea" />
|
||||
</Modal>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { McpPanelLogicOutput } from '../McpPanelLogic';
|
||||
import McpPanelWebBase from './McpPanelWebBase';
|
||||
|
||||
export default function McpPanelWebLarge2k({ agentId, logic }: { agentId: string; logic: McpPanelLogicOutput }) {
|
||||
return <McpPanelWebBase agentId={agentId} logic={logic} viewport="large2k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { McpPanelLogicOutput } from '../McpPanelLogic';
|
||||
import McpPanelWebBase from './McpPanelWebBase';
|
||||
|
||||
export default function McpPanelWebSmallPc({ agentId, logic }: { agentId: string; logic: McpPanelLogicOutput }) {
|
||||
return <McpPanelWebBase agentId={agentId} logic={logic} viewport="smallPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { McpPanelLogicOutput } from '../McpPanelLogic';
|
||||
import McpPanelWebBase from './McpPanelWebBase';
|
||||
|
||||
export default function McpPanelWebStandardPc({ agentId, logic }: { agentId: string; logic: McpPanelLogicOutput }) {
|
||||
return <McpPanelWebBase agentId={agentId} logic={logic} viewport="standardPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { McpPanelLogicOutput } from '../McpPanelLogic';
|
||||
import McpPanelWebBase from './McpPanelWebBase';
|
||||
|
||||
export default function McpPanelWebTablet({ agentId, logic }: { agentId: string; logic: McpPanelLogicOutput }) {
|
||||
return <McpPanelWebBase agentId={agentId} logic={logic} viewport="tablet" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { McpPanelLogicOutput } from '../McpPanelLogic';
|
||||
import McpPanelWebBase from './McpPanelWebBase';
|
||||
|
||||
export default function McpPanelWebUltra4k({ agentId, logic }: { agentId: string; logic: McpPanelLogicOutput }) {
|
||||
return <McpPanelWebBase agentId={agentId} logic={logic} viewport="ultra4k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.mcp-panel-web.desktop-large2k {
|
||||
--mcp-panel-max-width: 1760px;
|
||||
--mcp-command-max-width: 980px;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.mcp-panel-web.desktop-smallPc {
|
||||
--mcp-command-max-width: 560px;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.mcp-panel-web.desktop-standardPc {
|
||||
--mcp-command-max-width: 720px;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
.mcp-panel-web.desktop-tablet {
|
||||
--mcp-command-max-width: 420px;
|
||||
}
|
||||
|
||||
.mcp-panel-web.desktop-tablet .ant-card-head-wrapper,
|
||||
.mcp-panel-web.desktop-tablet .mcp-panel-web-actions {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.mcp-panel-web.desktop-ultra4k {
|
||||
--mcp-panel-max-width: 2240px;
|
||||
--mcp-command-max-width: 1280px;
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
@import './mcp-panel-web-tablet.css';
|
||||
@import './mcp-panel-web-small-pc.css';
|
||||
@import './mcp-panel-web-standard-pc.css';
|
||||
@import './mcp-panel-web-large-2k.css';
|
||||
@import './mcp-panel-web-ultra-4k.css';
|
||||
|
||||
.mcp-panel-web {
|
||||
max-width: var(--mcp-panel-max-width, 100%);
|
||||
}
|
||||
|
||||
.mcp-panel-web-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.mcp-panel-web-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mcp-panel-web-server-desc {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mcp-panel-web-command {
|
||||
display: block;
|
||||
max-width: min(100%, var(--mcp-command-max-width, 720px));
|
||||
overflow: hidden;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mcp-panel-web-tool-tag {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.mcp-panel-web-presets {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mcp-panel-web-import-textarea {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
max-height: 300px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mcp-panel-web-mono-input {
|
||||
font-family: Consolas, Menlo, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mcp-panel-web-form-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
export type DesktopViewport = 'tablet' | 'smallPc' | 'standardPc' | 'large2k' | 'ultra4k';
|
||||
|
||||
const getViewport = (width: number): DesktopViewport => {
|
||||
if (width < 1024) return 'tablet';
|
||||
if (width < 1280) return 'smallPc';
|
||||
if (width < 1920) return 'standardPc';
|
||||
if (width < 3840) return 'large2k';
|
||||
return 'ultra4k';
|
||||
};
|
||||
|
||||
const getCurrentViewport = () => {
|
||||
if (typeof window === 'undefined') return 'standardPc';
|
||||
return getViewport(window.innerWidth);
|
||||
};
|
||||
|
||||
export function useDesktopViewport() {
|
||||
const [viewport, setViewport] = useState<DesktopViewport>(getCurrentViewport);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => setViewport(getCurrentViewport());
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize();
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
return viewport;
|
||||
}
|
||||
|
||||
export const desktopViewportClass = (viewport: DesktopViewport) => `desktop-${viewport}`;
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import { ArrowRightOutlined, CompassOutlined, RobotOutlined } from '@ant-design/icons';
|
||||
import { App as AntApp, Button, Empty } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { DesktopViewport } from '../../../hooks/useDesktopViewport';
|
||||
import { desktopViewportClass } from '../../../hooks/useDesktopViewport';
|
||||
import type { AgentListLogicOutput } from '../AgentListLogic';
|
||||
import AgentListWebCard from './AgentListWebCard';
|
||||
|
||||
export interface AgentListWebVariantProps {
|
||||
logic: AgentListLogicOutput;
|
||||
viewport: DesktopViewport;
|
||||
}
|
||||
|
||||
export default function AgentListWebBase({ logic, viewport }: AgentListWebVariantProps) {
|
||||
const { message } = AntApp.useApp();
|
||||
const navigate = useNavigate();
|
||||
const { list, loading, stats } = logic;
|
||||
|
||||
return (
|
||||
<div className={`page-container agent-list-web ${desktopViewportClass(viewport)}`}>
|
||||
<div className="agent-list-web-hero">
|
||||
<div className="agent-list-web-hero-header">
|
||||
<div className="agent-list-web-hero-copy">
|
||||
<div className="agent-list-web-badge">
|
||||
<RobotOutlined />
|
||||
我的 Agent 资产
|
||||
</div>
|
||||
<h2 className="page-title agent-list-web-title">我的智能体</h2>
|
||||
<div className="page-subtitle agent-list-web-subtitle">
|
||||
把你的 AI 助手沉淀成一组可管理、可协作、可持续进化的能力单元。创建入口统一在智能体广场,这里负责查看、进入和运营它们。
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" size="large" icon={<CompassOutlined />} onClick={() => navigate('/marketplace')} className="agent-list-web-market-btn">
|
||||
前往智能体广场
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="agent-list-web-stats-grid">
|
||||
{stats.map((item) => (
|
||||
<div className="agent-list-web-stat-card" key={item.label}>
|
||||
<div className="agent-list-web-stat-label">{item.label}</div>
|
||||
<div className="agent-list-web-stat-row">
|
||||
<span className="agent-list-web-stat-value">{item.value}</span>
|
||||
<span className="agent-list-web-stat-chip" style={{ background: item.tone, color: item.color }}>
|
||||
实时统计
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!loading && list.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<Empty description="你还没有任何智能体">
|
||||
<Button type="primary" onClick={() => navigate('/marketplace')} className="agent-list-web-empty-btn">
|
||||
前往广场创建
|
||||
</Button>
|
||||
</Empty>
|
||||
</div>
|
||||
) : (
|
||||
<div className="agent-list-web-card-grid">
|
||||
{list.map((agent) => (
|
||||
<AgentListWebCard key={agent.id} agent={agent} logic={logic} notifyDeleted={() => message.success('已删除')} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{list.length > 0 && (
|
||||
<div className="agent-list-web-footer">
|
||||
<div>
|
||||
<div className="agent-list-web-footer-title">想创建新的智能体入口?</div>
|
||||
<div className="agent-list-web-footer-desc">统一从智能体广场进入,保证创建流程和发现体验保持一致。</div>
|
||||
</div>
|
||||
<Button type="text" icon={<ArrowRightOutlined />} onClick={() => navigate('/marketplace')} className="agent-list-web-footer-action">
|
||||
去广场继续发现
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { DeleteOutlined, EditOutlined, MessageOutlined } from '@ant-design/icons';
|
||||
import { Button, Popconfirm, Space, Tag } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { Agent } from '../../../api';
|
||||
import type { AgentListLogicOutput } from '../AgentListLogic';
|
||||
|
||||
interface Props {
|
||||
agent: Agent;
|
||||
logic: AgentListLogicOutput;
|
||||
notifyDeleted: () => void;
|
||||
}
|
||||
|
||||
export default function AgentListWebCard({ agent, logic, notifyDeleted }: Props) {
|
||||
const { handleDelete, isImageUrl, getModelLabel } = logic;
|
||||
const modelLabel = getModelLabel(agent.model);
|
||||
|
||||
return (
|
||||
<div className="agent-card agent-list-web-card">
|
||||
<div className="agent-list-web-card-head">
|
||||
<div className="avatar agent-list-web-avatar" style={{ background: agent.avatar || 'var(--gradient-brand)' }}>
|
||||
{isImageUrl(agent.avatar) ? <img src={agent.avatar} className="w-full h-full object-cover" alt="avatar" /> : (agent.name?.charAt(0) || '?').toUpperCase()}
|
||||
</div>
|
||||
<div className="agent-list-web-card-title-block">
|
||||
<div className="agent-list-web-card-title">{agent.name}</div>
|
||||
<div className="agent-list-web-card-meta">最近更新于 {dayjs(agent.updated_at).format('YYYY-MM-DD')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="agent-list-web-desc-box">
|
||||
<div className="desc agent-list-web-desc">{agent.description || '还没有填写描述,可以补充这个智能体适合解决什么问题。'}</div>
|
||||
</div>
|
||||
|
||||
<Space size={6} wrap className="agent-list-web-tags">
|
||||
{agent.visibility === 'public' && <Tag bordered={false} className="agent-list-web-tag-success">公开</Tag>}
|
||||
{agent.visibility === 'team' && <Tag bordered={false} className="agent-list-web-tag-info">团队</Tag>}
|
||||
{agent.visibility === 'private' && <Tag bordered={false} className="agent-list-web-tag-neutral">私有</Tag>}
|
||||
{modelLabel && (
|
||||
<Tag bordered={false} className="agent-list-web-tag-brand">
|
||||
<span className="agent-list-web-model-label">{modelLabel}</span>
|
||||
</Tag>
|
||||
)}
|
||||
{(agent.fork_count ?? 0) > 0 && <Tag bordered={false} className="agent-list-web-tag-neutral">Fork {agent.fork_count}</Tag>}
|
||||
</Space>
|
||||
|
||||
<div className="agent-list-web-card-actions">
|
||||
<Link to={`/chat/${agent.id}`} className="agent-list-web-action-link">
|
||||
<Button type="primary" block icon={<MessageOutlined />} className="agent-list-web-action-btn">
|
||||
聊天
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to={`/agents/${agent.id}`} className="agent-list-web-action-link">
|
||||
<Button block icon={<EditOutlined />} className="agent-list-web-action-btn">
|
||||
管理
|
||||
</Button>
|
||||
</Link>
|
||||
<Popconfirm
|
||||
title="确定删除该智能体?"
|
||||
description="将删除其知识库与对话记录"
|
||||
onConfirm={() => {
|
||||
handleDelete(agent.id);
|
||||
notifyDeleted();
|
||||
}}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button danger icon={<DeleteOutlined />} className="agent-list-web-delete-btn" />
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { AgentListLogicOutput } from '../AgentListLogic';
|
||||
import AgentListWebBase from './AgentListWebBase';
|
||||
|
||||
export default function AgentListWebLarge2k({ logic }: { logic: AgentListLogicOutput }) {
|
||||
return <AgentListWebBase logic={logic} viewport="large2k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { AgentListLogicOutput } from '../AgentListLogic';
|
||||
import AgentListWebBase from './AgentListWebBase';
|
||||
|
||||
export default function AgentListWebSmallPc({ logic }: { logic: AgentListLogicOutput }) {
|
||||
return <AgentListWebBase logic={logic} viewport="smallPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { AgentListLogicOutput } from '../AgentListLogic';
|
||||
import AgentListWebBase from './AgentListWebBase';
|
||||
|
||||
export default function AgentListWebStandardPc({ logic }: { logic: AgentListLogicOutput }) {
|
||||
return <AgentListWebBase logic={logic} viewport="standardPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { AgentListLogicOutput } from '../AgentListLogic';
|
||||
import AgentListWebBase from './AgentListWebBase';
|
||||
|
||||
export default function AgentListWebTablet({ logic }: { logic: AgentListLogicOutput }) {
|
||||
return <AgentListWebBase logic={logic} viewport="tablet" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { AgentListLogicOutput } from '../AgentListLogic';
|
||||
import AgentListWebBase from './AgentListWebBase';
|
||||
|
||||
export default function AgentListWebUltra4k({ logic }: { logic: AgentListLogicOutput }) {
|
||||
return <AgentListWebBase logic={logic} viewport="ultra4k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.agent-list-web.desktop-large2k {
|
||||
--agent-list-columns: 5;
|
||||
--agent-list-gap: 22px;
|
||||
--agent-list-max-width: 1760px;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.agent-list-web.desktop-smallPc {
|
||||
--agent-list-columns: 3;
|
||||
--agent-list-gap: 16px;
|
||||
--agent-list-max-width: 1080px;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.agent-list-web.desktop-standardPc {
|
||||
--agent-list-columns: 4;
|
||||
--agent-list-gap: 18px;
|
||||
--agent-list-max-width: 1240px;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
.agent-list-web.desktop-tablet {
|
||||
--agent-list-columns: 2;
|
||||
--agent-list-gap: 14px;
|
||||
--agent-list-max-width: 100%;
|
||||
}
|
||||
|
||||
.agent-list-web.desktop-tablet .agent-list-web-stats-grid,
|
||||
.agent-list-web.desktop-tablet .agent-list-web-hero-header {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.agent-list-web.desktop-tablet .agent-list-web-stats-grid,
|
||||
.agent-list-web.desktop-tablet .agent-list-web-hero-header {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.agent-list-web.desktop-tablet .agent-list-web-market-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.agent-list-web.desktop-ultra4k {
|
||||
--agent-list-columns: 6;
|
||||
--agent-list-gap: 24px;
|
||||
--agent-list-max-width: 2240px;
|
||||
}
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
@import './agent-list-web-tablet.css';
|
||||
@import './agent-list-web-small-pc.css';
|
||||
@import './agent-list-web-standard-pc.css';
|
||||
@import './agent-list-web-large-2k.css';
|
||||
@import './agent-list-web-ultra-4k.css';
|
||||
|
||||
.agent-list-web {
|
||||
max-width: var(--agent-list-max-width, 1240px);
|
||||
}
|
||||
.agent-list-web-hero {
|
||||
border-radius: 24px;
|
||||
padding: 30px 30px 26px;
|
||||
margin-bottom: 24px;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.98), rgba(236,253,245,0.92) 48%, rgba(239,246,255,0.96));
|
||||
border: 1px solid rgba(8, 145, 178, 0.12);
|
||||
box-shadow: 0 20px 48px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.agent-list-web-hero-header,
|
||||
.agent-list-web-footer,
|
||||
.agent-list-web-card-actions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.agent-list-web-hero-header,
|
||||
.agent-list-web-footer {
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.agent-list-web-hero-copy {
|
||||
max-width: 620px;
|
||||
}
|
||||
|
||||
.agent-list-web-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
margin-bottom: 16px;
|
||||
background: rgba(255,255,255,0.78);
|
||||
border: 1px solid rgba(8, 145, 178, 0.1);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.agent-list-web-title {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.agent-list-web-subtitle {
|
||||
margin-top: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
.agent-list-web-market-btn,
|
||||
.agent-list-web-empty-btn,
|
||||
.agent-list-web-action-btn,
|
||||
.agent-list-web-delete-btn {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.agent-list-web-stats-grid,
|
||||
.agent-list-web-card-grid {
|
||||
display: grid;
|
||||
gap: var(--agent-list-gap, 18px);
|
||||
}
|
||||
|
||||
.agent-list-web-stats-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.agent-list-web-stat-card {
|
||||
border-radius: 18px;
|
||||
padding: 16px 18px;
|
||||
background: rgba(255,255,255,0.72);
|
||||
border: 1px solid rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.agent-list-web-stat-label,
|
||||
.agent-list-web-card-meta,
|
||||
.agent-list-web-footer-desc {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.agent-list-web-stat-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.agent-list-web-stat-value {
|
||||
color: var(--color-text);
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.agent-list-web-stat-chip {
|
||||
border-radius: 999px;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.agent-list-web-card-grid {
|
||||
grid-template-columns: repeat(var(--agent-list-columns, 4), minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.agent-list-web-card {
|
||||
min-width: 0;
|
||||
min-height: 292px;
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(252,252,253,1));
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.045);
|
||||
}
|
||||
|
||||
.agent-list-web-card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.agent-list-web-avatar {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.agent-list-web-card-title-block,
|
||||
.agent-list-web-model-label {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-list-web-card-title {
|
||||
margin-bottom: 4px;
|
||||
color: var(--color-text);
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.agent-list-web-desc-box {
|
||||
margin-top: 16px;
|
||||
padding: 16px 16px 18px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, rgba(248,250,252,0.9), rgba(255,255,255,0.95));
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
}
|
||||
|
||||
.agent-list-web-desc {
|
||||
min-height: 66px;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.agent-list-web-tags {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.agent-list-web-tag-success,
|
||||
.agent-list-web-tag-info,
|
||||
.agent-list-web-tag-neutral,
|
||||
.agent-list-web-tag-brand {
|
||||
border-radius: 999px;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.agent-list-web-tag-success {
|
||||
background: var(--color-success-soft);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.agent-list-web-tag-info {
|
||||
background: var(--color-info-soft);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.agent-list-web-tag-neutral {
|
||||
background: var(--color-surface-2);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.agent-list-web-tag-brand {
|
||||
background: var(--color-brand-soft);
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.agent-list-web-model-label {
|
||||
display: inline-block;
|
||||
max-width: 190px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agent-list-web-card-actions {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.agent-list-web-action-link {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-list-web-action-btn,
|
||||
.agent-list-web-delete-btn {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.agent-list-web-action-btn {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.agent-list-web-delete-btn {
|
||||
width: 40px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.agent-list-web-footer {
|
||||
align-items: center;
|
||||
border-radius: 20px;
|
||||
padding: 18px 20px;
|
||||
margin-top: 24px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.agent-list-web-footer-title {
|
||||
margin-bottom: 4px;
|
||||
color: var(--color-text);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.agent-list-web-footer-action {
|
||||
color: var(--color-brand);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { CheckCircleOutlined } from '@ant-design/icons';
|
||||
import type { DesktopViewport } from '../../../hooks/useDesktopViewport';
|
||||
import { desktopViewportClass } from '../../../hooks/useDesktopViewport';
|
||||
import kaiwuIcon from '../../../assets/brand/kaiwu-icon-gradient-transparent.png';
|
||||
import type { LoginPageLogic } from '../LoginPageLogic';
|
||||
import LoginFormCard from './LoginFormCard';
|
||||
|
||||
export interface LoginPageWebVariantProps {
|
||||
logic: LoginPageLogic;
|
||||
viewport: DesktopViewport;
|
||||
}
|
||||
|
||||
const WEB_FEATURES = ['多模型即插即用', '知识库与工具闭环', '可分享智能体'];
|
||||
|
||||
export default function LoginPageWebBase({ logic, viewport }: LoginPageWebVariantProps) {
|
||||
return (
|
||||
<div className={`login-page login-page-web ${desktopViewportClass(viewport)}`}>
|
||||
<div className="login-deco login-deco-1" />
|
||||
<div className="login-deco login-deco-2" />
|
||||
|
||||
<div className="login-content login-page-web-content">
|
||||
<div className="login-brand-panel">
|
||||
<div className="login-brand-header">
|
||||
<img src={kaiwuIcon} alt="鲸域AI" className="login-brand-logo" />
|
||||
<span className="login-brand-name">鲸域AI</span>
|
||||
</div>
|
||||
|
||||
<h1 className="login-title">
|
||||
为业务流程配置
|
||||
<br />
|
||||
<span className="login-title-highlight">可调用的 AI 智能体</span>
|
||||
</h1>
|
||||
|
||||
<p className="login-subtitle">
|
||||
将知识库、工具、额度与数据看板放进同一个闭环,让团队从试用走向可复购的平台化交付。
|
||||
</p>
|
||||
|
||||
<div className="login-features">
|
||||
{WEB_FEATURES.map((text) => (
|
||||
<div key={text} className="login-feature-item">
|
||||
<CheckCircleOutlined className="login-feature-icon" />
|
||||
{text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-form-panel">
|
||||
<LoginFormCard logic={logic} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { LoginPageLogic } from '../LoginPageLogic';
|
||||
import LoginPageWebBase from './LoginPageWebBase';
|
||||
|
||||
export default function LoginPageWebLarge2k({ logic }: { logic: LoginPageLogic }) {
|
||||
return <LoginPageWebBase logic={logic} viewport="large2k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { LoginPageLogic } from '../LoginPageLogic';
|
||||
import LoginPageWebBase from './LoginPageWebBase';
|
||||
|
||||
export default function LoginPageWebSmallPc({ logic }: { logic: LoginPageLogic }) {
|
||||
return <LoginPageWebBase logic={logic} viewport="smallPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { LoginPageLogic } from '../LoginPageLogic';
|
||||
import LoginPageWebBase from './LoginPageWebBase';
|
||||
|
||||
export default function LoginPageWebStandardPc({ logic }: { logic: LoginPageLogic }) {
|
||||
return <LoginPageWebBase logic={logic} viewport="standardPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { LoginPageLogic } from '../LoginPageLogic';
|
||||
import LoginPageWebBase from './LoginPageWebBase';
|
||||
|
||||
export default function LoginPageWebTablet({ logic }: { logic: LoginPageLogic }) {
|
||||
return <LoginPageWebBase logic={logic} viewport="tablet" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { LoginPageLogic } from '../LoginPageLogic';
|
||||
import LoginPageWebBase from './LoginPageWebBase';
|
||||
|
||||
export default function LoginPageWebUltra4k({ logic }: { logic: LoginPageLogic }) {
|
||||
return <LoginPageWebBase logic={logic} viewport="ultra4k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
.login-page-web.desktop-large2k .login-page-web-content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 520px;
|
||||
width: min(100%, 1720px);
|
||||
}
|
||||
|
||||
.login-page-web.desktop-large2k .login-brand-panel {
|
||||
padding: 80px 110px;
|
||||
}
|
||||
|
||||
.login-page-web.desktop-large2k .login-title {
|
||||
max-width: 680px;
|
||||
font-size: 58px;
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
.login-page-web.desktop-smallPc .login-page-web-content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 420px;
|
||||
width: min(100%, 1100px);
|
||||
}
|
||||
|
||||
.login-page-web.desktop-smallPc .login-brand-panel {
|
||||
padding: 48px 42px;
|
||||
}
|
||||
|
||||
.login-page-web.desktop-smallPc .login-title {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.login-page-web.desktop-smallPc .login-form-panel {
|
||||
width: auto;
|
||||
padding: 32px;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.login-page-web.desktop-standardPc .login-page-web-content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 480px;
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
.login-page-web.desktop-tablet {
|
||||
display: block;
|
||||
min-height: 100svh;
|
||||
justify-content: initial;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 20px 16px 24px;
|
||||
}
|
||||
|
||||
.login-page-web.desktop-tablet .login-page-web-content {
|
||||
width: 100%;
|
||||
min-height: auto;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.login-page-web.desktop-tablet .login-brand-panel {
|
||||
padding: 4px 0 0;
|
||||
}
|
||||
|
||||
.login-page-web.desktop-tablet .login-brand-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-page-web.desktop-tablet .login-title {
|
||||
max-width: none;
|
||||
font-size: 32px;
|
||||
line-height: 1.18;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.login-page-web.desktop-tablet .login-subtitle {
|
||||
max-width: none;
|
||||
margin-top: 14px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.login-page-web.desktop-tablet .login-features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.login-page-web.desktop-tablet .login-feature-item {
|
||||
min-height: 44px;
|
||||
border: 1px solid rgba(17, 103, 255, 0.14);
|
||||
border-radius: 14px;
|
||||
padding: 10px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.login-page-web.desktop-tablet .login-form-panel {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
.login-page-web.desktop-ultra4k .login-page-web-content {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 560px;
|
||||
width: min(100%, 2240px);
|
||||
}
|
||||
|
||||
.login-page-web.desktop-ultra4k .login-brand-panel {
|
||||
padding: 110px 160px;
|
||||
}
|
||||
|
||||
.login-page-web.desktop-ultra4k .login-title {
|
||||
max-width: 760px;
|
||||
font-size: 68px;
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import { CompassOutlined, FireOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { Button, Empty, Input, Space, Spin, Tag } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { DesktopViewport } from '../../../hooks/useDesktopViewport';
|
||||
import { desktopViewportClass } from '../../../hooks/useDesktopViewport';
|
||||
import type { MarketplaceAgent } from '../../../api';
|
||||
import type { MarketplacePageLogicOutput } from '../MarketplacePageLogic';
|
||||
|
||||
export interface MarketplacePageWebVariantProps {
|
||||
logic: MarketplacePageLogicOutput;
|
||||
viewport: DesktopViewport;
|
||||
}
|
||||
|
||||
const CardAvatar = ({ agent, isImageUrl }: { agent: MarketplaceAgent; isImageUrl: (url: string) => boolean }) => (
|
||||
<div className="marketplace-web-avatar">
|
||||
{agent.avatar && isImageUrl(agent.avatar) ? <img src={agent.avatar} className="w-full h-full object-cover" alt="avatar" /> : (agent.name?.charAt(0) || '?').toUpperCase()}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function MarketplacePageWebBase({ logic, viewport }: MarketplacePageWebVariantProps) {
|
||||
const navigate = useNavigate();
|
||||
const { loading, q, filtered, setQ, handleFork, isImageUrl } = logic;
|
||||
|
||||
return (
|
||||
<div className={`marketplace-page-web ${desktopViewportClass(viewport)}`}>
|
||||
<div className="page-hero marketplace-web-hero">
|
||||
<div className="marketplace-web-hero-inner">
|
||||
<div className="marketplace-web-badge">
|
||||
<CompassOutlined />
|
||||
探索社区智能体
|
||||
</div>
|
||||
<h1 className="hero-title">找到更适合你的 AI 伙伴</h1>
|
||||
<p className="hero-subtitle">浏览社区创建的智能体,快速复制、微调并投入你的日常工作流。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-container marketplace-web-container">
|
||||
<div className="marketplace-web-toolbar">
|
||||
<Input
|
||||
placeholder="搜索智能体名称、描述或作者..."
|
||||
prefix={<SearchOutlined className="marketplace-web-muted-icon" />}
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
className="marketplace-web-search"
|
||||
allowClear
|
||||
/>
|
||||
<Button type="primary" size="large" icon={<PlusOutlined />} onClick={() => navigate('/agents/new')} className="marketplace-web-create">
|
||||
创建新智能体
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="marketplace-web-loading">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="marketplace-web-card-grid">
|
||||
{!q && (
|
||||
<div onClick={() => navigate('/agents/new')} className="create-card marketplace-web-create-card">
|
||||
<div className="create-icon">
|
||||
<PlusOutlined />
|
||||
</div>
|
||||
<div className="marketplace-web-card-copy">
|
||||
<div className="marketplace-web-card-title">新建智能体</div>
|
||||
<div className="marketplace-web-card-meta">从空白开始</div>
|
||||
<div className="desc">从名称、提示词、模型和能力配置开始,搭建你的专属 AI 助手。</div>
|
||||
</div>
|
||||
<Button block className="marketplace-web-card-action">
|
||||
开始创建
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filtered.map((agent) => (
|
||||
<div className="agent-card marketplace-web-agent-card" key={agent.id}>
|
||||
<div className="marketplace-web-card-top">
|
||||
<CardAvatar agent={agent} isImageUrl={isImageUrl} />
|
||||
{agent.fork_count > 10 && <Tag bordered={false} icon={<FireOutlined />} className="marketplace-web-hot-tag">热门</Tag>}
|
||||
</div>
|
||||
<div className="marketplace-web-card-copy">
|
||||
<div className="marketplace-web-card-title">{agent.name}</div>
|
||||
<div className="marketplace-web-card-meta">by {agent.ownerName || '匿名作者'}</div>
|
||||
<div className="desc">{agent.description || '暂无详细描述'}</div>
|
||||
</div>
|
||||
<Space size={4} wrap className="marketplace-web-tags">
|
||||
{agent.kbCount > 0 && <Tag bordered={false} className="marketplace-web-info-tag">{agent.kbCount} 知识</Tag>}
|
||||
{agent.skillCount > 0 && <Tag bordered={false} className="marketplace-web-success-tag">{agent.skillCount} 技能</Tag>}
|
||||
</Space>
|
||||
<Button block onClick={() => handleFork(agent)} className="marketplace-web-card-action">
|
||||
复制到我的
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filtered.length === 0 && !loading && <Empty description="没有找到匹配的智能体" className="marketplace-web-empty" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { MarketplacePageLogicOutput } from '../MarketplacePageLogic';
|
||||
import MarketplacePageWebBase from './MarketplacePageWebBase';
|
||||
|
||||
export default function MarketplacePageWebLarge2k({ logic }: { logic: MarketplacePageLogicOutput }) {
|
||||
return <MarketplacePageWebBase logic={logic} viewport="large2k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { MarketplacePageLogicOutput } from '../MarketplacePageLogic';
|
||||
import MarketplacePageWebBase from './MarketplacePageWebBase';
|
||||
|
||||
export default function MarketplacePageWebSmallPc({ logic }: { logic: MarketplacePageLogicOutput }) {
|
||||
return <MarketplacePageWebBase logic={logic} viewport="smallPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { MarketplacePageLogicOutput } from '../MarketplacePageLogic';
|
||||
import MarketplacePageWebBase from './MarketplacePageWebBase';
|
||||
|
||||
export default function MarketplacePageWebStandardPc({ logic }: { logic: MarketplacePageLogicOutput }) {
|
||||
return <MarketplacePageWebBase logic={logic} viewport="standardPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { MarketplacePageLogicOutput } from '../MarketplacePageLogic';
|
||||
import MarketplacePageWebBase from './MarketplacePageWebBase';
|
||||
|
||||
export default function MarketplacePageWebTablet({ logic }: { logic: MarketplacePageLogicOutput }) {
|
||||
return <MarketplacePageWebBase logic={logic} viewport="tablet" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { MarketplacePageLogicOutput } from '../MarketplacePageLogic';
|
||||
import MarketplacePageWebBase from './MarketplacePageWebBase';
|
||||
|
||||
export default function MarketplacePageWebUltra4k({ logic }: { logic: MarketplacePageLogicOutput }) {
|
||||
return <MarketplacePageWebBase logic={logic} viewport="ultra4k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.marketplace-page-web.desktop-large2k {
|
||||
--marketplace-columns: 5;
|
||||
--marketplace-gap: 22px;
|
||||
--marketplace-max-width: 1760px;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.marketplace-page-web.desktop-smallPc {
|
||||
--marketplace-columns: 3;
|
||||
--marketplace-gap: 16px;
|
||||
--marketplace-max-width: 1080px;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.marketplace-page-web.desktop-standardPc {
|
||||
--marketplace-columns: 4;
|
||||
--marketplace-gap: 20px;
|
||||
--marketplace-max-width: 1240px;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
.marketplace-page-web.desktop-tablet {
|
||||
--marketplace-columns: 2;
|
||||
--marketplace-gap: 14px;
|
||||
--marketplace-max-width: 100%;
|
||||
}
|
||||
|
||||
.marketplace-page-web.desktop-tablet .marketplace-web-toolbar {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.marketplace-page-web.desktop-tablet .marketplace-web-create {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.marketplace-page-web.desktop-ultra4k {
|
||||
--marketplace-columns: 6;
|
||||
--marketplace-gap: 24px;
|
||||
--marketplace-max-width: 2240px;
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
@import './marketplace-page-web-tablet.css';
|
||||
@import './marketplace-page-web-small-pc.css';
|
||||
@import './marketplace-page-web-standard-pc.css';
|
||||
@import './marketplace-page-web-large-2k.css';
|
||||
@import './marketplace-page-web-ultra-4k.css';
|
||||
|
||||
.marketplace-web-hero-inner {
|
||||
max-width: var(--marketplace-max-width, 1240px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.marketplace-web-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
margin-bottom: 18px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.marketplace-web-container {
|
||||
max-width: var(--marketplace-max-width, 1240px);
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.marketplace-web-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 520px) max-content;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.marketplace-web-search,
|
||||
.marketplace-web-create {
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.marketplace-web-create {
|
||||
padding: 0 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.marketplace-web-muted-icon {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.marketplace-web-loading {
|
||||
padding: 60px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.marketplace-web-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--marketplace-columns, 4), minmax(0, 1fr));
|
||||
gap: var(--marketplace-gap, 20px);
|
||||
}
|
||||
|
||||
.marketplace-web-create-card,
|
||||
.marketplace-web-agent-card {
|
||||
min-width: 0;
|
||||
min-height: 292px;
|
||||
}
|
||||
|
||||
.marketplace-web-card-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.marketplace-web-avatar,
|
||||
.marketplace-web-create-card .create-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.marketplace-web-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background: var(--gradient-brand);
|
||||
color: #fff;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.marketplace-web-card-copy {
|
||||
min-width: 0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.marketplace-web-card-title {
|
||||
margin-bottom: 4px;
|
||||
color: var(--color-text);
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.marketplace-web-card-meta {
|
||||
margin-bottom: 12px;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.marketplace-web-tags {
|
||||
margin-top: auto;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.marketplace-web-hot-tag,
|
||||
.marketplace-web-info-tag,
|
||||
.marketplace-web-success-tag {
|
||||
border-radius: 999px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.marketplace-web-hot-tag {
|
||||
background: var(--color-warning-soft);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.marketplace-web-info-tag {
|
||||
background: var(--color-info-soft);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.marketplace-web-success-tag {
|
||||
background: var(--color-success-soft);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.marketplace-web-card-action {
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.marketplace-web-empty {
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { Form } from 'antd';
|
||||
import type { DesktopViewport } from '../../../hooks/useDesktopViewport';
|
||||
import { desktopViewportClass } from '../../../hooks/useDesktopViewport';
|
||||
import type { PointsMallPageLogicOutput } from '../PointsMallPageLogic';
|
||||
import type { ExchangeFormValues } from '../types';
|
||||
import ConfirmExchangeModal from './ConfirmExchangeModal';
|
||||
import ExchangeModal from './ExchangeModal';
|
||||
import PointsMallWebProducts from './PointsMallWebProducts';
|
||||
import PointsMallWebTop from './PointsMallWebTop';
|
||||
|
||||
export interface PointsMallPageWebVariantProps {
|
||||
logic: PointsMallPageLogicOutput;
|
||||
viewport: DesktopViewport;
|
||||
}
|
||||
|
||||
export default function PointsMallPageWebBase({ logic, viewport }: PointsMallPageWebVariantProps) {
|
||||
const [exchangeForm] = Form.useForm<ExchangeFormValues>();
|
||||
|
||||
return (
|
||||
<div className={`page-container points-mall-page-web ${desktopViewportClass(viewport)}`}>
|
||||
<PointsMallWebTop logic={logic} />
|
||||
<PointsMallWebProducts logic={logic} />
|
||||
<ExchangeModal
|
||||
open={logic.exchangeModalVisible}
|
||||
product={logic.selectedProduct}
|
||||
quantity={logic.exchangeQuantity}
|
||||
expiresAt={logic.pendingExpiresAt}
|
||||
loading={logic.exchangeLoading}
|
||||
form={exchangeForm}
|
||||
onSubmit={() => exchangeForm.validateFields().then((values) => logic.handleExchangeSubmit(values))}
|
||||
onCancel={() => {
|
||||
logic.setExchangeModalVisible(false);
|
||||
exchangeForm.resetFields();
|
||||
logic.setPendingOrderId(null);
|
||||
logic.setPendingExpiresAt(null);
|
||||
logic.setSelectedProduct(null);
|
||||
logic.setExchangeQuantity(1);
|
||||
}}
|
||||
/>
|
||||
<ConfirmExchangeModal
|
||||
open={logic.confirmModalVisible}
|
||||
product={logic.selectedProduct}
|
||||
userPoints={logic.userPoints}
|
||||
quantity={logic.exchangeQuantity}
|
||||
loading={logic.exchangeLoading}
|
||||
onQuantityChange={(value) => logic.setExchangeQuantity(Math.max(1, Math.floor(value || 1)))}
|
||||
onConfirm={logic.handleConfirmExchange}
|
||||
onCancel={() => {
|
||||
if (logic.exchangeLoading) return;
|
||||
logic.setConfirmModalVisible(false);
|
||||
logic.setSelectedProduct(null);
|
||||
logic.setExchangeQuantity(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { PointsMallPageLogicOutput } from '../PointsMallPageLogic';
|
||||
import PointsMallPageWebBase from './PointsMallPageWebBase';
|
||||
|
||||
export default function PointsMallPageWebLarge2k({ logic }: { logic: PointsMallPageLogicOutput }) {
|
||||
return <PointsMallPageWebBase logic={logic} viewport="large2k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { PointsMallPageLogicOutput } from '../PointsMallPageLogic';
|
||||
import PointsMallPageWebBase from './PointsMallPageWebBase';
|
||||
|
||||
export default function PointsMallPageWebSmallPc({ logic }: { logic: PointsMallPageLogicOutput }) {
|
||||
return <PointsMallPageWebBase logic={logic} viewport="smallPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { PointsMallPageLogicOutput } from '../PointsMallPageLogic';
|
||||
import PointsMallPageWebBase from './PointsMallPageWebBase';
|
||||
|
||||
export default function PointsMallPageWebStandardPc({ logic }: { logic: PointsMallPageLogicOutput }) {
|
||||
return <PointsMallPageWebBase logic={logic} viewport="standardPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { PointsMallPageLogicOutput } from '../PointsMallPageLogic';
|
||||
import PointsMallPageWebBase from './PointsMallPageWebBase';
|
||||
|
||||
export default function PointsMallPageWebTablet({ logic }: { logic: PointsMallPageLogicOutput }) {
|
||||
return <PointsMallPageWebBase logic={logic} viewport="tablet" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { PointsMallPageLogicOutput } from '../PointsMallPageLogic';
|
||||
import PointsMallPageWebBase from './PointsMallPageWebBase';
|
||||
|
||||
export default function PointsMallPageWebUltra4k({ logic }: { logic: PointsMallPageLogicOutput }) {
|
||||
return <PointsMallPageWebBase logic={logic} viewport="ultra4k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Empty, Input, Select, Space, Spin, Tag } from 'antd';
|
||||
import type { PointsMallProduct } from '../../../api';
|
||||
import type { PointsMallPageLogicOutput } from '../PointsMallPageLogic';
|
||||
|
||||
const pageSizeOptions = [12, 24, 48].map((value) => ({ value, label: `每页 ${value}` }));
|
||||
const sortOptions = [
|
||||
{ value: 'popular', label: '热度优先' },
|
||||
{ value: 'newest', label: '最新上架' },
|
||||
{ value: 'price_asc', label: 'Token从低到高' },
|
||||
{ value: 'price_desc', label: 'Token从高到低' },
|
||||
];
|
||||
|
||||
function ProductCard({ product, logic }: { product: PointsMallProduct; logic: PointsMallPageLogicOutput }) {
|
||||
const { userPoints, exchangeLoading, handleExchangeClick } = logic;
|
||||
return (
|
||||
<div className="points-mall-product-card">
|
||||
<div className="points-mall-product-cover" style={{ backgroundImage: `url(${product.coverUrl})` }} />
|
||||
<div className="points-mall-product-body">
|
||||
<div className="points-mall-product-header">
|
||||
<div className="points-mall-product-info">
|
||||
<div className="points-mall-product-name">{product.name}</div>
|
||||
<div className="points-mall-product-desc">{product.subtitle}</div>
|
||||
</div>
|
||||
{product.tags?.length ? <Tag bordered={false} className="points-mall-product-tag">{product.tags[0]}</Tag> : null}
|
||||
</div>
|
||||
<div className="points-mall-product-price-row">
|
||||
<div>
|
||||
<span className="points-mall-product-price">{product.pointsPrice >= 1000 ? `${(product.pointsPrice / 1000).toFixed(1)} K` : Number(product.pointsPrice).toLocaleString()}</span>
|
||||
<span className="points-mall-product-price-label">Token</span>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
className="points-mall-exchange-btn points-mall-product-exchange-btn"
|
||||
disabled={userPoints < product.pointsPrice || exchangeLoading}
|
||||
onClick={() => handleExchangeClick(product)}
|
||||
>
|
||||
{userPoints < product.pointsPrice ? 'Token不足' : '兑换'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="points-mall-product-footer">
|
||||
<span>库存 {product.stock}</span>
|
||||
<span>已兑 {product.sold}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PointsMallWebProducts({ logic }: { logic: PointsMallPageLogicOutput }) {
|
||||
const { q, sort, page, pageSize, productsLoading, products, total, setQ, setSort, setPage, setPageSize } = logic;
|
||||
|
||||
return (
|
||||
<Card className="stats-page-chart-card points-mall-web-products-card">
|
||||
<div className="points-mall-filters-row">
|
||||
<Space size={10} wrap className="points-mall-filters-left">
|
||||
<Input value={q} onChange={(e) => { setPage(1); setQ(e.target.value); }} prefix={<SearchOutlined />} placeholder="搜索商品" allowClear className="points-mall-search-input" />
|
||||
<Select value={sort} className="points-mall-filter-select" onChange={(value) => { setPage(1); setSort(value); }} options={sortOptions} />
|
||||
<Select value={pageSize} className="points-mall-filter-select-small" onChange={(value) => { setPage(1); setPageSize(value); }} options={pageSizeOptions} />
|
||||
</Space>
|
||||
<div className="points-mall-total-text">共 {total.toLocaleString()} 件商品</div>
|
||||
</div>
|
||||
|
||||
{productsLoading ? (
|
||||
<Spin className="points-mall-state-spin" />
|
||||
) : products.length === 0 ? (
|
||||
<Empty description="暂无商品" className="points-mall-empty-state" />
|
||||
) : (
|
||||
<div className="points-mall-products-grid">
|
||||
{products.map((product) => <ProductCard key={product.id} product={product} logic={logic} />)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!!total && (
|
||||
<div className="points-mall-pagination">
|
||||
<Space size={10}>
|
||||
<Button disabled={page <= 1} onClick={() => setPage((value) => Math.max(1, value - 1))} className="points-mall-exchange-btn">上一页</Button>
|
||||
<Tag bordered={false} className="points-mall-pagination-tag">第 {page} 页</Tag>
|
||||
<Button disabled={page * pageSize >= total} onClick={() => setPage((value) => value + 1)} className="points-mall-exchange-btn">下一页</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import { Button, Card, Spin, Tag } from 'antd';
|
||||
import type { PointsMallPageLogicOutput } from '../PointsMallPageLogic';
|
||||
|
||||
export default function PointsMallWebTop({ logic }: { logic: PointsMallPageLogicOutput }) {
|
||||
const { overview, overviewLoading, categories, categoryId, userPoints, totalSpentUSD, banner, promoEntries } = logic;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="points-mall-hero">
|
||||
<div className="points-mall-header">
|
||||
<div className="points-mall-title-section">
|
||||
<h1 className="page-title stats-page-title">Token商城</h1>
|
||||
<p className="page-subtitle stats-page-subtitle">使用Token兑换权益、工具和活动礼包。Token通过 API 调用消费自动累积。</p>
|
||||
</div>
|
||||
<div className="points-mall-balance-card">
|
||||
{overviewLoading ? (
|
||||
<Spin />
|
||||
) : (
|
||||
<div className="points-balance-row">
|
||||
<div className="points-balance-main">
|
||||
<div className="points-balance-label">我的Token</div>
|
||||
<div className="points-balance-value">{userPoints >= 1000 ? `${(userPoints / 1000).toFixed(1)} K` : userPoints.toLocaleString()}</div>
|
||||
<div className="points-balance-subtext">累计消费 ${typeof totalSpentUSD === 'number' ? totalSpentUSD.toFixed(2) : '--'}</div>
|
||||
</div>
|
||||
<Tag bordered={false} className="points-mall-level-tag">{String(overview?.me?.level || 'Lv.0')}</Tag>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="points-mall-category-card" styles={{ body: { padding: 0 } }}>
|
||||
<div className="points-mall-category-body">
|
||||
<div className="points-mall-category-row">
|
||||
<span className="points-mall-category-label">商品分类</span>
|
||||
{categories.map((category) => (
|
||||
<Button
|
||||
key={category.id}
|
||||
size="small"
|
||||
type={category.id === categoryId ? 'primary' : 'default'}
|
||||
className="points-mall-category-button"
|
||||
onClick={() => {
|
||||
logic.setPage(1);
|
||||
logic.setCategoryId(category.id);
|
||||
}}
|
||||
>
|
||||
{category.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="points-mall-banner-section">
|
||||
<div className="points-mall-banner-card">
|
||||
<div className="points-mall-banner-header">
|
||||
<div>
|
||||
<div className="points-mall-banner-title">{banner?.title || '本期活动'}</div>
|
||||
<div className="points-mall-banner-subtitle">{banner?.subtitle || 'Up to 25% Off'}</div>
|
||||
</div>
|
||||
<Button type="primary" className="points-mall-banner-action">查看活动</Button>
|
||||
</div>
|
||||
<div className="points-mall-banner-footer">banner 图片与跳转链接由后端配置</div>
|
||||
</div>
|
||||
|
||||
<div className="points-mall-promo-grid">
|
||||
{promoEntries.slice(0, 2).map((promo) => (
|
||||
<div key={promo.id} className="points-mall-promo-card">
|
||||
<div className="points-mall-promo-body">
|
||||
<div className="points-mall-promo-title">{promo.title}</div>
|
||||
<div className="points-mall-promo-subtitle">{promo.subtitle}</div>
|
||||
</div>
|
||||
<Button size="small" className="points-mall-exchange-btn points-mall-promo-action">进入</Button>
|
||||
</div>
|
||||
))}
|
||||
{promoEntries.length < 2 && <div className="points-mall-promo-empty">促销入口由后端配置</div>}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.points-mall-page-web.desktop-large2k {
|
||||
--points-product-columns: 5;
|
||||
--points-web-gap: 18px;
|
||||
--points-web-max-width: 1760px;
|
||||
--points-banner-main: 1.8fr;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.points-mall-page-web.desktop-smallPc {
|
||||
--points-product-columns: 3;
|
||||
--points-web-gap: 14px;
|
||||
--points-web-max-width: 1080px;
|
||||
--points-banner-main: 1.3fr;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.points-mall-page-web.desktop-standardPc {
|
||||
--points-product-columns: 4;
|
||||
--points-web-gap: 14px;
|
||||
--points-web-max-width: 1400px;
|
||||
--points-banner-main: 1.55fr;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
.points-mall-page-web.desktop-tablet {
|
||||
--points-product-columns: 2;
|
||||
--points-web-gap: 12px;
|
||||
--points-web-max-width: 100%;
|
||||
}
|
||||
|
||||
.points-mall-page-web.desktop-tablet .points-mall-header,
|
||||
.points-mall-page-web.desktop-tablet .points-mall-banner-section {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.points-mall-page-web.desktop-tablet .points-mall-filters-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.points-mall-page-web.desktop-tablet .points-mall-search-input,
|
||||
.points-mall-page-web.desktop-tablet .points-mall-filter-select,
|
||||
.points-mall-page-web.desktop-tablet .points-mall-filter-select-small {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.points-mall-page-web.desktop-ultra4k {
|
||||
--points-product-columns: 6;
|
||||
--points-web-gap: 22px;
|
||||
--points-web-max-width: 2240px;
|
||||
--points-banner-main: 2fr;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
@import './points-mall-web-tablet.css';
|
||||
@import './points-mall-web-small-pc.css';
|
||||
@import './points-mall-web-standard-pc.css';
|
||||
@import './points-mall-web-large-2k.css';
|
||||
@import './points-mall-web-ultra-4k.css';
|
||||
|
||||
.points-mall-page-web {
|
||||
max-width: var(--points-web-max-width, 1400px);
|
||||
}
|
||||
|
||||
.points-mall-page-web .points-mall-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(260px, 320px);
|
||||
}
|
||||
|
||||
.points-mall-page-web .points-mall-banner-section {
|
||||
grid-template-columns: minmax(0, var(--points-banner-main, 1.55fr)) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.points-mall-page-web .points-mall-products-grid {
|
||||
grid-template-columns: repeat(var(--points-product-columns, 4), minmax(0, 1fr));
|
||||
gap: var(--points-web-gap, 14px);
|
||||
}
|
||||
|
||||
.points-mall-page-web .points-mall-category-row {
|
||||
display: grid;
|
||||
grid-template-columns: max-content repeat(auto-fit, minmax(96px, max-content));
|
||||
}
|
||||
|
||||
.points-mall-page-web .points-mall-category-button {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.points-mall-page-web .points-mall-banner-action {
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.points-mall-page-web .points-mall-promo-body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.points-mall-page-web .points-mall-web-products-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.points-mall-page-web .points-mall-state-spin {
|
||||
display: block;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.points-mall-page-web .points-mall-empty-state {
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import { BarChartOutlined } from '@ant-design/icons';
|
||||
import { Empty, Spin } from 'antd';
|
||||
import type { DesktopViewport } from '../../../hooks/useDesktopViewport';
|
||||
import { desktopViewportClass } from '../../../hooks/useDesktopViewport';
|
||||
import type { StatsPageLogic } from '../StatsPageLogic';
|
||||
import StatsMetricCards from './StatsMetricCards';
|
||||
import StatsPointsCard from './StatsPointsCard';
|
||||
import StatsTokenSection from './StatsTokenSection';
|
||||
import StatsTopAgentsCard from './StatsTopAgentsCard';
|
||||
import StatsTrendCard from './StatsTrendCard';
|
||||
|
||||
export interface StatsPageWebVariantProps {
|
||||
logic: StatsPageLogic;
|
||||
viewport: DesktopViewport;
|
||||
}
|
||||
|
||||
export default function StatsPageWebBase({ logic, viewport }: StatsPageWebVariantProps) {
|
||||
if (logic.loading) return <Spin className="stats-state-spin" />;
|
||||
if (!logic.data) return <Empty description="暂无数据" className="stats-state-empty" />;
|
||||
|
||||
return (
|
||||
<div className={`page-container stats-page-web ${desktopViewportClass(viewport)}`}>
|
||||
<div className="stats-page-hero">
|
||||
<div className="stats-page-header">
|
||||
<div className="stats-page-title-section">
|
||||
<div className="stats-page-badge">
|
||||
<BarChartOutlined className="stats-page-badge-icon" />
|
||||
数据洞察看板
|
||||
</div>
|
||||
<h1 className="page-title stats-page-title">调用统计</h1>
|
||||
<p className="page-subtitle stats-page-subtitle">
|
||||
不只是查看调用数量,而是帮助你感知哪些智能体正在被频繁使用、最近的消息趋势如何,以及整体会话是否健康增长。
|
||||
</p>
|
||||
</div>
|
||||
<div className="stats-page-summary-card">
|
||||
<div className="stats-page-summary-label">近 7 天消息总量</div>
|
||||
<div className="stats-page-summary-value">{logic.last7Days}</div>
|
||||
<div className="stats-page-summary-desc">平均每个会话 {logic.avgSessionMessages} 条消息</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatsMetricCards logic={logic} />
|
||||
</div>
|
||||
|
||||
<StatsPointsCard logic={logic} />
|
||||
|
||||
<div className="stats-page-main-grid stats-page-mt-18">
|
||||
<StatsTrendCard logic={logic} />
|
||||
<StatsTopAgentsCard logic={logic} />
|
||||
</div>
|
||||
|
||||
<StatsTokenSection logic={logic} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { StatsPageLogic } from '../StatsPageLogic';
|
||||
import StatsPageWebBase from './StatsPageWebBase';
|
||||
|
||||
export default function StatsPageWebLarge2k({ logic }: { logic: StatsPageLogic }) {
|
||||
return <StatsPageWebBase logic={logic} viewport="large2k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { StatsPageLogic } from '../StatsPageLogic';
|
||||
import StatsPageWebBase from './StatsPageWebBase';
|
||||
|
||||
export default function StatsPageWebSmallPc({ logic }: { logic: StatsPageLogic }) {
|
||||
return <StatsPageWebBase logic={logic} viewport="smallPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { StatsPageLogic } from '../StatsPageLogic';
|
||||
import StatsPageWebBase from './StatsPageWebBase';
|
||||
|
||||
export default function StatsPageWebStandardPc({ logic }: { logic: StatsPageLogic }) {
|
||||
return <StatsPageWebBase logic={logic} viewport="standardPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { StatsPageLogic } from '../StatsPageLogic';
|
||||
import StatsPageWebBase from './StatsPageWebBase';
|
||||
|
||||
export default function StatsPageWebTablet({ logic }: { logic: StatsPageLogic }) {
|
||||
return <StatsPageWebBase logic={logic} viewport="tablet" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { StatsPageLogic } from '../StatsPageLogic';
|
||||
import StatsPageWebBase from './StatsPageWebBase';
|
||||
|
||||
export default function StatsPageWebUltra4k({ logic }: { logic: StatsPageLogic }) {
|
||||
return <StatsPageWebBase logic={logic} viewport="ultra4k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.stats-page-web.desktop-large2k {
|
||||
max-width: 1760px;
|
||||
}
|
||||
|
||||
.stats-page-web.desktop-large2k .stats-page-main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.45fr) minmax(360px, 0.55fr);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
.stats-page-web.desktop-smallPc {
|
||||
max-width: 1080px;
|
||||
}
|
||||
|
||||
.stats-page-web.desktop-smallPc .stats-page-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 260px;
|
||||
}
|
||||
|
||||
.stats-page-web.desktop-smallPc .stats-page-main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.stats-page-web.desktop-standardPc {
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
.stats-page-web.desktop-standardPc .stats-page-main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(300px, 0.65fr);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
.stats-page-web.desktop-tablet {
|
||||
max-width: 100%;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.stats-page-web.desktop-tablet .stats-page-header,
|
||||
.stats-page-web.desktop-tablet .stats-page-main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.stats-page-web.desktop-tablet .stats-page-summary-card {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.stats-page-web.desktop-ultra4k {
|
||||
max-width: 2240px;
|
||||
}
|
||||
|
||||
.stats-page-web.desktop-ultra4k .stats-page-main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.6fr) minmax(420px, 0.4fr);
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { Empty } from 'antd';
|
||||
import type { DesktopViewport } from '../../../hooks/useDesktopViewport';
|
||||
import { desktopViewportClass } from '../../../hooks/useDesktopViewport';
|
||||
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||
import TeamsPageWebDetail from './TeamsPageWebDetail';
|
||||
import TeamsPageWebHero from './TeamsPageWebHero';
|
||||
import TeamsPageWebList from './TeamsPageWebList';
|
||||
import TeamsPageWebModals from './TeamsPageWebModals';
|
||||
|
||||
export interface TeamsPageWebVariantProps {
|
||||
logic: TeamsPageLogicOutput;
|
||||
viewport: DesktopViewport;
|
||||
}
|
||||
|
||||
export default function TeamsPageWebBase({ logic, viewport }: TeamsPageWebVariantProps) {
|
||||
return (
|
||||
<div className="feature-cover-container">
|
||||
<div className={`page-container teams-page-web ${desktopViewportClass(viewport)}`}>
|
||||
<TeamsPageWebHero logic={logic} />
|
||||
<div className="teams-page-web-main-grid">
|
||||
<TeamsPageWebList logic={logic} />
|
||||
<TeamsPageWebDetail logic={logic} />
|
||||
</div>
|
||||
<TeamsPageWebModals logic={logic} />
|
||||
</div>
|
||||
<div className="feature-cover">
|
||||
<Empty description="功能规划中,本期不支持" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import { DeleteOutlined, MailOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Empty, List, Popconfirm, Space, Tag } from 'antd';
|
||||
import type { Team } from '../../../api';
|
||||
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||
|
||||
const roleClass = (role?: string) => {
|
||||
if (role === 'owner') return 'teams-page-web-tag-warning';
|
||||
if (role === 'admin') return 'teams-page-web-tag-info';
|
||||
return 'teams-page-web-tag-neutral';
|
||||
};
|
||||
|
||||
function TeamSummary({ active }: { active: Team }) {
|
||||
const items = [
|
||||
{ label: '成员规模', value: active.members?.length || 0, className: 'teams-page-web-mini-brand' },
|
||||
{ label: '共享智能体', value: active.agentCount ?? 0, className: 'teams-page-web-mini-success' },
|
||||
{ label: '当前身份', value: active.myRole, className: 'teams-page-web-mini-warning' },
|
||||
];
|
||||
return (
|
||||
<div className="teams-page-web-mini-grid">
|
||||
{items.map((item) => (
|
||||
<div className={`teams-page-web-mini-card ${item.className}`} key={item.label}>
|
||||
<div className="teams-page-web-mini-label">{item.label}</div>
|
||||
<div className="teams-page-web-mini-value">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TeamsPageWebDetail({ logic }: { logic: TeamsPageLogicOutput }) {
|
||||
const { active, handleDelete, handleRemoveMember } = logic;
|
||||
if (!active) return <Empty description="选择或创建一个团队" className="teams-page-web-empty-detail" />;
|
||||
|
||||
const canManage = active.myRole === 'owner' || active.myRole === 'admin';
|
||||
|
||||
return (
|
||||
<Card className="teams-page-web-detail-card" styles={{ body: { padding: 22 } }}>
|
||||
<div className="teams-page-web-detail-head">
|
||||
<div>
|
||||
<div className="teams-page-web-active-title-row">
|
||||
<span className="teams-page-web-active-title">{active.name}</span>
|
||||
<Tag bordered={false} className="teams-page-web-tag-brand">{active.myRole}</Tag>
|
||||
<Tag bordered={false} className="teams-page-web-tag-neutral">{active.agentCount ?? 0} 智能体</Tag>
|
||||
</div>
|
||||
<div className="teams-page-web-section-desc">管理成员权限、邀请新伙伴,并协同维护团队共享的智能体资产。</div>
|
||||
</div>
|
||||
<Space wrap>
|
||||
{canManage && (
|
||||
<Button icon={<MailOutlined />} onClick={() => logic.setInviteOpen(true)} className="teams-page-web-soft-btn">
|
||||
生成邀请码
|
||||
</Button>
|
||||
)}
|
||||
{active.myRole === 'owner' && (
|
||||
<Popconfirm title="确定删除该团队?团队内的智能体会变成 owner 私有" onConfirm={() => handleDelete(active.id)}>
|
||||
<Button danger icon={<DeleteOutlined />} className="teams-page-web-soft-btn">
|
||||
删除团队
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<TeamSummary active={active} />
|
||||
|
||||
<div className="teams-page-web-members-title">成员 ({active.members?.length || 0})</div>
|
||||
<List
|
||||
dataSource={active.members || []}
|
||||
renderItem={(member) => (
|
||||
<List.Item
|
||||
className="teams-page-web-member-item"
|
||||
actions={canManage && member.role !== 'owner' ? [
|
||||
<Popconfirm key="kick" title="移除该成员?" onConfirm={() => handleRemoveMember(member.id)}>
|
||||
<Button size="small" danger className="teams-page-web-soft-btn">移除</Button>
|
||||
</Popconfirm>,
|
||||
] : []}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={<div className="teams-page-web-member-avatar"><UserOutlined /></div>}
|
||||
title={<Space wrap><span className="teams-page-web-member-name">{member.name}</span><Tag bordered={false} className={roleClass(member.role)}>{member.role}</Tag></Space>}
|
||||
description={<div className="teams-page-web-member-desc"><span>{member.email}</span><span>加入时间 {new Date(member.joinedAt).toLocaleDateString('zh-CN')}</span></div>}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { PlusOutlined, TeamOutlined } from '@ant-design/icons';
|
||||
import { Button } from 'antd';
|
||||
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||
|
||||
export default function TeamsPageWebHero({ logic }: { logic: TeamsPageLogicOutput }) {
|
||||
const { list, active, setCreateOpen } = logic;
|
||||
const stats = [
|
||||
{ label: '团队数量', value: list.length, tone: 'rgba(8, 145, 178, 0.10)', color: 'var(--color-brand)' },
|
||||
{ label: '当前成员数', value: active?.members?.length ?? 0, tone: 'rgba(14, 165, 233, 0.10)', color: 'var(--color-info)' },
|
||||
{ label: '共享智能体', value: active?.agentCount ?? 0, tone: 'rgba(34, 197, 94, 0.10)', color: 'var(--color-success)' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="teams-page-web-hero">
|
||||
<div className="teams-page-web-hero-header">
|
||||
<div className="teams-page-web-hero-copy">
|
||||
<div className="teams-page-web-badge">
|
||||
<TeamOutlined />
|
||||
协作组织空间
|
||||
</div>
|
||||
<h1 className="page-title teams-page-web-title">团队管理</h1>
|
||||
<div className="page-subtitle teams-page-web-subtitle">
|
||||
团队不只是成员列表,更是共享智能体、协同运营和权限分工的组织单元。这里统一查看团队、成员和邀请状态。
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" size="large" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)} className="teams-page-web-create-btn">
|
||||
创建团队
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="teams-page-web-stats-grid">
|
||||
{stats.map((item) => (
|
||||
<div className="teams-page-web-stat-card" key={item.label}>
|
||||
<div className="teams-page-web-stat-label">{item.label}</div>
|
||||
<div className="teams-page-web-stat-row">
|
||||
<span className="teams-page-web-stat-value">{item.value}</span>
|
||||
<span className="teams-page-web-stat-chip" style={{ background: item.tone, color: item.color }}>
|
||||
当前选中
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||
import TeamsPageWebBase from './TeamsPageWebBase';
|
||||
|
||||
export default function TeamsPageWebLarge2k({ logic }: { logic: TeamsPageLogicOutput }) {
|
||||
return <TeamsPageWebBase logic={logic} viewport="large2k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { Empty, List } from 'antd';
|
||||
import { TeamAPI } from '../../../api';
|
||||
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||
|
||||
export default function TeamsPageWebList({ logic }: { logic: TeamsPageLogicOutput }) {
|
||||
const { list, active, setActive } = logic;
|
||||
|
||||
return (
|
||||
<aside className="teams-page-web-list-panel">
|
||||
<div className="teams-page-web-list-head">
|
||||
<div className="teams-page-web-section-title">团队列表</div>
|
||||
<div className="teams-page-web-section-desc">选择一个团队查看成员与邀请</div>
|
||||
</div>
|
||||
{list.length === 0 ? (
|
||||
<Empty description="还没有团队" />
|
||||
) : (
|
||||
<List
|
||||
dataSource={list}
|
||||
renderItem={(item) => (
|
||||
<button
|
||||
className={`teams-page-web-nav-item ${active?.id === item.id ? 'active' : ''}`}
|
||||
onClick={async () => setActive(await TeamAPI.detail(item.id))}
|
||||
type="button"
|
||||
>
|
||||
<span>{item.name}</span>
|
||||
<small>{item.agentCount ?? 0} 个智能体</small>
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { App as AntApp, Button, Form, Input, Modal } from 'antd';
|
||||
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||
|
||||
export default function TeamsPageWebModals({ logic }: { logic: TeamsPageLogicOutput }) {
|
||||
const { message } = AntApp.useApp();
|
||||
const { active } = logic;
|
||||
|
||||
return (
|
||||
<>
|
||||
<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 className="teams-page-web-invite-label">邀请码生成成功,请发给受邀者:</div>
|
||||
<Input.TextArea value={logic.lastInviteCode} readOnly autoSize className="teams-page-web-invite-code" />
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
className="teams-page-web-copy-btn"
|
||||
onClick={() => navigator.clipboard?.writeText(logic.lastInviteCode || '').then(() => message.success('邀请码已复制'))}
|
||||
>
|
||||
复制邀请码
|
||||
</Button>
|
||||
<div className="teams-page-web-invite-tip">受邀者在注册页填入此邀请码即可加入团队</div>
|
||||
</div>
|
||||
) : (
|
||||
<Form layout="vertical" onFinish={logic.handleInvite}>
|
||||
<Form.Item name="email" label="限定邮箱(可选)">
|
||||
<Input placeholder="只允许该邮箱使用此邀请码" />
|
||||
</Form.Item>
|
||||
<Form.Item name="ttlHours" label="有效期(小时)" initialValue={168}>
|
||||
<Input type="number" placeholder="168 = 7 天" />
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" block>
|
||||
生成邀请码
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||
import TeamsPageWebBase from './TeamsPageWebBase';
|
||||
|
||||
export default function TeamsPageWebSmallPc({ logic }: { logic: TeamsPageLogicOutput }) {
|
||||
return <TeamsPageWebBase logic={logic} viewport="smallPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||
import TeamsPageWebBase from './TeamsPageWebBase';
|
||||
|
||||
export default function TeamsPageWebStandardPc({ logic }: { logic: TeamsPageLogicOutput }) {
|
||||
return <TeamsPageWebBase logic={logic} viewport="standardPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||
import TeamsPageWebBase from './TeamsPageWebBase';
|
||||
|
||||
export default function TeamsPageWebTablet({ logic }: { logic: TeamsPageLogicOutput }) {
|
||||
return <TeamsPageWebBase logic={logic} viewport="tablet" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { TeamsPageLogicOutput } from '../TeamsPageLogic';
|
||||
import TeamsPageWebBase from './TeamsPageWebBase';
|
||||
|
||||
export default function TeamsPageWebUltra4k({ logic }: { logic: TeamsPageLogicOutput }) {
|
||||
return <TeamsPageWebBase logic={logic} viewport="ultra4k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
.teams-page-web-tag-brand,
|
||||
.teams-page-web-tag-neutral,
|
||||
.teams-page-web-tag-info,
|
||||
.teams-page-web-tag-warning {
|
||||
border-radius: 999px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.teams-page-web-tag-brand {
|
||||
background: var(--color-brand-soft);
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.teams-page-web-tag-neutral {
|
||||
background: var(--color-surface-2);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.teams-page-web-tag-info {
|
||||
background: var(--color-info-soft);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.teams-page-web-tag-warning {
|
||||
background: var(--color-warning-soft);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.teams-page-web-mini-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.teams-page-web-mini-card {
|
||||
border: 1px solid rgba(8, 145, 178, 0.1);
|
||||
}
|
||||
|
||||
.teams-page-web-mini-brand {
|
||||
background: rgba(8, 145, 178, 0.06);
|
||||
}
|
||||
|
||||
.teams-page-web-mini-success {
|
||||
background: rgba(34, 197, 94, 0.06);
|
||||
}
|
||||
|
||||
.teams-page-web-mini-warning {
|
||||
background: rgba(249, 115, 22, 0.06);
|
||||
}
|
||||
|
||||
.teams-page-web-mini-value {
|
||||
margin-top: 8px;
|
||||
color: var(--color-text);
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.teams-page-web-members-title {
|
||||
margin-bottom: 14px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.teams-page-web-member-item {
|
||||
padding: 14px 0;
|
||||
}
|
||||
|
||||
.teams-page-web-member-avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 999px;
|
||||
background: rgba(8, 145, 178, 0.1);
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.teams-page-web-member-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.teams-page-web-member-desc {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.teams-page-web-empty-detail {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.teams-page-web-invite-label {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.teams-page-web-invite-code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.teams-page-web-copy-btn {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.teams-page-web-invite-tip {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.teams-page-web.desktop-large2k {
|
||||
--teams-gap: 24px;
|
||||
--teams-max-width: 1760px;
|
||||
--teams-sidebar-width: 320px;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.teams-page-web.desktop-smallPc {
|
||||
--teams-gap: 18px;
|
||||
--teams-max-width: 1080px;
|
||||
--teams-sidebar-width: 240px;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.teams-page-web.desktop-standardPc {
|
||||
--teams-gap: 22px;
|
||||
--teams-max-width: 1180px;
|
||||
--teams-sidebar-width: 260px;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
.teams-page-web.desktop-tablet {
|
||||
--teams-gap: 14px;
|
||||
--teams-max-width: 100%;
|
||||
}
|
||||
|
||||
.teams-page-web.desktop-tablet .teams-page-web-main-grid,
|
||||
.teams-page-web.desktop-tablet .teams-page-web-stats-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.teams-page-web.desktop-tablet .teams-page-web-create-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.teams-page-web.desktop-ultra4k {
|
||||
--teams-gap: 28px;
|
||||
--teams-max-width: 2240px;
|
||||
--teams-sidebar-width: 380px;
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
@import './teams-page-web-tablet.css';
|
||||
@import './teams-page-web-small-pc.css';
|
||||
@import './teams-page-web-standard-pc.css';
|
||||
@import './teams-page-web-large-2k.css';
|
||||
@import './teams-page-web-ultra-4k.css';
|
||||
@import './teams-page-web-detail.css';
|
||||
|
||||
.teams-page-web {
|
||||
max-width: var(--teams-max-width, 1080px);
|
||||
}
|
||||
|
||||
.teams-page-web-hero {
|
||||
border-radius: 24px;
|
||||
padding: 30px 30px 26px;
|
||||
margin-bottom: 24px;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.98), rgba(236,253,245,0.92) 42%, rgba(239,246,255,0.96));
|
||||
border: 1px solid rgba(8, 145, 178, 0.12);
|
||||
box-shadow: 0 20px 48px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.teams-page-web-hero-header,
|
||||
.teams-page-web-detail-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.teams-page-web-hero-copy {
|
||||
max-width: 620px;
|
||||
}
|
||||
|
||||
.teams-page-web-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
margin-bottom: 16px;
|
||||
background: rgba(255,255,255,0.78);
|
||||
border: 1px solid rgba(8, 145, 178, 0.1);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.teams-page-web-title {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.teams-page-web-subtitle {
|
||||
margin-top: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.teams-page-web-create-btn,
|
||||
.teams-page-web-soft-btn,
|
||||
.teams-page-web-copy-btn {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.teams-page-web-stats-grid,
|
||||
.teams-page-web-main-grid,
|
||||
.teams-page-web-mini-grid {
|
||||
display: grid;
|
||||
gap: var(--teams-gap, 14px);
|
||||
}
|
||||
|
||||
.teams-page-web-stats-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.teams-page-web-stat-card,
|
||||
.teams-page-web-mini-card {
|
||||
border-radius: 18px;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.teams-page-web-stat-card {
|
||||
background: rgba(255,255,255,0.72);
|
||||
border: 1px solid rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.teams-page-web-stat-label,
|
||||
.teams-page-web-section-desc,
|
||||
.teams-page-web-mini-label,
|
||||
.teams-page-web-member-desc,
|
||||
.teams-page-web-invite-tip {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.teams-page-web-stat-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.teams-page-web-stat-value {
|
||||
color: var(--color-text);
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.teams-page-web-stat-chip {
|
||||
border-radius: 999px;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.teams-page-web-main-grid {
|
||||
grid-template-columns: var(--teams-sidebar-width, 260px) minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.teams-page-web-list-panel,
|
||||
.teams-page-web-detail-card {
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(252,252,253,1));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.045);
|
||||
}
|
||||
|
||||
.teams-page-web-list-panel {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.teams-page-web-list-head {
|
||||
padding: 8px 10px 14px;
|
||||
}
|
||||
|
||||
.teams-page-web-section-title,
|
||||
.teams-page-web-members-title {
|
||||
color: var(--color-text);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.teams-page-web-section-title {
|
||||
margin-bottom: 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.teams-page-web-nav-item {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 14px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 6px;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.teams-page-web-nav-item.active {
|
||||
background: rgba(8, 145, 178, 0.1);
|
||||
border-color: rgba(8, 145, 178, 0.16);
|
||||
color: var(--color-brand);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.teams-page-web-nav-item small {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.teams-page-web-detail-head {
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.teams-page-web-active-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.teams-page-web-active-title {
|
||||
color: var(--color-text);
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { ApartmentOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Empty, Spin } from 'antd';
|
||||
import type { DesktopViewport } from '../../../hooks/useDesktopViewport';
|
||||
import { desktopViewportClass } from '../../../hooks/useDesktopViewport';
|
||||
import type { WorkflowsPageLogic } from '../WorkflowsPageLogic';
|
||||
import WorkflowCard from './WorkflowCard';
|
||||
import WorkflowStatsStrip from './WorkflowStatsStrip';
|
||||
|
||||
export interface WorkflowsPageWebVariantProps {
|
||||
logic: WorkflowsPageLogic;
|
||||
viewport: DesktopViewport;
|
||||
}
|
||||
|
||||
export default function WorkflowsPageWebBase({ logic, viewport }: WorkflowsPageWebVariantProps) {
|
||||
return (
|
||||
<div className={`page-container workflows-page-web ${desktopViewportClass(viewport)}`}>
|
||||
<div className="workflows-hero">
|
||||
<div className="workflows-hero-header">
|
||||
<div className="workflows-hero-copy">
|
||||
<div className="workflows-badge">
|
||||
<ApartmentOutlined />
|
||||
自动化编排中心
|
||||
</div>
|
||||
<h1 className="page-title workflows-title">工作流编排</h1>
|
||||
<div className="page-subtitle workflows-subtitle">
|
||||
让多个 Agent、技能、HTTP 请求与数据转换连成一条可运行的自动化链路。这里更像一个流程画廊,而不是传统表格后台。
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" size="large" icon={<PlusOutlined />} onClick={logic.onCreate} className="workflows-create-btn">
|
||||
新建工作流
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<WorkflowStatsStrip logic={logic} />
|
||||
</div>
|
||||
|
||||
{logic.loading ? (
|
||||
<Spin className="workflows-state-spin" />
|
||||
) : logic.list.length === 0 ? (
|
||||
<div className="workflows-empty-card">
|
||||
<Empty description="还没有工作流,点击上方开始搭建第一条自动化流程" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="workflows-card-grid">
|
||||
{logic.list.map((workflow) => (
|
||||
<WorkflowCard
|
||||
key={workflow.id}
|
||||
workflow={workflow}
|
||||
onEdit={logic.onEdit}
|
||||
onDelete={logic.onDelete}
|
||||
onOpenRuns={logic.openRuns}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { WorkflowsPageLogic } from '../WorkflowsPageLogic';
|
||||
import WorkflowsPageWebBase from './WorkflowsPageWebBase';
|
||||
|
||||
export default function WorkflowsPageWebLarge2k({ logic }: { logic: WorkflowsPageLogic }) {
|
||||
return <WorkflowsPageWebBase logic={logic} viewport="large2k" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { WorkflowsPageLogic } from '../WorkflowsPageLogic';
|
||||
import WorkflowsPageWebBase from './WorkflowsPageWebBase';
|
||||
|
||||
export default function WorkflowsPageWebSmallPc({ logic }: { logic: WorkflowsPageLogic }) {
|
||||
return <WorkflowsPageWebBase logic={logic} viewport="smallPc" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import type { WorkflowsPageLogic } from '../WorkflowsPageLogic';
|
||||
import WorkflowsPageWebBase from './WorkflowsPageWebBase';
|
||||
|
||||
export default function WorkflowsPageWebStandardPc({ logic }: { logic: WorkflowsPageLogic }) {
|
||||
return <WorkflowsPageWebBase logic={logic} viewport="standardPc" />;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue