Files
spring-festival-greetings/pages/avatar/index.vue
2026-01-31 20:59:51 +08:00

1014 lines
24 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="avatar-page" :style="{ paddingTop: getBavBarHeight() + 'px' }">
<view class="nav-bar">
<view class="back" @tap="goBack"></view>
<text class="nav-title-left">新春头像挂饰</text>
</view>
<view class="preview-card">
<view class="preview-square">
<image
class="avatar-img"
:src="currentAvatar?.imageUrl"
mode="aspectFill"
/>
<image
v-if="selectedFrame"
class="frame-img"
:src="selectedFrame?.imageUrl"
mode="aspectFill"
/>
<image
v-if="selectedDecor"
class="decor-img"
:src="selectedDecor?.imageUrl"
mode="aspectFit"
:style="decorStyle"
@touchstart.stop="onTouchStart"
@touchmove.stop="onTouchMove"
@touchend.stop="onTouchEnd"
/>
</view>
<view v-if="selectedDecor" class="interaction-tip">
<text>👆 单指拖动</text>
<text class="tip-divider">|</text>
<text> 双指缩放/旋转</text>
</view>
<text v-else class="preview-tip">实时预览效果</text>
</view>
<view class="action-buttons">
<button class="btn secondary" @tap="saveAndUse">保存</button>
<button class="btn primary" open-type="share">分享给朋友</button>
</view>
<view class="section">
<view class="section-header">
<text class="section-title">选择底图</text>
<view class="more-btn" @tap="openMorePopup">
<text>查看更多</text>
<text class="arrow"></text>
</view>
</view>
<scroll-view scroll-x class="avatar-scroll" show-scrollbar="false">
<view class="avatar-list">
<view class="avatar-card upload-card" @tap="useWeChatAvatar">
<view class="upload-icon">📷</view>
<text class="upload-text">微信头像</text>
</view>
<view
v-for="(item, i) in systemAvatars"
:key="i"
class="avatar-card"
:class="{ active: currentAvatar === item }"
@tap="currentAvatar = item"
>
<image
:src="item.imageUrl"
class="avatar-thumb"
mode="aspectFill"
/>
<view v-if="currentAvatar === item" class="check"></view>
</view>
</view>
</scroll-view>
</view>
<view class="tabs">
<view
class="tab"
:class="{ active: activeTab === 'frame' }"
@tap="activeTab = 'frame'"
>头像框</view
>
<view
class="tab"
:class="{ active: activeTab === 'decor' }"
@tap="activeTab = 'decor'"
>挂饰配件</view
>
</view>
<view v-if="activeTab === 'frame'" class="grid">
<view
v-for="(frame, i) in frames"
:key="i"
class="grid-item"
:class="{ active: selectedFrame === frame }"
@tap="toggleFrame(frame)"
>
<image :src="frame.imageUrl" class="grid-img" mode="aspectFill" />
<view v-if="selectedFrame === frame" class="check"></view>
</view>
</view>
<view v-else class="grid">
<view
v-for="(decor, i) in decors"
:key="i"
class="grid-item"
:class="{ active: selectedDecor === decor }"
@tap="selectedDecor = decor"
>
<image :src="decor.imageUrl" class="grid-img" mode="aspectFit" />
<view v-if="selectedDecor === decor" class="check"></view>
</view>
</view>
<canvas
type="2d"
id="avatarCanvas"
canvas-id="avatarCanvas"
class="hidden-canvas"
style="width: 600px; height: 600px"
/>
<!-- Login Popup -->
<LoginPopup ref="loginPopupRef" @logind="handleLogind" />
<!-- More Avatar Popup -->
<uni-popup ref="morePopup" type="bottom" background-color="#fff">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">选择头像</text>
<view class="close-btn" @tap="closeMorePopup"></view>
</view>
<scroll-view
scroll-y
class="popup-scroll"
@scrolltolower="loadMoreAvatars"
>
<view class="popup-grid">
<view
v-for="(item, i) in moreAvatars"
:key="i"
class="popup-item"
@tap="selectMoreAvatar(item)"
>
<image :src="item.imageUrl" class="popup-img" mode="aspectFill" />
</view>
</view>
<view v-if="loading" class="loading-text">加载中...</view>
<view v-if="!hasMore && moreAvatars.length > 0" class="no-more-text">
没有更多了
</view>
</scroll-view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, computed } from "vue";
import { onShareAppMessage, onLoad, onReachBottom } from "@dcloudio/uni-app";
import { getBavBarHeight, getDeviceInfo } from "@/utils/system";
import { useUserStore } from "@/stores/user";
import {
createShareToken,
getShareReward,
abilityCheck,
} from "@/api/system.js";
import {
getAvatarSystemList,
getAvatarFrameList,
getAvatarDecorList,
avatarCreateComplete,
} from "@/api/avatar.js";
import {
saveRecordRequest,
getShareToken,
generateObjectId,
uploadImage,
} from "@/utils/common.js";
const userStore = useUserStore();
const loginPopupRef = ref(null);
const isLoggedIn = computed(() => !!userStore.userInfo.nickName);
const systemAvatars = ref([]);
const frames = ref([]);
const decors = ref([]);
// Pagination states
const framePage = ref(1);
const frameHasNext = ref(true);
const frameLoading = ref(false);
const decorPage = ref(1);
const decorHasNext = ref(true);
const decorLoading = ref(false);
const currentAvatar = ref(null);
const selectedFrame = ref(null);
const selectedDecor = ref(null);
const activeTab = ref("frame");
// More Popup logic
const morePopup = ref(null);
const moreAvatars = ref([]);
const page = ref(1);
const hasMore = ref(true);
const loading = ref(false);
const loadFrames = async () => {
if (frameLoading.value || !frameHasNext.value) return;
frameLoading.value = true;
try {
const res = await getAvatarFrameList(framePage.value);
const list = res?.list || [];
if (list.length > 0) {
frames.value.push(
...list.map((item) => ({
id: item.id,
imageUrl: item.imageUrl,
})),
);
framePage.value++;
}
if (typeof res.hasNext !== "undefined") {
frameHasNext.value = res.hasNext;
} else {
if (list.length < 10) frameHasNext.value = false;
}
} catch (e) {
console.error(e);
} finally {
frameLoading.value = false;
}
};
const loadDecors = async () => {
if (decorLoading.value || !decorHasNext.value) return;
decorLoading.value = true;
try {
const res = await getAvatarDecorList(decorPage.value);
const list = res?.list || [];
if (list.length > 0) {
decors.value.push(
...list.map((item) => ({
id: item.id,
imageUrl: item.imageUrl,
})),
);
decorPage.value++;
}
if (typeof res.hasNext !== "undefined") {
decorHasNext.value = res.hasNext;
} else {
if (list.length < 10) decorHasNext.value = false;
}
} catch (e) {
console.error(e);
} finally {
decorLoading.value = false;
}
};
const initSystemAvatars = async () => {
try {
const res = await getAvatarSystemList(1);
const list = res?.list || [];
if (list.length > 0) {
// 取前3个展示在首页
systemAvatars.value = list
.slice(0, 3)
.map((item) => ({ id: item.id, imageUrl: item.imageUrl }));
// 默认选中第一个
if (systemAvatars.value.length > 0 && !currentAvatar.value) {
currentAvatar.value = systemAvatars.value[0];
}
}
} catch (e) {
console.error("Failed to load system avatars:", e);
}
};
onLoad((options) => {
initSystemAvatars();
loadFrames();
loadDecors();
// 处理推荐跳转参数
if (options && options.recommendId && options.type && options.imageUrl) {
const recommendItem = {
id: options.recommendId,
imageUrl: decodeURIComponent(options.imageUrl),
};
if (options.type === "frame") {
activeTab.value = "frame";
selectedFrame.value = recommendItem;
} else if (options.type === "decor") {
activeTab.value = "decor";
selectedDecor.value = recommendItem;
} else if (options.type === "avatar") {
currentAvatar.value = recommendItem;
}
}
});
onReachBottom(() => {
if (activeTab.value === "frame") {
loadFrames();
} else if (activeTab.value === "decor") {
loadDecors();
}
});
const openMorePopup = () => {
morePopup.value.open();
if (moreAvatars.value.length === 0) {
// 重新加载第一页,因为 systemAvatars 只取了前3个
// 这里我们简单处理:重新请求第一页作为更多列表的开始
// 或者你可以复用 systemAvatars 的数据,然后 page 从 2 开始请求
// 为了逻辑简单和数据一致性,这里选择重新请求第一页填充更多列表
loadMoreAvatars();
}
};
const closeMorePopup = () => {
morePopup.value.close();
};
const loadMoreAvatars = async () => {
if (loading.value || !hasMore.value) return;
loading.value = true;
try {
const res = await getAvatarSystemList(page.value);
const list = res?.list || [];
if (list.length > 0) {
const newAvatars = list.map((item) => ({
id: item.id,
imageUrl: item.imageUrl,
}));
moreAvatars.value.push(...newAvatars);
page.value++;
}
// 根据接口返回的 hasNext 字段判断是否还有更多数据
// 如果接口没有返回 hasNext则降级使用列表长度判断假设每页10条
if (typeof res.hasNext !== "undefined") {
hasMore.value = res.hasNext;
} else {
if (list.length < 10) {
hasMore.value = false;
}
}
} catch (e) {
console.error(e);
} finally {
loading.value = false;
}
};
const createAvatarId = () => {
const id = generateObjectId();
// avatarCreateComplete({ id });
return id;
};
const selectMoreAvatar = (item) => {
currentAvatar.value = item;
closeMorePopup();
};
const toggleFrame = (frame) => {
if (selectedFrame.value === frame) {
selectedFrame.value = null;
} else {
selectedFrame.value = frame;
}
};
// 挂饰状态
const decorState = ref({
x: 300, // 初始中心 X (rpx)
y: 80, // 初始中心 Y (rpx)
scale: 1,
rotate: 0,
});
const decorStyle = computed(() => {
return {
transform: `translate(${decorState.value.x - 120}rpx, ${
decorState.value.y - 120
}rpx) rotate(${decorState.value.rotate}deg) scale(${
decorState.value.scale
})`,
};
});
// 触摸状态
let startTouches = [];
let initialDecorState = {};
const getDistance = (p1, p2) => {
const x = p1.clientX - p2.clientX;
const y = p1.clientY - p2.clientY;
return Math.sqrt(x * x + y * y);
};
const getAngle = (p1, p2) => {
const x = p1.clientX - p2.clientX;
const y = p1.clientY - p2.clientY;
return (Math.atan2(y, x) * 180) / Math.PI;
};
const onTouchStart = (e) => {
startTouches = e.touches;
initialDecorState = { ...decorState.value };
};
const onTouchMove = (e) => {
if (e.touches.length === 1 && startTouches.length === 1) {
// 单指移动
const dx = e.touches[0].clientX - startTouches[0].clientX;
const dy = e.touches[0].clientY - startTouches[0].clientY;
// px 转 rpx
const systemInfo = uni.getSystemInfoSync();
const ratio = 750 / systemInfo.windowWidth;
decorState.value.x = initialDecorState.x + dx * ratio;
decorState.value.y = initialDecorState.y + dy * ratio;
} else if (e.touches.length === 2 && startTouches.length === 2) {
// 双指缩放/旋转
const p1 = e.touches[0];
const p2 = e.touches[1];
const startP1 = startTouches[0];
const startP2 = startTouches[1];
const currentDist = getDistance(p1, p2);
const startDist = getDistance(startP1, startP2);
const currentAngle = getAngle(p1, p2);
const startAngle = getAngle(startP1, startP2);
decorState.value.scale =
initialDecorState.scale * (currentDist / startDist);
decorState.value.rotate =
initialDecorState.rotate + (currentAngle - startAngle);
}
};
const onTouchEnd = () => {
// 可以在这里做边界检查等
};
const handleLogind = async () => {
// Logic after successful login if needed
};
const useWeChatAvatar = () => {
if (!isLoggedIn.value) {
loginPopupRef.value.open();
} else {
currentAvatar.value = {
id: "wechat_" + Date.now(),
imageUrl: userStore.userInfo.avatarUrl,
};
}
};
const goBack = () => {
uni.navigateBack();
};
const saveByCanvas = async (save = true) => {
return new Promise((resolve, reject) => {
const query = uni.createSelectorQuery();
query
.select("#avatarCanvas")
.fields({ node: true, size: true })
.exec(async (res) => {
if (!res[0] || !res[0].node) {
reject("Canvas not found");
return;
}
const canvas = res[0].node;
const ctx = canvas.getContext("2d");
const loadCanvasImage = (url) => {
return new Promise((resolve, reject) => {
const img = canvas.createImage();
img.onload = () => resolve(img);
img.onerror = (e) => reject(e);
img.src = url;
});
};
// 初始化画布尺寸
const size = 600;
canvas.width = size;
canvas.height = size;
const avatarPath = await loadCanvasImage(currentAvatar.value.imageUrl);
ctx.clearRect(0, 0, size, size);
ctx.drawImage(avatarPath, 0, 0, size, size);
if (selectedFrame.value) {
const framePath = await loadCanvasImage(selectedFrame.value.imageUrl);
ctx.drawImage(framePath, 0, 0, size, size);
}
if (selectedDecor.value) {
const decorPath = await loadCanvasImage(selectedDecor.value.imageUrl);
ctx.save();
// 映射 rpx 坐标到 Canvas 坐标 (假设 1rpx = 1 unit for 600x600 canvas logic)
// Canvas size is 600, Preview is 600rpx. Ratio is 1:1 in logical space.
ctx.translate(decorState.value.x, decorState.value.y);
ctx.rotate((decorState.value.rotate * Math.PI) / 180);
const scale = decorState.value.scale;
// 绘制图片,宽高 240
ctx.drawImage(
decorPath,
-120 * scale,
-120 * scale,
240 * scale,
240 * scale,
);
ctx.restore();
}
uni.canvasToTempFilePath({
canvas: canvas, // Canvas 2D 必须传 canvas 实例
width: 600,
height: 600,
destWidth: 600,
destHeight: 600,
success: (res) => {
if (save) saveImage(res.tempFilePath);
resolve(res.tempFilePath);
},
fail: (err) => reject(err),
});
// ctx.draw(false, () => {
// uni.canvasToTempFilePath({
// canvasId: "avatarCanvas",
// success: (res) => {
// uni.saveImageToPhotosAlbum({
// filePath: res.tempFilePath,
// success: () => {
// uni.showToast({ title: "已保存到相册", icon: "success" });
// },
// });
// },
// });
// });
});
});
};
const saveImage = (path) => {
uni.saveImageToPhotosAlbum({
filePath: path,
success() {
uni.showToast({ title: "已保存到相册" });
},
fail() {
uni.showModal({
title: "提示",
content: "请授权保存到相册",
});
},
});
};
const saveAndUse = async () => {
if (!isLoggedIn.value) {
loginPopupRef.value.open();
return;
}
const abilityRes = await abilityCheck("avatar_download");
if (!abilityRes.canUse) {
if (
abilityRes?.blockType === "need_share" &&
abilityRes?.message === "分享可继续"
) {
uni.showToast({
title: "分享到群聊可继续使用",
icon: "none",
});
return;
}
uni.showToast({
title: "您今日头像下载次数已用完,明日再试",
icon: "none",
});
return;
}
const tempPath = await saveByCanvas(true);
const id = createAvatarId();
saveRecordRequest(tempPath, id, "avatar_download");
completeCardInfo(id);
return;
// 调用avatarDownloadRecord API记录下载次数
// await avatarDownloadRecord({
// avatarUrl: currentAvatar.value,
// });
// saveRecordRequest('', )
// const ctx = uni.createCanvasContext("avatarCanvas");
// const size = 600;
// const avatarPath = await loadImage(currentAvatar.value);
// ctx.clearRect(0, 0, size, size);
// ctx.drawImage(avatarPath, 0, 0, size, size);
// if (selectedFrame.value) {
// const framePath = await loadImage(selectedFrame.value);
// ctx.drawImage(framePath, 0, 0, size, size);
// }
// if (selectedDecor.value) {
// const decorPath = await loadImage(selectedDecor.value);
// ctx.save();
// // 映射 rpx 坐标到 Canvas 坐标 (假设 1rpx = 1 unit for 600x600 canvas logic)
// // Canvas size is 600, Preview is 600rpx. Ratio is 1:1 in logical space.
// ctx.translate(decorState.value.x, decorState.value.y);
// ctx.rotate((decorState.value.rotate * Math.PI) / 180);
// const scale = decorState.value.scale;
// // 绘制图片,宽高 240
// ctx.drawImage(
// decorPath,
// -120 * scale,
// -120 * scale,
// 240 * scale,
// 240 * scale,
// );
// ctx.restore();
// }
// ctx.draw(false, () => {
// uni.canvasToTempFilePath({
// canvasId: "avatarCanvas",
// success: (res) => {
// uni.saveImageToPhotosAlbum({
// filePath: res.tempFilePath,
// success: () => {
// uni.showToast({ title: "已保存到相册", icon: "success" });
// },
// });
// },
// });
// });
};
const completeCardInfo = async (id) => {
const tempPath = await saveByCanvas(false);
const imageUrl = await uploadImage(tempPath);
avatarCreateComplete({
id,
imageUrl,
avatarId: currentAvatar?.value?.id,
decorId: selectedDecor?.value?.id,
frameId: selectedFrame?.value?.id,
});
};
onShareAppMessage(async () => {
getShareReward({ scene: "avatar_download" });
if (!isLoggedIn.value) {
const shareTokenRes = await getShareToken("avatar_download_not_login", "");
return {
title: "新春祝福",
path: `/pages/index/index?shareToken=${shareTokenRes.shareToken}`,
};
}
const id = createAvatarId();
// const shareTokenRes = {
// shareToken: "iFmK8WjRm6TK",
// };
const shareTokenRes = await getShareToken("avatar_download", id);
completeCardInfo(id);
return {
title: "制作我的新春头像",
path: `/pages/avatar/detail?shareToken=${shareTokenRes.shareToken}`,
imageUrl:
"https://file.lihailezzc.com/resource/b48c41054c2633c478463ac1b1f1ca23.png", // 使用默认封面或 popularCards 的封面
};
});
// onShareTimeline(() => {
// return {
// title: "制作我的新春头像",
// imageUrl:
// "https://file.lihailezzc.com/resource/b48c41054c2633c478463ac1b1f1ca23.png",
// };
// });
const loadImage = (url) => {
return new Promise((resolve, reject) => {
uni.getImageInfo({
src: url,
success: (res) => resolve(res.path),
fail: reject,
});
});
};
</script>
<style lang="scss" scoped>
.avatar-page {
min-height: 100vh;
background: #fff;
box-sizing: border-box;
padding-bottom: env(safe-area-inset-bottom);
}
.nav-bar {
display: flex;
align-items: center;
padding: 16rpx 24rpx;
}
.back {
font-size: 40rpx;
margin-right: 12rpx;
}
.nav-title-left {
font-size: 32rpx;
font-weight: 600;
}
.preview-card {
margin: 20rpx 24rpx;
background: #fff;
border-radius: 24rpx;
padding: 24rpx;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.06);
}
.preview-square {
width: 600rpx;
height: 600rpx;
margin: 0 auto;
border-radius: 24rpx;
background: #f5dfc9;
position: relative;
overflow: hidden;
}
.avatar-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.frame-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.decor-img {
position: absolute;
top: 0;
left: 0;
width: 240rpx;
height: 240rpx;
/* 移除原有的 transform 和 center positioning改为由 inline style 控制 */
}
.preview-tip {
display: block;
text-align: center;
color: #999;
font-size: 22rpx;
margin-top: 12rpx;
}
.interaction-tip {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
margin-top: 20rpx;
font-size: 24rpx;
color: #ff3b30;
background: #fff0f0;
padding: 12rpx 32rpx;
border-radius: 999rpx;
width: fit-content;
margin-left: auto;
margin-right: auto;
}
.tip-divider {
color: #ffccc7;
margin: 0 4rpx;
}
.action-buttons {
display: flex;
padding: 0 48rpx;
gap: 32rpx;
margin-top: 32rpx;
}
.btn {
flex: 1;
height: 88rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 600;
margin: 0;
padding: 0;
border: none;
line-height: normal;
}
.btn::after {
border: none;
}
.section {
margin-top: 24rpx;
padding: 0 24rpx;
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.section-title {
font-size: 26rpx;
font-weight: 600;
}
.avatar-scroll {
width: 100%;
}
.avatar-list {
display: flex;
}
.avatar-card {
width: 160rpx;
height: 160rpx;
border-radius: 16rpx;
overflow: hidden;
margin-right: 16rpx;
position: relative;
background: #fff;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);
}
.avatar-card.active {
outline: 4rpx solid #ff3b30;
}
.upload-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #fff5f5;
border: 2rpx dashed #ffccc7;
box-sizing: border-box;
}
.upload-icon {
font-size: 48rpx;
margin-bottom: 8rpx;
}
.upload-text {
font-size: 22rpx;
color: #ff3b30;
font-weight: 500;
}
.avatar-thumb {
width: 100%;
height: 100%;
}
.check {
position: absolute;
right: 8rpx;
top: 8rpx;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background: #ff3b30;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
}
.tabs {
display: flex;
padding: 16rpx 24rpx 8rpx;
gap: 12rpx;
}
.tab {
flex: 1;
text-align: center;
background: #f7f7f7;
border-radius: 999rpx;
height: 64rpx;
line-height: 64rpx;
font-size: 26rpx;
color: #666;
}
.tab.active {
background: #fff0f0;
color: #ff3b30;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16rpx;
padding: 8rpx 24rpx 24rpx;
}
.grid-item {
height: 200rpx;
border-radius: 16rpx;
background: #fff;
overflow: hidden;
position: relative;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);
}
.grid-item.active {
outline: 4rpx solid #ff3b30;
}
.grid-img {
width: 100%;
height: 100%;
}
.btn.primary {
background: #ff3b30;
color: #fff;
box-shadow: 0 12rpx 24rpx rgba(255, 59, 48, 0.35);
}
.btn.secondary {
background: #f5f5f5;
color: #333;
}
.hidden-canvas {
position: fixed;
left: -9999px;
top: -9999px;
}
/* More Popup Styles */
.more-btn {
margin-left: auto;
font-size: 24rpx;
color: #999;
display: flex;
align-items: center;
}
.arrow {
font-size: 32rpx;
margin-left: 4rpx;
line-height: 1;
position: relative;
top: -2rpx;
}
.popup-content {
background: #fff;
border-radius: 24rpx 24rpx 0 0;
height: 60vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.popup-header {
padding: 32rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
border-bottom: 2rpx solid #f5f5f5;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
}
.close-btn {
position: absolute;
right: 32rpx;
top: 50%;
transform: translateY(-50%);
font-size: 32rpx;
color: #999;
padding: 10rpx;
}
.popup-scroll {
flex: 1;
height: 0;
}
.popup-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
padding: 24rpx;
}
.popup-item {
aspect-ratio: 1;
border-radius: 16rpx;
overflow: hidden;
background: #f5f5f5;
}
.popup-img {
width: 100%;
height: 100%;
}
.loading-text,
.no-more-text {
text-align: center;
padding: 24rpx;
color: #999;
font-size: 24rpx;
}
</style>