This commit is contained in:
2026-07-20 11:50:47 +08:00
parent 95d43746f6
commit 019ed28d37
401 changed files with 61622 additions and 23869 deletions
@@ -0,0 +1,739 @@
<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>