Merge remote-tracking branch 'refs/remotes/origin/master' into feature/bfi-api-rate
# Conflicts: # pnpm-lock.yaml # src/axios.js # src/views/settlement/formal-settlement.vue # src/views/vehicle/customer-archive.vue
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
type="danger"
|
||||
icon="el-icon-delete"
|
||||
plain
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
@click="handleDelete"
|
||||
>删除
|
||||
</el-button>
|
||||
@@ -43,19 +43,19 @@
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="row.status === -1 && userInfo.authority.includes('admin')"
|
||||
v-if="row.status === -1 && isAdministrator"
|
||||
@click.stop="handleEnable(row)"
|
||||
>启用
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="row.status === 1 && userInfo.authority.includes('admin')"
|
||||
v-if="row.status === 1 && isAdministrator"
|
||||
@click.stop="handleDisable(row)"
|
||||
>禁用
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
@click.stop="handleRowLog(row)"
|
||||
>日志
|
||||
</el-link>
|
||||
@@ -522,7 +522,7 @@ sortable: true, span: 24, width: 180 },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo']),
|
||||
...mapGetters(['userInfo', 'isAdministrator']),
|
||||
ids() {
|
||||
let ids = [];
|
||||
this.selectionList.forEach(ele => {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
type="danger"
|
||||
icon="el-icon-delete"
|
||||
plain
|
||||
v-if="userInfo.authority.includes('administrator')"
|
||||
v-if="isAdministrator"
|
||||
@click="handleDelete"
|
||||
>删除
|
||||
</el-button>
|
||||
@@ -54,19 +54,19 @@
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="row.status !== 1 && userInfo.authority.includes('administrator')"
|
||||
v-if="row.status !== 1 && isAdministrator"
|
||||
@click.stop="handleEnable(row)"
|
||||
>启用
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="row.status === 1 && userInfo.authority.includes('administrator')"
|
||||
v-if="row.status === 1 && isAdministrator"
|
||||
@click.stop="handleDisable(row)"
|
||||
>禁用
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="userInfo.authority.includes('administrator')"
|
||||
v-if="isAdministrator"
|
||||
@click.stop="handleRowLog(row)"
|
||||
>日志
|
||||
</el-link>
|
||||
@@ -401,7 +401,7 @@ sortable: true, span: 24, width: 170 },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo']),
|
||||
...mapGetters(['userInfo', 'isAdministrator']),
|
||||
ids() {
|
||||
let ids = [];
|
||||
this.selectionList.forEach(ele => {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<el-button
|
||||
icon="el-icon-setting"
|
||||
@click="handleRole"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
plain
|
||||
>权限设置
|
||||
</el-button>
|
||||
@@ -39,7 +39,7 @@
|
||||
<template #menu="{ row }">
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
style="border: 0; background-color: transparent !important"
|
||||
@click.stop="handleRowRole(row)"
|
||||
>权限设置
|
||||
@@ -354,7 +354,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.role_add, false),
|
||||
|
||||
@@ -150,7 +150,7 @@ import { mapGetters } from 'vuex';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import NProgress from 'nprogress';
|
||||
@@ -460,6 +460,13 @@ export default {
|
||||
showWordLimit: true,
|
||||
rules: [{ max: 200, message: '备注不能超过200字', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '更新人',
|
||||
prop: 'updateUserName',
|
||||
@@ -548,10 +555,7 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
let ids = [];
|
||||
|
||||
@@ -182,8 +182,8 @@
|
||||
<el-input
|
||||
v-model="form.regionName"
|
||||
readonly
|
||||
disabled
|
||||
placeholder="行政区划自动带出"
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
<el-input
|
||||
v-model="form.detailAddress"
|
||||
@@ -269,8 +269,8 @@ import { exportBlob } from '@/api/common';
|
||||
import { createOption } from '@/option/base/common-address';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { normalizeMapSearchResults } from '@/utils/map-search';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
import NProgress from 'nprogress';
|
||||
import 'nprogress/nprogress.css';
|
||||
@@ -324,16 +324,16 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.isAdmin) {
|
||||
this.allDept = 1;
|
||||
}
|
||||
this.initDeptTree();
|
||||
},
|
||||
methods: {
|
||||
@@ -1112,31 +1112,32 @@ export default {
|
||||
this.$message.warning('请输入地址关键词');
|
||||
return;
|
||||
}
|
||||
if (this.mapLoading) return;
|
||||
this.mapSearchResults = [];
|
||||
this.mapLoading = true;
|
||||
this.loadAmap()
|
||||
.then(() => {
|
||||
this.mapLoading = true;
|
||||
this.ensureAmapGeocoder()
|
||||
.then(() => this.runAmapGeocode('location', keyword))
|
||||
.then(result => {
|
||||
this.mapSearchResults = normalizeMapSearchResults(result, keyword);
|
||||
const point = this.resolveMapPoint(result);
|
||||
if (!point) {
|
||||
this.mapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
return;
|
||||
}
|
||||
this.pickMapPoint(point, keyword);
|
||||
})
|
||||
.catch(() => {
|
||||
this.mapStatus = '地图搜索失败';
|
||||
this.$message.error('地图搜索失败,请稍后重试');
|
||||
})
|
||||
.finally(() => {
|
||||
this.mapLoading = false;
|
||||
});
|
||||
.then(() => searchAmapPlaceText(keyword))
|
||||
.then(pois => {
|
||||
this.mapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||
const first = this.mapSearchResults[0];
|
||||
if (!first?.location) {
|
||||
this.mapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
return;
|
||||
}
|
||||
return this.$nextTick().then(() => {
|
||||
if (typeof this.amap?.resize === 'function') {
|
||||
this.amap.resize();
|
||||
}
|
||||
this.pickMapPoint(first.location, first.address || first.name || keyword);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
|
||||
this.mapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
})
|
||||
.finally(() => {
|
||||
this.mapLoading = false;
|
||||
});
|
||||
},
|
||||
selectMapSearchResult(item) {
|
||||
|
||||
@@ -238,6 +238,13 @@ export default {
|
||||
value: 'value',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
minWidth: 120,
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
},
|
||||
{
|
||||
label: '更新人',
|
||||
prop: 'updateUserName',
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getDetail, getList, remove, submit } from '@/api/base/insurance-ocr-template';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const insuranceFieldKeys = [
|
||||
'保险类型',
|
||||
@@ -203,7 +204,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
return String(this.userInfo.authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
selectionIds() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { changeStatus, getDetail, getList, remove, submit } from '@/api/base/measurement-unit';
|
||||
import { createOption } from '@/option/base/measurement-unit';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
@@ -111,7 +112,7 @@ export default {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
isAdmin() {
|
||||
return String(this.userInfo?.authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -270,7 +270,7 @@ import { mapGetters } from 'vuex';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import NProgress from 'nprogress';
|
||||
@@ -621,6 +621,13 @@ export default {
|
||||
showWordLimit: true,
|
||||
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '更新人',
|
||||
prop: 'updateUserName',
|
||||
@@ -708,10 +715,7 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
isParentRegionLocked() {
|
||||
return this.form.category === '码头';
|
||||
|
||||
@@ -150,7 +150,7 @@ import { mapGetters } from 'vuex';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import NProgress from 'nprogress';
|
||||
@@ -433,6 +433,13 @@ export default {
|
||||
showWordLimit: true,
|
||||
rules: [{ max: 200, message: '备注不能超过200字', trigger: 'blur' }],
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '更新人',
|
||||
prop: 'updateUserName',
|
||||
@@ -519,10 +526,7 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
let ids = [];
|
||||
|
||||
+54
-41
@@ -26,6 +26,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="区域层级">
|
||||
<el-select v-model="searchForm.regionLevel" clearable placeholder="请选择" style="width: 200px;">
|
||||
<el-option label="国家" :value="0" />
|
||||
<el-option label="省份" :value="1" />
|
||||
<el-option label="省市" :value="2" />
|
||||
<el-option label="区县" :value="3" />
|
||||
@@ -42,6 +43,13 @@
|
||||
</section-card>
|
||||
|
||||
<el-button-group>
|
||||
<el-button
|
||||
v-if="permission.region_add"
|
||||
type="primary"
|
||||
icon="el-icon-plus"
|
||||
@click="addCountry"
|
||||
>新增国家
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="permission.region_add"
|
||||
type="primary"
|
||||
@@ -112,14 +120,7 @@
|
||||
placeholder="请输入国家编码,如 +86"
|
||||
v-model="regionForm.code"
|
||||
/>
|
||||
<el-input
|
||||
v-else-if="isProvinceRegion"
|
||||
placeholder="请输入省级区划编号"
|
||||
v-model="regionForm.subCode"
|
||||
/>
|
||||
<el-input v-else placeholder="请输入 区划子编号" v-model="regionForm.subCode">
|
||||
<template #prepend>{{ regionForm.parentCode }}</template>
|
||||
</el-input>
|
||||
<el-input v-else placeholder="请输入区划编号" v-model="regionForm.subCode" />
|
||||
</template>
|
||||
</avue-form>
|
||||
<el-dialog title="行政区划数据导入" append-to-body v-model="excelBox" width="555px">
|
||||
@@ -253,9 +254,10 @@ export default {
|
||||
dicUrl: '/blade-system/dict/dictionary?code=region',
|
||||
dicFormatter: response => {
|
||||
const list = Array.isArray(response) ? response : response?.data;
|
||||
return (Array.isArray(list) ? list : [])
|
||||
.filter(item => Number(item.dictKey) !== 0)
|
||||
.map(item => ({ ...item, dictKey: Number(item.dictKey) }));
|
||||
return (Array.isArray(list) ? list : []).map(item => ({
|
||||
...item,
|
||||
dictKey: Number(item.dictKey),
|
||||
}));
|
||||
},
|
||||
props: {
|
||||
label: 'dictValue',
|
||||
@@ -410,9 +412,6 @@ export default {
|
||||
isCountryRegion() {
|
||||
return Number(this.regionForm.regionLevel) === 0;
|
||||
},
|
||||
isProvinceRegion() {
|
||||
return Number(this.regionForm.regionLevel) === 1;
|
||||
},
|
||||
isEditRegion() {
|
||||
return !validatenull(this.regionForm.originalCode);
|
||||
},
|
||||
@@ -476,10 +475,7 @@ export default {
|
||||
getDetail(this.treeCode).then(res => {
|
||||
this.regionForm = res.data.data;
|
||||
this.regionForm.originalCode = this.regionForm.code;
|
||||
this.regionForm.subCode =
|
||||
this.regionForm.parentCode === this.topCode
|
||||
? this.regionForm.code
|
||||
: this.regionForm.code.replace(this.regionForm.parentCode, '');
|
||||
this.regionForm.subCode = this.regionForm.code;
|
||||
this.loadParentOptions(this.regionForm.parentCode, this.regionForm.parentName);
|
||||
});
|
||||
},
|
||||
@@ -500,25 +496,31 @@ export default {
|
||||
};
|
||||
},
|
||||
addChildren() {
|
||||
if (validatenull(this.regionForm.code) || validatenull(this.regionForm.name)) {
|
||||
const current = this.regionForm || {};
|
||||
const parentCode = current.originalCode || current.code;
|
||||
if (validatenull(parentCode) || validatenull(current.name)) {
|
||||
this.$message.warning('请先选择一项区划');
|
||||
return;
|
||||
}
|
||||
if (Number(this.regionForm.regionLevel) >= 3) {
|
||||
if (Number(current.regionLevel) >= 3) {
|
||||
this.$message.warning('区县已是末级,不能新增下级');
|
||||
return;
|
||||
}
|
||||
const parentCode = this.regionForm.code;
|
||||
const parentName = this.regionForm.name;
|
||||
const parentGroupCode = this.regionForm.parentCode || this.topCode;
|
||||
const parentName = current.name;
|
||||
const parentGroupCode = current.parentCode || this.topCode;
|
||||
this.setRegionColumnDisplay('code', true);
|
||||
this.regionForm.parentCode = parentCode;
|
||||
this.regionForm.parentName = parentName;
|
||||
this.regionForm.code = '';
|
||||
this.regionForm.subCode = '';
|
||||
this.regionForm.originalCode = '';
|
||||
this.regionForm.name = '';
|
||||
this.regionForm.regionLevel = Number(this.regionForm.regionLevel) + 1;
|
||||
this.regionForm = {
|
||||
parentCode,
|
||||
parentName,
|
||||
code: '',
|
||||
subCode: '',
|
||||
originalCode: '',
|
||||
name: '',
|
||||
regionLevel: Number(current.regionLevel) + 1,
|
||||
sort: 1,
|
||||
remark: '',
|
||||
status: 1,
|
||||
};
|
||||
this.loadParentOptionsByGroup(parentGroupCode, parentCode, parentName);
|
||||
},
|
||||
setRegionColumnDisplay(prop, display) {
|
||||
@@ -536,8 +538,7 @@ export default {
|
||||
if (this.isCountryRegion) {
|
||||
return;
|
||||
}
|
||||
const parentCode = this.isProvinceRegion ? '' : this.regionForm.parentCode || '';
|
||||
this.regionForm.code = parentCode + (this.regionForm.subCode || '');
|
||||
this.regionForm.code = this.regionForm.subCode || '';
|
||||
},
|
||||
getRootParentOptions() {
|
||||
return [
|
||||
@@ -586,16 +587,28 @@ export default {
|
||||
this.parentOptions = this.ensureSelectedParent(options, selectedCode, selectedName);
|
||||
});
|
||||
},
|
||||
handleSubmit(form, done) {
|
||||
if (Number(form.regionLevel) === 0) {
|
||||
form.parentCode = this.topCode;
|
||||
form.ancestors = this.topCode;
|
||||
form.subCode = form.code;
|
||||
} else {
|
||||
const parentCode = Number(form.regionLevel) === 1 ? '' : form.parentCode;
|
||||
form.code = parentCode + form.subCode;
|
||||
buildRegionPayload(form) {
|
||||
const source = validatenull(this.regionForm) ? form || {} : this.regionForm;
|
||||
const regionLevel = Number(source.regionLevel);
|
||||
const isNew = validatenull(source.originalCode);
|
||||
const payload = {
|
||||
code: regionLevel === 0 ? source.code : source.subCode,
|
||||
parentCode: regionLevel === 0 ? this.topCode : source.parentCode,
|
||||
name: source.name,
|
||||
regionLevel,
|
||||
sort: source.sort,
|
||||
remark: source.remark,
|
||||
status: source.status == null || source.status === '' ? 1 : source.status,
|
||||
};
|
||||
if (!isNew) {
|
||||
payload.originalCode = source.originalCode;
|
||||
payload.dataSource = source.dataSource;
|
||||
}
|
||||
submit(form).then(
|
||||
return payload;
|
||||
},
|
||||
handleSubmit(form, done) {
|
||||
const payload = this.buildRegionPayload(form);
|
||||
submit(payload).then(
|
||||
() => {
|
||||
this.$message({
|
||||
type: 'success',
|
||||
|
||||
@@ -204,7 +204,7 @@ import { exportBlob } from '@/api/common';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { config, excelOption, formatCargoValue, option } from '@/option/business/common-cargo';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -250,16 +250,14 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.applyDefaultDeptSearch();
|
||||
this.initDeptTree();
|
||||
this.loadPriceUnitOptions();
|
||||
},
|
||||
@@ -273,6 +271,26 @@ export default {
|
||||
if (Array.isArray(data)) return data;
|
||||
return (data && data.records) || [];
|
||||
},
|
||||
defaultDeptId() {
|
||||
// 用户可能绑定多个组织(逗号分隔),搜索下拉是单选,只取当前会话第一个组织 ID。
|
||||
const raw = (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
|
||||
return String(raw)
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean)[0] || '';
|
||||
},
|
||||
applyDefaultDeptSearch() {
|
||||
const deptId = this.defaultDeptId();
|
||||
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
|
||||
if (!deptId || !deptColumn) return;
|
||||
deptColumn.searchValue = deptId;
|
||||
this.query = { ...this.query, deptId };
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.crud?.searchForm) {
|
||||
this.$refs.crud.searchForm.searchDeptId = deptId;
|
||||
}
|
||||
});
|
||||
},
|
||||
initDeptTree() {
|
||||
getDeptTree(this.userInfo.tenantId).then(res => {
|
||||
this.deptOptions = this.flattenDept(res.data.data || []);
|
||||
@@ -280,6 +298,18 @@ export default {
|
||||
if (deptColumn) {
|
||||
deptColumn.dicData = this.deptOptions;
|
||||
}
|
||||
// dicData 异步到位后再回填,避免下拉选项为空时清空已选组织,导致请求丢失 deptId。
|
||||
const deptId = String(this.query.deptId || this.defaultDeptId() || '');
|
||||
if (!deptId) return;
|
||||
if (deptColumn) {
|
||||
deptColumn.searchValue = deptId;
|
||||
}
|
||||
this.query = { ...this.query, deptId };
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.crud?.searchForm) {
|
||||
this.$refs.crud.searchForm.searchDeptId = deptId;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
loadPriceUnitOptions() {
|
||||
@@ -307,9 +337,13 @@ export default {
|
||||
flattenDept(tree, level = 0) {
|
||||
const result = [];
|
||||
tree.forEach(item => {
|
||||
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
|
||||
const id =
|
||||
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
|
||||
if (!id) return;
|
||||
result.push({
|
||||
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
|
||||
value: item.id,
|
||||
value: id,
|
||||
});
|
||||
if (item.children && item.children.length) {
|
||||
result.push(...this.flattenDept(item.children, level + 1));
|
||||
@@ -317,6 +351,18 @@ export default {
|
||||
});
|
||||
return result;
|
||||
},
|
||||
normalizeListQuery(params = {}) {
|
||||
const query = { ...params, ...this.query };
|
||||
const rawDeptId = query.deptId ?? query.searchDeptId;
|
||||
delete query.searchDeptId;
|
||||
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
|
||||
query.deptId = String(rawDeptId).split(',')[0].trim();
|
||||
} else {
|
||||
delete query.deptId;
|
||||
}
|
||||
query.allDept = this.isAdmin ? 1 : 0;
|
||||
return query;
|
||||
},
|
||||
normalizeCargoTypeOptions(list = []) {
|
||||
return list
|
||||
.map(item => ({
|
||||
@@ -485,7 +531,7 @@ export default {
|
||||
return false;
|
||||
}
|
||||
if (row.cargoValue && !/^\d+(\.\d{1,2})?$/.test(String(row.cargoValue))) {
|
||||
this.$message.warning('货值只能输入数字,最多保留2位小数');
|
||||
this.$message.warning('单价只能输入数字,最多保留2位小数');
|
||||
return false;
|
||||
}
|
||||
if (!row.model) {
|
||||
@@ -619,13 +665,14 @@ export default {
|
||||
},
|
||||
searchReset() {
|
||||
this.query = {};
|
||||
this.applyDefaultDeptSearch();
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page);
|
||||
this.onLoad(this.page, this.query);
|
||||
},
|
||||
searchChange(params, done) {
|
||||
this.query = params;
|
||||
this.query = this.normalizeListQuery(params);
|
||||
this.page.currentPage = 1;
|
||||
this.onLoad(this.page, params);
|
||||
this.onLoad(this.page, this.query);
|
||||
done();
|
||||
},
|
||||
selectionChange(list) {
|
||||
@@ -648,8 +695,10 @@ export default {
|
||||
},
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
const query = this.normalizeListQuery(params);
|
||||
this.query = query;
|
||||
api
|
||||
.getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: 0 })
|
||||
.getList(page.currentPage, page.pageSize, query)
|
||||
.then(res => {
|
||||
const result = res.data.data;
|
||||
this.page.total = result.total;
|
||||
@@ -662,8 +711,7 @@ export default {
|
||||
},
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
allDept: 0,
|
||||
...this.normalizeListQuery(this.query),
|
||||
ids: this.ids,
|
||||
[this.website.tokenHeader]: getToken(),
|
||||
};
|
||||
|
||||
@@ -321,9 +321,9 @@ import { exportBlob } from '@/api/common';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { addressTypeOptions } from '@/option/base/common-address';
|
||||
import { config, excelOption, option } from '@/option/business/common-route';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { normalizeMapSearchResults } from '@/utils/map-search';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
import { List, Search } from '@element-plus/icons-vue';
|
||||
@@ -390,10 +390,7 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
@@ -490,7 +487,7 @@ export default {
|
||||
this.addressLoading = true;
|
||||
getCommonAddressList(this.addressPage.currentPage, this.addressPage.pageSize, {
|
||||
...this.addressQuery,
|
||||
allDept: 0,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
})
|
||||
.then(res => {
|
||||
const data = res.data.data || {};
|
||||
@@ -610,25 +607,29 @@ export default {
|
||||
this.$message.warning('请输入地址关键词');
|
||||
return;
|
||||
}
|
||||
if (this.routeMapLoading) return;
|
||||
this.routeMapSearchResults = [];
|
||||
this.routeMapLoading = true;
|
||||
this.loadAmap()
|
||||
.then(() => {
|
||||
this.routeMapLoading = true;
|
||||
return this.ensureRouteAmapGeocoder();
|
||||
})
|
||||
.then(() => this.runAmapGeocode('location', keyword))
|
||||
.then(result => {
|
||||
this.routeMapSearchResults = normalizeMapSearchResults(result, keyword);
|
||||
const point = this.resolveMapPoint(result);
|
||||
if (!point) {
|
||||
.then(() => searchAmapPlaceText(keyword))
|
||||
.then(pois => {
|
||||
this.routeMapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||
const first = this.routeMapSearchResults[0];
|
||||
if (!first?.location) {
|
||||
this.routeMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
return;
|
||||
}
|
||||
this.pickRouteMapPoint(point, keyword);
|
||||
return this.$nextTick().then(() => {
|
||||
if (typeof this.routeAmap?.resize === 'function') {
|
||||
this.routeAmap.resize();
|
||||
}
|
||||
this.pickRouteMapPoint(first.location, first.address || first.name || keyword);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.routeMapStatus = '地图搜索失败';
|
||||
this.$message.error('地图搜索失败,请稍后重试');
|
||||
this.routeMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
})
|
||||
.finally(() => {
|
||||
this.routeMapLoading = false;
|
||||
@@ -1024,7 +1025,11 @@ export default {
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
api
|
||||
.getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: 0 })
|
||||
.getList(page.currentPage, page.pageSize, {
|
||||
...params,
|
||||
...this.query,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
})
|
||||
.then(res => {
|
||||
const result = res.data.data;
|
||||
this.page.total = result.total;
|
||||
@@ -1038,7 +1043,7 @@ export default {
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
allDept: 0,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
ids: this.ids,
|
||||
[this.website.tokenHeader]: getToken(),
|
||||
};
|
||||
|
||||
@@ -2423,7 +2423,7 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" width="200" />
|
||||
<el-table-column prop="vehicleNo" label="车牌号/船班号/船次号/班次号" width="200" />
|
||||
<el-table-column prop="driverName" label="司机" width="128" />
|
||||
<el-table-column prop="carrierName" label="承运商" width="192" />
|
||||
<el-table-column label="运输方式" width="192">
|
||||
@@ -2909,14 +2909,15 @@
|
||||
原:{{ row.originalDepartureAddress || '-' }}
|
||||
</div>
|
||||
<el-input
|
||||
v-model="row.departureAddress"
|
||||
placeholder="请选择或输入地址"
|
||||
@input="updateWaybillRouteChangeNodes"
|
||||
:model-value="row.departureAddress"
|
||||
readonly
|
||||
placeholder="请点击地图选点"
|
||||
@click="openWaybillRouteChangeMapDialog(row, 'departure')"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon
|
||||
class="business-crud-page__route-change-location"
|
||||
@click="openWaybillCommonAddressDialog(row, 'departure')"
|
||||
@click.stop="openWaybillRouteChangeMapDialog(row, 'departure')"
|
||||
>
|
||||
<Location />
|
||||
</el-icon>
|
||||
@@ -2930,14 +2931,15 @@
|
||||
原:{{ row.originalArrivalAddress || '-' }}
|
||||
</div>
|
||||
<el-input
|
||||
v-model="row.arrivalAddress"
|
||||
placeholder="请选择或输入地址"
|
||||
@input="updateWaybillRouteChangeNodes"
|
||||
:model-value="row.arrivalAddress"
|
||||
readonly
|
||||
placeholder="请点击地图选点"
|
||||
@click="openWaybillRouteChangeMapDialog(row, 'arrival')"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon
|
||||
class="business-crud-page__route-change-location"
|
||||
@click="openWaybillCommonAddressDialog(row, 'arrival')"
|
||||
@click.stop="openWaybillRouteChangeMapDialog(row, 'arrival')"
|
||||
>
|
||||
<Location />
|
||||
</el-icon>
|
||||
@@ -2993,49 +2995,6 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="waybillCommonAddressBox"
|
||||
title="选择常用地址"
|
||||
append-to-body
|
||||
width="900px"
|
||||
class="business-crud-page__common-address-dialog"
|
||||
>
|
||||
<div class="business-crud-page__common-address-toolbar">
|
||||
<el-input
|
||||
v-model="waybillCommonAddressQuery.addressName"
|
||||
clearable
|
||||
placeholder="请输入地址名称"
|
||||
@keyup.enter="loadWaybillCommonAddresses"
|
||||
/>
|
||||
<el-button type="primary" @click="loadWaybillCommonAddresses">查询</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="waybillCommonAddressLoading"
|
||||
:data="waybillCommonAddressRows"
|
||||
border
|
||||
@row-dblclick="selectWaybillCommonAddress"
|
||||
>
|
||||
<el-table-column prop="addressName" label="地址名称" min-width="180" />
|
||||
<el-table-column prop="regionName" label="区域" min-width="180" />
|
||||
<el-table-column prop="detailAddress" label="详细地址" min-width="260" />
|
||||
<el-table-column label="操作" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="selectWaybillCommonAddress(row)">选择</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-pagination
|
||||
v-model:current-page="waybillCommonAddressPage.currentPage"
|
||||
v-model:page-size="waybillCommonAddressPage.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="waybillCommonAddressPage.total"
|
||||
@current-change="loadWaybillCommonAddresses"
|
||||
/>
|
||||
<el-button @click="waybillCommonAddressBox = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-if="config.enableTransportPlanImport"
|
||||
v-model="transportPlanImportBox"
|
||||
@@ -3514,7 +3473,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="vehicleNo"
|
||||
label="车牌号/航班号/船号/班列号"
|
||||
label="车牌号/船班号/船次号/班次号"
|
||||
min-width="200"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
@@ -4169,7 +4128,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="dispatchItemForm.carrierType"
|
||||
:label="dispatchIsNonRoadTransport ? '船/航/班列号' : '车牌号'"
|
||||
:label="dispatchIsNonRoadTransport ? '船班号/船次号/班次号' : '车牌号'"
|
||||
required
|
||||
>
|
||||
<el-input v-model="dispatchItemForm.vehicleNo" placeholder="请输入" />
|
||||
@@ -4982,6 +4941,7 @@
|
||||
v-model="transportMapBox"
|
||||
width="920px"
|
||||
@opened="initTransportAmap"
|
||||
@closed="waybillRouteChangeMapTarget = null"
|
||||
>
|
||||
<div class="business-crud-page__map-toolbar">
|
||||
<el-input
|
||||
@@ -5010,7 +4970,7 @@
|
||||
>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="transportMapBox = false">取消</el-button>
|
||||
<el-button @click="closeTransportMapDialog">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!transportMapSelected.longitude"
|
||||
@@ -5281,9 +5241,9 @@ import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { addressTypeOptions } from '@/option/base/common-address';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { normalizeMapSearchResults } from '@/utils/map-search';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
@@ -5633,18 +5593,7 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
waybillCommonAddressBox: false,
|
||||
waybillCommonAddressLoading: false,
|
||||
waybillCommonAddressQuery: {
|
||||
addressName: '',
|
||||
},
|
||||
waybillCommonAddressRows: [],
|
||||
waybillCommonAddressTarget: null,
|
||||
waybillCommonAddressPage: {
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
},
|
||||
waybillRouteChangeMapTarget: null,
|
||||
transportPlanImportBox: false,
|
||||
transportPlanImportLoading: false,
|
||||
transportPlanImportFiles: [],
|
||||
@@ -6075,7 +6024,7 @@ export default {
|
||||
return this.config.permission === 'waybill_manage' && !this.isRoadTransport;
|
||||
},
|
||||
transportVehicleNoLabel() {
|
||||
return this.isRoadTransport ? '车牌号' : '船/航/班列号';
|
||||
return this.isRoadTransport ? '车牌号' : '船班号/船次号/班次号';
|
||||
},
|
||||
transportPlanImportDuplicateCount() {
|
||||
return this.transportPlanImportRows.filter(row => row._duplicate).length;
|
||||
@@ -6399,10 +6348,7 @@ export default {
|
||||
return this.currencyRemark(currency);
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
canViewAllDept() {
|
||||
return this.isAdmin;
|
||||
@@ -7121,7 +7067,7 @@ export default {
|
||||
return String(row.carrierTypeName || row.carrierType || '').trim() === '承运商';
|
||||
},
|
||||
waybillVehicleNoLabel(row = {}) {
|
||||
return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号';
|
||||
return this.waybillIsRoadTransport(row) ? '车牌号' : '船班号/船次号/班次号';
|
||||
},
|
||||
waybillUnitPrice(row = {}) {
|
||||
const price = row.unitPrice || row.price || '';
|
||||
@@ -7440,43 +7386,22 @@ export default {
|
||||
updateWaybillRouteChangeNodes() {
|
||||
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
|
||||
},
|
||||
openWaybillCommonAddressDialog(row, field) {
|
||||
this.waybillCommonAddressTarget = { row, field };
|
||||
this.waybillCommonAddressQuery = { addressName: '' };
|
||||
this.waybillCommonAddressPage.currentPage = 1;
|
||||
this.waybillCommonAddressBox = true;
|
||||
this.loadWaybillCommonAddresses();
|
||||
},
|
||||
loadWaybillCommonAddresses() {
|
||||
this.waybillCommonAddressLoading = true;
|
||||
getCommonAddressList(
|
||||
this.waybillCommonAddressPage.currentPage,
|
||||
this.waybillCommonAddressPage.pageSize,
|
||||
{
|
||||
...this.waybillCommonAddressQuery,
|
||||
allDept: 0,
|
||||
}
|
||||
)
|
||||
.then(res => {
|
||||
const data = res.data?.data || {};
|
||||
this.waybillCommonAddressRows = data.records || [];
|
||||
this.waybillCommonAddressPage.total = data.total || 0;
|
||||
})
|
||||
.finally(() => {
|
||||
this.waybillCommonAddressLoading = false;
|
||||
});
|
||||
},
|
||||
selectWaybillCommonAddress(address) {
|
||||
const target = this.waybillCommonAddressTarget;
|
||||
if (!target) return;
|
||||
target.row[`${target.field}Address`] =
|
||||
[address.regionName, address.detailAddress || address.addressName]
|
||||
.filter(Boolean)
|
||||
.join('') ||
|
||||
address.addressName ||
|
||||
'';
|
||||
this.updateWaybillRouteChangeNodes();
|
||||
this.waybillCommonAddressBox = false;
|
||||
openWaybillRouteChangeMapDialog(row, field) {
|
||||
if (!row || !field) return;
|
||||
this.waybillRouteChangeMapTarget = { row, field };
|
||||
this.transportMapTarget = field;
|
||||
this.transportMapKeyword = row[`${field}Address`] || row[`${field}Name`] || '';
|
||||
this.transportMapSelected = this.buildTransportMapSelection({
|
||||
lng: row[`${field}Longitude`],
|
||||
lat: row[`${field}Latitude`],
|
||||
address: row[`${field}Address`],
|
||||
regionName: row[`${field}Name`],
|
||||
regionCode: row[`${field}RegionCode`],
|
||||
});
|
||||
this.transportMapStatus = this.transportMapSelected.longitude
|
||||
? '已加载当前选点,可重新选点'
|
||||
: '可搜索地址或点击地图选点';
|
||||
this.transportMapBox = true;
|
||||
},
|
||||
async confirmWaybillRouteChange() {
|
||||
const row = this.waybillRouteChangeRows[0] || {};
|
||||
@@ -9491,7 +9416,7 @@ export default {
|
||||
}
|
||||
} else {
|
||||
const invalidLengthField = [
|
||||
['vehicleNo', '船/航/班列号', 30],
|
||||
['vehicleNo', '船班号/船次号/班次号', 30],
|
||||
['captainName', '船长', 20],
|
||||
['cabinNo', '舱位', 30],
|
||||
['containerNo', '箱号', 30],
|
||||
@@ -10201,10 +10126,15 @@ export default {
|
||||
this.$refs.crud?.clearValidate?.([`${prefix}Address`]);
|
||||
});
|
||||
},
|
||||
closeTransportMapDialog() {
|
||||
this.transportMapBox = false;
|
||||
this.waybillRouteChangeMapTarget = null;
|
||||
},
|
||||
openTransportMapDialog(target) {
|
||||
if (this.dialogReadonly) return;
|
||||
if (!this.ensureTransportTypeBeforeAddress()) return;
|
||||
const prefix = target === 'departure' ? 'departure' : 'arrival';
|
||||
this.waybillRouteChangeMapTarget = null;
|
||||
this.transportMapTarget = prefix;
|
||||
this.transportMapKeyword = this.form[`${prefix}Address`] || this.form[`${prefix}Name`] || '';
|
||||
this.transportMapSelected = this.buildTransportMapSelection({
|
||||
@@ -10281,31 +10211,32 @@ export default {
|
||||
this.$message.warning('请输入地址关键词');
|
||||
return;
|
||||
}
|
||||
if (this.transportMapLoading) return;
|
||||
this.transportMapSearchResults = [];
|
||||
this.transportMapLoading = true;
|
||||
this.loadAmap()
|
||||
.then(() => {
|
||||
this.transportMapLoading = true;
|
||||
this.ensureTransportAmapGeocoder()
|
||||
.then(() => this.runAmapGeocode('location', keyword))
|
||||
.then(result => {
|
||||
this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
|
||||
const point = this.resolveMapPoint(result);
|
||||
if (!point) {
|
||||
this.transportMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
return;
|
||||
}
|
||||
this.pickTransportMapPoint(point, keyword);
|
||||
})
|
||||
.catch(() => {
|
||||
this.transportMapStatus = '地图搜索失败';
|
||||
this.$message.error('地图搜索失败,请稍后重试');
|
||||
})
|
||||
.finally(() => {
|
||||
this.transportMapLoading = false;
|
||||
});
|
||||
.then(() => searchAmapPlaceText(keyword))
|
||||
.then(pois => {
|
||||
this.transportMapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||
const first = this.transportMapSearchResults[0];
|
||||
if (!first?.location) {
|
||||
this.transportMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
return;
|
||||
}
|
||||
return this.$nextTick().then(() => {
|
||||
if (typeof this.transportAmap?.resize === 'function') {
|
||||
this.transportAmap.resize();
|
||||
}
|
||||
this.pickTransportMapPoint(first.location, first.address || first.name || keyword);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
|
||||
this.transportMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
})
|
||||
.finally(() => {
|
||||
this.transportMapLoading = false;
|
||||
});
|
||||
},
|
||||
selectTransportMapSearchResult(item) {
|
||||
@@ -10363,6 +10294,23 @@ export default {
|
||||
this.$message.warning('请先搜索或点击地图完成选点');
|
||||
return;
|
||||
}
|
||||
if (this.waybillRouteChangeMapTarget) {
|
||||
const { row, field } = this.waybillRouteChangeMapTarget;
|
||||
row[`${field}Address`] =
|
||||
this.transportMapSelected.detailAddress || row[`${field}Address`] || '';
|
||||
if (this.transportMapSelected.regionName) {
|
||||
row[`${field}Name`] = this.transportMapSelected.regionName;
|
||||
}
|
||||
row[`${field}Longitude`] = this.transportMapSelected.longitude;
|
||||
row[`${field}Latitude`] = this.transportMapSelected.latitude;
|
||||
if (this.transportMapSelected.regionCode) {
|
||||
row[`${field}RegionCode`] = this.transportMapSelected.regionCode;
|
||||
}
|
||||
this.updateWaybillRouteChangeNodes();
|
||||
this.waybillRouteChangeMapTarget = null;
|
||||
this.transportMapBox = false;
|
||||
return;
|
||||
}
|
||||
const prefix = this.transportMapTarget;
|
||||
this.form[`${prefix}Address`] =
|
||||
this.transportMapSelected.detailAddress || this.form[`${prefix}Address`];
|
||||
@@ -13278,7 +13226,7 @@ export default {
|
||||
[
|
||||
'vehicleNo',
|
||||
['water', 'rail', 'air'].includes(this.resolveTransportMode(row.transportType))
|
||||
? '船/航/班列号'
|
||||
? '船班号/船次号/班次号'
|
||||
: '车牌号',
|
||||
],
|
||||
];
|
||||
@@ -13358,7 +13306,7 @@ export default {
|
||||
['escortPhone', '押运人手机号', 20],
|
||||
]
|
||||
: [
|
||||
['vehicleNo', '船/航/班列号', 30],
|
||||
['vehicleNo', '船班号/船次号/班次号', 30],
|
||||
['captainName', '船长', 20],
|
||||
['cabinNo', '舱位', 30],
|
||||
['containerNo', '箱号', 30],
|
||||
@@ -15914,16 +15862,6 @@ export default {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
&__common-address-toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.el-input {
|
||||
width: 260px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
&__waybill-summary-grid {
|
||||
grid-template-columns: repeat(4, minmax(130px, 1fr));
|
||||
|
||||
@@ -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,16 +196,59 @@
|
||||
<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>
|
||||
<el-col :span="6"><el-form-item label="船/航/班列号" required><el-input v-model="route.vehicleNo" 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 :span="6"><el-form-item label="船长"><el-input v-model="route.captainName" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="联系电话"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="箱号"><el-input v-model="route.containerNo" placeholder="请输入" /></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>
|
||||
@@ -601,7 +612,7 @@ let amapLoader;
|
||||
import { getList as getCommonRouteList } from '@/api/business/common-route';
|
||||
import * as api from '@/api/business/master-order';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { normalizeMapSearchResults } from '@/utils/map-search';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
export default {
|
||||
components: {
|
||||
@@ -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;
|
||||
@@ -1274,24 +1313,31 @@ export default {
|
||||
.catch(() => this.$message.error('高德地图组件加载失败'));
|
||||
},
|
||||
searchAddressMap() {
|
||||
if (!this.mapKeyword.trim()) return this.$message.warning('请输入地址关键词');
|
||||
const keyword = String(this.mapKeyword || '').trim();
|
||||
if (!keyword) return this.$message.warning('请输入地址关键词');
|
||||
if (this.mapLoading) return;
|
||||
this.mapSearchResults = [];
|
||||
this.mapLoading = true;
|
||||
this.loadAmap()
|
||||
.then(() => this.ensureAddressGeocoder())
|
||||
.then(
|
||||
() =>
|
||||
new Promise((resolve, reject) =>
|
||||
this.addressMapGeocoder.getLocation(this.mapKeyword, (status, result) =>
|
||||
status === 'complete' ? resolve(result) : reject(result)
|
||||
)
|
||||
)
|
||||
)
|
||||
.then(result => {
|
||||
this.mapSearchResults = normalizeMapSearchResults(result, this.mapKeyword);
|
||||
const point = this.mapSearchResults[0]?.location;
|
||||
if (!point) return this.$message.warning('地图搜索无匹配地址');
|
||||
this.pickAddressMap(point, this.mapKeyword);
|
||||
.then(() => searchAmapPlaceText(keyword))
|
||||
.then(pois => {
|
||||
this.mapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||
const first = this.mapSearchResults[0];
|
||||
if (!first?.location) {
|
||||
this.mapStatus = '未找到匹配地址';
|
||||
return this.$message.warning('地图搜索无匹配地址');
|
||||
}
|
||||
return this.$nextTick().then(() => {
|
||||
this.pickAddressMap(first.location, first.address || first.name || keyword);
|
||||
});
|
||||
})
|
||||
.catch(() => this.$message.error('地图搜索失败'));
|
||||
.catch(() => {
|
||||
this.mapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
})
|
||||
.finally(() => {
|
||||
this.mapLoading = false;
|
||||
});
|
||||
},
|
||||
selectAddressMapSearchResult(item) {
|
||||
if (item && item.location) {
|
||||
@@ -1657,6 +1703,7 @@ export default {
|
||||
});
|
||||
},
|
||||
openRouteDialog() {
|
||||
if (this.isBasicRouteReadonly) return;
|
||||
this.routeQuery = {};
|
||||
this.routePage.current = 1;
|
||||
this.routeDialogVisible = true;
|
||||
@@ -1714,12 +1761,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 +1792,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 +1806,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 +1964,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 +2050,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;
|
||||
|
||||
@@ -1098,7 +1098,7 @@ import {
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { normalizeMapSearchResults } from '@/utils/map-search';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
import { Location } from '@element-plus/icons-vue';
|
||||
@@ -1115,6 +1115,7 @@ import '@open-file-viewer/core/style.css';
|
||||
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||
import { h, resolveComponent } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||
@@ -1361,7 +1362,7 @@ export default {
|
||||
},
|
||||
isAdmin() {
|
||||
return (
|
||||
String(this.userInfo?.authority || '').includes('admin') ||
|
||||
isAdministrator(this.userInfo) ||
|
||||
this.userInfo?.authority?.includes?.('admin')
|
||||
);
|
||||
},
|
||||
@@ -2611,21 +2612,22 @@ export default {
|
||||
searchTransportMapKeyword() {
|
||||
const keyword = String(this.transportMapKeyword || '').trim();
|
||||
if (!keyword) return this.$message.warning('请输入地址关键词');
|
||||
if (this.transportMapLoading) return;
|
||||
this.transportMapSearchResults = [];
|
||||
this.transportMapLoading = true;
|
||||
this.loadAmap()
|
||||
.then(() => this.ensureTransportMapGeocoder())
|
||||
.then(() => this.runTransportMapGeocode('location', keyword))
|
||||
.then(result => {
|
||||
this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
|
||||
const point = this.resolveTransportMapPoint(result);
|
||||
if (!point) throw new Error('未找到匹配地址');
|
||||
.then(() => searchAmapPlaceText(keyword))
|
||||
.then(pois => {
|
||||
this.transportMapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||
const first = this.transportMapSearchResults[0];
|
||||
if (!first?.location) throw new Error('未找到匹配地址');
|
||||
return this.$nextTick().then(() => {
|
||||
this.transportMapInstance?.resize();
|
||||
return this.pickMapPoint(point, keyword);
|
||||
return this.pickMapPoint(first.location, first.address || first.name || keyword);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.transportMapStatus = '地图搜索失败';
|
||||
this.transportMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -756,7 +756,7 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" width="200" />
|
||||
<el-table-column prop="vehicleNo" label="车牌号/船班号/船次号/班次号" width="200" />
|
||||
<el-table-column prop="driverName" label="司机" width="128" />
|
||||
<el-table-column prop="carrierName" label="承运商" width="192" />
|
||||
<el-table-column label="运输方式" width="192">
|
||||
@@ -1401,7 +1401,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="vehicleNo"
|
||||
label="车牌号/航班号/船号/班列号"
|
||||
label="车牌号/船班号/船次号/班次号"
|
||||
min-width="200"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
@@ -2097,7 +2097,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="dispatchItemForm.carrierType"
|
||||
:label="dispatchIsNonRoadTransport ? '船/航/班列号' : '车牌号'"
|
||||
:label="dispatchIsNonRoadTransport ? '船班号/船次号/班次号' : '车牌号'"
|
||||
required
|
||||
>
|
||||
<el-input v-model="dispatchItemForm.vehicleNo" placeholder="请输入" />
|
||||
@@ -2740,8 +2740,8 @@ import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { addressTypeOptions } from '@/option/base/common-address';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { normalizeMapSearchResults } from '@/utils/map-search';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
@@ -3610,10 +3610,7 @@ export default {
|
||||
return this.currencyRemark(value);
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
canCreate() {
|
||||
return this.hasPermission(`${this.config.permission}_add`);
|
||||
@@ -5771,31 +5768,32 @@ export default {
|
||||
this.$message.warning('请输入地址关键词');
|
||||
return;
|
||||
}
|
||||
if (this.transportMapLoading) return;
|
||||
this.transportMapSearchResults = [];
|
||||
this.transportMapLoading = true;
|
||||
this.loadAmap()
|
||||
.then(() => {
|
||||
this.transportMapLoading = true;
|
||||
this.ensureTransportAmapGeocoder()
|
||||
.then(() => this.runAmapGeocode('location', keyword))
|
||||
.then(result => {
|
||||
this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
|
||||
const point = this.resolveMapPoint(result);
|
||||
if (!point) {
|
||||
this.transportMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
return;
|
||||
}
|
||||
this.pickTransportMapPoint(point, keyword);
|
||||
})
|
||||
.catch(() => {
|
||||
this.transportMapStatus = '地图搜索失败';
|
||||
this.$message.error('地图搜索失败,请稍后重试');
|
||||
})
|
||||
.finally(() => {
|
||||
this.transportMapLoading = false;
|
||||
});
|
||||
.then(() => searchAmapPlaceText(keyword))
|
||||
.then(pois => {
|
||||
this.transportMapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||
const first = this.transportMapSearchResults[0];
|
||||
if (!first?.location) {
|
||||
this.transportMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
return;
|
||||
}
|
||||
return this.$nextTick().then(() => {
|
||||
if (typeof this.transportAmap?.resize === 'function') {
|
||||
this.transportAmap.resize();
|
||||
}
|
||||
this.pickTransportMapPoint(first.location, first.address || first.name || keyword);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
|
||||
this.transportMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
})
|
||||
.finally(() => {
|
||||
this.transportMapLoading = false;
|
||||
});
|
||||
},
|
||||
selectTransportMapSearchResult(item) {
|
||||
@@ -8123,7 +8121,7 @@ export default {
|
||||
[
|
||||
'vehicleNo',
|
||||
['water', 'rail', 'air'].includes(this.resolveTransportMode(row.transportType))
|
||||
? '船/航/班列号'
|
||||
? '船班号/船次号/班次号'
|
||||
: '车牌号',
|
||||
],
|
||||
];
|
||||
@@ -8204,7 +8202,7 @@ export default {
|
||||
['escortPhone', '押运人手机号', 20],
|
||||
]
|
||||
: [
|
||||
['vehicleNo', '船/航/班列号', 30],
|
||||
['vehicleNo', '船班号/船次号/班次号', 30],
|
||||
['captainName', '船长', 20],
|
||||
['cabinNo', '舱位', 30],
|
||||
['containerNo', '箱号', 30],
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="voucher-folder-page voucher-folder-page--embedded">
|
||||
<section class="voucher-folder-page__search">
|
||||
<el-form :model="query" label-position="right" label-width="160px">
|
||||
<div class="voucher-folder-page__search-grid">
|
||||
<el-form-item label="车牌号">
|
||||
<el-input v-model="query.plateNo" clearable placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否关联运单">
|
||||
<el-select v-model="query.matched" clearable placeholder="全部">
|
||||
<el-option label="是" :value="1" />
|
||||
<el-option label="否" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="voucher-folder-page__search-actions">
|
||||
<el-button type="primary" @click="search">查询</el-button>
|
||||
<el-button @click="reset">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
<section class="voucher-folder-page__table-wrap">
|
||||
<div class="voucher-folder-page__title">{{ batchTitle }}</div>
|
||||
<el-table v-loading="loading" :data="pageRows" border class="voucher-folder-page__table">
|
||||
<el-table-column type="index" label="序号" width="70" :index="rowIndex" />
|
||||
<el-table-column prop="voucherNo" label="执行凭证号" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openViewer(row)">{{ row.voucherNo }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="plateNo" label="车牌号" min-width="150" />
|
||||
<el-table-column prop="folderName" label="文件夹名称" min-width="160" />
|
||||
<el-table-column prop="matched" label="是否关联运单" width="130">
|
||||
<template #default="{ row }">{{ row.matched === 1 ? '是' : '否' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="processStatus" label="处理状态" width="130" />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="180" />
|
||||
<el-table-column prop="updateTime" label="更新时间" min-width="180" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="voucher-folder-page__actions">
|
||||
<el-link type="primary" @click="openViewer(row)">查看</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="voucher-folder-page__pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page.current"
|
||||
v-model:page-size="page.size"
|
||||
:total="filteredRows.length"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, prev, pager, next, sizes"
|
||||
@size-change="page.current = 1"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="viewerVisible" title="执行凭证详情" width="960px" destroy-on-close>
|
||||
<div v-loading="viewerLoading" class="voucher-folder-page__viewer">
|
||||
<div class="voucher-folder-page__viewer-info">
|
||||
<span>执行凭证号:{{ viewer.voucherNo || '-' }}</span>
|
||||
<span>文件夹名称:{{ viewer.folderName || '-' }}</span>
|
||||
<span>是否关联运单:{{ viewer.matched === 1 ? '是' : '否' }}</span>
|
||||
<span>
|
||||
配载单号:
|
||||
<el-link v-if="viewer.loadingNo" type="primary" @click="goToLoading(viewer.loadingNo)">
|
||||
{{ viewer.loadingNo }}
|
||||
</el-link>
|
||||
<template v-else>-</template>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="viewerFiles.length" class="voucher-folder-page__images">
|
||||
<el-image
|
||||
v-for="(file, index) in viewerFiles"
|
||||
:key="file.id || file.url || index"
|
||||
:src="file.url"
|
||||
:preview-src-list="viewerFiles.map(item => item.url).filter(Boolean)"
|
||||
:initial-index="index"
|
||||
fit="cover"
|
||||
/>
|
||||
</div>
|
||||
<el-empty v-else description="暂无凭证文件" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { getVoucherFolderDetail, getVoucherFolders } from '@/api/business/process-config';
|
||||
|
||||
const props = defineProps({
|
||||
waybillId: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const rows = ref([]);
|
||||
const page = reactive({ current: 1, size: 10 });
|
||||
const query = reactive({ plateNo: '', matched: undefined });
|
||||
const appliedQuery = reactive({ plateNo: '', matched: undefined });
|
||||
const viewerVisible = ref(false);
|
||||
const viewerLoading = ref(false);
|
||||
const viewer = ref({});
|
||||
const viewerFiles = ref([]);
|
||||
|
||||
const batchTitle = computed(() => {
|
||||
const batchNos = [...new Set(rows.value.map(item => item.voucherBatchNo).filter(Boolean))];
|
||||
return batchNos.length === 1 ? batchNos[0] : '执行凭证详情';
|
||||
});
|
||||
const filteredRows = computed(() => {
|
||||
const plateNo = String(appliedQuery.plateNo || '').trim();
|
||||
return rows.value.filter(item => {
|
||||
const plateMatched = !plateNo || String(item.plateNo || '').includes(plateNo);
|
||||
const relatedMatched =
|
||||
appliedQuery.matched === undefined || appliedQuery.matched === null || appliedQuery.matched === ''
|
||||
? true
|
||||
: item.matched === appliedQuery.matched;
|
||||
return plateMatched && relatedMatched;
|
||||
});
|
||||
});
|
||||
const pageRows = computed(() => {
|
||||
const start = (page.current - 1) * page.size;
|
||||
return filteredRows.value.slice(start, start + page.size);
|
||||
});
|
||||
const rowIndex = index => (page.current - 1) * page.size + index + 1;
|
||||
|
||||
const load = async () => {
|
||||
if (!props.waybillId) {
|
||||
rows.value = [];
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getVoucherFolders(props.waybillId);
|
||||
rows.value = res.data?.data || [];
|
||||
} catch (error) {
|
||||
rows.value = [];
|
||||
ElMessage.error(error.message || '获取执行凭证失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
const search = () => {
|
||||
appliedQuery.plateNo = query.plateNo;
|
||||
appliedQuery.matched = query.matched;
|
||||
page.current = 1;
|
||||
};
|
||||
const reset = () => {
|
||||
query.plateNo = '';
|
||||
query.matched = undefined;
|
||||
search();
|
||||
};
|
||||
const openViewer = async row => {
|
||||
viewerVisible.value = true;
|
||||
viewerLoading.value = true;
|
||||
viewer.value = row;
|
||||
viewerFiles.value = [];
|
||||
try {
|
||||
const res = await getVoucherFolderDetail(row.voucherId, row.plateNo);
|
||||
viewer.value = res.data?.data || row;
|
||||
viewerFiles.value = viewer.value.files || [];
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '获取凭证详情失败');
|
||||
} finally {
|
||||
viewerLoading.value = false;
|
||||
}
|
||||
};
|
||||
const goToLoading = loadingNo => {
|
||||
router.push({ path: '/business/loading-manage', query: { loadingNo } });
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.waybillId,
|
||||
() => {
|
||||
page.current = 1;
|
||||
load();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.voucher-folder-page {
|
||||
&__search {
|
||||
padding: 12px 12px 4px;
|
||||
margin-bottom: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
&__search-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 360px 1fr;
|
||||
gap: 8px 24px;
|
||||
}
|
||||
&__search-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
&__table-wrap {
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
&__title {
|
||||
padding-bottom: 12px;
|
||||
color: #303133;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
&__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
&__pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 12px;
|
||||
}
|
||||
&__viewer {
|
||||
min-height: 360px;
|
||||
}
|
||||
&__viewer-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 28px;
|
||||
padding-bottom: 20px;
|
||||
color: #303133;
|
||||
}
|
||||
&__images {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 140px);
|
||||
gap: 20px;
|
||||
}
|
||||
&__images :deep(.el-image) {
|
||||
width: 140px;
|
||||
height: 100px;
|
||||
border: 6px solid #cfcfcf;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
:deep(.el-table) {
|
||||
--el-table-border-color: #eff1f7;
|
||||
}
|
||||
:deep(.el-table__body tr:nth-child(even) > td) {
|
||||
background: #fafafa;
|
||||
}
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
:deep(.el-form-item__label) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
:deep(.el-input),
|
||||
:deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.voucher-folder-page__search-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.voucher-folder-page__search-actions {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -202,7 +202,7 @@
|
||||
<section-card class="waybill-search">
|
||||
<el-form :inline="true" :model="detailQuery"
|
||||
>
|
||||
<el-form-item label="车牌号/船号"
|
||||
<el-form-item label="车牌号/船班号/船次号/班次号"
|
||||
><el-input v-model="detailQuery.vehicleNo" clearable /></el-form-item
|
||||
><el-form-item label="司机/船长姓名"
|
||||
><el-input v-model="detailQuery.driverName" clearable placeholder="请输入" /></el-form-item
|
||||
@@ -219,12 +219,14 @@
|
||||
prop="loadingIdentifier"
|
||||
label="配载标识号"
|
||||
min-width="130"
|
||||
/><el-table-column
|
||||
>
|
||||
<template #default="{ row }">{{ row.loadingIdentifier || row.loadingNo || '' }}</template>
|
||||
</el-table-column><el-table-column
|
||||
prop="vehicleNo"
|
||||
label="车牌号/航班号/船号/班列号"
|
||||
label="车牌号/船班号/船次号/班次号"
|
||||
min-width="190"
|
||||
/><el-table-column prop="transportType" label="运输方式" width="110">
|
||||
<template #default="{ row }">{{ transportTypeLabel(row.transportType) }}</template>
|
||||
<template #default="{ row }">{{ transportTypeLabel(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="driverName"
|
||||
@@ -334,6 +336,7 @@
|
||||
label-width="auto"
|
||||
class="archive-form"
|
||||
>
|
||||
<div class="dialog-section-title">项目信息</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="项目" prop="projectId">
|
||||
@@ -444,16 +447,21 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="dialog-section-title waybill-import-create__import-title">导入运单明细</div>
|
||||
<p v-if="!projectInfoReady" class="waybill-import-create__import-hint">
|
||||
请先选择项目、客户合同和承运信息,再导入运单明细
|
||||
</p>
|
||||
<el-form-item label="运单明细表" prop="file"
|
||||
><el-upload
|
||||
action="#"
|
||||
accept=".xls,.xlsx"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:disabled="!projectInfoReady"
|
||||
:file-list="files"
|
||||
:on-change="fileChange"
|
||||
:on-remove="fileRemove"
|
||||
><el-button type="primary">添加附件</el-button></el-upload
|
||||
><el-button type="primary" :disabled="!projectInfoReady">导入</el-button></el-upload
|
||||
><span class="waybill-import-create__file-tip">请上传运单明细表,仅支持 excel 格式;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法</span
|
||||
><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item
|
||||
>
|
||||
@@ -479,6 +487,7 @@
|
||||
:data="previewRows"
|
||||
border
|
||||
max-height="440"
|
||||
:row-class-name="({ row }) => (row._duplicate ? 'is-duplicate-row' : '')"
|
||||
@selection-change="rowSelection = $event"
|
||||
><el-table-column type="selection" width="55" /><el-table-column
|
||||
type="index"
|
||||
@@ -564,14 +573,18 @@
|
||||
/><span v-else>{{ formatCell(row, column) }}</span></template
|
||||
><span v-else>{{ formatCell(row, column) }}</span></template
|
||||
></el-table-column
|
||||
><el-table-column label="操作" width="180" fixed="right"
|
||||
><el-table-column label="操作" width="220" fixed="right"
|
||||
><template #default="{ row }"
|
||||
><template v-if="row._editing"
|
||||
><el-link type="primary" @click="saveRow(row)">保存</el-link
|
||||
><el-link type="primary" @click="cancelRow(row)">取消</el-link></template
|
||||
><template v-else
|
||||
><el-link type="primary" @click="editRow(row)">编辑</el-link
|
||||
><el-link type="danger" @click="deleteRow(row)">删除</el-link></template
|
||||
><div class="waybill-import-create__row-actions"
|
||||
><template v-if="row._editing"
|
||||
><el-link type="primary" @click="saveRow(row)">保存</el-link
|
||||
><el-link type="primary" @click="cancelRow(row)">取消</el-link></template
|
||||
><template v-else
|
||||
><el-link v-if="row._duplicate" type="primary" @click="confirmDuplicateRow(row)"
|
||||
>确认</el-link
|
||||
><el-link type="primary" @click="editRow(row)">编辑</el-link
|
||||
><el-link type="danger" @click="deleteRow(row)">删除</el-link></template
|
||||
></div
|
||||
></template
|
||||
></el-table-column
|
||||
></el-table
|
||||
@@ -579,6 +592,7 @@
|
||||
</section>
|
||||
<div v-if="createPage" class="waybill-import-create__footer">
|
||||
<el-button @click="closeCreate">取消</el-button>
|
||||
<el-button @click="saveDraft">保存草稿</el-button>
|
||||
<el-button type="primary" @click="confirmImport">确认导入</el-button>
|
||||
</div>
|
||||
</component>
|
||||
@@ -586,7 +600,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||
@@ -601,6 +615,7 @@ import SectionCard from '@/components/section-card/main.vue';
|
||||
|
||||
const props = defineProps({ modelValue: Boolean, standalone: Boolean, createPage: Boolean });
|
||||
const emit = defineEmits(['update:modelValue', 'closed']);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
@@ -656,10 +671,10 @@ const createDefaultForm = () => ({
|
||||
file: null,
|
||||
});
|
||||
const form = reactive(createDefaultForm());
|
||||
// 批量导入状态仅保留草稿与导入完成两种,与后端 importStatus 取值一致。
|
||||
// 导入状态与运单业务状态一致:进行中、已完成。
|
||||
const importStatusOptions = [
|
||||
{ label: '进行中', value: 'draft' },
|
||||
{ label: '导入完成', value: 'completed' },
|
||||
{ label: '进行中', value: 'running' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
];
|
||||
// 编辑草稿时明细已入库,此时不再强制重新上传附件。
|
||||
const validateImportFile = (rule, value, callback) =>
|
||||
@@ -705,6 +720,21 @@ const carrierContractsLoaded = ref(false),
|
||||
hasCarrierContracts = ref(null);
|
||||
const carrierTypes = ['承运商', '自运'];
|
||||
const isSelfOperated = computed(() => form.carrierType === '自运');
|
||||
const projectInfoReady = computed(() => {
|
||||
if (
|
||||
!form.projectId ||
|
||||
!String(form.customerName || '').trim() ||
|
||||
!form.contractId ||
|
||||
!form.carrierType ||
|
||||
!form.status ||
|
||||
!form.importType
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return isSelfOperated.value
|
||||
? Boolean(String(form.carrierName || '').trim())
|
||||
: Boolean(form.carrierContractId);
|
||||
});
|
||||
// 项目下没有承运商合同时只允许自运,与运单管理的承运类型收窄规则一致。
|
||||
const carrierTypeOptions = computed(() =>
|
||||
carrierContractsLoaded.value && hasCarrierContracts.value === false ? ['自运'] : carrierTypes
|
||||
@@ -733,7 +763,7 @@ const detailColumns = [
|
||||
{ prop: 'batchNo', label: '运单批次号', minWidth: 150 },
|
||||
{ prop: 'originalNo', label: '原始单号', editor: 'input', minWidth: 150 },
|
||||
{ prop: 'loadingIdentifier', label: '配载标识号', editor: 'input', minWidth: 150 },
|
||||
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号', editor: 'input', minWidth: 220 },
|
||||
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号', editor: 'input', minWidth: 220 },
|
||||
{ prop: 'transportType', label: '运输方式', editor: 'transportType', minWidth: 150 },
|
||||
{ prop: 'driverName', label: '司机/船长姓名', editor: 'driver', minWidth: 170 },
|
||||
{ prop: 'driverPhone', label: '司机/船长手机号', editor: 'input', minWidth: 150 },
|
||||
@@ -763,7 +793,7 @@ const detailColumns = [
|
||||
{ prop: 'waybillIdentifier', label: '同一运单标识号', editor: 'input', minWidth: 150 },
|
||||
];
|
||||
const requiredDetailFields = [
|
||||
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
|
||||
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号' },
|
||||
{ prop: 'transportType', label: '运输方式' },
|
||||
{ prop: 'departureAddress', label: '发货地址' },
|
||||
{ prop: 'arrivalAddress', label: '到货地址' },
|
||||
@@ -976,33 +1006,78 @@ const loadCommonCargoOptions = async row => {
|
||||
commonCargoLoadingMap.value = { ...commonCargoLoadingMap.value, [key]: false };
|
||||
}
|
||||
};
|
||||
const TRANSPORT_TYPE_DEFAULTS = [
|
||||
{ label: '公路运输', value: 'road', dictKey: 'road', dictValue: '公路运输' },
|
||||
{ label: '铁路运输', value: 'railway', dictKey: 'railway', dictValue: '铁路运输' },
|
||||
{ label: '水路运输', value: 'river', dictKey: 'river', dictValue: '水路运输' },
|
||||
{ label: '航空运输', value: 'air', dictKey: 'air', dictValue: '航空运输' },
|
||||
];
|
||||
const readDictionaryList = res => {
|
||||
const payload = res?.data?.data ?? res?.data ?? res;
|
||||
if (Array.isArray(payload)) return payload;
|
||||
if (Array.isArray(payload?.records)) return payload.records;
|
||||
if (Array.isArray(payload?.data)) return payload.data;
|
||||
return [];
|
||||
};
|
||||
const normalizeTransportTypeOptions = list => {
|
||||
const options = [];
|
||||
const append = item => {
|
||||
if (!item || typeof item !== 'object') return;
|
||||
const dictKey = String(item.dictKey ?? item.value ?? '').trim();
|
||||
const dictValue = String(item.dictValue ?? item.label ?? '').trim();
|
||||
if (dictKey === '-1') {
|
||||
(item.children || []).forEach(append);
|
||||
return;
|
||||
}
|
||||
if (dictKey || dictValue) {
|
||||
options.push({
|
||||
label: dictValue || dictKey,
|
||||
value: dictKey || dictValue,
|
||||
dictKey,
|
||||
dictValue,
|
||||
});
|
||||
}
|
||||
(item.children || []).forEach(append);
|
||||
};
|
||||
(list || []).forEach(append);
|
||||
return options.length ? options : TRANSPORT_TYPE_DEFAULTS;
|
||||
};
|
||||
const applyTransportTypeOptions = res => {
|
||||
transportTypeOptions.value = normalizeTransportTypeOptions(readDictionaryList(res));
|
||||
};
|
||||
const loadEditorOptions = async () => {
|
||||
const [dictRes, cargoTypeRes, driverRes] = await Promise.all([
|
||||
getDictionary({ code: 'transport_type' }),
|
||||
getCargoTypeList(1, 9999),
|
||||
getDriverList(1, 9999, {}),
|
||||
]);
|
||||
transportTypeOptions.value = extractRecords(dictRes).map(item => ({
|
||||
label: item.dictValue || item.label,
|
||||
value: item.dictKey || item.value,
|
||||
}));
|
||||
applyTransportTypeOptions(dictRes);
|
||||
cargoTypeOptions.value = buildCargoTypeTree(extractRecords(cargoTypeRes));
|
||||
cargoTypeFlatOptions.value = flattenCargoTypeOptions(cargoTypeOptions.value);
|
||||
driverOptions.value = extractRecords(driverRes);
|
||||
};
|
||||
const ensureTransportTypeOptions = async () => {
|
||||
if (transportTypeOptions.value.length) return;
|
||||
const dictRes = await getDictionary({ code: 'transport_type' });
|
||||
transportTypeOptions.value = extractRecords(dictRes).map(item => ({
|
||||
label: item.dictValue || item.label,
|
||||
value: item.dictKey || item.value,
|
||||
}));
|
||||
try {
|
||||
applyTransportTypeOptions(await getDictionary({ code: 'transport_type' }));
|
||||
} catch (error) {
|
||||
transportTypeOptions.value = TRANSPORT_TYPE_DEFAULTS;
|
||||
}
|
||||
};
|
||||
const transportTypeLabel = value => {
|
||||
if (value === null || value === undefined || value === '') return '-';
|
||||
return (
|
||||
transportTypeOptions.value.find(item => String(item.value) === String(value))?.label || value
|
||||
const source = value && typeof value === 'object' ? value : null;
|
||||
const stored = source ? source.transportType : value;
|
||||
const named = String(source?.transportTypeName || '').trim();
|
||||
const text = String(stored ?? '').trim();
|
||||
if (named && named !== text) return named;
|
||||
if (!text && !named) return '-';
|
||||
const target = (named || text).toLowerCase();
|
||||
const matched = transportTypeOptions.value.find(item =>
|
||||
[item.value, item.dictKey, item.label, item.dictValue].some(
|
||||
candidate => String(candidate ?? '').trim().toLowerCase() === target
|
||||
)
|
||||
);
|
||||
return matched?.dictValue || matched?.label || named || text || '-';
|
||||
};
|
||||
const loadBatches = async () => {
|
||||
const res = await api.getImportBatches({ ...query, current: page.current, size: page.size });
|
||||
@@ -1011,7 +1086,7 @@ const loadBatches = async () => {
|
||||
};
|
||||
onMounted(() => {
|
||||
if (props.standalone) loadBatches();
|
||||
if (props.createPage) openCreate();
|
||||
if (props.createPage) openCreatePage();
|
||||
});
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
@@ -1034,16 +1109,18 @@ const loadDetails = async () => {
|
||||
details.value = res.data?.data?.records || [];
|
||||
detailPage.total = res.data?.data?.total || 0;
|
||||
};
|
||||
const openCreate = async () => {
|
||||
const openCreate = () => prepareCreateForm(true);
|
||||
const prepareCreateForm = async generateCode => {
|
||||
resetCreateForm();
|
||||
createVisible.value = true;
|
||||
const [projectRes, , optionRes, batchNo] = await Promise.all([
|
||||
const requests = [
|
||||
getProjectList(1, 9999, projectQueryParams),
|
||||
loadEditorOptions(),
|
||||
api.getImportOptions?.(),
|
||||
generateBatchNo(),
|
||||
]);
|
||||
form.batchNo = batchNo;
|
||||
];
|
||||
if (generateCode) requests.push(generateBatchNo());
|
||||
const [projectRes, , optionRes, batchNo] = await Promise.all(requests);
|
||||
if (generateCode) form.batchNo = batchNo || '';
|
||||
projects.value = projectRes.data?.data?.records || [];
|
||||
const data = optionRes?.data?.data || {};
|
||||
queryCarriers.value = data.carriers || [];
|
||||
@@ -1082,27 +1159,78 @@ const loadBatchRows = async batchId => {
|
||||
_key: `${id || 'row'}-${index}`,
|
||||
batchNo: item.batchNo || form.batchNo,
|
||||
}));
|
||||
refreshDuplicateFlags();
|
||||
rowSelection.value = [];
|
||||
previewTab.value = 'all';
|
||||
};
|
||||
const editBatch = async row => {
|
||||
await openCreate();
|
||||
const editBatch = row => {
|
||||
router.push({
|
||||
path: '/business/waybill-import/form',
|
||||
query: { id: row.id, batchNo: row.batchNo || '' },
|
||||
});
|
||||
};
|
||||
const openCreatePage = async () => {
|
||||
const batchId = route.query.id;
|
||||
if (!batchId) {
|
||||
await openCreate();
|
||||
return;
|
||||
}
|
||||
const res = await api.getImportBatches({
|
||||
id: batchId,
|
||||
batchNo: route.query.batchNo || '',
|
||||
current: 1,
|
||||
size: 20,
|
||||
});
|
||||
const batch = (res.data?.data?.records || []).find(item => String(item.id) === String(batchId));
|
||||
if (!batch) {
|
||||
ElMessage.warning('未找到导入批次');
|
||||
router.push({ path: '/business/waybill-import' });
|
||||
return;
|
||||
}
|
||||
await fillEditForm(batch);
|
||||
};
|
||||
const fillEditForm = async row => {
|
||||
await prepareCreateForm(false);
|
||||
const snapshot = { ...row };
|
||||
const savedCarrierId = String(snapshot.carrierIds || snapshot.carrierId || '')
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.find(Boolean);
|
||||
if (snapshot.projectId) await projectChange(snapshot.projectId);
|
||||
const project = projects.value.find(item => String(item.id) === String(snapshot.projectId));
|
||||
Object.assign(form, snapshot, {
|
||||
projectId: snapshot.projectId || '',
|
||||
id: snapshot.id || '',
|
||||
batchNo: snapshot.batchNo || '',
|
||||
projectId: project?.id || snapshot.projectId || '',
|
||||
projectName: snapshot.projectName || form.projectName,
|
||||
// 列表返回的是 importStatus,表单沿用 status 字段提交给后端。
|
||||
status: snapshot.importStatus || 'completed',
|
||||
status:
|
||||
snapshot.importStatus === 'running' || snapshot.importStatus === 'completed'
|
||||
? snapshot.importStatus
|
||||
: 'completed',
|
||||
file: null,
|
||||
carrierIds: [],
|
||||
});
|
||||
const plan = plans.value.find(item => String(item.id) === String(snapshot.planId));
|
||||
// 接口数字 null 会序列化为 -1,未选计划时回填为空
|
||||
form.planId =
|
||||
plan && Number(plan.id) !== -1 && Number(plan.id) > 0
|
||||
? plan.id
|
||||
: normalizeOptionalPlanId(snapshot.planId) || '';
|
||||
const contract = contracts.value.find(item => String(item.id) === String(snapshot.contractId));
|
||||
form.contractId = contract?.id || '';
|
||||
form.carrierType = snapshot.carrierType || '承运商';
|
||||
contractChange(form.contractId);
|
||||
syncCarrierOptions();
|
||||
const carrierOption =
|
||||
form.carrierType === '自运'
|
||||
? null
|
||||
: carrierOptions.value.find(item => String(item.id) === String(savedCarrierId || '')) ||
|
||||
carrierOptions.value.find(item => item.carrierName === snapshot.carrierName);
|
||||
const carrierValue =
|
||||
form.carrierType === '自运' ? snapshot.carrierName : snapshot.carrierContractId;
|
||||
form.carrierType === '自运'
|
||||
? snapshot.carrierName
|
||||
: snapshot.carrierContractId || carrierOption?.value;
|
||||
if (carrierValue) carrierChange(carrierValue);
|
||||
await loadBatchRows(snapshot.id);
|
||||
};
|
||||
@@ -1170,6 +1298,12 @@ const carrierChange = value => {
|
||||
if (!isSelfOperated.value) form.carrierContractId = item.carrierContractId || '';
|
||||
};
|
||||
const fileChange = async (file, list) => {
|
||||
if (!projectInfoReady.value) {
|
||||
ElMessage.warning('请先选择项目、客户合同和承运信息,再导入运单明细');
|
||||
files.value = [];
|
||||
form.file = null;
|
||||
return;
|
||||
}
|
||||
if (file.raw?.size > 50 * 1024 * 1024) {
|
||||
ElMessage.warning('单个文件大小不能超过50M');
|
||||
files.value = [];
|
||||
@@ -1190,7 +1324,7 @@ const fileChange = async (file, list) => {
|
||||
const keyMap = {
|
||||
originalNo: ['原始单号'],
|
||||
loadingIdentifier: ['配载标识号'],
|
||||
vehicleNo: ['车牌号/航班号/船号/班列号'],
|
||||
vehicleNo: ['车牌号/船班号/船次号/班次号', '车牌号/航班号/船号/班列号'],
|
||||
transportType: ['运输方式', '运输类型'],
|
||||
driverName: ['司机/船长姓名', '司机/船长'],
|
||||
driverPhone: ['司机/船长手机号'],
|
||||
@@ -1227,7 +1361,6 @@ const fileChange = async (file, list) => {
|
||||
const text = String(value ?? '').trim();
|
||||
return text.slice(0, 10);
|
||||
};
|
||||
const count = new Map();
|
||||
// 先解析数据,但不填充到 rows.value
|
||||
const parsedRows = source.map((item, index) => {
|
||||
const row = { _key: `${Date.now()}-${index}`, ...item, batchNo: form.batchNo };
|
||||
@@ -1241,14 +1374,9 @@ const fileChange = async (file, list) => {
|
||||
row.actualEndDate = normalizeImportDate(row.actualEndDate);
|
||||
row.planStartDate = normalizeImportDate(row.planStartDate);
|
||||
row.planEndDate = normalizeImportDate(row.planEndDate);
|
||||
const duplicateKey = JSON.stringify(Object.keys(keyMap).map(key => row[key]));
|
||||
count.set(duplicateKey, (count.get(duplicateKey) || 0) + 1);
|
||||
row._duplicateKey = duplicateKey;
|
||||
return row;
|
||||
});
|
||||
parsedRows.forEach(row => {
|
||||
row._duplicate = count.get(row._duplicateKey) > 1;
|
||||
});
|
||||
refreshDuplicateFlags(parsedRows);
|
||||
|
||||
// 调用后端校验接口,校验通过才填充表格
|
||||
const isValid = await performValidation(parsedRows);
|
||||
@@ -1268,67 +1396,175 @@ const fileRemove = () => {
|
||||
form.file = null;
|
||||
};
|
||||
|
||||
const performValidation = async (parsedRows) => {
|
||||
/** 运单批量导入每次请求最多提交行数,避免网关 body 超限。 */
|
||||
const IMPORT_BATCH_SIZE = 100;
|
||||
|
||||
const importFieldText = value => String(value ?? '').trim();
|
||||
|
||||
/** 同一运单批次号下,全部导入字段一致时标记为疑似重复。已在线确认的行不再标为重复。 */
|
||||
const refreshDuplicateFlags = (list = rows.value) => {
|
||||
const groups = new Map();
|
||||
list.forEach(row => {
|
||||
const signature = detailColumns.map(column => importFieldText(row[column.prop])).join('\u0001');
|
||||
const batchNo = importFieldText(row.batchNo || form.batchNo);
|
||||
if (row._duplicateConfirmed && row._duplicateSignature !== signature) {
|
||||
row._duplicateConfirmed = false;
|
||||
}
|
||||
row._duplicateSignature = signature;
|
||||
const groupKey = `${batchNo}\u0002${signature}`;
|
||||
const group = groups.get(groupKey) || [];
|
||||
group.push(row);
|
||||
groups.set(groupKey, group);
|
||||
});
|
||||
groups.forEach(group => {
|
||||
const duplicated = group.length > 1;
|
||||
group.forEach(row => {
|
||||
row._duplicate = duplicated && !row._duplicateConfirmed;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const confirmDuplicateRow = row => {
|
||||
row._duplicateConfirmed = true;
|
||||
refreshDuplicateFlags();
|
||||
ElMessage.success('已确认,该行将按正常数据导入');
|
||||
};
|
||||
|
||||
const stripInternalRowFields = row => {
|
||||
const { _key, _duplicate, _duplicateKey, _duplicateConfirmed, _duplicateSignature, _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`);
|
||||
};
|
||||
|
||||
/** 按每批 100 条请求校验,全部批次结束后再汇总失败行。 */
|
||||
const collectImportValidationErrors = async rowList => {
|
||||
const errorMap = new Map();
|
||||
for (const chunk of chunkImportRows(rowList)) {
|
||||
const response = await api.validateImport(buildImportPayload(form.status, chunk.rows));
|
||||
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;
|
||||
}
|
||||
errorMap.set(chunk.globalIndexes[localIndex], error.errorMessage || '校验失败');
|
||||
});
|
||||
}
|
||||
}
|
||||
return errorMap;
|
||||
};
|
||||
|
||||
const exportCollectedImportFailures = async (rowList, errorMap, exportAll = false) => {
|
||||
const indexedRows = exportAll
|
||||
? rowList.map((row, globalIndex) => [globalIndex, errorMap.get(globalIndex) || ''])
|
||||
: [...errorMap.entries()].sort((left, right) => left[0] - right[0]);
|
||||
const failureRows = indexedRows.map(([globalIndex, errorMessage]) => ({
|
||||
...stripInternalRowFields(rowList[globalIndex]),
|
||||
errorMessage,
|
||||
}));
|
||||
await downloadImportFailures(failureRows);
|
||||
ElMessage.warning('数据校验失败,已自动下载错误明细表,请修正后重新上传');
|
||||
};
|
||||
|
||||
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) {
|
||||
await exportCollectedImportFailures(parsedRows, errorMap);
|
||||
return false;
|
||||
}
|
||||
|
||||
ElMessage.success('数据校验通过');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('校验失败:', error);
|
||||
ElMessage.error('校验接口调用失败');
|
||||
ElMessage.error(error?.message || '校验接口调用失败');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const formatCell = (row, column) => row[column.prop] || '';
|
||||
const formatCell = (row, column) =>
|
||||
column.prop === 'transportType' ? transportTypeLabel(row) : row[column.prop] || '';
|
||||
const updateEditorValue = (column, row, value) => {
|
||||
if (column.editor === 'number') {
|
||||
const [integer = '', decimal = ''] = String(value || '')
|
||||
@@ -1403,6 +1639,7 @@ const editRow = row => {
|
||||
const saveRow = row => {
|
||||
row._editing = false;
|
||||
delete row._editSnapshot;
|
||||
refreshDuplicateFlags();
|
||||
};
|
||||
const cancelRow = row => {
|
||||
if (row._editSnapshot) Object.assign(row, row._editSnapshot);
|
||||
@@ -1411,11 +1648,13 @@ const cancelRow = row => {
|
||||
};
|
||||
const deleteRow = row => {
|
||||
rows.value = rows.value.filter(item => item !== row);
|
||||
refreshDuplicateFlags();
|
||||
};
|
||||
const removeSelectedRows = () => {
|
||||
const keys = new Set(rowSelection.value.map(row => row._key));
|
||||
rows.value = rows.value.filter(row => !keys.has(row._key));
|
||||
rowSelection.value = [];
|
||||
refreshDuplicateFlags();
|
||||
};
|
||||
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
|
||||
const normalizeOptionalImportNumber = (value, emptySentinels = []) => {
|
||||
@@ -1442,9 +1681,22 @@ const normalizeImportRow = row => ({
|
||||
row['*运输方式']
|
||||
),
|
||||
});
|
||||
const buildImportPayload = (status = form.status) => {
|
||||
const normalizeOptionalPlanId = value => {
|
||||
if (value === undefined || value === null || value === '') return null;
|
||||
return Number(value) === -1 || Number(value) <= 0 ? null : value;
|
||||
};
|
||||
const buildImportPayload = (status = form.status, rowList = rows.value, extra = {}) => {
|
||||
const { file, ...batch } = form;
|
||||
return { ...batch, status, rows: rows.value.map(normalizeImportRow) };
|
||||
const planId = normalizeOptionalPlanId(batch.planId);
|
||||
return {
|
||||
...batch,
|
||||
planId,
|
||||
// 未选计划时不带占位名称,避免落库/回显 -1
|
||||
planName: planId ? batch.planName || undefined : null,
|
||||
status,
|
||||
rows: rowList.map(normalizeImportRow).map(stripInternalRowFields),
|
||||
...extra,
|
||||
};
|
||||
};
|
||||
// 独立页面下返回批次列表路由,弹窗形态下关闭新建面板并刷新列表。
|
||||
const backToList = () => {
|
||||
@@ -1454,22 +1706,72 @@ 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();
|
||||
};
|
||||
const confirmImport = async () => {
|
||||
// 导入状态选择草稿时不生成正式运单,等同于保存草稿。
|
||||
if (form.status === 'draft') return saveDraft();
|
||||
await formRef.value?.validate();
|
||||
if (!validateImportRows()) return;
|
||||
const count = rows.value.filter(row => row._duplicate).length;
|
||||
if (count) await ElMessageBox.confirm(`当前导入有${count}条重复数据,是否确认导入?`, '提示');
|
||||
if (form.importType === 'settlement')
|
||||
if (count) await ElMessageBox.confirm(`当前导入有${count}条疑似重复数据,是否确认导入?`, '提示');
|
||||
if (form.importType === 'settlement' && form.status === 'completed')
|
||||
await ElMessageBox.confirm('确认后将同时生成运单、应收应付明细、计结算单,是否继续?', '提示');
|
||||
await api.confirmImport(buildImportPayload());
|
||||
ElMessage.success('导入成功');
|
||||
backToList();
|
||||
|
||||
const chunks = chunkImportRows(rows.value);
|
||||
let errorMap;
|
||||
try {
|
||||
errorMap = await collectImportValidationErrors(rows.value);
|
||||
} catch (error) {
|
||||
console.error('校验失败:', error);
|
||||
ElMessage.error(error?.message || '校验接口调用失败');
|
||||
return;
|
||||
}
|
||||
if (errorMap.size) {
|
||||
await exportCollectedImportFailures(rows.value, errorMap, true);
|
||||
return;
|
||||
}
|
||||
// 校验全部通过后才分批落库,避免前面批次已入库、后面批次校验失败。
|
||||
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>
|
||||
|
||||
@@ -1562,6 +1864,15 @@ const confirmImport = async () => {
|
||||
&__detail-panel {
|
||||
margin-top: 12px;
|
||||
}
|
||||
&__import-title {
|
||||
margin-top: 8px;
|
||||
}
|
||||
&__import-hint {
|
||||
margin: 0 0 12px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
&__form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -1574,6 +1885,14 @@ const confirmImport = async () => {
|
||||
gap: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
&__row-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
:deep(.is-duplicate-row > td.el-table__cell) {
|
||||
background: #fff7e8;
|
||||
}
|
||||
&__file-tip {
|
||||
margin: 0 24px;
|
||||
color: #606266;
|
||||
|
||||
@@ -492,6 +492,7 @@
|
||||
v-model="row.quantityUnit"
|
||||
clearable
|
||||
:disabled="dialogReadonly"
|
||||
@change="handleTransportCargoQuantityUnitChange"
|
||||
@visible-change="visible => visible && loadTaskQuantityUnitOptions()"
|
||||
>
|
||||
<el-option
|
||||
@@ -1091,20 +1092,6 @@
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="数量合计">
|
||||
<el-input :model-value="taskFullFreightGroupQuantity(group)" disabled>
|
||||
<template #append>
|
||||
<el-select
|
||||
:model-value="group.quantityUnit"
|
||||
class="waybill-manage-page__append-select waybill-manage-page__append-select--wide"
|
||||
placeholder="单位"
|
||||
disabled
|
||||
>
|
||||
<el-option :label="fixedQuantityUnit" :value="fixedQuantityUnit" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="运费">
|
||||
<el-input
|
||||
:model-value="taskFullFreightGroupAmount(group)"
|
||||
@@ -1117,11 +1104,6 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="运费合计">
|
||||
<el-input :model-value="taskFullFreightTotal" disabled>
|
||||
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="其他运费合计">
|
||||
<el-input
|
||||
v-model="form.otherFeeTotal"
|
||||
@@ -1133,6 +1115,11 @@
|
||||
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="运费合计">
|
||||
<el-input :model-value="taskFullFreightTotal" disabled>
|
||||
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1317,7 +1304,7 @@
|
||||
@change="handleTaskCargoTypeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="货物名称">
|
||||
<el-form-item label="货物名称" required>
|
||||
<el-autocomplete
|
||||
v-model="form.cargoName"
|
||||
placeholder="请输入货物名称"
|
||||
@@ -1347,7 +1334,7 @@
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="型号">
|
||||
<el-form-item label="型号" required>
|
||||
<el-input
|
||||
v-model="form.model"
|
||||
placeholder="选择货物自动回填"
|
||||
@@ -1355,7 +1342,7 @@
|
||||
:disabled="dialogReadonly"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="数量">
|
||||
<el-form-item label="数量" required>
|
||||
<el-input
|
||||
v-model="form.quantity"
|
||||
placeholder="请输入数量"
|
||||
@@ -1802,14 +1789,14 @@
|
||||
:class="{ 'is-plan': item[0] === 'planName' }"
|
||||
>
|
||||
<span class="waybill-manage-page__waybill-label">{{ item[1] }}</span>
|
||||
<el-link
|
||||
<span
|
||||
v-if="canOpenWaybillDetailSummaryLink(item[0])"
|
||||
type="primary"
|
||||
class="waybill-manage-page__waybill-value is-link"
|
||||
role="link"
|
||||
@click="handleWaybillDetailSummaryClick(item[0])"
|
||||
>
|
||||
{{ formatDetailValue(detailRow, item[0]) }}
|
||||
</el-link>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
:class="['waybill-manage-page__waybill-value', { 'is-link': item[2] }]"
|
||||
@@ -1962,7 +1949,7 @@
|
||||
>
|
||||
</div>
|
||||
<div class="waybill-manage-page__waybill-detail-field">
|
||||
<span>航班号/船次号/班列号</span>
|
||||
<span>船班号/船次号/班次号</span>
|
||||
<strong>{{ detailRow.vehicleNo || '-' }}</strong>
|
||||
</div>
|
||||
<div class="waybill-manage-page__waybill-detail-field">
|
||||
@@ -2162,8 +2149,12 @@
|
||||
label="批量补录"
|
||||
name="batchSupplement"
|
||||
>
|
||||
<voucher-execution-detail
|
||||
v-if="waybillIsCarrier(detailRow)"
|
||||
:waybill-id="detailRow.id"
|
||||
/>
|
||||
<div
|
||||
v-if="waybillVoucherFolderView"
|
||||
v-else-if="waybillVoucherFolderView"
|
||||
class="waybill-manage-page__voucher-folder-toolbar"
|
||||
>
|
||||
<el-button text type="primary" @click="backToWaybillVoucherFolders">
|
||||
@@ -2172,6 +2163,7 @@
|
||||
<span>{{ waybillVoucherFolderName(waybillVoucherFolder) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!waybillIsCarrier(detailRow)"
|
||||
v-loading="waybillVoucherImagesLoading"
|
||||
class="waybill-manage-page__voucher-image-grid"
|
||||
>
|
||||
@@ -2356,7 +2348,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="detailPageLocked && !isPublicWaybillView" class="waybill-manage-page__detail-footer">
|
||||
<div v-if="detailPageLocked" class="waybill-manage-page__detail-footer">
|
||||
<el-button type="primary" @click="closeDetail">关闭</el-button>
|
||||
</div>
|
||||
<template v-if="!detailPageLocked" #footer>
|
||||
@@ -2421,14 +2413,15 @@
|
||||
原:{{ row.originalDepartureAddress || '-' }}
|
||||
</div>
|
||||
<el-input
|
||||
v-model="row.departureAddress"
|
||||
placeholder="请选择或输入地址"
|
||||
@input="updateWaybillRouteChangeNodes"
|
||||
:model-value="row.departureAddress"
|
||||
readonly
|
||||
placeholder="请点击地图选点"
|
||||
@click="openWaybillRouteChangeMapDialog(row, 'departure')"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon
|
||||
class="waybill-manage-page__route-change-location"
|
||||
@click="openWaybillCommonAddressDialog(row, 'departure')"
|
||||
@click.stop="openWaybillRouteChangeMapDialog(row, 'departure')"
|
||||
>
|
||||
<Location />
|
||||
</el-icon>
|
||||
@@ -2442,14 +2435,15 @@
|
||||
原:{{ row.originalArrivalAddress || '-' }}
|
||||
</div>
|
||||
<el-input
|
||||
v-model="row.arrivalAddress"
|
||||
placeholder="请选择或输入地址"
|
||||
@input="updateWaybillRouteChangeNodes"
|
||||
:model-value="row.arrivalAddress"
|
||||
readonly
|
||||
placeholder="请点击地图选点"
|
||||
@click="openWaybillRouteChangeMapDialog(row, 'arrival')"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon
|
||||
class="waybill-manage-page__route-change-location"
|
||||
@click="openWaybillCommonAddressDialog(row, 'arrival')"
|
||||
@click.stop="openWaybillRouteChangeMapDialog(row, 'arrival')"
|
||||
>
|
||||
<Location />
|
||||
</el-icon>
|
||||
@@ -2505,49 +2499,6 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="waybillCommonAddressBox"
|
||||
title="选择常用地址"
|
||||
append-to-body
|
||||
width="900px"
|
||||
class="waybill-manage-page__common-address-dialog"
|
||||
>
|
||||
<div class="waybill-manage-page__common-address-toolbar">
|
||||
<el-input
|
||||
v-model="waybillCommonAddressQuery.addressName"
|
||||
clearable
|
||||
placeholder="请输入地址名称"
|
||||
@keyup.enter="loadWaybillCommonAddresses"
|
||||
/>
|
||||
<el-button type="primary" @click="loadWaybillCommonAddresses">查询</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="waybillCommonAddressLoading"
|
||||
:data="waybillCommonAddressRows"
|
||||
border
|
||||
@row-dblclick="selectWaybillCommonAddress"
|
||||
>
|
||||
<el-table-column prop="addressName" label="地址名称" min-width="180" />
|
||||
<el-table-column prop="regionName" label="区域" min-width="180" />
|
||||
<el-table-column prop="detailAddress" label="详细地址" min-width="260" />
|
||||
<el-table-column label="操作" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="selectWaybillCommonAddress(row)">选择</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-pagination
|
||||
v-model:current-page="waybillCommonAddressPage.currentPage"
|
||||
v-model:page-size="waybillCommonAddressPage.pageSize"
|
||||
layout="total, prev, pager, next"
|
||||
:total="waybillCommonAddressPage.total"
|
||||
@current-change="loadWaybillCommonAddresses"
|
||||
/>
|
||||
<el-button @click="waybillCommonAddressBox = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<flow-design
|
||||
v-if="website.design.designMode"
|
||||
is-dialog
|
||||
@@ -2858,6 +2809,7 @@
|
||||
v-model="transportMapBox"
|
||||
width="920px"
|
||||
@opened="initTransportAmap"
|
||||
@closed="waybillRouteChangeMapTarget = null"
|
||||
>
|
||||
<div class="waybill-manage-page__map-toolbar">
|
||||
<el-input
|
||||
@@ -2886,7 +2838,7 @@
|
||||
>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="transportMapBox = false">取消</el-button>
|
||||
<el-button @click="closeTransportMapDialog">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!transportMapSelected.longitude"
|
||||
@@ -3135,7 +3087,7 @@ import {
|
||||
getVoucherImages as getProcessConfigVoucherImages,
|
||||
} from '@/api/business/process-config';
|
||||
import { getPunchRecords as getWaybillPunchRecords, locateVehicle, trackVehicle } from '@/api/business/waybill-manage';
|
||||
import { getMkPublicDetail } from '@/api/mk-process';
|
||||
import VoucherExecutionDetail from './voucher-execution-detail.vue';
|
||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||
import {
|
||||
fetchDriversByCarrierOrganizations,
|
||||
@@ -3145,10 +3097,9 @@ import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { addressTypeOptions } from '@/option/base/common-address';
|
||||
import { packageOptions } from '@/option/business/common';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { normalizeMapSearchResults } from '@/utils/map-search';
|
||||
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
|
||||
import { applyTableMenuWidth } from '@/utils/table-menu';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { isMobile } from '@/utils/validate';
|
||||
@@ -3173,9 +3124,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(),
|
||||
@@ -3330,6 +3283,7 @@ export default {
|
||||
WarnTriangleFilled,
|
||||
ElImageViewer,
|
||||
OpenFileViewer,
|
||||
VoucherExecutionDetail,
|
||||
},
|
||||
name: 'WaybillManagePage',
|
||||
props: {
|
||||
@@ -3471,18 +3425,7 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
waybillCommonAddressBox: false,
|
||||
waybillCommonAddressLoading: false,
|
||||
waybillCommonAddressQuery: {
|
||||
addressName: '',
|
||||
},
|
||||
waybillCommonAddressRows: [],
|
||||
waybillCommonAddressTarget: null,
|
||||
waybillCommonAddressPage: {
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
},
|
||||
waybillRouteChangeMapTarget: null,
|
||||
draftSaveLoading: false,
|
||||
crudDialogType: '',
|
||||
defaultDialogButtons: null,
|
||||
@@ -3662,7 +3605,6 @@ export default {
|
||||
this.formPageLocked = this.isStandaloneWaybillFormPage;
|
||||
this.detailPageLocked = this.isStandaloneWaybillDetailPage;
|
||||
this.formModeLocked = this.$route.query.mode || '';
|
||||
if (this.isPublicWaybillView) return;
|
||||
this.setupSearchAutocompletes();
|
||||
if (this.config.enableAllDept && this.isAdmin) {
|
||||
this.allDept = 1;
|
||||
@@ -3692,6 +3634,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();
|
||||
},
|
||||
@@ -3727,14 +3677,8 @@ export default {
|
||||
isStandaloneWaybillFormPage() {
|
||||
return this.isStandaloneWaybillPage && ['add', 'edit'].includes(this.$route.query.mode);
|
||||
},
|
||||
isPublicWaybillView() {
|
||||
return this.$route.path === '/business/waybill-manage/public-view';
|
||||
},
|
||||
isStandaloneWaybillDetailPage() {
|
||||
return (
|
||||
(this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute) ||
|
||||
this.isPublicWaybillView
|
||||
);
|
||||
return this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute;
|
||||
},
|
||||
// 容器形态读实例创建时锁定的标志,不随 $route 翻转(详见 data 中 formPageLocked 的说明)
|
||||
// 当前路由是否仍属于本实例(tab.js 按 fullPath 建实例,一个实例只对应一个 path)。
|
||||
@@ -3794,7 +3738,7 @@ export default {
|
||||
return !this.isRoadTransport;
|
||||
},
|
||||
transportVehicleNoLabel() {
|
||||
return this.isRoadTransport ? '车牌号' : '船/航/班列号';
|
||||
return this.isRoadTransport ? '车牌号' : '船班号/船次号/班次号';
|
||||
},
|
||||
actionButtonLikeSearch() {
|
||||
return this.config.actionButtonLikeSearch === true;
|
||||
@@ -3863,9 +3807,6 @@ export default {
|
||||
if (!total) return '';
|
||||
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(3)));
|
||||
},
|
||||
fixedQuantityUnit() {
|
||||
return WAYBILL_QUANTITY_UNIT;
|
||||
},
|
||||
taskFreightAmount() {
|
||||
const quantity = Number(this.taskCargoQuantityTotal || this.form.quantity || 0);
|
||||
const unitPrice = Number(this.form.unitPrice || 0);
|
||||
@@ -3896,10 +3837,7 @@ export default {
|
||||
return this.currencyRemark(currency);
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
canViewAllDept() {
|
||||
return this.isAdmin;
|
||||
@@ -3999,7 +3937,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 || '')
|
||||
@@ -4038,7 +3988,7 @@ export default {
|
||||
this.attachmentDocumentPreviewVisible = false;
|
||||
this.attachmentImagePreviewVisible = false;
|
||||
this.waybillRouteChangeBox = false;
|
||||
this.waybillCommonAddressBox = false;
|
||||
this.waybillRouteChangeMapTarget = null;
|
||||
this.flowBox = false;
|
||||
this.transportRouteBox = false;
|
||||
this.transportAddressBox = false;
|
||||
@@ -4505,6 +4455,20 @@ export default {
|
||||
if (prop === 'feeGenerationMode') {
|
||||
return row[prop] === 'manual' ? '账单导入生成' : '系统生成';
|
||||
}
|
||||
// 计划未选时接口可能回传 planId=-1 / planName=-1,详情统一显示为空
|
||||
if (prop === 'planName' || prop === 'planId') {
|
||||
const planName = row.planName;
|
||||
if (
|
||||
planName === undefined ||
|
||||
planName === null ||
|
||||
planName === '' ||
|
||||
String(planName) === '-1' ||
|
||||
Number(planName) === -1
|
||||
) {
|
||||
return '-';
|
||||
}
|
||||
return planName;
|
||||
}
|
||||
const column = this.findColumn(this.option.column, prop);
|
||||
if (column?.formatter) return column.formatter(row, {}, row[prop]);
|
||||
if (column?.dicData) {
|
||||
@@ -4512,6 +4476,15 @@ export default {
|
||||
if (item) return item.label;
|
||||
}
|
||||
const value = row[prop];
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
Number(value) === -1 ||
|
||||
String(value) === '-1'
|
||||
) {
|
||||
return '-';
|
||||
}
|
||||
if (value && typeof value === 'object') return JSON.stringify(value);
|
||||
if (typeof value === 'string' && /^[\[{]/.test(value.trim())) {
|
||||
try {
|
||||
@@ -4521,7 +4494,7 @@ export default {
|
||||
// 非 JSON 文本按原值展示。
|
||||
}
|
||||
}
|
||||
return value === undefined || value === null || value === '' ? '-' : value;
|
||||
return value;
|
||||
},
|
||||
waybillTransportMode(row = {}) {
|
||||
return (
|
||||
@@ -4595,7 +4568,7 @@ export default {
|
||||
return text.slice(0, 10);
|
||||
},
|
||||
waybillVehicleNoLabel(row = {}) {
|
||||
return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号';
|
||||
return this.waybillIsRoadTransport(row) ? '车牌号' : '船班号/船次号/班次号';
|
||||
},
|
||||
waybillUnitPrice(row = {}) {
|
||||
const price = row.unitPrice || row.price || '';
|
||||
@@ -4655,19 +4628,14 @@ export default {
|
||||
this.waybillVoucherFolder = null;
|
||||
this.waybillVoucherFolderView = false;
|
||||
this.waybillVoucherImagesLoaded = false;
|
||||
const request = this.isPublicWaybillView
|
||||
? getMkPublicDetail('waybill-manage', row.id)
|
||||
: typeof this.api.getDetail === 'function'
|
||||
const request =
|
||||
typeof this.api.getDetail === 'function'
|
||||
? this.api.getDetail(row.id)
|
||||
: Promise.resolve({ data: { data: row } });
|
||||
request
|
||||
.then(res => {
|
||||
const detail = res?.data?.data || res?.data || row;
|
||||
this.detailRow = detail;
|
||||
if (this.isPublicWaybillView) {
|
||||
this.applyFormDetail(detail);
|
||||
return;
|
||||
}
|
||||
this.loadWaybillPunchRecords();
|
||||
this.loadWaybillVoucherImages();
|
||||
if (detail.contractId) {
|
||||
@@ -4708,8 +4676,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;
|
||||
@@ -4742,7 +4711,6 @@ export default {
|
||||
return row.id || row.customerId || '';
|
||||
},
|
||||
canOpenWaybillDetailSummaryLink(prop) {
|
||||
if (this.isPublicWaybillView) return false;
|
||||
const row = this.detailRow || {};
|
||||
const value = this.formatDetailValue(row, prop);
|
||||
if (!value || value === '-') return false;
|
||||
@@ -5015,43 +4983,22 @@ export default {
|
||||
updateWaybillRouteChangeNodes() {
|
||||
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
|
||||
},
|
||||
openWaybillCommonAddressDialog(row, field) {
|
||||
this.waybillCommonAddressTarget = { row, field };
|
||||
this.waybillCommonAddressQuery = { addressName: '' };
|
||||
this.waybillCommonAddressPage.currentPage = 1;
|
||||
this.waybillCommonAddressBox = true;
|
||||
this.loadWaybillCommonAddresses();
|
||||
},
|
||||
loadWaybillCommonAddresses() {
|
||||
this.waybillCommonAddressLoading = true;
|
||||
getCommonAddressList(
|
||||
this.waybillCommonAddressPage.currentPage,
|
||||
this.waybillCommonAddressPage.pageSize,
|
||||
{
|
||||
...this.waybillCommonAddressQuery,
|
||||
allDept: 0,
|
||||
}
|
||||
)
|
||||
.then(res => {
|
||||
const data = res.data?.data || {};
|
||||
this.waybillCommonAddressRows = data.records || [];
|
||||
this.waybillCommonAddressPage.total = data.total || 0;
|
||||
})
|
||||
.finally(() => {
|
||||
this.waybillCommonAddressLoading = false;
|
||||
});
|
||||
},
|
||||
selectWaybillCommonAddress(address) {
|
||||
const target = this.waybillCommonAddressTarget;
|
||||
if (!target) return;
|
||||
target.row[`${target.field}Address`] =
|
||||
[address.regionName, address.detailAddress || address.addressName]
|
||||
.filter(Boolean)
|
||||
.join('') ||
|
||||
address.addressName ||
|
||||
'';
|
||||
this.updateWaybillRouteChangeNodes();
|
||||
this.waybillCommonAddressBox = false;
|
||||
openWaybillRouteChangeMapDialog(row, field) {
|
||||
if (!row || !field) return;
|
||||
this.waybillRouteChangeMapTarget = { row, field };
|
||||
this.transportMapTarget = field;
|
||||
this.transportMapKeyword = row[`${field}Address`] || row[`${field}Name`] || '';
|
||||
this.transportMapSelected = this.buildTransportMapSelection({
|
||||
lng: row[`${field}Longitude`],
|
||||
lat: row[`${field}Latitude`],
|
||||
address: row[`${field}Address`],
|
||||
regionName: row[`${field}Name`],
|
||||
regionCode: row[`${field}RegionCode`],
|
||||
});
|
||||
this.transportMapStatus = this.transportMapSelected.longitude
|
||||
? '已加载当前选点,可重新选点'
|
||||
: '可搜索地址或点击地图选点';
|
||||
this.transportMapBox = true;
|
||||
},
|
||||
async confirmWaybillRouteChange() {
|
||||
const row = this.waybillRouteChangeRows[0] || {};
|
||||
@@ -5438,6 +5385,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;
|
||||
@@ -5455,6 +5408,26 @@ export default {
|
||||
submitRow[key] = submitRow[key].trim();
|
||||
}
|
||||
});
|
||||
// 未选计划时清空 -1 占位,避免落库后编辑/查看回显 -1
|
||||
if (
|
||||
submitRow.planId === undefined ||
|
||||
submitRow.planId === null ||
|
||||
submitRow.planId === '' ||
|
||||
Number(submitRow.planId) === -1 ||
|
||||
Number(submitRow.planId) <= 0
|
||||
) {
|
||||
submitRow.planId = null;
|
||||
if (
|
||||
!submitRow.planName ||
|
||||
String(submitRow.planName) === '-1' ||
|
||||
Number(submitRow.planName) === -1
|
||||
) {
|
||||
submitRow.planName = null;
|
||||
}
|
||||
}
|
||||
if (String(submitRow.planName) === '-1' || Number(submitRow.planName) === -1) {
|
||||
submitRow.planName = null;
|
||||
}
|
||||
return submitRow;
|
||||
},
|
||||
validateMobileFields(fields = []) {
|
||||
@@ -5664,22 +5637,11 @@ export default {
|
||||
}
|
||||
this.getSaveRequest()(submitRow).then(
|
||||
res => {
|
||||
const data = res.data?.data || {};
|
||||
const id = data.id || submitRow.id;
|
||||
const after = id
|
||||
? submitMkApprovalFlow({
|
||||
bizType: 'waybill-manage',
|
||||
formInstanceId: id,
|
||||
subjectName: data.waybillNo || submitRow.waybillNo || '',
|
||||
approvalStatus: data.approvalStatus || submitRow.approvalStatus || '',
|
||||
})
|
||||
: Promise.resolve();
|
||||
return after.then(() => {
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '提交成功!' });
|
||||
done();
|
||||
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
|
||||
});
|
||||
this.markWaybillListNeedRefresh();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '提交成功!' });
|
||||
done();
|
||||
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
|
||||
},
|
||||
error => {
|
||||
window.console.log(error);
|
||||
@@ -5698,26 +5660,14 @@ export default {
|
||||
? this.api.updateAttachments
|
||||
: this.getSaveRequest();
|
||||
request(submitRow).then(
|
||||
res => {
|
||||
() => {
|
||||
const skipMk = this.attachmentUploadMode;
|
||||
const data = res.data?.data || {};
|
||||
const id = data.id || submitRow.id;
|
||||
const after =
|
||||
skipMk || !id
|
||||
? Promise.resolve()
|
||||
: submitMkApprovalFlow({
|
||||
bizType: 'waybill-manage',
|
||||
formInstanceId: id,
|
||||
subjectName: data.waybillNo || submitRow.waybillNo || '',
|
||||
approvalStatus: data.approvalStatus || submitRow.approvalStatus || '',
|
||||
});
|
||||
return after.then(() => {
|
||||
this.attachmentUploadMode = false;
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: skipMk ? '操作成功!' : '提交成功!' });
|
||||
done();
|
||||
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
|
||||
});
|
||||
this.attachmentUploadMode = false;
|
||||
this.markWaybillListNeedRefresh();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: skipMk ? '操作成功!' : '提交成功!' });
|
||||
done();
|
||||
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
|
||||
},
|
||||
error => {
|
||||
this.attachmentUploadMode = false;
|
||||
@@ -5729,6 +5679,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 +5692,7 @@ export default {
|
||||
request(submitRow)
|
||||
.then(() => {
|
||||
this.$message.success(`${this.config.draftSaveText || '保存'}成功`);
|
||||
this.markWaybillListNeedRefresh();
|
||||
this.onLoad(this.page, this.query);
|
||||
this.closeCrudDialog();
|
||||
})
|
||||
@@ -5748,8 +5702,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;
|
||||
@@ -6371,14 +6327,11 @@ export default {
|
||||
if (!rows.length) return false;
|
||||
const quantityUnits = rows.map(row => String(row.quantityUnit || '').trim());
|
||||
if (quantityUnits.some(unit => !unit) || new Set(quantityUnits).size !== 1) return false;
|
||||
const quantityUnit = quantityUnits[0];
|
||||
return rows.every(row => {
|
||||
const priceUnit = String(row.priceUnit || '').trim();
|
||||
const separatorIndex = Math.max(priceUnit.lastIndexOf('/'), priceUnit.lastIndexOf('/'));
|
||||
const priceQuantityUnit =
|
||||
separatorIndex >= 0 ? priceUnit.slice(separatorIndex + 1).trim() : priceUnit;
|
||||
return !this.isBillingFieldEmpty(row.unitPrice) && priceQuantityUnit === quantityUnit;
|
||||
});
|
||||
const expectedPriceUnit = this.resolvePriceUnitByQuantityUnit(quantityUnits[0]);
|
||||
if (!expectedPriceUnit) return false;
|
||||
return rows.every(
|
||||
row => !this.isBillingFieldEmpty(row.unitPrice) && String(row.priceUnit || '').trim() === expectedPriceUnit
|
||||
);
|
||||
},
|
||||
taskFullFreightQuantity(index) {
|
||||
return this.formatTaskFullFreightNumber(this.transportCargoRows[index]?.quantity || 0);
|
||||
@@ -6394,10 +6347,6 @@ export default {
|
||||
Number(cargo.unitPrice || 0) * Number(cargo.quantity || 0)
|
||||
);
|
||||
},
|
||||
taskFullFreightGroupQuantity(group = {}) {
|
||||
const total = (group.rows || []).reduce((sum, cargo) => sum + Number(cargo.quantity || 0), 0);
|
||||
return this.formatTaskFullFreightNumber(total);
|
||||
},
|
||||
taskFullFreightGroupUnitPrice(group = {}) {
|
||||
const prices = (group.rows || [])
|
||||
.map(cargo => String(cargo.unitPrice ?? '').trim())
|
||||
@@ -6406,7 +6355,11 @@ export default {
|
||||
return prices[0];
|
||||
},
|
||||
taskFullFreightGroupPriceUnit(group = {}) {
|
||||
return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : '');
|
||||
return (
|
||||
group.rows?.[0]?.priceUnit ||
|
||||
this.resolvePriceUnitByQuantityUnit(group.rows?.[0]?.quantityUnit) ||
|
||||
''
|
||||
);
|
||||
},
|
||||
taskFullFreightGroupAmount(group = {}) {
|
||||
const total = (group.rows || []).reduce(
|
||||
@@ -7137,6 +7090,11 @@ export default {
|
||||
requiredFields.push(['driverName', '司机']);
|
||||
requiredFields.push(['driverPhone', '手机号']);
|
||||
}
|
||||
if (!this.isTaskFullMode) {
|
||||
requiredFields.push(['cargoName', '货物名称']);
|
||||
requiredFields.push(['model', '型号']);
|
||||
requiredFields.push(['quantity', '数量']);
|
||||
}
|
||||
const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
|
||||
if (emptyField) {
|
||||
this.$message.warning(`请选择或输入${emptyField[1]}`);
|
||||
@@ -7171,7 +7129,7 @@ export default {
|
||||
}
|
||||
} else {
|
||||
const invalidLengthField = [
|
||||
['vehicleNo', '船/航/班列号', 30],
|
||||
['vehicleNo', '船班号/船次号/班次号', 30],
|
||||
['captainName', '船长', 20],
|
||||
['cabinNo', '舱位', 30],
|
||||
['containerNo', '箱号', 30],
|
||||
@@ -7429,6 +7387,24 @@ export default {
|
||||
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 3)}` : parts[0];
|
||||
this.syncTransportCargoJson();
|
||||
},
|
||||
resolvePriceUnitByQuantityUnit(quantityUnit) {
|
||||
return (
|
||||
{
|
||||
吨: '元/吨',
|
||||
方: '元/立方',
|
||||
件: '元/件',
|
||||
}[String(quantityUnit || '').trim()] || ''
|
||||
);
|
||||
},
|
||||
handleTransportCargoQuantityUnitChange(quantityUnit) {
|
||||
const priceUnit = this.resolvePriceUnitByQuantityUnit(quantityUnit);
|
||||
if (!priceUnit) return;
|
||||
(this.transportCargoRows || []).forEach(cargo => {
|
||||
cargo.priceUnit = priceUnit;
|
||||
});
|
||||
this.form.priceUnit = priceUnit;
|
||||
this.syncTransportCargoJson();
|
||||
},
|
||||
handleTransportCargoTypeChange(row, value) {
|
||||
const path = this.normalizeCargoTypePath(value);
|
||||
const cargoType = this.findCargoTypeByPath(path);
|
||||
@@ -7861,10 +7837,15 @@ export default {
|
||||
this.$refs.crud?.clearValidate?.([`${prefix}Address`]);
|
||||
});
|
||||
},
|
||||
closeTransportMapDialog() {
|
||||
this.transportMapBox = false;
|
||||
this.waybillRouteChangeMapTarget = null;
|
||||
},
|
||||
openTransportMapDialog(target) {
|
||||
if (this.dialogReadonly) return;
|
||||
if (!this.ensureTransportTypeBeforeAddress()) return;
|
||||
const prefix = target === 'departure' ? 'departure' : 'arrival';
|
||||
this.waybillRouteChangeMapTarget = null;
|
||||
this.transportMapTarget = prefix;
|
||||
this.transportMapKeyword = this.form[`${prefix}Address`] || this.form[`${prefix}Name`] || '';
|
||||
this.transportMapSelected = this.buildTransportMapSelection({
|
||||
@@ -7941,31 +7922,32 @@ export default {
|
||||
this.$message.warning('请输入地址关键词');
|
||||
return;
|
||||
}
|
||||
if (this.transportMapLoading) return;
|
||||
this.transportMapSearchResults = [];
|
||||
this.transportMapLoading = true;
|
||||
this.loadAmap()
|
||||
.then(() => {
|
||||
this.transportMapLoading = true;
|
||||
this.ensureTransportAmapGeocoder()
|
||||
.then(() => this.runAmapGeocode('location', keyword))
|
||||
.then(result => {
|
||||
this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
|
||||
const point = this.resolveMapPoint(result);
|
||||
if (!point) {
|
||||
this.transportMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
return;
|
||||
}
|
||||
this.pickTransportMapPoint(point, keyword);
|
||||
})
|
||||
.catch(() => {
|
||||
this.transportMapStatus = '地图搜索失败';
|
||||
this.$message.error('地图搜索失败,请稍后重试');
|
||||
})
|
||||
.finally(() => {
|
||||
this.transportMapLoading = false;
|
||||
});
|
||||
.then(() => searchAmapPlaceText(keyword))
|
||||
.then(pois => {
|
||||
this.transportMapSearchResults = normalizeMapSearchResults(pois, keyword);
|
||||
const first = this.transportMapSearchResults[0];
|
||||
if (!first?.location) {
|
||||
this.transportMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
return;
|
||||
}
|
||||
return this.$nextTick().then(() => {
|
||||
if (typeof this.transportAmap?.resize === 'function') {
|
||||
this.transportAmap.resize();
|
||||
}
|
||||
this.pickTransportMapPoint(first.location, first.address || first.name || keyword);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
|
||||
this.transportMapStatus = '未找到匹配地址';
|
||||
this.$message.warning('地图搜索无匹配地址');
|
||||
})
|
||||
.finally(() => {
|
||||
this.transportMapLoading = false;
|
||||
});
|
||||
},
|
||||
selectTransportMapSearchResult(item) {
|
||||
@@ -8023,6 +8005,23 @@ export default {
|
||||
this.$message.warning('请先搜索或点击地图完成选点');
|
||||
return;
|
||||
}
|
||||
if (this.waybillRouteChangeMapTarget) {
|
||||
const { row, field } = this.waybillRouteChangeMapTarget;
|
||||
row[`${field}Address`] =
|
||||
this.transportMapSelected.detailAddress || row[`${field}Address`] || '';
|
||||
if (this.transportMapSelected.regionName) {
|
||||
row[`${field}Name`] = this.transportMapSelected.regionName;
|
||||
}
|
||||
row[`${field}Longitude`] = this.transportMapSelected.longitude;
|
||||
row[`${field}Latitude`] = this.transportMapSelected.latitude;
|
||||
if (this.transportMapSelected.regionCode) {
|
||||
row[`${field}RegionCode`] = this.transportMapSelected.regionCode;
|
||||
}
|
||||
this.updateWaybillRouteChangeNodes();
|
||||
this.waybillRouteChangeMapTarget = null;
|
||||
this.transportMapBox = false;
|
||||
return;
|
||||
}
|
||||
const prefix = this.transportMapTarget;
|
||||
this.form[`${prefix}Address`] =
|
||||
this.transportMapSelected.detailAddress || this.form[`${prefix}Address`];
|
||||
@@ -9750,10 +9749,16 @@ export default {
|
||||
}
|
||||
|
||||
&--road {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
column-gap: 56px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
column-gap: 24px;
|
||||
row-gap: 16px;
|
||||
|
||||
:deep(.el-form-item__content > .el-input),
|
||||
:deep(.el-form-item__content > .el-select) {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:deep(.waybill-manage-page__append-select) {
|
||||
width: 112px;
|
||||
}
|
||||
@@ -10107,6 +10112,10 @@ export default {
|
||||
font-weight: 500;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
&.is-link[role='link'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__waybill-route {
|
||||
|
||||
@@ -678,7 +678,7 @@ export default {
|
||||
removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); },
|
||||
validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; },
|
||||
validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; },
|
||||
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', formInstanceId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected' }); this.$message.success('变更已提交'); this.$router.back(); },
|
||||
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', bizId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected' }); this.$message.success('变更已提交'); this.$router.back(); },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1017,7 +1017,7 @@ import { getDeptTree } from '@/api/system/dept';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||
import { config, option } from '@/option/business/contract-manage';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import BillingPlanEditor from './components/billing-plan-editor.vue';
|
||||
@@ -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: {
|
||||
@@ -1304,24 +1312,19 @@ export default {
|
||||
return { addBtn: this.canCreate };
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo?.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
canCreate() {
|
||||
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 +1487,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 +1513,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 +1524,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,
|
||||
@@ -1579,7 +1607,7 @@ export default {
|
||||
});
|
||||
},
|
||||
normalizeSearch(params = {}) {
|
||||
const query = { ...params };
|
||||
const query = { ...params, allDept: this.isAdmin ? 1 : 0 };
|
||||
if (query.organizationName) {
|
||||
const id = Array.isArray(query.organizationName)
|
||||
? query.organizationName.at(-1)
|
||||
@@ -1648,7 +1676,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 +1769,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') {
|
||||
@@ -1840,6 +1869,7 @@ export default {
|
||||
submitCreate(action) {
|
||||
if (this.submitAction || !this.validateBase(action)) return;
|
||||
this.submitAction = action;
|
||||
const previousApprovalStatus = this.form.approvalStatus || '';
|
||||
const submit = {
|
||||
...this.normalizeSubmit(),
|
||||
contractStage: action === 'formal' ? 'temporary' : 'draft',
|
||||
@@ -1858,9 +1888,9 @@ export default {
|
||||
).then(() =>
|
||||
submitMkApprovalFlow({
|
||||
bizType: 'contract-manage',
|
||||
formInstanceId: id,
|
||||
bizId: id,
|
||||
subjectName: saved.contractName || submit.contractName || '',
|
||||
approvalStatus: saved.approvalStatus || submit.approvalStatus || '',
|
||||
approvalStatus: previousApprovalStatus || saved.approvalStatus || submit.approvalStatus || '',
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1896,7 +1926,7 @@ export default {
|
||||
}
|
||||
return submitMkApprovalFlow({
|
||||
bizType: 'contract-manage',
|
||||
formInstanceId: payload.id || this.form.id,
|
||||
bizId: payload.id || this.form.id,
|
||||
subjectName: payload.contractName || this.form.contractName || '',
|
||||
approvalStatus: this.form.approvalStatus || 'change_rejected',
|
||||
}).then(() => {
|
||||
@@ -2532,8 +2562,9 @@ export default {
|
||||
});
|
||||
},
|
||||
closeDetail() {
|
||||
this.markContractManageListNeedRefresh();
|
||||
this.$router.$avueRouter?.closeTag?.();
|
||||
this.$router.push('/business/contract-manage');
|
||||
this.$router.push(contractManageListRoute);
|
||||
},
|
||||
applyDetailState(detail = {}) {
|
||||
this.detailRow = {
|
||||
@@ -2842,7 +2873,7 @@ export default {
|
||||
? request().then(() =>
|
||||
submitMkApprovalFlow({
|
||||
bizType: 'contract-manage',
|
||||
formInstanceId: row.id,
|
||||
bizId: row.id,
|
||||
subjectName: row.contractName || '',
|
||||
approvalStatus: row.approvalStatus || '',
|
||||
})
|
||||
@@ -2876,7 +2907,7 @@ export default {
|
||||
},
|
||||
handleCopy(row) {
|
||||
this.$router.push({
|
||||
path: '/business/contract-manage/form',
|
||||
path: contractManageFormRoute,
|
||||
query: { mode: 'add', copyId: row.id, name: '新增合同管理' },
|
||||
});
|
||||
},
|
||||
|
||||
+1047
-232
File diff suppressed because it is too large
Load Diff
@@ -70,6 +70,8 @@
|
||||
<p class="project-name">{{ row.projectName || '-' }}</p>
|
||||
<p>{{ timeRange(row) }}</p>
|
||||
<p>{{ goodsSummary(row) }}</p>
|
||||
<p>创建人:{{ row.createUserName || '-' }}</p>
|
||||
<p>创建时间:{{ row.createTime || '-' }}</p>
|
||||
</section>
|
||||
<section class="route-progress">
|
||||
<div class="route-track">
|
||||
|
||||
@@ -416,7 +416,7 @@ import { getList as getProjectList } from '@/api/business/project-apply';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { config, option } from '@/option/business/process-config';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { mapGetters } from 'vuex';
|
||||
import NProgress from 'nprogress';
|
||||
@@ -524,10 +524,7 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
ids() {
|
||||
return this.selectionList.map(item => item.id).join(',');
|
||||
@@ -696,7 +693,7 @@ export default {
|
||||
return getProjectList(1, 50, {
|
||||
projectName: keyword,
|
||||
approvalStatus: 'approved',
|
||||
allDept: 0,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
})
|
||||
.then(res => {
|
||||
const data = res.data.data || {};
|
||||
|
||||
@@ -911,9 +911,10 @@ import { getDeptTree, getPlatformCompanySelect } from '@/api/system/dept';
|
||||
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
|
||||
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
|
||||
import { getList as getUserList } from '@/api/system/user';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
import PdfPreview from '@/components/pdf-preview/main.vue';
|
||||
@@ -1008,6 +1009,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 = [
|
||||
@@ -1113,6 +1118,7 @@ export default {
|
||||
// 否则 isProjectFormPage 变 false 后容器会从 div 切成 el-dialog(append-to-body),
|
||||
// 盖住后续打开的页面(如客商档案)。
|
||||
isFormPageInstance: false,
|
||||
mkEnabled: false,
|
||||
projectBox: false,
|
||||
dialogType: 'add',
|
||||
dialogReadonly: false,
|
||||
@@ -1280,10 +1286,7 @@ export default {
|
||||
};
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo && this.userInfo.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
canViewAllDept() {
|
||||
return this.isAdmin;
|
||||
@@ -1358,16 +1361,26 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.isAdmin) {
|
||||
this.allDept = 1;
|
||||
}
|
||||
if (this.isPublicViewPage) {
|
||||
this.isFormPageInstance = true;
|
||||
this.openPublicProjectForm();
|
||||
return;
|
||||
}
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = !!enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
this.loadDeptOptions();
|
||||
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 +1390,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 +1541,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;
|
||||
}
|
||||
@@ -1535,7 +1572,11 @@ export default {
|
||||
);
|
||||
},
|
||||
customOperations(row) {
|
||||
return (this.config.operations || []).filter(operation => this.canOperation(operation, row));
|
||||
return (this.config.operations || []).filter(operation => {
|
||||
if (operation.mkOnly && !this.mkEnabled) return false;
|
||||
if (operation.localOnly && this.mkEnabled) return false;
|
||||
return this.canOperation(operation, row);
|
||||
});
|
||||
},
|
||||
canOperation(operation, row) {
|
||||
const permission = operation.permission || `${this.config.permission}_${operation.action}`;
|
||||
@@ -1661,6 +1702,10 @@ export default {
|
||||
this.openProjectDialog('change', row);
|
||||
return;
|
||||
}
|
||||
if (operation.action === 'mkAudit') {
|
||||
this.handleMkAudit(row);
|
||||
return;
|
||||
}
|
||||
const run = value => {
|
||||
const args = operation.prompt ? [row.id, value] : [row.id];
|
||||
const request = () => this.api[operation.action](...args);
|
||||
@@ -1668,7 +1713,7 @@ export default {
|
||||
operation.action === 'submitApproval'
|
||||
? submitMkApprovalFlow({
|
||||
bizType: 'project-apply',
|
||||
formInstanceId: row.id,
|
||||
bizId: row.id,
|
||||
subjectName: row.projectName || '',
|
||||
approvalStatus: row.approvalStatus || '',
|
||||
}).then(() => request())
|
||||
@@ -1697,6 +1742,24 @@ export default {
|
||||
type: 'warning',
|
||||
}).then(() => run());
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('项目ID为空,无法打开审核页');
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
.then(res => {
|
||||
const url = res?.data?.data;
|
||||
if (!url) {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
});
|
||||
},
|
||||
buildExportParams() {
|
||||
return {
|
||||
...this.query,
|
||||
@@ -1736,7 +1799,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();
|
||||
@@ -1844,7 +1907,8 @@ export default {
|
||||
const data = res.data?.data || {};
|
||||
const id = data.id || this.form.id;
|
||||
const name = data.projectName || this.form.projectName || '';
|
||||
const status = data.approvalStatus || this.form.approvalStatus || '';
|
||||
// 必须用提交前状态:业务 submit 后可能已变成 reviewing,导致无法识别「通过后再提交」
|
||||
const status = this.form.approvalStatus || data.approvalStatus || '';
|
||||
if (!id) {
|
||||
this.$message.success('操作成功!');
|
||||
this.closeProjectForm();
|
||||
@@ -1853,7 +1917,7 @@ export default {
|
||||
}
|
||||
return submitMkApprovalFlow({
|
||||
bizType: 'project-apply',
|
||||
formInstanceId: id,
|
||||
bizId: id,
|
||||
subjectName: name,
|
||||
approvalStatus: status,
|
||||
}).then(() => {
|
||||
@@ -1924,7 +1988,7 @@ export default {
|
||||
const id = this.form.id;
|
||||
return submitMkApprovalFlow({
|
||||
bizType: 'project-apply',
|
||||
formInstanceId: id,
|
||||
bizId: id,
|
||||
subjectName: this.form.projectName || '',
|
||||
approvalStatus: this.form.approvalStatus || 'change_rejected',
|
||||
}).then(() => {
|
||||
|
||||
@@ -309,7 +309,7 @@ import {
|
||||
getList as getProjectList,
|
||||
} from '@/api/business/project-apply';
|
||||
import { config, option } from '@/option/business/temporary-credit-limit';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { ElImageViewer } from 'element-plus';
|
||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||
@@ -423,10 +423,7 @@ export default {
|
||||
return String(width);
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo?.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
canCreate() {
|
||||
return this.hasPermission(`${this.config.permission}_add`);
|
||||
@@ -537,7 +534,11 @@ export default {
|
||||
onLoad(page, params = {}) {
|
||||
this.loading = true;
|
||||
this.api
|
||||
.getList(page.currentPage, page.pageSize, { ...params, ...this.query })
|
||||
.getList(page.currentPage, page.pageSize, {
|
||||
...params,
|
||||
...this.query,
|
||||
allDept: this.isAdmin ? 1 : 0,
|
||||
})
|
||||
.then(res => {
|
||||
const result = res.data?.data || {};
|
||||
this.page.total = result.total || 0;
|
||||
@@ -942,7 +943,7 @@ export default {
|
||||
NProgress.start();
|
||||
exportBlob(
|
||||
this.config.exportUrl,
|
||||
{ ...this.query, ids: this.ids, [this.website.tokenHeader]: getToken() },
|
||||
{ ...this.query, allDept: this.isAdmin ? 1 : 0, ids: this.ids, [this.website.tokenHeader]: getToken() },
|
||||
{ feedback: true }
|
||||
)
|
||||
.then(res => {
|
||||
|
||||
@@ -45,6 +45,7 @@ import { useStore } from 'vuex';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import md5 from 'js-md5';
|
||||
import * as api from '@/api/business/voucher-manage';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -52,9 +53,7 @@ const store = useStore();
|
||||
const voucherId = route.query.id;
|
||||
const isSuperAdmin = computed(() => {
|
||||
const authority = store.getters.userInfo?.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator({ authority });
|
||||
});
|
||||
const loading = ref(false), rows = ref([]), voucher = ref({ voucherBatchNo: route.query.batchNo || '' });
|
||||
const page = reactive({ current: 1, size: 10, total: 0 });
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
prop="relatedWaybillCount"
|
||||
label="凭证已关联数量"
|
||||
width="125"
|
||||
/><el-table-column prop="unRelatedWaybillCount" label="凭证未关联数量" width="125" />
|
||||
/> <el-table-column prop="unRelatedWaybillCount" label="凭证未关联数量" width="125" />
|
||||
<el-table-column prop="createUserName" label="创建人" min-width="120" />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" />
|
||||
<el-table-column
|
||||
prop="auditStatus"
|
||||
@@ -504,6 +505,7 @@
|
||||
import { computed, reactive, ref, watch, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import { isAdministrator as checkAdministrator } from '@/utils/auth';
|
||||
import md5 from 'js-md5';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { WarnTriangleFilled } from '@element-plus/icons-vue';
|
||||
@@ -517,7 +519,7 @@ const store = useStore();
|
||||
// 权限判断
|
||||
const permission = computed(() => store.getters.permission);
|
||||
const userInfo = computed(() => store.getters.userInfo);
|
||||
const isAdmin = computed(() => userInfo.value?.authority?.includes('admin'));
|
||||
const isAdmin = computed(() => checkAdministrator(userInfo.value));
|
||||
const hasPermission = code => {
|
||||
if (isAdmin.value) return true;
|
||||
return permission.value?.[code] === true;
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<template>
|
||||
<basic-container class="waybill-import-form-page-container">
|
||||
<div class="waybill-import-form-page-title">新建导入</div>
|
||||
<div class="waybill-import-form-page-title">{{ pageTitle }}</div>
|
||||
<waybill-import-dialog create-page />
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import WaybillImportDialog from './components/waybill-import-dialog.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const pageTitle = computed(() => (route.query.id ? '编辑导入' : '新建导入'));
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<template>
|
||||
<mk-public-shell biz-type="waybill-manage" :get-form="getForm">
|
||||
<waybill-manage-page
|
||||
ref="page"
|
||||
:api="api"
|
||||
:config="config"
|
||||
:crud-option="option"
|
||||
:detail-id="detailId"
|
||||
standalone-detail-page
|
||||
/>
|
||||
</mk-public-shell>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
|
||||
import WaybillManagePage from '@/views/business/components/waybill-manage-page.vue';
|
||||
import * as api from '@/api/business/waybill-manage';
|
||||
import { config, option } from '@/option/business/waybill-manage';
|
||||
|
||||
export default {
|
||||
name: 'WaybillManagePublicView',
|
||||
components: { MkPublicShell, WaybillManagePage },
|
||||
data() {
|
||||
return { api, config, option };
|
||||
},
|
||||
computed: {
|
||||
detailId() {
|
||||
return this.$route.query.id || '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getForm() {
|
||||
const row = this.$refs.page?.detailRow || {};
|
||||
return { ...row, subject: row.waybillNo || '' };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -42,17 +42,6 @@ const FIELD_MAP = {
|
||||
{ label: '乙方', prop: 'partyB' },
|
||||
{ label: '项目名称', prop: 'projectName', span: 2 },
|
||||
],
|
||||
'waybill-manage': [
|
||||
{ label: '运单号', prop: 'waybillNo' },
|
||||
{ label: '项目', prop: 'projectName' },
|
||||
{ label: '客户合同', prop: 'contractName' },
|
||||
{ label: '客户名称', prop: 'customerName' },
|
||||
{ label: '货物名称', prop: 'cargoName' },
|
||||
{ label: '承运商', prop: 'carrierName' },
|
||||
{ label: '当前过程节点', prop: 'currentProcessNode' },
|
||||
{ label: '发货地', prop: 'departureName' },
|
||||
{ label: '收货地', prop: 'arrivalName' },
|
||||
],
|
||||
'pre-settlement': [
|
||||
{ label: '预结算单号', prop: 'preSettlementNo' },
|
||||
{ label: '合同名称', prop: 'contractName' },
|
||||
@@ -88,7 +77,6 @@ const FIELD_MAP = {
|
||||
const TITLE_MAP = {
|
||||
'project-apply': '查看项目信息',
|
||||
'contract-manage': '查看合同信息',
|
||||
'waybill-manage': '查看运单信息',
|
||||
'pre-settlement': '查看预结算信息',
|
||||
'formal-settlement': '查看正式结算信息',
|
||||
'payment-application': '查看付款申请信息',
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" :max-size="10" />
|
||||
</section-card>
|
||||
|
||||
<div class="bill-payment-form-page__actions">
|
||||
<div v-if="!isPublicViewPage" class="bill-payment-form-page__actions">
|
||||
<template v-if="!readonly">
|
||||
<el-button type="primary" :loading="saving" :disabled="submitting" @click="saveDraft">
|
||||
保存
|
||||
@@ -139,6 +139,8 @@ import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/payment/billPayment';
|
||||
import * as billLedgerApi from '@/api/payment/billLedger';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { getMkPublicDetail } from '@/api/mk-process';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
|
||||
const emptyForm = () => ({
|
||||
id: null,
|
||||
@@ -194,7 +196,10 @@ export default {
|
||||
return this.$route.query.id || '';
|
||||
},
|
||||
readonly() {
|
||||
return this.$route.query.mode === 'view';
|
||||
return this.$route.query.mode === 'view' || this.isPublicViewPage;
|
||||
},
|
||||
isPublicViewPage() {
|
||||
return this.$route.path === '/payment/bill-payment/public-view';
|
||||
},
|
||||
},
|
||||
created() {
|
||||
@@ -215,6 +220,10 @@ export default {
|
||||
}
|
||||
},
|
||||
async initialize() {
|
||||
if (this.isPublicViewPage) {
|
||||
await this.loadPublicDetail();
|
||||
return;
|
||||
}
|
||||
if (this.recordId) {
|
||||
const data = this.unwrapData(await api.getDetail(this.recordId));
|
||||
this.form = {
|
||||
@@ -228,6 +237,15 @@ export default {
|
||||
this.form.deptName = this.userInfo?.deptName || this.userInfo?.dept_name || '';
|
||||
this.form.paymentDate = this.$dayjs().format('YYYY-MM-DD');
|
||||
},
|
||||
async loadPublicDetail() {
|
||||
if (!this.recordId) return;
|
||||
const data = this.unwrapData(await getMkPublicDetail('bill-payment', this.recordId));
|
||||
this.form = {
|
||||
...emptyForm(),
|
||||
...data,
|
||||
attachments: this.parse(data.attachmentsJson),
|
||||
};
|
||||
},
|
||||
validateUsedAmount(rule, value, callback) {
|
||||
const amount = Number(value);
|
||||
const balance = Number(this.form.availableBalance || 0);
|
||||
@@ -323,7 +341,14 @@ export default {
|
||||
try {
|
||||
const id = this.unwrapData(await api.save(this.payload()));
|
||||
this.form.id = id;
|
||||
const previousApprovalStatus = this.form.approvalStatus || '';
|
||||
await api.submit({ id: this.form.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'bill-payment',
|
||||
bizId: this.form.id,
|
||||
subjectName: this.form.paymentNo || this.form.billNo || '',
|
||||
approvalStatus: previousApprovalStatus,
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.goBack();
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<mk-public-shell biz-type="bill-payment" :get-form="getForm">
|
||||
<bill-payment-form ref="page" />
|
||||
</mk-public-shell>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
|
||||
import BillPaymentForm from '@/views/payment/bill-payment-form.vue';
|
||||
|
||||
export default {
|
||||
name: 'BillPaymentPublicView',
|
||||
components: { MkPublicShell, BillPaymentForm },
|
||||
methods: {
|
||||
getForm() {
|
||||
const form = this.$refs.page?.form || {};
|
||||
return { ...form, subject: form.paymentNo || form.billNo || '' };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -10,38 +10,26 @@
|
||||
:rows="rows"
|
||||
:loading="loading"
|
||||
:page="page"
|
||||
:mk-enabled="mkEnabled"
|
||||
@add="openCreate"
|
||||
@view="openView"
|
||||
@edit="openEdit"
|
||||
@delete="handleDelete"
|
||||
@flow="openFlow"
|
||||
@submit="handleSubmit"
|
||||
@mk-audit="handleMkAudit"
|
||||
@approve="handleApprove"
|
||||
@return="handleReturn"
|
||||
@void="handleVoid"
|
||||
@page-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
<el-dialog v-model="flowDialog.visible" title="审批流程" width="620px" append-to-body>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="单据号">
|
||||
{{ flowDialog.row.paymentNo || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="单据状态">
|
||||
{{ flowDialog.row.approvalStatusName || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前节点">
|
||||
{{ flowDialog.row.currentNode || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前处理人">
|
||||
{{ flowDialog.row.currentProcessor || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as api from '@/api/payment/billPayment';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import BillPaymentSearch from './components/bill-payment-search.vue';
|
||||
import BillPaymentTable from './components/bill-payment-table.vue';
|
||||
|
||||
@@ -62,11 +50,18 @@ export default {
|
||||
rows: [],
|
||||
loading: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
flowDialog: { visible: false, row: {} },
|
||||
mkEnabled: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadTable();
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = !!enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
unwrapData(response) {
|
||||
@@ -125,9 +120,35 @@ export default {
|
||||
if (this.rows.length === 1 && this.page.current > 1) this.page.current -= 1;
|
||||
this.loadTable();
|
||||
},
|
||||
openFlow(row) {
|
||||
this.flowDialog.row = row;
|
||||
this.flowDialog.visible = true;
|
||||
async handleSubmit(row) {
|
||||
const previousApprovalStatus = row.approvalStatus || '';
|
||||
await api.submit({ id: row.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'bill-payment',
|
||||
bizId: row.id,
|
||||
subjectName: row.paymentNo || row.billNo || '',
|
||||
approvalStatus: previousApprovalStatus,
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.loadTable();
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('汇票付款ID为空,无法打开审核页');
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
.then(res => {
|
||||
const url = res?.data?.data;
|
||||
if (!url) {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
});
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await api.approve({ id: row.id });
|
||||
|
||||
@@ -50,19 +50,40 @@
|
||||
>编辑</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="row.approvalStatus !== 'draft'"
|
||||
v-if="
|
||||
['draft', 'returned'].includes(row.approvalStatus) &&
|
||||
hasPermission('bill_payment_submit')
|
||||
"
|
||||
type="primary"
|
||||
@click="$emit('flow', row)"
|
||||
@click="$emit('submit', row)"
|
||||
>提交</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
mkEnabled &&
|
||||
row.approvalStatus === 'reviewing' &&
|
||||
hasPermission('bill_payment_approve')
|
||||
"
|
||||
type="primary"
|
||||
@click="$emit('mk-audit', row)"
|
||||
>流程</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="row.approvalStatus === 'reviewing' && hasPermission('bill_payment_approve')"
|
||||
v-if="
|
||||
!mkEnabled &&
|
||||
row.approvalStatus === 'reviewing' &&
|
||||
hasPermission('bill_payment_approve')
|
||||
"
|
||||
type="primary"
|
||||
@click="$emit('approve', row)"
|
||||
>通过</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="row.approvalStatus === 'reviewing' && hasPermission('bill_payment_approve')"
|
||||
v-if="
|
||||
!mkEnabled &&
|
||||
row.approvalStatus === 'reviewing' &&
|
||||
hasPermission('bill_payment_approve')
|
||||
"
|
||||
type="danger"
|
||||
@click="$emit('return', row)"
|
||||
>驳回</el-link
|
||||
@@ -94,6 +115,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { billPaymentTableColumns } from '@/option/payment/billPayment';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
export default {
|
||||
name: 'BillPaymentTable',
|
||||
@@ -101,13 +123,15 @@ export default {
|
||||
rows: { type: Array, default: () => [] },
|
||||
loading: { type: Boolean, default: false },
|
||||
page: { type: Object, required: true },
|
||||
mkEnabled: { type: Boolean, default: false },
|
||||
},
|
||||
emits: [
|
||||
'add',
|
||||
'view',
|
||||
'edit',
|
||||
'delete',
|
||||
'flow',
|
||||
'submit',
|
||||
'mk-audit',
|
||||
'approve',
|
||||
'return',
|
||||
'void',
|
||||
@@ -120,7 +144,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
return String(this.userInfo?.authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -405,7 +405,7 @@
|
||||
</el-form-item>
|
||||
</section-card>
|
||||
|
||||
<div class="invoice-form-page__actions">
|
||||
<div v-if="!isPublicViewPage" class="invoice-form-page__actions">
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button v-if="!readonly" type="primary" plain @click="syncReferenceData">同步</el-button>
|
||||
<el-button v-if="!readonly && canSave" type="primary" plain @click="saveDraft"
|
||||
@@ -491,6 +491,8 @@ import { getList as getReceivablePayableDetailList } from '@/api/settlement/rece
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { invoiceApplicationDetailColumns } from '@/option/payment/invoiceApplication';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { getMkPublicDetail } from '@/api/mk-process';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
|
||||
let localId = 0;
|
||||
const emptyLine = () => ({
|
||||
@@ -584,7 +586,10 @@ export default {
|
||||
return this.$route.query.id || '';
|
||||
},
|
||||
readonly() {
|
||||
return this.$route.query.mode === 'view';
|
||||
return this.$route.query.mode === 'view' || this.isPublicViewPage;
|
||||
},
|
||||
isPublicViewPage() {
|
||||
return this.$route.path === '/payment/invoice-application/public-view';
|
||||
},
|
||||
canSave() {
|
||||
const code = this.recordId ? 'invoice_application_edit' : 'invoice_application_add';
|
||||
@@ -656,6 +661,10 @@ export default {
|
||||
.filter(Boolean);
|
||||
},
|
||||
async initialize() {
|
||||
if (this.isPublicViewPage) {
|
||||
await this.loadPublicDetail();
|
||||
return;
|
||||
}
|
||||
await Promise.all([this.loadInvoiceItems(), this.loadTransportTypeOptions()]);
|
||||
const userInfo = this.$store.getters.userInfo || {};
|
||||
if (!this.recordId) {
|
||||
@@ -663,6 +672,21 @@ export default {
|
||||
return;
|
||||
}
|
||||
const data = this.unwrapData(await api.getDetail(this.recordId));
|
||||
this.applyDetail(data);
|
||||
if (this.settlementIds.length) {
|
||||
await Promise.all([this.loadReceiverInformation(), this.loadSettlementDetails(false)]);
|
||||
}
|
||||
if (this.readonly)
|
||||
this.$nextTick(() => this.$refs.formRef?.clearValidate('departmentEmails'));
|
||||
this.$nextTick(this.restoreDetailSelection);
|
||||
},
|
||||
async loadPublicDetail() {
|
||||
if (!this.recordId) return;
|
||||
const data = this.unwrapData(await getMkPublicDetail('invoice-application', this.recordId));
|
||||
this.applyDetail(data);
|
||||
this.$nextTick(() => this.$refs.formRef?.clearValidate('departmentEmails'));
|
||||
},
|
||||
applyDetail(data) {
|
||||
this.form = {
|
||||
...emptyForm(),
|
||||
...data,
|
||||
@@ -683,12 +707,6 @@ export default {
|
||||
sheet.lines.forEach(line => this.syncInvoiceItemLine(line, false))
|
||||
);
|
||||
this.selectedDetailIds = (data.details || []).map(item => item.formalSettlementDetailId);
|
||||
if (this.settlementIds.length) {
|
||||
await Promise.all([this.loadReceiverInformation(), this.loadSettlementDetails(false)]);
|
||||
}
|
||||
if (this.readonly)
|
||||
this.$nextTick(() => this.$refs.formRef?.clearValidate('departmentEmails'));
|
||||
this.$nextTick(this.restoreDetailSelection);
|
||||
},
|
||||
async loadInvoiceItems() {
|
||||
const data = this.unwrapData(await getInvoiceItems());
|
||||
@@ -1183,7 +1201,14 @@ export default {
|
||||
}
|
||||
const saved = await this.saveDraft();
|
||||
if (!saved) return;
|
||||
const previousApprovalStatus = this.form.approvalStatus || '';
|
||||
await api.submit({ id: this.form.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'invoice-application',
|
||||
bizId: this.form.id,
|
||||
subjectName: this.form.applicationNo || '',
|
||||
approvalStatus: previousApprovalStatus,
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.goBack();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<mk-public-shell biz-type="invoice-application" :get-form="getForm">
|
||||
<invoice-application-form ref="page" />
|
||||
</mk-public-shell>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
|
||||
import InvoiceApplicationForm from '@/views/payment/invoice-application-form.vue';
|
||||
|
||||
export default {
|
||||
name: 'InvoiceApplicationPublicView',
|
||||
components: { MkPublicShell, InvoiceApplicationForm },
|
||||
methods: {
|
||||
getForm() {
|
||||
const form = this.$refs.page?.form || {};
|
||||
return { ...form, subject: form.applicationNo || form.projectName || '' };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -152,14 +152,20 @@
|
||||
>提交</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('invoice_application_view') && row.approvalStatus !== 'draft'"
|
||||
v-if="
|
||||
mkEnabled &&
|
||||
hasPermission('invoice_application_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="openFlow(row)"
|
||||
@click="handleMkAudit(row)"
|
||||
>流程</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('invoice_application_approve') && row.approvalStatus === 'reviewing'
|
||||
!mkEnabled &&
|
||||
hasPermission('invoice_application_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleApprove(row)"
|
||||
@@ -167,7 +173,9 @@
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('invoice_application_approve') && row.approvalStatus === 'reviewing'
|
||||
!mkEnabled &&
|
||||
hasPermission('invoice_application_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleReturn(row)"
|
||||
@@ -198,42 +206,6 @@
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="flowDialog.visible" title="审批流程" width="900px" append-to-body>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="开票申请单号">
|
||||
{{ flowDialog.row.applicationNo || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
{{ flowDialog.row.approvalStatusName || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前节点">
|
||||
{{ flowDialog.row.currentNode || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前处理人">
|
||||
{{ flowDialog.row.currentProcessor || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table
|
||||
v-loading="flowDialog.loading"
|
||||
:data="flowDialog.records"
|
||||
border
|
||||
class="invoice-page__flow-table"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column prop="actionName" label="操作" min-width="110" align="center" />
|
||||
<el-table-column label="原状态" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ statusName(row.fromStatus) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="新状态" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ statusName(row.toStatus) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operatorName" label="操作人" min-width="110" align="center" />
|
||||
<el-table-column prop="createTime" label="操作时间" min-width="170" align="center" />
|
||||
<el-table-column prop="reason" label="原因" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" />
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -245,6 +217,8 @@ import * as api from '@/api/payment/invoiceApplication';
|
||||
import * as formalSettlementApi from '@/api/settlement/formalSettlement';
|
||||
import { invoiceApplicationTableColumns } from '@/option/payment/invoiceApplication';
|
||||
import organizationSearch from '@/mixins/organization-search';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
applicationNo: '',
|
||||
@@ -273,7 +247,7 @@ export default {
|
||||
loading: false,
|
||||
batchSyncing: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
flowDialog: { visible: false, loading: false, row: {}, records: [] },
|
||||
mkEnabled: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -282,6 +256,13 @@ export default {
|
||||
mounted() {
|
||||
this.loadOrganizationOptions();
|
||||
this.loadTable();
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = !!enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
@@ -358,19 +339,6 @@ export default {
|
||||
this.$message.error('正式结算单详情加载失败');
|
||||
}
|
||||
},
|
||||
async openFlow(row) {
|
||||
this.flowDialog.row = { ...row };
|
||||
this.flowDialog.records = [];
|
||||
this.flowDialog.visible = true;
|
||||
this.flowDialog.loading = true;
|
||||
try {
|
||||
const detail = this.unwrapData(await api.getDetail(row.id));
|
||||
this.flowDialog.row = { ...row, ...detail };
|
||||
this.flowDialog.records = detail.records || [];
|
||||
} finally {
|
||||
this.flowDialog.loading = false;
|
||||
}
|
||||
},
|
||||
async handleDelete(row) {
|
||||
await this.$confirm('确认删除该开票申请?', '提示', { type: 'warning' });
|
||||
await api.remove(row.id);
|
||||
@@ -378,10 +346,35 @@ export default {
|
||||
this.loadTable();
|
||||
},
|
||||
async handleSubmit(row) {
|
||||
const previousApprovalStatus = row.approvalStatus || '';
|
||||
await api.submit({ id: row.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'invoice-application',
|
||||
bizId: row.id,
|
||||
subjectName: row.applicationNo || '',
|
||||
approvalStatus: previousApprovalStatus,
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.loadTable();
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('开票申请ID为空,无法打开审核页');
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
.then(res => {
|
||||
const url = res?.data?.data;
|
||||
if (!url) {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
});
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('确认审批通过该开票申请?', '提示', { type: 'warning' });
|
||||
await api.approve({ id: row.id });
|
||||
@@ -455,19 +448,6 @@ export default {
|
||||
''
|
||||
);
|
||||
},
|
||||
statusName(status) {
|
||||
return (
|
||||
{
|
||||
draft: '草稿',
|
||||
reviewing: '审批中',
|
||||
approved: '审批通过',
|
||||
returned: '已驳回',
|
||||
voided: '已作废',
|
||||
}[status] ||
|
||||
status ||
|
||||
'-'
|
||||
);
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
</el-form-item>
|
||||
</section-card>
|
||||
|
||||
<div class="receipt-form-page__actions">
|
||||
<div v-if="!isPublicViewPage" class="receipt-form-page__actions">
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button v-if="!readonly" type="primary" plain @click="syncReferenceData">同步</el-button>
|
||||
<el-button v-if="!readonly && canSave" type="primary" plain @click="saveDraft"
|
||||
@@ -212,6 +212,8 @@ import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/payment/invoiceReceipt';
|
||||
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { getMkPublicDetail } from '@/api/mk-process';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
|
||||
const emptyForm = () => ({
|
||||
id: null,
|
||||
@@ -274,7 +276,10 @@ export default {
|
||||
return this.$route.query.id || '';
|
||||
},
|
||||
readonly() {
|
||||
return this.$route.query.mode === 'view';
|
||||
return this.$route.query.mode === 'view' || this.isPublicViewPage;
|
||||
},
|
||||
isPublicViewPage() {
|
||||
return this.$route.path === '/payment/invoice-receipt/public-view';
|
||||
},
|
||||
canSave() {
|
||||
const code = this.recordId ? 'invoice_receipt_edit' : 'invoice_receipt_add';
|
||||
@@ -323,6 +328,10 @@ export default {
|
||||
.filter(Boolean);
|
||||
},
|
||||
async initialize() {
|
||||
if (this.isPublicViewPage) {
|
||||
await this.loadPublicDetail();
|
||||
return;
|
||||
}
|
||||
const userInfo = this.$store.getters.userInfo || {};
|
||||
const userEmail = userInfo.email || '';
|
||||
this.departmentEmailOptions = userEmail ? [userEmail] : [];
|
||||
@@ -331,6 +340,15 @@ export default {
|
||||
return;
|
||||
}
|
||||
const data = this.unwrapData(await api.getDetail(this.recordId));
|
||||
this.applyDetail(data);
|
||||
if (this.settlementIds.length) await this.loadReferenceInformation();
|
||||
},
|
||||
async loadPublicDetail() {
|
||||
if (!this.recordId) return;
|
||||
const data = this.unwrapData(await getMkPublicDetail('invoice-receipt', this.recordId));
|
||||
this.applyDetail(data);
|
||||
},
|
||||
applyDetail(data) {
|
||||
this.form = {
|
||||
...emptyForm(),
|
||||
...data,
|
||||
@@ -342,7 +360,6 @@ export default {
|
||||
})),
|
||||
attachments: this.parse(data.attachmentsJson),
|
||||
};
|
||||
if (this.settlementIds.length) await this.loadReferenceInformation();
|
||||
},
|
||||
validateInvoice(rule, value, callback) {
|
||||
if (!value) {
|
||||
@@ -541,7 +558,14 @@ export default {
|
||||
async submitForm() {
|
||||
const saved = await this.saveDraft();
|
||||
if (!saved) return;
|
||||
const previousApprovalStatus = this.form.approvalStatus || '';
|
||||
await api.submit({ id: this.form.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'invoice-receipt',
|
||||
bizId: this.form.id,
|
||||
subjectName: this.form.invoiceNo || '',
|
||||
approvalStatus: previousApprovalStatus,
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.goBack();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<mk-public-shell biz-type="invoice-receipt" :get-form="getForm">
|
||||
<invoice-receipt-form ref="page" />
|
||||
</mk-public-shell>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
|
||||
import InvoiceReceiptForm from '@/views/payment/invoice-receipt-form.vue';
|
||||
|
||||
export default {
|
||||
name: 'InvoiceReceiptPublicView',
|
||||
components: { MkPublicShell, InvoiceReceiptForm },
|
||||
methods: {
|
||||
getForm() {
|
||||
const form = this.$refs.page?.form || {};
|
||||
return { ...form, subject: form.invoiceNo || form.projectName || '' };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -147,14 +147,20 @@
|
||||
>提交</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('invoice_receipt_view') && row.approvalStatus !== 'draft'"
|
||||
v-if="
|
||||
mkEnabled &&
|
||||
hasPermission('invoice_receipt_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="openFlow(row)"
|
||||
@click="handleMkAudit(row)"
|
||||
>流程</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('invoice_receipt_approve') && row.approvalStatus === 'reviewing'
|
||||
!mkEnabled &&
|
||||
hasPermission('invoice_receipt_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleApprove(row)"
|
||||
@@ -162,7 +168,9 @@
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('invoice_receipt_approve') && row.approvalStatus === 'reviewing'
|
||||
!mkEnabled &&
|
||||
hasPermission('invoice_receipt_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleReturn(row)"
|
||||
@@ -191,42 +199,6 @@
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="flowDialog.visible" title="审批流程" width="900px" append-to-body>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="发票号码">
|
||||
{{ flowDialog.row.invoiceNo || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="审核状态">
|
||||
{{ flowDialog.row.approvalStatusName || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前节点">
|
||||
{{ flowDialog.row.currentNode || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前处理人">
|
||||
{{ flowDialog.row.currentProcessor || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table
|
||||
v-loading="flowDialog.loading"
|
||||
:data="flowDialog.records"
|
||||
border
|
||||
class="receipt-page__flow-table"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||
<el-table-column prop="actionName" label="操作" min-width="110" align="center" />
|
||||
<el-table-column label="原状态" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ statusName(row.fromStatus) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="新状态" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ statusName(row.toStatus) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operatorName" label="操作人" min-width="110" align="center" />
|
||||
<el-table-column prop="createTime" label="操作时间" min-width="170" align="center" />
|
||||
<el-table-column prop="reason" label="原因" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" />
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -237,6 +209,8 @@ import * as XLSX from 'xlsx';
|
||||
import * as api from '@/api/payment/invoiceReceipt';
|
||||
import { invoiceReceiptTableColumns } from '@/option/payment/invoiceReceipt';
|
||||
import organizationSearch from '@/mixins/organization-search';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
invoiceNo: '',
|
||||
@@ -266,7 +240,7 @@ export default {
|
||||
loading: false,
|
||||
pushLoading: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
flowDialog: { visible: false, loading: false, row: {}, records: [] },
|
||||
mkEnabled: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -275,6 +249,13 @@ export default {
|
||||
mounted() {
|
||||
this.loadOrganizationOptions();
|
||||
this.loadTable();
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = !!enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
@@ -323,19 +304,6 @@ export default {
|
||||
query: { mode: 'view', id: row.id },
|
||||
});
|
||||
},
|
||||
async openFlow(row) {
|
||||
this.flowDialog.row = { ...row };
|
||||
this.flowDialog.records = [];
|
||||
this.flowDialog.visible = true;
|
||||
this.flowDialog.loading = true;
|
||||
try {
|
||||
const detail = this.unwrapData(await api.getDetail(row.id));
|
||||
this.flowDialog.row = { ...row, ...detail };
|
||||
this.flowDialog.records = detail.records || [];
|
||||
} finally {
|
||||
this.flowDialog.loading = false;
|
||||
}
|
||||
},
|
||||
async handleDelete(row) {
|
||||
await this.$confirm('确认删除该收票登记?', '提示', { type: 'warning' });
|
||||
await api.remove(row.id);
|
||||
@@ -343,10 +311,35 @@ export default {
|
||||
this.loadTable();
|
||||
},
|
||||
async handleSubmit(row) {
|
||||
const previousApprovalStatus = row.approvalStatus || '';
|
||||
await api.submit({ id: row.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'invoice-receipt',
|
||||
bizId: row.id,
|
||||
subjectName: row.invoiceNo || '',
|
||||
approvalStatus: previousApprovalStatus,
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.loadTable();
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('收票登记ID为空,无法打开审核页');
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
.then(res => {
|
||||
const url = res?.data?.data;
|
||||
if (!url) {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
});
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('确认审批通过该收票登记?', '提示', { type: 'warning' });
|
||||
await api.approve({ id: row.id });
|
||||
@@ -418,19 +411,6 @@ export default {
|
||||
''
|
||||
);
|
||||
},
|
||||
statusName(status) {
|
||||
return (
|
||||
{
|
||||
draft: '草稿',
|
||||
reviewing: '审批中',
|
||||
approved: '审批通过',
|
||||
returned: '已驳回',
|
||||
voided: '已作废',
|
||||
}[status] ||
|
||||
status ||
|
||||
'-'
|
||||
);
|
||||
},
|
||||
displayValue(value) {
|
||||
return value === null || value === undefined || value === '' ? '-' : value;
|
||||
},
|
||||
|
||||
@@ -2672,12 +2672,13 @@ export default {
|
||||
// 提交前静默保存,避免同时弹出「保存成功」与材料缺失 toast
|
||||
const saved = await this.saveDraft({ showSuccess: false });
|
||||
if (!saved) return;
|
||||
const previousApprovalStatus = this.form.approvalStatus || '';
|
||||
await api.submit({ id: this.form.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'payment-application',
|
||||
formInstanceId: this.form.id,
|
||||
bizId: this.form.id,
|
||||
subjectName: this.form.paymentNo || '',
|
||||
approvalStatus: this.form.approvalStatus || '',
|
||||
approvalStatus: previousApprovalStatus,
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.goBack();
|
||||
|
||||
@@ -160,14 +160,29 @@
|
||||
>编辑</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('payment_application_submit') && row.approvalStatus === 'draft'"
|
||||
v-if="
|
||||
hasPermission('payment_application_submit') &&
|
||||
['draft', 'returned'].includes(row.approvalStatus)
|
||||
"
|
||||
type="primary"
|
||||
@click="handleSubmit(row)"
|
||||
>提交</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('payment_application_approve') && row.approvalStatus === 'reviewing'
|
||||
mkEnabled &&
|
||||
hasPermission('payment_application_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleMkAudit(row)"
|
||||
>流程</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
!mkEnabled &&
|
||||
hasPermission('payment_application_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleApprove(row)"
|
||||
@@ -175,18 +190,14 @@
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('payment_application_approve') && row.approvalStatus === 'reviewing'
|
||||
!mkEnabled &&
|
||||
hasPermission('payment_application_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleReturn(row)"
|
||||
>驳回</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="hasPermission('payment_application_view') && row.approvalStatus !== 'draft'"
|
||||
type="primary"
|
||||
@click="openFlow(row)"
|
||||
>流程</el-link
|
||||
>
|
||||
<el-link
|
||||
v-if="
|
||||
hasPermission('payment_application_void') && row.approvalStatus === 'approved'
|
||||
@@ -217,22 +228,6 @@
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<el-dialog v-model="flowDialog.visible" title="审批流程" width="620px" append-to-body>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="付款申请单号">
|
||||
{{ flowDialog.row.paymentNo || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="审核状态">
|
||||
{{ flowDialog.row.approvalStatusName || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前节点">
|
||||
{{ flowDialog.row.currentNode || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前处理人">
|
||||
{{ flowDialog.row.currentProcessor || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -242,7 +237,8 @@ import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/payment/paymentApplication';
|
||||
import { paymentApplicationTableColumns } from '@/option/payment/paymentApplication';
|
||||
import organizationSearch from '@/mixins/organization-search';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
paymentNo: '',
|
||||
@@ -276,7 +272,7 @@ export default {
|
||||
syncLoading: false,
|
||||
resultSyncing: false,
|
||||
page: { current: 1, size: 10, total: 0 },
|
||||
flowDialog: { visible: false, row: {} },
|
||||
mkEnabled: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -288,6 +284,13 @@ export default {
|
||||
mounted() {
|
||||
this.loadOrganizationOptions();
|
||||
this.loadTable();
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = !!enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
@@ -372,12 +375,13 @@ export default {
|
||||
this.loadTable();
|
||||
},
|
||||
async handleSubmit(row) {
|
||||
const previousApprovalStatus = row.approvalStatus || '';
|
||||
await api.submit({ id: row.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'payment-application',
|
||||
formInstanceId: row.id,
|
||||
bizId: row.id,
|
||||
subjectName: row.paymentNo || '',
|
||||
approvalStatus: row.approvalStatus || '',
|
||||
approvalStatus: previousApprovalStatus,
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.loadTable();
|
||||
@@ -387,16 +391,30 @@ export default {
|
||||
this.$message.success('审批通过');
|
||||
this.loadTable();
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('付款申请ID为空,无法打开审核页');
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
.then(res => {
|
||||
const url = res?.data?.data;
|
||||
if (!url) {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
});
|
||||
},
|
||||
async handleReturn(row) {
|
||||
const { value } = await this.$prompt('请输入驳回原因', '审批驳回');
|
||||
await api.returnBill({ id: row.id, reason: value });
|
||||
this.$message.success('已驳回');
|
||||
this.loadTable();
|
||||
},
|
||||
openFlow(row) {
|
||||
this.flowDialog.row = row;
|
||||
this.flowDialog.visible = true;
|
||||
},
|
||||
async handleVoid(row) {
|
||||
const { value } = await this.$prompt('请输入作废原因', '作废付款申请');
|
||||
await api.voidBill({ id: row.id, reason: value });
|
||||
|
||||
@@ -157,6 +157,7 @@ import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/payment/receiptClaimRecord';
|
||||
import { receiptClaimRecordTableColumns } from '@/option/payment/receiptClaimRecord';
|
||||
import SectionCard from '@/components/section-card/main.vue';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
receiptNoticeNo: '',
|
||||
@@ -195,10 +196,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo?.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -271,6 +271,7 @@ import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as api from '@/api/payment/receiptFlow';
|
||||
import { receiptFlowTableColumns } from '@/option/payment/receiptFlow';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
receiptNoticeNo: '',
|
||||
@@ -312,10 +313,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo?.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -26,13 +26,13 @@
|
||||
<el-link
|
||||
type="primary"
|
||||
@click.stop="handleDesign(scope.row.name)"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
>设计
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
@click.stop="handlePreview(scope.row.name)"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
>预览
|
||||
</el-link>
|
||||
</template>
|
||||
@@ -97,7 +97,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: false,
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<template #menu="scope">
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
@click="handleDebug(scope.row)"
|
||||
>调试
|
||||
</el-link>
|
||||
@@ -292,7 +292,7 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.oss_add),
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<template #menu="scope">
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
@click="handleDebug(scope.row)"
|
||||
>调试
|
||||
</el-link>
|
||||
@@ -313,7 +313,7 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.sms_add, false),
|
||||
|
||||
@@ -136,6 +136,7 @@ export default {
|
||||
syncLoading: { type: Boolean, default: false },
|
||||
page: { type: Object, required: true },
|
||||
hasPermission: { type: Function, required: true },
|
||||
mkEnabled: { type: Boolean, default: false },
|
||||
},
|
||||
emits: [
|
||||
'create',
|
||||
@@ -195,7 +196,10 @@ export default {
|
||||
actions.push({ type: 'delete', label: '删除', danger: true });
|
||||
if (this.hasPermission('formal_settlement_submit') && this.isEditable(row))
|
||||
actions.push({ type: 'submit', label: '提交' });
|
||||
if (this.hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing') {
|
||||
if (this.mkEnabled && this.hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing') {
|
||||
actions.push({ type: 'mkAudit', label: '流程' });
|
||||
}
|
||||
if (!this.mkEnabled && this.hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing') {
|
||||
actions.push({ type: 'approve', label: '通过' });
|
||||
actions.push({ type: 'return', label: '驳回', danger: true });
|
||||
}
|
||||
|
||||
@@ -966,6 +966,7 @@ import {
|
||||
import { downloadFileByUrl } from '@/utils/util';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
export default {
|
||||
name: 'PreSettlementEditor',
|
||||
@@ -1164,10 +1165,7 @@ export default {
|
||||
return !this.readonly && ['draft', 'returned'].includes(this.form.approvalStatus);
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo?.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
canAdjustDetail() {
|
||||
return this.editable && this.hasPermission('pre_settlement_adjust');
|
||||
@@ -1611,12 +1609,13 @@ export default {
|
||||
await this.loadDetail();
|
||||
await this.persistPendingAdjustments();
|
||||
if (shouldSubmit) {
|
||||
const previousApprovalStatus = this.form.approvalStatus || '';
|
||||
await submit({ id: this.form.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'pre-settlement',
|
||||
formInstanceId: this.form.id,
|
||||
bizId: this.form.id,
|
||||
subjectName: this.form.preSettlementNo || this.form.contractName || '',
|
||||
approvalStatus: this.form.approvalStatus || '',
|
||||
approvalStatus: previousApprovalStatus,
|
||||
});
|
||||
this.$message.success('审批流程已发起');
|
||||
this.visible = false;
|
||||
|
||||
@@ -127,6 +127,13 @@
|
||||
</div>
|
||||
<el-button @click="resetInternalQuery">重置</el-button>
|
||||
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!filteredInternalRows.length"
|
||||
@click="exportInternalDetails"
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table
|
||||
:data="filteredInternalRows"
|
||||
@@ -616,6 +623,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import * as XLSX from 'xlsx';
|
||||
import * as api from '@/api/settlement/transportReconciliation';
|
||||
import * as formalSettlementApi from '@/api/settlement/formalSettlement';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
@@ -1807,6 +1815,63 @@ export default {
|
||||
formatNumber(value) {
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
formatExportMoney(value) {
|
||||
if (value === null || value === undefined || value === '' || Number(value) === -1) {
|
||||
return '';
|
||||
}
|
||||
return Number(value || 0).toFixed(2);
|
||||
},
|
||||
formatInternalExportCell(row, column) {
|
||||
if (column.prop === 'matchResult') return this.matchName(row.matchResult);
|
||||
if (column.prop === 'updateResult') return this.updateName(row.updateResult);
|
||||
if (column.prop === 'matchedExternalLineNo') {
|
||||
return this.hasMatchedExternal(row)
|
||||
? this.formatMatchedExternalLineNo(row.matchedExternalLineNo)
|
||||
: '';
|
||||
}
|
||||
if (column.prop === 'transportType') return this.transportTypeLabel(row.transportType);
|
||||
if (column.prop === 'transportQuantity') {
|
||||
return row.transportQuantity === null ||
|
||||
row.transportQuantity === undefined ||
|
||||
row.transportQuantity === ''
|
||||
? ''
|
||||
: Number(row.transportQuantity || 0).toFixed(2);
|
||||
}
|
||||
if (column.feeItemName) {
|
||||
return this.formatExportMoney(this.getFeeItemAmount(row, column.feeItemName));
|
||||
}
|
||||
if (column.prop === 'mileage') {
|
||||
return Number(row.mileage) === -1 ? '' : this.formatExportMoney(row.mileage);
|
||||
}
|
||||
if (column.prop === 'actualDepartureTime' || column.prop === 'actualCompletionTime') {
|
||||
const text = this.formatDate(row[column.prop]);
|
||||
return text === '-' ? '' : text;
|
||||
}
|
||||
if (column.money || column.number) return this.formatExportMoney(row[column.prop]);
|
||||
const value = row[column.prop];
|
||||
return value === null || value === undefined || value === '' ? '' : value;
|
||||
},
|
||||
exportInternalDetails() {
|
||||
if (!this.filteredInternalRows.length) {
|
||||
this.$message.warning('暂无可导出的内部账单明细');
|
||||
return;
|
||||
}
|
||||
const rows = this.filteredInternalRows.map((row, index) => {
|
||||
const result = { 序号: index + 1 };
|
||||
this.internalTableColumns.forEach(column => {
|
||||
result[column.label] = this.formatInternalExportCell(row, column);
|
||||
});
|
||||
return result;
|
||||
});
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(rows), '内部账单明细');
|
||||
XLSX.writeFile(
|
||||
workbook,
|
||||
`${this.form.reconciliationNo || '运输对账'}内部账单明细${this.$dayjs().format(
|
||||
'YYYY-MM-DD HH-mm-ss'
|
||||
)}.xlsx`
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
:page="page"
|
||||
:has-permission="hasPermissionFn"
|
||||
:sync-loading="syncLoading"
|
||||
:mk-enabled="mkEnabled"
|
||||
@create="openCreate"
|
||||
@payment="openPaymentDialog"
|
||||
@sync="handleSync"
|
||||
@@ -84,6 +85,7 @@
|
||||
:page="page"
|
||||
:has-permission="hasPermissionFn"
|
||||
:sync-loading="syncLoading"
|
||||
:mk-enabled="mkEnabled"
|
||||
@create="openCreate"
|
||||
@payment="openPaymentDialog"
|
||||
@sync="handleSync"
|
||||
@@ -184,7 +186,8 @@ import organizationSearch from '@/mixins/organization-search';
|
||||
import { formalSettlementTableColumns } from '@/option/settlement/formalSettlementTable';
|
||||
import FormalSettlementEditor from './components/formal-settlement-editor.vue';
|
||||
import FormalSettlementTablePanel from './components/formal-settlement-table-panel.vue';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { submitMkApprovalFlow, loadMkEnabled } from '@/utils/mk-approval';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
formalSettlementNo: '',
|
||||
@@ -227,6 +230,7 @@ export default {
|
||||
rows: [],
|
||||
form: { appliedAmount: 0, remark: '' },
|
||||
},
|
||||
mkEnabled: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -250,6 +254,13 @@ export default {
|
||||
this.loadOrganizationOptions();
|
||||
this.loadTable();
|
||||
this.openRouteDetail();
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = !!enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
@@ -293,6 +304,7 @@ export default {
|
||||
edit: () => this.openEdit(row),
|
||||
delete: () => this.handleDelete(row),
|
||||
submit: () => this.handleSubmit(row),
|
||||
mkAudit: () => this.handleMkAudit(row),
|
||||
approve: () => this.handleApprove(row),
|
||||
return: () => this.handleReturn(row),
|
||||
void: () => this.handleVoid(row),
|
||||
@@ -340,16 +352,35 @@ export default {
|
||||
},
|
||||
async handleSubmit(row) {
|
||||
await this.$confirm('提交后来源预结算将保持锁定,确认提交?', '提示', { type: 'warning' });
|
||||
const previousApprovalStatus = row.approvalStatus || '';
|
||||
await api.submit({ id: row.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'formal-settlement',
|
||||
formInstanceId: row.id,
|
||||
bizId: row.id,
|
||||
subjectName: row.formalSettlementNo || '',
|
||||
approvalStatus: row.approvalStatus || '',
|
||||
approvalStatus: previousApprovalStatus,
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.loadTable();
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('正式结算单ID为空,无法打开审核页');
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
.then(res => {
|
||||
const url = res?.data?.data;
|
||||
if (!url) {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
});
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm(
|
||||
'审核通过后正式结算生效,金额只能通过结算调整单处理,确认通过?',
|
||||
|
||||
@@ -172,21 +172,43 @@
|
||||
删除
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_view') && row.approvalStatus !== 'draft'"
|
||||
v-if="
|
||||
hasPermission('pre_settlement_submit') &&
|
||||
['draft', 'returned'].includes(row.approvalStatus)
|
||||
"
|
||||
type="primary"
|
||||
@click="openFlow(row)"
|
||||
@click="handleSubmit(row)"
|
||||
>
|
||||
提交
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="
|
||||
mkEnabled &&
|
||||
hasPermission('pre_settlement_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleMkAudit(row)"
|
||||
>
|
||||
流程
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_approve') && row.approvalStatus === 'reviewing'"
|
||||
v-if="
|
||||
!mkEnabled &&
|
||||
hasPermission('pre_settlement_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="primary"
|
||||
@click="handleApprove(row)"
|
||||
>
|
||||
通过
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasPermission('pre_settlement_approve') && row.approvalStatus === 'reviewing'"
|
||||
v-if="
|
||||
!mkEnabled &&
|
||||
hasPermission('pre_settlement_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
type="danger"
|
||||
@click="handleReturn(row)"
|
||||
>
|
||||
@@ -259,23 +281,6 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="flowDialog.visible" title="审批流程" width="620px" append-to-body>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="预结算单号">
|
||||
{{ flowDialog.row.preSettlementNo || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="审核状态">
|
||||
{{ flowDialog.row.approvalStatusName || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前节点">
|
||||
{{ flowDialog.row.currentNode || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="当前处理人">
|
||||
{{ flowDialog.row.currentProcessor || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</basic-container>
|
||||
</template>
|
||||
|
||||
@@ -290,6 +295,7 @@ import {
|
||||
getPrintTemplates,
|
||||
remove,
|
||||
returnBill,
|
||||
submit,
|
||||
voidBill,
|
||||
} from '@/api/settlement/preSettlement';
|
||||
import { preSettlementSearchFields } from '@/option/settlement/preSettlementSearch';
|
||||
@@ -297,7 +303,10 @@ import organizationSearch from '@/mixins/organization-search';
|
||||
import { preSettlementTableColumns } from '@/option/settlement/preSettlementTable';
|
||||
import { createSettlementTransfer } from '@/utils/settlement-transfer';
|
||||
import { downloadFile } from '@/utils/util';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import PreSettlementEditor from './components/pre-settlement-editor.vue';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
preSettlementNo: '',
|
||||
@@ -345,19 +354,13 @@ export default {
|
||||
templates: [],
|
||||
template: '',
|
||||
},
|
||||
flowDialog: {
|
||||
visible: false,
|
||||
row: {},
|
||||
},
|
||||
mkEnabled: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo?.authority;
|
||||
return Array.isArray(authority)
|
||||
? authority.includes('admin')
|
||||
: String(authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
visibleSearchFields() {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
@@ -372,6 +375,13 @@ export default {
|
||||
this.loadOrganizationOptions();
|
||||
this.loadTable();
|
||||
this.openRouteDetail();
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = !!enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
hasPermission(code) {
|
||||
@@ -628,9 +638,35 @@ export default {
|
||||
const { data } = await exportList(params);
|
||||
downloadFile(data, `预结算单${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`);
|
||||
},
|
||||
openFlow(row) {
|
||||
this.flowDialog.row = row;
|
||||
this.flowDialog.visible = true;
|
||||
async handleSubmit(row) {
|
||||
const previousApprovalStatus = row.approvalStatus || '';
|
||||
await submit({ id: row.id });
|
||||
await submitMkApprovalFlow({
|
||||
bizType: 'pre-settlement',
|
||||
bizId: row.id,
|
||||
subjectName: row.preSettlementNo || '',
|
||||
approvalStatus: previousApprovalStatus,
|
||||
});
|
||||
this.$message.success('提交成功');
|
||||
this.loadTable();
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('预结算单ID为空,无法打开审核页');
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
.then(res => {
|
||||
const url = res?.data?.data;
|
||||
if (!url) {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
});
|
||||
},
|
||||
async handleApprove(row) {
|
||||
await this.$confirm('确认审批通过该预结算单?', '提示', { type: 'warning' });
|
||||
|
||||
@@ -172,6 +172,7 @@ import { settlementAdjustmentSearchFields } from '@/option/settlement/settlement
|
||||
import organizationSearch from '@/mixins/organization-search';
|
||||
import { settlementAdjustmentTableColumns } from '@/option/settlement/settlementAdjustmentTable';
|
||||
import SettlementAdjustmentEditor from './components/settlement-adjustment-editor.vue';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const emptyQuery = () => ({
|
||||
adjustmentNo: '',
|
||||
@@ -207,7 +208,7 @@ export default {
|
||||
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
|
||||
},
|
||||
isAdmin() {
|
||||
return String(this.userInfo?.authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
|
||||
+185
-38
@@ -34,7 +34,7 @@
|
||||
type="primary"
|
||||
plain
|
||||
:loading="oaSyncLoading"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
@click="handleOaOrgSync"
|
||||
>自动同步组织
|
||||
</el-button>
|
||||
@@ -43,12 +43,12 @@
|
||||
<el-link
|
||||
type="primary"
|
||||
@click.stop="handleAdd(scope.row, scope.index)"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
>新增子项
|
||||
</el-link>
|
||||
</template>
|
||||
<template #deptCategory="{ row }">
|
||||
<el-tag>{{ getDeptCategoryLabel(row.deptCategory) }}</el-tag>
|
||||
<el-tag>{{ row.deptCategoryName || getDeptCategoryLabel(row.deptCategory) }}</el-tag>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<el-tag :type="Number(row.status) === 1 ? 'success' : 'info'" class="status-text">
|
||||
@@ -181,18 +181,21 @@ import {
|
||||
clearNonTopDept,
|
||||
} from '@/api/system/dept';
|
||||
import { getLeaderList } from '@/api/system/user';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
|
||||
import { mapGetters } from 'vuex';
|
||||
import website from '@/config/website';
|
||||
import func from '@/utils/func';
|
||||
|
||||
const escapeRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const ORGANIZATION_TYPE_DICT_CODE = 'organization_type';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: {},
|
||||
leaderOptions: [],
|
||||
organizationTypeOptions: [],
|
||||
parentDeptCode: '',
|
||||
parentDept: null,
|
||||
carrierDialogVisible: false,
|
||||
@@ -315,6 +318,31 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '金蝶部门代码',
|
||||
prop: 'kingdeeDeptCode',
|
||||
minWidth: 140,
|
||||
order: 65,
|
||||
maxlength: 64,
|
||||
showWordLimit: true,
|
||||
rules: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const code = String(value || '').trim();
|
||||
if (!code) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
if (code.length > 64) {
|
||||
callback(new Error('金蝶部门代码不能超过64个字符'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '组织简称',
|
||||
prop: 'shortName',
|
||||
@@ -336,14 +364,7 @@ export default {
|
||||
{
|
||||
label: '组织类型',
|
||||
type: 'select',
|
||||
dicData: [
|
||||
{ label: '一级集团', value: 1 },
|
||||
{ label: '二级事业部', value: 2 },
|
||||
{ label: '部门', value: 3 },
|
||||
{ label: '三级分公司', value: 4 },
|
||||
{ label: '物流园区', value: 5 },
|
||||
{ label: '承运商', value: 6 },
|
||||
],
|
||||
dicData: [],
|
||||
dataType: 'number',
|
||||
minWidth: 120,
|
||||
order: 90,
|
||||
@@ -486,9 +507,10 @@ export default {
|
||||
// ignore storage errors
|
||||
}
|
||||
this.loadLeaderOptions();
|
||||
this.loadOrganizationTypeOptions();
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.dept_add, false),
|
||||
@@ -725,38 +747,154 @@ export default {
|
||||
});
|
||||
});
|
||||
},
|
||||
loadOrganizationTypeOptions() {
|
||||
return getDictionary({ code: ORGANIZATION_TYPE_DICT_CODE }).then(res => {
|
||||
const list = res.data.data || [];
|
||||
this.organizationTypeOptions = list
|
||||
.map(item => this.normalizeOrganizationTypeOption(item))
|
||||
.filter(Boolean);
|
||||
this.resetDeptCategoryOptions();
|
||||
if (list.length && !this.organizationTypeOptions.length) {
|
||||
this.$message.warning('组织类型字典 dictKey 须为数字 1~6,当前无法解析,请检查业务字典配置');
|
||||
}
|
||||
// 字典异步返回后触发表格组织类型标签重绘
|
||||
if (Array.isArray(this.data) && this.data.length) {
|
||||
this.data = [...this.data];
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 业务字典项转下拉选项。后端 deptCategory 为 Integer,value 必须始终为数字。
|
||||
* dictKey 为纯数字时直接使用;若配成中文则回退用 sort(1~6)作为层级码。
|
||||
*/
|
||||
normalizeOrganizationTypeOption(item) {
|
||||
const label = item?.dictValue ?? item?.label;
|
||||
if (label === undefined || label === null || String(label).trim() === '') {
|
||||
return null;
|
||||
}
|
||||
const rawKey = item?.dictKey ?? item?.value;
|
||||
const keyText = rawKey === undefined || rawKey === null ? '' : String(rawKey).trim();
|
||||
const keyNumber = Number(keyText);
|
||||
const isPureNumber = keyText !== '' && !Number.isNaN(keyNumber) && String(keyNumber) === keyText;
|
||||
if (isPureNumber) {
|
||||
return {
|
||||
label: String(label),
|
||||
value: keyNumber,
|
||||
};
|
||||
}
|
||||
// 兼容 dictKey 误配成中文:用排序号作为 1~6 层级码,保证下拉有数据且可提交 Integer
|
||||
const sortNumber = Number(item?.sort);
|
||||
if (!Number.isNaN(sortNumber) && sortNumber >= 1 && sortNumber <= 6) {
|
||||
return {
|
||||
label: String(label),
|
||||
value: sortNumber,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* 提交前将组织类型规范为 Integer;空状态字段去掉,避免 "" 反序列化失败。
|
||||
*/
|
||||
prepareDeptSubmitRow(row) {
|
||||
const category = this.resolveDeptCategoryValue(row.deptCategory);
|
||||
if (category === null) {
|
||||
return false;
|
||||
}
|
||||
row.deptCategory = category;
|
||||
if (row.status === '' || row.status === null || row.status === undefined) {
|
||||
delete row.status;
|
||||
} else {
|
||||
row.status = Number(row.status);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
resolveDeptCategoryValue(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
this.$message.warning('请选择组织类型');
|
||||
return null;
|
||||
}
|
||||
const asNumber = Number(value);
|
||||
if (!Number.isNaN(asNumber) && String(asNumber) === String(value).trim()) {
|
||||
return asNumber;
|
||||
}
|
||||
// 容错:表单里残留了中文标签时,按字典项 label 反查数字编码
|
||||
const matched = this.organizationTypeOptions.find(
|
||||
item => String(item.label) === String(value) || this.isSameDeptCategory(item.value, value)
|
||||
);
|
||||
if (matched && typeof matched.value === 'number' && !Number.isNaN(matched.value)) {
|
||||
return matched.value;
|
||||
}
|
||||
this.$message.warning('组织类型无效,请重新选择(字典键须为数字 1~6)');
|
||||
return null;
|
||||
},
|
||||
resetDeptCategoryOptions() {
|
||||
const categoryColumn = this.findColumn(this.option.column, 'deptCategory');
|
||||
if (!categoryColumn) {
|
||||
return;
|
||||
}
|
||||
categoryColumn.dicData = [...this.organizationTypeOptions];
|
||||
},
|
||||
isSameDeptCategory(left, right) {
|
||||
if (
|
||||
left === null ||
|
||||
left === undefined ||
|
||||
left === '' ||
|
||||
right === null ||
|
||||
right === undefined ||
|
||||
right === ''
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (String(left) === String(right)) {
|
||||
return true;
|
||||
}
|
||||
const leftNumber = Number(left);
|
||||
const rightNumber = Number(right);
|
||||
return !Number.isNaN(leftNumber) && !Number.isNaN(rightNumber) && leftNumber === rightNumber;
|
||||
},
|
||||
isCarrierDeptCategory(value) {
|
||||
if (Number(value) === 6) {
|
||||
return true;
|
||||
}
|
||||
const matched = this.organizationTypeOptions.find(item => this.isSameDeptCategory(item.value, value));
|
||||
const text = matched?.label || value;
|
||||
return String(text).includes('承运商');
|
||||
},
|
||||
filterOrganizationTypeOptions(values) {
|
||||
const valueSet = new Set(values.map(item => String(item)));
|
||||
const filtered = this.organizationTypeOptions.filter(item => valueSet.has(String(item.value)));
|
||||
// 字典 key 非数字时无法按历史层级码过滤,回退展示全部,避免下拉无选项
|
||||
return filtered.length ? filtered : [...this.organizationTypeOptions];
|
||||
},
|
||||
updateDeptCategoryOptions() {
|
||||
const categoryColumn = this.findColumn(this.option.column, 'deptCategory');
|
||||
const parent = this.parentDept;
|
||||
if (!parent) return;
|
||||
if (!parent || !categoryColumn) {
|
||||
return;
|
||||
}
|
||||
let dicData = [];
|
||||
if (this.isRootDept(parent)) {
|
||||
dicData =
|
||||
parent.deptName === '外部组织'
|
||||
? [{ label: '承运商', value: 6 }]
|
||||
: [{ label: '集团', value: 1 }];
|
||||
? this.filterOrganizationTypeOptions([6])
|
||||
: this.filterOrganizationTypeOptions([1]);
|
||||
} else if (Number(parent.deptCategory) === 1) {
|
||||
dicData = [
|
||||
{ label: '事业部', value: 2 },
|
||||
{ label: '部门', value: 3 },
|
||||
];
|
||||
dicData = this.filterOrganizationTypeOptions([2, 3]);
|
||||
} else if ([2, 3].includes(Number(parent.deptCategory))) {
|
||||
dicData = [
|
||||
{ label: '分公司', value: 4 },
|
||||
{ label: '物流园区', value: 5 },
|
||||
{ label: '承运商', value: 6 },
|
||||
];
|
||||
dicData = this.filterOrganizationTypeOptions([4, 5, 6]);
|
||||
} else {
|
||||
dicData = [...this.organizationTypeOptions];
|
||||
}
|
||||
categoryColumn.dicData = dicData;
|
||||
if (!dicData.some(item => item.value === Number(this.form.deptCategory))) {
|
||||
if (!dicData.some(item => this.isSameDeptCategory(item.value, this.form.deptCategory))) {
|
||||
this.form.deptCategory = dicData[0]?.value;
|
||||
}
|
||||
if (Number(this.form.deptCategory) === 6) {
|
||||
if (this.isCarrierDeptCategory(this.form.deptCategory)) {
|
||||
this.openCarrierDialog();
|
||||
}
|
||||
},
|
||||
handleDeptCategoryChange(value) {
|
||||
if (Number(value) === 6) {
|
||||
if (this.isCarrierDeptCategory(value)) {
|
||||
this.openCarrierDialog();
|
||||
}
|
||||
},
|
||||
@@ -816,15 +954,13 @@ export default {
|
||||
});
|
||||
},
|
||||
getDeptCategoryLabel(deptCategory) {
|
||||
const categoryMap = {
|
||||
1: '集团',
|
||||
2: '事业部',
|
||||
3: '部门',
|
||||
4: '分公司',
|
||||
5: '物流园区',
|
||||
6: '承运商',
|
||||
};
|
||||
return categoryMap[Number(deptCategory)] || '-';
|
||||
if (deptCategory === null || deptCategory === undefined || deptCategory === '') {
|
||||
return '-';
|
||||
}
|
||||
const matched = this.organizationTypeOptions.find(item =>
|
||||
this.isSameDeptCategory(item.value, deptCategory)
|
||||
);
|
||||
return matched?.label || String(deptCategory);
|
||||
},
|
||||
getLeaderValue(leaderId, field) {
|
||||
// func.split 空值时返回 '',需兼容为数组,避免表格 formatter 渲染崩溃导致列错位
|
||||
@@ -861,9 +997,14 @@ export default {
|
||||
rowSave(row, done, loading) {
|
||||
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
|
||||
row.deptCode = String(row.deptCode || '').trim();
|
||||
row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim();
|
||||
row.leaderId = func.join(row.leaderId);
|
||||
this.normalizeTopParent(row);
|
||||
if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) {
|
||||
if (!this.prepareDeptSubmitRow(row)) {
|
||||
loading();
|
||||
return;
|
||||
}
|
||||
if (this.isCarrierDeptCategory(row.deptCategory) && !row.carrierCustomerId) {
|
||||
this.$message.warning('请选择承运商');
|
||||
loading();
|
||||
return;
|
||||
@@ -891,9 +1032,14 @@ export default {
|
||||
rowUpdate(row, index, done, loading) {
|
||||
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
|
||||
row.deptCode = String(row.deptCode || '').trim();
|
||||
row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim();
|
||||
row.leaderId = func.join(row.leaderId);
|
||||
this.normalizeTopParent(row);
|
||||
if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) {
|
||||
if (!this.prepareDeptSubmitRow(row)) {
|
||||
loading();
|
||||
return;
|
||||
}
|
||||
if (this.isCarrierDeptCategory(row.deptCategory) && !row.carrierCustomerId) {
|
||||
this.$message.warning('请选择承运商');
|
||||
loading();
|
||||
return;
|
||||
@@ -1026,6 +1172,7 @@ export default {
|
||||
const column = this.findColumn(this.option.column, 'parentId');
|
||||
column.value = '';
|
||||
column.addDisabled = false;
|
||||
this.resetDeptCategoryOptions();
|
||||
done();
|
||||
},
|
||||
currentChange(currentPage) {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<el-link
|
||||
type="primary"
|
||||
@click.stop="handleRowClick(scope.row)"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
>字典配置
|
||||
</el-link>
|
||||
</template>
|
||||
@@ -81,7 +81,7 @@
|
||||
<el-link
|
||||
type="primary"
|
||||
@click.stop="handleAdd(scope.row, scope.index)"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
>新增子项
|
||||
</el-link>
|
||||
</template>
|
||||
@@ -137,7 +137,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.dict_add, false),
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<el-link
|
||||
type="primary"
|
||||
@click.stop="handleRowClick(scope.row)"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
>字典配置
|
||||
</el-link>
|
||||
</template>
|
||||
@@ -81,7 +81,7 @@
|
||||
<el-link
|
||||
type="primary"
|
||||
@click.stop="handleAdd(scope.row, scope.index)"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
>新增子项
|
||||
</el-link>
|
||||
</template>
|
||||
@@ -137,7 +137,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.dictbiz_add, false),
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<el-link
|
||||
type="primary"
|
||||
@click.stop="handleAdd(scope.row, scope.index)"
|
||||
v-if="userInfo.authority.includes('admin') && scope.row.category === 1"
|
||||
v-if="isAdministrator && scope.row.category === 1"
|
||||
>新增子项
|
||||
</el-link>
|
||||
</template>
|
||||
@@ -280,7 +280,7 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.menu_add, false),
|
||||
|
||||
+10
-10
@@ -40,7 +40,7 @@
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-refresh"
|
||||
v-if="userInfo.authority.includes('administrator') && !recycleMode"
|
||||
v-if="isAdministrator && !recycleMode"
|
||||
@click="handleRecycle"
|
||||
>回收站
|
||||
</el-button>
|
||||
@@ -48,7 +48,7 @@
|
||||
type="success"
|
||||
plain
|
||||
icon="el-icon-check"
|
||||
v-if="userInfo.authority.includes('administrator') && recycleMode"
|
||||
v-if="isAdministrator && recycleMode"
|
||||
@click="handleRecyclePass"
|
||||
>恢 复
|
||||
</el-button>
|
||||
@@ -56,7 +56,7 @@
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-close"
|
||||
v-if="userInfo.authority.includes('administrator') && recycleMode"
|
||||
v-if="isAdministrator && recycleMode"
|
||||
@click="handleRecycleRemove"
|
||||
>删除
|
||||
</el-button>
|
||||
@@ -64,7 +64,7 @@
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-refresh-left"
|
||||
v-if="userInfo.authority.includes('administrator') && recycleMode"
|
||||
v-if="isAdministrator && recycleMode"
|
||||
@click="handleRecycleBack"
|
||||
>返 回
|
||||
</el-button>
|
||||
@@ -77,7 +77,7 @@
|
||||
<el-button
|
||||
plain
|
||||
type="info"
|
||||
v-if="userInfo.authority.includes('administrator') && !recycleMode"
|
||||
v-if="isAdministrator && !recycleMode"
|
||||
icon="el-icon-setting"
|
||||
@click="handleSetting"
|
||||
>批量授权配置
|
||||
@@ -93,7 +93,7 @@
|
||||
>
|
||||
<el-button
|
||||
plain
|
||||
v-if="userInfo.authority.includes('administrator') && !recycleMode"
|
||||
v-if="isAdministrator && !recycleMode"
|
||||
icon="el-icon-message-box"
|
||||
@click="handleDatasourceSetting"
|
||||
>数据源管理
|
||||
@@ -107,7 +107,7 @@
|
||||
>
|
||||
<el-button
|
||||
plain
|
||||
v-if="userInfo.authority.includes('administrator') && !recycleMode"
|
||||
v-if="isAdministrator && !recycleMode"
|
||||
icon="el-icon-set-up"
|
||||
@click="handlePackageSetting"
|
||||
>产品包管理
|
||||
@@ -116,13 +116,13 @@
|
||||
</template>
|
||||
<template #menu="scope">
|
||||
<el-link
|
||||
v-if="userInfo.authority.includes('administrator') && !recycleMode"
|
||||
v-if="isAdministrator && !recycleMode"
|
||||
type="primary"
|
||||
@click.stop="handleRowDatasource(scope.row)"
|
||||
>数据源
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="userInfo.authority.includes('administrator') && !recycleMode"
|
||||
v-if="isAdministrator && !recycleMode"
|
||||
type="primary"
|
||||
@click.stop="handleRowPackage(scope.row)"
|
||||
>产品包
|
||||
@@ -436,7 +436,7 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.tenant_add, false),
|
||||
|
||||
+92
-25
@@ -52,7 +52,7 @@
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-operation"
|
||||
v-if="userInfo.authority.includes('admin') && !auditMode"
|
||||
v-if="isAdministrator && !auditMode"
|
||||
@click="handleAudit"
|
||||
>审 核
|
||||
</el-button>
|
||||
@@ -60,7 +60,7 @@
|
||||
type="primary"
|
||||
plain
|
||||
:loading="iamSyncLoading"
|
||||
v-if="userInfo.authority.includes('admin') && !auditMode"
|
||||
v-if="isAdministrator && !auditMode"
|
||||
@click="handleIamSync"
|
||||
>同步人员
|
||||
</el-button>
|
||||
@@ -68,7 +68,7 @@
|
||||
type="success"
|
||||
plain
|
||||
icon="el-icon-check"
|
||||
v-if="userInfo.authority.includes('admin') && auditMode"
|
||||
v-if="isAdministrator && auditMode"
|
||||
@click="handleAuditPass"
|
||||
>通 过
|
||||
</el-button>
|
||||
@@ -76,7 +76,7 @@
|
||||
type="danger"
|
||||
plain
|
||||
icon="el-icon-close"
|
||||
v-if="userInfo.authority.includes('admin') && auditMode"
|
||||
v-if="isAdministrator && auditMode"
|
||||
@click="handleAuditRefuse"
|
||||
>拒 绝
|
||||
</el-button>
|
||||
@@ -84,7 +84,7 @@
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-refresh-left"
|
||||
v-if="userInfo.authority.includes('admin') && auditMode"
|
||||
v-if="isAdministrator && auditMode"
|
||||
@click="handleAuditBack"
|
||||
>返 回
|
||||
</el-button>
|
||||
@@ -97,7 +97,7 @@
|
||||
>角色配置
|
||||
</el-button>
|
||||
<el-dropdown
|
||||
v-if="userInfo.authority.includes('admin') && !auditMode"
|
||||
v-if="isAdministrator && !auditMode"
|
||||
@command="handleDataManageCommand"
|
||||
style="margin-left: 10px"
|
||||
>
|
||||
@@ -120,13 +120,13 @@
|
||||
</template>
|
||||
<template #menu-right>
|
||||
<el-button
|
||||
v-if="userInfo.authority.includes('admin') && !auditMode"
|
||||
v-if="isAdministrator && !auditMode"
|
||||
icon="el-icon-document"
|
||||
@click="$refs.authLog.openAuthLog()"
|
||||
>认证日志
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="userInfo.authority.includes('admin') && !auditMode"
|
||||
v-if="isAdministrator && !auditMode"
|
||||
icon="el-icon-lock"
|
||||
@click="$refs.authLock.openLockLog()"
|
||||
>锁定管理
|
||||
@@ -149,16 +149,16 @@
|
||||
<el-link type="primary">更多 </el-link>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="setLeader" v-if="userInfo.authority.includes('admin')">
|
||||
<el-dropdown-item command="setLeader" v-if="isAdministrator">
|
||||
{{ row.isLeader === 1 ? '取消主管' : '设置主管' }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="grantRole" v-if="permission.user_role">
|
||||
角色配置
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="authLog" v-if="userInfo.authority.includes('admin')">
|
||||
<el-dropdown-item command="authLog" v-if="isAdministrator">
|
||||
认证日志
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="lockUser" v-if="userInfo.authority.includes('admin')">
|
||||
<el-dropdown-item command="lockUser" v-if="isAdministrator">
|
||||
锁定账号
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="resetPassword" v-if="permission.user_reset">
|
||||
@@ -178,7 +178,14 @@
|
||||
<el-tag>{{ row.roleName }}</el-tag>
|
||||
</template>
|
||||
<template #deptName="{ row }">
|
||||
<el-tag>{{ row.deptName }}</el-tag>
|
||||
<template v-if="formatDeptNames(row.deptName).length">
|
||||
<el-tag
|
||||
v-for="name in formatDeptNames(row.deptName)"
|
||||
:key="name"
|
||||
class="user-dept-tag"
|
||||
>{{ name }}</el-tag>
|
||||
</template>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</avue-crud>
|
||||
|
||||
@@ -214,12 +221,36 @@
|
||||
<el-col :span="6"><el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="性别"><el-select v-model="form.sex"><el-option label="男" :value="1" /><el-option label="女" :value="2" /><el-option label="未知" :value="3" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="岗位" prop="postId"><el-tree-select v-model="form.postId" :data="postList" :props="{ label: 'postName', value: 'id' }" check-strictly /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="所属组织" prop="deptId"><el-cascader v-model="deptCascaderValue" :options="permissionDeptTree" :props="deptCascaderProps" clearable filterable placeholder="请选择 所属组织" /></el-form-item></el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="所属组织" prop="deptId">
|
||||
<el-cascader
|
||||
v-model="deptCascaderValue"
|
||||
:options="permissionDeptTree"
|
||||
:props="deptCascaderProps"
|
||||
clearable
|
||||
filterable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="请选择所属组织(可多选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6"><el-form-item label="工号"><el-input v-model="form.code" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="人员类别"><el-select v-model="form.personCategory"><el-option label="内部员工" :value="1" /><el-option label="承运商" :value="2" /></el-select></el-form-item></el-col>
|
||||
<el-col :span="18"><el-form-item label="数据权限范围"><el-radio-group v-model="form.dataScopeRange"><el-radio :value="1">仅所属组织</el-radio><el-radio :value="2">全部组织</el-radio><el-radio :value="3">自定义</el-radio></el-radio-group></el-form-item></el-col>
|
||||
<el-col :span="18" :offset="6"><el-form-item label="数据层级范围"><el-radio-group v-model="form.dataLevelRange"><el-radio :value="1">包含下级</el-radio><el-radio :value="2">仅本级</el-radio></el-radio-group></el-form-item></el-col>
|
||||
<el-col :span="24"><el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" maxlength="200" show-word-limit /></el-form-item></el-col>
|
||||
<el-col :span="18"><el-form-item label="数据层级范围"><el-radio-group v-model="form.dataLevelRange"><el-radio :value="1">包含下级</el-radio><el-radio :value="2">仅本级</el-radio></el-radio-group></el-form-item></el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</section-card>
|
||||
@@ -505,7 +536,24 @@ export default {
|
||||
password: [{ required: true, validator: validateLoginPassword, trigger: 'blur' }],
|
||||
realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
|
||||
deptId: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
|
||||
deptId: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
const ids = Array.isArray(value)
|
||||
? value.filter(item => item !== undefined && item !== null && item !== '')
|
||||
: value
|
||||
? String(value).split(',').filter(Boolean)
|
||||
: [];
|
||||
if (!ids.length) {
|
||||
callback(new Error('请选择所属组织'));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
},
|
||||
excelForm: {},
|
||||
excelOption: excelOption,
|
||||
@@ -520,7 +568,7 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
oaSyncDialogTitle() {
|
||||
if (this.oaSyncStatus === 'done') {
|
||||
return '同步完成';
|
||||
@@ -561,31 +609,36 @@ export default {
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
// 所属组织级联配置:checkStrictly 为 true 时任意一级都可选择
|
||||
// 所属组织级联配置:多选,checkStrictly 为 true 时任意一级都可选择
|
||||
deptCascaderProps() {
|
||||
return {
|
||||
label: 'title',
|
||||
value: 'id',
|
||||
children: 'children',
|
||||
emitPath: false,
|
||||
multiple: true,
|
||||
checkStrictly: true,
|
||||
expandTrigger: 'click',
|
||||
leaf: data => !data.children || data.children.length === 0,
|
||||
};
|
||||
},
|
||||
// 所属组织为单选级联:form.deptId 为数组(提交时 join),级联返回单个 id 值
|
||||
// 所属组织多选级联:form.deptId 为 id 数组(提交时 join)
|
||||
deptCascaderValue: {
|
||||
get() {
|
||||
const ids = Array.isArray(this.form.deptId)
|
||||
? this.form.deptId
|
||||
: this.form.deptId
|
||||
? [this.form.deptId]
|
||||
? String(this.form.deptId).split(',').filter(Boolean)
|
||||
: [];
|
||||
const id = ids.find(item => item !== undefined && item !== null && item !== '');
|
||||
return id === undefined ? '' : id;
|
||||
return ids
|
||||
.filter(item => item !== undefined && item !== null && item !== '')
|
||||
.map(item => (typeof item === 'number' ? item : String(item)));
|
||||
},
|
||||
set(value) {
|
||||
this.form.deptId = value ? [value] : [];
|
||||
this.form.deptId = Array.isArray(value)
|
||||
? value.filter(item => item !== undefined && item !== null && item !== '')
|
||||
: value
|
||||
? [value]
|
||||
: [];
|
||||
this.$refs.userFormRef?.validateField('deptId');
|
||||
},
|
||||
},
|
||||
@@ -844,6 +897,15 @@ export default {
|
||||
handleCustomerSelection(rows) {
|
||||
this.form.customerIds = rows.map(row => row.id);
|
||||
},
|
||||
formatDeptNames(deptName) {
|
||||
if (!deptName) {
|
||||
return [];
|
||||
}
|
||||
return String(deptName)
|
||||
.split(',')
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
},
|
||||
openUserDialog(mode, row = {}) {
|
||||
this.userDialogMode = mode;
|
||||
if (mode === 'add') {
|
||||
@@ -872,9 +934,10 @@ export default {
|
||||
}
|
||||
getUser(row.id).then(res => {
|
||||
const user = res.data.data;
|
||||
const deptIds = func.split(user.deptId || '');
|
||||
this.form = {
|
||||
...user,
|
||||
deptId: func.split(user.deptId || ''),
|
||||
deptId: Array.isArray(deptIds) ? deptIds : [],
|
||||
postId: func.split(user.postId || '')[0] || '',
|
||||
roleId: func.join(user.roleId || ''),
|
||||
dataScopeDeptIds: user.dataScopeDeptIds || [],
|
||||
@@ -1416,6 +1479,10 @@ export default {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.user-dept-tag + .user-dept-tag {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.avue-dialog .el-form-item__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
>
|
||||
<el-button
|
||||
plain
|
||||
v-if="userInfo.authority.includes('administrator')"
|
||||
v-if="isAdministrator"
|
||||
:loading="!componentLoaded"
|
||||
icon="el-icon-connection"
|
||||
@click="handleFormSetting"
|
||||
@@ -74,7 +74,7 @@
|
||||
>
|
||||
<el-button
|
||||
plain
|
||||
v-if="userInfo.authority.includes('administrator')"
|
||||
v-if="isAdministrator"
|
||||
icon="el-icon-message-box"
|
||||
@click="handleCodeSetting"
|
||||
>默认配置管理
|
||||
@@ -91,7 +91,7 @@
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="userInfo.authority.includes('administrator')"
|
||||
v-if="isAdministrator"
|
||||
class="none-border"
|
||||
@click.stop="handleBuildSingle(scope.row)"
|
||||
>生成
|
||||
@@ -302,7 +302,7 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.code_add, false),
|
||||
|
||||
@@ -648,7 +648,7 @@ import organizationSearch from '@/mixins/organization-search';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getLazyTree } from '@/api/base/region';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { getUploadHeaders } from '@/utils/upload';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import ImageUploadField from '@/components/image-upload-field/main.vue';
|
||||
@@ -877,7 +877,7 @@ export default {
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
|
||||
@@ -588,7 +588,7 @@ import {
|
||||
recognizeBaiduOcr,
|
||||
} from '@/api/transportCapacity/transport-ship';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { getUploadHeaders } from '@/utils/upload';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import ImageUploadField from '@/components/image-upload-field/main.vue';
|
||||
@@ -721,7 +721,7 @@ export default {
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
|
||||
@@ -142,6 +142,7 @@ import { getList as getVehicleList } from '@/api/transportCapacity/transport-veh
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { option, statusName } from '@/option/transportCapacity/vehicle-dispatch';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -178,7 +179,7 @@ export default {
|
||||
statusName,
|
||||
hasPermission(code) {
|
||||
return (
|
||||
String(this.userInfo?.authority || '').includes('admin') || this.permission?.[code] === true
|
||||
isAdministrator(this.userInfo) || this.permission?.[code] === true
|
||||
);
|
||||
},
|
||||
statusTagType(status) {
|
||||
|
||||
@@ -817,7 +817,7 @@ import {
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import organizationSearch from '@/mixins/organization-search';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { getUploadHeaders } from '@/utils/upload';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import ImageUploadField from '@/components/image-upload-field/main.vue';
|
||||
@@ -967,7 +967,7 @@ export default {
|
||||
},
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<el-link
|
||||
type="primary"
|
||||
@click.stop="handleAdd(scope.row, scope.index)"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
>新增子项
|
||||
</el-link>
|
||||
</template>
|
||||
@@ -149,7 +149,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.dict_add, false),
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<el-link
|
||||
type="primary"
|
||||
@click.stop="handleAdd(scope.row, scope.index)"
|
||||
v-if="userInfo.authority.includes('admin')"
|
||||
v-if="isAdministrator"
|
||||
>新增子项
|
||||
</el-link>
|
||||
</template>
|
||||
@@ -149,7 +149,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'permission']),
|
||||
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
|
||||
permissionList() {
|
||||
return {
|
||||
addBtn: this.validData(this.permission.dictbiz_add, false),
|
||||
|
||||
@@ -157,7 +157,7 @@ import { getDeptTree } from '@/api/system/dept';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -204,7 +204,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -171,7 +171,7 @@ import { getDeptTree } from '@/api/system/dept';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { excelOption, exportColumns, option } from '@/option/vehicle/annual-inspection-record';
|
||||
@@ -214,7 +214,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -470,6 +470,7 @@ import {
|
||||
} from '@/api/vehicle/credit-score-quantification';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { isAdministrator } from '@/utils/auth';
|
||||
|
||||
const createTimeRangeMap = {
|
||||
createTimeRange: ['createTimeStart', 'createTimeEnd'],
|
||||
@@ -599,7 +600,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
|
||||
@@ -161,9 +161,21 @@
|
||||
>
|
||||
复评
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
v-if="
|
||||
mkEnabled &&
|
||||
hasPermission('customer_archive_approve') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
@click="handleMkAudit(row)"
|
||||
>
|
||||
审核
|
||||
</el-link>
|
||||
<el-link
|
||||
type="success"
|
||||
v-if="
|
||||
!mkEnabled &&
|
||||
hasPermission('customer_archive_approve') &&
|
||||
row.approvalStatus === 'reviewing' &&
|
||||
row.reviewStatus === '已完成'
|
||||
@@ -174,7 +186,11 @@
|
||||
</el-link>
|
||||
<el-link
|
||||
type="warning"
|
||||
v-if="hasPermission('customer_archive_reject') && row.approvalStatus === 'reviewing'"
|
||||
v-if="
|
||||
!mkEnabled &&
|
||||
hasPermission('customer_archive_reject') &&
|
||||
row.approvalStatus === 'reviewing'
|
||||
"
|
||||
@click="handleReject(row)"
|
||||
>
|
||||
审核不通过
|
||||
@@ -404,7 +420,7 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="所在地区" prop="registeredDetailAddress" required>
|
||||
<el-form-item label="所在地区" prop="registeredRegionName" required>
|
||||
<div class="archive-form__address">
|
||||
<el-cascader
|
||||
ref="registeredRegionCascader"
|
||||
@@ -1544,12 +1560,13 @@ import {
|
||||
getDetail as getCreditScoreQuantificationDetail,
|
||||
} from '@/api/vehicle/credit-score-quantification';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { getMKApprovalUrl } from '@/api/system/business-process';
|
||||
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { getLazyTree } from '@/api/base/region';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { getUploadHeaders } from '@/utils/upload';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import { ArrowRight } from '@element-plus/icons-vue';
|
||||
@@ -1565,6 +1582,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'],
|
||||
};
|
||||
@@ -1631,19 +1652,6 @@ export default {
|
||||
}
|
||||
callback();
|
||||
};
|
||||
const validateRegisteredAddress = (rule, value, callback) => {
|
||||
const regionName = (this.archiveForm.registeredRegionName || '').trim();
|
||||
const detailAddress = String(value || '').trim();
|
||||
if (!regionName) {
|
||||
callback(new Error('请选择省市区'));
|
||||
} else if (!detailAddress) {
|
||||
callback(new Error('请输入详细地址'));
|
||||
} else if (detailAddress.length > 255) {
|
||||
callback(new Error('详细地址最多255个字符'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
return {
|
||||
form: {},
|
||||
query: {},
|
||||
@@ -1665,6 +1673,8 @@ export default {
|
||||
readonly: false,
|
||||
reviewMode: false,
|
||||
listActivated: false,
|
||||
// 表单页实例标记:创建时按路由落定,避免 keep-alive 缓存后随全局 $route 翻转
|
||||
isFormPageInstance: false,
|
||||
originalAccessType: '',
|
||||
activeTab: 'scores',
|
||||
archiveForm: this.emptyArchive(),
|
||||
@@ -1775,6 +1785,8 @@ export default {
|
||||
approved: { label: '审核通过', type: 'success' },
|
||||
rejected: { label: '审核不通过', type: 'danger' },
|
||||
},
|
||||
/** 业务字典 mk_template「开启MK」=1 */
|
||||
mkEnabled: false,
|
||||
formRules: {
|
||||
customerCode: [{ max: 20, message: '客商编号最多20个字符', trigger: 'blur' }],
|
||||
fullName: [{ required: true, message: '请输入客商名称', trigger: 'blur' }],
|
||||
@@ -1797,8 +1809,10 @@ export default {
|
||||
registeredCapital: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
maxCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
applyCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }],
|
||||
registeredRegionName: [{ required: true, message: '请选择省市区', trigger: 'change' }],
|
||||
registeredDetailAddress: [
|
||||
{ validator: validateRegisteredAddress, trigger: ['change', 'blur'] },
|
||||
{ required: true, message: '请输入详细地址', trigger: 'blur' },
|
||||
{ max: 255, message: '详细地址最多255个字符', trigger: 'blur' },
|
||||
],
|
||||
businessScope: [
|
||||
{
|
||||
@@ -2041,6 +2055,24 @@ export default {
|
||||
],
|
||||
clearable: true,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
sortable: true,
|
||||
type: 'datetime',
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
minWidth: 170,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -2057,9 +2089,11 @@ export default {
|
||||
this.openArchive({ id }, true);
|
||||
return;
|
||||
}
|
||||
this.isFormPageInstance = this.$route.path === customerArchiveFormRoute;
|
||||
this.initDeptTree();
|
||||
this.initRegionOptions();
|
||||
this.initBusinessDictionaries();
|
||||
this.initMkEnabled();
|
||||
this.initScoreQuantificationOptions();
|
||||
if (this.isArchivePage) {
|
||||
this.reviewMode = this.$route.query.review === '1';
|
||||
@@ -2069,7 +2103,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;
|
||||
@@ -2080,7 +2120,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
kingdeeLocked() {
|
||||
return this.archiveForm.kingdeeStatus === 'exist';
|
||||
@@ -2091,9 +2131,7 @@ export default {
|
||||
};
|
||||
},
|
||||
isArchivePage() {
|
||||
return (
|
||||
this.$route.path === '/vehicle/customer-archive/form' || this.isPublicViewPage
|
||||
);
|
||||
return this.isFormPageInstance || this.isPublicViewPage;
|
||||
},
|
||||
isPublicViewPage() {
|
||||
return (
|
||||
@@ -2282,6 +2320,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.unifiedCreditCode'(val) {
|
||||
this.handleKingdeeCreditCodeChange(val);
|
||||
},
|
||||
@@ -2293,6 +2341,9 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
markCustomerArchiveListNeedRefresh() {
|
||||
sessionStorage.setItem(customerArchiveListRefreshKey, '1');
|
||||
},
|
||||
hasPermission(code) {
|
||||
return this.isAdmin || this.validData(this.permission[code], false);
|
||||
},
|
||||
@@ -3017,12 +3068,12 @@ export default {
|
||||
handleRegisteredRegionChange(value) {
|
||||
if (!value || !value.length) {
|
||||
this.archiveForm.registeredRegionName = '';
|
||||
this.$refs.archiveForm?.validateField('registeredDetailAddress');
|
||||
this.$refs.archiveForm?.validateField('registeredRegionName');
|
||||
return;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.archiveForm.registeredRegionName = this.getRegisteredRegionLabels(value).join('');
|
||||
this.$refs.archiveForm?.validateField('registeredDetailAddress');
|
||||
this.$refs.archiveForm?.validateField('registeredRegionName');
|
||||
});
|
||||
},
|
||||
handleInvoiceRegisteredRegionChange(value) {
|
||||
@@ -3053,6 +3104,15 @@ export default {
|
||||
this.sortQualificationFiles();
|
||||
});
|
||||
},
|
||||
initMkEnabled() {
|
||||
loadMkEnabled()
|
||||
.then(enabled => {
|
||||
this.mkEnabled = !!enabled;
|
||||
})
|
||||
.catch(() => {
|
||||
this.mkEnabled = false;
|
||||
});
|
||||
},
|
||||
initScoreQuantificationOptions() {
|
||||
this.scoreQuantificationLoading = true;
|
||||
return getCreditScoreQuantificationList(1, 9999, { status: 1 })
|
||||
@@ -4152,27 +4212,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;
|
||||
@@ -4370,30 +4431,37 @@ export default {
|
||||
// 仅提交时落变更记录;保存不记录
|
||||
archive.recordChange = true;
|
||||
if (!this.validateScoreForApproval(archive)) return;
|
||||
submit(archive).then(res => {
|
||||
const data = res.data.data;
|
||||
const id = (data && typeof data === 'object' ? data.id : data) || this.archiveForm.id;
|
||||
if (!id) {
|
||||
this.closeArchive();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '保存成功,请在列表提交审批' });
|
||||
return;
|
||||
}
|
||||
const fullName =
|
||||
(data && typeof data === 'object' ? data.fullName : '') ||
|
||||
archive.fullName ||
|
||||
this.archiveForm.fullName ||
|
||||
'';
|
||||
this.submitCustomerApproval(
|
||||
id,
|
||||
fullName,
|
||||
archive.approvalStatus || this.archiveForm.approvalStatus
|
||||
).then(() => {
|
||||
this.closeArchive();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '提交成功!' });
|
||||
});
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '提交中',
|
||||
background: 'rgba(255, 255, 255, 0.7)',
|
||||
});
|
||||
submit(archive)
|
||||
.then(res => {
|
||||
const data = res.data.data;
|
||||
const id = (data && typeof data === 'object' ? data.id : data) || this.archiveForm.id;
|
||||
if (!id) {
|
||||
this.closeArchive();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '保存成功,请在列表提交审批' });
|
||||
return;
|
||||
}
|
||||
const fullName =
|
||||
(data && typeof data === 'object' ? data.fullName : '') ||
|
||||
archive.fullName ||
|
||||
this.archiveForm.fullName ||
|
||||
'';
|
||||
return this.submitCustomerApproval(
|
||||
id,
|
||||
fullName,
|
||||
archive.approvalStatus || this.archiveForm.approvalStatus
|
||||
).then(() => {
|
||||
this.closeArchive();
|
||||
this.onLoad(this.page);
|
||||
this.$message({ type: 'success', message: '提交成功!' });
|
||||
});
|
||||
})
|
||||
.finally(() => loading.close());
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -4402,7 +4470,7 @@ export default {
|
||||
submitCustomerApproval(id, fullName = '', approvalStatus = '') {
|
||||
return submitMkApprovalFlow({
|
||||
bizType: 'customer-archive',
|
||||
formInstanceId: id,
|
||||
bizId: id,
|
||||
subjectName: fullName,
|
||||
approvalStatus,
|
||||
}).then(() => submitApproval(id));
|
||||
@@ -5092,6 +5160,24 @@ export default {
|
||||
this.$message({ type: 'success', message: '已撤回至草稿状态!' });
|
||||
});
|
||||
},
|
||||
handleMkAudit(row) {
|
||||
if (!row?.id) {
|
||||
this.$message.warning('客商ID为空,无法打开审核页');
|
||||
return;
|
||||
}
|
||||
getMKApprovalUrl({ bizId: String(row.id) })
|
||||
.then(res => {
|
||||
const url = res?.data?.data;
|
||||
if (!url) {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank');
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error('获取审核跳转地址失败');
|
||||
});
|
||||
},
|
||||
handleApprove(row) {
|
||||
this.$confirm('是否确认审核通过?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
|
||||
@@ -152,7 +152,7 @@ import { getList as getShipList } from '@/api/transportCapacity/transport-ship';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { getDeptTree } from '@/api/system/dept';
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
@@ -186,7 +186,7 @@ export default {
|
||||
computed: {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
return (this.userInfo.authority || '').includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
|
||||
@@ -139,7 +139,7 @@ import { getDeptTree } from '@/api/system/dept';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { excelOption, exportColumns, option } from '@/option/vehicle/etc-record';
|
||||
@@ -180,7 +180,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
|
||||
@@ -218,7 +218,7 @@ import { getDeptTree } from '@/api/system/dept';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { handleImportExcel } from '@/utils/import-excel';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { getUploadHeaders } from '@/utils/upload';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -265,7 +265,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
@@ -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 || '');
|
||||
|
||||
@@ -194,7 +194,7 @@ import { exportBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
@@ -220,6 +220,8 @@ const exportColumns = [
|
||||
{ prop: 'nextMaintenanceTime', label: '下次保养时间' },
|
||||
{ prop: 'nextMaintenanceMileage', label: '下次保养里程/航程' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
];
|
||||
|
||||
export default {
|
||||
@@ -436,6 +438,14 @@ export default {
|
||||
slot: true,
|
||||
formslot: true,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
display: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -537,7 +547,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
@@ -620,11 +630,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 +657,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 +696,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 +814,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 = [];
|
||||
|
||||
@@ -167,7 +167,7 @@ import { exportBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { formatUpdateUserName } from '@/utils/audit';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
@@ -193,6 +193,8 @@ const exportColumns = [
|
||||
{ prop: 'factoryTime', label: '出厂时间' },
|
||||
{ prop: 'mileage', label: '里程/航程数' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'createUserName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
];
|
||||
|
||||
export default {
|
||||
@@ -409,6 +411,14 @@ export default {
|
||||
slot: true,
|
||||
formslot: true,
|
||||
},
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUserName',
|
||||
addDisplay: false,
|
||||
editDisplay: false,
|
||||
display: false,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
@@ -510,7 +520,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
|
||||
@@ -209,7 +209,7 @@ import { getDeptTree } from '@/api/system/dept';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -255,7 +255,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
@@ -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 || '');
|
||||
|
||||
@@ -171,7 +171,7 @@ import { getDeptTree } from '@/api/system/dept';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { excelOption, exportColumns, option } from '@/option/vehicle/oil-electric-record';
|
||||
@@ -213,7 +213,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -141,7 +141,7 @@ import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { excelOption, exportColumns, option } from '@/option/vehicle/other-expense-record';
|
||||
@@ -184,7 +184,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
|
||||
@@ -154,7 +154,7 @@ import { getDeptTree } from '@/api/system/dept';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -200,7 +200,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
@@ -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 || '');
|
||||
});
|
||||
|
||||
@@ -129,7 +129,7 @@ import { getDeptTree } from '@/api/system/dept';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
@@ -176,7 +176,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -196,7 +196,7 @@ import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { exportBlob } from '@/api/common';
|
||||
import { downloadXls } from '@/utils/util';
|
||||
import { openImportDialog } from '@/utils/import-excel';
|
||||
import { getToken } from '@/utils/auth';
|
||||
import { getToken, isAdministrator } from '@/utils/auth';
|
||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||
import AddressMapPicker from '@/components/address-map-picker/main.vue';
|
||||
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
|
||||
@@ -247,7 +247,7 @@ export default {
|
||||
...mapGetters(['permission', 'userInfo']),
|
||||
isAdmin() {
|
||||
const authority = this.userInfo.authority || '';
|
||||
return authority.includes('admin');
|
||||
return isAdministrator(this.userInfo);
|
||||
},
|
||||
permissionList() {
|
||||
return {
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* 首页看板 ECharts 通用配置生成器
|
||||
* 所有数据均为 Mock,对接后端统计接口后由各板块组件传入真实数据
|
||||
*/
|
||||
|
||||
/* 横向渐变色(排行条形图) */
|
||||
function hGrad(color, from = 0.9, to = 0.55) {
|
||||
return {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 1,
|
||||
y2: 0,
|
||||
colorStops: [
|
||||
{ offset: 0, color: color },
|
||||
{ offset: 1, color: hexFade(color, to) },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/* 十六进制颜色加透明度 */
|
||||
function hexFade(hex, alpha) {
|
||||
const h = hex.replace('#', '');
|
||||
const r = parseInt(h.slice(0, 2), 16);
|
||||
const g = parseInt(h.slice(2, 4), 16);
|
||||
const b = parseInt(h.slice(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
/* 千分位 */
|
||||
export function fmt(n) {
|
||||
if (n === null || n === undefined || n === '') return '-';
|
||||
const num = Number(n);
|
||||
if (Number.isNaN(num)) return n;
|
||||
return num.toLocaleString('zh-CN', { maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 排行条形图(左名称 + 中条形 + 右数值/涨跌)
|
||||
* items: [{ name, value, change? }] change 为百分比涨跌(number,可空)
|
||||
*/
|
||||
export function rankOption(items, { color = '#4a90e2', unit = '', nameWidth = 100 } = {}) {
|
||||
const sorted = [...items].sort((a, b) => a.value - b.value);
|
||||
const hasChange = sorted.some(i => typeof i.change === 'number');
|
||||
const rich = {
|
||||
v: { color: '#303133', fontWeight: 600, fontSize: 12, align: 'right' },
|
||||
u: { color: '#909399', fontSize: 11, align: 'right' },
|
||||
up: { color: '#f04747', fontSize: 11, align: 'right' },
|
||||
down: { color: '#2ba471', fontSize: 11, align: 'right' },
|
||||
};
|
||||
return {
|
||||
grid: { left: 4, right: hasChange ? 96 : 58, top: 6, bottom: 6, containLabel: true },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
formatter: p => {
|
||||
const d = p[0].data;
|
||||
let s = `${p[0].name}<br/>${d.value}${unit}`;
|
||||
if (typeof d.change === 'number') {
|
||||
s += `(环比 ${d.change >= 0 ? '+' : ''}${d.change}%)`;
|
||||
}
|
||||
return s;
|
||||
},
|
||||
},
|
||||
xAxis: { type: 'value', show: false },
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: sorted.map(i => i.name),
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: false },
|
||||
axisLabel: { color: '#606266', fontSize: 12, width: nameWidth, overflow: 'truncate' },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: 10,
|
||||
showBackground: true,
|
||||
backgroundStyle: { color: '#f0f2f5', borderRadius: [0, 5, 5, 0] },
|
||||
itemStyle: { color: hGrad(color), borderRadius: [0, 5, 5, 0] },
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
distance: 8,
|
||||
formatter: p => {
|
||||
const d = p.data;
|
||||
let s = `{v|${fmt(d.value)}${unit}}`;
|
||||
if (typeof d.change === 'number') {
|
||||
const cls = d.change >= 0 ? 'up' : 'down';
|
||||
const txt = `${d.change >= 0 ? '↑ +' : '↓ '}${d.change}%`;
|
||||
s += ` {${cls}|${txt}}`;
|
||||
}
|
||||
return s;
|
||||
},
|
||||
rich,
|
||||
},
|
||||
data: sorted.map(i => ({ value: i.value, change: i.change })),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 盈亏双向对比条形图(正=红,负=绿)
|
||||
* items: [{ name, value }]
|
||||
*/
|
||||
export function diffOption(items) {
|
||||
const sorted = [...items].sort((a, b) => b.value - a.value);
|
||||
return {
|
||||
grid: { left: 4, right: 60, top: 6, bottom: 6, containLabel: true },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
formatter: p => `${p[0].name}<br/>${p[0].value > 0 ? '盈利' : '亏损'} ${fmt(Math.abs(p[0].value))} 万元`,
|
||||
},
|
||||
xAxis: { type: 'value', show: false },
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: sorted.map(i => i.name),
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: false },
|
||||
axisLabel: { color: '#606266', fontSize: 12, width: 100, overflow: 'truncate' },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'bar',
|
||||
barWidth: 10,
|
||||
itemStyle: {
|
||||
borderRadius: 5,
|
||||
color: p => (p.value >= 0 ? '#e04b4b' : '#3aa15c'),
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
distance: 8,
|
||||
formatter: p => `${p.value > 0 ? '+' : ''}${fmt(p.value)}`,
|
||||
color: '#303133',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
},
|
||||
data: sorted.map(i => i.value),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 环形占比图
|
||||
* items: [{ name, value }]
|
||||
*/
|
||||
export function donutOption(items, { centerTitle = '', centerSub = '' } = {}) {
|
||||
const palette = ['#4a90e2', '#2bb3a3', '#e0b14c', '#8b5cf6', '#e04b4b', '#3aa15c', '#9aa5b5', '#5aa9ff'];
|
||||
return {
|
||||
color: palette,
|
||||
tooltip: { trigger: 'item', formatter: p => `${p.name}<br/>${fmt(p.value)}(${p.percent}%)` },
|
||||
legend: { show: false },
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['52%', '74%'],
|
||||
center: ['50%', '50%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: { borderColor: '#fff', borderWidth: 2 },
|
||||
label: {
|
||||
show: true,
|
||||
color: '#606266',
|
||||
fontSize: 11,
|
||||
formatter: p => `${p.name}\n${p.percent}%`,
|
||||
},
|
||||
labelLine: { length: 10, length2: 8, lineStyle: { color: '#c0c4cc' } },
|
||||
data: items,
|
||||
},
|
||||
],
|
||||
graphic: centerTitle
|
||||
? [
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: '44%',
|
||||
style: { text: centerTitle, fontSize: 18, fontWeight: 700, fill: '#303133' },
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
left: 'center',
|
||||
top: '56%',
|
||||
style: { text: centerSub, fontSize: 11, fill: '#909399' },
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组柱状图(收入/费用对比)
|
||||
* categories: string[];series: [{ name, data, color }]
|
||||
*/
|
||||
export function groupBarOption(categories, series) {
|
||||
return {
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { top: 0, right: 0, itemWidth: 14, itemHeight: 8, textStyle: { color: '#606266', fontSize: 12 } },
|
||||
grid: { left: 8, right: 16, top: 36, bottom: 4, containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisTick: { show: false },
|
||||
axisLine: { lineStyle: { color: '#e4e7ed' } },
|
||||
axisLabel: { color: '#909399', fontSize: 11, interval: 0, rotate: categories.length > 8 ? 30 : 0 },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { lineStyle: { color: '#f0f2f5' } },
|
||||
axisLabel: { color: '#909399', fontSize: 11 },
|
||||
},
|
||||
series: series.map(s => ({
|
||||
name: s.name,
|
||||
type: 'bar',
|
||||
barWidth: 14,
|
||||
itemStyle: { color: s.color, borderRadius: [3, 3, 0, 0] },
|
||||
data: s.data,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 面积折线图
|
||||
* categories: string[];series: [{ name, data, color }]
|
||||
*/
|
||||
export function lineAreaOption(categories, series, { rotate = false } = {}) {
|
||||
return {
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: series.length > 1
|
||||
? { top: 0, right: 0, itemWidth: 16, itemHeight: 8, textStyle: { color: '#606266', fontSize: 12 } }
|
||||
: { show: false },
|
||||
grid: { left: 8, right: 20, top: series.length > 1 ? 36 : 20, bottom: 4, containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: categories,
|
||||
axisTick: { show: false },
|
||||
axisLine: { lineStyle: { color: '#e4e7ed' } },
|
||||
axisLabel: { color: '#909399', fontSize: 11, rotate: rotate ? 30 : 0 },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { lineStyle: { color: '#f0f2f5' } },
|
||||
axisLabel: { color: '#909399', fontSize: 11 },
|
||||
},
|
||||
series: series.map(s => ({
|
||||
name: s.name,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
lineStyle: { color: s.color, width: 2 },
|
||||
itemStyle: { color: s.color },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0, y: 0, x2: 0, y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: hexFade(s.color, 0.18) },
|
||||
{ offset: 1, color: hexFade(s.color, 0.02) },
|
||||
],
|
||||
},
|
||||
},
|
||||
data: s.data,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 堆叠柱状图
|
||||
* categories: string[];series: [{ name, data, color }]
|
||||
*/
|
||||
export function stackBarOption(categories, series) {
|
||||
return {
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { top: 0, right: 0, itemWidth: 14, itemHeight: 8, textStyle: { color: '#606266', fontSize: 12 } },
|
||||
grid: { left: 8, right: 16, top: 36, bottom: 4, containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisTick: { show: false },
|
||||
axisLine: { lineStyle: { color: '#e4e7ed' } },
|
||||
axisLabel: { color: '#909399', fontSize: 11, interval: 0, rotate: categories.length > 8 ? 30 : 0 },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { lineStyle: { color: '#f0f2f5' } },
|
||||
axisLabel: { color: '#909399', fontSize: 11 },
|
||||
},
|
||||
series: series.map(s => ({
|
||||
name: s.name,
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
barWidth: 22,
|
||||
itemStyle: { color: s.color },
|
||||
data: s.data,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="wel-card rank-panel">
|
||||
<div class="panel-title">
|
||||
{{ title }}
|
||||
<span v-if="unit" class="panel-title__unit">{{ unit }}</span>
|
||||
</div>
|
||||
<wel-chart
|
||||
v-if="type === 'rank'"
|
||||
:option="rankOpt"
|
||||
:height="height"
|
||||
/>
|
||||
<wel-chart v-else :option="diffOpt" :height="height" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WelChart from './wel-chart.vue';
|
||||
import { rankOption, diffOption } from '../chart-options';
|
||||
|
||||
export default {
|
||||
name: 'rank-panel',
|
||||
components: { WelChart },
|
||||
props: {
|
||||
title: { type: String, required: true },
|
||||
unit: { type: String, default: '' },
|
||||
/* rank = 普通排行;diff = 盈亏双向对比 */
|
||||
type: { type: String, default: 'rank' },
|
||||
color: { type: String, default: '#4a90e2' },
|
||||
items: { type: Array, required: true },
|
||||
height: { type: String, default: '300px' },
|
||||
nameWidth: { type: Number, default: 100 },
|
||||
},
|
||||
computed: {
|
||||
rankOpt() {
|
||||
return rankOption(this.items, { color: this.color, unit: this.unit, nameWidth: this.nameWidth });
|
||||
},
|
||||
diffOpt() {
|
||||
return diffOption(this.items);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../dashboard.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<section class="wel-sec">
|
||||
<div class="wel-sec__head">
|
||||
<span class="wel-sec__title">业务单元情况</span>
|
||||
<wel-filters condition-label="业务单元" />
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col v-for="p in panels" :key="p.title" :xs="24" :md="12">
|
||||
<rank-panel
|
||||
:title="p.title"
|
||||
:unit="p.unit"
|
||||
:type="p.type"
|
||||
:color="p.color"
|
||||
:items="p.items"
|
||||
:height="p.height"
|
||||
:name-width="p.nameWidth"
|
||||
class="business-panel"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RankPanel from './rank-panel.vue';
|
||||
import WelFilters from './wel-filters.vue';
|
||||
|
||||
export default {
|
||||
name: 'section-business',
|
||||
components: { RankPanel, WelFilters },
|
||||
data() {
|
||||
return {
|
||||
// Mock:业务单元排行数据,对接统计接口后替换
|
||||
panels: [
|
||||
{
|
||||
title: '营业收入排行',
|
||||
unit: '万元',
|
||||
type: 'rank',
|
||||
color: '#4a90e2',
|
||||
height: '320px',
|
||||
items: [
|
||||
{ name: '广西物产集团', value: 5218.7, change: 12.3 },
|
||||
{ name: '南宁港务物流', value: 4183.4, change: 8.6 },
|
||||
{ name: '柳州钢铁物流', value: 3603.9, change: -2.4 },
|
||||
{ name: '北部湾港务', value: 3131.7, change: 5.1 },
|
||||
{ name: '桂林粮油储运', value: 2750.3, change: 9.8 },
|
||||
{ name: '玉林建材集团', value: 2455.5, change: -1.2 },
|
||||
{ name: '钦州石化物流', value: 2088.3, change: 6.5 },
|
||||
{ name: '贵港航运公司', value: 1823.5, change: 3.9 },
|
||||
{ name: '百色矿产运输', value: 1588.4, change: -4.7 },
|
||||
{ name: '梧州商贸物流', value: 1214.8, change: 2.2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '运输情况排行',
|
||||
unit: '单',
|
||||
type: 'rank',
|
||||
color: '#2bb3a3',
|
||||
height: '320px',
|
||||
items: [
|
||||
{ name: '广西物产集团', value: 238.2, change: 9.4 },
|
||||
{ name: '南宁港务物流', value: 196.3, change: 6.7 },
|
||||
{ name: '柳州钢铁物流', value: 168.1, change: 3.2 },
|
||||
{ name: '北部湾港务', value: 154.7, change: -2.8 },
|
||||
{ name: '桂林粮油储运', value: 139.2, change: 7.5 },
|
||||
{ name: '玉林建材集团', value: 128.6, change: 4.1 },
|
||||
{ name: '钦州石化物流', value: 118.3, change: -1.6 },
|
||||
{ name: '贵港航运公司', value: 106.2, change: 5.3 },
|
||||
{ name: '百色矿产运输', value: 98.8, change: 2.9 },
|
||||
{ name: '梧州商贸物流', value: 89.6, change: -3.4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '各部门毛利占比',
|
||||
unit: '%',
|
||||
type: 'rank',
|
||||
color: '#c9a227',
|
||||
height: '320px',
|
||||
items: [
|
||||
{ name: '公路运输一部', value: 30.7 },
|
||||
{ name: '公路运输二部', value: 28.2 },
|
||||
{ name: '铁路物流部', value: 23.2 },
|
||||
{ name: '水运事业部', value: 19.6 },
|
||||
{ name: '多式联运部', value: 15.2 },
|
||||
{ name: '仓储配送部', value: 13.1 },
|
||||
{ name: '冷链事业部', value: 10.4 },
|
||||
{ name: '大件运输部', value: 8.6 },
|
||||
{ name: '危险品运输部', value: 5.2 },
|
||||
{ name: '综合业务部', value: 2.8 },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '项目盈亏对比',
|
||||
unit: '万元',
|
||||
type: 'diff',
|
||||
color: '#e04b4b',
|
||||
height: '320px',
|
||||
items: [
|
||||
{ name: '铝锭外运项目', value: 156.5 },
|
||||
{ name: '冷链城配项目', value: 134.8 },
|
||||
{ name: '钢材南下项目', value: 118.2 },
|
||||
{ name: '粮食储备调运', value: 96.1 },
|
||||
{ name: '矿石短驳项目', value: 82.6 },
|
||||
{ name: '集装箱班列', value: 74.3 },
|
||||
{ name: '化工原料运输', value: 61.2 },
|
||||
{ name: '建材集散运输', value: 48.9 },
|
||||
{ name: '汽车零部件配送', value: 21.5 },
|
||||
{ name: '设备大件运输', value: 16.2 },
|
||||
{ name: '生鲜冷链专线', value: -8.3 },
|
||||
{ name: '危化品试点线', value: -25.4 },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '运单量排行',
|
||||
unit: '单',
|
||||
type: 'rank',
|
||||
color: '#4a90e2',
|
||||
height: '320px',
|
||||
items: [
|
||||
{ name: '南宁分部', value: 320, change: 6.3 },
|
||||
{ name: '柳州分部', value: 273, change: 4.8 },
|
||||
{ name: '桂林分部', value: 241, change: -2.1 },
|
||||
{ name: '玉林分部', value: 215, change: 8.2 },
|
||||
{ name: '钦州港分部', value: 203, change: 3.6 },
|
||||
{ name: '贵港分部', value: 187, change: -1.4 },
|
||||
{ name: '百色分部', value: 158, change: 5.7 },
|
||||
{ name: '梧州分部', value: 132, change: 2.3 },
|
||||
{ name: '北海分部', value: 108, change: -3.9 },
|
||||
{ name: '河池分部', value: 86, change: 1.6 },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '运输收入排行',
|
||||
unit: '万元',
|
||||
type: 'rank',
|
||||
color: '#8b5cf6',
|
||||
height: '320px',
|
||||
items: [
|
||||
{ name: '公路整车', value: 1863.2, change: 7.1 },
|
||||
{ name: '铁路整车', value: 1421.8, change: 4.5 },
|
||||
{ name: '水运集装箱', value: 1182.3, change: 9.2 },
|
||||
{ name: '公路零担', value: 968.5, change: 2.8 },
|
||||
{ name: '多式联运', value: 812.4, change: 11.6 },
|
||||
{ name: '航空货运', value: 655.7, change: -1.8 },
|
||||
{ name: '冷链专线', value: 542.1, change: 6.4 },
|
||||
{ name: '大件运输', value: 388.9, change: 3.1 },
|
||||
{ name: '危险品运输', value: 296.3, change: -2.5 },
|
||||
{ name: '仓储增值服务', value: 184.6, change: 8.8 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../dashboard.scss';
|
||||
|
||||
.business-panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<section class="wel-sec">
|
||||
<div class="wel-sec__head">
|
||||
<span class="wel-sec__title">财务情况</span>
|
||||
<wel-filters condition-label="核算口径" />
|
||||
</div>
|
||||
|
||||
<div class="wel-card finance-panel">
|
||||
<div class="panel-title">
|
||||
营业收支对比
|
||||
<span class="panel-title__unit">万元</span>
|
||||
</div>
|
||||
<wel-chart :option="barOpt" height="300px" />
|
||||
</div>
|
||||
|
||||
<div class="wel-card finance-panel">
|
||||
<div class="panel-title">
|
||||
主营业务收入/费用趋势
|
||||
<span class="panel-title__unit">万元</span>
|
||||
</div>
|
||||
<wel-chart :option="lineOpt" height="280px" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WelChart from './wel-chart.vue';
|
||||
import WelFilters from './wel-filters.vue';
|
||||
import { groupBarOption, lineAreaOption } from '../chart-options';
|
||||
|
||||
export default {
|
||||
name: 'section-finance',
|
||||
components: { WelChart, WelFilters },
|
||||
data() {
|
||||
const months = Array.from({ length: 12 }, (_, i) => `${i + 1}月`);
|
||||
return {
|
||||
months,
|
||||
// Mock:财务收支数据,对接统计接口后替换
|
||||
incomeData: [2860, 1520, 1180, 1150, 980, 920, 760, 720, 690, 520, 380, 260],
|
||||
costData: [2410, 1330, 1010, 980, 850, 800, 660, 630, 610, 470, 340, 230],
|
||||
trendIncome: [1780, 1850, 1810, 1880, 1920, 1950, 1720, 1650, 1830, 1900, 1980, 2060],
|
||||
trendCost: [1520, 1560, 1540, 1590, 1610, 1620, 1450, 1400, 1530, 1580, 1640, 1700],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
barOpt() {
|
||||
return groupBarOption(this.months, [
|
||||
{ name: '收入', data: this.incomeData, color: '#4a90e2' },
|
||||
{ name: '费用', data: this.costData, color: '#d9dce3' },
|
||||
]);
|
||||
},
|
||||
lineOpt() {
|
||||
return lineAreaOption(this.months, [
|
||||
{ name: '主营业务收入', data: this.trendIncome, color: '#4a90e2' },
|
||||
{ name: '主营业务费用', data: this.trendCost, color: '#2bb3a3' },
|
||||
]);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../dashboard.scss';
|
||||
|
||||
.finance-panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<section class="wel-sec">
|
||||
<div class="wel-sec__head">
|
||||
<span class="wel-sec__title">车队与线路</span>
|
||||
<wel-filters condition-label="运输方式" />
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :md="12">
|
||||
<rank-panel
|
||||
title="下游车队 TOP10"
|
||||
unit="单"
|
||||
color="#1a9e94"
|
||||
:items="fleetItems"
|
||||
height="320px"
|
||||
:name-width="120"
|
||||
class="fleet-panel"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12">
|
||||
<rank-panel
|
||||
title="运输线路 TOP5"
|
||||
unit="单"
|
||||
color="#c9a227"
|
||||
:items="routeItems"
|
||||
height="320px"
|
||||
:name-width="120"
|
||||
class="fleet-panel"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RankPanel from './rank-panel.vue';
|
||||
import WelFilters from './wel-filters.vue';
|
||||
|
||||
export default {
|
||||
name: 'section-fleet',
|
||||
components: { RankPanel, WelFilters },
|
||||
data() {
|
||||
return {
|
||||
// Mock:车队/线路排行数据,对接统计接口后替换
|
||||
fleetItems: [
|
||||
{ name: '顺达车队', value: 186.5 },
|
||||
{ name: '穗联物流车队', value: 158.2 },
|
||||
{ name: '宏运车队', value: 132.6 },
|
||||
{ name: '桂海运输车队', value: 118.4 },
|
||||
{ name: '南北快运车队', value: 96.7 },
|
||||
{ name: '恒通货运车队', value: 85.3 },
|
||||
{ name: '运达车队', value: 74.2 },
|
||||
{ name: '捷安物流车队', value: 62.8 },
|
||||
{ name: '广通运输车队', value: 54.6 },
|
||||
{ name: '长顺车队', value: 43.1 },
|
||||
],
|
||||
routeItems: [
|
||||
{ name: '南宁 → 柳州', value: 342.5 },
|
||||
{ name: '钦州港 → 南宁', value: 285.6 },
|
||||
{ name: '贵港 → 广州', value: 226.3 },
|
||||
{ name: '桂林 → 南宁', value: 168.2 },
|
||||
{ name: '玉林 → 深圳', value: 142.6 },
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../dashboard.scss';
|
||||
|
||||
.fleet-panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<section class="wel-sec">
|
||||
<div class="wel-sec__head">
|
||||
<span class="wel-sec__title">核心经营指标</span>
|
||||
<wel-filters />
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col v-for="card in cards" :key="card.label" :xs="24" :md="8">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-card__top">
|
||||
<span class="kpi-card__label">{{ card.label }}</span>
|
||||
<span
|
||||
class="kpi-card__badge"
|
||||
:class="card.trend >= 0 ? 'is-up' : 'is-down'"
|
||||
>
|
||||
环比 {{ card.trend >= 0 ? '+' : '' }}{{ card.trend }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="kpi-card__value">
|
||||
{{ card.value }}<span class="kpi-card__unit">{{ card.unit }}</span>
|
||||
</div>
|
||||
<ul class="kpi-card__subs">
|
||||
<li v-for="s in card.subs" :key="s.name">
|
||||
<span class="kpi-card__sub-name">{{ s.name }}</span>
|
||||
<span class="kpi-card__sub-value">{{ s.value }}</span>
|
||||
<span
|
||||
v-if="typeof s.change === 'number'"
|
||||
class="kpi-card__sub-change"
|
||||
:class="s.change >= 0 ? 'is-up' : 'is-down'"
|
||||
>
|
||||
{{ s.change >= 0 ? '↑' : '↓' }} {{ Math.abs(s.change) }}%
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WelFilters from './wel-filters.vue';
|
||||
|
||||
export default {
|
||||
name: 'section-kpi',
|
||||
components: { WelFilters },
|
||||
data() {
|
||||
return {
|
||||
// Mock:营业额/运单量/利润等核心指标,对接统计接口后替换
|
||||
cards: [
|
||||
{
|
||||
label: '营业额',
|
||||
value: '58,814.0',
|
||||
unit: '万元',
|
||||
trend: 12.6,
|
||||
subs: [
|
||||
{ name: '毛利率', value: '23.5%', change: 1.2 },
|
||||
{ name: '运输成本', value: '45,031 万元', change: -0.8 },
|
||||
{ name: '回款金额', value: '39,640 万元', change: 3.4 },
|
||||
{ name: '应收账款', value: '19,174 万元', change: -2.1 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '运单量',
|
||||
value: '1,497.1',
|
||||
unit: '单',
|
||||
trend: 8.4,
|
||||
subs: [
|
||||
{ name: '已完成', value: '1,205 单', change: 6.2 },
|
||||
{ name: '在途', value: '135 单', change: 12.0 },
|
||||
{ name: '待发运', value: '92 单', change: -4.5 },
|
||||
{ name: '异常单', value: '65 单', change: 1.8 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '运输利润',
|
||||
value: '2,058.2',
|
||||
unit: '万元',
|
||||
trend: 15.7,
|
||||
subs: [
|
||||
{ name: '利润率', value: '13.7%', change: 1.5 },
|
||||
{ name: '单均利润', value: '1.38 万元', change: 5.6 },
|
||||
{ name: '燃油成本', value: '8,931 万元', change: -3.2 },
|
||||
{ name: '过路费用', value: '4,208 万元', change: 0.9 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../dashboard.scss';
|
||||
|
||||
.kpi-card {
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
padding: 18px 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.kpi-card__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.kpi-card__label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.kpi-card__badge {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
|
||||
&.is-up {
|
||||
color: #f04747;
|
||||
background: #fdecec;
|
||||
}
|
||||
|
||||
&.is-down {
|
||||
color: #2ba471;
|
||||
background: #e8f7ef;
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-card__value {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
margin: 8px 0 12px;
|
||||
}
|
||||
|
||||
.kpi-card__unit {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.kpi-card__subs {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 10px 0 0;
|
||||
border-top: 1px solid #f0f2f5;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-card__sub-name {
|
||||
color: #909399;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.kpi-card__sub-value {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.kpi-card__sub-change {
|
||||
width: 56px;
|
||||
text-align: right;
|
||||
|
||||
&.is-up {
|
||||
color: #f04747;
|
||||
}
|
||||
|
||||
&.is-down {
|
||||
color: #2ba471;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<section class="wel-sec">
|
||||
<div class="wel-sec__head">
|
||||
<span class="wel-sec__title">项目情况</span>
|
||||
<wel-filters condition-label="项目状态" />
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :md="12">
|
||||
<rank-panel
|
||||
title="项目收入 TOP5"
|
||||
unit="万元"
|
||||
color="#8b5cf6"
|
||||
:items="incomeItems"
|
||||
height="280px"
|
||||
:name-width="180"
|
||||
class="project-panel"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12">
|
||||
<rank-panel
|
||||
title="项目运量 TOP5"
|
||||
unit="万吨"
|
||||
color="#2bb3a3"
|
||||
:items="volumeItems"
|
||||
height="280px"
|
||||
:name-width="180"
|
||||
class="project-panel"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RankPanel from './rank-panel.vue';
|
||||
import WelFilters from './wel-filters.vue';
|
||||
|
||||
export default {
|
||||
name: 'section-project',
|
||||
components: { RankPanel, WelFilters },
|
||||
data() {
|
||||
return {
|
||||
// Mock:项目排行数据,对接统计接口后替换
|
||||
incomeItems: [
|
||||
{ name: '广西华谊氯碱化工物流项目', value: 1285.6 },
|
||||
{ name: '柳钢集团钢材外运项目(二期)', value: 982.4 },
|
||||
{ name: '南宁轨道交通建材配送项目', value: 756.3 },
|
||||
{ name: '北部湾港集装箱短驳项目', value: 621.7 },
|
||||
{ name: '中粮集团粮食储备调运项目', value: 468.2 },
|
||||
],
|
||||
volumeItems: [
|
||||
{ name: '广西华谊氯碱化工物流项目', value: 32.5 },
|
||||
{ name: '柳钢集团钢材外运项目(二期)', value: 26.8 },
|
||||
{ name: '南宁轨道交通建材配送项目', value: 21.4 },
|
||||
{ name: '北部湾港集装箱短驳项目', value: 18.6 },
|
||||
{ name: '中粮集团粮食储备调运项目', value: 15.2 },
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../dashboard.scss';
|
||||
|
||||
.project-panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<section class="wel-sec">
|
||||
<div class="wel-sec__head">
|
||||
<span class="wel-sec__title">运输结构</span>
|
||||
<wel-filters condition-label="业务类型" />
|
||||
</div>
|
||||
|
||||
<!-- 小指标卡 -->
|
||||
<el-row :gutter="16" class="structure-cards">
|
||||
<el-col v-for="c in miniCards" :key="c.label" :xs="12" :md="6">
|
||||
<div class="mini-card structure-cards__item">
|
||||
<div class="mini-card__label">{{ c.label }}</div>
|
||||
<div class="mini-card__value">
|
||||
{{ c.value }}<span class="mini-card__unit">{{ c.unit }}</span>
|
||||
</div>
|
||||
<div v-if="typeof c.change === 'number'" class="mini-card__trend" :class="c.change >= 0 ? 'is-up' : 'is-down'">
|
||||
环比 {{ c.change >= 0 ? '+' : '' }}{{ c.change }}%
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 三个占比环形图 -->
|
||||
<el-row :gutter="16">
|
||||
<el-col v-for="d in donuts" :key="d.title" :xs="24" :md="8">
|
||||
<div class="wel-card donut-panel">
|
||||
<div class="panel-title">
|
||||
{{ d.title }}
|
||||
<span class="panel-title__unit">{{ d.unit }}</span>
|
||||
</div>
|
||||
<wel-chart :option="donutOpt(d)" height="230px" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WelChart from './wel-chart.vue';
|
||||
import WelFilters from './wel-filters.vue';
|
||||
import { donutOption } from '../chart-options';
|
||||
|
||||
export default {
|
||||
name: 'section-structure',
|
||||
components: { WelChart, WelFilters },
|
||||
data() {
|
||||
return {
|
||||
// Mock:运输结构统计,对接统计接口后替换
|
||||
miniCards: [
|
||||
{ label: '总运单量', value: '1,391', unit: '单', change: 30 },
|
||||
{ label: '在途运单', value: '487', unit: '单', change: 12.5 },
|
||||
{ label: '待发运', value: '135', unit: '单', change: -6.3 },
|
||||
{ label: '总运量', value: '15.2', unit: '万吨', change: 8.1 },
|
||||
],
|
||||
donuts: [
|
||||
{
|
||||
title: '运输方式占比',
|
||||
unit: '按运单量',
|
||||
centerTitle: '1,391',
|
||||
centerSub: '总运单(单)',
|
||||
items: [
|
||||
{ name: '公路', value: 620 },
|
||||
{ name: '铁路', value: 335 },
|
||||
{ name: '水运', value: 215 },
|
||||
{ name: '航空', value: 121 },
|
||||
{ name: '多式联运', value: 100 },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '货物类型占比',
|
||||
unit: '按运量',
|
||||
centerTitle: '15.2',
|
||||
centerSub: '总运量(万吨)',
|
||||
items: [
|
||||
{ name: '普货', value: 6.4 },
|
||||
{ name: '冷链', value: 3.2 },
|
||||
{ name: '大件', value: 2.6 },
|
||||
{ name: '危险品', value: 1.8 },
|
||||
{ name: '其他', value: 1.2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '客户类型占比',
|
||||
unit: '按运单量',
|
||||
centerTitle: '326',
|
||||
centerSub: '合作客户(家)',
|
||||
items: [
|
||||
{ name: '合同客户', value: 810 },
|
||||
{ name: '散单客户', value: 372 },
|
||||
{ name: '平台客户', value: 209 },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
donutOpt(d) {
|
||||
return donutOption(d.items, { centerTitle: d.centerTitle, centerSub: d.centerSub });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../dashboard.scss';
|
||||
|
||||
.structure-cards__item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.donut-panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<section class="wel-sec">
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :md="12">
|
||||
<div class="wel-card summary-panel">
|
||||
<div class="panel-title">
|
||||
车辆耗时占比
|
||||
<span class="panel-title__unit">按小时</span>
|
||||
</div>
|
||||
<wel-chart :option="donutOpt" height="260px" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12">
|
||||
<div class="wel-card summary-panel">
|
||||
<div class="panel-title">
|
||||
单据完成趋势
|
||||
<span class="panel-title__unit">按周</span>
|
||||
</div>
|
||||
<wel-chart :option="lineOpt" height="260px" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WelChart from './wel-chart.vue';
|
||||
import { donutOption, lineAreaOption } from '../chart-options';
|
||||
|
||||
export default {
|
||||
name: 'section-summary',
|
||||
components: { WelChart },
|
||||
data() {
|
||||
return {
|
||||
// Mock:车辆耗时与单据完成数据,对接统计接口后替换
|
||||
timeItems: [
|
||||
{ name: '在途运输', value: 86.2 },
|
||||
{ name: '装货等待', value: 32.5 },
|
||||
{ name: '卸货作业', value: 28.4 },
|
||||
{ name: '返程空驶', value: 21.6 },
|
||||
{ name: '场站等待', value: 18.3 },
|
||||
{ name: '维修保养', value: 9.8 },
|
||||
],
|
||||
weeks: ['1周', '2周', '3周', '4周', '5周', '6周', '7周'],
|
||||
doneData: [520, 545, 508, 532, 560, 548, 572],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
donutOpt() {
|
||||
return donutOption(this.timeItems, { centerTitle: '196.8', centerSub: '总耗时(小时)' });
|
||||
},
|
||||
lineOpt() {
|
||||
return lineAreaOption(this.weeks, [
|
||||
{ name: '完成单据', data: this.doneData, color: '#1a9e94' },
|
||||
]);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../dashboard.scss';
|
||||
|
||||
.summary-panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<section class="wel-sec">
|
||||
<div class="wel-sec__head">
|
||||
<span class="wel-sec__title">车务管理</span>
|
||||
<wel-filters condition-label="车辆类型" />
|
||||
</div>
|
||||
|
||||
<!-- 车务小指标卡 -->
|
||||
<el-row :gutter="16" class="vehicle-cards">
|
||||
<el-col v-for="c in miniCards" :key="c.label" :xs="12" :md="6">
|
||||
<div class="mini-card vehicle-cards__item">
|
||||
<div class="mini-card__label">{{ c.label }}</div>
|
||||
<div class="mini-card__value">
|
||||
{{ c.value }}<span class="mini-card__unit">{{ c.unit }}</span>
|
||||
</div>
|
||||
<div v-if="typeof c.change === 'number'" class="mini-card__trend" :class="c.change >= 0 ? 'is-up' : 'is-down'">
|
||||
环比 {{ c.change >= 0 ? '+' : '' }}{{ c.change }}%
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 车辆耗费构成堆叠柱状图 -->
|
||||
<div class="wel-card">
|
||||
<div class="panel-title">
|
||||
车辆耗费构成
|
||||
<span class="panel-title__unit">万元</span>
|
||||
</div>
|
||||
<wel-chart :option="stackOpt" height="320px" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WelChart from './wel-chart.vue';
|
||||
import WelFilters from './wel-filters.vue';
|
||||
import { stackBarOption } from '../chart-options';
|
||||
|
||||
export default {
|
||||
name: 'section-vehicle',
|
||||
components: { WelChart, WelFilters },
|
||||
data() {
|
||||
const months = Array.from({ length: 12 }, (_, i) => `${i + 1}月`);
|
||||
return {
|
||||
months,
|
||||
// Mock:车务统计与耗费数据,对接统计接口后替换
|
||||
miniCards: [
|
||||
{ label: '可用车辆', value: 47, unit: '辆', change: 4.2 },
|
||||
{ label: '维修保养中', value: 7, unit: '辆', change: -8.6 },
|
||||
{ label: '年检临期', value: 9, unit: '辆', change: 12.5 },
|
||||
{ label: '违章未处理', value: 12, unit: '起', change: -2.1 },
|
||||
],
|
||||
fuelData: [68, 52, 41, 36, 32, 38, 24, 46, 38, 28, 12, 14],
|
||||
repairData: [22, 25, 18, 20, 16, 18, 14, 12, 15, 11, 6, 8],
|
||||
otherData: [15, 14, 12, 13, 11, 12, 9, 11, 10, 8, 5, 6],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
stackOpt() {
|
||||
return stackBarOption(this.months, [
|
||||
{ name: '油费', data: this.fuelData, color: '#e0b14c' },
|
||||
{ name: '维修保养', data: this.repairData, color: '#58b98a' },
|
||||
{ name: '其他费用', data: this.otherData, color: '#4a90e2' },
|
||||
]);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../dashboard.scss';
|
||||
|
||||
.vehicle-cards__item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div ref="chartEl" class="wel-chart" :style="{ height: height }"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
export default {
|
||||
name: 'wel-chart',
|
||||
props: {
|
||||
option: { type: Object, required: true },
|
||||
height: { type: String, default: '260px' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
resizeObserver: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
option: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.render();
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.chart = echarts.init(this.$refs.chartEl);
|
||||
this.render();
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.chart && this.chart.resize();
|
||||
});
|
||||
this.resizeObserver.observe(this.$refs.chartEl);
|
||||
},
|
||||
activated() {
|
||||
/* keep-alive 恢复时重算尺寸 */
|
||||
this.chart && this.chart.resize();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.resizeObserver && this.resizeObserver.disconnect();
|
||||
this.chart && this.chart.dispose();
|
||||
this.chart = null;
|
||||
},
|
||||
methods: {
|
||||
render() {
|
||||
this.chart && this.chart.setOption(this.option, true);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wel-chart {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="wel-filter">
|
||||
<span class="wel-filter__label">统计时间</span>
|
||||
<el-select v-model="year" size="small">
|
||||
<el-option v-for="y in yearOptions" :key="y" :label="y + '年'" :value="y" />
|
||||
</el-select>
|
||||
<el-select v-model="range" size="small" class="wel-filter__range">
|
||||
<el-option v-for="r in rangeOptions" :key="r.value" :label="r.label" :value="r.value" />
|
||||
</el-select>
|
||||
<template v-if="conditionLabel">
|
||||
<span class="wel-filter__label">{{ conditionLabel }}</span>
|
||||
<el-select v-model="condition" size="small">
|
||||
<el-option label="全部" value="all" />
|
||||
</el-select>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'wel-filters',
|
||||
props: {
|
||||
conditionLabel: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
const now = new Date().getFullYear();
|
||||
return {
|
||||
year: now,
|
||||
yearOptions: [now, now - 1, now - 2, now - 3],
|
||||
range: 'year',
|
||||
rangeOptions: [
|
||||
{ label: '全年', value: 'year' },
|
||||
{ label: '上半年', value: 'h1' },
|
||||
{ label: '下半年', value: 'h2' },
|
||||
{ label: '自定义', value: 'custom' },
|
||||
],
|
||||
condition: 'all',
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../dashboard.scss';
|
||||
|
||||
.wel-filter__range {
|
||||
width: 110px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
/* 首页看板公共样式:各板块组件 scoped 引入 */
|
||||
.wel-card {
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
padding: 16px 20px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.wel-sec__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.wel-sec__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.wel-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.wel-filter__label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wel-filter .el-select {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.panel-title__unit {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.mini-card {
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
padding: 14px 18px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
|
||||
.mini-card__label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.mini-card__value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.mini-card__unit {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #909399;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.mini-card__trend {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
color: #909399;
|
||||
|
||||
&.is-up {
|
||||
color: #f04747;
|
||||
}
|
||||
|
||||
&.is-down {
|
||||
color: #2ba471;
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-767
@@ -1,788 +1,56 @@
|
||||
<template>
|
||||
<div class="wel-dashboard">
|
||||
<!-- 欢迎横幅 -->
|
||||
<div class="wel-banner">
|
||||
<div class="wel-banner__deco">
|
||||
<span class="wel-banner__circle wel-banner__circle--1"></span>
|
||||
<span class="wel-banner__circle wel-banner__circle--2"></span>
|
||||
<span class="wel-banner__circle wel-banner__circle--3"></span>
|
||||
<span class="wel-banner__dot-grid">
|
||||
<i v-for="n in 6" :key="'d'+n"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="wel-banner__left">
|
||||
<div class="wel-banner__avatar">
|
||||
<el-icon><Avatar /></el-icon>
|
||||
</div>
|
||||
<div class="wel-banner__info">
|
||||
<h2 class="wel-banner__hello">
|
||||
你好,{{ userName }}<span class="wel-banner__wave">👋</span>
|
||||
</h2>
|
||||
<p class="wel-banner__sub">
|
||||
TMS-ERP 货运管理系统 · {{ todayText }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wel-banner__right">
|
||||
<el-button type="primary" :icon="Plus" @click="goPath('/business/waybill-manage')">
|
||||
新建运单
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<section-kpi />
|
||||
<section-structure />
|
||||
<section-business />
|
||||
<section-project />
|
||||
<section-finance />
|
||||
<section-fleet />
|
||||
<section-vehicle />
|
||||
<section-summary />
|
||||
|
||||
<!-- 核心指标卡 -->
|
||||
<el-row :gutter="16" class="wel-metrics">
|
||||
<el-col v-for="m in metrics" :key="m.key" :xs="12" :sm="12" :md="6">
|
||||
<div class="metric-card" :style="{ '--accent': m.color }">
|
||||
<div class="metric-card__icon">
|
||||
<el-icon><component :is="m.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="metric-card__body">
|
||||
<div class="metric-card__label">{{ m.label }}</div>
|
||||
<div class="metric-card__value">
|
||||
{{ m.value }}<span class="metric-card__unit">{{ m.unit }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="metric-card__trend"
|
||||
:class="m.trend >= 0 ? 'is-up' : 'is-down'"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="m.trend >= 0 ? 'Top' : 'Bottom'" />
|
||||
</el-icon>
|
||||
{{ Math.abs(m.trend) }}%
|
||||
<span class="metric-card__trend-sub">{{ m.sub }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区 -->
|
||||
<el-row :gutter="16" class="wel-charts">
|
||||
<el-col :xs="24" :md="16">
|
||||
<div class="panel">
|
||||
<div class="panel__head">
|
||||
<span class="panel__title">运单量趋势</span>
|
||||
<span class="panel__extra">近 7 日(单)</span>
|
||||
</div>
|
||||
<div class="trend">
|
||||
<div v-for="b in trendBars" :key="b.label" class="trend__col">
|
||||
<div class="trend__val">{{ b.value }}</div>
|
||||
<div class="trend__bar">
|
||||
<div
|
||||
class="trend__fill"
|
||||
:style="{ height: b.percent + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="trend__label">{{ b.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :md="8">
|
||||
<div class="panel">
|
||||
<div class="panel__head">
|
||||
<span class="panel__title">货物类型分布</span>
|
||||
</div>
|
||||
<div class="donut-wrap">
|
||||
<div class="donut">
|
||||
<svg viewBox="0 0 140 140" class="donut__svg">
|
||||
<circle
|
||||
cx="70"
|
||||
cy="70"
|
||||
r="52"
|
||||
fill="none"
|
||||
stroke="#eef0f5"
|
||||
stroke-width="14"
|
||||
/>
|
||||
<circle
|
||||
v-for="(s, i) in donutSegments"
|
||||
:key="i"
|
||||
cx="70"
|
||||
cy="70"
|
||||
r="52"
|
||||
fill="none"
|
||||
:stroke="s.color"
|
||||
stroke-width="14"
|
||||
:stroke-dasharray="s.dash"
|
||||
:stroke-dashoffset="s.offset"
|
||||
transform="rotate(-90 70 70)"
|
||||
/>
|
||||
</svg>
|
||||
<div class="donut__center">
|
||||
<div class="donut__num">{{ donutTotal }}</div>
|
||||
<div class="donut__unit">总运单</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="donut-legend">
|
||||
<li v-for="(s, i) in donutSegments" :key="i">
|
||||
<span class="dot" :style="{ background: s.color }"></span>
|
||||
{{ s.name }}
|
||||
<em>{{ s.value }}%</em>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 下方区 -->
|
||||
<el-row :gutter="16" class="wel-bottom">
|
||||
<el-col :xs="24" :md="14">
|
||||
<div class="panel">
|
||||
<div class="panel__head">
|
||||
<span class="panel__title">运输方式占比</span>
|
||||
</div>
|
||||
<div class="bar-list">
|
||||
<div v-for="t in transportData" :key="t.name" class="bar-row">
|
||||
<div class="bar-row__name">{{ t.name }}</div>
|
||||
<div class="bar-row__track">
|
||||
<div
|
||||
class="bar-row__fill"
|
||||
:style="{ width: t.value + '%', background: t.color }"
|
||||
></div>
|
||||
</div>
|
||||
<div class="bar-row__val">{{ t.value }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :md="10">
|
||||
<div class="panel">
|
||||
<div class="panel__head">
|
||||
<span class="panel__title">预警提醒</span>
|
||||
<el-badge :value="alerts.length" class="panel__badge" />
|
||||
</div>
|
||||
<ul class="alert-list">
|
||||
<li
|
||||
v-for="(a, i) in alerts"
|
||||
:key="i"
|
||||
class="alert-item"
|
||||
:class="'lv-' + a.level"
|
||||
>
|
||||
<span class="alert-item__dot"></span>
|
||||
<div class="alert-item__main">
|
||||
<div class="alert-item__title">{{ a.title }}</div>
|
||||
<div class="alert-item__desc">{{ a.desc }}</div>
|
||||
</div>
|
||||
<div class="alert-item__time">{{ a.time }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<div class="panel wel-quick">
|
||||
<div class="panel__head">
|
||||
<span class="panel__title">快捷入口</span>
|
||||
</div>
|
||||
<div class="quick-grid">
|
||||
<div
|
||||
v-for="q in quickEntries"
|
||||
:key="q.path"
|
||||
class="quick-item"
|
||||
:style="{ '--accent': q.color }"
|
||||
@click="goPath(q.path)"
|
||||
>
|
||||
<div class="quick-item__icon">
|
||||
<el-icon><component :is="q.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="quick-item__name">{{ q.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="wel-footer">
|
||||
© 2026 gxwebsoft · TMS-ERP 货运管理系统
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import SectionKpi from './components/section-kpi.vue';
|
||||
import SectionStructure from './components/section-structure.vue';
|
||||
import SectionBusiness from './components/section-business.vue';
|
||||
import SectionProject from './components/section-project.vue';
|
||||
import SectionFinance from './components/section-finance.vue';
|
||||
import SectionFleet from './components/section-fleet.vue';
|
||||
import SectionVehicle from './components/section-vehicle.vue';
|
||||
import SectionSummary from './components/section-summary.vue';
|
||||
|
||||
export default {
|
||||
name: 'welIndex',
|
||||
data() {
|
||||
return {
|
||||
Plus,
|
||||
// ===== Mock 数据:实际接入时替换为对应后端接口返回值 =====
|
||||
// 核心指标:可对接各模块统计接口(如 /blade-business/waybill/count)
|
||||
metrics: [
|
||||
{ key: 'waybill', label: '今日运单', value: 186, unit: '单', icon: 'Document', color: '#409eff', trend: 12.5, sub: '较昨日' },
|
||||
{ key: 'transit', label: '在途运输', value: 43, unit: '单', icon: 'Van', color: '#67c23a', trend: 3.2, sub: '在途运单' },
|
||||
{ key: 'todo', label: '待办事项', value: 9, unit: '项', icon: 'Bell', color: '#e6a23c', trend: -2.0, sub: '待处理' },
|
||||
{ key: 'capacity', label: '运力车辆', value: 128, unit: '辆', icon: 'Truck', color: '#9254de', trend: 5.1, sub: '在运+待命' },
|
||||
],
|
||||
// 运单趋势:可对接 /blade-business/waybill/trend?days=7
|
||||
trendData: [
|
||||
{ label: '周一', value: 120 },
|
||||
{ label: '周二', value: 132 },
|
||||
{ label: '周三', value: 101 },
|
||||
{ label: '周四', value: 158 },
|
||||
{ label: '周五', value: 190 },
|
||||
{ label: '周六', value: 142 },
|
||||
{ label: '周日', value: 186 },
|
||||
],
|
||||
// 货物类型分布:可对接 /blade-business/waybill/cargoStat
|
||||
cargoData: [
|
||||
{ name: '普货', value: 42, color: '#409eff' },
|
||||
{ name: '冷链', value: 21, color: '#13c2c2' },
|
||||
{ name: '大件', value: 16, color: '#fa8c16' },
|
||||
{ name: '危险品', value: 12, color: '#f5222d' },
|
||||
{ name: '其他', value: 9, color: '#bfbfbf' },
|
||||
],
|
||||
// 运输方式占比:可对接 /blade-business/waybill/transportStat
|
||||
transportData: [
|
||||
{ name: '海运', value: 48, color: '#409eff' },
|
||||
{ name: '铁路', value: 22, color: '#52c41a' },
|
||||
{ name: '公路', value: 20, color: '#fa8c16' },
|
||||
{ name: '空运', value: 10, color: '#722ed1' },
|
||||
],
|
||||
// 预警提醒:可对接车辆年检/保险/违章 + 合同待审批等接口聚合
|
||||
alerts: [
|
||||
{ level: 'danger', title: '3 辆车年检临期', desc: '桂B·12345 等将于 7 日内到期', time: '今天' },
|
||||
{ level: 'warning', title: '5 单合同待审批', desc: '客户「广西物产」等提交待审核', time: '今天' },
|
||||
{ level: 'danger', title: '2 起违章未处理', desc: '桂A·88xxx 超速记录待缴纳', time: '昨天' },
|
||||
{ level: 'info', title: '4 辆保险下月到期', desc: '建议提前安排续保', time: '本周' },
|
||||
],
|
||||
// 快捷入口:path 与菜单路由一致
|
||||
quickEntries: [
|
||||
{ name: '运单管理', path: '/business/waybill-manage', icon: 'Document', color: '#409eff' },
|
||||
{ name: '运输计划', path: '/business/transport-plan', icon: 'Promotion', color: '#13c2c2' },
|
||||
{ name: '合同管理', path: '/business/contract-manage', icon: 'Files', color: '#fa8c16' },
|
||||
{ name: '车辆管理', path: '/vehicle/customer-archive', icon: 'Van', color: '#67c23a' },
|
||||
{ name: '维修记录', path: '/vehicle/maintenance-record', icon: 'Tools', color: '#9254de' },
|
||||
{ name: '违章记录', path: '/vehicle/violation-record', icon: 'Warning', color: '#f5222d' },
|
||||
{ name: '工作待办', path: '/work/todo', icon: 'Bell', color: '#e6a23c' },
|
||||
{ name: '报表中心', path: '/report/reportlist', icon: 'DataLine', color: '#1677ff' },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo']),
|
||||
userName() {
|
||||
return this.userInfo.real_name || this.userInfo.user_name || '用户';
|
||||
},
|
||||
todayText() {
|
||||
const d = new Date();
|
||||
const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
||||
const pad = n => (n < 10 ? '0' + n : n);
|
||||
return `${d.getFullYear()}年${pad(d.getMonth() + 1)}月${pad(d.getDate())}日 ${week[d.getDay()]}`;
|
||||
},
|
||||
trendBars() {
|
||||
const max = Math.max(...this.trendData.map(d => d.value)) || 1;
|
||||
return this.trendData.map(d => ({
|
||||
...d,
|
||||
percent: Math.round((d.value / max) * 100),
|
||||
}));
|
||||
},
|
||||
donutTotal() {
|
||||
return this.cargoData.reduce((s, d) => s + d.value, 0);
|
||||
},
|
||||
donutSegments() {
|
||||
const total = this.cargoData.reduce((s, d) => s + d.value, 0) || 1;
|
||||
const r = 52;
|
||||
const c = 2 * Math.PI * r;
|
||||
let acc = 0;
|
||||
return this.cargoData.map(d => {
|
||||
const len = (d.value / total) * c;
|
||||
const seg = { ...d, dash: `${len} ${c - len}`, offset: -acc };
|
||||
acc += len;
|
||||
return seg;
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
goPath(path) {
|
||||
this.$router.push({ path });
|
||||
},
|
||||
components: {
|
||||
SectionKpi,
|
||||
SectionStructure,
|
||||
SectionBusiness,
|
||||
SectionProject,
|
||||
SectionFinance,
|
||||
SectionFleet,
|
||||
SectionVehicle,
|
||||
SectionSummary,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wel-dashboard {
|
||||
padding: 8px 4px;
|
||||
padding: 12px 4px 0;
|
||||
background: #f5f7fa;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* 横幅 */
|
||||
.wel-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 26px 30px;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 横幅装饰元素 */
|
||||
.wel-banner__deco {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wel-banner__circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(115, 209, 61, 0.07);
|
||||
&--1 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
right: -40px;
|
||||
top: -60px;
|
||||
background: rgba(115, 209, 61, 0.1);
|
||||
}
|
||||
&--2 {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
right: 120px;
|
||||
bottom: -50px;
|
||||
background: rgba(115, 209, 61, 0.06);
|
||||
}
|
||||
&--3 {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
left: 25%;
|
||||
top: -20px;
|
||||
background: rgba(115, 209, 61, 0.05);
|
||||
}
|
||||
}
|
||||
.wel-banner__dot-grid {
|
||||
position: absolute;
|
||||
right: 28px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 5px);
|
||||
gap: 8px;
|
||||
i {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: rgba(115, 209, 61, 0.2);
|
||||
&:nth-child(3n) { background: rgba(115, 209, 61, 0.35); }
|
||||
&:nth-child(3n + 1) { background: rgba(115, 209, 61, 0.12); }
|
||||
}
|
||||
}
|
||||
.wel-banner__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.wel-banner__avatar {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: rgba(115, 209, 61, 0.15);
|
||||
color: #389e0d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 30px;
|
||||
}
|
||||
.wel-banner__hello {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.wel-banner__wave {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.wel-banner__sub {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.wel-banner__right {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 指标卡 */
|
||||
.wel-metrics {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 图表区:左右面板等高 */
|
||||
.wel-charts {
|
||||
margin-bottom: 0;
|
||||
align-items: stretch;
|
||||
|
||||
.el-col {
|
||||
display: flex;
|
||||
|
||||
> .panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 下方区:左右面板等高 */
|
||||
.wel-bottom {
|
||||
align-items: stretch;
|
||||
|
||||
.el-col {
|
||||
display: flex;
|
||||
|
||||
> .panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
.metric-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 18px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-left: 4px solid var(--accent);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
.metric-card__icon {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26px;
|
||||
color: #fff;
|
||||
background: var(--accent);
|
||||
}
|
||||
.metric-card__label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
.metric-card__value {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.metric-card__unit {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: #909399;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.metric-card__trend {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
&.is-up {
|
||||
color: #67c23a;
|
||||
}
|
||||
&.is-down {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
.metric-card__trend-sub {
|
||||
color: #c0c4cc;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* 面板通用 */
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 18px 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
|
||||
// 当面板在 flex 列容器中时,内容区自动撑满
|
||||
.trend,
|
||||
.donut-wrap,
|
||||
.bar-list,
|
||||
.alert-list {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.panel__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
padding-left: 8px;
|
||||
border-left: 4px solid #409eff;
|
||||
}
|
||||
.panel__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.panel__extra {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.panel__badge {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
/* 趋势柱 */
|
||||
.trend {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
height: 200px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.trend__col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.trend__val {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.trend__bar {
|
||||
flex: 1;
|
||||
width: 60%;
|
||||
max-width: 36px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 6px 6px 0 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
overflow: hidden;
|
||||
}
|
||||
.trend__fill {
|
||||
width: 100%;
|
||||
background: linear-gradient(180deg, #5aa9ff, #409eff);
|
||||
border-radius: 6px 6px 0 0;
|
||||
transition: height 0.4s ease;
|
||||
}
|
||||
.trend__label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 环形图 */
|
||||
.donut-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
min-height: 200px;
|
||||
}
|
||||
.donut {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.donut__svg {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
.donut__center {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.donut__num {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
}
|
||||
.donut__unit {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
.donut-legend {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
padding: 5px 0;
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
em {
|
||||
margin-left: auto;
|
||||
font-style: normal;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 横向条 */
|
||||
.bar-list {
|
||||
padding: 4px 0;
|
||||
}
|
||||
.bar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.bar-row__name {
|
||||
width: 48px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
.bar-row__track {
|
||||
flex: 1;
|
||||
height: 10px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin: 0 12px;
|
||||
}
|
||||
.bar-row__fill {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
.bar-row__val {
|
||||
width: 44px;
|
||||
text-align: right;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
/* 预警列表 */
|
||||
.alert-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.alert-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
.alert-item__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.alert-item.lv-danger .alert-item__dot {
|
||||
background: #f56c6c;
|
||||
}
|
||||
.alert-item.lv-warning .alert-item__dot {
|
||||
background: #e6a23c;
|
||||
}
|
||||
.alert-item.lv-info .alert-item__dot {
|
||||
background: #409eff;
|
||||
}
|
||||
.alert-item__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.alert-item__title {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
}
|
||||
.alert-item__desc {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.alert-item__time {
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* 快捷入口 */
|
||||
.wel-quick {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
.quick-item {
|
||||
background: #fafbfc;
|
||||
border: 1px solid #eef0f5;
|
||||
border-radius: 10px;
|
||||
padding: 18px 12px;
|
||||
.wel-footer {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
&:hover {
|
||||
border-color: var(--accent);
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
.quick-item__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0 auto 10px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
color: #fff;
|
||||
background: var(--accent);
|
||||
}
|
||||
.quick-item__name {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.quick-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
.wel-banner {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
padding: 8px 0 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user