Compare commits

...

2 Commits

Author SHA1 Message Date
sp mac bookpro 2605 b7c4159f1a chore: 更新代码提交规则
明确默认允许直接提交并推送当前分支,包括 main 分支。
2026-07-18 12:31:25 +08:00
sp mac bookpro 2605 8cf48b775b feat: 统一 API 地址并完善响应式页面
统一前端请求地址并移除代理路径重写,同时提交各业务页面的响应式布局实现。
2026-07-18 12:28:53 +08:00
120 changed files with 2772 additions and 36 deletions

13
.trae/rules/rule.md Normal file
View File

@ -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. **文档** - 在用户没有明确要求先提供文档时,禁止创建文档。避免生产垃圾文件。用户明确要求提供文档时,才创建文档。

View File

@ -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);

View File

@ -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,

View File

@ -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 }),

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -0,0 +1,4 @@
.mcp-panel-web.desktop-large2k {
--mcp-panel-max-width: 1760px;
--mcp-command-max-width: 980px;
}

View File

@ -0,0 +1,3 @@
.mcp-panel-web.desktop-smallPc {
--mcp-command-max-width: 560px;
}

View File

@ -0,0 +1,3 @@
.mcp-panel-web.desktop-standardPc {
--mcp-command-max-width: 720px;
}

View File

@ -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;
}

View File

@ -0,0 +1,4 @@
.mcp-panel-web.desktop-ultra4k {
--mcp-panel-max-width: 2240px;
--mcp-command-max-width: 1280px;
}

View File

@ -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;
}

View File

@ -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}`;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -0,0 +1,5 @@
.agent-list-web.desktop-large2k {
--agent-list-columns: 5;
--agent-list-gap: 22px;
--agent-list-max-width: 1760px;
}

View File

@ -0,0 +1,5 @@
.agent-list-web.desktop-smallPc {
--agent-list-columns: 3;
--agent-list-gap: 16px;
--agent-list-max-width: 1080px;
}

View File

@ -0,0 +1,5 @@
.agent-list-web.desktop-standardPc {
--agent-list-columns: 4;
--agent-list-gap: 18px;
--agent-list-max-width: 1240px;
}

View File

@ -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%;
}

View File

@ -0,0 +1,5 @@
.agent-list-web.desktop-ultra4k {
--agent-list-columns: 6;
--agent-list-gap: 24px;
--agent-list-max-width: 2240px;
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,4 @@
.login-page-web.desktop-standardPc .login-page-web-content {
display: grid;
grid-template-columns: minmax(0, 1fr) 480px;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -0,0 +1,5 @@
.marketplace-page-web.desktop-large2k {
--marketplace-columns: 5;
--marketplace-gap: 22px;
--marketplace-max-width: 1760px;
}

View File

@ -0,0 +1,5 @@
.marketplace-page-web.desktop-smallPc {
--marketplace-columns: 3;
--marketplace-gap: 16px;
--marketplace-max-width: 1080px;
}

View File

@ -0,0 +1,5 @@
.marketplace-page-web.desktop-standardPc {
--marketplace-columns: 4;
--marketplace-gap: 20px;
--marketplace-max-width: 1240px;
}

View File

@ -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%;
}

View File

@ -0,0 +1,5 @@
.marketplace-page-web.desktop-ultra4k {
--marketplace-columns: 6;
--marketplace-gap: 24px;
--marketplace-max-width: 2240px;
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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>
);
}

View File

@ -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">使TokenToken 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>
</>
);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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%;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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%;
}

View File

@ -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);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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" />;
}

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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;
}

View File

@ -0,0 +1,5 @@
.teams-page-web.desktop-large2k {
--teams-gap: 24px;
--teams-max-width: 1760px;
--teams-sidebar-width: 320px;
}

View File

@ -0,0 +1,5 @@
.teams-page-web.desktop-smallPc {
--teams-gap: 18px;
--teams-max-width: 1080px;
--teams-sidebar-width: 240px;
}

View File

@ -0,0 +1,5 @@
.teams-page-web.desktop-standardPc {
--teams-gap: 22px;
--teams-max-width: 1180px;
--teams-sidebar-width: 260px;
}

View File

@ -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%;
}

View File

@ -0,0 +1,5 @@
.teams-page-web.desktop-ultra4k {
--teams-gap: 28px;
--teams-max-width: 2240px;
--teams-sidebar-width: 380px;
}

View File

@ -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;
}

View File

@ -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">
AgentHTTP
</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>
);
}

View File

@ -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" />;
}

View File

@ -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" />;
}

View File

@ -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