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

Add component docs demos and tests

上级 962a1402
<template>
<div style="display: flex; gap: 12px;">
<ZButton disabled>
默认禁用
</ZButton>
<ZButton type="primary" disabled>
主要禁用
</ZButton>
<ZButton type="danger" disabled>
危险禁用
</ZButton>
</div>
</template>
# 按钮 Button
TODO: 补充组件用途、适用场景和注意事项。
按钮是页面里最常用的触发动作的控件,承担着新建、提交、确认、取消等行为。
## 何时使用
- 用户需要进行一个明确的操作时使用,例如保存、提交、删除。
- 当一个动作需要强调其重要程度时,可使用 `type` 属性区分主要/次要/危险按钮。
- 不要把按钮当作纯粹的链接,跳转优先使用链接组件。
## 演示
下面的示例分别演示:
- `basic`:通过 `type` 控制视觉强调(默认 / 主要 / 危险)。
- `custom`:按钮内容来自默认插槽,可以加图标或多段文本。
- `disabled`:设置 `disabled` 后按钮不可点击,也不会触发 `click` 事件。
- `loading``loading``true` 时显示加载图标,并自动屏蔽点击事件。
<!-- DEMO -->
<demo vue="button/demos/basic.vue" />
<demo vue="button/demos/custom.vue" />
<demo vue="button/demos/disabled.vue" />
<demo vue="button/demos/loading.vue" />
<!-- DEMO -->
......@@ -14,7 +30,24 @@ TODO: 补充组件用途、适用场景和注意事项。
<!-- API -->
TODO: 对照 `src/button/src/interface.ts` 补充 Props、Events、Slots 表格。
### Button Props
<!-- API -->
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| type | `'default' \| 'primary' \| 'danger'` | `'default'` | 按钮类型,控制视觉强调程度 |
| loading | `boolean` | `false` | 是否展示加载状态,启用后自动禁用点击 |
| disabled | `boolean` | `false` | 是否禁用按钮 |
### Button Events
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| click | `(event: MouseEvent)` | 点击按钮时触发,禁用或加载状态下不会触发 |
### Button Slots
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| default | `()` | 按钮内容,通常为文字或图标加文字 |
<!-- API -->
<script setup lang="ts">
import { ref } from 'vue'
const loading = ref(false)
function submit() {
loading.value = true
setTimeout(() => {
loading.value = false
}, 1500)
}
</script>
<template>
<div style="display: flex; gap: 12px; align-items: center;">
<ZButton type="primary" loading>
加载中
</ZButton>
<ZButton type="primary" :loading="loading" @click="submit">
{{ loading ? '提交中…' : '点击提交' }}
</ZButton>
</div>
</template>
import { mount } from '@vue/test-utils'
import { ZButton } from '../index'
describe('button', () => {
it('renders slot content', () => {
const wrapper = mount(ZButton, {
slots: {
default: '保存',
},
})
expect(wrapper.text()).toContain('保存')
})
it('renders as a <button type="button"> by default', () => {
const wrapper = mount(ZButton)
expect(wrapper.element.tagName).toBe('BUTTON')
expect(wrapper.attributes('type')).toBe('button')
expect(wrapper.attributes('disabled')).toBeUndefined()
})
it('emits click event with native MouseEvent', async () => {
const wrapper = mount(ZButton)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toHaveLength(1)
expect(wrapper.emitted('click')![0][0]).toBeInstanceOf(MouseEvent)
})
it('does not emit click when disabled', async () => {
const wrapper = mount(ZButton, {
props: { disabled: true },
})
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeUndefined()
expect(wrapper.attributes('disabled')).toBeDefined()
})
it('does not emit click when loading and shows a spinner', async () => {
const wrapper = mount(ZButton, {
props: { loading: true },
})
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeUndefined()
expect(wrapper.attributes('disabled')).toBeDefined()
expect(wrapper.find('span[aria-hidden="true"]').exists()).toBe(true)
})
it('does not render the spinner when not loading', () => {
const wrapper = mount(ZButton)
expect(wrapper.find('span[aria-hidden="true"]').exists()).toBe(false)
})
it('mounts every supported type variant', () => {
for (const type of ['default', 'primary', 'danger'] as const) {
const wrapper = mount(ZButton, {
props: { type },
slots: { default: type },
})
expect(wrapper.text()).toContain(type)
}
})
})
<template>
<ZCard title="无边框卡片" :bordered="false">
嵌入到色块或灰底的页面里时,使用 bordered = false 让边界更轻。
</ZCard>
</template>
# 卡片 Card
TODO: 补充基础卡片、标题、extra 插槽、无边框和内容布局示例。
Card 是页面内容的基础容器,常用于把信息分块展示。
## 何时使用
- 页面里多组信息需要相互区分,例如统计数据、详情区块、表单段落。
- 列表里一行内容信息较多、需要做视觉聚合时。
- 不要嵌套太多层卡片,超过两层时考虑改用更轻的分组方式。
## 演示
下面的示例分别演示:
- `basic`:带标题的简单卡片。
- `borderless`:嵌入到带底色页面时关闭边框,让卡片更轻。
- `title-extra``extra` 插槽用于放置查看更多、筛选等次级操作。
- `title-slot``title` 插槽优先级高于 `title` 属性,可放图标、状态点或其他富文本。
<!-- DEMO -->
<demo vue="card/demos/basic.vue" />
<demo vue="card/demos/borderless.vue" />
<demo vue="card/demos/title-extra.vue" />
<demo vue="card/demos/title-slot.vue" />
<!-- DEMO -->
......@@ -14,7 +30,19 @@ TODO: 补充基础卡片、标题、extra 插槽、无边框和内容布局示
<!-- API -->
TODO: 对照 `src/card/src/interface.ts` 补充 Props 和 Slots 表格。
### Card Props
<!-- API -->
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| title | `string` | `undefined` | 卡片标题文案,可被 `title` 插槽覆盖 |
| bordered | `boolean` | `true` | 是否显示边框 |
### Card Slots
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| title | `()` | 标题区域,优先级高于 `title` 属性 |
| extra | `()` | 标题栏右侧的操作或标记区域 |
| default | `()` | 卡片主体内容 |
<!-- API -->
<template>
<ZCard title="本月新增成员">
<template #extra>
<ZButton>查看全部</ZButton>
</template>
本月新增 8 位成员,其中 5 位来自市场部,3 位来自研发部。
</ZCard>
</template>
<template>
<ZCard>
<template #title>
<span style="display: inline-flex; gap: 6px; align-items: center;">
<ZStatusDot status="success" />
服务运行正常
</span>
</template>
<template #extra>
<ZTag color="#e8f3ff" text-color="#1677ff">
生产
</ZTag>
</template>
最近 24 小时无异常告警,平均响应时间 132ms。
</ZCard>
</template>
import { mount } from '@vue/test-utils'
import { ZCard } from '../index'
describe('card', () => {
it('renders the body via the default slot', () => {
const wrapper = mount(ZCard, {
slots: { default: '<p class="content">内容</p>' },
})
expect(wrapper.find('p.content').exists()).toBe(true)
expect(wrapper.text()).toContain('内容')
})
it('renders the title prop in the header', () => {
const wrapper = mount(ZCard, {
props: { title: '基础信息' },
slots: { default: '内容' },
})
expect(wrapper.find('header').exists()).toBe(true)
expect(wrapper.find('header').text()).toContain('基础信息')
})
it('prefers the title slot over the title prop', () => {
const wrapper = mount(ZCard, {
props: { title: '默认标题' },
slots: {
title: '<span class="custom">自定义标题</span>',
default: '内容',
},
})
expect(wrapper.find('.custom').exists()).toBe(true)
expect(wrapper.find('header').text()).toContain('自定义标题')
expect(wrapper.find('header').text()).not.toContain('默认标题')
})
it('renders the extra slot inside the header even without a title', () => {
const wrapper = mount(ZCard, {
slots: {
extra: '<button class="more">更多</button>',
default: '内容',
},
})
expect(wrapper.find('header').exists()).toBe(true)
expect(wrapper.find('header .more').exists()).toBe(true)
})
it('omits the header when neither title nor extra slot is provided', () => {
const wrapper = mount(ZCard, {
slots: { default: '内容' },
})
expect(wrapper.find('header').exists()).toBe(false)
})
it('mounts when bordered is set to false', () => {
const wrapper = mount(ZCard, {
props: { bordered: false },
slots: { default: '内容' },
})
expect(wrapper.text()).toContain('内容')
})
})
<script setup lang="ts">
import { ref } from 'vue'
const copied = ref(false)
let timer: ReturnType<typeof setTimeout> | null = null
function handleCopied() {
copied.value = true
if (timer)
clearTimeout(timer)
timer = setTimeout(() => {
copied.value = false
}, 1500)
}
</script>
<template>
<div style="display: flex; gap: 12px; align-items: center;">
<ZCopyText text="hello@example.com" @copied="handleCopied">
复制邮箱
</ZCopyText>
<span v-if="copied" style="color: #2f9e44;">已复制到剪贴板</span>
</div>
</template>
# 复制文本 CopyText
TODO: 补充基础复制、自定义内容、复制成功反馈和异常处理示例。
CopyText 把"复制到剪贴板"这个动作做成一个独立的小按钮,常用于订单号、邀请链接、API Key 等可被用户复用的字符串。
## 何时使用
- 详情页里有需要复制给别人的字符串(订单号、ID、邀请链接、Token 等)。
- 不希望用户手动选中再 Ctrl+C,希望一次点击直接完成复制。
- 注意:组件内部依赖 `navigator.clipboard`,需要 https 或 localhost 环境,且页面需要被聚焦。
## 演示
下面的示例分别演示:
- `basic`:传入 `text` 后点击按钮即可复制。
- `feedback`:监听 `copied` 事件,自行管理 toast 或临时文案。
- `slot`:默认插槽内可放任意富文本,文字与待复制的 `text` 可以不同。
<!-- DEMO -->
<demo vue="copy-text/demos/basic.vue" />
<demo vue="copy-text/demos/feedback.vue" />
<demo vue="copy-text/demos/slot.vue" />
<!-- DEMO -->
......@@ -14,7 +28,22 @@ TODO: 补充基础复制、自定义内容、复制成功反馈和异常处理
<!-- API -->
TODO: 对照 `src/copy-text/src/interface.ts` 补充 Props、Events 和 Slots 表格。
### CopyText Props
<!-- API -->
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| text | `string` | `''` | 要写入剪贴板的字符串;为空字符串时点击不会触发任何操作 |
### CopyText Events
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| copied | `(text: string)` | 写入剪贴板成功后触发,参数为实际写入的文本 |
### CopyText Slots
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| default | `()` | 按钮的可视内容;不传则展示 `text` 字符串本身 |
<!-- API -->
<template>
<ZCopyText text="ORDER-20260503-0001">
<span style="display: inline-flex; gap: 6px; align-items: center;">
<span style="color: #4e5969;">订单号</span>
<span>ORDER-20260503-0001</span>
<span style="color: #1677ff;">复制</span>
</span>
</ZCopyText>
</template>
import { mount } from '@vue/test-utils'
import { ZCopyText } from '../index'
function stubClipboard() {
const writeText = vi.fn().mockResolvedValue(undefined)
Object.defineProperty(globalThis.navigator, 'clipboard', {
value: { writeText },
configurable: true,
})
return writeText
}
describe('copyText', () => {
beforeEach(() => {
stubClipboard()
})
it('renders the text prop as button content', () => {
const wrapper = mount(ZCopyText, {
props: { text: 'abc-123' },
})
expect(wrapper.element.tagName).toBe('BUTTON')
expect(wrapper.text()).toContain('abc-123')
})
it('prefers the default slot over the text prop for display', () => {
const wrapper = mount(ZCopyText, {
props: { text: 'abc-123' },
slots: { default: '<span class="label">复制订单号</span>' },
})
expect(wrapper.find('.label').exists()).toBe(true)
expect(wrapper.text()).not.toContain('abc-123')
})
it('writes text to the clipboard and emits copied on click', async () => {
const writeText = stubClipboard()
const wrapper = mount(ZCopyText, {
props: { text: 'hello' },
})
await wrapper.trigger('click')
await Promise.resolve()
expect(writeText).toHaveBeenCalledTimes(1)
expect(writeText).toHaveBeenCalledWith('hello')
expect(wrapper.emitted('copied')).toEqual([['hello']])
})
it('does nothing when the text prop is empty', async () => {
const writeText = stubClipboard()
const wrapper = mount(ZCopyText)
await wrapper.trigger('click')
await Promise.resolve()
expect(writeText).not.toHaveBeenCalled()
expect(wrapper.emitted('copied')).toBeUndefined()
})
})
<template>
<div>
<span>已完成步骤</span>
<ZDivider dashed>
继续
</ZDivider>
<span>下一步操作</span>
</div>
</template>
# 分割线 Divider
TODO: 补充水平分割线、带文字分割线、虚线和垂直分割线示例。
Divider 用于在内容之间添加视觉上的分隔,可以是水平也可以是竖直。
## 何时使用
- 段落、卡片或区块之间需要明显但不打断阅读的分隔。
- 行内多个操作或元数据之间,使用竖直分割线区分。
- 不要用 Divider 当作页眉/页脚边框,它只是一个轻量分隔。
## 演示
下面的示例分别演示:
- `basic`:默认水平分割线。
- `dashed``dashed``true` 时分割线变为虚线。
- `vertical``direction``vertical` 时用于行内分隔,常配合操作组使用。
- `with-text`:默认插槽用于在分割线中间插入文字,仅在水平方向生效。
<!-- DEMO -->
<demo vue="divider/demos/basic.vue" />
<demo vue="divider/demos/dashed.vue" />
<demo vue="divider/demos/vertical.vue" />
<demo vue="divider/demos/with-text.vue" />
<!-- DEMO -->
......@@ -14,7 +30,17 @@ TODO: 补充水平分割线、带文字分割线、虚线和垂直分割线示
<!-- API -->
TODO: 对照 `src/divider/src/interface.ts` 补充 Props 和 Slots 表格。
### Divider Props
<!-- API -->
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| direction | `'horizontal' \| 'vertical'` | `'horizontal'` | 分割线方向 |
| dashed | `boolean` | `false` | 是否使用虚线样式 |
### Divider Slots
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| default | `()` | 分割线中间的文字,仅在 `direction = 'horizontal'` 时渲染 |
<!-- API -->
<template>
<div style="display: flex; align-items: center; gap: 8px;">
<span>编辑</span>
<ZDivider direction="vertical" />
<span>复制</span>
<ZDivider direction="vertical" />
<span style="color: #d92d20;">删除</span>
</div>
</template>
<template>
<div>
<span>登录</span>
<ZDivider></ZDivider>
<span>使用第三方账号</span>
</div>
</template>
import { mount } from '@vue/test-utils'
import { ZDivider } from '../index'
describe('divider', () => {
it('exposes the separator role on the root', () => {
const wrapper = mount(ZDivider)
expect(wrapper.attributes('role')).toBe('separator')
})
it('renders the default slot text in horizontal mode (default)', () => {
const wrapper = mount(ZDivider, {
slots: { default: '' },
})
expect(wrapper.text()).toBe('')
expect(wrapper.find('span').exists()).toBe(true)
})
it('omits the slot text when direction is vertical', () => {
const wrapper = mount(ZDivider, {
props: { direction: 'vertical' },
slots: { default: '' },
})
expect(wrapper.text()).toBe('')
expect(wrapper.find('span').exists()).toBe(false)
})
it('renders without text when no slot is provided', () => {
const wrapper = mount(ZDivider)
expect(wrapper.find('span').exists()).toBe(false)
})
it('mounts both dashed and solid variants', () => {
const dashed = mount(ZDivider, { props: { dashed: true } })
const solid = mount(ZDivider, { props: { dashed: false } })
expect(dashed.attributes('role')).toBe('separator')
expect(solid.attributes('role')).toBe('separator')
})
})
<template>
<div style="display: flex; flex-direction: column; gap: 24px;">
<ZEmpty description="没有匹配的搜索结果" />
<ZEmpty description="请先选择一个项目" />
</div>
</template>
<template>
<ZEmpty description="自定义占位图标">
<template #image>
<div
style="width: 64px; height: 64px; border-radius: 50%; background: #f2f3f5;
display: inline-flex; align-items: center; justify-content: center;
font-size: 28px; color: #86909c;"
>
?
</div>
</template>
</ZEmpty>
</template>
支持 Markdown
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册