未验证 提交 e41f14ee 编辑于 作者: cyole's avatar cyole
浏览文件

Add component docs demos and tests

上级 962a1402
......@@ -35,6 +35,7 @@ const Start: DefaultTheme.NavItemWithLink[] = [
{ text: '快速上手', link: '/docs/guide/getting-started' },
{ text: '任务提交', link: '/docs/guide/submission' },
{ text: '常见问题', link: '/docs/guide/faq' },
{ text: '组件库待办与问题', link: '/docs/guide/component-issues' },
]
function getComponentsList(): DefaultTheme.NavItemWithLink[] {
......
# 组件总览
当前项目保留了一个完整参考组件:
按照用途对当前组件进行分组。带 ★ 的是完整参考组件,可以直接对照它的目录与写法。
- [示例按钮 ExampleButton](/components/example-button)
## 通用
同时预置了以下待补充组件:
页面里最常用的基础控件。
- [按钮 Button](/components/button)
- [提示 Tip](/components/tip)
- [头像标签 AvatarTag](/components/avatar-tag)
- [徽标 Badge](/components/badge)
- [示例按钮 ExampleButton ★](/components/example-button)
## 反馈
向用户反馈系统状态或操作结果。
- [警告提示 Alert](/components/alert)
- [卡片 Card](/components/card)
- [复制文本 CopyText](/components/copy-text)
- [分割线 Divider](/components/divider)
- [空状态 Empty](/components/empty)
- [提示 Tip](/components/tip)
- [加载 Loading](/components/loading)
- [进度条 Progress](/components/progress)
- [区块标题 SectionTitle](/components/section-title)
- [空状态 Empty](/components/empty)
## 数据展示
把数据以可读的形式呈现给用户。
- [卡片 Card](/components/card)
- [统计卡片 StatCard](/components/stat-card)
- [状态点 StatusDot](/components/status-dot)
- [进度条 Progress](/components/progress)
- [徽标 Badge](/components/badge)
- [标签 Tag](/components/tag)
- [状态点 StatusDot](/components/status-dot)
- [头像标签 AvatarTag](/components/avatar-tag)
## 排版
页面布局与分隔。
- [区块标题 SectionTitle](/components/section-title)
- [分割线 Divider](/components/divider)
## 工具
帮助用户完成单点操作。
- [复制文本 CopyText](/components/copy-text)
## 文档建设进度
| 状态 | 含义 |
| ------ | ------------------------------------------------------- |
| 已完成 | 介绍 / 何时使用 / 多个 demo / API 表 / 单元测试都已就绪 |
| 进行中 | 至少补齐了 API 表和一个非基础 demo,仍有待打磨 |
| 待完善 | 仅有 basic demo,文档还是占位 |
参考组件用于说明完整写法;预置组件用于同学补充 demo、文档、API 表格和测试场景
当前所有列出的预置组件均已进入"已完成"状态,新发现的待办与改进建议汇总在 [组件库待办与问题](/docs/guide/component-issues)
# 组件库待办与问题
这个页面用于汇总在文档、demo、API 校对和测试场景整理过程中发现的小问题与改进建议,作为后续 PR 或迭代的输入。
格式约定:每条记录至少包含「组件 / 类型 / 复现或现状 / 建议」。
## 类型分类
- `code`:组件实现有可观察到的问题或边界值未处理。
- `api``interface.ts` 与文档/直觉不一致,或缺少必要的字段。
- `docs`:文档表达可以更准确或更友好。
- `demo`:示例覆盖不足或写法不规范。
- `test`:测试场景缺漏或断言不稳定。
- `infra`:站点配置、构建、CI 等基础设施。
## 已发现
### 1. badge:模板未消费 `BadgeSlots`
- 类型:`code`
- 现状:`src/badge/src/Badge.vue` 没有调用 `defineSlots<BadgeSlots>()`,但 `interface.ts` 里已经定义了该类型。其他组件都明确做了 `defineSlots`,保持一致更利于后续维护与 IDE 类型补全。
- 建议:在 `<script setup>` 中补一行 `defineSlots<BadgeSlots>()`
### 2. loading:`LoadingEvents` 缺失但当前没有事件
- 类型:`api`
- 现状:组件没有 `defineEmits``interface.ts` 也没有 `LoadingEvents` 类型。不算 bug,但和其他组件不统一。
- 建议:在新增事件之前不必处理;一旦要加事件,记得同步 `interface.ts``defineEmits`
### 3. empty:默认描述硬编码中文
- 类型:`code` / `infra`
- 现状:`src/empty/src/interface.ts``description` 默认值写死为 `'暂无数据'`。如果未来组件库要做国际化,需要从 `defaults` 抽到 i18n 注入点。
- 建议:暂不处理,先在 README 或本页留 issue;做 i18n 时一并改。
### 4. badge:`max` 仅对数字 `value` 生效
- 类型:`docs`
- 现状:`max` 属性只在 `typeof value === 'number'` 时生效,字符串值不会被 clamp。这一行为目前只能从源码看出,文档未提示。
- 建议:保持现状,但在 Badge 文档的 API 段落明确提示 "max 仅对数字值生效"(已在新版文档中点明)。
### 5. progress:负值与超过 100 的值没有警告
- 类型:`demo` / `docs`
- 现状:组件内部会自动 clamp 到 0~100,对调用方来说是友好的,但调试时不容易发现传错。
- 建议:保留现行实现;在文档的"边界值"demo 里通过传 `-10``200` 直接演示,让调用者一眼看到行为(已新增 `progress/demos/boundary.vue`)。
### 6. copy-text:依赖 `navigator.clipboard`
- 类型:`docs`
- 现状:`navigator.clipboard.writeText` 仅在 https / localhost / 受信任上下文里可用,且页面需要被聚焦。在不可用环境下点击会抛错。
- 建议:在文档的「何时使用」里点明运行环境要求(已添加);测试通过 `Object.defineProperty` mock 掉 clipboard。后续可考虑增加失败回调或 `try/catch` 兜底。
### 7. divider:竖直方向忽略默认插槽
- 类型:`docs`
- 现状:`direction === 'vertical'` 时即使提供了默认插槽也不会渲染文字,这是有意为之。
- 建议:在 API 表格里显式说明该约束(已添加:"仅在 `direction = 'horizontal'` 时渲染")。
### 8. status-dot:`processing` 状态应有呼吸动画
- 类型:`code`(待确认)
- 现状:组件给 `processing` 加了一个 `processing` 类,但是否最终视觉上有动画依赖 `styles/index.ts` 的实现。需在站点上手验证。
- 建议:跑 `pnpm dev` 进入 `/components/status-dot` 检查 processing 状态点是否有动画;没有的话在 styles 中加 `@keyframes``animation`
### 9. 站点:组件总览页缺乏分类
- 类型:`infra`(已修复)
- 现状:`docs/components/index.md` 原本是平铺列表,组件多了之后不易索引。
- 处理:已重组为「通用 / 反馈 / 数据展示 / 排版 / 工具」五个分组。
## 提交建议
每发现一个新问题,按上面的格式追加一条;若已修复,改为 `### N. xxx(已修复)` 并在条目最后写明修复方式或 PR 链接,便于回溯。
<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(true)
function reset() {
visible.value = true
}
</script>
<template>
<div style="display: flex; flex-direction: column; gap: 12px;">
<ZAlert
v-if="visible"
type="success"
message="保存成功"
description="可以点击右侧 × 关闭这条提示。"
closable
@close="visible = false"
/>
<ZButton v-else @click="reset">
再来一次
</ZButton>
</div>
</template>
<template>
<div style="display: flex; flex-direction: column; gap: 12px;">
<ZAlert
type="info"
message="新版本已发布"
description="本次更新包含若干 bug 修复以及新的导出功能,建议尽快升级。"
/>
<ZAlert type="warning" message="单纯的警告,不带描述" />
</div>
</template>
# 警告提示 Alert
TODO: 补充不同类型、描述信息、可关闭和插槽自定义示例。
Alert 用于在页面上展示需要用户阅读的提示信息,覆盖一般通知、成功反馈、警告与错误四种语义。
## 何时使用
- 表单顶部、详情页等需要长期可见的反馈区域。
- 需要用户在继续操作前先看到的注意事项。
- 不要用 Alert 替代瞬时反馈(成功/失败的短消息),那种场景请使用 Toast。
## 演示
下面的示例分别演示:
- `basic``info` 类型的 Alert,包含标题与描述。
- `closable`:设置 `closable` 后渲染关闭按钮,点击触发 `close` 事件。
- `description`:只填 `message` 时是一行紧凑提示,加上 `description` 变两行。
- `types`:四种语义类型 `info / success / warning / error`
<!-- DEMO -->
<demo vue="alert/demos/basic.vue" />
<demo vue="alert/demos/closable.vue" />
<demo vue="alert/demos/description.vue" />
<demo vue="alert/demos/types.vue" />
<!-- DEMO -->
......@@ -14,7 +30,26 @@ TODO: 补充不同类型、描述信息、可关闭和插槽自定义示例。
<!-- API -->
TODO: 对照 `src/alert/src/interface.ts` 补充 Props、Events 和 Slots 表格。
### Alert Props
<!-- API -->
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| type | `'info' \| 'success' \| 'warning' \| 'error'` | `'info'` | 警告语义类型 |
| message | `string` | `undefined` | 主文案,可被 `message` 插槽覆盖 |
| description | `string` | `undefined` | 描述文案,可被默认插槽覆盖;不填则不渲染描述区域 |
| closable | `boolean` | `false` | 是否显示关闭按钮 |
### Alert Events
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| close | `()` | 点击关闭按钮时触发,需要父组件控制显隐 |
### Alert Slots
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| message | `()` | 主文案插槽,优先级高于 `message` 属性 |
| default | `()` | 描述插槽,优先级高于 `description` 属性 |
<!-- API -->
<template>
<div style="display: flex; flex-direction: column; gap: 12px;">
<ZAlert type="info" message="info:用于一般性提示" />
<ZAlert type="success" message="success:用于操作成功反馈" />
<ZAlert type="warning" message="warning:用于需要用户注意的警告" />
<ZAlert type="error" message="error:用于操作失败或异常状态" />
</div>
</template>
import { mount } from '@vue/test-utils'
import { ZAlert } from '../index'
describe('alert', () => {
it('renders message via prop', () => {
const wrapper = mount(ZAlert, {
props: { message: '保存成功' },
})
expect(wrapper.text()).toContain('保存成功')
})
it('prefers message slot over message prop', () => {
const wrapper = mount(ZAlert, {
props: { message: '默认消息' },
slots: { message: '<strong>插槽消息</strong>' },
})
expect(wrapper.text()).toContain('插槽消息')
expect(wrapper.text()).not.toContain('默认消息')
expect(wrapper.find('strong').exists()).toBe(true)
})
it('renders description via prop', () => {
const wrapper = mount(ZAlert, {
props: {
message: '提示',
description: '更多说明',
},
})
expect(wrapper.text()).toContain('更多说明')
})
it('prefers default slot over description prop', () => {
const wrapper = mount(ZAlert, {
props: {
message: '提示',
description: '默认描述',
},
slots: { default: '插槽描述' },
})
expect(wrapper.text()).toContain('插槽描述')
expect(wrapper.text()).not.toContain('默认描述')
})
it('omits description block when neither prop nor default slot is provided', () => {
const wrapper = mount(ZAlert, {
props: { message: '提示' },
})
expect(wrapper.text()).toBe('提示')
})
it('does not render the close button by default', () => {
const wrapper = mount(ZAlert, {
props: { message: '提示' },
})
expect(wrapper.find('button').exists()).toBe(false)
})
it('emits close event when the close button is clicked', async () => {
const wrapper = mount(ZAlert, {
props: {
message: '提示',
closable: true,
},
})
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('close')).toHaveLength(1)
})
it('mounts every supported type variant', () => {
for (const type of ['info', 'success', 'warning', 'error'] as const) {
const wrapper = mount(ZAlert, {
props: { type, message: type },
})
expect(wrapper.text()).toContain(type)
}
})
})
<template>
<div style="display: flex; gap: 16px; align-items: center;">
<ZAvatarTag
name="Alice"
avatar="https://api.dicebear.com/7.x/avataaars/svg?seed=Alice"
/>
<ZAvatarTag
name="Bob"
avatar="https://api.dicebear.com/7.x/avataaars/svg?seed=Bob"
/>
</div>
</template>
<template>
<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
<ZAvatarTag name="张三" />
<ZAvatarTag name="李四" color="#2f9e44" />
<ZAvatarTag name="王五" color="#f59f00" />
<ZAvatarTag name="赵六" color="#d92d20" />
</div>
</template>
# 头像标签 AvatarTag
TODO: 补充组件用途、头像加载、颜色、默认插槽和空名称场景。
AvatarTag 把头像和姓名合并到一行展示,常用于成员列表、操作日志、@提及等场景。
## 何时使用
- 列表或表格的成员列里需要既显示头像又显示姓名。
- 操作日志/审计列表里展示"谁做了什么"。
- 头像缺失时需要使用文字首字母作为占位。
## 演示
下面的示例分别演示:
- `avatar`:传入 `avatar` 后渲染 `img` 元素,`alt` 自动使用 `name`
- `basic`:只传 `name`,组件会自动取首字母作为占位头像。
- `color`:未提供 `avatar` 时,通过 `color` 指定占位背景色。
- `slot`:默认插槽优先级高于 `name`,可在姓名旁加角色标签等富文本。
<!-- DEMO -->
<demo vue="avatar-tag/demos/avatar.vue" />
<demo vue="avatar-tag/demos/basic.vue" />
<demo vue="avatar-tag/demos/color.vue" />
<demo vue="avatar-tag/demos/slot.vue" />
<!-- DEMO -->
......@@ -14,7 +30,18 @@ TODO: 补充组件用途、头像加载、颜色、默认插槽和空名称场
<!-- API -->
TODO: 对照 `src/avatar-tag/src/interface.ts` 补充 Props 和 Slots 表格。
### AvatarTag Props
<!-- API -->
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| name | `string` | `''` | 姓名;同时用作占位头像的首字母来源与 img 的 alt |
| avatar | `string` | `undefined` | 头像图片地址;不传则使用首字母占位 |
| color | `string` | `'#1677ff'` | 占位头像的背景色,仅在未传 `avatar` 时生效 |
### AvatarTag Slots
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| default | `()` | 姓名展示区域,优先级高于 `name` 属性 |
<!-- API -->
<template>
<ZAvatarTag name="Alice">
<span style="display: inline-flex; gap: 6px; align-items: center;">
<span>Alice</span>
<ZTag color="#e8f3ff" text-color="#1677ff">
管理员
</ZTag>
</span>
</ZAvatarTag>
</template>
import { mount } from '@vue/test-utils'
import { ZAvatarTag } from '../index'
describe('avatarTag', () => {
it('renders the uppercase initial of the name when no avatar is provided', () => {
const wrapper = mount(ZAvatarTag, {
props: { name: 'alice' },
})
expect(wrapper.find('img').exists()).toBe(false)
expect(wrapper.text()).toContain('A')
expect(wrapper.text()).toContain('alice')
})
it('handles names with leading whitespace by trimming first', () => {
const wrapper = mount(ZAvatarTag, {
props: { name: ' bob' },
})
const spans = wrapper.findAll('span')
expect(spans[1].text()).toBe('B')
})
it('renders an <img> element when avatar prop is provided', () => {
const wrapper = mount(ZAvatarTag, {
props: {
name: 'Alice',
avatar: 'https://example.com/a.png',
},
})
const img = wrapper.find('img')
expect(img.exists()).toBe(true)
expect(img.attributes('src')).toBe('https://example.com/a.png')
expect(img.attributes('alt')).toBe('Alice')
})
it('uses the color prop as the placeholder background', () => {
const wrapper = mount(ZAvatarTag, {
props: { name: 'A', color: 'rgb(0, 128, 255)' },
})
const placeholder = wrapper.findAll('span').at(1)!
const style = placeholder.attributes('style') ?? ''
expect(style).toContain('background-color: rgb(0, 128, 255)')
})
it('prefers the default slot over the name for the displayed label', () => {
const wrapper = mount(ZAvatarTag, {
props: { name: 'Alice' },
slots: { default: '<b class="custom">爱丽丝</b>' },
})
expect(wrapper.find('.custom').exists()).toBe(true)
expect(wrapper.text()).not.toContain('Alice')
})
it('falls back to an empty initial when name is empty', () => {
const wrapper = mount(ZAvatarTag, {
props: { name: '' },
})
const placeholder = wrapper.findAll('span').at(1)!
expect(placeholder.text()).toBe('')
})
})
<template>
<div style="display: flex; gap: 24px; align-items: center;">
<ZBadge :value="3" color="#1677ff">
<ZButton>蓝色</ZButton>
</ZBadge>
<ZBadge :value="3" color="#2f9e44">
<ZButton>绿色</ZButton>
</ZBadge>
<ZBadge :value="3" color="#f59f00">
<ZButton>橙色</ZButton>
</ZBadge>
</div>
</template>
<template>
<div style="display: flex; gap: 24px; align-items: center;">
<ZBadge dot>
<ZButton>消息</ZButton>
</ZBadge>
<ZBadge dot color="#2f9e44">
<ZButton>状态</ZButton>
</ZBadge>
</div>
</template>
# 徽标 Badge
TODO: 补充数字徽标、红点、自定义颜色、最大值等示例。
Badge 用于在按钮、图标或其他元素的角落显示数字、文字或红点,常用于通知未读数、新内容等场景。
## 何时使用
- 列表项、菜单项需要标记新消息或未读数量。
- 想让用户注意到入口的新状态,但不希望中断当前操作。
- 不需要显示具体数字时,使用 `dot` 模式做简单提示。
## 演示
下面的示例分别演示:
- `basic`:通过 `value` 显示一个数字徽标。
- `color`:通过 `color` 自定义角标背景色。
- `dot``dot``true` 时只显示一个圆点,不展示具体值。
- `max``value` 大于 `max` 时显示为 `${max}+`
- `string``value` 也支持字符串,常用于 `NEW``HOT` 等标识。
<!-- DEMO -->
<demo vue="badge/demos/basic.vue" />
<demo vue="badge/demos/color.vue" />
<demo vue="badge/demos/dot.vue" />
<demo vue="badge/demos/max.vue" />
<demo vue="badge/demos/string.vue" />
<!-- DEMO -->
......@@ -14,7 +32,19 @@ TODO: 补充数字徽标、红点、自定义颜色、最大值等示例。
<!-- API -->
TODO: 对照 `src/badge/src/interface.ts` 补充 Props 和 Slots 表格。
### Badge Props
<!-- API -->
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| value | `string \| number` | `undefined` | 角标内容;`dot` 为 true 时此值会被忽略 |
| max | `number` | `undefined` | 数字上限,仅当 `value` 为数字且大于 `max` 时显示为 `${max}+` |
| dot | `boolean` | `false` | 是否使用红点模式 |
| color | `string` | `'#ff4d4f'` | 角标背景色 |
### Badge Slots
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| default | `()` | 被角标修饰的内容,例如按钮、图标 |
<!-- API -->
<template>
<div style="display: flex; gap: 24px; align-items: center;">
<ZBadge :value="5" :max="99">
<ZButton>消息</ZButton>
</ZBadge>
<ZBadge :value="99" :max="99">
<ZButton>未读</ZButton>
</ZBadge>
<ZBadge :value="120" :max="99">
<ZButton>超额</ZButton>
</ZBadge>
</div>
</template>
<template>
<div style="display: flex; gap: 24px; align-items: center;">
<ZBadge value="NEW">
<ZButton>新功能</ZButton>
</ZBadge>
<ZBadge value="HOT" color="#d92d20">
<ZButton>热门</ZButton>
</ZBadge>
</div>
</template>
import { mount } from '@vue/test-utils'
import { ZBadge } from '../index'
describe('badge', () => {
it('renders the default slot content', () => {
const wrapper = mount(ZBadge, {
slots: { default: '<span class="anchor">消息</span>' },
})
expect(wrapper.find('.anchor').exists()).toBe(true)
})
it('renders a numeric value', () => {
const wrapper = mount(ZBadge, {
props: { value: 3 },
})
expect(wrapper.find('sup').text()).toBe('3')
})
it('renders a string value as-is', () => {
const wrapper = mount(ZBadge, {
props: { value: 'NEW' },
})
expect(wrapper.find('sup').text()).toBe('NEW')
})
it('caps the value with max plus when value exceeds max', () => {
const wrapper = mount(ZBadge, {
props: { value: 120, max: 99 },
})
expect(wrapper.find('sup').text()).toBe('99+')
})
it('keeps the original value when not exceeding max', () => {
const wrapper = mount(ZBadge, {
props: { value: 99, max: 99 },
})
expect(wrapper.find('sup').text()).toBe('99')
})
it('renders nothing inside the badge when dot is true', () => {
const wrapper = mount(ZBadge, {
props: { value: 99, dot: true },
})
expect(wrapper.find('sup').text()).toBe('')
})
it('applies the color prop as backgroundColor on the badge', () => {
const wrapper = mount(ZBadge, {
props: { value: 1, color: 'rgb(0, 128, 0)' },
})
const style = wrapper.find('sup').attributes('style') ?? ''
expect(style).toContain('background-color: rgb(0, 128, 0)')
})
})
<template>
<div style="display: flex; gap: 12px; align-items: center;">
<ZButton type="primary">
<span style="margin-right: 4px;"></span>新建
</ZButton>
<ZButton type="danger">
删除所选
</ZButton>
<ZButton>
取消
</ZButton>
</div>
</template>
支持 Markdown
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册