Files
tuanwei-admin/src/views/gxmu/reviewList/components/moduleDataModal.vue
T
2026-07-20 11:50:47 +08:00

740 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<ele-modal
:width="960"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="modalTitle"
:body-style="{ paddingBottom: '20px' }"
okText="关闭"
@update:visible="updateVisible"
@ok="updateVisible(false)"
>
<a-spin :spinning="loading">
<a-empty
v-if="!loading && !descriptionItems.length"
:description="emptyDescription"
/>
<a-descriptions v-else bordered :column="2" size="middle">
<a-descriptions-item
v-for="item in descriptionItems"
:key="item.key"
:label="item.label"
:span="item.span"
>
<template v-if="isFileField(item.key)">
<div
v-for="(file, index) in normalizeFileEntries(item.value)"
:key="`${item.key}-file-${index}`"
class="file-entry"
>
<a :href="file.url" target="_blank" rel="noreferrer">
{{ file.name }}
</a>
</div>
</template>
<a-image
v-else-if="isImageField(item.key) && typeof item.value === 'string'"
:src="item.value"
:width="120"
/>
<template v-else-if="Array.isArray(item.value)">
<div
v-for="(entry, index) in item.value"
:key="`${item.key}-${index}`"
class="array-entry"
>
<div class="array-entry-index">第{{ index + 1 }}项</div>
<div
v-for="(line, lineIndex) in formatArrayEntry(entry)"
:key="`${item.key}-${index}-${lineIndex}`"
class="array-entry-line"
>
{{ line }}
</div>
</div>
</template>
<template v-else-if="isPlainObject(item.value)">
<div
v-for="(line, index) in formatObjectLines(item.value)"
:key="`${item.key}-obj-${index}`"
class="array-entry-line"
>
{{ line }}
</div>
</template>
<!-- eslint-disable vue/no-v-html -->
<div
v-else-if="isRichTextField(item.key)"
class="rich-text-content markdown-body"
v-html="formatRichText(item.value)"
></div>
<div v-else :class="{ 'multiline-text': isWideField(item.key) }">
{{ formatDisplayValue(item.key, item.value) }}
</div>
</a-descriptions-item>
</a-descriptions>
</a-spin>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { toDateString } from 'ele-admin-pro';
import 'github-markdown-css/github-markdown-light.css';
import { getManuscript } from '@/api/cms/manuscript';
import { getQmgcForm } from '@/api/gxmu/qmgc-form';
import { getSjqnForm } from '@/api/gxmu/sjqn-form';
import { getSjtbzbsjForm } from '@/api/gxmu/sjtbzbsj-form';
import { getTyglForm } from '@/api/gxmu/tygl-form';
import { getTzbcyForm } from '@/api/gxmu/tzbcy-form';
import { getWshqtwForm } from '@/api/gxmu/wshqtw-form';
import { getWshqtzbForm } from '@/api/gxmu/wshqtzb-form';
import { getWxxzxForm } from '@/api/gxmu/wxxzx-form';
import { getYxgqtdgbForm } from '@/api/gxmu/yxgqtdgb-form';
import { getYxgqtyForm } from '@/api/gxmu/yxgqty-form';
interface DescriptionItem {
key: string;
label: string;
value: unknown;
span: number;
}
interface ModuleConfig {
title: string;
fetcher: (id: number) => Promise<Record<string, unknown>>;
labels: Record<string, string>;
}
const COMMON_LABELS: Record<string, string> = {
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<string, ModuleConfig> = {
cms_manuscript: {
title: '稿件',
fetcher: (id) => getManuscript(id) as Promise<Record<string, unknown>>,
labels: {
...COMMON_LABELS
}
},
gxmu_sjqn: {
title: '十佳青年岗位能手',
fetcher: (id) => getSjqnForm(id) as Promise<Record<string, unknown>>,
labels: {
...COMMON_LABELS
}
},
gxmu_sjtbzbsj: {
title: '十佳团支部书记',
fetcher: (id) => getSjtbzbsjForm(id) as Promise<Record<string, unknown>>,
labels: {
...COMMON_LABELS
}
},
gxmu_wshqtw: {
title: '五四红旗团委',
fetcher: (id) => getWshqtwForm(id) as Promise<Record<string, unknown>>,
labels: {
...COMMON_LABELS
}
},
gxmu_wshqtzb: {
title: '五四红旗团支部',
fetcher: (id) => getWshqtzbForm(id) as Promise<Record<string, unknown>>,
labels: {
...COMMON_LABELS
}
},
gxmu_yxgqtdgb: {
title: '优秀共青团干部',
fetcher: (id) => getYxgqtdgbForm(id) as Promise<Record<string, unknown>>,
labels: {
...COMMON_LABELS
}
},
gxmu_yxgqty: {
title: '优秀共青团员',
fetcher: (id) => getYxgqtyForm(id) as Promise<Record<string, unknown>>,
labels: {
...COMMON_LABELS
}
},
gxmu_tzbcy_form: {
title: '挑战杯',
fetcher: (id) => getTzbcyForm(id) as Promise<Record<string, unknown>>,
labels: {
...COMMON_LABELS
}
},
gxmu_qmgc_form: {
title: '青马工程',
fetcher: (id) => getQmgcForm(id) as Promise<Record<string, unknown>>,
labels: {
...COMMON_LABELS
}
},
gxmu_wxxzx_project: {
title: '未来学术之星',
fetcher: (id) => getWxxzxForm(id) as Promise<Record<string, unknown>>,
labels: {
...COMMON_LABELS,
title: '职称'
}
},
gxmu_tygl_form: {
title: '团员管理',
fetcher: (id) => getTyglForm(id) as Promise<Record<string, unknown>>,
labels: {
...COMMON_LABELS
}
},
gxmu_tygl: {
title: '团员管理',
fetcher: (id) => getTyglForm(id) as Promise<Record<string, unknown>>,
labels: {
...COMMON_LABELS
}
}
};
const HIDDEN_FIELDS = new Set([
'reviewList',
'deleted',
'tenantId',
'userId',
'branchOrganizationId'
]);
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',
'practiceProcess',
'innovationMeaning',
'developmentProspect',
'teamCooperation',
'projectMaterials',
'otherProofs',
'projectSummary',
'teamIntro',
'practiceLog',
'resume',
'awards',
'academicPerformance',
'secondaryLeagueOpinion',
'secondaryPartyOpinion',
'schoolLeagueOpinion',
'experience',
'mainStory',
'leagueOpinion',
'partyOpinion',
'schoolOpinion',
'workSummaryThreeYears',
'honorsFiveYears',
'rationale',
'teacherOpinion',
'reviewOpinion',
'collegeOpinion',
'cadreExperience',
'midtermRemark',
'finalRemark',
'archiveAwards'
]);
const props = defineProps<{
visible: boolean;
module?: string;
moduleName?: string;
pk?: number;
}>();
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void;
}>();
const loading = ref(false);
const maxable = ref(true);
const detail = ref<Record<string, unknown>>({});
const loadFailed = ref(false);
const currentConfig = computed(() => {
return props.module ? MODULE_CONFIGS[props.module] : undefined;
});
const modalTitle = computed(() => {
return `${props.moduleName || currentConfig.value?.title || '数据'}详情`;
});
const emptyDescription = computed(() => {
if (loadFailed.value) {
return '数据加载失败';
}
if (!currentConfig.value) {
return '暂不支持查看该模块数据';
}
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<string, unknown> => {
return Object.prototype.toString.call(value) === '[object Object]';
};
const hasVisibleValue = (value: unknown) => {
if (value === null || value === undefined) {
return false;
}
if (typeof value === 'string') {
return value.trim().length > 0;
}
if (Array.isArray(value)) {
return value.length > 0;
}
if (isPlainObject(value)) {
return Object.values(value).some((item) => hasVisibleValue(item));
}
return true;
};
const isWideField = (key: string) => {
return WIDE_FIELDS.has(key);
};
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) => {
if (value === null || value === undefined || value === '') {
return '-';
}
return String(value);
};
const formatStatusValue = (value: unknown) => {
if (props.module === 'cms_manuscript') {
if (Number(value) === 0) {
return '待审核';
}
if (Number(value) === 1) {
return '显示';
}
}
return formatSimpleValue(value);
};
const formatDateTimeValue = (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 (key === 'status') {
return formatStatusValue(value);
}
return formatSimpleValue(value);
};
const formatRichText = (value: unknown) => {
if (value === null || value === undefined || value === '') {
return '<p>-</p>';
}
return String(value);
};
const formatObjectLines = (value: Record<string, unknown>) => {
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 : ['-'];
}
return [formatSimpleValue(entry)];
};
const descriptionItems = computed<DescriptionItem[]>(() => {
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)
);
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 updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const loadDetail = async () => {
if (!props.visible) {
return;
}
detail.value = {};
loadFailed.value = false;
if (!props.pk || !currentConfig.value) {
if (props.module && !currentConfig.value) {
loadFailed.value = true;
}
return;
}
loading.value = true;
try {
detail.value = (await currentConfig.value.fetcher(props.pk)) || {};
} catch (e: any) {
loadFailed.value = true;
message.error(e.message || '数据加载失败');
} finally {
loading.value = false;
}
};
watch(
() => [props.visible, props.module, props.pk],
([visible]) => {
if (visible) {
loadDetail();
} else {
detail.value = {};
loadFailed.value = false;
loading.value = false;
}
},
{ immediate: true }
);
</script>
<style lang="less" scoped>
:deep(.markdown-body) {
background: #fff;
color: rgba(0, 0, 0, 0.88);
}
.multiline-text {
white-space: pre-wrap;
line-height: 1.8;
}
.rich-text-content {
min-height: 32px;
line-height: 1.8;
word-break: break-word;
}
.rich-text-content:deep(p:last-child) {
margin-bottom: 0;
}
.array-entry {
padding: 12px;
border-radius: 6px;
background: #fafafa;
}
.array-entry + .array-entry {
margin-top: 12px;
}
.array-entry-index {
margin-bottom: 8px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
.array-entry-line {
line-height: 1.8;
white-space: pre-wrap;
word-break: break-all;
}
.file-entry + .file-entry {
margin-top: 8px;
}
</style>