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,741 @@
<template>
<view class="search-page-container flex flex-col">
<!-- 微信安全区 -->
<safety-zone></safety-zone>
<!-- 搜索框 -->
<view class="search-container border-b">
<!-- #ifdef MP-WEIXIN -->
<text class="cancel-text" @click="jumpNavigateBack">{{
t("Mobile.Common.cancel")
}}</text>
<!-- #endif -->
<view class="search-box" :class="{ focused: isFocused }">
<text class="iconfont icon-Search"></text>
<input
ref="searchInputRef"
class="search-input"
:placeholder="searchPlaceholder"
placeholder-class="placeholder-class"
type="search"
confirm-type="search"
:value="searchKeyword"
@confirm="handleSearch($event.detail.value)"
@input="handleSearch($event.detail.value)"
@focus="handleFocus"
@blur="handleBlur"
:auto-focus="shouldAutoFocus"
/>
<template v-if="searchKeyword">
<text
class="iconfont icon-a-Xcircle-fill clear-icon"
@click="handleSearch('')"
></text>
</template>
</view>
<!-- #ifndef MP-WEIXIN -->
<text class="cancel-text" @click="jumpNavigateBack">{{
t("Mobile.Common.cancel")
}}</text>
<!-- #endif -->
<!-- 微信胶囊按钮功能区 -->
<!-- #ifdef MP-WEIXIN -->
<view
class="right-safety"
:style="{
width: clientRect.width + 13 + 'px',
height: clientRect.height + 'px',
}"
></view>
<!-- #endif -->
</view>
<!-- 滚动部分 -->
<view class="scroll-section flex-1">
<scroll-view
class="h-full"
@scrolltolower="handleLoadMore"
:refresher-enabled="searchMode === 'api'"
:refresher-triggered="refreshing || false"
@refresherrefresh="handleRefresh"
:show-scrollbar="false"
>
<template v-if="(agentList || []).length">
<template v-for="info in agentList" :key="info.id">
<!-- 页面应用 -->
<page-card
v-if="cardComponentType === 'PageApp'"
:coverImg="info.coverImg"
:name="info.name"
:userName="info.publishUser?.userName"
:created="info.created"
@click="handleAgentClick(info)"
/>
<!-- 首页 -->
<recent-used-agent-item
v-else-if="cardComponentType === 'Home'"
:icon="info.icon"
:name="info.name"
:description="info.description"
:paymentRequired="info.paymentRequired && enableSubscription === 1"
:subscribed="info.subscribed"
@click="handleRecentUsedAgentClick(info)"
/>
<!-- 聊天机器人: 默认 cardComponentType 为 ChatBot -->
<agent-component
v-else
: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"
:paymentRequired="info.paymentRequired && enableSubscription === 1"
:subscribed="info.subscribed"
@click="handleAgentClick(info)"
/>
</template>
</template>
<template v-if="loading">
<view class="loading-state">
<text class="loading-text">{{ t("Mobile.Page.loadingMore") }}</text>
</view>
</template>
<template
v-else-if="!loading && searchKeyword && agentList?.length === 0"
>
<view class="empty-state">
<image
:src="noData"
class="empty-image"
:alt="t('Mobile.Common.noDataImageAlt')"
/>
<text class="empty-text">{{
t("Mobile.AgentSearch.emptyResult")
}}</text>
</view>
</template>
<template v-if="showNoMore">
<view class="no-more-state">
<text class="no-more-text">{{
t("Mobile.Common.noMoreData")
}}</text>
</view>
</template>
</scroll-view>
</view>
</view>
</template>
<script lang="uts" setup>
import { apiPublishedAgentList } from "@/servers/square";
import { apiUserUsedAgentList } from "@/servers/agentDev";
import {
SquarePublishedListParams,
SquarePublishedItemInfo,
} from "@/types/interfaces/square";
import { SquareAgentTypeEnum } from "@/types/enums/square";
import { Page, RequestResponse } from "@/types/interfaces/request";
import { SUCCESS_CODE } from "@/constants/codes.constants";
import { AgentComponentTypeEnum } from "@/types/enums/agent";
import RecentUsedAgentItem from "@/components/recent-used-agent-item/recent-used-agent-item.uvue";
import { apiTenantConfig } from "@/servers/account";
import { TENANT_CONFIG_INFO } from "@/constants/home.constants";
import type { TenantConfigInfo } from "@/types/interfaces/login";
import {
jumpToAgentDetailPage,
getCurrentPagePath,
} from "@/utils/commonBusiness";
import type { AgentInfo } from "@/types/interfaces/agent";
import noData from "@/static/assets/no_data.png";
// import { jumpNavigateBack } from '@/utils/common'
import type { CategoryItemInfo } from "@/types/interfaces/agentConfig";
import { AllowCopyEnum } from "@/types/enums/agent";
import { PluginTypeEnum } from "@/types/enums/plugin";
import { isTabPage } from "@/utils/commonBusiness";
import { setCurrentPageNavigationBarTitle } from "@/utils/system.uts";
import { useI18n, translateText } from "@/utils/i18n";
const { t } = useI18n();
const searchKeyword = ref<string>("");
const searchTimeoutRef = ref<number | null>(null);
const agentList = ref<SquarePublishedItemInfo[] | AgentInfo[]>([]);
const hasMore = ref<boolean>(true);
const page = ref<number>(1);
const loading = ref<boolean>(false);
const refreshing = ref<boolean>(false);
// 搜索输入框ref
const searchInputRef = ref<UniInputElement | null>(null);
const isFocused = ref<boolean>(false);
const cardComponentType = ref<"Home" | "PageApp" | "ChatBot">("ChatBot");
// 租户信息
const tenantConfigInfo = ref<TenantConfigInfo | null>(null);
// 租户订阅限制强受控标志
const enableSubscription = computed((): number => {
return tenantConfigInfo.value?.enableSubscription ?? 0;
});
// 搜索模式:'api' 接口搜索模式,'local' 前端过滤模式
const searchMode = ref<"api" | "local">("api");
// 前端过滤模式下的原始列表数据
const localAgentList = ref<CategoryItemInfo[]>([]);
// 搜索框 placeholder key运行时翻译保证语言切换可生效
const searchPlaceholderSource = ref<string>("Mobile.AgentSearch.placeholder");
const searchPlaceholder = computed(() => {
// 默认占位符 Key
let placeholderKey = "Mobile.AgentSearch.placeholder";
// 如果是 PageApp 类型,切换 Key
if (cardComponentType.value === "PageApp") {
placeholderKey = "Mobile.AgentSearch.pageAppPlaceholder";
}
// 如果 searchPlaceholderSource 有值且不是默认值(通常是 local 模式下从外部传入),则优先使用它
const finalKey =
searchPlaceholderSource.value &&
searchPlaceholderSource.value !== "Mobile.AgentSearch.placeholder"
? searchPlaceholderSource.value
: placeholderKey;
return translateText(finalKey);
});
// 小程序平台控制 auto-focus
const shouldAutoFocus = ref<boolean>(false);
// 返回地址
const backUrl = ref<string>("");
// #ifdef MP-WEIXIN
const clientRect = uni.getMenuButtonBoundingClientRect();
// #endif
const jumpNavigateBack = () => {
const toUrl = backUrl.value ? backUrl.value : "/pages/index/index";
console.log(toUrl);
if (isTabPage(toUrl)) {
uni.switchTab({ url: toUrl });
} else {
uni.redirectTo({ url: toUrl });
}
};
// 获取租户配置
const fetchTenantConfig = async () => {
const { code, data } = await apiTenantConfig();
if (code === SUCCESS_CODE) {
tenantConfigInfo.value = data;
// 缓存租户信息
uni.setStorageSync(TENANT_CONFIG_INFO, JSON.stringify(data));
}
};
// 初始化租户配置数据
const initTenantConfig = async () => {
// 1. 首先尝试从本地缓存读取
const tenantConfigInfoString = await uni.getStorageSync(TENANT_CONFIG_INFO);
if (tenantConfigInfoString) {
tenantConfigInfo.value = JSON.parse(tenantConfigInfoString);
}
// 2. 然后必须发起网络强同步请求,以获得最新的 enableSubscription 状态
await fetchTenantConfig();
};
onLoad(
async (options: {
type: "Home" | "PageApp" | "ChatBot";
searchMode?: "api" | "local";
backUrl?: string;
}) => {
// 强同步获取并更新租户配置信息,保障收费 tag 状态的百分百实时受控
await initTenantConfig();
const { type, searchMode: mode } = options;
backUrl.value = options.backUrl
? decodeURIComponent(options.backUrl)
: "";
cardComponentType.value = type;
// 设置当前页面导航栏标题
setCurrentPageNavigationBarTitle();
// 检测 searchMode 参数,区分搜索模式
if (mode === "local") {
// 从 globalData 获取列表数据和文案
const app = getApp();
// 兜底逻辑:如果 globalData 没有相关数据,回退到 api 模式
if (
app.globalData &&
app.globalData.localAgentList &&
Array.isArray(app.globalData.localAgentList) &&
app.globalData.localAgentList.length > 0
) {
// 有数据,使用前端过滤模式
searchMode.value = "local";
localAgentList.value = app.globalData
.localAgentList as CategoryItemInfo[];
// 默认不显示全部列表,只有搜索时才显示
agentList.value = [];
hasMore.value = false;
// 获取搜索文案,如果没有则使用默认值
if (app.globalData.searchPlaceholder) {
searchPlaceholderSource.value = app.globalData
.searchPlaceholder as string;
}
} else {
// 没有数据,回退到 api 模式
searchMode.value = "api";
}
} else {
searchMode.value = "api";
}
},
);
onMounted(() => {
// 页面加载完成后自动获取输入框焦点
// #ifndef MP-WEIXIN
// 非小程序平台可以直接调用 focus
nextTick(() => {
if (
searchInputRef.value &&
typeof searchInputRef.value.focus === "function"
) {
searchInputRef.value.focus();
}
});
// #endif
});
// 小程序平台使用 onReady 确保页面渲染完成后再聚焦
onReady(() => {
// #ifdef MP-WEIXIN
// 小程序平台:通过设置 auto-focus 为 true 来触发聚焦
shouldAutoFocus.value = true;
// #endif
});
// 小程序页面显示时也尝试聚焦
onShow(() => {
// #ifdef MP-WEIXIN
// 小程序平台:通过重置 auto-focus 来重新触发聚焦
shouldAutoFocus.value = false;
nextTick(() => {
shouldAutoFocus.value = true;
});
// #endif
});
// 是否显示没有更多数据(前端过滤模式下不显示)
const showNoMore = computed(() => {
if (searchMode.value === "local") {
return false;
}
return !loading.value && !hasMore.value && agentList.value?.length > 8;
});
/**
* 将 CategoryItemInfo 转换为 SquarePublishedItemInfo 格式
* @param items CategoryItemInfo 数组
* @returns SquarePublishedItemInfo 数组
*/
const convertCategoryItemsToSquareItems = (
items: CategoryItemInfo[],
): SquarePublishedItemInfo[] => {
return items.map((item) => ({
id: item.targetId, // 使用 targetId 作为 id
tenantId: 0,
spaceId: 0,
targetType: item.targetType as SquareAgentTypeEnum,
agentType: item.agentType,
targetId: item.targetId,
name: item.name,
description: item.description || null,
icon: item.icon,
remark: "",
modified: "",
created: "",
statistics: item.statistics,
publishUser: item.publishUser,
category: "",
allowCopy: AllowCopyEnum.No,
pluginType: PluginTypeEnum.HTTP,
collect: item.collect,
paymentRequired: (item as any).paymentRequired,
subscribed: (item as any).subscribed,
}));
};
const handleFocus = () => {
isFocused.value = true;
};
const handleBlur = () => {
isFocused.value = false;
};
// 获取智能体、页面应用列表
const fetchAgentList = async (
pageNum: number = 1,
keyword: string = searchKeyword.value,
) => {
const params: SquarePublishedListParams = {
targetType: AgentComponentTypeEnum.Agent,
targetSubType:
cardComponentType.value === "PageApp" ? "PageApp" : "ChatBot",
page: pageNum,
pageSize: 10,
category: "",
kw: keyword,
};
// 获取智能体列表
const response = await apiPublishedAgentList(params);
const { code, data, message } = response as unknown as RequestResponse<
Page<SquarePublishedItemInfo>
>;
// 成功
if (code === SUCCESS_CODE) {
const newList = data.records || [];
if (pageNum > 1) {
agentList.value = [...agentList.value, ...newList];
} else {
agentList.value = newList;
}
hasMore.value = pageNum < data.pages;
page.value = pageNum;
loading.value = false;
} else {
uni.showToast({
title: message || t("Mobile.AgentSearch.fetchAgentFailed"),
icon: "error",
});
loading.value = false;
}
};
// 查询用户最近使用过的智能体列表
const fetchUserUsedAgentList = async (
pageNum: number = 1,
keyword: string = searchKeyword.value,
) => {
const res = await apiUserUsedAgentList({
pageIndex: pageNum,
size: 20,
keyword,
});
const { code, data } = res || {};
if (code === SUCCESS_CODE) {
const newList = data || [];
if (pageNum > 1) {
// 追加数据
agentList.value = [...(agentList.value || []), ...newList];
} else {
// 替换数据
agentList.value = newList;
}
// 是否有更多数据, 如果数据长度大于0则有更多数据
hasMore.value = newList?.length > 0;
page.value = pageNum;
loading.value = false;
} else {
uni.showToast({
title: t("Mobile.AgentSearch.fetchRecentUsedFailed"),
icon: "none",
});
loading.value = false;
}
};
// 获取智能体列表
const fetchList = async (
pageNum: number = 1,
keyword: string = searchKeyword.value,
) => {
// 前端过滤模式下不调用接口
if (searchMode.value === "local") {
return;
}
if (cardComponentType.value === "Home") {
await fetchUserUsedAgentList(pageNum, keyword);
} else {
await fetchAgentList(pageNum, keyword);
}
};
/**
* 前端过滤搜索(本地过滤)
* @param keyword 搜索关键词
*/
const filterLocalList = (keyword: string) => {
// 如果没有关键词,返回空列表
if (!keyword || !localAgentList.value.length) {
agentList.value = [];
return;
}
// 对本地列表进行过滤,匹配 name 或 description不区分大小写
const lowerKeyword = keyword.toLowerCase();
const filtered = localAgentList.value.filter((item) => {
const nameMatch =
item.name?.toLowerCase().includes(lowerKeyword) || false;
const descMatch =
item.description?.toLowerCase().includes(lowerKeyword) || false;
return nameMatch || descMatch;
});
agentList.value = convertCategoryItemsToSquareItems(filtered);
};
// 处理搜索
const handleSearch = (keyword: string) => {
searchKeyword.value = keyword;
// 清除之前的定时器
if (searchTimeoutRef.value) {
clearTimeout(searchTimeoutRef.value);
}
// 如果搜索关键词为空
if (!keyword) {
// 前端过滤模式和接口搜索模式都清空列表
agentList.value = [];
loading.value = false;
// #ifdef H5 || WEB
nextTick(() => {
if (searchInputRef.value) {
searchInputRef.value?.focus();
}
});
// #endif
return;
}
// 根据搜索模式选择不同的处理方式
if (searchMode.value === "local") {
// 前端过滤模式:本地过滤
loading.value = true;
// 设置新的定时器防抖500ms
searchTimeoutRef.value = setTimeout(() => {
filterLocalList(keyword);
loading.value = false;
}, 500);
} else {
// 接口搜索模式:调用接口
loading.value = true;
// 设置新的定时器防抖500ms
searchTimeoutRef.value = setTimeout(() => {
page.value = 1;
hasMore.value = true;
fetchList(1, keyword);
}, 500);
}
};
// 加载更多
const handleLoadMore = () => {
// 前端过滤模式下禁用加载更多
if (searchMode.value === "local") {
return;
}
if (hasMore.value && !loading.value) {
const nextPage = page.value + 1;
fetchList(nextPage);
}
};
// 下拉刷新
const handleRefresh = () => {
// 前端过滤模式下禁用下拉刷新
if (searchMode.value === "local") {
refreshing.value = false;
return;
}
if (refreshing.value) {
return;
}
refreshing.value = true;
fetchList().finally(() => {
refreshing.value = false;
});
};
// 跳转到智能体详情页
const handleAgentClick = (info: SquarePublishedItemInfo) => {
jumpToAgentDetailPage(info.targetId, null, info.agentType, info.name);
};
// 跳转到智能体详情页
const handleRecentUsedAgentClick = (info: AgentInfo) => {
jumpToAgentDetailPage(info.agentId, null, info.agentType, info.name);
};
</script>
<style lang="scss" scoped>
.search-page-container {
height: 100vh;
overflow: hidden;
background-color: #fff;
/* #ifdef MP-WEIXIN */
// padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
/* #endif */
/* 搜索框 */
.search-container {
display: flex;
flex-direction: row;
align-items: center;
padding: 10rpx 0 10rpx 32rpx;
flex-shrink: 0;
.search-box {
flex: 1;
gap: 16rpx;
display: flex;
align-items: center;
flex-direction: row;
height: 76rpx;
padding: 0 24rpx;
border-radius: 16rpx;
border: 2rpx solid transparent;
background: rgba(12, 20, 102, 0.04);
transition: border-color 0.3s ease;
&.focused {
border-color: #5147ff;
}
.iconfont {
font-size: 32rpx;
color: #666;
}
.search-input {
flex: 1;
border: none;
font-size: 28rpx;
color: #15171f;
outline: none;
font-weight: 400;
}
.placeholder-class {
color: rgba(21, 23, 31, 0.5);
font-size: 28rpx;
font-weight: 400;
}
.clear-icon {
font-size: 32rpx;
color: #999 !important;
cursor: pointer;
}
}
.cancel-text {
display: flex;
align-items: center;
justify-content: center;
height: 76rpx;
font-weight: 400;
padding: 0 32rpx;
color: rgba(21, 23, 31, 0.5);
font-size: 32rpx;
transition: background-color 0.3s ease;
border-radius: 8rpx;
&:active {
background-color: rgba(12, 20, 102, 0.04);
}
/* #ifdef H5 */
&:hover {
cursor: pointer;
}
/* #endif */
}
}
/* 滚动部分 */
.scroll-section {
flex: 1;
overflow: hidden;
/* 空状态 */
.empty-state {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.empty-image {
width: 172rpx;
height: 172rpx;
}
.empty-text {
font-size: 32rpx;
color: #15171f;
font-weight: 400;
margin-top: 32rpx;
}
}
/* 加载状态 */
.loading-state {
height: 100%;
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;
}
}
}
</style>