✨ 完善合同附件上传回填、类型匹配与列表附件弹窗
修复上传成功后表格不回显,附件类型可切换,未匹配文件名默认其他文件;列表附件上传支持合同文件/其它附件。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -212,7 +212,14 @@ export default {
|
||||
.filter(Boolean);
|
||||
},
|
||||
acceptText() {
|
||||
return this.acceptedList.join(',');
|
||||
return this.acceptedList
|
||||
.map(item => {
|
||||
const value = String(item).trim();
|
||||
if (!value || value.includes('/') || value.startsWith('.')) return value;
|
||||
return `.${value}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(',');
|
||||
},
|
||||
tipText() {
|
||||
return this.tip || `${UNIFIED_ATTACHMENT_TIP_PREFIX}${this.maxSize}M`;
|
||||
@@ -304,12 +311,22 @@ export default {
|
||||
});
|
||||
},
|
||||
handleSuccess(response, file, files) {
|
||||
if (!response || response.success === false || (response.code && response.code !== 200)) {
|
||||
const code = response?.code;
|
||||
const codeInvalid = code !== undefined && code !== null && code !== '' && Number(code) !== 200;
|
||||
if (!response || response.success === false || codeInvalid) {
|
||||
this.$message.error((response && response.msg) || '上传失败');
|
||||
return;
|
||||
}
|
||||
this.emitUploadFiles(this.multiple ? files : [file]);
|
||||
this.$emit('success', this.normalizeUploadFile(file));
|
||||
if (file && !file.response) {
|
||||
file.response = response;
|
||||
}
|
||||
const normalized = this.normalizeUploadFile(file, response);
|
||||
if (!normalized.url) {
|
||||
this.$message.error('上传成功但未返回文件地址');
|
||||
return;
|
||||
}
|
||||
this.$emit('success', normalized);
|
||||
this.emitUploadFiles(this.multiple ? files : [file], normalized);
|
||||
this.$message.success('上传成功');
|
||||
},
|
||||
handleChange(file, files) {
|
||||
@@ -323,35 +340,89 @@ export default {
|
||||
this.emitUploadFiles(files);
|
||||
return true;
|
||||
},
|
||||
emitUploadFiles(files) {
|
||||
const list = files
|
||||
.filter(file => file.status === 'success' || this.getFileUrl(file))
|
||||
.map(this.normalizeUploadFile)
|
||||
emitUploadFiles(files, preferredFile) {
|
||||
const list = (files || [])
|
||||
.filter(file => {
|
||||
if (file?.status === 'fail') return false;
|
||||
return (
|
||||
file?.status === 'success' ||
|
||||
this.getFileUrl(file) ||
|
||||
file?.response?.data ||
|
||||
file?.response?.link ||
|
||||
file?.response
|
||||
);
|
||||
})
|
||||
.map(file => this.normalizeUploadFile(file))
|
||||
.filter(item => item.url);
|
||||
this.$emit('update:modelValue', list);
|
||||
this.$emit('change', list);
|
||||
|
||||
if (preferredFile?.url) {
|
||||
const exists = list.some(
|
||||
item =>
|
||||
(preferredFile.uid && item.uid && String(item.uid) === String(preferredFile.uid)) ||
|
||||
item.url === preferredFile.url
|
||||
);
|
||||
if (!exists) list.push(preferredFile);
|
||||
}
|
||||
|
||||
const current = this.parseValue(this.modelValue).map(item => ({
|
||||
...item,
|
||||
url: this.getFileUrl(item),
|
||||
}));
|
||||
const merged = [...current];
|
||||
list.forEach(file => {
|
||||
const index = merged.findIndex(
|
||||
item =>
|
||||
(file.uid && item.uid && String(item.uid) === String(file.uid)) ||
|
||||
(file.url && this.getFileUrl(item) === file.url)
|
||||
);
|
||||
if (index >= 0) merged.splice(index, 1, { ...merged[index], ...file });
|
||||
else merged.push(file);
|
||||
});
|
||||
|
||||
const result = merged.filter(item => this.getFileUrl(item));
|
||||
this.$emit('update:modelValue', result);
|
||||
this.$emit('change', result);
|
||||
},
|
||||
normalizeUploadFile(file) {
|
||||
const data = (file.response && file.response.data) || file.data || file;
|
||||
const url = data.link || data.url || data.domain || file.url || '';
|
||||
normalizeUploadFile(file, responseOverride) {
|
||||
const response = responseOverride || file?.response;
|
||||
let data = {};
|
||||
if (response && typeof response === 'object') {
|
||||
if (response.data && typeof response.data === 'object') {
|
||||
data = response.data;
|
||||
} else if (response.link || response.url || response.domain) {
|
||||
data = response;
|
||||
}
|
||||
}
|
||||
if (!data.link && !data.url && !data.domain) {
|
||||
data = file?.data && typeof file.data === 'object' ? file.data : file || {};
|
||||
}
|
||||
const url =
|
||||
data.link ||
|
||||
data.url ||
|
||||
data.domain ||
|
||||
data.fileLink ||
|
||||
data.fileUrl ||
|
||||
file?.url ||
|
||||
file?.link ||
|
||||
'';
|
||||
const originalName =
|
||||
data.originalName ||
|
||||
file.originalName ||
|
||||
file.name ||
|
||||
file?.originalName ||
|
||||
file?.name ||
|
||||
data.name ||
|
||||
this.getFileName(url) ||
|
||||
'附件';
|
||||
const extension = this.getExtension({ name: originalName, url });
|
||||
return {
|
||||
...data,
|
||||
uid: file.uid || data.uid,
|
||||
uid: file?.uid || data.uid,
|
||||
originalName,
|
||||
name: originalName,
|
||||
url,
|
||||
link: data.link || url,
|
||||
size: data.size || data.attachSize || file.size || '',
|
||||
size: data.size || data.attachSize || file?.size || '',
|
||||
extension,
|
||||
mimeType: data.mimeType || data.contentType || file.raw?.type || MIME_MAP[extension] || '',
|
||||
mimeType: data.mimeType || data.contentType || file?.raw?.type || MIME_MAP[extension] || '',
|
||||
};
|
||||
},
|
||||
handlePreview(file) {
|
||||
|
||||
@@ -1,94 +1,102 @@
|
||||
<template>
|
||||
<section
|
||||
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"
|
||||
<div class="contract-attachment-section">
|
||||
<section
|
||||
v-if="!dialogOnly"
|
||||
class="contract-manage-form__section contract-manage-form__section--panel contract-manage-form__section--attachment"
|
||||
>
|
||||
<el-table-column v-if="!readonly" 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 }">
|
||||
<span v-if="readonly || isRowLocked(row)">{{ row.fileType || '-' }}</span>
|
||||
<el-select
|
||||
v-else
|
||||
:model-value="row.fileType"
|
||||
placeholder="请选择"
|
||||
:teleported="false"
|
||||
:fit-input-width="true"
|
||||
@update:model-value="value => updateRowFileType(row, value)"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in contractAttachmentTypeOptions"
|
||||
:key="item.value"
|
||||
: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
|
||||
<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"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="preview?.(row, rows)">{{ attachmentName(row) }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="description" label="附件描述" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<span v-if="readonly || isRowLocked(row)">{{ row.description || '-' }}</span>
|
||||
<el-input
|
||||
v-else
|
||||
:model-value="row.description"
|
||||
maxlength="200"
|
||||
@update:model-value="value => updateRowDescription(row, 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>
|
||||
<el-table-column v-if="!readonly" 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
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="preview?.(row, rows)">{{
|
||||
attachmentName(row)
|
||||
}}</el-link>
|
||||
</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="上传附件"
|
||||
@update:model-value="handleChange"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
<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"
|
||||
@@ -103,7 +111,12 @@
|
||||
<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%">
|
||||
<el-select
|
||||
v-model="uploadDialogLocation"
|
||||
placeholder="请选择附件位置"
|
||||
style="width: 100%"
|
||||
@change="syncUploadDialogTypeByLocation"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in resolvedLocationOptions"
|
||||
:key="item"
|
||||
@@ -149,7 +162,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -159,11 +172,11 @@ import { getUploadHeaders } from '@/utils/upload';
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||
|
||||
const CONTRACT_ATTACHMENT_TYPE_OTHER = '其它文件';
|
||||
const CONTRACT_ATTACHMENT_TYPE_OTHER = '其他文件';
|
||||
export const defaultContractAttachmentTypeOptions = [
|
||||
{ label: '合同文件', value: '合同文件' },
|
||||
{ label: '双章归档文件', value: '双章归档文件' },
|
||||
{ label: '其它文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||||
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||||
];
|
||||
/** @deprecated 兼容旧引用,实际选项由业务字典 contract_attachment_type 动态加载 */
|
||||
export const contractAttachmentTypeOptions = defaultContractAttachmentTypeOptions;
|
||||
@@ -206,21 +219,32 @@ const normalizeAttachmentTypeText = value => {
|
||||
.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 => {
|
||||
if (fileType === '双章归档文件') return ['双章归档文件', '双章归档', '双章'];
|
||||
if (fileType === '合同文件') return ['合同文件', '合同'];
|
||||
return [fileType];
|
||||
const text = String(fileType || '');
|
||||
if (text.includes('双章')) return ['双章归档文件', '双章归档', '双章'];
|
||||
if (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(
|
||||
item =>
|
||||
String(item.value) === CONTRACT_ATTACHMENT_TYPE_OTHER ||
|
||||
String(item.label).includes('其它') ||
|
||||
String(item.label).includes('其他')
|
||||
);
|
||||
return matched?.value || list[list.length - 1]?.value || CONTRACT_ATTACHMENT_TYPE_OTHER;
|
||||
const matched = list.find(isOtherAttachmentTypeOption);
|
||||
return matched?.value || CONTRACT_ATTACHMENT_TYPE_OTHER;
|
||||
};
|
||||
|
||||
export const resolveContractAttachmentType = (
|
||||
@@ -228,22 +252,27 @@ export const resolveContractAttachmentType = (
|
||||
options = defaultContractAttachmentTypeOptions
|
||||
) => {
|
||||
const list = options.length ? options : defaultContractAttachmentTypeOptions;
|
||||
const otherType = resolveOtherAttachmentType(list);
|
||||
const values = list.map(item => item.value);
|
||||
if (values.includes(row.fileType)) return row.fileType;
|
||||
const matchedByLabel = list.find(item => String(item.label) === String(row.fileType));
|
||||
if (matchedByLabel) return matchedByLabel.value;
|
||||
const fileName = normalizeAttachmentTypeText(attachmentName(row));
|
||||
const otherType = resolveOtherAttachmentType(list);
|
||||
if (!fileName) return otherType;
|
||||
const matched = list
|
||||
.filter(item => item.value !== otherType)
|
||||
.filter(item => !isOtherAttachmentTypeOption(item))
|
||||
.flatMap(item =>
|
||||
attachmentTypeKeywords(item.value).map(keyword => ({
|
||||
value: item.value,
|
||||
keyword: normalizeAttachmentTypeText(keyword),
|
||||
}))
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -277,11 +306,12 @@ export default {
|
||||
lockApproved: Boolean,
|
||||
markApprovedOnUpload: Boolean,
|
||||
useUploadDialog: Boolean,
|
||||
dialogOnly: Boolean,
|
||||
attachmentLocation: { type: String, default: '合同文件' },
|
||||
attachmentLocationOptions: { type: Array, default: null },
|
||||
preview: { type: Function, default: null },
|
||||
},
|
||||
emits: ['update:rows', 'upload-to-location'],
|
||||
emits: ['update:rows', 'upload-to-location', 'upload-confirmed'],
|
||||
data() {
|
||||
return {
|
||||
selected: [],
|
||||
@@ -323,12 +353,28 @@ export default {
|
||||
.then(res => {
|
||||
const options = mapDictOptions(res);
|
||||
if (options.length) {
|
||||
this.contractAttachmentTypeOptions = options;
|
||||
// 字典缺少「其他文件」时补上,保证未匹配文件名可默认选中
|
||||
this.contractAttachmentTypeOptions = options.some(isOtherAttachmentTypeOption)
|
||||
? options
|
||||
: [...options, { label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }];
|
||||
if (
|
||||
!options.some(item => String(item.value) === String(this.uploadDialogType))
|
||||
!this.contractAttachmentTypeOptions.some(
|
||||
item => String(item.value) === String(this.uploadDialogType)
|
||||
)
|
||||
) {
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
}
|
||||
// 字典加载后,把已有行的中文类型名对齐到 dictKey,保证下拉可选中
|
||||
if (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(() => {
|
||||
@@ -339,24 +385,76 @@ export default {
|
||||
return this.lockApproved && isAttachmentApproved(row);
|
||||
},
|
||||
update(rows) {
|
||||
this.$emit('update:rows', rows);
|
||||
this.$emit('update:rows', Array.isArray(rows) ? rows.map(item => ({ ...item })) : []);
|
||||
},
|
||||
updateRowFileType(row, value) {
|
||||
if (this.isRowLocked(row)) return;
|
||||
row.fileType = value;
|
||||
this.update([...this.rows]);
|
||||
enrichUploadedRow(row, existing) {
|
||||
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
||||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
const next = {
|
||||
...existing,
|
||||
...row,
|
||||
description: row.description || existing?.description || '',
|
||||
uploadUserName: existing?.uploadUserName || row.uploadUserName || 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;
|
||||
},
|
||||
updateRowDescription(row, value) {
|
||||
if (this.isRowLocked(row)) return;
|
||||
row.description = value;
|
||||
this.update([...this.rows]);
|
||||
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 (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('合同')
|
||||
);
|
||||
return matched?.value || options[0]?.value || this.defaultAttachmentType;
|
||||
},
|
||||
syncUploadDialogTypeByLocation() {
|
||||
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation);
|
||||
},
|
||||
openUploadDialog() {
|
||||
const options = this.resolvedLocationOptions;
|
||||
this.uploadDialogLocation = options.includes(this.attachmentLocation)
|
||||
? this.attachmentLocation
|
||||
: options[0] || this.attachmentLocation;
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
this.syncUploadDialogTypeByLocation();
|
||||
this.uploadDialogFiles = [];
|
||||
this.uploadDialogFileList = [];
|
||||
this.uploadDialogVisible = true;
|
||||
@@ -365,7 +463,7 @@ export default {
|
||||
this.uploadDialogFiles = [];
|
||||
this.uploadDialogFileList = [];
|
||||
this.uploadDialogLocation = this.attachmentLocation;
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
this.syncUploadDialogTypeByLocation();
|
||||
},
|
||||
beforeUpload(file) {
|
||||
const extension = String(file.name || '')
|
||||
@@ -408,13 +506,18 @@ export default {
|
||||
...this.uploadDialogFiles.filter(item => String(item.uid) !== String(file.uid)),
|
||||
normalized,
|
||||
];
|
||||
this.uploadDialogType = resolveContractAttachmentType(
|
||||
{
|
||||
...normalized,
|
||||
fileType: '',
|
||||
},
|
||||
this.contractAttachmentTypeOptions
|
||||
);
|
||||
const locationText = String(this.uploadDialogLocation || '');
|
||||
if (!(locationText.includes('其它') || locationText.includes('其他'))) {
|
||||
this.uploadDialogType = resolveContractAttachmentType(
|
||||
{
|
||||
...normalized,
|
||||
fileType: '',
|
||||
},
|
||||
this.contractAttachmentTypeOptions
|
||||
);
|
||||
} else {
|
||||
this.uploadDialogType = this.defaultAttachmentType;
|
||||
}
|
||||
this.$message.success('上传成功');
|
||||
},
|
||||
handleDialogError() {
|
||||
@@ -451,50 +554,39 @@ export default {
|
||||
uploadTime: row.uploadTime || uploadTime,
|
||||
...(this.markApprovedOnUpload ? { approved: true } : {}),
|
||||
}));
|
||||
if (this.uploadDialogLocation === this.attachmentLocation) {
|
||||
this.update([...(this.rows || []), ...appended]);
|
||||
} else {
|
||||
this.$emit('upload-to-location', {
|
||||
location: this.uploadDialogLocation,
|
||||
rows: appended,
|
||||
});
|
||||
}
|
||||
// 统一交给父组件按附件位置写入对应列表,避免同位置双写或跨位置不显示
|
||||
this.$emit('upload-to-location', {
|
||||
location: this.uploadDialogLocation,
|
||||
rows: appended,
|
||||
});
|
||||
this.uploadDialogVisible = false;
|
||||
this.$emit('upload-confirmed', {
|
||||
location: this.uploadDialogLocation,
|
||||
rows: appended,
|
||||
});
|
||||
},
|
||||
handleChange(rows) {
|
||||
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
||||
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
handleChange(list) {
|
||||
if (!Array.isArray(list)) return;
|
||||
// 上传组件偶发回传空列表时,禁止清空已有表格数据
|
||||
if (!list.length) return;
|
||||
const existingRows = this.rows || [];
|
||||
this.update(
|
||||
(rows || []).map(row => {
|
||||
const fileUrl = attachmentUrl(row);
|
||||
const existing = existingRows.find(item => {
|
||||
const sameUid = row.uid && item.uid && String(row.uid) === String(item.uid);
|
||||
const sameUrl = fileUrl && attachmentUrl(item) === fileUrl;
|
||||
return sameUid || sameUrl;
|
||||
});
|
||||
if (existing && this.isRowLocked(existing)) return { ...existing };
|
||||
const next = {
|
||||
...existing,
|
||||
...row,
|
||||
description: row.description || existing?.description || '',
|
||||
uploadUserName: existing?.uploadUserName || row.uploadUserName || uploadUserName,
|
||||
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
|
||||
approved: existing?.approved || row.approved || false,
|
||||
};
|
||||
if (this.attachmentType) {
|
||||
next.fileType = resolveContractAttachmentType(
|
||||
{
|
||||
...next,
|
||||
fileType: existing?.fileType || row.fileType,
|
||||
},
|
||||
this.contractAttachmentTypeOptions
|
||||
);
|
||||
}
|
||||
if (this.markApprovedOnUpload && !existing) next.approved = true;
|
||||
return next;
|
||||
})
|
||||
);
|
||||
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];
|
||||
@@ -502,7 +594,7 @@ export default {
|
||||
this.$message.warning('已审核通过的附件不允许删除');
|
||||
return;
|
||||
}
|
||||
const rows = [...this.rows];
|
||||
const rows = [...(this.rows || [])];
|
||||
rows.splice(index, 1);
|
||||
this.update(rows);
|
||||
},
|
||||
@@ -515,7 +607,7 @@ export default {
|
||||
downloadFileByUrl(url, attachmentName(row));
|
||||
},
|
||||
batchDownload() {
|
||||
(this.selected.length ? this.selected : this.rows).forEach(this.download);
|
||||
(this.selected.length ? this.selected : this.rows || []).forEach(this.download);
|
||||
},
|
||||
formatSize(size) {
|
||||
const value = Number(size);
|
||||
@@ -532,7 +624,7 @@ export default {
|
||||
.contract-manage-form__section {
|
||||
margin: 12px 0 0;
|
||||
padding: 14px 16px 16px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
@@ -555,6 +647,15 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
@@ -189,7 +189,6 @@
|
||||
title="合同文件"
|
||||
description
|
||||
attachment-type
|
||||
use-upload-dialog
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
:rows="contractFileRows"
|
||||
:preview="previewAttachment"
|
||||
@@ -258,7 +257,6 @@
|
||||
title="其它附件"
|
||||
description
|
||||
attachment-type
|
||||
use-upload-dialog
|
||||
attachment-location="其它附件"
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
:rows="attachments"
|
||||
@@ -285,7 +283,6 @@
|
||||
title="变更材料"
|
||||
description
|
||||
attachment-type
|
||||
use-upload-dialog
|
||||
attachment-location="变更材料"
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
:rows="changeMaterials"
|
||||
|
||||
@@ -592,7 +592,6 @@
|
||||
title="合同文件"
|
||||
description
|
||||
attachment-type
|
||||
use-upload-dialog
|
||||
:attachment-location-options="contractAttachmentLocationOptions"
|
||||
:lock-approved="isAttachmentUploadMode || hasApprovedContractFiles"
|
||||
:mark-approved-on-upload="isAttachmentUploadMode"
|
||||
@@ -813,7 +812,6 @@
|
||||
title="其它附件"
|
||||
description
|
||||
attachment-type
|
||||
use-upload-dialog
|
||||
attachment-location="其它附件"
|
||||
:attachment-location-options="contractAttachmentLocationOptions"
|
||||
:readonly="isAttachmentUploadMode"
|
||||
@@ -925,6 +923,21 @@
|
||||
@save="saveBillingPlan"
|
||||
/>
|
||||
|
||||
<contract-attachment-section
|
||||
ref="listAttachmentUploader"
|
||||
dialog-only
|
||||
title="合同文件"
|
||||
description
|
||||
attachment-type
|
||||
use-upload-dialog
|
||||
mark-approved-on-upload
|
||||
:attachment-location-options="['合同文件', '其它附件']"
|
||||
:rows="listUploadContractFileRows"
|
||||
@update:rows="listUploadContractFileRows = $event"
|
||||
@upload-to-location="handleListAttachmentUploadToLocation"
|
||||
@upload-confirmed="handleListAttachmentUploadConfirmed"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
v-model="attachmentDocumentPreviewVisible"
|
||||
:title="attachmentPreviewFile.name || '附件预览'"
|
||||
@@ -1236,6 +1249,9 @@ export default {
|
||||
organizationLoading: false,
|
||||
contractFileRows: [],
|
||||
attachmentRows: [],
|
||||
listUploadContractFileRows: [],
|
||||
listUploadAttachmentRows: [],
|
||||
attachmentUploadFromList: false,
|
||||
attachmentImagePreviewVisible: false,
|
||||
attachmentImagePreviewUrls: [],
|
||||
attachmentImagePreviewIndex: 0,
|
||||
@@ -1899,20 +1915,86 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.submitAction = 'attachment';
|
||||
this.api
|
||||
const contractFileRows = this.attachmentUploadFromList
|
||||
? this.listUploadContractFileRows
|
||||
: this.contractFileRows;
|
||||
const attachmentRows = this.attachmentUploadFromList
|
||||
? this.listUploadAttachmentRows
|
||||
: this.attachmentRows;
|
||||
return this.api
|
||||
.updateAttachments({
|
||||
id: this.form.id,
|
||||
contractFileJson: JSON.stringify(this.contractFileRows),
|
||||
attachmentsJson: JSON.stringify(this.attachmentRows),
|
||||
contractFileJson: JSON.stringify(contractFileRows),
|
||||
attachmentsJson: JSON.stringify(attachmentRows),
|
||||
})
|
||||
.then(() => {
|
||||
this.$message.success('附件保存成功');
|
||||
if (this.attachmentUploadFromList) {
|
||||
this.attachmentUploadFromList = false;
|
||||
this.listUploadContractFileRows = [];
|
||||
this.listUploadAttachmentRows = [];
|
||||
this.onLoad(this.page, this.query);
|
||||
return;
|
||||
}
|
||||
this.closeForm();
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitAction = '';
|
||||
});
|
||||
},
|
||||
openAttachmentUploadDialog(row = {}) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('合同不存在,无法上传附件');
|
||||
return;
|
||||
}
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: '加载中',
|
||||
background: 'rgba(255, 255, 255, 0.6)',
|
||||
});
|
||||
this.attachmentUploadFromList = true;
|
||||
this.listUploadContractFileRows = [];
|
||||
this.listUploadAttachmentRows = [];
|
||||
this.form = { ...defaultForm(), id: row.id };
|
||||
this.api
|
||||
.getDetail(row.id)
|
||||
.then(res => {
|
||||
const detail = res.data?.data || {};
|
||||
this.form = {
|
||||
...defaultForm(),
|
||||
...detail,
|
||||
id: detail.id || row.id,
|
||||
};
|
||||
this.listUploadContractFileRows = normalizeContractFileRows(
|
||||
parseArray(detail.contractFileJson)
|
||||
);
|
||||
this.listUploadAttachmentRows = parseArray(detail.attachmentsJson);
|
||||
this.$nextTick(() => {
|
||||
this.$refs.listAttachmentUploader?.openUploadDialog?.();
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.attachmentUploadFromList = false;
|
||||
this.$message.error('合同详情加载失败');
|
||||
})
|
||||
.finally(() => {
|
||||
loading.close();
|
||||
});
|
||||
},
|
||||
handleListAttachmentUploadConfirmed() {
|
||||
if (!this.attachmentUploadFromList) return;
|
||||
this.submitAttachmentUpload();
|
||||
},
|
||||
handleListAttachmentUploadToLocation({ location, rows = [] }) {
|
||||
if (!rows.length) return;
|
||||
if (location === '合同文件') {
|
||||
this.listUploadContractFileRows = [...(this.listUploadContractFileRows || []), ...rows];
|
||||
return;
|
||||
}
|
||||
if (location === '其它附件') {
|
||||
this.listUploadAttachmentRows = [...(this.listUploadAttachmentRows || []), ...rows];
|
||||
}
|
||||
},
|
||||
loadProjectOptions() {
|
||||
if (this.projectLoading) return;
|
||||
this.projectLoading = true;
|
||||
@@ -2749,15 +2831,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
if (operation.action === 'attachmentUpload') {
|
||||
this.$router.push({
|
||||
path: '/business/contract-manage/form',
|
||||
query: {
|
||||
mode: 'edit',
|
||||
id: row.id,
|
||||
name: '附件上传',
|
||||
attachmentUpload: '1',
|
||||
},
|
||||
});
|
||||
this.openAttachmentUploadDialog(row);
|
||||
return;
|
||||
}
|
||||
const run = value => {
|
||||
@@ -2978,6 +3052,10 @@ export default {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
&--attachment {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
> .dialog-section-title,
|
||||
:deep(.dialog-section-title) {
|
||||
margin-bottom: 20px;
|
||||
|
||||
Reference in New Issue
Block a user