迭代 ONE-OS 多原型:统一标注壳与操作规范,租赁明细/合同/提车应收款增强,新增任务工单,同步导航与合包页面。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
339
scripts/enrich-prototype-registry-from-git.mjs
Normal file
339
scripts/enrich-prototype-registry-from-git.mjs
Normal file
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* 从 Git 提交历史生成/补全原型导航注册表中的详细变更日志。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/enrich-prototype-registry-from-git.mjs
|
||||
* node scripts/enrich-prototype-registry-from-git.mjs --prototype vehicle-h2-fee-ledger
|
||||
*/
|
||||
import { execSync } from 'node:child_process';
|
||||
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 REGISTRY_PATH = path.join(projectRoot, 'src/prototypes/oneos-prototype-nav/prototype-registry.json');
|
||||
const TRACKED_EXTENSIONS = new Set(['.tsx', '.ts', '.jsx', '.js', '.css', '.json', '.md', '.html']);
|
||||
const IGNORED_FILE_NAMES = new Set(['prototype-registry.json', 'nav-menu.json']);
|
||||
const MAX_CHANGELOG = 30;
|
||||
const MAX_RECENT = 40;
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { prototype: '' };
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
if (argv[i] === '--prototype' && argv[i + 1]) args.prototype = argv[++i].trim();
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function shouldTrackFile(relativePath) {
|
||||
const base = path.basename(relativePath);
|
||||
if (IGNORED_FILE_NAMES.has(base)) return false;
|
||||
if (relativePath === 'annotation-source.json') return false;
|
||||
return TRACKED_EXTENSIONS.has(path.extname(relativePath).toLowerCase());
|
||||
}
|
||||
|
||||
function normalizePrototypePath(filePath, prototypeId) {
|
||||
const normalized = String(filePath || '').replace(/\\/gu, '/').trim();
|
||||
const prefix = `src/prototypes/${prototypeId}/`;
|
||||
if (normalized.startsWith(prefix)) return normalized.slice(prefix.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseCommitBlocks(raw) {
|
||||
const blocks = raw.split(/\n?----\n/u).map((block) => block.trim()).filter(Boolean);
|
||||
const commits = [];
|
||||
for (const block of blocks) {
|
||||
const lines = block.split('\n');
|
||||
const hash = lines[0]?.trim();
|
||||
if (!/^[0-9a-f]{7,40}$/iu.test(hash || '')) continue;
|
||||
const dateLine = lines[1]?.trim();
|
||||
const subject = lines[2]?.trim() || '';
|
||||
if (!dateLine) continue;
|
||||
|
||||
const fileStart = lines.findIndex((line, index) => index > 2 && line.trim() === '');
|
||||
const bodyEnd = fileStart >= 0 ? fileStart : lines.length;
|
||||
const body = lines.slice(3, bodyEnd).join('\n').trim();
|
||||
const files = (fileStart >= 0 ? lines.slice(fileStart + 1) : [])
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
commits.push({ hash, dateLine, subject, body, files });
|
||||
}
|
||||
return commits;
|
||||
}
|
||||
|
||||
function getPrototypeCommits(prototypeId) {
|
||||
const relDir = `src/prototypes/${prototypeId}`;
|
||||
if (!fs.existsSync(path.join(projectRoot, relDir))) return [];
|
||||
let raw = '';
|
||||
try {
|
||||
raw = execSync(
|
||||
`git log --pretty=format:----%n%H%n%ci%n%s%n%b --name-only -- ${relDir}`,
|
||||
{ cwd: projectRoot, encoding: 'utf8', maxBuffer: 20 * 1024 * 1024 },
|
||||
);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return parseCommitBlocks(raw);
|
||||
}
|
||||
|
||||
function formatDateParts(dateLine) {
|
||||
const match = dateLine.match(/^(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})/u);
|
||||
if (!match) {
|
||||
const date = new Date(dateLine);
|
||||
if (Number.isNaN(date.getTime())) return { date: '1970-01-01', time: '00:00', iso: null };
|
||||
return {
|
||||
date: date.toISOString().slice(0, 10),
|
||||
time: `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`,
|
||||
iso: date.toISOString(),
|
||||
};
|
||||
}
|
||||
const [, y, m, d, hh, mm] = match;
|
||||
return {
|
||||
date: `${y}-${m}-${d}`,
|
||||
time: `${hh}:${mm}`,
|
||||
iso: new Date(`${y}-${m}-${d}T${hh}:${mm}:00+08:00`).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function extractBodyLines(body) {
|
||||
return body
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && !/^Co-authored-by:/iu.test(line))
|
||||
.map((line) => line.replace(/^[-*•]\s*/u, '').trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function strictKeywords(title, prototypeId) {
|
||||
const keywords = new Set([title, prototypeId]);
|
||||
for (const part of String(title || '').split(/[·、/()()\s「」]+/u)) {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed.length >= 3) keywords.add(trimmed);
|
||||
}
|
||||
if (title.includes('氢费明细')) keywords.add('氢费明细');
|
||||
else if (title.includes('氢费')) keywords.add('氢费');
|
||||
if (title.includes('原型导航')) keywords.add('原型导航');
|
||||
if (title.includes('合同模板')) keywords.add('合同模板');
|
||||
if (title.includes('租赁合同')) keywords.add('租赁合同');
|
||||
if (title.includes('租赁业务')) keywords.add('租赁业务');
|
||||
if (title.includes('收款记录')) keywords.add('收款记录');
|
||||
if (title.includes('维修保养')) keywords.add('维修保养');
|
||||
if (title.includes('还车应结')) keywords.add('还车应结');
|
||||
return [...keywords];
|
||||
}
|
||||
|
||||
function lineMatchesPrototype(line, title, prototypeId) {
|
||||
if (!line) return false;
|
||||
if (line.includes(title)) return true;
|
||||
if (line.toLowerCase().includes(prototypeId.toLowerCase())) return true;
|
||||
return strictKeywords(title, prototypeId).some((keyword) => (
|
||||
keyword.length >= 3 && line.includes(keyword)
|
||||
));
|
||||
}
|
||||
|
||||
function commitIsRelevant(commit, title, prototypeId, changedFiles, allChangedFiles, bodyLines) {
|
||||
if (changedFiles.length > 0) return true;
|
||||
|
||||
const onlyAnnotation = allChangedFiles.length > 0
|
||||
&& allChangedFiles.every((file) => file === 'annotation-source.json');
|
||||
const textMatches = [commit.subject, ...bodyLines].some((line) => (
|
||||
lineMatchesPrototype(line, title, prototypeId)
|
||||
));
|
||||
|
||||
if (textMatches && onlyAnnotation) return true;
|
||||
if (textMatches) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function summarizeFileChanges(files) {
|
||||
const notes = [];
|
||||
const prd = files.filter((f) => f.includes('.spec/') && f.endsWith('.md'));
|
||||
const comments = files.filter((f) => f.includes('prototype-comments') || f.includes('annotation-source'));
|
||||
const styles = files.filter((f) => f.endsWith('.css'));
|
||||
const logic = files.filter((f) => /\.(tsx|jsx|ts|js)$/u.test(f) && !f.endsWith('.css'));
|
||||
const data = files.filter((f) => f.includes('/data/') || f.endsWith('.json'));
|
||||
|
||||
if (logic.length) {
|
||||
const names = [...new Set(logic.map((f) => path.basename(f)))].slice(0, 4);
|
||||
notes.push(`页面逻辑与交互:${names.join('、')}${logic.length > names.length ? ' 等' : ''}`);
|
||||
}
|
||||
if (styles.length) notes.push(`样式与布局:${[...new Set(styles.map((f) => path.basename(f)))].join('、')}`);
|
||||
if (prd.length) notes.push(`需求说明与 PRD:${prd.map((f) => path.basename(f)).join('、')}`);
|
||||
if (comments.length) notes.push('原型标注目录与批注说明同步');
|
||||
if (data.length) notes.push(`演示数据与配置:${data.map((f) => path.basename(f)).join('、')}`);
|
||||
return notes;
|
||||
}
|
||||
|
||||
function buildSummary(subject, prototypeId, title, bodyLines, changedFiles, onlyAnnotation) {
|
||||
const matched = bodyLines.filter((line) => lineMatchesPrototype(line, title, prototypeId));
|
||||
if (matched.length === 1) return matched[0];
|
||||
if (matched.length > 1) return matched[0];
|
||||
|
||||
const fragments = splitSubjectFragments(subject, prototypeId, title);
|
||||
if (fragments.length === 1) return fragments[0];
|
||||
if (fragments.length > 1) return fragments[0];
|
||||
|
||||
if (onlyAnnotation && changedFiles.length === 0) return '原型标注目录同步';
|
||||
if (changedFiles.length) return summarizeFileChanges(changedFiles)[0] || subject;
|
||||
if (lineMatchesPrototype(subject, title, prototypeId)) return subject;
|
||||
|
||||
return subject;
|
||||
}
|
||||
|
||||
function dedupeDetails(summary, details) {
|
||||
return [...new Set(details)].filter((line) => (
|
||||
line
|
||||
&& line !== summary
|
||||
&& !summary.includes(line)
|
||||
&& line.length > 2
|
||||
));
|
||||
}
|
||||
|
||||
function splitSubjectFragments(subject, prototypeId, title) {
|
||||
const segments = subject
|
||||
.split(/[;;]/u)
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
const matched = segments.filter((segment) => lineMatchesPrototype(segment, title, prototypeId));
|
||||
if (!matched.length) return [];
|
||||
|
||||
const fragments = [];
|
||||
for (const segment of matched) {
|
||||
const colonIndex = segment.indexOf(':');
|
||||
const tail = colonIndex >= 0 ? segment.slice(colonIndex + 1) : segment;
|
||||
tail.split(/[、,,]/u)
|
||||
.map((part) => part.trim())
|
||||
.filter((part) => part.length >= 4)
|
||||
.forEach((part) => fragments.push(part));
|
||||
}
|
||||
return [...new Set(fragments)];
|
||||
}
|
||||
|
||||
function buildDetails(subject, bodyLines, prototypeId, title, changedFiles) {
|
||||
const details = [];
|
||||
const matched = bodyLines.filter((line) => lineMatchesPrototype(line, title, prototypeId));
|
||||
details.push(...matched);
|
||||
|
||||
for (const fragment of splitSubjectFragments(subject, prototypeId, title)) {
|
||||
if (!details.includes(fragment) && fragment !== subject) details.push(fragment);
|
||||
}
|
||||
|
||||
for (const note of summarizeFileChanges(changedFiles)) {
|
||||
if (!details.includes(note)) details.push(note);
|
||||
}
|
||||
|
||||
if (!details.length && changedFiles.length) {
|
||||
details.push(`变更 ${changedFiles.length} 个文件`);
|
||||
}
|
||||
|
||||
return details.filter((line) => line !== subject);
|
||||
}
|
||||
|
||||
function bumpVersion(indexFromOldest) {
|
||||
return `v1.${indexFromOldest}`;
|
||||
}
|
||||
|
||||
function buildChangelogFromGit(prototypeId, title) {
|
||||
const commits = getPrototypeCommits(prototypeId);
|
||||
if (!commits.length) return [];
|
||||
|
||||
const chronological = [...commits].reverse();
|
||||
const entries = [];
|
||||
|
||||
for (const commit of chronological) {
|
||||
const { date, time } = formatDateParts(commit.dateLine);
|
||||
const bodyLines = extractBodyLines(commit.body);
|
||||
const changedFiles = commit.files
|
||||
.map((file) => normalizePrototypePath(file, prototypeId))
|
||||
.filter((file) => file && shouldTrackFile(file));
|
||||
const allChangedFiles = commit.files
|
||||
.map((file) => normalizePrototypePath(file, prototypeId))
|
||||
.filter(Boolean);
|
||||
const onlyAnnotation = allChangedFiles.length > 0
|
||||
&& allChangedFiles.every((file) => file === 'annotation-source.json');
|
||||
|
||||
if (!commitIsRelevant(commit, title, prototypeId, changedFiles, allChangedFiles, bodyLines)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const summary = buildSummary(commit.subject, prototypeId, title, bodyLines, changedFiles, onlyAnnotation);
|
||||
const details = dedupeDetails(
|
||||
summary,
|
||||
buildDetails(commit.subject, bodyLines, prototypeId, title, changedFiles),
|
||||
);
|
||||
|
||||
entries.push({
|
||||
version: '',
|
||||
date,
|
||||
time,
|
||||
summary,
|
||||
details,
|
||||
commit: commit.hash.slice(0, 7),
|
||||
files: [...new Set(changedFiles)].slice(0, 12),
|
||||
});
|
||||
}
|
||||
|
||||
return entries.reverse().map((entry, index, list) => ({
|
||||
...entry,
|
||||
version: bumpVersion(list.length - index),
|
||||
}));
|
||||
}
|
||||
|
||||
function rebuildRecentUpdates(registry) {
|
||||
const rows = [];
|
||||
for (const [prototypeId, record] of Object.entries(registry.prototypes || {})) {
|
||||
const latest = record.changelog?.[0];
|
||||
if (!latest) continue;
|
||||
rows.push({
|
||||
prototypeId,
|
||||
title: record.title || prototypeId,
|
||||
version: latest.version,
|
||||
date: latest.date,
|
||||
time: latest.time,
|
||||
summary: latest.summary,
|
||||
details: latest.details || [],
|
||||
files: latest.files || [],
|
||||
commit: latest.commit,
|
||||
_sort: `${latest.date}T${latest.time}`,
|
||||
});
|
||||
}
|
||||
rows.sort((a, b) => b._sort.localeCompare(a._sort));
|
||||
return rows.slice(0, MAX_RECENT).map(({ _sort, ...row }) => row);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const registry = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
|
||||
const prototypeIds = args.prototype
|
||||
? [args.prototype]
|
||||
: Object.keys(registry.prototypes || {});
|
||||
|
||||
let updated = 0;
|
||||
for (const prototypeId of prototypeIds) {
|
||||
const record = registry.prototypes[prototypeId];
|
||||
if (!record) continue;
|
||||
const changelog = buildChangelogFromGit(prototypeId, record.title || prototypeId);
|
||||
if (!changelog.length) continue;
|
||||
|
||||
record.changelog = changelog.slice(0, MAX_CHANGELOG);
|
||||
record.revision = changelog.length;
|
||||
record.version = changelog[0].version;
|
||||
const latestIso = getPrototypeCommits(prototypeId)[0];
|
||||
if (latestIso) {
|
||||
const { iso } = formatDateParts(latestIso.dateLine);
|
||||
if (iso) record.lastUpdated = iso;
|
||||
}
|
||||
registry.prototypes[prototypeId] = record;
|
||||
updated += 1;
|
||||
console.log(`[git] ${prototypeId}: ${changelog.length} entries → ${changelog[0].version}`);
|
||||
}
|
||||
|
||||
registry.recentUpdates = rebuildRecentUpdates(registry);
|
||||
registry.updatedAt = new Date().toISOString();
|
||||
fs.writeFileSync(REGISTRY_PATH, `${JSON.stringify(registry, null, 2)}\n`, 'utf8');
|
||||
console.log(`完成:补全 ${updated} 个原型的 Git 变更日志`);
|
||||
}
|
||||
|
||||
main();
|
||||
62
scripts/migrate-prototype-annotation-host.mjs
Normal file
62
scripts/migrate-prototype-annotation-host.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 将原型 index.tsx 中的 AnnotationViewer 迁移为 PrototypeAnnotationHost。
|
||||
*/
|
||||
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 prototypesRoot = path.join(projectRoot, 'src/prototypes');
|
||||
const hostImport = "import { PrototypeAnnotationHost } from '../../common/prototype-annotation-host';";
|
||||
|
||||
function listIndexFiles() {
|
||||
return fs.readdirSync(prototypesRoot, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join(prototypesRoot, entry.name, 'index.tsx'))
|
||||
.filter((filePath) => fs.existsSync(filePath))
|
||||
.filter((filePath) => fs.readFileSync(filePath, 'utf8').includes('AnnotationViewer'));
|
||||
}
|
||||
|
||||
function migrateFile(filePath) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
if (!content.includes('AnnotationViewer')) return false;
|
||||
if (content.includes('PrototypeAnnotationHost')) {
|
||||
console.log(`[skip] ${path.relative(projectRoot, filePath)}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
content = content.replace(
|
||||
/import\s*\{([^}]*)\}\s*from\s*'@axhub\/annotation';/u,
|
||||
(match, importsBlock) => {
|
||||
const cleaned = importsBlock
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item && item !== 'AnnotationViewer')
|
||||
.join(',\n ');
|
||||
return `import {\n ${cleaned}\n} from '@axhub/annotation';`;
|
||||
},
|
||||
);
|
||||
|
||||
if (!content.includes(hostImport)) {
|
||||
content = content.replace(
|
||||
/\} from '@axhub\/annotation';/u,
|
||||
`} from '@axhub/annotation';\n${hostImport}`,
|
||||
);
|
||||
}
|
||||
|
||||
content = content
|
||||
.replace(/<AnnotationViewer\b/gu, '<PrototypeAnnotationHost')
|
||||
.replace(/<\/AnnotationViewer>/gu, '</PrototypeAnnotationHost>')
|
||||
.replace(/React\.createElement\(AnnotationViewer,/gu, 'React.createElement(PrototypeAnnotationHost,');
|
||||
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
console.log(`[migrate] ${path.relative(projectRoot, filePath)}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
let changed = 0;
|
||||
for (const filePath of listIndexFiles()) {
|
||||
if (migrateFile(filePath)) changed += 1;
|
||||
}
|
||||
console.log(`完成:迁移 ${changed} 个 index.tsx`);
|
||||
@@ -66,7 +66,7 @@ function toPublishPath(resource) {
|
||||
function resolveObjectPrefix(entry) {
|
||||
const normalized = entry.path.replace(/^\/+|\/+$/gu, '');
|
||||
const prototypeMatch = normalized.match(/^(?:src\/)?prototypes\/(.+)$/u);
|
||||
if (prototypeMatch?.[1]) return prototypeMatch[1];
|
||||
if (prototypeMatch?.[1]) return `prototypes/${prototypeMatch[1]}`;
|
||||
const themeMatch = normalized.match(/^(?:src\/)?themes\/(.+)$/u);
|
||||
if (themeMatch?.[1]) return `themes/${themeMatch[1]}`;
|
||||
return entry.id;
|
||||
|
||||
112
scripts/remove-breadcrumbs-pass2.mjs
Normal file
112
scripts/remove-breadcrumbs-pass2.mjs
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const ROOT = path.resolve('src/prototypes');
|
||||
|
||||
function walk(dir, out = []) {
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
const full = path.join(dir, name);
|
||||
if (fs.statSync(full).isDirectory()) {
|
||||
if (name === 'node_modules' || name === '.spec') continue;
|
||||
walk(full, out);
|
||||
} else if (/\.(jsx|tsx)$/.test(name)) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function cleanup(source) {
|
||||
let s = source;
|
||||
|
||||
// Fix corrupted fragment from partial breadcrumb removal (37-新增后装设备)
|
||||
s = s.replace(
|
||||
/\{ style: styles\.page \},\s*\{ e\.preventDefault\(\); \} \}, '运维管理'\),[\s\S]*?React\.createElement\('span', \{ style: styles\.breadcrumbCurrent \}, '[^']+'\)\s*\),/g,
|
||||
'{ style: styles.page },'
|
||||
);
|
||||
|
||||
// Top bar: breadcrumb spans + req button -> req button only
|
||||
s = s.replace(
|
||||
/React\.createElement\('div', \{ style: \{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 \} \},\s*React\.createElement\('span'[\s\S]*?\),\s*(React\.createElement\(Button, \{ type: 'link'[\s\S]*?'查看需求说明'\)\s*)\),/g,
|
||||
`React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: 16 } }, $1),`
|
||||
);
|
||||
|
||||
// Lease contract rows with broken partial breadcrumb
|
||||
s = s.replace(
|
||||
/React\.createElement\('div', \{ style: \{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 \} \},\s*React\.createElement\('span'[\s\S]*?\),\s*(React\.createElement\('(?:button|span)'[\s\S]*?'查看需求说明'\)[^)]*\))/g,
|
||||
`React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: 16 } }, $1)`
|
||||
);
|
||||
|
||||
// Lease contract row - only breadcrumb spans ending with )), no req
|
||||
s = s.replace(
|
||||
/React\.createElement\('div', \{ style: \{ marginBottom: 16 \} \},\s*React\.createElement\('span'[\s\S]*?\)\),/g,
|
||||
''
|
||||
);
|
||||
|
||||
// 后装设备 list page partial breadcrumb block inside createElement
|
||||
s = s.replace(
|
||||
/React\.createElement\('div', \{ style: styles\.breadcrumbLeft \},[\s\S]*?React\.createElement\('span', \{ style: styles\.breadcrumbCurrent \}, '[^']+'\)\s*\),/g,
|
||||
''
|
||||
);
|
||||
|
||||
// Orphan breadcrumb link lines after broken removal
|
||||
s = s.replace(
|
||||
/\n\t\t\tReact\.createElement\('a', \{ href: '#', style: styles\.breadcrumbLink[\s\S]*?breadcrumbCurrent \}, '[^']+'\)\n\t\t\),/g,
|
||||
''
|
||||
);
|
||||
|
||||
// Remove breadcrumbItems variable blocks
|
||||
s = s.replace(/\n\tvar breadcrumbItems = \[[\s\S]*?\];\n/g, '\n');
|
||||
s = s.replace(/\n\tvar breadcrumbItems = \[[\s\S]*?\];\n/g, '\n');
|
||||
s = s.replace(/\n\t\tbreadcrumbItems\.push\([^)]+\);\n/g, '');
|
||||
|
||||
// 备车/交车任务 inline breadcrumb rows (partial)
|
||||
s = s.replace(
|
||||
/React\.createElement\('div', \{ style: styles\.breadcrumb[^}]*\},[\s\S]*?\),?\n?/g,
|
||||
''
|
||||
);
|
||||
s = s.replace(
|
||||
/React\.createElement\('span', \{ key: '[^']+', style: styles\.breadcrumbSep \}, ' \/ '\),?\n?\s*/g,
|
||||
''
|
||||
);
|
||||
|
||||
// Arco/fault pages: remove Breadcrumb from Layout header if any remain
|
||||
s = s.replace(/,\s*breadcrumb:\s*React\.createElement\(Breadcrumb[\s\S]*?\)/g, '');
|
||||
|
||||
// business pages partial breadcrumb in flex div
|
||||
s = s.replace(
|
||||
/React\.createElement\('div', \{ style: \{ display: 'flex', alignItems: 'center', marginBottom: 16 \} \},[\s\S]*?styles\.breadcrumbSep[\s\S]*?\),/g,
|
||||
''
|
||||
);
|
||||
|
||||
// Fix double spaces in lease contract lines
|
||||
s = s.replace(/marginBottom: 16 \} \},\s{2}React/g, 'marginBottom: 16 } }, React');
|
||||
|
||||
// 27-交车管理: broken topbar after breadcrumb removal
|
||||
s = s.replace(
|
||||
/React\.createElement\('div', \{ style: \{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 \} \},\s*React\.createElement\(Button,/g,
|
||||
`React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: 16 } }, React.createElement(Button,`
|
||||
);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
function fixCollectPageIndent(source) {
|
||||
return source.replace(
|
||||
/(\}, '返回'\),\n)\s+React\.createElement\(Button,/g,
|
||||
`$1\t\t\tReact.createElement(Button,`
|
||||
);
|
||||
}
|
||||
|
||||
const files = walk(ROOT);
|
||||
let changed = 0;
|
||||
for (const file of files) {
|
||||
let source = fs.readFileSync(file, 'utf8');
|
||||
if (!/breadcrumb|Breadcrumb|面包屑/.test(source)) continue;
|
||||
const next = fixCollectPageIndent(cleanup(source));
|
||||
if (next !== source) {
|
||||
fs.writeFileSync(file, next);
|
||||
changed++;
|
||||
console.log('fixed:', path.relative(process.cwd(), file));
|
||||
}
|
||||
}
|
||||
console.log(`Pass 2 done. ${changed} file(s).`);
|
||||
62
scripts/remove-breadcrumbs-pass3.mjs
Normal file
62
scripts/remove-breadcrumbs-pass3.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const ROOT = path.resolve('src/prototypes');
|
||||
|
||||
function walk(dir, out = []) {
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
const full = path.join(dir, name);
|
||||
if (fs.statSync(full).isDirectory()) {
|
||||
if (name === 'node_modules' || name === '.spec') continue;
|
||||
walk(full, out);
|
||||
} else if (/\.(jsx|tsx)$/.test(name)) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function cleanup(source) {
|
||||
let s = source;
|
||||
|
||||
// Lease contract topbar: remove span trail, keep req action
|
||||
s = s.replace(
|
||||
/React\.createElement\('div', \{ style: \{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: 16 \} \}, React\.createElement\('span'[\s\S]*?\)\),\s*(React\.createElement\('(?:button|span)'[\s\S]*?'查看需求说明'\)[^)]*\)\),/g,
|
||||
`React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: 16 } }, $1),`
|
||||
);
|
||||
|
||||
// Topbar only breadcrumb spans (no req button)
|
||||
s = s.replace(
|
||||
/React\.createElement\('div', \{ style: \{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: 16 \} \}, React\.createElement\('span'[\s\S]*?\)\),/g,
|
||||
''
|
||||
);
|
||||
|
||||
// Unused breadcrumb vars
|
||||
s = s.replace(/\n\tvar breadcrumbItems = \[[\s\S]*?\];\n/g, '\n');
|
||||
s = s.replace(/\n\t\tvar breadcrumbItems = \[[\s\S]*?\];\n/g, '\n');
|
||||
s = s.replace(/\n\tvar breadcrumbNodes = \[[\s\S]*?\];\n/g, '\n');
|
||||
|
||||
// Broken 后装设备 page wrapper
|
||||
s = s.replace(
|
||||
/(\{ style: styles\.page \},\n)\s*React\.createElement\('a', \{ href: '#', style: styles\.requirementLink[\s\S]*?'查看需求说明'\)\n\t\t\),/g,
|
||||
`$1\t\tReact.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', marginBottom: 16 } },\n\t\t\tReact.createElement('a', { href: '#', style: styles.requirementLink, onClick: function (e) { e.preventDefault(); setShowRequirementModal(true); } }, '查看需求说明')\n\t\t),\n`
|
||||
);
|
||||
|
||||
// Empty if blocks left from breadcrumb removal in 业务台账
|
||||
s = s.replace(/\n\tif \(view === 'sales' \|\| view === 'project'\) \{\s*\}\n\tif \(view === 'project'\) \{\s*\}\n/g, '\n');
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
const files = walk(ROOT);
|
||||
let changed = 0;
|
||||
for (const file of files) {
|
||||
let source = fs.readFileSync(file, 'utf8');
|
||||
if (!/breadcrumb|Breadcrumb|面包屑/.test(source)) continue;
|
||||
const next = cleanup(source);
|
||||
if (next !== source) {
|
||||
fs.writeFileSync(file, next);
|
||||
changed++;
|
||||
console.log('fixed:', path.relative(process.cwd(), file));
|
||||
}
|
||||
}
|
||||
console.log(`Pass 3 done. ${changed} file(s).`);
|
||||
213
scripts/remove-breadcrumbs.mjs
Normal file
213
scripts/remove-breadcrumbs.mjs
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Remove breadcrumb UI from prototype pages under src/prototypes.
|
||||
* Keeps sibling actions such as「查看需求说明」when present in breadcrumbRight.
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const ROOT = path.resolve('src/prototypes');
|
||||
|
||||
function walk(dir, out = []) {
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
const full = path.join(dir, name);
|
||||
const stat = fs.statSync(full);
|
||||
if (stat.isDirectory()) {
|
||||
if (name === 'node_modules' || name === '.spec') continue;
|
||||
walk(full, out);
|
||||
} else if (/\.(jsx|tsx)$/.test(name)) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function removeBalancedFrom(source, openIndex) {
|
||||
let i = openIndex;
|
||||
if (source[i] !== '(') return null;
|
||||
let depth = 0;
|
||||
let inStr = null;
|
||||
let escape = false;
|
||||
for (; i < source.length; i++) {
|
||||
const ch = source[i];
|
||||
if (inStr) {
|
||||
if (escape) {
|
||||
escape = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '\\') {
|
||||
escape = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === inStr) inStr = null;
|
||||
continue;
|
||||
}
|
||||
if (ch === "'" || ch === '"' || ch === '`') {
|
||||
inStr = ch;
|
||||
continue;
|
||||
}
|
||||
if (ch === '(') depth++;
|
||||
else if (ch === ')') {
|
||||
depth--;
|
||||
if (depth === 0) return source.slice(openIndex, i + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function removeReactBreadcrumbCalls(source) {
|
||||
const token = 'React.createElement(Breadcrumb,';
|
||||
let result = source;
|
||||
let idx = 0;
|
||||
while ((idx = result.indexOf(token, idx)) !== -1) {
|
||||
const open = result.indexOf('(', idx);
|
||||
const chunk = removeBalancedFrom(result, open);
|
||||
if (!chunk) break;
|
||||
let start = idx;
|
||||
let end = open + chunk.length;
|
||||
while (start > 0 && /[\t ]/.test(result[start - 1])) start--;
|
||||
if (result[start - 1] === ',') start--;
|
||||
if (result[start - 1] === '\n' && result[start - 2] === ',') start--;
|
||||
while (end < result.length && /[\t ,]/.test(result[end])) end++;
|
||||
if (result[end] === '\n') end++;
|
||||
result = result.slice(0, start) + result.slice(end);
|
||||
idx = start;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function removeBreadcrumbImports(source) {
|
||||
return source
|
||||
.replace(/^\s*var Breadcrumb = antd\.Breadcrumb;\n/gm, '')
|
||||
.replace(/^\s*const Breadcrumb = antd\.Breadcrumb;\n/gm, '')
|
||||
.replace(/^\s*import\s+\{[^}]*\bBreadcrumb\b[^}]*\}\s+from\s+['"][^'"]+['"];\n/gm, (line) =>
|
||||
line.replace(/\bBreadcrumb,?\s*/g, '').replace(/,\s*,/g, ',').replace(/\{\s*,/g, '{').replace(/,\s*\}/g, '}').replace(/import\s+\{\s*\}\s+from[^;]+;\n/g, '')
|
||||
);
|
||||
}
|
||||
|
||||
function removeNavBreadcrumbs(source) {
|
||||
return source.replace(
|
||||
/\n?\s*<nav[^>]*breadcrumb[^>]*>[\s\S]*?<\/nav>\n?/gi,
|
||||
'\n'
|
||||
);
|
||||
}
|
||||
|
||||
function replaceJsxBreadcrumbBar(source) {
|
||||
// JSX: breadcrumb row with optional right actions
|
||||
return source.replace(
|
||||
/\{\/\*[\s*]*面包屑[\s*]*\*\/\}\s*\n?\s*<div style=\{styles\.breadcrumb\}>[\s\S]*?<div style=\{styles\.breadcrumbRight\}>([\s\S]*?)<\/div>\s*<\/div>/g,
|
||||
(_, right) => `\n\t\t\t<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>${right.trim()}</div>`
|
||||
);
|
||||
}
|
||||
|
||||
function replaceCreateElementBreadcrumbBar(source) {
|
||||
// createElement breadcrumb with Left + Right — keep Right only
|
||||
const pattern = /React\.createElement\(\s*'div',\s*\{ style: styles\.breadcrumb \},[\s\S]*?React\.createElement\(\s*'div',\s*\{ style: styles\.breadcrumbRight \},([\s\S]*?)\)\s*\)/g;
|
||||
return source.replace(
|
||||
pattern,
|
||||
(_, right) =>
|
||||
`React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', marginBottom: 16 } }, ${right.trim()})`
|
||||
);
|
||||
}
|
||||
|
||||
function replaceSimpleCreateElementBreadcrumb(source) {
|
||||
// div with only styles.breadcrumb path segments (no Right sibling)
|
||||
return source.replace(
|
||||
/React\.createElement\(\s*'div',\s*\{ style: styles\.breadcrumb[^}]*\},[\s\S]*?\),?\n?/g,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
function replaceLeaseContractBreadcrumbRow(source) {
|
||||
// Long inline breadcrumb + req button row
|
||||
return source.replace(
|
||||
/React\.createElement\('div',\s*\{ style: \{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 \} \},\s*React\.createElement\('div',\s*\{ style: styles\.breadcrumb[^}]+\}[\s\S]*?'查看需求说明'\)\),/g,
|
||||
(match) => {
|
||||
const req = match.match(/React\.createElement\('button'[\s\S]*?'查看需求说明'\)/);
|
||||
if (!req) return '';
|
||||
return `React.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginBottom: 16 } }, ${req[0]}),`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function removeEmptyBreadcrumbWrappers(source) {
|
||||
// div wrapper that only wrapped breadcrumb (now empty)
|
||||
return source
|
||||
.replace(
|
||||
/React\.createElement\('div',\s*\{ style: \{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 \} \},\s*\),?\n?/g,
|
||||
''
|
||||
)
|
||||
.replace(
|
||||
/React\.createElement\('div',\s*\{ style: \{ marginBottom: 16 \} \},\s*\),?\n?/g,
|
||||
''
|
||||
)
|
||||
.replace(
|
||||
/React\.createElement\('div',\s*\{ className: 'vpr-collect-topbar__main' \},\s*\),?\n?/g,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
function transformFile(filePath) {
|
||||
let source = fs.readFileSync(filePath, 'utf8');
|
||||
if (!/breadcrumb|Breadcrumb|面包屑/.test(source)) return false;
|
||||
|
||||
const original = source;
|
||||
source = removeReactBreadcrumbCalls(source);
|
||||
source = removeBreadcrumbImports(source);
|
||||
source = removeNavBreadcrumbs(source);
|
||||
source = replaceJsxBreadcrumbBar(source);
|
||||
source = replaceCreateElementBreadcrumbBar(source);
|
||||
source = replaceSimpleCreateElementBreadcrumb(source);
|
||||
source = replaceLeaseContractBreadcrumbRow(source);
|
||||
source = removeEmptyBreadcrumbWrappers(source);
|
||||
|
||||
// header with only req link after breadcrumb removed
|
||||
source = source.replace(
|
||||
/React\.createElement\(\s*'header',\s*\{ className: 'vr-page-header' \},\s*React\.createElement\(\s*Button,/g,
|
||||
`React.createElement('header', { className: 'vr-page-header', style: { display: 'flex', justifyContent: 'flex-end', marginBottom: 16 } }, React.createElement(Button,`
|
||||
);
|
||||
|
||||
// flex topbar: space-between -> flex-end when breadcrumb was first child
|
||||
source = source.replace(
|
||||
/justifyContent: 'space-between', marginBottom: 16 \} \},\s*React\.createElement\(Button, \{ type: 'link'/g,
|
||||
`justifyContent: 'flex-end', marginBottom: 16 } }, React.createElement(Button, { type: 'link'`
|
||||
);
|
||||
|
||||
if (source !== original) {
|
||||
fs.writeFileSync(filePath, source);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const files = walk(ROOT);
|
||||
let changed = 0;
|
||||
for (const file of files) {
|
||||
if (transformFile(file)) {
|
||||
changed++;
|
||||
console.log('updated:', path.relative(process.cwd(), file));
|
||||
}
|
||||
}
|
||||
|
||||
// CSS tweak for collect topbar
|
||||
const vprCss = path.join(ROOT, 'vehicle-pickup-receivable/styles/index.css');
|
||||
if (fs.existsSync(vprCss)) {
|
||||
let css = fs.readFileSync(vprCss, 'utf8');
|
||||
const next = css
|
||||
.replace('/* 办理页顶栏:返回 + 面包屑 */', '/* 办理页顶栏:返回 + 操作 */')
|
||||
.replace(
|
||||
/\.vm-page\.vpr-collect-page \.vpr-collect-topbar \{\n display: flex;\n align-items: center;\n gap: 16px;/,
|
||||
`.vm-page.vpr-collect-page .vpr-collect-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;`
|
||||
)
|
||||
.replace(/\n\.vm-page\.vpr-collect-page \.vpr-collect-topbar__main \{[^}]+\}\n?/g, '\n');
|
||||
if (next !== css) {
|
||||
fs.writeFileSync(vprCss, next);
|
||||
console.log('updated:', path.relative(process.cwd(), vprCss));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nDone. ${changed} file(s) updated.`);
|
||||
92
scripts/strip-prototype-nav-from-directory.mjs
Normal file
92
scripts/strip-prototype-nav-from-directory.mjs
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 从各原型 annotation-source.json 移除 ONE-OS 原型导航目录节点。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/strip-prototype-nav-from-directory.mjs
|
||||
* node scripts/strip-prototype-nav-from-directory.mjs --prototype lease-contract-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 stripNavNodes(nodes) {
|
||||
const list = Array.isArray(nodes) ? nodes : [];
|
||||
return list
|
||||
.filter((node) => node?.id !== NAV_FOLDER_ID)
|
||||
.map((node) => {
|
||||
if (node?.type === 'folder' && Array.isArray(node.children)) {
|
||||
return { ...node, children: stripNavNodes(node.children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
function listPrototypeIds(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 stripPrototype(prototypeId, projectRoot) {
|
||||
const annotationPath = path.join(projectRoot, 'src/prototypes', prototypeId, 'annotation-source.json');
|
||||
if (!fs.existsSync(annotationPath)) return false;
|
||||
|
||||
const annotation = readJson(annotationPath);
|
||||
const before = JSON.stringify(annotation.directory?.nodes || []);
|
||||
annotation.directory = {
|
||||
nodes: stripNavNodes(annotation.directory?.nodes),
|
||||
};
|
||||
const after = JSON.stringify(annotation.directory.nodes || []);
|
||||
if (before === after) {
|
||||
console.log(`[skip] ${prototypeId}: 无导航节点`);
|
||||
return false;
|
||||
}
|
||||
|
||||
annotation.data = annotation.data || {};
|
||||
annotation.data.updatedAt = Date.now();
|
||||
writeJson(annotationPath, annotation);
|
||||
console.log(`[strip] ${prototypeId}: 已移除原型导航目录`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const projectRoot = args.projectRoot || path.resolve(__dirname, '..');
|
||||
const targets = args.prototype ? [args.prototype] : listPrototypeIds(projectRoot);
|
||||
|
||||
let changed = 0;
|
||||
for (const prototypeId of targets) {
|
||||
if (stripPrototype(prototypeId, projectRoot)) changed += 1;
|
||||
}
|
||||
console.log(`完成:处理 ${targets.length} 个原型,更新 ${changed} 个 annotation-source.json`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -26,6 +26,7 @@ const STANDALONE_MODULES = [
|
||||
{ slug: 'vehicle-maintenance-ledger', title: '车辆维修明细', skipCodegen: true },
|
||||
{ slug: 'oneos-web-h2-station-site', title: '站点信息', skipCodegen: true },
|
||||
{ slug: 'vehicle-return-settlement', title: '还车应结款', skipCodegen: true },
|
||||
{ slug: 'vehicle-pickup-receivable', title: '提车应收款', skipCodegen: true },
|
||||
];
|
||||
|
||||
const MODULES = [
|
||||
@@ -39,7 +40,17 @@ const MODULES = [
|
||||
title: '财务管理',
|
||||
dir: '财务管理',
|
||||
excludeDirs: ['文档'],
|
||||
excludeFiles: ['还车应结款.jsx', '还车应结款-查看.jsx', '还车应结款-费用明细.jsx'],
|
||||
excludeFiles: [
|
||||
'还车应结款.jsx',
|
||||
'还车应结款-查看.jsx',
|
||||
'还车应结款-费用明细.jsx',
|
||||
'提车应收款.jsx',
|
||||
'提车应收款-查看.jsx',
|
||||
'提车应收款-开票信息.jsx',
|
||||
'提车应收款-审核.jsx',
|
||||
'提车应收款-提车收款单.jsx',
|
||||
'提车收款单-编辑.jsx',
|
||||
],
|
||||
},
|
||||
{ slug: 'oneos-web-procurement', title: '采购管理', dir: '采购管理' },
|
||||
{ slug: 'oneos-web-lease-contract', title: '车辆租赁合同', dir: '车辆租赁合同' },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* 将 .axhub/make/sidebar-tree.json 左侧菜单同步到各原型 annotation-source.json 的「原型目录」。
|
||||
* 将 .axhub/make/sidebar-tree.json 同步到原型导航页 nav-menu.json。
|
||||
* 不再向各原型 annotation-source.json 注入「ONE-OS 原型导航」目录。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/sync-project-prototype-directory.mjs
|
||||
@@ -33,56 +34,6 @@ 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)) {
|
||||
@@ -102,6 +53,18 @@ function listPrototypeIdsWithAnnotation(projectRoot) {
|
||||
.sort((a, b) => a.localeCompare(b, 'zh-CN'));
|
||||
}
|
||||
|
||||
function stripNavNodes(nodes) {
|
||||
const list = Array.isArray(nodes) ? nodes : [];
|
||||
return list
|
||||
.filter((node) => node?.id !== NAV_FOLDER_ID)
|
||||
.map((node) => {
|
||||
if (node?.type === 'folder' && Array.isArray(node.children)) {
|
||||
return { ...node, children: stripNavNodes(node.children) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
function fixPublishedLinks(nodes, prototypeId) {
|
||||
const walk = (items) => {
|
||||
for (const node of items || []) {
|
||||
@@ -127,7 +90,7 @@ function fixPublishedLinks(nodes, prototypeId) {
|
||||
walk(nodes);
|
||||
}
|
||||
|
||||
function syncPrototypeDirectory(prototypeId, sidebarItems, projectRoot) {
|
||||
function cleanupPrototypeDirectory(prototypeId, projectRoot) {
|
||||
const annotationPath = path.join(projectRoot, 'src/prototypes', prototypeId, 'annotation-source.json');
|
||||
if (!fs.existsSync(annotationPath)) {
|
||||
console.log(`[skip] ${prototypeId}: 无 annotation-source.json`);
|
||||
@@ -135,27 +98,22 @@ function syncPrototypeDirectory(prototypeId, sidebarItems, projectRoot) {
|
||||
}
|
||||
|
||||
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);
|
||||
const cleanedNodes = stripNavNodes(existingNodes);
|
||||
fixPublishedLinks(cleanedNodes, prototypeId);
|
||||
|
||||
annotation.directory = {
|
||||
nodes: [navFolder, ...preservedNodes],
|
||||
};
|
||||
const before = JSON.stringify(existingNodes);
|
||||
const after = JSON.stringify(cleanedNodes);
|
||||
if (before === after) {
|
||||
console.log(`[ok] ${prototypeId}: 目录已不含原型导航`);
|
||||
return false;
|
||||
}
|
||||
|
||||
annotation.directory = { nodes: cleanedNodes };
|
||||
annotation.data = annotation.data || {};
|
||||
annotation.data.updatedAt = Date.now();
|
||||
|
||||
writeJson(annotationPath, annotation);
|
||||
console.log(`[sync] ${prototypeId}: 已写入 ${navChildren.length} 个顶级目录项(含分组)`);
|
||||
console.log(`[cleanup] ${prototypeId}: 已移除原型导航目录节点`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -186,11 +144,11 @@ function main() {
|
||||
|
||||
let changed = 0;
|
||||
for (const prototypeId of targets) {
|
||||
if (syncPrototypeDirectory(prototypeId, sidebarItems, projectRoot)) {
|
||||
if (cleanupPrototypeDirectory(prototypeId, projectRoot)) {
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
console.log(`完成:处理 ${targets.length} 个原型,更新 ${changed} 个 annotation-source.json`);
|
||||
console.log(`完成:处理 ${targets.length} 个原型,清理 ${changed} 个 annotation-source.json`);
|
||||
syncPrototypeNavMenu(projectRoot, sidebarItems);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user