299 lines
10 KiB
JavaScript
299 lines
10 KiB
JavaScript
#!/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 XLL_MODULES = [
|
||
{ slug: 'oneos-web-safety-training', title: '业务-司机安全培训', skipCodegen: true },
|
||
];
|
||
|
||
const MODULES = [
|
||
{ slug: 'oneos-web-workbench', title: '工作台', files: ['工作台.jsx'] },
|
||
{ slug: 'oneos-web-login', 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 '../../common/oneosWebLegacy/legacyGlobals';
|
||
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 toSidebarItem(module) {
|
||
return {
|
||
id: `item:prototypes:${module.slug}`,
|
||
kind: 'item',
|
||
title: module.title,
|
||
itemKey: `prototypes/${module.slug}`,
|
||
};
|
||
}
|
||
|
||
function updateSidebar(webModules, xllModules) {
|
||
const sidebar = JSON.parse(fs.readFileSync(sidebarPath, 'utf8'));
|
||
const prefix = 'item:prototypes:oneos-web-';
|
||
const existing = (sidebar.prototypes ?? []).filter(
|
||
(item) =>
|
||
item.id !== 'folder-prototypes-oneos-web'
|
||
&& item.id !== 'folder-prototypes-xll-miniapp'
|
||
&& item.itemKey !== 'prototypes/contract-template-management'
|
||
&& !String(item.id).startsWith(prefix)
|
||
&& !String(item.itemKey).startsWith('prototypes/oneos-web-'),
|
||
);
|
||
|
||
const xllFolder = {
|
||
id: 'folder-prototypes-xll-miniapp',
|
||
kind: 'folder',
|
||
title: '小羚羚小程序',
|
||
children: xllModules.map(toSidebarItem),
|
||
};
|
||
|
||
const oneosFolder = {
|
||
id: 'folder-prototypes-oneos-web',
|
||
kind: 'folder',
|
||
title: 'ONE-OS Web 端',
|
||
defaultExpanded: true,
|
||
children: webModules.map(toSidebarItem),
|
||
};
|
||
|
||
sidebar.prototypes = [...existing, xllFolder, 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);
|
||
}
|
||
|
||
const allModules = [...XLL_MODULES, ...MODULES];
|
||
|
||
for (const module of allModules) {
|
||
if (module.skipCodegen) {
|
||
console.log(`[skip codegen] ${module.slug}(入口由项目内手写维护)`);
|
||
continue;
|
||
}
|
||
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, XLL_MODULES);
|
||
console.log('已更新 .axhub/make/sidebar-tree.json');
|
||
}
|
||
|
||
main();
|