diff --git a/packages/devui-vue/devui/skeleton/index.ts b/packages/devui-vue/devui/skeleton/index.ts index 6bb2132be868bfb012143b3d03069b3352fbf120..e27275e67da74c3fbbe24c47571b470d0b7b0f0d 100644 --- a/packages/devui-vue/devui/skeleton/index.ts +++ b/packages/devui-vue/devui/skeleton/index.ts @@ -1,11 +1,13 @@ import type { App } from 'vue' import Skeleton from './src/skeleton' +import SkeletonItem from './src/item/item' Skeleton.install = function(app: App): void { app.component(Skeleton.name, Skeleton) + app.component(SkeletonItem.name, SkeletonItem) } -export { Skeleton } +export { Skeleton,SkeletonItem } export default { title: 'Skeleton 骨架屏', diff --git a/packages/devui-vue/devui/skeleton/src/item/item-types.ts b/packages/devui-vue/devui/skeleton/src/item/item-types.ts new file mode 100644 index 0000000000000000000000000000000000000000..e1a9324fb06eba3624e748983cb48ba63f611679 --- /dev/null +++ b/packages/devui-vue/devui/skeleton/src/item/item-types.ts @@ -0,0 +1,39 @@ +import type { ExtractPropTypes, PropType } from 'vue' + +export type ModelValue = number | string + +export const itemProps = { + row: { + type: Number, + default: 0 + }, + animate: { + type: Boolean, + default: true + }, + round: { + type: Boolean, + default: false + }, + loading: { + type: Boolean, + default: true + }, + avatarShape: { + type: String as PropType<'round' | 'square'>, + default: 'round' + }, + titleWidth: { + type: [String, Number] as PropType, + default: '40%' + }, + rowWidth: { + type: [Number, String, Array] as PropType>, + default: ['100%'] + }, + shape: { + type: String as PropType<'avatar' | 'image' | 'title' | 'paragraph' | 'button'>, + } +} as const + +export type ItemProps = ExtractPropTypes diff --git a/packages/devui-vue/devui/skeleton/src/item/item.scss b/packages/devui-vue/devui/skeleton/src/item/item.scss new file mode 100644 index 0000000000000000000000000000000000000000..8090ea49ec648a808950a4c2dd0948048dfaf645 --- /dev/null +++ b/packages/devui-vue/devui/skeleton/src/item/item.scss @@ -0,0 +1,62 @@ +@import "../../../styles-var/devui-var.scss"; + +.devui-skeleton__shape__avatar, +.devui-skeleton__shape__image, +.devui-skeleton__shape__title, +.devui-skeleton__shape__button { + background-color: #f2f2f2; +} +.devui-skeleton__shape__avatar { + width: 40px; + height: 40px; + background-color: #f2f2f2; +} +.devui-skeleton__shape__image { + width: 200px; + height: 150px; + border-radius: 4px; +} +.devui-skeleton__shape__title { + width: 40%; + height: 24px; +} +.devui-skeleton__shape__paragraph { + &__item { + background-color: #f2f2f2; + width: 100%; + height: 16px; + margin-bottom: 10px; + } + + &__item:last-child { + width: 60%; + } +} +.devui-skeleton__shape__button { + width: 115px; + height: 32px; +} + +@keyframes skeletonLoading { + to { + background-position-x: -20%; + } +} + +.devui-skeleton__animated .devui-skeleton__shape__avatar, +.devui-skeleton__animated.devui-skeleton__shape__avatar, +.devui-skeleton__animated.devui-skeleton__shape__image, +.devui-skeleton__animated.devui-skeleton__shape__title, +.devui-skeleton__animated.devui-skeleton__shape__paragraph > .devui-skeleton__shape__paragraph__item, +.devui-skeleton__animated.devui-skeleton__shape__button { + background: linear-gradient( + 100deg, + rgba(255, 255, 255, 0) 40%, + rgba(255, 255, 255, 0.5) 50%, + rgba(255, 255, 255, 0) 60% + ) + #f2f2f2; + background-size: 200% 100%; + background-position-x: 180%; + animation: 2s skeletonLoading ease-in-out infinite; +} diff --git a/packages/devui-vue/devui/skeleton/src/item/item.tsx b/packages/devui-vue/devui/skeleton/src/item/item.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ddadd8b06d152bc7c8182cc9b7ee1f73105ecdbd --- /dev/null +++ b/packages/devui-vue/devui/skeleton/src/item/item.tsx @@ -0,0 +1,86 @@ +import './item.scss' + +import { defineComponent } from 'vue' +import { itemProps, ItemProps } from './item-types' + +export default defineComponent({ + name: 'DSkeletonItem', + props: itemProps, + setup(props: ItemProps, ctx) { + const { slots } = ctx; + + function renderAnimate(isAnimated) { + return isAnimated ? 'devui-skeleton__animated' : '' + } + + function renderShapeParagraph(rowNum, rowWidth, round) { + const arr = [] + + function pushIntoArray(type) { + for (let index = 0; index < rowNum; index++) { + arr.push({ width: type }) + } + } + (function handleRowWidth() { + if (rowWidth instanceof Array) { + for (let index = 0; index < rowNum; index++) { + if (rowWidth[index]) { + switch (typeof rowWidth[index]) { + case 'string': + arr.push({ width: rowWidth[index] }) + break + case 'number': + arr.push({ width: `${rowWidth[index]}px` }) + } + } else { + arr.push({ width: 1 }) + } + } + } else { + switch (typeof rowWidth) { + case 'string': + pushIntoArray(rowWidth) + break + case 'number': + pushIntoArray(`${rowWidth}px`) + break + } + } + })() + + return
{ + arr.map(item => { + return
+ }) + }
+ } + + function renderAvatarStyle(avatarShape) { + function renderAvatarShape(avatarShape) { + return avatarShape === 'square' ? '' : 'border-radius:50%;' + } + + return (renderAvatarShape(avatarShape)) + } + + return () => { + if (props.loading && props.shape) { + switch (props.shape) { + case 'avatar': + return <> +
+ + case 'paragraph': + return <> + {renderShapeParagraph(props.row, props.rowWidth, props.round)} + + default: + return <> +
+ + } + } + return <>{slots.default?.()} + } + } +}) diff --git a/packages/devui-vue/devui/skeleton/src/skeleton-types.ts b/packages/devui-vue/devui/skeleton/src/skeleton-types.ts index e5e0bae4944efe9a0708697d68f8541a20f17163..5e8e5f371bfb36517fa69122821704d8952ed017 100644 --- a/packages/devui-vue/devui/skeleton/src/skeleton-types.ts +++ b/packages/devui-vue/devui/skeleton/src/skeleton-types.ts @@ -36,7 +36,7 @@ export const skeletonProps = { default: '40px' }, avatarShape: { - value: String as PropType<'round' | 'square'>, + type: String as PropType<'round' | 'square'>, default: 'round' }, titleWidth: { diff --git a/packages/devui-vue/devui/skeleton/src/skeleton.scss b/packages/devui-vue/devui/skeleton/src/skeleton.scss index 39c952314389e0878e7d1f06226122f5a9068c7e..28bccd986206a758272be81df2b1bb952c1d06dd 100644 --- a/packages/devui-vue/devui/skeleton/src/skeleton.scss +++ b/packages/devui-vue/devui/skeleton/src/skeleton.scss @@ -1,10 +1,10 @@ -@import '../../styles-var/devui-var.scss'; +@import "../../styles-var/devui-var.scss"; .devui-skeleton { display: flex; justify-content: space-between; - .devui-skeleton__avatar { + &__avatar { display: flex; flex: 1; justify-content: center; @@ -17,41 +17,46 @@ } } - .devui-skeleton__item__group { + &__group { flex: 11; + } - .devui-skeleton__item, - .devui-skeleton__title { - width: 100%; - height: 16px; - background-color: #f2f2f2; - } + &__item, + &__title { + width: 100%; + height: 16px; + background-color: #f2f2f2; + } - .devui-skeleton__title { - margin-top: 24px; - } + &__title { + margin-top: 24px; + } - .devui-skeleton__paragraph { - margin-top: 12px; - } + &__paragraph { + margin-top: 12px; + } - .devui-skeleton__item:last-child { - width: 60%; - } + &__item:last-child { + width: 60%; + } + + &__avatar > .avatar, + &__group > div > &__item { + margin-top: 12px; } } -.devui-skeleton-animated > .devui-skeleton__item__group > .devui-skeleton__title, -.devui-skeleton-animated > .devui-skeleton__avatar > .avatar, -.devui-skeleton-animated > .devui-skeleton__item__group > div > .devui-skeleton__item { - @keyframes skeletonLoading { - to { - background-position-x: -20%; - } + +@keyframes skeletonLoading { + to { + background-position-x: -20%; } +} - background: - linear-gradient( +.devui-skeleton__animated > .devui-skeleton__group > .devui-skeleton__title, +.devui-skeleton__animated > .devui-skeleton__group > div > .devui-skeleton__item, +.devui-skeleton__animated > .devui-skeleton__avatar > .avatar { + background: linear-gradient( 100deg, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0.5) 50%, @@ -62,12 +67,3 @@ background-position-x: 180%; animation: 2s skeletonLoading ease-in-out infinite; } - -.devui-skeleton__avatar > .avatar, -.devui-skeleton__item__group > div > .devui-skeleton__item { - margin-top: 12px; -} - -.devui-skeleton-animated > .devui-skeleton__avatar > .avatar { - animation-delay: 0.1s; -} diff --git a/packages/devui-vue/devui/skeleton/src/skeleton.tsx b/packages/devui-vue/devui/skeleton/src/skeleton.tsx index 0aad34479eb64e9b13c315fe5f4fbffc53355115..d69bd02af23cfe8272c04ddf07aa9af8c430d67a 100644 --- a/packages/devui-vue/devui/skeleton/src/skeleton.tsx +++ b/packages/devui-vue/devui/skeleton/src/skeleton.tsx @@ -10,7 +10,7 @@ export default defineComponent({ const { slots } = ctx; function renderAnimate(isAnimated) { - return isAnimated ? 'devui-skeleton-animated' : '' + return isAnimated ? 'devui-skeleton__animated' : '' } function renderBorderRadius(isRound) { return isRound ? 'border-radius: 1em;' : '' @@ -56,7 +56,11 @@ export default defineComponent({ }) }
} + function renderAvatarStyle(avatarSize, avatarShape) { + function renderAvatarShape(avatarShape) { + return avatarShape === 'square' ? '' : 'border-radius:50%;' + } function renderAvatarSize(avatarSize) { switch (typeof avatarSize) { case 'string': @@ -65,9 +69,6 @@ export default defineComponent({ return `width:${avatarSize}px;height:${avatarSize}px;` } } - function renderAvatarShape(avatarShape) { - return avatarShape === 'square' ? '' : 'border-radius:50%;' - } return (renderAvatarSize(avatarSize) + renderAvatarShape(avatarShape)) } @@ -86,26 +87,25 @@ export default defineComponent({ return (renderTitleWidth(titleWidth) + renderBorderRadius(isRound) + renderTitleVisibility(isVisible)) } - function renderSkeleton(isLoading) { - if (isLoading) { - return <> -
-
-
-
-
- {renderParagraph(props.paragraph, props.row, props.rowWidth, props.round)} -
- - } - - return <>{slots.default?.()} + function renderDefaultSkeleton() { + return <> +
+
+
+
+
+ {renderParagraph(props.paragraph, props.row, props.rowWidth, props.round)} +
+ } return () => { - return
- {renderSkeleton(props.loading)} -
+ if (props.loading) { + return
+ {renderDefaultSkeleton()} +
+ } + return <>{slots.default?.()} } } }) diff --git a/packages/devui-vue/docs/components/skeleton/index.md b/packages/devui-vue/docs/components/skeleton/index.md index 2aae393d3cfc9df4cbac4aa5723e37cfbd7525b2..2328d551f04d375d2f4b29c401079e08cd75a57f 100644 --- a/packages/devui-vue/docs/components/skeleton/index.md +++ b/packages/devui-vue/docs/components/skeleton/index.md @@ -1,10 +1,13 @@ # Skeleton 骨架屏 + 用于在内容加载过程中展示一组占位图形。 ### 何时使用 + 在需要等待加载内容的位置设置一个骨架屏,某些场景下比 Loading 的视觉效果更好。 ### 基本用法 + 最基本的占位效果。 :::demo @@ -14,10 +17,12 @@ ``` + ::: ### 复杂组合 + :::demo ```vue @@ -98,36 +103,67 @@ export default defineComponent({ } ``` + ::: -### API -d-skeleton -| 参数 | 类型 | 默认 | 说明 | -| :-----: | :-------: | :-----: | :-------------------------------------------- | -| loading | `boolean` | `true` | 是否显示骨架屏,传 `false` 时会展示子组件内容 | -| animate | `boolean` | `true` | 是否开启动画 | -| avatar | `boolean` | `false` | 是否显示头像占位图 | -| title | `boolean` | `true` | 是否显示标题占位图 | -| paragraph | `boolean` | `true` | 是否显示段落占位图 | -| round | `boolean` | `false` | 是否将标题和段落显示为圆角风格 | +### 拼接模式 + +提供细粒度的骨架屏元素,给予开发者更灵活的定制能力。 +:::demo + +```vue + +``` + +::: + +### d-skeleton Props + +| 参数 | 类型 | 默认 | 说明 | +| :-------: | :-------: | :-----: | :-------------------------------------------- | +| loading | `boolean` | `true` | 是否显示骨架屏,传 `false` 时会展示子组件内容 | +| animate | `boolean` | `true` | 是否开启动画 | +| avatar | `boolean` | `false` | 是否显示头像占位图 | +| title | `boolean` | `true` | 是否显示标题占位图 | +| paragraph | `boolean` | `true` | 是否显示段落占位图 | +| round | `boolean` | `false` | 是否将标题和段落显示为圆角风格 | + +### d-skeleton__avatar Props + +| 参数 | 类型 | 默认 | 说明 | +| :----------: | :----------------: | :-----: | :------------------------------- | +| avatar-size | `number \| string` | `40px` | 头像占位图大小 | +| avatar-shape | `string` | `round` | 头像占位图形状,可选值为`square` | -d-skeleton-avatar-props -| 参数 | 类型 | 默认 | 说明 | -| :-----: | :-------: | :-----: | :-------------------------------------------- | -| avatar-size | `number \| string` | `40px` | 头像占位图大小 | -| avatar-shape | `string` | `round` | 头像占位图形状,可选值为`square` | +### d-skeleton__title Props +| 参数 | 类型 | 默认 | 说明 | +| :---------: | :----------------: | :---: | :------------------- | +| title-width | `number \| string` | `40%` | 设置标题占位图的宽度 | -d-skeleton-title-props -| 参数 | 类型 | 默认 | 说明 | -| :-----: | :-------: | :-----: | :-------------------------------------------- | -| title-width | `number \| string` | `40%` | 设置标题占位图的宽度 | +### d-skeleton__paragraph Props +| 参数 | 类型 | 默认 | 说明 | +| :-------: | :----------------------------------------: | :--------: | :----------------------------------------- | +| row | `number` | `0` | 段落占位图行数 | +| row-width | `number \| string \| (number \| string)[]` | `["100%"]` | 段落占位图宽度,可传数组来设置每一行的宽度 | -d-skeleton-paragraph-props -| 参数 | 类型 | 默认 | 说明 | -| :-----: | :-------: | :-----: | :-------------------------------------------- | -| row | `number` | `0` | 段落占位图行数 | -| row-width | `number \| string \| (number \| string)[]` | `["100%"]` | 段落占位图宽度,可传数组来设置每一行的宽度 | +### d-skeleton-item Props +拼接模式 +| 参数 | 类型 | 默认 | 说明 | +| :-----: | :-------: | :----: | :------------------------------------------------------ | +| shape | `string` | - | 可选值为`avatar`,`image`,`title`,`paragraph`,`button`。 | +| animate | `boolean` | `true` | 是否开启动画 | +> paragraph 的 API 与默认模式相同; +### d-skeleton-item__avatar Props +| 参数 | 类型 | 默认 | 说明 | +| :----------: | :----------------: | :-----: | :------------------------------- | +| avatar-shape | `string` | `round` | 头像占位图形状,可选值为`square` | \ No newline at end of file