17 KiB
17 KiB
FMS Vue AI 组件库开发规范
本文档是 AI 修改或新增 fms-vue UI 组件时的默认约束。目标是保持组件库稳定、轻量、可预测,并把性能放在功能完成之前。
1. 优先级
按以下顺序做技术决策:
- 正确性和现有 API 行为
- 性能和资源释放
- 组件写法一致
- 可访问性和交互反馈
- 视觉细节
当需求与现有约定冲突时,优先保持组件库的一致性;确需破坏 API 时,必须在回复中明确说明。
2. AI 工作边界
按任务类型区分默认行为:
业务开发任务(新增/修改组件、页面、store、服务等):
- 默认只阅读、修改和静态检查与需求相关的源码。
- 不主动执行构建、全量测试、预览服务或大范围格式化。
- 只有用户明确要求时,才执行
pnpm build、测试命令或启动开发服务。 - 改动涉及已测对象时,至少运行一次
pnpm test:file <相关测试文件>验证不回归;但不应在未获许可时跑全量pnpm test或pnpm test:coverage。 - 不修改用户已有的无关改动,不删除生成目录,不重置工作区。
- 修改前先检查目标文件和相邻组件,避免重复抽象或引入第二套写法。
- 完成后只报告实际修改内容和静态检查结果;没有执行构建或测试时必须明确说明。
测试任务(编写/补齐/运行测试,或用户明确说"测试"):
- 以
tests/README.md为唯一权威约定:目录与命名、编写模板、mock 接缝、每个对象的测试清单、覆盖率门槛。 - 必须实际执行测试命令并报告结果(
pnpm test/pnpm test:file <目标>/pnpm test:coverage),不允许只做静态检查。 - 默认目标是:用例全绿 +
pnpm test:coverage达到 60% 门槛(或明确报告缺口与下一步)。 - 遵循
tests/README.md第 7 节"AI 工作流"的固定步骤。
任务类型不明确时,向用户确认;用户说"测试""补测试""覆盖率"等词时按测试任务处理。
2.1 参考组件库
- 新增或重构组件前,先参考
fms/参考组件库中已有组件的功能设计、交互状态和 UI 细节;从fms-vue目录查看时路径为../参考组件库。 - 参考重点包括:控件状态、键盘操作、焦点样式、间距层级、禁用态、错误态、加载态和响应式行为。
- 参考组件库用于借鉴设计和验证常见交互,不直接复制整套实现;最终代码必须遵守本项目的 Vue、CSS token、Lucide 图标和性能规则。
- 如果本项目的现有 API 或视觉 token 与参考组件库不同,保持本项目约定,并在必要时说明差异原因。
- AI 在开始编码前,应先搜索参考组件库是否已有相同或相近能力,避免重复设计组件行为。
3. 目录和文件结构
组件统一放在 src/components/ui 下,每个组件使用独立目录:
src/components/ui/
input/
input.vue
index.scss
button/
button.vue
index.scss
utils/
dom.js
number.js
约定:
- 组件入口使用小写文件名,例如
input.vue、button.vue。 - 组件样式放在同目录
index.scss,组件内通过@use './index.scss'引入。 - 公用方法只放在
src/components/ui/utils,按职责拆分文件。 utils只能放无副作用、可复用的纯函数或明确的 DOM 辅助函数,不依赖具体组件状态。- 工具函数禁止反向导入组件,避免循环依赖。
- 新增目录时不额外创建 barrel index,除非项目已有明确的统一导出入口。
4. Vue 组件统一写法
4.1 基础结构
使用 Vue 3 <script setup>,顺序保持一致:
<script setup>
import { computed, ref } from 'vue'
const props = defineProps({
modelValue: { type: String, default: '' },
})
const emit = defineEmits(['update:modelValue'])
const classes = computed(() => ['component-name'])
</script>
<template>
<div :class="classes">
<slot />
</div>
</template>
<style scoped lang="scss">
@use './index.scss';
</style>
4.2 Props 和事件
- 每个 Prop 必须有明确类型和默认值,枚举值必须提供
validator。 - Prop 名称使用 camelCase,DOM 属性在模板中使用 kebab-case。
- 双向值统一使用
modelValue和update:modelValue。 - 事件名使用小写短语,例如
focus、blur、clear、change。 - 原生事件转发前,先更新组件内部状态,再
emit给父组件。 - 不要把业务用的枚举值直接传给原生 DOM。例如业务
type="default"应映射为原生type="text"。 Input的type当前只允许default、password、number、textarea;number默认只接受 ASCII 数字和一个小数点。输入时保留12.等字符串中间态,触发change或失焦时使用Number()转为数字;空值仍保持'',避免被转换为0。type="number"的外部初始值应为number或''。后端返回的数字字符串应在业务数据入口统一转换,不在每个输入组件挂载或渲染时重复清洗;字符串只作为用户编辑小数时的临时状态。- 只保留当前确认的 API,不为旧 API 增加兼容分支。兼容会增加条件判断、文档复杂度和维护成本。
4.3 响应式状态
- 能从 Props 或状态直接计算出的值使用
computed,不要复制到另一个ref。 - 只在确有副作用或需要同步 DOM 时使用
watch。 watch必须限定明确依赖,禁止深度监听大对象来解决局部问题。- 临时交互状态使用普通变量,不要为了拖拽起点、动画帧 ID 等数据创建响应式变量。
- 组件卸载时清理所有
document、window监听器、计时器、动画帧和全局样式修改。
4.4 组件内代码分块和注释
一个组件支持多个模式时,必须按职责分块书写。不要把 textarea、password、number 的状态、计算属性和事件处理交错放在一起。
统一使用以下顺序:
1. imports
2. props / emits / slots
3. 公共状态和公共 computed
4. 公共方法
5. default 专属逻辑
6. password 专属逻辑
7. number 专属逻辑
8. textarea 专属逻辑
9. 生命周期和监听器
10. template
11. style
推荐使用清晰的分隔注释:
<script setup>
// imports
// props / emits / slots
/* ---------- 公共状态 ---------- */
const focused = ref(false)
const hasValue = computed(() => ...)
/* ---------- 公共事件 ---------- */
function onFocus(event) { ... }
function onBlur(event) { ... }
/* ---------- password 模式 ---------- */
const revealed = ref(false)
function toggleReveal() { ... }
/* ---------- number 模式 ---------- */
function sanitizeNumberInput(target) { ... }
/* ---------- textarea 模式 ---------- */
const textareaEl = ref(null)
function onResizeStart(event) { ... }
/* ---------- 生命周期 ---------- */
onMounted(() => { ... })
onBeforeUnmount(() => { ... })
</script>
分块规则:
- 公共逻辑放在所有模式专属逻辑之前,公共逻辑不能依赖某个模式的 DOM 引用。
- 每个模式只维护自己的状态、computed、事件和清理逻辑;跨模式判断集中在
isPassword、isNumber、isTextarea等少量 computed 中。 - 一个模式的事件函数不要混入另一个模式的分支。需要共用时,提取为公共函数或
components/ui/utils工具函数。 - 生命周期统一放在逻辑分块之后;监听器的注册和清理必须在同一分块或同一生命周期区域中成对出现。
- 模板也按公共结构、单行输入、密码控件、数字控件、文本域和辅助操作的顺序排列,并与脚本分块顺序对应。
- 同一个模式的代码尽量连续出现,避免在文件顶部声明状态、文件底部再实现对应事件。
注释规则:
- 每个有独立职责的代码块必须有一个短标题注释。
- 注释解释“为什么这样做”或边界条件,不重复描述代码字面行为。
- 高频交互、DOM 布局读取、浏览器兼容处理、输入过滤和资源清理必须写明原因。
- 简单的赋值、显而易见的 computed 和普通事件转发不写逐行注释,避免噪声。
- 注释语言跟随项目现有语言,当前组件库使用中文注释;函数名和变量名仍使用英文。
- 不使用无信息量的注释,例如“定义变量”“处理点击”“返回结果”。
Input 这类多模式组件的最低分块要求:
- 公共:Props、事件、焦点状态、清除、字数统计和 class 计算。
password:显隐状态、显隐切换和密码图标。number:小数键盘提示、非法字符过滤、单个小数点约束、光标修正和数值回显。textarea:默认高度测量、拖拽状态、动画帧合并、键盘调整和卸载清理。
模式专属逻辑即使超过约 30 行,也应优先继续保留在当前组件对应的分块内,保证该模式的状态、事件和清理逻辑集中可读。只有被两个或以上组件复用的公共逻辑,才考虑抽到 components/ui/utils;不要为模式专属逻辑单独创建 composable 或工具函数。
5. 性能优先规则
5.1 渲染和更新
- 模板中避免调用会创建新对象或执行计算的函数;预先使用
computed或普通变量准备结果。 - 高频事件(
pointermove、mousemove、scroll、resize)必须节流、合并或使用requestAnimationFrame。 - 高频事件中不要同时读取布局和写入样式,避免 layout thrashing。先集中读取,再在下一帧写入。
- 拖拽、滚动等连续交互优先直接更新必要的 DOM 样式,只有需要驱动模板或无障碍属性时才更新响应式状态。
- 不要在每个字符输入时执行无关的深度计算、网络请求或全局状态更新。
- 大列表、复杂内容和重组件不应在输入组件内部同步渲染;需要时由上层使用虚拟列表或异步加载。
5.2 DOM 和事件
offsetHeight、getBoundingClientRect、getComputedStyle等布局读取应尽量只发生在初始化、开始拖拽或明确的布局变化时。- 监听器只在需要交互时注册,交互结束立即移除,不要长期注册全局监听器。
- 触控拖拽设置
touch-action,避免浏览器默认手势与组件逻辑竞争。 - 事件处理函数保持短小,只做状态更新、必要的 DOM 操作和事件派发。
- 能使用事件委托时不要为大量重复节点分别注册监听器。
5.3 资源和依赖
- 优先复用已有依赖和 Lucide 图标,不引入只服务于一个小功能的新依赖。
- 不在组件中内联大段 SVG、图片或重复配置。
- 图标按需导入,不导入整个图标包。
- 不添加无意义动画。动画必须有交互价值,并支持
prefers-reduced-motion。
6. 公用方法抽取规则
当相同逻辑在两个或以上组件中出现,或逻辑本身独立且需要单元验证时,考虑抽到 src/components/ui/utils。
推荐的工具函数类型:
// src/components/ui/utils/dom.js
export function restoreStyle(element, property, value) {
if (element) element.style[property] = value
}
// src/components/ui/utils/number.js
export function clamp(value, min, max = Number.POSITIVE_INFINITY) {
return Math.min(max, Math.max(min, value))
}
抽取要求:
- 函数输入和返回值清晰,优先使用纯函数。
- 不在工具函数中读取组件 Props、访问 Pinia 或隐式依赖全局变量。
- DOM 工具函数接收元素作为参数,不自行查询整个文档。
- 工具函数不能为了减少几行代码而增加抽象层;重复逻辑、边界处理或跨组件复用才值得抽取。
- 抽取后组件仍需保留业务语义,不能把所有逻辑堆进一个
utils.js。
7. CSS 最小化规则
- 优先复用现有 CSS 变量:颜色、间距、圆角、字号、阴影和控件高度必须使用
--fms-*token。 - 优先使用父级布局解决间距和对齐,避免给每个子元素增加独立定位规则。
- 同一状态只保留一套选择器,不重复写 hover、focus、disabled 样式。
- 不使用
!important,除非明确处理第三方样式且无法通过组件边界解决。 - 不为单次布局引入复杂计算、过多绝对定位或额外包裹层。
- 图标尺寸使用
1em或现有控件尺寸,不在每个组件中重复定义固定 SVG 尺寸。 - 输入类组件的内部控件默认
padding: 0,由外层统一控制空间,避免出现叠加留白。 - 过渡效果限定在颜色、边框和阴影等低成本属性;避免持续动画
width、height、top、left。 - 需要响应式布局时使用现有断点和流式布局,不新增只服务单个组件的断点。
7.1 尺寸与 token 规范
- 组件不提供
size(small/large/sm/lg等)尺寸枚举,统一保持默认大小,避免组件 API 膨胀。 - 控件尺寸、间距、圆角、字号等样式优先复用
src/theme/tokens.css中的--fms-*token,不在组件index.scss中硬编码具体数值。 - 确实缺少的尺寸或样式 token,统一新增到
tokens.css(--fms-*命名,与主题无关的尺寸只写在:root),再由组件引用,不在单个组件样式里私自定义固定大小。 - 现有 token 无法直接表达的比例关系(如 switch 滑块位移 = 轨道宽 - 滑块直径 - 2 × 边距),优先用
calc()派生,而不是另设一个重复的尺寸 token。
7.2 浮层组件宽度策略
使用 Popover 派生浮层类组件(Select、DatePicker、RangePicker 等)时,浮层宽度按以下规则处理,保持视觉统一与可读性:
- 浮层默认跟随触发器宽度:为 Popover 传
follow-trigger-width,让下拉/面板与触发框严格等宽,视觉统一。不要靠内容宽度撑开浮层,除非该组件的内容宽度是固定的(如纯提示型 Tooltip)。 - 内容有结构性宽度下限时(如日历 7 列日期、表格列等),
min-width加在组件根元素上,而不是浮层或面板上。根元素被撑到 ≥ min-width 后,浮层跟随根元素自然达到下限,同时保持等宽。 - 不要在浮层面板上设
min-width,否则会出现浮层比触发器宽、上下不对齐的视觉断裂。 - 不设置最大宽度:容器过宽时允许浮层随之变宽,内容超宽属于无法控制的范围,交由定位逻辑的 shift 兜底。
- 面板内部自适应区域用
flex: 1 1 auto/width: 100%填满剩余空间,固定尺寸区域(如时间列)保持固定宽度,两者搭配让面板在任意宽度下不塌缩。 - 参考实现:
Select(根min-width: 200px)与DatePicker/RangePicker(根min-width: 240px,对齐日历 7 列可读性)。
8. 交互和可访问性底线
- 图标按钮必须有
aria-label或可见文本。 - 键盘可操作控件必须有清晰的
:focus-visible样式。 - 自定义拖拽、切换和滑块控件应提供对应的 ARIA role 和键盘操作。
- 禁用和只读状态必须同时反映到原生属性和视觉状态。
- 不要只依赖颜色表达错误、警告或选中状态。
- 交互命中区域不能因为视觉压缩而无法点击;需要紧贴边角时,通过透明命中区域保留可用尺寸。
9. 修改流程
AI 每次修改 UI 组件时按以下流程执行:
- 阅读目标组件、相邻组件和对应样式文件。
- 确认 Props、事件、插槽和现有 CSS token,避免重复定义。
- 先实现最小可用逻辑,再处理边界状态和可访问性。
- 高频交互先设计事件生命周期和清理路径,再编写模板和样式。
- 检查是否存在可复用方法;符合条件时抽到
components/ui/utils。 - 只做相关文件的静态检查和格式整理,不主动构建或全量测试;涉及已测对象时运行
pnpm test:file <相关测试>验证不回归(测试任务按tests/README.md执行)。 - 回复中列出变更文件、性能处理、未执行的构建/测试,以及仍存在的已知风险。
10. 交付检查清单
- API 是否只包含需求明确要求的 Props 和事件?
- 枚举 Props 是否有
validator? - 是否避免了旧 API 兼容分支和重复状态?
- 高频事件是否节流或使用
requestAnimationFrame? - 是否避免在事件循环中反复读取布局?
- 所有全局监听器、计时器和动画帧是否在结束和卸载时清理?
- 公用逻辑是否按规则放在
src/components/ui/utils? - CSS 是否复用 token,且没有无必要的嵌套和重复规则?
- 是否未添加
size属性,尺寸/大小均复用--fms-*token(缺失时已补充到tokens.css)? - 图标按钮、焦点状态和键盘操作是否可用?
- 是否遵守“业务开发默认不主动构建和全量测试、涉及已测对象须跑相关测试”的工作边界,并在结果中说明?(测试任务遵循
tests/README.md)