c81e24c977
- 详情弹窗改为「申报表 / 上传材料」两个页签,按模块配置分组渲染字段 - 明细数组(团队成员、指导老师、评审成员、团员记录、成果归档等)渲染为表格 - 新增附件解析工具与只读附件列表组件,兼容单地址、JSON 数组、对象数组 - 未来学术之星展示经费/中期/结题/成果归档材料,挑战杯拉取 gxmu_tzbcy_material - 未在配置中声明的字段兜底进「其他信息」,避免数据遗漏
119 lines
3.1 KiB
TypeScript
119 lines
3.1 KiB
TypeScript
export interface AttachmentFile {
|
|
name: string;
|
|
url: string;
|
|
}
|
|
|
|
const FILE_EXT_PATTERN =
|
|
/\.(pdf|docx?|xlsx?|pptx?|csv|txt|zip|rar|7z|jpe?g|png|gif|webp|bmp|svg|mp4|mp3|wav)$/i;
|
|
|
|
/**
|
|
* 从文件地址中取出展示用的文件名
|
|
*/
|
|
export function getAttachmentName(url?: string) {
|
|
if (!url) {
|
|
return '未命名文件';
|
|
}
|
|
const cleanUrl = url.split('?')[0];
|
|
const name = cleanUrl.split('/').pop() || cleanUrl;
|
|
try {
|
|
return decodeURIComponent(name);
|
|
} catch (e) {
|
|
return name;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 判断字符串是否像文件地址(http(s)、协议相对地址、带扩展名的相对路径)
|
|
*/
|
|
export function isFileUrl(value?: string) {
|
|
const text = value?.trim();
|
|
if (!text) {
|
|
return false;
|
|
}
|
|
if (/^(https?:)?\/\//i.test(text)) {
|
|
return true;
|
|
}
|
|
return FILE_EXT_PATTERN.test(text) && !/\s/.test(text);
|
|
}
|
|
|
|
export function isImageFile(url?: string) {
|
|
const text = (url || '').split('?')[0];
|
|
return /\.(jpe?g|png|gif|webp|bmp|svg)$/i.test(text);
|
|
}
|
|
|
|
const normalizeEntry = (item: unknown): AttachmentFile | null => {
|
|
if (typeof item === 'string') {
|
|
return isFileUrl(item)
|
|
? { name: getAttachmentName(item), url: item.trim() }
|
|
: null;
|
|
}
|
|
if (item && typeof item === 'object') {
|
|
const record = item as Record<string, unknown>;
|
|
const rawUrl = [
|
|
record.url,
|
|
record.fileUrl,
|
|
record.downloadUrl,
|
|
record.path
|
|
].find((value) => typeof value === 'string' && isFileUrl(value as string));
|
|
if (typeof rawUrl !== 'string') {
|
|
return null;
|
|
}
|
|
const rawName = [record.name, record.fileName, record.originalName].find(
|
|
(value) => typeof value === 'string' && (value as string).trim()
|
|
);
|
|
return {
|
|
name: (rawName as string | undefined) || getAttachmentName(rawUrl),
|
|
url: rawUrl.trim()
|
|
};
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* 把后端存储的附件字段解析成文件列表。
|
|
* 兼容:单个地址、JSON 数组字符串、对象数组(name/url、fileName/fileUrl 等)
|
|
*/
|
|
export function parseAttachments(value: unknown): AttachmentFile[] {
|
|
if (value === null || value === undefined || value === '') {
|
|
return [];
|
|
}
|
|
if (typeof value === 'string') {
|
|
const text = value.trim();
|
|
if (!text) {
|
|
return [];
|
|
}
|
|
if (text.startsWith('[') || text.startsWith('{')) {
|
|
try {
|
|
return parseAttachments(JSON.parse(text));
|
|
} catch (e) {
|
|
// 解析失败时按普通地址处理
|
|
}
|
|
}
|
|
const single = normalizeEntry(text);
|
|
return single ? [single] : [];
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value
|
|
.map((item) => normalizeEntry(item))
|
|
.filter((item): item is AttachmentFile => !!item);
|
|
}
|
|
const single = normalizeEntry(value);
|
|
return single ? [single] : [];
|
|
}
|
|
|
|
/**
|
|
* 把附件字段解析结果转换成图片地址列表
|
|
*/
|
|
export function parseAttachmentImages(value: unknown) {
|
|
const urls: string[] = [];
|
|
parseAttachments(value).forEach((item) => {
|
|
if (isImageFile(item.url)) {
|
|
urls.push(item.url);
|
|
}
|
|
});
|
|
if (!urls.length && typeof value === 'string' && isImageFile(value)) {
|
|
urls.push(value);
|
|
}
|
|
return urls;
|
|
}
|