Add ONE-OS Web legacy prototypes and sidebar menu folder.
Import desktop web originals into dedicated prototypes with a shared legacy shell, sync script, and a new「ONE-OS Web 端」sidebar section for review alongside existing refactored pages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
270
scripts/sync-oneos-web-prototypes.mjs
Normal file
270
scripts/sync-oneos-web-prototypes.mjs
Normal file
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 从 src/resources/oneos-web-legacy 生成 ONE-OS Web 端原型与侧边栏菜单项。
|
||||
*
|
||||
* 用法:node scripts/sync-oneos-web-prototypes.mjs
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const projectRoot = path.resolve(__dirname, '..');
|
||||
const legacyRoot = path.join(projectRoot, 'src/resources/oneos-web-legacy');
|
||||
const prototypesRoot = path.join(projectRoot, 'src/prototypes');
|
||||
const sidebarPath = path.join(projectRoot, '.axhub/make/sidebar-tree.json');
|
||||
|
||||
const PAGE_ID_RE = /^[a-z0-9-]+$/u;
|
||||
|
||||
const MODULES = [
|
||||
{ slug: 'oneos-web-workbench', title: '工作台', files: ['工作台.jsx'] },
|
||||
{ slug: 'oneos-web-login', title: '登录', files: ['登录.jsx'] },
|
||||
{ slug: 'oneos-web-safety-training', title: '安全培训扫码', files: ['安全培训扫码.jsx'] },
|
||||
{ slug: 'oneos-web-vehicle-asset', title: '车辆管理', files: ['车辆管理.jsx', '车辆管理-查看.jsx'] },
|
||||
{ slug: 'oneos-web-contract-template', title: '合同模板管理', files: ['合同模板管理.jsx'] },
|
||||
{ slug: 'oneos-web-help-center', title: '帮助中心', dir: '帮助中心' },
|
||||
{ slug: 'oneos-web-finance', title: '财务管理', dir: '财务管理', excludeDirs: ['文档'] },
|
||||
{ slug: 'oneos-web-procurement', title: '采购管理', dir: '采购管理' },
|
||||
{ slug: 'oneos-web-lease-contract', title: '车辆租赁合同', dir: '车辆租赁合同' },
|
||||
{ slug: 'oneos-web-h2-station', title: '加氢站管理', dir: '加氢站管理', excludeDirs: ['export-tools', 'AI-加氢站站点信息-complete'] },
|
||||
{ slug: 'oneos-web-data-analysis', title: '数据分析', dir: '数据分析' },
|
||||
{ slug: 'oneos-web-ledger-data', title: '台账数据', dir: '台账数据', excludeDirs: ['docs'] },
|
||||
{ slug: 'oneos-web-business', title: '业务管理', dir: '业务管理', excludeDirs: ['文档', 'export-tools', 'AI-保险采购-complete'] },
|
||||
{ slug: 'oneos-web-ops', title: '运维管理', dir: '运维管理' },
|
||||
{ slug: 'oneos-web-requirements', title: '需求说明', dir: '需求说明', docsOnly: true },
|
||||
];
|
||||
|
||||
function slugifyPageId(relPath) {
|
||||
const stem = relPath.replace(/\.jsx$/u, '').replace(/[\\/]/g, '-');
|
||||
const ascii = stem.replace(/[^\x00-\x7F]/gu, (ch) => `u${ch.charCodeAt(0).toString(16)}`);
|
||||
let id = ascii.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
||||
if (!id || !PAGE_ID_RE.test(id)) {
|
||||
id = `p${Math.abs(hashCode(relPath)).toString(36)}`;
|
||||
}
|
||||
return id.slice(0, 48);
|
||||
}
|
||||
|
||||
function hashCode(value) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
hash = ((hash << 5) - hash) + value.charCodeAt(i);
|
||||
hash |= 0;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
function collectJsxFiles(module) {
|
||||
if (module.docsOnly) {
|
||||
return [];
|
||||
}
|
||||
if (module.files) {
|
||||
return module.files.map((file) => ({
|
||||
rel: file,
|
||||
abs: path.join(legacyRoot, file),
|
||||
title: file.replace(/\.jsx$/u, ''),
|
||||
}));
|
||||
}
|
||||
const baseDir = path.join(legacyRoot, module.dir);
|
||||
const results = [];
|
||||
const walk = (dir, prefix = '') => {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name.startsWith('.')) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
const rel = path.posix.join(module.dir, prefix, entry.name).replace(/\\/g, '/');
|
||||
if (entry.isDirectory()) {
|
||||
if (module.excludeDirs?.includes(entry.name)) continue;
|
||||
walk(full, prefix ? `${prefix}/${entry.name}` : entry.name);
|
||||
continue;
|
||||
}
|
||||
if (!entry.name.endsWith('.jsx')) continue;
|
||||
results.push({
|
||||
rel,
|
||||
abs: full,
|
||||
title: entry.name.replace(/\.jsx$/u, ''),
|
||||
});
|
||||
}
|
||||
};
|
||||
walk(baseDir);
|
||||
return results.sort((a, b) => a.rel.localeCompare(b.rel, 'zh-CN'));
|
||||
}
|
||||
|
||||
function prepareLegacyJsx(sourcePath, targetPath) {
|
||||
const raw = fs.readFileSync(sourcePath, 'utf8');
|
||||
let content = raw;
|
||||
if (!/export\s+default\s+Component/u.test(content)) {
|
||||
content = `${content.trim()}\n\nexport default Component;\n`;
|
||||
}
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
fs.writeFileSync(targetPath, content, 'utf8');
|
||||
}
|
||||
|
||||
function writeRequirementsPrototype(module) {
|
||||
const protoDir = path.join(prototypesRoot, module.slug);
|
||||
fs.mkdirSync(protoDir, { recursive: true });
|
||||
const indexTsx = `/**
|
||||
* @name ${module.title}
|
||||
* 自 ONE-OS web端/需求说明 归档
|
||||
*/
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Card, Col, Input, List, Row, Typography } from 'antd';
|
||||
|
||||
const DOC_MAP = import.meta.glob('../../resources/oneos-web-legacy/需求说明/**/*', {
|
||||
query: '?raw',
|
||||
import: 'default',
|
||||
eager: true,
|
||||
});
|
||||
|
||||
const FILES = Object.keys(DOC_MAP)
|
||||
.map((key) => key.replace('../../resources/oneos-web-legacy/需求说明/', ''))
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.localeCompare(b, 'zh-CN'))
|
||||
.map((rel) => ({ rel, title: rel }));
|
||||
|
||||
export default function OneosWebRequirementsPage() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [active, setActive] = useState(FILES[0]?.rel ?? '');
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim();
|
||||
if (!q) return FILES;
|
||||
return FILES.filter((item) => item.title.includes(q));
|
||||
}, [query]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (!active) return '';
|
||||
const key = \`../../resources/oneos-web-legacy/需求说明/\${active}\`;
|
||||
return String(DOC_MAP[key] ?? '无法加载文档');
|
||||
}, [active]);
|
||||
|
||||
return (
|
||||
<Row gutter={16} style={{ padding: 16, minHeight: '100vh', background: '#f5f7fa' }}>
|
||||
<Col span={7}>
|
||||
<Card title="${module.title}" extra={<Input.Search allowClear placeholder="搜索文档" onSearch={setQuery} onChange={(e) => setQuery(e.target.value)} style={{ width: 160 }} />}>
|
||||
<List
|
||||
size="small"
|
||||
dataSource={filtered}
|
||||
renderItem={(item) => (
|
||||
<List.Item style={{ cursor: 'pointer', background: item.rel === active ? '#e8f3ff' : undefined }} onClick={() => setActive(item.rel)}>
|
||||
{item.title}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={17}>
|
||||
<Card>
|
||||
<Typography.Paragraph style={{ whiteSpace: 'pre-wrap', marginBottom: 0 }}>{content}</Typography.Paragraph>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
`;
|
||||
fs.writeFileSync(path.join(protoDir, 'index.tsx'), indexTsx, 'utf8');
|
||||
}
|
||||
|
||||
function writePrototype(module, pages) {
|
||||
const protoDir = path.join(prototypesRoot, module.slug);
|
||||
const pagesDir = path.join(protoDir, 'pages');
|
||||
fs.mkdirSync(pagesDir, { recursive: true });
|
||||
|
||||
const pageEntries = [];
|
||||
const importLines = [];
|
||||
const usedIds = new Set();
|
||||
|
||||
pages.forEach((page, index) => {
|
||||
const fileName = `${String(index + 1).padStart(2, '0')}-${path.basename(page.rel)}`;
|
||||
const target = path.join(pagesDir, fileName);
|
||||
prepareLegacyJsx(page.abs, target);
|
||||
const importName = `Page${index + 1}`;
|
||||
importLines.push(`import ${importName} from './pages/${fileName}';`);
|
||||
let id = slugifyPageId(page.rel);
|
||||
while (usedIds.has(id)) id = `${id}-${index + 1}`;
|
||||
usedIds.add(id);
|
||||
pageEntries.push({ id, title: page.title, importName });
|
||||
});
|
||||
|
||||
const pagesArray = pageEntries.map((p) => ` { id: '${p.id}', title: '${p.title.replace(/'/g, "\\'")}', component: ${p.importName} },`).join('\n');
|
||||
|
||||
const indexTsx = `/**
|
||||
* @name ${module.title}
|
||||
* 自 ONE-OS web端 原稿复刻(${pages.length} 个页面)
|
||||
*/
|
||||
import React from 'react';
|
||||
${importLines.join('\n')}
|
||||
import { OneosWebLegacyShell } from '../../common/oneosWebLegacy/OneosWebLegacyShell';
|
||||
|
||||
const pages = [
|
||||
${pagesArray}
|
||||
];
|
||||
|
||||
export default function ${toComponentName(module.slug)}() {
|
||||
return (
|
||||
<OneosWebLegacyShell
|
||||
moduleTitle="${module.title}"
|
||||
pages={pages}
|
||||
defaultPageId="${pageEntries[0]?.id ?? ''}"
|
||||
/>
|
||||
);
|
||||
}
|
||||
`;
|
||||
fs.writeFileSync(path.join(protoDir, 'index.tsx'), indexTsx, 'utf8');
|
||||
}
|
||||
|
||||
function toComponentName(slug) {
|
||||
return slug
|
||||
.split('-')
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join('');
|
||||
}
|
||||
|
||||
function updateSidebar(modules) {
|
||||
const sidebar = JSON.parse(fs.readFileSync(sidebarPath, 'utf8'));
|
||||
const prefix = 'item:prototypes:oneos-web-';
|
||||
const existing = (sidebar.prototypes ?? []).filter((item) => !String(item.id).startsWith(prefix) && !String(item.itemKey).startsWith('prototypes/oneos-web-'));
|
||||
|
||||
const oneosFolder = {
|
||||
id: 'folder-prototypes-oneos-web',
|
||||
kind: 'folder',
|
||||
title: 'ONE-OS Web 端',
|
||||
defaultExpanded: true,
|
||||
children: modules.map((module) => ({
|
||||
id: `item:prototypes:${module.slug}`,
|
||||
kind: 'item',
|
||||
title: module.title,
|
||||
itemKey: `prototypes/${module.slug}`,
|
||||
})),
|
||||
};
|
||||
|
||||
sidebar.prototypes = [...existing, oneosFolder];
|
||||
sidebar.updatedAt = new Date().toISOString();
|
||||
fs.writeFileSync(sidebarPath, `${JSON.stringify(sidebar, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(legacyRoot)) {
|
||||
console.error('缺少 src/resources/oneos-web-legacy,请先复制 ONE-OS web端 资料。');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
for (const module of MODULES) {
|
||||
if (module.docsOnly) {
|
||||
writeRequirementsPrototype(module);
|
||||
console.log(`[ok] ${module.slug} (需求说明文档浏览)`);
|
||||
continue;
|
||||
}
|
||||
const pages = collectJsxFiles(module);
|
||||
if (!pages.length) {
|
||||
console.warn(`[skip] ${module.slug}: 未找到 jsx 页面`);
|
||||
continue;
|
||||
}
|
||||
writePrototype(module, pages);
|
||||
console.log(`[ok] ${module.slug} (${pages.length} pages)`);
|
||||
}
|
||||
|
||||
updateSidebar(MODULES);
|
||||
console.log('已更新 .axhub/make/sidebar-tree.json');
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user