Files
qiming/qiming-mobile/pages/agent-union-record/agent-union-record.uvue

644 lines
18 KiB
Plaintext
Raw 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 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>