diff --git a/docs/markdown/v2-progress-tracking.md b/docs/markdown/v2-progress-tracking.md index 9dccc54a9..81683c277 100644 --- a/docs/markdown/v2-progress-tracking.md +++ b/docs/markdown/v2-progress-tracking.md @@ -1,4 +1,4 @@ -## Vant 2.0 改动一览 +# Vant 2.0 改动一览 ## 不兼容更新 @@ -49,6 +49,8 @@ ## 新特性 +- 新增`Skeleton`骨架屏组件 + ### Sku - 新增`preview-open`事件 diff --git a/docs/src/demo-entry.js b/docs/src/demo-entry.js index 6f0a73fc6..b69130510 100644 --- a/docs/src/demo-entry.js +++ b/docs/src/demo-entry.js @@ -40,6 +40,7 @@ export default { 'rate': () => wrapper(import('../../packages/rate/demo'), 'rate'), 'search': () => wrapper(import('../../packages/search/demo'), 'search'), 'sidebar': () => wrapper(import('../../packages/sidebar/demo'), 'sidebar'), + 'skeleton': () => wrapper(import('../../packages/skeleton/demo'), 'skeleton'), 'sku': () => wrapper(import('../../packages/sku/demo'), 'sku'), 'slider': () => wrapper(import('../../packages/slider/demo'), 'slider'), 'stepper': () => wrapper(import('../../packages/stepper/demo'), 'stepper'), diff --git a/docs/src/doc.config.js b/docs/src/doc.config.js index 6a009488e..ac339f471 100644 --- a/docs/src/doc.config.js +++ b/docs/src/doc.config.js @@ -233,6 +233,10 @@ module.exports = { path: '/progress', title: 'Progress 进度条' }, + { + path: '/skeleton', + title: 'Skeleton 骨架屏' + }, { path: '/steps', title: 'Steps 步骤条' @@ -540,6 +544,10 @@ module.exports = { path: '/progress', title: 'Progress' }, + { + path: '/skeleton', + title: 'Skeleton' + }, { path: '/steps', title: 'Steps' diff --git a/docs/src/docs-entry.js b/docs/src/docs-entry.js index 5e9291552..ba4bb3c31 100644 --- a/docs/src/docs-entry.js +++ b/docs/src/docs-entry.js @@ -89,6 +89,8 @@ export default { 'search.zh-CN': () => import('../../packages/search/zh-CN.md'), 'sidebar.en-US': () => import('../../packages/sidebar/en-US.md'), 'sidebar.zh-CN': () => import('../../packages/sidebar/zh-CN.md'), + 'skeleton.en-US': () => import('../../packages/skeleton/en-US.md'), + 'skeleton.zh-CN': () => import('../../packages/skeleton/zh-CN.md'), 'sku.en-US': () => import('../../packages/sku/en-US.md'), 'sku.zh-CN': () => import('../../packages/sku/zh-CN.md'), 'slider.en-US': () => import('../../packages/slider/en-US.md'), diff --git a/packages/index.less b/packages/index.less index 9ef92ef24..d902afd5e 100644 --- a/packages/index.less +++ b/packages/index.less @@ -29,6 +29,7 @@ @import './progress/index'; @import './sidebar/index'; @import './sidebar-item/index'; +@import './skeleton/index'; @import './slider/index'; @import './stepper/index'; @import './swipe/index'; diff --git a/packages/index.ts b/packages/index.ts index 3005a095d..3f425d56e 100644 --- a/packages/index.ts +++ b/packages/index.ts @@ -53,6 +53,7 @@ import Row from './row'; import Search from './search'; import Sidebar from './sidebar'; import SidebarItem from './sidebar-item'; +import Skeleton from './skeleton'; import Sku from './sku'; import Slider from './slider'; import Step from './step'; @@ -131,6 +132,7 @@ const components = [ Search, Sidebar, SidebarItem, + Skeleton, Sku, Slider, Step, @@ -218,6 +220,7 @@ export { Search, Sidebar, SidebarItem, + Skeleton, Sku, Slider, Step, diff --git a/packages/skeleton/demo/index.vue b/packages/skeleton/demo/index.vue new file mode 100644 index 000000000..089597493 --- /dev/null +++ b/packages/skeleton/demo/index.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/packages/skeleton/en-US.md b/packages/skeleton/en-US.md new file mode 100644 index 000000000..7e0709304 --- /dev/null +++ b/packages/skeleton/en-US.md @@ -0,0 +1,62 @@ +## Skeleton + +### Install + +``` javascript +import { Skeleton } from 'vant'; + +Vue.use(Skeleton); +``` + +### Usage + +#### Basic Usage + +```html + +``` + +#### Show Avatar + +```html + +``` + +#### Show Children + +```html + +
Content
+
+``` + +```js +export default { + data() { + return { + loading: true + } + }, + mounted() { + this.loading = false; + } +}; +``` +### API + +| Attribute | Description | Type | Default | +|------|------|------|------| +| row | Row count | `Number` | `0` | +| row-width | Row width, can be array | `Number | String | Array` | `100%` | +| title | Whether to show title placeholder | `Boolean` | `false` | +| title-width | Title width | `Number | String` | `40%` | +| avatar | Whether to show avatar placeholder | `Boolean` | `false` | +| avatar-size | Size of avatar placeholder | `Number | String` | `32px` | +| avatar-shape | Shape of avatar placeholder,can be set to `square` | `String` | `round` | +| loading | Whether to show skeleton,pass `false` to show child component | `Boolean` | `true` | +| animate | Whether to enable animation | `Boolean` | `true` | diff --git a/packages/skeleton/index.less b/packages/skeleton/index.less new file mode 100644 index 000000000..fa9925387 --- /dev/null +++ b/packages/skeleton/index.less @@ -0,0 +1,54 @@ +@import '../style/var'; + +.van-skeleton { + display: flex; + padding: 0 15px; + + &__avatar { + flex-shrink: 0; + margin-right: 15px; + background-color: @active-color; + + &--round { + border-radius: 100%; + } + } + + &__content { + width: 100%; + } + + &__avatar + &__content { + padding-top: 8px; + } + + &__row, + &__title { + height: 16px; + background-color: @active-color; + } + + &__title { + margin: 0; + } + + &__row { + &:not(:first-child) { + margin-top: 12px; + } + } + + &__title + &__row { + margin-top: 20px; + } + + &--animate { + animation: van-skeleton-blink 1.2s ease-in-out infinite; + } +} + +@keyframes van-skeleton-blink { + 50% { + opacity: .6; + } +} diff --git a/packages/skeleton/index.tsx b/packages/skeleton/index.tsx new file mode 100644 index 000000000..b893b43db --- /dev/null +++ b/packages/skeleton/index.tsx @@ -0,0 +1,122 @@ +import { use } from '../utils'; +import { inherit } from '../utils/functional'; +import { isNumber } from '../utils/validate/number'; + +// Types +import { CreateElement, RenderContext } from 'vue/types'; +import { DefaultSlots } from '../utils/use/sfc'; + +export type SkeletonProps = { + row: number; + title?: boolean; + avatar?: boolean; + loading: boolean; + animate: boolean; + avatarSize: string; + avatarShape: 'square' | 'round'; + titleWidth: number | string; + rowWidth: number | string | (number | string)[]; +}; + +const [sfc, bem] = use('skeleton'); +const DEFAULT_ROW_WIDTH = '100%'; +const DEFAULT_LAST_ROW_WIDTH = '60%'; + +function suffixPx(value: string | number): string { + value = String(value); + return isNumber(value) ? `${value}px` : value; +} + +function Skeleton( + h: CreateElement, + props: SkeletonProps, + slots: DefaultSlots, + ctx: RenderContext +) { + if (!props.loading) { + return slots.default && slots.default(); + } + + function Title() { + if (props.title) { + return

; + } + } + + function Rows() { + const Rows = []; + const { rowWidth } = props; + + function getRowWidth(index: number) { + if (rowWidth === DEFAULT_ROW_WIDTH && index === props.row - 1) { + return DEFAULT_LAST_ROW_WIDTH; + } + + if (Array.isArray(rowWidth)) { + return rowWidth[index]; + } + + return rowWidth; + } + + for (let i = 0; i < props.row; i++) { + Rows.push(
); + } + + return Rows; + } + + function Avatar() { + if (props.avatar) { + const size = suffixPx(props.avatarSize); + return ( +
+ ); + } + } + + return ( +
+ {Avatar()} +
+ {Title()} + {Rows()} +
+
+ ); +} + +Skeleton.props = { + row: Number, + title: Boolean, + avatar: Boolean, + loading: { + type: Boolean, + default: true + }, + animate: { + type: Boolean, + default: true + }, + avatarSize: { + type: String, + default: '32px' + }, + avatarShape: { + type: String, + default: 'round' + }, + titleWidth: { + type: [Number, String], + default: '40%' + }, + rowWidth: { + type: [Number, String, Array], + default: DEFAULT_ROW_WIDTH + } +}; + +export default sfc(Skeleton); diff --git a/packages/skeleton/test/__snapshots__/demo.spec.js.snap b/packages/skeleton/test/__snapshots__/demo.spec.js.snap new file mode 100644 index 000000000..ba1e1dadb --- /dev/null +++ b/packages/skeleton/test/__snapshots__/demo.spec.js.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders demo correctly 1`] = ` +
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+`; diff --git a/packages/skeleton/test/__snapshots__/index.spec.js.snap b/packages/skeleton/test/__snapshots__/index.spec.js.snap new file mode 100644 index 000000000..2d6923cc0 --- /dev/null +++ b/packages/skeleton/test/__snapshots__/index.spec.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`avatar shape 1`] = ` +
+
+
+
+`; + +exports[`disable animate 1`] = ` +
+
+
+
+
+`; + +exports[`render chidren 1`] = `
Content
`; + +exports[`row-width array 1`] = ` +
+
+
+
+
+
+
+
+`; diff --git a/packages/skeleton/test/demo.spec.js b/packages/skeleton/test/demo.spec.js new file mode 100644 index 000000000..d647cfabc --- /dev/null +++ b/packages/skeleton/test/demo.spec.js @@ -0,0 +1,4 @@ +import Demo from '../demo'; +import demoTest from '../../../test/demo-test'; + +demoTest(Demo); diff --git a/packages/skeleton/test/index.spec.js b/packages/skeleton/test/index.spec.js new file mode 100644 index 000000000..dd3790a1f --- /dev/null +++ b/packages/skeleton/test/index.spec.js @@ -0,0 +1,45 @@ +import { mount } from '../../../test/utils'; +import Skeleton from '..'; + +test('row-width array', () => { + const wrapper = mount(Skeleton, { + propsData: { + row: 4, + rowWidth: ['100%', 30, '5rem'] + } + }); + expect(wrapper).toMatchSnapshot(); +}); + +test('render chidren', () => { + const wrapper = mount({ + template: ` + +
Content
+
+ `, + components: { Skeleton } + }); + expect(wrapper).toMatchSnapshot(); +}); + +test('avatar shape', () => { + const wrapper = mount(Skeleton, { + propsData: { + avatar: true, + avatarSize: 20, + avatarShape: 'square' + } + }); + expect(wrapper).toMatchSnapshot(); +}); + +test('disable animate', () => { + const wrapper = mount(Skeleton, { + propsData: { + row: 1, + aniamte: false + } + }); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/packages/skeleton/zh-CN.md b/packages/skeleton/zh-CN.md new file mode 100644 index 000000000..a833ee5ed --- /dev/null +++ b/packages/skeleton/zh-CN.md @@ -0,0 +1,69 @@ +## Skeleton 骨架屏 + +### 使用指南 + +``` javascript +import { Skeleton } from 'vant'; + +Vue.use(Skeleton); +``` + +### 代码演示 + +#### 基础用法 + +通过`title`属性显示标题占位图,通过`row`属性配置占位段落行数 + +```html + +``` + +#### 显示头像 + +通过`avatar`属性显示头像占位图 + +```html + +``` + +#### 展示子组件 + +将`loading`属性设置成`false`表示内容加载完成,此时会隐藏占位图,并显示`Skeleton`的子组件 + +```html + +
实际内容
+
+``` + +```js +export default { + data() { + return { + loading: true + } + }, + mounted() { + this.loading = false; + } +}; +``` + +### API + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +|------|------|------|------|------| +| row | 段落占位图行数 | `Number` | `0` | - | +| row-width | 段落占位图宽度,可传数组来设置每一行的宽度 | `Number | String | Array` | `100%` | - | +| title | 是否显示标题占位图 | `Boolean` | `false` | - | +| title-width | 标题占位图宽度 | `Number | String` | `40%` | - | +| avatar | 是否显示头像占位图 | `Boolean` | `false` | - | +| avatar-size | 头像占位图大小 | `Number | String` | `32px` | - | +| avatar-shape | 头像占位图形状,可选值为`square` | `String` | `round` | - | +| loading | 是否显示占位图,传`false`时会展示子组件内容 | `Boolean` | `true` | - | +| animate | 是否开启动画 | `Boolean` | `true` | - |