465 lines
10 KiB
Vue
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> |