Files
workspace/code/fms/.codebuddy/plans/spin-component_97283c1c.md
T
2026-08-16 22:01:32 +08:00

172 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: spin-component
overview: 实现 Spin 加载组件(形态一:独立指示器;形态二:包裹内容+遮罩,且不破坏内部布局),配套 demo 与注册。
design:
architecture:
framework: vue
styleKeywords:
- 加载反馈
- 轻量遮罩
- 居中指示器
fontSystem:
fontFamily: system-ui
heading:
size: 14px
weight: 500
subheading:
size: 12px
weight: 400
body:
size: 12px
weight: 400
colorSystem:
primary:
- "#0f172b"
background:
- transparent
text:
- "#86909c"
functional:
- "#0f172b"
todos:
- id: spin-tokens
content: 用 [skill:antdv-next] 确认形态二结构,tokens.css 新增 --fms-spin-size
status: completed
- id: spin-style
content: 创建 spin/index.scss(指示器旋转动画 + 形态二容器/轻量遮罩/绝对居中指示器 + reduced-motion)
status: completed
dependencies:
- spin-tokens
- id: spin-component
content: "创建 spin/spin.vue(spinning/delay/description + #indicator/#description/默认插槽 + delay 定时器清理 + ARIA)"
status: completed
dependencies:
- spin-style
- id: spin-demo
content: 创建 demo/spin.vue 并注册到 App.vue「反馈」分类与 demoMap
status: completed
dependencies:
- spin-component
- id: spin-verify
content: 用 [skill:playwright-cli] 实测形态二内部布局零偏移 + oxlint 静态检查并汇报
status: completed
dependencies:
- spin-demo
---
## 用户需求
实现 Spin 加载中组件,支持两种形态:
- **形态一(无子元素)**:独立加载指示器,可带文案,如 `<Spin spinning description="加载中" />`
- **形态二(有子元素)**:包裹内容并覆盖半透明遮罩,如 `<Spin :spinning="loading"><表格/></Spin>`
**重点要求**:形态二必须做好——不能因为添加 Spin 导致内部元素位置/尺寸错乱。遮罩和指示器应绝对定位脱离文档流,内容容器只加 `position: relative`,加载时内容降透明并禁用交互,布局保持不变。
## 核心功能
- `spinning`(Boolean,默认 true):是否加载中
- `delay`(Number,默认 0):延迟显示(毫秒),防短请求闪烁,卸载时清理定时器
- `description`(String):加载文案,显示在指示器下方
- 插槽:`#indicator` 自定义指示器(默认 LoaderCircle 旋转)、`#description` 自定义文案、默认插槽(传入则启用形态二)
- 形态二:内容容器加载时 `opacity` 降低 + `pointer-events: none` 禁交互 + 轻量遮罩;指示器与文案绝对居中
- ARIA:容器 `aria-busy`、指示器 `role="status"` + `aria-label="加载中"`
- 不提供 size 枚举(遵循开发规范 7.1)
## 技术栈
- Vue 3 `<script setup>` + scoped SCSS(`@use './index.scss'`)
- 设计 token:`src/theme/tokens.css` 新增 `--fms-spin-size`
- 图标:`@lucide/vue` 按需导入 `LoaderCircle`(与 Select 加载态一致)
- 动画:`@keyframes spin` 旋转 800ms linear infinite(对齐 Button 的 `button-spin`),`prefers-reduced-motion` 停动画
## 实现方案
### 形态二布局防错乱(核心)
参考 antdv-next Spin 结构,采用"内容容器 + 绝对定位覆盖层"方案,所有覆盖元素脱离文档流:
```text
<div class="spin"> ← 仅形态二;position: relative(不改变内部布局)
<div class="spin-container"> ← 包裹 children,只加 position: relative
<slot /> ← 内部元素位置/尺寸完全不受影响
</div>
<div v-if="spinning" class="spin-mask" /> ← 绝对定位 inset:0,轻量半透明遮罩
<div v-if="spinning" class="spin-section"> ← 绝对定位 50%/50% translate(-50%,-50%) 居中
<LoaderCircle class="spin-indicator" /> ← 或 #indicator 插槽
<div class="spin-description">加载中</div> ← 或 #description 插槽
</div>
</div>
```
关键决策:
- 遮罩与指示器全部 `position: absolute`,不占文档流 → 内部元素位置不会偏移
- `.spin-container` 不加任何尺寸/布局属性,只做 `position: relative` 承载遮罩层 → 内部布局零影响
- 加载时容器 `opacity: 0.5; user-select: none; pointer-events: none`(对齐 antdv),遮罩 `z-index` 高于容器、低于指示器
- 形态一(无默认插槽)渲染为 `inline-flex` 指示器区,不产生包裹层
### Props 与状态
- `spinning` 默认 true;`delay` 用 `setTimeout` 延迟内部显示,`watch` + `onBeforeUnmount` 清理定时器(规范 4.3)
- 内部 `active` ref 为实际显示状态(delay 生效后),模板基于 `active` 渲染
### 样式与 token
- `--fms-spin-size: 20px` 加入 tokens.css `:root`(尺寸不随主题)
- 指示器颜色 `var(--fms-primary)`;遮罩用 `color-mix(in srgb, var(--fms-text) 6%, transparent)` 派生轻量色(比 Modal 的 `--fms-mask-bg` 0.45 轻,不压暗内容可读性)
- 文案用 `--fms-text-secondary` 12px
### 性能与可靠性
- 无高频事件、无全局监听;动画仅 transform(低成本),reduced-motion 关闭
- `delay` 定时器在卸载与 spinning 翻转时清理,避免内存泄漏
- 遮罩层拦截点击(pointer-events 由容器 none、遮罩 auto 恢复),防止加载中误操作
## 目录结构
```
fms-vue/src/
├── theme/
│ └── tokens.css # [MODIFY] 新增 --fms-spin-size: 20px
├── components/ui/
│ ├── spin/
│ │ ├── spin.vue # [NEW] 主组件:spinning/delay/description + #indicator/#description/默认插槽;形态一/二分支;delay 定时器清理;ARIA
│ │ └── index.scss # [NEW] 指示器旋转动画、形态二容器/遮罩/居中指示器、reduced-motion
│ └── demo/
│ └── spin.vue # [NEW] 演示:基础指示器/带文案/包裹内容遮罩(重点验证内部布局不偏移)/delay 防闪烁/自定义 indicator
└── App.vue # [MODIFY] 「反馈」分类注册 Spin + demoMap
```
## 关键代码结构
```js
// spin.vue props
defineProps({
spinning: { type: Boolean, default: true },
delay: { type: Number, default: 0 },
description: { type: String, default: '' },
})
// 内部 active 状态:delay 后置 true;watch(spinning/delay) + onBeforeUnmount 清理 setTimeout
// 模板:hasDefaultSlot ? 形态二(container+mask+section 绝对定位) : 形态一(inline-flex section)
```
## 设计说明
Spin 为反馈类加载组件,视觉遵循项目现有加载态语言(Button/Select 的旋转 Loader 图标):
- **指示器**:`LoaderCircle` 图标 20px(`--fms-spin-size`),主色 `--fms-primary`,800ms 匀速旋转;文案 12px 次级灰 `--fms-text-secondary`,指示器与文案垂直排列、居中
- **形态一**:inline-flex 水平/垂直居中的指示器区,可独立嵌入按钮、卡片等任意容器
- **形态二**:内容保持原布局不变,加载时内容降透明(opacity 0.5)并禁用交互;轻量遮罩(文字色 6% 混合)覆盖内容区,指示器+文案绝对居中于遮罩之上,形成"内容不可操作、加载居中"的视觉反馈
- 支持 `prefers-reduced-motion`:动画关闭,仅静态指示器
## Agent Extensions
### Skill
- **antdv-next**
- Purpose: 确认 Spin 形态二(包裹内容 + 遮罩)的 DOM 结构与交互细节(延迟显示、遮罩层级、指示器居中),确保方案与参考库一致
- Expected outcome: 形态二实现不破坏内部布局的准确依据
- **playwright-cli**
- Purpose: 在 dev server(5082)上实测 Spin demo,重点验证形态二包裹内容后内部元素位置/尺寸与未包裹时一致
- Expected outcome: 实测通过:指示器旋转、遮罩覆盖、内部布局零偏移、delay 生效、自定义 indicator 正常