From c81e24c977ae28ad3c4e0747446053f8b1ab316b Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Sun, 1 Nov 2026 17:06:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(gxmu):=20=E5=AE=A1=E6=A0=B8=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E3=80=8C=E6=9F=A5=E7=9C=8B=E6=95=B0=E6=8D=AE=E3=80=8D?= =?UTF-8?q?=E6=8C=89=E7=94=B3=E6=8A=A5=E8=A1=A8=E5=88=86=E7=BB=84=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E5=B9=B6=E6=96=B0=E5=A2=9E=E4=B8=8A=E4=BC=A0=E6=9D=90?= =?UTF-8?q?=E6=96=99=E9=A1=B5=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 详情弹窗改为「申报表 / 上传材料」两个页签,按模块配置分组渲染字段 - 明细数组(团队成员、指导老师、评审成员、团员记录、成果归档等)渲染为表格 - 新增附件解析工具与只读附件列表组件,兼容单地址、JSON 数组、对象数组 - 未来学术之星展示经费/中期/结题/成果归档材料,挑战杯拉取 gxmu_tzbcy_material - 未在配置中声明的字段兜底进「其他信息」,避免数据遗漏 --- src/views/gxmu/components/attachmentList.vue | 120 ++ .../reviewList/components/moduleDataModal.vue | 1159 ++++++++++------- .../components/moduleDetailConfig.ts | 904 +++++++++++++ src/views/gxmu/utils/attachment.ts | 118 ++ 4 files changed, 1843 insertions(+), 458 deletions(-) create mode 100644 src/views/gxmu/components/attachmentList.vue create mode 100644 src/views/gxmu/reviewList/components/moduleDetailConfig.ts create mode 100644 src/views/gxmu/utils/attachment.ts diff --git a/src/views/gxmu/components/attachmentList.vue b/src/views/gxmu/components/attachmentList.vue new file mode 100644 index 0000000..210fb87 --- /dev/null +++ b/src/views/gxmu/components/attachmentList.vue @@ -0,0 +1,120 @@ + + + + + + + diff --git a/src/views/gxmu/reviewList/components/moduleDataModal.vue b/src/views/gxmu/reviewList/components/moduleDataModal.vue index 299f573..7dad046 100644 --- a/src/views/gxmu/reviewList/components/moduleDataModal.vue +++ b/src/views/gxmu/reviewList/components/moduleDataModal.vue @@ -1,6 +1,6 @@ @@ -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; +}