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,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>