File size: 6,093 Bytes
494c9e4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | /**
* Demo路径和文件名工具函数
* 统一管理所有与demo路径、文件名相关的工具函数
*/
import type { AnalyzeResponse } from '../api/GLTR_API';
import { tr } from '../lang/i18n-lite';
/**
* 标准化文件夹路径
* - 确保以 / 开头
* - 去除多余的连续斜杠
* - 去除尾部斜杠(除非是根路径 /)
* - 空字符串或 null/undefined 返回 '/'
*
* @param path 原始路径
* @returns 标准化后的路径,空路径返回 '/'
*
* @example
* normalizeFolderPath('') => '/'
* normalizeFolderPath(null) => '/'
* normalizeFolderPath('/folder1//folder2/') => '/folder1/folder2'
* normalizeFolderPath('folder1/folder2') => '/folder1/folder2'
*/
export const normalizeFolderPath = (path: string | null | undefined): string => {
if (!path || path.trim() === '' || path === '/') {
return '/';
}
const prefixed = path.startsWith('/') ? path : `/${path}`;
const condensed = prefixed.replace(/\/{2,}/g, '/');
return condensed.length > 1 && condensed.endsWith('/') ? condensed.slice(0, -1) : condensed;
};
/**
* 组合 demo 的完整路径
*
* @param folderPath 文件夹路径
* @param fileName 文件名(不含扩展名)
* @returns 完整路径,如果文件名为空则返回 null
*
* @example
* composeDemoFullPath('/', 'demo1') => '/demo1'
* composeDemoFullPath('/folder1', 'demo1') => '/folder1/demo1'
*/
export const composeDemoFullPath = (folderPath: string | null | undefined, fileName: string | null | undefined): string | null => {
if (!fileName || !fileName.trim()) {
return null;
}
const normalizedFolder = normalizeFolderPath(folderPath);
const safeFileName = fileName.trim();
return normalizedFolder === '/' ? `/${safeFileName}` : `${normalizedFolder}/${safeFileName}`;
};
/**
* 验证文件名合法性
* @param fileName 文件名(不含扩展名)
* @returns 验证结果:{ valid: boolean, message?: string }
*/
export const validateFileName = (fileName: string): { valid: boolean; message?: string } => {
if (!fileName || !fileName.trim()) {
return { valid: false, message: 'File name cannot be empty' };
}
const trimmed = fileName.trim();
// 检查长度
if (trimmed.length > 255) {
return { valid: false, message: 'File name too long (max 255 characters)' };
}
// Windows 和 Unix 系统都不允许的字符
const illegalChars = /[<>:"|?*\x00-\x1f]/;
if (illegalChars.test(trimmed)) {
return { valid: false, message: 'File name contains invalid characters (cannot contain < > : " | ? * or control characters)' };
}
// 检查保留名称(Windows)
const reservedNames = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)/i;
if (reservedNames.test(trimmed)) {
return { valid: false, message: 'File name cannot be a system reserved name' };
}
// 检查不能以点或空格开头/结尾(某些系统)
if (trimmed.startsWith('.') || trimmed.endsWith('.')) {
return { valid: false, message: 'File name cannot start or end with a dot' };
}
// 检查不能包含路径分隔符
if (trimmed.includes('/') || trimmed.includes('\\')) {
return { valid: false, message: 'File name cannot contain path separators' };
}
return { valid: true };
};
/**
* 生成 demo 默认名称:优先使用现有文件名,否则取分析文本第一行的前50个字符
*
* @param currentData 当前分析数据
* @param textFieldValue 文本输入框的值
* @param existingFileName 可选的现有文件名(如果提供且有效,则优先使用)
* @returns 默认名称
*/
export const getDefaultDemoName = (
currentData: AnalyzeResponse | null,
textFieldValue: string,
existingFileName?: string | null
): string => {
// 如果提供了现有文件名且有效,则使用它(去掉 .json 后缀)
if (existingFileName && existingFileName.trim() && existingFileName !== '未选择文件') {
const trimmed = existingFileName.trim();
// 去掉 .json 后缀(如果存在)
const nameWithoutExt = trimmed.toLowerCase().endsWith('.json')
? trimmed.slice(0, -5)
: trimmed;
if (nameWithoutExt) {
return nameWithoutExt;
}
}
// 否则,使用第一行逻辑(与主文本框一致不 trim)
const rawText = currentData ? currentData.request.text : textFieldValue || '';
if (!rawText) {
return '新Demo';
}
const firstLineBreak = rawText.search(/[\r\n]/);
const firstLine = firstLineBreak === -1 ? rawText : rawText.slice(0, firstLineBreak);
return (firstLine.length ? firstLine : '新Demo').slice(0, 50);
};
/**
* 构建保存目录选项(根目录优先)
* 统一文件夹排序和选项生成逻辑
*
* @param folders 文件夹列表
* @param lastPath 上次保存的路径(从 localStorage 获取)
* @returns 选项列表和默认路径
*/
export const buildFolderOptions = (
folders: string[],
lastPath: string | null
): { options: Array<{ value: string; text: string }>, defaultPath: string } => {
// 排序文件夹列表:根目录排在最前面
const sorted = [...folders].sort((a, b) => {
const normA = normalizeFolderPath(a);
const normB = normalizeFolderPath(b);
if (normA === '/' && normB !== '/') return -1;
if (normA !== '/' && normB === '/') return 1;
return normA.localeCompare(normB, 'zh-CN', { numeric: true, sensitivity: 'base' });
});
// 转换为选项格式
const options = sorted.map(folder => ({
value: folder,
text: folder === '' || folder === '/' ? tr('/ (Root)') : folder
}));
// 设置默认路径:优先使用上次保存的路径,如果不存在则使用根目录
let defaultPath = '/';
if (lastPath && sorted.includes(lastPath)) {
defaultPath = lastPath;
} else if (sorted.length > 0) {
defaultPath = sorted[0] || '/';
}
return { options, defaultPath };
};
|