Initial commit of akmon project
This commit is contained in:
36
uni_modules/lime-loading/changelog.md
Normal file
36
uni_modules/lime-loading/changelog.md
Normal file
@@ -0,0 +1,36 @@
|
||||
## 0.1.8(2025-05-30)
|
||||
- feat: 重构,针对uniapp x app增加`mode`属性,用于是使用计时器还是元素的animate实现
|
||||
## 0.1.7(2025-05-16)
|
||||
- fix: 修复 uniappx ios 初始化尺寸为0导致不生效的问题
|
||||
## 0.1.6(2025-04-21)
|
||||
- fix: uniappx 鸿蒙next 尺寸从0变化时无法渲染
|
||||
## 0.1.5(2025-04-21)
|
||||
- feat: 兼容uniappx 鸿蒙next
|
||||
## 0.1.4(2025-04-10)
|
||||
- feat: 增加暂停
|
||||
## 0.1.3(2025-03-12)
|
||||
- feat: uniappx app使用`requestAnimationFrame`
|
||||
## 0.1.2(2025-02-13)
|
||||
- fix: 修复因uniapp x ios监听元素不生效导致不生效问题
|
||||
## 0.1.1(2025-02-09)
|
||||
- chore: 去掉多余console
|
||||
## 0.1.0(2025-02-09)
|
||||
- feat: 重构useLoading
|
||||
## 0.0.9(2025-01-14)
|
||||
- feat: useLoading 增加color ref
|
||||
## 0.0.8(2024-12-24)
|
||||
- feat: 改用监听尺寸方式
|
||||
## 0.0.7(2024-12-18)
|
||||
- fix: 修复vue2 微信小程序不显示加载图标的问题
|
||||
## 0.0.6(2024-12-18)
|
||||
- chore: 更新文档
|
||||
## 0.0.5(2024-09-30)
|
||||
- fix: vue2 app 使用渐变
|
||||
## 0.0.4(2024-09-29)
|
||||
- chore: 非uvue app size使用css变量
|
||||
## 0.0.3(2024-09-25)
|
||||
- fix: useLoading
|
||||
## 0.0.2(2024-04-05)
|
||||
- feat: 支持 uniapp x ios(app-js)
|
||||
## 0.0.1(2023-10-13)
|
||||
- 首次上传
|
||||
254
uni_modules/lime-loading/components/l-loading/index-u.scss
Normal file
254
uni_modules/lime-loading/components/l-loading/index-u.scss
Normal file
@@ -0,0 +1,254 @@
|
||||
@import '@/uni_modules/lime-style/index.scss';
|
||||
|
||||
$loading-color: create-var(loading-color, $primary-color);
|
||||
$loading-size: create-var(loading-size, 40rpx);
|
||||
$loading-text-color: create-var(loading-text-color, $text-color-3);
|
||||
$loading-font-size: create-var(loading-font-size, $font-size);
|
||||
|
||||
/* #ifndef APP-ANDROID || APP-HARMONY || APP-IOS || APP-NVUE */
|
||||
/* #ifndef MP-ALIPAY */
|
||||
$loading-duration: var(--l-loading-duration, 2s);
|
||||
|
||||
@property --l-loading-start {
|
||||
syntax: '<length-percentage>';
|
||||
initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
@property --l-loading-end {
|
||||
syntax: '<length-percentage>';
|
||||
initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
@property --l-left {
|
||||
syntax: '<length-percentage>';
|
||||
initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
@property --l-loadding-ball-size {
|
||||
syntax: '<length> | <length-percentage>';
|
||||
// initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
/* #ifdef MP-ALIPAY */
|
||||
$loading-duration: var(--l-loading-duration, 1s);
|
||||
/* #endif */
|
||||
|
||||
|
||||
/* #endif */
|
||||
|
||||
|
||||
|
||||
.l-loading {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
// align-self: flex-start;
|
||||
/* #ifdef APP-ANDROID || APP-HARMONY || APP-IOS */
|
||||
border-left-color: $loading-color;
|
||||
border-left-width: 0;
|
||||
/* #endif */
|
||||
/* #ifndef APP-ANDROID || APP-HARMONY || APP-IOS || APP-NVUE */
|
||||
color: $loading-color;
|
||||
&--ball{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.l-loading {
|
||||
&__ball {
|
||||
position: relative;
|
||||
perspective: calc(var(--l-loadding-ball-size) * 4);
|
||||
transform-style: preserve-3d;
|
||||
// border: 1px solid;
|
||||
|
||||
&:before{
|
||||
background-color: $primary-color;
|
||||
left: 0%;
|
||||
// mix-blend-mode: darken;
|
||||
animation-name: l-ball-before;
|
||||
}
|
||||
&:after{
|
||||
right: 0;
|
||||
background-color: red;
|
||||
// mix-blend-mode: darken;
|
||||
animation-name: l-ball-after;
|
||||
}
|
||||
&:before,&:after{
|
||||
top: 0;
|
||||
content: '';
|
||||
position: absolute;
|
||||
// width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 50%;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: -100ms;
|
||||
animation-duration: 900ms;
|
||||
mix-blend-mode: darken;
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&--circular {
|
||||
.l-loading {
|
||||
&__circular {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
animation: l-rotate $loading-duration linear infinite;
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
vertical-align: middle;
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
/* #ifndef MP-ALIPAY */
|
||||
background: conic-gradient(
|
||||
transparent 0%,
|
||||
transparent var(--l-loading-start, 0%), var(--l-loading-color-1, currentColor) var(--l-loading-start, 0%),
|
||||
var(--l-loading-color-2, currentColor) var(--l-loading-end, 0%), transparent var(--l-loading-end, 0%),
|
||||
transparent 100%);
|
||||
/* #endif */
|
||||
/* #ifdef MP-ALIPAY */
|
||||
background: conic-gradient(
|
||||
var(--l-loading-color-1, transparent) 0%,
|
||||
var(--l-loading-color-2, currentColor) 100%);
|
||||
/* #endif */
|
||||
mask: radial-gradient(closest-side, transparent calc(80% - 1px), #fff 80%);
|
||||
-webkit-mask: radial-gradient(closest-side, transparent calc(80% - 1px), #fff 80%);
|
||||
animation: l-circular 3s ease-in-out infinite;
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&--spinner {
|
||||
.l-loading {
|
||||
&__spinner {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// max-width: 100%;
|
||||
// max-height: 100%;
|
||||
animation-timing-function: steps(12);
|
||||
animation: l-rotate 1.5s linear infinite;
|
||||
animation-play-state: var(--l-play-state, running)
|
||||
}
|
||||
&__dot {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(calc(var(--l-loading-dot, 1) * 30deg));
|
||||
opacity: calc(var(--l-loading-dot, 1) / 12);
|
||||
&::before {
|
||||
display: block;
|
||||
width: 5rpx;
|
||||
height: 25%;
|
||||
margin: 0 auto;
|
||||
background-color: currentColor;
|
||||
border-radius: 40%;
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
/* #ifdef APP-ANDROID || APP-HARMONY || APP-IOS || APP-NVUE */
|
||||
&__view{
|
||||
// background-color: aqua;
|
||||
// background-color: #1677ff;
|
||||
// transition-duration: 1.5s;
|
||||
// transition-property: transform;
|
||||
// transition-timing-function: linear;
|
||||
}
|
||||
/* #endif */
|
||||
&__text {
|
||||
margin-left: $spacer-xs;
|
||||
color: $loading-text-color;
|
||||
font-size: $loading-font-size;
|
||||
}
|
||||
|
||||
|
||||
&.is-vertical {
|
||||
flex-direction: column;
|
||||
.l-loading__text {
|
||||
margin: $spacer-tn 0 0;
|
||||
}
|
||||
}
|
||||
&__ball,&__circular,&__spinner {
|
||||
width: $loading-size;
|
||||
height: $loading-size;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* #ifndef APP-ANDROID || APP-HARMONY || APP-IOS || APP-NVUE */
|
||||
@keyframes l-circular {
|
||||
0% {
|
||||
--l-loading-start: 0%;
|
||||
--l-loading-end: 0%;
|
||||
}
|
||||
50% {
|
||||
--l-loading-start: 0%;
|
||||
--l-loading-end: 100%;
|
||||
}
|
||||
100% {
|
||||
--l-loading-start: 100%;
|
||||
--l-loading-end: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes l-rotate {
|
||||
to {
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes l-ball-before {
|
||||
0%{
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
25% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2);
|
||||
transform: translate3d(var(--l-left), 0, var(--l-loadding-ball-size));
|
||||
}
|
||||
50% {
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)));
|
||||
animation-timing-function:ease-in;
|
||||
transform: translate3d(var(--l-left), 0, 0);
|
||||
}
|
||||
75% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2);
|
||||
transform: translate3d(var(--l-left), 0, calc(var(--l-loadding-ball-size) * -1));
|
||||
}
|
||||
}
|
||||
@keyframes l-ball-after {
|
||||
0%{
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
25% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2 * -1);
|
||||
transform: translate3d(var(--l-left), 0, calc(var(--l-loadding-ball-size) * -1));
|
||||
}
|
||||
50% {
|
||||
animation-timing-function:ease-in;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) * -1);
|
||||
transform: translate3d(var(--l-left), 0, 0);
|
||||
}
|
||||
75% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2 * -1);
|
||||
transform: translate3d(var(--l-left), 0, var(--l-loadding-ball-size));
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
246
uni_modules/lime-loading/components/l-loading/index.scss
Normal file
246
uni_modules/lime-loading/components/l-loading/index.scss
Normal file
@@ -0,0 +1,246 @@
|
||||
@import '@/uni_modules/lime-style/index.scss';
|
||||
|
||||
$loading-color: create-var(loading-color, $primary-color);
|
||||
$loading-size: create-var(loading-size, 40rpx);
|
||||
$loading-text-color: create-var(loading-text-color, $text-color-3);
|
||||
$loading-font-size: create-var(loading-font-size, $font-size);
|
||||
|
||||
/* #ifndef MP-ALIPAY */
|
||||
$loading-duration: create-var(loading-duration, 2s);
|
||||
/* #endif */
|
||||
/* #ifdef MP-ALIPAY */
|
||||
$loading-duration: create-var(loading-duration, 1s);
|
||||
/* #endif */
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
|
||||
|
||||
|
||||
/* #ifndef MP-ALIPAY */
|
||||
@property --l-loading-start {
|
||||
syntax: '<length-percentage>';
|
||||
initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
@property --l-loading-end {
|
||||
syntax: '<length-percentage>';
|
||||
initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
@property --l-left {
|
||||
syntax: '<length-percentage>';
|
||||
initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
@property --l-loadding-ball-size {
|
||||
syntax: '<length> | <length-percentage>';
|
||||
// initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
:host {
|
||||
display: inline-flex;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
|
||||
.l-loading {
|
||||
position: relative;
|
||||
// color: #c8c9cc;
|
||||
color: $loading-color;
|
||||
font-size: 0;
|
||||
vertical-align: middle;
|
||||
&--ball{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
.l-loading {
|
||||
&__ball {
|
||||
position: relative;
|
||||
perspective: calc(var(--l-loadding-ball-size) * 4);
|
||||
transform-style: preserve-3d;
|
||||
// border: 1px solid;
|
||||
|
||||
&:before{
|
||||
background-color: $primary-color;
|
||||
left: 0%;
|
||||
// mix-blend-mode: darken;
|
||||
animation-name: l-ball-before;
|
||||
}
|
||||
&:after{
|
||||
right: 0;
|
||||
background-color: red;
|
||||
// mix-blend-mode: darken;
|
||||
animation-name: l-ball-after;
|
||||
}
|
||||
&:before,&:after{
|
||||
top: 0;
|
||||
content: '';
|
||||
position: absolute;
|
||||
// width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 50%;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: -100ms;
|
||||
animation-duration: 900ms;
|
||||
mix-blend-mode: darken;
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&--circular {
|
||||
.l-loading {
|
||||
&__circular {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
animation: l-rotate $loading-duration linear infinite;
|
||||
vertical-align: middle;
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
/* #ifndef MP-ALIPAY || APP-VUE */
|
||||
background-image: conic-gradient(
|
||||
transparent 0%,
|
||||
transparent var(--l-loading-start, 0%), var(--l-loading-color-1, currentColor) var(--l-loading-start, 0%),
|
||||
var(--l-loading-color-2, currentColor) var(--l-loading-end, 0%), transparent var(--l-loading-end, 0%),
|
||||
transparent 100%);
|
||||
/* #endif */
|
||||
/* #ifdef MP-ALIPAY || APP-VUE */
|
||||
background-image: conic-gradient(
|
||||
var(--l-loading-color-1, transparent) 0%,
|
||||
var(--l-loading-color-2, currentColor) 100%);
|
||||
/* #endif */
|
||||
mask: radial-gradient(closest-side, transparent calc(80% - 1px), #fff 80%);
|
||||
-webkit-mask: radial-gradient(closest-side, transparent calc(80% - 1px), #fff 80%);
|
||||
animation: l-circular 2.5s ease-in-out infinite;
|
||||
transform: rotate(90deg);
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
}
|
||||
}
|
||||
&--spinner {
|
||||
.l-loading {
|
||||
&__spinner {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
animation-timing-function: steps(12);
|
||||
animation: l-rotate 0.8s linear infinite;
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
}
|
||||
&__dot {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(calc(var(--l-loading-dot, 1) * 30deg));
|
||||
opacity: calc(var(--l-loading-dot, 1) / 12);
|
||||
&::before {
|
||||
display: block;
|
||||
width: 5rpx;
|
||||
height: 25%;
|
||||
margin: 0 auto;
|
||||
background-color: currentColor;
|
||||
border-radius: 40%;
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
&__text{
|
||||
display: inline-block;
|
||||
margin-left: $spacer-xs;
|
||||
color: $loading-text-color;
|
||||
font-size: $loading-font-size;
|
||||
vertical-align: middle;
|
||||
}
|
||||
&.is-vertical {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.l-loading__text {
|
||||
margin: $spacer-tn 0 0;
|
||||
}
|
||||
}
|
||||
&__ball,&__circular,&__spinner {
|
||||
width: $loading-size;
|
||||
height: $loading-size;
|
||||
}
|
||||
}
|
||||
/* #ifndef APP-NVUE */
|
||||
@keyframes l-circular {
|
||||
0% {
|
||||
--l-loading-start: 0%;
|
||||
--l-loading-end: 0%;
|
||||
}
|
||||
50% {
|
||||
--l-loading-start: 0%;
|
||||
--l-loading-end: 100%;
|
||||
}
|
||||
100% {
|
||||
--l-loading-start: 100%;
|
||||
--l-loading-end: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes l-rotate {
|
||||
to {
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes l-ball-before {
|
||||
0%{
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
25% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2);
|
||||
transform: translate3d(var(--l-left), 0, var(--l-loadding-ball-size));
|
||||
}
|
||||
50% {
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)));
|
||||
animation-timing-function:ease-in;
|
||||
transform: translate3d(var(--l-left), 0, 0);
|
||||
}
|
||||
75% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2);
|
||||
transform: translate3d(var(--l-left), 0, calc(var(--l-loadding-ball-size) * -1));
|
||||
}
|
||||
}
|
||||
@keyframes l-ball-after {
|
||||
0%{
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
25% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2 * -1);
|
||||
transform: translate3d(var(--l-left), 0, calc(var(--l-loadding-ball-size) * -1));
|
||||
}
|
||||
50% {
|
||||
animation-timing-function:ease-in;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) * -1);
|
||||
transform: translate3d(var(--l-left), 0, 0);
|
||||
}
|
||||
75% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2 * -1);
|
||||
transform: translate3d(var(--l-left), 0, var(--l-loadding-ball-size));
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
117
uni_modules/lime-loading/components/l-loading/l-loading.uvue
Normal file
117
uni_modules/lime-loading/components/l-loading/l-loading.uvue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<view class="l-loading" :class="classes">
|
||||
<!-- #ifndef APP-ANDROID || APP-IOS || APP-HARMONY -->
|
||||
<view class="l-loading__ball" v-if="type == 'ball'" :style="[spinnerStyle]"></view>
|
||||
<view class="l-loading__circular" v-if="type == 'circular'" :style="[spinnerStyle]"></view>
|
||||
<view class="l-loading__spinner" v-if="type == 'spinner'" :style="[spinnerStyle]">
|
||||
<view class="l-loading__dot" v-for="item in 12" :key="item" :style="{'--l-loading-dot': item}"></view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-ANDROID || APP-IOS || APP-HARMONY -->
|
||||
<view class="l-loading__view" ref="loadingRef" :style="spinnerStyle"></view>
|
||||
<!-- #endif -->
|
||||
<text class="l-loading__text" v-if="$slots['default'] != null || text != null" :style="textStyle">
|
||||
<slot>{{text}}</slot>
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="uts" setup>
|
||||
/**
|
||||
* Loading 加载指示器
|
||||
* @description 用于表示加载中的过渡状态,支持多种动画类型和布局方式
|
||||
* <br> 插件类型:LLoadingComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-loading
|
||||
*
|
||||
* @property {string} color 加载图标颜色(默认:主题色)
|
||||
* @property {'circular' | 'spinner' | 'failed'} mode 动画实现的模式.只针对APP
|
||||
* @value raf 延时
|
||||
* @value animate 基于元素的annimate方法
|
||||
* @property {'circular' | 'spinner' | 'failed'} type 加载状态类型
|
||||
* @value circular 环形旋转动画(默认)
|
||||
* @value spinner 菊花转动画
|
||||
* @value failed 加载失败提示
|
||||
* @property {string} text 提示文字内容
|
||||
* @property {string} textColor 文字颜色(默认同color)
|
||||
* @property {string} textSize 文字字号(默认:14px)
|
||||
* @property {boolean} vertical 是否垂直排列图标和文字
|
||||
* @property {boolean} animated 是否启用旋转动画(failed类型自动禁用)
|
||||
* @property {string} size 图标尺寸(默认:'40px')
|
||||
*/
|
||||
import { LoadingProps } from './type'
|
||||
// #ifdef APP
|
||||
// import {useLoading} from './useLoading'
|
||||
import {useLoading} from '@/uni_modules/lime-loading'
|
||||
// #endif
|
||||
const name = 'l-loading'
|
||||
const props = withDefaults(defineProps<LoadingProps>(), {
|
||||
// #ifdef APP
|
||||
size: '40rpx',
|
||||
// #endif
|
||||
type: 'circular',
|
||||
mode: 'raf',
|
||||
animated: true,
|
||||
vertical: false,
|
||||
})
|
||||
|
||||
|
||||
const classes = computed<Map<string,any>>(():Map<string,any> => {
|
||||
const cls = new Map<string,any>()
|
||||
cls.set(name + '--' + props.type, true)
|
||||
if (props.vertical) {
|
||||
cls.set('is-vertical', props.vertical)
|
||||
} else {
|
||||
cls.set('is-horizontal', !props.vertical)
|
||||
}
|
||||
return cls
|
||||
})
|
||||
|
||||
const spinnerStyle = computed<Map<string,any>>(():Map<string,any> => {
|
||||
const style = new Map<string,any>()
|
||||
style.set('width', props.size)
|
||||
style.set('height', props.size)
|
||||
// #ifndef APP
|
||||
style.set('color', props.color)
|
||||
style.set('--l-play-state', props.animated ? 'running' : 'paused')
|
||||
// #endif
|
||||
return style
|
||||
})
|
||||
|
||||
const textStyle = computed<Map<string,any>>(():Map<string,any> => {
|
||||
const style = new Map<string,any>()
|
||||
if (props.textColor != null) {
|
||||
style.set('color', props.textColor!)
|
||||
}
|
||||
if (props.textSize != null) {
|
||||
style.set('font-size', props.textSize!)
|
||||
}
|
||||
return style
|
||||
})
|
||||
// #ifdef APP
|
||||
const loadingRef = ref<UniElement|null>(null)
|
||||
// const {state, color} = useLoading(loadingRef, props.type, props.color, 1)
|
||||
const loading = useLoading(loadingRef)
|
||||
loading.type = props.type;
|
||||
loading.mode = props.mode;
|
||||
if(props.animated){
|
||||
loading.play()
|
||||
}
|
||||
|
||||
// state.value = true
|
||||
watchEffect(()=>{
|
||||
if(loadingRef.value == null) return
|
||||
const color = props.color ?? loadingRef.value?.style.getPropertyValue('border-left-color')
|
||||
loading.color = color == null || color.length == 0 ? '#1677ff' : color
|
||||
|
||||
if(props.animated){
|
||||
loading.play()
|
||||
} else {
|
||||
loading.pause()
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index-u.scss';
|
||||
</style>
|
||||
79
uni_modules/lime-loading/components/l-loading/l-loading.vue
Normal file
79
uni_modules/lime-loading/components/l-loading/l-loading.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<view class="l-class l-loading" :class="['l-loading--' + type, {'is-vertical': vertical}]" :style="{color: inheritColor ? 'inherit': ''}">
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<loading-indicator class="l-loading__circular" :style="[spinnerStyle]" :animating="true"/>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<view class="l-loading__ball" v-if="type == 'ball'" :style="[spinnerStyle]"></view>
|
||||
<view class="l-loading__circular" v-if="type == 'circular'" :style="[spinnerStyle]"></view>
|
||||
<view class="l-loading__spinner" v-if="type == 'spinner'" :style="[spinnerStyle]">
|
||||
<view class="l-loading__dot" v-for="item in 12" :key="item" :style="{'--l-loading-dot': item}"></view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<text class="l-loading__text" v-if="$slots['default']||text" :style="[textStyle]">
|
||||
{{text}}<slot></slot>
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Loading 加载指示器
|
||||
* @description 用于表示加载中的过渡状态,支持多种动画类型和布局方式
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-loading
|
||||
*
|
||||
* @property {string} color 加载图标颜色(默认:主题色)
|
||||
* @property {'circular' | 'spinner' | 'failed'} type 加载状态类型
|
||||
* @value circular 环形旋转动画(默认)
|
||||
* @value spinner 菊花转动画
|
||||
* @value failed 加载失败提示
|
||||
* @property {string} text 提示文字内容
|
||||
* @property {string} textColor 文字颜色(默认同color)
|
||||
* @property {string} textSize 文字字号(默认:14px)
|
||||
* @property {boolean} vertical 是否垂直排列图标和文字
|
||||
* @property {boolean} animated 是否启用旋转动画(failed类型自动禁用)
|
||||
* @property {string} size 图标尺寸(默认:'40px')
|
||||
*/
|
||||
import {computed, defineComponent} from '@/uni_modules/lime-shared/vue';
|
||||
import {addUnit} from '@/uni_modules/lime-shared/addUnit';
|
||||
import {unitConvert} from '@/uni_modules/lime-shared/unitConvert';
|
||||
import LoadingProps from './props';
|
||||
const name = 'l-loading';
|
||||
|
||||
export default defineComponent({
|
||||
// name,
|
||||
props:LoadingProps,
|
||||
setup(props) {
|
||||
const classes = computed(() => {
|
||||
const {type, vertical} = props
|
||||
return {
|
||||
[name + '--' + type]: type,
|
||||
['is-vertical']: vertical,
|
||||
}
|
||||
})
|
||||
const spinnerStyle = computed(() => {
|
||||
const size = unitConvert(props.size ?? 0) * (props.type == 'ball' ? 0.6 : 1);
|
||||
return {
|
||||
color: props.color,
|
||||
width: size != 0 && (props.type == 'ball' ? addUnit(size * 2.1) : addUnit(size)),
|
||||
height: size != 0 && addUnit(size),
|
||||
'--l-loadding-ball-size': size != 0 && addUnit(size)
|
||||
}
|
||||
})
|
||||
const textStyle = computed(() => {
|
||||
return {
|
||||
color: props.textColor,
|
||||
fontSize: props.textSize,
|
||||
}
|
||||
})
|
||||
return {
|
||||
classes,
|
||||
spinnerStyle,
|
||||
textStyle
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index.scss';
|
||||
</style>
|
||||
26
uni_modules/lime-loading/components/l-loading/props.ts
Normal file
26
uni_modules/lime-loading/components/l-loading/props.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// import {PropType} from 'vue'
|
||||
export default {
|
||||
color: {
|
||||
type: String,
|
||||
// default: '#c9c9c9'
|
||||
},
|
||||
type: {
|
||||
type: String, //as PropType<'circular'|'spinner'>,
|
||||
default: 'circular'
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
// #ifdef APP-NVUE
|
||||
default: '40rpx'
|
||||
// #endif
|
||||
},
|
||||
text: String,
|
||||
textColor: String,
|
||||
textSize: String,
|
||||
vertical: Boolean,
|
||||
inheritColor: Boolean,
|
||||
animated: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
}
|
||||
59
uni_modules/lime-loading/components/l-loading/type.ts
Normal file
59
uni_modules/lime-loading/components/l-loading/type.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// 完整类型定义
|
||||
export interface LoadingProps {
|
||||
color?: string;
|
||||
type: 'circular' | 'spinner' | 'failed';
|
||||
// #ifndef APP
|
||||
size?: string;
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
size: string;
|
||||
// #endif
|
||||
text?: string;
|
||||
textColor?: string;
|
||||
textSize?: string;
|
||||
mode: 'raf' | 'animate';
|
||||
vertical: boolean;
|
||||
animated: boolean;
|
||||
}
|
||||
|
||||
// defineOptions({
|
||||
// name: 'l-loading'
|
||||
// })
|
||||
// const props = defineProps({
|
||||
// color: {
|
||||
// type: String,
|
||||
// // #ifdef APP
|
||||
// default: '#1677ff' // '#c9c9c9'
|
||||
// // #endif
|
||||
// },
|
||||
// type: {
|
||||
// type: String,
|
||||
// default: 'circular'
|
||||
// },
|
||||
// size: {
|
||||
// type: String,
|
||||
// // #ifdef APP
|
||||
// default: '40rpx',
|
||||
// // #endif
|
||||
// },
|
||||
// text: {
|
||||
// type: String,
|
||||
// default: ''
|
||||
// },
|
||||
// textColor: {
|
||||
// type: String,
|
||||
// default: ''
|
||||
// },
|
||||
// textSize: {
|
||||
// type: String,
|
||||
// default: ''
|
||||
// },
|
||||
// vertical: {
|
||||
// type: Boolean,
|
||||
// default: false
|
||||
// },
|
||||
// animated: {
|
||||
// type: Boolean,
|
||||
// default: true
|
||||
// }
|
||||
// })
|
||||
330
uni_modules/lime-loading/components/l-loading/useLoading.uts
Normal file
330
uni_modules/lime-loading/components/l-loading/useLoading.uts
Normal file
@@ -0,0 +1,330 @@
|
||||
// type UseLoadingOtions = {
|
||||
// type: string,
|
||||
// color: string,
|
||||
// el: UniElement
|
||||
// }
|
||||
import {tinyColor} from '@/uni_modules/lime-color'
|
||||
function easeInOutCubic(t : number) : number {
|
||||
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
|
||||
}
|
||||
|
||||
type useLoadingReturnType = {
|
||||
state : Ref<boolean>
|
||||
color : Ref<string>
|
||||
play: () => void
|
||||
failed: () => void
|
||||
clear : () => void
|
||||
destroy : () => void
|
||||
}
|
||||
type Point = {
|
||||
x1: number
|
||||
y1: number
|
||||
x2: number
|
||||
y2: number
|
||||
}
|
||||
export function useLoading(
|
||||
element : Ref<UniElement | null>,
|
||||
type : 'circular' | 'spinner',
|
||||
strokeColor : string,
|
||||
ratio : number,
|
||||
immediate: boolean = false,
|
||||
) : useLoadingReturnType {
|
||||
const state = ref(false)
|
||||
const color = ref(strokeColor)
|
||||
let tick = 0 // 0 不绘制 | 1 旋转 | 2 错误
|
||||
let init = false
|
||||
let isDestroy = ref(false)
|
||||
let width = 0
|
||||
let height = 0
|
||||
let size = 0
|
||||
let x = 0
|
||||
let y = 0
|
||||
let ctx : DrawableContext | null = null
|
||||
let timer = -1
|
||||
let isClear = false;
|
||||
let drawing = false;
|
||||
const updateSize = () => {
|
||||
if (element.value == null) return
|
||||
|
||||
const rect = element.value!.getBoundingClientRect();
|
||||
ctx = element.value!.getDrawableContext()! as DrawableContext
|
||||
width = rect.width
|
||||
height = rect.height
|
||||
size = ratio > 1 ? ratio : Math.floor(Math.min(width, height) * ratio)
|
||||
x = width / 2
|
||||
y = height / 2
|
||||
}
|
||||
const circular = () => {
|
||||
if (ctx == null) return
|
||||
let _ctx = ctx!
|
||||
let startAngle = 0;
|
||||
let endAngle = 0;
|
||||
let startSpeed = 0;
|
||||
let endSpeed = 0;
|
||||
let rotate = 0;
|
||||
|
||||
// 不使用360的原因是加上rotate后,会导致闪烁
|
||||
const ARC_LENGTH = 359.5
|
||||
const PI = Math.PI / 180
|
||||
const SPEED = 0.018
|
||||
const ROTATE_INTERVAL = 0.09
|
||||
const center = size / 2
|
||||
const lineWidth = size / 10;
|
||||
|
||||
function draw() {
|
||||
if(isClear) return
|
||||
_ctx.reset();
|
||||
_ctx.beginPath();
|
||||
_ctx.arc(
|
||||
x,
|
||||
y,
|
||||
center - lineWidth,
|
||||
startAngle * PI + rotate,
|
||||
endAngle * PI + rotate);
|
||||
_ctx.lineWidth = lineWidth;
|
||||
_ctx.strokeStyle = color.value;
|
||||
_ctx.stroke();
|
||||
|
||||
if (endAngle < ARC_LENGTH && startAngle == 0) {
|
||||
endSpeed += SPEED
|
||||
endAngle = Math.min(ARC_LENGTH, easeInOutCubic(endSpeed) * ARC_LENGTH)
|
||||
} else if (endAngle == ARC_LENGTH && startAngle < ARC_LENGTH) {
|
||||
startSpeed += SPEED
|
||||
startAngle = Math.min(ARC_LENGTH, easeInOutCubic(startSpeed) * ARC_LENGTH);
|
||||
} else if (endAngle >= ARC_LENGTH && startAngle >= ARC_LENGTH) {
|
||||
endSpeed = 0
|
||||
startSpeed = 0
|
||||
startAngle = 0;
|
||||
endAngle = 0;
|
||||
}
|
||||
rotate += ROTATE_INTERVAL;
|
||||
_ctx.update()
|
||||
// clearTimeout(timer)
|
||||
|
||||
timer = setTimeout(() => draw(), 24)
|
||||
}
|
||||
draw()
|
||||
}
|
||||
const spinner = () => {
|
||||
if (ctx == null) return
|
||||
let _ctx = ctx!
|
||||
const steps = 12;
|
||||
let step = 0;
|
||||
const lineWidth = size / 10;
|
||||
// 线长度和距离圆心距离
|
||||
const length = size / 4 - lineWidth;
|
||||
const offset = size / 4;
|
||||
|
||||
|
||||
function generateColorGradient(hex: string, steps: number):string[]{
|
||||
const colors:string[] = []
|
||||
const _color = tinyColor(hex)
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
_color.setAlpha(i/steps);
|
||||
colors.push(_color.toRgbString());
|
||||
}
|
||||
return colors
|
||||
}
|
||||
let colors = computed(():string[]=> generateColorGradient(color.value, steps))
|
||||
|
||||
function draw() {
|
||||
if(tick == 0) return
|
||||
_ctx.reset();
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const stepAngle = 360 / steps
|
||||
const angle = stepAngle * i;
|
||||
const index =(steps + i - (step % steps)) % steps
|
||||
// 正余弦
|
||||
const sin = Math.sin(angle / 180 * Math.PI);
|
||||
const cos = Math.cos(angle / 180 * Math.PI);
|
||||
// 开始绘制
|
||||
_ctx.lineWidth = lineWidth;
|
||||
_ctx.lineCap = 'round';
|
||||
_ctx.beginPath();
|
||||
_ctx.moveTo(size / 2 + offset * cos, size / 2 + offset * sin);
|
||||
_ctx.lineTo(size / 2 + (offset + length) * cos, size / 2 + (offset + length) * sin);
|
||||
_ctx.strokeStyle = colors.value[index]
|
||||
_ctx.stroke();
|
||||
}
|
||||
step += 1
|
||||
_ctx.update()
|
||||
timer = setTimeout(() => draw(), 1000/10)
|
||||
}
|
||||
draw()
|
||||
}
|
||||
const clear = () => {
|
||||
clearTimeout(timer)
|
||||
drawing = false
|
||||
tick = 0
|
||||
if(ctx == null) return
|
||||
// ctx?.reset()
|
||||
// ctx?.update()
|
||||
setTimeout(()=>{
|
||||
ctx!.reset()
|
||||
ctx!.update()
|
||||
},1000)
|
||||
|
||||
}
|
||||
const failed = () => {
|
||||
if(tick == 1) {
|
||||
drawing = false
|
||||
}
|
||||
clearTimeout(timer)
|
||||
tick = 2
|
||||
if (ctx == null || drawing) return
|
||||
let _ctx = ctx!
|
||||
const _size = size * 0.61
|
||||
const _sizeX = _size * 0.65
|
||||
const lineWidth = _size / 6;
|
||||
const lineLength = Math.ceil(Math.sqrt(Math.pow(_sizeX, 2) * 2))
|
||||
|
||||
const startX1 = (width - _sizeX) * 0.5
|
||||
const startY = (height - _sizeX) * 0.5
|
||||
const startX2 = startX1 + _sizeX
|
||||
|
||||
// 添加圆的参数
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const radius = (_size * Math.sqrt(2)) / 2 + lineWidth / 2;
|
||||
const totalSteps = 36;
|
||||
|
||||
function generateSteps(stepsCount: number):Point[][] {
|
||||
|
||||
const halfStepsCount = stepsCount / 2;
|
||||
const step = lineLength / halfStepsCount //Math.floor(lineLength / 18);
|
||||
const steps:Point[][] = []
|
||||
for (let i = 0; i < stepsCount; i++) {
|
||||
const sub:Point[] = []
|
||||
const index = i % 18 + 1
|
||||
if(i < halfStepsCount) {
|
||||
|
||||
const x2 = Math.sin(45 * Math.PI / 180) * step * index + startX1
|
||||
const y2 = Math.cos(45 * Math.PI / 180) * step * index + startY
|
||||
|
||||
const start1 = {
|
||||
x1: startX1,
|
||||
y1: startY,
|
||||
x2,
|
||||
y2,
|
||||
} as Point
|
||||
|
||||
sub.push(start1)
|
||||
} else {
|
||||
sub.push(steps[halfStepsCount-1][0])
|
||||
const x2 = Math.sin((45 - 90) * Math.PI / 180) * step * index + startX2
|
||||
const y2 = Math.cos((45 - 90) * Math.PI / 180) * step * index + startY
|
||||
|
||||
const start2 = {
|
||||
x1: startX2,
|
||||
y1: startY,
|
||||
x2,
|
||||
y2,
|
||||
} as Point
|
||||
sub.push(start2)
|
||||
}
|
||||
steps.push(sub)
|
||||
}
|
||||
|
||||
return steps
|
||||
}
|
||||
const steps = generateSteps(36);
|
||||
function draw(){
|
||||
if(steps.length == 0 || tick == 0) {
|
||||
clearTimeout(timer)
|
||||
return
|
||||
}
|
||||
const drawStep = steps.shift()!
|
||||
_ctx.reset()
|
||||
_ctx.lineWidth = lineWidth;
|
||||
_ctx.strokeStyle = color.value;
|
||||
|
||||
// 绘制逐渐显示的圆
|
||||
_ctx.beginPath();
|
||||
_ctx.arc(centerX, centerY, radius, 0, (2 * Math.PI) * (totalSteps - steps.length) / totalSteps);
|
||||
_ctx.lineWidth = lineWidth;
|
||||
_ctx.strokeStyle = color.value;
|
||||
_ctx.stroke();
|
||||
|
||||
// 绘制X
|
||||
_ctx.beginPath();
|
||||
drawStep.forEach(item => {
|
||||
_ctx.beginPath();
|
||||
_ctx.moveTo(item.x1, item.y1)
|
||||
_ctx.lineTo(item.x2, item.y2)
|
||||
_ctx.stroke();
|
||||
})
|
||||
_ctx.update()
|
||||
timer = setTimeout(() => draw(), 1000/30)
|
||||
}
|
||||
draw()
|
||||
}
|
||||
const destroy = () => {
|
||||
isDestroy.value = true;
|
||||
clear()
|
||||
}
|
||||
const play = () => {
|
||||
if(tick == 2) {
|
||||
drawing = false
|
||||
}
|
||||
if(drawing) return
|
||||
tick = 1
|
||||
if(width == 0 || height == 0) return
|
||||
if (type == 'circular') {
|
||||
circular()
|
||||
} else if (type == 'spinner') {
|
||||
spinner()
|
||||
}
|
||||
drawing = true
|
||||
}
|
||||
|
||||
const _watch = (v:boolean) => {
|
||||
if(isDestroy.value) return
|
||||
if (v) {
|
||||
play()
|
||||
} else {
|
||||
failed()
|
||||
}
|
||||
}
|
||||
const stopWatchState = watch(state, _watch)
|
||||
|
||||
const ob = new UniResizeObserver((entries: UniResizeObserverEntry[])=>{
|
||||
if(isDestroy.value) return
|
||||
entries.forEach(entry => {
|
||||
if(isDestroy.value) return
|
||||
const rect = entry.target.getBoundingClientRect();
|
||||
if(rect.width > 0 && rect.height > 0) {
|
||||
updateSize();
|
||||
if(tick == 1) {
|
||||
play()
|
||||
state.value = true
|
||||
} else if(tick == 2) {
|
||||
failed()
|
||||
state.value = false
|
||||
} else if(immediate && !init) {
|
||||
_watch(state.value)
|
||||
init = true
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const stopWatchElement = watch(element, (el:UniElement|null) => {
|
||||
if(el == null || isDestroy.value) return
|
||||
ob.observe(el)
|
||||
})
|
||||
|
||||
onUnmounted(()=>{
|
||||
stopWatchState()
|
||||
stopWatchElement()
|
||||
clear()
|
||||
ob.disconnect()
|
||||
})
|
||||
return {
|
||||
state,
|
||||
play,
|
||||
failed,
|
||||
clear,
|
||||
color,
|
||||
destroy
|
||||
} as useLoadingReturnType
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<view class="demo-block">
|
||||
<text class="demo-block__title-text ultra">加载</text>
|
||||
<text class="demo-block__desc-text">用于表示加载中的过渡状态。</text>
|
||||
<view class="demo-block__body">
|
||||
|
||||
<view class="demo-block card">
|
||||
<text class="demo-block__title-text large">基础</text>
|
||||
<view class="demo-block__body">
|
||||
<view class="row">
|
||||
<l-loading :animated="animated"></l-loading>
|
||||
<l-loading type="spinner" :color="color"></l-loading>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="demo-block card">
|
||||
<text class="demo-block__title-text large">文字</text>
|
||||
<view class="demo-block__body">
|
||||
<l-loading>加载中…</l-loading>
|
||||
<l-loading text="加载中…"></l-loading>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="demo-block card">
|
||||
<text class="demo-block__title-text large">垂直</text>
|
||||
<view class="demo-block__body">
|
||||
<l-loading :vertical="true">加载中…</l-loading>
|
||||
<l-loading :vertical="true" text="加载中…"></l-loading>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="demo-block card">
|
||||
<text class="demo-block__title-text large">尺寸</text>
|
||||
<view class="demo-block__body">
|
||||
<view class="row">
|
||||
<l-loading size="40rpx"></l-loading>
|
||||
<l-loading size="60rpx"></l-loading>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="demo-block card">
|
||||
<text class="demo-block__title-text large">颜色</text>
|
||||
<view class="demo-block__body">
|
||||
<view class="row">
|
||||
<l-loading color="red"></l-loading>
|
||||
<l-loading color="red" type="spinner"></l-loading>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup lang="uts">
|
||||
const color = ref('')
|
||||
const animated = ref(true)
|
||||
|
||||
setTimeout(()=>{
|
||||
color.value = '#34c471'
|
||||
animated.value = false
|
||||
},2000)
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
/* #ifndef UNI-APP-X */
|
||||
gap: 50rpx;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.demo-block {
|
||||
margin: 32px 10px 0;
|
||||
overflow: visible;
|
||||
&.card{
|
||||
padding: 30rpx;
|
||||
background-color: white;
|
||||
margin-bottom: 20rpx !important;
|
||||
}
|
||||
&__title {
|
||||
margin: 0;
|
||||
margin-top: 8px;
|
||||
|
||||
&-text {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
|
||||
&.large {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
&.ultra {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__desc-text {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
margin: 8px 16px 0 0;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin: 16px 0;
|
||||
overflow: visible;
|
||||
|
||||
.demo-block {
|
||||
// margin-top: 0px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<demo-block title="加载" type="ultra">
|
||||
<demo-block title="基础">
|
||||
<view class="row">
|
||||
<l-loading></l-loading>
|
||||
<l-loading type="spinner"></l-loading>
|
||||
<l-loading inherit-color type="ball"></l-loading>
|
||||
</view>
|
||||
</demo-block>
|
||||
<demo-block title="文字">
|
||||
<!-- <l-loading>加载中…</l-loading> -->
|
||||
<l-loading text="加载中…"></l-loading>
|
||||
</demo-block>
|
||||
<demo-block title="垂直">
|
||||
<!-- <l-loading :vertical="true">加载中…</l-loading> -->
|
||||
<l-loading :vertical="true" text="加载中…"></l-loading>
|
||||
</demo-block>
|
||||
<demo-block title="尺寸">
|
||||
<view class="row">
|
||||
<l-loading size="40rpx"></l-loading>
|
||||
<l-loading size="60rpx"></l-loading>
|
||||
</view>
|
||||
</demo-block>
|
||||
<demo-block title="颜色">
|
||||
<view class="row">
|
||||
<!-- #ifndef UNI-APP-X -->
|
||||
<l-loading inherit-color></l-loading>
|
||||
<l-loading inherit-color type="spinner"></l-loading>
|
||||
<l-loading color="red"></l-loading>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef UNI-APP-X -->
|
||||
<l-loading color="red"></l-loading>
|
||||
<l-loading color="red" type="spinner"></l-loading>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</demo-block>
|
||||
</demo-block>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
/* #ifndef UNI-APP-X */
|
||||
gap: 50rpx;
|
||||
/* #endif */
|
||||
}
|
||||
</style>
|
||||
393
uni_modules/lime-loading/index.uts
Normal file
393
uni_modules/lime-loading/index.uts
Normal file
@@ -0,0 +1,393 @@
|
||||
// 引入颜色处理库
|
||||
import { tinyColor } from '@/uni_modules/lime-color';
|
||||
|
||||
|
||||
/**
|
||||
* 操作类型
|
||||
* play: 开始动画
|
||||
* failed: 显示失败状态
|
||||
* clear: 清除动画
|
||||
* destroy: 销毁实例
|
||||
*/
|
||||
export type TickType = 'play' | 'failed' | 'clear' | 'destroy' | 'pause'
|
||||
/**
|
||||
* 加载动画类型
|
||||
* circular: 环形加载动画
|
||||
* spinner: 旋转器加载动画
|
||||
* failed: 失败状态动画
|
||||
*/
|
||||
export type LoadingType = 'circular' | 'spinner' | 'failed';
|
||||
/**
|
||||
* 加载组件返回接口
|
||||
*/
|
||||
export type UseLoadingReturn = {
|
||||
ratio : 1;
|
||||
type : LoadingType;
|
||||
mode : 'raf' | 'animate'; //
|
||||
color : string;//Ref<string>;
|
||||
play : () => void;
|
||||
failed : () => void;
|
||||
clear : () => void;
|
||||
destroy : () => void;
|
||||
pause : () => void;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 计算圆周上指定角度的点的坐标
|
||||
* @param centerX 圆心的 X 坐标
|
||||
* @param centerY 圆心的 Y 坐标
|
||||
* @param radius 圆的半径
|
||||
* @param angleDegrees 角度(以度为单位)
|
||||
* @returns 包含 X 和 Y 坐标的对象
|
||||
*/
|
||||
function getPointOnCircle(
|
||||
centerX : number,
|
||||
centerY : number,
|
||||
radius : number,
|
||||
angleDegrees : number
|
||||
) : number[] {
|
||||
// 将角度转换为弧度
|
||||
const angleRadians = (angleDegrees * Math.PI) / 180;
|
||||
|
||||
// 计算点的 X 和 Y 坐标
|
||||
const x = centerX + radius * Math.cos(angleRadians);
|
||||
const y = centerY + radius * Math.sin(angleRadians);
|
||||
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
export function useLoading(element : Ref<UniElement | null>) : UseLoadingReturn {
|
||||
|
||||
const tick = ref<TickType>('pause')
|
||||
|
||||
const state = reactive<UseLoadingReturn>({
|
||||
color: '#000',
|
||||
type: 'circular',
|
||||
ratio: 1,
|
||||
mode: 'raf',
|
||||
play: () => {
|
||||
tick.value = 'play'
|
||||
},
|
||||
failed: () => {
|
||||
tick.value = 'failed'
|
||||
},
|
||||
clear: () => {
|
||||
tick.value = 'clear'
|
||||
},
|
||||
destroy: () => {
|
||||
tick.value = 'destroy'
|
||||
},
|
||||
pause: () => {
|
||||
tick.value = 'pause'
|
||||
}
|
||||
})
|
||||
|
||||
const context = shallowRef<DrawableContext | null>(null);
|
||||
// let ctx:DrawableContext|null = null
|
||||
|
||||
// let rotation = 0
|
||||
let isPlaying = false
|
||||
let canvasWidth = ref(0)
|
||||
let canvasHeight = ref(0)
|
||||
let canvasSize = ref(0)
|
||||
|
||||
let animationFrameId = -1
|
||||
let animation : UniAnimation | null = null
|
||||
|
||||
let drawFrame : (() => void) | null = null
|
||||
const size = computed(() : number => state.ratio > 1 ? state.ratio : canvasSize.value * state.ratio)
|
||||
// 绘制圆形加载
|
||||
const drawCircular = () => {
|
||||
let startAngle = 0; // 起始角度
|
||||
let endAngle = 0; // 结束角度
|
||||
let rotate = 0; // 旋转角度
|
||||
|
||||
// const ctx = context.value!
|
||||
// 动画参数配置
|
||||
const MIN_ANGLE = 5; // 最小保持角度
|
||||
const ARC_LENGTH = 359.5 // 最大弧长(避免闭合)
|
||||
const PI = Math.PI / 180 // 角度转弧度系数
|
||||
const SPEED = 0.018 / 4 // 动画速度
|
||||
const ROTATE_INTERVAL = 0.09 / 4 // 旋转增量
|
||||
|
||||
const lineWidth = size.value / 10; // 线宽计算
|
||||
const x = canvasWidth.value / 2 // 中心点X
|
||||
const y = canvasHeight.value / 2 // 中心点Y
|
||||
const radius = size.value / 2 - lineWidth // 实际绘制半径
|
||||
|
||||
drawFrame = () => {
|
||||
if (context.value == null || !isPlaying) return
|
||||
let ctx = context.value!
|
||||
|
||||
|
||||
// console.log('radius', radius, size.value)
|
||||
ctx.reset();
|
||||
|
||||
// 绘制圆弧
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
x,
|
||||
y,
|
||||
radius,
|
||||
startAngle * PI + rotate,
|
||||
endAngle * PI + rotate
|
||||
);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = state.color;
|
||||
ctx.stroke();
|
||||
|
||||
// 角度更新逻辑
|
||||
if (endAngle < ARC_LENGTH) {
|
||||
endAngle = Math.min(ARC_LENGTH, endAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED);
|
||||
} else if (startAngle < ARC_LENGTH) {
|
||||
startAngle = Math.min(ARC_LENGTH, startAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED);
|
||||
} else {
|
||||
// 重置时保留最小可见角度
|
||||
startAngle = 0;
|
||||
endAngle = MIN_ANGLE;
|
||||
}
|
||||
|
||||
ctx.update()
|
||||
|
||||
|
||||
|
||||
if (state.mode == 'raf') {
|
||||
rotate = (rotate + ROTATE_INTERVAL) % 360; // 持续旋转并限制范围
|
||||
if (isPlaying && drawFrame != null) {
|
||||
animationFrameId = requestAnimationFrame(drawFrame!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let lastTime = Date.now();
|
||||
const drawSpinner = () => {
|
||||
const steps = 12; // 旋转线条数量
|
||||
// const size = state.ratio > 1 ? state.ratio : canvasSize.value
|
||||
const lineWidth = size.value / 10; // 线宽
|
||||
const x = canvasWidth.value / 2 // 中心坐标
|
||||
const y = canvasHeight.value / 2
|
||||
|
||||
let step = 0; // 当前步数
|
||||
// #ifdef APP-HARMONY
|
||||
const length = size.value / 3.4 - lineWidth; // 线长
|
||||
// #endif
|
||||
// #ifndef APP-HARMONY
|
||||
const length = size.value / 3.6 - lineWidth; // 线长
|
||||
// #endif
|
||||
const offset = size.value / 4; // 距中心偏移
|
||||
|
||||
/** 生成颜色渐变数组 */
|
||||
function generateColorGradient(hex : string, steps : number) : string[] {
|
||||
const colors : string[] = []
|
||||
const _color = tinyColor(hex)
|
||||
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
_color.setAlpha(i / steps);
|
||||
colors.push(_color.toRgbString());
|
||||
}
|
||||
return colors
|
||||
}
|
||||
|
||||
// 计算颜色渐变
|
||||
let colors = computed(() : string[] => generateColorGradient(state.color, steps))
|
||||
|
||||
/** 帧绘制函数 */
|
||||
drawFrame = () => {
|
||||
if (context.value == null || !isPlaying) return
|
||||
const delta = Date.now() - lastTime;
|
||||
|
||||
|
||||
if (delta >= 1000 / 10) {
|
||||
lastTime = Date.now();
|
||||
let ctx = context.value!
|
||||
ctx.reset();
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const stepAngle = 360 / steps; // 单步角度
|
||||
const angle = stepAngle * i; // 当前角度
|
||||
const index = (steps + i - step) % steps // 颜色索引
|
||||
// 计算线段坐标
|
||||
const radian = angle * Math.PI / 180;
|
||||
const cos = Math.cos(radian);
|
||||
const sin = Math.sin(radian);
|
||||
|
||||
// 绘制线段
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + offset * cos, y + offset * sin);
|
||||
ctx.lineTo(x + (offset + length) * cos, y + (offset + length) * sin);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = colors.value[index];
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.update()
|
||||
if(state.mode == 'raf') {
|
||||
// step += 1
|
||||
step = (step + 1) % steps; // 限制step范围
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (state.mode == 'raf') {
|
||||
if (isPlaying && drawFrame != null) {
|
||||
animationFrameId = requestAnimationFrame(drawFrame!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const drwaFailed = () => {
|
||||
if (context.value == null) return
|
||||
let ctx = context.value!
|
||||
|
||||
// const size = state.ratio > 1 ? state.ratio : canvasSize.value
|
||||
const innerSize = size.value * 0.8 // 内圈尺寸
|
||||
const lineWidth = innerSize / 10; // 线宽
|
||||
const lineLength = (size.value - lineWidth) / 2 // X长度
|
||||
const centerX = canvasWidth.value / 2;
|
||||
const centerY = canvasHeight.value / 2;
|
||||
const radius = (size.value - lineWidth) / 2
|
||||
|
||||
|
||||
|
||||
const angleRadians1 = 45 * Math.PI / 180
|
||||
const angleRadians2 = (45 - 90) * Math.PI / 180
|
||||
|
||||
ctx.reset()
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = state.color;
|
||||
|
||||
// 绘制逐渐显示的圆
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = state.color;
|
||||
ctx.stroke();
|
||||
|
||||
const [startX1, startY] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 45)
|
||||
const [startX2] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 90 + 45)
|
||||
|
||||
const x2 = Math.sin(angleRadians1) * lineLength + startX1
|
||||
const y2 = Math.cos(angleRadians1) * lineLength + startY
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX1, startY)
|
||||
ctx.lineTo(x2, y2)
|
||||
ctx.stroke();
|
||||
|
||||
const x3 = Math.sin(angleRadians2) * lineLength + startX2
|
||||
const y3 = Math.cos(angleRadians2) * lineLength + startY
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX2, startY)
|
||||
ctx.lineTo(x3, y3)
|
||||
ctx.stroke();
|
||||
|
||||
|
||||
ctx.update()
|
||||
}
|
||||
|
||||
let currentType : LoadingType | null = null
|
||||
const useMode = () => {
|
||||
if (state.mode != 'raf') {
|
||||
const keyframes = [{ transform: 'rotate(0)' }, { transform: 'rotate(360)' }]
|
||||
animation = element.value!.animate(keyframes, {
|
||||
duration: 80000,
|
||||
easing: 'linear',
|
||||
// fill: 'forwards',
|
||||
iterations: Infinity
|
||||
})
|
||||
}
|
||||
}
|
||||
const startAnimation = (type : string) => {
|
||||
if (context.value == null || element.value == null) return
|
||||
animation?.pause()
|
||||
|
||||
if (currentType == type) {
|
||||
isPlaying = true
|
||||
animation?.play()
|
||||
drawFrame?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (type == 'circular') {
|
||||
currentType = 'circular'
|
||||
drawCircular()
|
||||
useMode()
|
||||
|
||||
}
|
||||
|
||||
if (type == 'spinner') {
|
||||
currentType = 'spinner'
|
||||
drawSpinner()
|
||||
useMode()
|
||||
}
|
||||
|
||||
isPlaying = true
|
||||
drawFrame?.()
|
||||
}
|
||||
|
||||
// 监听元素尺寸
|
||||
const resizeObserver : UniResizeObserver = new UniResizeObserver((_entries : UniResizeObserverEntry[]) => {
|
||||
requestAnimationFrame(()=> {
|
||||
element.value?.getBoundingClientRectAsync()?.then(rect => {
|
||||
if (rect.width == 0 || rect.height == 0) return
|
||||
context.value = element.value!.getDrawableContext() as DrawableContext;
|
||||
canvasWidth.value = rect.width;
|
||||
canvasHeight.value = rect.height;
|
||||
canvasSize.value = Math.min(rect.width, rect.height);
|
||||
// startAnimation(state.type)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (element.value == null) return
|
||||
resizeObserver.observe(element.value!);
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (context.value == null) return
|
||||
if (tick.value == 'play') {
|
||||
startAnimation(state.type)
|
||||
}
|
||||
if (tick.value == 'failed') {
|
||||
clearTimeout(animationFrameId)
|
||||
animation?.pause()
|
||||
animation?.cancel()
|
||||
drwaFailed()
|
||||
return
|
||||
}
|
||||
if (tick.value == 'clear') {
|
||||
clearTimeout(animationFrameId)
|
||||
animation?.pause()
|
||||
animation?.cancel()
|
||||
context.value?.reset();
|
||||
context.value?.update();
|
||||
isPlaying = false
|
||||
return
|
||||
}
|
||||
if (tick.value == 'destroy') {
|
||||
clearTimeout(animationFrameId)
|
||||
animation?.pause()
|
||||
animation?.cancel()
|
||||
context.value?.reset();
|
||||
context.value?.update();
|
||||
context.value = null
|
||||
animation = null
|
||||
isPlaying = false
|
||||
return
|
||||
}
|
||||
if (tick.value == 'pause') {
|
||||
clearTimeout(animationFrameId)
|
||||
isPlaying = false
|
||||
animation?.pause()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
return state
|
||||
}
|
||||
595
uni_modules/lime-loading/index_old.uts
Normal file
595
uni_modules/lime-loading/index_old.uts
Normal file
@@ -0,0 +1,595 @@
|
||||
// 引入颜色处理库
|
||||
import { tinyColor } from '@/uni_modules/lime-color';
|
||||
|
||||
|
||||
// ===================== 类型定义 =====================
|
||||
/**
|
||||
* 加载动画类型
|
||||
* circular: 环形加载动画
|
||||
* spinner: 旋转器加载动画
|
||||
* failed: 失败状态动画
|
||||
*/
|
||||
export type LoadingType = 'circular' | 'spinner' | 'failed';
|
||||
|
||||
/**
|
||||
* 操作类型
|
||||
* play: 开始动画
|
||||
* failed: 显示失败状态
|
||||
* clear: 清除动画
|
||||
* destroy: 销毁实例
|
||||
*/
|
||||
export type TickType = 'play' | 'failed' | 'clear' | 'destroy' | 'pause'
|
||||
|
||||
/**
|
||||
* 加载组件配置选项
|
||||
* @property type - 初始动画类型
|
||||
* @property strokeColor - 线条颜色
|
||||
* @property ratio - 尺寸比例
|
||||
* @property immediate - 是否立即启动
|
||||
*/
|
||||
export type UseLoadingOptions = {
|
||||
type : LoadingType;
|
||||
strokeColor : string;
|
||||
ratio : number;
|
||||
immediate ?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载组件返回接口
|
||||
*/
|
||||
export type UseLoadingReturn = {
|
||||
// state : Ref<boolean>;
|
||||
// setOptions: (options: UseLoadingOptions) => void
|
||||
ratio : 1;
|
||||
type : LoadingType;
|
||||
color : string;//Ref<string>;
|
||||
play : () => void;
|
||||
failed : () => void;
|
||||
clear : () => void;
|
||||
destroy : () => void;
|
||||
pause : () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 画布尺寸信息
|
||||
*/
|
||||
export type Dimensions = {
|
||||
width : number;
|
||||
height : number;
|
||||
size : number
|
||||
}
|
||||
|
||||
/**
|
||||
* 线段坐标点
|
||||
*/
|
||||
type Point = {
|
||||
x1 : number
|
||||
y1 : number
|
||||
x2 : number
|
||||
y2 : number
|
||||
}
|
||||
|
||||
/**
|
||||
* 画布上下文信息
|
||||
*/
|
||||
type LoadingCanvasContext = {
|
||||
ctx : Ref<DrawableContext | null>;
|
||||
dimensions : Ref<Dimensions>;
|
||||
updateDimensions : (el : UniElement) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* 动画参数配置
|
||||
*/
|
||||
type AnimationParams = {
|
||||
width : number
|
||||
height : number
|
||||
center : number[] // 元组类型,明确表示两个数值的坐标
|
||||
color : string // 使用Ref类型包裹字符串
|
||||
size : number // 数值类型尺寸
|
||||
}
|
||||
|
||||
// ===================== 动画管理器 =====================
|
||||
type AnimationFrameHandler = () => boolean;
|
||||
|
||||
/**
|
||||
* 动画管理类
|
||||
* 封装动画的启动/停止逻辑
|
||||
*/
|
||||
export class AnimationManager {
|
||||
time : number = 1000 / 60 // 默认帧率60fps
|
||||
private timer : number = -1;// 定时器ID
|
||||
private isDestroyed : boolean = false; // 销毁状态
|
||||
private drawFrame : AnimationFrameHandler// 帧绘制函数
|
||||
private lastTime : number = 0;
|
||||
private isRunning : boolean = false;
|
||||
type : LoadingType;
|
||||
constructor(drawFrame : AnimationFrameHandler, type : LoadingType = 'circular') {
|
||||
this.type = type
|
||||
this.drawFrame = drawFrame
|
||||
}
|
||||
/** 启动动画循环 */
|
||||
start() {
|
||||
if (this.isRunning) return;
|
||||
this.isRunning = true;
|
||||
this.lastTime = Date.now();
|
||||
let animate : ((task : number) => void) | null = null
|
||||
|
||||
animate = (task : number) => {
|
||||
if (!this.isRunning) return;
|
||||
if (this.isDestroyed) return;
|
||||
const delta = Date.now() - this.lastTime;
|
||||
if (delta >= this.time || delta < 10) {
|
||||
const shouldContinue : boolean = this.drawFrame();
|
||||
this.lastTime = Date.now()
|
||||
if (!shouldContinue) {
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.timer = requestAnimationFrame(animate!);
|
||||
};
|
||||
|
||||
animate(Date.now());
|
||||
}
|
||||
/** 停止动画并清理资源 */
|
||||
stop() {
|
||||
cancelAnimationFrame(this.timer)
|
||||
this.isDestroyed = true;
|
||||
}
|
||||
pause() {
|
||||
this.isRunning = false;
|
||||
cancelAnimationFrame(this.timer);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 工具函数 =====================
|
||||
/**
|
||||
* 缓动函数 - 三次缓入缓出
|
||||
* @param t 时间系数 (0-1)
|
||||
* @returns 计算后的进度值
|
||||
*/
|
||||
function easeInOutCubic(t : number) : number {
|
||||
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
|
||||
}
|
||||
|
||||
// ===================== 画布管理 =====================
|
||||
/**
|
||||
* 获取画布上下文信息
|
||||
* @param element 画布元素引用
|
||||
* @returns 包含画布上下文和尺寸信息的对象
|
||||
*/
|
||||
//_element : Ref<UniElement | null>
|
||||
export function useCanvas() : LoadingCanvasContext {
|
||||
const ctx = shallowRef<DrawableContext | null>(null);
|
||||
const dimensions = ref<Dimensions>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
size: 0
|
||||
});
|
||||
|
||||
const updateDimensions = (el : UniElement) => {
|
||||
// const rect = el.getBoundingClientRect();
|
||||
// // 鸿蒙尺寸为0时 再次尺寸变化会不渲染
|
||||
// if(rect.width == 0) return
|
||||
// if(ctx.value == null) {
|
||||
// ctx.value = el.getDrawableContext() as DrawableContext;
|
||||
// }
|
||||
// dimensions.value.width = rect.width;
|
||||
// dimensions.value.height = rect.height;
|
||||
// dimensions.value.size = Math.min(rect.width, rect.height);
|
||||
el.getBoundingClientRectAsync()?.then(rect => {
|
||||
if (rect.width == 0 || rect.height == 0) return
|
||||
ctx.value = el.getDrawableContext() as DrawableContext;
|
||||
dimensions.value.width = rect.width;
|
||||
dimensions.value.height = rect.height;
|
||||
dimensions.value.size = Math.min(rect.width, rect.height);
|
||||
})
|
||||
};
|
||||
|
||||
return {
|
||||
ctx,
|
||||
dimensions,
|
||||
updateDimensions
|
||||
} as LoadingCanvasContext
|
||||
}
|
||||
|
||||
// ===================== 动画创建函数 =====================
|
||||
/**
|
||||
* 创建环形加载动画
|
||||
* @param ctx 画布上下文
|
||||
* @param animationParams 动画参数
|
||||
* @returns 动画管理器实例
|
||||
*/
|
||||
function createCircularAnimation(ctx : DrawableContext, animationParams : AnimationParams) : AnimationManager {
|
||||
const { size, color, width, height } = animationParams
|
||||
let startAngle = 0; // 起始角度
|
||||
let endAngle = 0; // 结束角度
|
||||
let rotate = 0; // 旋转角度
|
||||
|
||||
// 动画参数配置
|
||||
const MIN_ANGLE = 5; // 最小保持角度
|
||||
const ARC_LENGTH = 359.5 // 最大弧长(避免闭合)
|
||||
const PI = Math.PI / 180 // 角度转弧度系数
|
||||
const SPEED = 0.018 // 动画速度
|
||||
const ROTATE_INTERVAL = 0.09 // 旋转增量
|
||||
const lineWidth = size / 10; // 线宽计算
|
||||
const x = width / 2 // 中心点X
|
||||
const y = height / 2 // 中心点Y
|
||||
const radius = size / 2 - lineWidth // 实际绘制半径
|
||||
|
||||
/** 帧绘制函数 */
|
||||
const drawFrame = () : boolean => {
|
||||
ctx.reset();
|
||||
|
||||
// 绘制圆弧
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
x,
|
||||
y,
|
||||
radius,
|
||||
startAngle * PI + rotate,
|
||||
endAngle * PI + rotate
|
||||
);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.stroke();
|
||||
|
||||
// 角度更新逻辑
|
||||
if (endAngle < ARC_LENGTH) {
|
||||
endAngle = Math.min(ARC_LENGTH, endAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED);
|
||||
} else if (startAngle < ARC_LENGTH) {
|
||||
startAngle = Math.min(ARC_LENGTH, startAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED);
|
||||
} else {
|
||||
// 重置时保留最小可见角度
|
||||
startAngle = 0;
|
||||
endAngle = MIN_ANGLE;
|
||||
}
|
||||
|
||||
rotate = (rotate + ROTATE_INTERVAL) % 360; // 持续旋转并限制范围
|
||||
ctx.update()
|
||||
return true
|
||||
}
|
||||
|
||||
return new AnimationManager(drawFrame)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建旋转器动画
|
||||
* @param ctx 画布上下文
|
||||
* @param animationParams 动画参数
|
||||
* @returns 动画管理器实例
|
||||
*/
|
||||
function createSpinnerAnimation(ctx : DrawableContext, animationParams : AnimationParams) : AnimationManager {
|
||||
const { size, color, center } = animationParams
|
||||
const steps = 12; // 旋转线条数量
|
||||
let step = 0; // 当前步数
|
||||
const lineWidth = size / 10; // 线宽
|
||||
// #ifdef APP-HARMONY
|
||||
const length = size / 3.4 - lineWidth; // 线长
|
||||
// #endif
|
||||
// #ifndef APP-HARMONY
|
||||
const length = size / 3.6 - lineWidth; // 线长
|
||||
// #endif
|
||||
const offset = size / 4; // 距中心偏移
|
||||
const [x, y] = center // 中心坐标
|
||||
|
||||
/** 生成颜色渐变数组 */
|
||||
function generateColorGradient(hex : string, steps : number) : string[] {
|
||||
const colors : string[] = []
|
||||
const _color = tinyColor(hex)
|
||||
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
_color.setAlpha(i / steps);
|
||||
colors.push(_color.toRgbString());
|
||||
}
|
||||
return colors
|
||||
}
|
||||
|
||||
// 计算颜色渐变
|
||||
let colors = computed(() : string[] => generateColorGradient(color, steps))
|
||||
/** 帧绘制函数 */
|
||||
const drawFrame = () : boolean => {
|
||||
ctx.reset();
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const stepAngle = 360 / steps; // 单步角度
|
||||
const angle = stepAngle * i; // 当前角度
|
||||
const index = (steps + i - step) % steps // 颜色索引
|
||||
// const index = (i + step) % steps;
|
||||
// console.log('index', index)
|
||||
// 计算线段坐标
|
||||
const radian = angle * Math.PI / 180;
|
||||
const cos = Math.cos(radian);
|
||||
const sin = Math.sin(radian);
|
||||
|
||||
// 绘制线段
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + offset * cos, y + offset * sin);
|
||||
ctx.lineTo(x + (offset + length) * cos, y + (offset + length) * sin);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = colors.value[index];
|
||||
ctx.stroke();
|
||||
}
|
||||
// step += 1
|
||||
step = (step + 1) % steps; // 限制step范围
|
||||
ctx.update()
|
||||
return true
|
||||
}
|
||||
return new AnimationManager(drawFrame)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算圆周上指定角度的点的坐标
|
||||
* @param centerX 圆心的 X 坐标
|
||||
* @param centerY 圆心的 Y 坐标
|
||||
* @param radius 圆的半径
|
||||
* @param angleDegrees 角度(以度为单位)
|
||||
* @returns 包含 X 和 Y 坐标的对象
|
||||
*/
|
||||
function getPointOnCircle(
|
||||
centerX : number,
|
||||
centerY : number,
|
||||
radius : number,
|
||||
angleDegrees : number
|
||||
) : number[] {
|
||||
// 将角度转换为弧度
|
||||
const angleRadians = (angleDegrees * Math.PI) / 180;
|
||||
|
||||
// 计算点的 X 和 Y 坐标
|
||||
const x = centerX + radius * Math.cos(angleRadians);
|
||||
const y = centerY + radius * Math.sin(angleRadians);
|
||||
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建失败状态动画(包含X图标和外围圆圈)
|
||||
* @param ctx 画布上下文
|
||||
* @param animationParams 动画参数
|
||||
* @returns 动画管理器实例
|
||||
*/
|
||||
function createFailedAnimation(ctx : DrawableContext, animationParams : AnimationParams) : AnimationManager {
|
||||
|
||||
const { width, height, size, color } = animationParams
|
||||
const innerSize = size * 0.8 // 内圈尺寸
|
||||
const lineWidth = innerSize / 10; // 线宽
|
||||
const lineLength = (size - lineWidth) / 2 // X长度
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
const [startX1, startY] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 45)
|
||||
const [startX2] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 90 + 45)
|
||||
const angleRadians1 = 45 * Math.PI / 180
|
||||
const angleRadians2 = (45 - 90) * Math.PI / 180
|
||||
|
||||
const radius = (size - lineWidth) / 2
|
||||
const totalSteps = 36; // 总动画步数
|
||||
|
||||
function generateSteps(stepsCount : number) : Point[][] {
|
||||
|
||||
const halfStepsCount = stepsCount / 2;
|
||||
const step = lineLength / halfStepsCount
|
||||
const steps : Point[][] = []
|
||||
for (let i = 0; i < stepsCount; i++) {
|
||||
const sub : Point[] = []
|
||||
const index = i % 18 + 1
|
||||
if (i < halfStepsCount) {
|
||||
|
||||
const x2 = Math.sin(angleRadians1) * step * index + startX1
|
||||
const y2 = Math.cos(angleRadians1) * step * index + startY
|
||||
|
||||
const start1 = {
|
||||
x1: startX1,
|
||||
y1: startY,
|
||||
x2,
|
||||
y2,
|
||||
} as Point
|
||||
|
||||
sub.push(start1)
|
||||
} else {
|
||||
sub.push(steps[halfStepsCount - 1][0])
|
||||
const x2 = Math.sin(angleRadians2) * step * index + startX2
|
||||
const y2 = Math.cos(angleRadians2) * step * index + startY
|
||||
|
||||
const start2 = {
|
||||
x1: startX2,
|
||||
y1: startY,
|
||||
x2,
|
||||
y2,
|
||||
} as Point
|
||||
sub.push(start2)
|
||||
}
|
||||
steps.push(sub)
|
||||
}
|
||||
|
||||
return steps
|
||||
}
|
||||
const steps = generateSteps(totalSteps);
|
||||
|
||||
const drawFrame = () : boolean => {
|
||||
const drawStep = steps.shift()!
|
||||
ctx.reset()
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = color;
|
||||
|
||||
// 绘制逐渐显示的圆
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, 0, (2 * Math.PI) * (totalSteps - steps.length) / totalSteps);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.stroke();
|
||||
|
||||
// 绘制X
|
||||
ctx.beginPath();
|
||||
drawStep.forEach(item => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(item.x1, item.y1)
|
||||
ctx.lineTo(item.x2, item.y2)
|
||||
ctx.stroke();
|
||||
})
|
||||
ctx.update()
|
||||
return steps.length != 0
|
||||
}
|
||||
return new AnimationManager(drawFrame)
|
||||
}
|
||||
|
||||
|
||||
// ===================== 主Hook函数 =====================
|
||||
/**
|
||||
* 加载动画组合式函数
|
||||
* @param element 画布元素引用
|
||||
* @returns 加载控制器实例
|
||||
*/
|
||||
export function useLoading(
|
||||
element : Ref<UniElement | null>,
|
||||
// options : UseLoadingOptions
|
||||
) : UseLoadingReturn {
|
||||
const ticks = ref<TickType[]>([]);
|
||||
const currentTick = ref<TickType>('clear');
|
||||
const currentAnimation = shallowRef<AnimationManager | null>(null);
|
||||
const state = reactive<UseLoadingReturn>({
|
||||
color: '#000',
|
||||
type: 'circular',
|
||||
ratio: 1,
|
||||
play: () => {
|
||||
// if (currentTick.value == 'pause' && currentAnimation.value != null) {
|
||||
// // 从暂停状态恢复时直接启动动画管理器
|
||||
// currentAnimation.value?.start()
|
||||
// } else {
|
||||
// // 首次播放或类型变化时重新初始化
|
||||
// ticks.value.length = 0
|
||||
// ticks.value.push('play')
|
||||
// }
|
||||
ticks.value.length = 0
|
||||
ticks.value.push('play')
|
||||
},
|
||||
failed: () => {
|
||||
ticks.value.length = 0
|
||||
ticks.value.push('failed')
|
||||
},
|
||||
clear: () => {
|
||||
ticks.value.length = 0
|
||||
ticks.value.push('clear')
|
||||
},
|
||||
destroy: () => {
|
||||
ticks.value.length = 0
|
||||
ticks.value.push('destroy')
|
||||
},
|
||||
pause: () => {
|
||||
ticks.value.push('pause')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const { ctx, dimensions, updateDimensions } = useCanvas();
|
||||
const resizeObserver : UniResizeObserver = new UniResizeObserver((_entries : UniResizeObserverEntry[]) => {
|
||||
updateDimensions(element.value!)
|
||||
});
|
||||
|
||||
|
||||
// 计算动画参数
|
||||
const animationParams = computed(() : AnimationParams => {
|
||||
return {
|
||||
width: dimensions.value.width,
|
||||
height: dimensions.value.height,
|
||||
center: [dimensions.value.width / 2, dimensions.value.height / 2],
|
||||
color: state.color,
|
||||
size: state.ratio > 1 ? state.ratio : dimensions.value.size * state.ratio
|
||||
} as AnimationParams
|
||||
})
|
||||
|
||||
const startAnimation = (type : LoadingType) => {
|
||||
currentAnimation.value?.pause();
|
||||
if (currentAnimation.value?.type == type) {
|
||||
currentAnimation.value!.start()
|
||||
return
|
||||
}
|
||||
if (type == 'circular') {
|
||||
currentAnimation.value = createCircularAnimation(ctx.value!, animationParams.value)
|
||||
currentAnimation.value!.time = 1000 / 30
|
||||
currentAnimation.value!.start()
|
||||
return
|
||||
}
|
||||
if (type == 'spinner') {
|
||||
currentAnimation.value = createSpinnerAnimation(ctx.value!, animationParams.value)
|
||||
currentAnimation.value!.time = 1000 / 10
|
||||
currentAnimation.value!.start()
|
||||
return
|
||||
}
|
||||
if (type == 'failed') {
|
||||
currentAnimation.value = createFailedAnimation(ctx.value!, animationParams.value)
|
||||
currentAnimation.value?.start()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const failed = () => {
|
||||
startAnimation('failed')
|
||||
}
|
||||
const play = () => {
|
||||
startAnimation(state.type)
|
||||
}
|
||||
const clear = () => {
|
||||
currentAnimation.value?.stop();
|
||||
ctx.value?.reset();
|
||||
ctx.value?.update();
|
||||
}
|
||||
const destroy = () => {
|
||||
clear();
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
|
||||
watch(animationParams, () => {
|
||||
if (['clear', 'destroy', 'pause'].includes(currentTick.value)) return
|
||||
startAnimation(state.type)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (ctx.value == null) return
|
||||
|
||||
const tick = ticks.value.pop()
|
||||
if (ticks.value.length > 0) { }
|
||||
if (tick != null) {
|
||||
currentTick.value = tick
|
||||
}
|
||||
if (tick == 'play') {
|
||||
play()
|
||||
return
|
||||
}
|
||||
if (tick == 'failed') {
|
||||
failed()
|
||||
return
|
||||
}
|
||||
if (tick == 'clear') {
|
||||
clear()
|
||||
return
|
||||
}
|
||||
if (tick == 'destroy') {
|
||||
destroy()
|
||||
return
|
||||
}
|
||||
if (tick == 'pause') {
|
||||
if (currentAnimation.value == null) {
|
||||
play()
|
||||
}
|
||||
currentAnimation.value?.pause();
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (element.value == null) return
|
||||
resizeObserver.observe(element.value!);
|
||||
// #ifdef APP-IOS || APP-HARMONY
|
||||
// APP-HARMONY 在下拉组件中使用居中会影响绘制 故先获取一次
|
||||
updateDimensions(element.value!)
|
||||
// #endif
|
||||
})
|
||||
|
||||
onUnmounted(destroy);
|
||||
|
||||
|
||||
return state
|
||||
}
|
||||
91
uni_modules/lime-loading/package.json
Normal file
91
uni_modules/lime-loading/package.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"id": "lime-loading",
|
||||
"displayName": "lime-loading 加载",
|
||||
"version": "0.1.8",
|
||||
"description": "lime-loading 全端通用加载插件.用于表示页面或操作的加载状态,给予用户反馈的同时减缓等待的焦虑感。支持uniapp/uniappx",
|
||||
"keywords": [
|
||||
"loading",
|
||||
"lime-loading",
|
||||
"加载",
|
||||
"uvue"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.92"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": ""
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"lime-shared",
|
||||
"lime-style",
|
||||
"lime-color"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y",
|
||||
"alipay": "n"
|
||||
},
|
||||
"client": {
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "y"
|
||||
},
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "y",
|
||||
"app-uvue": "y",
|
||||
"app-harmony": "u"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "y"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "y",
|
||||
"IE": "u",
|
||||
"Edge": "u",
|
||||
"Firefox": "u",
|
||||
"Safari": "u"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "u",
|
||||
"百度": "u",
|
||||
"字节跳动": "u",
|
||||
"QQ": "u",
|
||||
"钉钉": "u",
|
||||
"快手": "u",
|
||||
"飞书": "u",
|
||||
"京东": "u"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "u",
|
||||
"联盟": "u"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
uni_modules/lime-loading/readme.md
Normal file
110
uni_modules/lime-loading/readme.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# lime-loading 加载中
|
||||
- 用于表示页面或操作的加载状态,给予用户反馈的同时减缓等待的焦虑感,由一个或一组反馈动效组成。
|
||||
|
||||
## 文档
|
||||
[loading](https://limex.qcoon.cn/components/loading.html)
|
||||
|
||||
|
||||
## 安装
|
||||
插件市场导入即可,首次导入可以需要重新编译
|
||||
|
||||
## 代码演示
|
||||
### 基础
|
||||
通过 `type` 属性可以设置加载图标的类型,默认为 `circular`,可选值为 `spinner`。nvue只有`circular`,因为其使用的是原生组件。
|
||||
|
||||
```vue
|
||||
<l-loading />
|
||||
<l-loading type="spinner"/>
|
||||
```
|
||||
|
||||
### 横向文字
|
||||
通过`text`或`插槽`设置加载文本,uvue建议使用`text`
|
||||
|
||||
```vue
|
||||
<l-loading>加载中…</l-loading>
|
||||
<l-loading text="加载中…"></l-loading>
|
||||
```
|
||||
|
||||
### 竖向文字
|
||||
|
||||
```vue
|
||||
<l-loading :vertical="true">加载中…</l-loading>
|
||||
```
|
||||
|
||||
### 尺寸
|
||||
通过 `size` 属性设置加载图标的大小,默认单位为 `px`, uvue必须带单位。
|
||||
|
||||
```vue
|
||||
<l-loading size="40rpx"></l-loading>
|
||||
<l-loading size="60rpx"></l-loading>
|
||||
```
|
||||
|
||||
### 颜色
|
||||
通过 `color` 属性设置加载图标的颜色。
|
||||
|
||||
```vue
|
||||
<l-loading color="red" ></l-loading>
|
||||
```
|
||||
|
||||
### 查看示例
|
||||
- 导入后直接使用这个标签查看演示效果
|
||||
|
||||
```html
|
||||
<!-- // 代码位于 uni_modules/lime-loading/compoents/lime-loading -->
|
||||
<lime-loading />
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 插件标签
|
||||
- 默认 l-loading 为 component
|
||||
- 默认 lime-loading 为 demo
|
||||
|
||||
|
||||
### 关于vue2的使用方式
|
||||
- 插件使用了`composition-api`, 如果你希望在vue2中使用请按官方的教程[vue-composition-api](https://uniapp.dcloud.net.cn/tutorial/vue-composition-api.html)配置
|
||||
- 关键代码是: 在main.js中 在vue2部分加上这一段即可.
|
||||
```js
|
||||
// vue2
|
||||
import Vue from 'vue'
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
Vue.use(VueCompositionAPI)
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| ---------- | ----------------------------- | ------------------ | ---------- |
|
||||
| color | 颜色 | _string_ | `` |
|
||||
| type | 类型,可选值为 `spinner` | _string_ | `circular` |
|
||||
| size | 加载图标大小,默认单位为 `px`,uvue只支持string | _number \| string_ | `40rpx` |
|
||||
| text | 加载文本 | _string_ | `-` |
|
||||
| textColor | 文本颜色 | _string_ | `-` |
|
||||
| textSize | 文本大小 `px`,uvue只支持string | _number \| string_ | `-` |
|
||||
| vertical | 是否垂直排列图标和文字内容 | _boolean_ | `false` |
|
||||
| mode | 动画实现的模式.只针对APP,可选值有:`animate` | _string_ | `raf` |
|
||||
|
||||
### Slots
|
||||
|
||||
| 名称 | 说明 |
|
||||
| ------- | -------------- |
|
||||
| default | 加载文案 |
|
||||
|
||||
|
||||
|
||||
## 主题定制
|
||||
|
||||
### 样式变量
|
||||
|
||||
组件提供了下列 CSS 变量,可用于自定义样式。uvue和nvue不支持!
|
||||
|
||||
| 名称 | 默认值 | 描述 |
|
||||
| ------------------------------ | ------------------------- | ---- |
|
||||
| --l-loading-color | _#1677ff_ | - |
|
||||
| --l-loading-text-color | _rgba(0,0,0,0.45)_ | - |
|
||||
| --l-loading-font-size | _28rpx_ | - |
|
||||
| --l-loading-duration | _2s_ | - |
|
||||
Reference in New Issue
Block a user