1、调整合同

2、调整项目
3、调整付款管理
This commit is contained in:
2026-08-26 02:31:12 +08:00
parent 3da2d428f3
commit dc5a2235eb
13 changed files with 1184 additions and 235 deletions
@@ -4552,7 +4552,7 @@
<el-cascader
v-model="billingMatchForm.cargoTypePath"
:options="billingCargoTypeOptions"
:props="billingCargoTypeCascaderProps"
:props="billingMatchCargoTypeCascaderProps"
:placeholder="billingMatchForm.cargoType || '请选择货物类型'"
:loading="billingCargoTypeLoading"
:disabled="dialogReadonly"
@@ -4564,6 +4564,32 @@
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="货物名称">
<el-select
v-model="billingMatchForm.cargoNames"
multiple
collapse-tags
collapse-tags-tooltip
clearable
filterable
:loading="billingMatchCargoNameLoading"
:disabled="dialogReadonly || !billingMatchForm.cargoTypePath?.length"
:placeholder="
billingMatchForm.cargoTypePath?.length
? '请选择货物名称(可多选)'
: '请先选择货物类型'
"
>
<el-option
v-for="item in billingMatchCargoNameOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
@@ -5304,6 +5330,7 @@ const defaultBillingRule = () => ({
cargoType: '',
cargoTypeCode: '',
cargoTypePath: [],
cargoNames: [],
},
});
@@ -5685,6 +5712,9 @@ export default {
billingMatchRegionOptions: [],
billingMatchRegionLoading: false,
billingMatchRegionRequest: null,
billingMatchCargoNameOptions: [],
billingMatchCargoNameLoading: false,
billingMatchCargoNameRequestId: 0,
billingCargoTypeOptions: [],
billingCargoTypeFlatOptions: [],
billingCargoTypeLoading: false,
@@ -6240,6 +6270,16 @@ export default {
emitPath: true,
};
},
billingMatchCargoTypeCascaderProps() {
return {
label: 'cargoName',
value: 'id',
children: 'children',
leaf: 'leaf',
checkStrictly: true,
emitPath: true,
};
},
billingCargoTypeCascaderProps() {
return {
label: 'cargoName',
@@ -10476,6 +10516,33 @@ export default {
null
);
},
findBillingMatchCargoTypeByPath(path = []) {
const values = this.normalizeBillingMatchCargoTypePath(path);
if (!values.length) return null;
return (
this.billingCargoTypeFlatOptions.find(
item =>
item.path?.length === values.length &&
item.path.every((value, index) => String(value) === values[index])
) || null
);
},
findBillingMatchCargoTypeByCodeOrName(condition = {}) {
const code = String(condition.cargoTypeCode || '').trim();
const name = String(condition.cargoType || '').trim();
return (
this.billingCargoTypeFlatOptions.find(item => {
const cargoCode = String(item.cargoCode || item.code || item.id || '');
return code && cargoCode === code;
}) ||
this.billingCargoTypeFlatOptions.find(item => {
const cargoName = this.formatBillingCargoTypeLabel(item);
const pathName = this.getBillingCargoTypePathLabels(item.path).join('/');
return name && (cargoName === name || pathName === name);
}) ||
null
);
},
resolveCommonCargoTypePath(row = {}) {
const path = this.normalizeBillingCargoTypePath(row.cargoTypePath);
if (path.length) return path;
@@ -10490,17 +10557,17 @@ export default {
});
return cargoType?.path || [];
},
handleBillingMatchCargoTypeChange(value) {
const path =
Array.isArray(value) && value.length >= 2
? value.slice(0, 2).map(item => String(item))
: [];
const cargoType = this.findBillingCargoTypeByPath(path);
handleBillingMatchCargoTypeChange(value, resetCargoNames = true) {
const path = this.normalizeBillingMatchCargoTypePath(value);
const cargoType = this.findBillingMatchCargoTypeByPath(path);
const labels = this.getBillingCargoTypePathLabels(path);
this.billingMatchForm.cargoTypePath = path;
if (resetCargoNames) this.billingMatchForm.cargoNames = [];
if (!path.length || !cargoType) {
this.billingMatchForm.cargoType = '';
this.billingMatchForm.cargoTypeCode = '';
this.billingMatchCargoNameOptions = [];
this.billingMatchCargoNameRequestId += 1;
return;
}
this.billingMatchForm.cargoType =
@@ -10509,16 +10576,81 @@ export default {
: labels[0] || this.formatBillingCargoTypeLabel(cargoType || {});
this.billingMatchForm.cargoTypeCode =
cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
this.loadBillingMatchCargoNameOptions(path, !resetCargoNames);
},
loadBillingMatchCargoNameOptions(path = [], preserveSelected = false) {
const normalizedPath = this.normalizeBillingMatchCargoTypePath(path);
if (!normalizedPath.length) {
this.billingMatchCargoNameOptions = [];
this.billingMatchCargoNameLoading = false;
return Promise.resolve([]);
}
const requestId = ++this.billingMatchCargoNameRequestId;
const selected = preserveSelected
? this.normalizeBillingMatchCargoNames(this.billingMatchForm)
: [];
const firstCargoType = this.billingCargoTypeFlatOptions.find(
item => String(item.id) === normalizedPath[0] && item.path?.length === 1
);
const secondCargoType = this.findBillingCargoTypeByPath(normalizedPath);
const firstCargoTypeCode = firstCargoType?.cargoCode || firstCargoType?.code || '';
const secondCargoTypeCode = secondCargoType?.cargoCode || secondCargoType?.code || '';
this.billingMatchCargoNameLoading = true;
return getCommonCargoList(1, 9999, {
...(firstCargoTypeCode
? { firstCargoTypeCode }
: { firstCargoTypeId: normalizedPath[0] }),
...(normalizedPath[1]
? secondCargoTypeCode
? { secondCargoTypeCode }
: { secondCargoTypeId: normalizedPath[1] }
: {}),
allDept: 0,
})
.then(res => {
if (requestId !== this.billingMatchCargoNameRequestId) return [];
const names = extractRecords(res)
.map(item => String(item.cargoName || '').trim())
.filter(Boolean);
const options = [...new Set([...selected, ...names])];
this.billingMatchCargoNameOptions = options.map(value => ({ label: value, value }));
return this.billingMatchCargoNameOptions;
})
.catch(error => {
if (requestId === this.billingMatchCargoNameRequestId) {
this.billingMatchCargoNameOptions = selected.map(value => ({ label: value, value }));
}
window.console.log(error);
return [];
})
.finally(() => {
if (requestId === this.billingMatchCargoNameRequestId) {
this.billingMatchCargoNameLoading = false;
}
});
},
normalizeBillingCargoTypePath(path) {
return Array.isArray(path) && path.length >= 2
? path.slice(0, 2).map(value => String(value))
: [];
},
normalizeBillingMatchCargoTypePath(path) {
return Array.isArray(path) && path.length
? path.slice(0, 2).map(value => String(value))
: [];
},
normalizeBillingMatchCargoNames(condition = {}) {
const values = Array.isArray(condition.cargoNames)
? condition.cargoNames
: condition.cargoName
? [condition.cargoName]
: [];
return [...new Set(values.map(value => String(value || '').trim()).filter(Boolean))];
},
resolveBillingMatchCargoTypePath(condition = {}) {
const path = this.normalizeBillingCargoTypePath(condition.cargoTypePath);
const path = this.normalizeBillingMatchCargoTypePath(condition.cargoTypePath);
if (path.length) return path;
const cargoType = this.findBillingCargoTypeByCodeOrName(condition);
const cargoType = this.findBillingMatchCargoTypeByCodeOrName(condition);
return cargoType?.path || [];
},
normalizeBillingMatchRegionPath(path) {
@@ -10536,7 +10668,10 @@ export default {
nextCondition.destinationPath = this.normalizeBillingMatchRegionPath(
condition?.destinationPath
);
nextCondition.cargoTypePath = this.normalizeBillingCargoTypePath(condition?.cargoTypePath);
nextCondition.cargoTypePath = this.normalizeBillingMatchCargoTypePath(
condition?.cargoTypePath
);
nextCondition.cargoNames = this.normalizeBillingMatchCargoNames(condition);
return nextCondition;
},
ensureBillingFeeCategoryOptions() {
@@ -10892,12 +11027,13 @@ export default {
openBillingMatch(row, index) {
this.billingMatchRuleIndex = index;
this.billingMatchForm = this.normalizeBillingMatchCondition(row.matchCondition);
this.billingMatchCargoNameOptions = [];
this.billingMatchBox = true;
this.ensureBillingMatchRegionOptions();
this.ensureBillingCargoTypeOptions().then(() => {
const path = this.resolveBillingMatchCargoTypePath(this.billingMatchForm);
if (path.length) {
this.handleBillingMatchCargoTypeChange(path);
this.handleBillingMatchCargoTypeChange(path, false);
}
});
},
@@ -10906,7 +11042,7 @@ export default {
if (row) {
this.handleBillingMatchRegionChange('origin', this.billingMatchForm.originPath);
this.handleBillingMatchRegionChange('destination', this.billingMatchForm.destinationPath);
this.handleBillingMatchCargoTypeChange(this.billingMatchForm.cargoTypePath);
this.handleBillingMatchCargoTypeChange(this.billingMatchForm.cargoTypePath, false);
row.matchCondition = this.cloneData(this.billingMatchForm);
}
this.billingMatchBox = false;
@@ -103,6 +103,11 @@ const viewerPlugins = [
textPlugin(),
fallbackPlugin(),
];
const normalizeOptionalInteger = value => {
if (value === undefined || value === null || value === '' || Number(value) < 0) return null;
const number = Number(value);
return Number.isFinite(number) ? Math.trunc(number) : null;
};
export default {
components: { BillingPlanEditor, ElImageViewer, OpenFileViewer },
@@ -111,7 +116,7 @@ export default {
mounted() { this.load(); },
watch: { settlementConfigTab(tab, oldTab) { if (tab === oldTab) return; if (oldTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; this.settlementRule = { ...(tab === 'pre' ? this.preSettlementConfig : this.formalSettlementConfig) }; } },
methods: {
async load() { const id = this.$route.query.id; if (!id) return; const res = await api.getDetail(id); const data = res.data?.data || res.data || {}; this.form = { ...data, changeType: '合同信息变更' }; this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : []; this.plans = this.parse(data.billingPlanJson); this.attachments = this.parse(data.attachmentsJson); this.contractFileRows = this.parse(data.contractFileJson); const rules = this.parseObject(data.settlementRuleJson); const pre = this.parseObject(data.preSettlementConfigJson); const formal = this.parseObject(data.formalSettlementConfigJson); const legacy = Object.keys(rules).some(key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)) ? rules : {}; this.preSettlementConfig = rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy); this.formalSettlementConfig = rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy); this.settlementRule = { ...this.preSettlementConfig }; this.feeGenerationMode = data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system'); this.paymentRatioRows = this.parse(data.paymentRatioJson); },
async load() { const id = this.$route.query.id; if (!id) return; const res = await api.getDetail(id); const data = res.data?.data || res.data || {}; this.form = { ...data, copyCount: normalizeOptionalInteger(data.copyCount), paymentDays: normalizeOptionalInteger(data.paymentDays), changeType: '合同信息变更' }; this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : []; this.plans = this.parse(data.billingPlanJson); this.attachments = this.parse(data.attachmentsJson); this.contractFileRows = this.parse(data.contractFileJson); const rules = this.parseObject(data.settlementRuleJson); const pre = this.parseObject(data.preSettlementConfigJson); const formal = this.parseObject(data.formalSettlementConfigJson); const legacy = Object.keys(rules).some(key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)) ? rules : {}; this.preSettlementConfig = rules.preSettlementConfig || (Object.keys(pre).length ? pre : legacy); this.formalSettlementConfig = rules.formalSettlementConfig || (Object.keys(formal).length ? formal : legacy); this.settlementRule = { ...this.preSettlementConfig }; this.feeGenerationMode = data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system'); this.paymentRatioRows = this.parse(data.paymentRatioJson); },
parse(value) { try { const result = JSON.parse(value || '[]'); return Array.isArray(result) ? result : []; } catch { return []; } },
parseObject(value) { try { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15, ...(JSON.parse(value || '{}') || {}) }; } catch { return { autoGenerate: 1, settlementType: '月结', billCycleType: '固定截单日', billCutoffDay: 25, cycleDays: 15 }; } },
addPlan() { this.planEditorIndex = -1; this.planEditor = { planName: `计费方案${this.plans.length + 1}`, defaultPlan: !this.plans.length, remark: '', rules: [{}] }; this.planDialogVisible = true; },
@@ -134,7 +139,7 @@ export default {
formatFileSize(value) { const size = Number(value || 0); return size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)}MB` : `${Math.max(1, Math.ceil(size / 1024))}KB`; },
handleChangeMaterial(event) { if (event.raw) this.changeMaterials.push(event.raw); },
addPaymentRatioRow() { this.paymentRatioRows.push({ paymentTerm: `${this.paymentRatioRows.length + 1}`, ratioLimit: '', remark: '' }); },
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeReason, changeReason: this.form.changeReason }); this.$message.success('变更已提交'); this.$router.back(); },
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, copyCount: normalizeOptionalInteger(this.form.copyCount), paymentDays: normalizeOptionalInteger(this.form.paymentDays), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeReason, changeReason: this.form.changeReason }); this.$message.success('变更已提交'); this.$router.back(); },
},
};
</script>
+23 -6
View File
@@ -804,6 +804,13 @@ import '@open-file-viewer/core/style.css';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
const clone = value => JSON.parse(JSON.stringify(value));
const normalizeOptionalInteger = (value, emptyValue = null) => {
if (value === undefined || value === null || value === '' || Number(value) < 0) {
return emptyValue;
}
const number = Number(value);
return Number.isFinite(number) ? Math.trunc(number) : emptyValue;
};
const defaultSettlementRule = () => ({
autoGenerate: 1,
billStartDate: '',
@@ -1480,6 +1487,8 @@ export default {
this.form = {
...defaultForm(),
...detail,
copyCount: normalizeOptionalInteger(detail.copyCount, ''),
paymentDays: normalizeOptionalInteger(detail.paymentDays, ''),
feeGenerationMode:
detail.feeGenerationMode || (Number(detail.billingEnabled) === 0 ? 'manual' : 'system'),
};
@@ -1554,6 +1563,8 @@ export default {
this.syncSettlementConfig();
return {
...this.form,
copyCount: normalizeOptionalInteger(this.form.copyCount),
paymentDays: normalizeOptionalInteger(this.form.paymentDays),
billingEnabled: this.form.feeGenerationMode === 'manual' ? 0 : 1,
contractFileJson: JSON.stringify(this.contractFileRows),
attachmentsJson: JSON.stringify(this.attachmentRows),
@@ -1914,7 +1925,11 @@ export default {
});
},
applyDetailState(detail = {}) {
this.detailRow = { ...detail };
this.detailRow = {
...detail,
copyCount: normalizeOptionalInteger(detail.copyCount),
paymentDays: normalizeOptionalInteger(detail.paymentDays),
};
this.detailContractFileRows = parseArray(detail.contractFileJson);
this.detailAttachmentRows = parseArray(detail.attachmentsJson);
this.detailBillingPlanRows = parseArray(detail.billingPlanJson);
@@ -1946,7 +1961,9 @@ export default {
},
detailUnitValue(prop, unit) {
const value = this.detailRow[prop];
return value === undefined || value === null || value === '' ? '-' : `${value}${unit}`;
return value === undefined || value === null || value === '' || Number(value) < 0
? ''
: `${value}${unit}`;
},
detailObjectUnitValue(row, prop, unit) {
const value = row?.[prop];
@@ -1985,7 +2002,7 @@ export default {
const run = value => {
const args = operation.prompt ? [row.id, value] : [row.id];
this.api[operation.action](...args).then(() => {
this.$message.success(`${operation.label}成功`);
this.$message.success(operation.successMessage || `${operation.label}成功`);
this.onLoad(this.page, this.query);
});
};
@@ -1997,9 +2014,9 @@ export default {
inputErrorMessage: '请输入内容',
}).then(({ value }) => run(value));
} else {
this.$confirm(`确定${operation.label}该合同?`, '提示', { type: 'warning' }).then(() =>
run()
);
this.$confirm(operation.confirmMessage || `确定${operation.label}该合同?`, '提示', {
type: 'warning',
}).then(() => run());
}
},
removeRow(row) {
+169 -11
View File
@@ -391,7 +391,11 @@
<el-table :data="customerRows" border class="project-apply-form__table">
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column prop="customer" label="客户" min-width="160" align="center" />
<el-table-column prop="credit" label="客户信用额度" min-width="150" align="center" />
<el-table-column prop="credit" label="客户信用额度" min-width="150" align="center">
<template #default="{ row }">
{{ normalizeReadonlyAmount(row.credit) }}
</template>
</el-table-column>
<el-table-column prop="companyNature" label="企业性质" min-width="140" align="center" />
<el-table-column prop="legalPerson" label="法定代表人" min-width="140" align="center" />
<el-table-column
@@ -505,7 +509,11 @@
</template>
</el-table-column>
<el-table-column label="文件名" min-width="220" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.originalName || row.name }}</template>
<template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="240" align="center">
<template #default="{ row }">
@@ -536,6 +544,7 @@
:readonly="dialogReadonly"
:file-types="attachmentFileTypes"
:max-size="500"
:show-file-list="false"
button-text="上传附件"
tip="支持pdfbmpjpegpngjpgdocdocxpptpptxxlsxxlsemlmsgzip的文件格式单个文件不超过500M"
@change="handleAttachmentChange"
@@ -642,6 +651,38 @@
</div>
</component>
<el-dialog
v-model="attachmentDocumentPreviewVisible"
:title="attachmentPreviewFile.name || '附件预览'"
append-to-body
destroy-on-close
width="90%"
top="4vh"
class="project-apply-form__attachment-viewer-dialog"
>
<open-file-viewer
v-if="attachmentDocumentPreviewVisible && attachmentPreviewFile.url"
:file="attachmentPreviewFile.url"
:file-name="attachmentPreviewFile.name"
:mime-type="attachmentPreviewFile.mimeType"
width="100%"
height="72vh"
fit="contain"
theme="auto"
locale="zh-CN"
:toolbar="attachmentViewerToolbar"
:plugins="attachmentViewerPlugins"
@unsupported="handleAttachmentPreviewUnsupported"
@error="handleAttachmentPreviewError"
/>
</el-dialog>
<el-image-viewer
v-if="attachmentImagePreviewVisible"
:url-list="attachmentImagePreviewUrls"
:initial-index="attachmentImagePreviewIndex"
@close="attachmentImagePreviewVisible = false"
/>
<el-dialog
v-model="userBox"
title="选择用户"
@@ -705,6 +746,17 @@ import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
import { getList as getUserList } from '@/api/system/user';
import { getToken } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import {
fallbackPlugin,
imagePlugin,
officePlugin,
pdfPlugin,
textPlugin,
} from '@open-file-viewer/core';
import '@open-file-viewer/core/style.css';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -767,8 +819,15 @@ const changeTypeOptions = [
{ label: '项目备案调整', value: '项目备案调整' },
];
const attachmentViewerPlugins = [
imagePlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
textPlugin(),
fallbackPlugin(),
];
const normalProjectAttachmentTypeOptions = [
'项目立项报告',
'利润测算表',
'单一来源供应商申请表/三方比价表',
'合同模板',
@@ -778,7 +837,6 @@ const normalProjectAttachmentTypeOptions = [
].map(item => ({ label: item, value: item }));
const majorProjectAttachmentTypeOptions = [
'项目立项报告',
'会议纪要',
'评审表决票',
'利润测算表',
@@ -793,6 +851,10 @@ const majorProjectAttachmentTypeOptions = [
].map(item => ({ label: item, value: item }));
export default {
components: {
ElImageViewer,
OpenFileViewer,
},
data() {
const validateAmount = (rule, value, callback) => {
if (value === '' || value === undefined || value === null) {
@@ -945,6 +1007,19 @@ export default {
carrierRows: [],
attachmentRows: [],
selectedAttachmentRows: [],
attachmentImagePreviewVisible: false,
attachmentImagePreviewUrls: [],
attachmentImagePreviewIndex: 0,
attachmentDocumentPreviewVisible: false,
attachmentPreviewFile: {},
attachmentViewerPlugins,
attachmentViewerToolbar: {
download: true,
fullscreen: true,
print: true,
rotate: true,
zoom: true,
},
attachmentFileTypes: [
'pdf',
'bmp',
@@ -1532,12 +1607,41 @@ export default {
return submitRow;
},
normalizeAttachmentFileTypes() {
const values = this.projectAttachmentTypeOptions.map(item => item.value);
this.attachmentRows = this.attachmentRows.map(row => ({
...row,
fileType: values.includes(row.fileType) ? row.fileType : '',
fileType: this.resolveAttachmentFileType(row),
}));
},
normalizeAttachmentTypeText(value) {
let text = String(value || '').split(/[?#]/)[0];
try {
text = decodeURIComponent(text);
} catch (error) {
// 文件名可能包含不完整的转义字符,直接使用原始名称继续匹配。
}
return text
.replace(/\.[^.\\/]+$/, '')
.toLowerCase()
.replace(/[\s_\-—–·•()()\[\]【】{}《》<>“”"'、,,。.]/g, '');
},
attachmentTypeKeywords(fileType) {
if (fileType === '单一来源供应商申请表/三方比价表') {
return ['单一来源供应商申请表', '三方比价表'];
}
return [fileType];
},
resolveAttachmentFileType(row = {}) {
const values = this.projectAttachmentTypeOptions.map(item => item.value);
if (values.includes(row.fileType)) return row.fileType;
const fileName = this.normalizeAttachmentTypeText(this.attachmentName(row));
if (!fileName) return '';
const option = this.projectAttachmentTypeOptions.find(item =>
this.attachmentTypeKeywords(item.value).some(keyword =>
fileName.includes(this.normalizeAttachmentTypeText(keyword))
)
);
return option?.value || '';
},
validateAttachmentFileTypes() {
const values = this.projectAttachmentTypeOptions.map(item => item.value);
const invalidIndex = this.attachmentRows.findIndex(row => !values.includes(row.fileType));
@@ -1840,10 +1944,9 @@ export default {
handleAttachmentChange(list) {
const userName = this.userInfo?.realName || this.userInfo?.userName || '';
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
const values = this.projectAttachmentTypeOptions.map(item => item.value);
this.attachmentRows = (list || []).map(item => ({
...item,
fileType: values.includes(item.fileType) ? item.fileType : '',
fileType: this.resolveAttachmentFileType(item),
description: item.description || '',
uploadUserName: item.uploadUserName || userName,
uploadTime: item.uploadTime || uploadTime,
@@ -1863,19 +1966,74 @@ export default {
if (number < 1024 * 1024) return `${(number / 1024).toFixed(1)}KB`;
return `${(number / 1024 / 1024).toFixed(1)}MB`;
},
attachmentUrl(row = {}) {
return row.url || row.link || row.fileUrl || row.src || row.domain || '';
},
attachmentName(row = {}) {
return row.originalName || row.name || row.fileName || '附件';
},
attachmentExtension(row = {}) {
const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0];
const index = source.lastIndexOf('.');
return index > -1 ? source.slice(index + 1).toLowerCase() : '';
},
isAttachmentImage(row) {
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(
this.attachmentExtension(row)
);
},
previewAttachment(row) {
const url = this.attachmentUrl(row);
if (!url) {
this.$message.warning('附件地址为空无法预览');
return;
}
if (this.isAttachmentImage(row)) {
this.attachmentImagePreviewUrls = this.attachmentRows
.filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item))
.map(item => this.attachmentUrl(item));
this.attachmentImagePreviewIndex = Math.max(
this.attachmentImagePreviewUrls.indexOf(url),
0
);
this.attachmentImagePreviewVisible = true;
return;
}
this.attachmentPreviewFile = {
name: this.attachmentName(row),
url,
mimeType: row.mimeType || row.contentType || '',
};
this.attachmentDocumentPreviewVisible = true;
},
handleAttachmentPreviewUnsupported() {
this.$message.warning('当前文件暂不支持在线预览');
},
handleAttachmentPreviewError() {
this.$message.error('附件预览失败');
},
downloadAttachment(row) {
const url = row.url || row.link;
const url = this.attachmentUrl(row);
if (!url) {
this.$message.warning('附件地址为空');
return;
}
downloadFileByUrl(url, row.originalName || row.name || '附件');
downloadFileByUrl(url, this.attachmentName(row));
},
handleBatchDownload() {
const rows = this.selectedAttachmentRows.length
? this.selectedAttachmentRows
: this.attachmentRows;
rows.forEach(row => this.downloadAttachment(row));
const downloadableRows = rows.filter(row => this.attachmentUrl(row));
if (!downloadableRows.length) {
this.$message.warning(
this.selectedAttachmentRows.length ? '所选附件暂无可下载地址' : '暂无可下载附件'
);
return;
}
downloadableRows.forEach((row, index) => {
window.setTimeout(() => this.downloadAttachment(row), index * 300);
});
},
handleChangeRecordAction() {
this.$message.info('变更记录详情由后端明细能力生成后展示');
+119 -2
View File
@@ -93,7 +93,7 @@
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="240" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="downloadAttachment(row)">
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
</template>
@@ -169,6 +169,38 @@
@load="onLoad(page, query)"
/>
<el-dialog
v-model="attachmentDocumentPreviewVisible"
:title="attachmentPreviewFile.name || '附件预览'"
append-to-body
destroy-on-close
width="90%"
top="4vh"
class="temporary-credit-limit-page__attachment-viewer-dialog"
>
<open-file-viewer
v-if="attachmentDocumentPreviewVisible && attachmentPreviewFile.url"
:file="attachmentPreviewFile.url"
:file-name="attachmentPreviewFile.name"
:mime-type="attachmentPreviewFile.mimeType"
width="100%"
height="72vh"
fit="contain"
theme="auto"
locale="zh-CN"
:toolbar="attachmentViewerToolbar"
:plugins="attachmentViewerPlugins"
@unsupported="handleAttachmentPreviewUnsupported"
@error="handleAttachmentPreviewError"
/>
</el-dialog>
<el-image-viewer
v-if="attachmentImagePreviewVisible"
:url-list="attachmentImagePreviewUrls"
:initial-index="attachmentImagePreviewIndex"
@close="attachmentImagePreviewVisible = false"
/>
<flow-design-step
v-if="website.design.designMode"
v-model:is-display="flowBox"
@@ -191,6 +223,17 @@ import {
import { config, option } from '@/option/business/temporary-credit-limit';
import { getToken } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import {
fallbackPlugin,
imagePlugin,
officePlugin,
pdfPlugin,
textPlugin,
} from '@open-file-viewer/core';
import '@open-file-viewer/core/style.css';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -212,6 +255,14 @@ const attachmentFileTypes = [
'zip',
];
const attachmentViewerPlugins = [
imagePlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
textPlugin(),
fallbackPlugin(),
];
const extractRecords = res => {
const data = res?.data?.data || res?.data || {};
return Array.isArray(data) ? data : data.records || [];
@@ -219,6 +270,10 @@ const extractRecords = res => {
export default {
name: 'TemporaryCreditLimit',
components: {
ElImageViewer,
OpenFileViewer,
},
data() {
return {
api,
@@ -242,6 +297,19 @@ export default {
projectLoading: false,
attachmentRows: [],
selectedAttachments: [],
attachmentImagePreviewVisible: false,
attachmentImagePreviewUrls: [],
attachmentImagePreviewIndex: 0,
attachmentDocumentPreviewVisible: false,
attachmentPreviewFile: {},
attachmentViewerPlugins,
attachmentViewerToolbar: {
download: true,
fullscreen: true,
print: true,
rotate: true,
zoom: true,
},
attachmentFileTypes,
flowBox: false,
flowUrl: '',
@@ -587,6 +655,46 @@ export default {
attachmentUrl(row = {}) {
return row.url || row.link || row.fileUrl || row.downloadUrl || row.domain || '';
},
attachmentExtension(row = {}) {
const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0];
const index = source.lastIndexOf('.');
return index > -1 ? source.slice(index + 1).toLowerCase() : '';
},
isAttachmentImage(row) {
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(
this.attachmentExtension(row)
);
},
previewAttachment(row) {
const url = this.attachmentUrl(row);
if (!url) {
this.$message.warning('附件地址为空,无法预览');
return;
}
if (this.isAttachmentImage(row)) {
this.attachmentImagePreviewUrls = this.attachmentRows
.filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item))
.map(item => this.attachmentUrl(item));
this.attachmentImagePreviewIndex = Math.max(
this.attachmentImagePreviewUrls.indexOf(url),
0
);
this.attachmentImagePreviewVisible = true;
return;
}
this.attachmentPreviewFile = {
name: this.attachmentName(row),
url,
mimeType: row.mimeType || row.contentType || '',
};
this.attachmentDocumentPreviewVisible = true;
},
handleAttachmentPreviewUnsupported() {
this.$message.warning('当前文件暂不支持在线预览');
},
handleAttachmentPreviewError() {
this.$message.error('附件预览失败');
},
downloadAttachment(row) {
const url = this.attachmentUrl(row);
if (!url) {
@@ -597,7 +705,16 @@ export default {
},
batchDownload() {
const rows = this.selectedAttachments.length ? this.selectedAttachments : this.attachmentRows;
rows.forEach(this.downloadAttachment);
const downloadableRows = rows.filter(row => this.attachmentUrl(row));
if (!downloadableRows.length) {
this.$message.warning(
this.selectedAttachments.length ? '所选附件暂无可下载地址' : '暂无可下载附件'
);
return;
}
downloadableRows.forEach((row, index) => {
window.setTimeout(() => this.downloadAttachment(row), index * 300);
});
},
formatFileSize(size) {
const value = Number(size);