feat(gxmu): 审核列表「查看数据」按申报表分组展示并新增上传材料页签
- 详情弹窗改为「申报表 / 上传材料」两个页签,按模块配置分组渲染字段 - 明细数组(团队成员、指导老师、评审成员、团员记录、成果归档等)渲染为表格 - 新增附件解析工具与只读附件列表组件,兼容单地址、JSON 数组、对象数组 - 未来学术之星展示经费/中期/结题/成果归档材料,挑战杯拉取 gxmu_tzbcy_material - 未在配置中声明的字段兜底进「其他信息」,避免数据遗漏
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user