Files
qiming/qiming-mobile/components/published-agent-list/published-agent-list.uvue

692 lines
22 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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="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>