🐛 修复车务非必填-1展示,并完善总单调度与运单表单体验
调度页限制上一段到达后才可调度;司机候选项点击展开;车务多模块-1显示为空;同步配载/总单编辑与导入相关优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -67,6 +67,16 @@ export const getImportDetails = params => request({ url: `${baseUrl}/import-batc
|
||||
export const removeImportBatches = ids => request({ url: `${baseUrl}/import-batch/remove`, method: 'post', params: { ids } });
|
||||
export const getImportOptions = () => request({ url: `${baseUrl}/import-batch/options`, method: 'get' });
|
||||
export const exportImportTemplate = () => request({ url: `${baseUrl}/import-batch/export-template`, method: 'get', responseType: 'blob' });
|
||||
export const saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data });
|
||||
export const validateImport = data => request({ url: `${baseUrl}/import-batch/validate`, method: 'post', data, responseType: 'blob' });
|
||||
export const confirmImport = data => request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data });
|
||||
export const saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data, timeout: 120000 });
|
||||
export const validateImport = data =>
|
||||
request({ url: `${baseUrl}/import-batch/validate`, method: 'post', data, timeout: 120000 });
|
||||
export const exportImportFailures = data =>
|
||||
request({
|
||||
url: `${baseUrl}/import-batch/export-failures`,
|
||||
method: 'post',
|
||||
data,
|
||||
responseType: 'blob',
|
||||
timeout: 120000,
|
||||
});
|
||||
export const confirmImport = data =>
|
||||
request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data, timeout: 120000 });
|
||||
|
||||
@@ -24,10 +24,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { normalizeMapSearchResults } from '@/utils/map-search';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
const AMAP_KEY = '13dcb15bc29bfe9c432d17ecd808b812';
|
||||
/** 地点搜索 v5/place/text 使用「Web 服务」类型 Key,由业务自行填写 */
|
||||
const AMAP_WEB_KEY = 'b43016a0d1f5b0c32211560392059708';
|
||||
const AMAP_SECURITY_CODE = 'f9d036ef29e376e15d309e0de5c0f7ab';
|
||||
let amapLoader;
|
||||
|
||||
export default {
|
||||
@@ -90,10 +92,24 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hasCoordinateValue(value) {
|
||||
return value !== '' && value !== null && value !== undefined;
|
||||
},
|
||||
buildInitialSelection() {
|
||||
// Number('') === 0,空字符串不能当有效坐标,否则会落到 [0,0](非洲附近)
|
||||
if (!this.hasCoordinateValue(this.longitude) || !this.hasCoordinateValue(this.latitude)) {
|
||||
return {};
|
||||
}
|
||||
const longitude = Number(this.longitude);
|
||||
const latitude = Number(this.latitude);
|
||||
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
|
||||
if (
|
||||
!Number.isFinite(longitude) ||
|
||||
!Number.isFinite(latitude) ||
|
||||
longitude < -180 ||
|
||||
longitude > 180 ||
|
||||
latitude < -90 ||
|
||||
latitude > 90
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
@@ -131,22 +147,24 @@ export default {
|
||||
this.$nextTick(() => {
|
||||
if (!this.$refs.map) return resolve(null);
|
||||
const initial = this.buildInitialSelection();
|
||||
const center =
|
||||
Number.isFinite(initial.longitude) && Number.isFinite(initial.latitude)
|
||||
? [initial.longitude, initial.latitude]
|
||||
: [116.40769, 39.89945];
|
||||
const hasPoint =
|
||||
Number.isFinite(initial.longitude) && Number.isFinite(initial.latitude);
|
||||
const center = hasPoint
|
||||
? [initial.longitude, initial.latitude]
|
||||
: [116.40769, 39.89945];
|
||||
const zoom = hasPoint ? 14 : 11;
|
||||
if (!this.amap) {
|
||||
this.amap = new window.AMap.Map(this.$refs.map, {
|
||||
center,
|
||||
zoom: initial.longitude ? 14 : 11,
|
||||
zoom,
|
||||
});
|
||||
this.amap.on('click', event => this.pickPoint(event.lnglat));
|
||||
} else {
|
||||
this.amap.resize();
|
||||
this.clearMarker();
|
||||
this.amap.setZoomAndCenter(initial.longitude ? 14 : 11, center);
|
||||
this.amap.setZoomAndCenter(zoom, center);
|
||||
}
|
||||
if (initial.longitude) {
|
||||
if (hasPoint) {
|
||||
const point = new window.AMap.LngLat(initial.longitude, initial.latitude);
|
||||
this.renderMarker(point);
|
||||
this.selected = { ...initial };
|
||||
@@ -204,22 +222,25 @@ export default {
|
||||
this.$message.warning('请输入地址关键词');
|
||||
return;
|
||||
}
|
||||
if (!AMAP_WEB_KEY) {
|
||||
this.$message.warning('请先配置高德 Web 服务 Key');
|
||||
return;
|
||||
}
|
||||
if (this.searching) return;
|
||||
this.searchResults = [];
|
||||
this.searching = true;
|
||||
this.initMap()
|
||||
.then(map => (map ? this.runGeocode('location', keyword) : null))
|
||||
.then(result => {
|
||||
if (!result) return;
|
||||
const geocodes = result.geocodes || (result.location ? [result] : []);
|
||||
this.searchResults = normalizeMapSearchResults(geocodes, keyword);
|
||||
const point = this.searchResults[0]?.location || result.location;
|
||||
if (!point) {
|
||||
.then(map => (map ? searchAmapPlaceText(keyword, { key: AMAP_WEB_KEY }) : null))
|
||||
.then(pois => {
|
||||
if (!pois) return;
|
||||
this.searchResults = normalizeMapSearchResults(pois, keyword);
|
||||
const first = this.searchResults[0];
|
||||
if (!first?.location) {
|
||||
throw new Error('未找到匹配地址');
|
||||
}
|
||||
return this.$nextTick().then(() => {
|
||||
this.amap?.resize();
|
||||
this.pickPoint(point, this.searchResults[0]?.address || keyword);
|
||||
this.pickPoint(first.location, first.address || first.name || keyword);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -236,8 +257,10 @@ export default {
|
||||
}
|
||||
},
|
||||
pickPoint(point, fallbackAddress = '') {
|
||||
const longitude = typeof point.getLng === 'function' ? point.getLng() : point.lng;
|
||||
const latitude = typeof point.getLat === 'function' ? point.getLat() : point.lat;
|
||||
const longitude =
|
||||
typeof point.getLng === 'function' ? point.getLng() : point.lng ?? point.longitude;
|
||||
const latitude =
|
||||
typeof point.getLat === 'function' ? point.getLat() : point.lat ?? point.latitude;
|
||||
if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
|
||||
this.$message.warning('选点坐标无效');
|
||||
return;
|
||||
|
||||
@@ -114,7 +114,7 @@ export const config = {
|
||||
operations: [
|
||||
{
|
||||
action: 'flow',
|
||||
label: '查看流程',
|
||||
label: '流程',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['reviewing', 'change_reviewing'],
|
||||
stage: ['temporary', 'formal'],
|
||||
@@ -206,7 +206,7 @@ export const config = {
|
||||
},
|
||||
{
|
||||
action: 'flow',
|
||||
label: '查看流程',
|
||||
label: '流程',
|
||||
statusProp: 'approvalStatus',
|
||||
status: ['approved', 'change_approved'],
|
||||
stage: ['formal', 'terminated'],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (Number(value) < 0) {
|
||||
callback(new Error('不能小于 0'));
|
||||
@@ -10,6 +10,9 @@ const validateNonNegative = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatEmptySentinel = (row, value) =>
|
||||
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
|
||||
|
||||
export const vehicleTypeDic = [
|
||||
{ label: '车辆', value: '车辆' },
|
||||
{ label: '船舶', value: '船舶' },
|
||||
@@ -135,6 +138,7 @@ export const option = {
|
||||
minWidth: 140,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -146,6 +150,7 @@ export const option = {
|
||||
minWidth: 140,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (Number(value) < 0) {
|
||||
callback(new Error('不能小于 0'));
|
||||
@@ -10,6 +10,9 @@ const validateNonNegative = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatEmptySentinel = (row, value) =>
|
||||
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
|
||||
|
||||
export const vehicleTypeDic = [
|
||||
{ label: '车辆', value: '车辆' },
|
||||
{ label: '船舶', value: '船舶' },
|
||||
@@ -159,6 +162,7 @@ export const option = {
|
||||
minWidth: 100,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [
|
||||
{ required: true, message: '请输入费用', trigger: 'blur' },
|
||||
{ validator: validateNonNegative, trigger: 'blur' },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (Number(value) < 0) {
|
||||
callback(new Error('不能小于 0'));
|
||||
@@ -141,6 +141,7 @@ export const option = {
|
||||
formslot: true,
|
||||
append: '元',
|
||||
minWidth: 120,
|
||||
formatter: (row, value) => (value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value),
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -150,6 +151,7 @@ export const option = {
|
||||
formslot: true,
|
||||
append: '元',
|
||||
minWidth: 120,
|
||||
formatter: (row, value) => (value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value),
|
||||
rules: [
|
||||
{ required: true, message: '请输入保费', trigger: 'blur' },
|
||||
{ validator: validateNonNegative, trigger: 'blur' },
|
||||
@@ -159,6 +161,7 @@ export const option = {
|
||||
label: '发票号',
|
||||
prop: 'invoiceNo',
|
||||
minWidth: 150,
|
||||
formatter: (row, value) => (value === undefined || value === null || value === '' || String(value) === '-1' ? '' : value),
|
||||
rules: [{ max: 80, message: '最多 80 个字符', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -168,6 +171,7 @@ export const option = {
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
minWidth: 120,
|
||||
formatter: (row, value) => (value === undefined || value === null || value === '' || String(value) === '-1' ? '' : value),
|
||||
},
|
||||
{
|
||||
label: '备注',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
|
||||
const isEmpty = value => value === undefined || value === null || value === '';
|
||||
const isEmpty = value =>
|
||||
value === undefined || value === null || value === '' || Number(value) === -1;
|
||||
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (isEmpty(value)) {
|
||||
@@ -12,6 +13,8 @@ const validateNonNegative = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatEmptySentinel = (row, value) => (isEmpty(value) ? '' : value);
|
||||
|
||||
export const option = {
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
@@ -60,6 +63,7 @@ export const option = {
|
||||
append: 'km',
|
||||
minWidth: 210,
|
||||
slot: true,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -70,6 +74,7 @@ export const option = {
|
||||
append: 'km',
|
||||
minWidth: 210,
|
||||
slot: true,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -80,6 +85,7 @@ export const option = {
|
||||
append: 'km',
|
||||
minWidth: 170,
|
||||
slot: true,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -90,6 +96,7 @@ export const option = {
|
||||
append: 'km',
|
||||
minWidth: 170,
|
||||
slot: true,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
|
||||
const isEmpty = value => value === undefined || value === null || value === '';
|
||||
const isEmpty = value =>
|
||||
value === undefined || value === null || value === '' || Number(value) === -1;
|
||||
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (isEmpty(value)) {
|
||||
@@ -12,6 +13,8 @@ const validateNonNegative = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatEmptySentinel = (row, value) => (isEmpty(value) ? '' : value);
|
||||
|
||||
export const vehicleTypeDic = [
|
||||
{ label: '车辆', value: '车辆' },
|
||||
{ label: '船舶', value: '船舶' },
|
||||
@@ -124,6 +127,7 @@ export const option = {
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
minWidth: 110,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -133,6 +137,7 @@ export const option = {
|
||||
formslot: true,
|
||||
append: '元/升',
|
||||
minWidth: 120,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -141,6 +146,7 @@ export const option = {
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
minWidth: 130,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [
|
||||
{ required: true, message: '请输入交易金额', trigger: 'blur' },
|
||||
{ validator: validateNonNegative, trigger: 'blur' },
|
||||
@@ -152,6 +158,7 @@ export const option = {
|
||||
type: 'input',
|
||||
formslot: true,
|
||||
minWidth: 110,
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (Number(value) < 0) {
|
||||
callback(new Error('不能小于 0'));
|
||||
@@ -11,7 +11,7 @@ const validateNonNegative = (rule, value, callback) => {
|
||||
};
|
||||
|
||||
const validatePositiveInteger = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (!Number.isInteger(Number(value)) || Number(value) <= 0) {
|
||||
callback(new Error('换胎数量必须为正整数'));
|
||||
@@ -20,6 +20,9 @@ const validatePositiveInteger = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatEmptySentinel = (row, value) =>
|
||||
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
|
||||
|
||||
export const option = {
|
||||
height: 'auto',
|
||||
calcHeight: 32,
|
||||
@@ -98,6 +101,7 @@ export const option = {
|
||||
minWidth: 110,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validatePositiveInteger, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -108,6 +112,7 @@ export const option = {
|
||||
minWidth: 120,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [
|
||||
{ required: true, message: '请输入换胎费用', trigger: 'blur' },
|
||||
{ validator: validateNonNegative, trigger: 'blur' },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
|
||||
const validateNonNegative = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (Number(value) < 0) {
|
||||
callback(new Error('不能小于 0'));
|
||||
@@ -11,7 +11,7 @@ const validateNonNegative = (rule, value, callback) => {
|
||||
};
|
||||
|
||||
const validateDeductPoints = (rule, value, callback) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
callback();
|
||||
} else if (!Number.isInteger(Number(value)) || Number(value) < 0 || Number(value) > 15) {
|
||||
callback(new Error('被扣分数范围为 0-15 分'));
|
||||
@@ -20,6 +20,9 @@ const validateDeductPoints = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatEmptySentinel = (row, value) =>
|
||||
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
|
||||
|
||||
export const vehicleTypeDic = [
|
||||
{ label: '车辆', value: '车辆' },
|
||||
{ label: '船舶', value: '船舶' },
|
||||
@@ -150,6 +153,7 @@ export const option = {
|
||||
minWidth: 100,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
@@ -160,6 +164,7 @@ export const option = {
|
||||
minWidth: 110,
|
||||
span: 12,
|
||||
placeholder: '请输入',
|
||||
formatter: formatEmptySentinel,
|
||||
rules: [{ validator: validateDeductPoints, trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
|
||||
+135
-15
@@ -1,6 +1,56 @@
|
||||
/**
|
||||
* 将高德 Geocoder 结果规范化为地图选址浮层列表数据。
|
||||
* @param {Array|Object} geocodesOrResult geocodes 数组,或含 geocodes/location 的结果对象
|
||||
* 高德地点关键词搜索(Web 服务 v5)。
|
||||
* @see https://restapi.amap.com/v5/place/text
|
||||
* @param {string} keyword 地点关键字
|
||||
* @param {{ key: string, pageSize?: number, pageNum?: number, region?: string, cityLimit?: boolean }} options
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
export function searchAmapPlaceText(keyword, options = {}) {
|
||||
const key = options.key;
|
||||
const pageSize = options.pageSize || 20;
|
||||
const pageNum = options.pageNum || 1;
|
||||
if (!key) {
|
||||
return Promise.reject(new Error('高德地图 Key 未配置'));
|
||||
}
|
||||
const trimmed = String(keyword || '').trim();
|
||||
if (!trimmed) {
|
||||
return Promise.reject(new Error('请输入地址关键词'));
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
key,
|
||||
keywords: trimmed,
|
||||
page_size: String(pageSize),
|
||||
page_num: String(pageNum),
|
||||
show_fields: 'photos',
|
||||
});
|
||||
if (options.region) {
|
||||
params.set('region', options.region);
|
||||
}
|
||||
if (options.cityLimit != null) {
|
||||
params.set('city_limit', options.cityLimit ? 'true' : 'false');
|
||||
}
|
||||
|
||||
return fetch(`https://restapi.amap.com/v5/place/text?${params.toString()}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('高德地点搜索请求失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (String(data?.status) === '1') {
|
||||
return Array.isArray(data.pois) ? data.pois : [];
|
||||
}
|
||||
const error = new Error(data?.info || '高德地点搜索失败');
|
||||
error.code = data?.infocode;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将高德 Geocoder / place/text 结果规范化为地图选址浮层列表数据。
|
||||
* @param {Array|Object} geocodesOrResult geocodes/pois 数组,或含 geocodes/pois/location 的结果对象
|
||||
* @param {string} keyword 搜索关键词兜底文案
|
||||
* @returns {Array<{id: string|number, name: string, address: string, location: any, photo: string}>}
|
||||
*/
|
||||
@@ -10,25 +60,95 @@ export function normalizeMapSearchResults(geocodesOrResult, keyword = '') {
|
||||
if (Array.isArray(geocodesOrResult)) {
|
||||
list = geocodesOrResult;
|
||||
} else if (geocodesOrResult && typeof geocodesOrResult === 'object') {
|
||||
if (Array.isArray(geocodesOrResult.geocodes)) {
|
||||
if (Array.isArray(geocodesOrResult.pois)) {
|
||||
list = geocodesOrResult.pois;
|
||||
} else if (Array.isArray(geocodesOrResult.geocodes)) {
|
||||
list = geocodesOrResult.geocodes;
|
||||
} else if (geocodesOrResult.location) {
|
||||
list = [geocodesOrResult];
|
||||
}
|
||||
}
|
||||
|
||||
return list.map((item, index) => {
|
||||
const buildingName = pickNamedField(item?.building);
|
||||
const neighborhoodName = pickNamedField(item?.neighborhood);
|
||||
const address = item?.formattedAddress || item?.address || fallback || '-';
|
||||
return {
|
||||
id: item?.id || index,
|
||||
name: buildingName || neighborhoodName || address,
|
||||
address,
|
||||
location: item?.location,
|
||||
photo: item?.photo || item?.image || '',
|
||||
};
|
||||
});
|
||||
return list
|
||||
.map((item, index) => {
|
||||
if (!item) return null;
|
||||
if (isPlacePoi(item)) {
|
||||
const address = buildPoiAddress(item) || fallback || '-';
|
||||
return {
|
||||
id: item.id || index,
|
||||
name: String(item.name || '').trim() || address,
|
||||
address,
|
||||
location: parseMapLocation(item.location),
|
||||
photo: pickPoiPhoto(item),
|
||||
};
|
||||
}
|
||||
|
||||
const buildingName = pickNamedField(item?.building);
|
||||
const neighborhoodName = pickNamedField(item?.neighborhood);
|
||||
const address = item?.formattedAddress || item?.address || fallback || '-';
|
||||
return {
|
||||
id: item?.id || index,
|
||||
name: buildingName || neighborhoodName || address,
|
||||
address,
|
||||
location: parseMapLocation(item?.location),
|
||||
photo: item?.photo || item?.image || '',
|
||||
};
|
||||
})
|
||||
.filter(item => item && item.location);
|
||||
}
|
||||
|
||||
export function parseMapLocation(location) {
|
||||
if (!location) return null;
|
||||
if (typeof location.getLng === 'function' && typeof location.getLat === 'function') {
|
||||
return location;
|
||||
}
|
||||
if (typeof location === 'string') {
|
||||
const parts = location.split(',');
|
||||
const lng = Number(parts[0]);
|
||||
const lat = Number(parts[1]);
|
||||
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null;
|
||||
return { lng, lat };
|
||||
}
|
||||
if (Number.isFinite(Number(location.lng)) && Number.isFinite(Number(location.lat))) {
|
||||
return { lng: Number(location.lng), lat: Number(location.lat) };
|
||||
}
|
||||
if (
|
||||
Number.isFinite(Number(location.longitude)) &&
|
||||
Number.isFinite(Number(location.latitude))
|
||||
) {
|
||||
return { lng: Number(location.longitude), lat: Number(location.latitude) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isPlacePoi(item) {
|
||||
return Boolean(item && item.name && item.location && (item.id || item.typecode || item.adname));
|
||||
}
|
||||
|
||||
function buildPoiAddress(item) {
|
||||
const detail = String(item.address || '').trim();
|
||||
const region = [item.pname, item.cityname, item.adname]
|
||||
.map(value => String(value || '').trim())
|
||||
.filter(Boolean);
|
||||
// 直辖市等场景省市名可能重复
|
||||
const uniqueRegion = region.filter((value, index) => region.indexOf(value) === index);
|
||||
const prefix = uniqueRegion.join('');
|
||||
if (prefix && detail) {
|
||||
if (detail.startsWith(prefix)) return detail;
|
||||
return `${prefix}${detail}`;
|
||||
}
|
||||
return detail || prefix || String(item.name || '').trim();
|
||||
}
|
||||
|
||||
function pickPoiPhoto(item) {
|
||||
const photos = item?.photos;
|
||||
if (Array.isArray(photos) && photos.length) {
|
||||
return photos[0]?.url || '';
|
||||
}
|
||||
if (photos && typeof photos === 'object') {
|
||||
return photos.url || '';
|
||||
}
|
||||
return item?.photo || item?.image || '';
|
||||
}
|
||||
|
||||
function pickNamedField(value) {
|
||||
|
||||
@@ -79,14 +79,21 @@
|
||||
<section v-if="master" class="master-goods-card">
|
||||
<div class="master-goods-card__heading"><h3>货物信息</h3></div>
|
||||
<el-table :data="master.goods || []" border>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="cargoName" label="货物名称" min-width="150" />
|
||||
<el-table-column prop="cargoType" label="类型" min-width="130" />
|
||||
<el-table-column prop="cargoType" min-width="130">
|
||||
<template #header
|
||||
><span>货物类型<span class="required-asterisk">*</span></span></template
|
||||
>
|
||||
</el-table-column>
|
||||
<el-table-column prop="packageType" label="包装" min-width="120" />
|
||||
<el-table-column label="数量" min-width="100"><template #default="{ row }">{{ row.quantity ?? '-' }}{{ row.quantityUnit ? ` ${row.quantityUnit}` : '' }}</template></el-table-column>
|
||||
<el-table-column prop="quantity" label="数量" min-width="100" />
|
||||
<el-table-column prop="quantityUnit" label="数量单位" width="100" />
|
||||
<el-table-column prop="brand" label="品牌" min-width="120" />
|
||||
<el-table-column prop="specification" label="规格" min-width="150" />
|
||||
<el-table-column prop="model" label="型号" min-width="150" />
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||||
<el-table-column prop="specification" label="规格" min-width="120" />
|
||||
<el-table-column prop="model" label="型号" min-width="120" />
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right" />
|
||||
</el-table>
|
||||
<el-empty v-if="!(master.goods || []).length" description="暂无货物信息" :image-size="56" />
|
||||
</section>
|
||||
@@ -344,5 +351,7 @@ export default {
|
||||
.transport-plan-detail { margin-top: 20px; h4 { margin: 0 0 12px; padding-left: 12px; border-left: 4px solid #409eff; font-size: 15px; } }
|
||||
.master-goods-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
||||
.master-goods-card__heading { padding: 20px 24px 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
||||
.master-goods-card :deep(.el-table) { margin: 0 24px 20px; width: auto; }
|
||||
.required-asterisk { margin-left: 4px; color: #f56c6c; }
|
||||
@media screen and (max-width: 1200px) { .detail-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
</style>
|
||||
|
||||
@@ -51,9 +51,29 @@
|
||||
<section v-for="(route, index) in routes" :key="route.segmentNo" class="dispatch-section" :data-segment="route.segmentNo">
|
||||
<header class="segment-header">
|
||||
<div>
|
||||
<el-checkbox v-model="route.selected">
|
||||
<el-tooltip
|
||||
v-if="!canDispatchSegment(route)"
|
||||
content="需等上一段运单完成到达后才能调度"
|
||||
placement="top"
|
||||
>
|
||||
<span class="segment-checkbox-wrap">
|
||||
<el-checkbox :model-value="false" disabled>
|
||||
<span class="segment-checkbox-label">
|
||||
<span class="segment-checkbox-no">{{ route.segmentNo }}:</span>
|
||||
<span class="segment-checkbox-path"
|
||||
>{{ dispatchLocationName(route, 'departure') }} → {{ dispatchLocationName(route, 'arrival') }}</span
|
||||
>
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-checkbox
|
||||
v-else
|
||||
v-model="route.selected"
|
||||
@change="value => handleSegmentSelect(route, value)"
|
||||
>
|
||||
<span class="segment-checkbox-label">
|
||||
<span>{{ route.segmentNo }}:</span>
|
||||
<span class="segment-checkbox-no">{{ route.segmentNo }}:</span>
|
||||
<el-tooltip
|
||||
:content="`${dispatchLocationName(route, 'departure')} → ${dispatchLocationName(route, 'arrival')}`"
|
||||
placement="top"
|
||||
@@ -65,7 +85,15 @@
|
||||
</span>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div>总量 {{ formatQuantity(totalQuantity) }} {{ quantityUnitLabel }},已调度 <em>{{ formatQuantity(dispatchedQuantity(route)) }} {{ quantityUnitLabel }}</em>,剩余 <em>{{ formatQuantity(remaining(route)) }} {{ quantityUnitLabel }}</em></div>
|
||||
<div>
|
||||
总量 {{ formatQuantity(totalQuantity) }} {{ quantityUnitLabel }},已调度
|
||||
<em>{{ formatQuantity(dispatchedQuantity(route)) }} {{ quantityUnitLabel }}</em>,剩余
|
||||
<em>{{ formatQuantity(remaining(route)) }} {{ quantityUnitLabel }}</em>
|
||||
<template v-if="index > 0">
|
||||
,上一段已到达
|
||||
<em>{{ formatQuantity(previousArrivedQuantity(route)) }} {{ quantityUnitLabel }}</em>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
<div v-show="route.selected" class="segment-content">
|
||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form">
|
||||
@@ -78,16 +106,28 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="发货地址" required><el-input :model-value="addressText(route.departureName, route.departureAddress)" disabled /></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.departurePhone" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="4"
|
||||
><el-form-item label="联系人" label-width="68px" class="dispatch-form__compact-label"
|
||||
><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="4"
|
||||
><el-form-item label="联系方式" label-width="80px" class="dispatch-form__compact-label"
|
||||
><el-input v-model="route.departurePhone" placeholder="请输入" /></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="4"><el-form-item :label="dateStartLabel(route)"><el-date-picker v-model="route.estimatedStartTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="收货地址" required><el-input :model-value="addressText(route.arrivalName, route.arrivalAddress)" disabled /></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.arrivalContact" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.arrivalPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="4"
|
||||
><el-form-item label="联系人" label-width="68px" class="dispatch-form__compact-label"
|
||||
><el-input v-model="route.arrivalContact" placeholder="请输入" /></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="4"
|
||||
><el-form-item label="联系方式" label-width="80px" class="dispatch-form__compact-label"
|
||||
><el-input v-model="route.arrivalPhone" placeholder="请输入" /></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="4"><el-form-item :label="dateEndLabel(route)"><el-date-picker v-model="route.estimatedEndTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
@@ -156,12 +196,55 @@
|
||||
<el-row :gutter="16">
|
||||
<template v-if="isRoad(route)">
|
||||
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType !== '自运'"><el-select :model-value="route.carrierType !== '自运' ? route.carrierContractId : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType !== '自运' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="司机" :required="route.carrierType !== '承运商'"><el-autocomplete :ref="element => setDriverInput(route.segmentNo, element)" v-model="route.driverName" :debounce="300" :fetch-suggestions="(query, cb) => fetchDriverSuggestions(route, query, cb)" clearable placeholder="请输入司机" :loading="driverLoading" @select="item => handleDriverSuggestionSelect(route, item)" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item :label="route.carrierType === '承运商' ? '手机号' : '司机手机号'" :required="route.carrierType !== '承运商'"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="车牌号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="挂车车牌号"><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人"><el-autocomplete v-model="route.escortName" :debounce="300" :fetch-suggestions="(query, cb) => fetchEscortSuggestions(route, query, cb)" clearable placeholder="请输入" :loading="escortLoading" @select="item => handleEscortSuggestionSelect(route, item)" /></el-form-item></el-col>
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号"><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="6"
|
||||
><el-form-item label="司机" :required="route.carrierType !== '承运商'"
|
||||
><el-autocomplete
|
||||
:ref="element => setDriverInput(route.segmentNo, element)"
|
||||
v-model="route.driverName"
|
||||
clearable
|
||||
trigger-on-focus
|
||||
:debounce="300"
|
||||
:fetch-suggestions="fetchDriverSuggestions"
|
||||
:loading="driverLoading"
|
||||
placeholder="请输入司机"
|
||||
@mousedown="() => handleDriverFocus(route)"
|
||||
@focus="() => handleDriverFocus(route)"
|
||||
@select="handleDriverSuggestionSelect"
|
||||
/></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="6"
|
||||
><el-form-item
|
||||
:label="route.carrierType === '承运商' ? '手机号' : '司机手机号'"
|
||||
:required="route.carrierType !== '承运商'"
|
||||
><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item
|
||||
></el-col>
|
||||
<el-col :span="6"
|
||||
><el-form-item label="车牌号" required
|
||||
><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item
|
||||
></el-col>
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"
|
||||
><el-form-item label="挂车车牌号"
|
||||
><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item
|
||||
></el-col>
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"
|
||||
><el-form-item label="押运人"
|
||||
><el-autocomplete
|
||||
v-model="route.escortName"
|
||||
clearable
|
||||
trigger-on-focus
|
||||
:debounce="300"
|
||||
:fetch-suggestions="fetchEscortSuggestions"
|
||||
:loading="escortLoading"
|
||||
placeholder="请输入"
|
||||
@mousedown="() => handleDriverFocus(route)"
|
||||
@focus="() => handleDriverFocus(route)"
|
||||
@select="handleEscortSuggestionSelect"
|
||||
/></el-form-item
|
||||
></el-col>
|
||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"
|
||||
><el-form-item label="押运人手机号"
|
||||
><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item
|
||||
></el-col>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType !== '自运'"><el-select :model-value="route.carrierType !== '自运' ? route.carrierContractId : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType !== '自运' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
|
||||
@@ -249,7 +332,23 @@ export default {
|
||||
props: { id: [String, Number] },
|
||||
emits: ['back'],
|
||||
data() {
|
||||
return { loading: false, submitting: false, master: null, routes: [], pending: [], pendingExpanded: false, editingId: null, driverInputs: {}, carrierOptions: [], driverOptions: [], carrierLoading: false, driverLoading: false, escortLoading: false, quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'] };
|
||||
return {
|
||||
loading: false,
|
||||
submitting: false,
|
||||
master: null,
|
||||
routes: [],
|
||||
pending: [],
|
||||
pendingExpanded: false,
|
||||
editingId: null,
|
||||
driverInputs: {},
|
||||
activeSuggestRoute: null,
|
||||
carrierOptions: [],
|
||||
driverOptions: [],
|
||||
carrierLoading: false,
|
||||
driverLoading: false,
|
||||
escortLoading: false,
|
||||
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
totalQuantity() { return (this.master?.goods || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); },
|
||||
@@ -453,12 +552,29 @@ export default {
|
||||
dispatchedQuantity(route) {
|
||||
return this.quantityNumber(route.dispatchedQuantity) + this.pendingSegmentQuantity(route);
|
||||
},
|
||||
previousArrivedQuantity(route) {
|
||||
const routeIndex = this.routes.indexOf(route);
|
||||
if (routeIndex <= 0) return null;
|
||||
return this.quantityNumber(this.routes[routeIndex - 1]?.arrivedQuantity);
|
||||
},
|
||||
canDispatchSegment(route) {
|
||||
const previousArrived = this.previousArrivedQuantity(route);
|
||||
if (previousArrived === null) return true;
|
||||
return previousArrived > 0;
|
||||
},
|
||||
remaining(route) {
|
||||
return Math.max(
|
||||
0,
|
||||
this.totalQuantity -
|
||||
this.dispatchedQuantity(route)
|
||||
);
|
||||
const masterRemaining = Math.max(0, this.totalQuantity - this.dispatchedQuantity(route));
|
||||
const previousArrived = this.previousArrivedQuantity(route);
|
||||
if (previousArrived === null) return masterRemaining;
|
||||
return Math.max(0, Math.min(masterRemaining, previousArrived - this.dispatchedQuantity(route)));
|
||||
},
|
||||
handleSegmentSelect(route, value) {
|
||||
if (value && !this.canDispatchSegment(route)) {
|
||||
route.selected = false;
|
||||
this.$message.warning(
|
||||
`${route.segmentNo}需等上一段运单完成到达后才能调度(上一段已到达 ${this.formatQuantity(this.previousArrivedQuantity(route))} ${this.quantityUnitLabel})`
|
||||
);
|
||||
}
|
||||
},
|
||||
formatQuantity(value) { const number = Number(value || 0); return Number.isInteger(number) ? number : number.toFixed(3).replace(/\.?0+$/, ''); },
|
||||
formatAmount(value) { return Number(value || 0).toFixed(2); },
|
||||
@@ -518,7 +634,7 @@ export default {
|
||||
})),
|
||||
});
|
||||
},
|
||||
currencyLabel(value) { return ({ CNY: '人民币', RMB: '人民币', USD: '美元', EUR: '欧元' })[value] || value || '人民币'; },
|
||||
currencyLabel(value) { return ({ CNY: '元', RMB: '元', USD: '美元', EUR: '欧元' })[value] || value || '元'; },
|
||||
quantityNumber(value) { return Number(value || 0); },
|
||||
goodsKey(goods = {}, includeUnit = true) {
|
||||
return [
|
||||
@@ -563,13 +679,14 @@ export default {
|
||||
}, 0);
|
||||
},
|
||||
validatePreviousSegmentCompletedQuantity(route, goods) {
|
||||
const routeIndex = this.routes.indexOf(route);
|
||||
if (routeIndex <= 0) return true;
|
||||
const previousCompletedQuantity = this.quantityNumber(
|
||||
this.routes[routeIndex - 1]?.arrivedQuantity
|
||||
);
|
||||
const previousCompletedQuantity = this.previousArrivedQuantity(route);
|
||||
if (previousCompletedQuantity === null) return true;
|
||||
if (previousCompletedQuantity <= 0) {
|
||||
this.$message.warning(`${route.segmentNo}需等上一段运单完成到达后才能调度`);
|
||||
return false;
|
||||
}
|
||||
const currentDispatchQuantity = (goods || []).reduce(
|
||||
(sum, item) => sum + this.quantityNumber(item.dispatchQuantity),
|
||||
(sum, item) => sum + this.quantityNumber(item.dispatchQuantity ?? item.quantity),
|
||||
0
|
||||
);
|
||||
const dispatchTotal =
|
||||
@@ -578,7 +695,7 @@ export default {
|
||||
currentDispatchQuantity;
|
||||
if (dispatchTotal <= previousCompletedQuantity) return true;
|
||||
this.$message.warning(
|
||||
`${route.segmentNo}调度总量不能大于上一段已完成数量(${this.formatQuantity(previousCompletedQuantity)} ${this.quantityUnitLabel})`
|
||||
`${route.segmentNo}调度总量不能大于上一段已到达数量(${this.formatQuantity(previousCompletedQuantity)} ${this.quantityUnitLabel})`
|
||||
);
|
||||
return false;
|
||||
},
|
||||
@@ -836,6 +953,9 @@ export default {
|
||||
route.carrierContractId = '';
|
||||
});
|
||||
},
|
||||
handleDriverFocus(route) {
|
||||
this.activeSuggestRoute = route;
|
||||
},
|
||||
async loadDriverOptions(route = {}, keyword = '') {
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
@@ -853,35 +973,50 @@ export default {
|
||||
this.driverLoading = false;
|
||||
}
|
||||
},
|
||||
fetchDriverSuggestions(route, queryString, callback) {
|
||||
fetchDriverSuggestions(queryString, callback) {
|
||||
const route = this.activeSuggestRoute || {};
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
const keyword = String(queryString || '').trim();
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
if (!keyword) this.$message.warning('请先选择承运商');
|
||||
return;
|
||||
}
|
||||
this.loadDriverOptions(route, String(queryString || '').trim())
|
||||
.then(() =>
|
||||
this.driverLoading = true;
|
||||
fetchDriversByCarrierOrganizations(
|
||||
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
|
||||
carrier
|
||||
)
|
||||
.then(records => {
|
||||
this.driverOptions = records;
|
||||
callback(
|
||||
this.driverOptions.map(item => ({
|
||||
records.map(item => ({
|
||||
...item,
|
||||
value: this.driverOptionLabel(item),
|
||||
}))
|
||||
)
|
||||
)
|
||||
.catch(() => callback([]));
|
||||
);
|
||||
})
|
||||
.catch(() => callback([]))
|
||||
.finally(() => {
|
||||
this.driverLoading = false;
|
||||
});
|
||||
},
|
||||
handleDriverSuggestionSelect(route, item = {}) {
|
||||
handleDriverSuggestionSelect(item = {}) {
|
||||
const route = this.activeSuggestRoute;
|
||||
if (!route) return;
|
||||
route.driverName = this.driverOptionLabel(item);
|
||||
route.driverId = item.id || item.driverId || '';
|
||||
route.driverPhone = item.mobile || item.driverPhone || item.phone || route.driverPhone || '';
|
||||
const drivingVehicle = String(item.drivingVehicle || '').trim();
|
||||
if (drivingVehicle) route.vehicleNo = drivingVehicle;
|
||||
},
|
||||
fetchEscortSuggestions(route, queryString, callback) {
|
||||
fetchEscortSuggestions(queryString, callback) {
|
||||
const route = this.activeSuggestRoute || {};
|
||||
const keyword = String(queryString || '').trim();
|
||||
const carrier = this.getRouteCarrierFilter(route);
|
||||
if (!carrier.carrierId && !carrier.carrierName) {
|
||||
callback([]);
|
||||
if (!keyword) this.$message.warning('请先选择承运商');
|
||||
return;
|
||||
}
|
||||
this.escortLoading = true;
|
||||
@@ -902,7 +1037,9 @@ export default {
|
||||
this.escortLoading = false;
|
||||
});
|
||||
},
|
||||
handleEscortSuggestionSelect(route, item = {}) {
|
||||
handleEscortSuggestionSelect(item = {}) {
|
||||
const route = this.activeSuggestRoute;
|
||||
if (!route) return;
|
||||
route.escortName = this.driverOptionLabel(item);
|
||||
const phone = item.mobile || item.driverPhone || item.phone || '';
|
||||
if (phone) route.escortPhone = phone;
|
||||
@@ -927,6 +1064,10 @@ export default {
|
||||
addToPending(route, continueDispatch) {
|
||||
const goods = route.goods.filter(item => Number(item.dispatchQuantity) > 0);
|
||||
if (!goods.length) return this.$message.warning('请填写本次数量');
|
||||
if (!this.canDispatchSegment(route)) {
|
||||
return this.$message.warning(`${route.segmentNo}需等上一段运单完成到达后才能调度`);
|
||||
}
|
||||
if (!this.validatePreviousSegmentCompletedQuantity(route, goods)) return;
|
||||
if (!this.validateDispatchGoods(route, goods)) return;
|
||||
this.syncFreightItems(route);
|
||||
if (!this.validateFreightItems(route)) return;
|
||||
@@ -1041,9 +1182,11 @@ export default {
|
||||
.pending-list-spacer { height: min(436px, calc(100vh - 130px)); }
|
||||
.master-overview, .dispatch-section { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
||||
.overview-heading, .segment-header, .pending-bar { display: flex; align-items: center; justify-content: space-between; }
|
||||
.segment-header :deep(.el-checkbox.is-checked .el-checkbox__label) { font-size: 16px; font-weight: 700; }
|
||||
.segment-checkbox-label { display: inline-flex; align-items: center; min-width: 0; }
|
||||
.segment-checkbox-path { display: inline-block; white-space: nowrap; }
|
||||
.segment-checkbox-wrap { display: inline-flex; max-width: 100%; }
|
||||
.segment-checkbox-label { display: inline-flex; align-items: center; min-width: 0; font-size: 14px; font-weight: 400; color: #303133; }
|
||||
/* 仅「段X:」加粗加大,冒号右侧路线保持常规字号 */
|
||||
.segment-checkbox-no { font-size: 16px; font-weight: 700; color: #303133; }
|
||||
.segment-checkbox-path { display: inline-block; white-space: nowrap; font-size: 14px; font-weight: 400; }
|
||||
.overview-heading { padding: 18px 24px 12px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
|
||||
.overview-content { display: grid; grid-template-columns: minmax(400px, .9fr) minmax(620px, 1.6fr); gap: 28px; padding: 8px 24px 20px; }
|
||||
.overview-meta { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 24px; margin: 0; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
|
||||
@@ -1055,6 +1198,12 @@ export default {
|
||||
.segment-header { min-height: 46px; padding: 0 20px; border-bottom: 1px solid #eff1f7; font-weight: 600; em { color: #409eff; font-style: normal; } }
|
||||
.segment-content { padding: 16px 24px; }
|
||||
.dispatch-form { :deep(.el-form-item) { margin-bottom: 14px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
|
||||
/* 短标签按自身宽度,避免被「预计发货日期」等长标签把左侧撑开 */
|
||||
.dispatch-form__compact-label {
|
||||
:deep(.el-form-item__label) {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
.transport-type-field :deep(.el-input) { width: 240px; }
|
||||
.goods-heading { display: flex; align-items: center; justify-content: space-between; margin: 0 -24px 12px; padding: 14px 24px; border-top: 1px solid #eff1f7; border-bottom: 1px solid #eff1f7; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } span { color: #909399; font-size: 13px; } }
|
||||
.freight-heading { margin: 16px 0 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
><el-select
|
||||
v-model="form.projectId"
|
||||
filterable
|
||||
:disabled="isBasicRouteReadonly"
|
||||
placeholder="请输入并选择"
|
||||
@change="projectChange"
|
||||
><el-option
|
||||
@@ -20,7 +21,7 @@
|
||||
><el-form-item label="客户合同" prop="contractName"
|
||||
><el-select
|
||||
v-model="form.contractId"
|
||||
:disabled="!form.projectId"
|
||||
:disabled="isBasicRouteReadonly || !form.projectId"
|
||||
placeholder="选择项目后带出"
|
||||
@change="contractChange"
|
||||
><el-option
|
||||
@@ -41,6 +42,7 @@
|
||||
><el-form-item label="备注"
|
||||
><el-input
|
||||
v-model="form.remark"
|
||||
:disabled="isBasicRouteReadonly"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入" /></el-form-item></el-col></el-row
|
||||
@@ -49,7 +51,7 @@
|
||||
<section>
|
||||
<div class="section-head">
|
||||
<h3>收发货信息</h3>
|
||||
<div class="section-head__actions">
|
||||
<div v-if="!isBasicRouteReadonly" class="section-head__actions">
|
||||
<el-link type="primary" @click="addRoute">新增途经点</el-link>
|
||||
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
|
||||
</div>
|
||||
@@ -62,13 +64,13 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="发货地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress('departure')" v-model="form.departureName" class="route-address-name" readonly placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths.departure" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('departure', value)" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly placeholder="请输入详细地址" @click="handleAddressDetailClick('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" @click="openCommonAddress('departure')" /></el-tooltip></div
|
||||
><el-input v-if="isNonRoadAddress('departure')" v-model="form.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly" placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths.departure" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('departure', value)" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly" placeholder="请输入详细地址" @click="handleAddressDetailClick('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly" @click="openCommonAddress('departure')" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人" class="route-contact-form-item"
|
||||
><el-input v-model="form.departureContact" placeholder="请输入"
|
||||
><el-input v-model="form.departureContact" :disabled="isBasicRouteReadonly" placeholder="请输入"
|
||||
/></el-form-item>
|
||||
<el-form-item label="联系方式" class="route-phone-form-item"
|
||||
><el-input v-model="form.departurePhone" placeholder="请输入"
|
||||
><el-input v-model="form.departurePhone" :disabled="isBasicRouteReadonly" placeholder="请输入"
|
||||
/></el-form-item>
|
||||
<el-form-item label="计划开始" class="route-date-form-item"
|
||||
><el-date-picker
|
||||
@@ -76,6 +78,7 @@
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled="isBasicRouteReadonly"
|
||||
placeholder="请选择"
|
||||
/></el-form-item>
|
||||
</div>
|
||||
@@ -83,7 +86,7 @@
|
||||
</div>
|
||||
<template v-for="(route, index) in form.routes" :key="index">
|
||||
<div class="route-flow-segment">
|
||||
<div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(index + 1) }} 段{{ index + 1 }}</span><el-tooltip :content="getRouteTitle(index).replace(`段${index + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getRouteTitle(index), index + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
|
||||
<div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(index + 1) }} 段{{ index + 1 }}</span><el-tooltip :content="getRouteTitle(index).replace(`段${index + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getRouteTitle(index), index + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="route.transportType" :disabled="isBasicRouteReadonly" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
|
||||
</div>
|
||||
<div class="route-flow-row">
|
||||
<span class="route-flow-marker route-flow-marker--middle">经</span>
|
||||
@@ -91,16 +94,16 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="途经地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人" class="route-contact-form-item"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item>
|
||||
<el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" placeholder="请输入" /><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></div></el-form-item>
|
||||
<el-form-item label="联系人" class="route-contact-form-item"><el-input v-model="route.departureContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||
<el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" :disabled="isBasicRouteReadonly" placeholder="请输入" /><template v-if="!isBasicRouteReadonly"><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></template></div></el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="route-flow-segment">
|
||||
<div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(form.routes.length + 1) }} 段{{ form.routes.length + 1 }}</span><el-tooltip :content="getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getFinalRouteTitle(), form.routes.length + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
|
||||
<div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(form.routes.length + 1) }} 段{{ form.routes.length + 1 }}</span><el-tooltip :content="getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getFinalRouteTitle(), form.routes.length + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="form.finalTransportType" :disabled="isBasicRouteReadonly" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
|
||||
</div>
|
||||
<div class="route-flow-row">
|
||||
<span class="route-flow-marker route-flow-marker--end">终</span>
|
||||
@@ -108,18 +111,19 @@
|
||||
<div class="route-node-form">
|
||||
<el-form-item label="收货地" required class="route-address-form-item"
|
||||
><div class="route-address-inputs"
|
||||
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||
></el-form-item>
|
||||
<el-form-item label="联系人" class="route-contact-form-item"
|
||||
><el-input v-model="form.arrivalContact" placeholder="请输入" /></el-form-item>
|
||||
><el-input v-model="form.arrivalContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||
<el-form-item label="联系方式" class="route-phone-form-item"
|
||||
><el-input v-model="form.arrivalPhone" placeholder="请输入" /></el-form-item>
|
||||
><el-input v-model="form.arrivalPhone" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
|
||||
<el-form-item label="计划结束" class="route-date-form-item"
|
||||
><el-date-picker
|
||||
v-model="form.planEndTime"
|
||||
type="date"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
:disabled="isBasicRouteReadonly"
|
||||
placeholder="请选择"
|
||||
/></el-form-item>
|
||||
</div>
|
||||
@@ -144,6 +148,7 @@
|
||||
><el-autocomplete
|
||||
v-model="row.cargoName"
|
||||
clearable
|
||||
:disabled="isGoodsCategoryLocked(row)"
|
||||
placeholder="请输入货物名称"
|
||||
:debounce="300"
|
||||
:fetch-suggestions="
|
||||
@@ -168,22 +173,29 @@
|
||||
:placeholder="row.cargoType || '请选择'"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="isGoodsCategoryLocked(row)"
|
||||
:loading="cargoTypeLoading"
|
||||
@visible-change="visible => visible && loadCargoTypeOptions()"
|
||||
@change="value => handleCargoTypeChange(row, value)"
|
||||
/></template></el-table-column
|
||||
><el-table-column label="数量" width="180" class-name="master-editor__quantity-cell"
|
||||
><el-table-column label="数量" width="200" class-name="master-editor__quantity-cell"
|
||||
><template #header><span>数量<span class="required-asterisk">*</span></span></template>
|
||||
<template #default="{ row }"
|
||||
><el-input
|
||||
v-model="row.quantity"
|
||||
inputmode="decimal"
|
||||
placeholder="请输入"
|
||||
@input="value => handleQuantityInput(row, value)" /></template></el-table-column
|
||||
><div class="master-editor__quantity-field"
|
||||
><el-input
|
||||
v-model="row.quantity"
|
||||
inputmode="decimal"
|
||||
placeholder="请输入"
|
||||
@input="value => handleQuantityInput(row, value)"
|
||||
@blur="() => enforceMinDispatchedQuantity(row)"
|
||||
/><small v-if="dispatchedQuantityOf(row) > 0" class="master-editor__quantity-hint"
|
||||
>已调度 {{ formatDispatchedQuantity(dispatchedQuantityOf(row)) }},不可低于此值</small
|
||||
></div
|
||||
></template></el-table-column
|
||||
><el-table-column label="数量单位" width="120"
|
||||
><template #header><span>数量单位<span class="required-asterisk">*</span></span></template>
|
||||
<template #default="{ row }"
|
||||
><el-select v-model="row.quantityUnit"
|
||||
><el-select v-model="row.quantityUnit" :disabled="isGoodsCategoryLocked(row)"
|
||||
><el-option v-for="unit in quantityUnitOptions" :key="unit" :label="unit" :value="unit" /></el-select></template></el-table-column
|
||||
><el-table-column label="包装" width="130"
|
||||
><template #default="{ row }"
|
||||
@@ -218,7 +230,7 @@
|
||||
>新增</el-link
|
||||
><el-link
|
||||
type="danger"
|
||||
:disabled="form.goods.length === 1"
|
||||
:disabled="form.goods.length === 1 || isGoodsCategoryLocked(form.goods[$index])"
|
||||
@click="removeGoods($index)"
|
||||
>删除</el-link
|
||||
></div
|
||||
@@ -272,10 +284,9 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
|
||||
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" sortable />
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row, $index }">
|
||||
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
|
||||
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -707,6 +718,31 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo']),
|
||||
// 非草稿编辑:基本信息与收发货路线只读
|
||||
isBasicRouteReadonly() {
|
||||
return Boolean(this.id) && Boolean(this.form.businessStatus) && this.form.businessStatus !== 'draft';
|
||||
},
|
||||
// 品类 → 已调度量(各分段取最大值;优先用详情返回的 dispatchedGoods)
|
||||
dispatchedGoodsMap() {
|
||||
const map = {};
|
||||
const source = this.form.dispatchedGoods;
|
||||
if (source && typeof source === 'object' && !Array.isArray(source)) {
|
||||
Object.entries(source).forEach(([key, qty]) => {
|
||||
const number = Number(qty);
|
||||
if (Number.isFinite(number)) map[key] = number;
|
||||
});
|
||||
return map;
|
||||
}
|
||||
(this.form.routeProgress || []).forEach(route => {
|
||||
const goods = route?.dispatchedGoods || {};
|
||||
Object.entries(goods).forEach(([key, qty]) => {
|
||||
const number = Number(qty);
|
||||
if (!Number.isFinite(number)) return;
|
||||
map[key] = Math.max(map[key] || 0, number);
|
||||
});
|
||||
});
|
||||
return map;
|
||||
},
|
||||
lastRouteName() {
|
||||
return this.form.routes[this.form.routes.length - 1]?.departureName || '途经地';
|
||||
},
|
||||
@@ -1105,6 +1141,7 @@ export default {
|
||||
return row.regionCode || row.districtCode || row.areaCode || row.adcode || '';
|
||||
},
|
||||
openCommonAddress(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.addressTarget = target;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.stationQuery = {};
|
||||
@@ -1210,6 +1247,7 @@ export default {
|
||||
this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget));
|
||||
},
|
||||
handleAddressDetailClick(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.openCommonAddress(target);
|
||||
return;
|
||||
@@ -1217,6 +1255,7 @@ export default {
|
||||
this.openAddressMap(target);
|
||||
},
|
||||
openAddressMap(target) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
if (this.addressTransportMode(target) !== 'road') {
|
||||
this.$message.info('非公路运输地址不能使用地图选择');
|
||||
return;
|
||||
@@ -1657,6 +1696,7 @@ export default {
|
||||
});
|
||||
},
|
||||
openRouteDialog() {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.routeQuery = {};
|
||||
this.routePage.current = 1;
|
||||
this.routeDialogVisible = true;
|
||||
@@ -1714,12 +1754,28 @@ export default {
|
||||
return { segmentNo: `段${index + 1}`, transportType: '' };
|
||||
},
|
||||
addRoute() {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.form.routes.push(this.createRoute());
|
||||
},
|
||||
removeRoute(index) {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.form.routes.splice(index, 1);
|
||||
this.syncAddressRegionPaths();
|
||||
},
|
||||
goodsKey(row = {}) {
|
||||
return `${row.cargoName || ''}\u0000${row.cargoType || ''}`;
|
||||
},
|
||||
dispatchedQuantityOf(row = {}) {
|
||||
return Number(this.dispatchedGoodsMap[this.goodsKey(row)] || 0);
|
||||
},
|
||||
formatDispatchedQuantity(value) {
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) return '0';
|
||||
return String(number);
|
||||
},
|
||||
isGoodsCategoryLocked(row = {}) {
|
||||
return this.dispatchedQuantityOf(row) > 0;
|
||||
},
|
||||
addGoods(index) {
|
||||
this.form.goods.splice(index + 1, 0, {
|
||||
quantityUnit: quantityUnitOptions[0],
|
||||
@@ -1729,6 +1785,13 @@ export default {
|
||||
});
|
||||
},
|
||||
removeGoods(index) {
|
||||
const row = this.form.goods[index];
|
||||
if (this.isGoodsCategoryLocked(row)) {
|
||||
this.$message.warning(
|
||||
`货物【${row.cargoName || ''}】已有调度量,不能删除`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.form.goods.length > 1) this.form.goods.splice(index, 1);
|
||||
},
|
||||
handleQuantityInput(row, value) {
|
||||
@@ -1736,6 +1799,42 @@ export default {
|
||||
const [integer = '', ...decimals] = text.split('.');
|
||||
row.quantity = decimals.length ? `${integer}.${decimals.join('').slice(0, 3)}` : integer;
|
||||
},
|
||||
enforceMinDispatchedQuantity(row = {}) {
|
||||
const min = this.dispatchedQuantityOf(row);
|
||||
if (min <= 0) return true;
|
||||
const current = Number(row.quantity);
|
||||
if (!Number.isFinite(current) || current < min) {
|
||||
row.quantity = String(min);
|
||||
this.$message.warning(
|
||||
`货物【${row.cargoName || ''}】数量不能低于已调度量(${this.formatDispatchedQuantity(min)})`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
validateGoodsDispatchedQuantity() {
|
||||
const totals = {};
|
||||
(this.form.goods || []).forEach(row => {
|
||||
const key = this.goodsKey(row);
|
||||
totals[key] = (totals[key] || 0) + (Number(row.quantity) || 0);
|
||||
});
|
||||
for (const [key, qty] of Object.entries(this.dispatchedGoodsMap)) {
|
||||
const dispatched = Number(qty) || 0;
|
||||
if (dispatched <= 0) continue;
|
||||
const cargoName = String(key).split('\u0000')[0] || '未知货物';
|
||||
if (!(key in totals)) {
|
||||
this.$message.warning(`货物【${cargoName}】已有调度量,不能删除或变更品类`);
|
||||
return false;
|
||||
}
|
||||
if ((totals[key] || 0) < dispatched) {
|
||||
this.$message.warning(
|
||||
`货物【${cargoName}】数量不能低于已调度量(${this.formatDispatchedQuantity(dispatched)})`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
syncAttachmentsJson() {
|
||||
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||||
},
|
||||
@@ -1858,6 +1957,7 @@ export default {
|
||||
return this.$message.warning(`请完整填写第${invalidRouteIndex + 1}段途经地及运输方式`);
|
||||
if (!this.form.finalTransportType) return this.$message.warning('请填写最后一段运输方式');
|
||||
}
|
||||
if (!this.validateGoodsDispatchedQuantity()) return false;
|
||||
const payload = {
|
||||
...this.form,
|
||||
planStartTime: this.normalizeDateTimeValue(this.form.planStartTime),
|
||||
@@ -1943,6 +2043,16 @@ export default {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
}
|
||||
.master-editor__quantity-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.master-editor__quantity-hint {
|
||||
color: #e6a23c;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.goods-heading {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
|
||||
@@ -1268,63 +1268,116 @@ const fileRemove = () => {
|
||||
form.file = null;
|
||||
};
|
||||
|
||||
const performValidation = async (parsedRows) => {
|
||||
/** 运单批量导入每次请求最多提交行数,避免网关 body 超限。 */
|
||||
const IMPORT_BATCH_SIZE = 100;
|
||||
|
||||
const stripInternalRowFields = row => {
|
||||
const { _key, _duplicate, _duplicateKey, _editing, _editSnapshot, ...data } = row;
|
||||
return data;
|
||||
};
|
||||
|
||||
/**
|
||||
* 按配载标识号 / 同一运单标识号尽量同组,再按 IMPORT_BATCH_SIZE 切分。
|
||||
* 返回 [{ rows, globalIndexes }],globalIndexes 与 rows 一一对应。
|
||||
*/
|
||||
const chunkImportRows = sourceRows => {
|
||||
if (!sourceRows?.length) return [];
|
||||
const groups = [];
|
||||
const groupIndexByKey = new Map();
|
||||
sourceRows.forEach((row, globalIndex) => {
|
||||
const loadingIdentifier = String(row.loadingIdentifier || '').trim();
|
||||
const waybillIdentifier = String(row.waybillIdentifier || '').trim();
|
||||
let groupKey = '';
|
||||
if (loadingIdentifier) groupKey = `L:${loadingIdentifier}`;
|
||||
else if (waybillIdentifier) groupKey = `W:${waybillIdentifier}`;
|
||||
else groupKey = `R:${globalIndex}`;
|
||||
let groupPos = groupIndexByKey.get(groupKey);
|
||||
if (groupPos == null) {
|
||||
groupPos = groups.length;
|
||||
groupIndexByKey.set(groupKey, groupPos);
|
||||
groups.push({ rows: [], globalIndexes: [] });
|
||||
}
|
||||
groups[groupPos].rows.push(row);
|
||||
groups[groupPos].globalIndexes.push(globalIndex);
|
||||
});
|
||||
|
||||
const chunks = [];
|
||||
let current = { rows: [], globalIndexes: [] };
|
||||
const flush = () => {
|
||||
if (!current.rows.length) return;
|
||||
chunks.push(current);
|
||||
current = { rows: [], globalIndexes: [] };
|
||||
};
|
||||
groups.forEach(group => {
|
||||
// 单组超过上限时整组单独成批,保证跨行一致性校验与多货物合并完整。
|
||||
if (group.rows.length > IMPORT_BATCH_SIZE) {
|
||||
flush();
|
||||
chunks.push(group);
|
||||
return;
|
||||
}
|
||||
if (current.rows.length + group.rows.length > IMPORT_BATCH_SIZE) {
|
||||
flush();
|
||||
}
|
||||
current.rows.push(...group.rows);
|
||||
current.globalIndexes.push(...group.globalIndexes);
|
||||
});
|
||||
flush();
|
||||
return chunks;
|
||||
};
|
||||
|
||||
const downloadImportFailures = async failureRows => {
|
||||
if (!failureRows.length) return;
|
||||
const response = await api.exportImportFailures({
|
||||
rows: failureRows.map(row => stripInternalRowFields(row)),
|
||||
});
|
||||
const blob = response.data || response;
|
||||
downloadXls(blob, `运单导入失败明细_${dayjs().format('YYYYMMDDHHmmss')}.xlsx`);
|
||||
};
|
||||
|
||||
const performValidation = async parsedRows => {
|
||||
if (!parsedRows || !parsedRows.length) return false;
|
||||
|
||||
try {
|
||||
// 构建校验请求参数
|
||||
const payload = {
|
||||
...form,
|
||||
rows: parsedRows.map(row => {
|
||||
const { _key, _duplicate, _duplicateKey, _editing, _editSnapshot, ...data } = row;
|
||||
return data;
|
||||
})
|
||||
};
|
||||
const chunks = chunkImportRows(parsedRows);
|
||||
const errorMap = new Map();
|
||||
|
||||
// 调用后端校验接口
|
||||
const response = await api.validateImport(payload);
|
||||
|
||||
// 检查响应类型
|
||||
const blob = response.data || response;
|
||||
|
||||
// 如果是 Blob,需要检查其 MIME 类型
|
||||
if (blob instanceof Blob) {
|
||||
// Excel 文件的 MIME 类型
|
||||
if (blob.type.includes('application/vnd.ms-excel') ||
|
||||
blob.type.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')) {
|
||||
// 校验失败,下载错误明细
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `运单导入失败明细_${dayjs().format('YYYYMMDDHHmmss')}.xlsx`;
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
ElMessage.warning('数据校验失败,已自动下载错误明细表,请修正后重新上传');
|
||||
return false;
|
||||
} else if (blob.type.includes('application/json')) {
|
||||
// 可能是 JSON 响应被当作 Blob,需要解析
|
||||
const text = await blob.text();
|
||||
const json = JSON.parse(text);
|
||||
if (json.success) {
|
||||
ElMessage.success('数据校验通过');
|
||||
return true;
|
||||
} else {
|
||||
ElMessage.error(json.msg || '校验失败');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// 其他类型,默认认为是校验通过
|
||||
ElMessage.success('数据校验通过');
|
||||
return true;
|
||||
for (const chunk of chunks) {
|
||||
const payload = {
|
||||
...form,
|
||||
rows: chunk.rows.map(stripInternalRowFields),
|
||||
};
|
||||
const response = await api.validateImport(payload);
|
||||
const result = response?.data?.data ?? response?.data ?? response;
|
||||
if (result?.success === false || (Array.isArray(result?.errors) && result.errors.length)) {
|
||||
(result.errors || []).forEach(error => {
|
||||
const localIndex = Number(error.rowIndex);
|
||||
if (Number.isNaN(localIndex) || localIndex < 0 || localIndex >= chunk.globalIndexes.length) {
|
||||
return;
|
||||
}
|
||||
const globalIndex = chunk.globalIndexes[localIndex];
|
||||
errorMap.set(globalIndex, error.errorMessage || '校验失败');
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 不是 Blob,直接判断
|
||||
ElMessage.success('数据校验通过');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (errorMap.size) {
|
||||
const failureRows = [];
|
||||
errorMap.forEach((errorMessage, globalIndex) => {
|
||||
failureRows.push({
|
||||
...stripInternalRowFields(parsedRows[globalIndex]),
|
||||
errorMessage,
|
||||
});
|
||||
});
|
||||
await downloadImportFailures(failureRows);
|
||||
ElMessage.warning('数据校验失败,已自动下载错误明细表,请修正后重新上传');
|
||||
return false;
|
||||
}
|
||||
|
||||
ElMessage.success('数据校验通过');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('校验失败:', error);
|
||||
ElMessage.error('校验接口调用失败');
|
||||
ElMessage.error(error?.message || '校验接口调用失败');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1442,9 +1495,14 @@ const normalizeImportRow = row => ({
|
||||
row['*运输方式']
|
||||
),
|
||||
});
|
||||
const buildImportPayload = (status = form.status) => {
|
||||
const buildImportPayload = (status = form.status, rowList = rows.value, extra = {}) => {
|
||||
const { file, ...batch } = form;
|
||||
return { ...batch, status, rows: rows.value.map(normalizeImportRow) };
|
||||
return {
|
||||
...batch,
|
||||
status,
|
||||
rows: rowList.map(normalizeImportRow).map(stripInternalRowFields),
|
||||
...extra,
|
||||
};
|
||||
};
|
||||
// 独立页面下返回批次列表路由,弹窗形态下关闭新建面板并刷新列表。
|
||||
const backToList = () => {
|
||||
@@ -1454,7 +1512,24 @@ const backToList = () => {
|
||||
// 保存草稿:批次与已解析明细一并入库,成功后返回批次列表。
|
||||
const saveDraft = async () => {
|
||||
await formRef.value?.validateField(draftRequiredFields);
|
||||
await api.saveImportDraft(buildImportPayload('draft'));
|
||||
const chunks = chunkImportRows(rows.value);
|
||||
if (!chunks.length) {
|
||||
ElMessage.warning('请上传至少一条运单明细');
|
||||
return;
|
||||
}
|
||||
// 草稿编辑时首批带原 id 覆盖;后续批 append,避免整包超限。
|
||||
let batchId = form.id || '';
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const append = i > 0;
|
||||
const payload = buildImportPayload('draft', chunks[i].rows, {
|
||||
id: append ? batchId : batchId || undefined,
|
||||
append,
|
||||
});
|
||||
const response = await api.saveImportDraft(payload);
|
||||
const batch = response?.data?.data ?? response?.data ?? response;
|
||||
batchId = batch?.id || batchId;
|
||||
}
|
||||
if (batchId) form.id = batchId;
|
||||
ElMessage.success('保存草稿成功');
|
||||
backToList();
|
||||
};
|
||||
@@ -1467,9 +1542,32 @@ const confirmImport = async () => {
|
||||
if (count) await ElMessageBox.confirm(`当前导入有${count}条重复数据,是否确认导入?`, '提示');
|
||||
if (form.importType === 'settlement')
|
||||
await ElMessageBox.confirm('确认后将同时生成运单、应收应付明细、计结算单,是否继续?', '提示');
|
||||
await api.confirmImport(buildImportPayload());
|
||||
ElMessage.success('导入成功');
|
||||
backToList();
|
||||
|
||||
const chunks = chunkImportRows(rows.value);
|
||||
// 仅草稿转确认时复用原批次 id;过程中不回写 form.id,避免失败重试误覆盖已完成批次。
|
||||
const draftBatchId = form.id || '';
|
||||
let batchId = '';
|
||||
try {
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
const append = i > 0;
|
||||
const payload = buildImportPayload(form.status, chunks[i].rows, {
|
||||
id: append ? batchId : draftBatchId || undefined,
|
||||
append,
|
||||
});
|
||||
const response = await api.confirmImport(payload);
|
||||
const batch = response?.data?.data ?? response?.data ?? response;
|
||||
batchId = batch?.id || batchId;
|
||||
}
|
||||
ElMessage.success('导入成功');
|
||||
backToList();
|
||||
} catch (error) {
|
||||
console.error('确认导入失败:', error);
|
||||
ElMessage.error(
|
||||
batchId
|
||||
? `部分数据可能已导入(批次已创建),请到批次详情核对后重试:${error?.message || '导入失败'}`
|
||||
: error?.message || '导入失败'
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3173,9 +3173,11 @@ const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
let amapLoader;
|
||||
const standaloneWaybillFormRoute = '/business/waybill-manage/form';
|
||||
const standaloneWaybillFormPaths = ['/business/waybill-manage', standaloneWaybillFormRoute];
|
||||
const standaloneWaybillListRoute = '/business/waybill-manage';
|
||||
const standaloneWaybillFormPaths = [standaloneWaybillListRoute, standaloneWaybillFormRoute];
|
||||
const standaloneWaybillDetailRoute = '/business/waybill-manage/detail';
|
||||
const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills';
|
||||
const waybillManageListRefreshKey = 'waybill-manage-list-refresh';
|
||||
|
||||
const attachmentViewerPlugins = [
|
||||
imagePlugin(),
|
||||
@@ -3692,6 +3694,14 @@ export default {
|
||||
deactivated() {
|
||||
this.closeInnerDialogs();
|
||||
},
|
||||
activated() {
|
||||
// 列表实例被 keep-alive 重新激活时,按标记拉取最新数据
|
||||
if (this.formPageLocked || this.detailPageLocked) return;
|
||||
if (sessionStorage.getItem(waybillManageListRefreshKey)) {
|
||||
sessionStorage.removeItem(waybillManageListRefreshKey);
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.closeInnerDialogs();
|
||||
},
|
||||
@@ -3999,7 +4009,19 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
if (!this.isStandaloneWaybillFormPage) return;
|
||||
// 列表实例:从表单/详情返回时刷新,避免 keep-alive 仍显示旧数据
|
||||
if (!this.formPageLocked && !this.detailPageLocked) {
|
||||
if (
|
||||
to.path === standaloneWaybillListRoute &&
|
||||
(from?.path?.includes('/waybill-manage/form') ||
|
||||
from?.path?.includes('/waybill-manage/detail'))
|
||||
) {
|
||||
sessionStorage.removeItem(waybillManageListRefreshKey);
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this.formPageLocked || !this.isStandaloneWaybillFormPage) return;
|
||||
if (
|
||||
to.query.mode !== from?.query?.mode ||
|
||||
String(to.query.id || '') !== String(from?.query?.id || '')
|
||||
@@ -4708,8 +4730,9 @@ export default {
|
||||
this.waybillTrackInfo = null;
|
||||
this.waybillTrackHint = '';
|
||||
if (this.isStandaloneWaybillDetailPage) {
|
||||
this.markWaybillListNeedRefresh();
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push({ path: '/business/waybill-manage', query: {} });
|
||||
this.$router.push({ path: standaloneWaybillListRoute, query: {} });
|
||||
return;
|
||||
}
|
||||
this.detailBox = false;
|
||||
@@ -5438,6 +5461,12 @@ export default {
|
||||
if (this.config.saveAsDraft && saveAsDraft) {
|
||||
submitRow[this.config.draftStatusProp || 'approvalStatus'] =
|
||||
this.config.draftStatus || 'draft';
|
||||
} else if (
|
||||
this.config.draftStatusProp === 'businessStatus' &&
|
||||
submitRow.businessStatus === (this.config.draftStatus || 'draft')
|
||||
) {
|
||||
// 正式提交:清空草稿态,由后端按过程配置校正为待执行/进行中
|
||||
submitRow.businessStatus = '';
|
||||
}
|
||||
delete submitRow.basicInfoTitle;
|
||||
delete submitRow.templateInfoTitle;
|
||||
@@ -5675,6 +5704,7 @@ export default {
|
||||
})
|
||||
: Promise.resolve();
|
||||
return after.then(() => {
|
||||
this.markWaybillListNeedRefresh();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '提交成功!' });
|
||||
done();
|
||||
@@ -5713,6 +5743,7 @@ export default {
|
||||
});
|
||||
return after.then(() => {
|
||||
this.attachmentUploadMode = false;
|
||||
this.markWaybillListNeedRefresh();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: skipMk ? '操作成功!' : '提交成功!' });
|
||||
done();
|
||||
@@ -5729,6 +5760,9 @@ export default {
|
||||
getSaveRequest() {
|
||||
return this.api.submit;
|
||||
},
|
||||
markWaybillListNeedRefresh() {
|
||||
sessionStorage.setItem(waybillManageListRefreshKey, '1');
|
||||
},
|
||||
handleDraftSave() {
|
||||
const request =
|
||||
typeof this.api.saveDraft === 'function' ? this.api.saveDraft : this.api.submit;
|
||||
@@ -5739,6 +5773,7 @@ export default {
|
||||
request(submitRow)
|
||||
.then(() => {
|
||||
this.$message.success(`${this.config.draftSaveText || '保存'}成功`);
|
||||
this.markWaybillListNeedRefresh();
|
||||
this.onLoad(this.page, this.query);
|
||||
this.closeCrudDialog();
|
||||
})
|
||||
@@ -5748,8 +5783,10 @@ export default {
|
||||
},
|
||||
closeCrudDialog() {
|
||||
if (this.isStandaloneWaybillPage) {
|
||||
// 关闭独立表单/详情页时标记列表需刷新(含直接点关闭未保存的场景)
|
||||
this.markWaybillListNeedRefresh();
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push({ path: '/business/waybill-manage', query: {} });
|
||||
this.$router.push({ path: standaloneWaybillListRoute, query: {} });
|
||||
return;
|
||||
}
|
||||
const crud = this.$refs.crud;
|
||||
|
||||
@@ -1034,6 +1034,11 @@ import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-fil
|
||||
import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
|
||||
const contractManageListRoute = '/business/contract-manage';
|
||||
const contractManageFormRoute = '/business/contract-manage/form';
|
||||
const contractManageDetailRoute = '/business/contract-manage/detail';
|
||||
const contractManageListRefreshKey = 'contract-manage-list-refresh';
|
||||
|
||||
const clone = value => JSON.parse(JSON.stringify(value));
|
||||
const normalizeOptionalInteger = (value, emptyValue = null) => {
|
||||
if (value === undefined || value === null || value === '' || Number(value) < 0) {
|
||||
@@ -1296,6 +1301,9 @@ export default {
|
||||
flowBox: false,
|
||||
flowUrl: '',
|
||||
processInstanceId: '',
|
||||
// 页面形态在实例创建时锁定,避免 keep-alive 缓存后随全局 $route 翻转
|
||||
formPageLocked: false,
|
||||
detailPageLocked: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -1313,15 +1321,13 @@ export default {
|
||||
return this.hasPermission(`${this.config.permission}_add`);
|
||||
},
|
||||
isListPage() {
|
||||
return !this.isFormPage && !this.isDetailPage;
|
||||
return !this.formPageLocked && !this.detailPageLocked && !this.isPublicViewPage;
|
||||
},
|
||||
isFormPage() {
|
||||
return this.$route.path === '/business/contract-manage/form';
|
||||
return this.formPageLocked;
|
||||
},
|
||||
isDetailPage() {
|
||||
return (
|
||||
this.$route.path === '/business/contract-manage/detail' || this.isPublicViewPage
|
||||
);
|
||||
return this.detailPageLocked || this.isPublicViewPage;
|
||||
},
|
||||
isPublicViewPage() {
|
||||
return this.$route.path === '/business/contract-manage/public-view';
|
||||
@@ -1484,9 +1490,21 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'$route.fullPath'() {
|
||||
if (this.isFormPage) this.initFormPage();
|
||||
if (this.isDetailPage) this.initDetailPage();
|
||||
$route(to, from) {
|
||||
if (this.isListPage) {
|
||||
if (
|
||||
to.path === contractManageListRoute &&
|
||||
(from?.path?.includes('/contract-manage/form') ||
|
||||
from?.path?.includes('/contract-manage/detail') ||
|
||||
from?.path?.includes('/contract-manage/change'))
|
||||
) {
|
||||
sessionStorage.removeItem(contractManageListRefreshKey);
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this.formPageLocked && to.path === contractManageFormRoute) this.initFormPage();
|
||||
if (this.detailPageLocked && to.path === contractManageDetailRoute) this.initDetailPage();
|
||||
},
|
||||
settlementConfigTab(tab, oldTab) {
|
||||
if (tab === oldTab) return;
|
||||
@@ -1498,6 +1516,9 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.formPageLocked = this.$route.path === contractManageFormRoute;
|
||||
this.detailPageLocked =
|
||||
this.$route.path === contractManageDetailRoute || this.isPublicViewPage;
|
||||
if (!this.isPublicViewPage) {
|
||||
this.loadProjectOptions();
|
||||
this.loadSettlementDictionaries();
|
||||
@@ -1506,7 +1527,17 @@ export default {
|
||||
if (this.isFormPage) this.initFormPage();
|
||||
if (this.isDetailPage) this.initDetailPage();
|
||||
},
|
||||
activated() {
|
||||
if (!this.isListPage) return;
|
||||
if (sessionStorage.getItem(contractManageListRefreshKey)) {
|
||||
sessionStorage.removeItem(contractManageListRefreshKey);
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markContractManageListNeedRefresh() {
|
||||
sessionStorage.setItem(contractManageListRefreshKey, '1');
|
||||
},
|
||||
buildTableOption() {
|
||||
return {
|
||||
...option,
|
||||
@@ -1648,7 +1679,7 @@ export default {
|
||||
},
|
||||
openForm(mode, row = {}) {
|
||||
this.$router.push({
|
||||
path: '/business/contract-manage/form',
|
||||
path: contractManageFormRoute,
|
||||
query:
|
||||
mode === 'edit'
|
||||
? { mode, id: row.id, name: '编辑合同管理' }
|
||||
@@ -1741,8 +1772,9 @@ export default {
|
||||
this.$nextTick(() => this.$refs.contractForm?.clearValidate());
|
||||
},
|
||||
closeForm() {
|
||||
this.markContractManageListNeedRefresh();
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push('/business/contract-manage');
|
||||
this.$router.push(contractManageListRoute);
|
||||
},
|
||||
validateBase(action) {
|
||||
if (action === 'temporary') {
|
||||
@@ -2532,8 +2564,9 @@ export default {
|
||||
});
|
||||
},
|
||||
closeDetail() {
|
||||
this.markContractManageListNeedRefresh();
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push('/business/contract-manage');
|
||||
this.$router.push(contractManageListRoute);
|
||||
},
|
||||
applyDetailState(detail = {}) {
|
||||
this.detailRow = {
|
||||
@@ -2876,7 +2909,7 @@ export default {
|
||||
},
|
||||
handleCopy(row) {
|
||||
this.$router.push({
|
||||
path: '/business/contract-manage/form',
|
||||
path: contractManageFormRoute,
|
||||
query: { mode: 'add', copyId: row.id, name: '新增合同管理' },
|
||||
});
|
||||
},
|
||||
|
||||
@@ -3009,6 +3009,8 @@ export default {
|
||||
this.cargoRows = (cargoRows.length ? cargoRows : this.buildCargoRows(this.selectedWaybillRows)).map(
|
||||
row => sanitizeCargoRow(row)
|
||||
);
|
||||
// 运单回填时 routeNodes 尚为空会清空发到货地,路线恢复后需再同步
|
||||
this.syncRouteAddressFields();
|
||||
},
|
||||
restoreRouteChangeRecords() {
|
||||
try {
|
||||
@@ -3644,7 +3646,8 @@ export default {
|
||||
try {
|
||||
const query = {
|
||||
...this.buildQueryParams(this.candidateQuery),
|
||||
businessStatus: 'pending',
|
||||
// 待执行(需接单未接)与进行中(无需接单或已接单)均可配载
|
||||
businessStatus: 'pending,running',
|
||||
onlyUnassignedLoading: 1,
|
||||
// 仅查当前账户所属组织及子组织下的运单
|
||||
allDept: 0,
|
||||
@@ -3764,7 +3767,10 @@ export default {
|
||||
this.routeNodes = this.buildRouteNodes(rows);
|
||||
}
|
||||
this.cargoRows = this.buildCargoRows(rows);
|
||||
this.syncRouteAddressFields();
|
||||
// 编辑回填阶段 routeNodes 尚未恢复时跳过,避免清空已有发到货地
|
||||
if (rebuildRoute || this.routeNodes.length) {
|
||||
this.syncRouteAddressFields();
|
||||
}
|
||||
},
|
||||
buildRouteNodes(rows) {
|
||||
const nodeMap = new Map();
|
||||
|
||||
@@ -1008,6 +1008,10 @@ const attachmentViewerPlugins = [
|
||||
fallbackPlugin(),
|
||||
];
|
||||
|
||||
const projectApplyListRoute = '/business/project-apply';
|
||||
const projectApplyFormRoute = '/business/project-apply/form';
|
||||
const projectApplyListRefreshKey = 'project-apply-list-refresh';
|
||||
|
||||
const otherAttachmentType = { label: '其他附件', value: '其他附件' };
|
||||
|
||||
const normalProjectAttachmentTypeOptions = [
|
||||
@@ -1367,7 +1371,7 @@ export default {
|
||||
this.loadCargoTypeOptions();
|
||||
this.loadTransportTypeOptions();
|
||||
this.loadSettlementModeOptions();
|
||||
if (this.$route.path === '/business/project-apply/form') {
|
||||
if (this.$route.path === projectApplyFormRoute) {
|
||||
this.isFormPageInstance = true;
|
||||
this.openProjectFormPage();
|
||||
}
|
||||
@@ -1377,10 +1381,32 @@ export default {
|
||||
deactivated() {
|
||||
this.closeInnerDialogs();
|
||||
},
|
||||
activated() {
|
||||
if (this.isFormPageInstance || this.isPublicViewPage) return;
|
||||
if (sessionStorage.getItem(projectApplyListRefreshKey)) {
|
||||
sessionStorage.removeItem(projectApplyListRefreshKey);
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.closeInnerDialogs();
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
if (this.isFormPageInstance || this.isPublicViewPage) return;
|
||||
if (
|
||||
to.path === projectApplyListRoute &&
|
||||
from?.path?.includes('/project-apply/form')
|
||||
) {
|
||||
sessionStorage.removeItem(projectApplyListRefreshKey);
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
markProjectApplyListNeedRefresh() {
|
||||
sessionStorage.setItem(projectApplyListRefreshKey, '1');
|
||||
},
|
||||
closeInnerDialogs() {
|
||||
this.changeRecordDetailVisible = false;
|
||||
this.attachmentDocumentPreviewVisible = false;
|
||||
@@ -1506,7 +1532,9 @@ export default {
|
||||
},
|
||||
closeProjectForm() {
|
||||
if (this.isProjectFormPage) {
|
||||
this.$router.push('/business/project-apply');
|
||||
this.markProjectApplyListNeedRefresh();
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push(projectApplyListRoute);
|
||||
} else {
|
||||
this.projectBox = false;
|
||||
}
|
||||
@@ -1736,7 +1764,7 @@ export default {
|
||||
else if (type === 'view') query.name = `查看${this.config.title}`;
|
||||
else if (type === 'majorSupplement') query.name = '重大项目补录';
|
||||
else if (type === 'change') query.name = '项目变更';
|
||||
this.$router.push({ path: '/business/project-apply/form', query });
|
||||
this.$router.push({ path: projectApplyFormRoute, query });
|
||||
},
|
||||
resetProjectDialog() {
|
||||
this.$refs.projectForm?.clearValidate();
|
||||
|
||||
@@ -332,15 +332,34 @@ export default {
|
||||
? values.vehicleNo.trim()
|
||||
: values.vehicleNo.trim().toUpperCase();
|
||||
}
|
||||
// 非必填金额未填写时不要提交 -1 占位值
|
||||
values.directEconomicLoss = this.normalizeOptionalAmount(values.directEconomicLoss);
|
||||
values.insuranceClaimAmount = this.normalizeOptionalAmount(values.insuranceClaimAmount);
|
||||
values.accidentLocation = this.normalizeOptionalText(values.accidentLocation);
|
||||
values.accidentReasonDamage = this.normalizeOptionalText(values.accidentReasonDamage);
|
||||
values.remark = this.normalizeOptionalText(values.remark);
|
||||
values.attachments = this.stringifyAttachments(values.attachments);
|
||||
return values;
|
||||
},
|
||||
normalizeOptionalAmount(value) {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
normalizeOptionalText(value) {
|
||||
if (value === undefined || value === null) return null;
|
||||
const text = String(value).trim();
|
||||
return !text || text === '-1' ? null : text;
|
||||
},
|
||||
validateRow(row) {
|
||||
if (Number(row.directEconomicLoss) < 0) {
|
||||
const directEconomicLoss = this.normalizeOptionalAmount(row.directEconomicLoss);
|
||||
const insuranceClaimAmount = this.normalizeOptionalAmount(row.insuranceClaimAmount);
|
||||
if (directEconomicLoss !== null && Number(directEconomicLoss) < 0) {
|
||||
this.$message.warning('直接经济损失不能小于 0');
|
||||
return false;
|
||||
}
|
||||
if (Number(row.insuranceClaimAmount) < 0) {
|
||||
if (insuranceClaimAmount !== null && Number(insuranceClaimAmount) < 0) {
|
||||
this.$message.warning('保险理赔金额不能小于 0');
|
||||
return false;
|
||||
}
|
||||
@@ -353,13 +372,9 @@ export default {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
row.directEconomicLoss !== undefined &&
|
||||
row.directEconomicLoss !== null &&
|
||||
row.directEconomicLoss !== '' &&
|
||||
row.insuranceClaimAmount !== undefined &&
|
||||
row.insuranceClaimAmount !== null &&
|
||||
row.insuranceClaimAmount !== '' &&
|
||||
Number(row.insuranceClaimAmount) > Number(row.directEconomicLoss)
|
||||
directEconomicLoss !== null &&
|
||||
insuranceClaimAmount !== null &&
|
||||
Number(insuranceClaimAmount) > Number(directEconomicLoss)
|
||||
) {
|
||||
this.$message.warning('保险理赔金额不能超过直接经济损失金额');
|
||||
return false;
|
||||
@@ -448,6 +463,11 @@ export default {
|
||||
const detail = res.data.data;
|
||||
detail.attachments = this.parseAttachments(detail.attachments);
|
||||
detail.vehicleType = detail.vehicleType || '车辆';
|
||||
detail.directEconomicLoss = this.normalizeOptionalAmount(detail.directEconomicLoss);
|
||||
detail.insuranceClaimAmount = this.normalizeOptionalAmount(detail.insuranceClaimAmount);
|
||||
detail.accidentLocation = this.normalizeOptionalText(detail.accidentLocation) || '';
|
||||
detail.accidentReasonDamage = this.normalizeOptionalText(detail.accidentReasonDamage) || '';
|
||||
detail.remark = this.normalizeOptionalText(detail.remark) || '';
|
||||
this.form = detail;
|
||||
this.updateVehicleTypeDisplays(detail.vehicleType);
|
||||
this.vehicleOptions = [];
|
||||
|
||||
@@ -367,11 +367,33 @@ export default {
|
||||
} else {
|
||||
values.shipInspectionType = undefined;
|
||||
}
|
||||
// 费用必填,-1 转 null 后由校验拦截;非必填文本清除 "-1"
|
||||
values.fee = this.normalizeOptionalAmount(values.fee);
|
||||
values.inspectionUnit = this.normalizeOptionalText(values.inspectionUnit);
|
||||
values.assessmentUnit = this.normalizeOptionalText(values.assessmentUnit);
|
||||
values.passengerTypeLevel = this.normalizeOptionalText(values.passengerTypeLevel);
|
||||
values.remark = this.normalizeOptionalText(values.remark);
|
||||
values.attachments = this.stringifyAttachments(values.attachments);
|
||||
return values;
|
||||
},
|
||||
normalizeOptionalAmount(value) {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
normalizeOptionalText(value) {
|
||||
if (value === undefined || value === null) return null;
|
||||
const text = String(value).trim();
|
||||
return !text || text === '-1' ? null : text;
|
||||
},
|
||||
validateRow(row) {
|
||||
if (Number(row.fee) < 0) {
|
||||
const fee = this.normalizeOptionalAmount(row.fee);
|
||||
if (fee === null) {
|
||||
this.$message.warning('请输入费用');
|
||||
return false;
|
||||
}
|
||||
if (Number(fee) < 0) {
|
||||
this.$message.warning('费用不能小于 0');
|
||||
return false;
|
||||
}
|
||||
@@ -465,6 +487,11 @@ export default {
|
||||
getDetail(this.form.id).then(res => {
|
||||
const detail = res.data.data;
|
||||
detail.attachments = this.parseAttachments(detail.attachments);
|
||||
detail.fee = this.normalizeOptionalAmount(detail.fee);
|
||||
detail.inspectionUnit = this.normalizeOptionalText(detail.inspectionUnit) || '';
|
||||
detail.assessmentUnit = this.normalizeOptionalText(detail.assessmentUnit) || '';
|
||||
detail.passengerTypeLevel = this.normalizeOptionalText(detail.passengerTypeLevel) || '';
|
||||
detail.remark = this.normalizeOptionalText(detail.remark) || '';
|
||||
this.form = detail;
|
||||
this.updateVehicleTypeDisplays(detail.vehicleType);
|
||||
this.vehicleOptions = [];
|
||||
|
||||
@@ -1549,6 +1549,10 @@ import SectionCard from '@/components/section-card/main.vue';
|
||||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||||
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
|
||||
|
||||
const customerArchiveListRoute = '/vehicle/customer-archive';
|
||||
const customerArchiveFormRoute = '/vehicle/customer-archive/form';
|
||||
const customerArchiveListRefreshKey = 'customer-archive-list-refresh';
|
||||
|
||||
const createTimeRangeMap = {
|
||||
createTimeRange: ['createTimeStart', 'createTimeEnd'],
|
||||
};
|
||||
@@ -1646,6 +1650,8 @@ export default {
|
||||
readonly: false,
|
||||
reviewMode: false,
|
||||
listActivated: false,
|
||||
// 表单页实例标记:创建时按路由落定,避免 keep-alive 缓存后随全局 $route 翻转
|
||||
isFormPageInstance: false,
|
||||
originalAccessType: '',
|
||||
activeTab: 'scores',
|
||||
archiveForm: this.emptyArchive(),
|
||||
@@ -2033,6 +2039,7 @@ export default {
|
||||
this.openArchive({ id }, true);
|
||||
return;
|
||||
}
|
||||
this.isFormPageInstance = this.$route.path === customerArchiveFormRoute;
|
||||
this.initDeptTree();
|
||||
this.initRegionOptions();
|
||||
this.initBusinessDictionaries();
|
||||
@@ -2046,7 +2053,13 @@ export default {
|
||||
}
|
||||
},
|
||||
activated() {
|
||||
if (this.isArchivePage || this.isPublicViewPage) return;
|
||||
if (this.isFormPageInstance || this.isPublicViewPage) return;
|
||||
if (sessionStorage.getItem(customerArchiveListRefreshKey)) {
|
||||
sessionStorage.removeItem(customerArchiveListRefreshKey);
|
||||
this.onLoad(this.page, this.query);
|
||||
this.listActivated = true;
|
||||
return;
|
||||
}
|
||||
if (!this.listActivated) {
|
||||
this.listActivated = true;
|
||||
return;
|
||||
@@ -2065,9 +2078,7 @@ export default {
|
||||
};
|
||||
},
|
||||
isArchivePage() {
|
||||
return (
|
||||
this.$route.path === '/vehicle/customer-archive/form' || this.isPublicViewPage
|
||||
);
|
||||
return this.isFormPageInstance || this.isPublicViewPage;
|
||||
},
|
||||
isPublicViewPage() {
|
||||
return (
|
||||
@@ -2256,6 +2267,16 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
if (this.isFormPageInstance || this.isPublicViewPage) return;
|
||||
if (
|
||||
to.path === customerArchiveListRoute &&
|
||||
from?.path?.includes('/customer-archive/form')
|
||||
) {
|
||||
sessionStorage.removeItem(customerArchiveListRefreshKey);
|
||||
this.onLoad(this.page, this.query);
|
||||
}
|
||||
},
|
||||
'archiveForm.registeredDetailAddress'() {
|
||||
this.$nextTick(() => this.checkOverflow('registeredDetailInput', 'registeredDetailOverflow'));
|
||||
},
|
||||
@@ -2264,6 +2285,9 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
markCustomerArchiveListNeedRefresh() {
|
||||
sessionStorage.setItem(customerArchiveListRefreshKey, '1');
|
||||
},
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission[code], false);
|
||||
},
|
||||
@@ -4016,27 +4040,28 @@ export default {
|
||||
openArchivePage(row) {
|
||||
const isEdit = Boolean(row?.id);
|
||||
this.$router.push({
|
||||
path: '/vehicle/customer-archive/form',
|
||||
path: customerArchiveFormRoute,
|
||||
query: isEdit ? { id: row.id, name: '编辑客商档案' } : { name: '新增客商档案' },
|
||||
});
|
||||
},
|
||||
viewArchive(row) {
|
||||
this.$router.push({
|
||||
path: '/vehicle/customer-archive/form',
|
||||
path: customerArchiveFormRoute,
|
||||
query: { id: row.id, name: '查看客商档案', view: '1' },
|
||||
});
|
||||
},
|
||||
openReview(row) {
|
||||
this.$router.push({
|
||||
path: '/vehicle/customer-archive/form',
|
||||
path: customerArchiveFormRoute,
|
||||
query: { id: row.id, name: '客商复评', review: '1' },
|
||||
});
|
||||
},
|
||||
closeArchive() {
|
||||
if (this.isPublicViewPage) return;
|
||||
this.archiveBox = false;
|
||||
this.markCustomerArchiveListNeedRefresh();
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push('/vehicle/customer-archive');
|
||||
this.$router.push(customerArchiveListRoute);
|
||||
},
|
||||
openArchive(row, readonly = false) {
|
||||
this.readonly = readonly;
|
||||
|
||||
@@ -381,8 +381,26 @@ export default {
|
||||
if (values.policyNo) {
|
||||
values.policyNo = values.policyNo.trim();
|
||||
}
|
||||
// 非必填项未填写时不要提交 -1 占位值
|
||||
values.insuredAmount = this.normalizeOptionalAmount(values.insuredAmount);
|
||||
values.premium = this.normalizeOptionalAmount(values.premium);
|
||||
values.invoiceNo = this.normalizeOptionalText(values.invoiceNo);
|
||||
values.invoiceDate = this.normalizeOptionalText(values.invoiceDate);
|
||||
values.remark = this.normalizeOptionalText(values.remark);
|
||||
values.policyFile = this.normalizeOptionalText(values.policyFile);
|
||||
return values;
|
||||
},
|
||||
normalizeOptionalAmount(value) {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
normalizeOptionalText(value) {
|
||||
if (value === undefined || value === null) return null;
|
||||
const text = String(value).trim();
|
||||
return !text || text === '-1' ? null : text;
|
||||
},
|
||||
normalizeDecimalInput(prop, value) {
|
||||
const sanitized = String(value ?? '').replace(/[^\d.]/g, '');
|
||||
const [integerPart, ...decimalParts] = sanitized.split('.');
|
||||
@@ -398,8 +416,18 @@ export default {
|
||||
this.$message.warning('结束日期不能早于开始日期');
|
||||
return false;
|
||||
}
|
||||
if (Number(row.insuredAmount) < 0 || Number(row.premium) < 0) {
|
||||
this.$message.warning('保额、保费不能小于 0');
|
||||
const insuredAmount = this.normalizeOptionalAmount(row.insuredAmount);
|
||||
const premium = this.normalizeOptionalAmount(row.premium);
|
||||
if (insuredAmount !== null && Number(insuredAmount) < 0) {
|
||||
this.$message.warning('保额不能小于 0');
|
||||
return false;
|
||||
}
|
||||
if (premium === null) {
|
||||
this.$message.warning('请输入保费');
|
||||
return false;
|
||||
}
|
||||
if (Number(premium) < 0) {
|
||||
this.$message.warning('保费不能小于 0');
|
||||
return false;
|
||||
}
|
||||
if (row.remark && row.remark.length > 200) {
|
||||
@@ -479,15 +507,25 @@ export default {
|
||||
this.vehicleOptions = [];
|
||||
this.loadVehicleOptions('');
|
||||
this.form.vehicleType = '车辆';
|
||||
this.form.insuredAmount = '';
|
||||
this.form.invoiceNo = '';
|
||||
this.form.invoiceDate = '';
|
||||
this.form.remark = '';
|
||||
this.initOcrTemplateOptions('车辆', true);
|
||||
}
|
||||
if (['edit', 'view'].includes(type)) {
|
||||
this.isDetailLoading = true;
|
||||
getDetail(this.form.id)
|
||||
.then(res => {
|
||||
const detail = res.data.data || {};
|
||||
this.form = {
|
||||
...res.data.data,
|
||||
vehicleType: res.data.data.vehicleType || '车辆',
|
||||
...detail,
|
||||
vehicleType: detail.vehicleType || '车辆',
|
||||
insuredAmount: this.normalizeOptionalAmount(detail.insuredAmount),
|
||||
premium: this.normalizeOptionalAmount(detail.premium),
|
||||
invoiceNo: this.normalizeOptionalText(detail.invoiceNo) || '',
|
||||
invoiceDate: this.normalizeOptionalText(detail.invoiceDate) || '',
|
||||
remark: this.normalizeOptionalText(detail.remark) || '',
|
||||
};
|
||||
this.vehicleOptions = [];
|
||||
this.loadVehicleOptions(this.form.vehicleNo || '');
|
||||
|
||||
@@ -620,11 +620,11 @@ export default {
|
||||
});
|
||||
},
|
||||
formatMileage(value, unit) {
|
||||
if (value === undefined || value === null || value === '') return '';
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) return '';
|
||||
return `${Math.max(Number(value), 0)} ${unit || '公里'}`;
|
||||
},
|
||||
isUnsetNextMaintenanceMileage(value) {
|
||||
return Number(value) === -1;
|
||||
return value === undefined || value === null || value === '' || Number(value) === -1;
|
||||
},
|
||||
formatNextMaintenanceMileage(value, unit) {
|
||||
if (this.isUnsetNextMaintenanceMileage(value)) return '';
|
||||
@@ -647,7 +647,7 @@ export default {
|
||||
}
|
||||
},
|
||||
isEmpty(value) {
|
||||
return value === undefined || value === null || value === '';
|
||||
return value === undefined || value === null || value === '' || Number(value) === -1;
|
||||
},
|
||||
toNumber(value) {
|
||||
return this.isEmpty(value) ? null : Number(value);
|
||||
@@ -686,12 +686,34 @@ export default {
|
||||
const values = { ...row };
|
||||
values.vehicleType = values.vehicleType || '车辆';
|
||||
values.mileageUnit = values.vehicleType === '船舶' ? '海里' : '公里';
|
||||
// 非必填里程未填写时不要提交 -1 占位值
|
||||
values.mileage = this.normalizeOptionalAmount(values.mileage);
|
||||
values.nextMaintenanceMileage = this.normalizeOptionalAmount(values.nextMaintenanceMileage);
|
||||
values.maintainer = this.normalizeOptionalText(values.maintainer);
|
||||
values.maintenanceItem = this.normalizeOptionalText(values.maintenanceItem);
|
||||
values.storeName = this.normalizeOptionalText(values.storeName);
|
||||
values.contactPhone = this.normalizeOptionalText(values.contactPhone);
|
||||
values.address = this.normalizeOptionalText(values.address);
|
||||
values.remark = this.normalizeOptionalText(values.remark);
|
||||
values.attachments = this.stringifyAttachments(values.attachments);
|
||||
return values;
|
||||
},
|
||||
normalizeOptionalAmount(value) {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
normalizeOptionalText(value) {
|
||||
if (value === undefined || value === null) return null;
|
||||
const text = String(value).trim();
|
||||
return !text || text === '-1' ? null : text;
|
||||
},
|
||||
validateRow(row) {
|
||||
const mileage = this.toNumber(row.mileage);
|
||||
const nextMaintenanceMileage = this.toNumber(row.nextMaintenanceMileage);
|
||||
const mileage = this.toNumber(this.normalizeOptionalAmount(row.mileage));
|
||||
const nextMaintenanceMileage = this.toNumber(
|
||||
this.normalizeOptionalAmount(row.nextMaintenanceMileage)
|
||||
);
|
||||
if (
|
||||
mileage !== null &&
|
||||
nextMaintenanceMileage !== null &&
|
||||
@@ -782,12 +804,16 @@ export default {
|
||||
detail.attachments = this.parseAttachments(detail.attachments);
|
||||
detail.vehicleType = detail.vehicleType || '车辆';
|
||||
detail.mileageUnit = detail.vehicleType === '船舶' ? '海里' : '公里';
|
||||
if (type === 'edit') {
|
||||
if (Number(detail.mileage) === -1) detail.mileage = '';
|
||||
if (Number(detail.nextMaintenanceMileage) === -1) {
|
||||
detail.nextMaintenanceMileage = '';
|
||||
}
|
||||
}
|
||||
detail.mileage = this.normalizeOptionalAmount(detail.mileage);
|
||||
detail.nextMaintenanceMileage = this.normalizeOptionalAmount(
|
||||
detail.nextMaintenanceMileage
|
||||
);
|
||||
detail.maintainer = this.normalizeOptionalText(detail.maintainer) || '';
|
||||
detail.maintenanceItem = this.normalizeOptionalText(detail.maintenanceItem) || '';
|
||||
detail.storeName = this.normalizeOptionalText(detail.storeName) || '';
|
||||
detail.contactPhone = this.normalizeOptionalText(detail.contactPhone) || '';
|
||||
detail.address = this.normalizeOptionalText(detail.address) || '';
|
||||
detail.remark = this.normalizeOptionalText(detail.remark) || '';
|
||||
this.form = detail;
|
||||
this.updateVehicleType(detail.vehicleType);
|
||||
this.vehicleOptions = [];
|
||||
|
||||
@@ -345,7 +345,7 @@ export default {
|
||||
return JSON.stringify(value);
|
||||
},
|
||||
isEmpty(value) {
|
||||
return value === undefined || value === null || value === '';
|
||||
return value === undefined || value === null || value === '' || Number(value) === -1;
|
||||
},
|
||||
toNumber(value) {
|
||||
return this.isEmpty(value) ? null : Number(value);
|
||||
@@ -356,6 +356,15 @@ export default {
|
||||
normalizeIntegerInput(prop, value) {
|
||||
this.form[prop] = String(value ?? '').replace(/\D/g, '');
|
||||
},
|
||||
normalizeOptionalAmount(value) {
|
||||
if (this.isEmpty(value)) return null;
|
||||
return value;
|
||||
},
|
||||
normalizeOptionalText(value) {
|
||||
if (value === undefined || value === null) return null;
|
||||
const text = String(value).trim();
|
||||
return !text || text === '-1' ? null : text;
|
||||
},
|
||||
normalizeRow(row) {
|
||||
const values = { ...row };
|
||||
values.vehicleType = '车辆';
|
||||
@@ -363,6 +372,12 @@ export default {
|
||||
if (values.vehicleNo) {
|
||||
values.vehicleNo = values.vehicleNo.trim().toUpperCase();
|
||||
}
|
||||
// 非必填里程未填写时不要提交 -1 占位值
|
||||
values.previousMonthMileage = this.normalizeOptionalAmount(values.previousMonthMileage);
|
||||
values.currentMonthMileage = this.normalizeOptionalAmount(values.currentMonthMileage);
|
||||
values.monthlyMileage = this.normalizeOptionalAmount(values.monthlyMileage);
|
||||
values.totalMileage = this.normalizeOptionalAmount(values.totalMileage);
|
||||
values.remark = this.normalizeOptionalText(values.remark);
|
||||
const previousMonthMileage = this.toNumber(values.previousMonthMileage);
|
||||
const currentMonthMileage = this.toNumber(values.currentMonthMileage);
|
||||
if (
|
||||
@@ -503,6 +518,11 @@ export default {
|
||||
detail.attachments = this.parseAttachments(detail.attachments);
|
||||
detail.vehicleType = '车辆';
|
||||
detail.mileageUnit = '公里';
|
||||
detail.previousMonthMileage = this.normalizeOptionalAmount(detail.previousMonthMileage);
|
||||
detail.currentMonthMileage = this.normalizeOptionalAmount(detail.currentMonthMileage);
|
||||
detail.monthlyMileage = this.normalizeOptionalAmount(detail.monthlyMileage);
|
||||
detail.totalMileage = this.normalizeOptionalAmount(detail.totalMileage);
|
||||
detail.remark = this.normalizeOptionalText(detail.remark) || '';
|
||||
this.form = detail;
|
||||
this.vehicleOptions = [];
|
||||
this.loadVehicleOptions(this.form.vehicleNo || '');
|
||||
|
||||
@@ -337,7 +337,7 @@ export default {
|
||||
return JSON.stringify(value);
|
||||
},
|
||||
isEmpty(value) {
|
||||
return value === undefined || value === null || value === '';
|
||||
return value === undefined || value === null || value === '' || Number(value) === -1;
|
||||
},
|
||||
normalizeDecimalInput(prop, value) {
|
||||
const sanitized = String(value ?? '').replace(/[^\d.]/g, '');
|
||||
@@ -346,6 +346,15 @@ export default {
|
||||
? `${integerPart || '0'}.${decimalParts.join('').slice(0, 2)}`
|
||||
: integerPart;
|
||||
},
|
||||
normalizeOptionalAmount(value) {
|
||||
if (this.isEmpty(value)) return null;
|
||||
return value;
|
||||
},
|
||||
normalizeOptionalText(value) {
|
||||
if (value === undefined || value === null) return null;
|
||||
const text = String(value).trim();
|
||||
return !text || text === '-1' ? null : text;
|
||||
},
|
||||
normalizeRow(row) {
|
||||
const values = { ...row };
|
||||
values.vehicleType = values.vehicleType || '车辆';
|
||||
@@ -359,6 +368,16 @@ export default {
|
||||
if (values.feeType === '充电') {
|
||||
values.oilProduct = undefined;
|
||||
}
|
||||
// 非必填数值未填写时不要提交 -1;交易金额必填,-1 转 null 后由校验拦截
|
||||
values.quantity = this.normalizeOptionalAmount(values.quantity);
|
||||
values.unitPrice = this.normalizeOptionalAmount(values.unitPrice);
|
||||
values.transactionAmount = this.normalizeOptionalAmount(values.transactionAmount);
|
||||
values.balance = this.normalizeOptionalAmount(values.balance);
|
||||
values.cardNo = this.normalizeOptionalText(values.cardNo);
|
||||
values.cardHolder = this.normalizeOptionalText(values.cardHolder);
|
||||
values.station = this.normalizeOptionalText(values.station);
|
||||
values.oilProduct = this.normalizeOptionalText(values.oilProduct);
|
||||
values.remark = this.normalizeOptionalText(values.remark);
|
||||
values.attachments = this.stringifyAttachments(values.attachments);
|
||||
return values;
|
||||
},
|
||||
@@ -472,6 +491,15 @@ export default {
|
||||
const detail = res.data.data;
|
||||
detail.attachments = this.parseAttachments(detail.attachments);
|
||||
detail.vehicleType = detail.vehicleType || '车辆';
|
||||
detail.quantity = this.normalizeOptionalAmount(detail.quantity);
|
||||
detail.unitPrice = this.normalizeOptionalAmount(detail.unitPrice);
|
||||
detail.transactionAmount = this.normalizeOptionalAmount(detail.transactionAmount);
|
||||
detail.balance = this.normalizeOptionalAmount(detail.balance);
|
||||
detail.cardNo = this.normalizeOptionalText(detail.cardNo) || '';
|
||||
detail.cardHolder = this.normalizeOptionalText(detail.cardHolder) || '';
|
||||
detail.station = this.normalizeOptionalText(detail.station) || '';
|
||||
detail.oilProduct = this.normalizeOptionalText(detail.oilProduct) || '';
|
||||
detail.remark = this.normalizeOptionalText(detail.remark) || '';
|
||||
this.form = detail;
|
||||
this.updateVehicleType(detail.vehicleType);
|
||||
this.vehicleOptions = [];
|
||||
|
||||
@@ -298,19 +298,41 @@ export default {
|
||||
if (values.vehicleNo) {
|
||||
values.vehicleNo = values.vehicleNo.trim().toUpperCase();
|
||||
}
|
||||
// 非必填数量未填写时不要提交 -1;费用必填,-1 转 null 后由校验拦截
|
||||
values.tireQuantity = this.normalizeOptionalAmount(values.tireQuantity);
|
||||
values.replacementCost = this.normalizeOptionalAmount(values.replacementCost);
|
||||
values.handler = this.normalizeOptionalText(values.handler);
|
||||
values.tireBrand = this.normalizeOptionalText(values.tireBrand);
|
||||
values.replacementDescription = this.normalizeOptionalText(values.replacementDescription);
|
||||
values.remark = this.normalizeOptionalText(values.remark);
|
||||
values.attachments = this.stringifyAttachments(values.attachments);
|
||||
return values;
|
||||
},
|
||||
normalizeOptionalAmount(value) {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
normalizeOptionalText(value) {
|
||||
if (value === undefined || value === null) return null;
|
||||
const text = String(value).trim();
|
||||
return !text || text === '-1' ? null : text;
|
||||
},
|
||||
validateRow(row) {
|
||||
if (Number(row.replacementCost) < 0) {
|
||||
const replacementCost = this.normalizeOptionalAmount(row.replacementCost);
|
||||
const tireQuantity = this.normalizeOptionalAmount(row.tireQuantity);
|
||||
if (replacementCost === null) {
|
||||
this.$message.warning('请输入换胎费用');
|
||||
return false;
|
||||
}
|
||||
if (Number(replacementCost) < 0) {
|
||||
this.$message.warning('换胎费用不能小于 0');
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
row.tireQuantity !== undefined &&
|
||||
row.tireQuantity !== null &&
|
||||
row.tireQuantity !== '' &&
|
||||
(!Number.isInteger(Number(row.tireQuantity)) || Number(row.tireQuantity) <= 0)
|
||||
tireQuantity !== null &&
|
||||
(!Number.isInteger(Number(tireQuantity)) || Number(tireQuantity) <= 0)
|
||||
) {
|
||||
this.$message.warning('换胎数量必须为正整数');
|
||||
return false;
|
||||
@@ -391,9 +413,16 @@ export default {
|
||||
}
|
||||
if (['edit', 'view'].includes(type)) {
|
||||
getDetail(this.form.id).then(res => {
|
||||
const detail = res.data.data;
|
||||
detail.attachments = this.parseAttachments(detail.attachments);
|
||||
this.form = detail;
|
||||
const detail = res.data.data;
|
||||
detail.attachments = this.parseAttachments(detail.attachments);
|
||||
detail.tireQuantity = this.normalizeOptionalAmount(detail.tireQuantity);
|
||||
detail.replacementCost = this.normalizeOptionalAmount(detail.replacementCost);
|
||||
detail.handler = this.normalizeOptionalText(detail.handler) || '';
|
||||
detail.tireBrand = this.normalizeOptionalText(detail.tireBrand) || '';
|
||||
detail.replacementDescription =
|
||||
this.normalizeOptionalText(detail.replacementDescription) || '';
|
||||
detail.remark = this.normalizeOptionalText(detail.remark) || '';
|
||||
this.form = detail;
|
||||
this.vehicleOptions = [];
|
||||
this.loadVehicleOptions(this.form.vehicleNo || '');
|
||||
});
|
||||
|
||||
@@ -289,15 +289,20 @@ export default {
|
||||
normalizeRow(row) {
|
||||
const values = { ...row };
|
||||
values.vehicleType = values.vehicleType || '车辆';
|
||||
if (values.vehicleNo) {
|
||||
values.vehicleNo =
|
||||
values.vehicleType === '船舶'
|
||||
? values.vehicleNo.trim()
|
||||
: values.vehicleNo.trim().toUpperCase();
|
||||
// 非必填文本未填写时不要提交 "-1" 占位值
|
||||
values.vehicleNo = this.normalizeOptionalText(values.vehicleNo);
|
||||
if (values.vehicleNo && values.vehicleType !== '船舶') {
|
||||
values.vehicleNo = values.vehicleNo.toUpperCase();
|
||||
}
|
||||
values.remark = this.normalizeOptionalText(values.remark);
|
||||
values.attachments = this.stringifyAttachments(values.attachments);
|
||||
return values;
|
||||
},
|
||||
normalizeOptionalText(value) {
|
||||
if (value === undefined || value === null) return null;
|
||||
const text = String(value).trim();
|
||||
return !text || text === '-1' ? null : text;
|
||||
},
|
||||
validateRow(row) {
|
||||
if (row.changeContent && row.changeContent.length > 200) {
|
||||
this.$message.warning('变更内容不能超过 200 字');
|
||||
@@ -389,6 +394,8 @@ export default {
|
||||
const detail = res.data.data;
|
||||
detail.attachments = this.parseAttachments(detail.attachments);
|
||||
detail.vehicleType = detail.vehicleType || '车辆';
|
||||
detail.vehicleNo = this.normalizeOptionalText(detail.vehicleNo) || '';
|
||||
detail.remark = this.normalizeOptionalText(detail.remark) || '';
|
||||
this.form = detail;
|
||||
this.updateVehicleType(detail.vehicleType);
|
||||
this.vehicleOptions = [];
|
||||
|
||||
@@ -448,9 +448,25 @@ export default {
|
||||
} else {
|
||||
values.violationItem = undefined;
|
||||
}
|
||||
// 非必填项未填写时不要提交 -1 占位值
|
||||
values.fineAmount = this.normalizeOptionalAmount(values.fineAmount);
|
||||
values.deductPoints = this.normalizeOptionalAmount(values.deductPoints);
|
||||
values.penaltyUnit = this.normalizeOptionalText(values.penaltyUnit);
|
||||
values.processResult = this.normalizeOptionalText(values.processResult);
|
||||
values.attachments = this.stringifyAttachments(values.attachments);
|
||||
return values;
|
||||
},
|
||||
normalizeOptionalAmount(value) {
|
||||
if (value === undefined || value === null || value === '' || Number(value) === -1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
normalizeOptionalText(value) {
|
||||
if (value === undefined || value === null) return null;
|
||||
const text = String(value).trim();
|
||||
return !text || text === '-1' ? null : text;
|
||||
},
|
||||
validateRow(row) {
|
||||
if (row.vehicleType === '船舶' && !row.violationItem) {
|
||||
this.$message.warning('请输入事项');
|
||||
@@ -467,17 +483,17 @@ export default {
|
||||
this.$message.warning('日期不能超过当前时间');
|
||||
return false;
|
||||
}
|
||||
if (Number(row.fineAmount) < 0) {
|
||||
const fineAmount = this.normalizeOptionalAmount(row.fineAmount);
|
||||
const deductPoints = this.normalizeOptionalAmount(row.deductPoints);
|
||||
if (fineAmount !== null && Number(fineAmount) < 0) {
|
||||
this.$message.warning('被罚金额不能小于 0');
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
row.deductPoints !== undefined &&
|
||||
row.deductPoints !== null &&
|
||||
row.deductPoints !== '' &&
|
||||
(!Number.isInteger(Number(row.deductPoints)) ||
|
||||
Number(row.deductPoints) < 0 ||
|
||||
Number(row.deductPoints) > 15)
|
||||
deductPoints !== null &&
|
||||
(!Number.isInteger(Number(deductPoints)) ||
|
||||
Number(deductPoints) < 0 ||
|
||||
Number(deductPoints) > 15)
|
||||
) {
|
||||
this.$message.warning('被扣分数范围为 0-15 分');
|
||||
return false;
|
||||
@@ -570,6 +586,10 @@ export default {
|
||||
detail.attachments = this.parseAttachments(detail.attachments);
|
||||
detail.vehicleType = detail.vehicleType || '车辆';
|
||||
detail.processStatus = detail.processStatus || '未处理';
|
||||
detail.fineAmount = this.normalizeOptionalAmount(detail.fineAmount);
|
||||
detail.deductPoints = this.normalizeOptionalAmount(detail.deductPoints);
|
||||
detail.penaltyUnit = this.normalizeOptionalText(detail.penaltyUnit) || '';
|
||||
detail.processResult = this.normalizeOptionalText(detail.processResult) || '';
|
||||
this.form = detail;
|
||||
this.updateVehicleTypeDisplays(detail.vehicleType);
|
||||
this.vehicleOptions = [];
|
||||
|
||||
Reference in New Issue
Block a user