新增 AutoRDO 需求清洗工作台与消息中枢,迭代 OneOS V2 设计规范及租赁合同/工作台/车辆等原型,同步云效技能与导航注册;并归档一批 legacy 原型快照。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
王冕
2026-07-28 15:50:35 +08:00
parent b14425da5a
commit 5d51a6bf7a
1393 changed files with 488042 additions and 10046 deletions

View 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';

View 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();

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);
});

View 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);
});

View 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);

View 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);

View 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);
});

View 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);
});

View 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);
});

View 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();