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
+252
View File
@@ -0,0 +1,252 @@
<template>
<el-dialog
v-model="visible"
title="地图选择地址"
width="920px"
append-to-body
@opened="initMap"
>
<div class="address-map-picker__toolbar">
<el-input
v-model="keyword"
clearable
placeholder="输入地址关键词"
@keyup.enter="searchKeyword"
/>
<el-button type="primary" :loading="searching" @click="searchKeyword">搜索</el-button>
</div>
<div ref="map" class="address-map-picker__map"></div>
<div class="address-map-picker__info">{{ status }}</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :disabled="!selected.longitude || resolving" @click="confirm">
确定
</el-button>
</template>
</el-dialog>
</template>
<script>
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let amapLoader;
export default {
props: {
modelValue: {
type: Boolean,
default: false,
},
address: {
type: String,
default: '',
},
},
emits: ['update:modelValue', 'confirm'],
data() {
return {
visible: false,
keyword: '',
status: '可搜索地址或点击地图选点',
searching: false,
resolving: false,
selected: {},
amap: null,
marker: null,
geocoder: null,
};
},
watch: {
modelValue: {
immediate: true,
handler(value) {
this.visible = value;
if (value) {
this.keyword = this.address || '';
this.selected = {};
this.status = '可搜索地址或点击地图选点';
}
},
},
visible(value) {
this.$emit('update:modelValue', value);
},
},
beforeUnmount() {
if (this.amap) {
this.amap.destroy();
}
},
methods: {
loadAmap() {
if (window.AMap && window.AMap.Map) {
return Promise.resolve();
}
if (!amapLoader) {
amapLoader = new Promise((resolve, reject) => {
window._AMapSecurityConfig = { securityJsCode: AMAP_SECURITY_CODE };
const script = document.createElement('script');
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`;
script.async = true;
script.onload = resolve;
script.onerror = () => reject(new Error('高德地图组件加载失败'));
document.body.appendChild(script);
});
}
return amapLoader;
},
initMap() {
this.loadAmap()
.then(() => {
this.$nextTick(() => {
if (!this.amap) {
this.amap = new window.AMap.Map(this.$refs.map, {
center: [116.40769, 39.89945],
zoom: 11,
});
this.amap.on('click', event => this.pickPoint(event.lnglat));
} else {
this.amap.resize();
this.clearMarker();
}
});
})
.catch(() => {
this.$message.error('高德地图组件加载失败,请稍后重试');
this.visible = false;
});
},
ensureGeocoder() {
if (this.geocoder) return Promise.resolve(this.geocoder);
return new Promise((resolve, reject) => {
window.AMap.plugin(['AMap.Geocoder'], () => {
try {
this.geocoder = new window.AMap.Geocoder();
resolve(this.geocoder);
} catch (error) {
reject(error);
}
});
});
},
runGeocode(action, input) {
return this.ensureGeocoder().then(
geocoder =>
new Promise((resolve, reject) => {
const done = (status, result) => {
if (status === 'complete' && result) {
resolve(result);
} else {
reject(new Error('高德地图请求失败'));
}
};
if (action === 'location') {
geocoder.getLocation(input, done);
} else {
geocoder.getAddress(input, done);
}
})
);
},
searchKeyword() {
const keyword = String(this.keyword || '').trim();
if (!keyword) {
this.$message.warning('请输入地址关键词');
return;
}
this.searching = true;
this.loadAmap()
.then(() => this.runGeocode('location', keyword))
.then(result => {
const point = result.geocodes?.[0]?.location || result.location;
if (!point) {
throw new Error('未找到匹配地址');
}
this.pickPoint(point, keyword);
})
.catch(() => {
this.status = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
})
.finally(() => {
this.searching = false;
});
},
pickPoint(point, fallbackAddress = '') {
const longitude = typeof point.getLng === 'function' ? point.getLng() : point.lng;
const latitude = typeof point.getLat === 'function' ? point.getLat() : point.lat;
if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
this.$message.warning('选点坐标无效');
return;
}
const lnglat = new window.AMap.LngLat(Number(longitude), Number(latitude));
this.renderMarker(lnglat);
this.selected = { longitude: Number(longitude), latitude: Number(latitude), address: fallbackAddress };
this.status = '正在解析地址...';
this.resolving = true;
this.runGeocode('address', lnglat)
.then(result => {
const detailAddress = result.regeocode?.formattedAddress || fallbackAddress;
this.selected = { ...this.selected, address: detailAddress };
this.keyword = detailAddress || this.keyword;
this.status = detailAddress || '已选点,可确认回填';
})
.catch(() => {
this.status = '地址解析失败,请重新选点';
this.$message.error('地址解析失败,请重新选点');
this.selected = {};
this.clearMarker();
})
.finally(() => {
this.resolving = false;
});
},
renderMarker(point) {
this.clearMarker();
this.marker = new window.AMap.Marker({ position: point });
this.marker.setMap(this.amap);
this.amap.setCenter(point);
},
clearMarker() {
if (this.marker) {
this.marker.setMap(null);
this.marker = null;
}
},
confirm() {
if (!this.selected.address) {
this.$message.warning('请等待地址解析完成后再确认');
return;
}
this.$emit('confirm', this.selected.address);
this.visible = false;
},
},
};
</script>
<style lang="scss" scoped>
.address-map-picker {
&__toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
.el-input {
flex: 1;
}
}
&__map {
width: 100%;
height: 420px;
border: 1px solid #eff1f7;
}
&__info {
margin-top: 10px;
color: #606266;
line-height: 1.6;
}
}
</style>
+4
View File
@@ -202,6 +202,7 @@ export const option = createCrudOption([
label: '单价', label: '单价',
prop: 'cargoValue', prop: 'cargoValue',
formslot: true, formslot: true,
formatter: row => (String(row.cargoValue) === '-1' ? '' : row.cargoValue),
minWidth: 120, minWidth: 120,
rules: [ rules: [
{ {
@@ -277,6 +278,9 @@ export const option = createCrudOption([
), ),
]); ]);
// 搜索栏标签统一加宽,给较长的货物类型字段预留足够展示空间。
option.searchLabelWidth = 160;
export const excelOption = { export const excelOption = {
submitBtn: false, submitBtn: false,
emptyBtn: false, emptyBtn: false,
+3
View File
@@ -50,6 +50,7 @@ export const option = {
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
indexWidth: 90,
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
@@ -128,6 +129,7 @@ export const option = {
prop: 'directEconomicLoss', prop: 'directEconomicLoss',
type: 'input', type: 'input',
formslot: true, formslot: true,
append: '元',
minWidth: 140, minWidth: 140,
span: 12, span: 12,
placeholder: '请输入', placeholder: '请输入',
@@ -138,6 +140,7 @@ export const option = {
prop: 'insuranceClaimAmount', prop: 'insuranceClaimAmount',
type: 'input', type: 'input',
formslot: true, formslot: true,
append: '元',
minWidth: 140, minWidth: 140,
span: 12, span: 12,
placeholder: '请输入', placeholder: '请输入',
@@ -49,6 +49,7 @@ export const option = {
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
indexWidth: 90,
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
@@ -145,6 +146,7 @@ export const option = {
prop: 'fee', prop: 'fee',
type: 'input', type: 'input',
formslot: true, formslot: true,
append: '元',
minWidth: 100, minWidth: 100,
span: 12, span: 12,
placeholder: '请输入', placeholder: '请输入',
+1
View File
@@ -30,6 +30,7 @@ export const option = {
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
indexWidth: 90,
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
+1
View File
@@ -39,6 +39,7 @@ export const option = {
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
indexWidth: 90,
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
+1
View File
@@ -43,6 +43,7 @@ export const option = {
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
indexWidth: 90,
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
+5
View File
@@ -35,6 +35,7 @@ export const option = {
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
indexWidth: 90,
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
@@ -70,6 +71,7 @@ export const option = {
prop: 'previousMonthMileage', prop: 'previousMonthMileage',
type: 'input', type: 'input',
formslot: true, formslot: true,
append: 'km',
minWidth: 210, minWidth: 210,
slot: true, slot: true,
rules: [{ validator: validateNonNegative, trigger: 'blur' }], rules: [{ validator: validateNonNegative, trigger: 'blur' }],
@@ -79,6 +81,7 @@ export const option = {
prop: 'currentMonthMileage', prop: 'currentMonthMileage',
type: 'input', type: 'input',
formslot: true, formslot: true,
append: 'km',
minWidth: 210, minWidth: 210,
slot: true, slot: true,
rules: [{ validator: validateNonNegative, trigger: 'blur' }], rules: [{ validator: validateNonNegative, trigger: 'blur' }],
@@ -88,6 +91,7 @@ export const option = {
prop: 'monthlyMileage', prop: 'monthlyMileage',
type: 'input', type: 'input',
formslot: true, formslot: true,
append: 'km',
minWidth: 170, minWidth: 170,
slot: true, slot: true,
rules: [{ validator: validateNonNegative, trigger: 'blur' }], rules: [{ validator: validateNonNegative, trigger: 'blur' }],
@@ -97,6 +101,7 @@ export const option = {
prop: 'totalMileage', prop: 'totalMileage',
type: 'input', type: 'input',
formslot: true, formslot: true,
append: 'km',
minWidth: 170, minWidth: 170,
slot: true, slot: true,
rules: [{ validator: validateNonNegative, trigger: 'blur' }], rules: [{ validator: validateNonNegative, trigger: 'blur' }],
@@ -36,6 +36,7 @@ export const option = {
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
indexWidth: 90,
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
@@ -123,6 +124,7 @@ export const option = {
prop: 'unitPrice', prop: 'unitPrice',
type: 'input', type: 'input',
formslot: true, formslot: true,
append: '元/升',
minWidth: 120, minWidth: 120,
rules: [{ validator: validateNonNegative, trigger: 'blur' }], rules: [{ validator: validateNonNegative, trigger: 'blur' }],
}, },
@@ -47,6 +47,7 @@ export const option = {
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
indexWidth: 90,
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
@@ -40,6 +40,7 @@ export const option = {
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
indexWidth: 90,
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
@@ -27,6 +27,7 @@ export const option = {
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
indexWidth: 90,
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
+3
View File
@@ -50,6 +50,7 @@ export const option = {
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
indexWidth: 90,
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
@@ -127,6 +128,8 @@ export const option = {
{ {
label: '地点', label: '地点',
prop: 'location', prop: 'location',
slot: true,
formslot: true,
minWidth: 180, minWidth: 180,
overHidden: true, overHidden: true,
span: 12, span: 12,
+19 -1
View File
@@ -14,6 +14,7 @@
@row-del="rowDel" @row-del="rowDel"
@search-change="searchChange" @search-change="searchChange"
@search-reset="searchReset" @search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange" @current-change="currentChange"
@size-change="sizeChange" @size-change="sizeChange"
@refresh-change="refreshChange" @refresh-change="refreshChange"
@@ -40,6 +41,7 @@
v-model="form.feeCategory" v-model="form.feeCategory"
class="fee-item-form-control" class="fee-item-form-control"
clearable clearable
:disabled="dialogType !== 'add'"
filterable filterable
placeholder="请选择费用类型" placeholder="请选择费用类型"
@change="handleFeeCategoryChange" @change="handleFeeCategoryChange"
@@ -58,6 +60,7 @@
:maxlength="feeItemCodeMaxlength" :maxlength="feeItemCodeMaxlength"
class="fee-item-form-control" class="fee-item-form-control"
clearable clearable
:disabled="dialogType !== 'add'"
placeholder="请输入费用项代码" placeholder="请输入费用项代码"
@change="handleFeeItemCodeChange" @change="handleFeeItemCodeChange"
> >
@@ -114,12 +117,14 @@ export default {
data: [], data: [],
excelBox: false, excelBox: false,
excelForm: {}, excelForm: {},
dialogType: 'add',
feeCategoryOptions: [], feeCategoryOptions: [],
page: { page: {
pageSize: 10, pageSize: 10,
currentPage: 1, currentPage: 1,
total: 0, total: 0,
}, },
selectionList: [],
option: { option: {
height: 'auto', height: 'auto',
calcHeight: 32, calcHeight: 32,
@@ -139,7 +144,7 @@ export default {
viewBtn: false, viewBtn: false,
delBtn: false, delBtn: false,
editBtn: false, editBtn: false,
selection: false, selection: true,
dialogClickModal: false, dialogClickModal: false,
menuWidth: 180, menuWidth: 180,
column: [ column: [
@@ -294,6 +299,9 @@ export default {
feeItemCodeMaxlength() { feeItemCodeMaxlength() {
return Math.max(1, 100 - this.feeCategoryPrefix.length); return Math.max(1, 100 - this.feeCategoryPrefix.length);
}, },
ids() {
return this.selectionList.map(item => item.id).join(',');
},
permissionList() { permissionList() {
return { return {
addBtn: this.validData(this.permission.fee_item_add, false), addBtn: this.validData(this.permission.fee_item_add, false),
@@ -418,6 +426,7 @@ export default {
}); });
}, },
beforeOpen(done, type) { beforeOpen(done, type) {
this.dialogType = type || 'add';
if (['edit', 'view'].includes(type)) { if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => { getDetail(this.form.id).then(res => {
this.form = this.normalizeFormForEdit(res.data.data || {}); this.form = this.normalizeFormForEdit(res.data.data || {});
@@ -439,6 +448,13 @@ export default {
this.onLoad(this.page, params); this.onLoad(this.page, params);
done(); done();
}, },
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
currentChange(currentPage) { currentChange(currentPage) {
this.page.currentPage = currentPage; this.page.currentPage = currentPage;
}, },
@@ -454,6 +470,7 @@ export default {
buildExportParams() { buildExportParams() {
return { return {
...this.query, ...this.query,
ids: this.ids,
[this.website.tokenHeader]: getToken(), [this.website.tokenHeader]: getToken(),
}; };
}, },
@@ -490,6 +507,7 @@ export default {
const data = res.data.data; const data = res.data.data;
this.page.total = data.total; this.page.total = data.total;
this.data = data.records; this.data = data.records;
this.selectionClear();
this.loading = false; this.loading = false;
}); });
}, },
+4 -86
View File
@@ -250,81 +250,6 @@ import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
const DEFAULT_COUNTRY_CODE = '+86'; 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 = '请选择类型'; const newLocal = '请选择类型';
export default { export default {
data() { data() {
@@ -1323,9 +1248,6 @@ export default {
'港口码头主数据', '港口码头主数据',
() => { () => {
this.loadPortOptions(); this.loadPortOptions();
},
{
failDetailDecorator: removeWorkbookColumnsByHeaders,
} }
); );
}, },
@@ -1339,11 +1261,8 @@ export default {
exportBlob('/blade-system/port-terminal/export-port-terminal', this.buildExportParams(), { exportBlob('/blade-system/port-terminal/export-port-terminal', this.buildExportParams(), {
feedback: true, feedback: true,
}) })
.then(async res => { .then(res => {
const blob = await removeExcelColumnsByHeaders(res.data, { downloadXls(res.data, `港口码头主数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
markRequiredHeaders: true,
});
downloadXls(blob, `港口码头主数据${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
}) })
.finally(() => { .finally(() => {
NProgress.done(); NProgress.done();
@@ -1362,9 +1281,8 @@ export default {
`/blade-system/port-terminal/export-template?${this.website.tokenHeader}=${getToken()}`, `/blade-system/port-terminal/export-template?${this.website.tokenHeader}=${getToken()}`,
undefined, undefined,
{ feedback: true } { feedback: true }
).then(async res => { ).then(res => {
const blob = await removeExcelColumnsByHeaders(res.data); downloadXls(res.data, '港口码头主数据模板.xlsx');
downloadXls(blob, '港口码头主数据模板.xlsx');
}); });
}, },
}, },
+1
View File
@@ -689,6 +689,7 @@ export default {
feedback: true, feedback: true,
}).then(res => { }).then(res => {
downloadXls(res.data, '行政区划模板.xlsx'); downloadXls(res.data, '行政区划模板.xlsx');
this.$message.success('模板下载成功');
}); });
}, },
}, },
+17 -2
View File
@@ -399,8 +399,17 @@ export default {
if (submitRow.cargoValue !== undefined && submitRow.cargoValue !== null) { if (submitRow.cargoValue !== undefined && submitRow.cargoValue !== null) {
submitRow.cargoValue = String(submitRow.cargoValue).trim(); submitRow.cargoValue = String(submitRow.cargoValue).trim();
} }
if (!submitRow.cargoValue || String(submitRow.cargoValue) === '-1') {
submitRow.cargoValue = null;
}
return submitRow; return submitRow;
}, },
normalizeCargoValue(row) {
if (row && String(row.cargoValue) === '-1') {
row.cargoValue = null;
}
return row;
},
validateRow(row) { validateRow(row) {
if (!row.firstCargoTypeName || !row.firstCargoTypeCode) { if (!row.firstCargoTypeName || !row.firstCargoTypeCode) {
this.$message.warning('请选择一级货物类型'); this.$message.warning('请选择一级货物类型');
@@ -539,7 +548,7 @@ export default {
} }
if (['edit', 'view'].includes(type)) { if (['edit', 'view'].includes(type)) {
api.getDetail(this.form.id).then(res => { api.getDetail(this.form.id).then(res => {
this.form = res.data.data || {}; this.form = this.normalizeCargoValue(res.data.data || {});
this.ensureCargoTypeOption('firstCargoTypeOptions', { this.ensureCargoTypeOption('firstCargoTypeOptions', {
cargoName: this.form.firstCargoTypeName, cargoName: this.form.firstCargoTypeName,
cargoCode: this.form.firstCargoTypeCode, cargoCode: this.form.firstCargoTypeCode,
@@ -593,7 +602,7 @@ export default {
.then(res => { .then(res => {
const result = res.data.data; const result = res.data.data;
this.page.total = result.total; this.page.total = result.total;
this.data = result.records; this.data = (result.records || []).map(row => this.normalizeCargoValue(row));
this.selectionClear(); this.selectionClear();
}) })
.finally(() => { .finally(() => {
@@ -654,6 +663,12 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.common-cargo-page { .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 { &__value-unit {
display: flex; display: flex;
align-items: center; align-items: center;
File diff suppressed because it is too large Load Diff
@@ -2105,7 +2105,7 @@
</template> </template>
<template #menu="{ row, index }"> <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 type="primary" v-if="showDetailButton(row)" @click="openDetail(row)"
>详情</el-link >详情</el-link
> >
@@ -2144,7 +2144,7 @@
{{ operation.label }} {{ operation.label }}
</el-link> </el-link>
<el-link type="danger" v-if="canDelete(row)" @click="rowDel(row)"> 删除 </el-link> <el-link type="danger" v-if="canDelete(row)" @click="rowDel(row)"> 删除 </el-link>
</template> </div>
</template> </template>
</component> </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) { :deep(.business-crud-page__full-form-item) {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
@@ -1705,6 +1705,10 @@ export default {
departureAddress: this.form.arrivalAddress, departureAddress: this.form.arrivalAddress,
departureContact: this.form.arrivalContact, departureContact: this.form.arrivalContact,
departurePhone: this.form.arrivalPhone, departurePhone: this.form.arrivalPhone,
departureLongitude: this.form.arrivalLongitude,
departureLatitude: this.form.arrivalLatitude,
departureRegionCode: this.form.arrivalRegionCode,
departureSiteCode: this.form.arrivalSiteCode,
transportType: this.form.finalTransportType, transportType: this.form.finalTransportType,
}, },
], ],
+301 -21
View File
@@ -580,29 +580,205 @@
title="合同详情" title="合同详情"
append-to-body append-to-body
destroy-on-close destroy-on-close
width="1200px" class="contract-manage-detail-dialog"
width="92%"
top="4vh" top="4vh"
> >
<div v-loading="detailLoading" class="contract-manage-detail"> <div v-loading="detailLoading" class="contract-manage-form contract-manage-detail">
<section v-for="section in config.detailSections" :key="section.title"> <section class="contract-manage-form__section contract-manage-form__section--panel">
<div class="dialog-section-title">{{ section.title }}</div> <div class="dialog-section-title">基本信息</div>
<el-descriptions :column="3" border> <el-descriptions :column="3" class="contract-manage-detail__descriptions">
<el-descriptions-item <el-descriptions-item label="签约类型">{{
v-for="field in section.fields" detailValue('signType')
:key="field[0]" }}</el-descriptions-item>
:label="field[1]" <el-descriptions-item label="合同编号">{{
:span="field[2] || 1" detailValue('contractNo')
> }}</el-descriptions-item>
{{ detailValue(field[0]) }} <el-descriptions-item label="合同名称">{{
</el-descriptions-item> 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> </el-descriptions>
</section> </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> </div>
<template #footer <template #footer
><el-button type="primary" @click="detailBox = false">关闭</el-button></template ><el-button type="primary" @click="detailBox = false">关闭</el-button></template
> >
</el-dialog> </el-dialog>
<billing-plan-editor
v-model="detailBillingPlanBox"
:value="detailBillingPlanForm"
:index="detailBillingPlanIndex"
readonly
/>
<flow-design <flow-design
v-if="website.design.designMode" v-if="website.design.designMode"
is-dialog is-dialog
@@ -743,6 +919,7 @@ const AttachmentSection = defineComponent({
title: { type: String, required: true }, title: { type: String, required: true },
rows: { type: Array, default: () => [] }, rows: { type: Array, default: () => [] },
description: Boolean, description: Boolean,
readonly: Boolean,
preview: { type: Function, default: null }, preview: { type: Function, default: null },
}, },
emits: ['update:rows'], emits: ['update:rows'],
@@ -797,7 +974,9 @@ const AttachmentSection = defineComponent({
const ElTableColumn = resolveComponent('el-table-column'); const ElTableColumn = resolveComponent('el-table-column');
const VehicleAttachmentUpload = resolveComponent('vehicle-attachment-upload'); const VehicleAttachmentUpload = resolveComponent('vehicle-attachment-upload');
const columns = [ 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, { type: 'index', label: '序号', width: 70, align: 'center' }),
h( h(
ElTableColumn, ElTableColumn,
@@ -828,7 +1007,9 @@ const AttachmentSection = defineComponent({
{ label: '附件描述', minWidth: 220 }, { label: '附件描述', minWidth: 220 },
{ {
default: ({ row }) => default: ({ row }) =>
h(ElInput, { this.readonly
? row.description || '-'
: h(ElInput, {
modelValue: row.description, modelValue: row.description,
maxlength: 200, maxlength: 200,
'onUpdate:modelValue': value => { 'onUpdate:modelValue': value => {
@@ -855,7 +1036,10 @@ const AttachmentSection = defineComponent({
width: 180, width: 180,
align: 'center', align: 'center',
sortable: true, sortable: true,
}), })
);
if (!this.readonly) {
columns.push(
h( h(
ElTableColumn, ElTableColumn,
{ label: '操作', width: 100, align: 'center', fixed: 'right' }, { label: '操作', width: 100, align: 'center', fixed: 'right' },
@@ -865,6 +1049,7 @@ const AttachmentSection = defineComponent({
} }
) )
); );
}
return h( return h(
'section', 'section',
{ {
@@ -874,6 +1059,8 @@ const AttachmentSection = defineComponent({
h('div', { class: 'dialog-section-title' }, this.title), h('div', { class: 'dialog-section-title' }, this.title),
h('div', { class: 'contract-manage-form__attachment-head' }, [ h('div', { class: 'contract-manage-form__attachment-head' }, [
h('div', { class: 'contract-manage-form__attachment-actions' }, [ h('div', { class: 'contract-manage-form__attachment-actions' }, [
...(!this.readonly
? [
h(VehicleAttachmentUpload, { h(VehicleAttachmentUpload, {
modelValue: this.rows, modelValue: this.rows,
fileTypes: this.attachmentFileTypes, fileTypes: this.attachmentFileTypes,
@@ -884,6 +1071,8 @@ const AttachmentSection = defineComponent({
'onUpdate:modelValue': this.handleChange, 'onUpdate:modelValue': this.handleChange,
onChange: this.handleChange, onChange: this.handleChange,
}), }),
]
: []),
h( h(
ElButton, ElButton,
{ type: 'primary', disabled: !this.rows.length, onClick: this.batchDownload }, { type: 'primary', disabled: !this.rows.length, onClick: this.batchDownload },
@@ -990,6 +1179,17 @@ export default {
detailBox: false, detailBox: false,
detailLoading: false, detailLoading: false,
detailRow: {}, detailRow: {},
detailContractFileRows: [],
detailAttachmentRows: [],
detailBillingPlanRows: [],
detailBillingPlanBox: false,
detailBillingPlanIndex: -1,
detailBillingPlanForm: {},
detailSettlementConfigTab: 'pre',
detailPreSettlementRuleForm: defaultSettlementRule(),
detailFormalSettlementRuleForm: defaultSettlementRule(),
detailPaymentRatioRows: [],
detailChangeRecordRows: [],
flowBox: false, flowBox: false,
flowUrl: '', flowUrl: '',
processInstanceId: '', processInstanceId: '',
@@ -1069,6 +1269,27 @@ export default {
value: index + 1, 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: { watch: {
'$route.fullPath'() { '$route.fullPath'() {
@@ -1718,16 +1939,55 @@ export default {
openDetail(row) { openDetail(row) {
this.detailBox = true; this.detailBox = true;
this.detailLoading = true; this.detailLoading = true;
this.detailRow = { ...row }; this.applyDetailState(row);
this.api this.api
.getDetail(row.id) .getDetail(row.id)
.then(res => { .then(res => {
this.detailRow = res.data?.data || row; this.applyDetailState(res.data?.data || row);
}) })
.finally(() => { .finally(() => {
this.detailLoading = false; 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) { detailValue(prop) {
const value = this.detailRow[prop]; const value = this.detailRow[prop];
if ( if (
@@ -1743,7 +2003,7 @@ export default {
if (prop === 'settlementRuleJson') return value ? '已配置' : '-'; if (prop === 'settlementRuleJson') return value ? '已配置' : '-';
if (prop === 'paymentRatioJson') return `${parseArray(value).length}项付款比例`; if (prop === 'paymentRatioJson') return `${parseArray(value).length}项付款比例`;
if (prop === 'feeGenerationMode') return value === 'manual' ? '手动生成' : '系统生成'; if (prop === 'feeGenerationMode') return value === 'manual' ? '手动生成' : '系统生成';
return value === undefined || value === null || value === '' ? '-' : value; return this.displayValue(value);
}, },
handleOperation(operation, row) { handleOperation(operation, row) {
if (operation.action === 'startChange') { if (operation.action === 'startChange') {
@@ -2107,8 +2367,28 @@ export default {
display: inline-flex; display: inline-flex;
} }
.contract-manage-detail section + section { .contract-manage-detail {
margin-top: 20px; 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) { @media (max-width: 1200px) {
+1 -1
View File
@@ -2175,7 +2175,7 @@ export default {
if (draftMode) { if (draftMode) {
return true; return true;
} }
if (this.dialogMode === 'add' && this.selectedWaybillRows.length < 2) { if (this.selectedWaybillRows.length < 2) {
ElMessage.warning('请至少选择两条待配载运单'); ElMessage.warning('请至少选择两条待配载运单');
return false; return false;
} }
+25 -5
View File
@@ -797,11 +797,25 @@ export default {
result.push( result.push(
`是否接单:${node.confirmMode === 'no_confirm_accept' ? '无需确认接单' : '是'}` `是否接单:${node.confirmMode === 'no_confirm_accept' ? '无需确认接单' : '是'}`
); );
result.push(
`确认接单人(司机):${node.confirmMode === 'yes' && node.confirmDriver ? '是' : '否'}`
);
return result; return result;
} }
if (node.key === 'return') { if (node.key === 'return') {
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) { if (node.uploadVoucher && node.voucherTypes.length) {
result.push(`上传凭证:${node.voucherTypes.join('、')}`); result.push(`凭证类型${node.voucherTypes.join('、')}`);
}
} }
return result; return result;
} }
@@ -816,11 +830,17 @@ export default {
result.push(`打卡操作:${node.punch ? '是' : '否'}`); result.push(`打卡操作:${node.punch ? '是' : '否'}`);
result.push(`打卡定位:${node.location ? '是' : '否'}`); result.push(`打卡定位:${node.location ? '是' : '否'}`);
} }
if (node.supportCargo && node.uploadCargo && node.cargoTypes.length) { if (node.supportCargo) {
result.push(`上传货量:${node.cargoTypes.join('')}`); result.push(`上传货量:${node.uploadCargo ? '是' : ''}`);
if (node.uploadCargo && node.cargoTypes.length) {
result.push(`上传货量类型:${node.cargoTypes.join('、')}`);
}
}
if (node.supportVoucher) {
result.push(`上传凭证:${node.uploadVoucher ? '是' : '否'}`);
if (node.uploadVoucher && node.voucherTypes.length) {
result.push(`凭证类型:${node.voucherTypes.join('、')}`);
} }
if (node.supportVoucher && node.uploadVoucher && node.voucherTypes.length) {
result.push(`上传凭证:${node.voucherTypes.join('、')}`);
} }
return result; return result;
}, },
@@ -709,18 +709,33 @@ export default {
if (!rows.length) return; if (!rows.length) return;
const first = rows[0]; const first = rows[0];
const settlementType = this.initialData.settlementType || first.settlementType || 'payable'; 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))) { if (!this.contracts.some(item => String(item.id) === String(contractId))) {
this.contracts.push({ this.contracts.push({
id: contractId, id: contractId,
contractNo: first.contractNo, contractNo: first.contractNo,
contractName: first.contractName, contractName: first.contractName,
projectId: first.projectId, projectId,
projectName: first.projectName, projectName,
deptId: first.deptId, deptId: first.deptId || matchedContract?.deptId,
deptName: first.deptName, deptName: first.deptName || matchedContract?.deptName,
payerName: first.payerName, payerName: first.payerName,
payeeName: first.payeeName, payeeName: first.payeeName,
partyA: matchedContract?.partyA,
partyB: matchedContract?.partyB,
settlementType, settlementType,
}); });
} }
@@ -729,8 +744,8 @@ export default {
contractId, contractId,
contractNo: first.contractNo || this.form.contractNo, contractNo: first.contractNo || this.form.contractNo,
contractName: first.contractName || this.form.contractName, contractName: first.contractName || this.form.contractName,
projectId: first.projectId || this.form.projectId, projectId: projectId || this.form.projectId,
projectName: first.projectName || this.form.projectName, projectName: projectName || this.form.projectName,
deptId: first.deptId || this.form.deptId, deptId: first.deptId || this.form.deptId,
deptName: first.deptName || this.form.deptName, deptName: first.deptName || this.form.deptName,
payerName: first.payerName || this.form.payerName, payerName: first.payerName || this.form.payerName,
@@ -1035,10 +1050,7 @@ export default {
return columns.map((column, index) => { return columns.map((column, index) => {
if (index === 0) return '合计'; if (index === 0) return '合计';
if (!sumProps.includes(column.property)) return ''; if (!sumProps.includes(column.property)) return '';
const total = data.reduce( const total = data.reduce((sum, row) => sum + Number(row[column.property] || 0), 0);
(sum, row) => sum + Number(row[column.property] || 0),
0
);
return this.formatMoney(total); return this.formatMoney(total);
}); });
}, },
@@ -1010,6 +1010,15 @@ export default {
this.$message.warning('请至少选择一条结算明细'); this.$message.warning('请至少选择一条结算明细');
return; 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'; const stateKey = shouldSubmit ? 'submitting' : 'saving';
this[stateKey] = true; this[stateKey] = true;
try { try {
@@ -1040,7 +1049,7 @@ export default {
sourceDetailIds: this.details.map(row => row.sourceDetailId || row.id), sourceDetailIds: this.details.map(row => row.sourceDetailId || row.id),
summaryFees: this.summaryFees.map(row => ({ summaryFees: this.summaryFees.map(row => ({
id: row.id || undefined, id: row.id || undefined,
feeType: row.feeType, feeType: row.feeType || '',
feeItem: row.feeItem, feeItem: row.feeItem,
adjustAmount: Number(row.adjustAmount || 0), adjustAmount: Number(row.adjustAmount || 0),
remark: row.remark, remark: row.remark,
@@ -1067,7 +1076,7 @@ export default {
return this.feeOptions.find(item => item.feeType === feeType)?.feeItems || []; return this.feeOptions.find(item => item.feeType === feeType)?.feeItems || [];
}, },
feeCategoryName(value) { feeCategoryName(value) {
if (value === undefined || value === null || value === '') return '-'; if (value === undefined || value === null || value === '') return '';
const option = this.feeCategoryOptions.find( const option = this.feeCategoryOptions.find(
item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value) item => String(item.dictKey) === String(value) || String(item.dictValue) === String(value)
); );
@@ -318,7 +318,7 @@
<el-form-item label="转结算类型"> <el-form-item label="转结算类型">
<el-radio-group <el-radio-group
v-model="transferForm.settlementBillType" v-model="transferForm.settlementBillType"
@change="loadTransferCandidates" @change="handleTransferTypeChange"
> >
<el-radio label="pre">预结算单</el-radio> <el-radio label="pre">预结算单</el-radio>
<el-radio label="formal">正式结算单</el-radio> <el-radio label="formal">正式结算单</el-radio>
@@ -343,18 +343,26 @@
<el-input v-else v-model="transferQuery[field.prop]" clearable placeholder="请输入" /> <el-input v-else v-model="transferQuery[field.prop]" clearable placeholder="请输入" />
</el-form-item> </el-form-item>
<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-item>
</el-form> </el-form>
</div> </div>
<el-table <el-table
ref="transferTableRef"
v-loading="transferDialog.loading" v-loading="transferDialog.loading"
:data="transferRows" :data="transferRows"
row-key="id"
border border
@selection-change="transferSelectionChange" @selection-change="transferSelectionChange"
> >
<el-table-column type="selection" width="52" align="center" /> <el-table-column type="selection" width="52" align="center" reserve-selection />
<el-table-column type="index" label="序号" width="70" align="center" /> <el-table-column
type="index"
label="序号"
width="70"
align="center"
:index="transferIndexMethod"
/>
<el-table-column <el-table-column
v-for="column in tableColumns.slice(0, 13)" v-for="column in tableColumns.slice(0, 13)"
:key="column.prop" :key="column.prop"
@@ -367,6 +375,17 @@
<template #default="{ row }">{{ formatColumnValue(row, column) }}</template> <template #default="{ row }">{{ formatColumnValue(row, column) }}</template>
</el-table-column> </el-table-column>
</el-table> </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> <template #footer>
<el-button @click="transferDialog.visible = false">取消</el-button> <el-button @click="transferDialog.visible = false">取消</el-button>
<el-button type="primary" :loading="transferDialog.submitting" @click="submitTransfer"> <el-button type="primary" :loading="transferDialog.submitting" @click="submitTransfer">
@@ -593,6 +612,7 @@ export default {
transferForm: { settlementBillType: 'formal' }, transferForm: { settlementBillType: 'formal' },
transferRows: [], transferRows: [],
transferSelection: [], transferSelection: [],
transferPage: { current: 1, size: 10, total: 0 },
generateDialog: { visible: false, loading: false }, generateDialog: { visible: false, loading: false },
generateQuery: {}, generateQuery: {},
generateRows: [], generateRows: [],
@@ -1029,11 +1049,12 @@ export default {
this.transferQuery = {}; this.transferQuery = {};
this.transferRows = []; this.transferRows = [];
this.transferSelection = []; this.transferSelection = [];
this.transferPage = { current: 1, size: 10, total: 0 };
this.$nextTick(() => this.$refs.transferTableRef?.clearSelection());
this.loadTransferCandidates(); this.loadTransferCandidates();
}, },
async loadTransferCandidates() { async loadTransferCandidates() {
this.transferDialog.loading = true; this.transferDialog.loading = true;
this.transferSelection = [];
try { try {
const params = this.buildRequestParams( const params = this.buildRequestParams(
this.normalizeQuery( this.normalizeQuery(
@@ -1043,20 +1064,46 @@ export default {
'generateEndDate' 'generateEndDate'
) )
); );
const res = await api.getTransferCandidates(1, 50, { const res = await api.getTransferCandidates(
this.transferPage.current,
this.transferPage.size,
{
...params, ...params,
settlementStatus: 'pending', settlementStatus: 'pending',
settlementType: this.settlementType || undefined, settlementType: this.settlementType || undefined,
settlementBillType: this.transferForm.settlementBillType, settlementBillType: this.transferForm.settlementBillType,
}); }
);
const data = this.unwrapPage(res); const data = this.unwrapPage(res);
this.transferRows = (data.records || []) this.transferRows = (data.records || [])
.filter(row => this.isTransferCandidate(row)) .filter(row => this.isTransferCandidate(row))
.map(this.decorateRow); .map(this.decorateRow);
this.transferPage.total = data.total || 0;
} finally { } finally {
this.transferDialog.loading = false; 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) { isTransferCandidate(row) {
const hasValue = value => { const hasValue = value => {
if (Array.isArray(value)) return value.length > 0; if (Array.isArray(value)) return value.length > 0;
@@ -1151,16 +1198,38 @@ export default {
} }
}, },
async resolveTransferContractIds(rows) { 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 contractNo = rows.find(item => item.contractNo)?.contractNo;
const { data } = await getSettlementContractOptions(contractNo); const { data } = await getSettlementContractOptions(contractNo);
const contracts = data?.data || []; const contracts = data?.data || [];
const contract = contracts.find(item => String(item.contractNo) === String(contractNo)); const contract = contracts.find(
if (!contract?.id) { item =>
this.$message.warning('未找到所选明细对应的有效合同,无法转结算'); (contractId && String(item.id) === String(contractId)) ||
(contractNo && String(item.contractNo) === String(contractNo))
);
if (!contract?.id || !contract.projectId) {
this.$message.warning('未找到所选明细对应的合同或项目信息,无法转结算');
return null; 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() { openGenerateDialog() {
this.generateDialog.visible = true; this.generateDialog.visible = true;
+9
View File
@@ -1309,6 +1309,11 @@ export default {
}, },
normalizeBirthday(value = '') { normalizeBirthday(value = '') {
const birthday = String(value || '').trim(); 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})日?$/); const match = birthday.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/);
if (!match) return birthday; if (!match) return birthday;
const [, year, month, day] = match; const [, year, month, day] = match;
@@ -1343,6 +1348,10 @@ export default {
} }
const row = { const row = {
...this.driverForm, ...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, qualificationNo: this.driverForm.qualificationNo || this.driverForm.idCardNo,
posts: this.driverForm.postList.join(','), posts: this.driverForm.postList.join(','),
}; };
+2 -2
View File
@@ -78,7 +78,7 @@
placeholder="请输入" placeholder="请输入"
@input="value => normalizeDecimalInput('directEconomicLoss', value)" @input="value => normalizeDecimalInput('directEconomicLoss', value)"
> >
<template #suffix></template> <template #append></template>
</el-input> </el-input>
</template> </template>
<template #insuranceClaimAmountForm> <template #insuranceClaimAmountForm>
@@ -89,7 +89,7 @@
placeholder="请输入" placeholder="请输入"
@input="value => normalizeDecimalInput('insuranceClaimAmount', value)" @input="value => normalizeDecimalInput('insuranceClaimAmount', value)"
> >
<template #suffix></template> <template #append></template>
</el-input> </el-input>
</template> </template>
<template #attachments="{ row }"> <template #attachments="{ row }">
@@ -78,7 +78,7 @@
placeholder="请输入" placeholder="请输入"
@input="value => normalizeDecimalInput('fee', value)" @input="value => normalizeDecimalInput('fee', value)"
> >
<template #suffix></template> <template #append></template>
</el-input> </el-input>
</template> </template>
<template #attachments="{ row }"> <template #attachments="{ row }">
+48 -4
View File
@@ -634,11 +634,20 @@
</el-select> </el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="文件名称" min-width="180"> <el-table-column label="文件名称" min-width="180" align="left">
<template #default="{ row }"> <template #default="{ row }">
<el-link type="primary" :disabled="!row.url" @click="previewQualificationFile(row)">{{ <div
row.originalName || row.name || '-' class="qualification-file-name-cell"
}}</el-link> :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> </template>
</el-table-column> </el-table-column>
<el-table-column label="附件描述" min-width="220"> <el-table-column label="附件描述" min-width="220">
@@ -4461,6 +4470,41 @@ export default {
font-weight: 400; 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 { .qualification-upload-bar {
display: grid; display: grid;
grid-template-columns: 1fr auto 1fr; grid-template-columns: 1fr auto 1fr;
+30
View File
@@ -70,6 +70,15 @@
class="maintenance-plan-page__input" class="maintenance-plan-page__input"
/> />
</template> </template>
<template #address-form>
<el-input
v-model="form.address"
:disabled="boxType === 'view'"
readonly
placeholder="点击地图选址"
@click="openAddressMapPicker"
/>
</template>
<template #mileageForm> <template #mileageForm>
<el-input <el-input
v-model="form.mileage" v-model="form.mileage"
@@ -140,6 +149,11 @@
</template> </template>
</avue-form> </avue-form>
</el-dialog> </el-dialog>
<address-map-picker
v-model="addressMapPickerVisible"
:address="form.address"
@confirm="handleAddressMapConfirm"
/>
</basic-container> </basic-container>
</template> </template>
@@ -154,6 +168,7 @@ import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range'; import { normalizeSearchRangeParams } from '@/utils/search-range';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
@@ -163,6 +178,9 @@ const createTimeRangeMap = {
}; };
export default { export default {
components: {
AddressMapPicker,
},
data() { data() {
const validateNonNegative = (rule, value, callback) => { const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '') { if (value === undefined || value === null || value === '') {
@@ -178,6 +196,7 @@ export default {
query: {}, query: {},
loading: true, loading: true,
excelBox: false, excelBox: false,
addressMapPickerVisible: false,
excelForm: {}, excelForm: {},
page: { page: {
pageSize: 10, pageSize: 10,
@@ -203,6 +222,7 @@ export default {
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
indexWidth: 90,
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
@@ -315,6 +335,8 @@ export default {
{ {
label: '地址', label: '地址',
prop: 'address', prop: 'address',
slot: true,
formslot: true,
minWidth: 180, minWidth: 180,
overHidden: true, overHidden: true,
span: 24, span: 24,
@@ -483,6 +505,14 @@ export default {
}, },
}, },
methods: { methods: {
openAddressMapPicker() {
if (this.boxType !== 'view') {
this.addressMapPickerVisible = true;
}
},
handleAddressMapConfirm(address) {
this.form.address = address;
},
hasPermission(code) { hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false); return this.isAdmin || this.validData(this.permission[code], false);
}, },
+30
View File
@@ -70,6 +70,15 @@
class="maintenance-record-page__input" class="maintenance-record-page__input"
/> />
</template> </template>
<template #address-form>
<el-input
v-model="form.address"
:disabled="boxType === 'view'"
readonly
placeholder="点击地图选址"
@click="openAddressMapPicker"
/>
</template>
<template #costForm> <template #costForm>
<el-input <el-input
v-model="form.cost" v-model="form.cost"
@@ -126,6 +135,11 @@
</template> </template>
</avue-form> </avue-form>
</el-dialog> </el-dialog>
<address-map-picker
v-model="addressMapPickerVisible"
:address="form.address"
@confirm="handleAddressMapConfirm"
/>
</basic-container> </basic-container>
</template> </template>
@@ -140,6 +154,7 @@ import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range'; import { normalizeSearchRangeParams } from '@/utils/search-range';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
@@ -149,6 +164,9 @@ const createTimeRangeMap = {
}; };
export default { export default {
components: {
AddressMapPicker,
},
data() { data() {
const validateNonNegative = (rule, value, callback) => { const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '') { if (value === undefined || value === null || value === '') {
@@ -164,6 +182,7 @@ export default {
query: {}, query: {},
loading: true, loading: true,
excelBox: false, excelBox: false,
addressMapPickerVisible: false,
excelForm: {}, excelForm: {},
isDetailLoading: false, isDetailLoading: false,
page: { page: {
@@ -190,6 +209,7 @@ export default {
border: true, border: true,
index: true, index: true,
indexLabel: '序号', indexLabel: '序号',
indexWidth: 90,
viewBtn: true, viewBtn: true,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
@@ -291,6 +311,8 @@ export default {
{ {
label: '地址', label: '地址',
prop: 'address', prop: 'address',
slot: true,
formslot: true,
minWidth: 180, minWidth: 180,
overHidden: true, overHidden: true,
span: 24, span: 24,
@@ -478,6 +500,14 @@ export default {
}, },
}, },
methods: { methods: {
openAddressMapPicker() {
if (this.boxType !== 'view') {
this.addressMapPickerVisible = true;
}
},
handleAddressMapConfirm(address) {
this.form.address = address;
},
hasPermission(code) { hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false); return this.isAdmin || this.validData(this.permission[code], false);
}, },
+4 -4
View File
@@ -90,7 +90,7 @@
placeholder="请输入" placeholder="请输入"
@input="value => normalizeIntegerInput('previousMonthMileage', value)" @input="value => normalizeIntegerInput('previousMonthMileage', value)"
> >
<template #suffix>km</template> <template #append>km</template>
</el-input> </el-input>
</template> </template>
<template #currentMonthMileageForm> <template #currentMonthMileageForm>
@@ -101,7 +101,7 @@
placeholder="请输入" placeholder="请输入"
@input="value => normalizeIntegerInput('currentMonthMileage', value)" @input="value => normalizeIntegerInput('currentMonthMileage', value)"
> >
<template #suffix>km</template> <template #append>km</template>
</el-input> </el-input>
</template> </template>
<template #monthlyMileageForm> <template #monthlyMileageForm>
@@ -112,7 +112,7 @@
placeholder="请输入" placeholder="请输入"
@input="value => normalizeIntegerInput('monthlyMileage', value)" @input="value => normalizeIntegerInput('monthlyMileage', value)"
> >
<template #suffix>km</template> <template #append>km</template>
</el-input> </el-input>
</template> </template>
<template #totalMileageForm> <template #totalMileageForm>
@@ -123,7 +123,7 @@
placeholder="请输入" placeholder="请输入"
@input="value => normalizeIntegerInput('totalMileage', value)" @input="value => normalizeIntegerInput('totalMileage', value)"
> >
<template #suffix>km</template> <template #append>km</template>
</el-input> </el-input>
</template> </template>
<template #totalMileageStart-search="{ row }"> <template #totalMileageStart-search="{ row }">
+1 -1
View File
@@ -87,7 +87,7 @@
placeholder="请输入" placeholder="请输入"
@input="value => normalizeDecimalInput('unitPrice', value)" @input="value => normalizeDecimalInput('unitPrice', value)"
> >
<template #suffix></template> <template #append>/</template>
</el-input> </el-input>
</template> </template>
<template #transactionAmountForm> <template #transactionAmountForm>
+27
View File
@@ -110,6 +110,15 @@
@input="value => normalizeIntegerInput('deductPoints', value)" @input="value => normalizeIntegerInput('deductPoints', value)"
/> />
</template> </template>
<template #location-form>
<el-input
v-model="form.location"
:disabled="boxType === 'view'"
readonly
placeholder="点击地图选址"
@click="openLocationMapPicker"
/>
</template>
<template #processStatus="{ row }"> <template #processStatus="{ row }">
<span :class="row.processStatus === '未处理' ? 'violation-record-page__danger' : ''"> <span :class="row.processStatus === '未处理' ? 'violation-record-page__danger' : ''">
{{ row.processStatus }} {{ row.processStatus }}
@@ -152,6 +161,11 @@
</template> </template>
</avue-form> </avue-form>
</el-dialog> </el-dialog>
<address-map-picker
v-model="locationMapPickerVisible"
:address="form.location"
@confirm="handleLocationMapConfirm"
/>
</basic-container> </basic-container>
</template> </template>
@@ -167,6 +181,7 @@ import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { normalizeSearchRangeParams } from '@/utils/search-range'; import { normalizeSearchRangeParams } from '@/utils/search-range';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { excelOption, option } from '@/option/vehicle/violation-record'; import { excelOption, option } from '@/option/vehicle/violation-record';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
@@ -177,12 +192,16 @@ const createTimeRangeMap = {
}; };
export default { export default {
components: {
AddressMapPicker,
},
data() { data() {
return { return {
form: {}, form: {},
query: {}, query: {},
loading: true, loading: true,
excelBox: false, excelBox: false,
locationMapPickerVisible: false,
excelForm: {}, excelForm: {},
isDetailLoading: false, isDetailLoading: false,
option, option,
@@ -242,6 +261,14 @@ export default {
}, },
}, },
methods: { methods: {
openLocationMapPicker() {
if (this.boxType !== 'view') {
this.locationMapPickerVisible = true;
}
},
handleLocationMapConfirm(address) {
this.form.location = address;
},
hasPermission(code) { hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false); return this.isAdmin || this.validData(this.permission[code], false);
}, },