Files

465 lines
10 KiB
Vue

<template>
<view class="skeleton-screen" :style="screenStyle">
<block v-if="type === 'list'">
<view
v-for="n in count"
:key="'list-' + n"
class="skeleton-list-item"
:style="cardStyle"
>
<view class="skeleton-avatar" :style="shimmerStyle"></view>
<view class="skeleton-content">
<view class="skeleton-title" :style="shimmerStyle"></view>
<view class="skeleton-subtitle" :style="shimmerStyle"></view>
<view class="skeleton-subtitle skeleton-subtitle--short" :style="shimmerStyle"></view>
<view class="skeleton-tags">
<view class="skeleton-tag" :style="shimmerStyle"></view>
<view class="skeleton-tag" :style="shimmerStyle"></view>
</view>
</view>
</view>
</block>
<block v-else-if="type === 'card'">
<view class="skeleton-card" :style="cardStyle">
<view class="skeleton-card-header">
<view class="skeleton-title skeleton-title--lg" :style="shimmerStyle"></view>
<view class="skeleton-subtitle" :style="shimmerStyle"></view>
</view>
<view class="skeleton-card-body">
<view class="skeleton-line" :style="shimmerStyle"></view>
<view class="skeleton-line" :style="shimmerStyle"></view>
<view class="skeleton-line skeleton-line--short" :style="shimmerStyle"></view>
</view>
<view class="skeleton-card-footer">
<view class="skeleton-line skeleton-line--footer" :style="shimmerStyle"></view>
<view class="skeleton-line skeleton-line--footer" :style="shimmerStyle"></view>
</view>
</view>
</block>
<block v-else-if="type === 'stats'">
<view
v-for="n in count"
:key="'stats-' + n"
class="skeleton-stats"
:style="cardStyle"
>
<view class="skeleton-stat-col">
<view class="skeleton-stat-value" :style="shimmerStyle"></view>
<view class="skeleton-stat-label" :style="shimmerStyle"></view>
</view>
<view class="skeleton-stat-col">
<view class="skeleton-stat-value" :style="shimmerStyle"></view>
<view class="skeleton-stat-label" :style="shimmerStyle"></view>
</view>
<view class="skeleton-stat-col">
<view class="skeleton-stat-value" :style="shimmerStyle"></view>
<view class="skeleton-stat-label" :style="shimmerStyle"></view>
</view>
</view>
</block>
<block v-else-if="type === 'dashboard'">
<view class="skeleton-dashboard" :style="cardStyle">
<view class="skeleton-ring-wrapper">
<view class="skeleton-ring" :style="ringStyle"></view>
<view class="skeleton-ring-inner" :style="shimmerStyle"></view>
</view>
<view class="skeleton-dashboard-stats">
<view class="skeleton-stat-col">
<view class="skeleton-stat-value" :style="shimmerStyle"></view>
<view class="skeleton-stat-label" :style="shimmerStyle"></view>
</view>
<view class="skeleton-stat-col">
<view class="skeleton-stat-value" :style="shimmerStyle"></view>
<view class="skeleton-stat-label" :style="shimmerStyle"></view>
</view>
</view>
</view>
<view
v-for="n in dashboardListCount"
:key="'dash-list-' + n"
class="skeleton-list-item skeleton-list-item--compact"
:style="cardStyle"
>
<view class="skeleton-dot" :style="shimmerStyle"></view>
<view class="skeleton-content">
<view class="skeleton-title" :style="shimmerStyle"></view>
<view class="skeleton-subtitle" :style="shimmerStyle"></view>
</view>
</view>
</block>
<block v-else-if="type === 'form'">
<view class="skeleton-form" :style="cardStyle">
<view
v-for="n in count"
:key="'form-' + n"
class="skeleton-form-field"
>
<view class="skeleton-label" :style="shimmerStyle"></view>
<view class="skeleton-input" :style="shimmerStyle"></view>
</view>
<view class="skeleton-form-actions">
<view class="skeleton-btn skeleton-btn--primary" :style="shimmerStyle"></view>
<view class="skeleton-btn skeleton-btn--secondary" :style="shimmerStyle"></view>
</view>
</view>
</block>
<block v-else-if="type === 'custom'">
<view class="skeleton-custom" :style="cardStyle">
<slot></slot>
</view>
</block>
</view>
</template>
<script>
import themeMixin from '@/mixins/themeMixin.js'
import { THEME_CHANGE_EVENT } from '@/utils/theme'
export default {
name: 'SkeletonScreen',
mixins: [themeMixin],
props: {
type: {
type: String,
default: 'list',
validator(val) {
return ['list', 'card', 'stats', 'dashboard', 'form', 'custom'].indexOf(val) !== -1
}
},
count: {
type: Number,
default: 3
},
animated: {
type: Boolean,
default: true
}
},
computed: {
screenStyle() {
return {
backgroundColor: this.themeConfig.bgColor
}
},
cardStyle() {
return {
backgroundColor: this.themeConfig.cardBgColor,
borderRadius: '20rpx',
padding: '24rpx',
marginBottom: '20rpx'
}
},
shimmerStyle() {
if (!this.animated) {
return {
backgroundColor: this.shimmerBaseColor
}
}
return {
background: 'linear-gradient(90deg, ' +
this.shimmerBaseColor + ' 25%, ' +
this.shimmerHighlightColor + ' 50%, ' +
this.shimmerBaseColor + ' 75%)',
backgroundSize: '200% 100%',
animation: 'skeleton-shimmer 1.5s infinite'
}
},
ringStyle() {
if (!this.animated) {
return {
border: '12rpx solid ' + this.shimmerBaseColor
}
}
return {
border: '12rpx solid transparent',
borderTopColor: this.shimmerBaseColor,
animation: 'skeleton-shimmer-ring 1.2s infinite linear'
}
},
shimmerBaseColor() {
return this.themeConfig.borderLight || '#f0f0f0'
},
shimmerHighlightColor() {
return this.lightenColor(this.shimmerBaseColor, 15)
},
dashboardListCount() {
return Math.min(this.count, 3)
}
},
created() {
this.syncThemeConfig()
this.watchThemeChange()
},
beforeUnmount() {
if (this.themeChangeHandler) {
uni.$off(THEME_CHANGE_EVENT, this.themeChangeHandler)
}
},
methods: {
lightenColor(hex, percent) {
if (!hex || hex.charAt(0) !== '#') return hex
const num = parseInt(hex.slice(1), 16)
const r = Math.min(255, ((num >> 16) & 0xff) + Math.round(255 * percent / 100))
const g = Math.min(255, ((num >> 8) & 0xff) + Math.round(255 * percent / 100))
const b = Math.min(255, (num & 0xff) + Math.round(255 * percent / 100))
return 'rgb(' + r + ', ' + g + ', ' + b + ')'
}
}
}
</script>
<style scoped>
.skeleton-screen {
min-height: 100vh;
padding: 24rpx;
box-sizing: border-box;
}
.skeleton-list-item {
display: flex;
padding: 24rpx;
border-radius: 20rpx;
margin-bottom: 20rpx;
}
.skeleton-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
flex-shrink: 0;
}
.skeleton-content {
flex: 1;
display: flex;
flex-direction: column;
padding-left: 20rpx;
justify-content: center;
}
.skeleton-title {
height: 36rpx;
width: 60%;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.skeleton-title--lg {
height: 44rpx;
width: 80%;
margin-bottom: 20rpx;
}
.skeleton-subtitle {
height: 28rpx;
width: 50%;
border-radius: 8rpx;
margin-bottom: 12rpx;
}
.skeleton-subtitle--short {
width: 35%;
margin-bottom: 16rpx;
}
.skeleton-tags {
display: flex;
margin-top: 8rpx;
}
.skeleton-tag {
height: 40rpx;
width: 80rpx;
border-radius: 20rpx;
margin-right: 16rpx;
}
.skeleton-card {
padding: 28rpx;
border-radius: 20rpx;
margin-bottom: 20rpx;
}
.skeleton-card-header {
margin-bottom: 24rpx;
}
.skeleton-card-body {
margin-bottom: 24rpx;
}
.skeleton-line {
height: 28rpx;
width: 100%;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.skeleton-line--short {
width: 70%;
}
.skeleton-card-footer {
display: flex;
flex-wrap: wrap;
}
.skeleton-line--footer {
width: 30%;
margin-right: 24rpx;
margin-bottom: 0;
}
.skeleton-stats {
display: flex;
padding: 32rpx 24rpx;
border-radius: 20rpx;
margin-bottom: 20rpx;
}
.skeleton-stat-col {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.skeleton-stat-value {
height: 48rpx;
width: 80%;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.skeleton-stat-label {
height: 24rpx;
width: 60%;
border-radius: 6rpx;
}
.skeleton-dashboard {
padding: 32rpx;
border-radius: 20rpx;
margin-bottom: 20rpx;
}
.skeleton-ring-wrapper {
display: flex;
justify-content: center;
align-items: center;
padding: 24rpx 0 32rpx;
position: relative;
width: 280rpx;
height: 280rpx;
margin: 0 auto;
}
.skeleton-ring {
width: 240rpx;
height: 240rpx;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.skeleton-ring-inner {
width: 180rpx;
height: 180rpx;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.skeleton-dashboard-stats {
display: flex;
padding-top: 24rpx;
border-top: 2rpx solid;
}
.skeleton-list-item--compact {
padding: 20rpx 24rpx;
align-items: center;
}
.skeleton-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
margin-right: 20rpx;
flex-shrink: 0;
}
.skeleton-form {
padding: 28rpx;
border-radius: 20rpx;
}
.skeleton-form-field {
margin-bottom: 28rpx;
}
.skeleton-label {
height: 24rpx;
width: 30%;
border-radius: 6rpx;
margin-bottom: 12rpx;
}
.skeleton-input {
height: 80rpx;
width: 100%;
border-radius: 12rpx;
}
.skeleton-form-actions {
display: flex;
justify-content: space-between;
margin-top: 32rpx;
}
.skeleton-btn {
height: 80rpx;
border-radius: 12rpx;
flex: 1;
}
.skeleton-btn--primary {
margin-right: 24rpx;
}
.skeleton-btn--secondary {
flex: 0.6;
}
.skeleton-custom {
padding: 24rpx;
border-radius: 20rpx;
}
@keyframes skeleton-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@keyframes skeleton-shimmer-ring {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
</style>