添加动画库
This commit is contained in:
1769
cool/animation/index.ts
Normal file
1769
cool/animation/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@ export function cool(app: VueApp) {
|
||||
console.log(app);
|
||||
}
|
||||
|
||||
export * from "./animation";
|
||||
export * from "./ctx";
|
||||
export * from "./hooks";
|
||||
export * from "./router";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cool-unix",
|
||||
"version": "8.0.21",
|
||||
"version": "8.0.22",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build-ui": "node ./uni_modules/cool-ui/scripts/generate-types.js",
|
||||
|
||||
@@ -441,6 +441,12 @@
|
||||
"style": {
|
||||
"navigationBarTitleText": "SlideVerify 滑动验证"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "other/animate",
|
||||
"style": {
|
||||
"navigationBarTitleText": "Animate 动画"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
283
pages/demo/other/animate.uvue
Normal file
283
pages/demo/other/animate.uvue
Normal file
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<cl-page>
|
||||
<view class="p-3">
|
||||
<demo-item :label="t('基础动画')">
|
||||
<view class="row">
|
||||
<view class="item" ref="rotateRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">rotate</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="scaleRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">scale</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="moveRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">move</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="opacityRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">opacity</cl-text>
|
||||
</view>
|
||||
</view>
|
||||
</demo-item>
|
||||
|
||||
<demo-item :label="t('淡入淡出')">
|
||||
<view class="row">
|
||||
<view class="item" ref="fadeInRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">fadeIn</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="fadeOutRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">fadeOut</cl-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<cl-button text border @tap="onFadeToggle">{{ t("播放动画") }}</cl-button>
|
||||
</demo-item>
|
||||
|
||||
<demo-item :label="t('滑入')">
|
||||
<view class="row">
|
||||
<view class="item" ref="slideLeftRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">slideLeft</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="slideRightRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">slideRight</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="slideUpRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">slideUp</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="slideDownRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">slideDown</cl-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<cl-button text border @tap="onSlideInToggle">{{ t("播放动画") }}</cl-button>
|
||||
</demo-item>
|
||||
|
||||
<demo-item label="缩放">
|
||||
<view class="row">
|
||||
<view class="item" ref="zoomInRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">zoomIn</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="zoomOutRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">zoomOut</cl-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<cl-button text border @tap="onZoomToggle">{{ t("播放动画") }}</cl-button>
|
||||
</demo-item>
|
||||
|
||||
<demo-item :label="t('旋转翻转')">
|
||||
<view class="row">
|
||||
<view class="item" ref="rotateInRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">rotateIn</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="flipXRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">flipX</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="flipYRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">flipY</cl-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<cl-button text border @tap="onRotateFlipToggle">{{ t("播放动画") }}</cl-button>
|
||||
</demo-item>
|
||||
|
||||
<demo-item :label="t('摇摆抖动')">
|
||||
<view class="row">
|
||||
<view class="item" ref="shakeRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">shake</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="swingRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">swing</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="wobbleRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">wobble</cl-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<cl-button text border @tap="onShakeToggle">{{ t("播放动画") }}</cl-button>
|
||||
</demo-item>
|
||||
|
||||
<demo-item :label="t('特殊效果')">
|
||||
<view class="row">
|
||||
<view class="item" ref="rollInRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">rollIn</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="lightSpeedRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">lightSpeed</cl-text>
|
||||
</view>
|
||||
<view class="item" ref="rippleRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">ripple</cl-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<cl-button text border @tap="onSpecialToggle">{{ t("播放动画") }}</cl-button>
|
||||
</demo-item>
|
||||
|
||||
<demo-item :label="t('组合动画')">
|
||||
<view class="row">
|
||||
<view class="item" ref="sequenceRef">
|
||||
<cl-text color="white" :pt="{ className: 'text-sm' }">sequence</cl-text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<cl-button text border @tap="onSequenceToggle">{{ t("播放动画") }}</cl-button>
|
||||
</demo-item>
|
||||
</view>
|
||||
</cl-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { AnimationEngine, createAnimation } from "@/cool";
|
||||
import { ref } from "vue";
|
||||
import DemoItem from "../components/item.uvue";
|
||||
import { t } from "@/locale";
|
||||
|
||||
// 基础动画引用
|
||||
const rotateRef = ref<UniElement | null>(null);
|
||||
const moveRef = ref<UniElement | null>(null);
|
||||
const scaleRef = ref<UniElement | null>(null);
|
||||
const opacityRef = ref<UniElement | null>(null);
|
||||
|
||||
// 淡入淡出动画引用
|
||||
const fadeInRef = ref<UniElement | null>(null);
|
||||
const fadeOutRef = ref<UniElement | null>(null);
|
||||
|
||||
// 滑入动画引用
|
||||
const slideLeftRef = ref<UniElement | null>(null);
|
||||
const slideRightRef = ref<UniElement | null>(null);
|
||||
const slideUpRef = ref<UniElement | null>(null);
|
||||
const slideDownRef = ref<UniElement | null>(null);
|
||||
|
||||
// 缩放动画引用
|
||||
const zoomInRef = ref<UniElement | null>(null);
|
||||
const zoomOutRef = ref<UniElement | null>(null);
|
||||
|
||||
// 旋转翻转动画引用
|
||||
const rotateInRef = ref<UniElement | null>(null);
|
||||
const flipXRef = ref<UniElement | null>(null);
|
||||
const flipYRef = ref<UniElement | null>(null);
|
||||
|
||||
// 摇摆抖动动画引用
|
||||
const shakeRef = ref<UniElement | null>(null);
|
||||
const swingRef = ref<UniElement | null>(null);
|
||||
const wobbleRef = ref<UniElement | null>(null);
|
||||
|
||||
// 特殊效果动画引用
|
||||
const rollInRef = ref<UniElement | null>(null);
|
||||
const lightSpeedRef = ref<UniElement | null>(null);
|
||||
const rippleRef = ref<UniElement | null>(null);
|
||||
|
||||
// 组合动画引用
|
||||
const sequenceRef = ref<UniElement | null>(null);
|
||||
|
||||
// 基础动画方法
|
||||
function onRotate() {
|
||||
createAnimation(rotateRef.value, {
|
||||
duration: 1000,
|
||||
loop: -1
|
||||
})
|
||||
.rotate("0deg", "360deg")
|
||||
.play();
|
||||
}
|
||||
|
||||
function onMove() {
|
||||
createAnimation(moveRef.value, {
|
||||
duration: 1000,
|
||||
loop: -1,
|
||||
alternate: true
|
||||
})
|
||||
.translateX("0rpx", "300rpx")
|
||||
.play();
|
||||
}
|
||||
|
||||
function onScale() {
|
||||
createAnimation(scaleRef.value, {
|
||||
duration: 500,
|
||||
loop: -1,
|
||||
alternate: true
|
||||
})
|
||||
.scale("1", "1.2")
|
||||
.play();
|
||||
}
|
||||
|
||||
function onOpacity() {
|
||||
createAnimation(opacityRef.value, {
|
||||
duration: 500,
|
||||
loop: -1,
|
||||
alternate: true
|
||||
})
|
||||
.opacity("1", "0.5")
|
||||
.play();
|
||||
}
|
||||
|
||||
// 淡入淡出动画方法
|
||||
function onFadeToggle() {
|
||||
createAnimation(fadeInRef.value).fadeIn(500).play();
|
||||
createAnimation(fadeOutRef.value).fadeOut(500).play();
|
||||
}
|
||||
|
||||
// 滑入动画方法
|
||||
function onSlideInToggle() {
|
||||
createAnimation(slideLeftRef.value).slideInLeft(400).play();
|
||||
createAnimation(slideRightRef.value).slideInRight(400).play();
|
||||
createAnimation(slideUpRef.value).slideInUp(400).play();
|
||||
createAnimation(slideDownRef.value).slideInDown(400).play();
|
||||
}
|
||||
|
||||
// 缩放动画方法
|
||||
function onZoomToggle() {
|
||||
createAnimation(zoomInRef.value).zoomIn(400).play();
|
||||
createAnimation(zoomOutRef.value).zoomOut(400).play();
|
||||
}
|
||||
|
||||
// 旋转翻转动画方法
|
||||
function onRotateFlipToggle() {
|
||||
createAnimation(rotateInRef.value).rotateIn(600).play();
|
||||
createAnimation(flipXRef.value).flipX(600).play();
|
||||
createAnimation(flipYRef.value).flipY(600).play();
|
||||
}
|
||||
|
||||
// 摇摆抖动动画方法
|
||||
function onShakeToggle() {
|
||||
createAnimation(shakeRef.value).shake(200).play();
|
||||
createAnimation(swingRef.value).swing(200).play();
|
||||
createAnimation(wobbleRef.value).wobble(200).play();
|
||||
}
|
||||
|
||||
// 特殊效果动画方法
|
||||
function onSpecialToggle() {
|
||||
createAnimation(rollInRef.value).rollIn(600).play();
|
||||
createAnimation(lightSpeedRef.value).lightSpeed(500).play();
|
||||
createAnimation(rippleRef.value).ripple(600).play();
|
||||
}
|
||||
|
||||
// 异步序列动画方法
|
||||
async function onSequenceToggle() {
|
||||
createAnimation(sequenceRef.value)
|
||||
.sequence([
|
||||
(engine: AnimationEngine) => engine.fadeIn(300),
|
||||
(engine: AnimationEngine) => engine.scale("1", "1.5").setDuration(400),
|
||||
(engine: AnimationEngine) => engine.rotate("0deg", "360deg").setDuration(500),
|
||||
(engine: AnimationEngine) => engine.scale("1.5", "1").setDuration(300),
|
||||
(engine: AnimationEngine) => engine.fadeOut(300)
|
||||
])
|
||||
.play();
|
||||
}
|
||||
|
||||
onReady(() => {
|
||||
onRotate();
|
||||
onMove();
|
||||
onScale();
|
||||
onOpacity();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.row {
|
||||
@apply flex flex-row justify-between items-center overflow-visible;
|
||||
margin: 0 -10rpx 20rpx -10rpx;
|
||||
|
||||
.item {
|
||||
@apply flex items-center justify-center h-16 bg-primary-500 rounded-xl flex-1;
|
||||
margin: 0 10rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -448,6 +448,11 @@ const data = computed<Item[]>(() => {
|
||||
label: "SlideVerify",
|
||||
icon: "contract-right-fill",
|
||||
path: "/pages/demo/other/slide-verify"
|
||||
},
|
||||
{
|
||||
label: "Animate",
|
||||
icon: "instance-line",
|
||||
path: "/pages/demo/other/animate"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
:maxlength="length"
|
||||
:hold-keyboard="false"
|
||||
:clearable="false"
|
||||
@focus="onFocus"
|
||||
@change="onChange"
|
||||
></cl-input>
|
||||
</view>
|
||||
@@ -47,6 +48,7 @@
|
||||
>{{ item }}</cl-text
|
||||
>
|
||||
<view
|
||||
ref="cursorRef"
|
||||
class="cl-input-otp__cursor"
|
||||
:class="[pt.cursor?.className]"
|
||||
v-if="value.length == index && isFocus && item == ''"
|
||||
@@ -57,9 +59,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, type PropType, type Ref } from "vue";
|
||||
import { computed, nextTick, onMounted, ref, watch, type PropType, type Ref } from "vue";
|
||||
import type { ClInputType, PassThroughProps } from "../../types";
|
||||
import { isDark, parsePt } from "@/cool";
|
||||
import { createAnimation, isDark, isEmpty, parsePt } from "@/cool";
|
||||
|
||||
defineOptions({
|
||||
name: "cl-input-otp"
|
||||
@@ -123,8 +125,11 @@ const pt = computed(() => parsePt<PassThrough>(props.pt));
|
||||
// 输入框引用
|
||||
const inputRef = ref<ClInputComponentPublicInstance | null>(null);
|
||||
|
||||
// 光标引用
|
||||
const cursorRef = ref<UniElement[]>([]);
|
||||
|
||||
// 输入值
|
||||
const value = ref(props.modelValue) as Ref<string>;
|
||||
const value = ref(props.modelValue);
|
||||
|
||||
/**
|
||||
* 是否聚焦状态
|
||||
@@ -156,15 +161,31 @@ const list = computed<string[]>(() => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听绑定值变化
|
||||
* 同步更新内部值
|
||||
* 光标动画
|
||||
*/
|
||||
watch(
|
||||
computed(() => props.modelValue),
|
||||
(val: string) => {
|
||||
value.value = val;
|
||||
async function onCursor() {
|
||||
await nextTick();
|
||||
|
||||
if (isEmpty(cursorRef.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 开始动画
|
||||
createAnimation(cursorRef.value[0], {
|
||||
duration: 600,
|
||||
loop: -1,
|
||||
alternate: true
|
||||
})
|
||||
.opacity("0", "1")
|
||||
.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入框聚焦
|
||||
*/
|
||||
function onFocus() {
|
||||
onCursor();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 输入事件处理
|
||||
@@ -179,7 +200,23 @@ function onChange(val: string) {
|
||||
uni.hideKeyboard();
|
||||
emit("done", val);
|
||||
}
|
||||
|
||||
// 更新光标动画
|
||||
onCursor();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
watch(
|
||||
computed(() => props.modelValue),
|
||||
(val: string) => {
|
||||
value.value = val;
|
||||
onCursor();
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -223,32 +260,11 @@ function onChange(val: string) {
|
||||
}
|
||||
|
||||
&__cursor {
|
||||
@apply absolute duration-100;
|
||||
@apply bg-primary-500;
|
||||
@apply absolute bg-primary-500;
|
||||
width: 2rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
|
||||
// #ifdef H5 || MP
|
||||
&__cursor {
|
||||
animation: flash 1s infinite ease;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
&--disabled {
|
||||
@apply opacity-50;
|
||||
}
|
||||
|
||||
@@ -264,37 +264,8 @@ function resetSwipe() {
|
||||
* 使用requestAnimationFrame实现平滑滑动动画
|
||||
*/
|
||||
function swipeTo(num: number) {
|
||||
// #ifdef APP
|
||||
function next() {
|
||||
requestAnimationFrame(() => {
|
||||
if (swipe.offsetX != num) {
|
||||
// 计算每次移动的距离
|
||||
const step = 2;
|
||||
const direction = swipe.offsetX < num ? 1 : -1;
|
||||
|
||||
// 更新偏移量
|
||||
swipe.offsetX += step * direction;
|
||||
|
||||
// 防止过度滑动
|
||||
if (direction > 0 ? swipe.offsetX > num : swipe.offsetX < num) {
|
||||
swipe.offsetX = num;
|
||||
}
|
||||
|
||||
next();
|
||||
} else {
|
||||
// 动画结束,更新结束位置
|
||||
swipe.endX = swipe.offsetX;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
// #endif
|
||||
|
||||
// #ifdef H5 || MP
|
||||
swipe.offsetX = num;
|
||||
swipe.endX = num;
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 点击态状态 - 用于显示点击效果
|
||||
@@ -424,14 +395,12 @@ defineExpose({
|
||||
@apply flex flex-col w-full relative;
|
||||
|
||||
&__wrapper {
|
||||
@apply w-full;
|
||||
@apply w-full transition-none;
|
||||
overflow: visible;
|
||||
|
||||
&.is-transition {
|
||||
// #ifdef H5 || MP
|
||||
@apply duration-200;
|
||||
transition-property: transform;
|
||||
transition-duration: 0.2s;
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,7 +419,6 @@ defineExpose({
|
||||
|
||||
&-left {
|
||||
@apply left-full;
|
||||
transform: translateX(1rpx);
|
||||
}
|
||||
|
||||
&-right {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
:class="[
|
||||
{
|
||||
'cl-loading--dark': isDark && color == '',
|
||||
'cl-loading--spin': loading,
|
||||
'!border-r-transparent': true
|
||||
},
|
||||
pt.className
|
||||
@@ -13,13 +12,11 @@
|
||||
:style="{
|
||||
height: getPx(size!),
|
||||
width: getPx(size!),
|
||||
// #ifndef APP
|
||||
borderWidth: '1px',
|
||||
borderTopColor: color,
|
||||
borderRightColor: 'transparent',
|
||||
borderBottomColor: color,
|
||||
borderLeftColor: color
|
||||
// #endif
|
||||
}"
|
||||
v-if="loading"
|
||||
>
|
||||
@@ -27,8 +24,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref, shallowRef, watch } from "vue";
|
||||
import { ctx, isDark, parsePt } from "@/cool";
|
||||
import { computed, onMounted, shallowRef, watch } from "vue";
|
||||
import { createAnimation, ctx, isDark, parsePt } from "@/cool";
|
||||
import type { ClIconProps } from "../cl-icon/props";
|
||||
import { useSize } from "../../hooks";
|
||||
|
||||
@@ -60,7 +57,7 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const { getPxValue, getPx } = useSize();
|
||||
const { getPx } = useSize();
|
||||
|
||||
// 透传样式类型定义
|
||||
type PassThrough = {
|
||||
@@ -74,6 +71,7 @@ const pt = computed(() => parsePt<PassThrough>(props.pt));
|
||||
// 组件引用
|
||||
const loadingRef = shallowRef<UniElement | null>(null);
|
||||
|
||||
// 颜色值
|
||||
const color = computed<string>(() => {
|
||||
if (props.color == "") {
|
||||
return isDark.value ? "#ffffff" : (ctx.color["surface-700"] as string);
|
||||
@@ -101,83 +99,19 @@ const color = computed<string>(() => {
|
||||
}
|
||||
});
|
||||
|
||||
async function drawLoading() {
|
||||
// #ifdef APP
|
||||
await nextTick();
|
||||
|
||||
if (loadingRef.value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const drawContext = loadingRef.value!.getDrawableContext();
|
||||
|
||||
// 重置画布,准备绘制新的loading图形
|
||||
drawContext!.reset();
|
||||
drawContext!.beginPath();
|
||||
|
||||
// 获取loading图标的尺寸和半径
|
||||
const size = getPxValue(props.size!);
|
||||
const radius = size / 2;
|
||||
const centerX = radius;
|
||||
const centerY = radius;
|
||||
|
||||
// 设置线宽
|
||||
const lineWidth = 1;
|
||||
|
||||
// 缺口角度为60度(Math.PI / 3),用于形成loading的缺口效果
|
||||
const gapAngle = Math.PI / 3; // 缺口60度
|
||||
|
||||
// 起始角度为顶部(-90度)
|
||||
const startAngle = -Math.PI / 2; // 从顶部开始
|
||||
|
||||
// 结束角度为起始角度加上300度(360-60),形成环形缺口
|
||||
const endAngle = startAngle + (2 * Math.PI - gapAngle); // 画300度
|
||||
|
||||
// 绘制圆弧,形成loading环
|
||||
drawContext!.arc(centerX, centerY, radius - lineWidth, startAngle, endAngle, false);
|
||||
|
||||
// 设置描边颜色和线宽
|
||||
drawContext!.strokeStyle = color.value;
|
||||
drawContext!.lineWidth = lineWidth;
|
||||
|
||||
// 执行描边操作
|
||||
drawContext!.stroke();
|
||||
|
||||
// 更新画布显示
|
||||
drawContext!.update();
|
||||
// #endif
|
||||
}
|
||||
|
||||
// 开始旋转动画
|
||||
async function start() {
|
||||
// #ifdef APP
|
||||
await drawLoading();
|
||||
|
||||
if (loadingRef.value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadingRef.value!.animate(
|
||||
[
|
||||
{
|
||||
transform: "rotate(0deg)"
|
||||
},
|
||||
{
|
||||
transform: "rotate(360deg)"
|
||||
}
|
||||
],
|
||||
{
|
||||
createAnimation(loadingRef.value, {
|
||||
duration: 2500,
|
||||
easing: "linear",
|
||||
iterations: 999999
|
||||
}
|
||||
);
|
||||
// #endif
|
||||
loop: -1,
|
||||
timingFunction: "linear"
|
||||
})
|
||||
.rotate("0deg", "360deg")
|
||||
.play();
|
||||
}
|
||||
|
||||
// 组件挂载后监听loading状态
|
||||
onMounted(() => {
|
||||
// #ifdef APP
|
||||
watch(
|
||||
computed(() => props.loading),
|
||||
(val: boolean) => {
|
||||
@@ -190,43 +124,17 @@ onMounted(() => {
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
computed(() => [props.color, props.size, isDark.value]),
|
||||
() => {
|
||||
drawLoading();
|
||||
}
|
||||
);
|
||||
// #endif
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cl-loading {
|
||||
@apply flex flex-row items-center justify-center rounded-full;
|
||||
|
||||
// #ifndef APP
|
||||
@apply border-surface-700 border-solid;
|
||||
// #endif
|
||||
|
||||
&--dark {
|
||||
border-color: white !important;
|
||||
border-right-color: transparent !important;
|
||||
}
|
||||
|
||||
// #ifdef H5 || MP
|
||||
&--spin {
|
||||
animation: spin 2.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
'is-loading': loading,
|
||||
'is-dark': isDark
|
||||
},
|
||||
pt.className,
|
||||
`cl-skeleton--${props.type}`,
|
||||
`${loading ? `${pt.loading?.className}` : ''}`
|
||||
`${loading ? `${pt.loading?.className}` : ''}`,
|
||||
pt.className
|
||||
]"
|
||||
:style="{
|
||||
opacity: loading ? opacity : 1
|
||||
}"
|
||||
ref="skeletonRef"
|
||||
>
|
||||
<template v-if="loading">
|
||||
<cl-icon
|
||||
@@ -34,7 +32,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch, type PropType } from "vue";
|
||||
import type { PassThroughProps } from "../../types";
|
||||
import { isDark, parsePt } from "@/cool";
|
||||
import { AnimationEngine, createAnimation, isDark, parsePt } from "@/cool";
|
||||
|
||||
defineOptions({
|
||||
name: "cl-skeleton"
|
||||
@@ -62,58 +60,34 @@ type PassThrough = {
|
||||
|
||||
const pt = computed(() => parsePt<PassThrough>(props.pt));
|
||||
|
||||
const opacity = ref(0.3);
|
||||
// 组件引用
|
||||
const skeletonRef = ref<UniElement | null>(null);
|
||||
|
||||
let animationId: number = 0;
|
||||
let startTime: number;
|
||||
// 动画实例
|
||||
let animation: AnimationEngine | null = null;
|
||||
|
||||
// 开始动画
|
||||
function start() {
|
||||
if (!props.loading) return;
|
||||
|
||||
startTime = 0;
|
||||
|
||||
function animate(currentTime: number) {
|
||||
if (startTime == 0) {
|
||||
startTime = currentTime;
|
||||
animation = createAnimation(skeletonRef.value, {
|
||||
duration: 2000,
|
||||
loop: -1,
|
||||
alternate: true
|
||||
})
|
||||
.opacity("0.3", "1")
|
||||
.play();
|
||||
}
|
||||
|
||||
// 计算动画进度 (0-1)
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = (elapsed % 2000) / 2000;
|
||||
|
||||
// 使用正弦波形创建平滑的闪动效果
|
||||
// 从0.3到1.0之间变化
|
||||
const minOpacity = 0.3;
|
||||
const maxOpacity = 1.0;
|
||||
opacity.value =
|
||||
minOpacity + (maxOpacity - minOpacity) * (Math.sin(progress * Math.PI * 2) * 0.5 + 0.5);
|
||||
|
||||
// 继续动画
|
||||
if (props.loading) {
|
||||
animationId = requestAnimationFrame((time) => {
|
||||
animate(time);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
animationId = requestAnimationFrame((time) => {
|
||||
animate(time);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止闪动动画
|
||||
*/
|
||||
// 停止动画
|
||||
function stop() {
|
||||
if (animationId != 0) {
|
||||
cancelAnimationFrame(animationId);
|
||||
animationId = 0;
|
||||
startTime = 0;
|
||||
if (animation != null) {
|
||||
animation!.stop();
|
||||
animation!.reset();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// #ifdef APP
|
||||
watch(
|
||||
computed(() => props.loading),
|
||||
(val: boolean) => {
|
||||
@@ -127,7 +101,6 @@ onMounted(() => {
|
||||
immediate: true
|
||||
}
|
||||
);
|
||||
// #endif
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -140,10 +113,6 @@ onMounted(() => {
|
||||
@apply bg-surface-600;
|
||||
}
|
||||
|
||||
// #ifdef MP | H5
|
||||
animation: shimmer-opacity 2s infinite;
|
||||
// #endif
|
||||
|
||||
&.cl-skeleton--text {
|
||||
height: 40rpx;
|
||||
width: 300rpx;
|
||||
@@ -168,19 +137,5 @@ onMounted(() => {
|
||||
width: 150rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// #ifdef MP | H5
|
||||
@keyframes shimmer-opacity {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user