This commit is contained in:
2026-08-25 00:10:45 +08:00
parent e1bade226f
commit 1ef050dda6
35 changed files with 2041 additions and 263 deletions
+19 -1
View File
@@ -14,6 +14,7 @@
@row-del="rowDel"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@@ -40,6 +41,7 @@
v-model="form.feeCategory"
class="fee-item-form-control"
clearable
:disabled="dialogType !== 'add'"
filterable
placeholder="请选择费用类型"
@change="handleFeeCategoryChange"
@@ -58,6 +60,7 @@
:maxlength="feeItemCodeMaxlength"
class="fee-item-form-control"
clearable
:disabled="dialogType !== 'add'"
placeholder="请输入费用项代码"
@change="handleFeeItemCodeChange"
>
@@ -114,12 +117,14 @@ export default {
data: [],
excelBox: false,
excelForm: {},
dialogType: 'add',
feeCategoryOptions: [],
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
selectionList: [],
option: {
height: 'auto',
calcHeight: 32,
@@ -139,7 +144,7 @@ export default {
viewBtn: false,
delBtn: false,
editBtn: false,
selection: false,
selection: true,
dialogClickModal: false,
menuWidth: 180,
column: [
@@ -294,6 +299,9 @@ export default {
feeItemCodeMaxlength() {
return Math.max(1, 100 - this.feeCategoryPrefix.length);
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
permissionList() {
return {
addBtn: this.validData(this.permission.fee_item_add, false),
@@ -418,6 +426,7 @@ export default {
});
},
beforeOpen(done, type) {
this.dialogType = type || 'add';
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = this.normalizeFormForEdit(res.data.data || {});
@@ -439,6 +448,13 @@ export default {
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
@@ -454,6 +470,7 @@ export default {
buildExportParams() {
return {
...this.query,
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
},
@@ -490,6 +507,7 @@ export default {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.selectionClear();
this.loading = false;
});
},
+4 -86
View File
@@ -250,81 +250,6 @@ import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const DEFAULT_COUNTRY_CODE = '+86';
const EXCLUDED_EXCEL_HEADERS = ['数据来源', '启停状态'];
const REQUIRED_EXPORT_HEADERS = [
'港口码头名称',
'港口/码头名称',
'类型',
'国家',
'城市',
'经度',
'纬度',
];
const getExcelCellText = value => {
if (value === undefined || value === null) return '';
if (typeof value === 'object') {
if (Array.isArray(value.richText)) {
return value.richText.map(item => item.text || '').join('');
}
if (value.text) return value.text;
if (value.result) return value.result;
}
return String(value);
};
const removeWorkbookColumnsByHeaders = workbook => {
workbook.eachSheet(worksheet => {
const maxHeaderRow = Math.min(worksheet.rowCount || 0, 10);
for (let rowIndex = 1; rowIndex <= maxHeaderRow; rowIndex += 1) {
const row = worksheet.getRow(rowIndex);
const indexes = [];
row.eachCell((cell, colNumber) => {
const text = getExcelCellText(cell.value).trim();
if (EXCLUDED_EXCEL_HEADERS.includes(text)) {
indexes.push(colNumber);
}
});
if (indexes.length) {
indexes.sort((a, b) => b - a).forEach(colNumber => worksheet.spliceColumns(colNumber, 1));
break;
}
}
});
};
const addWorkbookRequiredHeaderMarks = workbook => {
workbook.eachSheet(worksheet => {
const maxHeaderRow = Math.min(worksheet.rowCount || 0, 10);
for (let rowIndex = 1; rowIndex <= maxHeaderRow; rowIndex += 1) {
const row = worksheet.getRow(rowIndex);
let hasRequiredHeader = false;
row.eachCell(cell => {
const text = getExcelCellText(cell.value).trim();
const header = text.replace(/\*+$/g, '').trim();
if (REQUIRED_EXPORT_HEADERS.includes(header)) {
cell.value = `${header}*`;
hasRequiredHeader = true;
}
});
if (hasRequiredHeader) break;
}
});
};
const removeExcelColumnsByHeaders = async (blob, options = {}) => {
const { markRequiredHeaders = false } = options;
const ExcelJS = await import('exceljs');
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(await blob.arrayBuffer());
removeWorkbookColumnsByHeaders(workbook);
if (markRequiredHeaders) {
addWorkbookRequiredHeaderMarks(workbook);
}
const buffer = await workbook.xlsx.writeBuffer();
return new Blob([buffer], { type: 'application/vnd.ms-excel' });
};
const newLocal = '请选择类型';
export default {
data() {
@@ -1323,9 +1248,6 @@ export default {
'港口码头主数据',
() => {
this.loadPortOptions();
},
{
failDetailDecorator: removeWorkbookColumnsByHeaders,
}
);
},
@@ -1339,11 +1261,8 @@ export default {
exportBlob('/blade-system/port-terminal/export-port-terminal', this.buildExportParams(), {
feedback: true,
})
.then(async res => {
const blob = await removeExcelColumnsByHeaders(res.data, {
markRequiredHeaders: true,
});
downloadXls(blob, `港口码头主数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
.then(res => {
downloadXls(res.data, `港口码头主数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})
.finally(() => {
NProgress.done();
@@ -1362,9 +1281,8 @@ export default {
`/blade-system/port-terminal/export-template?${this.website.tokenHeader}=${getToken()}`,
undefined,
{ feedback: true }
).then(async res => {
const blob = await removeExcelColumnsByHeaders(res.data);
downloadXls(blob, '港口码头主数据模板.xlsx');
).then(res => {
downloadXls(res.data, '港口码头主数据模板.xlsx');
});
},
},
+1
View File
@@ -689,6 +689,7 @@ export default {
feedback: true,
}).then(res => {
downloadXls(res.data, '行政区划模板.xlsx');
this.$message.success('模板下载成功');
});
},
},
+17 -2
View File
@@ -399,8 +399,17 @@ export default {
if (submitRow.cargoValue !== undefined && submitRow.cargoValue !== null) {
submitRow.cargoValue = String(submitRow.cargoValue).trim();
}
if (!submitRow.cargoValue || String(submitRow.cargoValue) === '-1') {
submitRow.cargoValue = null;
}
return submitRow;
},
normalizeCargoValue(row) {
if (row && String(row.cargoValue) === '-1') {
row.cargoValue = null;
}
return row;
},
validateRow(row) {
if (!row.firstCargoTypeName || !row.firstCargoTypeCode) {
this.$message.warning('请选择一级货物类型');
@@ -539,7 +548,7 @@ export default {
}
if (['edit', 'view'].includes(type)) {
api.getDetail(this.form.id).then(res => {
this.form = res.data.data || {};
this.form = this.normalizeCargoValue(res.data.data || {});
this.ensureCargoTypeOption('firstCargoTypeOptions', {
cargoName: this.form.firstCargoTypeName,
cargoCode: this.form.firstCargoTypeCode,
@@ -593,7 +602,7 @@ export default {
.then(res => {
const result = res.data.data;
this.page.total = result.total;
this.data = result.records;
this.data = (result.records || []).map(row => this.normalizeCargoValue(row));
this.selectionClear();
})
.finally(() => {
@@ -654,6 +663,12 @@ export default {
<style lang="scss" scoped>
.common-cargo-page {
// 仅缩短本页面筛选栏中的输入框和下拉框,避免影响表单及其他页面。
:deep(.avue-crud__search .el-form-item__content > .el-input),
:deep(.avue-crud__search .el-form-item__content > .el-select) {
width: 70%;
}
&__value-unit {
display: flex;
align-items: center;
File diff suppressed because it is too large Load Diff
@@ -2105,7 +2105,7 @@
</template>
<template #menu="{ row, index }">
<template>
<div class="business-crud-page__row-actions">
<el-link type="primary" v-if="showDetailButton(row)" @click="openDetail(row)"
>详情</el-link
>
@@ -2144,7 +2144,7 @@
{{ operation.label }}
</el-link>
<el-link type="danger" v-if="canDelete(row)" @click="rowDel(row)"> 删除 </el-link>
</template>
</div>
</template>
</component>
@@ -13281,6 +13281,13 @@ export default {
}
}
&__row-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
:deep(.business-crud-page__full-form-item) {
width: 100%;
max-width: 100%;
@@ -1705,6 +1705,10 @@ export default {
departureAddress: this.form.arrivalAddress,
departureContact: this.form.arrivalContact,
departurePhone: this.form.arrivalPhone,
departureLongitude: this.form.arrivalLongitude,
departureLatitude: this.form.arrivalLatitude,
departureRegionCode: this.form.arrivalRegionCode,
departureSiteCode: this.form.arrivalSiteCode,
transportType: this.form.finalTransportType,
},
],
+326 -46
View File
@@ -580,29 +580,205 @@
title="合同详情"
append-to-body
destroy-on-close
width="1200px"
class="contract-manage-detail-dialog"
width="92%"
top="4vh"
>
<div v-loading="detailLoading" class="contract-manage-detail">
<section v-for="section in config.detailSections" :key="section.title">
<div class="dialog-section-title">{{ section.title }}</div>
<el-descriptions :column="3" border>
<el-descriptions-item
v-for="field in section.fields"
:key="field[0]"
:label="field[1]"
:span="field[2] || 1"
>
{{ detailValue(field[0]) }}
</el-descriptions-item>
<div v-loading="detailLoading" class="contract-manage-form contract-manage-detail">
<section class="contract-manage-form__section contract-manage-form__section--panel">
<div class="dialog-section-title">基本信息</div>
<el-descriptions :column="3" class="contract-manage-detail__descriptions">
<el-descriptions-item label="签约类型">{{
detailValue('signType')
}}</el-descriptions-item>
<el-descriptions-item label="合同编号">{{
detailValue('contractNo')
}}</el-descriptions-item>
<el-descriptions-item label="合同名称">{{
detailValue('contractName')
}}</el-descriptions-item>
<el-descriptions-item label="合同类型">{{
detailValue('contractCategory')
}}</el-descriptions-item>
<el-descriptions-item label="项目">{{
detailValue('projectName')
}}</el-descriptions-item>
<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="所属组织">{{
detailValue('organizationName')
}}</el-descriptions-item>
<el-descriptions-item label="经办人">{{
detailValue('handlerUserName')
}}</el-descriptions-item>
<el-descriptions-item label="签订日期">{{
detailValue('signDate')
}}</el-descriptions-item>
<el-descriptions-item label="结算类型">{{
detailValue('settlementMode')
}}</el-descriptions-item>
<el-descriptions-item label="合同格式">{{
detailValue('contractFormat')
}}</el-descriptions-item>
<el-descriptions-item label="是否需要加盖法人章">{{
detailLegalSealText
}}</el-descriptions-item>
<el-descriptions-item label="一式">{{
detailUnitValue('copyCount', '份')
}}</el-descriptions-item>
<el-descriptions-item label="回款账期">{{
detailUnitValue('paymentDays', '天')
}}</el-descriptions-item>
<el-descriptions-item label="备注" :span="3">{{
detailValue('remark')
}}</el-descriptions-item>
</el-descriptions>
</section>
<attachment-section
title="合同文件"
readonly
:rows="detailContractFileRows"
:preview="previewAttachment"
/>
<section class="contract-manage-form__section contract-manage-form__section--panel">
<div class="dialog-section-title">计费信息</div>
<el-descriptions :column="3" class="contract-manage-detail__descriptions">
<el-descriptions-item label="费用生成模式">{{
detailFeeGenerationMode
}}</el-descriptions-item>
</el-descriptions>
<el-table :data="detailBillingPlanRows" border>
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column prop="planName" label="方案名称" min-width="220" align="center" />
<el-table-column label="默认方案" width="140" align="center">
<template #default="{ row }">{{ isDefaultBillingPlan(row) ? '是' : '' }}</template>
</el-table-column>
<el-table-column
prop="remark"
label="备注"
min-width="260"
align="center"
show-overflow-tooltip
/>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row, $index }">
<el-link type="primary" @click="openDetailBillingPlan(row, $index)"
>查看详情</el-link
>
</template>
</el-table-column>
</el-table>
</section>
<section class="contract-manage-form__section contract-manage-form__section--panel">
<div class="dialog-section-title">结算单规则</div>
<el-tabs v-model="detailSettlementConfigTab">
<el-tab-pane label="预结算配置" name="pre" />
<el-tab-pane label="正式结算配置" name="formal" />
</el-tabs>
<el-descriptions :column="3" class="contract-manage-detail__descriptions">
<el-descriptions-item label="自动生成结算单">{{
Number(detailSettlementRule.autoGenerate) === 1 ? '开启' : '关闭'
}}</el-descriptions-item>
<template v-if="Number(detailSettlementRule.autoGenerate) === 1">
<el-descriptions-item label="账单起始日期">{{
displayValue(detailSettlementRule.billStartDate)
}}</el-descriptions-item>
<el-descriptions-item label="结算类型">{{
displayValue(detailSettlementRule.settlementType)
}}</el-descriptions-item>
<el-descriptions-item
v-if="detailSettlementRule.settlementType === '月结'"
label="账单周期类型"
>{{ displayValue(detailSettlementRule.billCycleType) }}</el-descriptions-item
>
<el-descriptions-item
v-if="
detailSettlementRule.settlementType === '月结' &&
detailSettlementRule.billCycleType === '固定截单日'
"
label="账单截单日"
>{{
detailObjectUnitValue(detailSettlementRule, 'billCutoffDay', '日')
}}</el-descriptions-item
>
<el-descriptions-item
v-if="detailSettlementRule.settlementType === '固定天数周期结算'"
label="周期天数"
>{{
detailObjectUnitValue(detailSettlementRule, 'cycleDays', '天')
}}</el-descriptions-item
>
</template>
</el-descriptions>
</section>
<section class="contract-manage-form__section contract-manage-form__section--panel">
<div class="dialog-section-title">付款比例设置</div>
<el-table :data="detailPaymentRatioRows" border>
<el-table-column type="index" label="序号" width="80" align="center" />
<el-table-column prop="paymentTerm" label="付款笔数" min-width="180" align="center" />
<el-table-column label="付款比例上限" min-width="220" align="center">
<template #default="{ row }">{{
detailObjectUnitValue(row, 'ratioLimit', '%')
}}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="220" />
</el-table>
</section>
<attachment-section
title="其它附件"
description
readonly
:rows="detailAttachmentRows"
:preview="previewAttachment"
/>
<section class="contract-manage-form__section contract-manage-form__section--panel">
<div class="dialog-section-title">变更记录</div>
<el-table :data="detailChangeRecordRows" border>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column
prop="changeDate"
label="变更日期"
min-width="150"
align="center"
sortable
/>
<el-table-column prop="handlerUserName" label="经办人" min-width="140" align="center" />
<el-table-column prop="changeType" label="变更类型" min-width="160" align="center" />
<el-table-column
prop="changeReason"
label="变更原因"
min-width="240"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="statusName" label="状态" min-width="140" align="center" />
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }"
><el-link type="primary" @click="openFlow(row)">流程</el-link></template
>
</el-table-column>
</el-table>
</section>
</div>
<template #footer
><el-button type="primary" @click="detailBox = false">关闭</el-button></template
>
</el-dialog>
<billing-plan-editor
v-model="detailBillingPlanBox"
:value="detailBillingPlanForm"
:index="detailBillingPlanIndex"
readonly
/>
<flow-design
v-if="website.design.designMode"
is-dialog
@@ -743,6 +919,7 @@ const AttachmentSection = defineComponent({
title: { type: String, required: true },
rows: { type: Array, default: () => [] },
description: Boolean,
readonly: Boolean,
preview: { type: Function, default: null },
},
emits: ['update:rows'],
@@ -797,7 +974,9 @@ const AttachmentSection = defineComponent({
const ElTableColumn = resolveComponent('el-table-column');
const VehicleAttachmentUpload = resolveComponent('vehicle-attachment-upload');
const columns = [
h(ElTableColumn, { type: 'selection', width: 55, align: 'center' }),
...(!this.readonly
? [h(ElTableColumn, { type: 'selection', width: 55, align: 'center' })]
: []),
h(ElTableColumn, { type: 'index', label: '序号', width: 70, align: 'center' }),
h(
ElTableColumn,
@@ -828,14 +1007,16 @@ const AttachmentSection = defineComponent({
{ label: '附件描述', minWidth: 220 },
{
default: ({ row }) =>
h(ElInput, {
modelValue: row.description,
maxlength: 200,
'onUpdate:modelValue': value => {
row.description = value;
this.update([...this.rows]);
},
}),
this.readonly
? row.description || '-'
: h(ElInput, {
modelValue: row.description,
maxlength: 200,
'onUpdate:modelValue': value => {
row.description = value;
this.update([...this.rows]);
},
}),
}
)
);
@@ -855,16 +1036,20 @@ const AttachmentSection = defineComponent({
width: 180,
align: 'center',
sortable: true,
}),
h(
ElTableColumn,
{ label: '操作', width: 100, align: 'center', fixed: 'right' },
{
default: ({ $index }) =>
h(ElLink, { type: 'danger', onClick: () => this.remove($index) }, () => '删除'),
}
)
})
);
if (!this.readonly) {
columns.push(
h(
ElTableColumn,
{ label: '操作', width: 100, align: 'center', fixed: 'right' },
{
default: ({ $index }) =>
h(ElLink, { type: 'danger', onClick: () => this.remove($index) }, () => '删除'),
}
)
);
}
return h(
'section',
{
@@ -874,16 +1059,20 @@ const AttachmentSection = defineComponent({
h('div', { class: 'dialog-section-title' }, this.title),
h('div', { class: 'contract-manage-form__attachment-head' }, [
h('div', { class: 'contract-manage-form__attachment-actions' }, [
h(VehicleAttachmentUpload, {
modelValue: this.rows,
fileTypes: this.attachmentFileTypes,
maxSize: 500,
showTip: false,
showFileList: false,
buttonText: '上传附件',
'onUpdate:modelValue': this.handleChange,
onChange: this.handleChange,
}),
...(!this.readonly
? [
h(VehicleAttachmentUpload, {
modelValue: this.rows,
fileTypes: this.attachmentFileTypes,
maxSize: 500,
showTip: false,
showFileList: false,
buttonText: '上传附件',
'onUpdate:modelValue': this.handleChange,
onChange: this.handleChange,
}),
]
: []),
h(
ElButton,
{ type: 'primary', disabled: !this.rows.length, onClick: this.batchDownload },
@@ -990,6 +1179,17 @@ export default {
detailBox: false,
detailLoading: false,
detailRow: {},
detailContractFileRows: [],
detailAttachmentRows: [],
detailBillingPlanRows: [],
detailBillingPlanBox: false,
detailBillingPlanIndex: -1,
detailBillingPlanForm: {},
detailSettlementConfigTab: 'pre',
detailPreSettlementRuleForm: defaultSettlementRule(),
detailFormalSettlementRuleForm: defaultSettlementRule(),
detailPaymentRatioRows: [],
detailChangeRecordRows: [],
flowBox: false,
flowUrl: '',
processInstanceId: '',
@@ -1069,6 +1269,27 @@ export default {
value: index + 1,
}));
},
detailContractPeriod() {
const startDate = this.detailRow.startDate || '-';
const endDate = this.detailRow.endDate || '-';
return `${startDate} 至 ${endDate}`;
},
detailLegalSealText() {
const value = this.detailRow.legalSealFlag;
if (value === undefined || value === null || value === '') return '-';
return Number(value) === 1 ? '是' : '否';
},
detailFeeGenerationMode() {
const value =
this.detailRow.feeGenerationMode ||
(Number(this.detailRow.billingEnabled) === 0 ? 'manual' : 'system');
return value === 'manual' ? '手动生成' : '系统生成';
},
detailSettlementRule() {
return this.detailSettlementConfigTab === 'formal'
? this.detailFormalSettlementRuleForm
: this.detailPreSettlementRuleForm;
},
},
watch: {
'$route.fullPath'() {
@@ -1718,16 +1939,55 @@ export default {
openDetail(row) {
this.detailBox = true;
this.detailLoading = true;
this.detailRow = { ...row };
this.applyDetailState(row);
this.api
.getDetail(row.id)
.then(res => {
this.detailRow = res.data?.data || row;
this.applyDetailState(res.data?.data || row);
})
.finally(() => {
this.detailLoading = false;
});
},
applyDetailState(detail = {}) {
this.detailRow = { ...detail };
this.detailContractFileRows = parseArray(detail.contractFileJson);
this.detailAttachmentRows = parseArray(detail.attachmentsJson);
this.detailBillingPlanRows = parseArray(detail.billingPlanJson);
this.detailPaymentRatioRows = parseArray(detail.paymentRatioJson);
this.detailChangeRecordRows = parseArray(detail.changeRecordJson);
const payload = parseObject(detail.settlementRuleJson);
const legacy = Object.keys(payload).some(
key => !['preSettlementConfig', 'formalSettlementConfig'].includes(key)
)
? payload
: {};
const preConfig = parseObject(detail.preSettlementConfigJson);
const formalConfig = parseObject(detail.formalSettlementConfigJson);
this.detailPreSettlementRuleForm = this.normalizeSettlementRule(
payload.preSettlementConfig || (Object.keys(preConfig).length ? preConfig : legacy)
);
this.detailFormalSettlementRuleForm = this.normalizeSettlementRule(
payload.formalSettlementConfig || (Object.keys(formalConfig).length ? formalConfig : legacy)
);
this.detailSettlementConfigTab = 'pre';
},
openDetailBillingPlan(row, index) {
this.detailBillingPlanForm = clone(row);
this.detailBillingPlanIndex = index;
this.detailBillingPlanBox = true;
},
displayValue(value) {
return value === undefined || value === null || value === '' ? '-' : value;
},
detailUnitValue(prop, unit) {
const value = this.detailRow[prop];
return value === undefined || value === null || value === '' ? '-' : `${value}${unit}`;
},
detailObjectUnitValue(row, prop, unit) {
const value = row?.[prop];
return value === undefined || value === null || value === '' ? '-' : `${value}${unit}`;
},
detailValue(prop) {
const value = this.detailRow[prop];
if (
@@ -1743,7 +2003,7 @@ export default {
if (prop === 'settlementRuleJson') return value ? '已配置' : '-';
if (prop === 'paymentRatioJson') return `${parseArray(value).length}项付款比例`;
if (prop === 'feeGenerationMode') return value === 'manual' ? '手动生成' : '系统生成';
return value === undefined || value === null || value === '' ? '-' : value;
return this.displayValue(value);
},
handleOperation(operation, row) {
if (operation.action === 'startChange') {
@@ -2107,8 +2367,28 @@ export default {
display: inline-flex;
}
.contract-manage-detail section + section {
margin-top: 20px;
.contract-manage-detail {
max-height: 74vh;
overflow-y: auto;
&__descriptions {
:deep(.el-descriptions__label) {
display: inline-block;
min-width: 96px;
padding-right: 12px;
color: #606266;
text-align: right;
}
:deep(.el-descriptions__content) {
color: #303133;
word-break: break-word;
}
:deep(.el-descriptions__cell) {
padding-bottom: 16px;
}
}
}
@media (max-width: 1200px) {
+1 -1
View File
@@ -2175,7 +2175,7 @@ export default {
if (draftMode) {
return true;
}
if (this.dialogMode === 'add' && this.selectedWaybillRows.length < 2) {
if (this.selectedWaybillRows.length < 2) {
ElMessage.warning('请至少选择两条待配载运单');
return false;
}
+26 -6
View File
@@ -797,11 +797,25 @@ export default {
result.push(
`是否接单:${node.confirmMode === 'no_confirm_accept' ? '无需确认接单' : '是'}`
);
result.push(
`确认接单人(司机):${node.confirmMode === 'yes' && node.confirmDriver ? '是' : '否'}`
);
return result;
}
if (node.key === 'return') {
if (node.uploadVoucher && node.voucherTypes.length) {
result.push(`上传凭证:${node.voucherTypes.join('、')}`);
result.push(
`是否确认:${node.confirmMode === 'no_confirm_complete' ? '无需确认完成' : '是'}`
);
result.push(
`确认完成(内部人员):${
node.confirmMode === 'yes' && node.confirmInternal ? '是' : '否'
}`
);
if (node.supportVoucher) {
result.push(`上传凭证:${node.uploadVoucher ? '是' : '否'}`);
if (node.uploadVoucher && node.voucherTypes.length) {
result.push(`凭证类型:${node.voucherTypes.join('、')}`);
}
}
return result;
}
@@ -816,11 +830,17 @@ export default {
result.push(`打卡操作:${node.punch ? '是' : '否'}`);
result.push(`打卡定位:${node.location ? '是' : '否'}`);
}
if (node.supportCargo && node.uploadCargo && node.cargoTypes.length) {
result.push(`上传货量:${node.cargoTypes.join('')}`);
if (node.supportCargo) {
result.push(`上传货量:${node.uploadCargo ? '是' : ''}`);
if (node.uploadCargo && node.cargoTypes.length) {
result.push(`上传货量类型:${node.cargoTypes.join('、')}`);
}
}
if (node.supportVoucher && node.uploadVoucher && node.voucherTypes.length) {
result.push(`上传凭证:${node.voucherTypes.join('')}`);
if (node.supportVoucher) {
result.push(`上传凭证:${node.uploadVoucher ? '是' : ''}`);
if (node.uploadVoucher && node.voucherTypes.length) {
result.push(`凭证类型:${node.voucherTypes.join('、')}`);
}
}
return result;
},
@@ -709,18 +709,33 @@ export default {
if (!rows.length) return;
const first = rows[0];
const settlementType = this.initialData.settlementType || first.settlementType || 'payable';
const contractId = first.contractId || null;
const matchedContract = this.allContracts.find(
item =>
(first.contractId && String(item.id) === String(first.contractId)) ||
(first.contractNo && String(item.contractNo) === String(first.contractNo))
);
const contractId = first.contractId || matchedContract?.id || null;
const projectId = first.projectId || matchedContract?.projectId || null;
const projectName = first.projectName || matchedContract?.projectName || '';
this.contracts = this.allContracts.filter(
item => projectId && String(item.projectId) === String(projectId)
);
if (projectId && !this.projects.some(item => String(item.id) === String(projectId))) {
this.projects.push({ id: projectId, name: projectName });
}
if (!this.contracts.some(item => String(item.id) === String(contractId))) {
this.contracts.push({
id: contractId,
contractNo: first.contractNo,
contractName: first.contractName,
projectId: first.projectId,
projectName: first.projectName,
deptId: first.deptId,
deptName: first.deptName,
projectId,
projectName,
deptId: first.deptId || matchedContract?.deptId,
deptName: first.deptName || matchedContract?.deptName,
payerName: first.payerName,
payeeName: first.payeeName,
partyA: matchedContract?.partyA,
partyB: matchedContract?.partyB,
settlementType,
});
}
@@ -729,8 +744,8 @@ export default {
contractId,
contractNo: first.contractNo || this.form.contractNo,
contractName: first.contractName || this.form.contractName,
projectId: first.projectId || this.form.projectId,
projectName: first.projectName || this.form.projectName,
projectId: projectId || this.form.projectId,
projectName: projectName || this.form.projectName,
deptId: first.deptId || this.form.deptId,
deptName: first.deptName || this.form.deptName,
payerName: first.payerName || this.form.payerName,
@@ -1035,10 +1050,7 @@ export default {
return columns.map((column, index) => {
if (index === 0) return '合计';
if (!sumProps.includes(column.property)) return '';
const total = data.reduce(
(sum, row) => sum + Number(row[column.property] || 0),
0
);
const total = data.reduce((sum, row) => sum + Number(row[column.property] || 0), 0);
return this.formatMoney(total);
});
},
@@ -1010,6 +1010,15 @@ export default {
this.$message.warning('请至少选择一条结算明细');
return;
}
const invalidManualFeeIndex = this.summaryFees.findIndex(
row =>
Number(row.manualFlag) === 1 &&
(!String(row.feeType || '').trim() || !String(row.feeItem || '').trim())
);
if (invalidManualFeeIndex >= 0) {
this.$message.warning(`请完善结算合计第${invalidManualFeeIndex + 1}行的费用类型和费用项`);
return;
}
const stateKey = shouldSubmit ? 'submitting' : 'saving';
this[stateKey] = true;
try {
@@ -1040,7 +1049,7 @@ export default {
sourceDetailIds: this.details.map(row => row.sourceDetailId || row.id),
summaryFees: this.summaryFees.map(row => ({
id: row.id || undefined,
feeType: row.feeType,
feeType: row.feeType || '',
feeItem: row.feeItem,
adjustAmount: Number(row.adjustAmount || 0),
remark: row.remark,
@@ -1067,7 +1076,7 @@ export default {
return this.feeOptions.find(item => item.feeType === feeType)?.feeItems || [];
},
feeCategoryName(value) {
if (value === undefined || value === null || value === '') return '-';
if (value === undefined || value === null || value === '') return '';
const option = this.feeCategoryOptions.find(
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
);
@@ -318,7 +318,7 @@
<el-form-item label="转结算类型">
<el-radio-group
v-model="transferForm.settlementBillType"
@change="loadTransferCandidates"
@change="handleTransferTypeChange"
>
<el-radio label="pre">预结算单</el-radio>
<el-radio label="formal">正式结算单</el-radio>
@@ -343,18 +343,26 @@
<el-input v-else v-model="transferQuery[field.prop]" clearable placeholder="请输入" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadTransferCandidates">查询</el-button>
<el-button type="primary" @click="handleTransferSearch">查询</el-button>
</el-form-item>
</el-form>
</div>
<el-table
ref="transferTableRef"
v-loading="transferDialog.loading"
:data="transferRows"
row-key="id"
border
@selection-change="transferSelectionChange"
>
<el-table-column type="selection" width="52" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column type="selection" width="52" align="center" reserve-selection />
<el-table-column
type="index"
label="序号"
width="70"
align="center"
:index="transferIndexMethod"
/>
<el-table-column
v-for="column in tableColumns.slice(0, 13)"
:key="column.prop"
@@ -367,6 +375,17 @@
<template #default="{ row }">{{ formatColumnValue(row, column) }}</template>
</el-table-column>
</el-table>
<div class="settlement-detail-page__pagination">
<el-pagination
v-model:current-page="transferPage.current"
v-model:page-size="transferPage.size"
:total="transferPage.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="loadTransferCandidates"
@size-change="handleTransferSizeChange"
/>
</div>
<template #footer>
<el-button @click="transferDialog.visible = false">取消</el-button>
<el-button type="primary" :loading="transferDialog.submitting" @click="submitTransfer">
@@ -593,6 +612,7 @@ export default {
transferForm: { settlementBillType: 'formal' },
transferRows: [],
transferSelection: [],
transferPage: { current: 1, size: 10, total: 0 },
generateDialog: { visible: false, loading: false },
generateQuery: {},
generateRows: [],
@@ -1029,11 +1049,12 @@ export default {
this.transferQuery = {};
this.transferRows = [];
this.transferSelection = [];
this.transferPage = { current: 1, size: 10, total: 0 };
this.$nextTick(() => this.$refs.transferTableRef?.clearSelection());
this.loadTransferCandidates();
},
async loadTransferCandidates() {
this.transferDialog.loading = true;
this.transferSelection = [];
try {
const params = this.buildRequestParams(
this.normalizeQuery(
@@ -1043,20 +1064,46 @@ export default {
'generateEndDate'
)
);
const res = await api.getTransferCandidates(1, 50, {
...params,
settlementStatus: 'pending',
settlementType: this.settlementType || undefined,
settlementBillType: this.transferForm.settlementBillType,
});
const res = await api.getTransferCandidates(
this.transferPage.current,
this.transferPage.size,
{
...params,
settlementStatus: 'pending',
settlementType: this.settlementType || undefined,
settlementBillType: this.transferForm.settlementBillType,
}
);
const data = this.unwrapPage(res);
this.transferRows = (data.records || [])
.filter(row => this.isTransferCandidate(row))
.map(this.decorateRow);
this.transferPage.total = data.total || 0;
} finally {
this.transferDialog.loading = false;
}
},
resetTransferSelection() {
this.transferSelection = [];
this.$nextTick(() => this.$refs.transferTableRef?.clearSelection());
},
handleTransferSearch() {
this.transferPage.current = 1;
this.resetTransferSelection();
this.loadTransferCandidates();
},
handleTransferTypeChange() {
this.transferPage.current = 1;
this.resetTransferSelection();
this.loadTransferCandidates();
},
handleTransferSizeChange() {
this.transferPage.current = 1;
this.loadTransferCandidates();
},
transferIndexMethod(index) {
return (this.transferPage.current - 1) * this.transferPage.size + index + 1;
},
isTransferCandidate(row) {
const hasValue = value => {
if (Array.isArray(value)) return value.length > 0;
@@ -1151,16 +1198,38 @@ export default {
}
},
async resolveTransferContractIds(rows) {
if (rows.every(item => item.contractId)) return rows;
if (rows.every(item => item.contractId && item.projectId)) return rows;
const contractId = rows.find(item => item.contractId)?.contractId;
const contractNo = rows.find(item => item.contractNo)?.contractNo;
const { data } = await getSettlementContractOptions(contractNo);
const contracts = data?.data || [];
const contract = contracts.find(item => String(item.contractNo) === String(contractNo));
if (!contract?.id) {
this.$message.warning('未找到所选明细对应的有效合同,无法转结算');
const contract = contracts.find(
item =>
(contractId && String(item.id) === String(contractId)) ||
(contractNo && String(item.contractNo) === String(contractNo))
);
if (!contract?.id || !contract.projectId) {
this.$message.warning('未找到所选明细对应的合同或项目信息,无法转结算');
return null;
}
return rows.map(item => ({ ...item, contractId: item.contractId || contract.id }));
return rows.map(item => {
const settlementType =
item.settlementType || this.settlementType || contract.settlementType;
return {
...item,
contractId: item.contractId || contract.id,
contractNo: item.contractNo || contract.contractNo,
contractName: item.contractName || contract.contractName,
projectId: item.projectId || contract.projectId,
projectName: item.projectName || contract.projectName,
deptId: item.deptId || contract.deptId,
deptName: item.deptName || contract.deptName,
payerName:
item.payerName || (settlementType === 'receivable' ? contract.partyB : contract.partyA),
payeeName:
item.payeeName || (settlementType === 'receivable' ? contract.partyA : contract.partyB),
};
});
},
openGenerateDialog() {
this.generateDialog.visible = true;
+9
View File
@@ -1309,6 +1309,11 @@ export default {
},
normalizeBirthday(value = '') {
const birthday = String(value || '').trim();
const compactMatch = birthday.match(/^(\d{4})(\d{2})(\d{2})$/);
if (compactMatch) {
const [, year, month, day] = compactMatch;
return `${year}-${month}-${day}`;
}
const match = birthday.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/);
if (!match) return birthday;
const [, year, month, day] = match;
@@ -1343,6 +1348,10 @@ export default {
}
const row = {
...this.driverForm,
birthday: this.normalizeBirthday(this.driverForm.birthday),
drivingLicenseStartDate: this.normalizeBirthday(this.driverForm.drivingLicenseStartDate),
drivingLicenseEndDate: this.normalizeBirthday(this.driverForm.drivingLicenseEndDate),
qualificationEndDate: this.normalizeBirthday(this.driverForm.qualificationEndDate),
qualificationNo: this.driverForm.qualificationNo || this.driverForm.idCardNo,
posts: this.driverForm.postList.join(','),
};
+2 -2
View File
@@ -78,7 +78,7 @@
placeholder="请输入"
@input="value => normalizeDecimalInput('directEconomicLoss', value)"
>
<template #suffix></template>
<template #append></template>
</el-input>
</template>
<template #insuranceClaimAmountForm>
@@ -89,7 +89,7 @@
placeholder="请输入"
@input="value => normalizeDecimalInput('insuranceClaimAmount', value)"
>
<template #suffix></template>
<template #append></template>
</el-input>
</template>
<template #attachments="{ row }">
@@ -78,7 +78,7 @@
placeholder="请输入"
@input="value => normalizeDecimalInput('fee', value)"
>
<template #suffix></template>
<template #append></template>
</el-input>
</template>
<template #attachments="{ row }">
+48 -4
View File
@@ -634,11 +634,20 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="文件名称" min-width="180">
<el-table-column label="文件名称" min-width="180" align="left">
<template #default="{ row }">
<el-link type="primary" :disabled="!row.url" @click="previewQualificationFile(row)">{{
row.originalName || row.name || '-'
}}</el-link>
<div
class="qualification-file-name-cell"
:title="row.originalName || row.name || '-'"
>
<span
class="qualification-file-name"
:class="{ 'is-disabled': !row.url }"
@click="row.url && previewQualificationFile(row)"
>
{{ row.originalName || row.name || '-' }}
</span>
</div>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220">
@@ -4461,6 +4470,41 @@ export default {
font-weight: 400;
}
.qualification-file-name-cell {
display: block;
width: 100%;
min-width: 0;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.qualification-file-name {
display: block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
cursor: pointer;
color: #606266;
transition: color 0.2s ease;
&:hover {
color: #409eff;
}
&.is-disabled {
cursor: default;
color: #606266;
&:hover {
color: #606266;
}
}
}
.qualification-upload-bar {
display: grid;
grid-template-columns: 1fr auto 1fr;
+30
View File
@@ -70,6 +70,15 @@
class="maintenance-plan-page__input"
/>
</template>
<template #address-form>
<el-input
v-model="form.address"
:disabled="boxType === 'view'"
readonly
placeholder="点击地图选址"
@click="openAddressMapPicker"
/>
</template>
<template #mileageForm>
<el-input
v-model="form.mileage"
@@ -140,6 +149,11 @@
</template>
</avue-form>
</el-dialog>
<address-map-picker
v-model="addressMapPickerVisible"
:address="form.address"
@confirm="handleAddressMapConfirm"
/>
</basic-container>
</template>
@@ -154,6 +168,7 @@ import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -163,6 +178,9 @@ const createTimeRangeMap = {
};
export default {
components: {
AddressMapPicker,
},
data() {
const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
@@ -178,6 +196,7 @@ export default {
query: {},
loading: true,
excelBox: false,
addressMapPickerVisible: false,
excelForm: {},
page: {
pageSize: 10,
@@ -203,6 +222,7 @@ export default {
border: true,
index: true,
indexLabel: '序号',
indexWidth: 90,
viewBtn: true,
selection: true,
dialogClickModal: false,
@@ -315,6 +335,8 @@ export default {
{
label: '地址',
prop: 'address',
slot: true,
formslot: true,
minWidth: 180,
overHidden: true,
span: 24,
@@ -483,6 +505,14 @@ export default {
},
},
methods: {
openAddressMapPicker() {
if (this.boxType !== 'view') {
this.addressMapPickerVisible = true;
}
},
handleAddressMapConfirm(address) {
this.form.address = address;
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
},
+30
View File
@@ -70,6 +70,15 @@
class="maintenance-record-page__input"
/>
</template>
<template #address-form>
<el-input
v-model="form.address"
:disabled="boxType === 'view'"
readonly
placeholder="点击地图选址"
@click="openAddressMapPicker"
/>
</template>
<template #costForm>
<el-input
v-model="form.cost"
@@ -126,6 +135,11 @@
</template>
</avue-form>
</el-dialog>
<address-map-picker
v-model="addressMapPickerVisible"
:address="form.address"
@confirm="handleAddressMapConfirm"
/>
</basic-container>
</template>
@@ -140,6 +154,7 @@ import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -149,6 +164,9 @@ const createTimeRangeMap = {
};
export default {
components: {
AddressMapPicker,
},
data() {
const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
@@ -164,6 +182,7 @@ export default {
query: {},
loading: true,
excelBox: false,
addressMapPickerVisible: false,
excelForm: {},
isDetailLoading: false,
page: {
@@ -190,6 +209,7 @@ export default {
border: true,
index: true,
indexLabel: '序号',
indexWidth: 90,
viewBtn: true,
selection: true,
dialogClickModal: false,
@@ -291,6 +311,8 @@ export default {
{
label: '地址',
prop: 'address',
slot: true,
formslot: true,
minWidth: 180,
overHidden: true,
span: 24,
@@ -478,6 +500,14 @@ export default {
},
},
methods: {
openAddressMapPicker() {
if (this.boxType !== 'view') {
this.addressMapPickerVisible = true;
}
},
handleAddressMapConfirm(address) {
this.form.address = address;
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
},
+4 -4
View File
@@ -90,7 +90,7 @@
placeholder="请输入"
@input="value => normalizeIntegerInput('previousMonthMileage', value)"
>
<template #suffix>km</template>
<template #append>km</template>
</el-input>
</template>
<template #currentMonthMileageForm>
@@ -101,7 +101,7 @@
placeholder="请输入"
@input="value => normalizeIntegerInput('currentMonthMileage', value)"
>
<template #suffix>km</template>
<template #append>km</template>
</el-input>
</template>
<template #monthlyMileageForm>
@@ -112,7 +112,7 @@
placeholder="请输入"
@input="value => normalizeIntegerInput('monthlyMileage', value)"
>
<template #suffix>km</template>
<template #append>km</template>
</el-input>
</template>
<template #totalMileageForm>
@@ -123,7 +123,7 @@
placeholder="请输入"
@input="value => normalizeIntegerInput('totalMileage', value)"
>
<template #suffix>km</template>
<template #append>km</template>
</el-input>
</template>
<template #totalMileageStart-search="{ row }">
+1 -1
View File
@@ -87,7 +87,7 @@
placeholder="请输入"
@input="value => normalizeDecimalInput('unitPrice', value)"
>
<template #suffix></template>
<template #append>/</template>
</el-input>
</template>
<template #transactionAmountForm>
+27
View File
@@ -110,6 +110,15 @@
@input="value => normalizeIntegerInput('deductPoints', value)"
/>
</template>
<template #location-form>
<el-input
v-model="form.location"
:disabled="boxType === 'view'"
readonly
placeholder="点击地图选址"
@click="openLocationMapPicker"
/>
</template>
<template #processStatus="{ row }">
<span :class="row.processStatus === '未处理' ? 'violation-record-page__danger' : ''">
{{ row.processStatus }}
@@ -152,6 +161,11 @@
</template>
</avue-form>
</el-dialog>
<address-map-picker
v-model="locationMapPickerVisible"
:address="form.location"
@confirm="handleLocationMapConfirm"
/>
</basic-container>
</template>
@@ -167,6 +181,7 @@ import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { getToken } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import { mapGetters } from 'vuex';
import { excelOption, option } from '@/option/vehicle/violation-record';
import NProgress from 'nprogress';
@@ -177,12 +192,16 @@ const createTimeRangeMap = {
};
export default {
components: {
AddressMapPicker,
},
data() {
return {
form: {},
query: {},
loading: true,
excelBox: false,
locationMapPickerVisible: false,
excelForm: {},
isDetailLoading: false,
option,
@@ -242,6 +261,14 @@ export default {
},
},
methods: {
openLocationMapPicker() {
if (this.boxType !== 'view') {
this.locationMapPickerVisible = true;
}
},
handleLocationMapConfirm(address) {
this.form.location = address;
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
},