chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,691 @@
<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>