20260816220132

This commit is contained in:
oneao committed 2026-08-16 22:01:32 +08:00
1 parent b6f9a4c912
commit 72993ecdcd
179 files changed
+20050 -633

No files matched your search

@@ -0,0 +1,172 @@
---
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 正常