Merge branch 'refs/heads/feature/bfi-api-rate' into feature/hcs-api-p0-web
# Conflicts: # src/option/business/contract-manage.js
This commit is contained in:
@@ -352,7 +352,8 @@ export default {
|
||||
return result;
|
||||
},
|
||||
normalizeListQuery(params = {}) {
|
||||
const query = { ...params, ...this.query };
|
||||
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
|
||||
const query = { ...this.query, ...params };
|
||||
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||
delete query.searchDeptId;
|
||||
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||
|
||||
@@ -415,19 +415,27 @@ export default {
|
||||
});
|
||||
},
|
||||
buildDeptTree(tree) {
|
||||
return tree.map(item => ({
|
||||
label: item.title || item.deptName || item.name,
|
||||
value: item.id,
|
||||
children:
|
||||
item.children && item.children.length ? this.buildDeptTree(item.children) : undefined,
|
||||
}));
|
||||
return tree.map(item => {
|
||||
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
|
||||
const id =
|
||||
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||
return {
|
||||
label: item.title || item.deptName || item.name,
|
||||
value: id,
|
||||
children:
|
||||
item.children && item.children.length ? this.buildDeptTree(item.children) : undefined,
|
||||
};
|
||||
}).filter(item => item.value);
|
||||
},
|
||||
flattenDept(tree, level = 0) {
|
||||
const result = [];
|
||||
tree.forEach(item => {
|
||||
const id =
|
||||
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||
if (!id) return;
|
||||
result.push({
|
||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||
value: item.id,
|
||||
value: id,
|
||||
});
|
||||
if (item.children && item.children.length) {
|
||||
result.push(...this.flattenDept(item.children, level + 1));
|
||||
@@ -436,7 +444,26 @@ export default {
|
||||
return result;
|
||||
},
|
||||
defaultDeptId() {
|
||||
return (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
|
||||
const raw = (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
|
||||
return (
|
||||
String(raw)
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)[0] || ''
|
||||
);
|
||||
},
|
||||
normalizeListQuery(params = {}) {
|
||||
// 搜索条件优先于缓存的 query,避免默认组织 ID 覆盖用户新选择的组织。
|
||||
const query = { ...this.query, ...params };
|
||||
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||
delete query.searchDeptId;
|
||||
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||
query.deptId = String(rawDeptId).split(',')[0].trim();
|
||||
} else {
|
||||
delete query.deptId;
|
||||
}
|
||||
query.allDept = this.isAdmin ? 1 : 0;
|
||||
return query;
|
||||
},
|
||||
applyDefaultDeptSearch() {
|
||||
const deptId = this.defaultDeptId();
|
||||
@@ -999,9 +1026,9 @@ export default {
|
||||
this.onLoad(this.page, this.query);
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = params;
|
||||
this.query = this.normalizeListQuery(params);
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page, params);
|
||||
this.onLoad(this.page, this.query);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
@@ -1024,12 +1051,10 @@ export default {
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
const query = this.normalizeListQuery(params);
|
||||
this.query = query;
|
||||
api
|
||||
.getList(page.currentPage, page.pageSize, {
|
||||
...params,
|
||||
...this.query,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
})
|
||||
.getList(page.currentPage, page.pageSize, query)
|
||||
.then(res => {
|
||||
const result = res.data.data;
|
||||
this.page.total = result.total;
|
||||
@@ -1042,8 +1067,7 @@ export default {
|
||||
},
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
...this.normalizeListQuery(this.query),
|
||||
ids: this.ids,
|
||||
[this.website.tokenHeader]: getToken(),
|
||||
};
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
:label="item.label"
|
||||
:value="item.value" /></el-select></template
|
||||
></el-table-column>
|
||||
<el-table-column label="单价(元)" width="180"
|
||||
<el-table-column :label="unitPriceColumnLabel" width="180"
|
||||
><template #default="{ row }"
|
||||
><div v-if="readonly && usesRangeUnitPrice(row)" class="limit-list">
|
||||
<span v-for="(item, rangeIndex) in getRanges(row)" :key="rangeIndex">{{
|
||||
@@ -379,6 +379,7 @@ import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
import { currencyUnit } from '@/utils/currency';
|
||||
|
||||
const clone = value => JSON.parse(JSON.stringify(value));
|
||||
/** 计费要素与计量单位维度的对应关系(这些要素可选计费单位) */
|
||||
@@ -386,12 +387,15 @@ const BILLING_ELEMENT_DIMENSION_MAP = {
|
||||
按重量: '重量',
|
||||
按体积: '体积',
|
||||
按数量: '数量',
|
||||
按里程: '里程',
|
||||
'按吨·公里': '吨·公里',
|
||||
'固定金额(整单一口价)': '整车',
|
||||
};
|
||||
const BILLING_UNIT_SELECTABLE_ELEMENTS = Object.keys(BILLING_ELEMENT_DIMENSION_MAP);
|
||||
/** 固定计费单位(不可编辑) */
|
||||
const FIXED_BILLING_UNIT_MAP = {
|
||||
/** 对应维度下无计量单位时的默认计费单位 */
|
||||
const DEFAULT_BILLING_UNIT_MAP = {
|
||||
按里程: '公里',
|
||||
'按吨·公里': '吨公里',
|
||||
'按吨·公里': '吨·公里',
|
||||
'固定金额(整单一口价)': '单',
|
||||
};
|
||||
const defaultRule = () => ({
|
||||
@@ -428,6 +432,7 @@ export default {
|
||||
value: { type: Object, default: () => ({}) },
|
||||
index: { type: Number, default: -1 },
|
||||
readonly: Boolean,
|
||||
settlementCurrency: { type: String, default: 'RMB' },
|
||||
},
|
||||
emits: ['update:modelValue', 'save'],
|
||||
data() {
|
||||
@@ -497,6 +502,9 @@ export default {
|
||||
emitPath: true,
|
||||
};
|
||||
},
|
||||
unitPriceColumnLabel() {
|
||||
return `单价(${currencyUnit(this.settlementCurrency)})`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.loadDictionaries();
|
||||
@@ -652,58 +660,61 @@ export default {
|
||||
}),
|
||||
]).finally(() => {
|
||||
this.unitLoading = false;
|
||||
(this.draft.rules || []).forEach(row => this.syncBillingUnit(row));
|
||||
});
|
||||
},
|
||||
measurementDimension(billingElement) {
|
||||
return BILLING_ELEMENT_DIMENSION_MAP[billingElement] || '';
|
||||
},
|
||||
fixedBillingUnit(billingElement) {
|
||||
return FIXED_BILLING_UNIT_MAP[String(billingElement || '').trim()] || '';
|
||||
defaultBillingUnit(billingElement) {
|
||||
return DEFAULT_BILLING_UNIT_MAP[String(billingElement || '').trim()] || '';
|
||||
},
|
||||
canSelectBillingUnit(row) {
|
||||
return BILLING_UNIT_SELECTABLE_ELEMENTS.includes(String(row?.billingElement || '').trim());
|
||||
},
|
||||
billingUnitPlaceholder(row) {
|
||||
if (!row?.billingElement) return '请先选择计费要素';
|
||||
if (this.fixedBillingUnit(row.billingElement)) return this.fixedBillingUnit(row.billingElement);
|
||||
if (!this.canSelectBillingUnit(row)) return '当前计费要素无需选择';
|
||||
return '请选择';
|
||||
},
|
||||
unitOptionsFor(row) {
|
||||
const fixedUnit = this.fixedBillingUnit(row?.billingElement);
|
||||
if (fixedUnit) {
|
||||
return [{ id: fixedUnit, label: fixedUnit, value: fixedUnit }];
|
||||
}
|
||||
if (!this.canSelectBillingUnit(row)) {
|
||||
return [];
|
||||
}
|
||||
const dimension = this.measurementDimension(row?.billingElement);
|
||||
if (dimension) {
|
||||
return this.measurementUnits
|
||||
.filter(item => String(item.dimension || '').trim() === dimension)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
label: item.unitName,
|
||||
value: item.unitName,
|
||||
}))
|
||||
.filter(item => item.value);
|
||||
const defaultUnit = this.defaultBillingUnit(row?.billingElement);
|
||||
const options = dimension
|
||||
? this.measurementUnits
|
||||
.filter(item => String(item.dimension || '').trim() === dimension)
|
||||
.map(item => ({
|
||||
id: item.id,
|
||||
label: item.unitName,
|
||||
value: item.unitName,
|
||||
}))
|
||||
.filter(item => item.value)
|
||||
: [];
|
||||
// 对应维度未配置计量单位时,下拉仅展示默认值
|
||||
if (!options.length && defaultUnit) {
|
||||
return [{ id: `default-${defaultUnit}`, label: defaultUnit, value: defaultUnit }];
|
||||
}
|
||||
return [];
|
||||
return options;
|
||||
},
|
||||
syncBillingUnit(row) {
|
||||
const fixedUnit = this.fixedBillingUnit(row?.billingElement);
|
||||
if (fixedUnit) {
|
||||
row.billingUnit = fixedUnit;
|
||||
return;
|
||||
}
|
||||
if (!this.canSelectBillingUnit(row)) {
|
||||
row.billingUnit = '';
|
||||
return;
|
||||
}
|
||||
const options = this.unitOptionsFor(row);
|
||||
if (!options.some(item => String(item.value) === String(row.billingUnit || ''))) {
|
||||
row.billingUnit = '';
|
||||
const defaultUnit = this.defaultBillingUnit(row?.billingElement);
|
||||
const current = String(row.billingUnit || '').trim();
|
||||
if (options.some(item => String(item.value) === current)) {
|
||||
return;
|
||||
}
|
||||
if (defaultUnit && options.some(item => String(item.value) === defaultUnit)) {
|
||||
row.billingUnit = defaultUnit;
|
||||
return;
|
||||
}
|
||||
row.billingUnit = options[0]?.value || defaultUnit || '';
|
||||
},
|
||||
feeTypeKey(row) {
|
||||
const option = this.feeCategories.find(
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -2065,11 +2065,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="200" align="left" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
align="left"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ row.originalName || row.name }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ row.originalName || row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -2338,14 +2349,16 @@
|
||||
<div class="business-crud-page__transport-plan-detail-field is-attachments">
|
||||
<span>附件</span>
|
||||
<div class="business-crud-page__transport-plan-detail-attachments">
|
||||
<el-link
|
||||
<span
|
||||
v-for="file in attachmentRows"
|
||||
:key="file.url || file.name || file.originalName"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>
|
||||
{{ attachmentName(file) }}
|
||||
</el-link>
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</span>
|
||||
<span v-if="!attachmentRows.length">-</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2531,14 +2544,16 @@
|
||||
<div class="business-crud-page__waybill-attachments">
|
||||
<span class="business-crud-page__waybill-label">附件</span>
|
||||
<div>
|
||||
<el-link
|
||||
<span
|
||||
v-for="file in attachmentRows"
|
||||
:key="file.url || file.name || file.originalName"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>
|
||||
{{ file.originalName || file.name || '-' }}
|
||||
</el-link>
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ file.originalName || file.name || '-' }}</span>
|
||||
<span v-if="!attachmentRows.length">-</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2820,11 +2835,21 @@
|
||||
</div>
|
||||
<el-table :data="attachmentRows" empty-text="暂无附件">
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="260">
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="260"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -2966,10 +2991,50 @@
|
||||
<el-table :data="waybillRouteChangeRecords" border>
|
||||
<el-table-column prop="changeTime" label="变更时间" min-width="170" />
|
||||
<el-table-column prop="changeUser" label="变更人" min-width="120" />
|
||||
<el-table-column prop="content" label="变更内容" min-width="360">
|
||||
<el-table-column
|
||||
prop="content"
|
||||
label="变更内容"
|
||||
min-width="360"
|
||||
:show-overflow-tooltip="false"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="business-crud-page__route-change-record-content">
|
||||
{{ row.content || '-' }}
|
||||
<div
|
||||
v-for="(group, groupIndex) in parseRouteChangeContent(row.content)"
|
||||
:key="groupIndex"
|
||||
class="business-crud-page__route-change-group"
|
||||
>
|
||||
<div v-if="group.title" class="business-crud-page__route-change-group-title">
|
||||
{{ group.title }}
|
||||
</div>
|
||||
<div v-if="group.text" class="business-crud-page__route-change-group-text">
|
||||
{{ group.text }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(field, fieldIndex) in group.fields"
|
||||
:key="fieldIndex"
|
||||
class="business-crud-page__route-change-field"
|
||||
>
|
||||
<span class="business-crud-page__route-change-field-label">
|
||||
{{ field.label }}
|
||||
</span>
|
||||
<template v-if="field.changed">
|
||||
<span class="business-crud-page__route-change-before">
|
||||
{{ field.before }}
|
||||
</span>
|
||||
<span class="business-crud-page__route-change-arrow">→</span>
|
||||
<span class="business-crud-page__route-change-after">
|
||||
{{ field.after }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="business-crud-page__route-change-single">
|
||||
{{ field.single }}
|
||||
<span v-if="field.unchanged" class="business-crud-page__route-change-flag">
|
||||
未变更
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -3056,7 +3121,7 @@
|
||||
<el-option
|
||||
v-for="item in transportPlanImportContractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -4225,11 +4290,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="200" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row, dispatchItemAttachmentRows)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
@keydown.enter="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
@keydown.space.prevent="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -5242,8 +5318,11 @@ import { addressTypeOptions } from '@/option/base/common-address';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
import { parseRouteChangeContent } from '@/utils/route-change';
|
||||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
@@ -5822,7 +5901,7 @@ export default {
|
||||
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
|
||||
reconciliationModeOptions: ['明细逐行核对', '按账单汇总核对', '跳过自动核对'],
|
||||
packageOptions,
|
||||
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
|
||||
quantityUnitOptions: [],
|
||||
attachmentFileTypes: [
|
||||
'pdf',
|
||||
'bmp',
|
||||
@@ -5865,7 +5944,6 @@ export default {
|
||||
this.ensureBillingUnitOptions();
|
||||
}
|
||||
if (this.config.enableTaskInfoForm) {
|
||||
this.loadTaskQuantityUnitOptions();
|
||||
this.loadTaskFeeUnitOptions();
|
||||
this.ensureBillingCargoTypeOptions();
|
||||
}
|
||||
@@ -5886,6 +5964,7 @@ export default {
|
||||
} else {
|
||||
this.consumeTemplateCreatePayload();
|
||||
}
|
||||
this.loadTaskQuantityUnitOptions();
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
@@ -6589,6 +6668,8 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
parseRouteChangeContent,
|
||||
initStandaloneFormPage() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||
@@ -7789,8 +7870,6 @@ export default {
|
||||
['contractName', '客户合同'],
|
||||
['planName', '计划名称'],
|
||||
['transportType', '运输方式'],
|
||||
['planStartDate', '计划开始日期'],
|
||||
['planEndDate', '计划结束日期'],
|
||||
['departureAddress', '发货地址'],
|
||||
['arrivalAddress', '收货地址'],
|
||||
];
|
||||
@@ -9270,9 +9349,10 @@ export default {
|
||||
return Promise.resolve(this.taskQuantityUnitOptions);
|
||||
}
|
||||
this.taskQuantityUnitLoading = true;
|
||||
return getDictionary({ code: 'quantity_unit' })
|
||||
.then(res => {
|
||||
this.taskQuantityUnitOptions = this.normalizeDictOptions(res.data.data || []);
|
||||
return loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.taskQuantityUnitOptions = options;
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.taskQuantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -14111,7 +14191,7 @@ export default {
|
||||
|
||||
&__dispatch-shipping-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14634,7 +14714,7 @@ export default {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-select),
|
||||
@@ -15859,7 +15939,65 @@ export default {
|
||||
}
|
||||
|
||||
&__route-change-record-content {
|
||||
white-space: pre-wrap;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
line-height: 22px;
|
||||
white-space: normal;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__route-change-group {
|
||||
padding-left: 8px;
|
||||
border-left: 2px solid #409eff;
|
||||
}
|
||||
|
||||
&__route-change-group-title {
|
||||
margin-bottom: 4px;
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__route-change-group-text {
|
||||
color: #606266;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
&__route-change-field {
|
||||
display: grid;
|
||||
grid-template-columns: 54px minmax(0, 1fr);
|
||||
column-gap: 8px;
|
||||
row-gap: 2px;
|
||||
}
|
||||
|
||||
&__route-change-field-label {
|
||||
color: #909399;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__route-change-before,
|
||||
&__route-change-single {
|
||||
color: #606266;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
&__route-change-arrow {
|
||||
color: #c0c4cc;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__route-change-after {
|
||||
color: #409eff;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
&__route-change-flag {
|
||||
margin-left: 6px;
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
@@ -16251,7 +16389,7 @@ export default {
|
||||
}
|
||||
|
||||
:global(.business-crud-page__mileage-dialog .business-crud-page__mileage-form .el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:global(.business-crud-page__mileage-dialog .business-crud-page__mileage-form .el-input),
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
class="contract-manage-form__attachment-table"
|
||||
@selection-change="selected = $event"
|
||||
>
|
||||
<el-table-column v-if="!readonly" type="selection" width="55" align="center" />
|
||||
<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 }">
|
||||
@@ -45,11 +45,17 @@
|
||||
align="left"
|
||||
header-align="left"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="preview?.(row, rows)">{{
|
||||
attachmentName(row)
|
||||
}}</el-link>
|
||||
<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">
|
||||
@@ -173,15 +179,43 @@ 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: '双章归档文件' },
|
||||
// 高风险/黑名单客商合同必传,后端按 fileType=特批说明 校验与转传特批(与后端 HcsSyncConstant.ATTACHMENT_TYPE_SPEC_EXPLANATION 对齐)
|
||||
{ label: '特批说明', value: '特批说明' },
|
||||
{ label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
|
||||
];
|
||||
/** @deprecated 兼容旧引用,实际选项由业务字典 contract_attachment_type 动态加载 */
|
||||
export const defaultContractSupportingAttachmentTypeOptions = [
|
||||
{ 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',
|
||||
@@ -236,8 +270,11 @@ const isOtherAttachmentTypeOption = item => {
|
||||
const attachmentTypeKeywords = fileType => {
|
||||
const text = String(fileType || '');
|
||||
if (text.includes('双章')) return ['双章归档文件', '双章归档', '双章'];
|
||||
if (text.includes('合同') && !text.includes('其他') && !text.includes('其它')) {
|
||||
return ['合同文件', '合同'];
|
||||
if (
|
||||
text.includes('用印') ||
|
||||
(text.includes('合同') && !text.includes('其他') && !text.includes('其它'))
|
||||
) {
|
||||
return ['用印材料', '合同文件', '用印', '合同'];
|
||||
}
|
||||
if (isOtherAttachmentTypeOption({ label: text, value: text })) return [];
|
||||
return text ? [text] : [];
|
||||
@@ -256,8 +293,13 @@ export const resolveContractAttachmentType = (
|
||||
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));
|
||||
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;
|
||||
const fileName = normalizeAttachmentTypeText(attachmentName(row));
|
||||
if (!fileName) return otherType;
|
||||
@@ -285,17 +327,32 @@ export const normalizeContractFileRows = (rows, options = defaultContractAttachm
|
||||
description: row.description || '',
|
||||
}));
|
||||
|
||||
const mapDictOptions = response => {
|
||||
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 => ({
|
||||
label: item.dictValue,
|
||||
value: item.dictKey,
|
||||
}))
|
||||
.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 },
|
||||
@@ -309,8 +366,13 @@ export default {
|
||||
markApprovedOnUpload: Boolean,
|
||||
useUploadDialog: Boolean,
|
||||
dialogOnly: Boolean,
|
||||
attachmentLocation: { type: String, default: '合同文件' },
|
||||
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'],
|
||||
@@ -319,8 +381,9 @@ export default {
|
||||
selected: [],
|
||||
attachmentFileTypes,
|
||||
contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions],
|
||||
activeAttachmentTypeDictCode: CONTRACT_ATTACHMENT_TYPE_DICT,
|
||||
uploadDialogVisible: false,
|
||||
uploadDialogLocation: '合同文件',
|
||||
uploadDialogLocation: CONTRACT_SEAL_MATERIAL_LOCATION,
|
||||
uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions),
|
||||
uploadDialogFiles: [],
|
||||
uploadDialogFileList: [],
|
||||
@@ -339,48 +402,91 @@ export default {
|
||||
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;
|
||||
return ['合同文件', '其它附件'];
|
||||
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.loadAttachmentTypeOptions(this.resolvedAttachmentTypeDictCode, true);
|
||||
},
|
||||
methods: {
|
||||
attachmentName,
|
||||
loadAttachmentTypeOptions() {
|
||||
return getBizDictionary({ code: 'contract_attachment_type' })
|
||||
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 => {
|
||||
const options = mapDictOptions(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 }];
|
||||
if (
|
||||
!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);
|
||||
}
|
||||
} 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) {
|
||||
@@ -390,13 +496,14 @@ export default {
|
||||
this.$emit('update:rows', Array.isArray(rows) ? rows.map(item => ({ ...item })) : []);
|
||||
},
|
||||
enrichUploadedRow(row, existing) {
|
||||
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
||||
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 || row.uploadUserName || uploadUserName,
|
||||
// 新上传行始终写入当前用户;已有行保留原上传人
|
||||
uploadUserName: existing?.uploadUserName || uploadUserName || row.uploadUserName || '',
|
||||
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
|
||||
approved: existing?.approved || row.approved || false,
|
||||
};
|
||||
@@ -436,20 +543,38 @@ export default {
|
||||
resolveTypeByLocation(location) {
|
||||
const options = this.contractAttachmentTypeOptions || [];
|
||||
const locationText = String(location || '');
|
||||
if (locationText.includes('其它') || locationText.includes('其他')) {
|
||||
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() {
|
||||
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation);
|
||||
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;
|
||||
@@ -546,13 +671,13 @@ export default {
|
||||
this.$message.warning('请选择附件类型');
|
||||
return;
|
||||
}
|
||||
const uploadUserName = this.$store.getters.userInfo?.realName || '';
|
||||
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: row.uploadUserName || uploadUserName,
|
||||
uploadUserName: uploadUserName || row.uploadUserName || '',
|
||||
uploadTime: row.uploadTime || uploadTime,
|
||||
...(this.markApprovedOnUpload ? { approved: true } : {}),
|
||||
}));
|
||||
@@ -609,7 +734,15 @@ export default {
|
||||
downloadFileByUrl(url, attachmentName(row));
|
||||
},
|
||||
batchDownload() {
|
||||
(this.selected.length ? this.selected : this.rows || []).forEach(this.download);
|
||||
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);
|
||||
|
||||
@@ -44,19 +44,22 @@
|
||||
<dt>附件</dt>
|
||||
<dd>
|
||||
<template v-if="attachments.length">
|
||||
<el-link
|
||||
<span
|
||||
v-for="file in attachments"
|
||||
:key="file.url || file.link || file.name || file.originalName"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</el-link
|
||||
>
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</span>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ segmentLocationName(route, 'departure') }}</strong><small>{{ route.departureAddress || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(segments[index - 1])}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(route)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ segmentLocationName(route, 'arrival') }}</strong><small>{{ route.arrivalAddress || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div></template></div>
|
||||
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ segmentLocationName(route, 'departure') }}</strong><small>{{ formatRegionAddress(route.departureAddress) || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(segments[index - 1])}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)} ${plannedUnit(route)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ segmentLocationName(route, 'arrival') }}</strong><small>{{ formatRegionAddress(route.arrivalAddress) || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }} {{ plannedUnit(route) }}</small></div></template></div>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="documentPreviewVisible" :title="previewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh">
|
||||
@@ -107,6 +110,7 @@ import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archiv
|
||||
import { CaretBottom, CaretTop } from '@element-plus/icons-vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import { formatRegionAddress } from '@/utils/region-address';
|
||||
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';
|
||||
@@ -161,6 +165,7 @@ export default {
|
||||
} finally { this.loading = false; }
|
||||
},
|
||||
methods: {
|
||||
formatRegionAddress,
|
||||
quantity(value) { const number = Number(value || 0); return Number.isInteger(number) ? number : number.toFixed(3).replace(/\.?0+$/, ''); },
|
||||
segmentNumber(route, index) { return String(route.segmentNo || '').replace(/^段/, '') || index + 1; },
|
||||
isSegmentExpanded(segmentNo) { return Boolean(this.expandedSegments[segmentNo]); },
|
||||
@@ -330,7 +335,7 @@ export default {
|
||||
.transport-flow-tag {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } :deep(.el-link) { font-size: 14px; } }
|
||||
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } .attachment-file-name { font-size: 14px; max-width: 100%; } }
|
||||
.route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; }
|
||||
.route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
|
||||
.route-map__progress { color: #303133 !important; }
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
<div class="route-overview__node">
|
||||
<span :class="['route-badge', index === 0 ? 'start' : 'middle']">{{ index === 0 ? '起' : '经' }}</span>
|
||||
<el-tooltip :content="route.departureName || '-'" placement="top" :show-after="120">
|
||||
<strong>{{ route.departureName || '-' }}</strong>
|
||||
<strong>{{ dispatchLocationName(route, 'departure') }}</strong>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="route.departureAddress || '-'" placement="top" :show-after="120">
|
||||
<small>{{ route.departureAddress || '-' }}</small>
|
||||
<small>{{ formatRegionAddress(route.departureAddress) || '-' }}</small>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="contactText(route.departureContact, route.departurePhone)" placement="top" :show-after="120">
|
||||
<small>{{ contactText(route.departureContact, route.departurePhone) }}</small>
|
||||
@@ -34,10 +34,10 @@
|
||||
<div v-if="index === routes.length - 1" class="route-overview__node">
|
||||
<span class="route-badge end">终</span>
|
||||
<el-tooltip :content="route.arrivalName || '-'" placement="top" :show-after="120">
|
||||
<strong>{{ route.arrivalName || '-' }}</strong>
|
||||
<strong>{{ dispatchLocationName(route, 'arrival') }}</strong>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="route.arrivalAddress || '-'" placement="top" :show-after="120">
|
||||
<small>{{ route.arrivalAddress || '-' }}</small>
|
||||
<small>{{ formatRegionAddress(route.arrivalAddress) || '-' }}</small>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="contactText(route.arrivalContact, route.arrivalPhone)" placement="top" :show-after="120">
|
||||
<small>{{ contactText(route.arrivalContact, route.arrivalPhone) }}</small>
|
||||
@@ -266,7 +266,7 @@
|
||||
<el-col :span="6">
|
||||
<el-form-item label="单价">
|
||||
<el-input :model-value="freightGroupUnitPrice(group)" inputmode="decimal" placeholder="请输入" @input="value => handleFreightGroupUnitPriceInput(group, value)" @clear="() => handleFreightGroupUnitPriceInput(group, '')">
|
||||
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /><el-option label="元/方" value="元/方" /></el-select></template>
|
||||
<template #append><el-select :model-value="freightGroupPriceUnit(group)" class="freight-unit-select" @change="value => handleFreightGroupPriceUnitChange(group, value)"><el-option v-for="item in priceUnitOptions(route)" :key="item.value" :label="item.label" :value="item.value" /></el-select></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -317,6 +317,14 @@
|
||||
<script>
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import {
|
||||
currencyUnit,
|
||||
remapPriceUnitCurrency,
|
||||
resolvePriceUnitByQuantityUnit,
|
||||
} from '@/utils/currency';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
import { formatRegionAddress } from '@/utils/region-address';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
|
||||
const unwrapRecords = res => {
|
||||
@@ -347,7 +355,8 @@ export default {
|
||||
carrierLoading: false,
|
||||
driverLoading: false,
|
||||
escortLoading: false,
|
||||
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -389,10 +398,24 @@ export default {
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadQuantityUnitOptions();
|
||||
await Promise.all([this.load(), this.loadCarrierOptions()]);
|
||||
this.applySelfOperatedCarrierDefaults();
|
||||
},
|
||||
methods: {
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.quantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.quantityUnitLoading = false;
|
||||
});
|
||||
return this.quantityUnitLoadPromise;
|
||||
},
|
||||
async load() {
|
||||
if (!this.id) return;
|
||||
this.loading = true;
|
||||
@@ -412,8 +435,10 @@ export default {
|
||||
},
|
||||
async loadContractCurrency() {
|
||||
const directCurrency = this.master?.settlementCurrency || this.master?.settlementCurrencyCode || this.master?.currency;
|
||||
const previousCurrency = this.master?.settlementCurrency;
|
||||
if (!this.master?.projectId || !this.master?.contractId) {
|
||||
this.master.settlementCurrency = this.normalizeCurrencyValue(directCurrency || 'RMB');
|
||||
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -427,6 +452,20 @@ export default {
|
||||
this.master.customerContractPartyB = '';
|
||||
this.master.settlementCurrency = 'RMB';
|
||||
}
|
||||
this.syncRouteCurrency(previousCurrency, this.master.settlementCurrency);
|
||||
},
|
||||
syncRouteCurrency(_previousCurrency, nextCurrency) {
|
||||
const currency = this.normalizeCurrencyValue(nextCurrency || 'RMB');
|
||||
(this.routes || []).forEach(route => {
|
||||
route.currency = currency;
|
||||
(route.freightItems || []).forEach(item => {
|
||||
if (item.priceUnit) {
|
||||
item.priceUnit = remapPriceUnitCurrency(item.priceUnit, currency);
|
||||
} else if (item.quantityUnit) {
|
||||
item.priceUnit = resolvePriceUnitByQuantityUnit(item.quantityUnit, currency);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
dispatchRouteNodes() {
|
||||
const routes = Array.isArray(this.master?.routes) ? this.master.routes : [];
|
||||
@@ -601,8 +640,23 @@ export default {
|
||||
const prices = (group.rows || []).map(item => String(item.unitPrice ?? '').trim()).filter(Boolean);
|
||||
return !prices.length || prices.some(price => price !== prices[0]) ? '' : prices[0];
|
||||
},
|
||||
freightGroupPriceUnit(group = {}) { return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : ''); },
|
||||
freightGroupPriceUnit(group = {}) {
|
||||
if (group.rows?.[0]?.priceUnit) return group.rows[0].priceUnit;
|
||||
if (!group.quantityUnit) return '';
|
||||
return (
|
||||
resolvePriceUnitByQuantityUnit(group.quantityUnit, this.master?.settlementCurrency || 'RMB') ||
|
||||
`${currencyUnit(this.master?.settlementCurrency || 'RMB')}/${group.quantityUnit}`
|
||||
);
|
||||
},
|
||||
freightGroupAmount(group = {}) { return (group.rows || []).reduce((sum, item) => sum + this.freightAmount(item), 0); },
|
||||
priceUnitOptions(route = {}) {
|
||||
const unit = currencyUnit(route.currency || this.master?.settlementCurrency || 'RMB');
|
||||
return [
|
||||
{ label: `${unit}/吨`, value: `${unit}/吨` },
|
||||
{ label: `${unit}/件`, value: `${unit}/件` },
|
||||
{ label: `${unit}/方`, value: `${unit}/方` },
|
||||
];
|
||||
},
|
||||
handleFreightGroupUnitPriceInput(group, value) {
|
||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||
const normalized = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||
@@ -612,6 +666,10 @@ export default {
|
||||
handleFreightGroupQuantityUnitChange(route, group, value) {
|
||||
(group.rows || []).forEach(item => {
|
||||
item.quantityUnit = value || '';
|
||||
item.priceUnit = resolvePriceUnitByQuantityUnit(
|
||||
value,
|
||||
route.currency || this.master?.settlementCurrency || 'RMB'
|
||||
) || item.priceUnit;
|
||||
const goods = (route.goods || []).find(row => String(row.sourceIndex) === String(item.sourceIndex));
|
||||
if (goods) goods.quantityUnit = value || '';
|
||||
});
|
||||
@@ -634,7 +692,7 @@ export default {
|
||||
})),
|
||||
});
|
||||
},
|
||||
currencyLabel(value) { return ({ CNY: '元', RMB: '元', USD: '美元', EUR: '欧元' })[value] || value || '元'; },
|
||||
currencyLabel(value) { return currencyUnit(value); },
|
||||
quantityNumber(value) { return Number(value || 0); },
|
||||
goodsKey(goods = {}, includeUnit = true) {
|
||||
return [
|
||||
@@ -794,7 +852,12 @@ export default {
|
||||
const mileage = Number(value);
|
||||
return Number.isFinite(mileage) && mileage >= 0 ? String(Math.trunc(mileage)) : '';
|
||||
},
|
||||
addressText(name, address) { return [name, address].filter(Boolean).join(' / ') || '-'; },
|
||||
formatRegionAddress,
|
||||
addressText(name, address) {
|
||||
// 名称与详细地址都要按「市 区县」展示;两者格式化后相同时只展示一次
|
||||
const values = [this.formatRegionAddress(name), this.formatRegionAddress(address)].filter(Boolean);
|
||||
return [...new Set(values)].join(' / ') || '-';
|
||||
},
|
||||
contactText(name, phone) { return [name, phone].filter(Boolean).join(' - ') || '-'; },
|
||||
dateStartLabel(route) { return route.documentType === '计划单' ? '计划开始日期' : '预计发货日期'; },
|
||||
dateEndLabel(route) { return route.documentType === '计划单' ? '计划结束日期' : '预计完成日期'; },
|
||||
@@ -824,6 +887,7 @@ export default {
|
||||
removeGoods(route, index) { route.goods.splice(index, 1); this.syncFreightItems(route); },
|
||||
syncFreightItems(route) {
|
||||
const oldItems = route.freightItems || [];
|
||||
const currency = route.currency || this.master?.settlementCurrency || 'RMB';
|
||||
route.freightItems = (route.goods || [])
|
||||
.filter(item => item.sourceIndex !== undefined && item.sourceIndex !== '')
|
||||
.map(item => {
|
||||
@@ -835,7 +899,10 @@ export default {
|
||||
quantity: item.dispatchQuantity || '',
|
||||
quantityUnit: item.quantityUnit || '',
|
||||
unitPrice: old.unitPrice || '',
|
||||
priceUnit: old.priceUnit || `元/${item.quantityUnit || '吨'}`,
|
||||
priceUnit:
|
||||
old.priceUnit ||
|
||||
resolvePriceUnitByQuantityUnit(item.quantityUnit || '吨', currency) ||
|
||||
`${currencyUnit(currency)}/${item.quantityUnit || '吨'}`,
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -888,9 +955,11 @@ export default {
|
||||
},
|
||||
carrierOptionLabel(item) {
|
||||
const duplicateCount = this.carrierOptions.filter(option => option.carrierName === item.carrierName).length;
|
||||
return duplicateCount > 1
|
||||
? `${item.carrierName}(${item.contractName || item.contractId})`
|
||||
: item.carrierName;
|
||||
if (duplicateCount <= 1) {
|
||||
return item.carrierName;
|
||||
}
|
||||
const contractLabel = formatContractOptionLabel(item);
|
||||
return contractLabel ? `${item.carrierName}(${contractLabel})` : item.carrierName;
|
||||
},
|
||||
driverOptionLabel(item) { return item.driverName || item.name || ''; },
|
||||
driverOptionKey(item) { return item.id || item.driverId || this.driverOptionLabel(item) || item.mobile; },
|
||||
@@ -1197,7 +1266,7 @@ export default {
|
||||
.route-overview__line { flex: 0 1 56px; min-width: 24px; height: 32px; margin-top: 0; border-bottom: 6px solid #e4e7ed; }
|
||||
.segment-header { min-height: 46px; padding: 0 20px; border-bottom: 1px solid #eff1f7; font-weight: 600; em { color: #409eff; font-style: normal; } }
|
||||
.segment-content { padding: 16px 24px; }
|
||||
.dispatch-form { :deep(.el-form-item) { margin-bottom: 14px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
|
||||
.dispatch-form { :deep(.el-form-item) { margin-bottom: 24px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
|
||||
/* 短标签按自身宽度,避免被「预计发货日期」等长标签把左侧撑开 */
|
||||
.dispatch-form__compact-label {
|
||||
:deep(.el-form-item__label) {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
><el-option
|
||||
v-for="contract in contracts"
|
||||
:key="contract.id"
|
||||
:label="contract.contractName"
|
||||
:label="formatContractOptionLabel(contract)"
|
||||
:value="contract.id" /></el-select></el-form-item></el-col
|
||||
><el-col :span="6"
|
||||
><el-form-item label="总单号" required
|
||||
@@ -261,11 +261,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="240" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="240"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -589,6 +600,7 @@ import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
|
||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||
import { exportBlob, importBlob } from '@/api/common';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { OfficeBuilding, Search } from '@element-plus/icons-vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
@@ -597,10 +609,10 @@ import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from
|
||||
import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
const quantityUnitOptions = ['吨', '件', '方'];
|
||||
const attachmentViewerPlugins = [
|
||||
imagePlugin(),
|
||||
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
||||
@@ -703,10 +715,11 @@ export default {
|
||||
'zip',
|
||||
],
|
||||
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
||||
quantityUnitOptions,
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
form: {
|
||||
routes: [{ segmentNo: '段1', transportType: '' }],
|
||||
goods: [{ quantityUnit: quantityUnitOptions[0] }],
|
||||
goods: [{ quantityUnit: '吨' }],
|
||||
attachmentsJson: '[]',
|
||||
transportOrganizationType: '多式联运',
|
||||
},
|
||||
@@ -773,6 +786,7 @@ export default {
|
||||
async mounted() {
|
||||
this.loading = true;
|
||||
try {
|
||||
await this.loadQuantityUnitOptions();
|
||||
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions(), this.loadRegionOptions()]);
|
||||
if (this.id) {
|
||||
await this.loadDetail(this.id);
|
||||
@@ -782,6 +796,23 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
defaultQuantityUnit() {
|
||||
return this.quantityUnitOptions[0] || '吨';
|
||||
},
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.quantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.quantityUnitLoading = false;
|
||||
});
|
||||
return this.quantityUnitLoadPromise;
|
||||
},
|
||||
responseData(response) {
|
||||
return response?.data?.data || response?.data || response || {};
|
||||
},
|
||||
@@ -958,7 +989,7 @@ export default {
|
||||
finalTransportType: hasFinalSegment
|
||||
? finalSegment.transportType
|
||||
: detail.finalTransportType || '',
|
||||
goods: detail.goods || [{ quantityUnit: quantityUnitOptions[0] }],
|
||||
goods: detail.goods || [{ quantityUnit: this.defaultQuantityUnit() }],
|
||||
attachmentsJson: detail.attachmentsJson || '[]',
|
||||
transportOrganizationType: '多式联运',
|
||||
};
|
||||
@@ -1659,7 +1690,7 @@ export default {
|
||||
'',
|
||||
cargoTypePath,
|
||||
quantity: row.quantity || '',
|
||||
quantityUnit: row.quantityUnit || quantityUnitOptions[0],
|
||||
quantityUnit: row.quantityUnit || this.defaultQuantityUnit(),
|
||||
packageType: row.packageType || row.package || '',
|
||||
brand: row.brand || '',
|
||||
specification: row.specification || row.spec || '',
|
||||
@@ -1785,7 +1816,7 @@ export default {
|
||||
},
|
||||
addGoods(index) {
|
||||
this.form.goods.splice(index + 1, 0, {
|
||||
quantityUnit: quantityUnitOptions[0],
|
||||
quantityUnit: this.defaultQuantityUnit(),
|
||||
materialCode: '',
|
||||
deviceCode: '',
|
||||
remark: '',
|
||||
@@ -2047,7 +2078,7 @@ export default {
|
||||
font-size: 16px;
|
||||
}
|
||||
.el-form-item {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
.master-editor__quantity-field {
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<div v-if="groups.length" class="punch-record-photos">
|
||||
<section v-for="group in groups" :key="group.type" class="punch-record-photos__group">
|
||||
<div class="punch-record-photos__head">
|
||||
<span class="punch-record-photos__title">{{ group.type }}</span>
|
||||
<span v-if="editable" class="punch-record-photos__count">{{ countText(group) }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="punch-record-photos__grid"
|
||||
:class="{ 'punch-record-photos__grid--edit': editable }"
|
||||
>
|
||||
<div
|
||||
v-for="(item, groupIndex) in group.items"
|
||||
:key="`${item.photo.url}-${item.index}`"
|
||||
class="punch-record-photos__item"
|
||||
@click="handlePreview(item.index)"
|
||||
>
|
||||
<el-image
|
||||
:src="item.photo.url"
|
||||
:preview-src-list="nativePreview ? previewUrls : []"
|
||||
:initial-index="nativePreview ? item.index : 0"
|
||||
:preview-teleported="nativePreview"
|
||||
:lazy="lazy"
|
||||
fit="cover"
|
||||
/>
|
||||
<!-- 主图 = 该类型的第一张,按上传顺序取;没有独立字段,也不可切换 -->
|
||||
<div v-if="groupIndex === 0" class="punch-record-photos__badge">主图</div>
|
||||
<el-link
|
||||
v-if="editable"
|
||||
class="punch-record-photos__remove"
|
||||
type="danger"
|
||||
:underline="false"
|
||||
@click.stop="$emit('remove', item.index)"
|
||||
>删除</el-link
|
||||
>
|
||||
</div>
|
||||
<el-upload
|
||||
v-if="editable && !group.full"
|
||||
class="punch-record-photos__upload"
|
||||
:action="uploadAction"
|
||||
name="file"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:before-upload="file => handleBeforeUpload(file, group.type)"
|
||||
:on-success="res => $emit('upload-success', res, group.type)"
|
||||
:on-error="error => $emit('upload-error', error, group.type)"
|
||||
>
|
||||
<div class="punch-record-photos__add">+ 添加</div>
|
||||
</el-upload>
|
||||
<div v-else-if="editable" class="punch-record-photos__add punch-record-photos__add--full">
|
||||
已满 {{ maxPerType }} 张
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
// 打卡照片是每个打卡节点必有的一个照片类型,不由过程配置的凭证类型决定;
|
||||
// 它与各凭证类型各自最多 9 张,同一类型下第一张即界面上的「主图」。
|
||||
const PUNCH_PHOTO_TYPE = '打卡照片';
|
||||
// 照片没带类型时的兜底名称,与后端落库时的兜底口径一致
|
||||
const PHOTO_TYPE_FALLBACK = '凭证';
|
||||
const MAX_PHOTOS_PER_TYPE = 9;
|
||||
|
||||
const props = defineProps({
|
||||
// 扁平照片数组:管理端回显用 voucherType,司机端回显与旧数据用 type
|
||||
photos: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 恒定展示的照片类型:「打卡照片」+ 该节点过程配置勾选的凭证类型。
|
||||
// 留空则只按照片自身带的类型分组(只读回显用)。
|
||||
groupTypes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 编辑态:每个类型自带「添加」格与删除链接,并显示已选/上限
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 每个类型的张数上限;0 表示不限制(在途打卡只有一个照片字段,张数由「只保留第一张」约束)
|
||||
maxPerType: {
|
||||
type: Number,
|
||||
default: MAX_PHOTOS_PER_TYPE,
|
||||
},
|
||||
// 编辑态用 el-image 自带预览;只读态不发预览事件,把点击交给调用方开自己的看图器
|
||||
nativePreview: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
uploadAction: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
uploadHeaders: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 返回 false 即中止本次上传;类型、体积、张数校验都在调用方
|
||||
beforeUpload: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['preview', 'remove', 'upload-success', 'upload-error']);
|
||||
|
||||
const photoType = photo =>
|
||||
String(photo?.voucherType || photo?.type || '').trim() || PHOTO_TYPE_FALLBACK;
|
||||
|
||||
const groups = computed(() => {
|
||||
const photos = Array.isArray(props.photos) ? props.photos : [];
|
||||
const names = [];
|
||||
const buckets = new Map();
|
||||
const ensure = name => {
|
||||
if (!buckets.has(name)) {
|
||||
buckets.set(name, []);
|
||||
names.push(name);
|
||||
}
|
||||
return buckets.get(name);
|
||||
};
|
||||
// 恒定展示的类型先占位:一张照片都没有的类型也要出现,否则编辑态下没有添加入口
|
||||
props.groupTypes.forEach(type => {
|
||||
const name = String(type || '').trim();
|
||||
if (name) ensure(name);
|
||||
});
|
||||
// 按上传顺序入组,组间顺序即类型首次出现的顺序,不重排照片
|
||||
photos.forEach((photo, index) => {
|
||||
if (!photo?.url) return;
|
||||
ensure(photoType(photo)).push({ photo, index });
|
||||
});
|
||||
const list = names.map(type => {
|
||||
const items = buckets.get(type);
|
||||
return {
|
||||
type,
|
||||
items,
|
||||
full: props.editable && props.maxPerType > 0 && items.length >= props.maxPerType,
|
||||
};
|
||||
});
|
||||
// 编辑态至少要有一个分组,否则记录里还没有照片时就没有添加入口
|
||||
if (props.editable && !list.length) {
|
||||
return [{ type: props.groupTypes[0] || PUNCH_PHOTO_TYPE, items: [], full: false }];
|
||||
}
|
||||
return list;
|
||||
});
|
||||
|
||||
// 预览列表按原始下标对齐,删除/分组都不改变点击某张时打开的是哪一张
|
||||
const previewUrls = computed(() =>
|
||||
(Array.isArray(props.photos) ? props.photos : []).map(photo => photo?.url || '')
|
||||
);
|
||||
|
||||
const handlePreview = index => {
|
||||
if (props.nativePreview) return;
|
||||
emit('preview', index);
|
||||
};
|
||||
|
||||
const handleBeforeUpload = (file, type) => {
|
||||
if (typeof props.beforeUpload !== 'function') return true;
|
||||
return props.beforeUpload(file, type);
|
||||
};
|
||||
|
||||
const countText = group =>
|
||||
props.maxPerType > 0 ? `${group.items.length}/${props.maxPerType}` : String(group.items.length);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.punch-record-photos {
|
||||
// 弹窗里本组件挂在 el-form-item__content 下,那是个 flex 容器:
|
||||
// 不给宽度就会收缩成内容宽,里面的网格跟着塌成一小块
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 4px;
|
||||
|
||||
&__group {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
// 只读回显:撑满容器宽度的五列网格
|
||||
&__grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
// 编辑态沿用弹窗原本的固定 96px 方图 + 自动换行:尺寸不随容器宽窄变化
|
||||
&__grid--edit {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.el-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border: 1px solid #eff1f7;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
}
|
||||
|
||||
&__grid--edit &__item {
|
||||
width: 96px;
|
||||
|
||||
.el-image {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px 0 4px 0;
|
||||
background: rgba(64, 158, 255, 0.9);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&__remove {
|
||||
align-self: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__grid--edit &__upload {
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
&__add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border: 1px dashed #dcdfe6;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__grid--edit &__add {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
aspect-ratio: auto;
|
||||
}
|
||||
|
||||
&__add--full {
|
||||
color: #c0c4cc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -113,7 +113,7 @@
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -153,11 +153,24 @@
|
||||
@click="handleTransportSecondaryAddressInputClick('departure')"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon @click.stop="openTransportMapDialog('departure')">
|
||||
<el-icon
|
||||
class="shipping-template-page__map-suffix"
|
||||
@click.stop="openTransportMapDialog('departure')"
|
||||
>
|
||||
<Location />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-tooltip content="选择常用地址" placement="top">
|
||||
<el-button
|
||||
class="shipping-template-page__address-pick-btn"
|
||||
type="primary"
|
||||
:icon="OfficeBuilding"
|
||||
link
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="openTransportAddressPicker('departure')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<span>联系人</span>
|
||||
<el-input
|
||||
v-model="form.departureContact"
|
||||
@@ -166,16 +179,6 @@
|
||||
/>
|
||||
<span>联系方式</span>
|
||||
<el-input v-model="form.departurePhone" placeholder="请输入" :disabled="dialogReadonly" />
|
||||
<el-tooltip content="选择常用地址" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="openTransportAddressPicker('departure')"
|
||||
>
|
||||
选择
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -197,25 +200,28 @@
|
||||
@click="handleTransportSecondaryAddressInputClick('arrival')"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon @click.stop="openTransportMapDialog('arrival')">
|
||||
<el-icon
|
||||
class="shipping-template-page__map-suffix"
|
||||
@click.stop="openTransportMapDialog('arrival')"
|
||||
>
|
||||
<Location />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-tooltip content="选择常用地址" placement="top">
|
||||
<el-button
|
||||
class="shipping-template-page__address-pick-btn"
|
||||
type="primary"
|
||||
:icon="OfficeBuilding"
|
||||
link
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="openTransportAddressPicker('arrival')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
<span>联系人</span>
|
||||
<el-input v-model="form.arrivalContact" placeholder="请输入" :disabled="dialogReadonly" />
|
||||
<span>联系方式</span>
|
||||
<el-input v-model="form.arrivalPhone" placeholder="请输入" :disabled="dialogReadonly" />
|
||||
<el-tooltip content="选择常用地址" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:disabled="transportAddressSelectDisabled"
|
||||
@click="openTransportAddressPicker('arrival')"
|
||||
>
|
||||
选择
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -454,11 +460,20 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column label="文件名" min-width="220"
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
><template #default="{ row }"
|
||||
><el-link type="primary" @click="previewAttachment(row)">{{
|
||||
attachmentName(row)
|
||||
}}</el-link></template
|
||||
><span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column label="附件描述" min-width="220"
|
||||
@@ -721,11 +736,21 @@
|
||||
</div>
|
||||
<el-table :data="detailAttachmentRows" border empty-text="暂无附件">
|
||||
<el-table-column type="index" label="序号" width="70" />
|
||||
<el-table-column label="文件名" min-width="220">
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">{{
|
||||
attachmentName(row)
|
||||
}}</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220">
|
||||
@@ -1098,10 +1123,12 @@ import {
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
import { Location } from '@element-plus/icons-vue';
|
||||
import { Location, OfficeBuilding } from '@element-plus/icons-vue';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import {
|
||||
@@ -1252,6 +1279,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
Location,
|
||||
OfficeBuilding,
|
||||
form: {},
|
||||
query: {},
|
||||
loading: true,
|
||||
@@ -1278,7 +1306,8 @@ export default {
|
||||
cargoTypeRequest: null,
|
||||
transportCargoRows: [],
|
||||
packageOptions,
|
||||
quantityUnitOptions: ['吨', '千克', '立方米', '立方', '公里', '单', '件', '车', '箱', '托盘'],
|
||||
quantityUnitOptions: [],
|
||||
quantityUnitLoading: false,
|
||||
feeUnitOptions: [],
|
||||
currencyOptions: [],
|
||||
currencyLoading: false,
|
||||
@@ -1525,11 +1554,26 @@ export default {
|
||||
created() {
|
||||
this.loadProjectOptions();
|
||||
this.ensureCargoTypeOptions();
|
||||
this.loadQuantityUnitOptions();
|
||||
this.loadFeeUnitOptions();
|
||||
this.loadCurrencyOptions();
|
||||
if (this.isStandaloneFormPage) this.$nextTick(() => this.initStandaloneFormPage());
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
loadQuantityUnitOptions() {
|
||||
if (this.quantityUnitLoading) return this.quantityUnitLoadPromise;
|
||||
this.quantityUnitLoading = true;
|
||||
this.quantityUnitLoadPromise = loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
return this.quantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.quantityUnitLoading = false;
|
||||
});
|
||||
return this.quantityUnitLoadPromise;
|
||||
},
|
||||
buildOption(option) {
|
||||
const next = { ...option, column: (option.column || []).map(column => ({ ...column })) };
|
||||
const tableOrder = this.config.tableColumnOrder || [];
|
||||
@@ -3384,10 +3428,25 @@ export default {
|
||||
.shipping-template-page__route-row {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(160px, 1fr) minmax(220px, 2fr) auto minmax(140px, 1fr) auto minmax(160px, 1fr)
|
||||
auto;
|
||||
minmax(160px, 1fr) minmax(220px, 2fr) 40px auto minmax(140px, 1fr) auto
|
||||
minmax(160px, 1fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
.shipping-template-page__address-pick-btn {
|
||||
:deep(.el-icon) {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.shipping-template-page__map-suffix {
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
font-size: 18px;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
.shipping-template-page__address-map-input {
|
||||
cursor: pointer;
|
||||
@@ -3506,7 +3565,7 @@ export default {
|
||||
}
|
||||
.shipping-template-page__detail-form :deep(.el-form-item) {
|
||||
min-width: 0;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.shipping-template-page__detail-form :deep(.el-form-item__label) {
|
||||
color: #909399;
|
||||
@@ -3695,7 +3754,7 @@ export default {
|
||||
}
|
||||
|
||||
:global(.shipping-template-form-page-dialog .avue-group .el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:global(.shipping-template-form-page-dialog .avue-form__menu) {
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -492,11 +492,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="200" align="left" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
align="left"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ row.originalName || row.name }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ row.originalName || row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -673,14 +684,16 @@
|
||||
<div class="transport-plan-page__transport-plan-detail-field is-attachments">
|
||||
<span>附件</span>
|
||||
<div class="transport-plan-page__transport-plan-detail-attachments">
|
||||
<el-link
|
||||
<span
|
||||
v-for="file in attachmentRows"
|
||||
:key="file.url || file.name || file.originalName"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>
|
||||
{{ attachmentName(file) }}
|
||||
</el-link>
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ attachmentName(file) }}</span>
|
||||
<span v-if="!attachmentRows.length">-</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -855,11 +868,21 @@
|
||||
</div>
|
||||
<el-table :data="attachmentRows" empty-text="暂无附件">
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="260">
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="260"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -980,7 +1003,7 @@
|
||||
<el-option
|
||||
v-for="item in transportPlanImportContractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -1744,11 +1767,11 @@
|
||||
><el-select
|
||||
:model-value="dispatchFreightGroupPriceUnit(group)"
|
||||
class="transport-plan-page__dispatch-unit-select"
|
||||
placeholder="元/吨"
|
||||
:placeholder="defaultPriceUnit"
|
||||
@change="value => handleDispatchFreightGroupPriceUnitChange(group, value)"
|
||||
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
|
||||
><el-option
|
||||
v-for="item in taskFeeUnitOptions"
|
||||
v-for="item in taskFeeUnitSelectOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value" /></el-select
|
||||
@@ -1969,17 +1992,17 @@
|
||||
<el-select
|
||||
v-model="dispatchItemForm.priceUnit"
|
||||
class="transport-plan-page__dispatch-unit-select"
|
||||
placeholder="元/吨"
|
||||
:placeholder="defaultPriceUnit"
|
||||
:loading="taskFeeUnitLoading"
|
||||
@visible-change="visible => visible && loadTaskFeeUnitOptions()"
|
||||
>
|
||||
<el-option
|
||||
v-if="!taskFeeUnitOptions.some(item => item.value === '元/吨')"
|
||||
label="元/吨"
|
||||
value="元/吨"
|
||||
v-if="!taskFeeUnitSelectOptions.some(item => item.value === defaultPriceUnit)"
|
||||
:label="defaultPriceUnit"
|
||||
:value="defaultPriceUnit"
|
||||
/>
|
||||
<el-option
|
||||
v-for="item in taskFeeUnitOptions"
|
||||
v-for="item in taskFeeUnitSelectOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
@@ -2201,11 +2224,22 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名" min-width="200" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row, dispatchItemAttachmentRows)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
@keydown.enter="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
@keydown.space.prevent="previewAttachment(row, dispatchItemAttachmentRows)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" align="center">
|
||||
@@ -2741,7 +2775,15 @@ import { addressTypeOptions } from '@/option/base/common-address';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import {
|
||||
currencyUnit,
|
||||
mapFeeUnitOptions,
|
||||
remapPriceUnitCurrency,
|
||||
resolvePriceUnitByQuantityUnit,
|
||||
} from '@/utils/currency';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { loadMeasurementUnitOptions } from '@/utils/measurement-unit';
|
||||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
@@ -3206,7 +3248,7 @@ export default {
|
||||
billingCargoTypeLoading: false,
|
||||
billingCargoTypeRequest: null,
|
||||
packageOptions,
|
||||
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
|
||||
quantityUnitOptions: [],
|
||||
attachmentFileTypes: [
|
||||
'pdf',
|
||||
'bmp',
|
||||
@@ -3245,6 +3287,7 @@ export default {
|
||||
} else {
|
||||
this.consumeTemplateCreatePayload();
|
||||
}
|
||||
this.loadTaskQuantityUnitOptions();
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
@@ -3609,6 +3652,22 @@ export default {
|
||||
const value = this.dispatchItemForm.freightCurrency || 'RMB';
|
||||
return this.currencyRemark(value);
|
||||
},
|
||||
taskFeeUnitSelectOptions() {
|
||||
const currency =
|
||||
this.dispatchItemForm.freightCurrency ||
|
||||
this.dispatchContractCurrency ||
|
||||
this.form.settlementCurrency ||
|
||||
'RMB';
|
||||
return mapFeeUnitOptions(this.taskFeeUnitOptions, currency);
|
||||
},
|
||||
defaultPriceUnit() {
|
||||
return `${currencyUnit(
|
||||
this.dispatchItemForm.freightCurrency ||
|
||||
this.dispatchContractCurrency ||
|
||||
this.form.settlementCurrency ||
|
||||
'RMB'
|
||||
)}/吨`;
|
||||
},
|
||||
isAdmin() {
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
@@ -3719,6 +3778,7 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
initStandaloneFormPage() {
|
||||
if (!this.isStandaloneFormPage) return;
|
||||
const mode = this.$route.query.mode === 'edit' ? 'edit' : 'add';
|
||||
@@ -4307,8 +4367,6 @@ export default {
|
||||
['contractName', '客户合同'],
|
||||
['planName', '计划名称'],
|
||||
['transportType', '运输方式'],
|
||||
['planStartDate', '计划开始日期'],
|
||||
['planEndDate', '计划结束日期'],
|
||||
['departureAddress', '发货地址'],
|
||||
['arrivalAddress', '收货地址'],
|
||||
];
|
||||
@@ -4804,19 +4862,49 @@ export default {
|
||||
contract.customerNames ||
|
||||
this.form.customerName ||
|
||||
'';
|
||||
const nextCurrency = this.getContractCurrency(contract);
|
||||
this.remapTransportPlanPriceUnits(nextCurrency);
|
||||
this.$refs.crud?.validateField?.('contractName');
|
||||
},
|
||||
remapTransportPlanPriceUnits(currency) {
|
||||
if (this.form.priceUnit) {
|
||||
this.form.priceUnit = remapPriceUnitCurrency(this.form.priceUnit, currency);
|
||||
}
|
||||
if (this.dispatchItemForm.priceUnit) {
|
||||
this.dispatchItemForm.priceUnit = remapPriceUnitCurrency(
|
||||
this.dispatchItemForm.priceUnit,
|
||||
currency
|
||||
);
|
||||
}
|
||||
this.dispatchItemForm.freightCurrency = currency;
|
||||
(this.dispatchItemCargoRows || []).forEach(cargo => {
|
||||
if (cargo.priceUnit) {
|
||||
cargo.priceUnit = remapPriceUnitCurrency(cargo.priceUnit, currency);
|
||||
}
|
||||
});
|
||||
(this.transportCargoRows || []).forEach(cargo => {
|
||||
if (cargo.priceUnit) {
|
||||
cargo.priceUnit = remapPriceUnitCurrency(cargo.priceUnit, currency);
|
||||
}
|
||||
});
|
||||
},
|
||||
loadDispatchContractCurrency(plan = {}) {
|
||||
const findContract = () =>
|
||||
this.contractOptions.find(item => String(item.id) === String(plan.contractId)) ||
|
||||
this.contractOptions.find(item => item.contractName === plan.contractName);
|
||||
const applyCurrency = contract => {
|
||||
const previousCurrency = this.dispatchContractCurrency;
|
||||
this.dispatchContractCurrency = this.getContractCurrency(
|
||||
contract &&
|
||||
(contract.settlementCurrency || contract.settlementCurrencyCode || contract.currency)
|
||||
? contract
|
||||
: { settlementCurrency: plan.settlementCurrency }
|
||||
);
|
||||
if (previousCurrency && previousCurrency !== this.dispatchContractCurrency) {
|
||||
this.remapTransportPlanPriceUnits(this.dispatchContractCurrency);
|
||||
} else {
|
||||
this.dispatchItemForm.freightCurrency = this.dispatchContractCurrency;
|
||||
}
|
||||
return this.dispatchContractCurrency;
|
||||
};
|
||||
const current = findContract();
|
||||
@@ -5076,15 +5164,40 @@ export default {
|
||||
return Promise.resolve(this.taskQuantityUnitOptions);
|
||||
}
|
||||
this.taskQuantityUnitLoading = true;
|
||||
return getDictionary({ code: 'quantity_unit' })
|
||||
.then(res => {
|
||||
this.taskQuantityUnitOptions = this.normalizeDictOptions(res.data.data || []);
|
||||
return loadMeasurementUnitOptions()
|
||||
.then(options => {
|
||||
this.taskQuantityUnitOptions = options;
|
||||
this.quantityUnitOptions = options.map(item => item.value);
|
||||
this.ensureQuantityUnitOptions([
|
||||
this.dispatchItemForm?.quantityUnit,
|
||||
...(this.dispatchItemCargoRows || []).map(row => row.quantityUnit),
|
||||
...(this.transportCargoRows || []).map(row => row.quantityUnit),
|
||||
]);
|
||||
return this.taskQuantityUnitOptions;
|
||||
})
|
||||
.finally(() => {
|
||||
this.taskQuantityUnitLoading = false;
|
||||
});
|
||||
},
|
||||
ensureQuantityUnitOptions(units = []) {
|
||||
const pending = [...new Set(
|
||||
(Array.isArray(units) ? units : [units])
|
||||
.map(item => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
)];
|
||||
if (!pending.length) return;
|
||||
pending.forEach(unit => {
|
||||
if (!this.quantityUnitOptions.includes(unit)) {
|
||||
this.quantityUnitOptions = [...this.quantityUnitOptions, unit];
|
||||
}
|
||||
if (!this.taskQuantityUnitOptions.some(item => item.value === unit)) {
|
||||
this.taskQuantityUnitOptions = [
|
||||
...this.taskQuantityUnitOptions,
|
||||
{ label: unit, value: unit },
|
||||
];
|
||||
}
|
||||
});
|
||||
},
|
||||
loadTaskFeeUnitOptions() {
|
||||
if (this.taskFeeUnitOptions.length || this.taskFeeUnitLoading) {
|
||||
return Promise.resolve(this.taskFeeUnitOptions);
|
||||
@@ -5146,6 +5259,9 @@ export default {
|
||||
});
|
||||
},
|
||||
normalizeTransportCargoRow(row = {}) {
|
||||
const quantityUnit = String(
|
||||
row.quantityUnit || row.goodsQuantityUnit || row.unit || ''
|
||||
).trim();
|
||||
const cargo = {
|
||||
...defaultTransportCargo(),
|
||||
...row,
|
||||
@@ -5162,10 +5278,14 @@ export default {
|
||||
packageType: row.packageType || row.package || '',
|
||||
specification: row.specification || row.spec || '',
|
||||
freightAmount: row.freightAmount ?? row.amount ?? row.totalAmount ?? '',
|
||||
quantityUnit,
|
||||
};
|
||||
['quantity', 'mileage', 'unitPrice', 'freightAmount'].forEach(prop => {
|
||||
if (Number(cargo[prop]) === -1) cargo[prop] = '';
|
||||
});
|
||||
if (quantityUnit) {
|
||||
this.ensureQuantityUnitOptions(quantityUnit);
|
||||
}
|
||||
return cargo;
|
||||
},
|
||||
hasMultipleConfiguredGoods(source = {}, rows = []) {
|
||||
@@ -5183,7 +5303,7 @@ export default {
|
||||
},
|
||||
addTransportCargoRow(index = -1, row = {}) {
|
||||
const nextRow = this.normalizeTransportCargoRow({
|
||||
priceUnit: this.form.priceUnit || '元/吨',
|
||||
priceUnit: this.form.priceUnit || this.defaultPriceUnit,
|
||||
...row,
|
||||
});
|
||||
if (
|
||||
@@ -7594,6 +7714,9 @@ export default {
|
||||
...baseRow,
|
||||
...(index >= 0 ? row : {}),
|
||||
};
|
||||
this.dispatchItemForm.quantityUnit =
|
||||
this.dispatchItemForm.quantityUnit || defaultUnit || TRANSPORT_PLAN_QUANTITY_UNIT;
|
||||
this.ensureQuantityUnitOptions(this.dispatchItemForm.quantityUnit);
|
||||
this.dispatchItemForm.otherFeeTotal = this.normalizeDispatchFeeValue(
|
||||
this.dispatchItemForm.otherFeeTotal
|
||||
);
|
||||
@@ -7603,7 +7726,11 @@ export default {
|
||||
});
|
||||
const goodsRows = this.parseJsonArray(this.dispatchItemForm.goodsJson);
|
||||
this.dispatchItemCargoRows = (goodsRows.length ? goodsRows : [this.dispatchItemForm]).map(
|
||||
item => this.normalizeTransportCargoRow(item)
|
||||
item =>
|
||||
this.normalizeTransportCargoRow({
|
||||
...item,
|
||||
quantityUnit: item.quantityUnit || this.dispatchItemForm.quantityUnit || defaultUnit,
|
||||
})
|
||||
);
|
||||
const hasMultiplePlanGoods = this.dispatchPlanGoodsRows.length > 1;
|
||||
const shouldAutoFillFullGoods =
|
||||
@@ -7613,11 +7740,18 @@ export default {
|
||||
const unscheduledGoodsRows = this.getDispatchUnscheduledGoodsRows();
|
||||
this.dispatchItemForm.taskEntryMode = 'full';
|
||||
this.dispatchItemForm.quantity = '';
|
||||
this.dispatchItemCargoRows = unscheduledGoodsRows.length
|
||||
? unscheduledGoodsRows
|
||||
: this.dispatchPlanGoodsRows.map(goods =>
|
||||
this.normalizeTransportCargoRow({ ...goods, quantity: '' })
|
||||
);
|
||||
this.dispatchItemCargoRows = (
|
||||
unscheduledGoodsRows.length
|
||||
? unscheduledGoodsRows
|
||||
: this.dispatchPlanGoodsRows.map(goods =>
|
||||
this.normalizeTransportCargoRow({ ...goods, quantity: '' })
|
||||
)
|
||||
).map(goods =>
|
||||
this.normalizeTransportCargoRow({
|
||||
...goods,
|
||||
quantityUnit: goods.quantityUnit || defaultUnit || TRANSPORT_PLAN_QUANTITY_UNIT,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!this.dispatchItemCargoRows.length) {
|
||||
this.dispatchItemCargoRows = [this.normalizeTransportCargoRow()];
|
||||
@@ -7632,7 +7766,10 @@ export default {
|
||||
this.dispatchItemCargoRows = this.dispatchItemCargoRows.map((cargo, itemIndex) => ({
|
||||
...cargo,
|
||||
unitPrice: cargo.unitPrice || freightInfo.freightItems[itemIndex]?.unitPrice || '',
|
||||
priceUnit: cargo.priceUnit || freightInfo.freightItems[itemIndex]?.priceUnit || '元/吨',
|
||||
priceUnit:
|
||||
cargo.priceUnit ||
|
||||
freightInfo.freightItems[itemIndex]?.priceUnit ||
|
||||
this.defaultPriceUnit,
|
||||
freightAmount:
|
||||
cargo.freightAmount || freightInfo.freightItems[itemIndex]?.freightAmount || '',
|
||||
}));
|
||||
@@ -7881,7 +8018,7 @@ export default {
|
||||
return value;
|
||||
},
|
||||
addDispatchCargoRow(index = -1) {
|
||||
const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' });
|
||||
const nextRow = this.normalizeTransportCargoRow({ priceUnit: this.defaultPriceUnit });
|
||||
if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow);
|
||||
else this.dispatchItemCargoRows.push(nextRow);
|
||||
},
|
||||
@@ -7925,7 +8062,14 @@ export default {
|
||||
return prices[0];
|
||||
},
|
||||
dispatchFreightGroupPriceUnit(group = {}) {
|
||||
return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : '');
|
||||
if (group.rows?.[0]?.priceUnit) return group.rows[0].priceUnit;
|
||||
if (!group.quantityUnit) return '';
|
||||
return (
|
||||
resolvePriceUnitByQuantityUnit(
|
||||
group.quantityUnit,
|
||||
this.dispatchItemForm.freightCurrency || this.dispatchContractCurrency || 'RMB'
|
||||
) || `${currencyUnit(this.dispatchContractCurrency || 'RMB')}/${group.quantityUnit}`
|
||||
);
|
||||
},
|
||||
dispatchFreightGroupAmount(group = {}) {
|
||||
const amount = (group.rows || []).reduce(
|
||||
@@ -8309,7 +8453,10 @@ export default {
|
||||
this.dispatchItemForm.cargoName ||
|
||||
'',
|
||||
quantity: quantityText,
|
||||
quantityUnit: firstGoods.quantityUnit || this.dispatchItemForm.quantityUnit || '',
|
||||
quantityUnit:
|
||||
firstGoods.quantityUnit ||
|
||||
this.dispatchItemForm.quantityUnit ||
|
||||
TRANSPORT_PLAN_QUANTITY_UNIT,
|
||||
unitPrice: firstGoods.unitPrice || '',
|
||||
priceUnit: firstGoods.priceUnit || '',
|
||||
freightTotal: this.dispatchItemFreightTotal,
|
||||
@@ -8324,7 +8471,7 @@ export default {
|
||||
unitPrice: cargo.unitPrice || '',
|
||||
priceUnit: cargo.priceUnit || '',
|
||||
quantity: cargo.quantity || '',
|
||||
quantityUnit: cargo.quantityUnit || '',
|
||||
quantityUnit: cargo.quantityUnit || firstGoods.quantityUnit || TRANSPORT_PLAN_QUANTITY_UNIT,
|
||||
freightAmount: this.dispatchCargoFreightAmount(cargo),
|
||||
})),
|
||||
}),
|
||||
@@ -8413,26 +8560,40 @@ export default {
|
||||
return {
|
||||
id: this.dispatchRow.id,
|
||||
mode,
|
||||
waybills: rows.map(row => ({
|
||||
...row,
|
||||
taskEntryMode: row.taskEntryMode || 'simple',
|
||||
taskRemark: row.remark || '',
|
||||
estimatedStartTime: this.formatDispatchDate(row.estimatedStartTime),
|
||||
estimatedEndTime: this.formatDispatchDate(row.estimatedEndTime),
|
||||
goodsJson:
|
||||
row.goodsJson ||
|
||||
JSON.stringify([
|
||||
waybills: rows.map(row => {
|
||||
const quantityUnit = this.getDispatchQuantityUnit(row);
|
||||
let goodsJson = row.goodsJson;
|
||||
const goodsRows = this.parseJsonArray(goodsJson);
|
||||
if (goodsRows.length) {
|
||||
goodsJson = JSON.stringify(
|
||||
goodsRows.map(goods => ({
|
||||
...goods,
|
||||
quantityUnit: goods.quantityUnit || quantityUnit,
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
goodsJson = JSON.stringify([
|
||||
{
|
||||
cargoName: row.cargoName,
|
||||
cargoType: row.cargoType,
|
||||
quantity: row.quantity,
|
||||
quantityUnit: row.quantityUnit,
|
||||
quantityUnit,
|
||||
specification: row.specification,
|
||||
model: row.model,
|
||||
packageType: row.packageType,
|
||||
},
|
||||
]),
|
||||
})),
|
||||
]);
|
||||
}
|
||||
return {
|
||||
...row,
|
||||
quantityUnit,
|
||||
taskEntryMode: row.taskEntryMode || 'simple',
|
||||
taskRemark: row.remark || '',
|
||||
estimatedStartTime: this.formatDispatchDate(row.estimatedStartTime),
|
||||
estimatedEndTime: this.formatDispatchDate(row.estimatedEndTime),
|
||||
goodsJson,
|
||||
};
|
||||
}),
|
||||
};
|
||||
},
|
||||
formatDispatchDate(value) {
|
||||
@@ -8680,7 +8841,7 @@ export default {
|
||||
|
||||
&__dispatch-shipping-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9871,7 +10032,7 @@ export default {
|
||||
}
|
||||
|
||||
:global(.transport-plan-form-page-dialog .avue-group .el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:global(.transport-plan-form-page-dialog .avue-group .dialog-section-title) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="voucher-folder-page voucher-folder-page--embedded">
|
||||
<section class="voucher-folder-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px">
|
||||
<el-form :model="query" label-position="right">
|
||||
<div class="voucher-folder-page__search-grid">
|
||||
<el-form-item label="车牌号">
|
||||
<el-input v-model="query.plateNo" clearable placeholder="请输入" />
|
||||
@@ -196,7 +196,7 @@ watch(
|
||||
}
|
||||
&__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 360px 1fr;
|
||||
grid-template-columns: 220px 220px 1fr;
|
||||
gap: 8px 24px;
|
||||
}
|
||||
&__search-actions {
|
||||
|
||||
@@ -270,15 +270,22 @@
|
||||
prop="mileage"
|
||||
label="里程(km)"
|
||||
width="100"
|
||||
/><el-table-column prop="unitPrice" label="单价" width="90" /><el-table-column
|
||||
prop="freight"
|
||||
label="运费"
|
||||
width="100"
|
||||
/><el-table-column prop="otherFeeTotal" label="其他费用合计" width="120" /><el-table-column
|
||||
prop="freightTotal"
|
||||
label="运费合计"
|
||||
width="100"
|
||||
/><el-table-column
|
||||
>
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.mileage) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unitPrice" label="单价" width="90">
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.unitPrice) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="freight" label="运费" width="100">
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.freight) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="otherFeeTotal" label="其他费用合计" width="120">
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.otherFeeTotal) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="freightTotal" label="运费合计" width="100">
|
||||
<template #default="{ row }">{{ formatOptionalNumber(row.freightTotal) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="actualStartDate"
|
||||
label="实际发货时间"
|
||||
width="160"
|
||||
@@ -374,7 +381,7 @@
|
||||
<el-option
|
||||
v-for="item in contracts"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -610,6 +617,7 @@ import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import * as api from '@/api/business/waybill-manage';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
|
||||
@@ -879,11 +887,14 @@ const syncCarrierOptions = () => {
|
||||
carrierOptions.value = [];
|
||||
return;
|
||||
}
|
||||
const carrierId =
|
||||
const carrierId = resolveCarrierArchiveId(
|
||||
customerContract?.partyBId ||
|
||||
customerContract?.partyBUserId ||
|
||||
customerContract?.customerId ||
|
||||
'';
|
||||
customerContract?.partyBUserId ||
|
||||
customerContract?.contractPartyBId ||
|
||||
customerContract?.customerId ||
|
||||
'',
|
||||
carrierName
|
||||
);
|
||||
carrierOptions.value = [{ id: carrierId, carrierName, label: carrierName, value: carrierName }];
|
||||
form.carrierId = carrierId;
|
||||
form.carrierIds = carrierId ? [carrierId] : [];
|
||||
@@ -1079,19 +1090,40 @@ const transportTypeLabel = value => {
|
||||
);
|
||||
return matched?.dictValue || matched?.label || named || text || '-';
|
||||
};
|
||||
const buildBatchQueryParams = () => ({
|
||||
batchNo: query.batchNo || undefined,
|
||||
carrierId: query.carrierId || undefined,
|
||||
createUser: query.createUser || undefined,
|
||||
createTimeStart: query.createTimeRange?.[0] || undefined,
|
||||
createTimeEnd: query.createTimeRange?.[1] || undefined,
|
||||
current: page.current,
|
||||
size: page.size,
|
||||
});
|
||||
const loadListOptions = async () => {
|
||||
const res = await api.getImportOptions?.();
|
||||
const data = res?.data?.data || {};
|
||||
queryCarriers.value = data.carriers || [];
|
||||
creators.value = data.creators || [];
|
||||
};
|
||||
const loadBatches = async () => {
|
||||
const res = await api.getImportBatches({ ...query, current: page.current, size: page.size });
|
||||
const res = await api.getImportBatches(buildBatchQueryParams());
|
||||
batches.value = res.data?.data?.records || [];
|
||||
page.total = res.data?.data?.total || 0;
|
||||
};
|
||||
onMounted(() => {
|
||||
if (props.standalone) loadBatches();
|
||||
if (props.standalone) {
|
||||
loadListOptions();
|
||||
loadBatches();
|
||||
}
|
||||
if (props.createPage) openCreatePage();
|
||||
});
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
value => {
|
||||
if (value) loadBatches();
|
||||
if (value) {
|
||||
loadListOptions();
|
||||
loadBatches();
|
||||
}
|
||||
}
|
||||
);
|
||||
const resetQuery = () => {
|
||||
@@ -1106,7 +1138,7 @@ const loadDetails = async () => {
|
||||
current: detailPage.current,
|
||||
size: detailPage.size,
|
||||
});
|
||||
details.value = res.data?.data?.records || [];
|
||||
details.value = (res.data?.data?.records || []).map(normalizeDetailRow);
|
||||
detailPage.total = res.data?.data?.total || 0;
|
||||
};
|
||||
const openCreate = () => prepareCreateForm(true);
|
||||
@@ -1155,7 +1187,7 @@ const loadBatchRows = async batchId => {
|
||||
if (!batchId) return;
|
||||
const res = await api.getImportDetails({ batchId, current: 1, size: 9999 });
|
||||
rows.value = extractRecords(res).map(({ id, ...item }, index) => ({
|
||||
...item,
|
||||
...normalizeDetailRow(item),
|
||||
_key: `${id || 'row'}-${index}`,
|
||||
batchNo: item.batchNo || form.batchNo,
|
||||
}));
|
||||
@@ -1288,15 +1320,60 @@ const contractChange = id => {
|
||||
const carrierTypeChange = () => {
|
||||
syncCarrierOptions();
|
||||
};
|
||||
const resolveCarrierArchiveId = (carrierId, carrierName) => {
|
||||
if (carrierId) return carrierId;
|
||||
const name = String(carrierName || '').trim();
|
||||
if (!name) return '';
|
||||
const matched = queryCarriers.value.find(item => String(item.name || '').trim() === name);
|
||||
return matched?.id || '';
|
||||
};
|
||||
const carrierChange = value => {
|
||||
const item = carrierOptions.value.find(option => String(option.value) === String(value || ''));
|
||||
clearCarrier();
|
||||
if (!item) return;
|
||||
form.carrierId = item.id || '';
|
||||
form.carrierIds = item.id ? [item.id] : [];
|
||||
const carrierId = resolveCarrierArchiveId(item.id, item.carrierName);
|
||||
form.carrierId = carrierId;
|
||||
form.carrierIds = carrierId ? [carrierId] : [];
|
||||
form.carrierName = item.carrierName || '';
|
||||
if (!isSelfOperated.value) form.carrierContractId = item.carrierContractId || '';
|
||||
};
|
||||
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
|
||||
const OPTIONAL_NUMBER_EMPTY_SENTINELS = [-1];
|
||||
const OPTIONAL_NUMBER_PROPS = ['mileage', 'unitPrice', 'freight', 'otherFeeTotal', 'freightTotal'];
|
||||
const normalizeOptionalImportNumber = (value, emptySentinels = OPTIONAL_NUMBER_EMPTY_SENTINELS) => {
|
||||
if (!String(value ?? '').trim()) return null;
|
||||
return emptySentinels.some(sentinel => Number(value) === sentinel) ? null : value;
|
||||
};
|
||||
const formatOptionalNumber = value => {
|
||||
const normalized = normalizeOptionalImportNumber(value);
|
||||
return normalized === null || normalized === undefined ? '' : normalized;
|
||||
};
|
||||
const normalizeDetailRow = row => {
|
||||
const next = { ...(row || {}) };
|
||||
OPTIONAL_NUMBER_PROPS.forEach(prop => {
|
||||
next[prop] = formatOptionalNumber(next[prop]);
|
||||
});
|
||||
return next;
|
||||
};
|
||||
const normalizeImportRow = row => ({
|
||||
...row,
|
||||
mileage: normalizeOptionalImportNumber(
|
||||
firstNotEmpty(row.mileage, row['里程'], row['里程(km)'], row['里程(公里)'])
|
||||
),
|
||||
otherFeeTotal: normalizeOptionalImportNumber(
|
||||
firstNotEmpty(row.otherFeeTotal, row['其他费用合计'])
|
||||
),
|
||||
unitPrice: normalizeOptionalImportNumber(firstNotEmpty(row.unitPrice, row['单价'])),
|
||||
freight: normalizeOptionalImportNumber(firstNotEmpty(row.freight, row['运费'])),
|
||||
freightTotal: normalizeOptionalImportNumber(firstNotEmpty(row.freightTotal, row['运费合计'])),
|
||||
transportType: firstNotEmpty(
|
||||
row.transportType,
|
||||
row['运输类型'],
|
||||
row['*运输类型'],
|
||||
row['运输方式'],
|
||||
row['*运输方式']
|
||||
),
|
||||
});
|
||||
const fileChange = async (file, list) => {
|
||||
if (!projectInfoReady.value) {
|
||||
ElMessage.warning('请先选择项目、客户合同和承运信息,再导入运单明细');
|
||||
@@ -1370,6 +1447,9 @@ const fileChange = async (file, list) => {
|
||||
item[key] ??
|
||||
'';
|
||||
});
|
||||
['mileage', 'unitPrice', 'freight', 'otherFeeTotal', 'freightTotal'].forEach(prop => {
|
||||
row[prop] = normalizeOptionalImportNumber(row[prop]);
|
||||
});
|
||||
row.actualStartDate = normalizeImportDate(row.actualStartDate);
|
||||
row.actualEndDate = normalizeImportDate(row.actualEndDate);
|
||||
row.planStartDate = normalizeImportDate(row.planStartDate);
|
||||
@@ -1618,6 +1698,7 @@ const downloadTemplate = async () => {
|
||||
ElMessage.error('模板下载失败');
|
||||
}
|
||||
};
|
||||
const isValidMobile = value => /^\d{11}$/.test(String(value || '').trim());
|
||||
const validateImportRows = () => {
|
||||
if (!rows.value.length) {
|
||||
ElMessage.warning('请上传至少一条运单明细');
|
||||
@@ -1629,6 +1710,11 @@ const validateImportRows = () => {
|
||||
ElMessage.warning(`第${index + 1}行${missingField.label}不能为空`);
|
||||
return false;
|
||||
}
|
||||
const driverPhone = String(row.driverPhone ?? '').trim();
|
||||
if (driverPhone && !isValidMobile(driverPhone)) {
|
||||
ElMessage.warning(`第${index + 1}行司机/船长手机号格式不正确,应为11位数字`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -1656,31 +1742,6 @@ const removeSelectedRows = () => {
|
||||
rowSelection.value = [];
|
||||
refreshDuplicateFlags();
|
||||
};
|
||||
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
|
||||
const normalizeOptionalImportNumber = (value, emptySentinels = []) => {
|
||||
if (!String(value ?? '').trim()) return null;
|
||||
return emptySentinels.some(sentinel => Number(value) === sentinel) ? null : value;
|
||||
};
|
||||
const normalizeImportRow = row => ({
|
||||
...row,
|
||||
mileage: normalizeOptionalImportNumber(
|
||||
firstNotEmpty(row.mileage, row['里程'], row['里程(km)'], row['里程(公里)']),
|
||||
[-1]
|
||||
),
|
||||
otherFeeTotal: normalizeOptionalImportNumber(
|
||||
firstNotEmpty(row.otherFeeTotal, row['其他费用合计'])
|
||||
),
|
||||
unitPrice: normalizeOptionalImportNumber(firstNotEmpty(row.unitPrice, row['单价'])),
|
||||
freight: normalizeOptionalImportNumber(firstNotEmpty(row.freight, row['运费'])),
|
||||
freightTotal: normalizeOptionalImportNumber(firstNotEmpty(row.freightTotal, row['运费合计'])),
|
||||
transportType: firstNotEmpty(
|
||||
row.transportType,
|
||||
row['运输类型'],
|
||||
row['*运输类型'],
|
||||
row['运输方式'],
|
||||
row['*运输方式']
|
||||
),
|
||||
});
|
||||
const normalizeOptionalPlanId = value => {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
return Number(value) === -1 || Number(value) <= 0 ? null : value;
|
||||
@@ -1688,11 +1749,23 @@ const normalizeOptionalPlanId = value => {
|
||||
const buildImportPayload = (status = form.status, rowList = rows.value, extra = {}) => {
|
||||
const { file, ...batch } = form;
|
||||
const planId = normalizeOptionalPlanId(batch.planId);
|
||||
const carrierId = resolveCarrierArchiveId(
|
||||
batch.carrierId || batch.carrierIds?.[0] || '',
|
||||
batch.carrierName
|
||||
);
|
||||
const carrierIds =
|
||||
Array.isArray(batch.carrierIds) && batch.carrierIds.length
|
||||
? batch.carrierIds
|
||||
: carrierId
|
||||
? [carrierId]
|
||||
: [];
|
||||
return {
|
||||
...batch,
|
||||
planId,
|
||||
// 未选计划时不带占位名称,避免落库/回显 -1
|
||||
planName: planId ? batch.planName || undefined : null,
|
||||
carrierId: carrierId || undefined,
|
||||
carrierIds,
|
||||
status,
|
||||
rows: rowList.map(normalizeImportRow).map(stripInternalRowFields),
|
||||
...extra,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,7 @@
|
||||
<el-form-item label="项目">
|
||||
<el-input v-model="form.projectName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属组织">
|
||||
<el-form-item label="合同负责部门">
|
||||
<el-input v-model="form.organizationName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="甲方">
|
||||
@@ -63,6 +63,15 @@
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="form.applyDate"
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同期限">
|
||||
<div class="contract-basic-section__date-range">
|
||||
<el-date-picker
|
||||
@@ -91,12 +100,13 @@
|
||||
<template #suffix>天</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同金额">
|
||||
<el-form-item label="合同金额" prop="contractAmount">
|
||||
<el-input
|
||||
:model-value="contractAmountDisplay"
|
||||
placeholder="请输入"
|
||||
clearable
|
||||
@input="handleContractAmountInput"
|
||||
@blur="formatContractAmountField"
|
||||
@clear="form.contractAmount = null"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -154,7 +164,7 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否需要加盖法人章">
|
||||
<el-select v-model="form.legalSealFlag" placeholder="请选择">
|
||||
<el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
@@ -173,20 +183,30 @@
|
||||
<el-input v-model="form.handlerUserName" disabled />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="备注" class="contract-basic-section__remark">
|
||||
<el-form-item label="合同主要内容" class="contract-basic-section__remark">
|
||||
<el-input
|
||||
v-model="form.mainContent"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入合同主要内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="其他事项说明" class="contract-basic-section__remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入备注"
|
||||
placeholder="请输入其他事项说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
</section>
|
||||
|
||||
<contract-attachment-section
|
||||
title="合同文件"
|
||||
title="用印材料"
|
||||
description
|
||||
attachment-type
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
@@ -251,13 +271,20 @@
|
||||
<el-button type="primary" plain @click="addPaymentRatioRow">添加</el-button>
|
||||
</section>
|
||||
|
||||
<billing-plan-editor v-model="planDialogVisible" :value="planEditor" :index="planEditorIndex" @save="savePlan" />
|
||||
<billing-plan-editor
|
||||
v-model="planDialogVisible"
|
||||
:value="planEditor"
|
||||
:index="planEditorIndex"
|
||||
:settlement-currency="form.settlementCurrency"
|
||||
@save="savePlan"
|
||||
/>
|
||||
|
||||
<contract-attachment-section
|
||||
title="其它附件"
|
||||
title="附件(佐证材料等)"
|
||||
description
|
||||
attachment-type
|
||||
attachment-location="其它附件"
|
||||
attachment-location="附件(佐证材料等)"
|
||||
attachment-type-dict-code="contract_supporting_attachment_type"
|
||||
:attachment-location-options="changeAttachmentLocationOptions"
|
||||
:rows="attachments"
|
||||
:preview="previewAttachment"
|
||||
@@ -308,6 +335,8 @@ import * as api from '@/api/business/contract-manage';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||||
import ContractAttachmentSection, {
|
||||
isContractSealMaterialLocation,
|
||||
isContractSupportingAttachmentLocation,
|
||||
normalizeContractFileRows,
|
||||
} from './components/contract-attachment-section.vue';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
@@ -358,6 +387,19 @@ const normalizeOptionalAmount = value => {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : null;
|
||||
};
|
||||
const formatContractAmountInput = (value, emptyValue = null) => {
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
Number(value) === -1 ||
|
||||
String(value) === '-1'
|
||||
) {
|
||||
return emptyValue;
|
||||
}
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0 ? number.toFixed(2) : emptyValue;
|
||||
};
|
||||
|
||||
|
||||
const normalizeCustomPeriods = (periods = []) =>
|
||||
@@ -424,6 +466,21 @@ export default {
|
||||
],
|
||||
rules: {
|
||||
contractName: [{ required: true, message: '请输入合同名称', trigger: 'blur' }],
|
||||
contractAmount: [
|
||||
{ required: true, message: '请输入合同金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return callback(new Error('请输入合同金额'));
|
||||
}
|
||||
if (Number(value) < 0) {
|
||||
return callback(new Error('合同金额不能小于0'));
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
changeReason: [{ max: 500, message: '变更原因不能超过500个字符', trigger: 'blur' }],
|
||||
},
|
||||
};
|
||||
@@ -433,7 +490,7 @@ export default {
|
||||
return this.$route.query.name || '合同变更';
|
||||
},
|
||||
changeAttachmentLocationOptions() {
|
||||
return ['合同文件', '其它附件', '变更材料'];
|
||||
return ['用印材料', '附件(佐证材料等)', '变更材料'];
|
||||
},
|
||||
contractAmountDisplay() {
|
||||
const value = this.form.contractAmount;
|
||||
@@ -498,6 +555,9 @@ export default {
|
||||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
|
||||
this.form.contractAmount = normalized === '' ? null : normalized;
|
||||
},
|
||||
formatContractAmountField() {
|
||||
this.form.contractAmount = formatContractAmountInput(this.form.contractAmount);
|
||||
},
|
||||
loadDictionaries() {
|
||||
Promise.all([
|
||||
getSystemDictionary({ code: 'currency_type' }),
|
||||
@@ -540,7 +600,7 @@ export default {
|
||||
copyCount: normalizeOptionalPositiveInteger(data.copyCount),
|
||||
invoiceCycle: normalizeOptionalPositiveInteger(data.invoiceCycle),
|
||||
paymentDays: normalizeOptionalPositiveInteger(data.paymentDays),
|
||||
contractAmount: normalizeOptionalAmount(data.contractAmount),
|
||||
contractAmount: formatContractAmountInput(data.contractAmount),
|
||||
templateFlag: data.templateFlag ?? 0,
|
||||
electronicSealFlag: data.electronicSealFlag ?? 0,
|
||||
archiveStatus: data.archiveStatus || '未归档',
|
||||
@@ -601,7 +661,7 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
// 审核通过后上一次的变更材料归集到「其它附件」,按文件地址/文件名去重,避免重复展示
|
||||
// 审核通过后上一次的变更材料归集到「附件(佐证材料等)」,按文件地址/文件名去重,避免重复展示
|
||||
mergeChangeAttachments(attachments = [], changeAttachments = []) {
|
||||
const key = item => this.attachmentUrl(item) || this.attachmentName(item);
|
||||
const exists = new Set(attachments.map(key));
|
||||
@@ -655,11 +715,11 @@ export default {
|
||||
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
|
||||
handleAttachmentUploadToLocation({ location, rows = [] }) {
|
||||
if (!rows.length) return;
|
||||
if (location === '合同文件') {
|
||||
if (isContractSealMaterialLocation(location)) {
|
||||
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
|
||||
return;
|
||||
}
|
||||
if (location === '其它附件') {
|
||||
if (isContractSupportingAttachmentLocation(location)) {
|
||||
this.attachments = [...(this.attachments || []), ...rows];
|
||||
return;
|
||||
}
|
||||
@@ -692,11 +752,18 @@ export default {
|
||||
.contract-basic-section > .dialog-section-title { margin-bottom: 20px; color: #303133; }
|
||||
.contract-basic-section__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 48px; }
|
||||
.contract-basic-section__change-type { grid-column: 1 / -1; }
|
||||
.contract-basic-section__remark { width: 100%; margin-bottom: 0 !important; }
|
||||
.contract-basic-section__remark {
|
||||
width: 100%;
|
||||
margin-bottom: 23px !important;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
.contract-basic-section__date-range { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 8px; width: 360px; max-width: 100%; }
|
||||
.contract-basic-section__date-range span { color: #606266; }
|
||||
.contract-basic-section__date-range :deep(.el-date-editor) { width: auto; min-width: 0; }
|
||||
.contract-basic-section :deep(.el-form-item) { align-items: center; margin-bottom: 16px; }
|
||||
.contract-basic-section :deep(.el-form-item) { align-items: center; }
|
||||
.contract-basic-section :deep(.el-form-item__label) { flex: 0 0 96px !important; width: 96px !important; height: auto; padding-right: 12px; white-space: normal; word-break: break-all; line-height: 18px; }
|
||||
.contract-basic-section :deep(.el-form-item__content) { min-width: 0; }
|
||||
.contract-basic-section :deep(.el-input),
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
<el-descriptions-item label="甲方">{{ detailValue('partyA') }}</el-descriptions-item>
|
||||
<el-descriptions-item label="乙方">{{ detailValue('partyB') }}</el-descriptions-item>
|
||||
<el-descriptions-item label="合同期限">{{ detailContractPeriod }}</el-descriptions-item>
|
||||
<el-descriptions-item label="所属组织">{{
|
||||
<el-descriptions-item label="合同负责部门">{{
|
||||
detailValue('organizationName')
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="经办人">{{
|
||||
@@ -150,6 +150,9 @@
|
||||
<el-descriptions-item label="签订日期">{{
|
||||
detailValue('signDate')
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="申请日期">{{
|
||||
detailValue('applyDate')
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="合同格式">{{
|
||||
detailValue('contractFormat')
|
||||
}}</el-descriptions-item>
|
||||
@@ -181,7 +184,10 @@
|
||||
<el-descriptions-item label="是否电子章">
|
||||
{{ detailElectronicSealText }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="3">{{
|
||||
<el-descriptions-item label="合同主要内容" :span="3">{{
|
||||
detailValue('mainContent')
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="其他事项说明" :span="3">{{
|
||||
detailValue('remark')
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="detailRiskTipText" label="风险客商提示" :span="3">
|
||||
@@ -197,7 +203,7 @@
|
||||
/>
|
||||
|
||||
<contract-attachment-section
|
||||
title="合同文件"
|
||||
title="用印材料"
|
||||
description
|
||||
attachment-type
|
||||
readonly
|
||||
@@ -313,7 +319,7 @@
|
||||
</section>
|
||||
|
||||
<contract-attachment-section
|
||||
title="其它附件"
|
||||
title="附件(佐证材料等)"
|
||||
description
|
||||
readonly
|
||||
:rows="detailAttachmentRows"
|
||||
@@ -396,8 +402,14 @@
|
||||
<el-form-item label="合同编号">
|
||||
<el-input
|
||||
v-model="form.contractNo"
|
||||
:disabled="formMode === 'edit'"
|
||||
placeholder="不填写,则系统自动生成"
|
||||
disabled
|
||||
:placeholder="
|
||||
formMode === 'add'
|
||||
? form.organizationId
|
||||
? '系统按部门规则自动生成'
|
||||
: '请先选择合同负责部门'
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同名称" prop="contractName">
|
||||
@@ -436,7 +448,7 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属组织" prop="organizationName">
|
||||
<el-form-item label="合同负责部门" prop="organizationName">
|
||||
<el-cascader
|
||||
v-model="organizationCascaderValue"
|
||||
:options="organizationOptions"
|
||||
@@ -492,6 +504,16 @@
|
||||
placeholder="请选择签订日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="form.applyDate"
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="提交正式合同时自动填充"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同期限" prop="startDate">
|
||||
<div class="contract-manage-form__date-range">
|
||||
<el-date-picker
|
||||
@@ -518,12 +540,13 @@
|
||||
@input="value => integerInput('paymentDays', value)"
|
||||
><template #suffix>天</template></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同金额">
|
||||
<el-form-item label="合同金额" prop="contractAmount">
|
||||
<el-input
|
||||
:model-value="contractAmountDisplay"
|
||||
placeholder="请输入"
|
||||
clearable
|
||||
@input="handleContractAmountInput"
|
||||
@blur="formatContractAmountField"
|
||||
@clear="form.contractAmount = null"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -578,7 +601,7 @@
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否需要加盖法人章" prop="legalSealFlag">
|
||||
<el-select v-model="form.legalSealFlag" placeholder="请选择">
|
||||
<el-select v-model="form.legalSealFlag" placeholder="请选择" clearable>
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
@@ -597,14 +620,24 @@
|
||||
<el-input v-model="form.handlerUserName" disabled />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="备注" prop="remark" class="contract-manage-form__remark">
|
||||
<el-form-item label="合同主要内容" prop="mainContent" class="contract-manage-form__remark">
|
||||
<el-input
|
||||
v-model="form.mainContent"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入合同主要内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="其他事项说明" prop="remark" class="contract-manage-form__remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="200"
|
||||
:rows="4"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
placeholder="请输入备注"
|
||||
placeholder="请输入其他事项说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 风险客商提示(基本信息备注后):外部客商高风险/黑名单时展示,文案由后端 riskTip 拼装 -->
|
||||
@@ -615,7 +648,7 @@
|
||||
</section>
|
||||
|
||||
<contract-attachment-section
|
||||
title="合同文件"
|
||||
title="用印材料"
|
||||
description
|
||||
attachment-type
|
||||
:attachment-location-options="contractAttachmentLocationOptions"
|
||||
@@ -835,10 +868,11 @@
|
||||
</section>
|
||||
|
||||
<contract-attachment-section
|
||||
title="其它附件"
|
||||
title="附件(佐证材料等)"
|
||||
description
|
||||
attachment-type
|
||||
attachment-location="其它附件"
|
||||
attachment-location="附件(佐证材料等)"
|
||||
attachment-type-dict-code="contract_supporting_attachment_type"
|
||||
:attachment-location-options="contractAttachmentLocationOptions"
|
||||
:readonly="isAttachmentUploadMode"
|
||||
:rows="attachmentRows"
|
||||
@@ -946,18 +980,19 @@
|
||||
v-model="billingPlanBox"
|
||||
:value="billingPlanForm"
|
||||
:index="billingPlanIndex"
|
||||
:settlement-currency="form.settlementCurrency"
|
||||
@save="saveBillingPlan"
|
||||
/>
|
||||
|
||||
<contract-attachment-section
|
||||
ref="listAttachmentUploader"
|
||||
dialog-only
|
||||
title="合同文件"
|
||||
title="用印材料"
|
||||
description
|
||||
attachment-type
|
||||
use-upload-dialog
|
||||
mark-approved-on-upload
|
||||
:attachment-location-options="['合同文件', '其它附件']"
|
||||
:attachment-location-options="['用印材料', '附件(佐证材料等)']"
|
||||
:rows="listUploadContractFileRows"
|
||||
@update:rows="listUploadContractFileRows = $event"
|
||||
@upload-to-location="handleListAttachmentUploadToLocation"
|
||||
@@ -1014,6 +1049,7 @@
|
||||
v-model="detailBillingPlanBox"
|
||||
:value="detailBillingPlanForm"
|
||||
:index="detailBillingPlanIndex"
|
||||
:settlement-currency="detailRow.settlementCurrency || form.settlementCurrency"
|
||||
readonly
|
||||
/>
|
||||
|
||||
@@ -1046,12 +1082,15 @@ import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||
import { config, option } from '@/option/business/contract-manage';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||||
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
|
||||
import ContractAttachmentSection, {
|
||||
attachmentName as sharedAttachmentName,
|
||||
attachmentUrl as sharedAttachmentUrl,
|
||||
isContractSealMaterialLocation,
|
||||
isContractSupportingAttachmentLocation,
|
||||
normalizeContractFileRows,
|
||||
} from './components/contract-attachment-section.vue';
|
||||
import HcsContractPanel from './components/hcs-contract-panel.vue';
|
||||
@@ -1088,6 +1127,19 @@ const normalizeOptionalAmount = (value, emptyValue = null) => {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : emptyValue;
|
||||
};
|
||||
const formatContractAmountInput = (value, emptyValue = null) => {
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
Number(value) === -1 ||
|
||||
String(value) === '-1'
|
||||
) {
|
||||
return emptyValue;
|
||||
}
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0 ? number.toFixed(2) : emptyValue;
|
||||
};
|
||||
const normalizeProjectValue = value => {
|
||||
if (value === undefined || value === null || value === '' || String(value) === '-1') return '';
|
||||
return value;
|
||||
@@ -1120,8 +1172,9 @@ const defaultForm = () => ({
|
||||
partyB: '',
|
||||
handlerUserName: '',
|
||||
signDate: '',
|
||||
applyDate: '',
|
||||
contractFormat: '',
|
||||
legalSealFlag: 0,
|
||||
legalSealFlag: null,
|
||||
copyCount: '',
|
||||
settlementCurrency: '',
|
||||
invoiceCycle: '',
|
||||
@@ -1137,6 +1190,7 @@ const defaultForm = () => ({
|
||||
effectiveType: '',
|
||||
feeGenerationMode: 'system',
|
||||
archiveStatus: '未归档',
|
||||
mainContent: '',
|
||||
remark: '',
|
||||
});
|
||||
// 复制新增:仅保留可编辑字段与明细,剔除主键、编号、审批与流程信息
|
||||
@@ -1148,6 +1202,7 @@ const createCopiedDetail = (detail = {}) => {
|
||||
return {
|
||||
...form,
|
||||
contractNo: '',
|
||||
applyDate: '',
|
||||
archiveStatus: '未归档',
|
||||
feeGenerationMode: detail.feeGenerationMode || '',
|
||||
billingEnabled: detail.billingEnabled,
|
||||
@@ -1243,13 +1298,12 @@ export default {
|
||||
signType: [{ required: true, message: '请选择签约类型', trigger: 'change' }],
|
||||
settlementCurrency: [{ required: true, message: '请选择结算币种', trigger: 'change' }],
|
||||
projectName: [{ required: true, message: '请选择所属项目', trigger: 'change' }],
|
||||
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
|
||||
organizationName: [{ required: true, message: '请选择合同负责部门', trigger: 'change' }],
|
||||
partyA: [{ required: true, message: '请选择甲方', trigger: 'change' }],
|
||||
partyB: [{ required: true, message: '请选择乙方', trigger: 'change' }],
|
||||
signDate: [{ required: true, message: '请选择签订日期', trigger: 'change' }],
|
||||
settlementMode: [{ required: true, message: '请选择结算方式', trigger: 'change' }],
|
||||
contractFormat: [{ required: true, message: '请选择合同格式', trigger: 'change' }],
|
||||
legalSealFlag: [{ required: true, message: '请选择是否需要加盖法人章', trigger: 'change' }],
|
||||
invoiceCycle: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
@@ -1264,6 +1318,22 @@ export default {
|
||||
],
|
||||
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }],
|
||||
endDate: [{ required: true, message: '请选择结束日期', trigger: 'change' }],
|
||||
contractAmount: [
|
||||
{ required: true, message: '请输入合同金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return callback(new Error('请输入合同金额'));
|
||||
}
|
||||
if (Number(value) < 0) {
|
||||
return callback(new Error('合同金额不能小于0'));
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
mainContent: [{ max: 1000, message: '合同主要内容不能超过1000个字符', trigger: 'blur' }],
|
||||
},
|
||||
submitAction: '',
|
||||
projectOptions: [],
|
||||
@@ -1335,6 +1405,7 @@ export default {
|
||||
flowBox: false,
|
||||
flowUrl: '',
|
||||
processInstanceId: '',
|
||||
mkEnabled: false,
|
||||
// 页面形态在实例创建时锁定,避免 keep-alive 缓存后随全局 $route 翻转
|
||||
formPageLocked: false,
|
||||
detailPageLocked: false,
|
||||
@@ -1377,8 +1448,8 @@ export default {
|
||||
return this.$route.query.attachmentUpload === '1';
|
||||
},
|
||||
contractAttachmentLocationOptions() {
|
||||
if (this.isAttachmentUploadMode) return ['合同文件'];
|
||||
return ['合同文件', '其它附件'];
|
||||
if (this.isAttachmentUploadMode) return ['用印材料'];
|
||||
return ['用印材料', '附件(佐证材料等)'];
|
||||
},
|
||||
contractAmountDisplay() {
|
||||
const value = this.form.contractAmount;
|
||||
@@ -1445,7 +1516,7 @@ export default {
|
||||
expandTrigger: 'click',
|
||||
};
|
||||
},
|
||||
// 所属组织级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径,
|
||||
// 合同负责部门级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径,
|
||||
// 两者在此双向转换;依赖 organizationOptions,树加载完成后回显自动刷新
|
||||
organizationCascaderValue: {
|
||||
get() {
|
||||
@@ -1580,6 +1651,13 @@ export default {
|
||||
this.loadProjectOptions();
|
||||
this.loadSettlementDictionaries();
|
||||
this.loadOrganizationOptions();
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = !!enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
}
|
||||
if (this.isFormPage) this.initFormPage();
|
||||
if (this.isDetailPage) this.initDetailPage();
|
||||
@@ -1828,7 +1906,7 @@ export default {
|
||||
copyCount: normalizeOptionalInteger(detail.copyCount, ''),
|
||||
invoiceCycle: normalizeOptionalInteger(detail.invoiceCycle, ''),
|
||||
paymentDays: normalizeOptionalInteger(detail.paymentDays, ''),
|
||||
contractAmount: normalizeOptionalAmount(detail.contractAmount, null),
|
||||
contractAmount: formatContractAmountInput(detail.contractAmount, null),
|
||||
archiveStatus: detail.archiveStatus || '未归档',
|
||||
feeGenerationMode:
|
||||
detail.feeGenerationMode || (Number(detail.billingEnabled) === 0 ? 'manual' : 'system'),
|
||||
@@ -1864,6 +1942,7 @@ export default {
|
||||
this.applyDetail(createCopiedDetail(detail));
|
||||
this.form.handlerUserName =
|
||||
this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || '';
|
||||
this.refreshPreviewContractNo();
|
||||
this.$nextTick(() => this.$refs.contractForm?.clearValidate());
|
||||
},
|
||||
closeForm() {
|
||||
@@ -1878,9 +1957,10 @@ export default {
|
||||
['contractName', '合同名称'],
|
||||
['contractCategory', '合同类型'],
|
||||
['projectName', '所属项目'],
|
||||
['organizationName', '所属组织'],
|
||||
['organizationName', '合同负责部门'],
|
||||
['partyA', '甲方'],
|
||||
['partyB', '乙方'],
|
||||
['contractAmount', '合同金额'],
|
||||
];
|
||||
const empty = required.find(([prop]) => {
|
||||
const value = this.form[prop];
|
||||
@@ -1888,10 +1968,16 @@ export default {
|
||||
});
|
||||
if (empty) {
|
||||
this.$message.warning(
|
||||
empty[0] === 'contractName' ? `请输入${empty[1]}` : `请选择${empty[1]}`
|
||||
['contractName', 'contractAmount'].includes(empty[0])
|
||||
? `请输入${empty[1]}`
|
||||
: `请选择${empty[1]}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (Number(this.form.contractAmount) < 0) {
|
||||
this.$message.warning('合同金额不能小于0');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!this.form.contractName?.trim()) {
|
||||
@@ -1914,7 +2000,7 @@ export default {
|
||||
if (action !== 'formal' && action !== 'edit') return true;
|
||||
const required = [
|
||||
['projectName', '所属项目'],
|
||||
['organizationName', '所属组织'],
|
||||
['organizationName', '合同负责部门'],
|
||||
['partyA', '甲方'],
|
||||
['partyB', '乙方'],
|
||||
['startDate', '合同开始日期'],
|
||||
@@ -1922,10 +2008,20 @@ export default {
|
||||
['signDate', '签订日期'],
|
||||
['settlementMode', '结算方式'],
|
||||
['contractFormat', '合同格式'],
|
||||
['contractAmount', '合同金额'],
|
||||
];
|
||||
const empty = required.find(([prop]) => !this.form[prop]);
|
||||
const empty = required.find(([prop]) => {
|
||||
const value = this.form[prop];
|
||||
return value === undefined || value === null || String(value).trim() === '';
|
||||
});
|
||||
if (empty) {
|
||||
this.$message.warning(`请选择或输入${empty[1]}`);
|
||||
this.$message.warning(
|
||||
empty[0] === 'contractAmount' ? `请输入${empty[1]}` : `请选择或输入${empty[1]}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (Number(this.form.contractAmount) < 0) {
|
||||
this.$message.warning('合同金额不能小于0');
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
@@ -2115,11 +2211,11 @@ export default {
|
||||
},
|
||||
handleListAttachmentUploadToLocation({ location, rows = [] }) {
|
||||
if (!rows.length) return;
|
||||
if (location === '合同文件') {
|
||||
if (isContractSealMaterialLocation(location)) {
|
||||
this.listUploadContractFileRows = [...(this.listUploadContractFileRows || []), ...rows];
|
||||
return;
|
||||
}
|
||||
if (location === '其它附件') {
|
||||
if (isContractSupportingAttachmentLocation(location)) {
|
||||
this.listUploadAttachmentRows = [...(this.listUploadAttachmentRows || []), ...rows];
|
||||
}
|
||||
},
|
||||
@@ -2277,6 +2373,9 @@ export default {
|
||||
column.renderSearch = scope => this.renderOrganizationSearch(scope);
|
||||
}
|
||||
this.syncCurrentOrganizationOption();
|
||||
if (this.isFormPage && this.formMode === 'add' && this.form.organizationId) {
|
||||
this.refreshPreviewContractNo();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.organizationLoading = false;
|
||||
@@ -2339,6 +2438,29 @@ export default {
|
||||
}
|
||||
: { organizationId: '', organizationName: '' }
|
||||
);
|
||||
if (this.formMode === 'add') {
|
||||
this.refreshPreviewContractNo();
|
||||
}
|
||||
},
|
||||
refreshPreviewContractNo() {
|
||||
if (this.formMode !== 'add') return;
|
||||
const organizationId = this.form.organizationId;
|
||||
if (!organizationId) {
|
||||
this.form.contractNo = '';
|
||||
return;
|
||||
}
|
||||
const requestId = (this.contractNoPreviewRequestId =
|
||||
(this.contractNoPreviewRequestId || 0) + 1);
|
||||
api
|
||||
.nextContractNo(organizationId)
|
||||
.then(res => {
|
||||
if (requestId !== this.contractNoPreviewRequestId) return;
|
||||
this.form.contractNo = res.data?.data || '';
|
||||
})
|
||||
.catch(() => {
|
||||
if (requestId !== this.contractNoPreviewRequestId) return;
|
||||
this.form.contractNo = '';
|
||||
});
|
||||
},
|
||||
syncCurrentOrganizationOption() {
|
||||
if (!this.form.organizationId || !this.form.organizationName) return;
|
||||
@@ -2352,7 +2474,7 @@ export default {
|
||||
this.organizationFlatOptions.unshift(item);
|
||||
},
|
||||
renderOrganizationSearch(scope) {
|
||||
// 搜索区所属组织:单选级联(与新增/编辑表单一致)
|
||||
// 搜索区合同负责部门:单选级联(与新增/编辑表单一致)
|
||||
// 级联返回 id 路径数组,提交时由 normalizeSearch 取末级 id 转组织名称
|
||||
return h(ElCascader, {
|
||||
modelValue: Array.isArray(scope.row?.organizationName) ? scope.row.organizationName : [],
|
||||
@@ -2371,7 +2493,7 @@ export default {
|
||||
filterable: true,
|
||||
clearable: true,
|
||||
style: 'width: 100%',
|
||||
placeholder: '请选择 所属组织',
|
||||
placeholder: '请选择 合同负责部门',
|
||||
});
|
||||
},
|
||||
integerInput(prop, value) {
|
||||
@@ -2384,6 +2506,9 @@ export default {
|
||||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
|
||||
this.form.contractAmount = normalized === '' ? null : normalized;
|
||||
},
|
||||
formatContractAmountField() {
|
||||
this.form.contractAmount = formatContractAmountInput(this.form.contractAmount, null);
|
||||
},
|
||||
positiveIntegerInput(prop, value) {
|
||||
const normalized = String(value || '').replace(/\D/g, '').replace(/^0+/, '');
|
||||
this.form[prop] = normalized;
|
||||
@@ -2416,11 +2541,11 @@ export default {
|
||||
},
|
||||
handleAttachmentUploadToLocation({ location, rows = [] }) {
|
||||
if (!rows.length) return;
|
||||
if (location === '合同文件') {
|
||||
if (isContractSealMaterialLocation(location)) {
|
||||
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
|
||||
return;
|
||||
}
|
||||
if (location === '其它附件') {
|
||||
if (isContractSupportingAttachmentLocation(location)) {
|
||||
this.attachmentRows = [...(this.attachmentRows || []), ...rows];
|
||||
}
|
||||
},
|
||||
@@ -2781,15 +2906,16 @@ export default {
|
||||
templateFlag: '是否范本',
|
||||
originalContractNo: '原件合同编号',
|
||||
electronicSealFlag: '是否电子章',
|
||||
remark: '备注',
|
||||
mainContent: '合同主要内容',
|
||||
remark: '其他事项说明',
|
||||
feeGenerationMode: '费用生成模式',
|
||||
billingPlanJson: '计费方案',
|
||||
settlementRuleJson: '结算单规则',
|
||||
preSettlementConfigJson: '预结算配置',
|
||||
formalSettlementConfigJson: '正式结算配置',
|
||||
paymentRatioJson: '付款比例设置',
|
||||
contractFileJson: '合同文件',
|
||||
attachmentsJson: '其它附件',
|
||||
contractFileJson: '用印材料',
|
||||
attachmentsJson: '附件(佐证材料等)',
|
||||
changeAttachmentsJson: '变更材料',
|
||||
};
|
||||
return labels[field] || field;
|
||||
@@ -2956,13 +3082,8 @@ export default {
|
||||
detailValue(prop) {
|
||||
const value = this.detailRow[prop];
|
||||
if (prop === 'contractAmount') {
|
||||
return value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
Number(value) === -1 ||
|
||||
String(value) === '-1'
|
||||
? '-'
|
||||
: value;
|
||||
const amountText = formatContractAmountInput(value, '');
|
||||
return amountText === '' ? '-' : amountText;
|
||||
}
|
||||
if (
|
||||
prop === 'contractStage' ||
|
||||
@@ -2994,6 +3115,10 @@ export default {
|
||||
return;
|
||||
}
|
||||
if (operation.action === 'flow') {
|
||||
if (this.mkEnabled) {
|
||||
this.handleMkAudit(row);
|
||||
return;
|
||||
}
|
||||
this.openFlow(row);
|
||||
return;
|
||||
}
|
||||
@@ -3047,6 +3172,24 @@ export default {
|
||||
query: { mode: 'add', copyId: row.id, name: '新增合同管理' },
|
||||
});
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('合同ID为空,无法打开审核页');
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
.then(res => {
|
||||
const url = res?.data?.data;
|
||||
if (!url) {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
});
|
||||
},
|
||||
openFlow(row, loaded = false) {
|
||||
const processInstanceId = row.processInstanceId || row.processInstanceID || row.procInstId;
|
||||
if (!processInstanceId && !loaded && row.id) {
|
||||
@@ -3276,7 +3419,11 @@ export default {
|
||||
|
||||
&__remark {
|
||||
width: 100%;
|
||||
margin-bottom: 0 !important;
|
||||
margin-bottom: 24px !important;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__date-range {
|
||||
@@ -3350,7 +3497,7 @@ export default {
|
||||
|
||||
:deep(.el-form-item) {
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="loading-manage-page">
|
||||
<div v-if="!isStandalonePage" class="loading-manage-page__search">
|
||||
<div v-if="!isStandalonePage && searchVisible" class="loading-manage-page__search">
|
||||
<el-form :model="searchForm" label-position="right" label-width="88px">
|
||||
<div class="loading-manage-page__search-grid">
|
||||
<el-form-item label="配载单号">
|
||||
@@ -228,17 +228,13 @@
|
||||
<el-button type="primary" plain @click="handleExport">批量导出</el-button>
|
||||
<el-button type="primary" plain @click="handleBatchComplete">批量完成</el-button>
|
||||
</div>
|
||||
<div class="loading-manage-page__toolbar-right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button :icon="Refresh" text @click="loadTable" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="表格密度" placement="top">
|
||||
<el-button :icon="Rank" text @click="toggleTableSize" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="列设置" placement="top">
|
||||
<el-button :icon="Setting" text @click="handleColumnSetting" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<table-header-tools
|
||||
:columns="columnSettingOptions"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
v-model:search-visible="searchVisible"
|
||||
@refresh="loadTable"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
@@ -254,7 +250,12 @@
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||
<el-table-column type="index" label="序号" width="58" fixed="left" align="center" />
|
||||
<el-table-column label="配载单号" prop="loadingNo" min-width="170">
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('loadingNo')"
|
||||
label="配载单号"
|
||||
prop="loadingNo"
|
||||
min-width="170"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
type="primary"
|
||||
@@ -264,7 +265,12 @@
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="运单号" prop="loadingSubNos" min-width="220">
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('loadingSubNos')"
|
||||
label="运单号"
|
||||
prop="loadingSubNos"
|
||||
min-width="220"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="loading-manage-page__sub-nos">
|
||||
<template v-if="splitText(row.loadingSubNos).length">
|
||||
@@ -283,11 +289,40 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="车牌号" prop="vehicleNo" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column label="司机" prop="driverName" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="联系电话" prop="driverPhone" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="发货地" prop="departureAddress" min-width="180">
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('vehicleNo')"
|
||||
label="车牌号"
|
||||
prop="vehicleNo"
|
||||
min-width="170"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('driverName')"
|
||||
label="司机"
|
||||
prop="driverName"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('driverPhone')"
|
||||
label="联系电话"
|
||||
prop="driverPhone"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('carrierType')"
|
||||
label="承运类型"
|
||||
prop="carrierType"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('departureAddress')"
|
||||
label="发货地"
|
||||
prop="departureAddress"
|
||||
min-width="180"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
v-if="formatFullRouteAddress(row, 'departure')"
|
||||
@@ -304,7 +339,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="途经地" prop="transitAddress" min-width="190">
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('transitAddress')"
|
||||
label="途经地"
|
||||
prop="transitAddress"
|
||||
min-width="190"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
v-if="formatFullRouteAddress(row, 'transit')"
|
||||
@@ -321,7 +361,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="到货地" prop="arrivalAddress" min-width="180">
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('arrivalAddress')"
|
||||
label="到货地"
|
||||
prop="arrivalAddress"
|
||||
min-width="180"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tooltip
|
||||
v-if="formatFullRouteAddress(row, 'arrival')"
|
||||
@@ -338,20 +383,40 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="运输方式" prop="transportType" min-width="150" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('carrierName')"
|
||||
label="承运商"
|
||||
prop="carrierName"
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('transportType')"
|
||||
label="运输方式"
|
||||
prop="transportType"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ transportTypeLabel(row.transportType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数据来源" prop="dataSource" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('dataSource')"
|
||||
label="数据来源"
|
||||
prop="dataSource"
|
||||
min-width="130"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('createUserName')"
|
||||
label="创建人"
|
||||
prop="createUserName"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('createTime')"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
min-width="170"
|
||||
@@ -359,6 +424,7 @@
|
||||
sortable
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('updateTime')"
|
||||
label="更新时间"
|
||||
prop="updateTime"
|
||||
min-width="170"
|
||||
@@ -366,12 +432,20 @@
|
||||
sortable
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('currentProcessNode')"
|
||||
label="过程节点"
|
||||
prop="currentProcessNode"
|
||||
min-width="130"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="状态" prop="businessStatus" width="110" fixed="right" align="center">
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('businessStatus')"
|
||||
label="状态"
|
||||
prop="businessStatus"
|
||||
width="110"
|
||||
fixed="right"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span :class="['loading-manage-page__status', `is-${row.businessStatus || 'unknown'}`]">
|
||||
{{ statusText(row) }}
|
||||
@@ -484,12 +558,16 @@
|
||||
<div class="loading-detail__field loading-detail__attachment-field">
|
||||
<span class="loading-detail__label">附件</span>
|
||||
<span class="loading-detail__value"
|
||||
><el-link
|
||||
><span
|
||||
v-for="file in detailAttachments"
|
||||
:key="file.url || file.name"
|
||||
type="primary"
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(file)"
|
||||
>{{ file.name }}</el-link
|
||||
@keydown.enter="previewAttachment(file)"
|
||||
@keydown.space.prevent="previewAttachment(file)"
|
||||
>{{ file.name }}</span
|
||||
><span v-if="!detailAttachments.length">-</span></span
|
||||
>
|
||||
</div>
|
||||
@@ -555,15 +633,12 @@
|
||||
prop="packageType"
|
||||
label="包装"
|
||||
min-width="100" /><el-table-column
|
||||
prop="weight"
|
||||
label="重量(吨)"
|
||||
min-width="110" /><el-table-column
|
||||
prop="volume"
|
||||
label="体积(方)"
|
||||
min-width="110" /><el-table-column
|
||||
prop="quantity"
|
||||
label="数量"
|
||||
min-width="90" /><el-table-column
|
||||
prop="quantityUnit"
|
||||
label="数量单位"
|
||||
min-width="100" /><el-table-column
|
||||
prop="materialCode"
|
||||
label="物料编码"
|
||||
min-width="120" /><el-table-column
|
||||
@@ -665,13 +740,6 @@
|
||||
record.statusName || (record.punched ? '已打卡' : '未打卡')
|
||||
}}</el-tag
|
||||
>
|
||||
<el-tag
|
||||
v-if="record.type === 'enroute'"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
>在途</el-tag
|
||||
>
|
||||
<el-tag
|
||||
v-if="record.exceptionFlag"
|
||||
size="small"
|
||||
@@ -701,22 +769,12 @@
|
||||
>
|
||||
备注:{{ record.remark }}
|
||||
</div>
|
||||
<div
|
||||
<punch-record-photos
|
||||
v-if="record.photos && record.photos.length"
|
||||
class="loading-detail__punch-record-photos"
|
||||
>
|
||||
<div
|
||||
v-for="(photo, pi) in record.photos"
|
||||
:key="`${record.nodeCode || index}-${pi}`"
|
||||
class="loading-detail__driver-upload-item"
|
||||
@click="previewDriverUploadPhoto(record.photos, pi)"
|
||||
>
|
||||
<el-image :src="photo.url" fit="cover" lazy />
|
||||
<div class="loading-detail__driver-upload-label">
|
||||
{{ photo.label || `${record.nodeName}-凭证` }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:photos="record.photos"
|
||||
lazy
|
||||
@preview="previewDriverUploadPhoto(record.photos, $event)"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="loading-detail__punch-record-line">暂未打卡</div>
|
||||
</div>
|
||||
@@ -1066,6 +1124,7 @@
|
||||
label="运单号"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.waybillNo" type="primary" @click="openWaybillDetail(row)">
|
||||
@@ -1357,7 +1416,7 @@
|
||||
<el-table-column prop="cargoType" label="货物类型" min-width="140" />
|
||||
<el-table-column prop="packageType" label="包装" min-width="100" />
|
||||
<el-table-column prop="quantity" label="数量" min-width="100" />
|
||||
<el-table-column prop="quantityUnit" label="单位" min-width="100" />
|
||||
<el-table-column prop="quantityUnit" label="数量单位" min-width="100" />
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||||
<el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
|
||||
<el-table-column prop="brand" label="品牌" min-width="120" />
|
||||
@@ -1400,7 +1459,11 @@
|
||||
<el-option
|
||||
v-for="item in taskCarrierOptions"
|
||||
:key="item.id || item.carrierName"
|
||||
:label="item.carrierName || item.partyB"
|
||||
:label="
|
||||
dialogForm.carrierType === '承运商'
|
||||
? formatTaskCarrierContractLabel(item)
|
||||
: item.carrierName || item.partyB
|
||||
"
|
||||
:value="
|
||||
dialogForm.carrierType === '承运商'
|
||||
? item.id
|
||||
@@ -1620,10 +1683,55 @@
|
||||
><el-table-column prop="changeTime" label="变更时间" min-width="170" /><el-table-column
|
||||
prop="changeUser"
|
||||
label="变更人"
|
||||
min-width="120" /><el-table-column prop="content" label="变更内容" min-width="360"
|
||||
min-width="120" /><el-table-column
|
||||
prop="content"
|
||||
label="变更内容"
|
||||
min-width="360"
|
||||
:show-overflow-tooltip="false"
|
||||
><template #default="{ row }"
|
||||
><div class="loading-route-change-dialog__record-content">
|
||||
{{ row.content || '-' }}
|
||||
<div
|
||||
v-for="(group, groupIndex) in parseRouteChangeContent(row.content)"
|
||||
:key="groupIndex"
|
||||
class="loading-route-change-dialog__change-group"
|
||||
>
|
||||
<div
|
||||
v-if="group.title"
|
||||
class="loading-route-change-dialog__change-group-title"
|
||||
>
|
||||
{{ group.title }}
|
||||
</div>
|
||||
<div v-if="group.text" class="loading-route-change-dialog__change-group-text">
|
||||
{{ group.text }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(field, fieldIndex) in group.fields"
|
||||
:key="fieldIndex"
|
||||
class="loading-route-change-dialog__change-field"
|
||||
>
|
||||
<span class="loading-route-change-dialog__change-field-label">
|
||||
{{ field.label }}
|
||||
</span>
|
||||
<template v-if="field.changed">
|
||||
<span class="loading-route-change-dialog__change-before">
|
||||
{{ field.before }}
|
||||
</span>
|
||||
<span class="loading-route-change-dialog__change-arrow">→</span>
|
||||
<span class="loading-route-change-dialog__change-after">
|
||||
{{ field.after }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-else class="loading-route-change-dialog__change-single">
|
||||
{{ field.single }}
|
||||
<span
|
||||
v-if="field.unchanged"
|
||||
class="loading-route-change-dialog__change-flag"
|
||||
>
|
||||
未变更
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div></template
|
||||
></el-table-column
|
||||
><el-table-column
|
||||
@@ -1716,12 +1824,14 @@ import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type';
|
||||
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import {
|
||||
getList as getProcessConfigList,
|
||||
getDetail as getProcessConfigDetail,
|
||||
getVoucherImages as getProcessConfigVoucherImages,
|
||||
} from '@/api/business/process-config';
|
||||
import VoucherExecutionDetail from './components/voucher-execution-detail.vue';
|
||||
import PunchRecordPhotos from './components/punch-record-photos.vue';
|
||||
import {
|
||||
carrierTypeOptions,
|
||||
loadingManageConfig,
|
||||
@@ -1730,10 +1840,12 @@ import {
|
||||
} from '@/option/business/loading-manage';
|
||||
import { option as waybillOption } from '@/option/business/waybill-manage';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Location, Rank, Refresh, Search, Setting } from '@element-plus/icons-vue';
|
||||
import { Location, Rank, Search } from '@element-plus/icons-vue';
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { parseRouteChangeContent } from '@/utils/route-change';
|
||||
|
||||
const defaultTransportType = '';
|
||||
const defaultCandidateTransportType = '';
|
||||
@@ -1927,21 +2039,44 @@ const uniqueText = (rows, prop) =>
|
||||
),
|
||||
].join('、');
|
||||
|
||||
const LOADING_TABLE_COLUMNS = [
|
||||
{ prop: 'loadingNo', label: '配载单号' },
|
||||
{ prop: 'loadingSubNos', label: '运单号' },
|
||||
{ prop: 'vehicleNo', label: '车牌号' },
|
||||
{ prop: 'driverName', label: '司机' },
|
||||
{ prop: 'driverPhone', label: '联系电话' },
|
||||
{ prop: 'carrierType', label: '承运类型' },
|
||||
{ prop: 'departureAddress', label: '发货地' },
|
||||
{ prop: 'transitAddress', label: '途经地' },
|
||||
{ prop: 'arrivalAddress', label: '到货地' },
|
||||
{ prop: 'carrierName', label: '承运商' },
|
||||
{ prop: 'transportType', label: '运输方式' },
|
||||
{ prop: 'dataSource', label: '数据来源' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
{ prop: 'updateTime', label: '更新时间' },
|
||||
{ prop: 'currentProcessNode', label: '过程节点' },
|
||||
{ prop: 'businessStatus', label: '状态' },
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Rank,
|
||||
Search,
|
||||
TableHeaderTools,
|
||||
VoucherExecutionDetail,
|
||||
PunchRecordPhotos,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
Rank,
|
||||
Location,
|
||||
Refresh,
|
||||
Search,
|
||||
Setting,
|
||||
loading: false,
|
||||
tableSize: 'default',
|
||||
searchVisible: true,
|
||||
columnSettingOptions: LOADING_TABLE_COLUMNS,
|
||||
visibleColumnKeys: LOADING_TABLE_COLUMNS.map(column => column.prop),
|
||||
tableData: [],
|
||||
page: {
|
||||
currentPage: 1,
|
||||
@@ -2189,6 +2324,15 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
parseRouteChangeContent,
|
||||
formatTaskCarrierContractLabel(item = {}) {
|
||||
const carrierName = item.carrierName || item.partyB || '';
|
||||
const contractLabel = formatContractOptionLabel(item);
|
||||
if (carrierName && contractLabel) {
|
||||
return `${carrierName}(${contractLabel})`;
|
||||
}
|
||||
return carrierName || contractLabel;
|
||||
},
|
||||
syncStandaloneTagTitle() {
|
||||
if (!this.isStandalonePage) return;
|
||||
const title = this.isStandaloneDetailPage ? this.detailPageTitle : this.formPageDefaultTitle;
|
||||
@@ -2248,13 +2392,8 @@ export default {
|
||||
selectionChange(rows) {
|
||||
this.selectionRows = rows || [];
|
||||
},
|
||||
toggleTableSize() {
|
||||
const order = ['default', 'small', 'large'];
|
||||
const index = order.indexOf(this.tableSize);
|
||||
this.tableSize = order[(index + 1) % order.length];
|
||||
},
|
||||
handleColumnSetting() {
|
||||
ElMessage.info('列设置按原型保留入口,当前默认展示全部列表字段');
|
||||
isColumnVisible(prop) {
|
||||
return this.visibleColumnKeys.includes(prop);
|
||||
},
|
||||
statusText(row) {
|
||||
return row.businessStatusName || loadingStatusMap[row.businessStatus] || '未知';
|
||||
@@ -2757,7 +2896,6 @@ export default {
|
||||
if (status === 'draft') {
|
||||
return [
|
||||
{ type: 'edit', label: '编辑' },
|
||||
{ type: 'copy', label: '复制' },
|
||||
{ type: 'delete', label: '删除' },
|
||||
];
|
||||
}
|
||||
@@ -2769,36 +2907,24 @@ export default {
|
||||
if (row.driverRejected) {
|
||||
actions.push({ type: 'reassign', label: '重新派单' });
|
||||
}
|
||||
actions.push(
|
||||
{ type: 'edit', label: '编辑' },
|
||||
{ type: 'copy', label: '复制' },
|
||||
{ type: 'cancel', label: '取消' }
|
||||
);
|
||||
actions.push({ type: 'edit', label: '编辑' }, { type: 'cancel', label: '取消' });
|
||||
return actions;
|
||||
}
|
||||
if (status === 'running') {
|
||||
return [
|
||||
{ type: 'view', label: '详情' },
|
||||
{ type: 'complete', label: '完成' },
|
||||
{ type: 'copy', label: '复制' },
|
||||
];
|
||||
}
|
||||
if (status === 'completed' || status === 'cancelled') {
|
||||
return [
|
||||
{ type: 'view', label: '详情' },
|
||||
{ type: 'copy', label: '复制' },
|
||||
];
|
||||
return [{ type: 'view', label: '详情' }];
|
||||
}
|
||||
return [
|
||||
{ type: 'view', label: '详情' },
|
||||
{ type: 'copy', label: '复制' },
|
||||
];
|
||||
return [{ type: 'view', label: '详情' }];
|
||||
},
|
||||
handleRowAction(type, row) {
|
||||
const actionMap = {
|
||||
view: () => this.gotoLoadingDetail(row),
|
||||
edit: () => this.openLoadingDialog('edit', row),
|
||||
copy: () => this.gotoLoadingCopy(row),
|
||||
reassign: () => this.gotoLoadingReassign(row),
|
||||
delete: () => this.deleteLoading(row),
|
||||
start: () => this.startLoading(row),
|
||||
@@ -4473,8 +4599,7 @@ export default {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.loading-manage-page__toolbar-left,
|
||||
.loading-manage-page__toolbar-right {
|
||||
.loading-manage-page__toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@@ -4733,7 +4858,7 @@ export default {
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
@@ -4928,7 +5053,6 @@ export default {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
.loading-detail__punch-record-photos,
|
||||
.loading-detail__driver-upload-grid,
|
||||
.loading-detail__voucher-image-grid {
|
||||
display: grid;
|
||||
@@ -5052,9 +5176,56 @@ export default {
|
||||
gap: 4px;
|
||||
}
|
||||
.loading-route-change-dialog__record-content {
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
line-height: 22px;
|
||||
white-space: normal;
|
||||
text-align: left;
|
||||
}
|
||||
.loading-route-change-dialog__change-group {
|
||||
padding-left: 8px;
|
||||
border-left: 2px solid #409eff;
|
||||
}
|
||||
.loading-route-change-dialog__change-group-title {
|
||||
margin-bottom: 4px;
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
.loading-route-change-dialog__change-group-text {
|
||||
color: #606266;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.loading-route-change-dialog__change-field {
|
||||
display: grid;
|
||||
grid-template-columns: 54px minmax(0, 1fr);
|
||||
column-gap: 8px;
|
||||
row-gap: 2px;
|
||||
}
|
||||
.loading-route-change-dialog__change-field-label {
|
||||
color: #909399;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.loading-route-change-dialog__change-before,
|
||||
.loading-route-change-dialog__change-single {
|
||||
color: #606266;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.loading-route-change-dialog__change-arrow {
|
||||
color: #c0c4cc;
|
||||
text-align: right;
|
||||
}
|
||||
.loading-route-change-dialog__change-after {
|
||||
color: #409eff;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.loading-route-change-dialog__change-flag {
|
||||
margin-left: 6px;
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.loading-route-change-map-dialog {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<basic-container class="master-order-page">
|
||||
<template v-if="mode === 'list'">
|
||||
<div class="master-order-page__search">
|
||||
<div v-show="searchVisible" class="master-order-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px" @submit.prevent>
|
||||
<div class="master-order-page__search-grid">
|
||||
<el-form-item v-for="field in primaryFields" :key="field.prop" :label="field.label">
|
||||
@@ -55,8 +55,16 @@
|
||||
</div>
|
||||
<section class="master-list-panel">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="goCreate()">新建多联总单</el-button
|
||||
><el-button type="primary" plain @click="download">批量导出</el-button>
|
||||
<div class="toolbar-left">
|
||||
<el-button type="primary" @click="goCreate()">新建多联总单</el-button>
|
||||
<el-button type="primary" plain @click="download">批量导出</el-button>
|
||||
</div>
|
||||
<table-header-tools
|
||||
:show-column="false"
|
||||
:show-grid="false"
|
||||
v-model:search-visible="searchVisible"
|
||||
@refresh="load"
|
||||
/>
|
||||
</div>
|
||||
<div v-loading="loading" class="master-cards">
|
||||
<article v-for="row in records" :key="row.id" class="master-card">
|
||||
@@ -147,12 +155,13 @@
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/business/master-order';
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
import MasterOrderEditor from './components/master-order-editor.vue';
|
||||
import MasterOrderDispatch from './components/master-order-dispatch.vue';
|
||||
import MasterOrderDetail from './components/master-order-detail.vue';
|
||||
|
||||
export default {
|
||||
components: { MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail },
|
||||
components: { TableHeaderTools, MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail },
|
||||
data() {
|
||||
return {
|
||||
// 实例创建时锁定所属路由。keep-alive 缓存后 $route 会变成其它页面,
|
||||
@@ -160,6 +169,7 @@ export default {
|
||||
//(mode=add 且无 id)会落入详情分支并请求缺少 id 的详情接口。
|
||||
routePathLocked: this.$route.path,
|
||||
searchExpanded: false,
|
||||
searchVisible: true,
|
||||
loading: false,
|
||||
records: [],
|
||||
query: { businessStatus: '' },
|
||||
@@ -633,8 +643,18 @@ export default {
|
||||
}
|
||||
.master-list-panel {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
.master-card {
|
||||
display: flex;
|
||||
|
||||
@@ -429,7 +429,7 @@ const NODE_TEMPLATES = [
|
||||
type: 'confirm',
|
||||
supportVoucher: false,
|
||||
},
|
||||
{ key: 'arrive_scene', name: '到场', type: 'punch', enabled: false },
|
||||
{ key: 'arrive_scene', name: '到场', type: 'punch', enabled: false, supportCargo: false },
|
||||
{
|
||||
key: 'load',
|
||||
name: '装货',
|
||||
@@ -485,6 +485,7 @@ const NODE_TEMPLATES = [
|
||||
key: 'return',
|
||||
name: '回单',
|
||||
type: 'confirm',
|
||||
supportCargo: false,
|
||||
supportVoucher: true,
|
||||
voucherOptions: ['磅单', '签收单'],
|
||||
},
|
||||
@@ -565,7 +566,13 @@ export default {
|
||||
});
|
||||
},
|
||||
defaultDeptId() {
|
||||
return (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
|
||||
const raw = (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
|
||||
return (
|
||||
String(raw)
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)[0] || ''
|
||||
);
|
||||
},
|
||||
applyDefaultDeptSearch() {
|
||||
const deptId = this.defaultDeptId();
|
||||
@@ -580,15 +587,32 @@ export default {
|
||||
});
|
||||
},
|
||||
flattenDept(tree, level = 0) {
|
||||
return tree.flatMap(item => [
|
||||
{
|
||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||
value: item.id,
|
||||
},
|
||||
...(item.children && item.children.length
|
||||
? this.flattenDept(item.children, level + 1)
|
||||
: []),
|
||||
]);
|
||||
return tree.flatMap(item => {
|
||||
const id =
|
||||
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||
if (!id) return [];
|
||||
return [
|
||||
{
|
||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||
value: id,
|
||||
},
|
||||
...(item.children && item.children.length
|
||||
? this.flattenDept(item.children, level + 1)
|
||||
: []),
|
||||
];
|
||||
});
|
||||
},
|
||||
normalizeListQuery(params = {}) {
|
||||
const query = { ...params, ...this.query };
|
||||
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||
delete query.searchDeptId;
|
||||
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||
query.deptId = String(rawDeptId).split(',')[0].trim();
|
||||
} else {
|
||||
delete query.deptId;
|
||||
}
|
||||
query.allDept = this.isAdmin ? 1 : 0;
|
||||
return query;
|
||||
},
|
||||
displayStatus(status) {
|
||||
const statusMap = {
|
||||
@@ -631,7 +655,9 @@ export default {
|
||||
enabled: false,
|
||||
punch: true,
|
||||
location: true,
|
||||
uploadCargo: true,
|
||||
// 不支持货量的节点(到场/回单/接单等)不得默认勾上,否则会写出
|
||||
// 「上传货量为是、货量类型为空」的配置,而这类节点在界面上没有货量开关可关
|
||||
uploadCargo: !!template?.supportCargo,
|
||||
cargoTypes: [],
|
||||
uploadVoucher: undefined,
|
||||
voucherTypes: [],
|
||||
@@ -646,6 +672,10 @@ export default {
|
||||
confirmInternal: false,
|
||||
...template,
|
||||
};
|
||||
if (!node.supportCargo) {
|
||||
node.uploadCargo = false;
|
||||
node.cargoTypes = [];
|
||||
}
|
||||
if ((node.key === 'accept' || node.name === '接单') && node.confirmMode === 'yes') {
|
||||
node.confirmDriver = true;
|
||||
} else if ((node.key === 'accept' || node.name === '接单') && node.confirmMode !== 'yes') {
|
||||
@@ -918,6 +948,15 @@ export default {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// 勾了上传货量却没选类型,在小程序侧等同于不采集货量;此处直接拦住,
|
||||
// 避免保存出「显示采集、实际不采集」的配置
|
||||
const cargoWithoutTypes = this.nodeRows.find(
|
||||
item => item.enabled && item.supportCargo && item.uploadCargo && !(item.cargoTypes || []).length
|
||||
);
|
||||
if (cargoWithoutTypes) {
|
||||
this.$message.warning(`「${cargoWithoutTypes.name}」已选择上传货量,请至少勾选一个货量类型`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
needsDuplicateVoucherConfirm() {
|
||||
@@ -1074,9 +1113,9 @@ export default {
|
||||
this.onLoad(this.page, this.query);
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = params;
|
||||
this.query = this.normalizeListQuery(params);
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page, params);
|
||||
this.onLoad(this.page, this.query);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
@@ -1099,12 +1138,10 @@ export default {
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
const query = this.normalizeListQuery(params);
|
||||
this.query = query;
|
||||
api
|
||||
.getList(page.currentPage, page.pageSize, {
|
||||
...params,
|
||||
...this.query,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
})
|
||||
.getList(page.currentPage, page.pageSize, query)
|
||||
.then(res => {
|
||||
const result = res.data.data || {};
|
||||
this.page.total = result.total || 0;
|
||||
@@ -1117,8 +1154,7 @@ export default {
|
||||
},
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
...this.normalizeListQuery(this.query),
|
||||
ids: this.ids,
|
||||
[this.website.tokenHeader]: getToken(),
|
||||
};
|
||||
|
||||
@@ -249,6 +249,9 @@
|
||||
<el-select
|
||||
v-model="form.undertakeDeptId"
|
||||
placeholder="请选择"
|
||||
multiple
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
clearable
|
||||
filterable
|
||||
:disabled="isBasicInfoReadonly"
|
||||
@@ -591,7 +594,7 @@
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入"
|
||||
maxlength="200"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -601,7 +604,7 @@
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入"
|
||||
maxlength="200"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -611,7 +614,7 @@
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入"
|
||||
maxlength="200"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -621,7 +624,7 @@
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入"
|
||||
maxlength="200"
|
||||
maxlength="1000"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -663,11 +666,22 @@
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="文件名" min-width="220" align="center" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名"
|
||||
min-width="220"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="240" align="center">
|
||||
@@ -864,15 +878,15 @@
|
||||
v-model="userQuery.realName"
|
||||
placeholder="用户姓名"
|
||||
clearable
|
||||
@keyup.enter="loadUsers"
|
||||
@keyup.enter="searchUsers"
|
||||
/>
|
||||
<el-input
|
||||
v-model="userQuery.phone"
|
||||
placeholder="手机号"
|
||||
clearable
|
||||
@keyup.enter="loadUsers"
|
||||
@keyup.enter="searchUsers"
|
||||
/>
|
||||
<el-button type="primary" @click="loadUsers">查询</el-button>
|
||||
<el-button type="primary" @click="searchUsers">查询</el-button>
|
||||
<el-button @click="resetUserQuery">重置</el-button>
|
||||
</div>
|
||||
<el-table :data="userData" border v-loading="userLoading" @row-dblclick="selectUser">
|
||||
@@ -964,7 +978,7 @@ const emptyForm = () => ({
|
||||
projectShortName: '',
|
||||
businessDeptId: '',
|
||||
businessDeptName: '',
|
||||
undertakeDeptId: '',
|
||||
undertakeDeptId: [],
|
||||
undertakeDeptName: '',
|
||||
projectSource: '',
|
||||
sourceRemark: '',
|
||||
@@ -1156,7 +1170,15 @@ export default {
|
||||
{ max: 20, message: '项目简称不能超过20个字符', trigger: 'blur' },
|
||||
],
|
||||
businessDeptId: [{ required: true, message: '请选择业务部门', trigger: 'change' }],
|
||||
undertakeDeptId: [{ required: true, message: '请选择平台公司', trigger: 'change' }],
|
||||
undertakeDeptId: [
|
||||
{
|
||||
type: 'array',
|
||||
required: true,
|
||||
min: 1,
|
||||
message: '请选择平台公司',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
businessMode: [{ required: true, message: '请选择业务模式', trigger: 'change' }],
|
||||
customerNames: [{ required: true, message: '请选择客户名称', trigger: 'change' }],
|
||||
carrierNames: [{ required: true, message: '请选择下游承运商', trigger: 'change' }],
|
||||
@@ -1209,6 +1231,18 @@ export default {
|
||||
changeReason: [
|
||||
{ max: 2000, message: '变更原因不能超过2000个字符', trigger: 'blur' },
|
||||
],
|
||||
projectIntro: [
|
||||
{ max: 1000, message: '项目简介不能超过1000个字符', trigger: 'blur' },
|
||||
],
|
||||
profitRemark: [
|
||||
{ max: 1000, message: '盈利模式及利润测算情况说明不能超过1000个字符', trigger: 'blur' },
|
||||
],
|
||||
riskPoint: [
|
||||
{ max: 1000, message: '项目风险点不能超过1000个字符', trigger: 'blur' },
|
||||
],
|
||||
emergencyPlan: [
|
||||
{ max: 1000, message: '风险防范措施及应急预案不能超过1000个字符', trigger: 'blur' },
|
||||
],
|
||||
},
|
||||
deptOptions: [],
|
||||
businessDeptTreeOptions: [],
|
||||
@@ -1506,13 +1540,13 @@ export default {
|
||||
this.businessDeptTreeOptions = [{ label, value: detail.businessDeptId }];
|
||||
this.deptOptions = [{ label, rawLabel: label, value: detail.businessDeptId }];
|
||||
}
|
||||
if (detail.undertakeDeptId) {
|
||||
this.platformCompanyOptions = [
|
||||
{
|
||||
label: detail.undertakeDeptName || String(detail.undertakeDeptId),
|
||||
value: detail.undertakeDeptId,
|
||||
},
|
||||
];
|
||||
const undertakeDeptIds = this.parseUndertakeDeptIds(detail.undertakeDeptId);
|
||||
const undertakeDeptNames = this.parseUndertakeDeptNames(detail.undertakeDeptName);
|
||||
if (undertakeDeptIds.length) {
|
||||
this.platformCompanyOptions = undertakeDeptIds.map((id, index) => ({
|
||||
label: undertakeDeptNames[index] || String(id),
|
||||
value: id,
|
||||
}));
|
||||
}
|
||||
},
|
||||
resolvePublicDictOptions(options, currentValue) {
|
||||
@@ -1897,6 +1931,8 @@ export default {
|
||||
this.form = {
|
||||
...form,
|
||||
...situation,
|
||||
undertakeDeptId: this.parseUndertakeDeptIds(form.undertakeDeptId),
|
||||
undertakeDeptName: form.undertakeDeptName || '',
|
||||
customerNames: this.getCustomerRowNames(customerRows, 'customer') || form.customerNames,
|
||||
carrierNames: this.getCustomerRowNames(carrierRows, 'carrier') || form.carrierNames,
|
||||
};
|
||||
@@ -2063,8 +2099,13 @@ export default {
|
||||
});
|
||||
},
|
||||
normalizeSubmitForm(options = {}) {
|
||||
const undertakeDeptIds = this.parseUndertakeDeptIds(this.form.undertakeDeptId);
|
||||
const undertakeDeptName =
|
||||
this.resolveUndertakeDeptNames(undertakeDeptIds) || this.form.undertakeDeptName || '';
|
||||
const submitRow = {
|
||||
...this.form,
|
||||
undertakeDeptId: undertakeDeptIds.join(','),
|
||||
undertakeDeptName,
|
||||
fundLimit: this.formatAmount(this.form.fundLimit),
|
||||
receivableLimit: this.formatAmount(this.form.receivableLimit),
|
||||
...Object.fromEntries(
|
||||
@@ -2456,11 +2497,40 @@ export default {
|
||||
this.$refs.projectForm?.validateField('businessDeptId');
|
||||
},
|
||||
handleUndertakeDeptChange(value) {
|
||||
const id = Array.isArray(value) ? value.at(-1) : value;
|
||||
const dept = this.platformCompanyOptions.find(item => String(item.value) === String(id));
|
||||
this.form.undertakeDeptName = dept ? dept.label : '';
|
||||
const ids = this.parseUndertakeDeptIds(value);
|
||||
this.form.undertakeDeptId = ids;
|
||||
this.form.undertakeDeptName = this.resolveUndertakeDeptNames(ids);
|
||||
this.$refs.projectForm?.validateField('undertakeDeptId');
|
||||
},
|
||||
parseUndertakeDeptIds(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map(item => String(item ?? '').trim())
|
||||
.filter(item => item && item !== '-1');
|
||||
}
|
||||
return String(value ?? '')
|
||||
.split(/[,,]/)
|
||||
.map(item => item.trim())
|
||||
.filter(item => item && item !== '-1');
|
||||
},
|
||||
parseUndertakeDeptNames(value) {
|
||||
return String(value ?? '')
|
||||
.split(/[,,、]/)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
resolveUndertakeDeptNames(ids = []) {
|
||||
return ids
|
||||
.map(id => {
|
||||
const dept = this.platformCompanyOptions.find(item => String(item.value) === String(id));
|
||||
return dept?.label || '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('、');
|
||||
},
|
||||
firstUndertakeDeptId(value) {
|
||||
return this.parseUndertakeDeptIds(value)[0] || '';
|
||||
},
|
||||
// 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null)
|
||||
findDeptPath(id, tree = [], parents = []) {
|
||||
const target = Array.isArray(id) ? id.at(-1) : id;
|
||||
@@ -2499,15 +2569,23 @@ export default {
|
||||
});
|
||||
},
|
||||
ensureCurrentPlatformOption() {
|
||||
const id = this.form.undertakeDeptId;
|
||||
if (!id) return;
|
||||
const exists = this.platformCompanyOptions.some(item => String(item.value) === String(id));
|
||||
if (!exists) {
|
||||
this.platformCompanyOptions.unshift({
|
||||
label: this.form.undertakeDeptName || String(id),
|
||||
value: id,
|
||||
});
|
||||
}
|
||||
const ids = this.parseUndertakeDeptIds(this.form.undertakeDeptId);
|
||||
if (!ids.length) return;
|
||||
const names = this.parseUndertakeDeptNames(this.form.undertakeDeptName);
|
||||
ids.forEach((id, index) => {
|
||||
const exists = this.platformCompanyOptions.some(item => String(item.value) === String(id));
|
||||
if (!exists) {
|
||||
this.platformCompanyOptions.unshift({
|
||||
label: names[index] || String(id),
|
||||
value: id,
|
||||
});
|
||||
}
|
||||
});
|
||||
// 选项 value 与已选 ID 类型对齐,避免回显失败
|
||||
this.form.undertakeDeptId = ids.map(id => {
|
||||
const matched = this.platformCompanyOptions.find(item => String(item.value) === String(id));
|
||||
return matched ? matched.value : id;
|
||||
});
|
||||
},
|
||||
excludeExternalOrganization(list = []) {
|
||||
return list.reduce((result, item) => {
|
||||
@@ -2816,11 +2894,20 @@ export default {
|
||||
if (this.dialogReadonly) return;
|
||||
this.userPickType = type;
|
||||
this.userBox = true;
|
||||
this.userPage.currentPage = 1;
|
||||
this.loadUsers();
|
||||
},
|
||||
searchUsers() {
|
||||
this.userPage.currentPage = 1;
|
||||
this.loadUsers();
|
||||
},
|
||||
loadUsers() {
|
||||
this.userLoading = true;
|
||||
getUserList(this.userPage.currentPage, this.userPage.pageSize, this.userQuery)
|
||||
const params = {
|
||||
realName: String(this.userQuery.realName || '').trim(),
|
||||
phone: String(this.userQuery.phone || '').trim(),
|
||||
};
|
||||
getUserList(this.userPage.currentPage, this.userPage.pageSize, params)
|
||||
.then(res => {
|
||||
const result = res.data.data || {};
|
||||
this.userData = result.records || [];
|
||||
@@ -2870,7 +2957,7 @@ export default {
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
//margin-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
|
||||
@@ -99,11 +99,21 @@
|
||||
<el-table :data="attachmentRows" border @selection-change="selectedAttachments = $event">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名称" min-width="240" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名称"
|
||||
min-width="240"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220">
|
||||
@@ -184,6 +194,7 @@
|
||||
title="查看临时额度申请"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
align-center
|
||||
:width="formDialogWidth"
|
||||
class="temporary-credit-limit-dialog temporary-credit-limit-detail-dialog"
|
||||
>
|
||||
@@ -216,11 +227,21 @@
|
||||
</div>
|
||||
<el-table :data="attachmentRows" empty-text="暂无附件">
|
||||
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||
<el-table-column label="文件名称" min-width="240" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
label="文件名称"
|
||||
min-width="240"
|
||||
show-overflow-tooltip
|
||||
class-name="is-ellipsis-link"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="previewAttachment(row)">
|
||||
{{ attachmentName(row) }}
|
||||
</el-link>
|
||||
<span
|
||||
class="attachment-file-name"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="previewAttachment(row)"
|
||||
@keydown.enter="previewAttachment(row)"
|
||||
@keydown.space.prevent="previewAttachment(row)"
|
||||
>{{ attachmentName(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件描述" min-width="220" show-overflow-tooltip>
|
||||
@@ -615,6 +636,12 @@ export default {
|
||||
}
|
||||
const value = row[prop];
|
||||
if (value === undefined || value === null || value === '') return '-';
|
||||
if (
|
||||
(prop === 'usedFundLimit' || prop === 'remainingFundLimit') &&
|
||||
Number(value) === -1
|
||||
) {
|
||||
return '-';
|
||||
}
|
||||
return value;
|
||||
},
|
||||
applyDetail(detail) {
|
||||
@@ -1188,10 +1215,10 @@ export default {
|
||||
}
|
||||
|
||||
// 新增/编辑/查看:宽度与高度规则统一(Avue setPx 对 '1100px' 会拼成 '1100pxpx',故 option 用数字 1100)
|
||||
// margin:auto 配合下方 unscoped 的 overlay flex,实现垂直水平居中
|
||||
.temporary-credit-limit-dialog.el-dialog {
|
||||
width: 1100px !important;
|
||||
margin-top: 20px !important;
|
||||
margin-bottom: 20px !important;
|
||||
margin: auto !important;
|
||||
height: auto !important;
|
||||
max-height: calc(100vh - 40px) !important;
|
||||
display: flex;
|
||||
@@ -1329,3 +1356,16 @@ export default {
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<!-- append-to-body 弹窗挂到 body,overlay 无 scoped 属性,需全局样式才能居中 -->
|
||||
<style lang="scss">
|
||||
.el-overlay-dialog:has(> .temporary-credit-limit-dialog) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.temporary-credit-limit-dialog.el-dialog {
|
||||
margin: auto !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -585,6 +585,10 @@ export default {
|
||||
this.$message.warning(`第${index + 1}条调度明细缺少车牌号`);
|
||||
return;
|
||||
}
|
||||
if (!this.resolveDispatchQuantityUnit(row)) {
|
||||
this.$message.warning(`第${index + 1}条调度明细缺少数量单位`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.$confirm(
|
||||
@@ -600,32 +604,57 @@ export default {
|
||||
this.submitDispatch();
|
||||
});
|
||||
},
|
||||
resolveDispatchQuantityUnit(row = {}) {
|
||||
const topUnit = String(row.quantityUnit || '').trim();
|
||||
if (topUnit) return topUnit;
|
||||
const goodsList = this.parseGoodsList(row.goodsJson);
|
||||
for (const goods of goodsList) {
|
||||
const unit = String(
|
||||
goods?.quantityUnit || goods?.goodsQuantityUnit || goods?.unit || ''
|
||||
).trim();
|
||||
if (unit) return unit;
|
||||
}
|
||||
return TRANSPORT_PLAN_QUANTITY_UNIT;
|
||||
},
|
||||
enrichDispatchGoodsJson(row = {}, quantityUnit) {
|
||||
const goodsList = this.parseGoodsList(row.goodsJson);
|
||||
if (goodsList.length) {
|
||||
const nextGoods = goodsList.map(goods => ({
|
||||
...goods,
|
||||
quantityUnit: String(goods.quantityUnit || quantityUnit || '').trim() || quantityUnit,
|
||||
}));
|
||||
return JSON.stringify(nextGoods);
|
||||
}
|
||||
return JSON.stringify([
|
||||
{
|
||||
cargoName: row.cargoName,
|
||||
cargoType: row.cargoType,
|
||||
quantity: row.quantity,
|
||||
quantityUnit,
|
||||
specification: row.specification,
|
||||
model: row.model,
|
||||
packageType: row.packageType,
|
||||
brand: row.brand,
|
||||
},
|
||||
]);
|
||||
},
|
||||
submitDispatch() {
|
||||
this.saving = true;
|
||||
const payload = {
|
||||
id: this.planId,
|
||||
mode: 'submit',
|
||||
waybills: this.dispatchList.map(row => ({
|
||||
...row,
|
||||
taskEntryMode: row.taskEntryMode || 'simple',
|
||||
taskRemark: row.remark || '',
|
||||
estimatedStartTime: row.estimatedStartTime || this.planData.planStartDate || '',
|
||||
estimatedEndTime: row.estimatedEndTime || this.planData.planEndDate || '',
|
||||
goodsJson:
|
||||
row.goodsJson ||
|
||||
JSON.stringify([
|
||||
{
|
||||
cargoName: row.cargoName,
|
||||
cargoType: row.cargoType,
|
||||
quantity: row.quantity,
|
||||
quantityUnit: row.quantityUnit,
|
||||
specification: row.specification,
|
||||
model: row.model,
|
||||
packageType: row.packageType,
|
||||
brand: row.brand,
|
||||
},
|
||||
]),
|
||||
})),
|
||||
waybills: this.dispatchList.map(row => {
|
||||
const quantityUnit = this.resolveDispatchQuantityUnit(row);
|
||||
return {
|
||||
...row,
|
||||
quantityUnit,
|
||||
taskEntryMode: row.taskEntryMode || 'simple',
|
||||
taskRemark: row.remark || '',
|
||||
estimatedStartTime: row.estimatedStartTime || this.planData.planStartDate || '',
|
||||
estimatedEndTime: row.estimatedEndTime || this.planData.planEndDate || '',
|
||||
goodsJson: this.enrichDispatchGoodsJson(row, quantityUnit),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
dispatch(payload)
|
||||
@@ -639,7 +668,7 @@ export default {
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('调度失败:', err);
|
||||
this.$message.error('调度失败');
|
||||
this.$message.error(err?.response?.data?.msg || err?.message || '调度失败');
|
||||
})
|
||||
.finally(() => {
|
||||
this.saving = false;
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<el-option
|
||||
v-for="item in contractOptions"
|
||||
:key="item.id"
|
||||
:label="item.contractName"
|
||||
:label="formatContractOptionLabel(item)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -165,6 +165,7 @@ import { getList as getContractList } from '@/api/business/contract-manage';
|
||||
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import { importTransportPlan, validateTransportPlan } from '@/api/business/transport-plan';
|
||||
import { config } from '@/option/business/transport-plan';
|
||||
import { formatContractOptionLabel } from '@/utils/contract';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@@ -271,6 +272,7 @@ export default {
|
||||
this.loadProjectOptions();
|
||||
},
|
||||
methods: {
|
||||
formatContractOptionLabel,
|
||||
loadProjectOptions() {
|
||||
if (this.projectLoading) return;
|
||||
this.projectLoading = true;
|
||||
@@ -651,7 +653,7 @@ export default {
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,11 +96,25 @@ const uploadReplaceFile = async file => {
|
||||
if (!task?.id || !task.objectKey) throw new Error('创建分片上传任务失败');
|
||||
replaceProgress.value = 0;
|
||||
for (let partNumber = 1; partNumber <= task.chunkTotal; partNumber += 1) {
|
||||
const urlRes = await api.getPartUploadUrl(task.id, partNumber);
|
||||
const part = file.slice((partNumber - 1) * chunkSize, Math.min(partNumber * chunkSize, file.size));
|
||||
const response = await fetch(urlRes.data?.data, { method: 'PUT', body: part });
|
||||
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
|
||||
const etag = String(response.headers.get('etag') || '').replaceAll('"', '');
|
||||
const urlRes = await api.getPartUploadUrl(task.id, partNumber);
|
||||
const url = urlRes.data?.data;
|
||||
let etag = '';
|
||||
const needProxy =
|
||||
typeof window !== 'undefined' &&
|
||||
window.location.protocol === 'https:' &&
|
||||
String(url || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.startsWith('http:');
|
||||
if (!url || needProxy) {
|
||||
const proxyRes = await api.uploadPartByProxy(task.id, partNumber, part);
|
||||
etag = String(proxyRes.data?.data || '').replaceAll('"', '');
|
||||
} else {
|
||||
const response = await fetch(url, { method: 'PUT', body: part });
|
||||
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
|
||||
etag = String(response.headers.get('etag') || '').replaceAll('"', '');
|
||||
}
|
||||
if (!etag) throw new Error('未获取到分片 ETag');
|
||||
await api.updateFileTask({ id: task.id, partNumber, etag });
|
||||
replaceProgress.value = Math.round((partNumber / task.chunkTotal) * 100);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<basic-container class="voucher-manage-page">
|
||||
<section class="voucher-manage-page__search">
|
||||
<section v-show="searchVisible" class="voucher-manage-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="88px">
|
||||
<div class="voucher-manage-page__search-grid">
|
||||
<el-form-item label="凭证批次号"
|
||||
@@ -55,10 +55,25 @@
|
||||
</section>
|
||||
<section>
|
||||
<div class="voucher-manage-page__toolbar">
|
||||
<el-button type="primary" @click="openUpload">批量导入凭证</el-button
|
||||
><el-button plain @click="openProgress">上传进度</el-button>
|
||||
<div class="voucher-manage-page__toolbar-left">
|
||||
<el-button type="primary" @click="openUpload">批量导入凭证</el-button>
|
||||
<el-button plain @click="openProgress">上传进度</el-button>
|
||||
</div>
|
||||
<table-header-tools
|
||||
:columns="columnSettingOptions"
|
||||
v-model:visible-column-keys="visibleColumnKeys"
|
||||
v-model:table-size="tableSize"
|
||||
v-model:search-visible="searchVisible"
|
||||
@refresh="load"
|
||||
/>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="rows" border class="voucher-manage-page__table">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
:size="tableSize"
|
||||
border
|
||||
class="voucher-manage-page__table"
|
||||
>
|
||||
<el-table-column type="selection" width="52" fixed="left" align="center" /><el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
@@ -66,27 +81,47 @@
|
||||
fixed="left"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column prop="voucherBatchNo" label="凭证批次号" min-width="150" fixed="left"
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('voucherBatchNo')"
|
||||
prop="voucherBatchNo"
|
||||
label="凭证批次号"
|
||||
min-width="150"
|
||||
fixed="left"
|
||||
><template #default="{ row }"
|
||||
><el-link class="voucher-manage-page__link" type="primary" @click="view(row)">{{
|
||||
row.voucherBatchNo
|
||||
}}</el-link></template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column prop="waybillBatchNo" label="运单批次号" min-width="170" /><el-table-column
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('waybillBatchNo')"
|
||||
prop="waybillBatchNo"
|
||||
label="运单批次号"
|
||||
min-width="170"
|
||||
/><el-table-column
|
||||
v-if="isColumnVisible('fileName')"
|
||||
prop="fileName"
|
||||
label="文件名"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column v-if="!isCarrier" prop="uploadSource" label="上传来源" width="100" /><el-table-column
|
||||
v-if="!isCarrier"
|
||||
<el-table-column
|
||||
v-if="!isCarrier && isColumnVisible('uploadSource')"
|
||||
prop="uploadSource"
|
||||
label="上传来源"
|
||||
width="100"
|
||||
/><el-table-column
|
||||
v-if="!isCarrier && isColumnVisible('carrierName')"
|
||||
prop="carrierName"
|
||||
label="承运商名称"
|
||||
min-width="160"
|
||||
><template #default="{ row }">{{ row.carrierName || '-' }}</template></el-table-column
|
||||
>
|
||||
<el-table-column prop="processStatus" label="处理状态" width="120"
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('processStatus')"
|
||||
prop="processStatus"
|
||||
label="处理状态"
|
||||
width="120"
|
||||
><template #default="{ row }"
|
||||
><span
|
||||
:class="{ 'voucher-manage-page__process-failed': row.processStatus === '处理失败' }"
|
||||
@@ -94,17 +129,35 @@
|
||||
></template
|
||||
></el-table-column
|
||||
><el-table-column
|
||||
v-if="isColumnVisible('voucherCount')"
|
||||
prop="voucherCount"
|
||||
label="凭证数量"
|
||||
width="100"
|
||||
/><el-table-column
|
||||
v-if="isColumnVisible('relatedWaybillCount')"
|
||||
prop="relatedWaybillCount"
|
||||
label="凭证已关联数量"
|
||||
width="125"
|
||||
/> <el-table-column prop="unRelatedWaybillCount" label="凭证未关联数量" width="125" />
|
||||
<el-table-column prop="createUserName" label="创建人" min-width="120" />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" />
|
||||
/><el-table-column
|
||||
v-if="isColumnVisible('unRelatedWaybillCount')"
|
||||
prop="unRelatedWaybillCount"
|
||||
label="凭证未关联数量"
|
||||
width="125"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('createUserName')"
|
||||
prop="createUserName"
|
||||
label="创建人"
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('createTime')"
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="170"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('auditStatus')"
|
||||
prop="auditStatus"
|
||||
label="审核状态"
|
||||
width="120"
|
||||
@@ -512,6 +565,7 @@ import { WarnTriangleFilled } from '@element-plus/icons-vue';
|
||||
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import * as api from '@/api/business/voucher-manage';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
import TableHeaderTools from '@/components/table-header-tools/main.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
@@ -539,6 +593,47 @@ const checkIfCarrier = () => {
|
||||
isCarrier.value = topDeptName === '外部组织';
|
||||
};
|
||||
|
||||
const VOUCHER_BASE_COLUMNS = [
|
||||
{ prop: 'voucherBatchNo', label: '凭证批次号' },
|
||||
{ prop: 'waybillBatchNo', label: '运单批次号' },
|
||||
{ prop: 'fileName', label: '文件名' },
|
||||
{ prop: 'uploadSource', label: '上传来源', carrierHidden: true },
|
||||
{ prop: 'carrierName', label: '承运商名称', carrierHidden: true },
|
||||
{ prop: 'processStatus', label: '处理状态' },
|
||||
{ prop: 'voucherCount', label: '凭证数量' },
|
||||
{ prop: 'relatedWaybillCount', label: '凭证已关联数量' },
|
||||
{ prop: 'unRelatedWaybillCount', label: '凭证未关联数量' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
{ prop: 'auditStatus', label: '审核状态' },
|
||||
];
|
||||
|
||||
const searchVisible = ref(true);
|
||||
const tableSize = ref('default');
|
||||
const visibleColumnKeys = ref([]);
|
||||
const columnSettingOptions = computed(() =>
|
||||
VOUCHER_BASE_COLUMNS.filter(column => !isCarrier.value || !column.carrierHidden).map(column => ({
|
||||
prop: column.prop,
|
||||
label: column.label,
|
||||
}))
|
||||
);
|
||||
const isColumnVisible = prop => visibleColumnKeys.value.includes(prop);
|
||||
|
||||
watch(
|
||||
columnSettingOptions,
|
||||
options => {
|
||||
const props = options.map(column => column.prop);
|
||||
if (!visibleColumnKeys.value.length) {
|
||||
visibleColumnKeys.value = props;
|
||||
return;
|
||||
}
|
||||
const visible = new Set(visibleColumnKeys.value);
|
||||
visibleColumnKeys.value = props.filter(prop => visible.has(prop));
|
||||
if (!visibleColumnKeys.value.length) visibleColumnKeys.value = props;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
checkIfCarrier();
|
||||
});
|
||||
@@ -742,6 +837,30 @@ const calculateMd5 = async file => {
|
||||
const buffer = await file.arrayBuffer();
|
||||
return md5.base64(buffer);
|
||||
};
|
||||
const needsUploadProxy = url =>
|
||||
typeof window !== 'undefined' &&
|
||||
window.location.protocol === 'https:' &&
|
||||
String(url || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.startsWith('http:');
|
||||
const uploadPartWithFallback = async (taskId, partNumber, part, signal) => {
|
||||
const urlRes = await api.getPartUploadUrl(taskId, partNumber);
|
||||
if (signal?.aborted) throw new DOMException('上传已暂停', 'AbortError');
|
||||
const url = urlRes.data?.data;
|
||||
// HTTPS 页面直连 http://IP MinIO 会被 Mixed Content 拦截,回退服务端代传
|
||||
if (!url || needsUploadProxy(url)) {
|
||||
const proxyRes = await api.uploadPartByProxy(taskId, partNumber, part);
|
||||
return String(proxyRes.data?.data || '').replaceAll('"', '');
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
body: part,
|
||||
signal,
|
||||
});
|
||||
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
|
||||
return String(response.headers.get('etag') || '').replaceAll('"', '');
|
||||
};
|
||||
const uploadParts = async (file, task, sessionId) => {
|
||||
uploading.value = true;
|
||||
activeUploadTaskId.value = task.id;
|
||||
@@ -756,21 +875,13 @@ const uploadParts = async (file, task, sessionId) => {
|
||||
) {
|
||||
ensureUploadSession(sessionId, task.id);
|
||||
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
|
||||
const urlRes = await api.getPartUploadUrl(task.id, partNumber);
|
||||
ensureUploadSession(sessionId, task.id);
|
||||
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
|
||||
const url = urlRes.data?.data;
|
||||
const part = file.slice(
|
||||
(partNumber - 1) * chunkSize,
|
||||
Math.min(partNumber * chunkSize, file.size)
|
||||
);
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
body: part,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
|
||||
const etag = String(response.headers.get('etag') || '').replaceAll('"', '');
|
||||
const etag = await uploadPartWithFallback(task.id, partNumber, part, abortController.signal);
|
||||
if (!etag) throw new Error('MinIO 未返回分片 ETag,请确认跨域配置已暴露 ETag 响应头');
|
||||
await api.updateFileTask({ id: task.id, partNumber, etag });
|
||||
ensureUploadSession(sessionId, task.id);
|
||||
@@ -1329,9 +1440,15 @@ load();
|
||||
&__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
&__toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
&__table {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1370,7 +1487,7 @@ load();
|
||||
display: none;
|
||||
}
|
||||
&__upload-form :deep(.el-form-item) {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
&__upload-tip {
|
||||
margin-left: 16px;
|
||||
|
||||
Reference in New Issue
Block a user