feat: 新增 ui-skeleton 骨架屏组件 (card / list-row / circle)

This commit is contained in:
2026-07-08 11:35:16 +08:00
parent 0bba403b00
commit 677a3ff64b
4 changed files with 138 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
Component({
properties: {
variant: { type: String, value: 'card' },
count: { type: Number, value: 1 },
active: { type: Boolean, value: true }
},
data: {
_minDisplayShown: false
},
lifetimes: {
attached() {
// 200ms 最小显示,避免骨架闪一下
this._minTimer = setTimeout(() => {
this.setData({ _minDisplayShown: true })
}, 200)
},
detached() {
if (this._minTimer) clearTimeout(this._minTimer)
}
}
})
+4
View File
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}
+28
View File
@@ -0,0 +1,28 @@
<view class="skeleton {{active ? 'skeleton--active' : ''}}" wx:if="{{_minDisplayShown || !active}}">
<!-- card variant -->
<block wx:if="{{variant === 'card'}}">
<view class="sk-card">
<view class="sk-bar sk-bar--title"></view>
<view class="sk-bar sk-bar--body"></view>
<view class="sk-bar sk-bar--small"></view>
</view>
</block>
<!-- list-row variant -->
<block wx:elif="{{variant === 'list-row'}}">
<view class="sk-list" wx:for="{{count}}" wx:key="*this">
<view class="sk-circle"></view>
<view class="sk-list-text">
<view class="sk-bar sk-bar--title"></view>
<view class="sk-bar sk-bar--small"></view>
</view>
</view>
</block>
<!-- circle variant (timer) -->
<block wx:elif="{{variant === 'circle'}}">
<view class="sk-circle sk-circle--big">
<view class="sk-bar sk-bar--center"></view>
</view>
</block>
</view>
+85
View File
@@ -0,0 +1,85 @@
.skeleton {
width: 100%;
}
.skeleton--active .sk-bar,
.skeleton--active .sk-circle {
animation: skShimmer 1.4s ease-in-out infinite;
background: linear-gradient(90deg,
rgba(0, 0, 0, 0.06) 0%,
rgba(0, 0, 0, 0.12) 50%,
rgba(0, 0, 0, 0.06) 100%);
background-size: 200% 100%;
}
@keyframes skShimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.sk-bar {
height: 24rpx;
border-radius: 8rpx;
margin-bottom: 16rpx;
background-color: rgba(0, 0, 0, 0.08);
}
.sk-bar--title { width: 60%; height: 32rpx; }
.sk-bar--body { width: 100%; }
.sk-bar--small { width: 40%; height: 20rpx; margin-bottom: 0; }
.sk-bar--center { width: 60%; height: 40rpx; }
.sk-card {
background: var(--card-bg, #FFFFFF);
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.06);
}
.sk-circle {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.08);
flex-shrink: 0;
}
.sk-circle--big {
width: 360rpx;
height: 360rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
.sk-list {
display: flex;
align-items: center;
padding: 24rpx 0;
gap: 24rpx;
}
.sk-list-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
/* dark mode */
@media (prefers-color-scheme: dark) {
.sk-card { background: var(--card-bg, #1C1C1E); }
.skeleton--active .sk-bar,
.skeleton--active .sk-circle {
background: linear-gradient(90deg,
rgba(255, 255, 255, 0.05) 0%,
rgba(255, 255, 255, 0.10) 50%,
rgba(255, 255, 255, 0.05) 100%);
background-size: 200% 100%;
}
.sk-bar,
.sk-circle {
background-color: rgba(255, 255, 255, 0.06);
}
}