#!/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, '"'); 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: `
页头
内容区
`, container: `
Header
Main content
`, typography: `

页面标题

辅助说明文字,展示信息层级和阅读节奏。

长文本省略示例:订单提交后将进入审核流程……

`, icon: `✓`, link: `查看详情下载文件`, radio: ``, checkbox: ``, inputnumber: `
`, switch: `启用通知`, timepicker: ``, datepicker: ``, datetimepicker: ``, form: `
`, pagination: `
共 128 条
`, badge: `消息8状态`, avatar: `林陈`, descriptions: `
订单号KOLE-20260920状态处理中
负责人林工更新时间今天 09:30
`, alert: ``, pageheader: `
用户详情
查看并编辑用户资料
`, tooltip: `
`, popover: `
`, divider: `
基础信息

安全设置
`, infinitescroll: `
活动记录 1
活动记录 2
`, drawer: `` }; return common[d.kind] || `
${esc(d.name)}

${esc(d.usage)}

`; } 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 ` Kole UI · ${esc(d.name)}(普通 H5)

${esc(d.name)}(纯 H5)

${esc(d.usage)}

默认与常用变体

${demoMarkup(d)}

状态与使用场景

${esc(d.usage)}
支持键盘焦点、状态反馈和令牌化样式。

零运行时依赖;数据请求、权限和国际化由宿主接入方决定。

`; } 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`
页头
{children || '内容区'}
页脚
`, container: String.raw`
Header
{children || 'Main content'}
Footer
`, typography: String.raw`

{text}

辅助说明文字,展示信息层级和省略。

`, icon: String.raw``, link: String.raw`{children || '查看详情'}`, radio: String.raw`
{opts.map((o) => )}
`, checkbox: String.raw`
{opts.map((o) => )}
`, inputnumber: String.raw`
onChange && onChange(Number(e.target.value))} />
`, switch: String.raw``, pagination: String.raw`
共 {total} 条 · 每页 {pageSize} 条
`, badge: String.raw`{children || '消息'}{count > 99 ? '99+' : count}`, avatar: String.raw`{src ? {avatar} : avatar}`, descriptions: String.raw`
订单号KOLE-20260920状态处理中
负责人林工更新时间今天 09:30
`, alert: String.raw`
{title}
{description}
{closable ? : null}
`, pageheader: String.raw`
{title}
{description}
`, tooltip: String.raw`
{tooltip}
`, popover: String.raw`
{open ?
{content}
: null}
`, divider: String.raw`
{title}
`, infinitescroll: String.raw`
活动记录 1
活动记录 2
`, drawer: String.raw`<>{open ? <>
: null}` }; return (body[d.kind] || String.raw`
{children || '__NAME__'}

__USAGE__

`) .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 (
${reactBody(d)}
); } `; } function vue3Template(d) { const s = `kole-${d.slug}`; const v = d.kind === 'radio' || d.kind === 'checkbox' ? `
` : null; if (v) return v; switch (d.kind) { case 'switch': return ``; case 'inputnumber': return `
`; case 'timepicker': return ``; case 'datepicker': return ``; case 'datetimepicker': return ``; case 'form': return `
{{ props.error }}
`; case 'pagination': return `
共 {{ props.total }} 条 · 每页 {{ props.pageSize }} 条
`; case 'alert': return ``; case 'pageheader': return `
{{ props.title }}
{{ props.description }}
`; case 'tooltip': return `
{{ props.tooltip }}
`; case 'popover': return `
`; case 'divider': return `
{{ props.title }}
`; case 'infinitescroll': return `
活动记录 1
活动记录 2
`; case 'drawer': return `
`; default: return `
{{ props.title || '${esc(d.name)}' }}

${esc(d.usage)}

`; } } 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 `\n\n\n\n\n`; } function vue2Template(d) { const s = `kole-${d.slug}`; if (d.kind === 'radio' || d.kind === 'checkbox') return `
`; if (d.kind === 'switch') return ``; if (d.kind === 'inputnumber') return `
`; if (['timepicker', 'datepicker', 'datetimepicker'].includes(d.kind)) return ``; if (d.kind === 'form') return `
{{ error }}
`; if (d.kind === 'pagination') return `
共 {{ total }} 条 · 每页 {{ pageSize }} 条
`; if (d.kind === 'alert') return ``; if (d.kind === 'pageheader') return `
{{ title }}
{{ description }}
`; if (d.kind === 'tooltip') return `
{{ tooltip }}
`; if (d.kind === 'popover') return `
`; if (d.kind === 'divider') return `
{{ title }}
`; if (d.kind === 'infinitescroll') return `
活动记录 1
活动记录 2
`; if (d.kind === 'drawer') return `
`; return `
{{ title || '${esc(d.name)}' }}

${esc(d.usage)}

`; } 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 `\n\n\n\n\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();