+
{{
+ sheetHeading
+ }}
-
- {{ file.name }}
-
+
{{ section.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ formatCellValue(record[column.dataIndex as string])
+ }}
+
+
+
+
+
+
+ {{ line }}
+
+
+
+ {{ item.text }}
+
+
+ {{ item.text }}
+
+
+
-
-
-
-
-
第{{ index + 1 }}项
-
+
+
-
-
-
- {{ line }}
-
-
-
-
-
- {{ formatDisplayValue(item.key, item.value) }}
-
-
+
+
+
+
+
+
{{ group.label }}
+
+
+
+
+
+
+
+
@@ -83,6 +196,7 @@
import { message } from 'ant-design-vue';
import { toDateString } from 'ele-admin-pro';
import 'github-markdown-css/github-markdown-light.css';
+ import AttachmentList from '@/views/gxmu/components/attachmentList.vue';
import { getManuscript } from '@/api/cms/manuscript';
import { getQmgcForm } from '@/api/gxmu/qmgc-form';
import { getSjqnForm } from '@/api/gxmu/sjqn-form';
@@ -94,294 +208,135 @@
import { getWxxzxForm } from '@/api/gxmu/wxxzx-form';
import { getYxgqtdgbForm } from '@/api/gxmu/yxgqtdgb-form';
import { getYxgqtyForm } from '@/api/gxmu/yxgqty-form';
+ import { listTzbcyMaterial } from '@/api/gxmu/tzbcy-material';
+ import type { TzbcyMaterial } from '@/api/gxmu/tzbcy-material/model';
+ import {
+ COMMON_LABELS,
+ TABLE_FIELD_LABELS,
+ getModuleDetailConfig,
+ type ModuleDetailConfig
+ } from './moduleDetailConfig';
+ import {
+ isFileUrl,
+ parseAttachments,
+ parseAttachmentImages,
+ type AttachmentFile
+ } from '@/views/gxmu/utils/attachment';
- interface DescriptionItem {
+ type SheetFieldType =
+ | 'text'
+ | 'longtext'
+ | 'rich'
+ | 'image'
+ | 'files'
+ | 'table'
+ | 'object';
+
+ interface TableColumnConfig {
+ title: string;
+ dataIndex: string;
+ key: string;
+ file?: boolean;
+ }
+
+ interface SheetItem {
key: string;
label: string;
- value: unknown;
+ type: SheetFieldType;
span: number;
+ text?: string;
+ images?: string[];
+ files?: AttachmentFile[];
+ lines?: string[];
+ columns?: TableColumnConfig[];
+ rows?: Record
[];
}
- interface ModuleConfig {
+ interface SheetSection {
+ title: string;
+ items: SheetItem[];
+ }
+
+ interface MaterialGroup {
+ key: string;
+ label: string;
+ files: AttachmentFile[];
+ }
+
+ interface MaterialTableGroup {
+ key: string;
+ label: string;
+ rows: { title: string; files: AttachmentFile[] }[];
+ }
+
+ interface ModuleFetcher {
title: string;
fetcher: (id: number) => Promise>;
- labels: Record;
}
- const COMMON_LABELS: Record = {
- year: '年份',
- id: 'ID',
- title: '标题',
- content: '内容',
- cover: '封面',
- createTime: '创建时间',
- updateTime: '更新时间',
- status: '状态',
- sortNumber: '排序',
- provinceCity: '省市',
- schoolName: '学校名称',
- projectName: '项目名称',
- projectType: '项目类型',
- projectGroup: '项目分组',
- publicProjectType: '公开展示项目类型',
- publicProjectGroup: '公开展示项目分组',
- leader: '负责人',
- phone: '联系电话',
- teamMembers: '团队成员',
- advisors: '指导老师',
- projectBrief: '项目简介',
- socialValue: '社会价值',
- practiceProcess: '实践过程',
- innovationMeaning: '创新意义',
- developmentProspect: '发展前景',
- teamCooperation: '团队协作情况',
- projectMaterials: '项目佐证材料',
- otherProofs: '其他证明材料',
- projectSummary: '项目总结',
- teamIntro: '团队介绍',
- teamSlogan: '团队口号',
- practiceLog: '实践日志',
- name: '姓名',
- gender: '性别',
- nation: '民族',
- photo: '电子版照片',
- birthMonth: '出生年月',
- politics: '政治面貌',
- nativePlace: '籍贯',
- wechat: '微信号',
- email: '邮箱',
- qq: 'QQ',
- idCardNo: '身份证号',
- hobby: '兴趣爱好',
- willingToWorkInGuangxi: '是否愿意留桂工作',
- schoolInfo: '学校信息',
- leaguePosition: '团内职务',
- resume: '个人简历',
- awards: '获得奖励',
- academicPerformance: '学习成绩',
- secondaryLeagueOpinion: '二级团组织意见',
- secondaryLeagueOpinionDate: '二级团组织意见日期',
- secondaryPartyOpinion: '二级党组织意见',
- secondaryPartyOpinionDate: '二级党组织意见日期',
- schoolLeagueOpinion: '学校团组织意见',
- schoolLeagueOpinionDate: '学校团组织意见日期',
- applyType: '申报类别',
- education: '学历',
- position: '职务',
- unit: '工作单位',
- experience: '主要经历',
- mainStory: '主要事迹',
- leagueOpinion: '团组织意见',
- partyOpinion: '党组织意见',
- schoolOpinion: '学校意见',
- collegeClass: '学院班级',
- branch: '团支部名称',
- eduEval: '教育评议情况',
- serialNo: '序号',
- archiveInCurrentOrg: '团籍是否在本组织',
- joinMonth: '入团时间',
- memberRecords: '团员记录',
- growthArchives: '成长档案',
- orgName: '组织名称',
- memberTotal: '团员总数',
- memberDeveloped2024: '上一年度发展团员数',
- smartSystemLogin: '智慧团建系统录入情况',
- committeeCount: '团委委员数',
- fulltimeCadreCount: '专职团干部数',
- parttimeCadreCount: '兼职团干部数',
- lastElectionTime: '最近一次换届时间',
- feeReceivable2024: '上一年度应收团费',
- feeReceived2024: '上一年度实收团费',
- feePayable2024: '上一年度应缴团费',
- feePaid2024: '上一年度实缴团费',
- branchCount: '团支部数量',
- standardizedWork2024: '上一年度标准化建设开展情况',
- recommendActivist2024: '上一年度推优入党人数',
- activistConfirmed: '入党积极分子确定情况',
- recommendDevTarget2024: '上一年度推荐发展对象人数',
- devTargetConfirmed: '发展对象确定情况',
- honorsFiveYears: '近五年获奖情况',
- workSummaryThreeYears: '近三年工作总结',
- branchName: '团支部名称',
- secondOrg: '所属二级党组织',
- secretary: '团支部书记',
- contact: '联系方式',
- establishTime: '成立时间',
- branchCommitteeMeetingCount: '支委会次数',
- branchMemberMeetingCount: '支部大会次数',
- eduEvalDone: '教育评议是否完成',
- annualRegDone: '年度团籍注册是否完成',
- classCount: '班级数量',
- smartSystem100: '智慧团建系统覆盖率',
- topicName: '课题名称',
- collegeGradeClass: '学院年级班级',
- advisor: '指导老师',
- topicType: '课题类别',
- guidanceTeachers: '指导教师组',
- applicants: '申请人信息',
- budgets: '经费预算',
- rationale: '立项依据',
- teacherOpinion: '指导教师意见',
- teacherSign: '指导教师签名',
- reviewOpinion: '评审意见',
- reviewScore: '评审成绩',
- reviewLeaderSign: '评审负责人签名',
- collegeOpinion: '学院意见',
- promiseSigner: '承诺签字人',
- promiseSignature: '承诺书签名',
- reviewCollege: '评审学院',
- reviewReporter: '汇报人',
- reviewDate: '评审日期',
- reviewMembers: '评审成员',
- fundingBudgetFile: '经费预算表',
- fundingAllocationFile: '经费划拨明细表',
- midtermRemark: '中期检查说明',
- midtermMaterials: '中期检查材料',
- finalRemark: '结题说明',
- finalMaterials: '结题材料',
- archiveAwards: '获奖情况',
- archiveProofs: '佐证材料',
- archiveRecords: '成果归档',
- grade: '年级',
- className: '班级',
- major: '专业',
- role: '角色',
- item: '项目',
- amount: '金额',
- reason: '原因',
- dept: '部门',
- sign: '签名',
- department: '部门',
- remark: '备注',
- type: '类型',
- recordTime: '时间',
- archiveTime: '时间',
- identity: '身份',
- organization: '所在组织',
- memberNo: '团员编号',
- currentDutyTime: '现任职务时间',
- cadreYears: '从事团学工作年限',
- assessment2024: '上一年度考核情况',
- volunteerRegTime: '志愿汇注册时间',
- cadreExperience: '团学工作经历',
- joinTime: '入团时间',
- collegeMajorClass: '学院专业班级',
- eduEval2024: '上一年度教育评议情况',
- smartSystem: '智慧团建系统录入情况',
- totalVolunteerHours: '累计志愿服务时长',
- volunteerHours2024: '上一年度志愿服务时长',
- duty: '职务',
- college: '学院',
- gradeMajor: '年级专业',
- birthDate: '出生日期',
- branchOrganizationName: '所属团支部'
- };
-
- const MODULE_CONFIGS: Record = {
+ const MODULE_FETCHERS: Record = {
cms_manuscript: {
title: '稿件',
- fetcher: (id) => getManuscript(id) as Promise>,
- labels: {
- ...COMMON_LABELS
- }
+ fetcher: (id) => getManuscript(id) as Promise>
},
gxmu_sjqn: {
title: '十佳青年岗位能手',
- fetcher: (id) => getSjqnForm(id) as Promise>,
- labels: {
- ...COMMON_LABELS
- }
+ fetcher: (id) => getSjqnForm(id) as Promise>
},
gxmu_sjtbzbsj: {
title: '十佳团支部书记',
- fetcher: (id) => getSjtbzbsjForm(id) as Promise>,
- labels: {
- ...COMMON_LABELS
- }
+ fetcher: (id) => getSjtbzbsjForm(id) as Promise>
},
gxmu_wshqtw: {
title: '五四红旗团委',
- fetcher: (id) => getWshqtwForm(id) as Promise>,
- labels: {
- ...COMMON_LABELS
- }
+ fetcher: (id) => getWshqtwForm(id) as Promise>
},
gxmu_wshqtzb: {
title: '五四红旗团支部',
- fetcher: (id) => getWshqtzbForm(id) as Promise>,
- labels: {
- ...COMMON_LABELS
- }
+ fetcher: (id) => getWshqtzbForm(id) as Promise>
},
gxmu_yxgqtdgb: {
title: '优秀共青团干部',
- fetcher: (id) => getYxgqtdgbForm(id) as Promise>,
- labels: {
- ...COMMON_LABELS
- }
+ fetcher: (id) => getYxgqtdgbForm(id) as Promise>
},
gxmu_yxgqty: {
title: '优秀共青团员',
- fetcher: (id) => getYxgqtyForm(id) as Promise>,
- labels: {
- ...COMMON_LABELS
- }
+ fetcher: (id) => getYxgqtyForm(id) as Promise>
},
gxmu_tzbcy_form: {
title: '挑战杯',
- fetcher: (id) => getTzbcyForm(id) as Promise>,
- labels: {
- ...COMMON_LABELS
- }
+ fetcher: (id) => getTzbcyForm(id) as Promise>
},
gxmu_qmgc_form: {
title: '青马工程',
- fetcher: (id) => getQmgcForm(id) as Promise>,
- labels: {
- ...COMMON_LABELS
- }
+ fetcher: (id) => getQmgcForm(id) as Promise>
},
gxmu_wxxzx_project: {
title: '未来学术之星',
- fetcher: (id) => getWxxzxForm(id) as Promise>,
- labels: {
- ...COMMON_LABELS,
- title: '职称'
- }
+ fetcher: (id) => getWxxzxForm(id) as Promise>
},
gxmu_tygl_form: {
title: '团员管理',
- fetcher: (id) => getTyglForm(id) as Promise>,
- labels: {
- ...COMMON_LABELS
- }
+ fetcher: (id) => getTyglForm(id) as Promise>
},
gxmu_tygl: {
title: '团员管理',
- fetcher: (id) => getTyglForm(id) as Promise>,
- labels: {
- ...COMMON_LABELS
- }
+ fetcher: (id) => getTyglForm(id) as Promise>
}
};
+ // 不在申报表中展示的字段
const HIDDEN_FIELDS = new Set([
+ 'id',
'reviewList',
'deleted',
'tenantId',
'userId',
- 'branchOrganizationId'
+ 'branchOrganizationId',
+ 'contentFormat'
]);
const IMAGE_FIELDS = new Set(['photo', 'cover', 'promiseSignature']);
const RICH_TEXT_FIELDS = new Set(['content']);
- const DATE_TIME_FIELDS = new Set(['createTime', 'updateTime']);
- const FILE_FIELDS = new Set([
- 'fundingBudgetFile',
- 'fundingAllocationFile',
- 'midtermMaterials',
- 'finalMaterials',
- 'archiveProofs'
- ]);
const WIDE_FIELDS = new Set([
'projectBrief',
'socialValue',
@@ -432,13 +387,32 @@
const maxable = ref(true);
const detail = ref>({});
const loadFailed = ref(false);
+ const activeTab = ref('sheet');
+ const tzbcyMaterials = ref([]);
+ const materialLoading = ref(false);
- const currentConfig = computed(() => {
- return props.module ? MODULE_CONFIGS[props.module] : undefined;
- });
+ const currentConfig = computed(() =>
+ getModuleDetailConfig(props.module)
+ );
const modalTitle = computed(() => {
- return `${props.moduleName || currentConfig.value?.title || '数据'}详情`;
+ return `${
+ props.moduleName ||
+ currentConfig.value?.title ||
+ MODULE_FETCHERS[props.module || '']?.title ||
+ '数据'
+ }详情`;
+ });
+
+ const sheetTabLabel = computed(() =>
+ props.module === 'cms_manuscript' ? '稿件内容' : '申报表'
+ );
+
+ const showEmpty = computed(() => {
+ if (loadFailed.value || !currentConfig.value) {
+ return true;
+ }
+ return !sheetSections.value.length && !showMaterialTab.value;
});
const emptyDescription = computed(() => {
@@ -451,17 +425,6 @@
return '暂无可展示数据';
});
- const getFieldLabel = (key: string) => {
- const label = currentConfig.value?.labels[key];
- if (label) {
- return label;
- }
- return key
- .replace(/([A-Z])/g, ' $1')
- .replace(/^./, (text) => text.toUpperCase())
- .trim();
- };
-
const isPlainObject = (value: unknown): value is Record => {
return Object.prototype.toString.call(value) === '[object Object]';
};
@@ -482,91 +445,25 @@
return true;
};
- const isWideField = (key: string) => {
- return WIDE_FIELDS.has(key);
+ const getFieldLabel = (key: string) => {
+ const label =
+ currentConfig.value?.labels[key] ||
+ COMMON_LABELS[key] ||
+ TABLE_FIELD_LABELS[key];
+ if (label) {
+ return label;
+ }
+ return key
+ .replace(/([A-Z])/g, ' $1')
+ .replace(/^./, (text) => text.toUpperCase())
+ .trim();
};
- const isImageField = (key: string) => {
- return IMAGE_FIELDS.has(key);
- };
-
- const isFileField = (key: string) => {
- return FILE_FIELDS.has(key);
- };
-
- const isRichTextField = (key: string) => {
- return props.module === 'cms_manuscript' && RICH_TEXT_FIELDS.has(key);
- };
-
- const getFileName = (url?: string) => {
- if (!url) {
- return '未命名文件';
- }
- const cleanUrl = url.split('?')[0];
- return decodeURIComponent(cleanUrl.split('/').pop() || cleanUrl);
- };
-
- const normalizeFileEntries = (value: unknown) => {
- if (Array.isArray(value)) {
- return value
- .map((item) => {
- if (typeof item === 'string' && item.trim()) {
- return {
- name: getFileName(item),
- url: item
- };
- }
- if (
- isPlainObject(item) &&
- typeof item.url === 'string' &&
- item.url.trim()
- ) {
- return {
- name:
- (typeof item.name === 'string' && item.name.trim()) ||
- getFileName(item.url),
- url: item.url
- };
- }
- return null;
- })
- .filter((item): item is { name: string; url: string } => !!item);
- }
- if (typeof value === 'string' && value.trim()) {
- return [
- {
- name: getFileName(value),
- url: value
- }
- ];
- }
- return [];
- };
-
- const normalizeDetailValue = (key: string, value: unknown) => {
- if (!isFileField(key) || typeof value !== 'string') {
- return value;
- }
- const text = value.trim();
- if (!text) {
- return value;
- }
- if (key === 'fundingBudgetFile' || key === 'fundingAllocationFile') {
- return text;
- }
- try {
- const parsed = JSON.parse(text);
- return Array.isArray(parsed) ? parsed : text;
- } catch (e) {
- return text;
- }
- };
-
- const formatSimpleValue = (value: unknown) => {
+ const formatDateTime = (value: unknown) => {
if (value === null || value === undefined || value === '') {
return '-';
}
- return String(value);
+ return toDateString(String(value), 'yyyy-MM-dd HH:mm:ss');
};
const formatStatusValue = (value: unknown) => {
@@ -578,74 +475,355 @@
return '显示';
}
}
- return formatSimpleValue(value);
+ return String(value);
};
- const formatDateTimeValue = (value: unknown) => {
+ const formatCellValue = (value: unknown) => {
if (value === null || value === undefined || value === '') {
return '-';
}
- return toDateString(String(value), 'yyyy-MM-dd HH:mm:ss');
- };
-
- const formatDisplayValue = (key: string, value: unknown) => {
- if (DATE_TIME_FIELDS.has(key)) {
- return formatDateTimeValue(value);
+ if (Array.isArray(value)) {
+ return value.join('、');
}
- if (key === 'status') {
- return formatStatusValue(value);
- }
- return formatSimpleValue(value);
- };
-
- const formatRichText = (value: unknown) => {
- if (value === null || value === undefined || value === '') {
- return '-
';
+ if (isPlainObject(value)) {
+ return JSON.stringify(value);
}
return String(value);
};
- const formatObjectLines = (value: Record) => {
- return Object.keys(value)
- .filter((key) => !HIDDEN_FIELDS.has(key) && hasVisibleValue(value[key]))
- .map((key) => `${getFieldLabel(key)}:${formatDisplayValue(key, value[key])}`);
- };
-
- const formatArrayEntry = (entry: unknown) => {
- if (isPlainObject(entry)) {
- const lines = formatObjectLines(entry);
- return lines.length ? lines : ['-'];
+ const formatDisplayValue = (key: string, value: unknown) => {
+ if (key === 'createTime' || key === 'updateTime') {
+ return formatDateTime(value);
}
- return [formatSimpleValue(entry)];
+ if (key === 'status') {
+ return formatStatusValue(value);
+ }
+ return formatCellValue(value);
};
- const descriptionItems = computed(() => {
- const source = detail.value || {};
- const orderedKeys = Object.keys(currentConfig.value?.labels || {}).filter(
- (key) => !HIDDEN_FIELDS.has(key) && hasVisibleValue(source[key])
- );
- const extraKeys = Object.keys(source).filter(
- (key) =>
- !HIDDEN_FIELDS.has(key) &&
- hasVisibleValue(source[key]) &&
- !orderedKeys.includes(key)
- );
+ /**
+ * 判断字段值是否是一组附件(全部为文件地址时才认为命中)
+ */
+ const isAttachmentValue = (value: unknown) => {
+ if (typeof value === 'string' || Array.isArray(value)) {
+ const files = parseAttachments(value);
+ return files.length > 0;
+ }
+ return false;
+ };
- return [...orderedKeys, ...extraKeys].map((key) => ({
- key,
- label: getFieldLabel(key),
- value: normalizeDetailValue(key, source[key]),
- span:
- isWideField(key) ||
- isFileField(key) ||
- isImageField(key) ||
- Array.isArray(normalizeDetailValue(key, source[key])) ||
- isPlainObject(normalizeDetailValue(key, source[key]))
- ? 2
- : 1
- }));
+ /**
+ * 上传材料的字段集合:配置声明 + 自动识别
+ */
+ const materialKeySet = computed(() => {
+ const keys = new Set(currentConfig.value?.materialFields || []);
+ Object.keys(detail.value).forEach((key) => {
+ if (HIDDEN_FIELDS.has(key) || keys.has(key)) {
+ return;
+ }
+ if (IMAGE_FIELDS.has(key)) {
+ return;
+ }
+ if (isAttachmentValue(detail.value[key])) {
+ keys.add(key);
+ }
+ });
+ return keys;
});
+ const showMaterialTab = computed(() => {
+ if (!currentConfig.value) {
+ return false;
+ }
+ if (materialKeySet.value.size) {
+ return true;
+ }
+ if (currentConfig.value.materialTables?.length) {
+ return true;
+ }
+ return currentConfig.value.materialSource === 'tzbcy';
+ });
+
+ const sheetHeading = computed(() => {
+ const name = currentConfig.value?.sheetName;
+ if (!name) {
+ return '';
+ }
+ const year = detail.value.year ? `${detail.value.year}年` : '';
+ const noSuffix = props.module === 'cms_manuscript' || name.endsWith('表');
+ return `${year}“${name}”${noSuffix ? '' : '申报表'}`;
+ });
+
+ const buildTableColumns = (rows: Record[]) => {
+ const columnKeys: string[] = [];
+ rows.forEach((row) => {
+ Object.keys(row).forEach((key) => {
+ if (HIDDEN_FIELDS.has(key) || key === '__rowKey') {
+ return;
+ }
+ if (!columnKeys.includes(key)) {
+ columnKeys.push(key);
+ }
+ });
+ });
+ return columnKeys.map((key) => ({
+ title: getFieldLabel(key),
+ dataIndex: key,
+ key,
+ file: key === 'proofFile'
+ }));
+ };
+
+ const buildSheetItem = (key: string, value: unknown): SheetItem => {
+ const label = getFieldLabel(key);
+ if (RICH_TEXT_FIELDS.has(key)) {
+ return {
+ key,
+ label,
+ type: 'rich',
+ span: 2,
+ text: String(value ?? '')
+ };
+ }
+ if (IMAGE_FIELDS.has(key)) {
+ const images = parseAttachmentImages(value);
+ if (images.length) {
+ return { key, label, type: 'image', span: 2, images };
+ }
+ // 非图片(如 pdf 扫描件)按附件展示
+ if (isFileUrl(String(value ?? ''))) {
+ return {
+ key,
+ label,
+ type: 'files',
+ span: 2,
+ files: parseAttachments(value)
+ };
+ }
+ return {
+ key,
+ label,
+ type: 'image',
+ span: 2,
+ images: []
+ };
+ }
+ if (Array.isArray(value)) {
+ const files = parseAttachments(value);
+ const allObjects = value.every((item) => isPlainObject(item));
+ if (files.length && !allObjects) {
+ return { key, label, type: 'files', span: 2, files };
+ }
+ if (allObjects) {
+ const rows = (value as Record[]).map((row, index) => ({
+ ...row,
+ __rowKey: `${key}-${index}`
+ }));
+ return {
+ key,
+ label,
+ type: 'table',
+ span: 2,
+ rows,
+ columns: buildTableColumns(rows)
+ };
+ }
+ return {
+ key,
+ label,
+ type: 'longtext',
+ span: 2,
+ text: value.map((item) => formatCellValue(item)).join('、')
+ };
+ }
+ if (isPlainObject(value)) {
+ const lines = Object.keys(value)
+ .filter(
+ (item) => !HIDDEN_FIELDS.has(item) && hasVisibleValue(value[item])
+ )
+ .map(
+ (item) => `${getFieldLabel(item)}:${formatCellValue(value[item])}`
+ );
+ return {
+ key,
+ label,
+ type: 'object',
+ span: 2,
+ lines: lines.length ? lines : ['-']
+ };
+ }
+ const text = formatDisplayValue(key, value);
+ const isLong =
+ WIDE_FIELDS.has(key) ||
+ (typeof value === 'string' &&
+ (value.includes('\n') || value.length > 60));
+ return {
+ key,
+ label,
+ type: isLong ? 'longtext' : 'text',
+ span: isLong ? 2 : 1,
+ text
+ };
+ };
+
+ const sheetSections = computed(() => {
+ const source = detail.value || {};
+ const config = currentConfig.value;
+ const usedKeys = new Set();
+ const sections: SheetSection[] = [];
+ const sectionsConfig = config?.sections?.length
+ ? config.sections
+ : [{ title: '全部字段', fields: Object.keys(source) }];
+
+ sectionsConfig.forEach((section) => {
+ const items: SheetItem[] = [];
+ section.fields.forEach((key) => {
+ usedKeys.add(key);
+ if (HIDDEN_FIELDS.has(key) || materialKeySet.value.has(key)) {
+ return;
+ }
+ const value = source[key];
+ if (!hasVisibleValue(value)) {
+ return;
+ }
+ items.push(buildSheetItem(key, value));
+ });
+ if (items.length) {
+ sections.push({ title: section.title, items });
+ }
+ });
+
+ // 兜底:未在配置中声明的字段归入其他信息,避免数据遗漏
+ const extraItems: SheetItem[] = Object.keys(source)
+ .filter((key) => {
+ if (usedKeys.has(key) || HIDDEN_FIELDS.has(key)) {
+ return false;
+ }
+ if (key === 'createTime' || key === 'updateTime') {
+ return false;
+ }
+ if (materialKeySet.value.has(key)) {
+ return false;
+ }
+ return hasVisibleValue(source[key]);
+ })
+ .map((key) => buildSheetItem(key, source[key]));
+ if (extraItems.length) {
+ sections.push({ title: '其他信息', items: extraItems });
+ }
+ return sections;
+ });
+
+ const materialGroups = computed(() => {
+ const config = currentConfig.value;
+ if (!config) {
+ return [];
+ }
+ const keys = new Set(config.materialFields || []);
+ materialKeySet.value.forEach((key) => keys.add(key));
+ return Array.from(keys)
+ .filter((key) => key !== 'photo' && key !== 'cover')
+ .map((key) => ({
+ key,
+ label: getFieldLabel(key),
+ files: parseAttachments(detail.value[key])
+ }))
+ .filter((group) => group.files.length > 0);
+ });
+
+ const materialTableGroups = computed(() => {
+ const config = currentConfig.value;
+ if (!config?.materialTables?.length) {
+ return [];
+ }
+ return config.materialTables.map((table) => {
+ const rows = Array.isArray(detail.value[table.field])
+ ? (detail.value[table.field] as Record[])
+ : [];
+ return {
+ key: table.field,
+ label: table.label,
+ rows: rows
+ .map((row) => ({
+ title:
+ (table.titleField
+ ? formatCellValue(row[table.titleField])
+ : '') || getFieldLabel(table.field),
+ files: parseAttachments(row[table.fileField])
+ }))
+ .filter((row) => row.files.length > 0)
+ };
+ });
+ });
+
+ const showTzbcyMaterials = computed(
+ () => currentConfig.value?.materialSource === 'tzbcy'
+ );
+
+ const tzbcyMaterialColumns = [
+ { title: '材料类型', dataIndex: 'materialType', key: 'materialType' },
+ { title: '文件名称', dataIndex: 'fileName', key: 'fileName' },
+ { title: '审核状态', dataIndex: 'status', key: 'status', width: 100 },
+ { title: '驳回原因', dataIndex: 'rejectReason', key: 'rejectReason' },
+ {
+ title: '上传时间',
+ dataIndex: 'createTime',
+ key: 'createTime',
+ width: 170
+ }
+ ];
+
+ const getMaterialStatusText = (status?: number) => {
+ if (status === 1) {
+ return '通过';
+ }
+ if (status === 2) {
+ return '驳回';
+ }
+ return '待审核';
+ };
+
+ const getMaterialStatusColor = (status?: number) => {
+ if (status === 1) {
+ return 'green';
+ }
+ if (status === 2) {
+ return 'red';
+ }
+ return 'default';
+ };
+
+ const isFileColumn = (column: Record) => {
+ return !!column.file;
+ };
+
+ const footerLines = computed(() => {
+ const lines: string[] = [];
+ if (hasVisibleValue(detail.value.createTime)) {
+ lines.push(`提交时间:${formatDateTime(detail.value.createTime)}`);
+ }
+ if (hasVisibleValue(detail.value.updateTime)) {
+ lines.push(`更新时间:${formatDateTime(detail.value.updateTime)}`);
+ }
+ return lines;
+ });
+
+ const loadTzbcyMaterials = async () => {
+ if (!props.pk) {
+ tzbcyMaterials.value = [];
+ return;
+ }
+ materialLoading.value = true;
+ try {
+ tzbcyMaterials.value = await listTzbcyMaterial({ formId: props.pk });
+ } catch (e: any) {
+ tzbcyMaterials.value = [];
+ message.error(e.message || '申报材料加载失败');
+ } finally {
+ materialLoading.value = false;
+ }
+ };
+
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
@@ -656,6 +834,8 @@
}
detail.value = {};
loadFailed.value = false;
+ activeTab.value = 'sheet';
+ tzbcyMaterials.value = [];
if (!props.pk || !currentConfig.value) {
if (props.module && !currentConfig.value) {
@@ -666,7 +846,15 @@
loading.value = true;
try {
- detail.value = (await currentConfig.value.fetcher(props.pk)) || {};
+ const fetcher = currentConfig.value
+ ? MODULE_FETCHERS[props.module || '']
+ : undefined;
+ if (fetcher) {
+ detail.value = (await fetcher.fetcher(props.pk)) || {};
+ }
+ if (currentConfig.value.materialSource === 'tzbcy') {
+ await loadTzbcyMaterials();
+ }
} catch (e: any) {
loadFailed.value = true;
message.error(e.message || '数据加载失败');
@@ -682,6 +870,7 @@
loadDetail();
} else {
detail.value = {};
+ tzbcyMaterials.value = [];
loadFailed.value = false;
loading.value = false;
}
@@ -690,15 +879,67 @@
);
+
+
diff --git a/src/views/gxmu/reviewList/components/moduleDetailConfig.ts b/src/views/gxmu/reviewList/components/moduleDetailConfig.ts
new file mode 100644
index 0000000..44b66da
--- /dev/null
+++ b/src/views/gxmu/reviewList/components/moduleDetailConfig.ts
@@ -0,0 +1,904 @@
+/**
+ * 审核列表「查看数据」详情的申报表配置。
+ *
+ * sections 决定申报表的字段分组与展示顺序,materialFields 声明需要在上传材料
+ * 中展示的附件字段,未列出的字段会兜底追加到「其他信息」分组,避免数据丢失。
+ */
+
+export interface ModuleSection {
+ // 分组标题
+ title: string;
+ // 分组内字段(按展示顺序)
+ fields: string[];
+}
+
+export interface ModuleMaterialTable {
+ // 明细数组字段
+ field: string;
+ // 明细中保存附件地址的字段
+ fileField: string;
+ // 明细标题字段
+ titleField?: string;
+ // 材料分组标题
+ label: string;
+}
+
+export interface ModuleDetailConfig {
+ // 模块名称
+ title: string;
+ // 申报表名称,用于拼接表单标题
+ sheetName: string;
+ // 字段中文名
+ labels: Record;
+ // 申报表分组
+ sections: ModuleSection[];
+ // 附件字段(上传材料)
+ materialFields: string[];
+ // 明细数组中的附件
+ materialTables?: ModuleMaterialTable[];
+ // 额外的材料数据来源
+ materialSource?: 'tzbcy';
+}
+
+/**
+ * 通用字段中文名
+ */
+export const COMMON_LABELS: Record = {
+ year: '年份',
+ id: 'ID',
+ title: '标题',
+ content: '内容',
+ contentFormat: '正文格式',
+ cover: '封面',
+ createTime: '提交时间',
+ updateTime: '更新时间',
+ status: '状态',
+ sortNumber: '排序',
+ provinceCity: '省市',
+ schoolName: '学校名称',
+ projectName: '项目名称',
+ projectType: '项目类型',
+ projectGroup: '项目分组',
+ publicProjectType: '公开展示项目类型',
+ publicProjectGroup: '公开展示项目分组',
+ leader: '负责人',
+ phone: '联系电话',
+ teamMembers: '团队成员',
+ advisors: '指导老师',
+ projectBrief: '项目简介',
+ socialValue: '社会价值',
+ practiceProcess: '实践过程',
+ innovationMeaning: '创新意义',
+ developmentProspect: '发展前景',
+ teamCooperation: '团队协作情况',
+ projectMaterials: '项目介绍材料',
+ otherProofs: '其他相关证明材料',
+ projectSummary: '项目总结',
+ teamIntro: '团队介绍',
+ teamSlogan: '团队口号',
+ practiceLog: '实践日志',
+ name: '姓名',
+ gender: '性别',
+ nation: '民族',
+ photo: '电子版照片',
+ birthMonth: '出生年月',
+ politics: '政治面貌',
+ nativePlace: '籍贯',
+ wechat: '微信号',
+ email: '邮箱',
+ qq: 'QQ',
+ idCardNo: '身份证号',
+ hobby: '兴趣爱好',
+ willingToWorkInGuangxi: '是否愿意留桂工作',
+ schoolInfo: '学校信息',
+ leaguePosition: '团内职务',
+ resume: '个人简历',
+ awards: '获得奖励',
+ academicPerformance: '学习成绩',
+ secondaryLeagueOpinion: '二级团组织意见',
+ secondaryLeagueOpinionDate: '二级团组织意见日期',
+ secondaryPartyOpinion: '二级党组织意见',
+ secondaryPartyOpinionDate: '二级党组织意见日期',
+ schoolLeagueOpinion: '学校团组织意见',
+ schoolLeagueOpinionDate: '学校团组织意见日期',
+ applyType: '申报类别',
+ education: '学历',
+ position: '职务',
+ unit: '工作单位',
+ experience: '主要经历',
+ mainStory: '主要事迹',
+ leagueOpinion: '团组织意见',
+ partyOpinion: '党组织意见',
+ schoolOpinion: '学校意见',
+ collegeClass: '学院班级',
+ branch: '团支部名称',
+ eduEval: '教育评议情况',
+ serialNo: '序号',
+ archiveInCurrentOrg: '团籍是否在本组织',
+ joinMonth: '入团时间',
+ memberRecords: '团员记录',
+ growthArchives: '成长档案',
+ orgName: '组织名称',
+ memberTotal: '团员总数',
+ memberDeveloped2024: '上一年度发展团员数',
+ smartSystemLogin: '智慧团建系统录入情况',
+ committeeCount: '团委委员数',
+ fulltimeCadreCount: '专职团干部数',
+ parttimeCadreCount: '兼职团干部数',
+ lastElectionTime: '最近一次换届时间',
+ feeReceivable2024: '上一年度应收团费',
+ feeReceived2024: '上一年度实收团费',
+ feePayable2024: '上一年度应缴团费',
+ feePaid2024: '上一年度实缴团费',
+ branchCount: '团支部数量',
+ standardizedWork2024: '上一年度标准化建设开展情况',
+ recommendActivist2024: '上一年度推优入党人数',
+ activistConfirmed: '入党积极分子确定情况',
+ recommendDevTarget2024: '上一年度推荐发展对象人数',
+ devTargetConfirmed: '发展对象确定情况',
+ honorsFiveYears: '近五年获奖情况',
+ workSummaryThreeYears: '近三年工作总结',
+ branchName: '团支部名称',
+ secondOrg: '所属二级党组织',
+ secretary: '团支部书记',
+ contact: '联系方式',
+ establishTime: '成立时间',
+ branchCommitteeMeetingCount: '支委会次数',
+ branchMemberMeetingCount: '支部大会次数',
+ eduEvalDone: '教育评议是否完成',
+ annualRegDone: '年度团籍注册是否完成',
+ classCount: '班级数量',
+ smartSystem100: '智慧团建系统覆盖率',
+ topicName: '课题名称',
+ collegeGradeClass: '学院年级班级',
+ advisor: '指导老师',
+ topicType: '课题类别',
+ guidanceTeachers: '指导教师组',
+ applicants: '申请人信息',
+ budgets: '经费预算',
+ rationale: '立项依据',
+ teacherOpinion: '指导教师意见',
+ teacherSign: '指导教师签名',
+ reviewOpinion: '评审意见',
+ reviewScore: '评审成绩',
+ reviewLeaderSign: '评审负责人签名',
+ collegeOpinion: '学院意见',
+ promiseSigner: '承诺签字人',
+ promiseSignature: '承诺书签名',
+ reviewCollege: '评审学院',
+ reviewReporter: '汇报人',
+ reviewDate: '评审日期',
+ reviewMembers: '评审成员',
+ fundingBudgetFile: '经费预算表',
+ fundingAllocationFile: '经费划拨明细表',
+ midtermRemark: '中期检查说明',
+ midtermMaterials: '中期检查材料',
+ finalRemark: '结题说明',
+ finalMaterials: '结题材料',
+ archiveAwards: '获奖情况',
+ archiveProofs: '佐证材料',
+ archiveRecords: '成果归档',
+ grade: '年级',
+ className: '班级',
+ major: '专业',
+ role: '角色',
+ item: '项目',
+ amount: '金额',
+ reason: '原因',
+ dept: '部门',
+ sign: '签名',
+ department: '部门',
+ remark: '备注',
+ type: '类型',
+ recordTime: '时间',
+ archiveTime: '时间',
+ identity: '身份',
+ organization: '所在组织',
+ memberNo: '团员编号',
+ currentDutyTime: '现任职务时间',
+ cadreYears: '从事团学工作年限',
+ assessment2024: '上一年度考核情况',
+ volunteerRegTime: '志愿汇注册时间',
+ cadreExperience: '团学工作经历',
+ joinTime: '入团时间',
+ collegeMajorClass: '学院专业班级',
+ eduEval2024: '上一年度教育评议情况',
+ smartSystem: '智慧团建系统录入情况',
+ totalVolunteerHours: '累计志愿服务时长',
+ volunteerHours2024: '上一年度志愿服务时长',
+ duty: '职务',
+ college: '学院',
+ gradeMajor: '年级专业',
+ birthDate: '出生日期',
+ branchOrganizationName: '所属团支部',
+ awardName: '获奖名称',
+ awardTime: '获奖时间',
+ proofFile: '证明材料',
+ archiveOrder: '序号'
+};
+
+/**
+ * 五四类申报表的意见栏分组
+ */
+const OPINION_SECTION: ModuleSection = {
+ title: '意见栏',
+ fields: ['leagueOpinion', 'partyOpinion', 'schoolOpinion']
+};
+
+export const MODULE_DETAIL_CONFIGS: Record = {
+ gxmu_sjqn: {
+ title: '十佳青年岗位能手',
+ sheetName: '十佳青年岗位能手',
+ labels: {
+ applyType: '申报类别',
+ education: '学历',
+ position: '职务',
+ title: '职称',
+ unit: '所在单位',
+ awards: '近三年曾获校级及以上奖励',
+ experience: '工作(学习)经历',
+ mainStory: '主要事迹',
+ leagueOpinion: '所在二级团组织意见',
+ partyOpinion: '所在单位党组织意见',
+ schoolOpinion: '校团委意见'
+ },
+ sections: [
+ {
+ title: '基本信息',
+ fields: [
+ 'year',
+ 'applyType',
+ 'name',
+ 'gender',
+ 'birthMonth',
+ 'nation',
+ 'politics',
+ 'education',
+ 'position',
+ 'title',
+ 'unit'
+ ]
+ },
+ {
+ title: '主要事迹与经历',
+ fields: ['awards', 'experience', 'mainStory']
+ },
+ OPINION_SECTION
+ ],
+ materialFields: []
+ },
+ gxmu_sjtbzbsj: {
+ title: '十佳团支部书记',
+ sheetName: '十佳团支部书记',
+ labels: {
+ collegeClass: '学院班级/单位科室',
+ branch: '所在团支部',
+ eduEval: '上一年度团员教育评议等次',
+ awards: '近三年曾获校级及以上奖励',
+ experience: '工作(学习)经历',
+ mainStory: '主要事迹',
+ leagueOpinion: '所在二级团组织意见',
+ partyOpinion: '所在单位党组织意见',
+ schoolOpinion: '校团委意见'
+ },
+ sections: [
+ {
+ title: '基本信息',
+ fields: [
+ 'year',
+ 'name',
+ 'gender',
+ 'birthMonth',
+ 'nation',
+ 'politics',
+ 'collegeClass',
+ 'branch',
+ 'eduEval'
+ ]
+ },
+ {
+ title: '主要事迹与经历',
+ fields: ['awards', 'experience', 'mainStory']
+ },
+ OPINION_SECTION
+ ],
+ materialFields: []
+ },
+ gxmu_wshqtw: {
+ title: '五四红旗团委',
+ sheetName: '五四红旗团委',
+ labels: {
+ orgName: '二级团组织全称',
+ leader: '负责人',
+ phone: '联系电话',
+ memberTotal: '现有团员总数',
+ memberDeveloped2024: '上一年度发展团员人数',
+ smartSystemLogin: '是否已登录智慧团建系统',
+ committeeCount: '团委委员人数',
+ fulltimeCadreCount: '专职团干部数',
+ parttimeCadreCount: '兼职团干部数',
+ lastElectionTime: '团委最近一次换届时间',
+ feeReceivable2024: '上一年度应收团费',
+ feeReceived2024: '上一年度实收团费',
+ feePayable2024: '上一年度应上缴团费',
+ feePaid2024: '上一年度实际上缴团费',
+ branchCount: '团支部数',
+ standardizedWork2024: '是否开展规范化建设工作',
+ recommendActivist2024: '推荐入党积极分子人数',
+ activistConfirmed: '确定为入党积极分子数',
+ recommendDevTarget2024: '推荐党的发展对象人数',
+ devTargetConfirmed: '确定为党的发展对象数',
+ honorsFiveYears: '近五年获得校级及以上荣誉情况',
+ workSummaryThreeYears: '近三年开展的主要工作及取得的效果',
+ partyOpinion: '所在单位党组织意见',
+ schoolOpinion: '校团委意见'
+ },
+ sections: [
+ { title: '组织信息', fields: ['year', 'orgName', 'leader', 'phone'] },
+ {
+ title: '团员与团干部情况',
+ fields: [
+ 'memberTotal',
+ 'memberDeveloped2024',
+ 'smartSystemLogin',
+ 'committeeCount',
+ 'fulltimeCadreCount',
+ 'parttimeCadreCount',
+ 'lastElectionTime'
+ ]
+ },
+ {
+ title: '团费收缴情况',
+ fields: [
+ 'feeReceivable2024',
+ 'feeReceived2024',
+ 'feePayable2024',
+ 'feePaid2024'
+ ]
+ },
+ {
+ title: '团支部与推优情况',
+ fields: [
+ 'branchCount',
+ 'recommendActivist2024',
+ 'activistConfirmed',
+ 'recommendDevTarget2024',
+ 'devTargetConfirmed'
+ ]
+ },
+ {
+ title: '工作与荣誉',
+ fields: [
+ 'standardizedWork2024',
+ 'honorsFiveYears',
+ 'workSummaryThreeYears'
+ ]
+ },
+ { title: '意见栏', fields: ['partyOpinion', 'schoolOpinion'] }
+ ],
+ materialFields: []
+ },
+ gxmu_wshqtzb: {
+ title: '五四红旗团支部',
+ sheetName: '五四红旗团支部',
+ labels: {
+ branchName: '团支部全称',
+ secondOrg: '所属二级团组织',
+ secretary: '团支部书记',
+ politics: '政治面貌',
+ contact: '联系方式',
+ establishTime: '成立时间',
+ lastElectionTime: '最近一次换届时间',
+ smartSystemLogin: '是否已登录智慧团建系统',
+ memberTotal: '现有团员总数',
+ memberDeveloped2024: '上一年度发展团员数',
+ feeReceivable2024: '上一年度应收团费',
+ feeReceived2024: '上一年度实收团费',
+ feePayable2024: '上一年度应上缴团费',
+ feePaid2024: '上一年度实际上缴团费',
+ recommendActivist2024: '推荐入党积极分子人数',
+ activistConfirmed: '确定为入党积极分子数',
+ recommendDevTarget2024: '推荐党的发展对象人数',
+ devTargetConfirmed: '确定为党的发展对象数',
+ branchCommitteeMeetingCount: '团支部委员会会议召开次数',
+ branchMemberMeetingCount: '团支部团员大会召开次数',
+ eduEvalDone: '是否开展团员教育评议',
+ annualRegDone: '是否开展团员年度团籍注册',
+ classCount: '开展团课次数',
+ smartSystem100: '是否100%录入智慧团建',
+ honorsFiveYears: '近五年获得院级及以上荣誉情况',
+ workSummaryThreeYears: '近三年开展的主要工作及取得的效果',
+ leagueOpinion: '所在二级团组织意见',
+ partyOpinion: '所在单位党组织意见',
+ schoolOpinion: '校团委意见'
+ },
+ sections: [
+ {
+ title: '支部信息',
+ fields: [
+ 'year',
+ 'branchName',
+ 'secondOrg',
+ 'secretary',
+ 'politics',
+ 'contact',
+ 'establishTime',
+ 'lastElectionTime'
+ ]
+ },
+ {
+ title: '团员与团费情况',
+ fields: [
+ 'smartSystemLogin',
+ 'memberTotal',
+ 'memberDeveloped2024',
+ 'feeReceivable2024',
+ 'feeReceived2024',
+ 'feePayable2024',
+ 'feePaid2024'
+ ]
+ },
+ {
+ title: '推优情况',
+ fields: [
+ 'recommendActivist2024',
+ 'activistConfirmed',
+ 'recommendDevTarget2024',
+ 'devTargetConfirmed'
+ ]
+ },
+ {
+ title: '组织生活',
+ fields: [
+ 'branchCommitteeMeetingCount',
+ 'branchMemberMeetingCount',
+ 'eduEvalDone',
+ 'annualRegDone',
+ 'classCount',
+ 'smartSystem100'
+ ]
+ },
+ {
+ title: '工作与荣誉',
+ fields: ['honorsFiveYears', 'workSummaryThreeYears']
+ },
+ OPINION_SECTION
+ ],
+ materialFields: []
+ },
+ gxmu_yxgqtdgb: {
+ title: '优秀共青团干部',
+ sheetName: '优秀共青团干部',
+ labels: {
+ identity: '身份',
+ organization: '所在团组织',
+ contact: '联系方式',
+ memberNo: '发展团员编号',
+ currentDutyTime: '任现团内职务时间',
+ cadreYears: '担任团干部年限',
+ assessment2024: '上一年度工作考核结果',
+ volunteerRegTime: '成为注册志愿者时间',
+ cadreExperience: '从事团干部经历',
+ honorsFiveYears: '近五年获得校级及以上荣誉情况',
+ mainStory: '主要事迹',
+ leagueOpinion: '所在单位团组织意见',
+ partyOpinion: '所在单位党组织意见'
+ },
+ sections: [
+ {
+ title: '基本信息',
+ fields: [
+ 'year',
+ 'name',
+ 'gender',
+ 'nation',
+ 'birthMonth',
+ 'politics',
+ 'position',
+ 'identity',
+ 'organization',
+ 'contact',
+ 'memberNo'
+ ]
+ },
+ {
+ title: '团干部履历',
+ fields: [
+ 'currentDutyTime',
+ 'cadreYears',
+ 'assessment2024',
+ 'volunteerRegTime',
+ 'cadreExperience'
+ ]
+ },
+ {
+ title: '主要事迹与荣誉',
+ fields: ['honorsFiveYears', 'mainStory']
+ },
+ { title: '意见栏', fields: ['leagueOpinion', 'partyOpinion'] }
+ ],
+ materialFields: []
+ },
+ gxmu_yxgqty: {
+ title: '优秀共青团员',
+ sheetName: '优秀共青团员',
+ labels: {
+ joinTime: '入团时间',
+ collegeMajorClass: '所在学院、专业、班级',
+ position: '职务',
+ volunteerRegTime: '成为注册志愿者时间',
+ eduEval2024: '上一年度团员教育评议等次',
+ smartSystem: '是否已登录智慧团建系统',
+ contact: '联系电话',
+ totalVolunteerHours: '累计志愿服务时长',
+ volunteerHours2024: '上一年度志愿服务时长',
+ memberNo: '发展团员编号',
+ honorsFiveYears: '近五年获得荣誉情况',
+ mainStory: '主要事迹',
+ leagueOpinion: '学生社团指导教师意见/所在单位团委',
+ partyOpinion: '业务指导单位意见/所在单位党组织',
+ schoolOpinion: '校团委意见'
+ },
+ sections: [
+ {
+ title: '基本信息',
+ fields: [
+ 'year',
+ 'name',
+ 'gender',
+ 'nation',
+ 'politics',
+ 'birthMonth',
+ 'joinTime',
+ 'collegeMajorClass',
+ 'position',
+ 'contact',
+ 'memberNo'
+ ]
+ },
+ {
+ title: '志愿服务与教育评议',
+ fields: [
+ 'volunteerRegTime',
+ 'eduEval2024',
+ 'smartSystem',
+ 'totalVolunteerHours',
+ 'volunteerHours2024'
+ ]
+ },
+ {
+ title: '主要事迹与荣誉',
+ fields: ['honorsFiveYears', 'mainStory']
+ },
+ OPINION_SECTION
+ ],
+ materialFields: []
+ },
+ gxmu_tzbcy_form: {
+ title: '挑战杯',
+ sheetName: '挑战杯',
+ labels: {
+ projectMaterials: '项目介绍材料',
+ otherProofs: '其他相关证明材料',
+ teamMembers: '团队成员',
+ advisors: '指导老师'
+ },
+ sections: [
+ {
+ title: '项目信息',
+ fields: [
+ 'year',
+ 'projectName',
+ 'provinceCity',
+ 'schoolName',
+ 'projectType',
+ 'projectGroup'
+ ]
+ },
+ { title: '团队成员', fields: ['teamMembers'] },
+ { title: '指导老师', fields: ['advisors'] },
+ {
+ title: '项目说明',
+ fields: [
+ 'projectBrief',
+ 'socialValue',
+ 'practiceProcess',
+ 'innovationMeaning',
+ 'developmentProspect',
+ 'teamCooperation',
+ 'projectMaterials',
+ 'otherProofs'
+ ]
+ },
+ {
+ title: '公开展示信息',
+ fields: [
+ 'publicProjectType',
+ 'publicProjectGroup',
+ 'projectSummary',
+ 'teamIntro',
+ 'teamSlogan',
+ 'practiceLog'
+ ]
+ }
+ ],
+ materialFields: [],
+ materialSource: 'tzbcy'
+ },
+ gxmu_qmgc_form: {
+ title: '青马工程',
+ sheetName: '青马工程',
+ labels: {
+ hobby: '兴趣爱好',
+ willingToWorkInGuangxi: '是否愿意留桂工作',
+ schoolInfo: '学校信息',
+ leaguePosition: '团内职务',
+ resume: '个人简历',
+ awards: '获得奖励',
+ academicPerformance: '学习成绩',
+ secondaryLeagueOpinion: '二级团组织意见',
+ secondaryLeagueOpinionDate: '二级团组织意见日期',
+ secondaryPartyOpinion: '二级党组织意见',
+ secondaryPartyOpinionDate: '二级党组织意见日期',
+ schoolLeagueOpinion: '学校团组织意见',
+ schoolLeagueOpinionDate: '学校团组织意见日期'
+ },
+ sections: [
+ {
+ title: '个人信息',
+ fields: [
+ 'year',
+ 'name',
+ 'gender',
+ 'nation',
+ 'photo',
+ 'birthMonth',
+ 'politics',
+ 'nativePlace',
+ 'phone',
+ 'wechat',
+ 'email',
+ 'qq',
+ 'idCardNo',
+ 'hobby',
+ 'willingToWorkInGuangxi',
+ 'schoolInfo',
+ 'leaguePosition'
+ ]
+ },
+ {
+ title: '学习与经历',
+ fields: ['resume', 'awards', 'academicPerformance']
+ },
+ {
+ title: '意见栏',
+ fields: [
+ 'secondaryLeagueOpinion',
+ 'secondaryLeagueOpinionDate',
+ 'secondaryPartyOpinion',
+ 'secondaryPartyOpinionDate',
+ 'schoolLeagueOpinion',
+ 'schoolLeagueOpinionDate'
+ ]
+ }
+ ],
+ materialFields: []
+ },
+ gxmu_wxxzx_project: {
+ title: '未来学术之星',
+ sheetName: '未来学术之星',
+ labels: {
+ topicName: '课题名称',
+ leader: '课题负责人',
+ collegeGradeClass: '学院年级班级',
+ phone: '联系电话',
+ advisor: '指导老师',
+ topicType: '课题类别',
+ guidanceTeachers: '指导教师组',
+ applicants: '申请人信息',
+ budgets: '经费预算',
+ rationale: '立项依据',
+ teacherOpinion: '指导教师意见',
+ teacherSign: '指导教师签名',
+ reviewCollege: '评审学院',
+ reviewReporter: '汇报人',
+ reviewDate: '评审日期',
+ reviewOpinion: '评审意见',
+ reviewScore: '评审成绩',
+ reviewLeaderSign: '评审负责人签名',
+ collegeOpinion: '学院意见',
+ schoolOpinion: '学校意见',
+ reviewMembers: '评审成员',
+ midtermRemark: '中期检查说明',
+ midtermMaterials: '中期检查材料',
+ finalRemark: '结题说明',
+ finalMaterials: '结题材料',
+ archiveRecords: '成果归档',
+ archiveAwards: '获奖情况',
+ archiveProofs: '佐证材料',
+ promiseSigner: '承诺签字人',
+ promiseSignature: '承诺书签名',
+ fundingBudgetFile: '经费预算表',
+ fundingAllocationFile: '经费划拨明细表',
+ proofFile: '证明材料'
+ },
+ sections: [
+ {
+ title: '课题信息',
+ fields: [
+ 'year',
+ 'topicName',
+ 'leader',
+ 'collegeGradeClass',
+ 'phone',
+ 'advisor',
+ 'topicType'
+ ]
+ },
+ { title: '指导教师组', fields: ['guidanceTeachers'] },
+ { title: '申请人信息', fields: ['applicants'] },
+ {
+ title: '经费预算',
+ fields: ['budgets', 'fundingBudgetFile', 'fundingAllocationFile']
+ },
+ { title: '立项依据', fields: ['rationale'] },
+ {
+ title: '审核评审',
+ fields: [
+ 'teacherOpinion',
+ 'teacherSign',
+ 'reviewCollege',
+ 'reviewReporter',
+ 'reviewDate',
+ 'reviewOpinion',
+ 'reviewScore',
+ 'reviewLeaderSign',
+ 'collegeOpinion',
+ 'schoolOpinion'
+ ]
+ },
+ { title: '评审成员', fields: ['reviewMembers'] },
+ {
+ title: '中期检查与结题',
+ fields: [
+ 'midtermRemark',
+ 'midtermMaterials',
+ 'finalRemark',
+ 'finalMaterials'
+ ]
+ },
+ {
+ title: '成果归档',
+ fields: ['archiveRecords', 'archiveAwards', 'archiveProofs']
+ },
+ {
+ title: '承诺书',
+ fields: ['promiseSigner', 'promiseSignature']
+ }
+ ],
+ materialFields: [
+ 'fundingBudgetFile',
+ 'fundingAllocationFile',
+ 'midtermMaterials',
+ 'finalMaterials',
+ 'archiveProofs'
+ ],
+ materialTables: [
+ {
+ field: 'archiveRecords',
+ fileField: 'proofFile',
+ titleField: 'awardName',
+ label: '成果归档证明材料'
+ }
+ ]
+ },
+ gxmu_tygl_form: {
+ title: '团员管理',
+ sheetName: '团员信息登记表',
+ labels: {
+ serialNo: '序号',
+ college: '学院',
+ className: '班级',
+ phone: '手机号码',
+ leaguePosition: '团内职务',
+ branchOrganizationName: '所属团支部',
+ idCardNo: '身份证号',
+ birthDate: '出生日期',
+ archiveInCurrentOrg: '团籍是否在本组织',
+ joinMonth: '入团年月',
+ memberRecords: '团员记录',
+ growthArchives: '成长档案'
+ },
+ sections: [
+ {
+ title: '基本信息',
+ fields: [
+ 'serialNo',
+ 'name',
+ 'gender',
+ 'nation',
+ 'politics',
+ 'college',
+ 'className',
+ 'phone',
+ 'leaguePosition',
+ 'branchOrganizationName',
+ 'idCardNo',
+ 'birthDate',
+ 'archiveInCurrentOrg',
+ 'joinMonth'
+ ]
+ },
+ { title: '团员记录', fields: ['memberRecords'] },
+ { title: '成长档案', fields: ['growthArchives'] }
+ ],
+ materialFields: []
+ },
+ cms_manuscript: {
+ title: '稿件',
+ sheetName: '稿件',
+ labels: {
+ title: '标题',
+ content: '正文',
+ contentFormat: '正文格式',
+ cover: '封面',
+ status: '状态',
+ sortNumber: '排序'
+ },
+ sections: [
+ {
+ title: '基本信息',
+ fields: ['title', 'status', 'sortNumber']
+ },
+ { title: '封面', fields: ['cover'] },
+ { title: '正文', fields: ['content'] }
+ ],
+ materialFields: []
+ }
+};
+
+// 团员管理模块历史上存在两个模块标识
+MODULE_DETAIL_CONFIGS.gxmu_tygl = MODULE_DETAIL_CONFIGS.gxmu_tygl_form;
+
+/**
+ * 明细数组表头字段中文名
+ */
+export const TABLE_FIELD_LABELS: Record = {
+ name: '姓名',
+ gender: '性别',
+ nation: '民族',
+ politics: '政治面貌',
+ birthMonth: '出生年月',
+ title: '职称',
+ duty: '职务',
+ department: '部门',
+ dept: '部门',
+ remark: '备注',
+ college: '学院',
+ gradeMajor: '年级专业',
+ grade: '年级',
+ className: '班级',
+ major: '专业',
+ role: '角色',
+ phone: '联系电话',
+ sign: '签名',
+ item: '项目',
+ amount: '金额',
+ reason: '原因',
+ type: '类型',
+ content: '内容',
+ recordTime: '时间',
+ archiveTime: '时间',
+ awardName: '获奖名称',
+ awardTime: '获奖时间',
+ archiveOrder: '序号',
+ proofFile: '证明材料',
+ year: '年份'
+};
+
+export const getModuleDetailConfig = (module?: string) => {
+ if (!module) {
+ return undefined;
+ }
+ return MODULE_DETAIL_CONFIGS[module];
+};
diff --git a/src/views/gxmu/utils/attachment.ts b/src/views/gxmu/utils/attachment.ts
new file mode 100644
index 0000000..27d2c40
--- /dev/null
+++ b/src/views/gxmu/utils/attachment.ts
@@ -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;
+ 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;
+}