c81e24c977
- 详情弹窗改为「申报表 / 上传材料」两个页签,按模块配置分组渲染字段 - 明细数组(团队成员、指导老师、评审成员、团员记录、成果归档等)渲染为表格 - 新增附件解析工具与只读附件列表组件,兼容单地址、JSON 数组、对象数组 - 未来学术之星展示经费/中期/结题/成果归档材料,挑战杯拉取 gxmu_tzbcy_material - 未在配置中声明的字段兜底进「其他信息」,避免数据遗漏
983 lines
27 KiB
Vue
983 lines
27 KiB
Vue
<template>
|
||
<ele-modal
|
||
:width="1000"
|
||
: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 && showEmpty" :description="emptyDescription" />
|
||
<a-tabs v-else v-model:activeKey="activeTab" class="detail-tabs">
|
||
<a-tab-pane key="sheet" :tab="sheetTabLabel">
|
||
<div class="sheet">
|
||
<div v-if="sheetHeading" class="sheet-title">{{
|
||
sheetHeading
|
||
}}</div>
|
||
<div
|
||
v-for="(section, sectionIndex) in sheetSections"
|
||
:key="`section-${sectionIndex}-${section.title}`"
|
||
class="sheet-section"
|
||
>
|
||
<div class="sheet-section-title">{{ section.title }}</div>
|
||
<a-descriptions
|
||
bordered
|
||
:column="2"
|
||
size="small"
|
||
:label-style="{ width: '160px' }"
|
||
>
|
||
<a-descriptions-item
|
||
v-for="item in section.items"
|
||
:key="item.key"
|
||
:label="item.label"
|
||
:span="item.span"
|
||
>
|
||
<template v-if="item.type === 'rich'">
|
||
<!-- eslint-disable vue/no-v-html -->
|
||
<div
|
||
class="rich-text-content markdown-body"
|
||
v-html="item.text"
|
||
></div>
|
||
</template>
|
||
<template v-else-if="item.type === 'image'">
|
||
<div class="image-list">
|
||
<a-image
|
||
v-for="(url, index) in item.images"
|
||
:key="`${item.key}-image-${index}`"
|
||
:src="url"
|
||
:width="120"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="item.type === 'files'">
|
||
<AttachmentList
|
||
:files="item.files"
|
||
empty-text="未上传材料"
|
||
/>
|
||
</template>
|
||
<template v-else-if="item.type === 'table'">
|
||
<a-table
|
||
size="small"
|
||
bordered
|
||
:columns="item.columns"
|
||
:data-source="item.rows"
|
||
:pagination="false"
|
||
:scroll="{ x: 'max-content' }"
|
||
row-key="__rowKey"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="isFileColumn(column)">
|
||
<AttachmentList
|
||
:value="record[column.dataIndex as string]"
|
||
empty-text="未上传"
|
||
/>
|
||
</template>
|
||
<template v-else>
|
||
{{
|
||
formatCellValue(record[column.dataIndex as string])
|
||
}}
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</template>
|
||
<template v-else-if="item.type === 'object'">
|
||
<div
|
||
v-for="(line, index) in item.lines"
|
||
:key="`${item.key}-line-${index}`"
|
||
class="field-line"
|
||
>
|
||
{{ line }}
|
||
</div>
|
||
</template>
|
||
<template v-else-if="item.type === 'longtext'">
|
||
<div class="field-line multiline-text">{{ item.text }}</div>
|
||
</template>
|
||
<template v-else>
|
||
{{ item.text }}
|
||
</template>
|
||
</a-descriptions-item>
|
||
</a-descriptions>
|
||
</div>
|
||
<div v-if="footerLines.length" class="sheet-footer">
|
||
<span
|
||
v-for="(line, index) in footerLines"
|
||
:key="`footer-${index}`"
|
||
class="sheet-footer-item"
|
||
>
|
||
{{ line }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</a-tab-pane>
|
||
<a-tab-pane v-if="showMaterialTab" key="materials" tab="上传材料">
|
||
<div class="material-pane">
|
||
<div
|
||
v-for="group in materialGroups"
|
||
:key="`material-${group.key}`"
|
||
class="material-group"
|
||
>
|
||
<div class="material-group-title">{{ group.label }}</div>
|
||
<AttachmentList :files="group.files" empty-text="未上传材料" />
|
||
</div>
|
||
<div
|
||
v-for="group in materialTableGroups"
|
||
:key="`material-table-${group.key}`"
|
||
class="material-group"
|
||
>
|
||
<div class="material-group-title">{{ group.label }}</div>
|
||
<div
|
||
v-for="(row, index) in group.rows"
|
||
:key="`${group.key}-${index}`"
|
||
class="material-row"
|
||
>
|
||
<div class="material-row-title">{{ row.title }}</div>
|
||
<AttachmentList :files="row.files" empty-text="未上传材料" />
|
||
</div>
|
||
<a-empty
|
||
v-if="!group.rows.length"
|
||
class="material-empty"
|
||
description="暂无归档材料"
|
||
/>
|
||
</div>
|
||
<div v-if="showTzbcyMaterials" class="material-group">
|
||
<div class="material-group-title">申报材料</div>
|
||
<a-table
|
||
size="small"
|
||
bordered
|
||
row-key="id"
|
||
:loading="materialLoading"
|
||
:columns="tzbcyMaterialColumns"
|
||
:data-source="tzbcyMaterials"
|
||
:pagination="false"
|
||
:scroll="{ x: 'max-content' }"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'fileName'">
|
||
<a
|
||
:href="record.downloadUrl || record.fileUrl"
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
>
|
||
{{ record.fileName || '未命名文件' }}
|
||
</a>
|
||
</template>
|
||
<template v-else-if="column.key === 'status'">
|
||
<a-tag :color="getMaterialStatusColor(record.status)">
|
||
{{ getMaterialStatusText(record.status) }}
|
||
</a-tag>
|
||
</template>
|
||
<template v-else-if="column.key === 'createTime'">
|
||
{{ formatDateTime(record.createTime) }}
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
<a-empty
|
||
v-if="
|
||
!materialGroups.length &&
|
||
!materialTableGroups.length &&
|
||
!showTzbcyMaterials
|
||
"
|
||
description="该模块暂无上传材料"
|
||
/>
|
||
</div>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
</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 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';
|
||
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';
|
||
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';
|
||
|
||
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;
|
||
type: SheetFieldType;
|
||
span: number;
|
||
text?: string;
|
||
images?: string[];
|
||
files?: AttachmentFile[];
|
||
lines?: string[];
|
||
columns?: TableColumnConfig[];
|
||
rows?: Record<string, unknown>[];
|
||
}
|
||
|
||
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<Record<string, unknown>>;
|
||
}
|
||
|
||
const MODULE_FETCHERS: Record<string, ModuleFetcher> = {
|
||
cms_manuscript: {
|
||
title: '稿件',
|
||
fetcher: (id) => getManuscript(id) as Promise<Record<string, unknown>>
|
||
},
|
||
gxmu_sjqn: {
|
||
title: '十佳青年岗位能手',
|
||
fetcher: (id) => getSjqnForm(id) as Promise<Record<string, unknown>>
|
||
},
|
||
gxmu_sjtbzbsj: {
|
||
title: '十佳团支部书记',
|
||
fetcher: (id) => getSjtbzbsjForm(id) as Promise<Record<string, unknown>>
|
||
},
|
||
gxmu_wshqtw: {
|
||
title: '五四红旗团委',
|
||
fetcher: (id) => getWshqtwForm(id) as Promise<Record<string, unknown>>
|
||
},
|
||
gxmu_wshqtzb: {
|
||
title: '五四红旗团支部',
|
||
fetcher: (id) => getWshqtzbForm(id) as Promise<Record<string, unknown>>
|
||
},
|
||
gxmu_yxgqtdgb: {
|
||
title: '优秀共青团干部',
|
||
fetcher: (id) => getYxgqtdgbForm(id) as Promise<Record<string, unknown>>
|
||
},
|
||
gxmu_yxgqty: {
|
||
title: '优秀共青团员',
|
||
fetcher: (id) => getYxgqtyForm(id) as Promise<Record<string, unknown>>
|
||
},
|
||
gxmu_tzbcy_form: {
|
||
title: '挑战杯',
|
||
fetcher: (id) => getTzbcyForm(id) as Promise<Record<string, unknown>>
|
||
},
|
||
gxmu_qmgc_form: {
|
||
title: '青马工程',
|
||
fetcher: (id) => getQmgcForm(id) as Promise<Record<string, unknown>>
|
||
},
|
||
gxmu_wxxzx_project: {
|
||
title: '未来学术之星',
|
||
fetcher: (id) => getWxxzxForm(id) as Promise<Record<string, unknown>>
|
||
},
|
||
gxmu_tygl_form: {
|
||
title: '团员管理',
|
||
fetcher: (id) => getTyglForm(id) as Promise<Record<string, unknown>>
|
||
},
|
||
gxmu_tygl: {
|
||
title: '团员管理',
|
||
fetcher: (id) => getTyglForm(id) as Promise<Record<string, unknown>>
|
||
}
|
||
};
|
||
|
||
// 不在申报表中展示的字段
|
||
const HIDDEN_FIELDS = new Set([
|
||
'id',
|
||
'reviewList',
|
||
'deleted',
|
||
'tenantId',
|
||
'userId',
|
||
'branchOrganizationId',
|
||
'contentFormat'
|
||
]);
|
||
const IMAGE_FIELDS = new Set(['photo', 'cover', 'promiseSignature']);
|
||
const RICH_TEXT_FIELDS = new Set(['content']);
|
||
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 activeTab = ref('sheet');
|
||
const tzbcyMaterials = ref<TzbcyMaterial[]>([]);
|
||
const materialLoading = ref(false);
|
||
|
||
const currentConfig = computed<ModuleDetailConfig | undefined>(() =>
|
||
getModuleDetailConfig(props.module)
|
||
);
|
||
|
||
const modalTitle = computed(() => {
|
||
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(() => {
|
||
if (loadFailed.value) {
|
||
return '数据加载失败';
|
||
}
|
||
if (!currentConfig.value) {
|
||
return '暂不支持查看该模块数据';
|
||
}
|
||
return '暂无可展示数据';
|
||
});
|
||
|
||
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 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 formatDateTime = (value: unknown) => {
|
||
if (value === null || value === undefined || value === '') {
|
||
return '-';
|
||
}
|
||
return toDateString(String(value), 'yyyy-MM-dd HH:mm:ss');
|
||
};
|
||
|
||
const formatStatusValue = (value: unknown) => {
|
||
if (props.module === 'cms_manuscript') {
|
||
if (Number(value) === 0) {
|
||
return '待审核';
|
||
}
|
||
if (Number(value) === 1) {
|
||
return '显示';
|
||
}
|
||
}
|
||
return String(value);
|
||
};
|
||
|
||
const formatCellValue = (value: unknown) => {
|
||
if (value === null || value === undefined || value === '') {
|
||
return '-';
|
||
}
|
||
if (Array.isArray(value)) {
|
||
return value.join('、');
|
||
}
|
||
if (isPlainObject(value)) {
|
||
return JSON.stringify(value);
|
||
}
|
||
return String(value);
|
||
};
|
||
|
||
const formatDisplayValue = (key: string, value: unknown) => {
|
||
if (key === 'createTime' || key === 'updateTime') {
|
||
return formatDateTime(value);
|
||
}
|
||
if (key === 'status') {
|
||
return formatStatusValue(value);
|
||
}
|
||
return formatCellValue(value);
|
||
};
|
||
|
||
/**
|
||
* 判断字段值是否是一组附件(全部为文件地址时才认为命中)
|
||
*/
|
||
const isAttachmentValue = (value: unknown) => {
|
||
if (typeof value === 'string' || Array.isArray(value)) {
|
||
const files = parseAttachments(value);
|
||
return files.length > 0;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
/**
|
||
* 上传材料的字段集合:配置声明 + 自动识别
|
||
*/
|
||
const materialKeySet = computed(() => {
|
||
const keys = new Set<string>(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<string, unknown>[]) => {
|
||
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<string, unknown>[]).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<SheetSection[]>(() => {
|
||
const source = detail.value || {};
|
||
const config = currentConfig.value;
|
||
const usedKeys = new Set<string>();
|
||
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<MaterialGroup[]>(() => {
|
||
const config = currentConfig.value;
|
||
if (!config) {
|
||
return [];
|
||
}
|
||
const keys = new Set<string>(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<MaterialTableGroup[]>(() => {
|
||
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<string, unknown>[])
|
||
: [];
|
||
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<string, unknown>) => {
|
||
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);
|
||
};
|
||
|
||
const loadDetail = async () => {
|
||
if (!props.visible) {
|
||
return;
|
||
}
|
||
detail.value = {};
|
||
loadFailed.value = false;
|
||
activeTab.value = 'sheet';
|
||
tzbcyMaterials.value = [];
|
||
|
||
if (!props.pk || !currentConfig.value) {
|
||
if (props.module && !currentConfig.value) {
|
||
loadFailed.value = true;
|
||
}
|
||
return;
|
||
}
|
||
|
||
loading.value = true;
|
||
try {
|
||
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 || '数据加载失败');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
watch(
|
||
() => [props.visible, props.module, props.pk],
|
||
([visible]) => {
|
||
if (visible) {
|
||
loadDetail();
|
||
} else {
|
||
detail.value = {};
|
||
tzbcyMaterials.value = [];
|
||
loadFailed.value = false;
|
||
loading.value = false;
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
</script>
|
||
|
||
<script lang="ts">
|
||
export default {
|
||
name: 'ModuleDataModal'
|
||
};
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.detail-tabs :deep(.ant-tabs-nav) {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.sheet-title {
|
||
margin-bottom: 16px;
|
||
color: #1f2329;
|
||
font-weight: 600;
|
||
font-size: 18px;
|
||
text-align: center;
|
||
}
|
||
|
||
.sheet-section + .sheet-section {
|
||
margin-top: 18px;
|
||
}
|
||
|
||
.sheet-section-title {
|
||
margin-bottom: 8px;
|
||
padding-left: 8px;
|
||
border-left: 3px solid #1677ff;
|
||
color: #1f2329;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
line-height: 16px;
|
||
}
|
||
|
||
.sheet-footer {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 24px;
|
||
margin-top: 14px;
|
||
color: #8c8c8c;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.field-line {
|
||
line-height: 1.8;
|
||
white-space: pre-wrap;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.multiline-text {
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.image-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
:deep(.markdown-body) {
|
||
background: #fff;
|
||
color: rgba(0, 0, 0, 0.88);
|
||
}
|
||
|
||
.rich-text-content {
|
||
min-height: 32px;
|
||
line-height: 1.8;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.rich-text-content:deep(p:last-child) {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.material-group + .material-group {
|
||
margin-top: 18px;
|
||
}
|
||
|
||
.material-group-title {
|
||
margin-bottom: 8px;
|
||
padding-left: 8px;
|
||
border-left: 3px solid #1677ff;
|
||
color: #1f2329;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
line-height: 16px;
|
||
}
|
||
|
||
.material-row + .material-row {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.material-row-title {
|
||
margin-bottom: 6px;
|
||
color: #5f6b7a;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.material-empty {
|
||
margin: 12px 0;
|
||
}
|
||
</style>
|