Files
qiming/qiming-mobile/subpackages/utils/fileTree.uts

473 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
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 type { FileNode } from '@/types/interfaces/agent'
import { ACCESS_TOKEN } from '@/constants/home.constants'
import { COMMON_HEADERS } from '@/constants/common.constants'
import { t } from "@/utils/i18n";
/**
* 文件相关常量
*/
export const FILE_CONSTANTS = {
// 支持预览的文件扩展名白名单
SUPPORTED_EXTENSIONS: [
// 图片文件
'jpg',
'jpeg',
'png',
'gif',
'bmp',
'webp',
'svg',
'ico',
'tiff',
// 代码文件
'ts',
'tsx',
'js',
'jsx',
'mjs',
'cjs',
'css',
'less',
'scss',
'sass',
'html',
'htm',
'vue',
'json',
'jsonc',
'yaml',
'yml',
'xml',
'toml',
'ini',
'py',
'java',
'c',
'cpp',
'cs',
'php',
'rb',
'go',
'rs',
'swift',
'kt',
'scala',
'sh',
'bash',
'zsh',
'fish',
'ps1',
'bat',
'sql',
'dockerfile',
'makefile',
// 文本文件
'txt',
'md',
'markdown',
'log',
'csv',
'tsv',
'rtf',
],
// 忽略的文件模式
IGNORED_FILE_PATTERNS: [
/^\./, // 以 . 开头的隐藏文件
/^\.DS_Store$/,
/^Thumbs\.db$/,
/\.tmp$/,
/\.bak$/,
],
DEFAULT_FILE_LANGUAGE: 'Plain Text',
FALLBACK_SIZE: 0,
TREE_ROOT_LEVEL: 0,
INDENT_SIZE: 16,
REQUEST_ID_PREFIX: 'req_',
SESSION_ID_PREFIX: 'session_',
};
/**
* 将扁平的文件列表转换为树形结构
*/
export const transformFlatListToTree = (files: any[]): FileNode[] => {
const root: FileNode[] = [];
const map = new Map<string, FileNode>();
// 过滤掉系统文件
const filteredFiles = files.filter((file) => {
const fileName = file.name.split('/').pop();
return !FILE_CONSTANTS.IGNORED_FILE_PATTERNS.some((pattern) =>
pattern.test(fileName || ''),
);
});
// 创建所有文件节点和必要的文件夹节点
filteredFiles.forEach((file) => {
const pathParts = file.name.split('/').filter(Boolean);
const fileName = pathParts[pathParts.length - 1];
// 如果文件是目录则认为是文件后端给了isDir字段表示是否为目录兼容之前逻辑
const isFile = !file.isDir || fileName.includes('.');
const node: FileNode = {
id: file.name,
name: fileName,
type: isFile ? 'file' : 'folder',
path: file.name,
children: [],
binary: file.binary || false,
size:
file.size || file.sizeExceeded
? FILE_CONSTANTS.FALLBACK_SIZE
: file.contents?.length || FILE_CONSTANTS.FALLBACK_SIZE,
status: file.status || null,
fullPath: file.name,
parentPath: pathParts.slice(0, -1).join('/') || null,
content: file.contents || '',
lastModified: Date.now(),
fileProxyUrl: file?.fileProxyUrl || '',
isLink: file?.isLink || false,
};
map.set(file.name, node);
// 如果文件在子目录中,确保创建所有必要的父文件夹节点
if (pathParts.length > 1) {
for (let i = pathParts.length - 2; i >= 0; i--) {
const parentPath = pathParts.slice(0, i + 1).join('/');
const parentName = pathParts[i];
if (!map.has(parentPath)) {
const parentNode: FileNode = {
id: parentPath,
name: parentName,
type: 'folder',
path: parentPath,
children: [],
parentPath: i > 0 ? pathParts.slice(0, i).join('/') : null,
lastModified: Date.now(),
};
map.set(parentPath, parentNode);
}
}
}
});
// 构建层次结构
map.forEach((node) => {
if (node.parentPath && map.has(node.parentPath)) {
const parentNode = map.get(node.parentPath)!;
if (
!parentNode.children?.find((child: FileNode) => child.id === node.id)
) {
parentNode.children?.push(node);
}
} else if (!node.parentPath) {
if (!root.find((item: FileNode) => item.id === node.id)) {
root.push(node);
}
}
});
// 排序:文件夹在前,文件在后,同类型按名称排序
const sortNodes = (nodes: FileNode[]): FileNode[] => {
return nodes.sort((a, b) => {
if (a.type !== b.type) {
return a.type === 'folder' ? -1 : 1;
}
return a.name.localeCompare(b.name);
});
};
return sortNodes(root).map((node) => ({
...node,
children: node.children ? sortNodes(node.children) : undefined,
}));
};
/**
* 从响应头中解析文件名
* @param contentDisposition Content-Disposition 响应头
* @param defaultName 默认文件名
*/
const parseFileName = (contentDisposition: string | undefined, defaultName: string): string => {
if (!contentDisposition) return defaultName;
// 解析 Content-Disposition 头中的文件名
const filenameMatch = contentDisposition.match(
/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/,
);
if (filenameMatch && filenameMatch[1]) {
return filenameMatch[1].replace(/['"]/g, '');
}
return defaultName;
};
/**
* H5 环境:导出整个项目压缩包
* @param blob Blob 对象
* @param headers 响应头
* @param defaultName 默认文件名称
*/
// #ifdef H5 || WEB
export const exportWholeProjectZipH5 = async (blob: Blob, headers: any, defaultName: string) => {
// 从响应头中获取文件名
const contentDisposition = headers?.['content-disposition'];
const filename = parseFileName(contentDisposition, defaultName);
// 创建下载链接
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
// 触发下载
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 清理URL对象
window.URL.revokeObjectURL(url);
};
// #endif
/**
* 小程序环境下载文件并打包成zip
* @param downloadUrl 下载地址应该是返回zip包的接口
* @param filename 文件名称
* @returns 返回保存的zip文件路径
*/
// #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
export const downloadFileInMiniProgram = async (downloadUrl: string, filename: string): Promise<string> => {
return new Promise<string>((resolve, reject) => {
// 获取访问令牌并构建请求头
const accessToken = uni.getStorageSync(ACCESS_TOKEN);
const header: any = {
// ...COMMON_HEADERS,
};
if (accessToken) {
header['Authorization'] = `Bearer ${accessToken}`;
}
uni.showLoading({ title: t("Mobile.FileTree.downloading") });
uni.downloadFile({
url: downloadUrl,
header, // 添加请求头,确保认证通过
success: (res) => {
console.log('下载文件响应:', res);
if (res.statusCode === 200) {
// 验证下载的文件是否为zip格式
const fs = uni.getFileSystemManager();
const tempPath = res.tempFilePath;
// 检查文件信息
fs.stat({
path: tempPath,
success: (statRes) => {
console.log('文件信息:', statRes);
// 如果文件太小小于1KB可能是JSON错误响应
if (statRes.size < 1024) {
// 尝试读取文件内容检查是否为JSON
fs.readFile({
filePath: tempPath,
encoding: 'utf8',
success: (readRes) => {
try {
const content = readRes.data as string;
if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
// 是JSON响应说明下载失败
uni.hideLoading();
const errorData = JSON.parse(content);
uni.showToast({
title:
errorData.message ||
t("Mobile.FileTree.downloadServerError"),
icon: 'none',
duration: 3000,
});
reject(
new Error(
errorData.message ||
t("Mobile.FileTree.serverError"),
),
);
return;
}
} catch (e) {
// 不是JSON继续处理
}
},
fail: () => {
// 读取失败,继续保存流程
},
});
}
// 先使用 uni.saveFile 保存文件然后尝试重命名为正确的zip文件名
// 确保文件名有 .zip 扩展名
const zipFileName = filename.endsWith('.zip') ? filename : `${filename}.zip`;
uni.saveFile({
tempFilePath: tempPath,
success: (saveRes) => {
let savedFilePath = saveRes.savedFilePath;
console.log('初始保存路径:', savedFilePath);
// 如果保存的文件没有正确的扩展名,尝试重命名
if (!savedFilePath.endsWith('.zip')) {
// 获取文件目录和原文件名
const pathParts = savedFilePath.split('/');
const dirPath = pathParts.slice(0, -1).join('/');
const newPath = `${dirPath}/${zipFileName}`;
// 尝试重命名文件(异步方式)
fs.rename({
oldPath: savedFilePath,
newPath: newPath,
success: () => {
savedFilePath = newPath;
console.log('文件已重命名为:', savedFilePath);
finishSave(savedFilePath, zipFileName);
},
fail: (renameErr) => {
console.warn('重命名文件失败,尝试复制方式:', renameErr);
// 如果重命名失败,尝试读取原文件并写入新文件
fs.readFile({
filePath: savedFilePath,
success: (readFileRes) => {
fs.writeFile({
filePath: newPath,
data: readFileRes.data as ArrayBuffer,
success: () => {
// 删除旧文件
fs.unlink({
filePath: savedFilePath,
success: () => {
console.log('已删除旧文件');
},
fail: (e) => {
console.warn('删除旧文件失败:', e);
},
});
savedFilePath = newPath;
console.log('文件已复制并重命名为:', savedFilePath);
finishSave(savedFilePath, zipFileName);
},
fail: (writeErr) => {
console.error('写入新文件失败:', writeErr);
// 如果复制也失败,使用原路径
finishSave(savedFilePath, zipFileName);
},
});
},
fail: (readErr) => {
console.error('读取文件失败:', readErr);
// 如果读取失败,使用原路径
finishSave(savedFilePath, zipFileName);
},
});
},
});
} else {
// 已经有正确的扩展名,直接完成保存
finishSave(savedFilePath, zipFileName);
}
},
fail: (err) => {
uni.hideLoading();
console.error('保存ZIP文件失败:', err);
uni.showToast({
title: t("Mobile.FileTree.saveFailed"),
icon: 'none',
duration: 2000,
});
reject(err);
},
});
// 完成保存流程的辅助函数
const finishSave = (filePath: string, zipFileName: string) => {
uni.hideLoading();
// 弹窗提示用户文件保存位置
uni.showModal({
title: t("Mobile.FileTree.downloadComplete"),
content: t("Mobile.FileTree.zipSavedContent", {
fileName: zipFileName,
filePath,
}),
confirmText: t("Mobile.Common.open"),
cancelText: t("Mobile.Common.cancel"),
success: (modalRes) => {
if (modalRes.confirm) {
// 打开文件
uni.openDocument({
filePath: filePath,
fileType: 'zip',
showMenu: true,
success: () => {
console.log('打开文件成功');
},
fail: (err) => {
console.error('打开文件失败:', err);
uni.showToast({
title: t("Mobile.FileTree.openFailed"),
icon: 'none',
duration: 3000,
});
},
});
}
},
});
resolve(filePath);
};
},
fail: (statErr) => {
uni.hideLoading();
console.error('获取文件信息失败:', statErr);
uni.showToast({
title: t("Mobile.FileTree.fileValidateFailed"),
icon: 'none',
duration: 2000,
});
reject(statErr);
},
});
} else {
uni.hideLoading();
uni.showToast({
title: t("Mobile.FileTree.downloadFailedWithCode", {
code: res.statusCode,
}),
icon: 'none',
duration: 2000,
});
reject(
new Error(
t("Mobile.FileTree.downloadFailedWithCode", {
code: res.statusCode,
}),
),
);
}
},
fail: (err) => {
uni.hideLoading();
console.error('下载文件失败:', err);
uni.showToast({
title: t("Mobile.FileTree.downloadNetworkFailed"),
icon: 'none',
duration: 2000,
});
reject(err);
},
});
});
};
// #endif