chore: initialize qiming workspace repository
This commit is contained in:
643
qiming-mobile/pages/agent-union-record/agent-union-record.uvue
Normal file
643
qiming-mobile/pages/agent-union-record/agent-union-record.uvue
Normal file
@@ -0,0 +1,643 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user