Add React Arco workbench prototype
This commit is contained in:
25
prototypes/senlinai-acro-react/.gitignore
vendored
Normal file
25
prototypes/senlinai-acro-react/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
test-results
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
8
prototypes/senlinai-acro-react/.oxlintrc.json
Normal file
8
prototypes/senlinai-acro-react/.oxlintrc.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["react", "typescript", "oxc"],
|
||||||
|
"rules": {
|
||||||
|
"react/rules-of-hooks": "error",
|
||||||
|
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
||||||
|
}
|
||||||
|
}
|
||||||
17
prototypes/senlinai-acro-react/README.md
Normal file
17
prototypes/senlinai-acro-react/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# SenlinAI Acro React Prototype
|
||||||
|
|
||||||
|
独立 React + Arco Design 高保真原型,用于验证 SenlinAI 登录页和系统主页最终设计方向。
|
||||||
|
|
||||||
|
该目录不替换现有 `apps/web` Svelte 应用。
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
npm run build
|
||||||
|
npm run lint
|
||||||
|
node scripts/visual-check.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
默认本地预览端口:`4174`。
|
||||||
13
prototypes/senlinai-acro-react/index.html
Normal file
13
prototypes/senlinai-acro-react/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>senlinai-acro-react</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1803
prototypes/senlinai-acro-react/package-lock.json
generated
Normal file
1803
prototypes/senlinai-acro-react/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
prototypes/senlinai-acro-react/package.json
Normal file
27
prototypes/senlinai-acro-react/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "senlinai-acro-react",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "oxlint",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@arco-design/web-react": "^2.66.16",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.13.2",
|
||||||
|
"@types/react": "^18.3.23",
|
||||||
|
"@types/react-dom": "^18.3.7",
|
||||||
|
"@vitejs/plugin-react": "^6.0.3",
|
||||||
|
"oxlint": "^1.71.0",
|
||||||
|
"playwright": "^1.61.1",
|
||||||
|
"typescript": "~6.0.2",
|
||||||
|
"vite": "^8.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
prototypes/senlinai-acro-react/public/favicon.svg
Normal file
1
prototypes/senlinai-acro-react/public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
45
prototypes/senlinai-acro-react/scripts/visual-check.mjs
Normal file
45
prototypes/senlinai-acro-react/scripts/visual-check.mjs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { createServer } from 'vite'
|
||||||
|
import { chromium } from 'playwright'
|
||||||
|
|
||||||
|
const port = Number(process.env.VISUAL_CHECK_PORT ?? 4175)
|
||||||
|
|
||||||
|
const server = await createServer({
|
||||||
|
root: process.cwd(),
|
||||||
|
server: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port,
|
||||||
|
strictPort: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await server.listen()
|
||||||
|
|
||||||
|
const browser = await chromium.launch({ headless: true })
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1440, height: 1024 }, deviceScaleFactor: 1 })
|
||||||
|
const errors = []
|
||||||
|
page.on('console', (message) => {
|
||||||
|
if (message.type() === 'error') errors.push(message.text())
|
||||||
|
})
|
||||||
|
|
||||||
|
await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'networkidle' })
|
||||||
|
await page.screenshot({ path: 'test-results/login-react-acro.png', fullPage: true })
|
||||||
|
await page.getByRole('button', { name: '登录' }).click()
|
||||||
|
await page.waitForSelector('.workbench-shell')
|
||||||
|
await page.screenshot({ path: 'test-results/workbench-react-acro-light.png', fullPage: true })
|
||||||
|
await page.getByRole('button', { name: '切换深色模式' }).click()
|
||||||
|
await page.screenshot({ path: 'test-results/workbench-react-acro-dark.png', fullPage: true })
|
||||||
|
|
||||||
|
const metrics = await page.evaluate(() => {
|
||||||
|
const statusbar = document.querySelector('.statusbar')
|
||||||
|
const search = document.querySelector('.global-search')
|
||||||
|
return {
|
||||||
|
statusbarHeight: statusbar?.getBoundingClientRect().height,
|
||||||
|
searchHeight: search?.getBoundingClientRect().height,
|
||||||
|
overflowX: document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
await server.close()
|
||||||
|
|
||||||
|
console.log(JSON.stringify({ metrics, errors }, null, 2))
|
||||||
676
prototypes/senlinai-acro-react/src/App.css
Normal file
676
prototypes/senlinai-acro-react/src/App.css
Normal file
@@ -0,0 +1,676 @@
|
|||||||
|
:root {
|
||||||
|
--senlin-primary: #165dff;
|
||||||
|
--senlin-bg: #f7f8fa;
|
||||||
|
--senlin-panel: #ffffff;
|
||||||
|
--senlin-border: #e5e6eb;
|
||||||
|
--senlin-text: #1d2129;
|
||||||
|
--senlin-muted: #86909c;
|
||||||
|
--senlin-soft-blue: #e8f3ff;
|
||||||
|
--senlin-status: #142033;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app {
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background: var(--senlin-bg);
|
||||||
|
color: var(--senlin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark {
|
||||||
|
--senlin-bg: #1e1e1e;
|
||||||
|
--senlin-panel: #252526;
|
||||||
|
--senlin-border: #3c3c3c;
|
||||||
|
--senlin-text: #cccccc;
|
||||||
|
--senlin-muted: #858585;
|
||||||
|
--senlin-soft-blue: rgba(0, 122, 204, 0.22);
|
||||||
|
--senlin-primary: #007acc;
|
||||||
|
--senlin-status: #007acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .arco-layout,
|
||||||
|
.theme-dark .arco-card,
|
||||||
|
.theme-dark .arco-tabs,
|
||||||
|
.theme-dark .arco-input,
|
||||||
|
.theme-dark .arco-textarea,
|
||||||
|
.theme-dark .arco-btn-secondary {
|
||||||
|
background: var(--senlin-panel);
|
||||||
|
color: var(--senlin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .arco-card,
|
||||||
|
.theme-dark .arco-input,
|
||||||
|
.theme-dark .arco-textarea,
|
||||||
|
.theme-dark .arco-btn,
|
||||||
|
.theme-dark .arco-tabs-header-title {
|
||||||
|
border-color: var(--senlin-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .arco-typography,
|
||||||
|
.theme-dark .arco-typography p,
|
||||||
|
.theme-dark .arco-typography h1,
|
||||||
|
.theme-dark .arco-typography h2,
|
||||||
|
.theme-dark .arco-typography h3,
|
||||||
|
.theme-dark .arco-typography h4,
|
||||||
|
.theme-dark .arco-typography h5,
|
||||||
|
.theme-dark .arco-typography h6 {
|
||||||
|
color: var(--senlin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark .arco-typography-secondary {
|
||||||
|
color: var(--senlin-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-screen {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 40px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 0 0, rgba(22, 93, 255, 0.08), transparent 28%),
|
||||||
|
linear-gradient(180deg, #f3f7ff 0%, #f7f8fa 42%, #ffffff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card.arco-card {
|
||||||
|
width: min(1180px, 96vw);
|
||||||
|
min-height: 430px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-color: var(--senlin-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 12px 36px rgba(29, 33, 41, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card .arco-card-body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1.1fr 1fr;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand-panel,
|
||||||
|
.login-form-panel,
|
||||||
|
.login-value-panel {
|
||||||
|
min-height: 430px;
|
||||||
|
padding: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand-panel,
|
||||||
|
.login-form-panel {
|
||||||
|
border-right: 1px solid var(--senlin-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand-panel {
|
||||||
|
display: grid;
|
||||||
|
align-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-lockup {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-lockup.large {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-symbol {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--senlin-primary);
|
||||||
|
color: white;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-lockup.large .brand-symbol {
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-slogan {
|
||||||
|
margin-top: 28px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer-links {
|
||||||
|
align-self: end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer-links .arco-typography {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form-panel {
|
||||||
|
display: grid;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-dot {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--senlin-primary);
|
||||||
|
box-shadow: inset 0 0 0 3px white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #7be188;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f0fff4;
|
||||||
|
color: #168a2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-title {
|
||||||
|
display: block;
|
||||||
|
color: #168a2f;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-value-panel {
|
||||||
|
display: grid;
|
||||||
|
align-content: center;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-point {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 42px 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-point > span {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--senlin-soft-blue);
|
||||||
|
color: var(--senlin-primary);
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-title {
|
||||||
|
display: block;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-shell {
|
||||||
|
height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 58px minmax(0, 1fr) 32px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--senlin-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 210px minmax(480px, 680px) 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
height: 58px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom: 1px solid var(--senlin-border);
|
||||||
|
background: var(--senlin-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-search {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 76px;
|
||||||
|
height: 42px;
|
||||||
|
border: 1px solid var(--senlin-primary);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-search .arco-input-inner-wrapper {
|
||||||
|
height: 40px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: var(--senlin-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-search .arco-btn {
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workbench-main {
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-rail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 14px 10px;
|
||||||
|
border-right: 1px solid var(--senlin-border);
|
||||||
|
background: var(--senlin-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-button,
|
||||||
|
.create-project {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-project {
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-button {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 1px solid var(--senlin-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--project-color) 9%, white);
|
||||||
|
color: var(--project-color);
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-button.active {
|
||||||
|
border-color: var(--project-color);
|
||||||
|
background: var(--project-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-badge .arco-badge-number {
|
||||||
|
box-shadow: 0 0 0 2px var(--senlin-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-badge.urgent .arco-badge-number {
|
||||||
|
background: #f53f3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-sidebar {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 16px;
|
||||||
|
border-right: 1px solid var(--senlin-border);
|
||||||
|
background: var(--senlin-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title h5 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--senlin-text);
|
||||||
|
padding: 7px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-button.active {
|
||||||
|
background: var(--senlin-soft-blue);
|
||||||
|
color: var(--senlin-primary);
|
||||||
|
box-shadow: inset 3px 0 0 var(--senlin-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-left {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-left .arco-typography {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-sessions,
|
||||||
|
.tag-cloud {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-sessions button {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
padding: 6px 8px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-sessions button:hover {
|
||||||
|
background: var(--senlin-soft-blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--senlin-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-page {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--senlin-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-head h4 {
|
||||||
|
margin: 3px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-row {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card.arco-card {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card .arco-card-body {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card h3 {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-delta {
|
||||||
|
color: #00b42a;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-section.arco-card {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queue-section .arco-card-body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h6 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-row {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 1.25fr) minmax(220px, 1fr) 82px 70px 82px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--senlin-border);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--senlin-text);
|
||||||
|
padding: 9px 0;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-row:first-of-type {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-row:hover {
|
||||||
|
background: color-mix(in srgb, var(--senlin-primary) 5%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-row span,
|
||||||
|
.data-row time {
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--senlin-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-row .row-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--senlin-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-row {
|
||||||
|
grid-template-columns: minmax(260px, 1fr) 82px 100px 100px 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-row {
|
||||||
|
grid-template-columns: minmax(260px, 1fr) minmax(260px, 1.2fr) 80px 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-row {
|
||||||
|
grid-template-columns: minmax(260px, 1fr) 80px 160px 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
border-left: 1px solid var(--senlin-border);
|
||||||
|
background: var(--senlin-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector .arco-tabs-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discussion-panel {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discussion-list {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discussion-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 30px 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discussion-item p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: var(--senlin-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.discussion-name {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-box {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 12px;
|
||||||
|
background: var(--senlin-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-list,
|
||||||
|
.more-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid var(--senlin-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusbar {
|
||||||
|
height: 32px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(310px, 0.9fr) minmax(480px, 1.2fr) minmax(320px, 0.9fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
background: var(--senlin-status);
|
||||||
|
color: rgba(255, 255, 255, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusbar .arco-typography,
|
||||||
|
.statusbar .arco-btn-text {
|
||||||
|
color: rgba(255, 255, 255, 0.86);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-user,
|
||||||
|
.status-tasks,
|
||||||
|
.status-system {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tasks {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tasks span,
|
||||||
|
.status-system span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-system {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-online {
|
||||||
|
display: block;
|
||||||
|
color: #7be188 !important;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-system b {
|
||||||
|
color: #7be188;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.workbench-shell {
|
||||||
|
min-width: 1180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card .arco-card-body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-brand-panel,
|
||||||
|
.login-form-panel {
|
||||||
|
min-height: auto;
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 1px solid var(--senlin-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-value-panel {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
569
prototypes/senlinai-acro-react/src/App.tsx
Normal file
569
prototypes/senlinai-acro-react/src/App.tsx
Normal file
@@ -0,0 +1,569 @@
|
|||||||
|
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={72}>
|
||||||
|
<Button className="dashboard-button" icon={<IconDashboard />} />
|
||||||
|
<Space 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" icon={<IconPlus />}>新建项目</Button>
|
||||||
|
</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
|
||||||
30
prototypes/senlinai-acro-react/src/index.css
Normal file
30
prototypes/senlinai-acro-react/src/index.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
ui-sans-serif,
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
sans-serif;
|
||||||
|
background: #f7f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
10
prototypes/senlinai-acro-react/src/main.tsx
Normal file
10
prototypes/senlinai-acro-react/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
26
prototypes/senlinai-acro-react/tsconfig.app.json
Normal file
26
prototypes/senlinai-acro-react/tsconfig.app.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "es2023",
|
||||||
|
"lib": ["ES2023", "DOM"],
|
||||||
|
"module": "esnext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"allowArbitraryExtensions": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
prototypes/senlinai-acro-react/tsconfig.json
Normal file
7
prototypes/senlinai-acro-react/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
23
prototypes/senlinai-acro-react/tsconfig.node.json
Normal file
23
prototypes/senlinai-acro-react/tsconfig.node.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "es2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"module": "nodenext",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
prototypes/senlinai-acro-react/vite.config.ts
Normal file
7
prototypes/senlinai-acro-react/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user