Split React client pages into modules
This commit is contained in:
37
apps/senlinai-acro-react/scripts/structure-check.mjs
Normal file
37
apps/senlinai-acro-react/scripts/structure-check.mjs
Normal file
@@ -0,0 +1,37 @@
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
|
||||
const requiredFiles = [
|
||||
'src/app/App.tsx',
|
||||
'src/pages/login.tsx',
|
||||
'src/pages/project.tsx',
|
||||
'src/pages/overview.tsx',
|
||||
'src/features/projects/project-data.tsx',
|
||||
'src/features/projects/project-rail.tsx',
|
||||
'src/features/projects/project-sidebar.tsx',
|
||||
'src/features/projects/project-topbar.tsx',
|
||||
'src/features/projects/project-inspector.tsx',
|
||||
'src/features/projects/project-statusbar.tsx',
|
||||
'src/features/projects/project-types.ts',
|
||||
]
|
||||
|
||||
const failures = requiredFiles.filter((file) => !existsSync(file)).map((file) => `missing ${file}`)
|
||||
|
||||
if (existsSync('src/App.tsx')) {
|
||||
failures.push('legacy src/App.tsx should be removed after page split')
|
||||
}
|
||||
|
||||
if (existsSync('src/app/App.tsx')) {
|
||||
const appSource = readFileSync('src/app/App.tsx', 'utf8')
|
||||
for (const forbidden of ['function LoginPage', 'function Workbench', 'function OverviewContent']) {
|
||||
if (appSource.includes(forbidden)) {
|
||||
failures.push(`src/app/App.tsx still contains ${forbidden}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
console.error(failures.join('\n'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log('structure ok')
|
||||
@@ -1,569 +0,0 @@
|
||||
import type { CSSProperties, ReactElement } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
ConfigProvider,
|
||||
Divider,
|
||||
Form,
|
||||
Grid,
|
||||
Input,
|
||||
Layout,
|
||||
Message,
|
||||
Space,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from '@arco-design/web-react'
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconBook,
|
||||
IconCalendar,
|
||||
IconCheckCircleFill,
|
||||
IconClockCircle,
|
||||
IconDashboard,
|
||||
IconEmail,
|
||||
IconFile,
|
||||
IconHome,
|
||||
IconLaunch,
|
||||
IconLink,
|
||||
IconList,
|
||||
IconMessage,
|
||||
IconMoon,
|
||||
IconMore,
|
||||
IconNotification,
|
||||
IconPlus,
|
||||
IconQuestionCircle,
|
||||
IconRobot,
|
||||
IconSafe,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
IconStorage,
|
||||
IconSun,
|
||||
IconThunderbolt,
|
||||
IconUserGroup,
|
||||
} from '@arco-design/web-react/icon'
|
||||
import '@arco-design/web-react/dist/css/arco.css'
|
||||
import './App.css'
|
||||
|
||||
const { Header, Sider, Content, Footer } = Layout
|
||||
const { Row, Col } = Grid
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
|
||||
type Screen = 'login' | 'workbench'
|
||||
type Theme = 'light' | 'dark'
|
||||
type ChannelKey = 'overview' | 'inbox' | 'tasks' | 'ai' | 'notes' | 'cron' | 'research' | 'design'
|
||||
|
||||
const projects = [
|
||||
{ id: 'a1', name: '项目 A1', short: 'A1', badge: 635, urgent: true, color: '#165DFF' },
|
||||
{ id: 'a2', name: '项目 A2', short: 'PM', badge: 36, color: '#00B42A' },
|
||||
{ id: 'b1', name: '项目 B1', short: 'DS', badge: 4, urgent: true, color: '#722ED1' },
|
||||
{ id: 'c3', name: '项目 C3', short: 'OP', badge: 45, color: '#FF7D00' },
|
||||
]
|
||||
|
||||
const channels: Array<{ key: ChannelKey; label: string; count?: number; icon: ReactElement }> = [
|
||||
{ key: 'overview', label: '概况', count: 635, icon: <IconHome /> },
|
||||
{ key: 'inbox', label: 'Inbox 消息流', count: 36, icon: <IconEmail /> },
|
||||
{ key: 'tasks', label: '工作计划', count: 12, icon: <IconList /> },
|
||||
{ key: 'ai', label: 'AI 会话', count: 4, icon: <IconRobot /> },
|
||||
{ key: 'notes', label: '笔记资料', count: 343, icon: <IconFile /> },
|
||||
{ key: 'cron', label: 'Cron 计划任务', count: 45, icon: <IconClockCircle /> },
|
||||
{ key: 'research', label: '客户研究频道', icon: <IconUserGroup /> },
|
||||
{ key: 'design', label: '产品设计频道', icon: <IconLink /> },
|
||||
]
|
||||
|
||||
const inbox = [
|
||||
{ title: '竞争对手定价更新监控', meta: '来源:产品研究 · 竞品追踪', owner: '李明', time: '2 小时前', tag: '市场情报' },
|
||||
{ title: '客户反馈:导出功能报错', meta: '来源:客户支持 · 工单 #CS-1287', owner: '王芳', time: '4 小时前', tag: '客户反馈' },
|
||||
{ title: 'API 调用策略评估建议', meta: '来源:后端架构讨论', owner: '张伟', time: '6 小时前', tag: '架构' },
|
||||
{ title: '更新需求文档 v1.3.2', meta: '来源:产品需求 · 文档评审', owner: '陈晨', time: '昨天', tag: '需求文档' },
|
||||
]
|
||||
|
||||
const tasks = [
|
||||
{ title: '智能报表导出功能', owner: '张伟', progress: '60%', due: '2026-07-24', tag: '进行中' },
|
||||
{ title: '企业版权限体系梳理', owner: '李明', progress: '30%', due: '2026-07-28', tag: '进行中' },
|
||||
{ title: 'Q3 营销策略制定', owner: '王芳', progress: '45%', due: '2026-07-31', tag: '进行中' },
|
||||
]
|
||||
|
||||
const aiSessions = [
|
||||
{ title: '生成 Q3 营销策略初稿', summary: '基于市场分析和竞品数据,输出 Q3 营销策略重点...', owner: '张伟', time: '今天 08:47' },
|
||||
{ title: '产品定价模型讨论', summary: '针对企业版与标准版定价模型进行对比分析...', owner: '李明', time: '昨天 16:05' },
|
||||
{ title: '客户流失原因分析', summary: '分析近三个月客户流失数据与反馈原因...', owner: '王芳', time: '昨天 10:22' },
|
||||
{ title: '行业趋势与机会洞察', summary: '围绕 AI 在企业协同领域的趋势与机会...', owner: '张伟', time: '7 月 18 日' },
|
||||
]
|
||||
|
||||
const notes = [
|
||||
{ title: '用户权限体系设计文档 v2.1', type: '文档', updated: '2026-07-19', owner: '李明' },
|
||||
{ title: '导出功能技术方案评审', type: '文档', updated: '2026-07-18', owner: '张伟' },
|
||||
{ title: '2026 Q3 竞品功能对比表', type: '表格', updated: '2026-07-18', owner: '王芳' },
|
||||
]
|
||||
|
||||
const discussions = [
|
||||
{ name: '李明', time: '2026-07-20 09:12', text: '关于导出报错的问题,已在测试环境复现,初步定位为权限校验逻辑异常。' },
|
||||
{ name: '张伟', time: '2026-07-20 09:18', text: '@李明 麻烦同步修复预计完成时间,我这边需要同步给客户。' },
|
||||
{ name: '王芳', time: '2026-07-20 10:05', text: '补充:客户反馈影响范围主要集中在企业版用户。' },
|
||||
{ name: '系统通知', time: '2026-07-20 10:30', text: '已将此讨论关联到任务:智能报表导出功能。' },
|
||||
]
|
||||
|
||||
function App() {
|
||||
const [screen, setScreen] = useState<Screen>('login')
|
||||
const [theme, setTheme] = useState<Theme>('light')
|
||||
const [activeProject, setActiveProject] = useState(projects[0])
|
||||
const [activeChannel, setActiveChannel] = useState<ChannelKey>('overview')
|
||||
const [selectedItem, setSelectedItem] = useState('Inbox 待处理(36)')
|
||||
|
||||
const dark = theme === 'dark'
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<main className={dark ? 'app theme-dark' : 'app'}>
|
||||
{screen === 'login' ? (
|
||||
<LoginPage onLogin={() => setScreen('workbench')} />
|
||||
) : (
|
||||
<Workbench
|
||||
activeProject={activeProject}
|
||||
activeChannel={activeChannel}
|
||||
selectedItem={selectedItem}
|
||||
theme={theme}
|
||||
onSelectProject={setActiveProject}
|
||||
onSelectChannel={setActiveChannel}
|
||||
onSelectItem={setSelectedItem}
|
||||
onToggleTheme={() => setTheme(dark ? 'light' : 'dark')}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function LoginPage({ onLogin }: { onLogin: () => void }) {
|
||||
const [server, setServer] = useState('https://10.0.0.15:8080')
|
||||
|
||||
return (
|
||||
<section className="login-screen">
|
||||
<Card className="login-card" bordered>
|
||||
<div className="login-brand-panel">
|
||||
<div className="brand-lockup large">
|
||||
<span className="brand-symbol">S</span>
|
||||
<Title heading={2}>SenlinAI</Title>
|
||||
</div>
|
||||
<Text className="login-slogan">私有部署 · 安全可控的智能协作平台</Text>
|
||||
<Text type="secondary">知识沉淀 · 团队协作 · AI 助理</Text>
|
||||
<Space className="login-footer-links" size={24}>
|
||||
<Text type="secondary">v1.0.0</Text>
|
||||
<Text type="secondary">隐私政策</Text>
|
||||
<Text type="secondary">服务协议</Text>
|
||||
<Text type="secondary">关于 SenlinAI</Text>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className="login-form-panel">
|
||||
<Title heading={4}>登录到您的 SenlinAI</Title>
|
||||
<Text type="secondary">连接私有工作台</Text>
|
||||
<Form layout="vertical" className="login-form">
|
||||
<Form.Item label="服务器地址(IP 或域名优先)">
|
||||
<Input value={server} onChange={setServer} placeholder="例如:https://10.0.0.15:8080" suffix={<IconLaunch />} />
|
||||
</Form.Item>
|
||||
<Form.Item label="账号">
|
||||
<Input placeholder="请输入用户名或邮箱" />
|
||||
</Form.Item>
|
||||
<Form.Item label="密码">
|
||||
<Input.Password placeholder="请输入密码" />
|
||||
</Form.Item>
|
||||
<div className="login-row">
|
||||
<Space size={8}>
|
||||
<span className="check-dot" />
|
||||
<Text>记住服务器地址</Text>
|
||||
</Space>
|
||||
<Button type="text" size="mini">无法连接?</Button>
|
||||
</div>
|
||||
<Button type="primary" long size="large" onClick={onLogin}>
|
||||
登录
|
||||
</Button>
|
||||
<div className="connection-card">
|
||||
<IconCheckCircleFill />
|
||||
<div>
|
||||
<Text className="connection-title">连接正常</Text>
|
||||
<Text type="secondary">已连接到 {server}</Text>
|
||||
</div>
|
||||
<Button type="text" size="mini">更换服务器</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div className="login-value-panel">
|
||||
<Title heading={3}>安全 · 专注 · 高效</Title>
|
||||
<Paragraph>
|
||||
SenlinAI 是面向知识工作者与内部团队的私有部署工作台,帮助团队在统一空间中收集信息、整理知识、协同决策。
|
||||
</Paragraph>
|
||||
<ValuePoint icon={<IconSafe />} title="私有部署,数据自有" desc="所有数据与模型运行在您的环境中,安全可控。" />
|
||||
<ValuePoint icon={<IconBook />} title="知识沉淀,结构清晰" desc="从收集到整理,形成可复用的团队知识库。" />
|
||||
<ValuePoint icon={<IconUserGroup />} title="协同高效,聚焦成果" desc="围绕项目与任务协同,减少沟通成本,专注交付。" />
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ValuePoint({ icon, title, desc }: { icon: ReactElement; title: string; desc: string }) {
|
||||
return (
|
||||
<div className="value-point">
|
||||
<span>{icon}</span>
|
||||
<div>
|
||||
<Text className="value-title">{title}</Text>
|
||||
<Text type="secondary">{desc}</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Workbench({
|
||||
activeProject,
|
||||
activeChannel,
|
||||
selectedItem,
|
||||
theme,
|
||||
onSelectProject,
|
||||
onSelectChannel,
|
||||
onSelectItem,
|
||||
onToggleTheme,
|
||||
}: {
|
||||
activeProject: (typeof projects)[number]
|
||||
activeChannel: ChannelKey
|
||||
selectedItem: string
|
||||
theme: Theme
|
||||
onSelectProject: (project: (typeof projects)[number]) => void
|
||||
onSelectChannel: (key: ChannelKey) => void
|
||||
onSelectItem: (title: string) => void
|
||||
onToggleTheme: () => void
|
||||
}) {
|
||||
return (
|
||||
<Layout className="workbench-shell">
|
||||
<Header className="topbar">
|
||||
<div className="brand-lockup">
|
||||
<span className="brand-symbol">S</span>
|
||||
<Text className="brand-name">SenlinAI</Text>
|
||||
</div>
|
||||
<div className="global-search" role="search">
|
||||
<Input size="large" prefix={<IconSearch />} placeholder="搜索项目、频道、文档、任务、联系人..." />
|
||||
<Button size="large" type="primary">搜索</Button>
|
||||
</div>
|
||||
<Space className="topbar-actions">
|
||||
<Button
|
||||
aria-label={theme === 'dark' ? '切换浅色模式' : '切换深色模式'}
|
||||
icon={theme === 'dark' ? <IconSun /> : <IconMoon />}
|
||||
onClick={onToggleTheme}
|
||||
/>
|
||||
<Button icon={<IconArrowLeft />} disabled />
|
||||
<Button icon={<IconArrowRight />} disabled />
|
||||
<Badge count={8} dot>
|
||||
<Button icon={<IconNotification />} />
|
||||
</Badge>
|
||||
<Button icon={<IconQuestionCircle />} />
|
||||
<Button icon={<IconApps />} />
|
||||
</Space>
|
||||
</Header>
|
||||
|
||||
<Layout className="workbench-main">
|
||||
<Sider className="project-rail" width={96}>
|
||||
<Button className="dashboard-button" icon={<IconDashboard />} />
|
||||
<Space className="project-stack" direction="vertical" size={12}>
|
||||
{projects.map((project) => (
|
||||
<Badge key={project.id} count={project.badge} dot={false} className={project.urgent ? 'project-badge urgent' : 'project-badge'}>
|
||||
<button
|
||||
className={activeProject.id === project.id ? 'project-button active' : 'project-button'}
|
||||
onClick={() => onSelectProject(project)}
|
||||
style={{ '--project-color': project.color } as CSSProperties}
|
||||
title={project.name}
|
||||
>
|
||||
{project.short}
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</Space>
|
||||
<Button className="create-project" aria-label="新建项目" icon={<IconPlus />} title="新建项目" />
|
||||
</Sider>
|
||||
|
||||
<Sider className="channel-sidebar" width={248}>
|
||||
<div className="project-title">
|
||||
<Space>
|
||||
<IconDashboard />
|
||||
<Title heading={5}>{activeProject.name}</Title>
|
||||
</Space>
|
||||
<Button icon={<IconSettings />} size="mini" />
|
||||
</div>
|
||||
|
||||
<div className="channel-list">
|
||||
{channels.map((channel) => (
|
||||
<button
|
||||
key={channel.key}
|
||||
className={activeChannel === channel.key ? 'channel-button active' : 'channel-button'}
|
||||
onClick={() => onSelectChannel(channel.key)}
|
||||
>
|
||||
<span className="channel-left">{channel.icon}<Text>{channel.label}</Text></span>
|
||||
{channel.count !== undefined ? <Badge count={channel.count} maxCount={999} /> : <IconMore />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<section className="recent-sessions">
|
||||
<Text className="section-title">最近会话</Text>
|
||||
{['需求评审要点整理', '生成 Q3 营销策略初稿', 'npx 安装 skill 至 codex', '产品定价模型讨论'].map((title, index) => (
|
||||
<button key={title} onClick={() => onSelectItem(title)}>
|
||||
<Text>{title}</Text>
|
||||
<Text type="secondary">{index === 0 ? '09:15' : index === 1 ? '08:47' : '昨天'}</Text>
|
||||
</button>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<Divider />
|
||||
<section className="tag-cloud">
|
||||
<Text className="section-title">标签</Text>
|
||||
<Space wrap>
|
||||
{['全部', '需求', '设计', '研发', '运营', 'AI'].map((tag) => (
|
||||
<Tag key={tag} color={tag === '全部' ? 'arcoblue' : 'gray'}>{tag}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</section>
|
||||
</Sider>
|
||||
|
||||
<Content className="stage">
|
||||
<OverviewContent onSelectItem={onSelectItem} />
|
||||
</Content>
|
||||
|
||||
<Sider className="inspector" width={360}>
|
||||
<Tabs defaultActiveTab="discussion">
|
||||
<Tabs.TabPane key="discussion" title="讨论">
|
||||
<InspectorDiscussion selectedItem={selectedItem} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="props" title="属性">
|
||||
<div className="property-list">
|
||||
<Property label="所属项目" value={activeProject.name} />
|
||||
<Property label="频道" value={channels.find((c) => c.key === activeChannel)?.label ?? '概况'} />
|
||||
<Property label="创建人" value="张伟" />
|
||||
<Property label="更新时间" value="2026-07-20 10:30" />
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="more" title="更多">
|
||||
<Space direction="vertical" className="more-actions">
|
||||
<Button long>复制链接</Button>
|
||||
<Button long>标记已处理</Button>
|
||||
<Button long status="danger">归档对象</Button>
|
||||
</Space>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Sider>
|
||||
</Layout>
|
||||
|
||||
<Footer className="statusbar">
|
||||
<div className="status-user">
|
||||
<Avatar size={26} style={{ backgroundColor: '#165DFF' }}>张</Avatar>
|
||||
<div>
|
||||
<Text className="status-name">张明</Text>
|
||||
<Text className="status-online">在线</Text>
|
||||
</div>
|
||||
<Button type="text" icon={<IconUserGroup />}>好友</Button>
|
||||
<Button type="text" icon={<IconMessage />}>私信</Button>
|
||||
<Button type="text" icon={<IconSettings />}>设置</Button>
|
||||
</div>
|
||||
<Space className="status-tasks" size={28}>
|
||||
<span><IconCheckCircleFill /> 同步完成</span>
|
||||
<span><IconInteractionFallback /> 3 个任务进行中</span>
|
||||
<span><IconRobot /> AI 队列空闲</span>
|
||||
<span><IconThunderbolt /> 本地草稿已保存</span>
|
||||
</Space>
|
||||
<Space className="status-system" size={24}>
|
||||
<span><IconStorage /> 系统存储 152GB 可用</span>
|
||||
<span>系统健康 <b>正常</b></span>
|
||||
<IconApps />
|
||||
</Space>
|
||||
</Footer>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
function IconInteractionFallback() {
|
||||
return <IconCalendar />
|
||||
}
|
||||
|
||||
function OverviewContent({ onSelectItem }: { onSelectItem: (title: string) => void }) {
|
||||
const metrics = useMemo(() => [
|
||||
{ title: 'Inbox 待处理', value: 36, delta: '较昨日 -8', icon: <IconEmail />, color: 'arcoblue' },
|
||||
{ title: '进行中的任务', value: 12, delta: '较昨日 -2', icon: <IconCheckCircleFill />, color: 'green' },
|
||||
{ title: 'AI 会话活跃', value: 4, delta: '较昨日 +1', icon: <IconRobot />, color: 'purple' },
|
||||
{ title: '笔记资料总数', value: 343, delta: '较昨日 +5', icon: <IconFile />, color: 'cyan' },
|
||||
{ title: '计划任务', value: 45, delta: '较昨日 +0', icon: <IconClockCircle />, color: 'orange' },
|
||||
], [])
|
||||
|
||||
return (
|
||||
<div className="overview-page">
|
||||
<div className="overview-head">
|
||||
<div>
|
||||
<Text type="secondary">项目整体快照,帮助你快速了解项目动态</Text>
|
||||
<Title heading={4}>概况</Title>
|
||||
</div>
|
||||
<Space>
|
||||
<Button icon={<IconCalendar />}>2026-07-20</Button>
|
||||
<Button icon={<IconMore />} />
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={12} className="metric-row">
|
||||
{metrics.map((metric) => (
|
||||
<Col span={24 / metrics.length} key={metric.title}>
|
||||
<Card className="metric-card" bordered>
|
||||
<Space align="start">
|
||||
<Tag color={metric.color}>{metric.icon}</Tag>
|
||||
<div>
|
||||
<Text type="secondary">{metric.title}</Text>
|
||||
<Title heading={3}>{metric.value}</Title>
|
||||
<Text className="metric-delta">{metric.delta}</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
<QueueSection title="Inbox 待处理(36)" items={inbox} onSelectItem={onSelectItem} />
|
||||
<TaskSection onSelectItem={onSelectItem} />
|
||||
<AISessionSection onSelectItem={onSelectItem} />
|
||||
<NoteSection onSelectItem={onSelectItem} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function QueueSection({ title, items, onSelectItem }: { title: string; items: typeof inbox; onSelectItem: (title: string) => void }) {
|
||||
return (
|
||||
<Card className="queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>{title}</Title>
|
||||
<Button type="text" size="mini">查看全部</Button>
|
||||
</div>
|
||||
{items.map((item) => (
|
||||
<button key={item.title} className="data-row" onClick={() => onSelectItem(item.title)}>
|
||||
<span className="row-title"><IconFile /> {item.title}</span>
|
||||
<span>{item.meta}</span>
|
||||
<Tag>{item.tag}</Tag>
|
||||
<span>{item.owner}</span>
|
||||
<time>{item.time}</time>
|
||||
</button>
|
||||
))}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function TaskSection({ onSelectItem }: { onSelectItem: (title: string) => void }) {
|
||||
return (
|
||||
<Card className="queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>进行中的任务(12)</Title>
|
||||
<Button type="text" size="mini">查看全部</Button>
|
||||
</div>
|
||||
{tasks.map((task) => (
|
||||
<button key={task.title} className="data-row task-row" onClick={() => onSelectItem(task.title)}>
|
||||
<span className="row-title"><IconCheckCircleFill /> {task.title}</span>
|
||||
<Tag color="arcoblue">{task.tag}</Tag>
|
||||
<span>进度 {task.progress}</span>
|
||||
<span>负责人 {task.owner}</span>
|
||||
<time>截止 {task.due}</time>
|
||||
</button>
|
||||
))}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function AISessionSection({ onSelectItem }: { onSelectItem: (title: string) => void }) {
|
||||
return (
|
||||
<Card className="queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>AI 会话(4)</Title>
|
||||
<Button type="text" size="mini">查看全部</Button>
|
||||
</div>
|
||||
{aiSessions.map((session) => (
|
||||
<button key={session.title} className="data-row ai-row" onClick={() => onSelectItem(session.title)}>
|
||||
<span className="row-title"><IconRobot /> {session.title}</span>
|
||||
<span>{session.summary}</span>
|
||||
<span>{session.owner}</span>
|
||||
<time>{session.time}</time>
|
||||
</button>
|
||||
))}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function NoteSection({ onSelectItem }: { onSelectItem: (title: string) => void }) {
|
||||
return (
|
||||
<Card className="queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>最近笔记资料(5)</Title>
|
||||
<Button type="text" size="mini">查看全部</Button>
|
||||
</div>
|
||||
{notes.map((note) => (
|
||||
<button key={note.title} className="data-row note-row" onClick={() => onSelectItem(note.title)}>
|
||||
<span className="row-title"><IconFile /> {note.title}</span>
|
||||
<Tag>{note.type}</Tag>
|
||||
<span>更新于 {note.updated}</span>
|
||||
<span>{note.owner}</span>
|
||||
</button>
|
||||
))}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function InspectorDiscussion({ selectedItem }: { selectedItem: string }) {
|
||||
return (
|
||||
<div className="discussion-panel">
|
||||
<div className="inspector-title">
|
||||
<Title heading={5}>关于「{selectedItem}」</Title>
|
||||
<Button size="mini" icon={<IconMore />} />
|
||||
</div>
|
||||
<Text type="secondary">注:同一对象的讨论与协作记录</Text>
|
||||
<Divider />
|
||||
<Space direction="vertical" size={18} className="discussion-list">
|
||||
{discussions.map((item) => (
|
||||
<div className="discussion-item" key={`${item.name}-${item.time}`}>
|
||||
<Avatar size={30}>{item.name.slice(0, 1)}</Avatar>
|
||||
<div>
|
||||
<Space>
|
||||
<Text className="discussion-name">{item.name}</Text>
|
||||
<Text type="secondary">{item.time}</Text>
|
||||
</Space>
|
||||
<Paragraph>{item.text}</Paragraph>
|
||||
<Button type="text" size="mini">回复</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
<div className="comment-box">
|
||||
<Input.TextArea placeholder="写下你的评论,Enter 发送,⌘ + Enter 换行" autoSize={{ minRows: 3, maxRows: 4 }} />
|
||||
<div className="comment-actions">
|
||||
<Space>
|
||||
<Button type="text" icon={<IconAttachmentFallback />} />
|
||||
<Button type="text" icon={<IconMessage />} />
|
||||
</Space>
|
||||
<Button type="primary" onClick={() => Message.success('评论已发送(原型)')}>发送</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function IconAttachmentFallback() {
|
||||
return <IconLink />
|
||||
}
|
||||
|
||||
function Property({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="property-row">
|
||||
<Text type="secondary">{label}</Text>
|
||||
<Text>{value}</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
41
apps/senlinai-acro-react/src/app/App.tsx
Normal file
41
apps/senlinai-acro-react/src/app/App.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useState } from 'react'
|
||||
import { ConfigProvider } from '@arco-design/web-react'
|
||||
import '@arco-design/web-react/dist/css/arco.css'
|
||||
import '../App.css'
|
||||
import { LoginPage } from '../pages/login'
|
||||
import { ProjectPage } from '../pages/project'
|
||||
import { projects } from '../features/projects/project-data'
|
||||
import type { ChannelKey, Screen, Theme } from '../features/projects/project-types'
|
||||
|
||||
function App() {
|
||||
const [screen, setScreen] = useState<Screen>('login')
|
||||
const [theme, setTheme] = useState<Theme>('light')
|
||||
const [activeProject, setActiveProject] = useState(projects[0])
|
||||
const [activeChannel, setActiveChannel] = useState<ChannelKey>('overview')
|
||||
const [selectedItem, setSelectedItem] = useState('Inbox 待处理(36)')
|
||||
|
||||
const dark = theme === 'dark'
|
||||
|
||||
return (
|
||||
<ConfigProvider>
|
||||
<main className={dark ? 'app theme-dark' : 'app'}>
|
||||
{screen === 'login' ? (
|
||||
<LoginPage onLogin={() => setScreen('workbench')} />
|
||||
) : (
|
||||
<ProjectPage
|
||||
activeProject={activeProject}
|
||||
activeChannel={activeChannel}
|
||||
selectedItem={selectedItem}
|
||||
theme={theme}
|
||||
onSelectProject={setActiveProject}
|
||||
onSelectChannel={setActiveChannel}
|
||||
onSelectItem={setSelectedItem}
|
||||
onToggleTheme={() => setTheme(dark ? 'light' : 'dark')}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,53 @@
|
||||
import { IconClockCircle, IconEmail, IconFile, IconHome, IconLink, IconList, IconRobot, IconUserGroup } from '@arco-design/web-react/icon'
|
||||
import type { Project, ProjectChannel } from './project-types'
|
||||
|
||||
export const projects: Project[] = [
|
||||
{ id: 'a1', name: '项目 A1', short: 'A1', badge: 635, urgent: true, color: '#165DFF' },
|
||||
{ id: 'a2', name: '项目 A2', short: 'PM', badge: 36, color: '#00B42A' },
|
||||
{ id: 'b1', name: '项目 B1', short: 'DS', badge: 4, urgent: true, color: '#722ED1' },
|
||||
{ id: 'c3', name: '项目 C3', short: 'OP', badge: 45, color: '#FF7D00' },
|
||||
]
|
||||
|
||||
export const channels: ProjectChannel[] = [
|
||||
{ key: 'overview', label: '概况', count: 635, icon: <IconHome /> },
|
||||
{ key: 'inbox', label: 'Inbox 消息流', count: 36, icon: <IconEmail /> },
|
||||
{ key: 'tasks', label: '工作计划', count: 12, icon: <IconList /> },
|
||||
{ key: 'ai', label: 'AI 会话', count: 4, icon: <IconRobot /> },
|
||||
{ key: 'notes', label: '笔记资料', count: 343, icon: <IconFile /> },
|
||||
{ key: 'cron', label: 'Cron 计划任务', count: 45, icon: <IconClockCircle /> },
|
||||
{ key: 'research', label: '客户研究频道', icon: <IconUserGroup /> },
|
||||
{ key: 'design', label: '产品设计频道', icon: <IconLink /> },
|
||||
]
|
||||
|
||||
export const inbox = [
|
||||
{ title: '竞争对手定价更新监控', meta: '来源:产品研究 · 竞品追踪', owner: '李明', time: '2 小时前', tag: '市场情报' },
|
||||
{ title: '客户反馈:导出功能报错', meta: '来源:客户支持 · 工单 #CS-1287', owner: '王芳', time: '4 小时前', tag: '客户反馈' },
|
||||
{ title: 'API 调用策略评估建议', meta: '来源:后端架构讨论', owner: '张伟', time: '6 小时前', tag: '架构' },
|
||||
{ title: '更新需求文档 v1.3.2', meta: '来源:产品需求 · 文档评审', owner: '陈晨', time: '昨天', tag: '需求文档' },
|
||||
]
|
||||
|
||||
export const tasks = [
|
||||
{ title: '智能报表导出功能', owner: '张伟', progress: '60%', due: '2026-07-24', tag: '进行中' },
|
||||
{ title: '企业版权限体系梳理', owner: '李明', progress: '30%', due: '2026-07-28', tag: '进行中' },
|
||||
{ title: 'Q3 营销策略制定', owner: '王芳', progress: '45%', due: '2026-07-31', tag: '进行中' },
|
||||
]
|
||||
|
||||
export const aiSessions = [
|
||||
{ title: '生成 Q3 营销策略初稿', summary: '基于市场分析和竞品数据,输出 Q3 营销策略重点...', owner: '张伟', time: '今天 08:47' },
|
||||
{ title: '产品定价模型讨论', summary: '针对企业版与标准版定价模型进行对比分析...', owner: '李明', time: '昨天 16:05' },
|
||||
{ title: '客户流失原因分析', summary: '分析近三个月客户流失数据与反馈原因...', owner: '王芳', time: '昨天 10:22' },
|
||||
{ title: '行业趋势与机会洞察', summary: '围绕 AI 在企业协同领域的趋势与机会...', owner: '张伟', time: '7 月 18 日' },
|
||||
]
|
||||
|
||||
export const notes = [
|
||||
{ title: '用户权限体系设计文档 v2.1', type: '文档', updated: '2026-07-19', owner: '李明' },
|
||||
{ title: '导出功能技术方案评审', type: '文档', updated: '2026-07-18', owner: '张伟' },
|
||||
{ title: '2026 Q3 竞品功能对比表', type: '表格', updated: '2026-07-18', owner: '王芳' },
|
||||
]
|
||||
|
||||
export const discussions = [
|
||||
{ name: '李明', time: '2026-07-20 09:12', text: '关于导出报错的问题,已在测试环境复现,初步定位为权限校验逻辑异常。' },
|
||||
{ name: '张伟', time: '2026-07-20 09:18', text: '@李明 麻烦同步修复预计完成时间,我这边需要同步给客户。' },
|
||||
{ name: '王芳', time: '2026-07-20 10:05', text: '补充:客户反馈影响范围主要集中在企业版用户。' },
|
||||
{ name: '系统通知', time: '2026-07-20 10:30', text: '已将此讨论关联到任务:智能报表导出功能。' },
|
||||
]
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Avatar, Button, Divider, Input, Layout, Message, Space, Tabs, Typography } from '@arco-design/web-react'
|
||||
import { IconLink, IconMessage, IconMore } from '@arco-design/web-react/icon'
|
||||
import { channels, discussions } from './project-data'
|
||||
import type { ChannelKey, Project } from './project-types'
|
||||
|
||||
const { Sider } = Layout
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
|
||||
export function ProjectInspector({ activeProject, activeChannel, selectedItem }: { activeProject: Project; activeChannel: ChannelKey; selectedItem: string }) {
|
||||
return (
|
||||
<Sider className="inspector" width={360}>
|
||||
<Tabs defaultActiveTab="discussion">
|
||||
<Tabs.TabPane key="discussion" title="讨论"><InspectorDiscussion selectedItem={selectedItem} /></Tabs.TabPane>
|
||||
<Tabs.TabPane key="props" title="属性"><div className="property-list"><Property label="所属项目" value={activeProject.name} /><Property label="频道" value={channels.find((c) => c.key === activeChannel)?.label ?? '概况'} /><Property label="创建人" value="张伟" /><Property label="更新时间" value="2026-07-20 10:30" /></div></Tabs.TabPane>
|
||||
<Tabs.TabPane key="more" title="更多"><Space direction="vertical" className="more-actions"><Button long>复制链接</Button><Button long>标记已处理</Button><Button long status="danger">归档对象</Button></Space></Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Sider>
|
||||
)
|
||||
}
|
||||
|
||||
function InspectorDiscussion({ selectedItem }: { selectedItem: string }) {
|
||||
return (
|
||||
<div className="discussion-panel">
|
||||
<div className="inspector-title">
|
||||
<Title heading={5}>关于「{selectedItem}」</Title>
|
||||
<Button size="mini" icon={<IconMore />} />
|
||||
</div>
|
||||
<Text type="secondary">注:同一对象的讨论与协作记录</Text>
|
||||
<Divider />
|
||||
<Space direction="vertical" size={18} className="discussion-list">
|
||||
{discussions.map((item) => (
|
||||
<div className="discussion-item" key={`${item.name}-${item.time}`}>
|
||||
<Avatar size={30}>{item.name.slice(0, 1)}</Avatar>
|
||||
<div>
|
||||
<Space>
|
||||
<Text className="discussion-name">{item.name}</Text>
|
||||
<Text type="secondary">{item.time}</Text>
|
||||
</Space>
|
||||
<Paragraph>{item.text}</Paragraph>
|
||||
<Button type="text" size="mini">回复</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
<div className="comment-box">
|
||||
<Input.TextArea placeholder="写下你的评论,Enter 发送,⌘ + Enter 换行" autoSize={{ minRows: 3, maxRows: 4 }} />
|
||||
<div className="comment-actions">
|
||||
<Space>
|
||||
<Button type="text" icon={<IconAttachmentFallback />} />
|
||||
<Button type="text" icon={<IconMessage />} />
|
||||
</Space>
|
||||
<Button type="primary" onClick={() => Message.success('评论已发送(原型)')}>发送</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function IconAttachmentFallback() {
|
||||
return <IconLink />
|
||||
}
|
||||
|
||||
function Property({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="property-row">
|
||||
<Text type="secondary">{label}</Text>
|
||||
<Text>{value}</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
import { Badge, Button, Layout, Space } from '@arco-design/web-react'
|
||||
import { IconDashboard, IconPlus } from '@arco-design/web-react/icon'
|
||||
import { projects } from './project-data'
|
||||
import type { Project } from './project-types'
|
||||
|
||||
const { Sider } = Layout
|
||||
|
||||
export function ProjectRail({ activeProject, onSelectProject }: { activeProject: Project; onSelectProject: (project: Project) => void }) {
|
||||
return (
|
||||
<Sider className="project-rail" width={96}>
|
||||
<Button className="dashboard-button" icon={<IconDashboard />} />
|
||||
<Space className="project-stack" direction="vertical" size={12}>
|
||||
{projects.map((project) => (
|
||||
<Badge key={project.id} count={project.badge} dot={false} className={project.urgent ? 'project-badge urgent' : 'project-badge'}>
|
||||
<button className={activeProject.id === project.id ? 'project-button active' : 'project-button'} onClick={() => onSelectProject(project)} style={{ '--project-color': project.color } as CSSProperties} title={project.name}>
|
||||
{project.short}
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</Space>
|
||||
<Button className="create-project" aria-label="新建项目" icon={<IconPlus />} title="新建项目" />
|
||||
</Sider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Badge, Button, Divider, Layout, Space, Tag, Typography } from '@arco-design/web-react'
|
||||
import { IconDashboard, IconMore, IconSettings } from '@arco-design/web-react/icon'
|
||||
import { channels } from './project-data'
|
||||
import type { ChannelKey, Project } from './project-types'
|
||||
|
||||
const { Sider } = Layout
|
||||
const { Title, Text } = Typography
|
||||
|
||||
export function ProjectSidebar({ activeProject, activeChannel, onSelectChannel, onSelectItem }: { activeProject: Project; activeChannel: ChannelKey; onSelectChannel: (key: ChannelKey) => void; onSelectItem: (title: string) => void }) {
|
||||
return (
|
||||
<Sider className="channel-sidebar" width={248}>
|
||||
<div className="project-title"><Space><IconDashboard /><Title heading={5}>{activeProject.name}</Title></Space><Button icon={<IconSettings />} size="mini" /></div>
|
||||
<div className="channel-list">
|
||||
{channels.map((channel) => (
|
||||
<button key={channel.key} className={activeChannel === channel.key ? 'channel-button active' : 'channel-button'} onClick={() => onSelectChannel(channel.key)}>
|
||||
<span className="channel-left">{channel.icon}<Text>{channel.label}</Text></span>
|
||||
{channel.count !== undefined ? <Badge count={channel.count} maxCount={999} /> : <IconMore />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Divider />
|
||||
<section className="recent-sessions">
|
||||
<Text className="section-title">最近会话</Text>
|
||||
{['需求评审要点整理', '生成 Q3 营销策略初稿', 'npx 安装 skill 至 codex', '产品定价模型讨论'].map((title, index) => (
|
||||
<button key={title} onClick={() => onSelectItem(title)}><Text>{title}</Text><Text type="secondary">{index === 0 ? '09:15' : index === 1 ? '08:47' : '昨天'}</Text></button>
|
||||
))}
|
||||
</section>
|
||||
<Divider />
|
||||
<section className="tag-cloud">
|
||||
<Text className="section-title">标签</Text>
|
||||
<Space wrap>{['全部', '需求', '设计', '研发', '运营', 'AI'].map((tag) => <Tag key={tag} color={tag === '全部' ? 'arcoblue' : 'gray'}>{tag}</Tag>)}</Space>
|
||||
</section>
|
||||
</Sider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Avatar, Button, Layout, Space, Typography } from '@arco-design/web-react'
|
||||
import { IconApps, IconCalendar, IconCheckCircleFill, IconMessage, IconRobot, IconSettings, IconStorage, IconThunderbolt, IconUserGroup } from '@arco-design/web-react/icon'
|
||||
|
||||
const { Footer } = Layout
|
||||
const { Text } = Typography
|
||||
|
||||
export function ProjectStatusbar() {
|
||||
return (
|
||||
<Footer className="statusbar">
|
||||
<div className="status-user"><Avatar size={26} style={{ backgroundColor: '#165DFF' }}>张</Avatar><div><Text className="status-name">张明</Text><Text className="status-online">在线</Text></div><Button type="text" icon={<IconUserGroup />}>好友</Button><Button type="text" icon={<IconMessage />}>私信</Button><Button type="text" icon={<IconSettings />}>设置</Button></div>
|
||||
<Space className="status-tasks" size={28}><span><IconCheckCircleFill /> 同步完成</span><span><IconInteractionFallback /> 3 个任务进行中</span><span><IconRobot /> AI 队列空闲</span><span><IconThunderbolt /> 本地草稿已保存</span></Space>
|
||||
<Space className="status-system" size={24}><span><IconStorage /> 系统存储 152GB 可用</span><span>系统健康 <b>正常</b></span><IconApps /></Space>
|
||||
</Footer>
|
||||
)
|
||||
}
|
||||
|
||||
function IconInteractionFallback() {
|
||||
return <IconCalendar />
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Badge, Button, Input, Layout, Space, Typography } from '@arco-design/web-react'
|
||||
import { IconApps, IconArrowLeft, IconArrowRight, IconMoon, IconNotification, IconQuestionCircle, IconSearch, IconSun } from '@arco-design/web-react/icon'
|
||||
import type { Theme } from './project-types'
|
||||
|
||||
const { Header } = Layout
|
||||
const { Text } = Typography
|
||||
|
||||
export function ProjectTopbar({ theme, onToggleTheme }: { theme: Theme; onToggleTheme: () => void }) {
|
||||
return (
|
||||
<Header className="topbar">
|
||||
<div className="brand-lockup">
|
||||
<span className="brand-symbol">S</span>
|
||||
<Text className="brand-name">SenlinAI</Text>
|
||||
</div>
|
||||
<div className="global-search" role="search">
|
||||
<Input size="large" prefix={<IconSearch />} placeholder="搜索项目、频道、文档、任务、联系人..." />
|
||||
<Button size="large" type="primary">搜索</Button>
|
||||
</div>
|
||||
<Space className="topbar-actions">
|
||||
<Button aria-label={theme === 'dark' ? '切换浅色模式' : '切换深色模式'} icon={theme === 'dark' ? <IconSun /> : <IconMoon />} onClick={onToggleTheme} />
|
||||
<Button icon={<IconArrowLeft />} disabled />
|
||||
<Button icon={<IconArrowRight />} disabled />
|
||||
<Badge count={8} dot><Button icon={<IconNotification />} /></Badge>
|
||||
<Button icon={<IconQuestionCircle />} />
|
||||
<Button icon={<IconApps />} />
|
||||
</Space>
|
||||
</Header>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { ReactElement } from 'react'
|
||||
|
||||
export type Screen = 'login' | 'workbench'
|
||||
export type Theme = 'light' | 'dark'
|
||||
export type ChannelKey = 'overview' | 'inbox' | 'tasks' | 'ai' | 'notes' | 'cron' | 'research' | 'design'
|
||||
|
||||
export type Project = {
|
||||
id: string
|
||||
name: string
|
||||
short: string
|
||||
badge: number
|
||||
urgent?: boolean
|
||||
color: string
|
||||
}
|
||||
|
||||
export type ProjectChannel = {
|
||||
key: ChannelKey
|
||||
label: string
|
||||
count?: number
|
||||
icon: ReactElement
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import App from './app/App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
|
||||
87
apps/senlinai-acro-react/src/pages/login.tsx
Normal file
87
apps/senlinai-acro-react/src/pages/login.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Button, Card, Form, Input, Space, Typography } from '@arco-design/web-react'
|
||||
import { IconBook, IconCheckCircleFill, IconLaunch, IconSafe, IconUserGroup } from '@arco-design/web-react/icon'
|
||||
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
|
||||
export function LoginPage({ onLogin }: { onLogin: () => void }) {
|
||||
const [server, setServer] = useState('https://10.0.0.15:8080')
|
||||
|
||||
return (
|
||||
<section className="login-screen">
|
||||
<Card className="login-card" bordered>
|
||||
<div className="login-brand-panel">
|
||||
<div className="brand-lockup large">
|
||||
<span className="brand-symbol">S</span>
|
||||
<Title heading={2}>SenlinAI</Title>
|
||||
</div>
|
||||
<Text className="login-slogan">私有部署 · 安全可控的智能协作平台</Text>
|
||||
<Text type="secondary">知识沉淀 · 团队协作 · AI 助理</Text>
|
||||
<Space className="login-footer-links" size={24}>
|
||||
<Text type="secondary">v1.0.0</Text>
|
||||
<Text type="secondary">隐私政策</Text>
|
||||
<Text type="secondary">服务协议</Text>
|
||||
<Text type="secondary">关于 SenlinAI</Text>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className="login-form-panel">
|
||||
<Title heading={4}>登录到您的 SenlinAI</Title>
|
||||
<Text type="secondary">连接私有工作台</Text>
|
||||
<Form layout="vertical" className="login-form">
|
||||
<Form.Item label="服务器地址(IP 或域名优先)">
|
||||
<Input value={server} onChange={setServer} placeholder="例如:https://10.0.0.15:8080" suffix={<IconLaunch />} />
|
||||
</Form.Item>
|
||||
<Form.Item label="账号">
|
||||
<Input placeholder="请输入用户名或邮箱" />
|
||||
</Form.Item>
|
||||
<Form.Item label="密码">
|
||||
<Input.Password placeholder="请输入密码" />
|
||||
</Form.Item>
|
||||
<div className="login-row">
|
||||
<Space size={8}>
|
||||
<span className="check-dot" />
|
||||
<Text>记住服务器地址</Text>
|
||||
</Space>
|
||||
<Button type="text" size="mini">无法连接?</Button>
|
||||
</div>
|
||||
<Button type="primary" long size="large" onClick={onLogin}>
|
||||
登录
|
||||
</Button>
|
||||
<div className="connection-card">
|
||||
<IconCheckCircleFill />
|
||||
<div>
|
||||
<Text className="connection-title">连接正常</Text>
|
||||
<Text type="secondary">已连接到 {server}</Text>
|
||||
</div>
|
||||
<Button type="text" size="mini">更换服务器</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div className="login-value-panel">
|
||||
<Title heading={3}>安全 · 专注 · 高效</Title>
|
||||
<Paragraph>
|
||||
SenlinAI 是面向知识工作者与内部团队的私有部署工作台,帮助团队在统一空间中收集信息、整理知识、协同决策。
|
||||
</Paragraph>
|
||||
<ValuePoint icon={<IconSafe />} title="私有部署,数据自有" desc="所有数据与模型运行在您的环境中,安全可控。" />
|
||||
<ValuePoint icon={<IconBook />} title="知识沉淀,结构清晰" desc="从收集到整理,形成可复用的团队知识库。" />
|
||||
<ValuePoint icon={<IconUserGroup />} title="协同高效,聚焦成果" desc="围绕项目与任务协同,减少沟通成本,专注交付。" />
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ValuePoint({ icon, title, desc }: { icon: ReactElement; title: string; desc: string }) {
|
||||
return (
|
||||
<div className="value-point">
|
||||
<span>{icon}</span>
|
||||
<div>
|
||||
<Text className="value-title">{title}</Text>
|
||||
<Text type="secondary">{desc}</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
132
apps/senlinai-acro-react/src/pages/overview.tsx
Normal file
132
apps/senlinai-acro-react/src/pages/overview.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Button, Card, Grid, Space, Tag, Typography } from '@arco-design/web-react'
|
||||
import { IconCalendar, IconCheckCircleFill, IconClockCircle, IconEmail, IconFile, IconMore, IconRobot } from '@arco-design/web-react/icon'
|
||||
import { aiSessions, inbox, notes, tasks } from '../features/projects/project-data'
|
||||
|
||||
const { Row, Col } = Grid
|
||||
const { Title, Text } = Typography
|
||||
|
||||
export function OverviewPage({ onSelectItem }: { onSelectItem: (title: string) => void }) {
|
||||
const metrics = useMemo(() => [
|
||||
{ title: 'Inbox 待处理', value: 36, delta: '较昨日 -8', icon: <IconEmail />, color: 'arcoblue' },
|
||||
{ title: '进行中的任务', value: 12, delta: '较昨日 -2', icon: <IconCheckCircleFill />, color: 'green' },
|
||||
{ title: 'AI 会话活跃', value: 4, delta: '较昨日 +1', icon: <IconRobot />, color: 'purple' },
|
||||
{ title: '笔记资料总数', value: 343, delta: '较昨日 +5', icon: <IconFile />, color: 'cyan' },
|
||||
{ title: '计划任务', value: 45, delta: '较昨日 +0', icon: <IconClockCircle />, color: 'orange' },
|
||||
], [])
|
||||
|
||||
return (
|
||||
<div className="overview-page">
|
||||
<div className="overview-head">
|
||||
<div>
|
||||
<Text type="secondary">项目整体快照,帮助你快速了解项目动态</Text>
|
||||
<Title heading={4}>概况</Title>
|
||||
</div>
|
||||
<Space>
|
||||
<Button icon={<IconCalendar />}>2026-07-20</Button>
|
||||
<Button icon={<IconMore />} />
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={12} className="metric-row">
|
||||
{metrics.map((metric) => (
|
||||
<Col span={24 / metrics.length} key={metric.title}>
|
||||
<Card className="metric-card" bordered>
|
||||
<Space align="start">
|
||||
<Tag color={metric.color}>{metric.icon}</Tag>
|
||||
<div>
|
||||
<Text type="secondary">{metric.title}</Text>
|
||||
<Title heading={3}>{metric.value}</Title>
|
||||
<Text className="metric-delta">{metric.delta}</Text>
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
<QueueSection title="Inbox 待处理(36)" items={inbox} onSelectItem={onSelectItem} />
|
||||
<TaskSection onSelectItem={onSelectItem} />
|
||||
<AISessionSection onSelectItem={onSelectItem} />
|
||||
<NoteSection onSelectItem={onSelectItem} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function QueueSection({ title, items, onSelectItem }: { title: string; items: typeof inbox; onSelectItem: (title: string) => void }) {
|
||||
return (
|
||||
<Card className="queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>{title}</Title>
|
||||
<Button type="text" size="mini">查看全部</Button>
|
||||
</div>
|
||||
{items.map((item) => (
|
||||
<button key={item.title} className="data-row" onClick={() => onSelectItem(item.title)}>
|
||||
<span className="row-title"><IconFile /> {item.title}</span>
|
||||
<span>{item.meta}</span>
|
||||
<Tag>{item.tag}</Tag>
|
||||
<span>{item.owner}</span>
|
||||
<time>{item.time}</time>
|
||||
</button>
|
||||
))}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function TaskSection({ onSelectItem }: { onSelectItem: (title: string) => void }) {
|
||||
return (
|
||||
<Card className="queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>进行中的任务(12)</Title>
|
||||
<Button type="text" size="mini">查看全部</Button>
|
||||
</div>
|
||||
{tasks.map((task) => (
|
||||
<button key={task.title} className="data-row task-row" onClick={() => onSelectItem(task.title)}>
|
||||
<span className="row-title"><IconCheckCircleFill /> {task.title}</span>
|
||||
<Tag color="arcoblue">{task.tag}</Tag>
|
||||
<span>进度 {task.progress}</span>
|
||||
<span>负责人 {task.owner}</span>
|
||||
<time>截止 {task.due}</time>
|
||||
</button>
|
||||
))}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function AISessionSection({ onSelectItem }: { onSelectItem: (title: string) => void }) {
|
||||
return (
|
||||
<Card className="queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>AI 会话(4)</Title>
|
||||
<Button type="text" size="mini">查看全部</Button>
|
||||
</div>
|
||||
{aiSessions.map((session) => (
|
||||
<button key={session.title} className="data-row ai-row" onClick={() => onSelectItem(session.title)}>
|
||||
<span className="row-title"><IconRobot /> {session.title}</span>
|
||||
<span>{session.summary}</span>
|
||||
<span>{session.owner}</span>
|
||||
<time>{session.time}</time>
|
||||
</button>
|
||||
))}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function NoteSection({ onSelectItem }: { onSelectItem: (title: string) => void }) {
|
||||
return (
|
||||
<Card className="queue-section" bordered>
|
||||
<div className="section-header">
|
||||
<Title heading={6}>最近笔记资料(5)</Title>
|
||||
<Button type="text" size="mini">查看全部</Button>
|
||||
</div>
|
||||
{notes.map((note) => (
|
||||
<button key={note.title} className="data-row note-row" onClick={() => onSelectItem(note.title)}>
|
||||
<span className="row-title"><IconFile /> {note.title}</span>
|
||||
<Tag>{note.type}</Tag>
|
||||
<span>更新于 {note.updated}</span>
|
||||
<span>{note.owner}</span>
|
||||
</button>
|
||||
))}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
55
apps/senlinai-acro-react/src/pages/project.tsx
Normal file
55
apps/senlinai-acro-react/src/pages/project.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Layout } from '@arco-design/web-react'
|
||||
import { OverviewPage } from './overview'
|
||||
import { ProjectInspector } from '../features/projects/project-inspector'
|
||||
import { ProjectRail } from '../features/projects/project-rail'
|
||||
import { ProjectSidebar } from '../features/projects/project-sidebar'
|
||||
import { ProjectStatusbar } from '../features/projects/project-statusbar'
|
||||
import { ProjectTopbar } from '../features/projects/project-topbar'
|
||||
import type { ChannelKey, Project, Theme } from '../features/projects/project-types'
|
||||
|
||||
const { Content } = Layout
|
||||
|
||||
export function ProjectPage({
|
||||
activeProject,
|
||||
activeChannel,
|
||||
selectedItem,
|
||||
theme,
|
||||
onSelectProject,
|
||||
onSelectChannel,
|
||||
onSelectItem,
|
||||
onToggleTheme,
|
||||
}: {
|
||||
activeProject: Project
|
||||
activeChannel: ChannelKey
|
||||
selectedItem: string
|
||||
theme: Theme
|
||||
onSelectProject: (project: Project) => void
|
||||
onSelectChannel: (key: ChannelKey) => void
|
||||
onSelectItem: (title: string) => void
|
||||
onToggleTheme: () => void
|
||||
}) {
|
||||
return (
|
||||
<Layout className="workbench-shell">
|
||||
<ProjectTopbar theme={theme} onToggleTheme={onToggleTheme} />
|
||||
|
||||
<Layout className="workbench-main">
|
||||
<ProjectRail activeProject={activeProject} onSelectProject={onSelectProject} />
|
||||
|
||||
<ProjectSidebar
|
||||
activeProject={activeProject}
|
||||
activeChannel={activeChannel}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onSelectItem={onSelectItem}
|
||||
/>
|
||||
|
||||
<Content className="stage">
|
||||
<OverviewPage onSelectItem={onSelectItem} />
|
||||
</Content>
|
||||
|
||||
<ProjectInspector activeProject={activeProject} activeChannel={activeChannel} selectedItem={selectedItem} />
|
||||
</Layout>
|
||||
|
||||
<ProjectStatusbar />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user