644 lines
18 KiB
Plaintext
644 lines
18 KiB
Plaintext
<template>
|
||
<view class="container relative flex flex-col border-b">
|
||
<!-- 顶部导航栏 -->
|
||
<custom-nav-bar :title="t('Mobile.Nav.unionRecord')" className="border-b">
|
||
<!-- #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>
|
||
<template v-if="loading">
|
||
<view class="loading-state">
|
||
<text class="loading-text">{{ t("Mobile.Page.loadingMore") }}</text>
|
||
</view>
|
||
</template>
|
||
<template v-else>
|
||
<view class="flex-1 relative">
|
||
<!-- 团队列表 -->
|
||
<scroll-view
|
||
class="h-full overflow-hide"
|
||
direction="vertical"
|
||
:scroll-with-animation="true"
|
||
:refresher-enabled="true"
|
||
:refresher-triggered="refreshing"
|
||
@refresherrefresh="handleRefresh"
|
||
:show-scrollbar="false"
|
||
:scroll-into-view="anchor"
|
||
>
|
||
<!-- 顶部导航栏 -->
|
||
<view
|
||
v-for="category in homeCategoryInfo.categories"
|
||
:key="category.type"
|
||
class="flex flex-row items-center content-between category-box"
|
||
hover-class="category-box-active"
|
||
@click="handleCategoryClick(category.type)"
|
||
>
|
||
<template v-if="category.categoryType === 'AGENT_COLLECT'">
|
||
<!-- 我的收藏 -->
|
||
<image
|
||
class="icon_item_category"
|
||
:src="iconAgentCollect"
|
||
mode="aspectFill"
|
||
:alt="category.name || t('Mobile.Common.agentIconAlt')"
|
||
/>
|
||
</template>
|
||
<template v-else-if="category.categoryType === 'TEAM_SPACE'">
|
||
<!-- 团队空间 -->
|
||
<image
|
||
class="icon_item_category"
|
||
:src="iconTeamSpace"
|
||
mode="aspectFill"
|
||
:alt="category.name || t('Mobile.Common.agentIconAlt')"
|
||
/>
|
||
</template>
|
||
<template v-else-if="category.categoryType === 'AGENT_RECOMMEND'">
|
||
<!-- 官方推荐 -->
|
||
<image
|
||
class="icon_item_category"
|
||
:src="iconAgentRecommend"
|
||
mode="aspectFill"
|
||
:alt="category.name || t('Mobile.Common.agentIconAlt')"
|
||
/>
|
||
</template>
|
||
<template v-else-if="category.categoryType === 'PERSONAL_SPACE'">
|
||
<!-- 个人空间 -->
|
||
<image
|
||
class="icon_item_category"
|
||
:src="iconPersonalSpace"
|
||
mode="aspectFill"
|
||
:alt="category.name || t('Mobile.Common.agentIconAlt')"
|
||
/>
|
||
</template>
|
||
<template v-else>
|
||
<!-- 默认 -->
|
||
<image
|
||
class="icon_item_category"
|
||
:src="iconPersonalSpace"
|
||
mode="aspectFill"
|
||
:alt="category.name || t('Mobile.Common.agentIconAlt')"
|
||
/>
|
||
</template>
|
||
<view class="flex-1 category-name text-ellipsis">{{
|
||
category.name
|
||
}}</view>
|
||
<text class="iconfont icon-a-Chevronright icon-right-arrow"></text>
|
||
</view>
|
||
<!-- 智能体列表 -->
|
||
<view
|
||
v-for="group in categoryList"
|
||
:key="group.key"
|
||
class="group"
|
||
:id="'id-' + group.key"
|
||
>
|
||
<view class="group__title">{{ group.label }}</view>
|
||
<view
|
||
v-for="item in group.children"
|
||
:key="item.targetId"
|
||
class="flex flex-row items-center item-card"
|
||
hover-class="item-card-active"
|
||
@click="handleAgentClick(item)"
|
||
>
|
||
<image
|
||
class="item-card__icon"
|
||
:src="getImageSrc(item.targetId, item.icon)"
|
||
mode="aspectFill"
|
||
:alt="item.name || t('Mobile.Common.agentIconAlt')"
|
||
@error="handleImageError(item.targetId)"
|
||
/>
|
||
<view class="item-card__content">
|
||
<text class="item-card__name text-ellipsis">{{
|
||
item.name
|
||
}}</text>
|
||
<text
|
||
class="item-card__desc text-ellipsis"
|
||
v-if="item.description"
|
||
>{{ item.description }}</text
|
||
>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
<!-- 右侧字母导航栏 -->
|
||
<view class="right-bar">
|
||
<view
|
||
class="right-bar__item"
|
||
v-for="L in letters"
|
||
:key="L"
|
||
@click="scrollToSection(L)"
|
||
>
|
||
<text>{{ L }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 登录弹窗 -->
|
||
<auth-login-popup ref="loginPopupRef" />
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="uts">
|
||
import {
|
||
SUCCESS_CODE,
|
||
USER_NO_LOGIN,
|
||
REDIRECT_LOGIN,
|
||
} from "@/constants/codes.constants.uts";
|
||
import { apiHomeCategoryList } from "@/servers/agentDev";
|
||
import {
|
||
HomeAgentCategoryInfo,
|
||
CategoryItemInfo,
|
||
} from "@/types/interfaces/agentConfig";
|
||
import { pinyin } from "@/utils/pinyin.uts";
|
||
import agentImage from "@/static/assets/agent_image.png";
|
||
import {
|
||
getCurrentPagePath,
|
||
jumpToAgentDetailPage,
|
||
} from "@/utils/commonBusiness";
|
||
import { onAddToFavorites } from "@dcloudio/uni-app";
|
||
import iconAgentCollect from "@/static/icons/agent_collect.svg";
|
||
import iconTeamSpace from "@/static/icons/team_space.svg";
|
||
import iconAgentRecommend from "@/static/icons/agent_recommend.svg";
|
||
import iconPersonalSpace from "@/static/icons/personal_space.svg";
|
||
import AuthLoginPopup from "@/components/auth-login-popup/auth-login-popup.uvue";
|
||
import { useAuthInterceptor } from "@/hooks/useAuthInterceptor";
|
||
import { setCurrentPageNavigationBarTitle } from "@/utils/system";
|
||
import { apiPublishedAgentList } from "@/servers/square";
|
||
import { AgentComponentTypeEnum } from "@/types/enums/agent";
|
||
import type { SquarePublishedListParams } from "@/types/interfaces/square";
|
||
import { useI18n } from "@/utils/i18n";
|
||
|
||
const { t } = useI18n();
|
||
|
||
const letters = ref<string[]>([]);
|
||
const anchor = ref<string>("");
|
||
const loading = ref<boolean>(false);
|
||
// 所有分类下的智能体列表,并且去重
|
||
const categoryList = ref<
|
||
{ label: string; key: string; children: CategoryItemInfo[] }[]
|
||
>([]);
|
||
// 分类信息,包括每一项分类下的智能体列表
|
||
const homeCategoryInfo = ref<HomeAgentCategoryInfo>({
|
||
// 首页分类
|
||
categories: [],
|
||
// 首页分类数据列表
|
||
categoryItems: {},
|
||
});
|
||
|
||
// 记录加载失败的图片ID
|
||
const failedImageIds = reactive<Set<string>>(new Set());
|
||
// 刷新状态
|
||
const refreshing = ref<boolean>(false);
|
||
|
||
// 新增标志变量,用于区分首次加载
|
||
const isFirstLoad = ref<boolean>(true);
|
||
|
||
// 使用登录拦截 composable
|
||
const {
|
||
loginPopupRef,
|
||
handleAgentClick: baseHandleAgentClick,
|
||
checkAuthAndShowPopup,
|
||
hasToken,
|
||
} = useAuthInterceptor();
|
||
|
||
// 包装 handleAgentClick 以适配 CategoryItemInfo 类型
|
||
const handleAgentClick = (info: CategoryItemInfo) => {
|
||
const { lastConversationId, targetId, name, targetType } = info || {};
|
||
if (lastConversationId) {
|
||
jumpToAgentDetailPage(targetId, lastConversationId);
|
||
} else {
|
||
baseHandleAgentClick({
|
||
targetId: targetId,
|
||
name: name,
|
||
targetType: targetType,
|
||
icon: info.icon,
|
||
description: info.description,
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 获取图片源地址
|
||
* @param id 智能体ID
|
||
* @param originalIcon 原始图片地址
|
||
* @returns 图片地址(加载失败则返回默认图片)
|
||
*/
|
||
const getImageSrc = (id: string, originalIcon: string): string => {
|
||
return failedImageIds.has(id) ? agentImage : originalIcon;
|
||
};
|
||
|
||
/**
|
||
* 图片加载错误时触发,切换为默认图片
|
||
* @param id 智能体ID
|
||
*/
|
||
const handleImageError = (id: string) => {
|
||
failedImageIds.add(id);
|
||
};
|
||
|
||
// 处理智联录列表搜索
|
||
const handleSearch = async () => {
|
||
// 检查登录状态,如果未登录则显示登录弹窗
|
||
const isLoggedIn = await checkAuthAndShowPopup();
|
||
if (!isLoggedIn) {
|
||
return;
|
||
}
|
||
// 将 categoryList 扁平化为一维数组
|
||
const flatList: CategoryItemInfo[] = categoryList.value.flatMap(
|
||
(group) => group.children || [],
|
||
);
|
||
|
||
// 使用 globalData 临时存储列表数据和文案
|
||
const app = getApp();
|
||
if (!app.globalData) {
|
||
app.globalData = {};
|
||
}
|
||
app.globalData.localAgentList = flatList;
|
||
// 传递搜索文案 key(在搜索页运行时翻译,保证语言切换一致)
|
||
app.globalData.searchPlaceholder = "Mobile.AgentUnion.searchPlaceholder";
|
||
|
||
const currentUrl = getCurrentPagePath();
|
||
// 跳转时添加 searchMode=local 参数标识前端过滤模式
|
||
uni.navigateTo({
|
||
url:
|
||
`/subpackages/pages/agent-search/agent-search?type=ChatBot&searchMode=local&backUrl=` +
|
||
encodeURIComponent(currentUrl),
|
||
});
|
||
};
|
||
|
||
// 根据智能体列表的name字段首字母进行分组,并排序呢
|
||
const groupByFirstLetterList = (list: CategoryItemInfo[]) => {
|
||
const grouped: Record<string, CategoryItemInfo[]> = list.reduce(
|
||
(acc, item) => {
|
||
const firstChar = item.name?.charAt(0) || "";
|
||
let firstLetter = pinyin(firstChar, {
|
||
pattern: "first",
|
||
tone: "none",
|
||
}).toUpperCase();
|
||
if (!/^[A-Z]$/.test(firstLetter)) {
|
||
firstLetter = "#";
|
||
}
|
||
if (!acc[firstLetter]) {
|
||
acc[firstLetter] = [];
|
||
}
|
||
acc[firstLetter].push(item);
|
||
return acc;
|
||
},
|
||
{},
|
||
);
|
||
|
||
// 获取按字母排序的键(仅A-Z)
|
||
const azKeys = Object.keys(grouped)
|
||
.filter((k) => /^[A-Z]$/.test(k))
|
||
.sort((a, b) => a.localeCompare(b));
|
||
|
||
// 如果有#组,添加到末尾
|
||
const sortedKeys = [...azKeys];
|
||
if (grouped["#"]) {
|
||
sortedKeys.push("#");
|
||
}
|
||
|
||
// 将首字母添加到字母列表中
|
||
letters.value = sortedKeys;
|
||
|
||
// 转换为指定数组格式,并对每个 children 按完整拼音排序
|
||
const formattedList = sortedKeys.map((key) => {
|
||
// 对 children 按 name 的完整拼音排序
|
||
const sortedChildren = grouped[key].sort((a, b) => {
|
||
const pinyinA = pinyin(a.name, { pattern: "pinyin", tone: "none" })
|
||
.replace(/\s/g, "")
|
||
.toLowerCase();
|
||
const pinyinB = pinyin(b.name, { pattern: "pinyin", tone: "none" })
|
||
.replace(/\s/g, "")
|
||
.toLowerCase();
|
||
return pinyinA.localeCompare(pinyinB);
|
||
});
|
||
|
||
const actualKey = key === "#" ? "hash" : key;
|
||
|
||
return {
|
||
label: key,
|
||
key: actualKey,
|
||
children: sortedChildren,
|
||
};
|
||
});
|
||
|
||
return formattedList; // 返回结果
|
||
};
|
||
|
||
// 点击分类标签的逻辑
|
||
const handleCategoryClick = (categoryKey: string) => {
|
||
uni.navigateTo({
|
||
url: `/subpackages/pages/category-agent-list/category-agent-list?categoryKey=${categoryKey}`,
|
||
});
|
||
};
|
||
|
||
// 滚动到对应字母分组的逻辑
|
||
const scrollToSection = (letter: string) => {
|
||
const actualLetter = letter === "#" ? "hash" : letter;
|
||
anchor.value = `id-${actualLetter}`;
|
||
};
|
||
|
||
// 主页智能体列表接口
|
||
const fetchHomeCategoryList = async () => {
|
||
const response = await apiHomeCategoryList();
|
||
loading.value = false;
|
||
const { code, data, message } = response || {};
|
||
if (code === SUCCESS_CODE) {
|
||
homeCategoryInfo.value = data;
|
||
const { categoryItems } = data || {};
|
||
// 判断数据是否为对象,并且有值
|
||
if (
|
||
categoryItems &&
|
||
typeof categoryItems === "object" &&
|
||
Object.keys(categoryItems).length > 0
|
||
) {
|
||
// 获取所有分类下的智能体列表,并且扁平化为一维数组
|
||
const allCategoryItems = Object.values(categoryItems).flat();
|
||
|
||
// map + values 去重
|
||
const uniqueCategoryItems = Array.from(
|
||
new Map(
|
||
allCategoryItems.map((item) => [item.targetId, item]),
|
||
).values(),
|
||
);
|
||
|
||
// 根据智能体列表的name字段首字母进行分组,并排序呢
|
||
categoryList.value = groupByFirstLetterList(uniqueCategoryItems);
|
||
}
|
||
} else if (code !== USER_NO_LOGIN && code !== REDIRECT_LOGIN) {
|
||
uni.showToast({
|
||
title: message || t("Mobile.AgentUnion.fetchHomeAgentListFailed"),
|
||
icon: "none",
|
||
});
|
||
}
|
||
};
|
||
|
||
// 获取公开的智能体列表(未登录时使用)
|
||
const fetchPublishedAgentList = async () => {
|
||
try {
|
||
const params: SquarePublishedListParams = {
|
||
targetType: AgentComponentTypeEnum.Agent,
|
||
targetSubType: "ChatBot",
|
||
page: 1,
|
||
pageSize: 10,
|
||
category: "",
|
||
kw: "",
|
||
};
|
||
const res = await apiPublishedAgentList(params);
|
||
loading.value = false;
|
||
const { code, data } = res;
|
||
if (code === SUCCESS_CODE) {
|
||
// 根据智能体列表的name字段首字母进行分组,并排序呢
|
||
categoryList.value = groupByFirstLetterList(data.records);
|
||
}
|
||
} catch (error) {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
// 下拉刷新
|
||
const handleRefresh = async () => {
|
||
if (refreshing.value) {
|
||
return;
|
||
}
|
||
refreshing.value = true;
|
||
|
||
const isLoggedIn = await hasToken();
|
||
|
||
if (isLoggedIn) {
|
||
// 已登录
|
||
fetchHomeCategoryList().finally(() => {
|
||
// 添加延迟,让loading效果更明显
|
||
setTimeout(() => {
|
||
refreshing.value = false;
|
||
}, 500);
|
||
});
|
||
} else {
|
||
// 未登录
|
||
fetchPublishedAgentList().finally(() => {
|
||
// 添加延迟,让loading效果更明显
|
||
setTimeout(() => {
|
||
refreshing.value = false;
|
||
}, 500);
|
||
});
|
||
}
|
||
};
|
||
|
||
onLoad(async () => {
|
||
// 设置当前页面导航栏标题
|
||
setCurrentPageNavigationBarTitle();
|
||
|
||
isFirstLoad.value = true;
|
||
// 检查登录状态,如果未登录则显示登录弹窗,不加载数据
|
||
const isLoggedIn = await hasToken();
|
||
if (!isLoggedIn) {
|
||
loading.value = false;
|
||
// 未登录,获取公开的智能体列表
|
||
fetchPublishedAgentList();
|
||
return;
|
||
}
|
||
loading.value = true;
|
||
fetchHomeCategoryList();
|
||
});
|
||
|
||
onTabItemTap(async () => {
|
||
await nextTick();
|
||
|
||
// 已登录的情况
|
||
// 如果是首次加载,标记为非首次,然后加载数据
|
||
if (isFirstLoad.value) {
|
||
isFirstLoad.value = false;
|
||
// 首次加载时,onLoad 已经处理了数据加载,这里不需要重复加载
|
||
return;
|
||
}
|
||
// 检查登录状态
|
||
const isLoggedIn = await hasToken();
|
||
if (isLoggedIn) {
|
||
// 非首次加载,刷新数据
|
||
fetchHomeCategoryList();
|
||
} else {
|
||
// 未登录,获取公开的智能体列表
|
||
fetchPublishedAgentList();
|
||
}
|
||
});
|
||
|
||
// 收藏
|
||
onAddToFavorites(() => {
|
||
return {
|
||
title: t("Mobile.Nav.unionRecord"),
|
||
};
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.container {
|
||
height: 100%; // 假设全屏
|
||
|
||
.icon-Search {
|
||
margin-left: 12rpx;
|
||
}
|
||
|
||
/* 加载状态 */
|
||
.loading-state {
|
||
flex: 1;
|
||
width: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.loading-text {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-direction: row;
|
||
|
||
@keyframes spin {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
&::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;
|
||
}
|
||
}
|
||
}
|
||
|
||
.category-box {
|
||
gap: 16rpx;
|
||
padding: 24rpx 32rpx;
|
||
border-bottom: 2rpx solid #f0f0f0;
|
||
transition: background-color 0.3s ease;
|
||
|
||
&-active {
|
||
background-color: rgba(12, 20, 102, 0.04);
|
||
}
|
||
|
||
.category-name {
|
||
font-size: 32rpx;
|
||
font-weight: 400;
|
||
color: rgb(51, 51, 51);
|
||
}
|
||
.icon_item_category {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
margin-right: 12rpx;
|
||
}
|
||
}
|
||
|
||
.item-category__icon {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.icon-right-arrow {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.group {
|
||
&__title {
|
||
padding: 12rpx 32rpx;
|
||
font-size: 28rpx;
|
||
background-color: #f5f5f5;
|
||
color: #6f6d6d;
|
||
}
|
||
}
|
||
|
||
.item-card {
|
||
height: 150rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 32rpx;
|
||
border-bottom: 2rpx solid #f0f0f0;
|
||
|
||
&-active {
|
||
background-color: rgba(12, 20, 102, 0.04);
|
||
}
|
||
|
||
&__icon {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
margin-right: 24rpx;
|
||
}
|
||
|
||
&__content {
|
||
flex: 1;
|
||
gap: 8rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
&__name {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #15171f;
|
||
}
|
||
|
||
&__desc {
|
||
font-size: 28rpx;
|
||
color: rgba(21, 23, 31, 0.7);
|
||
line-height: 44rpx;
|
||
font-weight: 400;
|
||
}
|
||
|
||
&__count {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
}
|
||
|
||
.right-bar {
|
||
position: absolute;
|
||
right: 22rpx;
|
||
bottom: 120rpx;
|
||
width: 32rpx;
|
||
max-height: calc(100% - 140rpx);
|
||
padding: 16rpx 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 8rpx;
|
||
border-radius: 16rpx;
|
||
overflow: auto;
|
||
z-index: 100;
|
||
background-color: rgb(244, 241, 241);
|
||
|
||
&__item {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
|
||
text {
|
||
font-size: 20rpx;
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|