

APIs
参数 | 说明 | 类型 | 默认值 | 必传 |
---|
animated | 是否展示动画效果 | boolean | true | false |
button | 是否使用按钮占位图 | boolean | SkeletonButtonProps | false | false |
avatar | 是否显示头像占位图 | boolean | SkeletonAvatarProps | false | false |
input | 是否使用输入框占位图 | boolean | SkeletonInputProps | false | false |
image | 是否使用图像占位图 | boolean | false | false |
title | 是否显示标题占位图 | boolean | SkeletonTitleProps | true | false |
paragraph | 是否显示段落占位图 | boolean | SkeletonParagraphProps | true | false |
loading | 为 true 时,显示占位图,反之则直接展示子组件 | boolean | true | false |
SkeletonButtonProps Type
名称 | 说明 | 类型 | 必传 |
---|
shape | 指定按钮的形状 | ‘default’ | ‘round’ | ‘circle’ | false |
size | 设置按钮的大小 | ‘default’ | ‘small’ | ‘large’ | false |
block | 将按钮宽度调整为其父宽度的选项 | boolean | false |
SkeletonAvatarProps Type
名称 | 说明 | 类型 | 必传 |
---|
shape | 指定头像的形状 | ‘circle’ | ‘square’ | false |
size | 设置头像占位图的大小 | number | ‘default’ | ‘small’ | ‘large’ | false |
SkeletonInputProps Type
名称 | 说明 | 类型 | 必传 |
---|
size | 设置输入框的大小 | ‘default’ | ‘small’ | ‘large’ | false |
SkeletonTitleProps Type
名称 | 说明 | 类型 | 必传 |
---|
width | 设置标题占位图的宽度 | number | string | false |
SkeletonParagraphProps Type
名称 | 说明 | 类型 | 必传 |
---|
rows | 设置段落占位图的行数 | number | string | false |
width | 设置段落占位图的宽度,若为数组时则为对应的每行宽度,反之则是最后一行的宽度 | number | string | Array<number|string> | false |
创建骨架屏组件Skeleton.vue
<script setup lang="ts">
import { computed } from 'vue'interface SkeletonButtonProps {shape?: 'default'|'round'|'circle' size?: 'default'|'small'|'large' block?: boolean
}
interface SkeletonAvatarProps {shape?: 'circle'|'square' size?: number|'default'|'small'|'large'
}
interface SkeletonInputProps {size: 'default'|'small'|'large'
}
interface SkeletonTitleProps {width?: number|string
}
interface SkeletonParagraphProps {rows?: number|string width?: number|string|Array<number|string>
}
interface Props {animated?: boolean button?: boolean|SkeletonButtonProps avatar?: boolean|SkeletonAvatarProps input?: boolean|SkeletonInputProps image?: boolean title?: boolean|SkeletonTitleProps paragraph?: boolean|SkeletonParagraphProps loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {animated: true,button: false,image: false,avatar: false,input: false,title: true,paragraph: true,loading: true
})
const buttonSize = computed(() => {if (typeof props.button === 'object') {if (props.button.size === 'large') {return 40}if (props.button.size === 'small') {return 24}return 32}
})
const titleTop = computed(() => {if (typeof props.avatar === 'boolean') {return 8} else {if (typeof props.avatar.size === 'number') {return (props.avatar.size - 16) / 2} else {const topMap = {default: 8,small: 4,large: 12}return topMap[props.avatar.size || 'default']}}
})
const titleWidth = computed(() => {if (typeof props.title === 'boolean') {return '38%'} else {if (typeof props.title.width === 'number') {return props.title.width + 'px'}return props.title.width || '38%'}
})
const paragraphRows = computed(() => {if (typeof props.paragraph === 'boolean') {return 3}return props.paragraph.rows
})
const paragraphWidth = computed(() => {if (typeof props.paragraph === 'boolean') {return Array(paragraphRows.value)} else {if (Array.isArray(props.paragraph.width)) {return props.paragraph.width.map((width: number|string) => {if (typeof width === 'number') {return width + 'px'} else {return width}})} else if (typeof props.paragraph.width === 'number') {return Array(paragraphRows.value).fill(props.paragraph.width + 'px')} else {return Array(paragraphRows.value).fill(props.paragraph.width)}}
})
</script>
<template><divv-if="loading":class="['m-skeleton', {'m-skeleton-avatar': avatar, 'm-skeleton-animated': animated}]":style="`--button-size: ${buttonSize}px; --title-top: ${titleTop}px;`"><spanv-if="button":class="['u-skeleton-button',{'u-button-round': typeof button !== 'boolean' && button.shape === 'round','u-button-circle': typeof button !== 'boolean' && button.shape === 'circle','u-button-sm': typeof button !== 'boolean' && button.size === 'small','u-button-lg': typeof button !== 'boolean' && button.size === 'large','u-button-block': typeof button !== 'boolean' && button.shape !== 'circle' && button.block,}]"></span><span:class="['u-skeleton-input',{'u-input-sm': typeof input !== 'boolean' && input.size === 'small','u-input-lg': typeof input !== 'boolean' && input.size === 'large',}]" v-if="input"></span><div class="m-skeleton-image" v-if="image"><svg viewBox="0 0 1098 1024" xmlns="http://www.w3.org/2000/svg" class="m-skeleton-image-svg"><path class="u-skeleton-image-path" d="M365.714286 329.142857q0 45.714286-32.036571 77.677714t-77.677714 32.036571-77.677714-32.036571-32.036571-77.677714 32.036571-77.677714 77.677714-32.036571 77.677714 32.036571 32.036571 77.677714zM950.857143 548.571429l0 256-804.571429 0 0-109.714286 182.857143-182.857143 91.428571 91.428571 292.571429-292.571429zM1005.714286 146.285714l-914.285714 0q-7.460571 0-12.873143 5.412571t-5.412571 12.873143l0 694.857143q0 7.460571 5.412571 12.873143t12.873143 5.412571l914.285714 0q7.460571 0 12.873143-5.412571t5.412571-12.873143l0-694.857143q0-7.460571-5.412571-12.873143t-12.873143-5.412571zM1097.142857 164.571429l0 694.857143q0 37.741714-26.843429 64.585143t-64.585143 26.843429l-914.285714 0q-37.741714 0-64.585143-26.843429t-26.843429-64.585143l0-694.857143q0-37.741714 26.843429-64.585143t64.585143-26.843429l914.285714 0q37.741714 0 64.585143 26.843429t26.843429 64.585143z"></path></svg></div><div class="m-skeleton-header" v-if="avatar"><span:class="['u-skeleton-avatar',{'u-avatar-sm': typeof avatar !== 'boolean' && avatar.size === 'small','u-avatar-lg': typeof avatar !== 'boolean' && avatar.size === 'large','u-avatar-square': typeof avatar !== 'boolean' && avatar.shape === 'square',}]"></span></div><template v-if="!button && !image && !input"><div class="m-skeleton-content"><h3 class="u-skeleton-title" :style="{ width: titleWidth }"></h3><ul class="u-skeleton-paragraph"><li v-for="n in paragraphRows" :key="n" :style="`width: ${paragraphWidth[(n as number) - 1]};`"></li></ul></div></template></div><slot v-else></slot>
</template>
<style lang="less" scoped>
.m-skeleton {display: table;width: 100%;.u-skeleton-button {display: inline-block;vertical-align: top;background: rgba(0, 0, 0, .06);border-radius: 4px;width: 64px;min-width: 64px;height: 32px;line-height: 32px;}.u-button-sm {width: 48px;min-width: 48px;height: 24px;line-height: 24px;}.u-button-lg {width: 80px;min-width: 80px;height: 40px;line-height: 40px;}.u-button-round {border-radius: var(--button-size);}.u-button-circle {width: var(--button-size);min-width: var(--button-size);border-radius: 50%;}.u-button-block {width: 100%;}.u-skeleton-input {display: inline-block;vertical-align: top;background: rgba(0, 0, 0, 0.06);border-radius: 4px;width: 160px;min-width: 160px;height: 32px;line-height: 32px;}.u-input-sm {width: 120px;min-width: 120px;height: 24px;line-height: 24px;}.u-input-lg {width: 200px;min-width: 200px;height: 40px;line-height: 40px;}.m-skeleton-image {display: flex;align-items: center;justify-content: center;vertical-align: top;background: rgba(0, 0, 0, .06);border-radius: 4px;width: 96px;height: 96px;line-height: 96px;.m-skeleton-image-svg {width: 48px;height: 48px;line-height: 48px;max-width: 192px;max-height: 192px;.u-skeleton-image-path {fill: #bfbfbf;}}}.m-skeleton-header {display: table-cell;padding-right: 16px;vertical-align: top;.u-skeleton-avatar {display: inline-block;vertical-align: top;background: rgba(0, 0, 0, .06);width: 32px;height: 32px;line-height: 32px;border-radius: 50%;}.u-avatar-sm {width: 24px;height: 24px;line-height: 24px;}.u-avatar-lg {width: 40px;height: 40px;line-height: 40px;}.u-avatar-square {border-radius: 6px;}}.m-skeleton-content {display: table-cell;width: 100%;vertical-align: top;.u-skeleton-title {margin: 0;height: 16px;background: rgba(0, 0, 0, .06);border-radius: 4px;}.u-skeleton-paragraph {margin-top: 24px;padding: 0;li {height: 16px;list-style: none;background: rgba(0, 0, 0, .06);border-radius: 4px;&:not(:first-child) {margin-top: 16px;}&:last-child {width: 61%;}}}}
}
.m-skeleton-avatar {.m-skeleton-content {.u-skeleton-title {margin-top: var(--title-top);}.u-skeleton-paragraph {margin-top: 28px;}}
}
.m-skeleton-animated {.u-skeleton-button,.u-skeleton-input,.m-skeleton-image,.m-skeleton-header .u-skeleton-avatar,.m-skeleton-content .u-skeleton-title,.m-skeleton-content .u-skeleton-paragraph li {position: relative;z-index: 0;overflow: hidden;background: transparent;&::after {position: absolute;top: 0;left: -150%;bottom: 0;right: -150%;background: linear-gradient(90deg, rgba(0, 0, 0, .06) 25%, rgba(0, 0, 0, .15) 37%, rgba(0, 0, 0, .06) 63%);animation-name: skeleton-loading;animation-duration: 1.4s;animation-timing-function: ease;animation-iteration-count: infinite;content: "";}@keyframes skeleton-loading {0% {transform: translateX(-37.5%);}100% {transform: translateX(37.5%);}}}
}
</style>
在要使用的页面引入
<script setup lang="ts">
import Skeleton from './Skeleton.vue'
import { ref } from 'vue'const loading = ref<boolean>(false)const showSkeleton = () => {loading.value = truesetTimeout(() => {loading.value = false}, 2000)
}
const animated = ref(false)
const block = ref(false)
const size = ref('default')
const buttonShape = ref('default')
const avatarShape = ref('circle')
const sizeOptions = ref([{label: 'Default',value: 'default'},{label: 'Large',value: 'large'},{label: 'Small',value: 'small'}
])
const buttonShapeOptions = ref([{label: 'Default',value: 'default'},{label: 'Round',value: 'round'},{label: 'Circle',value: 'circle'}
])
const avatarShapeOptions = ref([{label: 'Square',value: 'square'},{label: 'Circle',value: 'circle'}
])
</script>
<template><div><h1>{{ $route.name }} {{ $route.meta.title }}</h1><h2 class="mt30 mb10">基本使用</h2><Skeleton /><h2 class="mt30 mb10">复杂的组合</h2><Skeleton avatar :paragraph="{ rows: 4 }" /><h2 class="mt30 mb10">包含子组件</h2><Button :loading="loading" @click="showSkeleton">Show Skeleton</Button><br/><br/><Skeleton :loading="loading"><div><h4>Vue Amazing UI, a design language</h4><br/><p>We supply a series of design principles, practical patterns and high quality designresources, to help people create their product prototypes beautifully and efficiently.</p></div></Skeleton><h2 class="mt30 mb10">自定义标题和段落</h2><Skeleton avatar :title="{ width: '24%' }" :paragraph="{ rows: 4, width: ['48%', '72%', '96%', '60%'] }" /><h2 class="mt30 mb10">按钮 / 输入框 / 图像 / 头像</h2><Flex :gap="32"><Flex vertical :gap="12" width="50%"><Skeleton :animated="animated" :button="{ shape: buttonShape, size: size, block: block}" /><Skeleton style="width: 200px" :animated="animated" :input="{ size: size }" /><Skeleton :animated="animated" image /><Skeleton :avatar="{ shape: avatarShape, size: size }" :paragraph="{ rows: 2 }" /></Flex><Flex vertical :gap="36" width="50%"><Space :size="32"><Space align="center">animated: <Switch v-model:checked="animated" /></Space><Space align="center">Button Block: <Switch v-model:checked="block" /></Space></Space><Space align="center">Size: <Radio :options="sizeOptions" v-model:value="size" /></Space><Space align="center">Button Shape: <Radio :options="buttonShapeOptions" v-model:value="buttonShape" /></Space><Space align="center">Avatar Shape: <Radio :options="avatarShapeOptions" v-model:value="avatarShape" /></Space></Flex></Flex></div>
</template>