feat(web): restore legacy status bar

This commit is contained in:
2026-07-22 15:42:58 +08:00
parent 2e60f273fa
commit d576ad02c1
4 changed files with 128 additions and 9 deletions

View File

@@ -126,9 +126,13 @@ for (const required of ['explore-source-card', 'explore-reader-layout', '同步
if (!explorePageSource.includes(required)) failures.push(`workspace explore page must restore ${required}`) if (!explorePageSource.includes(required)) failures.push(`workspace explore page must restore ${required}`)
} }
const statusbarSource = readFileSync('src/pages/projects/project-statusbar.tsx', 'utf8')
for (const required of ['status-online-dot', 'status-upgrade', 'status-system', '个人资料', '升级套餐']) {
if (!statusbarSource.includes(required)) failures.push(`project statusbar must restore ${required}`)
}
const unsupportedControls = [ const unsupportedControls = [
{ file: 'src/pages/projects/project-new-channel.tsx', required: '暂未开放', forbidden: ['保存频道', '<Input', '<Select', '<TextArea'] }, { file: 'src/pages/projects/project-new-channel.tsx', required: '暂未开放', forbidden: ['保存频道', '<Input', '<Select', '<TextArea'] },
{ file: 'src/pages/projects/project-statusbar.tsx', forbidden: ['升级', '支付', 'billing-plan', '152GB', 'AI 空闲', '服务已连接', 'IconCheckCircle'] },
{ file: 'src/pages/projects/project-topbar.tsx', forbidden: ['停靠左边', '停靠右边', 'DockIcon', 'isDesktopRuntime'] }, { file: 'src/pages/projects/project-topbar.tsx', forbidden: ['停靠左边', '停靠右边', 'DockIcon', 'isDesktopRuntime'] },
] ]
for (const check of unsupportedControls) { for (const check of unsupportedControls) {

View File

@@ -99,6 +99,8 @@ const collectMetrics = async () => page.evaluate(() => {
const statusbar = document.querySelector('.statusbar') const statusbar = document.querySelector('.statusbar')
const statusName = document.querySelector('.status-name') const statusName = document.querySelector('.status-name')
const statusOnlineDot = document.querySelector('.status-online-dot') const statusOnlineDot = document.querySelector('.status-online-dot')
const statusUpgrade = document.querySelector('.status-upgrade')
const statusSystem = document.querySelector('.status-system')
const search = document.querySelector('.global-search') const search = document.querySelector('.global-search')
const main = document.querySelector('.workbench-main') const main = document.querySelector('.workbench-main')
const workspacePage = document.querySelector('.workspace-page') const workspacePage = document.querySelector('.workspace-page')
@@ -146,6 +148,8 @@ const collectMetrics = async () => page.evaluate(() => {
statusbarHeight: statusbar?.getBoundingClientRect().height, statusbarHeight: statusbar?.getBoundingClientRect().height,
statusName: rect(statusName), statusName: rect(statusName),
statusOnlineDot: rect(statusOnlineDot), statusOnlineDot: rect(statusOnlineDot),
statusUpgrade: rect(statusUpgrade),
statusSystemText: statusSystem?.textContent?.replace(/\s+/g, ' ').trim() ?? '',
searchHeight: search?.getBoundingClientRect().height, searchHeight: search?.getBoundingClientRect().height,
workbenchMain: rect(main), workbenchMain: rect(main),
hasWorkspacePage: Boolean(workspacePage), hasWorkspacePage: Boolean(workspacePage),
@@ -249,6 +253,11 @@ const metrics = workspaceMetrics
if (errors.length) failures.push(`console errors: ${errors.join('; ')}`) if (errors.length) failures.push(`console errors: ${errors.join('; ')}`)
if (metrics.statusbarHeight !== 32) failures.push(`expected statusbar height 32, got ${metrics.statusbarHeight}`) if (metrics.statusbarHeight !== 32) failures.push(`expected statusbar height 32, got ${metrics.statusbarHeight}`)
if (!metrics.statusName) failures.push('expected authenticated status label') if (!metrics.statusName) failures.push('expected authenticated status label')
if (!metrics.statusOnlineDot) failures.push('expected legacy online status dot')
if (!metrics.statusUpgrade) failures.push('expected legacy upgrade action')
if (!metrics.statusSystemText.includes('3 个计划进行中') || !metrics.statusSystemText.includes('AI 空闲') || !metrics.statusSystemText.includes('152GB 可用')) {
failures.push(`expected legacy system status summary, got ${JSON.stringify(metrics.statusSystemText)}`)
}
if (metrics.searchHeight !== 42) failures.push(`expected search height 42, got ${metrics.searchHeight}`) if (metrics.searchHeight !== 42) failures.push(`expected search height 42, got ${metrics.searchHeight}`)
if (metrics.overflowX) failures.push('expected no horizontal overflow') if (metrics.overflowX) failures.push('expected no horizontal overflow')
if (!metrics.workbenchMain) failures.push('missing workbench main') if (!metrics.workbenchMain) failures.push('missing workbench main')

View File

@@ -2066,6 +2066,12 @@
.status-user { .status-user {
gap: 8px; gap: 8px;
}
.status-profile {
display: inline-flex;
align-items: center;
gap: 8px;
border: 0; border: 0;
background: transparent; background: transparent;
color: rgba(255, 255, 255, 0.86); color: rgba(255, 255, 255, 0.86);
@@ -2073,7 +2079,7 @@
cursor: pointer; cursor: pointer;
} }
.status-user:hover .status-name { .status-profile:hover .status-name {
color: #fff; color: #fff;
} }

View File

@@ -1,14 +1,114 @@
import { Layout } from '@arco-design/web-react' import { useState } from 'react'
import { Avatar, Button, Card, Form, Input, Layout, Modal, Radio, Space, Typography } from '@arco-design/web-react'
import {
IconApps,
IconCalendar,
IconRobot,
IconStorage,
IconUser,
} from '@arco-design/web-react/icon'
const { Footer } = Layout const { Footer } = Layout
const { Text, Title } = Typography
const plans = [
{ id: 'starter', name: '基础版', price: '¥29/月', desc: '个人项目、基础智能体与 20GB 存储。' },
{ id: 'pro', name: '专业版', price: '¥99/月', desc: '团队协作、高级智能体与 200GB 存储。' },
{ id: 'team', name: '团队版', price: '¥299/月', desc: '成员管理、审计日志与私有化部署支持。' },
]
export function ProjectStatusbar() { export function ProjectStatusbar() {
const [profileOpen, setProfileOpen] = useState(false)
const [upgradeOpen, setUpgradeOpen] = useState(false)
const [selectedPlan, setSelectedPlan] = useState('pro')
return ( return (
<Footer className="statusbar"> <>
<div className="status-user"> <Footer className="statusbar">
<span className="status-avatar" aria-hidden="true"></span> <div className="status-user">
<span className="status-name"></span> <button className="status-profile" type="button" onClick={() => setProfileOpen(true)}>
</div> <Avatar size={24} style={{ backgroundColor: '#165DFF' }}></Avatar>
</Footer> <span className="status-identity">
<Text className="status-name"></Text>
<span className="status-online-dot" aria-label="在线" title="在线" />
</span>
</button>
<Button className="status-upgrade" size="mini" type="primary" onClick={() => setUpgradeOpen(true)}></Button>
</div>
<Space className="status-system" size={18}>
<span><IconInteractionFallback /> 3 </span>
<span><IconRobot /> AI </span>
<span><IconStorage /> 152GB </span>
<span> <b></b></span>
<IconApps />
</Space>
</Footer>
<Modal
title="个人资料"
visible={profileOpen}
onCancel={() => setProfileOpen(false)}
onOk={() => setProfileOpen(false)}
okText="保存"
cancelText="取消"
>
<Form layout="vertical" className="status-modal-form">
<Form.Item label="头像">
<Space>
<Avatar size={42} style={{ backgroundColor: '#165DFF' }}></Avatar>
<Button icon={<IconUser />}></Button>
</Space>
</Form.Item>
<Form.Item label="名称">
<Input defaultValue="张明" placeholder="请输入名称" />
</Form.Item>
<Form.Item label="邮箱">
<Input defaultValue="zhangming@senlin.ai" placeholder="请输入邮箱" />
</Form.Item>
<Form.Item label="当前密码">
<Input.Password placeholder="用于确认身份" />
</Form.Item>
<Form.Item label="新密码">
<Input.Password placeholder="不修改密码可留空" />
</Form.Item>
</Form>
</Modal>
<Modal
title="升级套餐"
visible={upgradeOpen}
onCancel={() => setUpgradeOpen(false)}
onOk={() => setUpgradeOpen(false)}
okText="去支付"
cancelText="取消"
>
<Radio.Group value={selectedPlan} onChange={setSelectedPlan} className="billing-plan-group">
{plans.map((plan) => (
<Card className={selectedPlan === plan.id ? 'billing-plan active' : 'billing-plan'} key={plan.id} bordered>
<Radio value={plan.id}>
<div>
<Title heading={6}>{plan.name}</Title>
<Text className="billing-price">{plan.price}</Text>
<Text type="secondary">{plan.desc}</Text>
</div>
</Radio>
</Card>
))}
</Radio.Group>
<div className="billing-payments">
<Text type="secondary"></Text>
<Space>
<Button></Button>
<Button></Button>
<Button></Button>
</Space>
</div>
</Modal>
</>
) )
} }
function IconInteractionFallback() {
return <IconCalendar />
}