692 lines
22 KiB
Plaintext
692 lines
22 KiB
Plaintext
<template>
|
||
<view class="container page-container relative border-b">
|
||
<!-- 导航栏 -->
|
||
<custom-nav-bar :title="title">
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<template v-slot:left>
|
||
<text class="iconfont icon-Search font-48" @click="handleSearch"></text>
|
||
</template>
|
||
<!-- #endif -->
|
||
<!-- #ifndef MP-WEIXIN -->
|
||
<template v-slot:right>
|
||
<text class="iconfont icon-Search font-48" @click="handleSearch"></text>
|
||
</template>
|
||
<!-- #endif -->
|
||
</custom-nav-bar>
|
||
|
||
<!-- 分类导航 -->
|
||
<view class="category-nav border-b">
|
||
<scroll-view
|
||
class="category-scroll"
|
||
direction="horizontal"
|
||
:scroll-with-animation="true"
|
||
:scroll-into-view="scrollIntoView"
|
||
:show-scrollbar="false"
|
||
>
|
||
<view
|
||
:id="category.key"
|
||
v-for="(category, index) in categories"
|
||
:key="category.key"
|
||
:class="`category-item ${activeCategory === category.key ? 'active' : ''}`"
|
||
@click="handleCategoryClick(category.key, index)"
|
||
>
|
||
<text class="category-text">{{
|
||
translateText(category.label)
|
||
}}</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 滚动部分 -->
|
||
<view class="scroll-section">
|
||
<swiper
|
||
class="h-full"
|
||
:current="activeSwiperIndex"
|
||
@change="handleSwiperChange"
|
||
:indicator-dots="false"
|
||
:circular="false"
|
||
:duration="300"
|
||
easing-function="easeOut"
|
||
>
|
||
<template v-for="(category, index) in categories" :key="category.key">
|
||
<swiper-item>
|
||
<!-- 只渲染当前页和相邻页,减少渲染压力 -->
|
||
<view v-if="shouldRenderCategory(index)" class="h-full">
|
||
<scroll-view
|
||
class="h-full"
|
||
@scrolltolower="handleLoadMore"
|
||
:refresher-enabled="true"
|
||
:refresher-triggered="refreshingMap[category.key] || false"
|
||
@refresherrefresh="handleRefresh"
|
||
:show-scrollbar="false"
|
||
scroll-with-animation
|
||
>
|
||
<template v-if="(agentListMap[category.key] || []).length">
|
||
<template
|
||
v-for="info in agentListMap[category.key]"
|
||
:key="info.id"
|
||
>
|
||
<template v-if="agentType === 'PageApp'">
|
||
<page-card
|
||
:coverImg="info.coverImg"
|
||
:name="info.name"
|
||
:avatar="info.publishUser?.avatar"
|
||
:userName="
|
||
info.publishUser?.nickName ||
|
||
info.publishUser?.userName
|
||
"
|
||
:created="info.created"
|
||
:userCount="info.statistics?.userCount"
|
||
:collectCount="info.statistics?.collectCount"
|
||
:collect="info.collect"
|
||
@click="handleAgentClick(info)"
|
||
@collect="handleToggleCollect(info, category.key)"
|
||
/>
|
||
</template>
|
||
<template v-else>
|
||
<agent-component
|
||
:icon="info.icon"
|
||
:name="info.name"
|
||
:avatar="info.publishUser?.avatar"
|
||
:userName="
|
||
info.publishUser?.nickName ||
|
||
info.publishUser?.userName
|
||
"
|
||
:description="info.description"
|
||
:userCount="info.statistics?.userCount"
|
||
:convCount="info.statistics?.convCount"
|
||
:collectCount="info.statistics?.collectCount"
|
||
:collect="info.collect"
|
||
:paymentRequired="info.paymentRequired && enableSubscription === 1"
|
||
:subscribed="info.subscribed"
|
||
@click="handleAgentClick(info)"
|
||
@collect="handleToggleCollect(info, category.key)"
|
||
/>
|
||
</template>
|
||
</template>
|
||
</template>
|
||
|
||
<!-- loading状态或刷新状态时隐藏空状态 -->
|
||
<template
|
||
v-else-if="
|
||
!loadingMap[category.key] && !refreshingMap[category.key]
|
||
"
|
||
>
|
||
<view class="empty-state">
|
||
<image
|
||
:src="noData"
|
||
class="empty-image"
|
||
:alt="t('Mobile.Common.noDataImageAlt')"
|
||
/>
|
||
<text class="empty-text">{{
|
||
t("Mobile.Common.noData")
|
||
}}</text>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 刷新状态时隐藏加载状态 -->
|
||
<template v-else-if="!refreshingMap[category.key]">
|
||
<view class="loading-state">
|
||
<text class="loading-text">{{
|
||
t("Mobile.Page.loadingMore")
|
||
}}</text>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 加载状态时隐藏没有更多数据状态 -->
|
||
<template
|
||
v-if="
|
||
!loadingMap[category.key] &&
|
||
!hasMoreMap[category.key] &&
|
||
(agentListMap[category.key] || []).length > 6
|
||
"
|
||
>
|
||
<view class="no-more-state">
|
||
<text class="no-more-text">{{
|
||
t("Mobile.Common.noMoreData")
|
||
}}</text>
|
||
</view>
|
||
</template>
|
||
</scroll-view>
|
||
</view>
|
||
<!-- 未加载的页面显示占位 -->
|
||
<view v-else class="h-full placeholder-view"></view>
|
||
</swiper-item>
|
||
</template>
|
||
</swiper>
|
||
</view>
|
||
|
||
<!-- 登录弹窗 -->
|
||
<auth-login-popup ref="loginPopupRef" />
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import { CONST_ALL } from "@/constants/common.constants";
|
||
import { SquarePublishedItemInfo } from "@/types/interfaces/square";
|
||
import { apiPublishedCategoryList } from "@/servers/square";
|
||
import { SUCCESS_CODE } from "@/constants/codes.constants";
|
||
import { SquareCategoryInfo } from "@/types/interfaces/square";
|
||
import { SquareAgentTypeEnum } from "@/types/enums/square";
|
||
import { AgentComponentTypeEnum } from "@/types/enums/agent";
|
||
import { apiPublishedAgentList } from "@/servers/square";
|
||
import { SquarePublishedListParams } from "@/types/interfaces/square";
|
||
import { Page } from "@/types/interfaces/request";
|
||
import { RequestResponse } from "@/types/interfaces/request";
|
||
import AgentComponent from "@/components/agent-component/agent-component.uvue";
|
||
import PageCard from "@/components/page-card/page-card.uvue";
|
||
import noData from "@/static/assets/no_data.png";
|
||
import { TENANT_CONFIG_INFO } from "@/constants/home.constants";
|
||
import { apiTenantConfig } from "@/servers/account";
|
||
import { getCurrentPagePath } from "@/utils/commonBusiness";
|
||
import CustomNavBar from "@/components/custom-nav-bar/custom-nav-bar.uvue";
|
||
import { apiUnCollectAgent, apiCollectAgent } from "@/servers/agentDev";
|
||
import { USER_NO_LOGIN, REDIRECT_LOGIN } from "@/constants/codes.constants";
|
||
import { useAuthInterceptor } from "@/hooks/useAuthInterceptor";
|
||
import AuthLoginPopup from "@/components/auth-login-popup/auth-login-popup.uvue";
|
||
import { useI18n, translateText } from "@/utils/i18n";
|
||
|
||
const { t } = useI18n();
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
agentType: "ChatBot" | "PageApp";
|
||
title: string;
|
||
}>(),
|
||
{
|
||
agentType: "ChatBot",
|
||
title: "Mobile.Nav.agent",
|
||
},
|
||
);
|
||
|
||
// 使用登录拦截 composable
|
||
const {
|
||
loginPopupRef,
|
||
handleAgentClick: baseHandleAgentClick,
|
||
checkAuthAndShowPopup,
|
||
} = useAuthInterceptor();
|
||
|
||
// 当前分类
|
||
const activeCategory = ref<string>(CONST_ALL);
|
||
// 当前swiper索引
|
||
const activeSwiperIndex = ref<number>(0);
|
||
// 分类列表
|
||
const categories = ref<{ key: string; label: string; active: boolean }[]>([]);
|
||
// 智能体列表
|
||
const agentListMap = ref<{ [key: string]: SquarePublishedItemInfo[] }>({});
|
||
// 加载状态
|
||
const loadingMap = ref<{ [key: string]: boolean }>({});
|
||
// 刷新状态
|
||
const refreshingMap = ref<{ [key: string]: boolean }>({});
|
||
// 页码
|
||
const pageMap = ref<{ [key: string]: number }>({});
|
||
// 是否还有更多
|
||
const hasMoreMap = ref<{ [key: string]: boolean }>({});
|
||
// 搜索关键词
|
||
const searchKeyword = ref<string>("");
|
||
// 滚动到对应的分类标签
|
||
const scrollIntoView = ref<string>("");
|
||
// 搜索定时器
|
||
const searchTimeoutRef = ref<number | null>(null);
|
||
// 租户信息
|
||
const tenantConfigInfo = ref<TenantConfigInfo>(null);
|
||
|
||
// 租户订阅限制强受控标志
|
||
const enableSubscription = computed((): number => {
|
||
return tenantConfigInfo.value?.enableSubscription ?? 0;
|
||
});
|
||
// 已渲染的分类索引集合(用于懒加载优化)
|
||
const renderedCategoryIndexes = ref<Set<number>>(new Set([0]));
|
||
|
||
// // 新增标志变量,用于区分首次加载
|
||
// const isFirstLoad = ref<boolean>(true)
|
||
|
||
/**
|
||
* 判断是否应该渲染该分类
|
||
* 只渲染当前页和相邻的前后各1页,减少DOM节点数量
|
||
*/
|
||
const shouldRenderCategory = (index: number): boolean => {
|
||
const currentIndex = activeSwiperIndex.value;
|
||
// 渲染当前页、前一页、后一页
|
||
const shouldRender = Math.abs(index - currentIndex) <= 1;
|
||
if (shouldRender) {
|
||
renderedCategoryIndexes.value.add(index);
|
||
}
|
||
// 已经渲染过的页面保持渲染状态,避免频繁创建销毁
|
||
return renderedCategoryIndexes.value.has(index);
|
||
};
|
||
|
||
// 获取用户配置
|
||
const fetchTenantConfig = async () => {
|
||
const { code, data } = await apiTenantConfig();
|
||
if (code === SUCCESS_CODE) {
|
||
tenantConfigInfo.value = data;
|
||
uni.setNavigationBarTitle({
|
||
title: data?.siteName,
|
||
});
|
||
// 缓存租户信息
|
||
uni.setStorageSync(TENANT_CONFIG_INFO, JSON.stringify(data));
|
||
}
|
||
};
|
||
|
||
// 获取分类列表
|
||
const fetchCategories = async () => {
|
||
const response = await apiPublishedCategoryList();
|
||
const { code, data } = response as unknown as RequestResponse<
|
||
SquareCategoryInfo[]
|
||
>;
|
||
if (code === SUCCESS_CODE) {
|
||
// 过滤掉游戏分类 - 微信小程序(属平台未允许内容)违反《微信小程序平台运营规范常见拒绝情形3.4》
|
||
//#ifdef MP-WEIXIN
|
||
data?.forEach((item) => {
|
||
if (item.type === "PageApp") {
|
||
item.children = item?.children.filter((child) => {
|
||
return child.key !== "Game";
|
||
});
|
||
}
|
||
});
|
||
//#endif
|
||
// 智能体需要单独处理
|
||
let filterType = props.agentType;
|
||
if (props.agentType === "ChatBot") {
|
||
filterType = SquareAgentTypeEnum.Agent;
|
||
}
|
||
|
||
// 获取智能体分类列表
|
||
const agentCategoryList =
|
||
data.find((item: SquareCategoryInfo) => item.type === filterType)
|
||
?.children || [];
|
||
// 转换为分类列表
|
||
const categoryData = agentCategoryList.map(
|
||
(item: SquareCategoryInfo) => ({
|
||
key: item.key,
|
||
label: item.label,
|
||
active: false,
|
||
}),
|
||
);
|
||
categories.value = [
|
||
{ key: CONST_ALL, label: "Mobile.Common.all", active: true },
|
||
...categoryData,
|
||
];
|
||
} else if (code !== USER_NO_LOGIN && code !== REDIRECT_LOGIN) {
|
||
uni.showToast({
|
||
title: t("Mobile.AgentList.fetchCategoryFailed"),
|
||
icon: "error",
|
||
});
|
||
}
|
||
};
|
||
|
||
// 获取智能体列表
|
||
const fetchAgentList = async (
|
||
category: string = CONST_ALL,
|
||
pageNum: number = 1,
|
||
keyword: string = searchKeyword.value,
|
||
) => {
|
||
if (loadingMap.value[category]) return;
|
||
// 设置加载状态 - 使用直接赋值而非解构,减少响应式更新
|
||
loadingMap.value[category] = true;
|
||
const params: SquarePublishedListParams = {
|
||
targetType: AgentComponentTypeEnum.Agent,
|
||
targetSubType: props.agentType,
|
||
page: pageNum,
|
||
pageSize: 10,
|
||
category: category === CONST_ALL ? "" : category,
|
||
kw: keyword,
|
||
};
|
||
// 获取智能体列表
|
||
const response = await apiPublishedAgentList(params);
|
||
const { code, data, message } = response as unknown as RequestResponse<
|
||
Page<SquarePublishedItemInfo>
|
||
>;
|
||
// 成功
|
||
if (code === SUCCESS_CODE) {
|
||
// 过滤掉游戏分类 - 微信小程序(属平台未允许内容)违反《微信小程序平台运营规范常见拒绝情形3.4》
|
||
//#ifdef MP-WEIXIN
|
||
data.records = data.records?.filter((item) => {
|
||
return item.category !== "Game";
|
||
});
|
||
//#endif
|
||
const newList = data.records || [];
|
||
if (pageNum > 1) {
|
||
// 追加数据
|
||
const existingList = agentListMap.value[category] || [];
|
||
agentListMap.value[category] = [...existingList, ...newList];
|
||
} else {
|
||
// 替换数据
|
||
agentListMap.value[category] = newList;
|
||
}
|
||
hasMoreMap.value[category] = data.pages > pageNum;
|
||
pageMap.value[category] = pageNum;
|
||
} else if (code !== USER_NO_LOGIN && code !== REDIRECT_LOGIN) {
|
||
uni.showToast({
|
||
title: message || t("Mobile.AgentList.fetchAgentFailed"),
|
||
icon: "error",
|
||
});
|
||
}
|
||
loadingMap.value[category] = false;
|
||
};
|
||
|
||
const initData = async () => {
|
||
// 获取分类列表
|
||
fetchCategories();
|
||
// 1. 首先尝试从本地缓存读取,以实现秒开级的初始渲染,防止 siteName 等标题闪烁
|
||
const tenantConfigInfoString = await uni.getStorageSync(TENANT_CONFIG_INFO);
|
||
if (tenantConfigInfoString) {
|
||
tenantConfigInfo.value = JSON.parse(tenantConfigInfoString);
|
||
uni.setNavigationBarTitle({
|
||
title: tenantConfigInfo.value?.siteName,
|
||
});
|
||
}
|
||
// 2. 然后必须发起强同步网络请求更新最新状态,防止缓存过期导致计费状态显示不准确
|
||
await fetchTenantConfig();
|
||
};
|
||
|
||
onPageShow(() => {
|
||
// isFirstLoad.value = true
|
||
initData();
|
||
// 预加载第一个分类的数据
|
||
fetchAgentList();
|
||
});
|
||
|
||
// onTabItemTap(async () => {
|
||
// if (isFirstLoad.value) {
|
||
// isFirstLoad.value = false
|
||
// return
|
||
// }
|
||
// // 预加载第一个分类的数据
|
||
// fetchAgentList(activeCategory.value)
|
||
// })
|
||
|
||
onUnload(() => {
|
||
if (searchTimeoutRef.value) {
|
||
clearTimeout(searchTimeoutRef.value);
|
||
}
|
||
});
|
||
|
||
// 点击智能体分类标签
|
||
const handleCategoryClick = (key: string, index: number) => {
|
||
// 当前分类
|
||
activeCategory.value = key;
|
||
// 当前swiper索引
|
||
activeSwiperIndex.value = index;
|
||
// 滚动到对应的分类标签
|
||
scrollIntoView.value = key;
|
||
|
||
// 如果该分类还没有数据,则加载数据
|
||
if (!agentListMap.value[key] || agentListMap.value[key].length === 0) {
|
||
pageMap.value[key] = 1;
|
||
hasMoreMap.value[key] = true;
|
||
fetchAgentList(key);
|
||
}
|
||
};
|
||
|
||
// 处理Swiper切换
|
||
const handleSwiperChange = (e: UniSwiperChangeEvent) => {
|
||
const index = e.detail.current;
|
||
const category = categories.value[index];
|
||
if (category && category.key !== activeCategory.value) {
|
||
// 当前分类
|
||
activeCategory.value = category.key;
|
||
// 当前swiper索引
|
||
activeSwiperIndex.value = index;
|
||
|
||
// 滚动到对应的分类标签
|
||
const id = categories.value[index].key;
|
||
scrollIntoView.value = id;
|
||
|
||
// 如果该分类还没有数据,则加载数据
|
||
if (
|
||
!agentListMap.value[category.key] ||
|
||
agentListMap.value[category.key].length === 0
|
||
) {
|
||
pageMap.value[category.key] = 1;
|
||
hasMoreMap.value[category.key] = true;
|
||
fetchAgentList(category.key);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 处理搜索
|
||
const handleSearch = async () => {
|
||
// 检查登录状态,如果未登录则显示登录弹窗
|
||
const isLoggedIn = await checkAuthAndShowPopup();
|
||
if (!isLoggedIn) {
|
||
return;
|
||
}
|
||
const currentUrl = getCurrentPagePath();
|
||
uni.navigateTo({
|
||
url:
|
||
`/subpackages/pages/agent-search/agent-search?type=${props.agentType}&backUrl=` +
|
||
encodeURIComponent(currentUrl),
|
||
});
|
||
};
|
||
|
||
// 跳转到智能体详情页
|
||
const handleAgentClick = (agentInfo: SquarePublishedItemInfo) => {
|
||
// 使用登录拦截逻辑处理点击
|
||
baseHandleAgentClick({
|
||
targetId: agentInfo.targetId,
|
||
name: agentInfo.name,
|
||
agentType: agentInfo.agentType,
|
||
icon: agentInfo.icon,
|
||
description: agentInfo.description,
|
||
});
|
||
};
|
||
|
||
// 处理收藏与取消收藏
|
||
const handleToggleCollect = async (
|
||
agentInfo: SquarePublishedItemInfo,
|
||
category: string,
|
||
) => {
|
||
const { targetId, collect } = agentInfo;
|
||
// 获取当前分类列表
|
||
const existingList = agentListMap.value[category] || [];
|
||
// 更新列表收藏状态,并更新统计信息
|
||
const updatedList = existingList.map((item: SquarePublishedItemInfo) => {
|
||
if (item.targetId === targetId) {
|
||
const collectCount = item.statistics?.collectCount || 0;
|
||
const newCollectCount = collect ? collectCount - 1 : collectCount + 1;
|
||
return {
|
||
...item,
|
||
collect: !collect,
|
||
statistics: {
|
||
...item.statistics,
|
||
collectCount: newCollectCount,
|
||
},
|
||
};
|
||
}
|
||
return item;
|
||
});
|
||
agentListMap.value[category] = updatedList;
|
||
// 更新数据库收藏状态
|
||
if (collect) {
|
||
const { code } = await apiUnCollectAgent(targetId);
|
||
if (code !== SUCCESS_CODE) {
|
||
agentListMap.value[category] = existingList;
|
||
uni.showToast({
|
||
title: t("Mobile.AgentList.unCollectFailed"),
|
||
icon: "none",
|
||
});
|
||
}
|
||
} else {
|
||
const { code } = await apiCollectAgent(targetId);
|
||
if (code !== SUCCESS_CODE) {
|
||
agentListMap.value[category] = existingList;
|
||
uni.showToast({
|
||
title: t("Mobile.AgentList.collectFailed"),
|
||
icon: "none",
|
||
});
|
||
}
|
||
}
|
||
};
|
||
|
||
// 下拉刷新
|
||
const handleRefresh = () => {
|
||
const currentCategory = activeCategory.value;
|
||
if (refreshingMap.value[currentCategory]) {
|
||
return;
|
||
}
|
||
refreshingMap.value[currentCategory] = true;
|
||
pageMap.value[currentCategory] = 1;
|
||
hasMoreMap.value[currentCategory] = true;
|
||
fetchAgentList(currentCategory).finally(() => {
|
||
// 添加延迟,让loading效果更明显
|
||
setTimeout(() => {
|
||
refreshingMap.value[currentCategory] = false;
|
||
}, 500);
|
||
});
|
||
};
|
||
|
||
// 加载更多
|
||
const handleLoadMore = () => {
|
||
const currentPage = pageMap.value[activeCategory.value] || 1;
|
||
const currentHasMore = hasMoreMap.value[activeCategory.value] !== false;
|
||
const currentLoading = loadingMap.value[activeCategory.value];
|
||
|
||
if (currentHasMore && !currentLoading) {
|
||
fetchAgentList(activeCategory.value, currentPage + 1);
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
background-color: #fff;
|
||
/* #ifdef MP-WEIXIN */
|
||
// padding-top: env(safe-area-inset-top);
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
/* #endif */
|
||
|
||
.icon-Search {
|
||
margin-left: 12rpx;
|
||
}
|
||
|
||
.category-nav {
|
||
padding: 0 32rpx 16rpx 32rpx;
|
||
|
||
/* 分类导航 */
|
||
.category-scroll {
|
||
scroll-behavior: smooth;
|
||
-webkit-overflow-scrolling: touch;
|
||
height: 80rpx;
|
||
display: flex;
|
||
// align-items: center;
|
||
flex-direction: row;
|
||
}
|
||
|
||
.category-item {
|
||
display: flex;
|
||
justify-content: center;
|
||
height: 80rpx;
|
||
padding: 0 32rpx;
|
||
border-radius: 16rpx;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
user-select: none;
|
||
color: #5e6470;
|
||
font-size: 32rpx;
|
||
font-weight: 400;
|
||
|
||
&.active {
|
||
background-color: #f2f2ff;
|
||
|
||
.category-text {
|
||
color: #5147ff;
|
||
}
|
||
}
|
||
|
||
.category-text {
|
||
font-size: 32rpx;
|
||
font-weight: 400;
|
||
color: #5e6470;
|
||
transition: color 0.3s ease;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 滚动部分 */
|
||
.scroll-section {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 空状态 */
|
||
.empty-state {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 160rpx 40rpx;
|
||
text-align: center;
|
||
|
||
.empty-image {
|
||
width: 172rpx;
|
||
height: 172rpx;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
margin-top: 32rpx;
|
||
}
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.loading-state {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40rpx;
|
||
}
|
||
|
||
.loading-text {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.loading-text::before {
|
||
content: "";
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
border: 4rpx solid #e8e8e8;
|
||
border-top: 4rpx solid #8b5cf6;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
/* 没有更多数据状态 */
|
||
.no-more-state {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 40rpx;
|
||
}
|
||
|
||
.no-more-text {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
/* 占位视图 */
|
||
.placeholder-view {
|
||
background-color: #f8f9fa;
|
||
}
|
||
}
|
||
</style>
|