import { ALLOW_EXTERNAL_LINK_DOMAIN } from "@/constants/config"; import { TENANT_CONFIG_INFO } from "@/constants/home.constants"; import { SUCCESS_CODE } from "@/constants/codes.constants"; import { apiTenantConfig } from "@/servers/account"; import { apiGetStaticFileList } from "@/servers/agentDev.uts"; import { t } from "@/utils/i18n"; const SYSTEM_INFO = uni.getSystemInfoSync(); export const getStatusBarHeight = () => SYSTEM_INFO.statusBarHeight || 15; export const formatDate = ( input: string | number | Date, format: | "YYYY-MM-DD" | "MM-DD" | "HH:mm" | "YYYY-MM-DD HH:mm:ss" | "YYYY-MM-DD HH:mm" = "MM-DD", ): string => { const date = new Date(input); const YYYY = date.getFullYear(); const MM = String(date.getMonth() + 1).padStart(2, "0"); const DD = String(date.getDate()).padStart(2, "0"); const HH = String(date.getHours()).padStart(2, "0"); const mm = String(date.getMinutes()).padStart(2, "0"); switch (format) { case "YYYY-MM-DD": return `${YYYY}-${MM}-${DD}`; case "YYYY-MM-DD HH:mm:ss": const ss = String(date.getSeconds()).padStart(2, "0"); return `${YYYY}-${MM}-${DD} ${HH}:${mm}:${ss}`; case "YYYY-MM-DD HH:mm": return `${YYYY}-${MM}-${DD} ${HH}:${mm}`; case "HH:mm": return `${HH}:${mm}`; default: return `${MM}-${DD}`; } }; // 判断是否是允许外部链接跳转的域名 export const isAllowExternalLinkDomain = (url: string) => { return ALLOW_EXTERNAL_LINK_DOMAIN.some((domain) => url.includes(domain)); }; /** * 根据链接地址判断不允许调整的页面需要复制链接到剪切板 * @param url 链接地址 * @returns void */ export const handleExternalLink = (url: string) => { // 如果链接地址为空,则提示链接地址配置错误 if (!url) { uni.showToast({ title: t("Mobile.Link.invalidConfig"), icon: "none", }); return; } // #ifdef H5 || WEB // H5 环境直接打开外部链接 try { // 判断是否为有效的 URL const isValidUrl = url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//"); if (isValidUrl) { // 尝试在新标签页中打开链接 const newWindow = window.open(url, "_blank"); // 检查是否被浏览器拦截(某些浏览器会阻止弹窗) if ( !newWindow || newWindow.closed || typeof newWindow.closed === "undefined" ) { // 如果被拦截,提示用户并复制链接到剪贴板 console.warn("链接被浏览器拦截,使用备用方案"); // 尝试复制到剪贴板 if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard .writeText(url) .then(() => { uni.showToast({ title: t("Mobile.Link.copyAndOpen"), icon: "none", duration: 3000, }); }) .catch(() => { // 如果复制失败,直接在当前页面打开 window.location.href = url; }); } else { // 如果不支持剪贴板 API,直接在当前页面打开 window.location.href = url; } } } else { // 如果不是完整的 URL,使用 navigateTo 跳转到 webview 页面 uni.navigateTo({ url: `/subpackages/pages/webview/webview?url=${encodeURIComponent(url)}`, }); } } catch (error) { console.error("打开链接失败:", error); uni.showToast({ title: t("Mobile.Link.openFailed"), icon: "none", }); } // #endif // #ifdef MP-WEIXIN // 如果允许外部链接跳转,则打开外部链接 if (isAllowExternalLinkDomain(url)) { uni.navigateTo({ url: `/subpackages/pages/webview/webview?url=${encodeURIComponent(url)}`, }); } else { uni.setClipboardData({ data: url, success: () => { uni.showToast({ title: t("Mobile.Link.copied"), icon: "none", }); }, }); } // #endif }; // 获取用户配置并设置页面标题和缓存租户信息 export const fetchTenantConfig = async () => { const { code, data } = await apiTenantConfig(); const siteName = data?.siteName; if (code === SUCCESS_CODE && siteName) { // 设置当前页面导航栏标题 uni.setNavigationBarTitle({ title: siteName }); // 缓存租户信息 uni.setStorageSync(TENANT_CONFIG_INFO, JSON.stringify(data)); } }; // 设置当前页面导航栏标题 export const setCurrentPageNavigationBarTitle = async () => { // 获取本地用户配置 const tenantConfigInfoString = await uni.getStorageSync(TENANT_CONFIG_INFO); if (tenantConfigInfoString) { try { // JSON.parse 返回 unknown,这里显式约束为仅关心 siteName 的结构 const tenantConfig = JSON.parse(tenantConfigInfoString) as { siteName?: string; }; const { siteName } = tenantConfig; if (siteName) { // 设置当前页面导航栏标题 uni.setNavigationBarTitle({ title: siteName }); return; } } catch (error) { console.error("获取本地用户配置失败:", error); } } fetchTenantConfig(); // 获取用户配置并设置页面标题和缓存租户信息 }; // 根据会话id和文件路径 跳转至预览页面 export const jumpToFilePreviewPage = ( conversationId: string, fileProxyUrl: string, ) => { if (!conversationId || !fileProxyUrl) { return; } const path = "/subpackages/pages/file-preview-page/file-preview-page"; const url = `${path}?cId=${conversationId}&fileProxyUrl=${encodeURIComponent(fileProxyUrl)}`; uni.navigateTo({ url }); }; // 根据会话id查询文件列表并根据文件路径获取到文件代理url export const getFileProxyUrlByConversationIdAndFilePath = async ( conversationId: string, filePath: string, ) => { if (!conversationId || !filePath) { return null; } // 接口要求 number 类型,先做显式转换并校验,避免把非法字符串传入服务层 const normalizedConversationId = Number(conversationId); if (Number.isNaN(normalizedConversationId)) { return null; } const result = await apiGetStaticFileList(normalizedConversationId); if (result.code === SUCCESS_CODE) { const { files } = result.data || {}; const file = files.find((item) => item.name === filePath); return file?.fileProxyUrl; } return null; }; /** * 任务结果数据 */ interface TaskResultData { hasTaskResult: boolean; description?: string; file?: string; } /** * 提取任务结果数据 * @param text 文本 * @returns 任务结果数据 */ export const extractTaskResult = (text: string): TaskResultData => { const result: TaskResultData = { hasTaskResult: false, }; if (!text) return result; // 1️⃣ 匹配 ... const taskResultMatch = text.match(/([\s\S]*?)<\/task-result>/); if (!taskResultMatch) { return result; } result.hasTaskResult = true; const taskResultContent = taskResultMatch[1]; // 2️⃣ 提取 description const descriptionMatch = taskResultContent.match( /([\s\S]*?)<\/description>/, ); if (descriptionMatch) { result.description = descriptionMatch[1].trim(); } // 3️⃣ 提取 file const fileMatch = taskResultContent.match(/([\s\S]*?)<\/file>/); if (fileMatch) { result.file = fileMatch[1].trim(); } return result; }; /** * 提取字符串中最后一个 内的 内容 * @param text 原始字符串 * @returns file 内容或 null */ export const extractLastTaskResultFile = (text: string): string | null => { if (!text) return null; // 匹配所有 ... const taskResultMatches = text.match(/[\s\S]*?<\/task-result>/g); if (!taskResultMatches || taskResultMatches.length === 0) { return null; } // 取最后一个 const lastTaskResult = taskResultMatches[taskResultMatches.length - 1]; // 在其中提取 内容 const fileMatch = lastTaskResult.match(/([\s\S]*?)<\/file>/); return fileMatch?.[1]?.trim() ?? null; }; /** * 健壮的千分位数字格式化工具函数(支持整数、浮点数以及负数,带有强健的边界容错降级机制) * @param value 输入数值或字符 * @returns 格式化后的千分位字符串 */ export const formatNumber = (value: any, precision: number = -1): string => { if (value == null) { return "0"; } let str = String(value).trim(); if (str === "" || str === "null" || str === "undefined") { return "0"; } const num = Number(str); if (Number.isNaN(num) || !Number.isFinite(num)) { return "0"; } if (precision >= 0) { str = num.toFixed(precision); } else { str = num.toString(); } const isNegative = str.startsWith("-"); if (isNegative) { str = str.substring(1); } const parts = str.split("."); const integerPart = parts[0]; const decimalPart = parts.length > 1 ? "." + parts[1] : ""; let result = ""; let counter = 0; for (let i = integerPart.length - 1; i >= 0; i--) { result = integerPart.charAt(i) + result; counter++; if (counter === 3 && i !== 0) { result = "," + result; counter = 0; } } return (isNegative ? "-" : "") + result + decimalPart; };