新增 AutoRDO 需求清洗工作台与消息中枢,迭代 OneOS V2 设计规范及租赁合同/工作台/车辆等原型,同步云效技能与导航注册;并归档一批 legacy 原型快照。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
14
scripts/lease-contract-theme-skins.mjs
Normal file
14
scripts/lease-contract-theme-skins.mjs
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 租赁合同主题比稿:兼容旧 import 路径,实现见 design-system/theme-variants。
|
||||
*/
|
||||
export {
|
||||
THEME_SHOTS,
|
||||
THEME_CATALOG,
|
||||
OFFICIAL_THEME,
|
||||
buildSkinCss,
|
||||
buildAntThemeConfig,
|
||||
buildOfficialAntTheme,
|
||||
getThemeById,
|
||||
resolvePalette,
|
||||
resolvePrimary,
|
||||
} from '../src/resources/design-system/theme-variants/build-skin.mjs';
|
||||
193
scripts/pack-oneos-v2-design-handoff.mjs
Normal file
193
scripts/pack-oneos-v2-design-handoff.mjs
Normal file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 打包 OneOS V2 设计规范「其他前端外发包」为 zip。
|
||||
*
|
||||
* 用法(在仓库根目录):
|
||||
* node scripts/pack-oneos-v2-design-handoff.mjs
|
||||
* node scripts/pack-oneos-v2-design-handoff.mjs --out ~/Desktop
|
||||
*
|
||||
* 产出目录默认:dist/oneos-v2-design-handoff/
|
||||
* zip 文件名:oneos-v2-design-handoff-v{版本}-{日期}.zip
|
||||
*
|
||||
* 不含 UIComponents.tsx / prototypes(另一套前端不依赖本仓库 React 组件)。
|
||||
*/
|
||||
|
||||
import { spawnSync } 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 ROOT = path.resolve(__dirname, '..');
|
||||
const DS = path.join(ROOT, 'src/resources/design-system');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = { outDir: path.join(ROOT, 'dist') };
|
||||
for (let i = 2; i < argv.length; i += 1) {
|
||||
if (argv[i] === '--out' && argv[i + 1]) {
|
||||
out.outDir = path.resolve(argv[++i]);
|
||||
} else if (argv[i] === '--help' || argv[i] === '-h') {
|
||||
out.help = true;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function readDesignVersion() {
|
||||
const designPath = path.join(DS, 'DESIGN.md');
|
||||
const text = fs.readFileSync(designPath, 'utf8');
|
||||
const m = text.match(/\|\s*文档版本\s*\|\s*\*\*([^*]+)\*\*/);
|
||||
if (m) {
|
||||
const raw = m[1].trim();
|
||||
const ver = raw.match(/v?\d+(?:\.\d+)*/i);
|
||||
return ver ? ver[0].replace(/^v/i, '') : 'unknown';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function dateStamp() {
|
||||
const d = new Date();
|
||||
const y = d.getFullYear();
|
||||
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}${mo}${day}`;
|
||||
}
|
||||
|
||||
/** @type {Array<{ from: string, to: string, optional?: boolean }>} */
|
||||
const FILES = [
|
||||
{ from: 'DESIGN.md', to: 'DESIGN.md' },
|
||||
{ from: 'README.md', to: 'README.md' },
|
||||
{ from: 'PRIORITY.md', to: 'PRIORITY.md' },
|
||||
{ from: 'HANDOFF-其他前端.md', to: 'HANDOFF-其他前端.md' },
|
||||
{ from: 'HANDOFF-Confluence摘要.md', to: 'HANDOFF-Confluence摘要.md' },
|
||||
{ from: 'HANDOFF-Codex开发.md', to: 'HANDOFF-Codex开发.md' },
|
||||
{ from: 'ai-prompt-template.md', to: 'ai-prompt-template.md' },
|
||||
{ from: 'tokens.json', to: 'tokens.json' },
|
||||
{ from: 'theme-variants.json', to: 'theme-variants.json', optional: true },
|
||||
{ from: 'oneos-ds-tokens.css', to: 'oneos-ds-tokens.css' },
|
||||
{ from: 'oneos-ds-filter-affordance.css', to: 'oneos-ds-filter-affordance.css' },
|
||||
{ from: 'ruoyi-theme-preset.json', to: 'ruoyi-theme-preset.json' },
|
||||
{ from: 'ruoyi-oneos-v2-theme.css', to: 'ruoyi-oneos-v2-theme.css' },
|
||||
];
|
||||
|
||||
function copyFile(src, dest, optional = false) {
|
||||
if (!fs.existsSync(src)) {
|
||||
if (optional) {
|
||||
console.warn(`[skip optional] ${src}`);
|
||||
return false;
|
||||
}
|
||||
throw new Error(`缺少文件: ${src}`);
|
||||
}
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.copyFileSync(src, dest);
|
||||
return true;
|
||||
}
|
||||
|
||||
function copyDirRecursive(srcDir, destDir) {
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
throw new Error(`缺少目录: ${srcDir}`);
|
||||
}
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
for (const name of fs.readdirSync(srcDir)) {
|
||||
const from = path.join(srcDir, name);
|
||||
const to = path.join(destDir, name);
|
||||
const st = fs.statSync(from);
|
||||
if (st.isDirectory()) {
|
||||
copyDirRecursive(from, to);
|
||||
} else if (st.isFile() && name.endsWith('.md')) {
|
||||
fs.copyFileSync(from, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writePackageReadme(packRoot, version) {
|
||||
const body = `# OneOS V2 设计规范 · 其他前端外发包
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| DESIGN 版本 | v${version} |
|
||||
| 打包日期 | ${dateStamp()} |
|
||||
| 展厅预览 | https://prototype.lnoneos.com/oneos-v2/index.html |
|
||||
| 台账母版 | https://prototype.lnoneos.com/lease-contract-management/index.html |
|
||||
|
||||
## 先读哪份
|
||||
|
||||
1. **一页摘要(可贴 Confluence)**:\`HANDOFF-Confluence摘要.md\`
|
||||
2. **完整接入步骤(其他前端)**:\`HANDOFF-其他前端.md\`
|
||||
3. **研发 Codex 接入**:\`HANDOFF-Codex开发.md\`
|
||||
4. **总规范**:\`DESIGN.md\`
|
||||
5. **给 AI**:\`ai-prompt-template.md\`(配合 HANDOFF §4 改路径)
|
||||
|
||||
## 建议落盘
|
||||
|
||||
解压后复制到对方仓库:
|
||||
|
||||
\`\`\`text
|
||||
docs/oneos-v2/
|
||||
\`\`\`
|
||||
|
||||
## 本包不含
|
||||
|
||||
- \`UIComponents.tsx\` 等 React 组件实现(请按规范自研等价控件)
|
||||
- Axhub 业务原型源码
|
||||
|
||||
维护方:OneOS 产品 / 设计规范。
|
||||
`;
|
||||
fs.writeFileSync(path.join(packRoot, '00-请先阅读.md'), body, 'utf8');
|
||||
}
|
||||
|
||||
function zipFolder(folderPath, zipPath) {
|
||||
const parent = path.dirname(folderPath);
|
||||
const base = path.basename(folderPath);
|
||||
// Prefer system zip for broad compatibility on macOS
|
||||
const result = spawnSync('zip', ['-r', '-q', zipPath, base], {
|
||||
cwd: parent,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`zip 失败: ${result.stderr || result.stdout || result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
if (args.help) {
|
||||
console.log(`Usage: node scripts/pack-oneos-v2-design-handoff.mjs [--out <dir>]`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(DS)) {
|
||||
console.error(`找不到设计规范目录: ${DS}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const version = readDesignVersion();
|
||||
const stamp = dateStamp();
|
||||
const packName = `oneos-v2-design-handoff-v${version}-${stamp}`;
|
||||
const stagingParent = path.join(args.outDir, 'oneos-v2-design-handoff');
|
||||
const packRoot = path.join(stagingParent, packName);
|
||||
const zipPath = path.join(args.outDir, `${packName}.zip`);
|
||||
|
||||
fs.rmSync(packRoot, { recursive: true, force: true });
|
||||
fs.mkdirSync(packRoot, { recursive: true });
|
||||
|
||||
for (const item of FILES) {
|
||||
copyFile(path.join(DS, item.from), path.join(packRoot, item.to), item.optional);
|
||||
}
|
||||
copyDirRecursive(path.join(DS, 'chapters'), path.join(packRoot, 'chapters'));
|
||||
writePackageReadme(packRoot, version);
|
||||
|
||||
fs.mkdirSync(path.dirname(zipPath), { recursive: true });
|
||||
if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
|
||||
zipFolder(packRoot, zipPath);
|
||||
|
||||
const sizeKb = Math.round(fs.statSync(zipPath).size / 1024);
|
||||
console.log('打包完成');
|
||||
console.log(` 版本: v${version}`);
|
||||
console.log(` 目录: ${packRoot}`);
|
||||
console.log(` ZIP : ${zipPath} (${sizeKb} KB)`);
|
||||
console.log('');
|
||||
console.log('下一步:把 ZIP 发给对方,并附上展厅链接 https://prototype.lnoneos.com/oneos-v2/index.html');
|
||||
}
|
||||
|
||||
main();
|
||||
31
scripts/shot-ds-badges.mjs
Normal file
31
scripts/shot-ds-badges.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import path from 'path';
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 2 });
|
||||
|
||||
const url = 'http://localhost:51720/prototypes/oneos-v2';
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
await page.evaluate(() => {
|
||||
const el = document.getElementById('sec-badges');
|
||||
if (el) el.scrollIntoView({ behavior: 'auto' });
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 600));
|
||||
|
||||
const shotBadgesPath = path.resolve('.tmp-ds-showcase-badges.png');
|
||||
await page.screenshot({ path: shotBadgesPath, fullPage: false });
|
||||
console.log(`Captured Badges section screenshot to ${shotBadgesPath}`);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
31
scripts/shot-ds-buttons.mjs
Normal file
31
scripts/shot-ds-buttons.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import path from 'path';
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 2 });
|
||||
|
||||
const url = 'http://localhost:51720/prototypes/oneos-v2';
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
await page.evaluate(() => {
|
||||
const el = document.getElementById('sec-buttons');
|
||||
if (el) el.scrollIntoView({ behavior: 'auto' });
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 600));
|
||||
|
||||
const shotButtonsPath = path.resolve('.tmp-ds-showcase-buttons.png');
|
||||
await page.screenshot({ path: shotButtonsPath, fullPage: false });
|
||||
console.log(`Captured Buttons section screenshot to ${shotButtonsPath}`);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
31
scripts/shot-ds-controls.mjs
Normal file
31
scripts/shot-ds-controls.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import path from 'path';
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 1200, deviceScaleFactor: 2 });
|
||||
|
||||
const url = 'http://localhost:51720/prototypes/oneos-v2';
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
await page.evaluate(() => {
|
||||
const el = document.getElementById('sec-controls');
|
||||
if (el) el.scrollIntoView({ behavior: 'auto', block: 'start' });
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
const shotControlsPath = path.resolve('.tmp-ds-showcase-controls-optimized.png');
|
||||
await page.screenshot({ path: shotControlsPath, fullPage: false });
|
||||
console.log(`Captured Controls section screenshot to ${shotControlsPath}`);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
53
scripts/shot-ds-h5-mobile.mjs
Normal file
53
scripts/shot-ds-h5-mobile.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import path from 'path';
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 1200, deviceScaleFactor: 2 });
|
||||
|
||||
const url = 'http://localhost:51720/prototypes/oneos-v2';
|
||||
console.log(`Navigating to ${url}...`);
|
||||
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
|
||||
// 1. Scroll to Mobile Nav Section
|
||||
await page.evaluate(() => {
|
||||
const el = document.getElementById('sec-mobile_nav');
|
||||
if (el) el.scrollIntoView({ behavior: 'auto' });
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
const shotMobileNavPath = path.resolve('.tmp-ds-mobile-nav-light.png');
|
||||
await page.screenshot({ path: shotMobileNavPath, fullPage: false });
|
||||
console.log(`Captured Light Mode screenshot to ${shotMobileNavPath}`);
|
||||
|
||||
// 2. Click the top-right Theme Mode toggle button specifically
|
||||
await page.evaluate(() => {
|
||||
const topBarBtns = Array.from(document.querySelectorAll('button'));
|
||||
const themeBtn = topBarBtns.find(b => b.textContent && (b.textContent.includes('浅色') || b.textContent.includes('暗色')));
|
||||
if (themeBtn) themeBtn.click();
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
// Scroll to Mobile Nav Section again
|
||||
await page.evaluate(() => {
|
||||
const el = document.getElementById('sec-mobile_nav');
|
||||
if (el) el.scrollIntoView({ behavior: 'auto' });
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
const shotDarkPath = path.resolve('.tmp-ds-mobile-nav-dark.png');
|
||||
await page.screenshot({ path: shotDarkPath, fullPage: false });
|
||||
console.log(`Captured Dark Mode screenshot to ${shotDarkPath}`);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
20
scripts/shot-ds-showcase-diag.mjs
Normal file
20
scripts/shot-ds-showcase-diag.mjs
Normal file
@@ -0,0 +1,20 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
|
||||
page.on('pageerror', err => console.log('PAGE ERROR:', err.toString()));
|
||||
|
||||
const url = 'http://localhost:51720/prototypes/oneos-v2';
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
43
scripts/shot-ds-showcase-range.mjs
Normal file
43
scripts/shot-ds-showcase-range.mjs
Normal file
@@ -0,0 +1,43 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import path from 'path';
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 1200, deviceScaleFactor: 2 });
|
||||
|
||||
const url = 'http://localhost:51720/prototypes/oneos-v2';
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
// Scroll to DatePicker section
|
||||
await page.evaluate(() => {
|
||||
const el = document.getElementById('sec-datepicker');
|
||||
if (el) el.scrollIntoView({ behavior: 'auto' });
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
// Click on the single input range picker trigger
|
||||
const spans = await page.$$('span');
|
||||
for (const span of spans) {
|
||||
const text = await page.evaluate(el => el.textContent, span);
|
||||
if (text && text.includes('2026-06-01 至 2026-07-31')) {
|
||||
await span.click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
const shotDualCalendarPath = path.resolve('.tmp-ds-showcase-single-input-dual-calendar.png');
|
||||
await page.screenshot({ path: shotDualCalendarPath, fullPage: false });
|
||||
console.log(`Captured Single Input Dual Calendar Popover screenshot to ${shotDualCalendarPath}`);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
61
scripts/shot-ds-showcase.mjs
Normal file
61
scripts/shot-ds-showcase.mjs
Normal file
@@ -0,0 +1,61 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import path from 'path';
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 1200, deviceScaleFactor: 2 });
|
||||
|
||||
const url = 'http://localhost:51720/prototypes/oneos-v2';
|
||||
console.log(`Navigating to ${url}...`);
|
||||
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
// 1. Capture Top Section
|
||||
const shotTopPath = path.resolve('.tmp-ds-showcase-top.png');
|
||||
await page.screenshot({ path: shotTopPath, fullPage: false });
|
||||
console.log(`Captured Top section screenshot to ${shotTopPath}`);
|
||||
|
||||
// 2. Scroll to DatePicker section and click trigger to open Custom Popover
|
||||
await page.evaluate(() => {
|
||||
const el = document.getElementById('sec-datepicker');
|
||||
if (el) el.scrollIntoView({ behavior: 'auto' });
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
// Find the single datepicker trigger and click it
|
||||
const datePickerTriggers = await page.$$('div');
|
||||
for (const trigger of datePickerTriggers) {
|
||||
const text = await page.evaluate(el => el.textContent, trigger);
|
||||
if (text && text.includes('2026-07-23') && text.length < 25) {
|
||||
await trigger.click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
const shotDatePickerPath = path.resolve('.tmp-ds-showcase-custom-datepicker.png');
|
||||
await page.screenshot({ path: shotDatePickerPath, fullPage: false });
|
||||
console.log(`Captured Custom DatePicker Popover screenshot to ${shotDatePickerPath}`);
|
||||
|
||||
// 3. Scroll to Composite Section
|
||||
await page.evaluate(() => {
|
||||
const el = document.getElementById('sec-composite');
|
||||
if (el) el.scrollIntoView({ behavior: 'auto' });
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
|
||||
const shotCompositePath = path.resolve('.tmp-ds-showcase-composite.png');
|
||||
await page.screenshot({ path: shotCompositePath, fullPage: false });
|
||||
console.log(`Captured Composite FilterBar screenshot to ${shotCompositePath}`);
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
144
scripts/shot-form-workbench-redesign.mjs
Normal file
144
scripts/shot-form-workbench-redesign.mjs
Normal file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 基于 Stripe Fintech 高端风格的 表单页 (故障处置) 与 工作台页 (不含版本日志) 浅/暗截图生成器
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const formOutDir = path.join(root, 'src/resources/design-system/fault-form-shots');
|
||||
const workbenchOutDir = path.join(root, 'src/resources/design-system/workbench-shots');
|
||||
|
||||
async function loadPuppeteer() {
|
||||
const require = createRequire(import.meta.url);
|
||||
try {
|
||||
return require('puppeteer');
|
||||
} catch {
|
||||
return require('puppeteer-core');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(formOutDir, { recursive: true });
|
||||
fs.mkdirSync(workbenchOutDir, { recursive: true });
|
||||
|
||||
const puppeteer = await loadPuppeteer();
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
defaultViewport: { width: 1440, height: 1024, deviceScaleFactor: 2 },
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
page.setDefaultTimeout(60000);
|
||||
|
||||
const baseUrl = 'http://127.0.0.1:51720';
|
||||
const modes = ['light', 'dark'];
|
||||
|
||||
// 1. 生成故障处置表单页截图 (Form)
|
||||
for (const mode of modes) {
|
||||
const demoUrl = `${baseUrl}/prototypes/oneos-prototype-demo/?oneosTheme=${mode}#proto=lease-contract-redesign`;
|
||||
console.log(`正在渲染 表单页 (故障处置) (${mode})...`);
|
||||
|
||||
await page.goto(demoUrl, { waitUntil: 'networkidle2' });
|
||||
await page.waitForSelector('.oneos-shell', { timeout: 30000 });
|
||||
await page.waitForSelector('.oneos-shell-frame', { timeout: 30000 });
|
||||
|
||||
await page.evaluate((m) => {
|
||||
document.documentElement.dataset.dsMode = m;
|
||||
document.documentElement.dataset.oneosTheme = m;
|
||||
const shell = document.querySelector('.oneos-shell');
|
||||
if (shell) shell.dataset.oneosTheme = m;
|
||||
}, mode);
|
||||
|
||||
const iframeElement = await page.$('.oneos-shell-frame');
|
||||
if (iframeElement) {
|
||||
const targetSrc = `${baseUrl}/prototypes/lease-contract-redesign/?concept=form&oneosTheme=${mode}`;
|
||||
await page.evaluate((el, src) => {
|
||||
el.src = src;
|
||||
}, iframeElement, targetSrc);
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
|
||||
const childFrame = page.frames().find(
|
||||
(f) => f !== page.mainFrame() && f.url().includes('lease-contract-redesign'),
|
||||
);
|
||||
|
||||
if (childFrame) {
|
||||
await childFrame.evaluate((m) => {
|
||||
document.documentElement.dataset.dsMode = m;
|
||||
document.documentElement.dataset.oneosTheme = m;
|
||||
}, mode);
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
|
||||
const fileName = `fault-form-${mode}.png`;
|
||||
const outPath = path.join(formOutDir, fileName);
|
||||
|
||||
await page.screenshot({ path: outPath, type: 'png', fullPage: false });
|
||||
console.log('Successfully written Form image:', fileName);
|
||||
}
|
||||
|
||||
// 2. 生成现代工作台页截图 (Workbench - 不含版本更新日志)
|
||||
for (const mode of modes) {
|
||||
const demoUrl = `${baseUrl}/prototypes/oneos-prototype-demo/?oneosTheme=${mode}#proto=lease-contract-redesign`;
|
||||
console.log(`正在渲染 工作台页 (无版本日志) (${mode})...`);
|
||||
|
||||
await page.goto(demoUrl, { waitUntil: 'networkidle2' });
|
||||
await page.waitForSelector('.oneos-shell', { timeout: 30000 });
|
||||
await page.waitForSelector('.oneos-shell-frame', { timeout: 30000 });
|
||||
|
||||
await page.evaluate((m) => {
|
||||
document.documentElement.dataset.dsMode = m;
|
||||
document.documentElement.dataset.oneosTheme = m;
|
||||
const shell = document.querySelector('.oneos-shell');
|
||||
if (shell) shell.dataset.oneosTheme = m;
|
||||
}, mode);
|
||||
|
||||
const iframeElement = await page.$('.oneos-shell-frame');
|
||||
if (iframeElement) {
|
||||
const targetSrc = `${baseUrl}/prototypes/lease-contract-redesign/?concept=workbench&oneosTheme=${mode}`;
|
||||
await page.evaluate((el, src) => {
|
||||
el.src = src;
|
||||
}, iframeElement, targetSrc);
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
|
||||
const childFrame = page.frames().find(
|
||||
(f) => f !== page.mainFrame() && f.url().includes('lease-contract-redesign'),
|
||||
);
|
||||
|
||||
if (childFrame) {
|
||||
await childFrame.evaluate((m) => {
|
||||
document.documentElement.dataset.dsMode = m;
|
||||
document.documentElement.dataset.oneosTheme = m;
|
||||
}, mode);
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
|
||||
const fileName = `workbench-${mode}.png`;
|
||||
const outPath = path.join(workbenchOutDir, fileName);
|
||||
|
||||
await page.screenshot({ path: outPath, type: 'png', fullPage: false });
|
||||
console.log('Successfully written Workbench image:', fileName);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
console.log('表单页与工作台页截图已成功全部生成!');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
246
scripts/shot-form-workbench-themes.mjs
Normal file
246
scripts/shot-form-workbench-themes.mjs
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 租赁合同表单页 + 工作台 · 5 主题 × 浅/暗 截图
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/shot-form-workbench-themes.mjs
|
||||
* node scripts/shot-form-workbench-themes.mjs --base=http://127.0.0.1:51720
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
import { THEME_SHOTS, buildSkinCss } from './lease-contract-theme-skins.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const outRoot = path.join(root, 'src/resources/design-system/lease-contract-theme-shots');
|
||||
|
||||
const PAGES = [
|
||||
{
|
||||
key: 'form',
|
||||
title: '租赁合同 · 新增表单',
|
||||
path: '/prototypes/lease-contract-management/',
|
||||
ready: '.vm-page.lc-page',
|
||||
shot: '.vm-page.lc-create-page, .vm-page.lc-page',
|
||||
async prepare(page) {
|
||||
// 已在表单页则跳过
|
||||
const onCreate = await page.$('.lc-create-page');
|
||||
if (onCreate) return;
|
||||
// 点工具栏「新增」
|
||||
const clicked = await page.evaluate(() => {
|
||||
const buttons = Array.from(document.querySelectorAll('button, a, .vm-btn'));
|
||||
const btn = buttons.find((el) => (el.textContent || '').trim() === '新增');
|
||||
if (btn) {
|
||||
btn.click();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!clicked) throw new Error('未找到「新增」按钮');
|
||||
await page.waitForSelector('.lc-create-page', { timeout: 30000 });
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'workbench',
|
||||
title: '工作台',
|
||||
path: '/prototypes/oneos-web-workbench-new/',
|
||||
ready: '.vm-page.wb-page',
|
||||
shot: '.vm-page.wb-page',
|
||||
/** 比稿截图不需要版本更新弹层遮挡首屏 */
|
||||
async beforeGoto(page) {
|
||||
await page.evaluateOnNewDocument(() => {
|
||||
const KEY = 'oneos-wb-release-seen-v1';
|
||||
const VERSION = '1.1.5';
|
||||
const operators = [
|
||||
'王冕', '周凯', '王磊', '刘洋', '黄倩', '孙敏', '陈静', '赵律师', '张明',
|
||||
];
|
||||
const map = {};
|
||||
try {
|
||||
const raw = window.localStorage.getItem(KEY);
|
||||
if (raw) Object.assign(map, JSON.parse(raw) || {});
|
||||
} catch { /* ignore */ }
|
||||
for (const name of operators) map[name] = VERSION;
|
||||
window.localStorage.setItem(KEY, JSON.stringify(map));
|
||||
});
|
||||
},
|
||||
async prepare(page) {
|
||||
// 若仍弹出(缓存/旧会话),点「知道了」或强制隐藏
|
||||
await page.evaluate(async () => {
|
||||
const confirm = document.querySelector('.wb-release-confirm');
|
||||
if (confirm instanceof HTMLElement) confirm.click();
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
let style = document.getElementById('ds-theme-shot-hide-release');
|
||||
if (!style) {
|
||||
style = document.createElement('style');
|
||||
style.id = 'ds-theme-shot-hide-release';
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
style.textContent = `
|
||||
[data-annotation-id="wb-release-notes"],
|
||||
.wb-overlay--release,
|
||||
.wb-modal--release {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
`;
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { base: 'http://127.0.0.1:51720' };
|
||||
for (const a of argv) {
|
||||
if (a.startsWith('--base=')) args.base = a.slice('--base='.length);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
async function loadPuppeteer() {
|
||||
const require = createRequire(import.meta.url);
|
||||
try {
|
||||
return require('puppeteer');
|
||||
} catch {
|
||||
return require('puppeteer-core');
|
||||
}
|
||||
}
|
||||
|
||||
async function applySkin(page, theme, mode) {
|
||||
const css = buildSkinCss(theme, mode);
|
||||
await page.evaluate((cssText, modeName) => {
|
||||
document.documentElement.dataset.dsMode = modeName;
|
||||
let el = document.getElementById('ds-theme-shot-skin');
|
||||
if (!el) {
|
||||
el = document.createElement('style');
|
||||
el.id = 'ds-theme-shot-skin';
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
el.textContent = cssText;
|
||||
}, css, mode);
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
}
|
||||
|
||||
async function hideAnnotation(page) {
|
||||
await page.evaluate(() => {
|
||||
let el = document.getElementById('ds-theme-shot-hide-ann');
|
||||
if (!el) {
|
||||
el = document.createElement('style');
|
||||
el.id = 'ds-theme-shot-hide-ann';
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
el.textContent = `
|
||||
.axhub-annotation-host, [data-axhub-annotation],
|
||||
[class*="Annotation"], [class*="annotation-toolbar"] {
|
||||
display: none !important; visibility: hidden !important;
|
||||
}
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const base = args.base.replace(/\/$/, '');
|
||||
const puppeteer = await loadPuppeteer();
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH
|
||||
|| '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
defaultViewport: { width: 1440, height: 1100, deviceScaleFactor: 2 },
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const allResults = [];
|
||||
|
||||
for (const pageCfg of PAGES) {
|
||||
const outDir = path.join(outRoot, pageCfg.key);
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const page = await browser.newPage();
|
||||
page.setDefaultTimeout(60000);
|
||||
const url = `${base}${pageCfg.path}`;
|
||||
console.log(`\n== ${pageCfg.title} ==\n打开 ${url}`);
|
||||
if (typeof pageCfg.beforeGoto === 'function') {
|
||||
await pageCfg.beforeGoto(page);
|
||||
}
|
||||
await page.goto(url, { waitUntil: 'networkidle2' });
|
||||
await page.waitForSelector(pageCfg.ready, { timeout: 60000 });
|
||||
await pageCfg.prepare(page);
|
||||
await hideAnnotation(page);
|
||||
|
||||
for (const theme of THEME_SHOTS) {
|
||||
for (const mode of ['light', 'dark']) {
|
||||
await applySkin(page, theme, mode);
|
||||
const file = `${theme.id}-${mode}.png`;
|
||||
const outPath = path.join(outDir, file);
|
||||
const handle = await page.$(pageCfg.shot);
|
||||
if (handle) {
|
||||
await handle.screenshot({ path: outPath, type: 'png' });
|
||||
} else {
|
||||
await page.screenshot({ path: outPath, type: 'png' });
|
||||
}
|
||||
console.log('wrote', pageCfg.key + '/' + file);
|
||||
allResults.push({
|
||||
page: pageCfg.key,
|
||||
pageTitle: pageCfg.title,
|
||||
file: `${pageCfg.key}/${file}`,
|
||||
theme: theme.name,
|
||||
mode,
|
||||
category: theme.category,
|
||||
});
|
||||
}
|
||||
}
|
||||
await page.close();
|
||||
|
||||
const readme = `# ${pageCfg.title} · 主题比稿截图
|
||||
|
||||
生成时间:${new Date().toISOString().slice(0, 10)}
|
||||
|
||||
| 文件 | 主题 | 模式 |
|
||||
|------|------|------|
|
||||
${allResults
|
||||
.filter((r) => r.page === pageCfg.key)
|
||||
.map((r) => `| \`${path.basename(r.file)}\` | ${r.theme} | ${r.mode === 'light' ? '浅色' : '暗色'} |`)
|
||||
.join('\n')}
|
||||
`;
|
||||
fs.writeFileSync(path.join(outDir, 'README.md'), readme);
|
||||
}
|
||||
|
||||
// 更新总 README
|
||||
const indexMd = `# 主题比稿截图总览
|
||||
|
||||
生成时间:${new Date().toISOString().slice(0, 10)}
|
||||
|
||||
## 页面
|
||||
|
||||
| 目录 | 页面 |
|
||||
|------|------|
|
||||
| \`.\`(根目录 png) | 租赁合同 · 列表 |
|
||||
| [\`form/\`](./form/) | 租赁合同 · 新增表单 |
|
||||
| [\`workbench/\`](./workbench/) | 工作台 |
|
||||
|
||||
## 五套主题
|
||||
|
||||
| 编号 | 主题 | 气质 |
|
||||
|------|------|------|
|
||||
| A | Linear | 冷 · 紫靛 · 精致中后台 |
|
||||
| B | Stripe | 冷 · 品牌紫 · 金融精致 |
|
||||
| C | Airbnb | 暖 · 珊瑚红 · 出行品牌 |
|
||||
| D | Claude | 暖 · 陶土米 · 人文克制 |
|
||||
| E | Intercom | 暖 · 米白黑 · 温和 B2B |
|
||||
|
||||
每页均为 5 主题 × 浅/暗 = 10 张。皮肤为 token 叠加预览,定稿后再完整落地。
|
||||
`;
|
||||
fs.writeFileSync(path.join(outRoot, 'README.md'), indexMd);
|
||||
|
||||
await browser.close();
|
||||
console.log('\n完成 →', outRoot);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
62
scripts/shot-h5-vehicle-all.mjs
Normal file
62
scripts/shot-h5-vehicle-all.mjs
Normal file
@@ -0,0 +1,62 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 960, deviceScaleFactor: 2 });
|
||||
|
||||
const url = 'http://localhost:51721/prototypes/oneos-v2?view=h5-vehicle';
|
||||
console.log('Navigating to', url);
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
|
||||
// 1. List View Screenshot
|
||||
await page.screenshot({ path: '/Users/sylvawong/oneos1.2/.tmp-h5-vehicle-list.png', fullPage: false });
|
||||
console.log('Saved List View screenshot');
|
||||
|
||||
// 2. Click Kanban Mode
|
||||
const buttons = await page.$$('button');
|
||||
for (const btn of buttons) {
|
||||
const text = await page.evaluate(el => el.textContent, btn);
|
||||
if (text && text.includes('2. 看板模式')) {
|
||||
await btn.click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
await page.screenshot({ path: '/Users/sylvawong/oneos1.2/.tmp-h5-vehicle-kanban.png', fullPage: false });
|
||||
console.log('Saved Kanban View screenshot');
|
||||
|
||||
// 3. Click Workbench Mode
|
||||
for (const btn of buttons) {
|
||||
const text = await page.evaluate(el => el.textContent, btn);
|
||||
if (text && text.includes('3. 档案工作台')) {
|
||||
await btn.click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
await page.screenshot({ path: '/Users/sylvawong/oneos1.2/.tmp-h5-vehicle-workbench.png', fullPage: false });
|
||||
console.log('Saved Workbench View screenshot');
|
||||
|
||||
// 4. Click Dark Mode toggle
|
||||
for (const btn of buttons) {
|
||||
const text = await page.evaluate(el => el.textContent, btn);
|
||||
if (text && (text.includes('暗色') || text.includes('深色'))) {
|
||||
await btn.click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
await page.screenshot({ path: '/Users/sylvawong/oneos1.2/.tmp-h5-vehicle-dark.png', fullPage: false });
|
||||
console.log('Saved Dark Mode screenshot');
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
27
scripts/shot-h5-vehicle.mjs
Normal file
27
scripts/shot-h5-vehicle.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
async function main() {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1440, height: 960, deviceScaleFactor: 2 });
|
||||
|
||||
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
|
||||
page.on('pageerror', err => console.log('PAGE ERROR:', err.toString()));
|
||||
|
||||
const url = 'http://localhost:51721/prototypes/oneos-v2?view=h5-vehicle';
|
||||
console.log('Navigating to', url);
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 15000 });
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
await page.screenshot({ path: '/Users/sylvawong/oneos1.2/.tmp-h5-vehicle-list.png', fullPage: false });
|
||||
console.log('Saved list view screenshot');
|
||||
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
140
scripts/shot-lease-contract-combo.mjs
Normal file
140
scripts/shot-lease-contract-combo.mjs
Normal file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 融合方案(Stripe Fintech Bento + 看板 + 左侧任务/右侧表单主从)三视图截图生成脚本
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const outDir = path.join(root, 'src/resources/design-system/lease-contract-combo-shots');
|
||||
|
||||
async function loadPuppeteer() {
|
||||
const require = createRequire(import.meta.url);
|
||||
try {
|
||||
return require('puppeteer');
|
||||
} catch {
|
||||
return require('puppeteer-core');
|
||||
}
|
||||
}
|
||||
|
||||
const COMBOS = [
|
||||
{
|
||||
view: 'list',
|
||||
viewLabel: '列表模式 (List View)',
|
||||
desc: 'Stripe Fintech 高端台账列表:显示项目、客户/签约主体、履约与审批状态、交付车辆与金额',
|
||||
},
|
||||
{
|
||||
view: 'kanban',
|
||||
viewLabel: '看板模式 (Kanban View)',
|
||||
desc: 'Pipeline 阶段履约看板:草稿箱、待我审批/盖章中、履约执行中、已终止/归档 4 大生命周期列',
|
||||
},
|
||||
{
|
||||
view: 'split',
|
||||
viewLabel: '主从/表单模式 (Master-Detail View)',
|
||||
desc: '左侧任务菜单 + 右侧表单内容区:包含合同概览、关联车辆与运单、电子盖章存证与合规审批',
|
||||
},
|
||||
];
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const puppeteer = await loadPuppeteer();
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
defaultViewport: { width: 1440, height: 1024, deviceScaleFactor: 2 },
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
page.setDefaultTimeout(60000);
|
||||
|
||||
const baseUrl = 'http://127.0.0.1:51720';
|
||||
const modes = ['light', 'dark'];
|
||||
const results = [];
|
||||
|
||||
for (const combo of COMBOS) {
|
||||
for (const mode of modes) {
|
||||
const demoUrl = `${baseUrl}/prototypes/oneos-prototype-demo/?oneosTheme=${mode}#proto=lease-contract-redesign`;
|
||||
console.log(`正在渲染 Combo [${combo.view}] (${mode})...`);
|
||||
|
||||
await page.goto(demoUrl, { waitUntil: 'networkidle2' });
|
||||
await page.waitForSelector('.oneos-shell', { timeout: 30000 });
|
||||
await page.waitForSelector('.oneos-shell-frame', { timeout: 30000 });
|
||||
|
||||
// 给外壳设置主题属性
|
||||
await page.evaluate((m) => {
|
||||
document.documentElement.dataset.dsMode = m;
|
||||
document.documentElement.dataset.oneosTheme = m;
|
||||
const shell = document.querySelector('.oneos-shell');
|
||||
if (shell) shell.dataset.oneosTheme = m;
|
||||
}, mode);
|
||||
|
||||
// 找到子 iframe 并修改 src 为带 ?concept=combo&view=list/kanban/split 的页面
|
||||
const iframeElement = await page.$('.oneos-shell-frame');
|
||||
if (iframeElement) {
|
||||
const targetSrc = `${baseUrl}/prototypes/lease-contract-redesign/?concept=combo&view=${combo.view}&oneosTheme=${mode}`;
|
||||
await page.evaluate((el, src) => {
|
||||
el.src = src;
|
||||
}, iframeElement, targetSrc);
|
||||
}
|
||||
|
||||
// 等待新 iframe 载入并对齐主题
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
|
||||
const childFrame = page.frames().find(
|
||||
(f) => f !== page.mainFrame() && f.url().includes('lease-contract-redesign'),
|
||||
);
|
||||
|
||||
if (childFrame) {
|
||||
await childFrame.evaluate((m) => {
|
||||
document.documentElement.dataset.dsMode = m;
|
||||
document.documentElement.dataset.oneosTheme = m;
|
||||
}, mode);
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
|
||||
const fileName = `combo-${combo.view}-${mode}.png`;
|
||||
const outPath = path.join(outDir, fileName);
|
||||
|
||||
await page.screenshot({ path: outPath, type: 'png', fullPage: false });
|
||||
console.log('Successfully written:', fileName);
|
||||
results.push({
|
||||
fileName,
|
||||
viewLabel: combo.viewLabel,
|
||||
desc: combo.desc,
|
||||
mode: mode === 'light' ? '浅色' : '暗色',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const readme = `# 租赁合同管理 — Stripe Bento 统一三视图 (列表 / 看板 / 左任务右表单) 截图
|
||||
|
||||
生成时间:${new Date().toISOString().slice(0, 10)}
|
||||
底稿风格:以概念 2 (Stripe Fintech UI) 为基础视觉基调,三视图规范统一下的无缝切换体验。
|
||||
|
||||
| 文件 | 视图模式 | 模式 | 页面特性说明 |
|
||||
|------|----------|------|--------------|
|
||||
${results
|
||||
.map(
|
||||
(r) =>
|
||||
`| \`${r.fileName}\` | **${r.viewLabel}** | ${r.mode} | ${r.desc} |`,
|
||||
)
|
||||
.join('\n')}
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(outDir, 'README.md'), readme);
|
||||
await browser.close();
|
||||
console.log('三视图融合方案截图全部成功生成并保存至:', outDir);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
166
scripts/shot-lease-contract-redesign.mjs
Normal file
166
scripts/shot-lease-contract-redesign.mjs
Normal file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 租赁合同全新 UI/UX 重构设计 (5 套完全不同布局交互) 截图生成器
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const outDir = path.join(root, 'src/resources/design-system/lease-contract-redesign-shots');
|
||||
|
||||
async function loadPuppeteer() {
|
||||
const require = createRequire(import.meta.url);
|
||||
try {
|
||||
return require('puppeteer');
|
||||
} catch {
|
||||
return require('puppeteer-core');
|
||||
}
|
||||
}
|
||||
|
||||
const CONCEPTS = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Linear 极速命令行与聚焦工作区',
|
||||
code: 'concept-1-linear',
|
||||
desc: '快捷键驱动 (⌘K)、右侧抽屉滑动预览、极窄高密表格与微状态指示点',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Stripe 金融台账与 Bento 分析枢纽',
|
||||
code: 'concept-2-stripe',
|
||||
desc: '高端 Fintech 视觉、Bento Bento Card 分析头图、Segmented Tabs 选项卡',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Studio 双栏联动合同工作台',
|
||||
code: 'concept-3-studio',
|
||||
desc: '35% 左侧列表 | 65% 右侧完整画卷,零上下文切换,直接在线查阅盖章与运单',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Stage Pipeline 阶段履约看板',
|
||||
code: 'concept-4-kanban',
|
||||
desc: '4 列生命周期 Pipeline 看板,清晰掌控草稿、审批、履约与归档卡点',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Executive 决策视界与优雅卡片阵列',
|
||||
code: 'concept-5-executive',
|
||||
desc: 'Apple 高管级 Glassmorphism 渐变半透明大盘、3 列卡片矩阵与氛围高亮',
|
||||
},
|
||||
];
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const puppeteer = await loadPuppeteer();
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
defaultViewport: { width: 1440, height: 1024, deviceScaleFactor: 2 },
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
page.setDefaultTimeout(60000);
|
||||
|
||||
const baseUrl = 'http://127.0.0.1:51720';
|
||||
const modes = ['light', 'dark'];
|
||||
const results = [];
|
||||
|
||||
for (const concept of CONCEPTS) {
|
||||
for (const mode of modes) {
|
||||
const demoUrl = `${baseUrl}/prototypes/oneos-prototype-demo/?oneosTheme=${mode}#proto=lease-contract-redesign`;
|
||||
console.log(`正在渲染 Concept ${concept.id} (${mode})...`);
|
||||
|
||||
await page.goto(demoUrl, { waitUntil: 'networkidle2' });
|
||||
await page.waitForSelector('.oneos-shell', { timeout: 30000 });
|
||||
await page.waitForSelector('.oneos-shell-frame', { timeout: 30000 });
|
||||
|
||||
// 给外壳设置主题属性
|
||||
await page.evaluate((m) => {
|
||||
document.documentElement.dataset.dsMode = m;
|
||||
document.documentElement.dataset.oneosTheme = m;
|
||||
const shell = document.querySelector('.oneos-shell');
|
||||
if (shell) shell.dataset.oneosTheme = m;
|
||||
}, mode);
|
||||
|
||||
// 找到子 iframe 并修改 src 为带有 ?concept=X 的页面
|
||||
let childFrame = null;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
childFrame = page.frames().find(
|
||||
(f) => f !== page.mainFrame() && f.url().includes('lease-contract-redesign'),
|
||||
);
|
||||
if (childFrame) break;
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
// 切换 iframe 内部 URL 为特定的 concept 与 mode
|
||||
const iframeElement = await page.$('.oneos-shell-frame');
|
||||
if (iframeElement) {
|
||||
const targetSrc = `${baseUrl}/prototypes/lease-contract-redesign/?concept=${concept.id}&oneosTheme=${mode}`;
|
||||
await page.evaluate((el, src) => {
|
||||
el.src = src;
|
||||
}, iframeElement, targetSrc);
|
||||
}
|
||||
|
||||
// 等待新 iframe 载入并对齐主题
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
|
||||
childFrame = page.frames().find(
|
||||
(f) => f !== page.mainFrame() && f.url().includes('lease-contract-redesign'),
|
||||
);
|
||||
|
||||
if (childFrame) {
|
||||
await childFrame.evaluate((m) => {
|
||||
document.documentElement.dataset.dsMode = m;
|
||||
document.documentElement.dataset.oneosTheme = m;
|
||||
}, mode);
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
|
||||
const fileName = `${concept.code}-${mode}.png`;
|
||||
const outPath = path.join(outDir, fileName);
|
||||
|
||||
await page.screenshot({ path: outPath, type: 'png', fullPage: false });
|
||||
console.log('Successfully written:', fileName);
|
||||
results.push({
|
||||
fileName,
|
||||
conceptId: concept.id,
|
||||
conceptName: concept.name,
|
||||
desc: concept.desc,
|
||||
mode: mode === 'light' ? '浅色' : '暗色',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const readme = `# 租赁合同管理 — 5 套全新 UI/UX 重构方案截图
|
||||
|
||||
生成时间:${new Date().toISOString().slice(0, 10)}
|
||||
统一标准:高大上(Premium / High-End SaaS)、全左侧菜单与顶栏外壳嵌入、100% 浅/暗双模式,支持真正差异化的 UI 布局与交互工作流。
|
||||
|
||||
| 文件 | 方案编号与名称 | 模式 | UI / UX 创新亮点 |
|
||||
|------|----------------|------|------------------|
|
||||
${results
|
||||
.map(
|
||||
(r) =>
|
||||
`| \`${r.fileName}\` | **Concept ${r.conceptId}**:${r.conceptName} | ${r.mode} | ${r.desc} |`,
|
||||
)
|
||||
.join('\n')}
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(outDir, 'README.md'), readme);
|
||||
await browser.close();
|
||||
console.log('全部 5 套全新的 UI/UX 设计稿已成功生成并保存至:', outDir);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
206
scripts/shot-lease-contract-themes.mjs
Normal file
206
scripts/shot-lease-contract-themes.mjs
Normal file
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 租赁合同列表(含 OneOS 侧栏 + 顶栏)· 5 主题 × 浅/暗 截图
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/shot-lease-contract-themes.mjs
|
||||
* node scripts/shot-lease-contract-themes.mjs --base=http://127.0.0.1:51720
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
import { THEME_SHOTS, buildSkinCss } from './lease-contract-theme-skins.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const outDir = path.join(root, 'src/resources/design-system/lease-contract-theme-shots');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { base: 'http://127.0.0.1:51720' };
|
||||
for (const a of argv) {
|
||||
if (a.startsWith('--base=')) args.base = a.slice('--base='.length);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
async function loadPuppeteer() {
|
||||
const require = createRequire(import.meta.url);
|
||||
try {
|
||||
return require('puppeteer');
|
||||
} catch {
|
||||
return require('puppeteer-core');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const demoUrl = `${args.base.replace(/\/$/, '')}/prototypes/oneos-prototype-demo/#proto=lease-contract-management`;
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const puppeteer = await loadPuppeteer();
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath:
|
||||
process.env.PUPPETEER_EXECUTABLE_PATH ||
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
defaultViewport: { width: 1440, height: 1024, deviceScaleFactor: 2 },
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
page.setDefaultTimeout(60000);
|
||||
|
||||
console.log('打开演示主页', demoUrl);
|
||||
await page.goto(demoUrl, { waitUntil: 'networkidle2' });
|
||||
|
||||
// 1. 等待主 Shell 框架
|
||||
await page.waitForSelector('.oneos-shell', { timeout: 60000 });
|
||||
await page.waitForSelector('.oneos-shell-frame', { timeout: 60000 });
|
||||
|
||||
// 2. 准确定位真正的子 iframe(排除主页面 mainFrame)
|
||||
let childFrame = null;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
childFrame = page.frames().find(
|
||||
(f) => f !== page.mainFrame() && f.url().includes('lease-contract-management'),
|
||||
);
|
||||
if (childFrame) {
|
||||
const ready = await childFrame.evaluate(() => !!document.querySelector('.vm-filter-card'));
|
||||
if (ready) {
|
||||
console.log(`子 iframe 页面已真正渲染就绪 (耗时 ${i * 200}ms)`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
if (!childFrame) {
|
||||
throw new Error('未在演示壳中找到已渲染的子 iframe');
|
||||
}
|
||||
|
||||
const modes = ['light', 'dark'];
|
||||
const results = [];
|
||||
|
||||
for (const theme of THEME_SHOTS) {
|
||||
for (const mode of modes) {
|
||||
const css = buildSkinCss(theme, mode);
|
||||
|
||||
// A) 给主外壳 (Main Page) 应用主题 css 与浅/深属性
|
||||
await page.evaluate(
|
||||
(cssText, modeName) => {
|
||||
document.documentElement.dataset.dsMode = modeName;
|
||||
document.documentElement.dataset.oneosTheme = modeName;
|
||||
const shell = document.querySelector('.oneos-shell');
|
||||
if (shell) shell.dataset.oneosTheme = modeName;
|
||||
|
||||
let el = document.getElementById('ds-theme-shot-skin');
|
||||
if (!el) {
|
||||
el = document.createElement('style');
|
||||
el.id = 'ds-theme-shot-skin';
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
el.textContent = cssText;
|
||||
|
||||
// 彻底隐去 ThemeLab 弹框和标注图层
|
||||
let hideEl = document.getElementById('ds-theme-shot-hide');
|
||||
if (!hideEl) {
|
||||
hideEl = document.createElement('style');
|
||||
hideEl.id = 'ds-theme-shot-hide';
|
||||
document.head.appendChild(hideEl);
|
||||
}
|
||||
hideEl.textContent = `
|
||||
.ds-theme-lab, .ds-theme-lab--collapsed, [data-annotation-id="lc-theme-lab"],
|
||||
.axhub-annotation-host, [data-axhub-annotation], .PrototypeAnnotationHost,
|
||||
iframe[src*="annotation"] {
|
||||
display: none !important; visibility: hidden !important; pointer-events: none !important;
|
||||
}
|
||||
`;
|
||||
},
|
||||
css,
|
||||
mode,
|
||||
);
|
||||
|
||||
// B) 给真正的子 iframe (Child Frame) 应用主题 css 与浅/深属性
|
||||
await childFrame.evaluate(
|
||||
(cssText, modeName) => {
|
||||
document.documentElement.dataset.dsMode = modeName;
|
||||
document.documentElement.dataset.oneosTheme = modeName;
|
||||
|
||||
let el = document.getElementById('ds-theme-shot-skin');
|
||||
if (!el) {
|
||||
el = document.createElement('style');
|
||||
el.id = 'ds-theme-shot-skin';
|
||||
document.head.appendChild(el);
|
||||
}
|
||||
el.textContent = cssText;
|
||||
|
||||
// 彻底隐去 ThemeLab 弹框和标注图层
|
||||
let hideEl = document.getElementById('ds-theme-shot-hide');
|
||||
if (!hideEl) {
|
||||
hideEl = document.createElement('style');
|
||||
hideEl.id = 'ds-theme-shot-hide';
|
||||
document.head.appendChild(hideEl);
|
||||
}
|
||||
hideEl.textContent = `
|
||||
.ds-theme-lab, .ds-theme-lab--collapsed, [data-annotation-id="lc-theme-lab"],
|
||||
.axhub-annotation-host, [data-axhub-annotation], .PrototypeAnnotationHost,
|
||||
iframe[src*="annotation"] {
|
||||
display: none !important; visibility: hidden !important; pointer-events: none !important;
|
||||
}
|
||||
`;
|
||||
|
||||
// 展开“更多筛选”呈现完整条件
|
||||
const expand = document.querySelector(
|
||||
'button.ldb-filter-toggle, button.vm-btn-link.ldb-filter-toggle',
|
||||
);
|
||||
if (expand && expand.textContent.includes('更多')) {
|
||||
expand.click();
|
||||
}
|
||||
},
|
||||
css,
|
||||
mode,
|
||||
);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 400));
|
||||
|
||||
const file = `${theme.id}-${mode}.png`;
|
||||
const outPath = path.join(outDir, file);
|
||||
|
||||
// 截取完整演示外壳页面
|
||||
await page.screenshot({ path: outPath, type: 'png', fullPage: false });
|
||||
console.log('wrote', file);
|
||||
results.push({ file, theme: theme.name, mode, category: theme.category });
|
||||
}
|
||||
}
|
||||
|
||||
const readme = `# 租赁合同(含 OneOS 侧栏 + 顶栏)· 5 主题比稿截图
|
||||
|
||||
生成时间:${new Date().toISOString().slice(0, 10)}
|
||||
底稿:演示外壳 \`/prototypes/oneos-prototype-demo/#proto=lease-contract-management\`(包含完整左侧菜单与顶部栏,外壳与内容主题/浅暗 100% 深度绑定)
|
||||
|
||||
| 文件 | 主题 | 分类 | 模式 |
|
||||
|------|------|------|------|
|
||||
${results.map((r) => `| \`${r.file}\` | ${r.theme} | ${r.category} | ${r.mode === 'light' ? '浅色' : '暗色'} |`).join('\n')}
|
||||
|
||||
## 五套主题风格
|
||||
|
||||
| 编号 | 主题 | 调性与气质 |
|
||||
|------|------|------|
|
||||
| A | Linear | 冷 · 紫靛 · 精致中后台 |
|
||||
| B | Stripe | 冷 · 品牌紫 · 金融精致 |
|
||||
| C | Airbnb | 暖 · 珊瑚红 · 出行品牌 |
|
||||
| D | Claude | 暖 · 陶土米 · 人文克制 |
|
||||
| E | Intercom | 暖 · 米白黑 · 温和 B2B(浅色主按钮用品牌橙) |
|
||||
|
||||
> 说明:外壳(侧边栏与顶栏)与内容区(租赁合同)在颜色调性与浅/暗模式下 100% 绑定一致,主题实验室悬浮弹框已移除。
|
||||
`;
|
||||
fs.writeFileSync(path.join(outDir, 'README.md'), readme);
|
||||
await browser.close();
|
||||
console.log('完成 →', outDir);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
220
scripts/snapshot-prototypes-legacy.mjs
Normal file
220
scripts/snapshot-prototypes-legacy.mjs
Normal file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 将 src/prototypes 下业务原型整套复制为 <id>-legacy,供设计规范 v2 对照。
|
||||
* 排除:oneos-prototype-nav、已有 *-legacy
|
||||
* 复制时跳过:.spec/acp、.spec/prototype-comment-assets(体积大、非页面源码)
|
||||
*
|
||||
* Usage: node scripts/snapshot-prototypes-legacy.mjs
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const prototypesDir = path.join(root, 'src/prototypes');
|
||||
const sidebarPath = path.join(root, '.axhub/make/sidebar-tree.json');
|
||||
|
||||
const SKIP_DIRS = new Set(['oneos-prototype-nav']);
|
||||
const RSYNC_EXCLUDES = [
|
||||
'--exclude=.spec/acp',
|
||||
'--exclude=.spec/prototype-comment-assets',
|
||||
'--exclude=node_modules',
|
||||
'--exclude=.DS_Store',
|
||||
];
|
||||
|
||||
function listSourceIds() {
|
||||
return fs
|
||||
.readdirSync(prototypesDir, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name)
|
||||
.filter((name) => !SKIP_DIRS.has(name) && !name.endsWith('-legacy'))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function collectTitles(node, map = new Map()) {
|
||||
if (!node) return map;
|
||||
if (Array.isArray(node)) {
|
||||
for (const child of node) collectTitles(child, map);
|
||||
return map;
|
||||
}
|
||||
if (node.kind === 'item' && node.itemKey?.startsWith('prototypes/')) {
|
||||
const id = node.itemKey.slice('prototypes/'.length);
|
||||
map.set(id, node.title || id);
|
||||
}
|
||||
if (node.children) collectTitles(node.children, map);
|
||||
if (node.prototypes) collectTitles(node.prototypes, map);
|
||||
return map;
|
||||
}
|
||||
|
||||
function walkFiles(dir, out = []) {
|
||||
if (!fs.existsSync(dir)) return out;
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
||||
walkFiles(full, out);
|
||||
} else if (/\.(tsx?|jsx?|css|md|json|html|js)$/i.test(entry.name)) {
|
||||
out.push(full);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function rewriteFileContent(content, sourceIds) {
|
||||
let next = content;
|
||||
// Longer ids first to avoid partial replaces
|
||||
const ids = [...sourceIds].sort((a, b) => b.length - a.length);
|
||||
for (const id of ids) {
|
||||
const legacy = `${id}-legacy`;
|
||||
// path segments / imports
|
||||
next = next.replaceAll(`prototypes/${id}/`, `prototypes/${legacy}/`);
|
||||
next = next.replaceAll(`prototypes/${id}'`, `prototypes/${legacy}'`);
|
||||
next = next.replaceAll(`prototypes/${id}"`, `prototypes/${legacy}"`);
|
||||
next = next.replaceAll(`../${id}/`, `../${legacy}/`);
|
||||
next = next.replaceAll(`'../${id}'`, `'../${legacy}'`);
|
||||
next = next.replaceAll(`"../${id}"`, `"../${legacy}"`);
|
||||
next = next.replaceAll(`/${id}/index.html`, `/${legacy}/index.html`);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function patchAnnotationTitles(filePath, legacyId, displayTitle) {
|
||||
if (!fs.existsSync(filePath)) return;
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
const doc = JSON.parse(raw);
|
||||
if (doc && typeof doc === 'object') {
|
||||
if (typeof doc.title === 'string') doc.title = displayTitle;
|
||||
if (typeof doc.name === 'string') doc.name = displayTitle;
|
||||
if (doc.meta && typeof doc.meta === 'object') {
|
||||
doc.meta.legacyOf = legacyId.replace(/-legacy$/, '');
|
||||
doc.meta.title = displayTitle;
|
||||
}
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(doc, null, 2)}\n`);
|
||||
}
|
||||
} catch {
|
||||
// leave as-is if not JSON object
|
||||
}
|
||||
}
|
||||
|
||||
function copyOne(id) {
|
||||
const src = path.join(prototypesDir, id);
|
||||
const dest = path.join(prototypesDir, `${id}-legacy`);
|
||||
if (fs.existsSync(dest)) {
|
||||
fs.rmSync(dest, { recursive: true, force: true });
|
||||
}
|
||||
const result = spawnSync(
|
||||
'rsync',
|
||||
['-a', ...RSYNC_EXCLUDES, `${src}/`, `${dest}/`],
|
||||
{ encoding: 'utf8' }
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
// fallback cp
|
||||
fs.cpSync(src, dest, {
|
||||
recursive: true,
|
||||
filter: (p) =>
|
||||
!p.includes(`${path.sep}.spec${path.sep}acp`) &&
|
||||
!p.includes(`${path.sep}prototype-comment-assets`) &&
|
||||
!p.includes(`${path.sep}node_modules`),
|
||||
});
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
|
||||
function addLegacySidebarFolder(sidebar, legacyEntries) {
|
||||
const folderId = 'folder-ds-v2-legacy-snapshots';
|
||||
const folder = {
|
||||
id: folderId,
|
||||
kind: 'folder',
|
||||
title: '设计规范对照·旧版',
|
||||
children: legacyEntries.map(({ id, title }) => ({
|
||||
id: `item:prototypes:${id}`,
|
||||
kind: 'item',
|
||||
title,
|
||||
itemKey: `prototypes/${id}`,
|
||||
})),
|
||||
};
|
||||
|
||||
// nav-menu 只同步 title === 'OneOS' 的分区,旧版必须挂在其下
|
||||
const list = sidebar.prototypes || [];
|
||||
const oneos = list.find((n) => n.kind === 'folder' && n.title === 'OneOS');
|
||||
if (!oneos) {
|
||||
throw new Error('sidebar-tree.json 中未找到 OneOS 分区');
|
||||
}
|
||||
oneos.children = Array.isArray(oneos.children) ? oneos.children : [];
|
||||
// 移除顶层误挂的同名文件夹
|
||||
sidebar.prototypes = list.filter((n) => n.id !== folderId);
|
||||
const idx = oneos.children.findIndex((n) => n.id === folderId);
|
||||
if (idx >= 0) oneos.children[idx] = folder;
|
||||
else oneos.children.push(folder);
|
||||
sidebar.updatedAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
function main() {
|
||||
const sourceIds = listSourceIds();
|
||||
const sidebar = JSON.parse(fs.readFileSync(sidebarPath, 'utf8'));
|
||||
const titleMap = collectTitles(sidebar);
|
||||
|
||||
console.log(`将复制 ${sourceIds.length} 个原型 → *-legacy`);
|
||||
|
||||
for (const id of sourceIds) {
|
||||
process.stdout.write(` copy ${id} ... `);
|
||||
copyOne(id);
|
||||
console.log('ok');
|
||||
}
|
||||
|
||||
const legacyIds = sourceIds.map((id) => `${id}-legacy`);
|
||||
const allSourceAndLegacy = new Set([...sourceIds, ...legacyIds]);
|
||||
|
||||
// Rewrite cross-refs inside legacy trees to stay frozen against live restyles
|
||||
for (const legacyId of legacyIds) {
|
||||
const dir = path.join(prototypesDir, legacyId);
|
||||
const baseId = legacyId.replace(/-legacy$/, '');
|
||||
const title = `【旧版】${titleMap.get(baseId) || baseId}`;
|
||||
const files = walkFiles(dir);
|
||||
for (const file of files) {
|
||||
const before = fs.readFileSync(file, 'utf8');
|
||||
let after = rewriteFileContent(before, sourceIds);
|
||||
if (after !== before) fs.writeFileSync(file, after);
|
||||
}
|
||||
patchAnnotationTitles(path.join(dir, 'annotation-source.json'), legacyId, title);
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'LEGACY.md'),
|
||||
`# ${title}
|
||||
|
||||
本目录为设计规范 v2 套用前的**冻结对照副本**(${new Date().toISOString().slice(0, 10)})。
|
||||
|
||||
- 源原型:\`src/prototypes/${baseId}/\`
|
||||
- 请勿在此目录继续改业务;正式改造只在源目录进行
|
||||
- 已跳过复制:\`.spec/acp\`、\`.spec/prototype-comment-assets\`(批注大图/会话)
|
||||
- 交叉引用已尽量改写为同套 \`*-legacy\`,避免引用被改版后的正式样式
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
const legacyEntries = sourceIds.map((id) => ({
|
||||
id: `${id}-legacy`,
|
||||
title: `【旧版】${titleMap.get(id) || id}`,
|
||||
}));
|
||||
addLegacySidebarFolder(sidebar, legacyEntries);
|
||||
fs.writeFileSync(sidebarPath, `${JSON.stringify(sidebar, null, 2)}\n`);
|
||||
console.log(`已更新 sidebar-tree.json(${legacyEntries.length} 个旧版入口)`);
|
||||
|
||||
const sync = spawnSync(
|
||||
'npm',
|
||||
['run', 'nav:sync', '--', '--note', '注册设计规范 v2 前整套 legacy 对照副本'],
|
||||
{ cwd: root, encoding: 'utf8', shell: true }
|
||||
);
|
||||
console.log(sync.stdout || '');
|
||||
if (sync.stderr) console.error(sync.stderr);
|
||||
if (sync.status !== 0) {
|
||||
console.error('nav:sync 失败,请手动执行 npm run nav:sync');
|
||||
process.exit(sync.status || 1);
|
||||
}
|
||||
|
||||
console.log('完成。');
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user