863 lines
30 KiB
Vue
863 lines
30 KiB
Vue
<template>
|
||
<div class="contract-attachment-section">
|
||
<section
|
||
v-if="!dialogOnly"
|
||
class="contract-manage-form__section contract-manage-form__section--panel contract-manage-form__section--attachment"
|
||
>
|
||
<div class="contract-manage-form__attachment-head">
|
||
<div class="dialog-section-title">{{ title }}</div>
|
||
<el-button type="primary" :disabled="!rows.length" @click="batchDownload"
|
||
>批量下载</el-button
|
||
>
|
||
</div>
|
||
<el-table
|
||
:data="rows"
|
||
border
|
||
class="contract-manage-form__attachment-table"
|
||
@selection-change="selected = $event"
|
||
>
|
||
<el-table-column type="selection" width="55" align="center" />
|
||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||
<el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center">
|
||
<template #default="{ row, $index }">
|
||
<span v-if="readonly || isRowLocked(row)">{{ row.fileType || '-' }}</span>
|
||
<el-select
|
||
v-else
|
||
:model-value="row.fileType"
|
||
placeholder="请选择"
|
||
style="width: 100%"
|
||
teleported
|
||
clearable
|
||
@update:model-value="value => updateRowFileType($index, value)"
|
||
>
|
||
<el-option
|
||
v-for="item in contractAttachmentTypeOptions"
|
||
:key="`${item.value}-${item.label}`"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="文件名"
|
||
min-width="240"
|
||
align="left"
|
||
header-align="left"
|
||
show-overflow-tooltip
|
||
class-name="is-ellipsis-link"
|
||
>
|
||
<template #default="{ row }">
|
||
<span
|
||
class="attachment-file-name"
|
||
role="button"
|
||
tabindex="0"
|
||
@click="preview?.(row, rows)"
|
||
@keydown.enter="preview?.(row, rows)"
|
||
@keydown.space.prevent="preview?.(row, rows)"
|
||
>{{ attachmentName(row) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column v-if="description" label="附件描述" min-width="220">
|
||
<template #default="{ row, $index }">
|
||
<span v-if="readonly || isRowLocked(row)">{{ row.description || '-' }}</span>
|
||
<el-input
|
||
v-else
|
||
:model-value="row.description"
|
||
maxlength="200"
|
||
@update:model-value="value => updateRowDescription($index, value)"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="文件大小" width="120" align="center">
|
||
<template #default="{ row }">{{ formatSize(row.size) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||
<el-table-column prop="uploadTime" label="上传时间" width="180" align="center" sortable />
|
||
<el-table-column v-if="!readonly" label="操作" width="100" align="center" fixed="right">
|
||
<template #default="{ row, $index }">
|
||
<span v-if="isRowLocked(row)">-</span>
|
||
<el-link v-else type="danger" @click="remove($index)">删除</el-link>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<div v-if="!readonly" class="contract-manage-form__attachment-upload">
|
||
<template v-if="useUploadDialog">
|
||
<el-button type="primary" plain @click="openUploadDialog">上传附件</el-button>
|
||
<span class="contract-manage-form__attachment-tip">
|
||
支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过50M
|
||
</span>
|
||
</template>
|
||
<vehicle-attachment-upload
|
||
v-else
|
||
:model-value="rows"
|
||
:file-types="attachmentFileTypes"
|
||
:max-size="50"
|
||
show-tip
|
||
tip="支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式,单个文件不超过50M"
|
||
:show-file-list="false"
|
||
button-text="上传附件"
|
||
@change="handleChange"
|
||
@success="handleUploadSuccess"
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
<el-dialog
|
||
v-model="uploadDialogVisible"
|
||
title="附件上传"
|
||
width="560px"
|
||
append-to-body
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
class="contract-attachment-upload-dialog"
|
||
@closed="resetUploadDialog"
|
||
>
|
||
<div class="contract-attachment-upload-dialog__body">
|
||
<div class="contract-attachment-upload-dialog__field">
|
||
<span class="contract-attachment-upload-dialog__label">附件位置</span>
|
||
<el-select
|
||
v-model="uploadDialogLocation"
|
||
placeholder="请选择附件位置"
|
||
style="width: 100%"
|
||
@change="syncUploadDialogTypeByLocation"
|
||
>
|
||
<el-option
|
||
v-for="item in resolvedLocationOptions"
|
||
:key="item"
|
||
:label="item"
|
||
:value="item"
|
||
/>
|
||
</el-select>
|
||
</div>
|
||
<div class="contract-attachment-upload-dialog__field">
|
||
<span class="contract-attachment-upload-dialog__label">附件类型</span>
|
||
<el-select v-model="uploadDialogType" placeholder="请选择附件类型" style="width: 100%">
|
||
<el-option
|
||
v-for="item in contractAttachmentTypeOptions"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value"
|
||
/>
|
||
</el-select>
|
||
</div>
|
||
<div class="contract-attachment-upload-dialog__uploader">
|
||
<el-upload
|
||
drag
|
||
multiple
|
||
:action="uploadAction"
|
||
:headers="uploadHeaders"
|
||
:accept="acceptText"
|
||
:show-file-list="true"
|
||
:before-upload="beforeUpload"
|
||
:on-success="handleDialogSuccess"
|
||
:on-error="handleDialogError"
|
||
:on-remove="handleDialogRemove"
|
||
:file-list="uploadDialogFileList"
|
||
>
|
||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||
</el-upload>
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<div class="contract-attachment-upload-dialog__footer">
|
||
<el-button type="primary" @click="confirmUploadDialog">确定</el-button>
|
||
<el-button type="primary" @click="uploadDialogVisible = false">取消</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { UploadFilled } from '@element-plus/icons-vue';
|
||
import { baseUrl } from '@/config/env';
|
||
import { getUploadHeaders } from '@/utils/upload';
|
||
import { downloadFileByUrl } from '@/utils/util';
|
||
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||
|
||
const CONTRACT_ATTACHMENT_TYPE_OTHER = '其他文件';
|
||
/** 用印材料历史取值兼容别名 */
|
||
export const CONTRACT_SEAL_MATERIAL_LOCATION = '用印材料';
|
||
export const CONTRACT_SEAL_MATERIAL_TYPE_ALIASES = {
|
||
合同文件: CONTRACT_SEAL_MATERIAL_LOCATION,
|
||
};
|
||
/** 佐证材料附件位置(历史:其它附件) */
|
||
export const CONTRACT_SUPPORTING_ATTACHMENT_LOCATION = '附件(佐证材料等)';
|
||
export const CONTRACT_ATTACHMENT_TYPE_DICT = 'contract_attachment_type';
|
||
export const CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT = 'contract_supporting_attachment_type';
|
||
export const isContractSupportingAttachmentLocation = location => {
|
||
const text = String(location || '');
|
||
return (
|
||
text === CONTRACT_SUPPORTING_ATTACHMENT_LOCATION ||
|
||
text === '其它附件' ||
|
||
text === '其他附件'
|
||
);
|
||
};
|
||
export const resolveAttachmentTypeDictCode = (location, fallback = CONTRACT_ATTACHMENT_TYPE_DICT) =>
|
||
isContractSupportingAttachmentLocation(location)
|
||
? CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT
|
||
: fallback || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||
export const defaultContractAttachmentTypeOptions = [
|
||
{ label: '用印材料', value: '用印材料' },
|
||
{ label: '双章归档文件', value: '双章归档文件' },
|
||
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||
];
|
||
// 特批附件归口附件(佐证材料等)区上传:高风险/黑名单客商合同必传,后端按 fileType=特批附件 校验并挂特批面板(与后端 HcsSyncConstant.ATTACHMENT_TYPE_SPEC_EXPLANATION 对齐)
|
||
export const defaultContractSupportingAttachmentTypeOptions = [
|
||
{ label: '特批附件', value: '特批附件' },
|
||
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||
];
|
||
/** @deprecated 兼容旧引用,实际选项由业务字典动态加载 */
|
||
export const contractAttachmentTypeOptions = defaultContractAttachmentTypeOptions;
|
||
export const isContractSealMaterialLocation = location => {
|
||
const text = String(location || '');
|
||
return text === CONTRACT_SEAL_MATERIAL_LOCATION || text === '合同文件';
|
||
};
|
||
export const attachmentFileTypes = [
|
||
'pdf',
|
||
'bmp',
|
||
'jpeg',
|
||
'png',
|
||
'jpg',
|
||
'doc',
|
||
'docx',
|
||
'ppt',
|
||
'pptx',
|
||
'xlsx',
|
||
'xls',
|
||
'eml',
|
||
'msg',
|
||
'zip',
|
||
];
|
||
|
||
export const attachmentName = row => row?.originalName || row?.name || row?.fileName || '附件';
|
||
export const attachmentUrl = row =>
|
||
row?.url || row?.link || row?.fileUrl || row?.downloadUrl || row?.domain || '';
|
||
export const isAttachmentApproved = row =>
|
||
row?.approved === true ||
|
||
row?.approved === 1 ||
|
||
row?.approved === '1' ||
|
||
row?.approvalStatus === 'approved';
|
||
|
||
const normalizeAttachmentTypeText = value => {
|
||
let text = String(value || '').split(/[?#]/)[0];
|
||
try {
|
||
text = decodeURIComponent(text);
|
||
} catch (error) {
|
||
// ignore decode error
|
||
}
|
||
return text
|
||
.replace(/\.[^.\\/]+$/, '')
|
||
.toLowerCase()
|
||
.replace(/[\s_\-—–·•()()\[\]【】{}《》<>“”"'、,,。.]/g, '');
|
||
};
|
||
|
||
const isOtherAttachmentTypeOption = item => {
|
||
const text = `${item?.label || ''}${item?.value || ''}`;
|
||
return (
|
||
String(item?.value) === CONTRACT_ATTACHMENT_TYPE_OTHER ||
|
||
String(item?.value) === '其它文件' ||
|
||
text.includes('其他文件') ||
|
||
text.includes('其它文件') ||
|
||
text.includes('其他') ||
|
||
text.includes('其它')
|
||
);
|
||
};
|
||
|
||
const attachmentTypeKeywords = fileType => {
|
||
const text = String(fileType || '');
|
||
if (text.includes('双章')) return ['双章归档文件', '双章归档', '双章'];
|
||
if (
|
||
text.includes('用印') ||
|
||
(text.includes('合同') && !text.includes('其他') && !text.includes('其它'))
|
||
) {
|
||
return ['用印材料', '合同文件', '用印', '合同'];
|
||
}
|
||
if (isOtherAttachmentTypeOption({ label: text, value: text })) return [];
|
||
return text ? [text] : [];
|
||
};
|
||
|
||
const resolveOtherAttachmentType = (options = []) => {
|
||
const list = options.length ? options : defaultContractAttachmentTypeOptions;
|
||
const matched = list.find(isOtherAttachmentTypeOption);
|
||
return matched?.value || CONTRACT_ATTACHMENT_TYPE_OTHER;
|
||
};
|
||
|
||
export const resolveContractAttachmentType = (
|
||
row = {},
|
||
options = defaultContractAttachmentTypeOptions
|
||
) => {
|
||
const list = options.length ? options : defaultContractAttachmentTypeOptions;
|
||
const otherType = resolveOtherAttachmentType(list);
|
||
const values = list.map(item => item.value);
|
||
const rawFileType = String(row.fileType || '');
|
||
const aliasedFileType = CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawFileType] || rawFileType;
|
||
if (values.includes(aliasedFileType)) return aliasedFileType;
|
||
if (values.includes(rawFileType)) return rawFileType;
|
||
const matchedByLabel = list.find(
|
||
item => String(item.label) === aliasedFileType || String(item.label) === rawFileType
|
||
);
|
||
if (matchedByLabel) return matchedByLabel.value;
|
||
// 行已带类型但字典未命中(如历史保存值、字典缺失)时保留原值,避免重开页面时被降级为其他文件
|
||
if (rawFileType) return aliasedFileType;
|
||
const fileName = normalizeAttachmentTypeText(attachmentName(row));
|
||
if (!fileName) return otherType;
|
||
const matched = list
|
||
.filter(item => !isOtherAttachmentTypeOption(item))
|
||
.flatMap(item =>
|
||
attachmentTypeKeywords(item.value)
|
||
.concat(attachmentTypeKeywords(item.label))
|
||
.map(keyword => ({
|
||
value: item.value,
|
||
keyword: normalizeAttachmentTypeText(keyword),
|
||
}))
|
||
)
|
||
.filter(item => item.keyword)
|
||
.sort((a, b) => b.keyword.length - a.keyword.length)
|
||
.find(item => fileName.includes(item.keyword));
|
||
// 文件名未命中任何附件类型时,默认「其他文件」
|
||
return matched?.value || otherType;
|
||
};
|
||
|
||
export const normalizeContractFileRows = (rows, options = defaultContractAttachmentTypeOptions) =>
|
||
(rows || []).map(row => ({
|
||
...row,
|
||
fileType: resolveContractAttachmentType(row, options),
|
||
description: row.description || '',
|
||
}));
|
||
|
||
const mapDictOptions = (response, dictCode = CONTRACT_ATTACHMENT_TYPE_DICT) => {
|
||
const data = response?.data?.data || response?.data || [];
|
||
const records = Array.isArray(data) ? data : data.records || [];
|
||
const applySealAlias = dictCode === CONTRACT_ATTACHMENT_TYPE_DICT;
|
||
return records
|
||
.map(item => {
|
||
const rawLabel = String(item.dictValue || '');
|
||
const rawValue = String(item.dictKey || '');
|
||
const label = applySealAlias
|
||
? CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawLabel] ||
|
||
(rawLabel === '合同文件' ? CONTRACT_SEAL_MATERIAL_LOCATION : rawLabel)
|
||
: rawLabel;
|
||
const value = applySealAlias
|
||
? CONTRACT_SEAL_MATERIAL_TYPE_ALIASES[rawValue] ||
|
||
(rawValue === '合同文件' ? CONTRACT_SEAL_MATERIAL_LOCATION : rawValue)
|
||
: rawValue;
|
||
return { label, value };
|
||
})
|
||
.filter(item => item.label && item.value);
|
||
};
|
||
|
||
const defaultOptionsByDictCode = dictCode =>
|
||
dictCode === CONTRACT_SUPPORTING_ATTACHMENT_TYPE_DICT
|
||
? [...defaultContractSupportingAttachmentTypeOptions]
|
||
: [...defaultContractAttachmentTypeOptions];
|
||
|
||
export default {
|
||
name: 'ContractAttachmentSection',
|
||
components: { UploadFilled },
|
||
props: {
|
||
title: { type: String, required: true },
|
||
rows: { type: Array, default: () => [] },
|
||
description: Boolean,
|
||
attachmentType: Boolean,
|
||
readonly: Boolean,
|
||
lockApproved: Boolean,
|
||
markApprovedOnUpload: Boolean,
|
||
useUploadDialog: Boolean,
|
||
dialogOnly: Boolean,
|
||
attachmentLocation: { type: String, default: CONTRACT_SEAL_MATERIAL_LOCATION },
|
||
attachmentLocationOptions: { type: Array, default: null },
|
||
/** 附件类型业务字典 code,默认用印材料字典;佐证材料传 contract_supporting_attachment_type */
|
||
attachmentTypeDictCode: {
|
||
type: String,
|
||
default: CONTRACT_ATTACHMENT_TYPE_DICT,
|
||
},
|
||
preview: { type: Function, default: null },
|
||
},
|
||
emits: ['update:rows', 'upload-to-location', 'upload-confirmed'],
|
||
data() {
|
||
return {
|
||
selected: [],
|
||
attachmentFileTypes,
|
||
contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions],
|
||
activeAttachmentTypeDictCode: CONTRACT_ATTACHMENT_TYPE_DICT,
|
||
uploadDialogVisible: false,
|
||
uploadDialogLocation: CONTRACT_SEAL_MATERIAL_LOCATION,
|
||
uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions),
|
||
uploadDialogFiles: [],
|
||
uploadDialogFileList: [],
|
||
};
|
||
},
|
||
computed: {
|
||
uploadAction() {
|
||
return `${baseUrl}/blade-resource/oss/endpoint/put-file`;
|
||
},
|
||
uploadHeaders() {
|
||
return getUploadHeaders();
|
||
},
|
||
acceptText() {
|
||
return this.attachmentFileTypes.map(item => `.${item}`).join(',');
|
||
},
|
||
defaultAttachmentType() {
|
||
return resolveOtherAttachmentType(this.contractAttachmentTypeOptions);
|
||
},
|
||
resolvedAttachmentTypeDictCode() {
|
||
return this.attachmentTypeDictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||
},
|
||
resolvedLocationOptions() {
|
||
const options = (this.attachmentLocationOptions || []).filter(Boolean);
|
||
if (options.length) {
|
||
return options.map(item => {
|
||
if (item === '合同文件') return CONTRACT_SEAL_MATERIAL_LOCATION;
|
||
if (item === '其它附件' || item === '其他附件') {
|
||
return CONTRACT_SUPPORTING_ATTACHMENT_LOCATION;
|
||
}
|
||
return item;
|
||
});
|
||
}
|
||
return [CONTRACT_SEAL_MATERIAL_LOCATION, CONTRACT_SUPPORTING_ATTACHMENT_LOCATION];
|
||
},
|
||
},
|
||
watch: {
|
||
attachmentTypeDictCode: {
|
||
immediate: false,
|
||
handler() {
|
||
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
|
||
},
|
||
},
|
||
},
|
||
created() {
|
||
this.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
|
||
},
|
||
methods: {
|
||
attachmentName,
|
||
currentUploadUserName() {
|
||
const raw = this.$store.getters.userInfo;
|
||
const userInfo = raw && !Array.isArray(raw) ? raw : {};
|
||
// 与顶部栏一致优先 real_name;用印材料 / 佐证材料共用
|
||
return (
|
||
userInfo.real_name ||
|
||
userInfo.realName ||
|
||
userInfo.nick_name ||
|
||
userInfo.nickName ||
|
||
userInfo.user_name ||
|
||
userInfo.userName ||
|
||
userInfo.account ||
|
||
''
|
||
);
|
||
},
|
||
loadAttachmentTypeOptions(dictCode = this.resolvedAttachmentTypeDictCode, alignRows = false) {
|
||
const code = dictCode || CONTRACT_ATTACHMENT_TYPE_DICT;
|
||
this.activeAttachmentTypeDictCode = code;
|
||
const fallbackOptions = defaultOptionsByDictCode(code);
|
||
return getBizDictionary({ code })
|
||
.then(res => {
|
||
// 切换字典期间若已发起更新请求,忽略过期响应
|
||
if (this.activeAttachmentTypeDictCode !== code) return;
|
||
const options = mapDictOptions(res, code);
|
||
if (options.length) {
|
||
// 字典缺少「其他文件」时补上,保证未匹配文件名可默认选中
|
||
this.contractAttachmentTypeOptions = options.some(isOtherAttachmentTypeOption)
|
||
? options
|
||
: [...options, { label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }];
|
||
} else {
|
||
this.contractAttachmentTypeOptions = fallbackOptions;
|
||
}
|
||
if (
|
||
!this.contractAttachmentTypeOptions.some(
|
||
item => String(item.value) === String(this.uploadDialogType)
|
||
)
|
||
) {
|
||
this.uploadDialogType = this.defaultAttachmentType;
|
||
}
|
||
// 字典加载后,把已有行的中文类型名对齐到 dictKey,保证下拉可选中
|
||
if (alignRows && this.attachmentType && (this.rows || []).length) {
|
||
const aligned = (this.rows || []).map(row => ({
|
||
...row,
|
||
fileType: resolveContractAttachmentType(row, this.contractAttachmentTypeOptions),
|
||
}));
|
||
const changed = aligned.some(
|
||
(row, index) => row.fileType !== this.rows[index]?.fileType
|
||
);
|
||
if (changed) this.update(aligned);
|
||
}
|
||
})
|
||
.catch(() => {
|
||
if (this.activeAttachmentTypeDictCode !== code) return;
|
||
this.contractAttachmentTypeOptions = fallbackOptions;
|
||
this.uploadDialogType = this.defaultAttachmentType;
|
||
});
|
||
},
|
||
isRowLocked(row) {
|
||
return this.lockApproved && isAttachmentApproved(row);
|
||
},
|
||
update(rows) {
|
||
this.$emit('update:rows', Array.isArray(rows) ? rows.map(item => ({ ...item })) : []);
|
||
},
|
||
enrichUploadedRow(row, existing) {
|
||
const uploadUserName = this.currentUploadUserName();
|
||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||
const next = {
|
||
...existing,
|
||
...row,
|
||
description: row.description || existing?.description || '',
|
||
// 新上传行始终写入当前用户;已有行保留原上传人
|
||
uploadUserName: existing?.uploadUserName || uploadUserName || row.uploadUserName || '',
|
||
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
|
||
approved: existing?.approved || row.approved || false,
|
||
};
|
||
if (this.attachmentType) {
|
||
next.fileType = existing?.fileType
|
||
? existing.fileType
|
||
: resolveContractAttachmentType(
|
||
{ ...next, fileType: '' },
|
||
this.contractAttachmentTypeOptions
|
||
);
|
||
}
|
||
if (this.markApprovedOnUpload && !existing) next.approved = true;
|
||
return next;
|
||
},
|
||
findExistingRow(row, rows = this.rows) {
|
||
const fileUrl = attachmentUrl(row);
|
||
return (rows || []).find(item => {
|
||
const sameUid = row.uid && item.uid && String(row.uid) === String(item.uid);
|
||
const sameUrl = fileUrl && attachmentUrl(item) === fileUrl;
|
||
return sameUid || sameUrl;
|
||
});
|
||
},
|
||
updateRowFileType(index, value) {
|
||
const rows = [...(this.rows || [])];
|
||
const row = rows[index];
|
||
if (!row || this.isRowLocked(row)) return;
|
||
rows[index] = { ...row, fileType: value };
|
||
this.update(rows);
|
||
},
|
||
updateRowDescription(index, value) {
|
||
const rows = [...(this.rows || [])];
|
||
const row = rows[index];
|
||
if (!row || this.isRowLocked(row)) return;
|
||
rows[index] = { ...row, description: value };
|
||
this.update(rows);
|
||
},
|
||
resolveTypeByLocation(location) {
|
||
const options = this.contractAttachmentTypeOptions || [];
|
||
const locationText = String(location || '');
|
||
if (
|
||
isContractSupportingAttachmentLocation(locationText) ||
|
||
locationText.includes('佐证') ||
|
||
locationText.includes('其它') ||
|
||
locationText.includes('其他')
|
||
) {
|
||
return this.defaultAttachmentType;
|
||
}
|
||
const matched = options.find(
|
||
item =>
|
||
String(item.value) === locationText ||
|
||
String(item.label) === locationText ||
|
||
String(item.value).includes('用印') ||
|
||
String(item.label).includes('用印') ||
|
||
String(item.value).includes('合同') ||
|
||
String(item.label).includes('合同')
|
||
);
|
||
return matched?.value || options[0]?.value || this.defaultAttachmentType;
|
||
},
|
||
syncUploadDialogTypeByLocation() {
|
||
const dictCode = resolveAttachmentTypeDictCode(
|
||
this.uploadDialogLocation,
|
||
this.resolvedAttachmentTypeDictCode
|
||
);
|
||
const applyType = () => {
|
||
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation);
|
||
};
|
||
if (dictCode !== this.activeAttachmentTypeDictCode) {
|
||
this.loadAttachmentTypeOptions(dictCode, false).finally(applyType);
|
||
return;
|
||
}
|
||
applyType();
|
||
},
|
||
openUploadDialog() {
|
||
const options = this.resolvedLocationOptions;
|
||
this.uploadDialogLocation = options.includes(this.attachmentLocation)
|
||
? this.attachmentLocation
|
||
: options[0] || this.attachmentLocation;
|
||
this.syncUploadDialogTypeByLocation();
|
||
this.uploadDialogFiles = [];
|
||
this.uploadDialogFileList = [];
|
||
this.uploadDialogVisible = true;
|
||
},
|
||
resetUploadDialog() {
|
||
this.uploadDialogFiles = [];
|
||
this.uploadDialogFileList = [];
|
||
this.uploadDialogLocation = this.attachmentLocation;
|
||
this.syncUploadDialogTypeByLocation();
|
||
},
|
||
beforeUpload(file) {
|
||
const extension = String(file.name || '')
|
||
.split('.')
|
||
.pop()
|
||
?.toLowerCase();
|
||
if (!this.attachmentFileTypes.includes(extension)) {
|
||
this.$message.warning('文件类型不符合上传要求');
|
||
return false;
|
||
}
|
||
if (file.size / 1024 / 1024 > 50) {
|
||
this.$message.warning('单文件大小不能超过 50MB');
|
||
return false;
|
||
}
|
||
return true;
|
||
},
|
||
normalizeUploadFile(file, response) {
|
||
const data = response?.data || file.response?.data || {};
|
||
const url = data.link || data.url || data.domain || file.url || '';
|
||
const originalName =
|
||
data.originalName || file.name || data.name || attachmentName({ url }) || '附件';
|
||
return {
|
||
uid: file.uid,
|
||
originalName,
|
||
name: originalName,
|
||
url,
|
||
link: data.link || url,
|
||
size: data.size || data.attachSize || file.size || '',
|
||
};
|
||
},
|
||
handleDialogSuccess(response, file, fileList) {
|
||
if (!response || response.success === false || (response.code && response.code !== 200)) {
|
||
this.$message.error(response?.msg || '上传失败');
|
||
this.uploadDialogFileList = (fileList || []).filter(item => item.uid !== file.uid);
|
||
return;
|
||
}
|
||
const normalized = this.normalizeUploadFile(file, response);
|
||
this.uploadDialogFileList = fileList || [];
|
||
this.uploadDialogFiles = [
|
||
...this.uploadDialogFiles.filter(item => String(item.uid) !== String(file.uid)),
|
||
normalized,
|
||
];
|
||
const locationText = String(this.uploadDialogLocation || '');
|
||
if (!(locationText.includes('其它') || locationText.includes('其他'))) {
|
||
this.uploadDialogType = resolveContractAttachmentType(
|
||
{
|
||
...normalized,
|
||
fileType: '',
|
||
},
|
||
this.contractAttachmentTypeOptions
|
||
);
|
||
} else {
|
||
this.uploadDialogType = this.uploadDialogType || this.defaultAttachmentType;
|
||
}
|
||
this.$message.success('上传成功');
|
||
},
|
||
handleDialogError() {
|
||
this.$message.error('上传失败');
|
||
},
|
||
handleDialogRemove(file) {
|
||
this.uploadDialogFiles = this.uploadDialogFiles.filter(
|
||
item => String(item.uid) !== String(file.uid)
|
||
);
|
||
this.uploadDialogFileList = this.uploadDialogFileList.filter(
|
||
item => String(item.uid) !== String(file.uid)
|
||
);
|
||
},
|
||
confirmUploadDialog() {
|
||
if (!this.uploadDialogFiles.length) {
|
||
this.$message.warning('请先上传附件');
|
||
return;
|
||
}
|
||
if (!this.uploadDialogLocation) {
|
||
this.$message.warning('请选择附件位置');
|
||
return;
|
||
}
|
||
if (!this.uploadDialogType) {
|
||
this.$message.warning('请选择附件类型');
|
||
return;
|
||
}
|
||
const uploadUserName = this.currentUploadUserName();
|
||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||
const appended = this.uploadDialogFiles.map(row => ({
|
||
...row,
|
||
fileType: this.uploadDialogType,
|
||
description: row.description || '',
|
||
uploadUserName: uploadUserName || row.uploadUserName || '',
|
||
uploadTime: row.uploadTime || uploadTime,
|
||
...(this.markApprovedOnUpload ? { approved: true } : {}),
|
||
}));
|
||
// 统一交给父组件按附件位置写入对应列表,避免同位置双写或跨位置不显示
|
||
this.$emit('upload-to-location', {
|
||
location: this.uploadDialogLocation,
|
||
rows: appended,
|
||
});
|
||
this.uploadDialogVisible = false;
|
||
this.$emit('upload-confirmed', {
|
||
location: this.uploadDialogLocation,
|
||
rows: appended,
|
||
});
|
||
},
|
||
handleChange(list) {
|
||
if (!Array.isArray(list)) return;
|
||
// 上传组件偶发回传空列表时,禁止清空已有表格数据
|
||
if (!list.length) return;
|
||
const existingRows = this.rows || [];
|
||
const nextRows = list.map(row => {
|
||
const existing = this.findExistingRow(row, existingRows);
|
||
if (existing && this.isRowLocked(existing)) return { ...existing };
|
||
return this.enrichUploadedRow(row, existing);
|
||
});
|
||
existingRows.forEach(item => {
|
||
if (!this.isRowLocked(item)) return;
|
||
if (this.findExistingRow(item, nextRows)) return;
|
||
nextRows.unshift({ ...item });
|
||
});
|
||
this.update(nextRows);
|
||
},
|
||
handleUploadSuccess(file) {
|
||
const url = attachmentUrl(file);
|
||
if (!file || !url) return;
|
||
if (this.findExistingRow(file)) return;
|
||
this.update([...(this.rows || []), this.enrichUploadedRow(file)]);
|
||
},
|
||
remove(index) {
|
||
const row = this.rows[index];
|
||
if (this.isRowLocked(row)) {
|
||
this.$message.warning('已审核通过的附件不允许删除');
|
||
return;
|
||
}
|
||
const rows = [...(this.rows || [])];
|
||
rows.splice(index, 1);
|
||
this.update(rows);
|
||
},
|
||
download(row) {
|
||
const url = attachmentUrl(row);
|
||
if (!url) {
|
||
this.$message.warning('附件地址为空,无法下载');
|
||
return;
|
||
}
|
||
downloadFileByUrl(url, attachmentName(row));
|
||
},
|
||
batchDownload() {
|
||
const sourceRows = this.selected.length ? this.selected : this.rows || [];
|
||
const downloadableRows = sourceRows.filter(row => attachmentUrl(row));
|
||
if (!downloadableRows.length) {
|
||
this.$message.warning(this.selected.length ? '所选附件暂无可下载地址' : '暂无可下载附件');
|
||
return;
|
||
}
|
||
downloadableRows.forEach((row, index) => {
|
||
window.setTimeout(() => this.download(row), index * 400);
|
||
});
|
||
},
|
||
formatSize(size) {
|
||
const value = Number(size);
|
||
if (!value) return '';
|
||
if (value < 1024) return `${value}B`;
|
||
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)}KB`;
|
||
return `${(value / 1024 / 1024).toFixed(1)}MB`;
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.contract-manage-form__section {
|
||
margin: 12px 0 0;
|
||
padding: 14px 16px 16px;
|
||
overflow: visible;
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||
|
||
.dialog-section-title {
|
||
margin-bottom: 20px;
|
||
color: #303133;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
|
||
&::before {
|
||
display: inline-block;
|
||
width: 4px;
|
||
height: 16px;
|
||
margin-right: 8px;
|
||
vertical-align: -2px;
|
||
background: #409eff;
|
||
content: '';
|
||
}
|
||
}
|
||
}
|
||
|
||
.contract-manage-form__attachment-table {
|
||
:deep(.el-table__body-wrapper),
|
||
:deep(.el-table__header-wrapper),
|
||
:deep(.el-table__cell),
|
||
:deep(.cell) {
|
||
overflow: visible;
|
||
}
|
||
}
|
||
|
||
.contract-manage-form__attachment-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
|
||
.dialog-section-title {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.contract-manage-form__attachment-upload {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.contract-manage-form__attachment-tip {
|
||
margin-left: 30px;
|
||
color: #909399;
|
||
font-size: 13px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.contract-attachment-upload-dialog {
|
||
&__body {
|
||
padding: 4px 8px 0;
|
||
}
|
||
|
||
&__field {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
&__label {
|
||
flex: none;
|
||
width: 72px;
|
||
color: #606266;
|
||
text-align: right;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
&__uploader {
|
||
margin-top: 8px;
|
||
|
||
:deep(.el-upload) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.el-upload-dragger) {
|
||
width: 100%;
|
||
height: 180px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
&__footer {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
</style>
|