迭代 ONE-OS 多原型:合同模板重构与租赁合同列表/表单增强
- 合同模板管理:模板数据层、章节编辑与品牌车型选择器 - 租赁合同管理:列表子表、交车安排、里程汇总及新建/编辑表单优化 - 同步各原型 annotation-source 与 PRD;新增项目导航同步脚本 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
188
scripts/sync-project-prototype-directory.mjs
Normal file
188
scripts/sync-project-prototype-directory.mjs
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* 将 .axhub/make/sidebar-tree.json 左侧菜单同步到各原型 annotation-source.json 的「原型目录」。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/sync-project-prototype-directory.mjs
|
||||
* node scripts/sync-project-prototype-directory.mjs --prototype vehicle-management
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const NAV_FOLDER_ID = 'oneos-project-nav';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { prototype: '', projectRoot: '' };
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--prototype' && argv[i + 1]) {
|
||||
args.prototype = argv[++i].trim();
|
||||
} else if (arg === '--project-root' && argv[i + 1]) {
|
||||
args.projectRoot = path.resolve(argv[++i].trim());
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
}
|
||||
|
||||
function writeJson(filePath, value) {
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
function prototypeIdFromItemKey(itemKey) {
|
||||
const normalized = String(itemKey || '').trim().replace(/^\/+/u, '');
|
||||
const match = normalized.match(/^prototypes\/(.+)$/u);
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
|
||||
function navLinkId(prototypeId) {
|
||||
return `nav-link-${prototypeId.replace(/[^a-zA-Z0-9]+/gu, '-').replace(/^-+|-+$/gu, '').toLowerCase()}`;
|
||||
}
|
||||
|
||||
function createLinkNode(sidebarItem, currentPrototypeId) {
|
||||
const prototypeId = prototypeIdFromItemKey(sidebarItem.itemKey);
|
||||
if (!prototypeId) return null;
|
||||
const isCurrent = prototypeId === currentPrototypeId;
|
||||
return {
|
||||
type: 'link',
|
||||
id: navLinkId(prototypeId),
|
||||
title: isCurrent ? `${sidebarItem.title}(当前)` : sidebarItem.title,
|
||||
href: `/prototypes/${prototypeId}`,
|
||||
target: 'self',
|
||||
};
|
||||
}
|
||||
|
||||
function sidebarItemsToDirectoryChildren(sidebarItems, currentPrototypeId) {
|
||||
const children = [];
|
||||
for (const item of sidebarItems || []) {
|
||||
if (item?.kind === 'item') {
|
||||
const link = createLinkNode(item, currentPrototypeId);
|
||||
if (link) children.push(link);
|
||||
continue;
|
||||
}
|
||||
if (item?.kind !== 'folder' || !Array.isArray(item.children)) {
|
||||
continue;
|
||||
}
|
||||
const folderChildren = item.children
|
||||
.filter((child) => child?.kind === 'item')
|
||||
.map((child) => createLinkNode(child, currentPrototypeId))
|
||||
.filter(Boolean);
|
||||
if (!folderChildren.length) continue;
|
||||
children.push({
|
||||
type: 'folder',
|
||||
id: item.id ? `nav-${item.id}` : `nav-folder-${children.length + 1}`,
|
||||
title: item.title || '分组',
|
||||
defaultExpanded: item.defaultExpanded !== false,
|
||||
children: folderChildren,
|
||||
});
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
function loadSidebarPrototypes(projectRoot) {
|
||||
const sidebarPath = path.join(projectRoot, '.axhub/make/sidebar-tree.json');
|
||||
if (!fs.existsSync(sidebarPath)) {
|
||||
throw new Error(`缺少侧边栏配置:${sidebarPath}`);
|
||||
}
|
||||
const sidebar = readJson(sidebarPath);
|
||||
return Array.isArray(sidebar.prototypes) ? sidebar.prototypes : [];
|
||||
}
|
||||
|
||||
function listPrototypeIdsWithAnnotation(projectRoot) {
|
||||
const prototypesRoot = path.join(projectRoot, 'src/prototypes');
|
||||
if (!fs.existsSync(prototypesRoot)) return [];
|
||||
return fs.readdirSync(prototypesRoot, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory()
|
||||
&& fs.existsSync(path.join(prototypesRoot, entry.name, 'annotation-source.json')))
|
||||
.map((entry) => entry.name)
|
||||
.sort((a, b) => a.localeCompare(b, 'zh-CN'));
|
||||
}
|
||||
|
||||
function fixPublishedLinks(nodes, prototypeId) {
|
||||
const walk = (items) => {
|
||||
for (const node of items || []) {
|
||||
if (node?.type === 'link' && typeof node.href === 'string') {
|
||||
const href = node.href.trim();
|
||||
const selfMatch = href.match(new RegExp(`^/prototypes/${prototypeId.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')}(?:[#?].*)?$`, 'u'));
|
||||
if (selfMatch) {
|
||||
node.href = './index.html';
|
||||
node.target = node.target || 'self';
|
||||
} else {
|
||||
const otherMatch = href.match(/^\/prototypes\/([^/?#]+)(?:[#?].*)?$/u);
|
||||
if (otherMatch) {
|
||||
node.href = `../${otherMatch[1]}/index.html`;
|
||||
node.target = node.target || 'self';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node?.type === 'folder' && Array.isArray(node.children)) {
|
||||
walk(node.children);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(nodes);
|
||||
}
|
||||
|
||||
function syncPrototypeDirectory(prototypeId, sidebarItems, projectRoot) {
|
||||
const annotationPath = path.join(projectRoot, 'src/prototypes', prototypeId, 'annotation-source.json');
|
||||
if (!fs.existsSync(annotationPath)) {
|
||||
console.log(`[skip] ${prototypeId}: 无 annotation-source.json`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const annotation = readJson(annotationPath);
|
||||
const navChildren = sidebarItemsToDirectoryChildren(sidebarItems, prototypeId);
|
||||
const navFolder = {
|
||||
type: 'folder',
|
||||
id: NAV_FOLDER_ID,
|
||||
title: 'ONE-OS 原型导航',
|
||||
defaultExpanded: true,
|
||||
children: navChildren,
|
||||
};
|
||||
|
||||
const existingNodes = Array.isArray(annotation.directory?.nodes) ? annotation.directory.nodes : [];
|
||||
const preservedNodes = existingNodes.filter((node) => node?.id !== NAV_FOLDER_ID);
|
||||
fixPublishedLinks(preservedNodes, prototypeId);
|
||||
|
||||
annotation.directory = {
|
||||
nodes: [navFolder, ...preservedNodes],
|
||||
};
|
||||
annotation.data = annotation.data || {};
|
||||
annotation.data.updatedAt = Date.now();
|
||||
|
||||
writeJson(annotationPath, annotation);
|
||||
console.log(`[sync] ${prototypeId}: 已写入 ${navChildren.length} 个顶级目录项(含分组)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const projectRoot = args.projectRoot || path.resolve(__dirname, '..');
|
||||
const sidebarItems = loadSidebarPrototypes(projectRoot);
|
||||
const targets = args.prototype
|
||||
? [args.prototype]
|
||||
: listPrototypeIdsWithAnnotation(projectRoot);
|
||||
|
||||
if (!sidebarItems.length) {
|
||||
console.error('sidebar-tree.json 中未找到 prototypes 菜单。');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!targets.length) {
|
||||
console.log('未找到带 annotation-source.json 的原型。');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let changed = 0;
|
||||
for (const prototypeId of targets) {
|
||||
if (syncPrototypeDirectory(prototypeId, sidebarItems, projectRoot)) {
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
console.log(`完成:处理 ${targets.length} 个原型,更新 ${changed} 个 annotation-source.json`);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user