Regression / regression (push) Canceled after 0s
## 品牌标识(本次会话) 起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」, 那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」, 移动端站已是「K」;移动端文档站则完全没有 favicon)。 - 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边; 描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」 - 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark); 顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28)) - 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg) - 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留) - 移动端 7 个模板补 favicon(此前计数 0) 验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/ theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致; 两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。 ## 并行会话成果(本次一并入库) - 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具 + IconPreview 预览页 + ICON-SPEC.md 冻结规格 - 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁 - PC 组件:103 个大后台组件 / 组件11 批次 - uni-app:PC 端试点 + 移动端端实现 + 真实编译验证 ## 工程 - .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库) - CHANGELOG 补品牌标识条目 - ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
397 lines
58 KiB
JavaScript
397 lines
58 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Add the PC components identified from the Element UI sidebar.
|
||
*
|
||
* This generator is intentionally limited to the 24 slugs in DEFS. It refuses
|
||
* existing slugs or source paths, writes through a temporary file, and keeps
|
||
* generated five-end sources readable so the normal build/test chain consumes
|
||
* ordinary repository files rather than a runtime registry.
|
||
*/
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||
const LIB = path.join(ROOT, '.design_library', 'kole-ui');
|
||
const COMPONENTS = path.join(LIB, 'components');
|
||
const FRAMEWORKS = path.join(ROOT, 'frameworks');
|
||
const ROOT_SPEC = path.join(ROOT, '组件11.txt');
|
||
const LIB_SPEC = path.join(LIB, 'specs', '组件11.txt');
|
||
|
||
const p = (name, type = 'String', def = "''") => ({ name, type, def });
|
||
const DEFS = [
|
||
{ slug: 'layout', prefix: 'Layout', name: '布局 Layout', category: 'general', kind: 'layout', usage: '搭建后台页面的侧边栏、页头、内容区与页脚,并支持折叠和响应式排列。', props: [p('direction', 'String', "'row'"), p('collapsed', 'Boolean', 'false'), p('gap', 'String', "'16px'")], events: [] },
|
||
{ slug: 'container', prefix: 'Container', name: '布局容器 Container', category: 'general', kind: 'container', usage: '组合页面级 Header、Sider、Main 与 Footer,统一内容宽度和间距。', props: [p('direction', 'String', "'column'"), p('width', 'String', "'100%'") , p('gap', 'String', "'16px'")], events: [] },
|
||
{ slug: 'typography', prefix: 'Typography', name: '排版 Typography', category: 'general', kind: 'typography', usage: '统一标题、正文、辅助文字、数字和省略文本的层级与阅读体验。', props: [p('variant', 'String', "'body'"), p('text', 'String', "'示例文本'"), p('align', 'String', "'left'"), p('maxLength', 'Number', '0')], events: ['copy'] },
|
||
{ slug: 'icon', prefix: 'Icon', name: '图标 Icon', category: 'general', kind: 'icon', usage: '为菜单、状态、操作和提示提供一致的图形语义,可使用文本或 SVG 图标名。', props: [p('icon', 'String', "'★'"), p('size', 'String', "'20px'"), p('color', 'String', "'brand'")], events: ['click'] },
|
||
{ slug: 'link', prefix: 'Link', name: '文字链接 Link', category: 'general', kind: 'link', usage: '跳转详情、下载文件或打开外部地址,提供主次和禁用状态。', props: [p('href', 'String', "'#'"), p('target', 'String', "'_self'"), p('variant', 'String', "'primary'"), p('disabled', 'Boolean', 'false')], events: ['click'] },
|
||
{ slug: 'radio', prefix: 'Radio', name: '单选框 Radio', category: 'input', kind: 'radio', usage: '在互斥选项中选择一个结果,例如审批结论、显示模式或筛选维度。', props: [p('modelValue', 'String', "'a'"), p('options', 'Array', '() => []'), p('disabled', 'Boolean', 'false')], events: ['update:modelValue', 'change'] },
|
||
{ slug: 'checkbox', prefix: 'Checkbox', name: '复选框 Checkbox', category: 'input', kind: 'checkbox', usage: '选择多个权限、筛选条件或数据记录,支持全选和半选状态。', props: [p('modelValue', 'Array', '() => []'), p('options', 'Array', '() => []'), p('disabled', 'Boolean', 'false')], events: ['update:modelValue', 'change'] },
|
||
{ slug: 'inputnumber', prefix: 'InputNumber', name: '计数器 InputNumber', category: 'input', kind: 'inputnumber', usage: '输入数量、金额或排序权重,支持最小值、最大值、步长和精度。', props: [p('value', 'Number', '1'), p('min', 'Number', '0'), p('max', 'Number', '100'), p('step', 'Number', '1'), p('precision', 'Number', '0'), p('disabled', 'Boolean', 'false')], events: ['change'] },
|
||
{ slug: 'switch', prefix: 'Switch', name: '开关 Switch', category: 'input', kind: 'switch', usage: '即时启停配置项或业务状态,例如启用通知、公开数据和自动同步。', props: [p('checked', 'Boolean', 'true'), p('disabled', 'Boolean', 'false'), p('size', 'String', "'default'")], events: ['change', 'update:modelValue'] },
|
||
{ slug: 'timepicker', prefix: 'TimePicker', name: '时间选择器 TimePicker', category: 'input', kind: 'timepicker', usage: '设置营业时段、任务执行时间或提醒时间,采用无运行时依赖的原生时间控件。', props: [p('value', 'String', "'09:30'"), p('format', 'String', "'HH:mm'")], events: ['change'] },
|
||
{ slug: 'datepicker', prefix: 'DatePicker', name: '日期选择器 DatePicker', category: 'input', kind: 'datepicker', usage: '选择单日或起止日期,适用于查询条件、有效期和排期设置。', props: [p('value', 'String', "'2026-09-20'"), p('range', 'Boolean', 'false'), p('format', 'String', "'YYYY-MM-DD'"), p('presets', 'Array', '() => []')], events: ['change'] },
|
||
{ slug: 'datetimepicker', prefix: 'DateTimePicker', name: '日期时间选择器 DateTimePicker', category: 'input', kind: 'datetimepicker', usage: '设置精确到时分的预约、生效或发布时刻。', props: [p('value', 'String', "'2026-09-20T09:30'"), p('format', 'String', "'YYYY-MM-DD HH:mm'")], events: ['change'] },
|
||
{ slug: 'form', prefix: 'Form', name: '表单 Form', category: 'input', kind: 'form', usage: '统一字段布局、必填标识、校验错误和提交/重置流程。', props: [p('fields', 'Array', '() => []'), p('label', 'String', "'名称'"), p('required', 'Boolean', 'true'), p('error', 'String', "''")], events: ['submit', 'reset'] },
|
||
{ slug: 'pagination', prefix: 'Pagination', name: '分页 Pagination', category: 'display', kind: 'pagination', usage: '在表格或卡片列表底部浏览大数据集,支持页码、总数、每页条数和跳转。', props: [p('total', 'Number', '128'), p('page', 'Number', '1'), p('pageSize', 'Number', '10'), p('size', 'String', "'default'")], events: ['pagechange', 'sizechange'] },
|
||
{ slug: 'badge', prefix: 'Badge', name: '标记 Badge', category: 'display', kind: 'badge', usage: '显示未读数、待办数、小红点或紧贴对象的状态标记。', props: [p('count', 'Number', '8'), p('status', 'String', "'error'"), p('variant', 'String', "'dot'")], events: [] },
|
||
{ slug: 'avatar', prefix: 'Avatar', name: '头像 Avatar', category: 'display', kind: 'avatar', usage: '展示成员、审批人或操作者,支持文字、图片、尺寸和在线状态点。', props: [p('src', 'String', "''"), p('avatar', 'String', "'林'"), p('size', 'String', "'40px'"), p('status', 'String', "'online'")], events: ['click'] },
|
||
{ slug: 'descriptions', prefix: 'Descriptions', name: '描述列表 Descriptions', category: 'display', kind: 'descriptions', usage: '以只读方式展示订单、客户、员工等对象的结构化详情,支持 1–4 列。', props: [p('fields', 'Array', '() => []'), p('columns', 'Number', '3'), p('bordered', 'Boolean', 'true')], events: [] },
|
||
{ slug: 'alert', prefix: 'Alert', name: '警告 Alert', category: 'feedback', kind: 'alert', usage: '在页面内持续展示风险、限制、成功结果或操作说明。', props: [p('type', 'String', "'info'"), p('title', 'String', "'系统提示'"), p('description', 'String', "'请检查当前配置后继续。'"), p('closable', 'Boolean', 'true')], events: ['close'] },
|
||
{ slug: 'pageheader', prefix: 'PageHeader', name: '页头 PageHeader', category: 'navigation', kind: 'pageheader', usage: '在详情页提供返回、标题、副标题和页面级操作。', props: [p('title', 'String', "'用户详情'"), p('description', 'String', "'查看并编辑用户资料'"), p('secondaryText', 'String', "'返回列表'")], events: ['back'] },
|
||
{ slug: 'tooltip', prefix: 'Tooltip', name: '文字提示 Tooltip', category: 'feedback', kind: 'tooltip', usage: '解释图标、截断内容或术语,适合短文本的 hover、focus 和键盘提示。', props: [p('tooltip', 'String', "'这是补充说明'"), p('placement', 'String', "'top'"), p('trigger', 'String', "'hover'")], events: ['show', 'hide'] },
|
||
{ slug: 'popover', prefix: 'Popover', name: '弹出框 Popover', category: 'feedback', kind: 'popover', usage: '展示比 Tooltip 更丰富的就近信息、表单或操作,支持点击和 Escape 关闭。', props: [p('content', 'String', "'这里可以放解释、统计或操作。'"), p('placement', 'String', "'bottom'"), p('trigger', 'String', "'click'"), p('open', 'Boolean', 'false')], events: ['open', 'close'] },
|
||
{ slug: 'divider', prefix: 'Divider', name: '分割线 Divider', category: 'display', kind: 'divider', usage: '分隔表单段落、菜单组或内容块,支持水平/垂直方向和带文字分隔。', props: [p('orientation', 'String', "'horizontal'"), p('title', 'String', "'更多信息'"), p('variant', 'String', "'solid'")], events: [] },
|
||
{ slug: 'infinitescroll', prefix: 'InfiniteScroll', name: '无限滚动 InfiniteScroll', category: 'display', kind: 'infinitescroll', usage: '逐批加载动态流或长列表,明确展示加载中、完成和可重试状态。', props: [p('items', 'Array', '() => []'), p('loading', 'Boolean', 'false'), p('status', 'String', "'more'"), p('height', 'String', "'180px'")], events: ['load'] },
|
||
{ slug: 'drawer', prefix: 'Drawer', name: '抽屉 Drawer', category: 'feedback', kind: 'drawer', usage: '不离开列表页,侧向查看或编辑详情,默认从右侧滑出并带遮罩。', props: [p('open', 'Boolean', 'false'), p('title', 'String', "'编辑资料'"), p('placement', 'String', "'right'"), p('maskClosable', 'Boolean', 'true')], events: ['open', 'close', 'confirm'] }
|
||
];
|
||
|
||
const bySlug = new Map(DEFS.map((d) => [d.slug, d]));
|
||
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
const jsQuote = (s) => JSON.stringify(String(s));
|
||
|
||
function atomicWrite(file, content) {
|
||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||
const tmp = `${file}.tmp-${process.pid}`;
|
||
fs.writeFileSync(tmp, content.replace(/\r\n/g, '\n'), 'utf8');
|
||
fs.renameSync(tmp, file);
|
||
}
|
||
function assertFree(idx) {
|
||
const existing = new Set(idx.components.map((c) => c.slug));
|
||
for (const d of DEFS) {
|
||
const registered = existing.has(d.slug);
|
||
const allSources = ['.css', '.html', '.jsx', '.vue2.vue', '.vue3.vue'].map((ext) => path.join(FRAMEWORKS, d.prefix + ext));
|
||
const sourceCount = allSources.filter((file) => fs.existsSync(file)).length;
|
||
if (registered && sourceCount !== 5) throw new Error(`refusing partial component: ${d.slug} has ${sourceCount}/5 source files`);
|
||
if (!registered && sourceCount) throw new Error(`refusing partial source set: ${d.slug} has ${sourceCount}/5 source files`);
|
||
}
|
||
}
|
||
|
||
function contract(d) {
|
||
const dims = d.props.filter((x) => ['variant', 'size', 'type', 'placement', 'orientation', 'status', 'range', 'trigger'].includes(x.name));
|
||
return {
|
||
schemaVersion: 2,
|
||
sourceKind: 'structured-spec',
|
||
confidence: 'high',
|
||
slug: d.slug,
|
||
name: d.name,
|
||
semanticTypeCandidates: [d.kind, d.kind === 'form' ? 'form' : 'widget'],
|
||
variantDimensions: dims.map((x) => ({ name: x.name, values: variantValues(d, x.name) })),
|
||
representativeVariants: variantValues(d, 'variant').map((v) => ({ variant: v, label: `${d.name.split(' ')[0]} · ${v}` })),
|
||
anatomy: anatomy(d),
|
||
structurePatterns: { root: `kole-${d.slug}`, states: 'default / hover / focus / disabled(按组件语义展示)', responsive: '使用 CSS 令牌与 flex/grid,宿主可覆盖容器宽度' },
|
||
usageHints: [d.usage, '五端保持相同的类名、状态和基础语义。', '交互控件优先使用原生元素,受控值变化通过 change 或 v-model 事件传递。'],
|
||
doNotInvent: ['不把本组件扩展为外部依赖提供的完整业务流程。', '未声明的高级主题、数据请求和服务端校验不由组件自行推断。'],
|
||
unknowns: ['宿主项目的国际化文案、数据请求和权限策略由接入方决定。']
|
||
};
|
||
}
|
||
function variantValues(d, name) {
|
||
const common = {
|
||
variant: ['default', 'primary', 'compact'], size: ['small', 'default', 'large'], type: ['info', 'success', 'warning', 'error'],
|
||
placement: ['top', 'right', 'bottom'], orientation: ['horizontal', 'vertical'], status: ['more', 'loading', 'done'],
|
||
range: ['single', 'range'], trigger: ['hover', 'click']
|
||
};
|
||
if (common[name]) return common[name];
|
||
if (d.kind === 'layout' || d.kind === 'container') return ['row', 'column'];
|
||
return ['default', 'active', 'disabled'];
|
||
}
|
||
function anatomy(d) {
|
||
const base = { root: `kole-${d.slug}`, content: `${d.name.split(' ')[0]} 主体内容`, state: '状态类 is-* 与 ARIA 属性同步' };
|
||
if (['tooltip', 'popover', 'drawer', 'alert'].includes(d.kind)) base.overlay = '提示/面板/状态容器与关闭控件';
|
||
if (['radio', 'checkbox', 'switch', 'inputnumber', 'timepicker', 'datepicker', 'datetimepicker', 'form'].includes(d.kind)) base.control = '原生表单控件或可聚焦操作控件';
|
||
return base;
|
||
}
|
||
|
||
function cssFor(d) {
|
||
const s = `kole-${d.slug}`;
|
||
return `/* Kole UI ${d.name} · generated from 组件11.txt */
|
||
.${s} { box-sizing: border-box; font-family: var(--kole-font-family); color: var(--kole-color-text-body); }
|
||
.${s} *, .${s} *::before, .${s} *::after { box-sizing: border-box; }
|
||
.${s}-surface, .${s}-panel { background: var(--kole-color-card-bg); border: 1px solid var(--kole-color-border); border-radius: var(--kole-radius-medium); box-shadow: var(--kole-shadow-low); }
|
||
.${s}-title { color: var(--kole-color-text-title); font-weight: var(--kole-type-h2-weight); }
|
||
.${s}-desc { color: var(--kole-color-text-secondary); font-size: var(--kole-type-caption-size); line-height: var(--kole-type-caption-height); }
|
||
.${s} button, .${s} input, .${s} select, .${s} a { font: inherit; }
|
||
.${s} button, .${s} a { cursor: pointer; }
|
||
.${s} button:focus-visible, .${s} a:focus-visible, .${s} input:focus-visible, .${s} select:focus-visible, .${s}-control:focus-visible { outline: 2px solid var(--kole-color-brand); outline-offset: 2px; }
|
||
.${s}-primary { color: var(--kole-color-text-inverse); background: var(--kole-color-brand); border-color: var(--kole-color-brand); }
|
||
.${s}-secondary { color: var(--kole-color-brand); background: var(--kole-color-brand-bg); border-color: var(--kole-color-brand); }
|
||
.${s}-compact { font-size: var(--kole-type-caption-size); padding: var(--kole-space-4) var(--kole-space-8); }
|
||
.${s}-disabled, .${s} [disabled] { cursor: not-allowed; opacity: .55; }
|
||
.${s}-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--kole-space-8); }
|
||
.${s}-button { min-height: var(--kole-control-height-default); padding: 0 var(--kole-space-12); border: 1px solid var(--kole-color-border); border-radius: var(--kole-radius-base); background: var(--kole-color-card-bg); color: var(--kole-color-text-body); }
|
||
.${s}-control { min-height: var(--kole-control-height-default); border: 1px solid var(--kole-color-border); border-radius: var(--kole-radius-base); background: var(--kole-color-card-bg); color: var(--kole-color-text-body); padding: 0 var(--kole-space-8); }
|
||
.${s}-item { display: inline-flex; align-items: center; gap: var(--kole-space-8); min-height: var(--kole-control-height-default); }
|
||
.${s}-panel { padding: var(--kole-space-16); min-width: 220px; }
|
||
.${s}-layout { display: grid; grid-template-columns: 180px 1fr; min-height: 180px; gap: var(--kole-space-16); }
|
||
.${s}-layout.is-collapsed { grid-template-columns: 64px 1fr; }
|
||
.${s}-header, .${s}-footer { padding: var(--kole-space-12) var(--kole-space-16); background: var(--kole-color-table-header-bg); }
|
||
.${s}-main { padding: var(--kole-space-16); }
|
||
.${s}-vertical { flex-direction: column; align-items: stretch; }
|
||
.${s}-horizontal { flex-direction: row; }
|
||
.${s}-input-row { display: flex; align-items: center; gap: var(--kole-space-4); }
|
||
.${s}-number { width: 96px; text-align: center; }
|
||
.${s}-switch { display: inline-flex; width: 44px; height: 22px; padding: 2px; border-radius: 999px; background: var(--kole-color-border-strong); align-items: center; transition: background var(--kole-duration-fast); }
|
||
.${s}-switch::after { content: ''; width: 18px; height: 18px; border-radius: 50%; background: var(--kole-color-card-bg); box-shadow: var(--kole-shadow-low); transition: transform var(--kole-duration-fast); }
|
||
.${s}-switch.is-on { background: var(--kole-color-brand); }
|
||
.${s}-switch.is-on::after { transform: translateX(22px); }
|
||
.${s}-option { display: inline-flex; align-items: center; gap: var(--kole-space-4); }
|
||
.${s}-form { display: grid; gap: var(--kole-space-12); max-width: 420px; }
|
||
.${s}-field { display: grid; gap: var(--kole-space-4); }
|
||
.${s}-error { color: var(--kole-color-error); font-size: var(--kole-type-caption-size); }
|
||
.${s}-pager { display: flex; align-items: center; flex-wrap: wrap; gap: var(--kole-space-4); }
|
||
.${s}-page, .${s}-action { min-width: 32px; min-height: 32px; border: 1px solid var(--kole-color-border); background: var(--kole-color-card-bg); border-radius: var(--kole-radius-base); }
|
||
.${s}-page.is-current { color: var(--kole-color-text-inverse); background: var(--kole-color-brand); border-color: var(--kole-color-brand); }
|
||
.${s}-badge-host { display: inline-flex; position: relative; padding: var(--kole-space-12); }
|
||
.${s}-badge { position: absolute; top: 0; right: 0; transform: translate(35%, -35%); min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; color: var(--kole-color-text-inverse); background: var(--kole-color-error); font-size: 11px; line-height: 18px; text-align: center; }
|
||
.${s}-badge.is-success { background: var(--kole-color-success); }
|
||
.${s}-badge.is-info { background: var(--kole-color-info); }
|
||
.${s}-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--kole-color-error); }
|
||
.${s}-avatar { display: inline-flex; width: 40px; height: 40px; align-items: center; justify-content: center; overflow: hidden; border-radius: 50%; color: var(--kole-color-text-inverse); background: var(--kole-color-brand); font-weight: 600; }
|
||
.${s}-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||
.${s}-avatar.is-offline { filter: grayscale(1); opacity: .65; }
|
||
.${s}-status { width: 9px; height: 9px; margin-left: -10px; margin-top: 28px; border: 2px solid var(--kole-color-card-bg); border-radius: 50%; background: var(--kole-color-success); }
|
||
.${s}-table { width: 100%; border-collapse: collapse; }
|
||
.${s}-table th, .${s}-table td { padding: var(--kole-space-8) var(--kole-space-12); border-bottom: 1px solid var(--kole-color-border); text-align: start; }
|
||
.${s}-table th { background: var(--kole-color-table-header-bg); color: var(--kole-color-text-secondary); }
|
||
.${s}-alert { display: flex; align-items: flex-start; gap: var(--kole-space-8); padding: var(--kole-space-12) var(--kole-space-16); border: 1px solid var(--kole-color-border); border-radius: var(--kole-radius-base); background: var(--kole-color-brand-bg); }
|
||
.${s}-alert.is-success { background: var(--kole-color-tag-green-bg); }
|
||
.${s}-alert.is-warning { background: color-mix(in srgb, var(--kole-color-warning) 10%, var(--kole-color-card-bg)); }
|
||
.${s}-alert.is-error { background: color-mix(in srgb, var(--kole-color-error) 10%, var(--kole-color-card-bg)); }
|
||
.${s}-close { margin-inline-start: auto; border: 0; background: transparent; color: var(--kole-color-text-secondary); }
|
||
.${s}-pageheader { display: flex; align-items: center; gap: var(--kole-space-12); padding: var(--kole-space-12) var(--kole-space-16); border-bottom: 1px solid var(--kole-color-border); }
|
||
.${s}-pageheader-main { flex: 1; }
|
||
.${s}-link { color: var(--kole-color-brand); text-decoration: none; }
|
||
.${s}-link.is-disabled { pointer-events: none; color: var(--kole-color-text-disabled); }
|
||
.${s}-tooltip-wrap, .${s}-popover-wrap { position: relative; display: inline-flex; }
|
||
.${s}-tip, .${s}-popover { position: absolute; z-index: 10; top: calc(100% + var(--kole-space-8)); left: 0; min-width: 160px; max-width: 260px; padding: var(--kole-space-8) var(--kole-space-12); border-radius: var(--kole-radius-base); box-shadow: var(--kole-shadow-medium); }
|
||
.${s}-tip { color: var(--kole-color-text-inverse); background: var(--kole-color-tooltip-bg); }
|
||
.${s}-popover { color: var(--kole-color-text-body); background: var(--kole-color-card-bg); border: 1px solid var(--kole-color-border); }
|
||
.${s}-tip[hidden], .${s}-popover[hidden], .${s}-drawer[hidden] { display: none; }
|
||
.${s}-divider { border: 0; border-top: 1px solid var(--kole-color-border); margin: var(--kole-space-16) 0; }
|
||
.${s}-divider.is-vertical { width: 1px; height: 32px; margin: 0 var(--kole-space-8); border-top: 0; border-left: 1px solid var(--kole-color-border); }
|
||
.${s}-scroll { height: 180px; overflow: auto; padding: var(--kole-space-8); border: 1px solid var(--kole-color-border); border-radius: var(--kole-radius-base); }
|
||
.${s}-scroll-item { padding: var(--kole-space-8); border-bottom: 1px solid var(--kole-color-border); }
|
||
.${s}-drawer-mask { position: fixed; inset: 0; z-index: 20; background: var(--kole-color-mask); }
|
||
.${s}-drawer { position: fixed; z-index: 21; top: 0; right: 0; width: 360px; max-width: 90vw; height: 100vh; padding: var(--kole-space-16); background: var(--kole-color-card-bg); box-shadow: var(--kole-shadow-high); }
|
||
.${s}-drawer-footer { position: absolute; right: var(--kole-space-16); bottom: var(--kole-space-16); left: var(--kole-space-16); display: flex; justify-content: flex-end; gap: var(--kole-space-8); }
|
||
@media (prefers-reduced-motion: reduce) { .${s} *, .${s} *::before, .${s} *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }
|
||
`;
|
||
}
|
||
|
||
function demoMarkup(d) {
|
||
const s = `kole-${d.slug}`;
|
||
const common = {
|
||
layout: `<div class="${s}-layout"><aside class="${s}-surface">导航<br>用户<br>设置</aside><section class="${s}-surface"><div class="${s}-header">页头</div><div class="${s}-main">内容区</div><div class="${s}-footer">页脚</div></section></div>`,
|
||
container: `<div class="${s} ${s}-surface" style="max-width:720px"><div class="${s}-header">Header</div><div class="${s}-main">Main content</div><div class="${s}-footer">Footer</div></div>`,
|
||
typography: `<div><h2 class="${s}-title">页面标题</h2><p class="${s}-desc">辅助说明文字,展示信息层级和阅读节奏。</p><p class="${s}-compact">长文本省略示例:订单提交后将进入审核流程……</p></div>`,
|
||
icon: `<button class="${s}-button ${s}-primary" type="button" aria-label="收藏"><span class="${s}-icon" aria-hidden="true">★</span> 收藏</button><span class="${s}-icon" role="img" aria-label="成功">✓</span>`,
|
||
link: `<a class="${s}-link" href="#detail">查看详情</a><a class="${s}-link ${s}-secondary" href="#download">下载文件</a>`,
|
||
radio: `<label class="${s}-option"><input type="radio" name="${s}-demo" value="a" checked> 选项 A</label><label class="${s}-option"><input type="radio" name="${s}-demo" value="b"> 选项 B</label>`,
|
||
checkbox: `<label class="${s}-option"><input type="checkbox" checked> 邮件通知</label><label class="${s}-option"><input type="checkbox"> 短信通知</label>`,
|
||
inputnumber: `<div class="${s}-input-row"><button class="${s}-action" type="button" data-minus aria-label="减少">−</button><input class="${s}-control ${s}-number" type="number" value="3" min="0" max="99" aria-label="数量"><button class="${s}-action" type="button" data-plus aria-label="增加">+</button></div>`,
|
||
switch: `<button class="${s}-switch is-on" type="button" role="switch" aria-checked="true" aria-label="启用通知" data-toggle></button><span>启用通知</span>`,
|
||
timepicker: `<label class="${s}-item">执行时间 <input class="${s}-control" type="time" value="09:30" aria-label="执行时间"></label>`,
|
||
datepicker: `<label class="${s}-item">日期 <input class="${s}-control" type="date" value="2026-09-20" aria-label="日期"></label><button class="${s}-button ${s}-secondary" type="button">本月</button>`,
|
||
datetimepicker: `<label class="${s}-item">生效时间 <input class="${s}-control" type="datetime-local" value="2026-09-20T09:30" aria-label="生效时间"></label>`,
|
||
form: `<form class="${s}-form" id="${s}-form"><label class="${s}-field"><span>名称 <b aria-hidden="true">*</b></span><input class="${s}-control" name="name" required value="示例项目"></label><span class="${s}-error" hidden>请输入名称</span><div class="${s}-row"><button class="${s}-button ${s}-primary" type="submit">提交</button><button class="${s}-button" type="reset">重置</button></div></form>`,
|
||
pagination: `<div class="${s}-pager" aria-label="分页"><button class="${s}-page" type="button" data-prev>上一页</button><button class="${s}-page is-current" type="button">1</button><button class="${s}-page" type="button">2</button><button class="${s}-page" type="button" data-next>下一页</button><span class="${s}-desc">共 128 条</span></div>`,
|
||
badge: `<span class="${s}-badge-host"><span class="${s}-surface" style="padding:12px">消息</span><span class="${s}-badge" aria-label="8 条未读">8</span></span><span class="${s}-badge-host"><span class="${s}-surface" style="padding:12px">状态</span><span class="${s}-dot" aria-label="异常"></span></span>`,
|
||
avatar: `<span class="${s}-item"><span class="${s}-avatar" aria-label="林工">林</span><span class="${s}-status" aria-label="在线"></span></span><span class="${s}-avatar ${s}-avatar is-offline" aria-label="陈工">陈</span>`,
|
||
descriptions: `<table class="${s}-table"><tbody><tr><th>订单号</th><td>KOLE-20260920</td><th>状态</th><td>处理中</td></tr><tr><th>负责人</th><td>林工</td><th>更新时间</th><td>今天 09:30</td></tr></tbody></table>`,
|
||
alert: `<div class="${s}-alert" role="alert"><span><strong>系统提示</strong><br><span class="${s}-desc">请检查当前配置后继续。</span></span><button class="${s}-close" type="button" aria-label="关闭" data-close>×</button></div>`,
|
||
pageheader: `<header class="${s}-pageheader"><button class="${s}-button" type="button" data-back>← 返回</button><div class="${s}-pageheader-main"><strong class="${s}-title">用户详情</strong><div class="${s}-desc">查看并编辑用户资料</div></div><button class="${s}-button ${s}-primary" type="button">保存</button></header>`,
|
||
tooltip: `<div class="${s}-tooltip-wrap"><button class="${s}-button ${s}-primary" type="button" aria-describedby="${s}-tip" data-tip>悬停或聚焦</button><span class="${s}-tip" id="${s}-tip" role="tooltip" hidden>这是补充说明</span></div>`,
|
||
popover: `<div class="${s}-popover-wrap"><button class="${s}-button ${s}-primary" type="button" aria-expanded="false" aria-controls="${s}-pop" data-pop>查看信息</button><div class="${s}-popover" id="${s}-pop" role="dialog" hidden>这里可以放解释、统计或操作。</div></div>`,
|
||
divider: `<div style="width:100%"><div class="${s}-desc">基础信息</div><hr class="${s}-divider"><div class="${s}-desc">安全设置</div></div><div class="${s}-divider is-vertical" role="separator" aria-label="分隔"></div>`,
|
||
infinitescroll: `<div class="${s}-scroll" role="feed" aria-label="动态列表"><div class="${s}-scroll-item">活动记录 1</div><div class="${s}-scroll-item">活动记录 2</div><button class="${s}-button" type="button" data-load>加载更多</button></div>`,
|
||
drawer: `<button class="${s}-button ${s}-primary" type="button" data-open>打开抽屉</button><div class="${s}-drawer-mask" hidden data-mask></div><aside class="${s}-drawer" hidden role="dialog" aria-modal="true" aria-labelledby="${s}-title"><h3 id="${s}-title" class="${s}-title">编辑资料</h3><p class="${s}-desc">在当前页面侧向编辑,不丢失列表上下文。</p><div class="${s}-drawer-footer"><button class="${s}-button" type="button" data-close>取消</button><button class="${s}-button ${s}-primary" type="button" data-confirm>确定</button></div></aside>`
|
||
};
|
||
return common[d.kind] || `<div class="${s}-surface"><strong class="${s}-title">${esc(d.name)}</strong><p class="${s}-desc">${esc(d.usage)}</p></div>`;
|
||
}
|
||
function demoScript(d) {
|
||
const s = `kole-${d.slug}`;
|
||
const lines = [];
|
||
if (['switch'].includes(d.kind)) lines.push(`document.querySelector('.${s} [data-toggle]').addEventListener('click', function () { var on = this.getAttribute('aria-checked') !== 'true'; this.setAttribute('aria-checked', String(on)); this.classList.toggle('is-on', on); });`);
|
||
if (d.kind === 'inputnumber') lines.push(`var n = document.querySelector('.${s}-number'); document.querySelector('.${s} [data-minus]').addEventListener('click', function () { n.stepDown(); }); document.querySelector('.${s} [data-plus]').addEventListener('click', function () { n.stepUp(); });`);
|
||
if (d.kind === 'form') lines.push(`document.getElementById('${s}-form').addEventListener('submit', function (e) { e.preventDefault(); this.querySelector('.${s}-error').hidden = this.elements.name.value.trim() !== ''; });`);
|
||
if (d.kind === 'alert') lines.push(`document.querySelector('.${s} [data-close]').addEventListener('click', function () { this.closest('.${s}-alert').hidden = true; });`);
|
||
if (d.kind === 'pageheader') lines.push(`document.querySelector('.${s} [data-back]').addEventListener('click', function () { this.setAttribute('aria-label', '已返回'); });`);
|
||
if (d.kind === 'tooltip') lines.push(`var tip = document.getElementById('${s}-tip'); var trigger = document.querySelector('.${s} [data-tip]'); trigger.addEventListener('mouseenter', function () { tip.hidden = false; }); trigger.addEventListener('mouseleave', function () { tip.hidden = true; }); trigger.addEventListener('focus', function () { tip.hidden = false; }); trigger.addEventListener('blur', function () { tip.hidden = true; });`);
|
||
if (d.kind === 'popover') lines.push(`var pop = document.getElementById('${s}-pop'); document.querySelector('.${s} [data-pop]').addEventListener('click', function () { pop.hidden = !pop.hidden; this.setAttribute('aria-expanded', String(!pop.hidden)); });`);
|
||
if (d.kind === 'infinitescroll') lines.push(`document.querySelector('.${s} [data-load]').addEventListener('click', function () { var item = document.createElement('div'); item.className = '${s}-scroll-item'; item.textContent = '活动记录 3'; this.before(item); this.textContent = '已加载'; this.disabled = true; });`);
|
||
if (d.kind === 'drawer') lines.push(`var drawer = document.querySelector('.${s}-drawer'); var mask = document.querySelector('.${s}-drawer-mask'); function closeDrawer() { drawer.hidden = true; mask.hidden = true; } document.querySelector('.${s} [data-open]').addEventListener('click', function () { drawer.hidden = false; mask.hidden = false; }); document.querySelector('.${s} [data-mask]').addEventListener('click', closeDrawer); document.querySelector('.${s} [data-close]').addEventListener('click', closeDrawer); document.querySelector('.${s} [data-confirm]').addEventListener('click', closeDrawer);`);
|
||
return lines.length ? `(function () { ${lines.join(' ')} }());` : '';
|
||
}
|
||
function htmlFor(d) {
|
||
const s = `kole-${d.slug}`;
|
||
return `<!DOCTYPE html>
|
||
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Kole UI · ${esc(d.name)}(普通 H5)</title>
|
||
<link rel="stylesheet" href="../.design_library/kole-ui/colors_and_type.css"><link rel="stylesheet" href="${d.prefix}.css">
|
||
<style>.demo{padding:24px;background:var(--kole-color-page-bg);font-family:var(--kole-font-family)}.demo h1{margin:0 0 4px;font-size:20px;color:var(--kole-color-text-title)}.sub{margin:0 0 20px;color:var(--kole-color-text-secondary);font-size:12px}.demo-block{padding:16px;margin-bottom:16px;background:var(--kole-color-card-bg);border:1px solid var(--kole-color-border);border-radius:var(--kole-radius-large)}.demo-block h2{margin:0 0 12px;font-size:14px;color:var(--kole-color-text-title)}.row{display:flex;flex-wrap:wrap;align-items:center;gap:12px}.demo-note{margin:12px 0 0;color:var(--kole-color-text-secondary);font-size:12px}</style></head>
|
||
<body><div class="demo"><h1>${esc(d.name)}(纯 H5)</h1><p class="sub">${esc(d.usage)}</p><div class="demo-block"><h2>默认与常用变体</h2><div class="row ${s}">${demoMarkup(d)}</div></div><div class="demo-block"><h2>状态与使用场景</h2><div class="row ${s}"><div class="${s}-surface" style="padding:12px">${esc(d.usage)}</div><span class="${s}-desc">支持键盘焦点、状态反馈和令牌化样式。</span></div><p class="demo-note">零运行时依赖;数据请求、权限和国际化由宿主接入方决定。</p></div></div><script>${demoScript(d)}</script></body></html>`;
|
||
}
|
||
|
||
function propObject(d, vue2 = false) {
|
||
return d.props.map((x) => {
|
||
const def = x.def === '() => []' ? (vue2 ? 'function () { return []; }' : '() => []') : x.def;
|
||
return ` ${x.name}: { type: ${x.type}, default: ${def} }, // ${descFor(x.name)}`;
|
||
}).join('\n');
|
||
}
|
||
function descFor(name) {
|
||
const map = { type: '类型', variant: '变体', size: '尺寸', state: '状态', status: '状态', mode: '模式', disabled: '是否禁用', loading: '是否加载中', readonly: '是否只读', required: '是否必填', placeholder: '占位提示文本', value: '绑定值', modelValue: '双向绑定值', options: '选项数据', data: '数据源', items: '数据项', multiple: '是否多选', clearable: '是否可清空', maxLength: '最大字符数', icon: '图标', title: '标题', description: '描述文本', content: '内容', align: '对齐方式', width: '宽度', height: '高度', columns: '列配置', total: '总数', page: '当前页码', pageSize: '每页条数', checked: '是否选中', open: '是否展开', visible: '是否可见', label: '标签文本', format: '格式', min: '最小值', max: '最大值', step: '步长', precision: '精度(小数位)', error: '错误提示', duration: '持续时间', closable: '是否可关闭', bordered: '是否显示边框', scrollable: '是否可滚动', placement: '弹出位置', trigger: '触发方式', theme: '主题', color: '颜色', count: '数量', avatar: '头像地址', src: '资源地址', href: '链接地址', target: '打开方式', collapsed: '是否折叠', direction: '方向', orientation: '方向', range: '取值范围', presets: '快捷预设', secondaryText: '次要文本', maskClosable: '点击遮罩是否可关闭' };
|
||
return map[name] || name;
|
||
}
|
||
function reactProps(d) {
|
||
const parts = d.props.map((x) => `${x.name} = ${x.def === '() => []' ? '[]' : x.def}`);
|
||
parts.push('children', 'onChange', 'onClick', 'onClose', 'onSubmit', 'onLoadMore');
|
||
return parts.join(', ');
|
||
}
|
||
function reactBody(d) {
|
||
const s = `kole-${d.slug}`;
|
||
const body = {
|
||
layout: String.raw`<div className="__S__ __S__-layout"><aside className="__S__-surface">导航<br />用户<br />设置</aside><section className="__S__-surface"><div className="__S__-header">页头</div><div className="__S__-main">{children || '内容区'}</div><div className="__S__-footer">页脚</div></section></div>`,
|
||
container: String.raw`<div className="__S__ __S__-surface"><div className="__S__-header">Header</div><div className="__S__-main">{children || 'Main content'}</div><div className="__S__-footer">Footer</div></div>`,
|
||
typography: String.raw`<div className="__S__"><h2 className="__S__-title">{text}</h2><p className="__S__-desc">辅助说明文字,展示信息层级和省略。</p><button type="button" className="__S__-button" onClick={() => onClick && onClick(text)}>复制文本</button></div>`,
|
||
icon: String.raw`<button type="button" className="__S__ __S__-button __S__-primary" style={{ fontSize: size, color: color === 'brand' ? undefined : color }} onClick={onClick} aria-label={icon}>{icon}</button>`,
|
||
link: String.raw`<a className={disabled ? '__S__-link __S__-disabled' : '__S__-link'} href={disabled ? undefined : href} target={target} onClick={onClick}>{children || '查看详情'}</a>`,
|
||
radio: String.raw`<div className="__S__" role="radiogroup">{opts.map((o) => <label key={o.value} className="__S__-option"><input type="radio" name="__S__-group" checked={modelValue === o.value} disabled={disabled} onChange={() => onChange && onChange(o.value)} />{o.label}</label>)}</div>`,
|
||
checkbox: String.raw`<div className="__S__" role="group">{opts.map((o) => <label key={o.value} className="__S__-option"><input type="checkbox" name="__S__-group" checked={Array.isArray(modelValue) && modelValue.includes(o.value)} disabled={disabled} onChange={() => onChange && onChange(o.value)} />{o.label}</label>)}</div>`,
|
||
inputnumber: String.raw`<div className="__S__ __S__-input-row"><button className="__S__-action" type="button" onClick={() => onChange && onChange(Number(value) - step)} aria-label="减少">−</button><input className="__S__-control __S__-number" type="number" value={value} min={min} max={max} step={step} disabled={disabled} onChange={(e) => onChange && onChange(Number(e.target.value))} /><button className="__S__-action" type="button" onClick={() => onChange && onChange(Number(value) + step)} aria-label="增加">+</button></div>`,
|
||
switch: String.raw`<button className={checked ? '__S__ __S__-switch is-on' : '__S__ __S__-switch'} type="button" role="switch" aria-checked={checked} disabled={disabled} onClick={() => onChange && onChange(!checked)} aria-label="切换状态" />`,
|
||
timepicker: String.raw`<label className="__S__ __S__-item">执行时间 <input className="__S__-control" type="time" value={value} onChange={(e) => onChange && onChange(e.target.value)} /></label>`,
|
||
datepicker: String.raw`<label className="__S__ __S__-item">日期 <input className="__S__-control" type="date" value={value} onChange={(e) => onChange && onChange(e.target.value)} /></label>`,
|
||
datetimepicker: String.raw`<label className="__S__ __S__-item">生效时间 <input className="__S__-control" type="datetime-local" value={value} onChange={(e) => onChange && onChange(e.target.value)} /></label>`,
|
||
form: String.raw`<form className="__S__ __S__-form" onSubmit={(e) => { e.preventDefault(); onSubmit && onSubmit(e); }}><label className="__S__-field"><span>{label} {required ? <b aria-hidden="true">*</b> : null}</span><input className="__S__-control" name="name" required={required} /></label>{error ? <span className="__S__-error">{error}</span> : null}<div className="__S__-row"><button className="__S__-button __S__-primary" type="submit">提交</button><button className="__S__-button" type="reset">重置</button></div></form>`,
|
||
pagination: String.raw`<div className="__S__ __S__-pager" aria-label="分页"><button className="__S__-page" type="button" onClick={() => onChange && onChange(Math.max(1, page - 1))}>上一页</button><button className="__S__-page is-current" type="button">{page}</button><button className="__S__-page" type="button" onClick={() => onChange && onChange(page + 1)}>下一页</button><span className="__S__-desc">共 {total} 条 · 每页 {pageSize} 条</span></div>`,
|
||
badge: String.raw`<span className="__S__ __S__-badge-host"><span className="__S__-surface" style={{ padding: 12 }}>{children || '消息'}</span><span className="__S__-badge" aria-label="未读数量">{count > 99 ? '99+' : count}</span></span>`,
|
||
avatar: String.raw`<span className="__S__ __S__-item"><span className="__S__-avatar" style={{ width: size, height: size }} aria-label={avatar}>{src ? <img src={src} alt={avatar} /> : avatar}</span><span className="__S__-status" aria-label={status}></span></span>`,
|
||
descriptions: String.raw`<table className="__S__ __S__-table"><tbody><tr><th>订单号</th><td>KOLE-20260920</td><th>状态</th><td>处理中</td></tr><tr><th>负责人</th><td>林工</td><th>更新时间</th><td>今天 09:30</td></tr></tbody></table>`,
|
||
alert: String.raw`<div className="__S__ __S__-alert" role="alert"><span><strong>{title}</strong><br /><span className="__S__-desc">{description}</span></span>{closable ? <button className="__S__-close" type="button" aria-label="关闭" onClick={onClose}>×</button> : null}</div>`,
|
||
pageheader: String.raw`<header className="__S__ __S__-pageheader"><button className="__S__-button" type="button" onClick={() => onClick && onClick()}>← {secondaryText}</button><div className="__S__-pageheader-main"><strong className="__S__-title">{title}</strong><div className="__S__-desc">{description}</div></div><button className="__S__-button __S__-primary" type="button">保存</button></header>`,
|
||
tooltip: String.raw`<div className="__S__ __S__-tooltip-wrap"><button className="__S__-button __S__-primary" type="button" aria-describedby="__S__-tip">悬停或聚焦</button><span className="__S__-tip" id="__S__-tip" role="tooltip">{tooltip}</span></div>`,
|
||
popover: String.raw`<div className="__S__ __S__-popover-wrap"><button className="__S__-button __S__-primary" type="button" aria-expanded={open} onClick={() => onClick && onClick()}>查看信息</button>{open ? <div className="__S__-popover" role="dialog">{content}</div> : null}</div>`,
|
||
divider: String.raw`<div className="__S__"><span className="__S__-desc">{title}</span><hr className="__S__-divider" /></div>`,
|
||
infinitescroll: String.raw`<div className="__S__ __S__-scroll" style={{ height }} role="feed"><div className="__S__-scroll-item">活动记录 1</div><div className="__S__-scroll-item">活动记录 2</div><button className="__S__-button" type="button" disabled={loading || status === 'done'} onClick={onLoadMore}>{loading ? '加载中…' : status === 'done' ? '已全部加载' : '加载更多'}</button></div>`,
|
||
drawer: String.raw`<><button className="__S__ __S__-button __S__-primary" type="button" onClick={() => onClick && onClick()}>打开抽屉</button>{open ? <><div className="__S__-drawer-mask" onClick={maskClosable ? onClose : undefined} /><aside className="__S__-drawer" role="dialog" aria-modal="true"><h3 className="__S__-title">{title}</h3><p className="__S__-desc">在当前页面侧向编辑,不丢失列表上下文。</p><div className="__S__-drawer-footer"><button className="__S__-button" type="button" onClick={onClose}>取消</button><button className="__S__-button __S__-primary" type="button" onClick={onClose}>确定</button></div></aside></> : null}</>`
|
||
};
|
||
return (body[d.kind] || String.raw`<div className="__S__ __S__-surface"><strong className="__S__-title">{children || '__NAME__'}</strong><p className="__S__-desc">__USAGE__</p></div>`)
|
||
.replaceAll('__S__', s).replaceAll('__NAME__', esc(d.name)).replaceAll('__USAGE__', esc(d.usage));
|
||
}
|
||
function jsxFor(d) {
|
||
return `import React, { useState } from 'react';
|
||
import './${d.prefix}.css';
|
||
|
||
export default function Kole${d.prefix}({ ${reactProps(d)} }) {
|
||
${d.kind === 'radio' || d.kind === 'checkbox' ? "const opts = options && options.length ? options : [{ label: '选项 A', value: 'a' }, { label: '选项 B', value: 'b' }];" : ''}
|
||
return (
|
||
<div className="kole-${d.slug}-root">
|
||
${reactBody(d)}
|
||
</div>
|
||
);
|
||
}
|
||
`;
|
||
}
|
||
function vue3Template(d) {
|
||
const s = `kole-${d.slug}`;
|
||
const v = d.kind === 'radio' || d.kind === 'checkbox' ? `<div class="${s}" role="${d.kind === 'radio' ? 'radiogroup' : 'group'}"><label v-for="(o, i) in optionList" :key="i" class="${s}-option"><input type="${d.kind === 'radio' ? 'radio' : 'checkbox'}" :checked="${d.kind === 'radio' ? 'props.modelValue === o.value' : 'Array.isArray(props.modelValue) && props.modelValue.includes(o.value)'}" :disabled="props.disabled" @change="change(o.value)" />{{ o.label }}</label></div>` : null;
|
||
if (v) return v;
|
||
switch (d.kind) {
|
||
case 'switch': return `<button class="${s} ${s}-switch" :class="{ 'is-on': checked }" type="button" role="switch" :aria-checked="checked" :disabled="props.disabled" @click="toggle">切换</button>`;
|
||
case 'inputnumber': return `<div class="${s} ${s}-input-row"><button class="${s}-action" type="button" @click="change(Number(props.value) - props.step)">−</button><input class="${s}-control ${s}-number" type="number" :value="props.value" :min="props.min" :max="props.max" :step="props.step" :disabled="props.disabled" @input="change(Number($event.target.value))" /><button class="${s}-action" type="button" @click="change(Number(props.value) + props.step)">+</button></div>`;
|
||
case 'timepicker': return `<label class="${s} ${s}-item">执行时间 <input class="${s}-control" type="time" :value="props.value" @input="change($event.target.value)" /></label>`;
|
||
case 'datepicker': return `<label class="${s} ${s}-item">日期 <input class="${s}-control" type="date" :value="props.value" @input="change($event.target.value)" /></label>`;
|
||
case 'datetimepicker': return `<label class="${s} ${s}-item">生效时间 <input class="${s}-control" type="datetime-local" :value="props.value" @input="change($event.target.value)" /></label>`;
|
||
case 'form': return `<form class="${s} ${s}-form" @submit.prevent="emit('submit', $event)"><label class="${s}-field"><span>{{ props.label }} <b v-if="props.required" aria-hidden="true">*</b></span><input class="${s}-control" name="name" :required="props.required" /></label><span v-if="props.error" class="${s}-error">{{ props.error }}</span><div class="${s}-row"><button class="${s}-button ${s}-primary" type="submit">提交</button><button class="${s}-button" type="reset" @click="emit('reset')">重置</button></div></form>`;
|
||
case 'pagination': return `<div class="${s} ${s}-pager" aria-label="分页"><button class="${s}-page" type="button" @click="change(Math.max(1, props.page - 1))">上一页</button><button class="${s}-page is-current" type="button">{{ props.page }}</button><button class="${s}-page" type="button" @click="change(props.page + 1)">下一页</button><span class="${s}-desc">共 {{ props.total }} 条 · 每页 {{ props.pageSize }} 条</span></div>`;
|
||
case 'alert': return `<div class="${s} ${s}-alert" :class="'is-' + props.type" role="alert"><span><strong>{{ props.title }}</strong><br /><span class="${s}-desc">{{ props.description }}</span></span><button v-if="props.closable" class="${s}-close" type="button" aria-label="关闭" @click="emit('close')">×</button></div>`;
|
||
case 'pageheader': return `<header class="${s} ${s}-pageheader"><button class="${s}-button" type="button" @click="emit('back')">← {{ props.secondaryText }}</button><div class="${s}-pageheader-main"><strong class="${s}-title">{{ props.title }}</strong><div class="${s}-desc">{{ props.description }}</div></div><button class="${s}-button ${s}-primary" type="button">保存</button></header>`;
|
||
case 'tooltip': return `<div class="${s} ${s}-tooltip-wrap"><button class="${s}-button ${s}-primary" type="button" aria-describedby="${s}-tip">悬停或聚焦</button><span class="${s}-tip" id="${s}-tip" role="tooltip">{{ props.tooltip }}</span></div>`;
|
||
case 'popover': return `<div class="${s} ${s}-popover-wrap"><button class="${s}-button ${s}-primary" type="button" :aria-expanded="open" @click="open = !open">查看信息</button><div v-if="open" class="${s}-popover" role="dialog">{{ props.content }}</div></div>`;
|
||
case 'divider': return `<div class="${s}"><span class="${s}-desc">{{ props.title }}</span><hr class="${s}-divider" :class="'is-' + props.orientation" /></div>`;
|
||
case 'infinitescroll': return `<div class="${s} ${s}-scroll" :style="{ height: props.height }" role="feed"><div class="${s}-scroll-item">活动记录 1</div><div class="${s}-scroll-item">活动记录 2</div><button class="${s}-button" type="button" :disabled="props.loading || props.status === 'done'" @click="emit('load')">{{ props.loading ? '加载中…' : props.status === 'done' ? '已全部加载' : '加载更多' }}</button></div>`;
|
||
case 'drawer': return `<div class="${s}"><button class="${s}-button ${s}-primary" type="button" @click="emit('open')">打开抽屉</button><div v-if="open" class="${s}-drawer-mask" @click.self="props.maskClosable && emit('close')"></div><aside v-if="open" class="${s}-drawer" role="dialog" aria-modal="true"><h3 class="${s}-title">{{ props.title }}</h3><p class="${s}-desc">在当前页面侧向编辑,不丢失列表上下文。</p><div class="${s}-drawer-footer"><button class="${s}-button" type="button" @click="emit('close')">取消</button><button class="${s}-button ${s}-primary" type="button" @click="emit('confirm')">确定</button></div></aside></div>`;
|
||
default: return `<div class="${s} ${s}-surface"><strong class="${s}-title">{{ props.title || '${esc(d.name)}' }}</strong><p class="${s}-desc">${esc(d.usage)}</p><slot /></div>`;
|
||
}
|
||
}
|
||
function vue3For(d) {
|
||
const emits = d.events.length ? `const emit = defineEmits([${d.events.map(jsQuote).join(', ')}]);` : 'const emit = defineEmits([]);';
|
||
const needsOpen = ['popover', 'drawer'].includes(d.kind);
|
||
const needsChecked = d.kind === 'switch';
|
||
const needsOptions = d.kind === 'radio' || d.kind === 'checkbox';
|
||
const needsChange = ['radio', 'checkbox', 'inputnumber', 'timepicker', 'datepicker', 'datetimepicker', 'pagination'].includes(d.kind);
|
||
const state = [];
|
||
if (needsOpen) state.push('const open = ref(Boolean(props.open));');
|
||
if (needsChecked) state.push('const checked = ref(Boolean(props.checked));');
|
||
if (needsOptions) state.push("const optionList = computed(() => props.options && props.options.length ? props.options : [{ label: '选项 A', value: 'a' }, { label: '选项 B', value: 'b' }]);");
|
||
const helpers = [];
|
||
if (needsChange) helpers.push("function change(value) { emit('update:modelValue', value); emit('change', value); emit('pagechange', value); }");
|
||
if (needsChecked) helpers.push("function toggle() { checked.value = !checked.value; emit('update:modelValue', checked.value); emit('change', checked.value); }");
|
||
return `<template>\n ${vue3Template(d)}\n</template>\n\n<script setup>\nimport { ref, computed } from 'vue';\nconst props = defineProps({\n${propObject(d)}\n});\n${emits}\n${state.join('\n')}\n${helpers.join('\n')}\n</script>\n\n<style src="./${d.prefix}.css"></style>\n`;
|
||
}
|
||
function vue2Template(d) {
|
||
const s = `kole-${d.slug}`;
|
||
if (d.kind === 'radio' || d.kind === 'checkbox') return `<div class="${s}" role="${d.kind === 'radio' ? 'radiogroup' : 'group'}"><label v-for="(o, i) in optionList" :key="i" class="${s}-option"><input type="${d.kind === 'radio' ? 'radio' : 'checkbox'}" :checked="${d.kind === 'radio' ? 'modelValue === o.value' : 'Array.isArray(modelValue) && modelValue.indexOf(o.value) >= 0'}" :disabled="disabled" @change="change(o.value)" />{{ o.label }}</label></div>`;
|
||
if (d.kind === 'switch') return `<button class="${s} ${s}-switch" :class="{ 'is-on': checked }" type="button" role="switch" :aria-checked="checked" :disabled="disabled" @click="toggle">切换</button>`;
|
||
if (d.kind === 'inputnumber') return `<div class="${s} ${s}-input-row"><button class="${s}-action" type="button" @click="change(Number(value) - step)">−</button><input class="${s}-control ${s}-number" type="number" :value="value" :min="min" :max="max" :step="step" :disabled="disabled" @input="change(Number($event.target.value))" /><button class="${s}-action" type="button" @click="change(Number(value) + step)">+</button></div>`;
|
||
if (['timepicker', 'datepicker', 'datetimepicker'].includes(d.kind)) return `<label class="${s} ${s}-item">选择值 <input class="${s}-control" type="${d.kind === 'timepicker' ? 'time' : d.kind === 'datepicker' ? 'date' : 'datetime-local'}" :value="value" @input="change($event.target.value)" /></label>`;
|
||
if (d.kind === 'form') return `<form class="${s} ${s}-form" @submit.prevent="$emit('submit', $event)"><label class="${s}-field"><span>{{ label }} <b v-if="required" aria-hidden="true">*</b></span><input class="${s}-control" name="name" :required="required" /></label><span v-if="error" class="${s}-error">{{ error }}</span><div class="${s}-row"><button class="${s}-button ${s}-primary" type="submit">提交</button><button class="${s}-button" type="reset" @click="$emit('reset')">重置</button></div></form>`;
|
||
if (d.kind === 'pagination') return `<div class="${s} ${s}-pager" aria-label="分页"><button class="${s}-page" type="button" @click="change(Math.max(1, page - 1))">上一页</button><button class="${s}-page is-current" type="button">{{ page }}</button><button class="${s}-page" type="button" @click="change(page + 1)">下一页</button><span class="${s}-desc">共 {{ total }} 条 · 每页 {{ pageSize }} 条</span></div>`;
|
||
if (d.kind === 'alert') return `<div class="${s} ${s}-alert" :class="'is-' + type" role="alert"><span><strong>{{ title }}</strong><br /><span class="${s}-desc">{{ description }}</span></span><button v-if="closable" class="${s}-close" type="button" aria-label="关闭" @click="$emit('close')">×</button></div>`;
|
||
if (d.kind === 'pageheader') return `<header class="${s} ${s}-pageheader"><button class="${s}-button" type="button" @click="$emit('back')">← {{ secondaryText }}</button><div class="${s}-pageheader-main"><strong class="${s}-title">{{ title }}</strong><div class="${s}-desc">{{ description }}</div></div><button class="${s}-button ${s}-primary" type="button">保存</button></header>`;
|
||
if (d.kind === 'tooltip') return `<div class="${s} ${s}-tooltip-wrap"><button class="${s}-button ${s}-primary" type="button" aria-describedby="${s}-tip">悬停或聚焦</button><span class="${s}-tip" id="${s}-tip" role="tooltip">{{ tooltip }}</span></div>`;
|
||
if (d.kind === 'popover') return `<div class="${s} ${s}-popover-wrap"><button class="${s}-button ${s}-primary" type="button" :aria-expanded="open" @click="open = !open">查看信息</button><div v-if="open" class="${s}-popover" role="dialog">{{ content }}</div></div>`;
|
||
if (d.kind === 'divider') return `<div class="${s}"><span class="${s}-desc">{{ title }}</span><hr class="${s}-divider" :class="'is-' + orientation" /></div>`;
|
||
if (d.kind === 'infinitescroll') return `<div class="${s} ${s}-scroll" :style="{ height: height }" role="feed"><div class="${s}-scroll-item">活动记录 1</div><div class="${s}-scroll-item">活动记录 2</div><button class="${s}-button" type="button" :disabled="loading || status === 'done'" @click="$emit('load')">{{ loading ? '加载中…' : status === 'done' ? '已全部加载' : '加载更多' }}</button></div>`;
|
||
if (d.kind === 'drawer') return `<div class="${s}"><button class="${s}-button ${s}-primary" type="button" @click="$emit('open')">打开抽屉</button><div v-if="open" class="${s}-drawer-mask" @click.self="maskClosable && $emit('close')"></div><aside v-if="open" class="${s}-drawer" role="dialog" aria-modal="true"><h3 class="${s}-title">{{ title }}</h3><p class="${s}-desc">在当前页面侧向编辑,不丢失列表上下文。</p><div class="${s}-drawer-footer"><button class="${s}-button" type="button" @click="$emit('close')">取消</button><button class="${s}-button ${s}-primary" type="button" @click="$emit('confirm')">确定</button></div></aside></div>`;
|
||
return `<div class="${s} ${s}-surface"><strong class="${s}-title">{{ title || '${esc(d.name)}' }}</strong><p class="${s}-desc">${esc(d.usage)}</p><slot /></div>`;
|
||
}
|
||
function vue2For(d) {
|
||
const props = propObject(d, true);
|
||
const needsOptions = d.kind === 'radio' || d.kind === 'checkbox';
|
||
const needsChecked = d.kind === 'switch';
|
||
const needsOpen = d.kind === 'popover' || d.kind === 'drawer';
|
||
const needsChange = ['radio', 'checkbox', 'inputnumber', 'timepicker', 'datepicker', 'datetimepicker', 'pagination'].includes(d.kind);
|
||
const computed = needsOptions ? "\n computed: { optionList: function () { return this.options && this.options.length ? this.options : [{ label: '选项 A', value: 'a' }, { label: '选项 B', value: 'b' }]; } }," : '';
|
||
const dataFields = [];
|
||
if (needsOpen) dataFields.push('open: false');
|
||
if (needsChecked) dataFields.push('checked: false');
|
||
const data = dataFields.length ? `\n data: function () { return { ${dataFields.join(', ')} }; },` : '';
|
||
const methods = [];
|
||
if (needsChange) methods.push(" change: function (value) { this.$emit('input', value); this.$emit('change', value); this.$emit('pagechange', value); }");
|
||
if (needsChecked) methods.push(" toggle: function () { this.checked = !this.checked; this.$emit('input', this.checked); this.$emit('change', this.checked); }");
|
||
const methodBlock = methods.length ? `\n methods: {\n${methods.join(',\n')}\n },` : '';
|
||
const emits = d.events.length ? `\n // events: ${d.events.join(', ')}` : '';
|
||
return `<template>\n ${vue2Template(d)}\n</template>\n\n<script>\nexport default {\n name: 'Kole${d.prefix}',\n props: {\n${props}\n },${data}${computed}${methodBlock}${emits}\n};\n</script>\n\n<style src="./${d.prefix}.css"></style>\n`;
|
||
}
|
||
|
||
function specText() {
|
||
const head = '# PC 常用组件补齐批次(Element UI 左侧目录对照)\n\n本批次补齐常用的后台基础、表单、数据、提示、导航和叠加层组件。实现以零运行时依赖为边界:原生日期/时间控件保留宿主能力,数据请求、权限、国际化和完整业务流程由接入方负责。\n\n';
|
||
return head + DEFS.map((d) => `### ${d.name}\n- 用途:${d.usage}\n- Anatomy:根节点 \`kole-${d.slug}\`,包含主体、状态/操作区域,交互控件保持原生语义。\n- 变体:${variantValues(d, 'variant').join('、')};具体组件按自身属性展示方向、尺寸、状态或位置。\n- 状态:default / hover / focus / disabled;含弹层或异步内容的组件另有 open、loading、done、error 状态。\n- 无障碍:提供可见焦点、键盘可达、label/role/aria 属性;对话框和抽屉关联标题,进度/状态使用可读文本。\n- 实现边界:${d.usage} 未声明的高级数据请求、服务端校验和第三方插件能力不由组件自行推断。\n`).join('\n');
|
||
}
|
||
|
||
function run() {
|
||
const indexPath = path.join(COMPONENTS, 'index.json');
|
||
const idx = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
||
assertFree(idx);
|
||
const registered = new Set(idx.components.map((c) => c.slug));
|
||
const additions = DEFS.filter((d) => !registered.has(d.slug)).map((d) => ({ slug: d.slug, name: d.name, tier: 'extension', confidence: 'high', specBatch: 11, specFile: '组件11.txt', frameworksPrefix: d.prefix, contract: `components/${d.slug}.json` }));
|
||
const total = idx.components.length + additions.length;
|
||
const next = { ...idx, description: `全量组件索引:登记 frameworks/ 下全部 ${total} 个已实现组件(10 个核心规范组件 + ${total - 10} 个扩展组件),并标注规格来源批次。`, components: [...idx.components, ...additions] };
|
||
atomicWrite(indexPath, JSON.stringify(next, null, 2) + '\n');
|
||
for (const d of DEFS) {
|
||
atomicWrite(path.join(COMPONENTS, `${d.slug}.json`), JSON.stringify(contract(d), null, 2) + '\n');
|
||
atomicWrite(path.join(FRAMEWORKS, `${d.prefix}.css`), cssFor(d));
|
||
atomicWrite(path.join(FRAMEWORKS, `${d.prefix}.html`), htmlFor(d));
|
||
atomicWrite(path.join(FRAMEWORKS, `${d.prefix}.jsx`), jsxFor(d));
|
||
atomicWrite(path.join(FRAMEWORKS, `${d.prefix}.vue2.vue`), vue2For(d));
|
||
atomicWrite(path.join(FRAMEWORKS, `${d.prefix}.vue3.vue`), vue3For(d));
|
||
}
|
||
const spec = specText();
|
||
atomicWrite(ROOT_SPEC, spec);
|
||
atomicWrite(LIB_SPEC, spec);
|
||
console.log(`[add-element-components] ensured ${DEFS.length} components / ${DEFS.length * 5} framework files / ${DEFS.length} contracts`);
|
||
console.log(`[add-element-components] added ${additions.length} index entries; index now ${next.components.length} components`);
|
||
}
|
||
run();
|