Files
qiming/qiming-mobile/utils/system.uts

330 lines
9.3 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
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⃣ 匹配 <task-result>...</task-result>
const taskResultMatch = text.match(/<task-result>([\s\S]*?)<\/task-result>/);
if (!taskResultMatch) {
return result;
}
result.hasTaskResult = true;
const taskResultContent = taskResultMatch[1];
// 2⃣ 提取 description
const descriptionMatch = taskResultContent.match(
/<description>([\s\S]*?)<\/description>/,
);
if (descriptionMatch) {
result.description = descriptionMatch[1].trim();
}
// 3⃣ 提取 file
const fileMatch = taskResultContent.match(/<file>([\s\S]*?)<\/file>/);
if (fileMatch) {
result.file = fileMatch[1].trim();
}
return result;
};
/**
* 提取字符串中最后一个 <task-result> 内的 <file> 内容
* @param text 原始字符串
* @returns file 内容或 null
*/
export const extractLastTaskResultFile = (text: string): string | null => {
if (!text) return null;
// 匹配所有 <task-result>...</task-result>
const taskResultMatches = text.match(/<task-result>[\s\S]*?<\/task-result>/g);
if (!taskResultMatches || taskResultMatches.length === 0) {
return null;
}
// 取最后一个 <task-result>
const lastTaskResult = taskResultMatches[taskResultMatches.length - 1];
// 在其中提取 <file> 内容
const fileMatch = lastTaskResult.match(/<file>([\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;
};