11 Commits

Author SHA1 Message Date
b2894lxlx aa26beda28 MK公开页、预结算体验优化、地图选址与认证密码规则完善 2026-09-23 18:36:18 +08:00
b2894lxlx a919781d4f 完善配载确认、承运商组织过滤司机与地址展示
配载草稿确认生成正式单并优化详情/列表地址市区展示;运单调度等按承运商联系人所属组织筛选司机车辆;统一地图搜索结果与登录默认账号清理。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:35:27 +08:00
b2894lxlx 694fe1a433 🔧 开发代理默认指向本地网关
补充本地网关端口说明,避免误把 Nacos 8080 当作业务网关。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 14:49:09 +08:00
b2894lxlx ae1f2fa09f 💄 调整临时额度弹窗字段宽度与 dialogWidth 配置
统一表单控件宽度,并修正 Avue dialogWidth 数值写法避免重复 px。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 14:49:09 +08:00
b2894lxlx fbfb563254 项目管理客户信息增加是否广西百强列
在企业性质右侧展示客商档案的广西百强标识。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 14:49:09 +08:00
b2894lxlx 5352d799af 运单详情已完成隐藏变更路线并移除路线编辑
已完成运单不再展示变更运输路线入口,变更弹窗去掉拖拽路线编辑区。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 14:49:09 +08:00
b2894lxlx e2e2eb9a2f 完善合同附件上传回填、类型匹配与列表附件弹窗
修复上传成功后表格不回显,附件类型可切换,未匹配文件名默认其他文件;列表附件上传支持合同文件/其它附件。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 14:49:09 +08:00
b2894lxlx 6212d68a77 付款申请表单优化与结算单过滤,并完善相关体验
进度预付仅可选预结算、尾款付款可选正式/预结算;同步收款账户、申请金额校验、关闭标签与费用明细残留等交互,并补充运单复制需求说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 00:17:07 +08:00
b2894lxlx ce1ce02551 完善变更记录、客商评分与车辆表单
变更记录详情统一为居中分页弹窗,并去掉经办人等信息行。
客商评分支持删除、提交前自评校验、复评后才能审批,以及 0 分提交。
车辆外廓尺寸和载质量单位改到输入框内,认证审核标签按四字换行,使用部门必填并默认当前部门。
临时额度的 -1 占位金额显示为空。新增项目编号可留空由系统生成。
修复缓存总单页在打开新增项目时误请求缺少 id 的详情接口。
详情页状态与运输方式标签样式统一。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 20:13:44 +08:00
b2894lxlx 3a8442e522 Merge remote-tracking branch 'websoft/master' 2026-09-22 12:06:53 +08:00
b2894lxlx ac87208ba9 MK公开页、预结算体验优化、地图选址与认证密码规则完善 2026-09-22 12:06:48 +08:00
68 changed files with 5214 additions and 1593 deletions
+3 -2
View File
@@ -3,8 +3,9 @@
VITE_APP_ENV = 'development' VITE_APP_ENV = 'development'
#接口地址 #接口地址
# 开发环境建议填 [/api]:由下方 vite.config.mjs 的 proxy 同源转发到后端(172.16.203.228:8000),避免跨域(CORS)被浏览器拦截 # 开发环境建议填 [/api]:由 vite.config.mjs 的 proxy 同源转发到本地网关 http://localhost80
# 如需直连线上绝对地址(如 http://172.16.203.228:8000/api),必须让后端 CORS 把 Allow-Origin 改为具体前端域名,不能用 `*`(配合 credentials 会被浏览器拒绝) # 本地需先启动:Nacos + blade-gateway(80) + blade-auth + blade-system 等;8080 端口一般是 Nacos 控制台不是网关
# 如需临时联调远程,把 vite proxy target 改为 http://172.16.203.228:8000 并去掉 rewrite
VITE_APP_API=/api VITE_APP_API=/api
#调试参数 #调试参数
+1 -1
View File
@@ -24,7 +24,7 @@ export const syncKingdeeBatch = ids =>
export const paymentTypeOptions = [ export const paymentTypeOptions = [
{ label: '项目预付', value: 'project_advance' }, { label: '项目预付', value: 'project_advance' },
{ label: '进度预付', value: 'progress_advance' }, { label: '进度预付', value: 'progress_advance' },
{ label: '结算付款', value: 'settlement_payment' }, { label: '尾款付款', value: 'settlement_payment' },
]; ];
export const approvalStatusOptions = [ export const approvalStatusOptions = [
{ label: '草稿', value: 'draft' }, { label: '草稿', value: 'draft' },
+9
View File
@@ -48,6 +48,15 @@ export const syncIamOrganizations = () => {
}); });
}; };
export const clearNonTopDept = signal => {
return request({
url: '/blade-system/dept/clear-non-top',
method: 'post',
timeout: 60000,
signal,
});
};
export const syncOaCompany = (current = 1, size = 20, signal) => { export const syncOaCompany = (current = 1, size = 20, signal) => {
return request({ return request({
url: '/blade-system/dept/sync-oa-company', url: '/blade-system/dept/sync-oa-company',
+65 -21
View File
@@ -10,12 +10,8 @@
<el-button type="primary" :loading="searching" @click="searchKeyword">搜索</el-button> <el-button type="primary" :loading="searching" @click="searchKeyword">搜索</el-button>
</div> </div>
<div class="address-map-picker__content"> <div class="address-map-picker__content">
<map-search-results
v-if="searchResults.length"
:results="searchResults"
@select="selectSearchResult"
/>
<div ref="map" class="address-map-picker__map"></div> <div ref="map" class="address-map-picker__map"></div>
<map-search-results :results="searchResults" @select="selectSearchResult" />
</div> </div>
<div class="address-map-picker__info">{{ status }}</div> <div class="address-map-picker__info">{{ status }}</div>
<template #footer> <template #footer>
@@ -28,6 +24,8 @@
</template> </template>
<script> <script>
import { normalizeMapSearchResults } from '@/utils/map-search';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315'; const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a'; const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let amapLoader; let amapLoader;
@@ -42,6 +40,14 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
longitude: {
type: [String, Number],
default: '',
},
latitude: {
type: [String, Number],
default: '',
},
}, },
emits: ['update:modelValue', 'confirm'], emits: ['update:modelValue', 'confirm'],
data() { data() {
@@ -66,9 +72,11 @@ export default {
this.visible = value; this.visible = value;
if (value) { if (value) {
this.keyword = this.address || ''; this.keyword = this.address || '';
this.selected = {};
this.status = '可搜索地址或点击地图选点';
this.searchResults = []; this.searchResults = [];
this.selected = this.buildInitialSelection();
this.status = this.selected.longitude
? this.selected.address || '已选点,可确认回填'
: '可搜索地址或点击地图选点';
} }
}, },
}, },
@@ -82,6 +90,18 @@ export default {
} }
}, },
methods: { methods: {
buildInitialSelection() {
const longitude = Number(this.longitude);
const latitude = Number(this.latitude);
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
return {};
}
return {
longitude,
latitude,
address: this.address || '',
};
},
loadAmap() { loadAmap() {
if (window.AMap && window.AMap.Map) { if (window.AMap && window.AMap.Map) {
return Promise.resolve(); return Promise.resolve();
@@ -110,15 +130,27 @@ export default {
return new Promise(resolve => { return new Promise(resolve => {
this.$nextTick(() => { this.$nextTick(() => {
if (!this.$refs.map) return resolve(null); if (!this.$refs.map) return resolve(null);
const initial = this.buildInitialSelection();
const center =
Number.isFinite(initial.longitude) && Number.isFinite(initial.latitude)
? [initial.longitude, initial.latitude]
: [116.40769, 39.89945];
if (!this.amap) { if (!this.amap) {
this.amap = new window.AMap.Map(this.$refs.map, { this.amap = new window.AMap.Map(this.$refs.map, {
center: [116.40769, 39.89945], center,
zoom: 11, zoom: initial.longitude ? 14 : 11,
}); });
this.amap.on('click', event => this.pickPoint(event.lnglat)); this.amap.on('click', event => this.pickPoint(event.lnglat));
} else { } else {
this.amap.resize(); this.amap.resize();
this.clearMarker(); this.clearMarker();
this.amap.setZoomAndCenter(initial.longitude ? 14 : 11, center);
}
if (initial.longitude) {
const point = new window.AMap.LngLat(initial.longitude, initial.latitude);
this.renderMarker(point);
this.selected = { ...initial };
this.status = initial.address || '已选点,可确认回填';
} }
resolve(this.amap); resolve(this.amap);
}); });
@@ -180,19 +212,14 @@ export default {
.then(result => { .then(result => {
if (!result) return; if (!result) return;
const geocodes = result.geocodes || (result.location ? [result] : []); const geocodes = result.geocodes || (result.location ? [result] : []);
this.searchResults = geocodes.map((item, index) => ({ this.searchResults = normalizeMapSearchResults(geocodes, keyword);
id: item.id || index, const point = this.searchResults[0]?.location || result.location;
name: item.formattedAddress || item.address || keyword,
address: item.formattedAddress || item.address || keyword,
location: item.location,
}));
const point = geocodes[0]?.location || result.location;
if (!point) { if (!point) {
throw new Error('未找到匹配地址'); throw new Error('未找到匹配地址');
} }
return this.$nextTick().then(() => { return this.$nextTick().then(() => {
this.amap?.resize(); this.amap?.resize();
this.pickPoint(point, geocodes[0]?.formattedAddress || keyword); this.pickPoint(point, this.searchResults[0]?.address || keyword);
}); });
}) })
.catch(() => { .catch(() => {
@@ -254,11 +281,19 @@ export default {
} }
}, },
confirm() { confirm() {
if (!this.selected.longitude || !this.selected.latitude) {
this.$message.warning('请先搜索或点击地图完成选点');
return;
}
if (!this.selected.address) { if (!this.selected.address) {
this.$message.warning('请等待地址解析完成后再确认'); this.$message.warning('请等待地址解析完成后再确认');
return; return;
} }
this.$emit('confirm', this.selected.address); this.$emit('confirm', {
address: this.selected.address,
longitude: this.selected.longitude,
latitude: this.selected.latitude,
});
this.visible = false; this.visible = false;
}, },
}, },
@@ -278,7 +313,6 @@ export default {
} }
&__map { &__map {
flex: 1 1 auto;
width: 100%; width: 100%;
min-width: 0; min-width: 0;
height: 420px; height: 420px;
@@ -286,9 +320,19 @@ export default {
} }
&__content { &__content {
display: flex; position: relative;
gap: 12px; display: block;
min-width: 0; min-width: 0;
overflow: visible;
.address-map-picker__map {
position: relative;
z-index: 1;
}
:deep(.map-search-results) {
z-index: 2000;
}
} }
&__info { &__info {
@@ -0,0 +1,108 @@
<template>
<el-dialog
:model-value="modelValue"
title="变更记录详情"
append-to-body
destroy-on-close
align-center
width="1100px"
class="change-record-detail-dialog"
@update:model-value="$emit('update:modelValue', $event)"
@open="resetPage"
>
<el-table
:data="pageRows"
border
max-height="60vh"
:show-overflow-tooltip="false"
>
<el-table-column prop="field" label="变更字段" min-width="180" />
<el-table-column
prop="before"
label="变更前"
min-width="360"
class-name="change-record-detail-value"
/>
<el-table-column
prop="after"
label="变更后"
min-width="500"
class-name="change-record-detail-value"
/>
</el-table>
<el-empty v-if="!total" description="暂无变更内容" :image-size="60" />
<div class="change-record-detail-dialog__pager">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
background
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
/>
</div>
<template #footer>
<el-button type="primary" @click="$emit('update:modelValue', false)">关闭</el-button>
</template>
</el-dialog>
</template>
<script>
export default {
name: 'ChangeRecordDetailDialog',
props: {
modelValue: {
type: Boolean,
default: false,
},
rows: {
type: Array,
default: () => [],
},
},
emits: ['update:modelValue'],
data() {
return {
currentPage: 1,
pageSize: 10,
};
},
computed: {
total() {
return this.rows.length;
},
pageRows() {
const start = (this.currentPage - 1) * this.pageSize;
return this.rows.slice(start, start + this.pageSize);
},
},
watch: {
rows() {
const maxPage = Math.max(1, Math.ceil(this.total / this.pageSize) || 1);
if (this.currentPage > maxPage) this.currentPage = 1;
},
},
methods: {
resetPage() {
this.currentPage = 1;
this.pageSize = 10;
},
},
};
</script>
<style lang="scss" scoped>
.change-record-detail-dialog__pager {
display: flex;
justify-content: flex-end;
margin-top: 12px;
}
:deep(.change-record-detail-value .cell) {
white-space: pre-wrap;
word-break: break-word;
overflow: visible;
text-overflow: clip;
line-height: 1.6;
}
</style>
+178 -19
View File
@@ -1,20 +1,55 @@
<template> <template>
<div v-if="results.length" class="map-search-results"> <div v-if="results.length" class="map-search-results">
<div class="map-search-results__title">搜索结果</div> <div class="map-search-results__list">
<el-empty v-if="!results.length" description="暂无搜索结果" :image-size="60" />
<div <div
v-for="(item, index) in results" v-for="(item, index) in pageResults"
:key="item.id || index" :key="item.id || `${currentPage}-${index}`"
class="map-search-results__item" class="map-search-results__item"
@click="$emit('select', item)" :class="{ 'is-active': isActive(item, index) }"
@click="handleSelect(item, index)"
> >
<div class="map-search-results__name">{{ item.name || item.address || '未命名地址' }}</div> <img
<div class="map-search-results__address">{{ item.address || item.name || '-' }}</div> v-if="item.photo || item.image"
class="map-search-results__thumb"
:src="item.photo || item.image"
alt=""
/>
<div class="map-search-results__body">
<div class="map-search-results__name">
{{ item.name || item.address || '未命名地址' }}
</div>
<div class="map-search-results__address">
地址{{ item.address || item.name || '-' }}
</div>
</div>
</div>
</div>
<div v-if="totalPages > 1" class="map-search-results__pager">
<button
v-for="page in visiblePages"
:key="page"
type="button"
class="map-search-results__page"
:class="{ 'is-active': page === currentPage }"
@click="currentPage = page"
>
{{ page }}
</button>
<button
type="button"
class="map-search-results__page map-search-results__page--next"
:disabled="currentPage >= totalPages"
@click="goNext"
>
下一页
</button>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
const PAGE_SIZE = 5;
export default { export default {
props: { props: {
results: { results: {
@@ -23,43 +58,167 @@ export default {
}, },
}, },
emits: ['select'], emits: ['select'],
data() {
return {
currentPage: 1,
activeKey: '',
};
},
computed: {
totalPages() {
return Math.max(1, Math.ceil((this.results || []).length / PAGE_SIZE));
},
pageResults() {
const start = (this.currentPage - 1) * PAGE_SIZE;
return (this.results || []).slice(start, start + PAGE_SIZE);
},
visiblePages() {
const maxButtons = 3;
let start = Math.max(1, this.currentPage - 1);
let end = Math.min(this.totalPages, start + maxButtons - 1);
start = Math.max(1, end - maxButtons + 1);
const pages = [];
for (let page = start; page <= end; page += 1) {
pages.push(page);
}
return pages;
},
},
watch: {
results() {
this.currentPage = 1;
this.activeKey = '';
},
},
methods: {
resultKey(item, index) {
return item?.id ?? `${this.currentPage}-${index}`;
},
isActive(item, index) {
return this.activeKey === this.resultKey(item, index);
},
handleSelect(item, index) {
this.activeKey = this.resultKey(item, index);
this.$emit('select', item);
},
goNext() {
if (this.currentPage < this.totalPages) {
this.currentPage += 1;
}
},
},
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.map-search-results { .map-search-results {
width: 260px; position: absolute;
height: 420px; top: 0;
overflow-y: auto; left: 0;
border: 1px solid #eff1f7; z-index: 2000;
display: flex;
flex-direction: column;
width: 360px;
max-height: 360px;
overflow: hidden;
background: #fff; background: #fff;
flex: 0 0 260px; border: 1px solid #dcdfe6;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
pointer-events: auto;
&__title { &__list {
padding: 10px 12px; flex: 1 1 auto;
border-bottom: 1px solid #eff1f7; min-height: 0;
font-weight: 600; overflow-y: auto;
} }
&__item { &__item {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 10px 12px; padding: 10px 12px;
cursor: pointer; cursor: pointer;
border-bottom: 1px solid #f5f5f5; border-bottom: 1px solid #ebeef5;
} }
&__item:hover { &__item:hover,
background: #f5f9ff; &__item.is-active {
background: #f5f5f5;
}
&__thumb {
flex: 0 0 48px;
width: 48px;
height: 48px;
object-fit: cover;
border: 1px solid #ebeef5;
background: #fafafa;
}
&__body {
flex: 1 1 auto;
min-width: 0;
} }
&__name { &__name {
overflow: hidden;
color: #303133; color: #303133;
font-size: 14px; font-size: 14px;
font-weight: 600;
line-height: 1.4;
white-space: nowrap;
text-overflow: ellipsis;
} }
&__address { &__address {
margin-top: 4px; margin-top: 4px;
color: #909399; color: #909399;
font-size: 12px; font-size: 12px;
line-height: 1.5;
word-break: break-all;
}
&__pager {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
gap: 6px;
align-items: center;
padding: 8px 10px;
border-top: 1px solid #ebeef5;
background: #fff;
}
&__page {
min-width: 28px;
height: 28px;
padding: 0 8px;
color: #606266;
font-size: 13px;
line-height: 26px;
text-align: center;
background: #fff;
border: 1px solid #dcdfe6;
cursor: pointer;
&:hover:not(:disabled) {
color: #409eff;
border-color: #409eff;
}
&.is-active {
color: #409eff;
border-color: #409eff;
}
&:disabled {
color: #c0c4cc;
cursor: not-allowed;
}
&--next {
min-width: 56px;
}
} }
} }
</style> </style>
@@ -212,7 +212,14 @@ export default {
.filter(Boolean); .filter(Boolean);
}, },
acceptText() { acceptText() {
return this.acceptedList.join(','); return this.acceptedList
.map(item => {
const value = String(item).trim();
if (!value || value.includes('/') || value.startsWith('.')) return value;
return `.${value}`;
})
.filter(Boolean)
.join(',');
}, },
tipText() { tipText() {
return this.tip || `${UNIFIED_ATTACHMENT_TIP_PREFIX}${this.maxSize}M`; return this.tip || `${UNIFIED_ATTACHMENT_TIP_PREFIX}${this.maxSize}M`;
@@ -304,12 +311,22 @@ export default {
}); });
}, },
handleSuccess(response, file, files) { handleSuccess(response, file, files) {
if (!response || response.success === false || (response.code && response.code !== 200)) { const code = response?.code;
const codeInvalid = code !== undefined && code !== null && code !== '' && Number(code) !== 200;
if (!response || response.success === false || codeInvalid) {
this.$message.error((response && response.msg) || '上传失败'); this.$message.error((response && response.msg) || '上传失败');
return; return;
} }
this.emitUploadFiles(this.multiple ? files : [file]); if (file && !file.response) {
this.$emit('success', this.normalizeUploadFile(file)); file.response = response;
}
const normalized = this.normalizeUploadFile(file, response);
if (!normalized.url) {
this.$message.error('上传成功但未返回文件地址');
return;
}
this.$emit('success', normalized);
this.emitUploadFiles(this.multiple ? files : [file], normalized);
this.$message.success('上传成功'); this.$message.success('上传成功');
}, },
handleChange(file, files) { handleChange(file, files) {
@@ -323,35 +340,89 @@ export default {
this.emitUploadFiles(files); this.emitUploadFiles(files);
return true; return true;
}, },
emitUploadFiles(files) { emitUploadFiles(files, preferredFile) {
const list = files const list = (files || [])
.filter(file => file.status === 'success' || this.getFileUrl(file)) .filter(file => {
.map(this.normalizeUploadFile) if (file?.status === 'fail') return false;
return (
file?.status === 'success' ||
this.getFileUrl(file) ||
file?.response?.data ||
file?.response?.link ||
file?.response
);
})
.map(file => this.normalizeUploadFile(file))
.filter(item => item.url); .filter(item => item.url);
this.$emit('update:modelValue', list);
this.$emit('change', list); if (preferredFile?.url) {
const exists = list.some(
item =>
(preferredFile.uid && item.uid && String(item.uid) === String(preferredFile.uid)) ||
item.url === preferredFile.url
);
if (!exists) list.push(preferredFile);
}
const current = this.parseValue(this.modelValue).map(item => ({
...item,
url: this.getFileUrl(item),
}));
const merged = [...current];
list.forEach(file => {
const index = merged.findIndex(
item =>
(file.uid && item.uid && String(item.uid) === String(file.uid)) ||
(file.url && this.getFileUrl(item) === file.url)
);
if (index >= 0) merged.splice(index, 1, { ...merged[index], ...file });
else merged.push(file);
});
const result = merged.filter(item => this.getFileUrl(item));
this.$emit('update:modelValue', result);
this.$emit('change', result);
}, },
normalizeUploadFile(file) { normalizeUploadFile(file, responseOverride) {
const data = (file.response && file.response.data) || file.data || file; const response = responseOverride || file?.response;
const url = data.link || data.url || data.domain || file.url || ''; let data = {};
if (response && typeof response === 'object') {
if (response.data && typeof response.data === 'object') {
data = response.data;
} else if (response.link || response.url || response.domain) {
data = response;
}
}
if (!data.link && !data.url && !data.domain) {
data = file?.data && typeof file.data === 'object' ? file.data : file || {};
}
const url =
data.link ||
data.url ||
data.domain ||
data.fileLink ||
data.fileUrl ||
file?.url ||
file?.link ||
'';
const originalName = const originalName =
data.originalName || data.originalName ||
file.originalName || file?.originalName ||
file.name || file?.name ||
data.name || data.name ||
this.getFileName(url) || this.getFileName(url) ||
'附件'; '附件';
const extension = this.getExtension({ name: originalName, url }); const extension = this.getExtension({ name: originalName, url });
return { return {
...data, ...data,
uid: file.uid || data.uid, uid: file?.uid || data.uid,
originalName, originalName,
name: originalName, name: originalName,
url, url,
link: data.link || url, link: data.link || url,
size: data.size || data.attachSize || file.size || '', size: data.size || data.attachSize || file?.size || '',
extension, extension,
mimeType: data.mimeType || data.contentType || file.raw?.type || MIME_MAP[extension] || '', mimeType: data.mimeType || data.contentType || file?.raw?.type || MIME_MAP[extension] || '',
}; };
}, },
handlePreview(file) { handlePreview(file) {
+1
View File
@@ -42,6 +42,7 @@ export const createOption = () => ({
label: '计量单位编码', label: '计量单位编码',
prop: 'unitCode', prop: 'unitCode',
minWidth: 150, minWidth: 150,
span: 24,
search: true, search: true,
searchOrder: 4, searchOrder: 4,
searchSpan: 6, searchSpan: 6,
@@ -334,7 +334,7 @@ export const option = {
}, },
...auditColumns.map(column => ({ ...column, hide: true })), ...auditColumns.map(column => ({ ...column, hide: true })),
])), ])),
dialogWidth: '96%', dialogWidth: 1100,
menuFixed: 'right', menuFixed: 'right',
menuWidth: 320, menuWidth: 320,
index: false, index: false,
+7 -1
View File
@@ -543,7 +543,8 @@ export const option = {
prop: 'projectName', prop: 'projectName',
formslot: true, formslot: true,
search: true, search: true,
searchLabel: '项目', searchLabel: '项目名称',
searchPlaceholder: '请选择或输入',
searchOrder: 22, searchOrder: 22,
span: 6, span: 6,
order: 890, order: 890,
@@ -555,6 +556,7 @@ export const option = {
prop: 'customerName', prop: 'customerName',
search: true, search: true,
searchLabel: '客户名称', searchLabel: '客户名称',
searchPlaceholder: '请选择或输入',
searchOrder: 21, searchOrder: 21,
minWidth: 150, minWidth: 150,
addDisplay: false, addDisplay: false,
@@ -576,6 +578,7 @@ export const option = {
prop: 'driverName', prop: 'driverName',
search: true, search: true,
searchLabel: '司机名称', searchLabel: '司机名称',
searchPlaceholder: '请选择或输入',
searchOrder: 14, searchOrder: 14,
minWidth: 120, minWidth: 120,
display: false, display: false,
@@ -626,6 +629,7 @@ export const option = {
prop: 'carrierName', prop: 'carrierName',
search: true, search: true,
searchLabel: '承运商名称', searchLabel: '承运商名称',
searchPlaceholder: '请选择或输入',
searchOrder: 15, searchOrder: 15,
minWidth: 150, minWidth: 150,
display: false, display: false,
@@ -729,6 +733,7 @@ export const option = {
prop: 'planName', prop: 'planName',
formslot: true, formslot: true,
search: true, search: true,
searchPlaceholder: '请选择或输入',
searchOrder: 7, searchOrder: 7,
span: 6, span: 6,
order: 870, order: 870,
@@ -738,6 +743,7 @@ export const option = {
label: '货物类型', label: '货物类型',
prop: 'cargoType', prop: 'cargoType',
search: true, search: true,
searchPlaceholder: '请选择或输入',
searchOrder: 18, searchOrder: 18,
formatter: row => formatGoodsField(row, ['cargoType', 'goodsType', 'typeName']), formatter: row => formatGoodsField(row, ['cargoType', 'goodsType', 'typeName']),
minWidth: 130, minWidth: 130,
+3 -2
View File
@@ -5,6 +5,7 @@ export const GRANT_TYPE_DIC = [
{ label: '社交登录', value: 'social' }, { label: '社交登录', value: 'social' },
{ label: '客户端凭证', value: 'client_credentials' }, { label: '客户端凭证', value: 'client_credentials' },
{ label: '刷新令牌', value: 'refresh_token' }, { label: '刷新令牌', value: 'refresh_token' },
{ label: '退出登录', value: 'logout' },
]; ];
export const authLogOption = { export const authLogOption = {
@@ -38,7 +39,7 @@ export const authLogOption = {
prop: 'realName', prop: 'realName',
}, },
{ {
label: '授权类型', label: '操作类型',
prop: 'grantType', prop: 'grantType',
type: 'select', type: 'select',
search: true, search: true,
@@ -80,7 +81,7 @@ export const authLogOption = {
width: 120, width: 120,
}, },
{ {
label: '登录时间', label: '操作时间',
prop: 'loginTime', prop: 'loginTime',
sortable: true, sortable: true,
span: 24, span: 24,
+2 -5
View File
@@ -1,12 +1,9 @@
import { getDeptLazyTree } from '@/api/system/dept'; import { getDeptLazyTree } from '@/api/system/dept';
import { validateLoginPassword } from '@/utils/validate';
export const userOption = safe => { export const userOption = safe => {
const validatePass = (rule, value, callback) => { const validatePass = (rule, value, callback) => {
if (value === '') { validateLoginPassword(rule, value, callback);
callback(new Error('请输入密码'));
} else {
callback();
}
}; };
const validatePass2 = (rule, value, callback) => { const validatePass2 = (rule, value, callback) => {
if (value === '') { if (value === '') {
+2 -2
View File
@@ -13,8 +13,8 @@ export default {
data() { data() {
return { return {
loginForm: { loginForm: {
username: 'admin', username: '',
password: '123456', password: '',
}, },
}; };
}, },
+3 -1
View File
@@ -24,6 +24,7 @@
--> -->
<userLogin v-if="activeName === 'user'"></userLogin> <userLogin v-if="activeName === 'user'"></userLogin>
<registerLogin v-else-if="activeName === 'register'"></registerLogin> <registerLogin v-else-if="activeName === 'register'"></registerLogin>
<!-- 暂时默认账号密码登录IAM 选择入口先隐藏
<div v-else class="iam-login"> <div v-else class="iam-login">
<el-button type="primary" class="login-submit" @click.prevent="handleIamLogin"> <el-button type="primary" class="login-submit" @click.prevent="handleIamLogin">
IAM统一身份认证 IAM统一身份认证
@@ -32,6 +33,7 @@
账号密码登录 账号密码登录
</el-button> </el-button>
</div> </div>
-->
</div> </div>
</div> </div>
</div> </div>
@@ -61,7 +63,7 @@ export default {
return { return {
website: website, website: website,
time: '', time: '',
activeName: 'iam', activeName: 'user',
socialForm: { socialForm: {
tenantId: '000000', tenantId: '000000',
source: '', source: '',
+2 -2
View File
@@ -93,9 +93,9 @@ export default {
//角色ID //角色ID
roleId: '', roleId: '',
//用户名 //用户名
username: 'admin', username: '',
//密码 //密码
password: 'admin', password: '',
//账号类型 //账号类型
type: 'account', type: 'account',
//验证码的值 //验证码的值
+6 -6
View File
@@ -87,7 +87,7 @@ export default [
{ {
path: '/business/project-apply/public-view', path: '/business/project-apply/public-view',
name: '查看项目信息', name: '查看项目信息',
component: () => import('@/views/mk/public-biz-view.vue'), component: () => import('@/views/business/project-apply-public-view.vue'),
meta: { meta: {
keepAlive: false, keepAlive: false,
isTab: false, isTab: false,
@@ -98,7 +98,7 @@ export default [
{ {
path: '/business/contract-manage/public-view', path: '/business/contract-manage/public-view',
name: '查看合同信息', name: '查看合同信息',
component: () => import('@/views/mk/public-biz-view.vue'), component: () => import('@/views/business/contract-manage-public-view.vue'),
meta: { meta: {
keepAlive: false, keepAlive: false,
isTab: false, isTab: false,
@@ -109,7 +109,7 @@ export default [
{ {
path: '/business/waybill-manage/public-view', path: '/business/waybill-manage/public-view',
name: '查看运单信息', name: '查看运单信息',
component: () => import('@/views/mk/public-biz-view.vue'), component: () => import('@/views/business/waybill-manage-public-view.vue'),
meta: { meta: {
keepAlive: false, keepAlive: false,
isTab: false, isTab: false,
@@ -120,7 +120,7 @@ export default [
{ {
path: '/settlement/pre-settlement/public-view', path: '/settlement/pre-settlement/public-view',
name: '查看预结算信息', name: '查看预结算信息',
component: () => import('@/views/mk/public-biz-view.vue'), component: () => import('@/views/settlement/pre-settlement-public-view.vue'),
meta: { meta: {
keepAlive: false, keepAlive: false,
isTab: false, isTab: false,
@@ -131,7 +131,7 @@ export default [
{ {
path: '/settlement/formal-settlement/public-view', path: '/settlement/formal-settlement/public-view',
name: '查看正式结算信息', name: '查看正式结算信息',
component: () => import('@/views/mk/public-biz-view.vue'), component: () => import('@/views/settlement/formal-settlement-public-view.vue'),
meta: { meta: {
keepAlive: false, keepAlive: false,
isTab: false, isTab: false,
@@ -142,7 +142,7 @@ export default [
{ {
path: '/payment/payment-application/public-view', path: '/payment/payment-application/public-view',
name: '查看付款申请信息', name: '查看付款申请信息',
component: () => import('@/views/mk/public-biz-view.vue'), component: () => import('@/views/payment/payment-application-public-view.vue'),
meta: { meta: {
keepAlive: false, keepAlive: false,
isTab: false, isTab: false,
+10 -3
View File
@@ -150,12 +150,15 @@ a {
bottom: 0; bottom: 0;
} }
.map-picker-content { .map-picker-content {
display: flex; position: relative;
gap: 12px; display: block;
min-width: 0;
overflow: visible;
> [class$='__map'], > [class$='__map'],
> .address-map { > .address-map {
flex: 1 1 auto; position: relative;
z-index: 1;
width: 100%; width: 100%;
min-width: 0; min-width: 0;
box-sizing: border-box; box-sizing: border-box;
@@ -165,4 +168,8 @@ a {
height: 100% !important; height: 100% !important;
} }
} }
> .map-search-results {
z-index: 2000;
}
} }
+140
View File
@@ -0,0 +1,140 @@
import { getDetail, getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { getList as getVehicleList } from '@/api/transportCapacity/transport-vehicle';
const orgNameCache = new Map();
const extractRecords = res => {
const data = res?.data?.data ?? res?.data ?? res;
if (Array.isArray(data)) return data;
if (Array.isArray(data?.records)) return data.records;
if (Array.isArray(data?.data)) return data.data;
return [];
};
const normalizeOrgName = value => String(value || '').trim();
const uniqueOrgNames = (values = []) => {
const seen = new Set();
const result = [];
values.forEach(value => {
const name = normalizeOrgName(value);
if (!name || seen.has(name)) return;
seen.add(name);
result.push(name);
});
return result;
};
/**
* 解析承运商客商档案上联系人的所属组织(branchName),无联系人时回退客商所属组织。
*/
export const resolveCarrierOrganizationNames = async ({ carrierId, carrierName } = {}) => {
const id = String(carrierId || '').trim();
const name = normalizeOrgName(carrierName);
const cacheKey = id || name;
if (!cacheKey) return [];
if (orgNameCache.has(cacheKey)) return orgNameCache.get(cacheKey);
let detail = null;
if (id) {
try {
const res = await getDetail(id);
detail = res?.data?.data || res?.data || null;
} catch (error) {
detail = null;
}
}
if (!detail && name) {
try {
const res = await getCustomerList(1, 20, { fullName: name });
const records = extractRecords(res);
const matched =
records.find(item => normalizeOrgName(item.fullName || item.customerName) === name) ||
records[0];
if (matched?.id) {
const detailRes = await getDetail(matched.id);
detail = detailRes?.data?.data || detailRes?.data || matched;
}
} catch (error) {
detail = null;
}
}
const contacts = Array.isArray(detail?.contacts) ? detail.contacts : [];
const orgNames = uniqueOrgNames([
...contacts.map(item => item.branchName),
detail?.deptName,
detail?.organizationName,
]);
orgNameCache.set(cacheKey, orgNames);
if (id && name) orgNameCache.set(name, orgNames);
return orgNames;
};
export const clearCarrierOrganizationCache = (carrier = {}) => {
const id = String(carrier.carrierId || '').trim();
const name = normalizeOrgName(carrier.carrierName);
if (id) orgNameCache.delete(id);
if (name) orgNameCache.delete(name);
};
export const matchOrganizationName = (value, orgNames = []) => {
const text = normalizeOrgName(value);
if (!text || !orgNames.length) return false;
return orgNames.some(org => text === org || text.includes(org) || org.includes(text));
};
const filterByOrganizations = (records = [], orgNames = []) => {
if (!orgNames.length) return [];
return records.filter(item => matchOrganizationName(item.organizationName, orgNames));
};
/**
* 按承运商联系人所属组织筛选司机。
* 未选择承运商时返回空列表(需先选承运商)。
*/
export const fetchDriversByCarrierOrganizations = async (
query = {},
{ carrierId, carrierName } = {}
) => {
if (!String(carrierId || '').trim() && !normalizeOrgName(carrierName)) {
return [];
}
const orgNames = await resolveCarrierOrganizationNames({ carrierId, carrierName });
if (!orgNames.length) return [];
const size = Math.max(Number(query.size) || 50, 50);
const params = { ...query };
delete params.size;
// 单组织时用后端模糊条件缩小范围;多组织再前端精确过滤
if (orgNames.length === 1) {
params.organizationName = orgNames[0];
}
const res = await getDriverList(1, Math.min(size * 5, 200), params);
return filterByOrganizations(extractRecords(res), orgNames).slice(0, size);
};
/**
* 按承运商联系人所属组织筛选车辆。
* 未选择承运商时返回空列表。
*/
export const fetchVehiclesByCarrierOrganizations = async (
query = {},
{ carrierId, carrierName } = {}
) => {
if (!String(carrierId || '').trim() && !normalizeOrgName(carrierName)) {
return [];
}
const orgNames = await resolveCarrierOrganizationNames({ carrierId, carrierName });
if (!orgNames.length) return [];
const size = Math.max(Number(query.size) || 50, 50);
const params = { ...query };
delete params.size;
if (orgNames.length === 1) {
params.organizationName = orgNames[0];
}
const res = await getVehicleList(1, Math.min(size * 5, 200), params);
return filterByOrganizations(extractRecords(res), orgNames).slice(0, size);
};
+39
View File
@@ -0,0 +1,39 @@
/**
* 将高德 Geocoder 结果规范化为地图选址浮层列表数据。
* @param {Array|Object} geocodesOrResult geocodes 数组,或含 geocodes/location 的结果对象
* @param {string} keyword 搜索关键词兜底文案
* @returns {Array<{id: string|number, name: string, address: string, location: any, photo: string}>}
*/
export function normalizeMapSearchResults(geocodesOrResult, keyword = '') {
const fallback = String(keyword || '').trim();
let list = [];
if (Array.isArray(geocodesOrResult)) {
list = geocodesOrResult;
} else if (geocodesOrResult && typeof geocodesOrResult === 'object') {
if (Array.isArray(geocodesOrResult.geocodes)) {
list = geocodesOrResult.geocodes;
} else if (geocodesOrResult.location) {
list = [geocodesOrResult];
}
}
return list.map((item, index) => {
const buildingName = pickNamedField(item?.building);
const neighborhoodName = pickNamedField(item?.neighborhood);
const address = item?.formattedAddress || item?.address || fallback || '-';
return {
id: item?.id || index,
name: buildingName || neighborhoodName || address,
address,
location: item?.location,
photo: item?.photo || item?.image || '',
};
});
}
function pickNamedField(value) {
if (!value) return '';
if (typeof value === 'string') return value.trim();
if (typeof value === 'object' && value.name) return String(value.name).trim();
return '';
}
+20 -4
View File
@@ -40,10 +40,22 @@ export const MK_BIZ = {
}, },
}; };
export async function resolveMkTemplateCode(dictName) { const MK_SWITCH_NAME = '开启MK';
async function loadMkTemplateList() {
const res = await getDictionary({ code: 'mk_template' }); const res = await getDictionary({ code: 'mk_template' });
const list = res?.data?.data || []; return res?.data?.data || [];
const matched = list.find(item => String(item.dictValue || '').trim() === dictName); }
/** 业务字典 mk_template:名称「开启MK」且键值为 1 时才请求 MK */
export function isMkEnabled(list = []) {
const matched = list.find(item => String(item.dictValue || '').trim() === MK_SWITCH_NAME);
return String(matched?.dictKey ?? '').trim() === '1';
}
export async function resolveMkTemplateCode(dictName, list) {
const dictList = list || (await loadMkTemplateList());
const matched = dictList.find(item => String(item.dictValue || '').trim() === dictName);
const templateCode = matched?.dictKey; const templateCode = matched?.dictKey;
if (!templateCode) { if (!templateCode) {
ElMessage.warning(`未配置业务字典 mk_template「${dictName}」,无法提交审核流`); ElMessage.warning(`未配置业务字典 mk_template「${dictName}」,无法提交审核流`);
@@ -62,10 +74,14 @@ export async function submitMkApprovalFlow({
if (!conf) { if (!conf) {
return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`)); return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`));
} }
const list = await loadMkTemplateList();
if (!isMkEnabled(list)) {
return;
}
if ((conf.rejected || []).includes(approvalStatus)) { if ((conf.rejected || []).includes(approvalStatus)) {
await processDelete({ formInstanceId: String(formInstanceId) }); await processDelete({ formInstanceId: String(formInstanceId) });
} }
const templateCode = await resolveMkTemplateCode(conf.dictName); const templateCode = await resolveMkTemplateCode(conf.dictName, list);
const subject = subjectName const subject = subjectName
? `${conf.subjectPrefix}${subjectName}` ? `${conf.subjectPrefix}${subjectName}`
: `${conf.subjectPrefix}${formInstanceId}`; : `${conf.subjectPrefix}${formInstanceId}`;
+28
View File
@@ -283,3 +283,31 @@ export function validatejson(val) {
// 非对象、非数组、非字符串,或者字符串不是 JSON // 非对象、非数组、非字符串,或者字符串不是 JSON
return false; return false;
} }
/** 登录密码规则提示 */
export const LOGIN_PASSWORD_RULE_MESSAGE =
'密码须大于8位,且同时包含字母、数字和特殊字符(.!@#$%^&*';
/**
* 校验登录密码强度:大于8位,同时包含字母、数字、特殊字符(.!@#$%^&*
* @param {string} password
* @returns {boolean}
*/
export function isValidLoginPassword(password) {
return /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.!@#$%^&*]).{9,}$/.test(String(password || ''));
}
/**
* Element Plus / Avue 表单校验器:登录密码强度
*/
export function validateLoginPassword(rule, value, callback) {
if (value === undefined || value === null || String(value).trim() === '') {
callback(new Error('请输入登录密码'));
return;
}
if (!isValidLoginPassword(value)) {
callback(new Error(LOGIN_PASSWORD_RULE_MESSAGE));
return;
}
callback();
}
+57 -3
View File
@@ -94,6 +94,17 @@
/> />
</el-select> </el-select>
</template> </template>
<template #detailAddress-form>
<el-input
v-model="form.detailAddress"
class="detail-address-input"
readonly
maxlength="255"
show-word-limit
placeholder="点击选择地图地址"
@click="openAddressMapPicker"
/>
</template>
<template #longitude-form> <template #longitude-form>
<el-input <el-input
v-model="form.longitude" v-model="form.longitude"
@@ -121,6 +132,13 @@
</template> </template>
</avue-form> </avue-form>
</el-dialog> </el-dialog>
<address-map-picker
v-model="addressMapPickerVisible"
:address="form.detailAddress"
:longitude="form.longitude"
:latitude="form.latitude"
@confirm="handleAddressMapConfirm"
/>
</basic-container> </basic-container>
</template> </template>
@@ -134,6 +152,7 @@ import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate'; import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
@@ -181,6 +200,9 @@ const addExcelRequiredHeaderMarks = async blob => {
}; };
export default { export default {
components: {
AddressMapPicker,
},
data() { data() {
const validateIataCode = (rule, value, callback) => { const validateIataCode = (rule, value, callback) => {
if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) { if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) {
@@ -220,6 +242,7 @@ export default {
loading: true, loading: true,
data: [], data: [],
excelBox: false, excelBox: false,
addressMapPickerVisible: false,
excelForm: {}, excelForm: {},
provinceOptions: [], provinceOptions: [],
cityOptions: [], cityOptions: [],
@@ -368,15 +391,14 @@ export default {
{ {
label: '详细地址', label: '详细地址',
prop: 'detailAddress', prop: 'detailAddress',
type: 'textarea', formslot: true,
minRows: 2,
span: 24, span: 24,
minWidth: 220, minWidth: 220,
overHidden: false, overHidden: false,
maxlength: 255, maxlength: 255,
showWordLimit: true, showWordLimit: true,
rules: [ rules: [
{ required: true, message: '请输入详细地址', trigger: 'blur' }, { required: true, message: '请选择详细地址', trigger: 'change' },
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' }, { max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
], ],
}, },
@@ -669,6 +691,29 @@ export default {
handleCoordinateInput(prop, value) { handleCoordinateInput(prop, value) {
this.form[prop] = normalizeCoordinateInput(value); this.form[prop] = normalizeCoordinateInput(value);
}, },
openAddressMapPicker() {
this.addressMapPickerVisible = true;
},
handleAddressMapConfirm(payload) {
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
if (selection.address) {
this.form.detailAddress = selection.address;
}
if (
selection.longitude !== undefined &&
selection.longitude !== null &&
selection.longitude !== ''
) {
this.form.longitude = Number(selection.longitude).toFixed(6);
}
if (
selection.latitude !== undefined &&
selection.latitude !== null &&
selection.latitude !== ''
) {
this.form.latitude = Number(selection.latitude).toFixed(6);
}
},
normalizeRow(row) { normalizeRow(row) {
row.code = row.iataCode ? `JC-${row.iataCode}` : row.code; row.code = row.iataCode ? `JC-${row.iataCode}` : row.code;
row.regionCode = String(row.regionCode || '').trim(); row.regionCode = String(row.regionCode || '').trim();
@@ -917,4 +962,13 @@ export default {
white-space: nowrap; white-space: nowrap;
} }
} }
.detail-address-input {
cursor: pointer;
:deep(.el-input__wrapper),
:deep(.el-input__inner) {
cursor: pointer;
}
}
</style> </style>
+3 -7
View File
@@ -233,8 +233,8 @@
</el-button> </el-button>
</div> </div>
<div class="map-picker-content"> <div class="map-picker-content">
<map-search-results :results="mapSearchResults" @select="selectMapSearchResult" />
<div ref="amap" class="common-address-page__map"></div> <div ref="amap" class="common-address-page__map"></div>
<map-search-results :results="mapSearchResults" @select="selectMapSearchResult" />
</div> </div>
<div class="common-address-page__map-info"> <div class="common-address-page__map-info">
<span>{{ mapStatus }}</span> <span>{{ mapStatus }}</span>
@@ -270,6 +270,7 @@ import { createOption } from '@/option/base/common-address';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { isMobile } from '@/utils/validate'; import { isMobile } from '@/utils/validate';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
@@ -1117,12 +1118,7 @@ export default {
this.ensureAmapGeocoder() this.ensureAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword)) .then(() => this.runAmapGeocode('location', keyword))
.then(result => { .then(result => {
this.mapSearchResults = (result.geocodes || []).map((item, index) => ({ this.mapSearchResults = normalizeMapSearchResults(result, keyword);
id: item.id || index,
name: item.formattedAddress || keyword,
address: item.formattedAddress || keyword,
location: item.location,
}));
const point = this.resolveMapPoint(result); const point = this.resolveMapPoint(result);
if (!point) { if (!point) {
this.mapStatus = '未找到匹配地址'; this.mapStatus = '未找到匹配地址';
+42 -5
View File
@@ -194,10 +194,11 @@
<el-input <el-input
v-model="form.detailAddress" v-model="form.detailAddress"
class="detail-address-input" class="detail-address-input"
clearable readonly
maxlength="255" maxlength="255"
show-word-limit show-word-limit
placeholder="请输入" placeholder="点击选择地图地址"
@click="openAddressMapPicker"
/> />
</template> </template>
<template #remark-form> <template #remark-form>
@@ -244,6 +245,13 @@
</template> </template>
</avue-form> </avue-form>
</el-dialog> </el-dialog>
<address-map-picker
v-model="addressMapPickerVisible"
:address="form.detailAddress"
:longitude="form.longitude"
:latitude="form.latitude"
@confirm="handleAddressMapConfirm"
/>
</basic-container> </basic-container>
</template> </template>
@@ -264,12 +272,16 @@ import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate'; import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
const DEFAULT_COUNTRY_CODE = '+86'; const DEFAULT_COUNTRY_CODE = '+86';
const newLocal = '请选择类型'; const newLocal = '请选择类型';
export default { export default {
components: {
AddressMapPicker,
},
data() { data() {
const validateCode = (rule, value, callback) => { const validateCode = (rule, value, callback) => {
const code = String(value || '').toUpperCase(); const code = String(value || '').toUpperCase();
@@ -315,6 +327,7 @@ export default {
loading: true, loading: true,
data: [], data: [],
excelBox: false, excelBox: false,
addressMapPickerVisible: false,
excelForm: {}, excelForm: {},
portOptions: [], portOptions: [],
countryOptions: [], countryOptions: [],
@@ -572,11 +585,11 @@ export default {
minWidth: 220, minWidth: 220,
overHidden: false, overHidden: false,
order: 85, order: 85,
placeholder: '请输入', placeholder: '点击选择地图地址',
maxlength: 255, maxlength: 255,
showWordLimit: true, showWordLimit: true,
rules: [ rules: [
{ required: true, message: '请输入详细地址', trigger: 'blur' }, { required: true, message: '请选择详细地址', trigger: 'change' },
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' }, { max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
], ],
}, },
@@ -1040,6 +1053,21 @@ export default {
handleCoordinateInput(prop, value) { handleCoordinateInput(prop, value) {
this.form[prop] = normalizeCoordinateInput(value); this.form[prop] = normalizeCoordinateInput(value);
}, },
openAddressMapPicker() {
this.addressMapPickerVisible = true;
},
handleAddressMapConfirm(payload) {
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
if (selection.address) {
this.form.detailAddress = selection.address;
}
if (selection.longitude !== undefined && selection.longitude !== null && selection.longitude !== '') {
this.form.longitude = Number(selection.longitude).toFixed(6);
}
if (selection.latitude !== undefined && selection.latitude !== null && selection.latitude !== '') {
this.form.latitude = Number(selection.latitude).toFixed(6);
}
},
resolveCountryCode(country) { resolveCountryCode(country) {
if (!country) { if (!country) {
return Promise.resolve(''); return Promise.resolve('');
@@ -1107,7 +1135,7 @@ export default {
return false; return false;
} }
if (isBlank(row.detailAddress)) { if (isBlank(row.detailAddress)) {
this.$message.warning('请输入详细地址'); this.$message.warning('请选择详细地址');
return false; return false;
} }
if (isBlank(row.longitude)) { if (isBlank(row.longitude)) {
@@ -1399,6 +1427,15 @@ export default {
line-height: 20px; line-height: 20px;
padding: 4px 0; padding: 4px 0;
} }
.detail-address-input {
cursor: pointer;
:deep(.el-input__wrapper),
:deep(.el-input__inner) {
cursor: pointer;
}
}
</style> </style>
<style lang="scss"> <style lang="scss">
+56 -4
View File
@@ -94,6 +94,17 @@
/> />
</el-select> </el-select>
</template> </template>
<template #detailAddress-form>
<el-input
v-model="form.detailAddress"
class="detail-address-input"
readonly
maxlength="255"
show-word-limit
placeholder="点击选择地图地址"
@click="openAddressMapPicker"
/>
</template>
<template #longitude-form> <template #longitude-form>
<el-input <el-input
v-model="form.longitude" v-model="form.longitude"
@@ -121,6 +132,13 @@
</template> </template>
</avue-form> </avue-form>
</el-dialog> </el-dialog>
<address-map-picker
v-model="addressMapPickerVisible"
:address="form.detailAddress"
:longitude="form.longitude"
:latitude="form.latitude"
@confirm="handleAddressMapConfirm"
/>
</basic-container> </basic-container>
</template> </template>
@@ -134,12 +152,16 @@ import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate'; import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
const DEFAULT_COUNTRY_CODE = '+86'; const DEFAULT_COUNTRY_CODE = '+86';
export default { export default {
components: {
AddressMapPicker,
},
data() { data() {
const validateTmisCode = (rule, value, callback) => { const validateTmisCode = (rule, value, callback) => {
if (!/^\d{5}$/.test(value || '')) { if (!/^\d{5}$/.test(value || '')) {
@@ -185,6 +207,7 @@ export default {
loading: true, loading: true,
data: [], data: [],
excelBox: false, excelBox: false,
addressMapPickerVisible: false,
excelForm: {}, excelForm: {},
provinceOptions: [], provinceOptions: [],
cityOptions: [], cityOptions: [],
@@ -340,15 +363,14 @@ export default {
{ {
label: '详细地址', label: '详细地址',
prop: 'detailAddress', prop: 'detailAddress',
type: 'textarea', formslot: true,
minRows: 2,
span: 24, span: 24,
minWidth: 220, minWidth: 220,
overHidden: false, overHidden: false,
maxlength: 255, maxlength: 255,
showWordLimit: true, showWordLimit: true,
rules: [ rules: [
{ required: true, message: '请输入详细地址', trigger: 'blur' }, { required: true, message: '请选择详细地址', trigger: 'change' },
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' }, { max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
], ],
}, },
@@ -652,6 +674,29 @@ export default {
handleCoordinateInput(prop, value) { handleCoordinateInput(prop, value) {
this.form[prop] = normalizeCoordinateInput(value); this.form[prop] = normalizeCoordinateInput(value);
}, },
openAddressMapPicker() {
this.addressMapPickerVisible = true;
},
handleAddressMapConfirm(payload) {
const selection = typeof payload === 'string' ? { address: payload } : payload || {};
if (selection.address) {
this.form.detailAddress = selection.address;
}
if (
selection.longitude !== undefined &&
selection.longitude !== null &&
selection.longitude !== ''
) {
this.form.longitude = Number(selection.longitude).toFixed(6);
}
if (
selection.latitude !== undefined &&
selection.latitude !== null &&
selection.latitude !== ''
) {
this.form.latitude = Number(selection.latitude).toFixed(6);
}
},
normalizeRow(row) { normalizeRow(row) {
row.code = row.tmisCode ? `TL${row.tmisCode}` : row.code; row.code = row.tmisCode ? `TL${row.tmisCode}` : row.code;
row.regionCode = String(row.regionCode || '').trim(); row.regionCode = String(row.regionCode || '').trim();
@@ -910,9 +955,16 @@ export default {
:deep(.el-table td .cell) { :deep(.el-table td .cell) {
white-space: nowrap; white-space: nowrap;
} }
} }
.detail-address-input {
cursor: pointer;
:deep(.el-input__wrapper),
:deep(.el-input__inner) {
cursor: pointer;
}
}
</style> </style>
<style lang="scss"> <style lang="scss">
+3 -7
View File
@@ -289,8 +289,8 @@
</el-button> </el-button>
</div> </div>
<div class="map-picker-content"> <div class="map-picker-content">
<map-search-results :results="routeMapSearchResults" @select="selectRouteMapSearchResult" />
<div ref="routeAmap" class="common-route-page__map"></div> <div ref="routeAmap" class="common-route-page__map"></div>
<map-search-results :results="routeMapSearchResults" @select="selectRouteMapSearchResult" />
</div> </div>
<div class="common-route-page__map-info"> <div class="common-route-page__map-info">
<span>{{ routeMapStatus }}</span> <span>{{ routeMapStatus }}</span>
@@ -323,6 +323,7 @@ import { addressTypeOptions } from '@/option/base/common-address';
import { config, excelOption, option } from '@/option/business/common-route'; import { config, excelOption, option } from '@/option/business/common-route';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate'; import { isMobile } from '@/utils/validate';
import { List, Search } from '@element-plus/icons-vue'; import { List, Search } from '@element-plus/icons-vue';
@@ -616,12 +617,7 @@ export default {
}) })
.then(() => this.runAmapGeocode('location', keyword)) .then(() => this.runAmapGeocode('location', keyword))
.then(result => { .then(result => {
this.routeMapSearchResults = (result.geocodes || []).map((item, index) => ({ this.routeMapSearchResults = normalizeMapSearchResults(result, keyword);
id: item.id || index,
name: item.formattedAddress || keyword,
address: item.formattedAddress || keyword,
location: item.location,
}));
const point = this.resolveMapPoint(result); const point = this.resolveMapPoint(result);
if (!point) { if (!point) {
this.routeMapStatus = '未找到匹配地址'; this.routeMapStatus = '未找到匹配地址';
@@ -162,9 +162,9 @@
v-model="row.billingUnit" v-model="row.billingUnit"
clearable clearable
filterable filterable
:disabled="!row.billingElement" :disabled="!canSelectBillingUnit(row)"
:loading="unitLoading" :loading="unitLoading"
:placeholder="row.billingElement ? '请选择' : '请先选择计费要素'" :placeholder="billingUnitPlaceholder(row)"
><el-option ><el-option
v-for="item in unitOptionsFor(row)" v-for="item in unitOptionsFor(row)"
:key="item.id || item.value" :key="item.id || item.value"
@@ -381,11 +381,18 @@ import { getDictionary } from '@/api/system/dictbiz';
import SectionCard from '@/components/section-card/main.vue'; import SectionCard from '@/components/section-card/main.vue';
const clone = value => JSON.parse(JSON.stringify(value)); const clone = value => JSON.parse(JSON.stringify(value));
/** 计费要素与计量单位维度的对应关系 */ /** 计费要素与计量单位维度的对应关系(这些要素可选计费单位) */
const BILLING_ELEMENT_DIMENSION_MAP = { const BILLING_ELEMENT_DIMENSION_MAP = {
按重量: '重量', 按重量: '重量',
按体积: '体积', 按体积: '体积',
车辆: '数量', 数量: '数量',
};
const BILLING_UNIT_SELECTABLE_ELEMENTS = Object.keys(BILLING_ELEMENT_DIMENSION_MAP);
/** 固定计费单位(不可编辑) */
const FIXED_BILLING_UNIT_MAP = {
按里程: '公里',
'按吨·公里': '吨公里',
'固定金额(整单一口价)': '单',
}; };
const defaultRule = () => ({ const defaultRule = () => ({
feeType: '', feeType: '',
@@ -436,7 +443,6 @@ export default {
billingElements: [ billingElements: [
'按重量', '按重量',
'按体积', '按体积',
'按车辆',
'按里程', '按里程',
'按吨·公里', '按吨·公里',
'固定金额(整单一口价)', '固定金额(整单一口价)',
@@ -445,7 +451,6 @@ export default {
typeMap: { typeMap: {
按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], 按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], 按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
按车辆: ['固定单价'],
按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], 按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
'按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'], '按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
'固定金额(整单一口价)': ['固定一口价'], '固定金额(整单一口价)': ['固定一口价'],
@@ -560,6 +565,7 @@ export default {
next.limitRanges = next.limitRanges.map(item => ({ ...item, minimumBillingWeight: '' })); next.limitRanges = next.limitRanges.map(item => ({ ...item, minimumBillingWeight: '' }));
this.syncLegacyLimit(next); this.syncLegacyLimit(next);
next.matchCondition = { ...defaultRule().matchCondition, ...(rule.matchCondition || {}) }; next.matchCondition = { ...defaultRule().matchCondition, ...(rule.matchCondition || {}) };
this.syncBillingUnit(next);
return next; return next;
}, },
normalizeRanges(row) { normalizeRanges(row) {
@@ -651,7 +657,26 @@ export default {
measurementDimension(billingElement) { measurementDimension(billingElement) {
return BILLING_ELEMENT_DIMENSION_MAP[billingElement] || ''; return BILLING_ELEMENT_DIMENSION_MAP[billingElement] || '';
}, },
fixedBillingUnit(billingElement) {
return FIXED_BILLING_UNIT_MAP[String(billingElement || '').trim()] || '';
},
canSelectBillingUnit(row) {
return BILLING_UNIT_SELECTABLE_ELEMENTS.includes(String(row?.billingElement || '').trim());
},
billingUnitPlaceholder(row) {
if (!row?.billingElement) return '请先选择计费要素';
if (this.fixedBillingUnit(row.billingElement)) return this.fixedBillingUnit(row.billingElement);
if (!this.canSelectBillingUnit(row)) return '当前计费要素无需选择';
return '请选择';
},
unitOptionsFor(row) { unitOptionsFor(row) {
const fixedUnit = this.fixedBillingUnit(row?.billingElement);
if (fixedUnit) {
return [{ id: fixedUnit, label: fixedUnit, value: fixedUnit }];
}
if (!this.canSelectBillingUnit(row)) {
return [];
}
const dimension = this.measurementDimension(row?.billingElement); const dimension = this.measurementDimension(row?.billingElement);
if (dimension) { if (dimension) {
return this.measurementUnits return this.measurementUnits
@@ -663,15 +688,18 @@ export default {
})) }))
.filter(item => item.value); .filter(item => item.value);
} }
return (this.unitOptions || []) return [];
.map(item => ({
id: item.id || item.dictKey || item.dictValue,
label: item.dictValue,
value: item.dictValue,
}))
.filter(item => item.value);
}, },
syncBillingUnit(row) { syncBillingUnit(row) {
const fixedUnit = this.fixedBillingUnit(row?.billingElement);
if (fixedUnit) {
row.billingUnit = fixedUnit;
return;
}
if (!this.canSelectBillingUnit(row)) {
row.billingUnit = '';
return;
}
const options = this.unitOptionsFor(row); const options = this.unitOptionsFor(row);
if (!options.some(item => String(item.value) === String(row.billingUnit || ''))) { if (!options.some(item => String(item.value) === String(row.billingUnit || ''))) {
row.billingUnit = ''; row.billingUnit = '';
@@ -892,7 +920,6 @@ export default {
['taxRate', '税率'], ['taxRate', '税率'],
['billingElement', '计费要素'], ['billingElement', '计费要素'],
['billingType', '计费类型'], ['billingType', '计费类型'],
['billingUnit', '计费单位'],
]; ];
for (const [i, row] of this.draft.rules.entries()) { for (const [i, row] of this.draft.rules.entries()) {
const empty = required.find( const empty = required.find(
@@ -902,6 +929,15 @@ export default {
this.$message.warning(`${i + 1}${empty[1]}不能为空`); this.$message.warning(`${i + 1}${empty[1]}不能为空`);
return false; return false;
} }
if (
this.canSelectBillingUnit(row) &&
(row.billingUnit === undefined ||
row.billingUnit === null ||
String(row.billingUnit).trim() === '')
) {
this.$message.warning(`${i + 1}行计费单位不能为空`);
return false;
}
const feeItem = String(row.feeItem).trim(); const feeItem = String(row.feeItem).trim();
if (feeItemSet.has(feeItem)) { if (feeItemSet.has(feeItem)) {
this.$message.warning(`费用项“${feeItem}”不能重复`); this.$message.warning(`费用项“${feeItem}”不能重复`);
@@ -1899,11 +1899,11 @@
label-width="auto" label-width="auto"
class="business-crud-page__module-form business-crud-page__module-form--three" class="business-crud-page__module-form business-crud-page__module-form--three"
> >
<el-form-item label="账单起始日" required :error="settlementRuleErrors.billStartDate"> <el-form-item label="账单起始日" required :error="settlementRuleErrors.billStartDate">
<el-date-picker <el-date-picker
v-model="settlementRuleForm.billStartDate" v-model="settlementRuleForm.billStartDate"
type="date" type="date"
placeholder="请选择账单起始日" placeholder="请选择账单起始日"
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
:disabled="dialogReadonly" :disabled="dialogReadonly"
@@ -2478,7 +2478,7 @@
{{ formatTransportPlanDetailTransportType(waybillTransportMode(detailRow)) }} {{ formatTransportPlanDetailTransportType(waybillTransportMode(detailRow)) }}
</span> </span>
<el-link <el-link
v-if="detailRow.id" v-if="detailRow.id && canChangeWaybillRoute(detailRow)"
class="business-crud-page__waybill-route-change-link" class="business-crud-page__waybill-route-change-link"
type="primary" type="primary"
@click="openWaybillRouteChangeDialog" @click="openWaybillRouteChangeDialog"
@@ -2947,35 +2947,6 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</section-card> </section-card>
<section-card title="路线编辑">
<template #extra>
<span class="business-crud-page__route-change-hint">拖拽调整顺序</span>
</template>
<div
v-if="waybillRouteChangeNodes.length"
class="business-crud-page__route-change-list"
>
<div
v-for="(node, index) in waybillRouteChangeNodes"
:key="node.key || index"
class="business-crud-page__route-change-item"
draggable="true"
@dragstart="handleWaybillRouteChangeDragStart(index)"
@dragover.prevent
@drop="handleWaybillRouteChangeDrop(index)"
>
<span class="business-crud-page__route-change-type">
{{ waybillRouteChangeTypeText(index) }}
</span>
<span>{{ node.address }}</span>
<span class="business-crud-page__route-change-tags">
<el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag>
</span>
<el-icon><Rank /></el-icon>
</div>
</div>
<el-empty v-else description="暂无路线" :image-size="60" />
</section-card>
<el-form label-width="auto"> <el-form label-width="auto">
<el-form-item label="变更备注" style="margin-top: 8px"> <el-form-item label="变更备注" style="margin-top: 8px">
<el-input <el-input
@@ -4036,7 +4007,7 @@
</el-input> </el-input>
<div class="business-crud-page__dispatch-quantity-hint"> <div class="business-crud-page__dispatch-quantity-hint">
剩余数量{{ 剩余数量{{
formatDispatchQuantity(dispatchItemRemainingQuantity(dispatchItemForm)) formatDispatchQuantity(dispatchItemHintRemainingQuantity(dispatchItemForm))
}} }}
{{ dispatchItemForm.quantityUnit || '吨' }} {{ dispatchItemForm.quantityUnit || '吨' }}
</div> </div>
@@ -4093,7 +4064,11 @@
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label="其他费用合计"> <el-form-item label="其他费用合计">
<el-input v-model="dispatchItemForm.otherFeeTotal" placeholder="请输入"> <el-input
v-model="dispatchItemForm.otherFeeTotal"
placeholder="请输入"
@input="value => handleDispatchNumberInput('otherFeeTotal', value)"
>
<template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template> <template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template>
</el-input> </el-input>
</el-form-item> </el-form-item>
@@ -5025,8 +5000,8 @@
</el-button> </el-button>
</div> </div>
<div class="map-picker-content"> <div class="map-picker-content">
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
<div ref="transportAmap" class="business-crud-page__map"></div> <div ref="transportAmap" class="business-crud-page__map"></div>
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
</div> </div>
<div class="business-crud-page__map-info"> <div class="business-crud-page__map-info">
<span>{{ transportMapStatus }}</span> <span>{{ transportMapStatus }}</span>
@@ -5300,7 +5275,7 @@ import {
getVoucherImages as getProcessConfigVoucherImages, getVoucherImages as getProcessConfigVoucherImages,
} from '@/api/business/process-config'; } from '@/api/business/process-config';
import { getList as getCarrierCustomerList } from '@/api/vehicle/customer-archive'; import { getList as getCarrierCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver'; import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { getDictionary as getSystemDictionary } from '@/api/system/dict'; import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address'; import { addressTypeOptions } from '@/option/base/common-address';
@@ -5308,10 +5283,11 @@ import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { applyTableMenuWidth } from '@/utils/table-menu'; import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util'; import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate'; import { isMobile } from '@/utils/validate';
import { Location, OfficeBuilding, Rank, Search } from '@element-plus/icons-vue'; import { Location, OfficeBuilding, Search } from '@element-plus/icons-vue';
import { ElImageViewer } from 'element-plus'; import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue'; import { OpenFileViewer } from '@open-file-viewer/vue';
import { import {
@@ -5574,7 +5550,6 @@ export default {
components: { components: {
BillingPlanEditor, BillingPlanEditor,
PageAvueForm, PageAvueForm,
Rank,
ElImageViewer, ElImageViewer,
OpenFileViewer, OpenFileViewer,
}, },
@@ -5637,7 +5612,6 @@ export default {
waybillRouteChangeNodes: [], waybillRouteChangeNodes: [],
waybillRouteChangeRemark: '', waybillRouteChangeRemark: '',
waybillRouteChangeRecords: [], waybillRouteChangeRecords: [],
waybillRouteChangeDragIndex: -1,
waybillRouteChangeSaving: false, waybillRouteChangeSaving: false,
mileageDialog: { mileageDialog: {
visible: false, visible: false,
@@ -6371,19 +6345,21 @@ export default {
if (this.dispatchItemForm.taskEntryMode !== 'full') { if (this.dispatchItemForm.taskEntryMode !== 'full') {
const quantity = Number(this.dispatchItemForm.quantity || 0); const quantity = Number(this.dispatchItemForm.quantity || 0);
const unitPrice = Number(this.dispatchItemForm.unitPrice || 0); const unitPrice = Number(this.dispatchItemForm.unitPrice || 0);
const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0); const otherFeeRaw = this.normalizeDispatchFeeValue(this.dispatchItemForm.otherFeeTotal);
const otherFeeTotal = Number(otherFeeRaw || 0);
const hasFreight = this.dispatchItemForm.unitPrice !== '' && quantity > 0; const hasFreight = this.dispatchItemForm.unitPrice !== '' && quantity > 0;
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== ''; const hasOtherFee = otherFeeRaw !== '';
if (!hasFreight && !hasOtherFee) return ''; if (!hasFreight && !hasOtherFee) return '';
const total = (hasFreight ? quantity * unitPrice : 0) + (hasOtherFee ? otherFeeTotal : 0); const total = (hasFreight ? quantity * unitPrice : 0) + (hasOtherFee ? otherFeeTotal : 0);
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2))); return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
} }
const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0); const otherFeeRaw = this.normalizeDispatchFeeValue(this.dispatchItemForm.otherFeeTotal);
const otherFeeTotal = Number(otherFeeRaw || 0);
const freightTotal = this.dispatchItemCargoRows.reduce( const freightTotal = this.dispatchItemCargoRows.reduce(
(total, cargo) => total + Number(this.dispatchCargoFreightAmount(cargo) || 0), (total, cargo) => total + Number(this.dispatchCargoFreightAmount(cargo) || 0),
0 0
); );
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== ''; const hasOtherFee = otherFeeRaw !== '';
if (!freightTotal && !hasOtherFee) return ''; if (!freightTotal && !hasOtherFee) return '';
const total = freightTotal + (hasOtherFee ? otherFeeTotal : 0); const total = freightTotal + (hasOtherFee ? otherFeeTotal : 0);
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2))); return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
@@ -7438,7 +7414,7 @@ export default {
: []; : [];
}, },
openWaybillRouteChangeDialog() { openWaybillRouteChangeDialog() {
if (!this.detailRow.id) return; if (!this.detailRow.id || !this.canChangeWaybillRoute(this.detailRow)) return;
this.restoreWaybillRouteChangeRecords(); this.restoreWaybillRouteChangeRecords();
this.waybillRouteChangeTab = 'change'; this.waybillRouteChangeTab = 'change';
this.waybillRouteChangeRemark = ''; this.waybillRouteChangeRemark = '';
@@ -7455,22 +7431,11 @@ export default {
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows); this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
this.waybillRouteChangeBox = true; this.waybillRouteChangeBox = true;
}, },
waybillRouteChangeTypeText(index) { canChangeWaybillRoute(row = {}) {
if (index === 0) return ''; const status = String(this.statusValue(row) || row.businessStatus || row.status || '');
if (index === this.waybillRouteChangeNodes.length - 1) return '终'; if (status === 'completed') return false;
return ''; const statusName = String(row.businessStatusName || row.statusName || '');
}, return !statusName.includes('已完成');
handleWaybillRouteChangeDragStart(index) {
this.waybillRouteChangeDragIndex = index;
},
handleWaybillRouteChangeDrop(index) {
if (this.waybillRouteChangeDragIndex < 0 || this.waybillRouteChangeDragIndex === index)
return;
const nodes = [...this.waybillRouteChangeNodes];
const [node] = nodes.splice(this.waybillRouteChangeDragIndex, 1);
nodes.splice(index, 0, node);
this.waybillRouteChangeNodes = nodes;
this.waybillRouteChangeDragIndex = -1;
}, },
updateWaybillRouteChangeNodes() { updateWaybillRouteChangeNodes() {
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows); this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
@@ -7519,9 +7484,8 @@ export default {
row.departureAddress !== row.originalDepartureAddress || row.departureAddress !== row.originalDepartureAddress ||
row.arrivalAddress !== row.originalArrivalAddress; row.arrivalAddress !== row.originalArrivalAddress;
const routeJson = JSON.stringify(this.waybillRouteChangeNodes); const routeJson = JSON.stringify(this.waybillRouteChangeNodes);
const originalRouteJson = this.detailRow.routeJson || ''; if (!changed && !this.waybillRouteChangeRemark) {
if (!changed && routeJson === originalRouteJson && !this.waybillRouteChangeRemark) { this.$message.warning('请修改地址或填写变更备注');
this.$message.warning('请修改地址、调整路线或填写变更备注');
return; return;
} }
if (typeof this.api.changeRoute !== 'function') { if (typeof this.api.changeRoute !== 'function') {
@@ -7529,11 +7493,9 @@ export default {
return; return;
} }
const content = changed const content = `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress || '-'}${
? `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress || '-'}${
row.departureAddress || '-' row.departureAddress || '-'
}到货地 ${row.originalArrivalAddress || '-'} ${row.arrivalAddress || '-'}` }到货地 ${row.originalArrivalAddress || '-'} ${row.arrivalAddress || '-'}`;
: '调整运输路线顺序';
const records = [ const records = [
{ {
changeTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'), changeTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
@@ -7784,8 +7746,8 @@ export default {
const rule = this.normalizeSettlementRule(source); const rule = this.normalizeSettlementRule(source);
this.settlementRuleErrors = {}; this.settlementRuleErrors = {};
if (this.isBillingFieldEmpty(rule.billStartDate)) { if (this.isBillingFieldEmpty(rule.billStartDate)) {
this.settlementRuleErrors = { billStartDate: '请选择账单起始日' }; this.settlementRuleErrors = { billStartDate: '请选择账单起始日' };
this.$message.warning(`${label}${label ? '' : ''}请选择账单起始日`); this.$message.warning(`${label}${label ? '' : ''}请选择账单起始日`);
return false; return false;
} }
if (this.isBillingFieldEmpty(rule.settlementType)) { if (this.isBillingFieldEmpty(rule.settlementType)) {
@@ -8964,6 +8926,7 @@ export default {
this.form.carrierContractId = ''; this.form.carrierContractId = '';
} }
this.form.carrierType = nextValue; this.form.carrierType = nextValue;
this.clearTaskDriverVehicleFields();
if (this.isWaybillDetailLayout) { if (this.isWaybillDetailLayout) {
this.taskCarrierRequestId += 1; this.taskCarrierRequestId += 1;
this.taskCarrierOptions = []; this.taskCarrierOptions = [];
@@ -9081,25 +9044,58 @@ export default {
}, },
handleTaskCarrierChange(value) { handleTaskCarrierChange(value) {
const carrier = this.taskCarrierOptions.find(item => const carrier = this.taskCarrierOptions.find(item =>
[item.value, item.customerName, item.carrierName, item.fullName, item.name].some(name => [
String(name || '') === String(value || '') item.value,
) item.customerName,
item.carrierName,
item.fullName,
item.name,
item.carrierContractId,
].some(name => String(name || '') === String(value || ''))
); );
if (this.isWaybillDetailLayout && this.form.carrierType === '承运商') { if (this.isWaybillDetailLayout && this.form.carrierType === '承运商') {
this.form.carrierContractId = carrier?.carrierContractId || ''; this.form.carrierContractId = carrier?.carrierContractId || '';
this.form.carrierId = carrier?.carrierId || ''; this.form.carrierId = carrier?.carrierId || '';
this.form.carrierName = carrier?.carrierName || value || ''; this.form.carrierName = carrier?.carrierName || value || '';
return; } else {
} this.form.carrierId = carrier?.id || carrier?.carrierId || '';
this.form.carrierId = carrier?.id || '';
this.form.carrierName = value || ''; this.form.carrierName = value || '';
}
this.clearTaskDriverVehicleFields();
},
clearTaskDriverVehicleFields() {
this.form.driverId = '';
this.form.driverName = '';
this.form.driverPhone = '';
this.form.vehicleNo = '';
this.form.trailerVehicleNo = '';
this.form.escortName = '';
this.form.escortPhone = '';
this.taskDriverOptions = [];
},
getActiveCarrierFilter() {
if (this.dispatchItemBox) {
return {
carrierId: this.dispatchItemForm.carrierId || '',
carrierName: this.dispatchItemForm.carrierName || '',
};
}
return {
carrierId: this.form.carrierId || '',
carrierName: this.form.carrierName || '',
};
}, },
loadTaskDriverOptions() { loadTaskDriverOptions() {
if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]); if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]);
const carrier = this.getActiveCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
this.taskDriverOptions = [];
return Promise.resolve([]);
}
this.taskDriverLoading = true; this.taskDriverLoading = true;
return getDriverList(1, 9999, {}) return fetchDriversByCarrierOrganizations({ size: 9999, posts: '司机' }, carrier)
.then(res => { .then(records => {
this.taskDriverOptions = extractRecords(res); this.taskDriverOptions = records;
return this.taskDriverOptions; return this.taskDriverOptions;
}) })
.finally(() => { .finally(() => {
@@ -9108,10 +9104,17 @@ export default {
}, },
fetchTaskDriverSuggestions(queryString, callback) { fetchTaskDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
const carrier = this.getActiveCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskDriverLoading = true; this.taskDriverLoading = true;
getDriverList(1, 20, keyword ? { driverName: keyword } : {}) fetchDriversByCarrierOrganizations(
.then(res => { { size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
const records = extractRecords(res); carrier
)
.then(records => {
this.taskDriverOptions = records; this.taskDriverOptions = records;
callback( callback(
records.map(item => ({ records.map(item => ({
@@ -10284,12 +10287,7 @@ export default {
this.ensureTransportAmapGeocoder() this.ensureTransportAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword)) .then(() => this.runAmapGeocode('location', keyword))
.then(result => { .then(result => {
this.transportMapSearchResults = (result.geocodes || []).map((item, index) => ({ this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
id: item.id || index,
name: item.formattedAddress || keyword,
address: item.formattedAddress || keyword,
location: item.location,
}));
const point = this.resolveMapPoint(result); const point = this.resolveMapPoint(result);
if (!point) { if (!point) {
this.transportMapStatus = '未找到匹配地址'; this.transportMapStatus = '未找到匹配地址';
@@ -12805,6 +12803,9 @@ export default {
...baseRow, ...baseRow,
...(index >= 0 ? row : {}), ...(index >= 0 ? row : {}),
}; };
this.dispatchItemForm.otherFeeTotal = this.normalizeDispatchFeeValue(
this.dispatchItemForm.otherFeeTotal
);
this.dispatchItemForm.carrierType = this.dispatchItemForm.carrierType || '承运商'; this.dispatchItemForm.carrierType = this.dispatchItemForm.carrierType || '承运商';
const goodsRows = this.parseJsonArray(this.dispatchItemForm.goodsJson); const goodsRows = this.parseJsonArray(this.dispatchItemForm.goodsJson);
this.dispatchItemCargoRows = (goodsRows.length ? goodsRows : [this.dispatchItemForm]).map( this.dispatchItemCargoRows = (goodsRows.length ? goodsRows : [this.dispatchItemForm]).map(
@@ -12861,6 +12862,7 @@ export default {
} }
} }
this.dispatchItemForm.carrierType = nextValue; this.dispatchItemForm.carrierType = nextValue;
this.clearDispatchDriverVehicleFields();
this.loadDispatchCarrierOptions(this.dispatchRow); this.loadDispatchCarrierOptions(this.dispatchRow);
}, },
isDispatchCarrierRequired(carrierType) { isDispatchCarrierRequired(carrierType) {
@@ -12990,20 +12992,39 @@ export default {
}); });
if (this.dispatchIsCarrierMode) { if (this.dispatchIsCarrierMode) {
this.dispatchItemForm.carrierContractId = carrier?.carrierContractId || ''; this.dispatchItemForm.carrierContractId = carrier?.carrierContractId || '';
this.dispatchItemForm.carrierId = ''; this.dispatchItemForm.carrierId = carrier?.carrierId || '';
this.dispatchItemForm.carrierName = carrier?.carrierName || ''; this.dispatchItemForm.carrierName = carrier?.carrierName || '';
return; } else {
}
this.dispatchItemForm.carrierContractId = ''; this.dispatchItemForm.carrierContractId = '';
this.dispatchItemForm.carrierId = carrier?.id || ''; this.dispatchItemForm.carrierId = carrier?.id || carrier?.carrierId || '';
this.dispatchItemForm.carrierName = carrier?.carrierName || value || ''; this.dispatchItemForm.carrierName = carrier?.carrierName || value || '';
}
this.clearDispatchDriverVehicleFields();
},
clearDispatchDriverVehicleFields() {
this.dispatchItemForm.driverId = '';
this.dispatchItemForm.driverName = '';
this.dispatchItemForm.driverPhone = '';
this.dispatchItemForm.vehicleNo = '';
this.dispatchItemForm.trailerVehicleNo = '';
this.dispatchItemForm.escortName = '';
this.dispatchItemForm.escortPhone = '';
this.taskDriverOptions = [];
}, },
loadDispatchDriverOptions() { loadDispatchDriverOptions() {
if (this.taskDriverLoading) return Promise.resolve([]); if (this.taskDriverLoading) return Promise.resolve([]);
const carrier = {
carrierId: this.dispatchItemForm.carrierId || '',
carrierName: this.dispatchItemForm.carrierName || '',
};
if (!carrier.carrierId && !carrier.carrierName) {
this.taskDriverOptions = [];
return Promise.resolve([]);
}
this.taskDriverLoading = true; this.taskDriverLoading = true;
return getDriverList(1, 9999, {}) return fetchDriversByCarrierOrganizations({ size: 9999, posts: '司机' }, carrier)
.then(res => { .then(records => {
this.taskDriverOptions = extractRecords(res); this.taskDriverOptions = records;
return this.taskDriverOptions; return this.taskDriverOptions;
}) })
.finally(() => { .finally(() => {
@@ -13080,6 +13101,11 @@ export default {
this.dispatchItemForm[prop] = this.dispatchItemForm[prop] =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0]; parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
}, },
normalizeDispatchFeeValue(value) {
if (value === undefined || value === null || value === '') return '';
if (Number(value) === -1) return '';
return value;
},
addDispatchCargoRow(index = -1) { addDispatchCargoRow(index = -1) {
const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' }); const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' });
if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow); if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow);
@@ -13163,6 +13189,12 @@ export default {
}, 0) }, 0)
); );
}, },
/** 精简录入提示:可调度余量扣减当前本次数量后的剩余 */
dispatchItemHintRemainingQuantity(row = {}) {
const available = this.dispatchItemRemainingQuantity(row);
const current = this.parseDispatchQuantity(row.quantity);
return Math.max(available - current, 0);
},
pruneDispatchPendingRows(rows = []) { pruneDispatchPendingRows(rows = []) {
const planQuantities = new Map(); const planQuantities = new Map();
this.dispatchPlanGoodsRows.forEach(goods => { this.dispatchPlanGoodsRows.forEach(goods => {
@@ -13409,14 +13441,29 @@ export default {
}), }),
]; ];
const firstGoods = goodsRows[0] || {}; const firstGoods = goodsRows[0] || {};
const quantitySum = goodsRows.reduce(
(sum, goods) => sum + this.parseDispatchQuantity(goods.quantity),
0
);
const quantityText = quantitySum
? this.formatDispatchQuantity(quantitySum)
: firstGoods.quantity || this.dispatchItemForm.quantity || '';
const otherFeeTotal = this.normalizeDispatchFeeValue(this.dispatchItemForm.otherFeeTotal);
const nextRow = { const nextRow = {
...this.dispatchItemForm, ...this.dispatchItemForm,
goodsJson: JSON.stringify(goodsRows), goodsJson: JSON.stringify(goodsRows),
cargoType: firstGoods.cargoType || this.dispatchItemForm.cargoType || '', cargoType: firstGoods.cargoType || this.dispatchItemForm.cargoType || '',
cargoTypeCode: firstGoods.cargoTypeCode || this.dispatchItemForm.cargoTypeCode || '', cargoTypeCode: firstGoods.cargoTypeCode || this.dispatchItemForm.cargoTypeCode || '',
cargoTypePath: firstGoods.cargoTypePath || this.dispatchItemForm.cargoTypePath || [], cargoTypePath: firstGoods.cargoTypePath || this.dispatchItemForm.cargoTypePath || [],
cargoName: firstGoods.cargoName || this.dispatchItemForm.cargoName || '', cargoName:
quantity: firstGoods.quantity || this.dispatchItemForm.quantity || '', goodsRows
.map(goods => goods.cargoName || goods.goodsName || '')
.filter(Boolean)
.join('.') ||
firstGoods.cargoName ||
this.dispatchItemForm.cargoName ||
'',
quantity: quantityText,
quantityUnit: firstGoods.quantityUnit || this.dispatchItemForm.quantityUnit || '', quantityUnit: firstGoods.quantityUnit || this.dispatchItemForm.quantityUnit || '',
unitPrice: firstGoods.unitPrice || '', unitPrice: firstGoods.unitPrice || '',
priceUnit: firstGoods.priceUnit || '', priceUnit: firstGoods.priceUnit || '',
@@ -13424,7 +13471,7 @@ export default {
freightJson: JSON.stringify({ freightJson: JSON.stringify({
currency: this.dispatchItemForm.freightCurrency || 'CNY', currency: this.dispatchItemForm.freightCurrency || 'CNY',
totalFreightAmount: this.dispatchItemFreightSubtotal, totalFreightAmount: this.dispatchItemFreightSubtotal,
otherFreightAmount: this.dispatchItemForm.otherFeeTotal || '', otherFreightAmount: otherFeeTotal,
freightItems: goodsRows.map((cargo, index) => ({ freightItems: goodsRows.map((cargo, index) => ({
cargoIndex: index, cargoIndex: index,
cargoName: cargo.cargoName || '', cargoName: cargo.cargoName || '',
@@ -13440,7 +13487,7 @@ export default {
}; };
nextRow.cargoInfo = this.formatDispatchCargoInfo({}, nextRow); nextRow.cargoInfo = this.formatDispatchCargoInfo({}, nextRow);
nextRow.freight = this.getDispatchFeeFields({}, nextRow).freight; nextRow.freight = this.getDispatchFeeFields({}, nextRow).freight;
nextRow.otherFeeTotal = this.dispatchItemForm.otherFeeTotal || ''; nextRow.otherFeeTotal = otherFeeTotal;
const quantityUnit = this.getDispatchQuantityUnit(nextRow); const quantityUnit = this.getDispatchQuantityUnit(nextRow);
const otherDispatchedQuantity = this.dispatchRows.reduce((total, row, index) => { const otherDispatchedQuantity = this.dispatchRows.reduce((total, row, index) => {
if ( if (
@@ -1,10 +1,14 @@
<template> <template>
<div class="contract-attachment-section">
<section <section
v-if="!dialogOnly"
class="contract-manage-form__section contract-manage-form__section--panel contract-manage-form__section--attachment" class="contract-manage-form__section contract-manage-form__section--panel contract-manage-form__section--attachment"
> >
<div class="contract-manage-form__attachment-head"> <div class="contract-manage-form__attachment-head">
<div class="dialog-section-title">{{ title }}</div> <div class="dialog-section-title">{{ title }}</div>
<el-button type="primary" :disabled="!rows.length" @click="batchDownload">批量下载</el-button> <el-button type="primary" :disabled="!rows.length" @click="batchDownload"
>批量下载</el-button
>
</div> </div>
<el-table <el-table
:data="rows" :data="rows"
@@ -15,19 +19,20 @@
<el-table-column v-if="!readonly" type="selection" width="55" align="center" /> <el-table-column v-if="!readonly" type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" /> <el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center"> <el-table-column v-if="attachmentType" label="附件类型" min-width="160" align="center">
<template #default="{ row }"> <template #default="{ row, $index }">
<span v-if="readonly || isRowLocked(row)">{{ row.fileType || '-' }}</span> <span v-if="readonly || isRowLocked(row)">{{ row.fileType || '-' }}</span>
<el-select <el-select
v-else v-else
:model-value="row.fileType" :model-value="row.fileType"
placeholder="请选择" placeholder="请选择"
:teleported="false" style="width: 100%"
:fit-input-width="true" teleported
@update:model-value="value => updateRowFileType(row, value)" clearable
@update:model-value="value => updateRowFileType($index, value)"
> >
<el-option <el-option
v-for="item in contractAttachmentTypeOptions" v-for="item in contractAttachmentTypeOptions"
:key="item.value" :key="`${item.value}-${item.label}`"
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
/> />
@@ -42,17 +47,19 @@
show-overflow-tooltip show-overflow-tooltip
> >
<template #default="{ row }"> <template #default="{ row }">
<el-link type="primary" @click="preview?.(row, rows)">{{ attachmentName(row) }}</el-link> <el-link type="primary" @click="preview?.(row, rows)">{{
attachmentName(row)
}}</el-link>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column v-if="description" label="附件描述" min-width="220"> <el-table-column v-if="description" label="附件描述" min-width="220">
<template #default="{ row }"> <template #default="{ row, $index }">
<span v-if="readonly || isRowLocked(row)">{{ row.description || '-' }}</span> <span v-if="readonly || isRowLocked(row)">{{ row.description || '-' }}</span>
<el-input <el-input
v-else v-else
:model-value="row.description" :model-value="row.description"
maxlength="200" maxlength="200"
@update:model-value="value => updateRowDescription(row, value)" @update:model-value="value => updateRowDescription($index, value)"
/> />
</template> </template>
</el-table-column> </el-table-column>
@@ -85,10 +92,11 @@
tip="支持pdfbmpjpegpngjpgdocdocxpptpptxxlsxxlsemlmsgzip的文件格式单个文件不超过50M" tip="支持pdfbmpjpegpngjpgdocdocxpptpptxxlsxxlsemlmsgzip的文件格式单个文件不超过50M"
:show-file-list="false" :show-file-list="false"
button-text="上传附件" button-text="上传附件"
@update:model-value="handleChange"
@change="handleChange" @change="handleChange"
@success="handleUploadSuccess"
/> />
</div> </div>
</section>
<el-dialog <el-dialog
v-model="uploadDialogVisible" v-model="uploadDialogVisible"
@@ -103,8 +111,18 @@
<div class="contract-attachment-upload-dialog__body"> <div class="contract-attachment-upload-dialog__body">
<div class="contract-attachment-upload-dialog__field"> <div class="contract-attachment-upload-dialog__field">
<span class="contract-attachment-upload-dialog__label">附件位置</span> <span class="contract-attachment-upload-dialog__label">附件位置</span>
<el-select :model-value="attachmentLocation" disabled style="width: 100%"> <el-select
<el-option :label="attachmentLocation" :value="attachmentLocation" /> v-model="uploadDialogLocation"
placeholder="请选择附件位置"
style="width: 100%"
@change="syncUploadDialogTypeByLocation"
>
<el-option
v-for="item in resolvedLocationOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select> </el-select>
</div> </div>
<div class="contract-attachment-upload-dialog__field"> <div class="contract-attachment-upload-dialog__field">
@@ -144,7 +162,7 @@
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
</section> </div>
</template> </template>
<script> <script>
@@ -154,11 +172,11 @@ import { getUploadHeaders } from '@/utils/upload';
import { downloadFileByUrl } from '@/utils/util'; import { downloadFileByUrl } from '@/utils/util';
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz'; import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
const CONTRACT_ATTACHMENT_TYPE_OTHER = '其文件'; const CONTRACT_ATTACHMENT_TYPE_OTHER = '其文件';
export const defaultContractAttachmentTypeOptions = [ export const defaultContractAttachmentTypeOptions = [
{ label: '合同文件', value: '合同文件' }, { label: '合同文件', value: '合同文件' },
{ label: '双章归档文件', value: '双章归档文件' }, { label: '双章归档文件', value: '双章归档文件' },
{ label: '其文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }, { label: '其文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER },
]; ];
/** @deprecated 兼容旧引用,实际选项由业务字典 contract_attachment_type 动态加载 */ /** @deprecated 兼容旧引用,实际选项由业务字典 contract_attachment_type 动态加载 */
export const contractAttachmentTypeOptions = defaultContractAttachmentTypeOptions; export const contractAttachmentTypeOptions = defaultContractAttachmentTypeOptions;
@@ -201,21 +219,32 @@ const normalizeAttachmentTypeText = value => {
.replace(/[\s_\-—–·•()()\[\]【】{}《》<>“”"'、,,。.]/g, ''); .replace(/[\s_\-—–·•()()\[\]【】{}《》<>“”"'、,,。.]/g, '');
}; };
const isOtherAttachmentTypeOption = item => {
const text = `${item?.label || ''}${item?.value || ''}`;
return (
String(item?.value) === CONTRACT_ATTACHMENT_TYPE_OTHER ||
String(item?.value) === '其它文件' ||
text.includes('其他文件') ||
text.includes('其它文件') ||
text.includes('其他') ||
text.includes('其它')
);
};
const attachmentTypeKeywords = fileType => { const attachmentTypeKeywords = fileType => {
if (fileType === '双章归档文件') return ['双章归档文件', '双章归档', '双章']; const text = String(fileType || '');
if (fileType === '合同文件') return ['合同文件', '合同']; if (text.includes('双章')) return ['双章归档文件', '双章归档', '双章'];
return [fileType]; if (text.includes('合同') && !text.includes('其他') && !text.includes('其它')) {
return ['合同文件', '合同'];
}
if (isOtherAttachmentTypeOption({ label: text, value: text })) return [];
return text ? [text] : [];
}; };
const resolveOtherAttachmentType = (options = []) => { const resolveOtherAttachmentType = (options = []) => {
const list = options.length ? options : defaultContractAttachmentTypeOptions; const list = options.length ? options : defaultContractAttachmentTypeOptions;
const matched = list.find( const matched = list.find(isOtherAttachmentTypeOption);
item => return matched?.value || CONTRACT_ATTACHMENT_TYPE_OTHER;
String(item.value) === CONTRACT_ATTACHMENT_TYPE_OTHER ||
String(item.label).includes('其它') ||
String(item.label).includes('其他')
);
return matched?.value || list[list.length - 1]?.value || CONTRACT_ATTACHMENT_TYPE_OTHER;
}; };
export const resolveContractAttachmentType = ( export const resolveContractAttachmentType = (
@@ -223,15 +252,19 @@ export const resolveContractAttachmentType = (
options = defaultContractAttachmentTypeOptions options = defaultContractAttachmentTypeOptions
) => { ) => {
const list = options.length ? options : defaultContractAttachmentTypeOptions; const list = options.length ? options : defaultContractAttachmentTypeOptions;
const otherType = resolveOtherAttachmentType(list);
const values = list.map(item => item.value); const values = list.map(item => item.value);
if (values.includes(row.fileType)) return row.fileType; if (values.includes(row.fileType)) return row.fileType;
const matchedByLabel = list.find(item => String(item.label) === String(row.fileType));
if (matchedByLabel) return matchedByLabel.value;
const fileName = normalizeAttachmentTypeText(attachmentName(row)); const fileName = normalizeAttachmentTypeText(attachmentName(row));
const otherType = resolveOtherAttachmentType(list);
if (!fileName) return otherType; if (!fileName) return otherType;
const matched = list const matched = list
.filter(item => item.value !== otherType) .filter(item => !isOtherAttachmentTypeOption(item))
.flatMap(item => .flatMap(item =>
attachmentTypeKeywords(item.value).map(keyword => ({ attachmentTypeKeywords(item.value)
.concat(attachmentTypeKeywords(item.label))
.map(keyword => ({
value: item.value, value: item.value,
keyword: normalizeAttachmentTypeText(keyword), keyword: normalizeAttachmentTypeText(keyword),
})) }))
@@ -239,6 +272,7 @@ export const resolveContractAttachmentType = (
.filter(item => item.keyword) .filter(item => item.keyword)
.sort((a, b) => b.keyword.length - a.keyword.length) .sort((a, b) => b.keyword.length - a.keyword.length)
.find(item => fileName.includes(item.keyword)); .find(item => fileName.includes(item.keyword));
// 文件名未命中任何附件类型时,默认「其他文件」
return matched?.value || otherType; return matched?.value || otherType;
}; };
@@ -272,16 +306,19 @@ export default {
lockApproved: Boolean, lockApproved: Boolean,
markApprovedOnUpload: Boolean, markApprovedOnUpload: Boolean,
useUploadDialog: Boolean, useUploadDialog: Boolean,
dialogOnly: Boolean,
attachmentLocation: { type: String, default: '合同文件' }, attachmentLocation: { type: String, default: '合同文件' },
attachmentLocationOptions: { type: Array, default: null },
preview: { type: Function, default: null }, preview: { type: Function, default: null },
}, },
emits: ['update:rows'], emits: ['update:rows', 'upload-to-location', 'upload-confirmed'],
data() { data() {
return { return {
selected: [], selected: [],
attachmentFileTypes, attachmentFileTypes,
contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions], contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions],
uploadDialogVisible: false, uploadDialogVisible: false,
uploadDialogLocation: '合同文件',
uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions), uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions),
uploadDialogFiles: [], uploadDialogFiles: [],
uploadDialogFileList: [], uploadDialogFileList: [],
@@ -300,6 +337,11 @@ export default {
defaultAttachmentType() { defaultAttachmentType() {
return resolveOtherAttachmentType(this.contractAttachmentTypeOptions); return resolveOtherAttachmentType(this.contractAttachmentTypeOptions);
}, },
resolvedLocationOptions() {
const options = (this.attachmentLocationOptions || []).filter(Boolean);
if (options.length) return options;
return ['合同文件', '其它附件'];
},
}, },
created() { created() {
this.loadAttachmentTypeOptions(); this.loadAttachmentTypeOptions();
@@ -311,12 +353,28 @@ export default {
.then(res => { .then(res => {
const options = mapDictOptions(res); const options = mapDictOptions(res);
if (options.length) { if (options.length) {
this.contractAttachmentTypeOptions = options; // 字典缺少「其他文件」时补上,保证未匹配文件名可默认选中
this.contractAttachmentTypeOptions = options.some(isOtherAttachmentTypeOption)
? options
: [...options, { label: '其他文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }];
if ( if (
!options.some(item => String(item.value) === String(this.uploadDialogType)) !this.contractAttachmentTypeOptions.some(
item => String(item.value) === String(this.uploadDialogType)
)
) { ) {
this.uploadDialogType = this.defaultAttachmentType; this.uploadDialogType = this.defaultAttachmentType;
} }
// 字典加载后,把已有行的中文类型名对齐到 dictKey,保证下拉可选中
if (this.attachmentType && (this.rows || []).length) {
const aligned = (this.rows || []).map(row => ({
...row,
fileType: resolveContractAttachmentType(row, this.contractAttachmentTypeOptions),
}));
const changed = aligned.some(
(row, index) => row.fileType !== this.rows[index]?.fileType
);
if (changed) this.update(aligned);
}
} }
}) })
.catch(() => { .catch(() => {
@@ -327,20 +385,76 @@ export default {
return this.lockApproved && isAttachmentApproved(row); return this.lockApproved && isAttachmentApproved(row);
}, },
update(rows) { update(rows) {
this.$emit('update:rows', rows); this.$emit('update:rows', Array.isArray(rows) ? rows.map(item => ({ ...item })) : []);
}, },
updateRowFileType(row, value) { enrichUploadedRow(row, existing) {
if (this.isRowLocked(row)) return; const uploadUserName = this.$store.getters.userInfo?.realName || '';
row.fileType = value; const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.update([...this.rows]); const next = {
...existing,
...row,
description: row.description || existing?.description || '',
uploadUserName: existing?.uploadUserName || row.uploadUserName || uploadUserName,
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
approved: existing?.approved || row.approved || false,
};
if (this.attachmentType) {
next.fileType = existing?.fileType
? existing.fileType
: resolveContractAttachmentType(
{ ...next, fileType: '' },
this.contractAttachmentTypeOptions
);
}
if (this.markApprovedOnUpload && !existing) next.approved = true;
return next;
}, },
updateRowDescription(row, value) { findExistingRow(row, rows = this.rows) {
if (this.isRowLocked(row)) return; const fileUrl = attachmentUrl(row);
row.description = value; return (rows || []).find(item => {
this.update([...this.rows]); const sameUid = row.uid && item.uid && String(row.uid) === String(item.uid);
const sameUrl = fileUrl && attachmentUrl(item) === fileUrl;
return sameUid || sameUrl;
});
},
updateRowFileType(index, value) {
const rows = [...(this.rows || [])];
const row = rows[index];
if (!row || this.isRowLocked(row)) return;
rows[index] = { ...row, fileType: value };
this.update(rows);
},
updateRowDescription(index, value) {
const rows = [...(this.rows || [])];
const row = rows[index];
if (!row || this.isRowLocked(row)) return;
rows[index] = { ...row, description: value };
this.update(rows);
},
resolveTypeByLocation(location) {
const options = this.contractAttachmentTypeOptions || [];
const locationText = String(location || '');
if (locationText.includes('其它') || locationText.includes('其他')) {
return this.defaultAttachmentType;
}
const matched = options.find(
item =>
String(item.value) === locationText ||
String(item.label) === locationText ||
String(item.value).includes('合同') ||
String(item.label).includes('合同')
);
return matched?.value || options[0]?.value || this.defaultAttachmentType;
},
syncUploadDialogTypeByLocation() {
this.uploadDialogType = this.resolveTypeByLocation(this.uploadDialogLocation);
}, },
openUploadDialog() { openUploadDialog() {
this.uploadDialogType = this.defaultAttachmentType; const options = this.resolvedLocationOptions;
this.uploadDialogLocation = options.includes(this.attachmentLocation)
? this.attachmentLocation
: options[0] || this.attachmentLocation;
this.syncUploadDialogTypeByLocation();
this.uploadDialogFiles = []; this.uploadDialogFiles = [];
this.uploadDialogFileList = []; this.uploadDialogFileList = [];
this.uploadDialogVisible = true; this.uploadDialogVisible = true;
@@ -348,7 +462,8 @@ export default {
resetUploadDialog() { resetUploadDialog() {
this.uploadDialogFiles = []; this.uploadDialogFiles = [];
this.uploadDialogFileList = []; this.uploadDialogFileList = [];
this.uploadDialogType = this.defaultAttachmentType; this.uploadDialogLocation = this.attachmentLocation;
this.syncUploadDialogTypeByLocation();
}, },
beforeUpload(file) { beforeUpload(file) {
const extension = String(file.name || '') const extension = String(file.name || '')
@@ -391,6 +506,8 @@ export default {
...this.uploadDialogFiles.filter(item => String(item.uid) !== String(file.uid)), ...this.uploadDialogFiles.filter(item => String(item.uid) !== String(file.uid)),
normalized, normalized,
]; ];
const locationText = String(this.uploadDialogLocation || '');
if (!(locationText.includes('其它') || locationText.includes('其他'))) {
this.uploadDialogType = resolveContractAttachmentType( this.uploadDialogType = resolveContractAttachmentType(
{ {
...normalized, ...normalized,
@@ -398,6 +515,9 @@ export default {
}, },
this.contractAttachmentTypeOptions this.contractAttachmentTypeOptions
); );
} else {
this.uploadDialogType = this.defaultAttachmentType;
}
this.$message.success('上传成功'); this.$message.success('上传成功');
}, },
handleDialogError() { handleDialogError() {
@@ -416,6 +536,10 @@ export default {
this.$message.warning('请先上传附件'); this.$message.warning('请先上传附件');
return; return;
} }
if (!this.uploadDialogLocation) {
this.$message.warning('请选择附件位置');
return;
}
if (!this.uploadDialogType) { if (!this.uploadDialogType) {
this.$message.warning('请选择附件类型'); this.$message.warning('请选择附件类型');
return; return;
@@ -430,43 +554,39 @@ export default {
uploadTime: row.uploadTime || uploadTime, uploadTime: row.uploadTime || uploadTime,
...(this.markApprovedOnUpload ? { approved: true } : {}), ...(this.markApprovedOnUpload ? { approved: true } : {}),
})); }));
this.update([...(this.rows || []), ...appended]); // 统一交给父组件按附件位置写入对应列表,避免同位置双写或跨位置不显示
this.uploadDialogVisible = false; this.$emit('upload-to-location', {
}, location: this.uploadDialogLocation,
handleChange(rows) { rows: appended,
const uploadUserName = this.$store.getters.userInfo?.realName || ''; });
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss'); this.uploadDialogVisible = false;
const existingRows = this.rows || []; this.$emit('upload-confirmed', {
this.update( location: this.uploadDialogLocation,
(rows || []).map(row => { rows: appended,
const fileUrl = attachmentUrl(row);
const existing = existingRows.find(item => {
const sameUid = row.uid && item.uid && String(row.uid) === String(item.uid);
const sameUrl = fileUrl && attachmentUrl(item) === fileUrl;
return sameUid || sameUrl;
}); });
if (existing && this.isRowLocked(existing)) return { ...existing };
const next = {
...existing,
...row,
description: row.description || existing?.description || '',
uploadUserName: existing?.uploadUserName || row.uploadUserName || uploadUserName,
uploadTime: existing?.uploadTime || row.uploadTime || uploadTime,
approved: existing?.approved || row.approved || false,
};
if (this.attachmentType) {
next.fileType = resolveContractAttachmentType(
{
...next,
fileType: existing?.fileType || row.fileType,
}, },
this.contractAttachmentTypeOptions handleChange(list) {
); if (!Array.isArray(list)) return;
} // 上传组件偶发回传空列表时,禁止清空已有表格数据
if (this.markApprovedOnUpload && !existing) next.approved = true; if (!list.length) return;
return next; const existingRows = this.rows || [];
}) const nextRows = list.map(row => {
); const existing = this.findExistingRow(row, existingRows);
if (existing && this.isRowLocked(existing)) return { ...existing };
return this.enrichUploadedRow(row, existing);
});
existingRows.forEach(item => {
if (!this.isRowLocked(item)) return;
if (this.findExistingRow(item, nextRows)) return;
nextRows.unshift({ ...item });
});
this.update(nextRows);
},
handleUploadSuccess(file) {
const url = attachmentUrl(file);
if (!file || !url) return;
if (this.findExistingRow(file)) return;
this.update([...(this.rows || []), this.enrichUploadedRow(file)]);
}, },
remove(index) { remove(index) {
const row = this.rows[index]; const row = this.rows[index];
@@ -474,7 +594,7 @@ export default {
this.$message.warning('已审核通过的附件不允许删除'); this.$message.warning('已审核通过的附件不允许删除');
return; return;
} }
const rows = [...this.rows]; const rows = [...(this.rows || [])];
rows.splice(index, 1); rows.splice(index, 1);
this.update(rows); this.update(rows);
}, },
@@ -487,7 +607,7 @@ export default {
downloadFileByUrl(url, attachmentName(row)); downloadFileByUrl(url, attachmentName(row));
}, },
batchDownload() { batchDownload() {
(this.selected.length ? this.selected : this.rows).forEach(this.download); (this.selected.length ? this.selected : this.rows || []).forEach(this.download);
}, },
formatSize(size) { formatSize(size) {
const value = Number(size); const value = Number(size);
@@ -504,7 +624,7 @@ export default {
.contract-manage-form__section { .contract-manage-form__section {
margin: 12px 0 0; margin: 12px 0 0;
padding: 14px 16px 16px; padding: 14px 16px 16px;
overflow: hidden; overflow: visible;
background: #fff; background: #fff;
border-radius: 6px; border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
@@ -527,6 +647,15 @@ export default {
} }
} }
.contract-manage-form__attachment-table {
:deep(.el-table__body-wrapper),
:deep(.el-table__header-wrapper),
:deep(.el-table__cell),
:deep(.cell) {
overflow: visible;
}
}
.contract-manage-form__attachment-head { .contract-manage-form__attachment-head {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1,7 +1,17 @@
<template> <template>
<div v-loading="loading" class="master-detail"> <div v-loading="loading" class="master-detail">
<section v-if="master" class="detail-overview"> <section v-if="master" class="detail-overview">
<div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)" class="status-text">{{ statusName(master.businessStatus) }}</el-tag><el-tag v-if="transportFlowLabel" type="info" class="transport-flow-tag">{{ transportFlowLabel }}</el-tag></div></div> <div class="detail-heading">
<div>
<h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2>
<el-tag :type="statusType(master.businessStatus)">{{
statusName(master.businessStatus)
}}</el-tag>
<el-tag v-if="transportFlowLabel" type="primary" class="transport-flow-tag">{{
transportFlowLabel
}}</el-tag>
</div>
</div>
<dl class="detail-meta"> <dl class="detail-meta">
<div> <div>
<dt>客户</dt> <dt>客户</dt>
@@ -132,6 +142,7 @@ export default {
dateRange() { return this.master?.planStartTime && this.master?.planEndTime ? `${String(this.master.planStartTime).slice(0, 10)} ~ ${String(this.master.planEndTime).slice(0, 10)}` : '-'; }, dateRange() { return this.master?.planStartTime && this.master?.planEndTime ? `${String(this.master.planStartTime).slice(0, 10)} ~ ${String(this.master.planEndTime).slice(0, 10)}` : '-'; },
}, },
async mounted() { async mounted() {
if (!this.id) return;
this.loading = true; this.loading = true;
try { try {
const res = await api.getDetail(this.id); const res = await api.getDetail(this.id);
@@ -282,8 +293,36 @@ export default {
<style scoped lang="scss"> <style scoped lang="scss">
.master-detail { padding-bottom: 20px; color: #303133; } .master-detail { padding-bottom: 20px; color: #303133; }
.detail-overview, .execution-detail-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; } .detail-overview, .execution-detail-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
.detail-heading { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } } .detail-heading {
.transport-flow-tag { margin-left: 8px; } display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 24px;
h2 {
display: inline-block;
margin: 0 16px 0 0;
font-size: 20px;
}
h2 span {
margin: 0 8px;
color: #909399;
font-weight: 400;
}
:deep(.el-tag) {
height: 28px;
padding: 0 12px;
border: 0;
border-radius: 8px;
font-size: 13px;
line-height: 28px;
}
}
.transport-flow-tag {
margin-left: 8px;
}
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } :deep(.el-link) { font-size: 14px; } } .detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #303133; font-size: 14px; word-break: break-all; } :deep(.el-link) { font-size: 14px; } }
.route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; } .route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; }
.route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } } .route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
@@ -156,11 +156,11 @@
<el-row :gutter="16"> <el-row :gutter="16">
<template v-if="isRoad(route)"> <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-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="fetchDriverSuggestions" clearable placeholder="请输入司机" :loading="driverLoading" @select="item => handleDriverSuggestionSelect(route, item)" /></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="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 :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-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="fetchEscortSuggestions" 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-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 v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号"><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
</template> </template>
<template v-else> <template v-else>
@@ -233,7 +233,7 @@
<script> <script>
import * as api from '@/api/business/master-order'; import * as api from '@/api/business/master-order';
import { getList as getDriverList } from '@/api/transportCapacity/driver'; import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
import { isMobile } from '@/utils/validate'; import { isMobile } from '@/utils/validate';
const unwrapRecords = res => { const unwrapRecords = res => {
@@ -290,11 +290,12 @@ export default {
}, },
}, },
async mounted() { async mounted() {
await Promise.all([this.load(), this.loadCarrierOptions(), this.loadDriverOptions()]); await Promise.all([this.load(), this.loadCarrierOptions()]);
this.applySelfOperatedCarrierDefaults(); this.applySelfOperatedCarrierDefaults();
}, },
methods: { methods: {
async load() { async load() {
if (!this.id) return;
this.loading = true; this.loading = true;
try { try {
const res = await api.getDetail(this.id); const res = await api.getDetail(this.id);
@@ -726,6 +727,7 @@ export default {
route.carrierName = ''; route.carrierName = '';
route.carrierId = ''; route.carrierId = '';
route.carrierContractId = ''; route.carrierContractId = '';
this.clearRouteDriverVehicleFields(route);
if (value === '承运商') { if (value === '承运商') {
route.trailerVehicleNo = ''; route.trailerVehicleNo = '';
route.escortName = ''; route.escortName = '';
@@ -745,6 +747,24 @@ export default {
route.carrierContractId = route.carrierType !== '自运' ? contract?.contractId || '' : ''; route.carrierContractId = route.carrierType !== '自运' ? contract?.contractId || '' : '';
route.carrierId = route.carrierType !== '自运' ? contract?.carrierId || '' : ''; route.carrierId = route.carrierType !== '自运' ? contract?.carrierId || '' : '';
route.carrierName = contract?.carrierName || ''; route.carrierName = contract?.carrierName || '';
this.clearRouteDriverVehicleFields(route);
},
clearRouteDriverVehicleFields(route) {
if (!route) return;
route.driverId = '';
route.driverName = '';
route.driverPhone = '';
route.vehicleNo = '';
route.trailerVehicleNo = '';
route.escortName = '';
route.escortPhone = '';
this.driverOptions = [];
},
getRouteCarrierFilter(route = {}) {
return {
carrierId: route.carrierId || '',
carrierName: route.carrierName || '',
};
}, },
carrierOptionValue(item = {}) { carrierOptionValue(item = {}) {
return item.contractId || item.id || item.value || item.carrierName || ''; return item.contractId || item.id || item.value || item.carrierName || '';
@@ -816,18 +836,30 @@ export default {
route.carrierContractId = ''; route.carrierContractId = '';
}); });
}, },
async loadDriverOptions(keyword = '') { async loadDriverOptions(route = {}, keyword = '') {
const carrier = this.getRouteCarrierFilter(route);
if (!carrier.carrierId && !carrier.carrierName) {
this.driverOptions = [];
return [];
}
this.driverLoading = true; this.driverLoading = true;
try { try {
this.driverOptions = unwrapRecords( this.driverOptions = await fetchDriversByCarrierOrganizations(
await getDriverList(1, 20, { driverName: keyword, posts: '司机' }) { size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
); );
return this.driverOptions;
} finally { } finally {
this.driverLoading = false; this.driverLoading = false;
} }
}, },
fetchDriverSuggestions(queryString, callback) { fetchDriverSuggestions(route, queryString, callback) {
this.loadDriverOptions(String(queryString || '').trim()) const carrier = this.getRouteCarrierFilter(route);
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.loadDriverOptions(route, String(queryString || '').trim())
.then(() => .then(() =>
callback( callback(
this.driverOptions.map(item => ({ this.driverOptions.map(item => ({
@@ -845,13 +877,21 @@ export default {
const drivingVehicle = String(item.drivingVehicle || '').trim(); const drivingVehicle = String(item.drivingVehicle || '').trim();
if (drivingVehicle) route.vehicleNo = drivingVehicle; if (drivingVehicle) route.vehicleNo = drivingVehicle;
}, },
fetchEscortSuggestions(queryString, callback) { fetchEscortSuggestions(route, queryString, callback) {
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
const carrier = this.getRouteCarrierFilter(route);
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.escortLoading = true; this.escortLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' }) fetchDriversByCarrierOrganizations(
.then(res => { { size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
carrier
)
.then(records => {
callback( callback(
unwrapRecords(res).map(item => ({ records.map(item => ({
...item, ...item,
value: this.driverOptionLabel(item), value: this.driverOptionLabel(item),
})) }))
@@ -402,8 +402,8 @@
> >
</div> </div>
<div class="map-picker-content"> <div class="map-picker-content">
<map-search-results :results="mapSearchResults" @select="selectAddressMapSearchResult" />
<div ref="addressMap" class="address-map" /> <div ref="addressMap" class="address-map" />
<map-search-results :results="mapSearchResults" @select="selectAddressMapSearchResult" />
</div> </div>
<div class="address-map-status"> <div class="address-map-status">
{{ mapStatus {{ mapStatus
@@ -601,6 +601,7 @@ let amapLoader;
import { getList as getCommonRouteList } from '@/api/business/common-route'; import { getList as getCommonRouteList } from '@/api/business/common-route';
import * as api from '@/api/business/master-order'; import * as api from '@/api/business/master-order';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { isMobile } from '@/utils/validate'; import { isMobile } from '@/utils/validate';
export default { export default {
components: { components: {
@@ -1285,14 +1286,8 @@ export default {
) )
) )
.then(result => { .then(result => {
const geocodes = result?.geocodes || []; this.mapSearchResults = normalizeMapSearchResults(result, this.mapKeyword);
this.mapSearchResults = geocodes.map((item, index) => ({ const point = this.mapSearchResults[0]?.location;
id: item.id || index,
name: item.formattedAddress || this.mapKeyword,
address: item.formattedAddress || this.mapKeyword,
location: item.location,
}));
const point = geocodes[0]?.location;
if (!point) return this.$message.warning('地图搜索无匹配地址'); if (!point) return this.$message.warning('地图搜索无匹配地址');
this.pickAddressMap(point, this.mapKeyword); this.pickAddressMap(point, this.mapKeyword);
}) })
@@ -2199,8 +2194,15 @@ export default {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-bottom: 12px; margin-bottom: 12px;
.el-input {
flex: 1;
min-width: 0;
}
} }
.address-map { .address-map {
width: 100%;
min-width: 0;
height: 440px; height: 440px;
} }
.address-map-status { .address-map-status {
@@ -931,11 +931,11 @@
/><el-button type="primary" @click="searchTransportMapKeyword">搜索</el-button> /><el-button type="primary" @click="searchTransportMapKeyword">搜索</el-button>
</div> </div>
<div class="map-picker-content"> <div class="map-picker-content">
<div ref="transportMap" class="shipping-template-page__map" />
<map-search-results <map-search-results
:results="transportMapSearchResults" :results="transportMapSearchResults"
@select="selectTransportMapSearchResult" @select="selectTransportMapSearchResult"
/> />
<div ref="transportMap" class="shipping-template-page__map" />
</div> </div>
<div class="shipping-template-page__map-info">{{ transportMapStatus }}</div> <div class="shipping-template-page__map-info">{{ transportMapStatus }}</div>
<template #footer <template #footer
@@ -1098,6 +1098,7 @@ import {
import { getDictionary as getSystemDictionary } from '@/api/system/dict'; import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { packageOptions } from '@/option/business/common'; import { packageOptions } from '@/option/business/common';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { downloadFileByUrl, downloadXls } from '@/utils/util'; import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate'; import { isMobile } from '@/utils/validate';
import { Location } from '@element-plus/icons-vue'; import { Location } from '@element-plus/icons-vue';
@@ -2615,12 +2616,7 @@ export default {
.then(() => this.ensureTransportMapGeocoder()) .then(() => this.ensureTransportMapGeocoder())
.then(() => this.runTransportMapGeocode('location', keyword)) .then(() => this.runTransportMapGeocode('location', keyword))
.then(result => { .then(result => {
this.transportMapSearchResults = (result.geocodes || []).map((item, index) => ({ this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
id: item.id || index,
name: item.formattedAddress || keyword,
address: item.formattedAddress || keyword,
location: item.location,
}));
const point = this.resolveTransportMapPoint(result); const point = this.resolveTransportMapPoint(result);
if (!point) throw new Error('未找到匹配地址'); if (!point) throw new Error('未找到匹配地址');
return this.$nextTick().then(() => { return this.$nextTick().then(() => {
@@ -3754,6 +3750,12 @@ export default {
.shipping-template-page__map-toolbar { .shipping-template-page__map-toolbar {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-bottom: 12px;
.el-input {
flex: 1;
min-width: 0;
}
} }
.shipping-template-page__dialog-search { .shipping-template-page__dialog-search {
margin-bottom: 8px; margin-bottom: 8px;
@@ -3766,11 +3768,9 @@ export default {
justify-content: flex-end; justify-content: flex-end;
} }
.shipping-template-page__map { .shipping-template-page__map {
flex: 1 1 auto;
width: 100%; width: 100%;
min-width: 0; min-width: 0;
height: 480px; height: 480px;
margin-top: 12px;
} }
:deep(.shipping-template-page__map > .amap-container) { :deep(.shipping-template-page__map > .amap-container) {
width: 100% !important; width: 100% !important;
@@ -637,13 +637,14 @@
<span>|</span> <span>|</span>
{{ detailRow.planNo || '-' }} {{ detailRow.planNo || '-' }}
</h2> </h2>
<span class="detail-status-text is-dispatching"> <span class="detail-status-text" :class="detailStatusTextClass">
{{ displayStatus(detailRow, 'businessStatus') }} {{ displayStatus(detailRow, 'businessStatus') }}
</span> </span>
<span class="detail-status-text detail-transport-type"> <span class="detail-status-text detail-transport-type">
{{ {{
detailRow.transportTypeName || detailRow.transportTypeName ||
formatTransportPlanDetailTransportType(detailRow.transportType) formatTransportPlanDetailTransportType(detailRow.transportType) ||
'-'
}} }}
</span> </span>
</div> </div>
@@ -1231,7 +1232,7 @@
{{ dispatchDialogStatusText }} {{ dispatchDialogStatusText }}
</el-tag> </el-tag>
<el-tag type="primary" effect="light"> <el-tag type="primary" effect="light">
{{ formatDispatchTransportType(dispatchRow.transportType) || '公路运输' }} {{ formatDispatchTransportType(dispatchRow.transportType) || '-' }}
</el-tag> </el-tag>
</div> </div>
</div> </div>
@@ -1931,7 +1932,7 @@
class="transport-plan-page__dispatch-quantity-hint" class="transport-plan-page__dispatch-quantity-hint"
> >
剩余数量{{ 剩余数量{{
formatDispatchQuantity(dispatchItemRemainingQuantity(dispatchItemForm)) formatDispatchQuantity(dispatchItemHintRemainingQuantity(dispatchItemForm))
}} }}
{{ dispatchItemForm.quantityUnit || '吨' }} {{ dispatchItemForm.quantityUnit || '吨' }}
</div> </div>
@@ -1997,7 +1998,11 @@
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label="其他费用合计"> <el-form-item label="其他费用合计">
<el-input v-model="dispatchItemForm.otherFeeTotal" placeholder="请输入"> <el-input
v-model="dispatchItemForm.otherFeeTotal"
placeholder="请输入"
@input="value => handleDispatchNumberInput('otherFeeTotal', value)"
>
<template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template> <template #suffix>{{ dispatchItemFreightCurrencyLabel }}</template>
</el-input> </el-input>
</el-form-item> </el-form-item>
@@ -2547,8 +2552,8 @@
</el-button> </el-button>
</div> </div>
<div class="map-picker-content"> <div class="map-picker-content">
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
<div ref="transportAmap" class="transport-plan-page__map"></div> <div ref="transportAmap" class="transport-plan-page__map"></div>
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
</div> </div>
<div class="transport-plan-page__map-info"> <div class="transport-plan-page__map-info">
<span>{{ transportMapStatus }}</span> <span>{{ transportMapStatus }}</span>
@@ -2729,13 +2734,14 @@ import {
getList as getProjectList, getList as getProjectList,
} from '@/api/business/project-apply'; } from '@/api/business/project-apply';
import { getList as getCommonRouteList } from '@/api/business/common-route'; import { getList as getCommonRouteList } from '@/api/business/common-route';
import { getList as getDriverList } from '@/api/transportCapacity/driver'; import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { getDictionary as getSystemDictionary } from '@/api/system/dict'; import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address'; import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common'; import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit'; import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { applyTableMenuWidth } from '@/utils/table-menu'; import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util'; import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate'; import { isMobile } from '@/utils/validate';
@@ -3284,6 +3290,23 @@ export default {
isTransportPlanDetailLayout() { isTransportPlanDetailLayout() {
return this.detailBox; return this.detailBox;
}, },
detailStatusTextClass() {
const status = this.detailRow?.businessStatus;
if (status === 2 || status === '2' || status === 'completed') {
return 'status-text-success';
}
if (status === 1 || status === '1' || status === 'dispatching') {
return 'is-dispatching';
}
if (
status === 3 ||
status === '3' ||
['rejected', 'change_rejected', 'cancelled', 'withdrawn'].includes(status)
) {
return 'is-danger';
}
return 'is-info';
},
transportPlanGoodsText() { transportPlanGoodsText() {
const text = this.formatTransportPlanGoodsInfo(this.detailRow); const text = this.formatTransportPlanGoodsInfo(this.detailRow);
return text && text !== '-' ? text : '暂无货物信息'; return text && text !== '-' ? text : '暂无货物信息';
@@ -3526,17 +3549,19 @@ export default {
}, },
dispatchItemFreightTotal() { dispatchItemFreightTotal() {
if (this.dispatchItemForm.taskEntryMode !== 'full') { if (this.dispatchItemForm.taskEntryMode !== 'full') {
const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0); const otherFeeRaw = this.normalizeDispatchFeeValue(this.dispatchItemForm.otherFeeTotal);
const otherFeeTotal = Number(otherFeeRaw || 0);
const freight = this.dispatchItemFreightSubtotal; const freight = this.dispatchItemFreightSubtotal;
const hasFreight = freight !== ''; const hasFreight = freight !== '';
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== ''; const hasOtherFee = otherFeeRaw !== '';
if (!hasFreight && !hasOtherFee) return ''; if (!hasFreight && !hasOtherFee) return '';
const total = (hasFreight ? Number(freight) : 0) + (hasOtherFee ? otherFeeTotal : 0); const total = (hasFreight ? Number(freight) : 0) + (hasOtherFee ? otherFeeTotal : 0);
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2))); return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
} }
const otherFeeTotal = Number(this.dispatchItemForm.otherFeeTotal || 0); const otherFeeRaw = this.normalizeDispatchFeeValue(this.dispatchItemForm.otherFeeTotal);
const otherFeeTotal = Number(otherFeeRaw || 0);
const freightSubtotal = this.dispatchItemFreightSubtotal; const freightSubtotal = this.dispatchItemFreightSubtotal;
const hasOtherFee = this.dispatchItemForm.otherFeeTotal !== ''; const hasOtherFee = otherFeeRaw !== '';
if (freightSubtotal === '' && !hasOtherFee) return ''; if (freightSubtotal === '' && !hasOtherFee) return '';
const total = Number(freightSubtotal || 0) + (hasOtherFee ? otherFeeTotal : 0); const total = Number(freightSubtotal || 0) + (hasOtherFee ? otherFeeTotal : 0);
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2))); return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(2)));
@@ -4841,10 +4866,17 @@ export default {
}, },
fetchTaskDriverSuggestions(queryString, callback) { fetchTaskDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
const carrier = this.getDispatchCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskDriverLoading = true; this.taskDriverLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' }) fetchDriversByCarrierOrganizations(
.then(res => { { size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
const records = extractRecords(res); carrier
)
.then(records => {
this.taskDriverOptions = records; this.taskDriverOptions = records;
callback( callback(
records.map(item => ({ records.map(item => ({
@@ -4863,10 +4895,17 @@ export default {
}, },
fetchTaskEscortSuggestions(queryString, callback) { fetchTaskEscortSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
const carrier = this.getDispatchCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskEscortLoading = true; this.taskEscortLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' }) fetchDriversByCarrierOrganizations(
.then(res => { { size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
const records = extractRecords(res); carrier
)
.then(records => {
this.taskEscortOptions = records; this.taskEscortOptions = records;
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' }))); callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
}) })
@@ -5738,12 +5777,7 @@ export default {
this.ensureTransportAmapGeocoder() this.ensureTransportAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword)) .then(() => this.runAmapGeocode('location', keyword))
.then(result => { .then(result => {
this.transportMapSearchResults = (result.geocodes || []).map((item, index) => ({ this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
id: item.id || index,
name: item.formattedAddress || keyword,
address: item.formattedAddress || keyword,
location: item.location,
}));
const point = this.resolveMapPoint(result); const point = this.resolveMapPoint(result);
if (!point) { if (!point) {
this.transportMapStatus = '未找到匹配地址'; this.transportMapStatus = '未找到匹配地址';
@@ -7241,11 +7275,13 @@ export default {
freightInfo.freightItems?.[0]?.unitPrice ?? freightInfo.freightItems?.[0]?.unitPrice ??
''; '';
const otherFeeTotal = const otherFeeTotal =
this.normalizeDispatchFeeValue(
item.otherFeeTotal ?? item.otherFeeTotal ??
plan.otherFeeTotal ?? plan.otherFeeTotal ??
freightInfo.otherFeeTotal ?? freightInfo.otherFeeTotal ??
freightInfo.otherFreightAmount ?? freightInfo.otherFreightAmount ??
''; ''
);
const quantity = Number( const quantity = Number(
item.quantity || item.quantity ||
item.cargoQuantity || item.cargoQuantity ||
@@ -7560,6 +7596,9 @@ export default {
...baseRow, ...baseRow,
...(index >= 0 ? row : {}), ...(index >= 0 ? row : {}),
}; };
this.dispatchItemForm.otherFeeTotal = this.normalizeDispatchFeeValue(
this.dispatchItemForm.otherFeeTotal
);
this.dispatchItemForm.carrierType = this.dispatchItemForm.carrierType || '承运商'; this.dispatchItemForm.carrierType = this.dispatchItemForm.carrierType || '承运商';
this.loadDispatchCarrierOptions(this.dispatchRow).then(() => { this.loadDispatchCarrierOptions(this.dispatchRow).then(() => {
this.applyDispatchSelfCarrierDefaults(); this.applyDispatchSelfCarrierDefaults();
@@ -7611,6 +7650,7 @@ export default {
this.dispatchItemForm.carrierName = ''; this.dispatchItemForm.carrierName = '';
this.dispatchItemForm.carrierId = ''; this.dispatchItemForm.carrierId = '';
this.dispatchItemForm.carrierContractId = ''; this.dispatchItemForm.carrierContractId = '';
this.clearDispatchDriverVehicleFields();
if (nextValue === '承运商') { if (nextValue === '承运商') {
this.dispatchItemForm.trailerVehicleNo = ''; this.dispatchItemForm.trailerVehicleNo = '';
this.dispatchItemForm.escortName = ''; this.dispatchItemForm.escortName = '';
@@ -7798,12 +7838,29 @@ export default {
this.dispatchItemForm.carrierContractId = carrier?.carrierContractId || ''; this.dispatchItemForm.carrierContractId = carrier?.carrierContractId || '';
this.dispatchItemForm.carrierId = carrier?.carrierId || ''; this.dispatchItemForm.carrierId = carrier?.carrierId || '';
this.dispatchItemForm.carrierName = carrier?.carrierName || ''; this.dispatchItemForm.carrierName = carrier?.carrierName || '';
return; } else {
}
this.dispatchItemForm.carrierContractId = ''; this.dispatchItemForm.carrierContractId = '';
this.dispatchItemForm.carrierId = this.dispatchItemForm.carrierId = '';
this.dispatchItemForm.carrierType === '自运' ? '' : carrier?.id || '';
this.dispatchItemForm.carrierName = carrier?.carrierName || value || ''; this.dispatchItemForm.carrierName = carrier?.carrierName || value || '';
}
this.clearDispatchDriverVehicleFields();
},
clearDispatchDriverVehicleFields() {
this.dispatchItemForm.driverId = '';
this.dispatchItemForm.driverName = '';
this.dispatchItemForm.driverPhone = '';
this.dispatchItemForm.vehicleNo = '';
this.dispatchItemForm.trailerVehicleNo = '';
this.dispatchItemForm.escortName = '';
this.dispatchItemForm.escortPhone = '';
this.taskDriverOptions = [];
this.taskEscortOptions = [];
},
getDispatchCarrierFilter() {
return {
carrierId: this.dispatchItemForm.carrierId || '',
carrierName: this.dispatchItemForm.carrierName || '',
};
}, },
handleDispatchCargoQuantityInput(row, value) { handleDispatchCargoQuantityInput(row, value) {
const text = String(value || '').replace(/[^\d.]/g, ''); const text = String(value || '').replace(/[^\d.]/g, '');
@@ -7820,6 +7877,11 @@ export default {
this.dispatchItemForm[prop] = this.dispatchItemForm[prop] =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0]; parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
}, },
normalizeDispatchFeeValue(value) {
if (value === undefined || value === null || value === '') return '';
if (Number(value) === -1) return '';
return value;
},
addDispatchCargoRow(index = -1) { addDispatchCargoRow(index = -1) {
const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' }); const nextRow = this.normalizeTransportCargoRow({ priceUnit: '元/吨' });
if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow); if (index > -1) this.dispatchItemCargoRows.splice(index + 1, 0, nextRow);
@@ -7972,6 +8034,12 @@ export default {
}, 0) }, 0)
); );
}, },
/** 精简录入提示:可调度余量扣减当前本次数量后的剩余 */
dispatchItemHintRemainingQuantity(row = {}) {
const available = this.dispatchItemRemainingQuantity(row);
const current = this.parseDispatchQuantity(row.quantity);
return Math.max(available - current, 0);
},
pruneDispatchPendingRows(rows = []) { pruneDispatchPendingRows(rows = []) {
const planQuantities = new Map(); const planQuantities = new Map();
this.dispatchPlanGoodsRows.forEach(goods => { this.dispatchPlanGoodsRows.forEach(goods => {
@@ -8220,14 +8288,29 @@ export default {
}), }),
]; ];
const firstGoods = goodsRows[0] || {}; const firstGoods = goodsRows[0] || {};
const quantitySum = goodsRows.reduce(
(sum, goods) => sum + this.parseDispatchQuantity(goods.quantity),
0
);
const quantityText = quantitySum
? this.formatDispatchQuantity(quantitySum)
: firstGoods.quantity || this.dispatchItemForm.quantity || '';
const otherFeeTotal = this.normalizeDispatchFeeValue(this.dispatchItemForm.otherFeeTotal);
const nextRow = { const nextRow = {
...this.dispatchItemForm, ...this.dispatchItemForm,
goodsJson: JSON.stringify(goodsRows), goodsJson: JSON.stringify(goodsRows),
cargoType: firstGoods.cargoType || this.dispatchItemForm.cargoType || '', cargoType: firstGoods.cargoType || this.dispatchItemForm.cargoType || '',
cargoTypeCode: firstGoods.cargoTypeCode || this.dispatchItemForm.cargoTypeCode || '', cargoTypeCode: firstGoods.cargoTypeCode || this.dispatchItemForm.cargoTypeCode || '',
cargoTypePath: firstGoods.cargoTypePath || this.dispatchItemForm.cargoTypePath || [], cargoTypePath: firstGoods.cargoTypePath || this.dispatchItemForm.cargoTypePath || [],
cargoName: firstGoods.cargoName || this.dispatchItemForm.cargoName || '', cargoName:
quantity: firstGoods.quantity || this.dispatchItemForm.quantity || '', goodsRows
.map(goods => goods.cargoName || goods.goodsName || '')
.filter(Boolean)
.join('.') ||
firstGoods.cargoName ||
this.dispatchItemForm.cargoName ||
'',
quantity: quantityText,
quantityUnit: firstGoods.quantityUnit || this.dispatchItemForm.quantityUnit || '', quantityUnit: firstGoods.quantityUnit || this.dispatchItemForm.quantityUnit || '',
unitPrice: firstGoods.unitPrice || '', unitPrice: firstGoods.unitPrice || '',
priceUnit: firstGoods.priceUnit || '', priceUnit: firstGoods.priceUnit || '',
@@ -8235,7 +8318,7 @@ export default {
freightJson: JSON.stringify({ freightJson: JSON.stringify({
currency: this.dispatchContractCurrency || this.dispatchItemForm.freightCurrency || 'RMB', currency: this.dispatchContractCurrency || this.dispatchItemForm.freightCurrency || 'RMB',
totalFreightAmount: this.dispatchItemFreightSubtotal, totalFreightAmount: this.dispatchItemFreightSubtotal,
otherFreightAmount: this.dispatchItemForm.otherFeeTotal || '', otherFreightAmount: otherFeeTotal,
freightItems: goodsRows.map((cargo, index) => ({ freightItems: goodsRows.map((cargo, index) => ({
cargoIndex: index, cargoIndex: index,
cargoName: cargo.cargoName || '', cargoName: cargo.cargoName || '',
@@ -8251,7 +8334,7 @@ export default {
}; };
nextRow.cargoInfo = this.formatDispatchCargoInfo({}, nextRow); nextRow.cargoInfo = this.formatDispatchCargoInfo({}, nextRow);
nextRow.freight = this.dispatchItemFreightSubtotal; nextRow.freight = this.dispatchItemFreightSubtotal;
nextRow.otherFeeTotal = this.dispatchItemForm.otherFeeTotal || ''; nextRow.otherFeeTotal = otherFeeTotal;
const quantityUnit = this.getDispatchQuantityUnit(nextRow); const quantityUnit = this.getDispatchQuantityUnit(nextRow);
const otherDispatchedQuantity = this.dispatchRows.reduce((total, row, index) => { const otherDispatchedQuantity = this.dispatchRows.reduce((total, row, index) => {
if ( if (
@@ -9348,19 +9431,36 @@ export default {
} }
} }
:deep(.el-tag) { .detail-status-text {
height: 36px; display: inline-flex;
padding: 0 14px; align-items: center;
border: 0; height: 28px;
padding: 0 12px;
border-radius: 8px; border-radius: 8px;
font-size: 14px; font-size: 13px;
line-height: 36px; line-height: 28px;
} }
:deep(.is-dispatching) { .status-text-success,
.is-dispatching {
color: #67c23a; color: #67c23a;
background: #e1f3d8; background: #e1f3d8;
} }
.detail-transport-type {
color: #409eff;
background: #ecf5ff;
}
.is-danger {
color: #f56c6c;
background: #fef0f0;
}
.is-info {
color: #909399;
background: #f4f4f5;
}
} }
&__transport-plan-detail-overview { &__transport-plan-detail-overview {
@@ -113,6 +113,12 @@
label="导入方式" label="导入方式"
width="100" width="100"
/> />
<el-table-column
v-if="columnVisible.statusName"
prop="statusName"
label="状态"
width="100"
/>
<el-table-column <el-table-column
v-if="columnVisible.createUserName" v-if="columnVisible.createUserName"
prop="createUserName" prop="createUserName"
@@ -121,12 +127,6 @@
> >
<template #default="{ row }">{{ row.createUserName || '-' }}</template> <template #default="{ row }">{{ row.createUserName || '-' }}</template>
</el-table-column> </el-table-column>
<el-table-column
v-if="columnVisible.statusName"
prop="statusName"
label="状态"
width="100"
/>
<el-table-column <el-table-column
v-if="columnVisible.createTime" v-if="columnVisible.createTime"
prop="createTime" prop="createTime"
@@ -621,8 +621,8 @@ const columnOptions = [
{ prop: 'carrierType', label: '承运类型' }, { prop: 'carrierType', label: '承运类型' },
{ prop: 'waybillCount', label: '运单数' }, { prop: 'waybillCount', label: '运单数' },
{ prop: 'importTypeName', label: '导入方式' }, { prop: 'importTypeName', label: '导入方式' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'statusName', label: '状态' }, { prop: 'statusName', label: '状态' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' }, { prop: 'createTime', label: '创建时间' },
{ prop: 'updateTime', label: '更新时间' }, { prop: 'updateTime', label: '更新时间' },
]; ];
@@ -1774,14 +1774,19 @@
<div class="waybill-manage-page__waybill-heading"> <div class="waybill-manage-page__waybill-heading">
<strong>运单详情</strong> <strong>运单详情</strong>
<span>{{ detailRow.waybillNo || '-' }}</span> <span>{{ detailRow.waybillNo || '-' }}</span>
<span class="detail-status-text status-text-success">{{ <el-tag :type="statusTagType(detailRow.businessStatus)">
displayStatus(detailRow, 'businessStatus') {{ displayStatus(detailRow, 'businessStatus') }}
}}</span> </el-tag>
<span class="detail-status-text detail-transport-type"> <el-tag type="primary">
{{ getTransportTypeLabel(waybillTransportMode(detailRow)) || '-' }} {{
</span> detailRow.transportTypeName ||
getTransportTypeLabel(detailRow.transportType) ||
waybillTransportMode(detailRow) ||
'-'
}}
</el-tag>
<el-link <el-link
v-if="detailRow.id" v-if="detailRow.id && canChangeWaybillRoute(detailRow)"
class="waybill-manage-page__waybill-route-change-link" class="waybill-manage-page__waybill-route-change-link"
type="primary" type="primary"
@click="openWaybillRouteChangeDialog" @click="openWaybillRouteChangeDialog"
@@ -1797,7 +1802,18 @@
:class="{ 'is-plan': item[0] === 'planName' }" :class="{ 'is-plan': item[0] === 'planName' }"
> >
<span class="waybill-manage-page__waybill-label">{{ item[1] }}</span> <span class="waybill-manage-page__waybill-label">{{ item[1] }}</span>
<span :class="['waybill-manage-page__waybill-value', { 'is-link': item[2] }]"> <el-link
v-if="canOpenWaybillDetailSummaryLink(item[0])"
type="primary"
class="waybill-manage-page__waybill-value is-link"
@click="handleWaybillDetailSummaryClick(item[0])"
>
{{ formatDetailValue(detailRow, item[0]) }}
</el-link>
<span
v-else
:class="['waybill-manage-page__waybill-value', { 'is-link': item[2] }]"
>
{{ formatDetailValue(detailRow, item[0]) }} {{ formatDetailValue(detailRow, item[0]) }}
</span> </span>
</div> </div>
@@ -1923,11 +1939,17 @@
}}</strong }}</strong
> >
</div> </div>
<div <div class="waybill-manage-page__waybill-detail-field">
v-if="!waybillIsCarrier(detailRow)" <span>里程(km)</span
class="waybill-manage-page__waybill-detail-field" ><strong>{{ waybillDetailMileage(detailRow) || '-' }}</strong>
> </div>
<span>里程(km)</span><strong>{{ detailRow.mileage || '-' }}</strong> <div class="waybill-manage-page__waybill-detail-field">
<span>计划发货日期</span
><strong>{{ waybillDetailEstimatedStartTime(detailRow) || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>计划完成日期</span
><strong>{{ waybillDetailEstimatedEndTime(detailRow) || '-' }}</strong>
</div> </div>
</template> </template>
<template v-else-if="waybillIsNonRoadTransport(detailRow)"> <template v-else-if="waybillIsNonRoadTransport(detailRow)">
@@ -1949,12 +1971,32 @@
<div class="waybill-manage-page__waybill-detail-field"> <div class="waybill-manage-page__waybill-detail-field">
<span>仓位</span><strong>{{ detailRow.cabinNo || '-' }}</strong> <span>仓位</span><strong>{{ detailRow.cabinNo || '-' }}</strong>
</div> </div>
<div class="waybill-manage-page__waybill-detail-field">
<span>预计发货日期</span
><strong>{{ waybillDetailEstimatedStartTime(detailRow) || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>预计完成日期</span
><strong>{{ waybillDetailEstimatedEndTime(detailRow) || '-' }}</strong>
</div>
</template> </template>
<template v-else> <template v-else>
<div class="waybill-manage-page__waybill-detail-field"> <div class="waybill-manage-page__waybill-detail-field">
<span>{{ waybillVehicleNoLabel(detailRow) }}</span> <span>{{ waybillVehicleNoLabel(detailRow) }}</span>
<strong>{{ detailRow.vehicleNo || '-' }}</strong> <strong>{{ detailRow.vehicleNo || '-' }}</strong>
</div> </div>
<div class="waybill-manage-page__waybill-detail-field">
<span>里程(km)</span
><strong>{{ waybillDetailMileage(detailRow) || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>计划发货日期</span
><strong>{{ waybillDetailEstimatedStartTime(detailRow) || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>计划完成日期</span
><strong>{{ waybillDetailEstimatedEndTime(detailRow) || '-' }}</strong>
</div>
</template> </template>
<div <div
class="waybill-manage-page__waybill-detail-field" class="waybill-manage-page__waybill-detail-field"
@@ -2183,7 +2225,11 @@
/> />
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="司机上传" name="driverUpload"> <el-tab-pane
v-if="waybillDriverUploads.length"
label="司机上传"
name="driverUpload"
>
<div <div
v-loading="waybillPunchRecordsLoading" v-loading="waybillPunchRecordsLoading"
class="waybill-manage-page__driver-upload-grid" class="waybill-manage-page__driver-upload-grid"
@@ -2200,11 +2246,6 @@
{{ photo.label || '凭证' }} {{ photo.label || '凭证' }}
</div> </div>
</div> </div>
<el-empty
v-if="!waybillPunchRecordsLoading && !waybillDriverUploads.length"
description="暂无司机上传数据"
:image-size="50"
/>
</div> </div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
@@ -2315,7 +2356,7 @@
</div> </div>
</template> </template>
</div> </div>
<div v-if="detailPageLocked" class="waybill-manage-page__detail-footer"> <div v-if="detailPageLocked && !isPublicWaybillView" class="waybill-manage-page__detail-footer">
<el-button type="primary" @click="closeDetail">关闭</el-button> <el-button type="primary" @click="closeDetail">关闭</el-button>
</div> </div>
<template v-if="!detailPageLocked" #footer> <template v-if="!detailPageLocked" #footer>
@@ -2418,35 +2459,6 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</section-card> </section-card>
<section-card title="路线编辑">
<template #extra>
<span class="waybill-manage-page__route-change-hint">拖拽调整顺序</span>
</template>
<div
v-if="waybillRouteChangeNodes.length"
class="waybill-manage-page__route-change-list"
>
<div
v-for="(node, index) in waybillRouteChangeNodes"
:key="node.key || index"
class="waybill-manage-page__route-change-item"
draggable="true"
@dragstart="handleWaybillRouteChangeDragStart(index)"
@dragover.prevent
@drop="handleWaybillRouteChangeDrop(index)"
>
<span class="waybill-manage-page__route-change-type">
{{ waybillRouteChangeTypeText(index) }}
</span>
<span>{{ node.address }}</span>
<span class="waybill-manage-page__route-change-tags">
<el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag>
</span>
<el-icon><Rank /></el-icon>
</div>
</div>
<el-empty v-else description="暂无路线" :image-size="60" />
</section-card>
<el-form label-width="auto"> <el-form label-width="auto">
<el-form-item label="变更备注" style="margin-top: 8px"> <el-form-item label="变更备注" style="margin-top: 8px">
<el-input <el-input
@@ -2864,8 +2876,8 @@
</el-button> </el-button>
</div> </div>
<div class="map-picker-content"> <div class="map-picker-content">
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
<div ref="transportAmap" class="waybill-manage-page__map"></div> <div ref="transportAmap" class="waybill-manage-page__map"></div>
<map-search-results :results="transportMapSearchResults" @select="selectTransportMapSearchResult" />
</div> </div>
<div class="waybill-manage-page__map-info"> <div class="waybill-manage-page__map-info">
<span>{{ transportMapStatus }}</span> <span>{{ transportMapStatus }}</span>
@@ -3111,6 +3123,7 @@ import {
getList as getProjectList, getList as getProjectList,
} from '@/api/business/project-apply'; } from '@/api/business/project-apply';
import { getList as getLoadingList } from '@/api/business/loading-manage'; import { getList as getLoadingList } from '@/api/business/loading-manage';
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
import { getList as getCommonRouteList } from '@/api/business/common-route'; import { getList as getCommonRouteList } from '@/api/business/common-route';
import { import {
getDetail as getShippingPlanDetail, getDetail as getShippingPlanDetail,
@@ -3122,7 +3135,11 @@ import {
getVoucherImages as getProcessConfigVoucherImages, getVoucherImages as getProcessConfigVoucherImages,
} from '@/api/business/process-config'; } from '@/api/business/process-config';
import { getPunchRecords as getWaybillPunchRecords, locateVehicle, trackVehicle } from '@/api/business/waybill-manage'; import { getPunchRecords as getWaybillPunchRecords, locateVehicle, trackVehicle } from '@/api/business/waybill-manage';
import { getMkPublicDetail } from '@/api/mk-process';
import { getList as getDriverList } from '@/api/transportCapacity/driver'; import { getList as getDriverList } from '@/api/transportCapacity/driver';
import {
fetchDriversByCarrierOrganizations,
} from '@/utils/carrier-org-resource';
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { getDictionary as getSystemDictionary } from '@/api/system/dict'; import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address'; import { addressTypeOptions } from '@/option/base/common-address';
@@ -3131,11 +3148,12 @@ import { formatUpdateUserName } from '@/utils/audit';
import { submitMkApprovalFlow } from '@/utils/mk-approval'; import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel'; import { openImportDialog } from '@/utils/import-excel';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { applyTableMenuWidth } from '@/utils/table-menu'; import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util'; import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate'; import { isMobile } from '@/utils/validate';
import { InfoFilled, Location, OfficeBuilding, Rank, Search, WarnTriangleFilled } from '@element-plus/icons-vue'; import { InfoFilled, Location, OfficeBuilding, Search, WarnTriangleFilled } from '@element-plus/icons-vue';
import { ElImageViewer } from 'element-plus'; import { ElAutocomplete, ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue'; import { OpenFileViewer } from '@open-file-viewer/vue';
import { import {
fallbackPlugin, fallbackPlugin,
@@ -3310,7 +3328,6 @@ export default {
PageDetail, PageDetail,
InfoFilled, InfoFilled,
WarnTriangleFilled, WarnTriangleFilled,
Rank,
ElImageViewer, ElImageViewer,
OpenFileViewer, OpenFileViewer,
}, },
@@ -3383,7 +3400,6 @@ export default {
waybillRouteChangeNodes: [], waybillRouteChangeNodes: [],
waybillRouteChangeRemark: '', waybillRouteChangeRemark: '',
waybillRouteChangeRecords: [], waybillRouteChangeRecords: [],
waybillRouteChangeDragIndex: -1,
waybillRouteChangeSaving: false, waybillRouteChangeSaving: false,
waybillLocateLoading: false, waybillLocateLoading: false,
waybillLocateInfo: null, waybillLocateInfo: null,
@@ -3646,6 +3662,8 @@ export default {
this.formPageLocked = this.isStandaloneWaybillFormPage; this.formPageLocked = this.isStandaloneWaybillFormPage;
this.detailPageLocked = this.isStandaloneWaybillDetailPage; this.detailPageLocked = this.isStandaloneWaybillDetailPage;
this.formModeLocked = this.$route.query.mode || ''; this.formModeLocked = this.$route.query.mode || '';
if (this.isPublicWaybillView) return;
this.setupSearchAutocompletes();
if (this.config.enableAllDept && this.isAdmin) { if (this.config.enableAllDept && this.isAdmin) {
this.allDept = 1; this.allDept = 1;
} }
@@ -3709,8 +3727,14 @@ export default {
isStandaloneWaybillFormPage() { isStandaloneWaybillFormPage() {
return this.isStandaloneWaybillPage && ['add', 'edit'].includes(this.$route.query.mode); return this.isStandaloneWaybillPage && ['add', 'edit'].includes(this.$route.query.mode);
}, },
isPublicWaybillView() {
return this.$route.path === '/business/waybill-manage/public-view';
},
isStandaloneWaybillDetailPage() { isStandaloneWaybillDetailPage() {
return this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute; return (
(this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute) ||
this.isPublicWaybillView
);
}, },
// $route data formPageLocked // $route data formPageLocked
// tab.js fullPath path // tab.js fullPath path
@@ -4298,8 +4322,12 @@ export default {
) { ) {
return false; return false;
} }
// config.canCancel / isInProgressStatus
if (typeof this.config.canCancel === 'function' && !this.config.canCancel(row)) {
return false;
}
const status = this.statusValue(row); const status = this.statusValue(row);
return ['pending', 'processing', 'running'].includes(status); return status === 'pending';
}, },
canComplete(row) { canComplete(row) {
if ( if (
@@ -4530,6 +4558,42 @@ export default {
waybillIsCarrier(row = {}) { waybillIsCarrier(row = {}) {
return String(row.carrierTypeName || row.carrierType || '').trim() === '承运商'; return String(row.carrierTypeName || row.carrierType || '').trim() === '承运商';
}, },
waybillDetailTaskInfo(row = {}) {
return this.parseJsonObject(row.taskInfoJson || row.taskInfo || {});
},
waybillDetailMileage(row = {}) {
const taskInfo = this.waybillDetailTaskInfo(row);
const goodsRows = this.parseJsonArray(row.goodsJson);
const firstGoods = goodsRows[0] || {};
return this.normalizeMileageValue(
row.mileage || taskInfo.mileage || firstGoods.mileage || ''
);
},
waybillDetailEstimatedStartTime(row = {}) {
const taskInfo = this.waybillDetailTaskInfo(row);
const value =
row.estimatedStartTime ||
row.planStartDate ||
taskInfo.estimatedStartTime ||
taskInfo.planStartDate ||
'';
return this.formatWaybillDetailDate(value);
},
waybillDetailEstimatedEndTime(row = {}) {
const taskInfo = this.waybillDetailTaskInfo(row);
const value =
row.estimatedEndTime ||
row.planEndDate ||
taskInfo.estimatedEndTime ||
taskInfo.planEndDate ||
'';
return this.formatWaybillDetailDate(value);
},
formatWaybillDetailDate(value) {
const text = String(value || '').trim();
if (!text) return '';
return text.slice(0, 10);
},
waybillVehicleNoLabel(row = {}) { waybillVehicleNoLabel(row = {}) {
return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号'; return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号';
}, },
@@ -4591,14 +4655,19 @@ export default {
this.waybillVoucherFolder = null; this.waybillVoucherFolder = null;
this.waybillVoucherFolderView = false; this.waybillVoucherFolderView = false;
this.waybillVoucherImagesLoaded = false; this.waybillVoucherImagesLoaded = false;
const request = const request = this.isPublicWaybillView
typeof this.api.getDetail === 'function' ? getMkPublicDetail('waybill-manage', row.id)
: typeof this.api.getDetail === 'function'
? this.api.getDetail(row.id) ? this.api.getDetail(row.id)
: Promise.resolve({ data: { data: row } }); : Promise.resolve({ data: { data: row } });
request request
.then(res => { .then(res => {
const detail = res?.data?.data || res?.data || row; const detail = res?.data?.data || res?.data || row;
this.detailRow = detail; this.detailRow = detail;
if (this.isPublicWaybillView) {
this.applyFormDetail(detail);
return;
}
this.loadWaybillPunchRecords(); this.loadWaybillPunchRecords();
this.loadWaybillVoucherImages(); this.loadWaybillVoucherImages();
if (detail.contractId) { if (detail.contractId) {
@@ -4666,6 +4735,90 @@ export default {
query: { id, name: '配载单详情' }, query: { id, name: '配载单详情' },
}); });
}, },
waybillDetailCustomerLabel(row = {}) {
return row.customer || row.fullName || row.shortName || row.customerName || row.customerCode || '';
},
waybillDetailCustomerId(row = {}) {
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;
if (prop === 'customerName') return Boolean(row.customerName);
if (prop === 'contractNo') return Boolean(row.contractId);
if (prop === 'projectName') return Boolean(row.projectId);
return false;
},
handleWaybillDetailSummaryClick(prop) {
if (prop === 'customerName') return this.openWaybillDetailCustomer();
if (prop === 'contractNo') return this.openWaybillDetailContract();
if (prop === 'projectName') return this.openWaybillDetailProject();
},
async resolveWaybillDetailCustomerId() {
const row = this.detailRow || {};
if (row.customerId) return row.customerId;
const name = String(row.customerName || '').trim();
if (!name) return '';
if (row.projectId) {
try {
const res = await getProjectDetail(row.projectId);
const customers = this.parseJsonArray(res.data?.data?.customerJson);
const matched =
customers.find(item => this.waybillDetailCustomerLabel(item) === name) ||
customers.find(item => {
const label = this.waybillDetailCustomerLabel(item);
return label && (name.includes(label) || label.includes(name));
});
const id = this.waybillDetailCustomerId(matched);
if (id) return id;
} catch (error) {
// 退
}
}
try {
const res = await getCustomerArchiveList(1, 20, { fullName: name });
const records = res.data?.data?.records || [];
const exact =
records.find(item => item.fullName === name || item.shortName === name) || records[0];
return exact?.id || '';
} catch (error) {
return '';
}
},
async openWaybillDetailCustomer() {
if (!this.detailRow?.customerName) return;
const customerId = await this.resolveWaybillDetailCustomerId();
if (!customerId) {
this.$message.warning('未找到对应客商档案,无法打开');
return;
}
this.$router.push({
path: '/vehicle/customer-archive/form',
query: { id: String(customerId), name: '查看客商档案', view: '1' },
});
},
openWaybillDetailContract() {
if (!this.detailRow?.contractId) {
this.$message.warning('合同信息缺失,无法打开');
return;
}
this.$router.push({
path: '/business/contract-manage/detail',
query: { id: this.detailRow.contractId, name: '合同详情' },
});
},
openWaybillDetailProject() {
if (!this.detailRow?.projectId) {
this.$message.warning('项目信息缺失,无法打开');
return;
}
this.$router.push({
path: '/business/project-apply/form',
query: { mode: 'view', id: this.detailRow.projectId, name: '查看项目管理' },
});
},
loadWaybillDetailProcessNodes(projectId) { loadWaybillDetailProcessNodes(projectId) {
this.waybillDetailProcessNodes = []; this.waybillDetailProcessNodes = [];
if (!projectId) return Promise.resolve([]); if (!projectId) return Promise.resolve([]);
@@ -4836,7 +4989,7 @@ export default {
: []; : [];
}, },
openWaybillRouteChangeDialog() { openWaybillRouteChangeDialog() {
if (!this.detailRow.id) return; if (!this.detailRow.id || !this.canChangeWaybillRoute(this.detailRow)) return;
this.restoreWaybillRouteChangeRecords(); this.restoreWaybillRouteChangeRecords();
this.waybillRouteChangeTab = 'change'; this.waybillRouteChangeTab = 'change';
this.waybillRouteChangeRemark = ''; this.waybillRouteChangeRemark = '';
@@ -4853,22 +5006,11 @@ export default {
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows); this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
this.waybillRouteChangeBox = true; this.waybillRouteChangeBox = true;
}, },
waybillRouteChangeTypeText(index) { canChangeWaybillRoute(row = {}) {
if (index === 0) return ''; const status = String(this.statusValue(row) || row.businessStatus || row.status || '');
if (index === this.waybillRouteChangeNodes.length - 1) return '终'; if (status === 'completed') return false;
return ''; const statusName = String(row.businessStatusName || row.statusName || '');
}, return !statusName.includes('已完成');
handleWaybillRouteChangeDragStart(index) {
this.waybillRouteChangeDragIndex = index;
},
handleWaybillRouteChangeDrop(index) {
if (this.waybillRouteChangeDragIndex < 0 || this.waybillRouteChangeDragIndex === index)
return;
const nodes = [...this.waybillRouteChangeNodes];
const [node] = nodes.splice(this.waybillRouteChangeDragIndex, 1);
nodes.splice(index, 0, node);
this.waybillRouteChangeNodes = nodes;
this.waybillRouteChangeDragIndex = -1;
}, },
updateWaybillRouteChangeNodes() { updateWaybillRouteChangeNodes() {
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows); this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
@@ -4917,9 +5059,8 @@ export default {
row.departureAddress !== row.originalDepartureAddress || row.departureAddress !== row.originalDepartureAddress ||
row.arrivalAddress !== row.originalArrivalAddress; row.arrivalAddress !== row.originalArrivalAddress;
const routeJson = JSON.stringify(this.waybillRouteChangeNodes); const routeJson = JSON.stringify(this.waybillRouteChangeNodes);
const originalRouteJson = this.detailRow.routeJson || ''; if (!changed && !this.waybillRouteChangeRemark) {
if (!changed && routeJson === originalRouteJson && !this.waybillRouteChangeRemark) { this.$message.warning('请修改地址或填写变更备注');
this.$message.warning('请修改地址、调整路线或填写变更备注');
return; return;
} }
if (typeof this.api.changeRoute !== 'function') { if (typeof this.api.changeRoute !== 'function') {
@@ -4927,11 +5068,9 @@ export default {
return; return;
} }
const content = changed const content = `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress || '-'}${
? `${row.waybillNo || '运单'}:发货地 ${row.originalDepartureAddress || '-'}${
row.departureAddress || '-' row.departureAddress || '-'
}到货地 ${row.originalArrivalAddress || '-'} ${row.arrivalAddress || '-'}` }到货地 ${row.originalArrivalAddress || '-'} ${row.arrivalAddress || '-'}`;
: '调整运输路线顺序';
const records = [ const records = [
{ {
changeTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'), changeTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
@@ -5251,7 +5390,9 @@ export default {
: defaultText; : defaultText;
}, },
statusTagType(status) { statusTagType(status) {
if ([1, 'pending', 'processing', 'approved', 'change_approved'].includes(status)) { if (
[1, 'pending', 'processing', 'running', 'approved', 'change_approved'].includes(status)
) {
return 'success'; return 'success';
} }
if ([2, 'cancelled', 'withdrawn', 'draft'].includes(status)) { if ([2, 'cancelled', 'withdrawn', 'draft'].includes(status)) {
@@ -6327,6 +6468,15 @@ export default {
this.form.carrierName = ''; this.form.carrierName = '';
this.form.carrierId = ''; this.form.carrierId = '';
this.form.carrierContractId = ''; this.form.carrierContractId = '';
this.form.driverId = '';
this.form.driverName = '';
this.form.driverPhone = '';
this.form.vehicleNo = '';
this.form.trailerVehicleNo = '';
this.form.escortName = '';
this.form.escortPhone = '';
this.taskDriverOptions = [];
this.taskEscortOptions = [];
if (nextValue === '承运商') { if (nextValue === '承运商') {
this.form.trailerVehicleNo = ''; this.form.trailerVehicleNo = '';
this.form.escortName = ''; this.form.escortName = '';
@@ -6580,17 +6730,36 @@ export default {
this.form.carrierContractId = carrier?.carrierContractId || ''; this.form.carrierContractId = carrier?.carrierContractId || '';
this.form.carrierId = carrier?.carrierId || ''; this.form.carrierId = carrier?.carrierId || '';
this.form.carrierName = carrier?.carrierName || ''; this.form.carrierName = carrier?.carrierName || '';
return; } else {
}
this.form.carrierId = carrier?.carrierContractId ? '' : carrier?.id || ''; this.form.carrierId = carrier?.carrierContractId ? '' : carrier?.id || '';
this.form.carrierName = value || ''; this.form.carrierName = value || '';
}
// /
this.form.driverId = '';
this.form.driverName = '';
this.form.driverPhone = '';
this.form.vehicleNo = '';
this.form.trailerVehicleNo = '';
this.form.escortName = '';
this.form.escortPhone = '';
this.taskDriverOptions = [];
this.taskEscortOptions = [];
},
getTaskCarrierFilter() {
return {
carrierId: this.form.carrierId || '',
carrierName: this.form.carrierName || '',
};
}, },
loadTaskDriverOptions() { loadTaskDriverOptions() {
if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]); if (!this.taskInfoFormEnabled || this.taskDriverLoading) return Promise.resolve([]);
this.taskDriverLoading = true; this.taskDriverLoading = true;
return getDriverList(1, 9999, { posts: '司机' }) return fetchDriversByCarrierOrganizations(
.then(res => { { size: 9999, posts: '司机' },
this.taskDriverOptions = extractRecords(res); this.getTaskCarrierFilter()
)
.then(records => {
this.taskDriverOptions = records;
return this.taskDriverOptions; return this.taskDriverOptions;
}) })
.finally(() => { .finally(() => {
@@ -6599,10 +6768,17 @@ export default {
}, },
fetchTaskDriverSuggestions(queryString, callback) { fetchTaskDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
const carrier = this.getTaskCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskDriverLoading = true; this.taskDriverLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' }) fetchDriversByCarrierOrganizations(
.then(res => { { size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
const records = extractRecords(res); carrier
)
.then(records => {
this.taskDriverOptions = records; this.taskDriverOptions = records;
callback( callback(
records.map(item => ({ records.map(item => ({
@@ -6621,10 +6797,17 @@ export default {
}, },
fetchTaskEscortSuggestions(queryString, callback) { fetchTaskEscortSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
const carrier = this.getTaskCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.taskEscortLoading = true; this.taskEscortLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' }) fetchDriversByCarrierOrganizations(
.then(res => { { size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
const records = extractRecords(res); carrier
)
.then(records => {
this.taskEscortOptions = records; this.taskEscortOptions = records;
callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' }))); callback(records.map(item => ({ ...item, value: item.driverName || item.name || '' })));
}) })
@@ -7764,12 +7947,7 @@ export default {
this.ensureTransportAmapGeocoder() this.ensureTransportAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword)) .then(() => this.runAmapGeocode('location', keyword))
.then(result => { .then(result => {
this.transportMapSearchResults = (result.geocodes || []).map((item, index) => ({ this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
id: item.id || index,
name: item.formattedAddress || keyword,
address: item.formattedAddress || keyword,
location: item.location,
}));
const point = this.resolveMapPoint(result); const point = this.resolveMapPoint(result);
if (!point) { if (!point) {
this.transportMapStatus = '未找到匹配地址'; this.transportMapStatus = '未找到匹配地址';
@@ -8392,6 +8570,123 @@ export default {
this.page.currentPage = 1; this.page.currentPage = 1;
this.onLoad(this.page); this.onLoad(this.page);
}, },
setupSearchAutocompletes() {
const fields = [
{
prop: 'projectName',
placeholder: '请选择或输入项目名称',
fetch: (queryString, callback) =>
this.fetchSearchNameSuggestions(queryString, callback, {
request: keyword =>
getProjectList(1, 20, keyword ? { projectName: keyword } : {}),
getValue: item => item.projectName || item.name || '',
}),
},
{
prop: 'customerName',
placeholder: '请选择或输入客户名称',
fetch: (queryString, callback) =>
this.fetchSearchNameSuggestions(queryString, callback, {
request: keyword =>
getCustomerArchiveList(1, 20, {
...(keyword ? { fullName: keyword } : {}),
customerType: '客户',
}),
getValue: item => item.fullName || item.customerName || item.name || '',
}),
},
{
prop: 'cargoType',
placeholder: '请选择或输入货物类型',
fetch: (queryString, callback) =>
this.fetchSearchNameSuggestions(queryString, callback, {
request: keyword =>
getCargoTypeList(1, 20, keyword ? { cargoName: keyword } : {}),
getValue: item => item.cargoName || item.typeName || item.name || item.label || '',
}),
},
{
prop: 'carrierName',
placeholder: '请选择或输入承运商名称',
fetch: (queryString, callback) =>
this.fetchSearchNameSuggestions(queryString, callback, {
request: keyword =>
getCustomerArchiveList(1, 20, {
...(keyword ? { fullName: keyword } : {}),
customerType: '承运商',
}),
getValue: item =>
item.fullName || item.carrierName || item.customerName || item.name || '',
}),
},
{
prop: 'driverName',
placeholder: '请选择或输入司机名称',
fetch: (queryString, callback) =>
this.fetchSearchNameSuggestions(queryString, callback, {
request: keyword =>
getDriverList(1, 20, {
...(keyword ? { driverName: keyword } : {}),
posts: '司机',
}),
getValue: item => item.driverName || item.name || '',
}),
},
{
prop: 'planName',
placeholder: '请选择或输入计划名称',
fetch: (queryString, callback) =>
this.fetchSearchNameSuggestions(queryString, callback, {
request: keyword =>
getShippingPlanList(1, 20, keyword ? { planName: keyword } : {}),
getValue: item => item.planName || item.name || '',
}),
},
];
fields.forEach(field => {
const column = this.findColumn?.(this.option.column, field.prop);
if (!column) return;
column.searchPlaceholder = field.placeholder;
column.renderSearch = scope =>
this.renderSearchAutocomplete(scope, field.prop, field.placeholder, field.fetch);
});
},
renderSearchAutocomplete(scope, prop, placeholder, fetchSuggestions) {
return h(ElAutocomplete, {
modelValue: scope.row?.[prop] ?? '',
'onUpdate:modelValue': value => {
if (scope.row) scope.row[prop] = value ?? '';
},
fetchSuggestions,
debounce: 300,
clearable: true,
triggerOnFocus: true,
placeholder,
style: 'width: 100%',
valueKey: 'value',
fitInputWidth: true,
});
},
fetchSearchNameSuggestions(queryString, callback, { request, getValue }) {
const keyword = String(queryString || '').trim();
Promise.resolve(request(keyword))
.then(res => {
const records = extractRecords(res);
const seen = new Set();
const options = [];
records.forEach(item => {
const value = String(getValue(item) || '').trim();
if (!value || seen.has(value)) return;
seen.add(value);
options.push({ ...item, value });
});
callback(options);
})
.catch(error => {
window.console.log(error);
callback([]);
});
},
searchChange(params, done) { searchChange(params, done) {
this.query = { this.query = {
...params, ...params,
@@ -8560,7 +8855,7 @@ export default {
}; };
this.mileageForm = { this.mileageForm = {
id: row.id, id: row.id,
mileage: row.mileage === null || row.mileage === undefined ? '' : String(row.mileage), mileage: this.normalizeMileageValue(row.mileage),
mileageRemark: row.mileageRemark || '', mileageRemark: row.mileageRemark || '',
}; };
this.$nextTick(() => this.$refs.mileageFormRef?.clearValidate()); this.$nextTick(() => this.$refs.mileageFormRef?.clearValidate());
@@ -8657,10 +8952,21 @@ export default {
}, },
fetchReassignDriverSuggestions(queryString, callback) { fetchReassignDriverSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
const row = this.reassignDrawer.row || {};
const carrier = {
carrierId: row.carrierId || '',
carrierName: row.carrierName || '',
};
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.reassignDrawer.driverLoading = true; this.reassignDrawer.driverLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' }) fetchDriversByCarrierOrganizations(
.then(res => { { size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
const records = extractRecords(res); carrier
)
.then(records => {
this.reassignDriverOptions = records; this.reassignDriverOptions = records;
callback( callback(
records.map(item => ({ records.map(item => ({
@@ -9749,6 +10055,15 @@ export default {
strong { strong {
font-size: 20px; font-size: 20px;
} }
:deep(.el-tag) {
height: 28px;
padding: 0 12px;
border: 0;
border-radius: 8px;
font-size: 13px;
line-height: 28px;
}
} }
&__waybill-route-change-link { &__waybill-route-change-link {
@@ -9785,7 +10100,12 @@ export default {
white-space: nowrap; white-space: nowrap;
&.is-link { &.is-link {
display: inline;
justify-content: flex-start;
max-width: 100%;
color: #409eff; color: #409eff;
font-weight: 500;
vertical-align: baseline;
} }
} }
+69 -13
View File
@@ -70,7 +70,7 @@
type="date" type="date"
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="YYYY-MM-DD" placeholder="请输入"
/> />
<span></span> <span></span>
<el-date-picker <el-date-picker
@@ -78,7 +78,7 @@
type="date" type="date"
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="YYYY-MM-DD" placeholder="请输入"
/> />
</div> </div>
</el-form-item> </el-form-item>
@@ -92,12 +92,12 @@
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label="合同金额"> <el-form-item label="合同金额">
<el-input-number <el-input
v-model="form.contractAmount" :model-value="contractAmountDisplay"
:min="0"
:precision="2"
:controls="false"
placeholder="请输入" placeholder="请输入"
clearable
@input="handleContractAmountInput"
@clear="form.contractAmount = null"
/> />
</el-form-item> </el-form-item>
<el-form-item label="是否范本"> <el-form-item label="是否范本">
@@ -189,10 +189,11 @@
title="合同文件" title="合同文件"
description description
attachment-type attachment-type
use-upload-dialog :attachment-location-options="changeAttachmentLocationOptions"
:rows="contractFileRows" :rows="contractFileRows"
:preview="previewAttachment" :preview="previewAttachment"
@update:rows="contractFileRows = $event" @update:rows="contractFileRows = $event"
@upload-to-location="handleAttachmentUploadToLocation"
/> />
<section class="change-section"> <section class="change-section">
@@ -218,7 +219,7 @@
</el-radio-group> </el-radio-group>
</div> </div>
<el-form v-if="settlementRule.autoGenerate === 1" :model="settlementRule" label-position="right" label-width="auto" class="settlement-form"> <el-form v-if="settlementRule.autoGenerate === 1" :model="settlementRule" label-position="right" label-width="auto" class="settlement-form">
<el-form-item label="账单起始日" required><el-date-picker v-model="settlementRule.billStartDate" type="date" placeholder="请选择账单起始日" format="YYYY-MM-DD" value-format="YYYY-MM-DD" /></el-form-item> <el-form-item label="账单起始日" required><el-date-picker v-model="settlementRule.billStartDate" type="date" placeholder="请选择账单起始日" format="YYYY-MM-DD" value-format="YYYY-MM-DD" /></el-form-item>
<el-form-item label="结算类型" required><el-select v-model="settlementRule.settlementType" placeholder="请选择结算类型" @change="handleSettlementTypeChange"><el-option v-for="item in settlementTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item> <el-form-item label="结算类型" required><el-select v-model="settlementRule.settlementType" placeholder="请选择结算类型" @change="handleSettlementTypeChange"><el-option v-for="item in settlementTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
<el-form-item v-if="showSettlementBillCycleType" label="结算周期" required><el-select v-model="settlementRule.billCycleType" placeholder="请选择结算周期" @change="handleCycleTypeChange"><el-option v-for="item in billCycleTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item> <el-form-item v-if="showSettlementBillCycleType" label="结算周期" required><el-select v-model="settlementRule.billCycleType" placeholder="请选择结算周期" @change="handleCycleTypeChange"><el-option v-for="item in billCycleTypeOptions" :key="item" :label="item" :value="item" /></el-select></el-form-item>
<el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"><el-option v-for="item in billCutoffDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item> <el-form-item v-if="showSettlementBillCutoffDay" label="账单截单日" required><el-select v-model="settlementRule.billCutoffDay" placeholder="请选择账单截单日"><el-option v-for="item in billCutoffDayOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
@@ -256,11 +257,12 @@
title="其它附件" title="其它附件"
description description
attachment-type attachment-type
use-upload-dialog
attachment-location="其它附件" attachment-location="其它附件"
:attachment-location-options="changeAttachmentLocationOptions"
:rows="attachments" :rows="attachments"
:preview="previewAttachment" :preview="previewAttachment"
@update:rows="attachments = $event" @update:rows="attachments = $event"
@upload-to-location="handleAttachmentUploadToLocation"
/> />
<section class="change-section change-reason-section"> <section class="change-section change-reason-section">
@@ -281,11 +283,12 @@
title="变更材料" title="变更材料"
description description
attachment-type attachment-type
use-upload-dialog
attachment-location="变更材料" attachment-location="变更材料"
:attachment-location-options="changeAttachmentLocationOptions"
:rows="changeMaterials" :rows="changeMaterials"
:preview="previewAttachment" :preview="previewAttachment"
@update:rows="changeMaterials = $event" @update:rows="changeMaterials = $event"
@upload-to-location="handleAttachmentUploadToLocation"
/> />
<div class="page-footer"> <div class="page-footer">
<el-button @click="$router.back()">取消</el-button> <el-button @click="$router.back()">取消</el-button>
@@ -343,7 +346,15 @@ const normalizeOptionalPositiveInteger = value => {
return Number.isInteger(number) && number > 0 ? number : null; return Number.isInteger(number) && number > 0 ? number : null;
}; };
const normalizeOptionalAmount = value => { const normalizeOptionalAmount = value => {
if (value === undefined || value === null || value === '') return null; if (
value === undefined ||
value === null ||
value === '' ||
Number(value) === -1 ||
String(value) === '-1'
) {
return null;
}
const number = Number(value); const number = Number(value);
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : null; return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : null;
}; };
@@ -421,6 +432,22 @@ export default {
pageTitle() { pageTitle() {
return this.$route.query.name || '合同变更'; return this.$route.query.name || '合同变更';
}, },
changeAttachmentLocationOptions() {
return ['合同文件', '其它附件', '变更材料'];
},
contractAmountDisplay() {
const value = this.form.contractAmount;
if (
value === undefined ||
value === null ||
value === '' ||
Number(value) === -1 ||
String(value) === '-1'
) {
return '';
}
return value;
},
contractCategoryOptions() { contractCategoryOptions() {
return this.contractCategoryDictOptions.length return this.contractCategoryDictOptions.length
? this.contractCategoryDictOptions ? this.contractCategoryDictOptions
@@ -464,6 +491,13 @@ export default {
}, },
}, },
methods: { methods: {
handleContractAmountInput(value) {
const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.');
const normalized =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
this.form.contractAmount = normalized === '' ? null : normalized;
},
loadDictionaries() { loadDictionaries() {
Promise.all([ Promise.all([
getSystemDictionary({ code: 'currency_type' }), getSystemDictionary({ code: 'currency_type' }),
@@ -619,6 +653,20 @@ export default {
attachmentExtension(row = {}) { const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0]; const index = source.lastIndexOf('.'); return index > -1 ? source.slice(index + 1).toLowerCase() : ''; }, attachmentExtension(row = {}) { const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0]; const index = source.lastIndexOf('.'); return index > -1 ? source.slice(index + 1).toLowerCase() : ''; },
isAttachmentImage(row) { return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(row)); }, isAttachmentImage(row) { return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(row)); },
previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; }, previewAttachment(row, rows = this.attachments) { const url = this.attachmentUrl(row); if (!url) { this.$message.warning('附件地址为空,无法预览'); return; } if (this.isAttachmentImage(row)) { this.imagePreviewUrls = (rows || []).filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item)).map(item => this.attachmentUrl(item)); this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0); this.imagePreviewVisible = true; return; } this.previewFile = { name: this.attachmentName(row), url, mimeType: row.mimeType || row.contentType || '' }; this.documentPreviewVisible = true; },
handleAttachmentUploadToLocation({ location, rows = [] }) {
if (!rows.length) return;
if (location === '合同文件') {
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
return;
}
if (location === '其它附件') {
this.attachments = [...(this.attachments || []), ...rows];
return;
}
if (location === '变更材料') {
this.changeMaterials = [...(this.changeMaterials || []), ...rows];
}
},
handlePreviewUnsupported() { this.$message.warning('当前文件暂不支持在线预览'); }, handlePreviewUnsupported() { this.$message.warning('当前文件暂不支持在线预览'); },
handlePreviewError() { this.$message.error('附件预览失败'); }, handlePreviewError() { this.$message.error('附件预览失败'); },
formatFileSize(value) { const size = Number(value || 0); return size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)}MB` : `${Math.max(1, Math.ceil(size / 1024))}KB`; }, formatFileSize(value) { const size = Number(value || 0); return size > 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(2)}MB` : `${Math.max(1, Math.ceil(size / 1024))}KB`; },
@@ -629,7 +677,7 @@ export default {
addCustomPeriodRow() { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); periods.push({ startDay: '', endDay: '' }); this.settlementRule.customPeriods = periods; }, addCustomPeriodRow() { const periods = normalizeCustomPeriods(this.settlementRule.customPeriods || []); periods.push({ startDay: '', endDay: '' }); this.settlementRule.customPeriods = periods; },
removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); }, 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; }, 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; }, 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', formInstanceId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected' }); this.$message.success('变更已提交'); this.$router.back(); },
}, },
}; };
@@ -654,6 +702,14 @@ export default {
.contract-basic-section :deep(.el-input), .contract-basic-section :deep(.el-input),
.contract-basic-section :deep(.el-select), .contract-basic-section :deep(.el-select),
.contract-basic-section :deep(.el-date-editor) { width: 360px; max-width: 100%; } .contract-basic-section :deep(.el-date-editor) { width: 360px; max-width: 100%; }
.contract-basic-section :deep(.el-input-number) {
width: 360px;
max-width: 100%;
.el-input__inner {
text-align: left;
}
}
.dialog-section-title { margin-bottom: 18px; font-size: 16px; font-weight: 600; } .dialog-section-title { margin-bottom: 18px; font-size: 16px; font-weight: 600; }
.dialog-section-title::before { display: inline-block; width: 4px; height: 16px; margin-right: 8px; vertical-align: -2px; background: #409eff; content: ''; } .dialog-section-title::before { display: inline-block; width: 4px; height: 16px; margin-right: 8px; vertical-align: -2px; background: #409eff; content: ''; }
.section-head { display: flex; align-items: center; justify-content: space-between; } .section-head { display: flex; align-items: center; justify-content: space-between; }
@@ -0,0 +1,21 @@
<template>
<mk-public-shell biz-type="contract-manage" :get-form="getForm">
<contract-manage ref="page" />
</mk-public-shell>
</template>
<script>
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
import ContractManage from '@/views/business/contract-manage.vue';
export default {
name: 'ContractManagePublicView',
components: { MkPublicShell, ContractManage },
methods: {
getForm() {
const row = this.$refs.page?.detailRow || {};
return { ...row, subject: row.contractName || row.contractNo || '' };
},
},
};
</script>
+207 -104
View File
@@ -222,7 +222,7 @@
Number(detailSettlementRule.autoGenerate) === 1 ? '开启' : '关闭' Number(detailSettlementRule.autoGenerate) === 1 ? '开启' : '关闭'
}}</el-descriptions-item> }}</el-descriptions-item>
<template v-if="Number(detailSettlementRule.autoGenerate) === 1"> <template v-if="Number(detailSettlementRule.autoGenerate) === 1">
<el-descriptions-item label="账单起始日">{{ <el-descriptions-item label="账单起始日">{{
displayValue(detailSettlementRule.billStartDate) displayValue(detailSettlementRule.billStartDate)
}}</el-descriptions-item> }}</el-descriptions-item>
<el-descriptions-item label="结算类型">{{ <el-descriptions-item label="结算类型">{{
@@ -244,7 +244,7 @@
}}</el-descriptions-item }}</el-descriptions-item
> >
<el-descriptions-item <el-descriptions-item
v-if="detailSettlementRule.settlementType === '固定天数周期结算'" v-if="isFixedCycleSettlementType(detailSettlementRule.settlementType)"
label="周期天数" label="周期天数"
>{{ >{{
detailObjectUnitValue(detailSettlementRule, 'cycleDays', '天') detailObjectUnitValue(detailSettlementRule, 'cycleDays', '天')
@@ -345,7 +345,7 @@
</section> </section>
</div> </div>
<div class="contract-manage-page__footer"> <div class="contract-manage-page__footer">
<el-button @click="closeDetail">关闭</el-button> <el-button v-if="!isPublicViewPage" @click="closeDetail">关闭</el-button>
</div> </div>
</template> </template>
@@ -478,7 +478,7 @@
type="date" type="date"
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="YYYY-MM-DD" placeholder="请输入"
/> />
<span>至</span> <span>至</span>
<el-date-picker <el-date-picker
@@ -486,7 +486,7 @@
type="date" type="date"
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="YYYY-MM-DD" placeholder="请输入"
/> />
</div> </div>
</el-form-item> </el-form-item>
@@ -498,12 +498,12 @@
><template #suffix>天</template></el-input> ><template #suffix>天</template></el-input>
</el-form-item> </el-form-item>
<el-form-item label="合同金额"> <el-form-item label="合同金额">
<el-input-number <el-input
v-model="form.contractAmount" :model-value="contractAmountDisplay"
:min="0"
:precision="2"
:controls="false"
placeholder="请输入" placeholder="请输入"
clearable
@input="handleContractAmountInput"
@clear="form.contractAmount = null"
/> />
</el-form-item> </el-form-item>
<el-form-item label="是否范本"> <el-form-item label="是否范本">
@@ -592,12 +592,13 @@
title="合同文件" title="合同文件"
description description
attachment-type attachment-type
use-upload-dialog :attachment-location-options="contractAttachmentLocationOptions"
:lock-approved="isAttachmentUploadMode || hasApprovedContractFiles" :lock-approved="isAttachmentUploadMode || hasApprovedContractFiles"
:mark-approved-on-upload="isAttachmentUploadMode" :mark-approved-on-upload="isAttachmentUploadMode"
:rows="contractFileRows" :rows="contractFileRows"
:preview="previewAttachment" :preview="previewAttachment"
@update:rows="contractFileRows = $event" @update:rows="contractFileRows = $event"
@upload-to-location="handleAttachmentUploadToLocation"
/> />
<section class="contract-manage-form__section contract-manage-form__section--panel"> <section class="contract-manage-form__section contract-manage-form__section--panel">
@@ -646,7 +647,7 @@
content="开启时,系统根据配置规则归集运单,定时生成结算单" content="开启时,系统根据配置规则归集运单,定时生成结算单"
placement="top" placement="top"
> >
<el-icon class="settlement-switch-tip"><QuestionFilled /></el-icon> <el-icon class="contract-manage-form__fee-mode-tip"><QuestionFilled /></el-icon>
</el-tooltip> </el-tooltip>
<el-radio :label="0">关闭</el-radio> <el-radio :label="0">关闭</el-radio>
</el-radio-group> </el-radio-group>
@@ -655,13 +656,13 @@
v-if="settlementRuleEnabled" v-if="settlementRuleEnabled"
class="contract-manage-form__grid contract-manage-form__settlement-grid" class="contract-manage-form__grid contract-manage-form__settlement-grid"
> >
<el-form-item label="账单起始日" required> <el-form-item label="账单起始日" required>
<el-date-picker <el-date-picker
v-model="settlementRuleForm.billStartDate" v-model="settlementRuleForm.billStartDate"
type="date" type="date"
format="YYYY-MM-DD" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" value-format="YYYY-MM-DD"
placeholder="请选择账单起始日" placeholder="请选择账单起始日"
/> />
</el-form-item> </el-form-item>
<el-form-item label="结算类型" required> <el-form-item label="结算类型" required>
@@ -811,12 +812,13 @@
title="其它附件" title="其它附件"
description description
attachment-type attachment-type
use-upload-dialog
attachment-location="其它附件" attachment-location="其它附件"
:attachment-location-options="contractAttachmentLocationOptions"
:readonly="isAttachmentUploadMode" :readonly="isAttachmentUploadMode"
:rows="attachmentRows" :rows="attachmentRows"
:preview="previewAttachment" :preview="previewAttachment"
@update:rows="attachmentRows = $event" @update:rows="attachmentRows = $event"
@upload-to-location="handleAttachmentUploadToLocation"
/> />
<section <section
@@ -921,6 +923,21 @@
@save="saveBillingPlan" @save="saveBillingPlan"
/> />
<contract-attachment-section
ref="listAttachmentUploader"
dialog-only
title="合同文件"
description
attachment-type
use-upload-dialog
mark-approved-on-upload
:attachment-location-options="['合同文件', '其它附件']"
:rows="listUploadContractFileRows"
@update:rows="listUploadContractFileRows = $event"
@upload-to-location="handleListAttachmentUploadToLocation"
@upload-confirmed="handleListAttachmentUploadConfirmed"
/>
<el-dialog <el-dialog
v-model="attachmentDocumentPreviewVisible" v-model="attachmentDocumentPreviewVisible"
:title="attachmentPreviewFile.name || '附件预览'" :title="attachmentPreviewFile.name || '附件预览'"
@@ -962,44 +979,10 @@
@close="attachmentImagePreviewVisible = false" @close="attachmentImagePreviewVisible = false"
/> />
<el-dialog <change-record-detail-dialog
v-model="detailChangeRecordVisible" v-model="detailChangeRecordVisible"
title="变更记录详情" :rows="detailChangeRecordDetailRows"
append-to-body
destroy-on-close
width="1100px"
top="10px"
class="contract-change-record-detail-dialog"
>
<div v-if="detailChangeRecord" class="contract-change-record-detail-meta">
<span>经办人:{{ detailChangeRecord.handlerUserName || '-' }}</span>
<span>变更类型:{{ detailChangeRecord.changeType || '-' }}</span>
<span>状态:{{ detailChangeRecord.statusName || detailChangeRecord.status || '-' }}</span>
</div>
<el-table :data="detailChangeRecordDetailRows" border :show-overflow-tooltip="false">
<el-table-column prop="field" label="变更字段" min-width="180" />
<el-table-column
prop="before"
label="变更前"
min-width="360"
class-name="contract-change-record-detail-value"
/> />
<el-table-column
prop="after"
label="变更后"
min-width="500"
class-name="contract-change-record-detail-value"
/>
</el-table>
<el-empty
v-if="!detailChangeRecordDetailRows.length"
description="暂无变更内容"
:image-size="60"
/>
<template #footer>
<el-button type="primary" @click="detailChangeRecordVisible = false">关闭</el-button>
</template>
</el-dialog>
<billing-plan-editor <billing-plan-editor
v-model="detailBillingPlanBox" v-model="detailBillingPlanBox"
@@ -1027,6 +1010,7 @@ import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
import { exportBlob } from '@/api/common'; import { exportBlob } from '@/api/common';
import * as api from '@/api/business/contract-manage'; import * as api from '@/api/business/contract-manage';
import { getMkPublicDetail } from '@/api/mk-process';
import { getList as getProjectList } from '@/api/business/project-apply'; import { getList as getProjectList } from '@/api/business/project-apply';
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive'; import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
import { getDeptTree } from '@/api/system/dept'; import { getDeptTree } from '@/api/system/dept';
@@ -1037,6 +1021,7 @@ import { getToken } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util'; import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval'; import { submitMkApprovalFlow } from '@/utils/mk-approval';
import BillingPlanEditor from './components/billing-plan-editor.vue'; import BillingPlanEditor from './components/billing-plan-editor.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
import ContractAttachmentSection, { import ContractAttachmentSection, {
attachmentName as sharedAttachmentName, attachmentName as sharedAttachmentName,
attachmentUrl as sharedAttachmentUrl, attachmentUrl as sharedAttachmentUrl,
@@ -1058,7 +1043,15 @@ const normalizeOptionalInteger = (value, emptyValue = null) => {
return Number.isFinite(number) ? Math.trunc(number) : emptyValue; return Number.isFinite(number) ? Math.trunc(number) : emptyValue;
}; };
const normalizeOptionalAmount = (value, emptyValue = null) => { const normalizeOptionalAmount = (value, emptyValue = null) => {
if (value === undefined || value === null || value === '') return emptyValue; if (
value === undefined ||
value === null ||
value === '' ||
Number(value) === -1 ||
String(value) === '-1'
) {
return emptyValue;
}
const number = Number(value); const number = Number(value);
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : emptyValue; return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : emptyValue;
}; };
@@ -1100,7 +1093,7 @@ const defaultForm = () => ({
settlementCurrency: '', settlementCurrency: '',
invoiceCycle: '', invoiceCycle: '',
paymentDays: '', paymentDays: '',
contractAmount: '', contractAmount: null,
templateFlag: 0, templateFlag: 0,
originalContractNo: '', originalContractNo: '',
electronicSealFlag: 0, electronicSealFlag: 0,
@@ -1181,7 +1174,14 @@ const attachmentViewerPlugins = [
export default { export default {
name: 'ContractManage', name: 'ContractManage',
components: { ContractAttachmentSection, BillingPlanEditor, ElImageViewer, OpenFileViewer, PdfPreview }, components: {
ContractAttachmentSection,
BillingPlanEditor,
ChangeRecordDetailDialog,
ElImageViewer,
OpenFileViewer,
PdfPreview,
},
data() { data() {
return { return {
api, api,
@@ -1249,6 +1249,9 @@ export default {
organizationLoading: false, organizationLoading: false,
contractFileRows: [], contractFileRows: [],
attachmentRows: [], attachmentRows: [],
listUploadContractFileRows: [],
listUploadAttachmentRows: [],
attachmentUploadFromList: false,
attachmentImagePreviewVisible: false, attachmentImagePreviewVisible: false,
attachmentImagePreviewUrls: [], attachmentImagePreviewUrls: [],
attachmentImagePreviewIndex: 0, attachmentImagePreviewIndex: 0,
@@ -1272,7 +1275,7 @@ export default {
formalSettlementRuleForm: defaultSettlementRule(), formalSettlementRuleForm: defaultSettlementRule(),
paymentRatioRows: [], paymentRatioRows: [],
changeRecordRows: [], changeRecordRows: [],
settlementTypeOptions: ['结', '日结', '周结', '月结', '固定天数周期结算'], settlementTypeOptions: ['结', '日结', '周结', '月结', '固定天数周期'],
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'], billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
detailLoading: false, detailLoading: false,
detailRow: {}, detailRow: {},
@@ -1316,7 +1319,12 @@ export default {
return this.$route.path === '/business/contract-manage/form'; return this.$route.path === '/business/contract-manage/form';
}, },
isDetailPage() { isDetailPage() {
return this.$route.path === '/business/contract-manage/detail'; return (
this.$route.path === '/business/contract-manage/detail' || this.isPublicViewPage
);
},
isPublicViewPage() {
return this.$route.path === '/business/contract-manage/public-view';
}, },
formMode() { formMode() {
return this.$route.query.mode === 'edit' ? 'edit' : 'add'; return this.$route.query.mode === 'edit' ? 'edit' : 'add';
@@ -1331,6 +1339,23 @@ export default {
isAttachmentUploadMode() { isAttachmentUploadMode() {
return this.$route.query.attachmentUpload === '1'; return this.$route.query.attachmentUpload === '1';
}, },
contractAttachmentLocationOptions() {
if (this.isAttachmentUploadMode) return ['合同文件'];
return ['合同文件', '其它附件'];
},
contractAmountDisplay() {
const value = this.form.contractAmount;
if (
value === undefined ||
value === null ||
value === '' ||
Number(value) === -1 ||
String(value) === '-1'
) {
return '';
}
return value;
},
hasApprovedContractFiles() { hasApprovedContractFiles() {
return (this.contractFileRows || []).some( return (this.contractFileRows || []).some(
row => row =>
@@ -1412,7 +1437,7 @@ export default {
return this.showBillCycleType && this.settlementRuleForm.billCycleType === '自定义多周期'; return this.showBillCycleType && this.settlementRuleForm.billCycleType === '自定义多周期';
}, },
showCycleDays() { showCycleDays() {
return this.settlementRuleForm.settlementType === '固定天数周期结算'; return this.isFixedCycleSettlementType(this.settlementRuleForm.settlementType);
}, },
billCutoffDayOptions() { billCutoffDayOptions() {
return Array.from({ length: 31 }, (_, index) => ({ return Array.from({ length: 31 }, (_, index) => ({
@@ -1473,9 +1498,11 @@ export default {
}, },
}, },
created() { created() {
if (!this.isPublicViewPage) {
this.loadProjectOptions(); this.loadProjectOptions();
this.loadSettlementDictionaries(); this.loadSettlementDictionaries();
this.loadOrganizationOptions(); this.loadOrganizationOptions();
}
if (this.isFormPage) this.initFormPage(); if (this.isFormPage) this.initFormPage();
if (this.isDetailPage) this.initDetailPage(); if (this.isDetailPage) this.initDetailPage();
}, },
@@ -1675,7 +1702,7 @@ export default {
copyCount: normalizeOptionalInteger(detail.copyCount, ''), copyCount: normalizeOptionalInteger(detail.copyCount, ''),
invoiceCycle: normalizeOptionalInteger(detail.invoiceCycle, ''), invoiceCycle: normalizeOptionalInteger(detail.invoiceCycle, ''),
paymentDays: normalizeOptionalInteger(detail.paymentDays, ''), paymentDays: normalizeOptionalInteger(detail.paymentDays, ''),
contractAmount: normalizeOptionalAmount(detail.contractAmount, ''), contractAmount: normalizeOptionalAmount(detail.contractAmount, null),
archiveStatus: detail.archiveStatus || '未归档', archiveStatus: detail.archiveStatus || '未归档',
feeGenerationMode: feeGenerationMode:
detail.feeGenerationMode || (Number(detail.billingEnabled) === 0 ? 'manual' : 'system'), detail.feeGenerationMode || (Number(detail.billingEnabled) === 0 ? 'manual' : 'system'),
@@ -1888,20 +1915,86 @@ export default {
return; return;
} }
this.submitAction = 'attachment'; this.submitAction = 'attachment';
this.api const contractFileRows = this.attachmentUploadFromList
? this.listUploadContractFileRows
: this.contractFileRows;
const attachmentRows = this.attachmentUploadFromList
? this.listUploadAttachmentRows
: this.attachmentRows;
return this.api
.updateAttachments({ .updateAttachments({
id: this.form.id, id: this.form.id,
contractFileJson: JSON.stringify(this.contractFileRows), contractFileJson: JSON.stringify(contractFileRows),
attachmentsJson: JSON.stringify(this.attachmentRows), attachmentsJson: JSON.stringify(attachmentRows),
}) })
.then(() => { .then(() => {
this.$message.success('附件保存成功'); this.$message.success('附件保存成功');
if (this.attachmentUploadFromList) {
this.attachmentUploadFromList = false;
this.listUploadContractFileRows = [];
this.listUploadAttachmentRows = [];
this.onLoad(this.page, this.query);
return;
}
this.closeForm(); this.closeForm();
}) })
.finally(() => { .finally(() => {
this.submitAction = ''; this.submitAction = '';
}); });
}, },
openAttachmentUploadDialog(row = {}) {
if (!row?.id) {
this.$message.warning('合同不存在,无法上传附件');
return;
}
const loading = this.$loading({
lock: true,
text: '加载中',
background: 'rgba(255, 255, 255, 0.6)',
});
this.attachmentUploadFromList = true;
this.listUploadContractFileRows = [];
this.listUploadAttachmentRows = [];
this.form = { ...defaultForm(), id: row.id };
this.api
.getDetail(row.id)
.then(res => {
const detail = res.data?.data || {};
this.form = {
...defaultForm(),
...detail,
id: detail.id || row.id,
};
this.listUploadContractFileRows = normalizeContractFileRows(
parseArray(detail.contractFileJson)
);
this.listUploadAttachmentRows = parseArray(detail.attachmentsJson);
this.$nextTick(() => {
this.$refs.listAttachmentUploader?.openUploadDialog?.();
});
})
.catch(() => {
this.attachmentUploadFromList = false;
this.$message.error('合同详情加载失败');
})
.finally(() => {
loading.close();
});
},
handleListAttachmentUploadConfirmed() {
if (!this.attachmentUploadFromList) return;
this.submitAttachmentUpload();
},
handleListAttachmentUploadToLocation({ location, rows = [] }) {
if (!rows.length) return;
if (location === '合同文件') {
this.listUploadContractFileRows = [...(this.listUploadContractFileRows || []), ...rows];
return;
}
if (location === '其它附件') {
this.listUploadAttachmentRows = [...(this.listUploadAttachmentRows || []), ...rows];
}
},
loadProjectOptions() { loadProjectOptions() {
if (this.projectLoading) return; if (this.projectLoading) return;
this.projectLoading = true; this.projectLoading = true;
@@ -2118,6 +2211,13 @@ export default {
integerInput(prop, value) { integerInput(prop, value) {
this.form[prop] = String(value || '').replace(/\D/g, ''); this.form[prop] = String(value || '').replace(/\D/g, '');
}, },
handleContractAmountInput(value) {
const text = String(value || '').replace(/[^\d.]/g, '');
const parts = text.split('.');
const normalized =
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 2)}` : parts[0];
this.form.contractAmount = normalized === '' ? null : normalized;
},
positiveIntegerInput(prop, value) { positiveIntegerInput(prop, value) {
const normalized = String(value || '').replace(/\D/g, '').replace(/^0+/, ''); const normalized = String(value || '').replace(/\D/g, '').replace(/^0+/, '');
this.form[prop] = normalized; this.form[prop] = normalized;
@@ -2148,6 +2248,16 @@ export default {
}; };
this.attachmentDocumentPreviewVisible = true; this.attachmentDocumentPreviewVisible = true;
}, },
handleAttachmentUploadToLocation({ location, rows = [] }) {
if (!rows.length) return;
if (location === '合同文件') {
this.contractFileRows = [...(this.contractFileRows || []), ...rows];
return;
}
if (location === '其它附件') {
this.attachmentRows = [...(this.attachmentRows || []), ...rows];
}
},
handleAttachmentPreviewUnsupported() { handleAttachmentPreviewUnsupported() {
this.$message.warning('当前文件暂不支持在线预览'); this.$message.warning('当前文件暂不支持在线预览');
}, },
@@ -2206,8 +2316,12 @@ export default {
} }
return true; return true;
}, },
isFixedCycleSettlementType(value) {
return value === '按固定天数周期' || value === '固定天数周期结算';
},
normalizeSettlementRule(rule = {}) { normalizeSettlementRule(rule = {}) {
const next = { ...defaultSettlementRule(), ...rule }; const next = { ...defaultSettlementRule(), ...rule };
if (next.settlementType === '固定天数周期结算') next.settlementType = '按固定天数周期';
if (next.settlementType !== '月结') { if (next.settlementType !== '月结') {
next.billCycleType = ''; next.billCycleType = '';
next.billCutoffDay = ''; next.billCutoffDay = '';
@@ -2222,7 +2336,7 @@ export default {
} else { } else {
next.customPeriods = []; next.customPeriods = [];
} }
if (next.settlementType !== '固定天数周期结算') next.cycleDays = ''; if (!this.isFixedCycleSettlementType(next.settlementType)) next.cycleDays = '';
return next; return next;
}, },
syncSettlementConfig() { syncSettlementConfig() {
@@ -2245,7 +2359,7 @@ export default {
this.settlementRuleForm.billCycleType = ''; this.settlementRuleForm.billCycleType = '';
this.settlementRuleForm.billCutoffDay = ''; this.settlementRuleForm.billCutoffDay = '';
this.settlementRuleForm.customPeriods = []; this.settlementRuleForm.customPeriods = [];
if (value !== '固定天数周期结算') this.settlementRuleForm.cycleDays = ''; if (!this.isFixedCycleSettlementType(value)) this.settlementRuleForm.cycleDays = '';
}, },
handleBillCycleTypeChange(value) { handleBillCycleTypeChange(value) {
if (value !== '固定截单日') this.settlementRuleForm.billCutoffDay = ''; if (value !== '固定截单日') this.settlementRuleForm.billCutoffDay = '';
@@ -2336,7 +2450,7 @@ export default {
validateSettlementRule(rule, label) { validateSettlementRule(rule, label) {
if (Number(rule.autoGenerate) !== 1) return true; if (Number(rule.autoGenerate) !== 1) return true;
if (!rule.billStartDate || !rule.settlementType) { if (!rule.billStartDate || !rule.settlementType) {
this.$message.warning(`${label}:请完整填写账单起始日和结算类型`); this.$message.warning(`${label}:请完整填写账单起始日和结算类型`);
return false; return false;
} }
if (rule.settlementType === '月结' && !rule.billCycleType) { if (rule.settlementType === '月结' && !rule.billCycleType) {
@@ -2354,7 +2468,7 @@ export default {
if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') { if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') {
return this.validateCustomPeriods(rule.customPeriods, label); return this.validateCustomPeriods(rule.customPeriods, label);
} }
if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { if (this.isFixedCycleSettlementType(rule.settlementType) && !rule.cycleDays) {
this.$message.warning(`${label}:请选择周期天数`); this.$message.warning(`${label}:请选择周期天数`);
return false; return false;
} }
@@ -2406,8 +2520,10 @@ export default {
if (!id) return; if (!id) return;
this.detailLoading = true; this.detailLoading = true;
this.applyDetailState({}); this.applyDetailState({});
this.api const request = this.isPublicViewPage
.getDetail(id) ? getMkPublicDetail('contract-manage', id)
: this.api.getDetail(id);
request
.then(res => { .then(res => {
this.applyDetailState(res.data?.data || {}); this.applyDetailState(res.data?.data || {});
}) })
@@ -2576,7 +2692,7 @@ export default {
.join('')}` .join('')}`
); );
} }
if (config.billStartDate) parts.push(`账单起始日${config.billStartDate}`); if (config.billStartDate) parts.push(`账单起始日:${config.billStartDate}`);
return parts.join(''); return parts.join('');
}; };
if (normalized.preSettlementConfig || normalized.formalSettlementConfig) { if (normalized.preSettlementConfig || normalized.formalSettlementConfig) {
@@ -2672,6 +2788,15 @@ export default {
}, },
detailValue(prop) { detailValue(prop) {
const value = this.detailRow[prop]; const value = this.detailRow[prop];
if (prop === 'contractAmount') {
return value === undefined ||
value === null ||
value === '' ||
Number(value) === -1 ||
String(value) === '-1'
? '-'
: value;
}
if ( if (
prop === 'contractStage' || prop === 'contractStage' ||
prop === 'approvalStatus' || prop === 'approvalStatus' ||
@@ -2706,15 +2831,7 @@ export default {
return; return;
} }
if (operation.action === 'attachmentUpload') { if (operation.action === 'attachmentUpload') {
this.$router.push({ this.openAttachmentUploadDialog(row);
path: '/business/contract-manage/form',
query: {
mode: 'edit',
id: row.id,
name: '附件上传',
attachmentUpload: '1',
},
});
return; return;
} }
const run = value => { const run = value => {
@@ -2900,25 +3017,6 @@ export default {
word-break: break-word; word-break: break-word;
} }
.contract-change-record-detail-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 32px;
margin-bottom: 16px;
color: #606266;
}
:deep(.contract-change-record-detail-dialog .el-dialog__body) {
max-height: 65vh;
overflow: auto;
padding-top: 12px;
}
:deep(.contract-change-record-detail-dialog .contract-change-record-detail-value .cell) {
white-space: pre-wrap;
word-break: break-all;
}
:global(.avue--collapse .contract-manage-page__footer) { :global(.avue--collapse .contract-manage-page__footer) {
left: 60px; left: 60px;
} }
@@ -2954,6 +3052,10 @@ export default {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
} }
&--attachment {
overflow: visible;
}
> .dialog-section-title, > .dialog-section-title,
:deep(.dialog-section-title) { :deep(.dialog-section-title) {
margin-bottom: 20px; margin-bottom: 20px;
@@ -3018,13 +3120,6 @@ export default {
align-items: center; align-items: center;
gap: 20px; gap: 20px;
margin-bottom: 16px; margin-bottom: 16px;
.settlement-switch-tip {
margin: 0 4px;
color: #a8abb2;
cursor: help;
font-size: 14px;
}
} }
&__settlement-grid { &__settlement-grid {
@@ -3082,11 +3177,19 @@ export default {
min-width: 0; min-width: 0;
} }
:deep(.el-input-number) {
width: 360px;
max-width: 100%;
.el-input__inner {
text-align: left;
}
}
:deep(.el-input), :deep(.el-input),
:deep(.el-select), :deep(.el-select),
:deep(.el-cascader), :deep(.el-cascader),
:deep(.el-tree-select), :deep(.el-tree-select),
:deep(.el-input-number),
:deep(.el-date-editor) { :deep(.el-date-editor) {
width: 360px; width: 360px;
max-width: 100%; max-width: 100%;
+559 -80
View File
@@ -289,29 +289,53 @@
<el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip /> <el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip />
<el-table-column label="发货地" prop="departureAddress" min-width="180"> <el-table-column label="发货地" prop="departureAddress" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
<el-tooltip :content="formatFullRouteAddress(row, 'departure')" placement="top"> <el-tooltip
v-if="formatFullRouteAddress(row, 'departure')"
:content="formatFullRouteAddress(row, 'departure')"
placement="top"
:show-after="120"
>
<div class="loading-manage-page__address-cell"> <div class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'departure') }} {{ formatRouteAddress(row, 'departure') }}
</div> </div>
</el-tooltip> </el-tooltip>
<div v-else class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'departure') }}
</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="途经地" prop="transitAddress" min-width="190"> <el-table-column label="途经地" prop="transitAddress" min-width="190">
<template #default="{ row }"> <template #default="{ row }">
<el-tooltip :content="formatFullRouteAddress(row, 'transit')" placement="top"> <el-tooltip
v-if="formatFullRouteAddress(row, 'transit')"
:content="formatFullRouteAddress(row, 'transit')"
placement="top"
:show-after="120"
>
<div class="loading-manage-page__address-cell"> <div class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'transit') }} {{ formatRouteAddress(row, 'transit') }}
</div> </div>
</el-tooltip> </el-tooltip>
<div v-else class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'transit') }}
</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="到货地" prop="arrivalAddress" min-width="180"> <el-table-column label="到货地" prop="arrivalAddress" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
<el-tooltip :content="formatFullRouteAddress(row, 'arrival')" placement="top"> <el-tooltip
v-if="formatFullRouteAddress(row, 'arrival')"
:content="formatFullRouteAddress(row, 'arrival')"
placement="top"
:show-after="120"
>
<div class="loading-manage-page__address-cell"> <div class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'arrival') }} {{ formatRouteAddress(row, 'arrival') }}
</div> </div>
</el-tooltip> </el-tooltip>
<div v-else class="loading-manage-page__address-cell">
{{ formatRouteAddress(row, 'arrival') }}
</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip /> <el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip />
@@ -386,32 +410,74 @@
v-if="isStandalonePage" v-if="isStandalonePage"
class="loading-manage-dialog loading-manage-form-dialog loading-manage-form-page" class="loading-manage-dialog loading-manage-form-dialog loading-manage-form-page"
> >
<div class="archive-page-form__title">{{ <div v-if="!isStandaloneDetailPage" class="archive-page-form__title">{{ formPageTitle }}</div>
isStandaloneDetailPage ? detailPageTitle : formPageTitle
}}</div>
<div v-loading="dialogLoading" class="loading-manage-dialog__body"> <div v-loading="dialogLoading" class="loading-manage-dialog__body">
<div v-if="dialogReadonly" class="loading-detail"> <div v-if="dialogReadonly" class="loading-detail">
<section-card title="配载单详情"> <section-card title="配载单详情">
<div class="loading-detail__summary"> <div class="loading-detail__summary">
<div class="loading-detail__heading">配载单详情</div>
<div>{{ dialogForm.loadingNo || '-' }}</div> <div>{{ dialogForm.loadingNo || '-' }}</div>
<div> <el-tag :type="detailStatusType">{{ statusText(dialogForm) }}</el-tag>
<el-tag :type="detailStatusType" class="status-text">{{ <el-tag type="primary">{{ transportTypeLabel(dialogForm.transportType) }}</el-tag>
statusText(dialogForm)
}}</el-tag>
</div>
<div>
<el-tag type="warning">{{ transportTypeLabel(dialogForm.transportType) }}</el-tag>
</div>
</div> </div>
<div class="loading-detail__task-row"> <div class="loading-detail__task-row">
<div class="loading-detail__task-main">
<span class="loading-detail__label">任务信息</span>
<div class="loading-detail__task-grid">
<div class="loading-detail__field"> <div class="loading-detail__field">
<span class="loading-detail__label">任务信息</span <span class="loading-detail__label">承运商</span>
><span class="loading-detail__value">{{ taskSummary }}</span> <span class="loading-detail__value">{{ detailTaskField('carrierName') }}</span>
</div> </div>
<div class="loading-detail__field"> <div class="loading-detail__field">
<span class="loading-detail__label">附件</span <span class="loading-detail__label">司机</span>
><span class="loading-detail__value" <span class="loading-detail__value">{{ detailTaskField('driverName') }}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">车牌号</span>
<span class="loading-detail__value">{{ detailTaskField('vehicleNo') }}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">手机号</span>
<span class="loading-detail__value">{{ detailTaskField('driverPhone') }}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">里程</span>
<span class="loading-detail__value">{{
detailTaskMileage ? `${detailTaskMileage} km` : '-'
}}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">预计发货日期</span>
<span class="loading-detail__value">{{
detailTaskDate('estimatedStartDate')
}}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">预计完成日期</span>
<span class="loading-detail__value">{{
detailTaskDate('estimatedEndDate')
}}</span>
</div>
<template v-if="isSelfCarrierDetail">
<div class="loading-detail__field">
<span class="loading-detail__label">挂车车牌号</span>
<span class="loading-detail__value">{{
detailTaskField('trailerVehicleNo')
}}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">押运人</span>
<span class="loading-detail__value">{{ detailTaskField('escortName') }}</span>
</div>
<div class="loading-detail__field">
<span class="loading-detail__label">押运人手机号</span>
<span class="loading-detail__value">{{ detailTaskField('escortPhone') }}</span>
</div>
</template>
</div>
</div>
<div class="loading-detail__field loading-detail__attachment-field">
<span class="loading-detail__label">附件</span>
<span class="loading-detail__value"
><el-link ><el-link
v-for="file in detailAttachments" v-for="file in detailAttachments"
:key="file.url || file.name" :key="file.url || file.name"
@@ -447,7 +513,19 @@
> >
</div> </div>
<div class="loading-detail__step-address"> <div class="loading-detail__step-address">
{{ node.displayAddress || formatAddressText(node.address) }} <el-tooltip
v-if="formatDetailRouteTooltip(node)"
:content="formatDetailRouteTooltip(node)"
placement="top"
:show-after="120"
>
<span class="loading-detail__step-address-text">{{
formatDetailRouteShort(node)
}}</span>
</el-tooltip>
<span v-else class="loading-detail__step-address-text">{{
formatDetailRouteShort(node)
}}</span>
</div> </div>
<div class="loading-detail__step-tags"> <div class="loading-detail__step-tags">
<el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag> <el-tag v-for="tag in node.tags || []" :key="tag" size="small">{{ tag }}</el-tag>
@@ -1181,9 +1259,10 @@
</el-form-item> </el-form-item>
<el-form-item label="里程km"> <el-form-item label="里程km">
<el-input <el-input
v-model="dialogForm.mileage" :model-value="normalizeLoadingMileage(dialogForm.mileage)"
clearable clearable
placeholder="请输入" placeholder="请输入"
:disabled="dialogReadonly"
@input="handleMileageInput" @input="handleMileageInput"
/> />
</el-form-item> </el-form-item>
@@ -1414,6 +1493,7 @@ import { getList as getContractList } from '@/api/business/contract-manage';
import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type'; import { getParentOptions as getCargoTypeOptions } from '@/api/base/cargo-type';
import { getList as getCustomerList } from '@/api/vehicle/customer-archive'; import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
import { getList as getDriverList } from '@/api/transportCapacity/driver'; import { getList as getDriverList } from '@/api/transportCapacity/driver';
import { fetchDriversByCarrierOrganizations } from '@/utils/carrier-org-resource';
import { getList as getProcessConfigList } from '@/api/business/process-config'; import { getList as getProcessConfigList } from '@/api/business/process-config';
import { getList as getCommonAddressList } from '@/api/base/common-address'; import { getList as getCommonAddressList } from '@/api/base/common-address';
import { import {
@@ -1433,6 +1513,7 @@ const defaultCandidateTransportType = '';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315'; const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a'; const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills'; const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills';
const loadingManageListRefreshKey = 'loading-manage-list-refresh';
let detailAmapLoader; let detailAmapLoader;
const createSearchForm = () => ({ const createSearchForm = () => ({
@@ -1517,6 +1598,14 @@ const normalizeLoadingMileage = value => {
return value; return value;
}; };
// 提交时未填写里程传 null,禁止自动填充 -1
const toSubmitMileage = value => {
const normalized = normalizeLoadingMileage(value);
if (normalized === '') return null;
const number = Number(normalized);
return Number.isFinite(number) ? number : null;
};
const createCopiedDialogForm = (detail = {}) => { const createCopiedDialogForm = (detail = {}) => {
const form = createDialogForm(); const form = createDialogForm();
Object.keys(form).forEach(prop => { Object.keys(form).forEach(prop => {
@@ -1683,11 +1772,12 @@ export default {
const typeMap = { const typeMap = {
pending: 'success', pending: 'success',
running: 'success', running: 'success',
completed: 'info', processing: 'success',
completed: 'primary',
cancelled: 'info', cancelled: 'info',
draft: 'warning', draft: 'info',
}; };
return typeMap[this.dialogForm.businessStatus] || 'info'; return typeMap[this.dialogForm.businessStatus] || 'warning';
}, },
taskSummary() { taskSummary() {
return ( return (
@@ -1696,6 +1786,12 @@ export default {
.join(' / ') || '-' .join(' / ') || '-'
); );
}, },
isSelfCarrierDetail() {
return String(this.dialogForm.carrierType || '').trim() === '自运';
},
detailTaskMileage() {
return normalizeLoadingMileage(this.dialogForm.mileage);
},
detailAttachments() { detailAttachments() {
return parseJsonArray(this.dialogForm.attachmentsJson || this.dialogForm.attachmentJson).map( return parseJsonArray(this.dialogForm.attachmentsJson || this.dialogForm.attachmentJson).map(
item => item =>
@@ -1753,12 +1849,29 @@ export default {
deactivated() { deactivated() {
this.closeInnerDialogs(); this.closeInnerDialogs();
}, },
activated() {
if (this.isStandalonePage) return;
if (sessionStorage.getItem(loadingManageListRefreshKey)) {
sessionStorage.removeItem(loadingManageListRefreshKey);
this.loadTable();
}
},
beforeUnmount() { beforeUnmount() {
this.closeInnerDialogs(); this.closeInnerDialogs();
}, },
watch: { watch: {
$route(to, from) { $route(to, from) {
if (!this.isStandalonePage) return; if (!this.isStandalonePage) {
// 从表单/详情返回列表时刷新,避免草稿确认后仍显示旧状态
if (
from?.path?.includes('/loading-manage/form') ||
from?.path?.includes('/loading-manage/detail')
) {
sessionStorage.removeItem(loadingManageListRefreshKey);
this.loadTable();
}
return;
}
this.syncStandaloneTagTitle(); this.syncStandaloneTagTitle();
if (this.isStandaloneDetailPage) { if (this.isStandaloneDetailPage) {
// 记录变化或上一个页面不是详情态(例如从表单页/列表页进来)时重新拉取详情 // 记录变化或上一个页面不是详情态(例如从表单页/列表页进来)时重新拉取详情
@@ -1987,6 +2100,157 @@ export default {
value.includes('highway') value.includes('highway')
); );
}, },
// 与总单详情一致:公路地址压缩为「市 区县」
formatRoadLocation(value) {
const text = String(value || '')
.replace(/[\/\s]+/g, '')
.trim();
if (!text) return '';
const parts = this.parseWaybillAddress(text);
let city = String(parts.city || '').trim();
let district = String(parts.district || '').trim();
let detail = String(parts.detailAddress || '').trim();
// 自治州/地区/盟下还有「XX市 + 区县」时,以市级作为展示主体
if (/(?:自治州|地区|盟)$/.test(city) && detail) {
const nestedCityMatch = detail.match(/^(.+?市)/);
if (nestedCityMatch) {
city = nestedCityMatch[1];
detail = detail.slice(nestedCityMatch[1].length);
const nestedDistrictMatch = detail.match(
/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/
);
if (nestedDistrictMatch) district = nestedDistrictMatch[1];
} else if (!district) {
const nestedDistrictMatch = detail.match(
/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/
);
if (nestedDistrictMatch) district = nestedDistrictMatch[1];
}
}
// 市级后紧跟区县,但 parse 未拆出时再补一次
if (city && !district && detail) {
const nestedDistrictMatch = detail.match(
/^(.+?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗))/
);
if (nestedDistrictMatch) district = nestedDistrictMatch[1];
}
if (city && district) {
// 兼容「梅县区」:命中「县」后还需保留后面的「区」
if (district.endsWith('县') && detail.startsWith(`${district}区`)) {
district = `${district}区`;
}
return `${city} ${district}`;
}
if (city) return city;
if (district) return district;
// 兜底压缩
const withoutProvince = text.replace(/^.*?(?:省|自治区|特别行政区)/, '');
const cityMatch = withoutProvince.match(/.+?(?:市|自治州|州(?!市)|盟(?!市)|地区)/);
if (!cityMatch) return text;
const fallbackCity = cityMatch[0];
const districtSource = withoutProvince.slice(fallbackCity.length);
const districtMatch = districtSource.match(
/^[^市州盟]*?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/
);
if (!districtMatch) {
// 州后面是县级市时,继续解析市 + 区
const nestedCityMatch = districtSource.match(/^(.+?市)/);
if (nestedCityMatch) {
const nestedDistrictSource = districtSource.slice(nestedCityMatch[1].length);
const nestedDistrictMatch = nestedDistrictSource.match(
/^[^市州盟]*?(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/
);
if (nestedDistrictMatch) {
return `${nestedCityMatch[1]} ${nestedDistrictMatch[0]}`;
}
return nestedCityMatch[1];
}
return fallbackCity;
}
const fallbackDistrict = `${districtMatch[0]}${
districtSource.slice(districtMatch[0].length).startsWith('区') ? '区' : ''
}`;
return `${fallbackCity} ${fallbackDistrict}`;
},
isStationLikeName(value) {
const text = String(value || '').trim();
if (!text) return false;
return (
/(?:火车站|高铁站|客运站|货运站|港口|码头|机场|空港|航站楼)$/.test(text) ||
/(?<!市|州|盟|区|县|旗)站$/.test(text)
);
},
// 详情运输路线短展示:公路仅市区,非公路仅名称
formatDetailRouteShort(node = {}) {
const name = String(node.name || '').trim();
const address = String(node.address || '').trim();
const displayAddress = String(node.displayAddress || '').trim();
if (!this.isRoadTransportType(this.dialogForm.transportType)) {
if (name) return name;
if (this.isStationLikeName(displayAddress)) return displayAddress;
if (this.isStationLikeName(address)) return address;
return displayAddress || address || '-';
}
const enriched = this.enrichRouteNodeRegion(node);
// 合并节点地址与运单上的市/区字段,避免只解析到市
const region = this.mergeRouteAddressParts(enriched.cityName, enriched.districtName, '');
const source = this.mergeRouteAddressParts(
address,
displayAddress,
region,
enriched.name || name
);
const formatted = this.formatRoadLocation(source);
if (enriched.cityName && enriched.districtName) {
return `${enriched.cityName} ${enriched.districtName}`;
}
if (/(?:市|州|盟|区|县|旗)/.test(formatted)) return formatted;
return formatted || name || displayAddress || address || '-';
},
// 公路悬停展示完整地址;非公路不展示 tooltip
formatDetailRouteTooltip(node = {}) {
if (!this.isRoadTransportType(this.dialogForm.transportType)) return '';
const enriched = this.enrichRouteNodeRegion(node);
const region = this.mergeRouteAddressParts(enriched.cityName, enriched.districtName, '');
const full = this.mergeRouteAddressParts(
String(node.address || '').trim(),
String(node.displayAddress || '').trim(),
region,
String(node.name || enriched.name || '').trim()
);
const short = this.formatDetailRouteShort(node);
if (!full) return '';
return full === short ? '' : full;
},
// 按装/卸标签从已选运单回填市、区,补充 routeJson 中缺失的区县
enrichRouteNodeRegion(node = {}) {
const tags = Array.isArray(node.tags) ? node.tags : [];
const result = { cityName: '', districtName: '', name: '' };
for (const tag of tags) {
const text = String(tag || '');
const loadMatch = text.match(/^装(\d+)$/);
const unloadMatch = text.match(/^卸(\d+)$/);
const index = Number((loadMatch || unloadMatch)?.[1] || 0) - 1;
if (index < 0) continue;
const row = this.selectedWaybillRows[index];
if (!row) continue;
const prefix = loadMatch ? 'departure' : 'arrival';
result.cityName = String(
row[`${prefix}CityName`] || row[`${prefix}City`] || ''
).trim();
result.districtName = String(
row[`${prefix}DistrictName`] || row[`${prefix}District`] || ''
).trim();
result.name = String(row[`${prefix}Name`] || '').trim();
if (result.cityName || result.districtName || result.name) break;
}
return result;
},
splitText(value) { splitText(value) {
if (!value) return []; if (!value) return [];
return String(value) return String(value)
@@ -2024,37 +2288,72 @@ export default {
formatRouteAddress(row, type) { formatRouteAddress(row, type) {
const prefix = const prefix =
type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit'; type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
if (type !== 'transit') { if (type === 'transit') {
const address = row[`${prefix}Address`]; const values = this.splitText(row.transitAddress || row.transitName || '')
const region = this.routeProvinceName(row, prefix); .map(value => this.formatListRouteShort(row, value, value))
const district = row[`${prefix}DistrictName`]; .filter(value => value && value !== '-');
const source = this.mergeRouteAddressParts(region, district, address); return [...new Set(values)].join('') || '-';
return this.formatProvinceCityDistrict(source) || '-';
} }
const values = [ const name = String(row[`${prefix}Name`] || '').trim();
row[`${prefix}RegionName`], const address = String(row[`${prefix}Address`] || '').trim();
row[`${prefix}DistrictName`], const cityName = String(
row[`${prefix}Address`], row[`${prefix}CityName`] || row[`${prefix}City`] || ''
] ).trim();
.filter(Boolean) const districtName = String(
.flatMap(value => this.splitText(value)) row[`${prefix}DistrictName`] || row[`${prefix}District`] || ''
.map(value => this.formatProvinceCityDistrict(value)) ).trim();
.filter(Boolean); if (
return [...new Set(values)].join(',') || '-'; this.isRoadTransportType(row?.transportType) &&
cityName &&
districtName
) {
return `${cityName} ${districtName}`;
}
const region = this.mergeRouteAddressParts(
this.routeProvinceName(row, prefix),
cityName,
districtName
);
const source = this.mergeRouteAddressParts(region, '', address || name);
return this.formatListRouteShort(row, name, source || address || name);
}, },
formatFullRouteAddress(row, type) { formatFullRouteAddress(row, type) {
if (!this.isRoadTransportType(row?.transportType)) return '';
const prefix = const prefix =
type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit'; type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
if (type !== 'transit') { if (type === 'transit') {
const address = row[`${prefix}Address`]; const values = this.splitText(row.transitAddress || '')
const region = this.routeProvinceName(row, prefix); .map(value => String(value || '').trim())
const district = row[`${prefix}DistrictName`]; .filter(Boolean);
return this.mergeRouteAddressParts(region, district, address) || '-'; const full = [...new Set(values)].join('');
const short = this.formatRouteAddress(row, 'transit');
return full && full !== short ? full : '';
} }
const values = [row.transitRegionName, row.transitDistrictName, row.transitAddress] const name = String(row[`${prefix}Name`] || '').trim();
.filter(Boolean) const address = String(row[`${prefix}Address`] || '').trim();
.flatMap(value => this.splitText(value)); const region = this.mergeRouteAddressParts(
return [...new Set(values)].join('') || '-'; this.routeProvinceName(row, prefix),
row[`${prefix}CityName`] || row[`${prefix}City`],
row[`${prefix}DistrictName`] || row[`${prefix}District`]
);
const full = this.mergeRouteAddressParts(region, '', address || name);
const short = this.formatRouteAddress(row, type);
if (!full) return '';
return full === short ? '' : full;
},
// 列表短地址:公路「市 区县」,非公路仅名称
formatListRouteShort(row = {}, name = '', address = '') {
const rawName = String(name || '').trim();
const rawAddress = String(address || '').trim();
if (!this.isRoadTransportType(row?.transportType)) {
if (rawName) return rawName;
if (this.isStationLikeName(rawAddress)) return rawAddress;
return rawAddress || '-';
}
const source = rawAddress || rawName;
const formatted = this.formatRoadLocation(source);
if (/(?:市|州|盟)/.test(formatted)) return formatted;
return formatted || rawName || rawAddress || '-';
}, },
formatWaybillAddress(row, type) { formatWaybillAddress(row, type) {
const prefix = type === 'departure' ? 'departure' : 'arrival'; const prefix = type === 'departure' ? 'departure' : 'arrival';
@@ -2280,7 +2579,8 @@ export default {
this.dialogForm = row.copy this.dialogForm = row.copy
? createCopiedDialogForm(detail) ? createCopiedDialogForm(detail)
: { ...createDialogForm(), ...detail }; : { ...createDialogForm(), ...detail };
this.dialogForm.mileage = normalizeLoadingMileage(this.dialogForm.mileage); this.applyLoadingTaskInfoFromDetail(this.dialogForm);
this.applyLoadingMileageFromDetail(this.dialogForm);
await this.restoreWaybillRows( await this.restoreWaybillRows(
this.dialogForm.waybillIdsJson, this.dialogForm.waybillIdsJson,
this.dialogForm.loadingSubNos this.dialogForm.loadingSubNos
@@ -2617,6 +2917,8 @@ export default {
...this.buildQueryParams(this.candidateQuery), ...this.buildQueryParams(this.candidateQuery),
businessStatus: 'pending', businessStatus: 'pending',
onlyUnassignedLoading: 1, onlyUnassignedLoading: 1,
// 仅查当前账户所属组织及子组织下的运单
allDept: 0,
...(this.dialogMode === 'add' ? { transportType: 'road' } : {}), ...(this.dialogMode === 'add' ? { transportType: 'road' } : {}),
}; };
const transportType = this.getCandidateTransportTypeQuery(query.transportType); const transportType = this.getCandidateTransportTypeQuery(query.transportType);
@@ -2711,12 +3013,16 @@ export default {
.map(row => row.waybillNo) .map(row => row.waybillNo)
.filter(Boolean) .filter(Boolean)
.join(''); .join('');
this.dialogForm.waybillIdsJson = JSON.stringify( const rowIds = rows
rows
.map(row => row.id) .map(row => row.id)
.filter(Boolean) .filter(Boolean)
.map(id => String(id)) .map(id => String(id));
); // 回显失败仅有运单号时,保留原 waybillIdsJson,避免确认提交清空运单绑定
if (rowIds.length) {
this.dialogForm.waybillIdsJson = JSON.stringify(rowIds);
} else if (!String(this.dialogForm.waybillIdsJson || '').trim()) {
this.dialogForm.waybillIdsJson = '[]';
}
this.dialogForm.projectName = uniqueText(rows, 'projectName'); this.dialogForm.projectName = uniqueText(rows, 'projectName');
this.dialogForm.customerName = uniqueText(rows, 'customerName'); this.dialogForm.customerName = uniqueText(rows, 'customerName');
this.dialogForm.originalNo = uniqueText(rows, 'originalNo'); this.dialogForm.originalNo = uniqueText(rows, 'originalNo');
@@ -2843,15 +3149,73 @@ export default {
remark: item.remark || '', remark: item.remark || '',
}; };
}, },
applyLoadingMileageFromDetail(form = this.dialogForm) {
let taskMileage = '';
try {
const taskInfo = JSON.parse(form.taskInfoJson || '{}');
taskMileage = taskInfo?.mileage;
} catch (error) {
taskMileage = '';
}
form.mileage = normalizeLoadingMileage(
form.mileage !== undefined && form.mileage !== null && form.mileage !== ''
? form.mileage
: taskMileage
);
},
applyLoadingTaskInfoFromDetail(form = this.dialogForm) {
let taskInfo = {};
try {
taskInfo = JSON.parse(form.taskInfoJson || '{}') || {};
} catch (error) {
taskInfo = {};
}
if (!taskInfo || typeof taskInfo !== 'object' || Array.isArray(taskInfo)) return;
const fields = [
'carrierType',
'carrierName',
'carrierContractId',
'driverName',
'driverPhone',
'vehicleNo',
'trailerVehicleNo',
'escortName',
'escortPhone',
'estimatedStartDate',
'estimatedEndDate',
'taskRemark',
];
fields.forEach(prop => {
if (
(form[prop] === undefined || form[prop] === null || form[prop] === '') &&
taskInfo[prop] !== undefined &&
taskInfo[prop] !== null &&
taskInfo[prop] !== ''
) {
form[prop] = taskInfo[prop];
}
});
if (
(form.mileage === undefined || form.mileage === null || form.mileage === '') &&
taskInfo.mileage !== undefined
) {
form.mileage = normalizeLoadingMileage(taskInfo.mileage);
}
},
detailTaskField(prop) {
const value = String(this.dialogForm?.[prop] ?? '').trim();
return value || '-';
},
detailTaskDate(prop) {
const value = String(this.dialogForm?.[prop] ?? '').trim();
return value ? value.slice(0, 10) : '-';
},
normalizePayload() { normalizePayload() {
this.syncRouteAddressFields(); this.syncRouteAddressFields();
this.rebuildSummaryFromWaybills(false); this.rebuildSummaryFromWaybills(false);
this.dialogForm.mileage = normalizeLoadingMileage(this.dialogForm.mileage); this.dialogForm.mileage = normalizeLoadingMileage(this.dialogForm.mileage);
return { const mileage = toSubmitMileage(this.dialogForm.mileage);
...this.dialogForm, const taskInfo = {
routeJson: JSON.stringify(this.routeNodes),
goodsJson: JSON.stringify(this.cargoRows),
taskInfoJson: JSON.stringify({
carrierType: this.dialogForm.carrierType, carrierType: this.dialogForm.carrierType,
carrierName: this.dialogForm.carrierName, carrierName: this.dialogForm.carrierName,
carrierContractId: this.dialogForm.carrierContractId, carrierContractId: this.dialogForm.carrierContractId,
@@ -2861,11 +3225,19 @@ export default {
trailerVehicleNo: this.dialogForm.trailerVehicleNo, trailerVehicleNo: this.dialogForm.trailerVehicleNo,
escortName: this.dialogForm.escortName, escortName: this.dialogForm.escortName,
escortPhone: this.dialogForm.escortPhone, escortPhone: this.dialogForm.escortPhone,
mileage: this.dialogForm.mileage,
estimatedStartDate: this.dialogForm.estimatedStartDate, estimatedStartDate: this.dialogForm.estimatedStartDate,
estimatedEndDate: this.dialogForm.estimatedEndDate, estimatedEndDate: this.dialogForm.estimatedEndDate,
taskRemark: this.dialogForm.taskRemark, taskRemark: this.dialogForm.taskRemark,
}), };
if (mileage !== null) {
taskInfo.mileage = mileage;
}
return {
...this.dialogForm,
mileage,
routeJson: JSON.stringify(this.routeNodes),
goodsJson: JSON.stringify(this.cargoRows),
taskInfoJson: JSON.stringify(taskInfo),
}; };
}, },
validatePayload(draftMode = false) { validatePayload(draftMode = false) {
@@ -2920,7 +3292,7 @@ export default {
return true; return true;
}, },
async saveDraft() { async saveDraft() {
if (!this.validatePayload(true)) return; // 暂存不做任何业务校验,直接保存
this.dialogSaving = 'draft'; this.dialogSaving = 'draft';
try { try {
const res = await loadingApi.saveDraft(this.normalizePayload()); const res = await loadingApi.saveDraft(this.normalizePayload());
@@ -2928,7 +3300,7 @@ export default {
...this.dialogForm, ...this.dialogForm,
...(res.data?.data || res.data || {}), ...(res.data?.data || res.data || {}),
}; };
this.dialogForm.mileage = normalizeLoadingMileage(this.dialogForm.mileage); this.applyLoadingMileageFromDetail(this.dialogForm);
ElMessage.success('暂存成功'); ElMessage.success('暂存成功');
this.loadTable(); this.loadTable();
} finally { } finally {
@@ -2939,7 +3311,11 @@ export default {
if (!this.validatePayload(false)) return; if (!this.validatePayload(false)) return;
this.dialogSaving = 'submit'; this.dialogSaving = 'submit';
try { try {
const payload = this.normalizePayload(); const payload = {
...this.normalizePayload(),
// 确认生成正式配载单(待执行),覆盖草稿状态
businessStatus: 'pending',
};
if (this.dialogMode === 'reassign') { if (this.dialogMode === 'reassign') {
await loadingApi.reassign(payload); await loadingApi.reassign(payload);
ElMessage.success('派单成功'); ElMessage.success('派单成功');
@@ -2947,6 +3323,7 @@ export default {
await loadingApi.submit(payload); await loadingApi.submit(payload);
ElMessage.success('确认成功'); ElMessage.success('确认成功');
} }
sessionStorage.setItem(loadingManageListRefreshKey, '1');
this.closeLoadingDialog(); this.closeLoadingDialog();
this.loadTable(); this.loadTable();
} finally { } finally {
@@ -3032,6 +3409,7 @@ export default {
this.dialogForm.carrierContractId = ''; this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = ''; this.dialogForm.carrierName = '';
this.dialogForm.carrierId = ''; this.dialogForm.carrierId = '';
this.clearDialogDriverVehicleFields();
if (this.dialogForm.carrierType === '承运商') { if (this.dialogForm.carrierType === '承运商') {
this.dialogForm.trailerVehicleNo = ''; this.dialogForm.trailerVehicleNo = '';
this.dialogForm.escortName = ''; this.dialogForm.escortName = '';
@@ -3051,6 +3429,23 @@ export default {
this.dialogForm.carrierContractId = this.dialogForm.carrierContractId =
this.dialogForm.carrierType === '承运商' && contract?.id ? contract.id : ''; this.dialogForm.carrierType === '承运商' && contract?.id ? contract.id : '';
this.dialogForm.carrierName = contract?.carrierName || ''; this.dialogForm.carrierName = contract?.carrierName || '';
this.dialogForm.carrierId = contract?.carrierId || contract?.customerId || '';
this.clearDialogDriverVehicleFields();
},
clearDialogDriverVehicleFields() {
this.dialogForm.driverName = '';
this.dialogForm.driverPhone = '';
this.dialogForm.vehicleNo = '';
this.dialogForm.trailerVehicleNo = '';
this.dialogForm.escortName = '';
this.dialogForm.escortPhone = '';
this.driverOptions = [];
},
getDialogCarrierFilter() {
return {
carrierId: this.dialogForm.carrierId || '',
carrierName: this.dialogForm.carrierName || '',
};
}, },
handleDriverChange(value) { handleDriverChange(value) {
const driver = this.driverOptions.find(item => (item.driverName || item.name) === value); const driver = this.driverOptions.find(item => (item.driverName || item.name) === value);
@@ -3062,7 +3457,7 @@ export default {
} }
}, },
fetchDriverSuggestions(queryString, callback) { fetchDriverSuggestions(queryString, callback) {
this.loadDriverOptions(String(queryString || '').trim()) this.loadDialogDriverOptions(String(queryString || '').trim())
.then(() => .then(() =>
callback( callback(
this.driverOptions.map(item => ({ this.driverOptions.map(item => ({
@@ -3085,11 +3480,19 @@ export default {
}, },
fetchEscortSuggestions(queryString, callback) { fetchEscortSuggestions(queryString, callback) {
const keyword = String(queryString || '').trim(); const keyword = String(queryString || '').trim();
const carrier = this.getDialogCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
return;
}
this.escortLoading = true; this.escortLoading = true;
getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '押运员' }) fetchDriversByCarrierOrganizations(
.then(res => { { size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '押运员' },
carrier
)
.then(records => {
callback( callback(
unwrapRecords(res).map(item => ({ records.map(item => ({
...item, ...item,
value: item.driverName || item.name || '', value: item.driverName || item.name || '',
})) }))
@@ -3109,6 +3512,7 @@ export default {
handleMileageInput(value) { handleMileageInput(value) {
this.dialogForm.mileage = String(value || '').replace(/[^\d.]/g, ''); this.dialogForm.mileage = String(value || '').replace(/[^\d.]/g, '');
}, },
normalizeLoadingMileage,
transportTypeLabel(value) { transportTypeLabel(value) {
if (!value) return '-'; if (!value) return '-';
return ( return (
@@ -3158,12 +3562,29 @@ export default {
this.driverLoading = true; this.driverLoading = true;
try { try {
this.driverOptions = unwrapRecords( this.driverOptions = unwrapRecords(
await getDriverList(1, 20, { driverName: keyword, posts: '司机' }) await getDriverList(1, 20, { ...(keyword ? { driverName: keyword } : {}), posts: '司机' })
); );
} finally { } finally {
this.driverLoading = false; this.driverLoading = false;
} }
}, },
async loadDialogDriverOptions(keyword = '') {
const carrier = this.getDialogCarrierFilter();
if (!carrier.carrierId && !carrier.carrierName) {
this.driverOptions = [];
return [];
}
this.driverLoading = true;
try {
this.driverOptions = await fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
);
return this.driverOptions;
} finally {
this.driverLoading = false;
}
},
async loadCarrierOptions(keyword = '') { async loadCarrierOptions(keyword = '') {
this.carrierLoading = true; this.carrierLoading = true;
try { try {
@@ -3225,15 +3646,39 @@ export default {
} else { } else {
this.dialogForm.carrierContractId = ''; this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = ''; this.dialogForm.carrierName = '';
this.dialogForm.carrierId = '';
} }
return this.taskCarrierOptions; return this.taskCarrierOptions;
} }
if (this.dialogForm.carrierType === '自运' && contracts.length) { if (this.dialogForm.carrierType === '自运' && contracts.length) {
this.taskCarrierOptions = contracts; this.taskCarrierOptions = contracts
const current = contracts.find(item => item.carrierName === this.dialogForm.carrierName); .map(item => ({
...item,
carrierName:
item.partyB ||
item.partyBName ||
item.contractPartyB ||
item.customerContractPartyB ||
item.carrierName ||
'',
carrierId:
item.partyBId ||
item.partyBUserId ||
item.contractPartyBId ||
item.customerContractPartyBId ||
item.carrierId ||
'',
}))
.filter(item => item.carrierName);
const current = this.taskCarrierOptions.find(
item => item.carrierName === this.dialogForm.carrierName
);
this.dialogForm.carrierContractId = ''; this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = current?.carrierName || contracts[0].carrierName || ''; this.dialogForm.carrierName =
return contracts; current?.carrierName || this.taskCarrierOptions[0]?.carrierName || '';
this.dialogForm.carrierId =
current?.carrierId || this.taskCarrierOptions[0]?.carrierId || '';
return this.taskCarrierOptions;
} }
if (this.dialogForm.carrierType !== '自运') { if (this.dialogForm.carrierType !== '自运') {
this.taskCarrierOptions = []; this.taskCarrierOptions = [];
@@ -3267,6 +3712,7 @@ export default {
})); }));
this.dialogForm.carrierContractId = ''; this.dialogForm.carrierContractId = '';
this.dialogForm.carrierName = partyBNames[0] || ''; this.dialogForm.carrierName = partyBNames[0] || '';
this.dialogForm.carrierId = '';
return this.taskCarrierOptions; return this.taskCarrierOptions;
} catch (error) { } catch (error) {
if (requestId !== this.taskCarrierRequestId) return []; if (requestId !== this.taskCarrierRequestId) return [];
@@ -3604,13 +4050,17 @@ export default {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 24px; gap: 12px;
padding: 4px 0 16px; padding: 4px 0 16px;
:deep(.el-tag) {
height: 28px;
padding: 0 12px;
border: 0;
border-radius: 8px;
font-size: 13px;
line-height: 28px;
} }
.loading-detail__heading {
font-size: 20px;
font-weight: 600;
margin-right: 8px;
} }
.loading-detail__route-title { .loading-detail__route-title {
display: inline-flex; display: inline-flex;
@@ -3623,9 +4073,24 @@ export default {
} }
.loading-detail__task-row { .loading-detail__task-row {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: minmax(0, 1fr) 220px;
gap: 24px; gap: 24px;
padding-bottom: 4px; padding-bottom: 4px;
align-items: start;
}
.loading-detail__task-main {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
.loading-detail__task-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 16px 24px;
}
.loading-detail__attachment-field {
min-width: 0;
} }
.loading-detail__field { .loading-detail__field {
display: flex; display: flex;
@@ -3641,6 +4106,8 @@ export default {
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
text-align: left; text-align: left;
color: #303133;
word-break: break-all;
} }
.loading-detail__steps { .loading-detail__steps {
display: flex; display: flex;
@@ -3697,6 +4164,14 @@ export default {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.loading-detail__step-address-text {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: bottom;
cursor: default;
}
.loading-detail__step-tags { .loading-detail__step-tags {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -3883,5 +4358,9 @@ export default {
.loading-detail__bottom-grid { .loading-detail__bottom-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.loading-detail__task-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
} }
</style> </style>
+11 -1
View File
@@ -132,7 +132,7 @@
/> />
</template> </template>
<master-order-dispatch v-else-if="mode === 'dispatch'" :id="routeId" @back="goList" /> <master-order-dispatch v-else-if="mode === 'dispatch'" :id="routeId" @back="goList" />
<master-order-detail v-else :id="routeId" /> <master-order-detail v-else-if="mode === 'detail'" :id="routeId" />
<el-dialog v-model="confirm.visible" title="提示" width="400px" <el-dialog v-model="confirm.visible" title="提示" width="400px"
><span>{{ confirm.message }}</span ><span>{{ confirm.message }}</span
><template #footer ><template #footer
@@ -153,6 +153,10 @@ export default {
components: { MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail }, components: { MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail },
data() { data() {
return { return {
// keep-alive $route
// query.mode / query.id
//mode=add id id
routePathLocked: this.$route.path,
searchExpanded: false, searchExpanded: false,
loading: false, loading: false,
records: [], records: [],
@@ -180,10 +184,15 @@ export default {
}; };
}, },
computed: { computed: {
isOwnedRoute() {
return this.$route.path === this.routePathLocked;
},
mode() { mode() {
if (!this.isOwnedRoute) return 'list';
return this.$route.query.mode || 'list'; return this.$route.query.mode || 'list';
}, },
routeId() { routeId() {
if (!this.isOwnedRoute) return '';
return this.$route.query.id; return this.$route.query.id;
}, },
masterEditorTitle() { masterEditorTitle() {
@@ -194,6 +203,7 @@ export default {
'$route.query': { '$route.query': {
immediate: true, immediate: true,
handler() { handler() {
if (!this.isOwnedRoute) return;
this.syncTagTitle(); this.syncTagTitle();
if (this.mode === 'list') this.load(); if (this.mode === 'list') this.load();
}, },
@@ -0,0 +1,149 @@
<template>
<div ref="page" class="project-apply-public-view">
<project-apply ref="project" />
</div>
</template>
<script>
import ProjectApply from './project-apply.vue';
import { postMkPublicProcessMessage } from '@/api/mk-process';
export default {
name: 'ProjectApplyPublicView',
components: {
ProjectApply,
},
created() {
this.handleIframeHeight = () => this.sendIframeHeight();
this.handleProcessMessage = event => this.onProcessMessage(event);
document.addEventListener('DOMContentLoaded', this.handleIframeHeight, false);
window.addEventListener('message', this.handleProcessMessage);
},
mounted() {
document.documentElement.classList.add('mk-iframe-page');
document.body.classList.add('mk-iframe-page');
const app = document.getElementById('app');
if (app) app.classList.add('mk-iframe-page');
this.handleIframeHeight();
window.addEventListener('load', this.handleIframeHeight);
this.mkHeightTimers = [300, 800, 1600].map(delay => setTimeout(this.handleIframeHeight, delay));
if (typeof ResizeObserver === 'undefined') return;
this.mkHeightObserver = new ResizeObserver(() => this.handleIframeHeight());
this.$nextTick(() => {
if (this.$refs.page) this.mkHeightObserver.observe(this.$refs.page);
this.mkHeightObserver.observe(document.body);
});
},
beforeUnmount() {
document.removeEventListener('DOMContentLoaded', this.handleIframeHeight, false);
window.removeEventListener('load', this.handleIframeHeight);
window.removeEventListener('message', this.handleProcessMessage);
(this.mkHeightTimers || []).forEach(timer => clearTimeout(timer));
if (this.mkHeightObserver) {
this.mkHeightObserver.disconnect();
this.mkHeightObserver = null;
}
document.documentElement.classList.remove('mk-iframe-page');
document.body.classList.remove('mk-iframe-page');
const app = document.getElementById('app');
if (app) app.classList.remove('mk-iframe-page');
},
methods: {
sendIframeHeight() {
this.$nextTick(() => {
window.parent.postMessage({ height: document.body.clientHeight }, '*');
});
},
onProcessMessage(event) {
const data = event && event.data;
if (!data || typeof data !== 'object') return;
if (data.height && !data.status && !data.type) return;
if (data.type === 'formValues' || data.type === 'afterSubmit') return;
const formValues = data.formValues;
if (data.status === 'submit') {
this.submitData(formValues);
} else if (data.status === 'save') {
this.saveData(formValues);
}
if (data.type === 'getFormValues') {
window.parent.postMessage({ type: 'formValues', formData: this.buildFormData() }, '*');
}
},
submitData(lbpmFormValues) {
const formData = this.buildFormData();
this.postFormData('submit', lbpmFormValues, formData);
if (!lbpmFormValues) return;
const parameters = Object.assign({}, lbpmFormValues, {
loginName: this.getLoginName(lbpmFormValues),
formInstanceId: this.getFormId(),
subject: formData.subject,
});
window.parent.postMessage({ type: 'afterSubmit', success: true, parameters }, '*');
},
saveData(lbpmFormValues) {
this.postFormData('save', lbpmFormValues, this.buildFormData());
},
postFormData(status, formValues, formData) {
postMkPublicProcessMessage('project-apply', {
status,
formValues: formValues || {},
formData,
}).catch(error => {
console.error('项目公开页提交流程数据失败:', error);
});
},
buildFormData() {
const form = this.getProjectForm();
return {
...form,
subject: form.projectName || form.projectShortName || '',
formInstanceId: this.getFormId(),
};
},
getProjectForm() {
const form = this.$refs.project && this.$refs.project.form;
if (!form) return {};
try {
return JSON.parse(JSON.stringify(form));
} catch (error) {
return { ...form };
}
},
getFormId() {
return this.$route.query.id || this.getProjectForm().id || '';
},
getLoginName(formValues = {}) {
return (
formValues.loginName ||
this.$route.query.loginName ||
this.$route.query.submitIdentity ||
''
);
},
},
};
</script>
<style lang="scss">
html.mk-iframe-page,
html.mk-iframe-page body,
html.mk-iframe-page #app,
html.mk-iframe-page #app.mk-iframe-page {
height: auto !important;
min-height: 100%;
overflow: visible;
}
</style>
<style lang="scss" scoped>
.project-apply-public-view {
min-height: 100%;
padding: 12px 0 24px;
box-sizing: border-box;
background: #f0f2f5;
:deep(.basic-container) {
padding: 0 12px;
}
}
</style>
+149 -78
View File
@@ -1,5 +1,5 @@
<template> <template>
<basic-container class="project-apply-page"> <basic-container class="project-apply-page" :class="{ 'is-public-view': isPublicViewPage }">
<avue-crud <avue-crud
v-if="!isProjectFormPage" v-if="!isProjectFormPage"
:option="tableOption" :option="tableOption"
@@ -196,7 +196,7 @@
<el-form-item label="项目编号" prop="projectCode"> <el-form-item label="项目编号" prop="projectCode">
<el-input <el-input
v-model="form.projectCode" v-model="form.projectCode"
placeholder="请输入" :placeholder="dialogType === 'add' ? '若不填写,系统自动生成' : '请输入'"
:disabled="dialogType === 'edit'" :disabled="dialogType === 'edit'"
/> />
</el-form-item> </el-form-item>
@@ -525,6 +525,11 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="companyNature" label="企业性质" min-width="140" align="center" /> <el-table-column prop="companyNature" label="企业性质" min-width="140" align="center" />
<el-table-column label="是否广西百强" min-width="140" align="center">
<template #default="{ row }">
{{ formatGuangxiTop100(row.guangxiTop100) }}
</template>
</el-table-column>
<el-table-column prop="legalPerson" label="法定代表人" min-width="140" align="center" /> <el-table-column prop="legalPerson" label="法定代表人" min-width="140" align="center" />
<el-table-column <el-table-column
prop="address" prop="address"
@@ -703,6 +708,7 @@
label="变更内容" label="变更内容"
min-width="180" min-width="180"
align="center" align="center"
:show-overflow-tooltip="false"
> >
<template #default="{ row }"> <template #default="{ row }">
<el-tooltip placement="top" :show-after="200"> <el-tooltip placement="top" :show-after="200">
@@ -730,43 +736,10 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
<el-dialog <change-record-detail-dialog
v-model="changeRecordDetailVisible" v-model="changeRecordDetailVisible"
title="变更记录详情" :rows="changeRecordDetailRows"
append-to-body
destroy-on-close
width="1100px"
top="10px"
class="project-change-record-detail-dialog"
>
<div v-if="changeRecordDetail" class="project-change-record-detail-meta">
<span>变更日期{{ changeRecordDetail.changeDate || '-' }}</span>
<span>变更账号{{ changeRecordDetail.handler || '-' }}</span>
</div>
<el-table :data="changeRecordDetailRows" border :show-overflow-tooltip="false">
<el-table-column prop="field" label="变更字段" min-width="180" />
<el-table-column
prop="before"
label="变更前"
min-width="360"
class-name="project-change-record-detail-value"
/> />
<el-table-column
prop="after"
label="变更后"
min-width="500"
class-name="project-change-record-detail-value"
/>
</el-table>
<el-empty
v-if="!changeRecordDetailRows.length"
description="暂无变更内容"
:image-size="60"
/>
<template #footer>
<el-button type="primary" @click="changeRecordDetailVisible = false">关闭</el-button>
</template>
</el-dialog>
<template v-if="isChangeDialog"> <template v-if="isChangeDialog">
<div class="dialog-section-title">变更原因</div> <div class="dialog-section-title">变更原因</div>
@@ -784,15 +757,13 @@
</el-form> </el-form>
<div <div
v-if="!isPublicViewPage"
class="project-apply-dialog__footer" class="project-apply-dialog__footer"
:class="{ 'project-apply-dialog__footer--change': isChangeDialog }" :class="{ 'project-apply-dialog__footer--change': isChangeDialog }"
> >
<template v-if="isChangeDialog"> <template v-if="isChangeDialog">
<el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button> <el-button v-if="!isProjectFormPage" @click="handleCancelProject">取消</el-button>
<el-button v-if="isProjectFormPage" @click="closeProjectForm">取消</el-button> <el-button v-if="isProjectFormPage" @click="closeProjectForm">取消</el-button>
<el-button type="primary" plain :loading="submitLoading" @click="saveChangeProject">
保存
</el-button>
<el-button type="primary" :loading="submitLoading" @click="submitChangeProject"> <el-button type="primary" :loading="submitLoading" @click="submitChangeProject">
提交 提交
</el-button> </el-button>
@@ -909,12 +880,29 @@
/> />
</div> </div>
</el-dialog> </el-dialog>
<el-dialog
v-model="publicCustomerVisible"
title="查看客商档案"
append-to-body
destroy-on-close
width="92%"
top="4vh"
class="project-apply-public-customer-dialog"
>
<customer-archive
v-if="publicCustomerVisible"
:embedded-public-id="publicCustomerId"
/>
</el-dialog>
</basic-container> </basic-container>
</template> </template>
<script> <script>
import { exportBlob } from '@/api/common'; import { exportBlob } from '@/api/common';
import { defineAsyncComponent } from 'vue';
import * as api from '@/api/business/project-apply'; import * as api from '@/api/business/project-apply';
import { getMkPublicDetail } from '@/api/mk-process';
import { import {
getList as getCustomerArchiveList, getList as getCustomerArchiveList,
getDetail as getCustomerArchiveDetail, getDetail as getCustomerArchiveDetail,
@@ -929,6 +917,7 @@ import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { ElImageViewer } from 'element-plus'; import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue'; import { OpenFileViewer } from '@open-file-viewer/vue';
import PdfPreview from '@/components/pdf-preview/main.vue'; import PdfPreview from '@/components/pdf-preview/main.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
import { import {
fallbackPlugin, fallbackPlugin,
imagePlugin, imagePlugin,
@@ -1045,7 +1034,10 @@ const majorProjectAttachmentTypeOptions = [
].map(item => ({ label: item, value: item })).concat(otherAttachmentType); ].map(item => ({ label: item, value: item })).concat(otherAttachmentType);
export default { export default {
name: 'ProjectApply',
components: { components: {
CustomerArchive: defineAsyncComponent(() => import('@/views/vehicle/customer-archive.vue')),
ChangeRecordDetailDialog,
ElImageViewer, ElImageViewer,
OpenFileViewer, OpenFileViewer,
PdfPreview, PdfPreview,
@@ -1239,6 +1231,8 @@ export default {
changeRecordDetail: null, changeRecordDetail: null,
changeRecordDetailRows: [], changeRecordDetailRows: [],
userBox: false, userBox: false,
publicCustomerVisible: false,
publicCustomerId: '',
userPickType: '', userPickType: '',
userLoading: false, userLoading: false,
userData: [], userData: [],
@@ -1315,6 +1309,9 @@ export default {
isProjectFormPage() { isProjectFormPage() {
return this.isFormPageInstance; return this.isFormPageInstance;
}, },
isPublicViewPage() {
return this.$route.path === '/business/project-apply/public-view';
},
projectFormContainer() { projectFormContainer() {
// keep-alive // keep-alive
return this.isFormPageInstance ? 'div' : 'el-dialog'; return this.isFormPageInstance ? 'div' : 'el-dialog';
@@ -1361,6 +1358,11 @@ export default {
}, },
}, },
created() { created() {
if (this.isPublicViewPage) {
this.isFormPageInstance = true;
this.openPublicProjectForm();
return;
}
this.loadDeptOptions(); this.loadDeptOptions();
this.loadCargoTypeOptions(); this.loadCargoTypeOptions();
this.loadTransportTypeOptions(); this.loadTransportTypeOptions();
@@ -1384,6 +1386,7 @@ export default {
this.attachmentDocumentPreviewVisible = false; this.attachmentDocumentPreviewVisible = false;
this.attachmentImagePreviewVisible = false; this.attachmentImagePreviewVisible = false;
this.userBox = false; this.userBox = false;
this.publicCustomerVisible = false;
}, },
buildTableOption() { buildTableOption() {
return { return {
@@ -1406,6 +1409,75 @@ export default {
hasPermission(code) { hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false); return this.isAdmin || this.validData(this.permission && this.permission[code], false);
}, },
openPublicProjectForm() {
const id = this.$route.query.id;
this.dialogType = 'view';
this.dialogReadonly = true;
this.projectBox = true;
if (!id) {
this.$message.error('缺少项目ID');
return;
}
getMkPublicDetail('project-apply', id)
.then(res => {
const detail = res.data?.data || {};
this.applyPublicDictOptions(detail);
const displayDetail = { ...detail };
delete displayDetail.cargoTypeOptions;
delete displayDetail.transportTypeOptions;
delete displayDetail.settlementModeOptions;
this.applyProjectDetail(displayDetail);
})
.catch(() => {
this.$message.error('项目信息加载失败');
});
},
applyPublicDictOptions(detail = {}) {
this.cargoTypeOptions = this.resolvePublicDictOptions(
detail.cargoTypeOptions,
detail.cargoType
);
this.transportTypeOptions = this.resolvePublicDictOptions(
detail.transportTypeOptions,
detail.transportType
);
this.settlementModeOptions = this.resolvePublicDictOptions(
detail.settlementModeOptions,
detail.settlementMode
);
if (detail.businessDeptId) {
const label = detail.businessDeptName || String(detail.businessDeptId);
this.businessDeptTreeOptions = [{ label, value: detail.businessDeptId }];
this.deptOptions = [{ label, rawLabel: label, value: detail.businessDeptId }];
}
if (detail.undertakeDeptId) {
this.platformCompanyOptions = [
{
label: detail.undertakeDeptName || String(detail.undertakeDeptId),
value: detail.undertakeDeptId,
},
];
}
},
resolvePublicDictOptions(options, currentValue) {
const list = Array.isArray(options)
? options
.filter(item => item && item.value !== undefined && item.value !== null && item.value !== '')
.map(item => ({
label: item.label || String(item.value),
value: item.value,
}))
: [];
if (
currentValue !== undefined &&
currentValue !== null &&
currentValue !== '' &&
!list.some(item => String(item.value) === String(currentValue))
) {
list.unshift({ label: String(currentValue), value: currentValue });
}
return list;
},
openProjectFormPage() { openProjectFormPage() {
const type = this.$route.query.mode || 'add'; const type = this.$route.query.mode || 'add';
const id = this.$route.query.id; const id = this.$route.query.id;
@@ -1420,6 +1492,7 @@ export default {
this.fillDefaultUsers(); this.fillDefaultUsers();
return; return;
} }
if (!id) return;
this.api.getDetail(id).then(res => { this.api.getDetail(id).then(res => {
const detail = res.data.data || {}; const detail = res.data.data || {};
const isChange = type === 'change'; const isChange = type === 'change';
@@ -1832,13 +1905,7 @@ export default {
this.closeProjectForm(); this.closeProjectForm();
}); });
}, },
saveChangeProject() {
this.submitChangeForm(false);
},
submitChangeProject() { submitChangeProject() {
this.submitChangeForm(true);
},
submitChangeForm(needSubmit) {
this.$refs.projectForm.validate(valid => { this.$refs.projectForm.validate(valid => {
if (!valid) { if (!valid) {
this.handleValidateFail(); this.handleValidateFail();
@@ -1846,19 +1913,12 @@ export default {
} }
if (!this.validateAttachmentFileTypes()) return; if (!this.validateAttachmentFileTypes()) return;
this.submitLoading = true; this.submitLoading = true;
const request = needSubmit ? this.api.submitChange : this.api.saveChange; // 稿
request(this.normalizeSubmitForm({ includeChangeType: true })) this.api
.submitChange(this.normalizeSubmitForm({ includeChangeType: true }))
.then(res => { .then(res => {
if (res.data?.success === false || res.data?.data === false) { if (res.data?.success === false || res.data?.data === false) {
this.$message.error( this.$message.error(res.data?.msg || '提交失败,请联系管理员');
res.data?.msg || `${needSubmit ? '提交' : '保存'}失败,请联系管理员`
);
return;
}
if (!needSubmit) {
this.$message.success('保存成功!');
this.closeProjectForm();
this.onLoad(this.page, this.query);
return; return;
} }
const id = this.form.id; const id = this.form.id;
@@ -2012,6 +2072,11 @@ export default {
this.$message.warning('客商档案 ID 为空,无法打开'); this.$message.warning('客商档案 ID 为空,无法打开');
return; return;
} }
if (this.isPublicViewPage) {
this.publicCustomerId = String(id);
this.publicCustomerVisible = true;
return;
}
this.$router.push({ this.$router.push({
path: '/vehicle/customer-archive/form', path: '/vehicle/customer-archive/form',
query: { id: String(id), name: '查看客商档案', view: '1' }, query: { id: String(id), name: '查看客商档案', view: '1' },
@@ -2046,6 +2111,11 @@ export default {
normalizeReadonlyAmount(value) { normalizeReadonlyAmount(value) {
return this.normalizeOptionalSentinel(value); return this.normalizeOptionalSentinel(value);
}, },
formatGuangxiTop100(value) {
if (value === 1 || value === '1') return '是';
if (value === 0 || value === '0') return '否';
return value || '';
},
normalizeOptionalSentinel(value, emptyValue = '') { normalizeOptionalSentinel(value, emptyValue = '') {
return value === null || value === undefined || value === '' || Number(value) === -1 return value === null || value === undefined || value === '' || Number(value) === -1
? emptyValue ? emptyValue
@@ -2452,6 +2522,8 @@ export default {
id: item.id, id: item.id,
credit: item.maxCreditLimit || item.applyCreditLimit || '', credit: item.maxCreditLimit || item.applyCreditLimit || '',
companyNature: item.customerNature || '', companyNature: item.customerNature || '',
guangxiTop100:
item.guangxiTop100 === 0 || item.guangxiTop100 === 1 ? item.guangxiTop100 : null,
legalPerson: item.legalPerson || '', legalPerson: item.legalPerson || '',
address: item.registeredAddress || '', address: item.registeredAddress || '',
contact: defaultContact.contactName || item.principal || '', contact: defaultContact.contactName || item.principal || '',
@@ -2594,6 +2666,12 @@ export default {
this.$message.warning('项目ID为空,无法查看变更详情'); this.$message.warning('项目ID为空,无法查看变更详情');
return; return;
} }
if (this.isPublicViewPage) {
this.changeRecordDetail = { ...row };
this.changeRecordDetailRows = this.buildChangeRecordDetailRows(this.changeRecordDetail);
this.changeRecordDetailVisible = true;
return;
}
try { try {
if (!this.transportTypeOptions.length) { if (!this.transportTypeOptions.length) {
await this.loadTransportTypeOptions(); await this.loadTransportTypeOptions();
@@ -2650,6 +2728,12 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.project-apply-page.is-public-view {
:deep(.el-link) {
pointer-events: auto;
}
}
.project-apply-form { .project-apply-form {
padding: 0; padding: 0;
@@ -2882,27 +2966,6 @@ export default {
} }
} }
:deep(.project-change-record-detail-dialog .el-dialog__body) {
max-height: 65vh;
overflow: auto;
}
:deep(.project-change-record-detail-dialog .project-change-record-detail-value .cell) {
white-space: pre-wrap;
word-break: break-word;
overflow: visible;
text-overflow: clip;
line-height: 1.6;
}
.project-change-record-detail-meta {
display: flex;
gap: 32px;
margin-bottom: 16px;
color: #606266;
font-size: 14px;
}
:global(.project-apply-dialog .el-dialog__body) { :global(.project-apply-dialog .el-dialog__body) {
max-height: 76vh; max-height: 76vh;
overflow-y: auto; overflow-y: auto;
@@ -3019,3 +3082,11 @@ export default {
} }
} }
</style> </style>
<style lang="scss">
.project-apply-public-customer-dialog .el-dialog__body {
max-height: 78vh;
overflow: auto;
padding-top: 8px;
}
</style>
+114 -22
View File
@@ -99,13 +99,25 @@
<el-table :data="attachmentRows" border @selection-change="selectedAttachments = $event"> <el-table :data="attachmentRows" border @selection-change="selectedAttachments = $event">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" /> <el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="240" show-overflow-tooltip> <el-table-column label="文件名" min-width="240" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)"> <el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }} {{ attachmentName(row) }}
</el-link> </el-link>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="附件描述" min-width="220">
<template #default="{ row }">
<span v-if="dialogReadonly">{{ row.description || '-' }}</span>
<el-input
v-else
v-model="row.description"
maxlength="200"
placeholder="请输入"
@input="syncAttachmentsJson"
/>
</template>
</el-table-column>
<el-table-column label="文件大小" width="120" align="center"> <el-table-column label="文件大小" width="120" align="center">
<template #default="{ row }">{{ formatFileSize(row.size) }}</template> <template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column> </el-table-column>
@@ -172,7 +184,7 @@
title="查看临时额度申请" title="查看临时额度申请"
append-to-body append-to-body
destroy-on-close destroy-on-close
width="96%" :width="formDialogWidth"
class="temporary-credit-limit-dialog temporary-credit-limit-detail-dialog" class="temporary-credit-limit-dialog temporary-credit-limit-detail-dialog"
> >
<div v-loading="detailLoading" class="business-crud-page__detail-content"> <div v-loading="detailLoading" class="business-crud-page__detail-content">
@@ -204,13 +216,16 @@
</div> </div>
<el-table :data="attachmentRows" empty-text="暂无附件"> <el-table :data="attachmentRows" empty-text="暂无附件">
<el-table-column type="index" label="序号" width="70" align="center" /> <el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="240" show-overflow-tooltip> <el-table-column label="文件名" min-width="240" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)"> <el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }} {{ attachmentName(row) }}
</el-link> </el-link>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="附件描述" min-width="220" show-overflow-tooltip>
<template #default="{ row }">{{ row.description || '-' }}</template>
</el-table-column>
<el-table-column label="文件大小" width="120" align="center"> <el-table-column label="文件大小" width="120" align="center">
<template #default="{ row }">{{ formatFileSize(row.size) }}</template> <template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column> </el-table-column>
@@ -400,6 +415,13 @@ export default {
detailSections() { detailSections() {
return this.config.detailSections || []; return this.config.detailSections || [];
}, },
formDialogWidth() {
const width = this.tableOption?.dialogWidth ?? option.dialogWidth ?? 1100;
if (typeof width === 'number' || /^\d+$/.test(String(width))) {
return `${width}px`;
}
return String(width);
},
isAdmin() { isAdmin() {
const authority = this.userInfo?.authority; const authority = this.userInfo?.authority;
return Array.isArray(authority) return Array.isArray(authority)
@@ -595,7 +617,12 @@ export default {
return value; return value;
}, },
applyDetail(detail) { applyDetail(detail) {
this.form = { ...detail }; this.form = {
...detail,
projectFundLimit: this.blankSentinelAmount(detail.projectFundLimit),
usedFundLimit: this.blankSentinelAmount(detail.usedFundLimit),
remainingFundLimit: this.blankSentinelAmount(detail.remainingFundLimit),
};
this.selectedProjectId = detail.projectId || ''; this.selectedProjectId = detail.projectId || '';
this.attachmentRows = this.parseJsonArray(detail.attachmentsJson); this.attachmentRows = this.parseJsonArray(detail.attachmentsJson);
this.selectedAttachments = []; this.selectedAttachments = [];
@@ -605,6 +632,9 @@ export default {
const { projectQuotaInfoTitle, temporaryCreditInfoTitle, ...payload } = row || {}; const { projectQuotaInfoTitle, temporaryCreditInfoTitle, ...payload } = row || {};
return { return {
...payload, ...payload,
projectFundLimit: this.toOptionalAmount(payload.projectFundLimit),
usedFundLimit: this.toOptionalAmount(payload.usedFundLimit),
remainingFundLimit: this.toOptionalAmount(payload.remainingFundLimit),
applyLimit: payload.applyLimit === '' ? '' : Number(payload.applyLimit), applyLimit: payload.applyLimit === '' ? '' : Number(payload.applyLimit),
attachmentsJson: JSON.stringify(this.attachmentRows), attachmentsJson: JSON.stringify(this.attachmentRows),
}; };
@@ -750,8 +780,8 @@ export default {
projectName: project.projectName || '', projectName: project.projectName || '',
projectCode: project.projectCode || '', projectCode: project.projectCode || '',
undertakeDeptName: project.undertakeDeptName || '', undertakeDeptName: project.undertakeDeptName || '',
projectFundLimit: project.fundLimit || project.projectFundLimit || 0, projectFundLimit: this.blankSentinelAmount(project.fundLimit ?? project.projectFundLimit),
usedFundLimit: project.usedFundLimit || 0, usedFundLimit: this.blankSentinelAmount(project.usedFundLimit),
}); });
this.form.remainingFundLimit = this.calculateRemainingFundLimit(); this.form.remainingFundLimit = this.calculateRemainingFundLimit();
getProjectDetail(project.id).then(res => { getProjectDetail(project.id).then(res => {
@@ -759,18 +789,32 @@ export default {
Object.assign(this.form, { Object.assign(this.form, {
projectCode: detail.projectCode || this.form.projectCode, projectCode: detail.projectCode || this.form.projectCode,
undertakeDeptName: detail.undertakeDeptName || this.form.undertakeDeptName, undertakeDeptName: detail.undertakeDeptName || this.form.undertakeDeptName,
projectFundLimit: projectFundLimit: this.blankSentinelAmount(
detail.fundLimit ?? detail.projectFundLimit ?? this.form.projectFundLimit, detail.fundLimit ?? detail.projectFundLimit ?? this.form.projectFundLimit
usedFundLimit: detail.usedFundLimit ?? this.form.usedFundLimit, ),
usedFundLimit: this.blankSentinelAmount(
detail.usedFundLimit ?? this.form.usedFundLimit
),
}); });
this.form.remainingFundLimit = this.calculateRemainingFundLimit(); this.form.remainingFundLimit = this.calculateRemainingFundLimit();
}); });
}, },
blankSentinelAmount(value) {
if (value === undefined || value === null || value === '') return '';
return Number(value) === -1 ? '' : value;
},
toOptionalAmount(value) {
const amount = this.blankSentinelAmount(value);
return amount === '' ? null : amount;
},
calculateRemainingFundLimit() { calculateRemainingFundLimit() {
const total = Number(this.form.projectFundLimit); const total = this.blankSentinelAmount(this.form.projectFundLimit);
const used = Number(this.form.usedFundLimit); const used = this.blankSentinelAmount(this.form.usedFundLimit);
if (!Number.isFinite(total) || !Number.isFinite(used)) return ''; if (total === '' || used === '') return '';
return Math.round((total - used + Number.EPSILON) * 100) / 100; const totalAmount = Number(total);
const usedAmount = Number(used);
if (!Number.isFinite(totalAmount) || !Number.isFinite(usedAmount)) return '';
return Math.round((totalAmount - usedAmount + Number.EPSILON) * 100) / 100;
}, },
handleAmountInput(value) { handleAmountInput(value) {
const text = String(value || '').replace(/[^\d.]/g, ''); const text = String(value || '').replace(/[^\d.]/g, '');
@@ -783,24 +827,37 @@ export default {
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss'); const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.attachmentRows = (list || []).map(item => ({ this.attachmentRows = (list || []).map(item => ({
...item, ...item,
description: item.description || '',
uploadUserName: item.uploadUserName || uploadUserName, uploadUserName: item.uploadUserName || uploadUserName,
uploadTime: item.uploadTime || uploadTime, uploadTime: item.uploadTime || uploadTime,
})); }));
this.form.attachmentsJson = JSON.stringify(this.attachmentRows); this.syncAttachmentsJson();
}, },
removeAttachment(index) { removeAttachment(index) {
this.attachmentRows.splice(index, 1); this.attachmentRows.splice(index, 1);
this.form.attachmentsJson = JSON.stringify(this.attachmentRows); this.syncAttachmentsJson();
},
syncAttachmentsJson() {
this.form.attachmentsJson = JSON.stringify(this.attachmentRows || []);
}, },
parseJsonArray(value) { parseJsonArray(value) {
if (Array.isArray(value)) return value; let list = [];
if (!value) return []; if (Array.isArray(value)) {
list = value;
} else if (!value) {
return [];
} else {
try { try {
const data = JSON.parse(value); const data = JSON.parse(value);
return Array.isArray(data) ? data : []; list = Array.isArray(data) ? data : [];
} catch (error) { } catch (error) {
return []; return [];
} }
}
return list.map(item => ({
...item,
description: item?.description || '',
}));
}, },
attachmentName(row = {}) { attachmentName(row = {}) {
return row.originalName || row.name || row.fileName || '附件'; return row.originalName || row.name || row.fileName || '附件';
@@ -906,7 +963,8 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.temporary-credit-limit-page { .temporary-credit-limit-page {
&__field { &__field {
width: 100%; width: 240px;
max-width: 100%;
} }
&__attachment-head { &__attachment-head {
@@ -993,7 +1051,7 @@ export default {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
width: 100%; width: 100%;
padding: 14px 16px 4px; padding: 14px 0 4px;
margin-bottom: 0; margin-bottom: 0;
color: #303133; color: #303133;
font-size: 15px; font-size: 15px;
@@ -1086,17 +1144,51 @@ export default {
background: transparent !important; background: transparent !important;
box-shadow: none !important; box-shadow: none !important;
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 16px 8px !important;
border-radius: 0 !important; border-radius: 0 !important;
} }
// /input/select 240px textarea
&:not(.temporary-credit-limit-detail-dialog) {
.el-form-item__content {
min-width: 0;
}
.el-form-item__content > .el-input,
.el-form-item__content > .el-select,
.el-form-item__content > .el-date-editor,
.el-form-item__content > .el-cascader,
.el-form-item__content > div > .el-input,
.el-form-item__content > div > .el-select,
.el-form-item__content > div > .el-date-editor,
.el-form-item__content > div > .el-cascader,
.temporary-credit-limit-page__field {
width: 240px !important;
max-width: 100%;
}
.el-form-item__content > .el-textarea,
.el-form-item__content > div > .el-textarea,
.el-form-item__content .el-textarea {
width: 100% !important;
max-width: 100% !important;
}
.el-date-editor.el-input,
.el-date-editor.el-input__wrapper {
width: 240px !important;
max-width: 100%;
}
}
} }
.temporary-credit-limit-dialog .business-crud-page__detail-content .el-descriptions__label { .temporary-credit-limit-dialog .business-crud-page__detail-content .el-descriptions__label {
width: 180px !important; width: 180px !important;
} }
// // // //Avue setPx '1100px' '1100pxpx' option 1100
.temporary-credit-limit-dialog.el-dialog { .temporary-credit-limit-dialog.el-dialog {
width: 1100px !important;
margin-top: 20px !important; margin-top: 20px !important;
margin-bottom: 20px !important; margin-bottom: 20px !important;
height: auto !important; height: auto !important;
+95 -6
View File
@@ -11,7 +11,7 @@
{{ planData.businessStatusName || '-' }} {{ planData.businessStatusName || '-' }}
</span> </span>
<span class="detail-status-text detail-transport-type"> <span class="detail-status-text detail-transport-type">
{{ planData.transportTypeName || '公路运输' }} {{ transportTypeLabel }}
</span> </span>
</div> </div>
<div class="transport-plan-dispatch-page__summary-grid"> <div class="transport-plan-dispatch-page__summary-grid">
@@ -123,16 +123,20 @@
</el-button> </el-button>
</template> </template>
<el-table <el-table
:data="dispatchList" :data="pagedDispatchList"
border border
height="100%" height="100%"
row-key="id" row-key="id"
class="transport-plan-dispatch-page__table" class="transport-plan-dispatch-page__table"
> >
<el-table-column type="index" label="序号" width="70" align="center" fixed="left" /> <el-table-column label="序号" width="70" align="center" fixed="left">
<template #default="{ $index }">
{{ dispatchRowIndex($index) }}
</template>
</el-table-column>
<el-table-column label="运输方式" min-width="180" align="center" show-overflow-tooltip> <el-table-column label="运输方式" min-width="180" align="center" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
{{ row.transportTypeName || '-' }} {{ formatTransportType(row.transportTypeName || row.transportType) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="carrierType" label="承运类型" min-width="150" align="center" show-overflow-tooltip /> <el-table-column prop="carrierType" label="承运类型" min-width="150" align="center" show-overflow-tooltip />
@@ -177,6 +181,17 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="transport-plan-dispatch-page__pagination">
<el-pagination
v-model:current-page="dispatchPage.currentPage"
v-model:page-size="dispatchPage.pageSize"
:page-sizes="dispatchPage.pageSizes"
layout="total, sizes, prev, pager, next"
:total="dispatchList.length"
@current-change="handleDispatchCurrentChange"
@size-change="handleDispatchSizeChange"
/>
</div>
</section-card> </section-card>
<!-- 底部按钮 --> <!-- 底部按钮 -->
@@ -202,6 +217,7 @@
<script> <script>
import { getDetail, dispatch } from '@/api/business/transport-plan'; import { getDetail, dispatch } from '@/api/business/transport-plan';
import { getDictionary } from '@/api/system/dictbiz';
import SectionCard from '@/components/section-card/main.vue'; import SectionCard from '@/components/section-card/main.vue';
import TransportPlanPage from './components/transport-plan-page.vue'; import TransportPlanPage from './components/transport-plan-page.vue';
import * as api from '@/api/business/transport-plan'; import * as api from '@/api/business/transport-plan';
@@ -209,6 +225,12 @@ import { config, option } from '@/option/business/transport-plan';
const TRANSPORT_PLAN_QUANTITY_UNIT = '吨'; const TRANSPORT_PLAN_QUANTITY_UNIT = '吨';
const normalizeDictOptions = (list = []) =>
(Array.isArray(list) ? list : []).map(item => ({
label: item.dictValue || item.label || '',
value: item.dictKey ?? item.value ?? '',
}));
export default { export default {
name: 'TransportPlanDispatch', name: 'TransportPlanDispatch',
components: { components: {
@@ -223,6 +245,12 @@ export default {
dispatchList: [], dispatchList: [],
attachmentList: [], attachmentList: [],
transportPlanPageVisible: false, transportPlanPageVisible: false,
transportTypeOptions: [],
dispatchPage: {
currentPage: 1,
pageSize: 10,
pageSizes: [10, 20, 50, 100],
},
api, api,
config, config,
option, option,
@@ -232,6 +260,15 @@ export default {
planId() { planId() {
return this.$route.query.planId; return this.$route.query.planId;
}, },
transportTypeLabel() {
return this.formatTransportType(
this.planData.transportTypeName || this.planData.transportType
);
},
pagedDispatchList() {
const start = (this.dispatchPage.currentPage - 1) * this.dispatchPage.pageSize;
return this.dispatchList.slice(start, start + this.dispatchPage.pageSize);
},
statusTextClass() { statusTextClass() {
const status = this.planData.businessStatus; const status = this.planData.businessStatus;
if (status === 2 || status === '2') return 'status-text-success'; if (status === 2 || status === '2') return 'status-text-success';
@@ -306,13 +343,34 @@ export default {
}, },
}, },
created() { created() {
this.loadTransportTypeOptions().finally(() => {
this.loadPlanData(); this.loadPlanData();
});
}, },
mounted() { mounted() {
// transport-plan-page // transport-plan-page
this.transportPlanPageVisible = true; this.transportPlanPageVisible = true;
}, },
methods: { methods: {
loadTransportTypeOptions() {
return getDictionary({ code: 'transport_type' })
.then(res => {
this.transportTypeOptions = normalizeDictOptions(res.data?.data || []);
})
.catch(() => {
this.transportTypeOptions = [];
});
},
formatTransportType(value) {
const transportType = String(value || '').trim();
if (!transportType) return '-';
//
if (/[\u4e00-\u9fff]/.test(transportType)) return transportType;
const item = this.transportTypeOptions.find(
option => String(option.value ?? '').toLowerCase() === transportType.toLowerCase()
);
return item?.label || transportType;
},
loadPlanData() { loadPlanData() {
if (!this.planId) { if (!this.planId) {
this.$message.error('缺少计划ID参数'); this.$message.error('缺少计划ID参数');
@@ -327,6 +385,7 @@ export default {
this.planData = res.data.data || {}; this.planData = res.data.data || {};
this.attachmentList = this.parseAttachments(this.planData.attachmentsJson); this.attachmentList = this.parseAttachments(this.planData.attachmentsJson);
this.dispatchList = this.parseDispatchList(this.planData); this.dispatchList = this.parseDispatchList(this.planData);
this.dispatchPage.currentPage = 1;
} else { } else {
this.$message.error(res.data?.msg || '加载数据失败'); this.$message.error(res.data?.msg || '加载数据失败');
} }
@@ -448,8 +507,30 @@ export default {
} }
this.openPageDispatchItemDialog(-1); this.openPageDispatchItemDialog(-1);
}, },
dispatchRowIndex(index) {
return (this.dispatchPage.currentPage - 1) * this.dispatchPage.pageSize + index + 1;
},
resolveDispatchGlobalIndex(pageIndex) {
return (this.dispatchPage.currentPage - 1) * this.dispatchPage.pageSize + pageIndex;
},
syncDispatchPage() {
const maxPage = Math.max(
1,
Math.ceil(this.dispatchList.length / this.dispatchPage.pageSize) || 1
);
if (this.dispatchPage.currentPage > maxPage) {
this.dispatchPage.currentPage = maxPage;
}
},
handleDispatchCurrentChange(currentPage) {
this.dispatchPage.currentPage = currentPage;
},
handleDispatchSizeChange(pageSize) {
this.dispatchPage.pageSize = pageSize;
this.dispatchPage.currentPage = 1;
},
handleEdit(row, index) { handleEdit(row, index) {
this.openPageDispatchItemDialog(index, row); this.openPageDispatchItemDialog(this.resolveDispatchGlobalIndex(index), row);
}, },
openPageDispatchItemDialog(index = -1, row) { openPageDispatchItemDialog(index = -1, row) {
this.$nextTick(() => { this.$nextTick(() => {
@@ -469,6 +550,7 @@ export default {
originalSave.apply(component, args); originalSave.apply(component, args);
if (!component.dispatchItemBox) { if (!component.dispatchItemBox) {
this.dispatchList = [...(component.dispatchRows || [])]; this.dispatchList = [...(component.dispatchRows || [])];
this.syncDispatchPage();
} }
}; };
component.__dispatchListSaveSynced = true; component.__dispatchListSaveSynced = true;
@@ -479,7 +561,8 @@ export default {
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}).then(() => { }).then(() => {
this.dispatchList.splice(index, 1); this.dispatchList.splice(this.resolveDispatchGlobalIndex(index), 1);
this.syncDispatchPage();
this.$message.success('删除成功'); this.$message.success('删除成功');
}); });
}, },
@@ -782,6 +865,12 @@ export default {
flex: 1; flex: 1;
} }
&__pagination {
display: flex;
justify-content: flex-end;
padding-top: 12px;
}
&__actions { &__actions {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -0,0 +1,38 @@
<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>
+155
View File
@@ -0,0 +1,155 @@
<template>
<div ref="page" class="mk-public-shell">
<slot />
</div>
</template>
<script>
import { postMkPublicProcessMessage } from '@/api/mk-process';
export default {
name: 'MkPublicShell',
props: {
bizType: {
type: String,
required: true,
},
getForm: {
type: Function,
default: null,
},
},
created() {
this.handleIframeHeight = () => this.sendIframeHeight();
this.handleProcessMessage = event => this.onProcessMessage(event);
document.addEventListener('DOMContentLoaded', this.handleIframeHeight, false);
window.addEventListener('message', this.handleProcessMessage);
},
mounted() {
document.documentElement.classList.add('mk-iframe-page');
document.body.classList.add('mk-iframe-page');
const app = document.getElementById('app');
if (app) app.classList.add('mk-iframe-page');
this.handleIframeHeight();
window.addEventListener('load', this.handleIframeHeight);
this.mkHeightTimers = [300, 800, 1600].map(delay => setTimeout(this.handleIframeHeight, delay));
if (typeof ResizeObserver === 'undefined') return;
this.mkHeightObserver = new ResizeObserver(() => this.handleIframeHeight());
this.$nextTick(() => {
if (this.$refs.page) this.mkHeightObserver.observe(this.$refs.page);
this.mkHeightObserver.observe(document.body);
});
},
beforeUnmount() {
document.removeEventListener('DOMContentLoaded', this.handleIframeHeight, false);
window.removeEventListener('load', this.handleIframeHeight);
window.removeEventListener('message', this.handleProcessMessage);
(this.mkHeightTimers || []).forEach(timer => clearTimeout(timer));
if (this.mkHeightObserver) {
this.mkHeightObserver.disconnect();
this.mkHeightObserver = null;
}
document.documentElement.classList.remove('mk-iframe-page');
document.body.classList.remove('mk-iframe-page');
const app = document.getElementById('app');
if (app) app.classList.remove('mk-iframe-page');
},
methods: {
sendIframeHeight() {
this.$nextTick(() => {
window.parent.postMessage({ height: document.body.clientHeight }, '*');
});
},
onProcessMessage(event) {
const data = event && event.data;
if (!data || typeof data !== 'object') return;
if (data.height && !data.status && !data.type) return;
if (data.type === 'formValues' || data.type === 'afterSubmit') return;
const formValues = data.formValues;
if (data.status === 'submit') {
this.submitData(formValues);
} else if (data.status === 'save') {
this.saveData(formValues);
}
if (data.type === 'getFormValues') {
window.parent.postMessage({ type: 'formValues', formData: this.buildFormData() }, '*');
}
},
submitData(lbpmFormValues) {
const formData = this.buildFormData();
this.postFormData('submit', lbpmFormValues, formData);
if (!lbpmFormValues) return;
const parameters = Object.assign({}, lbpmFormValues, {
loginName: this.getLoginName(lbpmFormValues),
formInstanceId: this.getFormId(),
subject: formData.subject,
});
window.parent.postMessage({ type: 'afterSubmit', success: true, parameters }, '*');
},
saveData(lbpmFormValues) {
this.postFormData('save', lbpmFormValues, this.buildFormData());
},
postFormData(status, formValues, formData) {
postMkPublicProcessMessage(this.bizType, {
status,
formValues: formValues || {},
formData,
}).catch(error => {
console.error('公开页提交流程数据失败:', error);
});
},
buildFormData() {
const form = this.readForm();
return {
...form,
subject: form.subject || '',
formInstanceId: this.getFormId(),
};
},
readForm() {
const form = typeof this.getForm === 'function' ? this.getForm() : {};
if (!form) return {};
try {
return JSON.parse(JSON.stringify(form));
} catch (error) {
return { ...form };
}
},
getFormId() {
return this.$route.query.id || this.readForm().id || '';
},
getLoginName(formValues = {}) {
return (
formValues.loginName ||
this.$route.query.loginName ||
this.$route.query.submitIdentity ||
''
);
},
},
};
</script>
<style lang="scss">
html.mk-iframe-page,
html.mk-iframe-page body,
html.mk-iframe-page #app,
html.mk-iframe-page #app.mk-iframe-page {
height: auto !important;
min-height: 100%;
overflow: visible;
}
</style>
<style lang="scss" scoped>
.mk-public-shell {
min-height: 100%;
padding: 12px 0 24px;
box-sizing: border-box;
background: #f0f2f5;
:deep(.basic-container) {
padding: 0 12px;
}
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
<template>
<mk-public-shell biz-type="payment-application" :get-form="getForm">
<payment-application-form ref="page" />
</mk-public-shell>
</template>
<script>
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
import PaymentApplicationForm from '@/views/payment/payment-application-form.vue';
export default {
name: 'PaymentApplicationPublicView',
components: { MkPublicShell, PaymentApplicationForm },
methods: {
getForm() {
const form = this.$refs.page?.form || {};
return { ...form, subject: form.paymentNo || form.projectName || '' };
},
},
};
</script>
+1 -1
View File
@@ -326,7 +326,7 @@ export default {
openProjectAdvance() { openProjectAdvance() {
this.$router.push({ this.$router.push({
path: '/payment/payment-application/form', path: '/payment/payment-application/form',
query: { mode: 'add', paymentType: 'project_advance' }, query: { mode: 'add', paymentType: 'progress_advance' },
}); });
}, },
openEdit(row) { openEdit(row) {
@@ -211,8 +211,15 @@
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<el-table v-show="!detailCollapsed" :data="filteredDetails" border> <el-table v-show="!detailCollapsed" :data="pagedDetails" border>
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" /> <el-table-column
type="index"
label="序号"
width="64"
fixed="left"
align="center"
:index="detailIndexMethod"
/>
<el-table-column <el-table-column
v-for="column in detailTableColumns" v-for="column in detailTableColumns"
:key="column.prop" :key="column.prop"
@@ -245,6 +252,16 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div v-show="!detailCollapsed" class="formal-editor__pagination">
<el-pagination
v-model:current-page="detailPage.current"
v-model:page-size="detailPage.size"
:total="filteredDetails.length"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleDetailPageSizeChange"
/>
</div>
</section-card> </section-card>
<section-card> <section-card>
@@ -623,7 +640,7 @@
>提交</el-button >提交</el-button
> >
</template> </template>
<div v-if="pageMode" class="formal-editor__page-actions"> <div v-if="pageMode && !publicMode" class="formal-editor__page-actions">
<el-button @click="visible = false">取消</el-button> <el-button @click="visible = false">取消</el-button>
<el-button <el-button
v-if="editable && !recordId" v-if="editable && !recordId"
@@ -929,6 +946,7 @@
</template> </template>
<script> <script>
import { getMkPublicDetail } from '@/api/mk-process';
import { import {
adjustDetail, adjustDetail,
getCandidateDetails, getCandidateDetails,
@@ -977,6 +995,10 @@ export default {
type: Object, type: Object,
default: null, default: null,
}, },
publicMode: {
type: Boolean,
default: false,
},
}, },
emits: ['update:modelValue', 'success'], emits: ['update:modelValue', 'success'],
data() { data() {
@@ -1088,6 +1110,10 @@ export default {
batchNo: '', batchNo: '',
cargoName: '', cargoName: '',
}, },
detailPage: {
current: 1,
size: 10,
},
}; };
}, },
computed: { computed: {
@@ -1135,6 +1161,13 @@ export default {
}) })
); );
}, },
pagedDetails() {
const list = this.filteredDetails;
const size = Number(this.detailPage.size) || 10;
const current = Math.max(Number(this.detailPage.current) || 1, 1);
const start = (current - 1) * size;
return list.slice(start, start + size);
},
missingAttachmentTypeText() { missingAttachmentTypeText() {
const missing = this.getMissingAttachmentTypes(); const missing = this.getMissingAttachmentTypes();
return missing.length ? `未上传:${missing.join('、')}` : ''; return missing.length ? `未上传:${missing.join('、')}` : '';
@@ -1159,6 +1192,16 @@ export default {
if (value) this.initialize(); if (value) this.initialize();
}, },
}, },
filteredDetails: {
handler(list) {
const size = Number(this.detailPage.size) || 10;
const maxPage = Math.max(1, Math.ceil((list?.length || 0) / size) || 1);
if (this.detailPage.current > maxPage) {
this.detailPage.current = maxPage;
}
},
immediate: true,
},
summaryTotal(value) { summaryTotal(value) {
this.form.settlementAmount = Number(value || 0).toFixed(2); this.form.settlementAmount = Number(value || 0).toFixed(2);
this.form.localSettlementAmount = ( this.form.localSettlementAmount = (
@@ -1173,7 +1216,9 @@ export default {
}, },
methods: { methods: {
async loadFormalDetail(id) { async loadFormalDetail(id) {
const response = await getDetail(id); const response = this.publicMode
? await getMkPublicDetail('formal-settlement', id)
: await getDetail(id);
const data = this.unwrapData(response); const data = this.unwrapData(response);
this.form = { this.form = {
...createFormalSettlementForm(), ...createFormalSettlementForm(),
@@ -1185,12 +1230,13 @@ export default {
}; };
this.sources = data.sources || []; this.sources = data.sources || [];
this.details = data.details || []; this.details = data.details || [];
this.detailPage.current = 1;
this.summaryFees = data.summaryFees || []; this.summaryFees = data.summaryFees || [];
// //
this.detailFeeSnapshots = {}; this.detailFeeSnapshots = {};
this.pendingAdjustments = {}; this.pendingAdjustments = {};
this.paymentApplications = data.paymentApplications || []; this.paymentApplications = data.paymentApplications || [];
if (this.readonly && this.form.settlementType === 'receivable') { if (this.readonly && this.form.settlementType === 'receivable' && !this.publicMode) {
this.receiptClaims = this.unwrapData(await getReceiptClaims(id)) || []; this.receiptClaims = this.unwrapData(await getReceiptClaims(id)) || [];
} }
this.adjustments = data.adjustments || []; this.adjustments = data.adjustments || [];
@@ -1237,7 +1283,9 @@ export default {
rule: null, rule: null,
}; };
this.detailCollapsed = false; this.detailCollapsed = false;
this.detailPage = { current: 1, size: 10 };
this.resetDetailQuery(false); this.resetDetailQuery(false);
if (!this.publicMode) {
await Promise.all([ await Promise.all([
this.loadAllContracts(), this.loadAllContracts(),
this.loadFeeOptions(), this.loadFeeOptions(),
@@ -1245,6 +1293,7 @@ export default {
this.loadAttachmentTypeOptions(), this.loadAttachmentTypeOptions(),
this.loadTransportTypeOptions(), this.loadTransportTypeOptions(),
]); ]);
}
if (!this.recordId) { if (!this.recordId) {
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD'); this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
if (this.initialData) await this.applyInitialData(); if (this.initialData) await this.applyInitialData();
@@ -1328,6 +1377,7 @@ export default {
settlementAmountTax: settlementAmountTax:
row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount, row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount,
})); }));
this.detailPage.current = 1;
this.form.sourceDetailIds = this.details.map(item => item.sourceDetailId); this.form.sourceDetailIds = this.details.map(item => item.sourceDetailId);
await this.refreshSummaryFees(); await this.refreshSummaryFees();
}, },
@@ -1373,6 +1423,7 @@ export default {
sourcePreSettlementId: settlement.id, sourcePreSettlementId: settlement.id,
})) }))
); );
this.detailPage.current = 1;
const summaryMap = new Map(); const summaryMap = new Map();
settlements settlements
.flatMap(settlement => settlement.summaryFees || []) .flatMap(settlement => settlement.summaryFees || [])
@@ -1601,6 +1652,7 @@ export default {
...this.details.filter(item => item.formalSettlementId), ...this.details.filter(item => item.formalSettlementId),
...existing.values(), ...existing.values(),
]; ];
this.detailPage.current = 1;
this.form.sourceDetailIds = [...existing.values()].map(item => item.sourceDetailId); this.form.sourceDetailIds = [...existing.values()].map(item => item.sourceDetailId);
this.detailCandidate.loading = true; this.detailCandidate.loading = true;
try { try {
@@ -1819,6 +1871,9 @@ export default {
return sourceDetailId ? `src:${sourceDetailId}` : ''; return sourceDetailId ? `src:${sourceDetailId}` : '';
}, },
async loadDetailFeeRows(detail) { async loadDetailFeeRows(detail) {
if (this.publicMode) {
return [this.normalizeAdjustRow(detail)];
}
if (detail.formalSettlementId && detail.id) { if (detail.formalSettlementId && detail.id) {
const rows = this.unwrapData(await getDetailFees(detail.id)) || []; const rows = this.unwrapData(await getDetailFees(detail.id)) || [];
return rows.map(item => this.normalizeAdjustRow(item)); return rows.map(item => this.normalizeAdjustRow(item));
@@ -1920,12 +1975,19 @@ export default {
}, },
handleDetailQuery() { handleDetailQuery() {
this.appliedDetailQuery = { ...this.detailQuery }; this.appliedDetailQuery = { ...this.detailQuery };
this.detailPage.current = 1;
}, },
resetDetailQuery(apply = true) { resetDetailQuery(apply = true) {
this.detailQuery = { documentNo: '', waybillNo: '', batchNo: '', cargoName: '' }; this.detailQuery = { documentNo: '', waybillNo: '', batchNo: '', cargoName: '' };
if (apply) this.handleDetailQuery(); if (apply) this.handleDetailQuery();
else this.appliedDetailQuery = { ...this.detailQuery }; else this.appliedDetailQuery = { ...this.detailQuery };
}, },
handleDetailPageSizeChange() {
this.detailPage.current = 1;
},
detailIndexMethod(index) {
return (this.detailPage.current - 1) * this.detailPage.size + index + 1;
},
exportDetails() { exportDetails() {
if (!this.details.length) return this.$message.warning('暂无可导出的结算明细'); if (!this.details.length) return this.$message.warning('暂无可导出的结算明细');
const rows = this.details.map((row, index) => { const rows = this.details.map((row, index) => {
@@ -66,6 +66,12 @@
<span v-else-if="field.prop === 'settlementType'" class="form-readonly"> <span v-else-if="field.prop === 'settlementType'" class="form-readonly">
{{ settlementTypeName }} {{ settlementTypeName }}
</span> </span>
<span v-else-if="field.prop === 'preSettlementNo'" class="form-readonly">
{{ form.preSettlementNo || (!form.id ? '系统自动生成' : '-') }}
</span>
<span v-else-if="field.prop === 'createTime'" class="form-readonly">
{{ formatCreateDate(form.createTime) }}
</span>
<span v-else-if="field.money" class="form-readonly"> <span v-else-if="field.money" class="form-readonly">
{{ formatMoney(form[field.prop], moneyCurrency(field.prop)) }} {{ formatMoney(form[field.prop], moneyCurrency(field.prop)) }}
</span> </span>
@@ -184,7 +190,6 @@
</template> </template>
<el-form <el-form
:model="detailQuery" :model="detailQuery"
inline
label-position="right" label-position="right"
label-width="auto" label-width="auto"
class="pre-settlement-editor__detail-filter" class="pre-settlement-editor__detail-filter"
@@ -228,8 +233,15 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<div v-show="!detailCollapsed"> <div v-show="!detailCollapsed">
<el-table :data="filteredDetails" border class="pre-settlement-editor__detail-table"> <el-table :data="pagedDetails" border class="pre-settlement-editor__detail-table">
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" /> <el-table-column
type="index"
label="序号"
width="64"
fixed="left"
align="center"
:index="detailIndexMethod"
/>
<el-table-column <el-table-column
v-for="column in visibleDetailColumns" v-for="column in visibleDetailColumns"
:key="column.prop" :key="column.prop"
@@ -300,6 +312,16 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="pre-settlement-editor__pagination">
<el-pagination
v-model:current-page="detailPage.current"
v-model:page-size="detailPage.size"
:total="filteredDetails.length"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleDetailPageSizeChange"
/>
</div>
</div> </div>
</section-card> </section-card>
@@ -313,13 +335,43 @@
<el-table :data="attachments" border @selection-change="handleAttachmentSelectionChange"> <el-table :data="attachments" border @selection-change="handleAttachmentSelectionChange">
<el-table-column type="selection" width="55" align="center" /> <el-table-column type="selection" width="55" align="center" />
<el-table-column type="index" label="序号" width="70" align="center" /> <el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="文件名" min-width="260" show-overflow-tooltip> <el-table-column label="附件类型" min-width="180" align="center">
<template #default="{ row }">
<el-select
v-if="editable"
v-model="row.type"
filterable
placeholder="请选择"
@change="sortAttachments"
>
<el-option
v-for="item in attachmentTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<span v-else>{{ attachmentTypeName(row.type) }}</span>
</template>
</el-table-column>
<el-table-column label="文件名称" min-width="260" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
<el-link type="primary" @click="previewAttachment(row)"> <el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }} {{ attachmentName(row) }}
</el-link> </el-link>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="附件描述" min-width="220">
<template #default="{ row }">
<span v-if="!editable">{{ row.description || '-' }}</span>
<el-input
v-else
v-model="row.description"
maxlength="200"
placeholder="请输入"
/>
</template>
</el-table-column>
<el-table-column label="文件大小" width="120" align="center"> <el-table-column label="文件大小" width="120" align="center">
<template #default="{ row }">{{ formatFileSize(row.size) }}</template> <template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column> </el-table-column>
@@ -425,7 +477,7 @@
提交 提交
</el-button> </el-button>
</template> </template>
<div v-if="pageMode" class="pre-settlement-editor__page-actions"> <div v-if="pageMode && !publicMode" class="pre-settlement-editor__page-actions">
<el-button @click="visible = false">取消</el-button> <el-button @click="visible = false">取消</el-button>
<el-button v-if="editable" type="primary" plain :loading="saving" @click="saveDraft(false)" <el-button v-if="editable" type="primary" plain :loading="saving" @click="saveDraft(false)"
>保存</el-button >保存</el-button
@@ -681,8 +733,6 @@
append-to-body append-to-body
destroy-on-close destroy-on-close
> >
<el-tabs v-model="adjustDialog.activeTab" class="pre-settlement-editor__adjust-tabs">
<el-tab-pane label="结算明细调整" name="adjust">
<el-table v-loading="adjustDialog.loading" :data="adjustRows" border> <el-table v-loading="adjustDialog.loading" :data="adjustRows" border>
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" /> <el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table-column prop="cargoName" label="货物名称" min-width="140" align="center" /> <el-table-column prop="cargoName" label="货物名称" min-width="140" align="center" />
@@ -780,35 +830,16 @@
class="pre-settlement-editor__adjust-reason" class="pre-settlement-editor__adjust-reason"
> >
<el-form-item label="调整原因"> <el-form-item label="调整原因">
<el-input v-model="adjustDialog.reason" maxlength="200" show-word-limit /> <el-input
v-model="adjustDialog.reason"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
placeholder="请输入"
/>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-tab-pane>
<el-tab-pane label="变更记录" name="records">
<el-table :data="changeRecords" border>
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column prop="changeTime" label="变更日期" min-width="170" align="center" />
<el-table-column prop="operatorName" label="经办人" min-width="140" align="center" />
<el-table-column prop="changeType" label="变更类型" min-width="160" align="center" />
<el-table-column
prop="changeReason"
label="变更原因"
min-width="240"
align="center"
show-overflow-tooltip
/>
<el-table-column label="状态" min-width="120" align="center">
<template #default>已生效</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row }">
<el-link type="primary" @click="openAdjustChangeRecord(row)">查看详情</el-link>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!adjustChangeRecords.length" description="暂无变更记录" :image-size="60" />
</el-tab-pane>
</el-tabs>
<template #footer> <template #footer>
<el-button @click="adjustDialog.visible = false">取消</el-button> <el-button @click="adjustDialog.visible = false">取消</el-button>
<el-button <el-button
@@ -822,35 +853,10 @@
</template> </template>
</el-dialog> </el-dialog>
<el-dialog <change-record-detail-dialog
v-model="adjustChangeRecordVisible" v-model="adjustChangeRecordVisible"
title="变更记录详情" :rows="adjustChangeRecordDetailRows"
append-to-body
destroy-on-close
width="1100px"
top="10px"
class="pre-settlement-change-record-detail-dialog"
>
<div v-if="adjustChangeRecord" class="pre-settlement-change-record-detail-meta">
<span>变更日期{{ adjustChangeRecord.changeTime || '-' }}</span>
<span>经办人{{ adjustChangeRecord.operatorName || '-' }}</span>
<span>变更类型{{ adjustChangeRecord.changeType || '-' }}</span>
<span>状态已生效</span>
</div>
<el-table :data="adjustChangeRecordDetailRows" border :show-overflow-tooltip="false">
<el-table-column prop="field" label="变更字段" min-width="220" />
<el-table-column prop="before" label="变更前" min-width="330" />
<el-table-column prop="after" label="变更后" min-width="420" />
</el-table>
<el-empty
v-if="!adjustChangeRecordDetailRows.length"
description="暂无变更内容"
:image-size="60"
/> />
<template #footer>
<el-button type="primary" @click="adjustChangeRecordVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog <el-dialog
v-model="billingRuleDialog.visible" v-model="billingRuleDialog.visible"
@@ -922,6 +928,8 @@
import { h } from 'vue'; import { h } from 'vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { InfoFilled } from '@element-plus/icons-vue'; import { InfoFilled } from '@element-plus/icons-vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
import { getMkPublicDetail } from '@/api/mk-process';
import { import {
adjustDetail, adjustDetail,
getCandidateDetails, getCandidateDetails,
@@ -960,7 +968,7 @@ import * as XLSX from 'xlsx';
export default { export default {
name: 'PreSettlementEditor', name: 'PreSettlementEditor',
components: { InfoFilled }, components: { InfoFilled, ChangeRecordDetailDialog },
props: { props: {
modelValue: { modelValue: {
type: Boolean, type: Boolean,
@@ -982,6 +990,10 @@ export default {
type: Object, type: Object,
default: null, default: null,
}, },
publicMode: {
type: Boolean,
default: false,
},
}, },
emits: ['update:modelValue', 'success'], emits: ['update:modelValue', 'success'],
data() { data() {
@@ -1054,9 +1066,14 @@ export default {
batchNo: '', batchNo: '',
cargoName: '', cargoName: '',
}, },
detailPage: {
current: 1,
size: 10,
},
advances: [], advances: [],
changeRecords: [], changeRecords: [],
attachments: [], attachments: [],
attachmentTypeOptions: [],
selectedAttachmentRows: [], selectedAttachmentRows: [],
candidateDialog: { candidateDialog: {
visible: false, visible: false,
@@ -1079,7 +1096,6 @@ export default {
loading: false, loading: false,
saving: false, saving: false,
readonly: false, readonly: false,
activeTab: 'adjust',
detailId: '', detailId: '',
sourceDetailId: '', sourceDetailId: '',
detailLineNo: '', detailLineNo: '',
@@ -1206,6 +1222,13 @@ export default {
}) })
); );
}, },
pagedDetails() {
const list = this.filteredDetails;
const size = Number(this.detailPage.size) || 10;
const current = Math.max(Number(this.detailPage.current) || 1, 1);
const start = (current - 1) * size;
return list.slice(start, start + size);
},
adjustFeeItemNames() { adjustFeeItemNames() {
const names = new Set(); const names = new Set();
this.adjustRows.forEach(row => { this.adjustRows.forEach(row => {
@@ -1232,6 +1255,16 @@ export default {
this.form.settlementAmount = Number(value || 0).toFixed(2); this.form.settlementAmount = Number(value || 0).toFixed(2);
this.recalculateLocalAmount(); this.recalculateLocalAmount();
}, },
filteredDetails: {
handler(list) {
const size = Number(this.detailPage.size) || 10;
const maxPage = Math.max(1, Math.ceil((list?.length || 0) / size) || 1);
if (this.detailPage.current > maxPage) {
this.detailPage.current = maxPage;
}
},
immediate: true,
},
}, },
methods: { methods: {
hasPermission(code) { hasPermission(code) {
@@ -1239,9 +1272,12 @@ export default {
}, },
async initialize() { async initialize() {
this.resetEditor(); this.resetEditor();
if (!this.publicMode) {
await this.loadFeeOptions(); await this.loadFeeOptions();
await this.loadFeeCategoryOptions(); await this.loadFeeCategoryOptions();
await this.loadTransportTypeOptions(); await this.loadTransportTypeOptions();
await this.loadAttachmentTypeOptions();
}
if (this.recordId) await this.loadDetail(); if (this.recordId) await this.loadDetail();
else if (this.initialData) await this.applyInitialData(); else if (this.initialData) await this.applyInitialData();
}, },
@@ -1281,6 +1317,7 @@ export default {
localCurrency: first.localCurrency || 'RMB', localCurrency: first.localCurrency || 'RMB',
}); });
this.details = rows.map(row => this.normalizeSourceDetail(row)); this.details = rows.map(row => this.normalizeSourceDetail(row));
this.detailPage.current = 1;
this.loading = true; this.loading = true;
try { try {
await this.ensureSourceFeeSnapshots(); await this.ensureSourceFeeSnapshots();
@@ -1342,6 +1379,7 @@ export default {
cargoName: '', cargoName: '',
}; };
this.appliedDetailQuery = { ...this.detailQuery }; this.appliedDetailQuery = { ...this.detailQuery };
this.detailPage = { current: 1, size: 10 };
this.advances = []; this.advances = [];
this.changeRecords = []; this.changeRecords = [];
this.attachments = []; this.attachments = [];
@@ -1350,7 +1388,6 @@ export default {
this.pendingAdjustments = {}; this.pendingAdjustments = {};
this.sourceFeeSnapshots = {}; this.sourceFeeSnapshots = {};
this.detailFeeCache = {}; this.detailFeeCache = {};
this.adjustDialog.activeTab = 'adjust';
this.adjustDialog.detailId = ''; this.adjustDialog.detailId = '';
this.adjustDialog.sourceDetailId = ''; this.adjustDialog.sourceDetailId = '';
this.adjustDialog.detailLineNo = ''; this.adjustDialog.detailLineNo = '';
@@ -1370,11 +1407,14 @@ export default {
async loadDetail() { async loadDetail() {
this.loading = true; this.loading = true;
try { try {
const { data } = await getDetail(this.form.id || this.recordId); const response = this.publicMode
const detail = data?.data || {}; ? await getMkPublicDetail('pre-settlement', this.form.id || this.recordId)
: await getDetail(this.form.id || this.recordId);
const detail = response.data?.data || {};
this.form = { ...emptyPreSettlementForm(), ...detail }; this.form = { ...emptyPreSettlementForm(), ...detail };
this.summaryFees = (detail.summaryFees || []).map(row => ({ ...row })); this.summaryFees = (detail.summaryFees || []).map(row => ({ ...row }));
this.details = (detail.details || []).map(row => this.normalizeSourceDetail(row)); this.details = (detail.details || []).map(row => this.normalizeSourceDetail(row));
this.detailPage.current = 1;
this.advances = (detail.advances || []).map(row => ({ this.advances = (detail.advances || []).map(row => ({
...row, ...row,
createUserName: row.createUserName || detail.createUserName, createUserName: row.createUserName || detail.createUserName,
@@ -1382,6 +1422,7 @@ export default {
})); }));
this.changeRecords = detail.changeRecords || []; this.changeRecords = detail.changeRecords || [];
this.attachments = this.parseAttachments(detail.attachmentsJson); this.attachments = this.parseAttachments(detail.attachmentsJson);
this.sortAttachments();
this.selectedAttachmentRows = []; this.selectedAttachmentRows = [];
if ( if (
detail.contractId && detail.contractId &&
@@ -1399,8 +1440,15 @@ export default {
partyB: detail.payeeName, partyB: detail.payeeName,
}); });
} }
// if (this.publicMode && detail.detailFees) {
Object.entries(detail.detailFees).forEach(([detailId, feeRows]) => {
if (Array.isArray(feeRows) && feeRows.length) {
this.detailFeeCache[detailId] = feeRows;
}
});
} else {
await this.loadAllDetailFees(); await this.loadAllDetailFees();
}
} finally { } finally {
this.loading = false; this.loading = false;
} }
@@ -1566,6 +1614,7 @@ export default {
return; return;
} }
this.$message.success('保存成功'); this.$message.success('保存成功');
this.visible = false;
this.$emit('success', this.form.id); this.$emit('success', this.form.id);
} finally { } finally {
this[stateKey] = false; this[stateKey] = false;
@@ -1895,6 +1944,7 @@ export default {
}, },
handleDetailQuery() { handleDetailQuery() {
this.appliedDetailQuery = { ...this.detailQuery }; this.appliedDetailQuery = { ...this.detailQuery };
this.detailPage.current = 1;
}, },
resetDetailQuery() { resetDetailQuery() {
this.detailQuery = { this.detailQuery = {
@@ -1904,6 +1954,13 @@ export default {
cargoName: '', cargoName: '',
}; };
this.appliedDetailQuery = { ...this.detailQuery }; this.appliedDetailQuery = { ...this.detailQuery };
this.detailPage.current = 1;
},
handleDetailPageSizeChange() {
this.detailPage.current = 1;
},
detailIndexMethod(index) {
return (this.detailPage.current - 1) * this.detailPage.size + index + 1;
}, },
normalizeAdjustRow(item, feeItemNames = []) { normalizeAdjustRow(item, feeItemNames = []) {
const rawFeeItems = const rawFeeItems =
@@ -2026,7 +2083,6 @@ export default {
this.adjustDialog.visible = true; this.adjustDialog.visible = true;
this.adjustDialog.loading = true; this.adjustDialog.loading = true;
this.adjustDialog.readonly = readonly; this.adjustDialog.readonly = readonly;
this.adjustDialog.activeTab = 'adjust';
this.adjustDialog.detailId = detailRow.id; this.adjustDialog.detailId = detailRow.id;
this.adjustDialog.detailLineNo = detailRow.lineNo; this.adjustDialog.detailLineNo = detailRow.lineNo;
this.adjustDialog.sourceDetailId = this.adjustDialog.sourceDetailId =
@@ -2042,6 +2098,11 @@ export default {
this.adjustRows = this.cloneAdjustRows(pending.rows); this.adjustRows = this.cloneAdjustRows(pending.rows);
return; return;
} }
if (this.publicMode) {
const cachedFees = this.detailFeeCache[detailRow.id] || [];
this.adjustRows = cachedFees.map(item => this.normalizeAdjustRow(item));
return;
}
if (sourceDetailId) { if (sourceDetailId) {
if (!this.sourceFeeSnapshots[sourceDetailId]) { if (!this.sourceFeeSnapshots[sourceDetailId]) {
this.sourceFeeSnapshots[sourceDetailId] = await this.loadSourceFeeRows(sourceDetailId); this.sourceFeeSnapshots[sourceDetailId] = await this.loadSourceFeeRows(sourceDetailId);
@@ -2049,6 +2110,11 @@ export default {
this.adjustRows = this.cloneAdjustRows(this.sourceFeeSnapshots[sourceDetailId]); this.adjustRows = this.cloneAdjustRows(this.sourceFeeSnapshots[sourceDetailId]);
return; return;
} }
const cachedFees = this.detailFeeCache[detailRow.id];
if (this.publicMode && Array.isArray(cachedFees)) {
this.adjustRows = cachedFees.map(item => this.normalizeAdjustRow(item));
return;
}
const response = await getDetailFees(detailRow.id); const response = await getDetailFees(detailRow.id);
const rows = response.data?.data || response.data || []; const rows = response.data?.data || response.data || [];
this.adjustRows = rows.map(item => this.normalizeAdjustRow(item)); this.adjustRows = rows.map(item => this.normalizeAdjustRow(item));
@@ -2237,10 +2303,6 @@ export default {
return String(name || '').includes('运费') || String(name || '').includes('运输费'); return String(name || '').includes('运费') || String(name || '').includes('运输费');
}, },
async saveAdjustment() { async saveAdjustment() {
if (!this.adjustDialog.reason.trim()) {
this.$message.warning('请输入调整原因');
return;
}
if (this.adjustRows.some(row => row.calculating)) { if (this.adjustRows.some(row => row.calculating)) {
this.$message.warning('费用正在重新计算,请稍候'); this.$message.warning('费用正在重新计算,请稍候');
return; return;
@@ -2419,13 +2481,92 @@ export default {
handleAttachmentChange(list) { handleAttachmentChange(list) {
const uploadUserName = this.userInfo?.realName || this.userInfo?.userName || ''; const uploadUserName = this.userInfo?.realName || this.userInfo?.userName || '';
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss'); const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.attachments = (list || []).map(file => ({ const existingRows = this.attachments || [];
this.attachments = (list || []).map(file => {
const fileUrl = this.attachmentUrl(file);
const existing = existingRows.find(item => {
const sameUid = file.uid && item.uid && String(file.uid) === String(item.uid);
const sameUrl = fileUrl && this.attachmentUrl(item) === fileUrl;
return sameUid || sameUrl;
});
const originalName = this.attachmentName(file);
return {
...file, ...file,
uploadUserName: file.uploadUserName || uploadUserName, type: existing?.type || file.type || this.resolveAttachmentType(originalName),
uploadTime: file.uploadTime || uploadTime, description: existing?.description || file.description || '',
})); uploadUserName: existing?.uploadUserName || file.uploadUserName || uploadUserName,
uploadTime: existing?.uploadTime || file.uploadTime || uploadTime,
};
});
this.sortAttachments();
this.selectedAttachmentRows = []; this.selectedAttachmentRows = [];
}, },
async loadAttachmentTypeOptions() {
try {
const response = await getDictionary({ code: 'pre_settlement_attachment_type' });
const data = response?.data?.data || [];
this.attachmentTypeOptions = data
.map(item => ({
label: item.dictValue,
value: item.dictValue,
}))
.filter(item => item.label && item.value);
} catch (error) {
this.attachmentTypeOptions = [];
}
this.sortAttachments();
},
resolveAttachmentType(fileName) {
const normalizedName = String(fileName || '').toLocaleLowerCase();
const matchedType = [...this.attachmentTypeOptions]
.filter(item => item?.label || item?.value)
.sort(
(a, b) =>
String(b.label || b.value || '').length - String(a.label || a.value || '').length
)
.find(item => {
const typeText = String(item.label || item.value || '').toLocaleLowerCase();
return typeText && normalizedName.includes(typeText);
});
if (matchedType?.value) return matchedType.value;
const otherType = this.attachmentTypeOptions.find(item => this.isOtherAttachmentType(item));
if (otherType?.value) return otherType.value;
return '';
},
isOtherAttachmentType(item) {
const typeText = String(item?.label || item?.value || '').trim();
return (
typeText === '其他' ||
typeText === '其它' ||
typeText === '其他附件' ||
typeText === '其它附件'
);
},
getAttachmentTypeOrder(type) {
const normalizedType = String(type || '').trim();
const index = this.attachmentTypeOptions.findIndex(
item =>
String(item.value || '').trim() === normalizedType ||
String(item.label || '').trim() === normalizedType
);
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
},
sortAttachments() {
this.attachments = (this.attachments || [])
.map((file, index) => ({ file, index }))
.sort((a, b) => {
const orderDifference =
this.getAttachmentTypeOrder(a.file.type) - this.getAttachmentTypeOrder(b.file.type);
return orderDifference || a.index - b.index;
})
.map(item => item.file);
},
attachmentTypeName(type) {
const option = this.attachmentTypeOptions.find(
item => String(item.value) === String(type) || String(item.label) === String(type)
);
return option?.label || type || '-';
},
handleAttachmentSelectionChange(rows) { handleAttachmentSelectionChange(rows) {
this.selectedAttachmentRows = rows || []; this.selectedAttachmentRows = rows || [];
}, },
@@ -2469,13 +2610,25 @@ export default {
}, },
parseAttachments(value) { parseAttachments(value) {
if (!value) return []; if (!value) return [];
if (Array.isArray(value)) return value; let list = [];
if (Array.isArray(value)) {
list = value;
} else {
try { try {
const parsed = JSON.parse(value); const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : []; list = Array.isArray(parsed) ? parsed : [];
} catch (error) { } catch (error) {
return []; return [];
} }
}
return list.map(file => {
const originalName = this.attachmentName(file);
return {
...file,
type: file?.type || this.resolveAttachmentType(originalName),
description: file?.description || '',
};
});
}, },
parseFeeItems(value) { parseFeeItems(value) {
if (!value) return {}; if (!value) return {};
@@ -2512,6 +2665,11 @@ export default {
if (!Number.isFinite(amount)) return '-'; if (!Number.isFinite(amount)) return '-';
return `${amount.toFixed(2)} ${currency || 'RMB'}`; return `${amount.toFixed(2)} ${currency || 'RMB'}`;
}, },
formatCreateDate(value) {
if (value === undefined || value === null || value === '') return '-';
const parsed = this.$dayjs(value);
return parsed.isValid() ? parsed.format('YYYY-MM-DD') : this.displayValue(value);
},
formatQuantity(row) { formatQuantity(row) {
const value = row.transportQuantity; const value = row.transportQuantity;
if (value === undefined || value === null || value === '') return '-'; if (value === undefined || value === null || value === '') return '-';
@@ -2662,22 +2820,43 @@ export default {
} }
&__detail-filter { &__detail-filter {
display: flex; display: grid;
flex-wrap: wrap; grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
gap: 0 24px; align-items: center;
gap: 8px 16px;
margin-bottom: 12px; margin-bottom: 12px;
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
:deep(.el-form-item) { :deep(.el-form-item) {
display: flex;
min-width: 0;
margin-right: 0;
margin-bottom: 0; margin-bottom: 0;
} }
:deep(.el-form-item__content) {
flex: 1;
min-width: 0;
}
:deep(.el-input) { :deep(.el-input) {
width: 220px; width: 100%;
} }
} }
&__detail-filter-actions { &__detail-filter-actions {
margin-left: auto; width: auto;
margin-left: 0;
white-space: nowrap;
justify-self: end;
:deep(.el-form-item__content) {
flex: none;
justify-content: flex-end;
flex-wrap: nowrap;
}
} }
&__contract-filter { &__contract-filter {
@@ -2790,23 +2969,6 @@ export default {
} }
} }
.pre-settlement-change-record-detail-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 32px;
margin-bottom: 16px;
color: #606266;
}
:deep(.pre-settlement-change-record-detail-dialog .el-dialog__body) {
padding-top: 12px;
}
:deep(.pre-settlement-change-record-detail-dialog .el-table .cell) {
white-space: pre-wrap;
word-break: break-all;
}
:deep(.pre-settlement-editor .el-dialog__body) { :deep(.pre-settlement-editor .el-dialog__body) {
padding: 12px 16px; padding: 12px 16px;
} }
@@ -56,6 +56,7 @@ export default {
methods: { methods: {
goBack() { goBack() {
removeSettlementTransfer(this.$route.query.transferToken); removeSettlementTransfer(this.$route.query.transferToken);
this.$router.$avueRouter.closeTag();
this.$router.push('/settlement/formal-settlement'); this.$router.push('/settlement/formal-settlement');
}, },
syncTagTitle() { syncTagTitle() {
@@ -0,0 +1,55 @@
<template>
<mk-public-shell biz-type="formal-settlement" :get-form="getForm">
<basic-container>
<div class="mk-public-form-title">查看正式结算</div>
<formal-settlement-editor
ref="page"
:model-value="true"
page-mode
readonly
public-mode
:record-id="recordId"
/>
</basic-container>
</mk-public-shell>
</template>
<script>
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
import FormalSettlementEditor from '@/views/settlement/components/formal-settlement-editor.vue';
export default {
name: 'FormalSettlementPublicView',
components: { MkPublicShell, FormalSettlementEditor },
computed: {
recordId() {
return this.$route.query.id || '';
},
},
methods: {
getForm() {
const form = this.$refs.page?.form || {};
return { ...form, subject: form.formalSettlementNo || form.contractName || '' };
},
},
};
</script>
<style lang="scss" scoped>
.mk-public-form-title {
display: flex;
align-items: center;
min-height: 24px;
margin-bottom: 16px;
font-size: 18px;
font-weight: 600;
&::before {
width: 4px;
height: 20px;
margin-right: 8px;
background: #409eff;
content: '';
}
}
</style>
@@ -50,6 +50,7 @@ export default {
}, },
goBack() { goBack() {
removeSettlementTransfer(this.$route.query.transferToken); removeSettlementTransfer(this.$route.query.transferToken);
this.$router.$avueRouter.closeTag();
this.$router.push('/settlement/pre-settlement'); this.$router.push('/settlement/pre-settlement');
}, },
syncTagTitle() { syncTagTitle() {
@@ -0,0 +1,55 @@
<template>
<mk-public-shell biz-type="pre-settlement" :get-form="getForm">
<basic-container>
<div class="mk-public-form-title">查看预结算</div>
<pre-settlement-editor
ref="page"
:model-value="true"
page-mode
readonly
public-mode
:record-id="recordId"
/>
</basic-container>
</mk-public-shell>
</template>
<script>
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
import PreSettlementEditor from '@/views/settlement/components/pre-settlement-editor.vue';
export default {
name: 'PreSettlementPublicView',
components: { MkPublicShell, PreSettlementEditor },
computed: {
recordId() {
return this.$route.query.id || '';
},
},
methods: {
getForm() {
const form = this.$refs.page?.form || {};
return { ...form, subject: form.preSettlementNo || form.contractName || '' };
},
},
};
</script>
<style lang="scss" scoped>
.mk-public-form-title {
display: flex;
align-items: center;
min-height: 24px;
margin-bottom: 16px;
font-size: 18px;
font-weight: 600;
&::before {
width: 4px;
height: 20px;
margin-right: 8px;
background: #409eff;
content: '';
}
}
</style>
+1
View File
@@ -497,6 +497,7 @@ export default {
path: '/payment/payment-application/form', path: '/payment/payment-application/form',
query: { query: {
mode: 'add', mode: 'add',
paymentType: 'progress_advance',
transferToken, transferToken,
preSettlementIds: sourcePreSettlements preSettlementIds: sourcePreSettlements
.map(row => row.preSettlementId) .map(row => row.preSettlementId)
@@ -183,6 +183,7 @@
title="变更记录详情" title="变更记录详情"
width="88%" width="88%"
append-to-body append-to-body
align-center
> >
<el-table v-loading="changeRecordsDialog.loading" :data="changeRows" border> <el-table v-loading="changeRecordsDialog.loading" :data="changeRows" border>
<el-table-column type="index" label="序号" width="64" align="center" /> <el-table-column type="index" label="序号" width="64" align="center" />
@@ -1153,8 +1154,7 @@ export default {
minWidth: 130, minWidth: 130,
align: 'right', align: 'right',
}; };
const dynamicColumns = this.isPayable const dynamicColumns = [
? [
{ {
label: '运输费', label: '运输费',
prop: 'tableFreightAmount', prop: 'tableFreightAmount',
@@ -1163,17 +1163,6 @@ export default {
align: 'right', align: 'right',
}, },
otherFeeColumn, otherFeeColumn,
]
: [
...this.tableFeeItemNames.map((name, index) => ({
label: name,
prop: `tableFeeItem${index}`,
feeItemName: name,
dynamic: true,
minWidth: 130,
align: 'right',
})),
otherFeeColumn,
]; ];
if (totalIndex < 0) return [...columns, ...dynamicColumns]; if (totalIndex < 0) return [...columns, ...dynamicColumns];
columns.splice(totalIndex, 0, ...dynamicColumns); columns.splice(totalIndex, 0, ...dynamicColumns);
@@ -1209,10 +1198,24 @@ export default {
await this.loadTable(); await this.loadTable();
await this.openRouteDetail(); await this.openRouteDetail();
}, },
// keep-alive teleport/append-to-body
deactivated() {
this.closeInnerDialogs();
},
beforeUnmount() { beforeUnmount() {
this.clearAdjustCalculations(); this.closeInnerDialogs();
}, },
methods: { methods: {
closeInnerDialogs() {
this.closeDetailPanel();
this.closeAdjustPanel();
this.changeRecordsDialog.visible = false;
this.updateFeeDialog.visible = false;
this.transferDialog.visible = false;
this.generateDialog.visible = false;
this.previewDialog.visible = false;
this.generateContractDialog.visible = false;
},
contractCategoryName(value) { contractCategoryName(value) {
return ( return (
this.contractCategoryOptions.find(item => String(item.value) === String(value))?.label || this.contractCategoryOptions.find(item => String(item.value) === String(value))?.label ||
@@ -1498,7 +1501,7 @@ export default {
const res = await api.getList(this.page.current, this.page.size, params); const res = await api.getList(this.page.current, this.page.size, params);
const data = this.unwrapPage(res); const data = this.unwrapPage(res);
this.rows = (data.records || []).map(this.decorateRow); this.rows = (data.records || []).map(this.decorateRow);
this.tableFeeItemNames = this.isPayable ? [] : this.collectFeeItemNames(this.rows); this.tableFeeItemNames = [];
this.page.total = data.total || 0; this.page.total = data.total || 0;
} finally { } finally {
this.loading = false; this.loading = false;
@@ -1738,7 +1741,7 @@ export default {
}); });
const adjusted = { const adjusted = {
...item, ...item,
transportQuantity: Number(item.transportQuantity || 0), transportQuantity: this.normalizeAdjustTransportQuantity(item),
mileage: mileage:
item.mileage === null || item.mileage === undefined || Number(item.mileage) === -1 item.mileage === null || item.mileage === undefined || Number(item.mileage) === -1
? null ? null
@@ -1767,6 +1770,21 @@ export default {
feeSourceLabel(value) { feeSourceLabel(value) {
return ['手动录入', '手动添加', '手工录入'].includes(value) ? '手动录入' : '自动生成'; return ['手动录入', '手动添加', '手工录入'].includes(value) ? '手动录入' : '自动生成';
}, },
normalizeAdjustTransportQuantity(item = {}) {
const value = item.transportQuantity;
if (value === undefined || value === null || value === '' || Number(value) === -1) {
return '';
}
const element = item.billingFactor;
// / 1
if (
(element === '按车辆' || element === '固定金额(整单一口价)') &&
Number(value) === 1
) {
return '';
}
return Number(value);
},
adjustBillingTypes(row) { adjustBillingTypes(row) {
return ADJUST_BILLING_TYPE_MAP[row.billingFactor] || []; return ADJUST_BILLING_TYPE_MAP[row.billingFactor] || [];
}, },
@@ -1894,7 +1912,12 @@ export default {
model: row.model, model: row.model,
billingFactor: row.billingFactor, billingFactor: row.billingFactor,
billingType: row.billingType, billingType: row.billingType,
transportQuantity: row.transportQuantity, transportQuantity:
row.transportQuantity === '' ||
row.transportQuantity === null ||
row.transportQuantity === undefined
? null
: Number(row.transportQuantity),
priceUnit: row.priceUnit, priceUnit: row.priceUnit,
unitPrice: row.unitPrice, unitPrice: row.unitPrice,
mileage: row.mileage, mileage: row.mileage,
+44 -16
View File
@@ -143,7 +143,8 @@
:status="oaSyncProgressStatus" :status="oaSyncProgressStatus"
:stroke-width="12" :stroke-width="12"
/> />
<div class="oa-sync-meta"> <div class="oa-sync-meta" v-if="oaSyncStage === 'clear'">当前阶段{{ oaSyncStageLabel }}</div>
<div class="oa-sync-meta" v-else>
当前阶段{{ oaSyncStageLabel }} {{ oaSyncProgress.current || 0 }} / {{ oaSyncTotalPage }} 当前阶段{{ oaSyncStageLabel }} {{ oaSyncProgress.current || 0 }} / {{ oaSyncTotalPage }}
</div> </div>
<div class="oa-sync-stats"> <div class="oa-sync-stats">
@@ -177,6 +178,7 @@ import {
getDeptTree, getDeptTree,
syncOaCompany, syncOaCompany,
syncOaDepartment, syncOaDepartment,
clearNonTopDept,
} from '@/api/system/dept'; } from '@/api/system/dept';
import { getLeaderList } from '@/api/system/user'; import { getLeaderList } from '@/api/system/user';
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive'; import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
@@ -215,6 +217,7 @@ export default {
oaSyncCancelled: false, oaSyncCancelled: false,
oaSyncStatus: 'running', oaSyncStatus: 'running',
oaSyncStage: 'company', oaSyncStage: 'company',
oaSyncClearNonTop: false,
oaSyncController: null, oaSyncController: null,
oaSyncProgress: { oaSyncProgress: {
current: 0, current: 0,
@@ -287,8 +290,9 @@ export default {
return; return;
} }
const parentId = this.normalizeParentId(this.form?.parentId); const parentId = this.normalizeParentId(this.form?.parentId);
//
if (!parentId) { if (!parentId) {
callback(new Error('请先选择上级组织')); callback();
return; return;
} }
if (!this.parentDept || String(this.parentDept.id) !== String(parentId)) { if (!this.parentDept || String(this.parentDept.id) !== String(parentId)) {
@@ -429,13 +433,6 @@ export default {
props: { props: {
label: 'title', label: 'title',
}, },
rules: [
{
required: true,
message: '请选择上级组织',
trigger: 'click',
},
],
}, },
{ {
label: '所属租户', label: '所属租户',
@@ -520,6 +517,9 @@ export default {
return '自动同步组织'; return '自动同步组织';
}, },
oaSyncStageLabel() { oaSyncStageLabel() {
if (this.oaSyncStage === 'clear') {
return '清除非顶级组织';
}
return this.oaSyncStage === 'department' ? '同步部门' : '同步公司'; return this.oaSyncStage === 'department' ? '同步部门' : '同步公司';
}, },
oaSyncTotalPage() { oaSyncTotalPage() {
@@ -555,21 +555,34 @@ export default {
}, },
methods: { methods: {
handleOaOrgSync() { handleOaOrgSync() {
this.$confirm('确定从OA先同步公司、再同步部门?', '提示', { this.$confirm(
confirmButtonText: '确定', '是否清除非顶级组织?选择“是”将先删除顶级以外的组织后再同步;选择“否”则直接同步。',
cancelButtonText: '取消', '提示',
{
confirmButtonText: '是',
cancelButtonText: '否',
distinguishCancelAndClose: true,
closeOnClickModal: false,
type: 'warning', type: 'warning',
}).then(() => { }
this.startOaOrgSync(); )
.then(() => {
this.startOaOrgSync(true);
})
.catch(action => {
if (action === 'cancel') {
this.startOaOrgSync(false);
}
}); });
}, },
startOaOrgSync() { startOaOrgSync(clearNonTop) {
this.oaSyncLoading = true; this.oaSyncLoading = true;
this.oaSyncVisible = true; this.oaSyncVisible = true;
this.oaSyncRunning = true; this.oaSyncRunning = true;
this.oaSyncCancelled = false; this.oaSyncCancelled = false;
this.oaSyncStatus = 'running'; this.oaSyncStatus = 'running';
this.oaSyncStage = 'company'; this.oaSyncClearNonTop = !!clearNonTop;
this.oaSyncStage = clearNonTop ? 'clear' : 'company';
this.oaSyncController = new AbortController(); this.oaSyncController = new AbortController();
this.oaSyncProgress = { this.oaSyncProgress = {
current: 0, current: 0,
@@ -607,6 +620,14 @@ export default {
}, },
async runOaOrgSyncPages() { async runOaOrgSyncPages() {
try { try {
if (this.oaSyncClearNonTop) {
this.oaSyncStage = 'clear';
await clearNonTopDept(this.oaSyncController?.signal);
if (this.oaSyncCancelled) {
this.oaSyncStatus = 'cancelled';
return;
}
}
this.oaSyncStage = 'company'; this.oaSyncStage = 'company';
await this.runOaOrgSyncStage(syncOaCompany); await this.runOaOrgSyncStage(syncOaCompany);
if (this.oaSyncCancelled) { if (this.oaSyncCancelled) {
@@ -821,6 +842,11 @@ export default {
.filter(Boolean); .filter(Boolean);
return result.length ? result.join('、') : '-'; return result.length ? result.join('、') : '-';
}, },
normalizeTopParent(row) {
if (!this.normalizeParentId(row?.parentId)) {
row.parentId = 0;
}
},
isRootDept(row) { isRootDept(row) {
return row && (row.parentId === 0 || String(row.parentId) === '0'); return row && (row.parentId === 0 || String(row.parentId) === '0');
}, },
@@ -836,6 +862,7 @@ export default {
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId; row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
row.deptCode = String(row.deptCode || '').trim(); row.deptCode = String(row.deptCode || '').trim();
row.leaderId = func.join(row.leaderId); row.leaderId = func.join(row.leaderId);
this.normalizeTopParent(row);
if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) { if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) {
this.$message.warning('请选择承运商'); this.$message.warning('请选择承运商');
loading(); loading();
@@ -865,6 +892,7 @@ export default {
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId; row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
row.deptCode = String(row.deptCode || '').trim(); row.deptCode = String(row.deptCode || '').trim();
row.leaderId = func.join(row.leaderId); row.leaderId = func.join(row.leaderId);
this.normalizeTopParent(row);
if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) { if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) {
this.$message.warning('请选择承运商'); this.$message.warning('请选择承运商');
loading(); loading();
+10 -4
View File
@@ -205,7 +205,7 @@
type="password" type="password"
show-password show-password
autocomplete="new-password" autocomplete="new-password"
placeholder="请输入登录密码" placeholder="大于8位,含字母、数字、特殊字符.!@#$%^&*"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -332,7 +332,12 @@
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="新密码" prop="password"> <el-form-item label="新密码" prop="password">
<el-input v-model="passwordForm.password" type="password" show-password /> <el-input
v-model="passwordForm.password"
type="password"
show-password
placeholder="大于8位,含字母、数字、特殊字符.!@#$%^&*"
/>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="24"> <el-col :span="24">
@@ -424,6 +429,7 @@ import { mapGetters } from 'vuex';
import { h } from 'vue'; import { h } from 'vue';
import { getToken } from '@/utils/auth'; import { getToken } from '@/utils/auth';
import { downloadXls } from '@/utils/util'; import { downloadXls } from '@/utils/util';
import { validateLoginPassword } from '@/utils/validate';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
import func from '@/utils/func'; import func from '@/utils/func';
@@ -487,8 +493,7 @@ export default {
passwordForm: {}, passwordForm: {},
passwordRules: { passwordRules: {
password: [ password: [
{ required: true, message: '请输入新密码', trigger: 'blur' }, { required: true, validator: validateLoginPassword, trigger: 'blur' },
{ min: 6, max: 32, message: '密码长度在6到32个字符', trigger: 'blur' },
], ],
password2: [ password2: [
{ required: true, message: '请再次输入新密码', trigger: 'blur' }, { required: true, message: '请再次输入新密码', trigger: 'blur' },
@@ -497,6 +502,7 @@ export default {
}, },
userRules: { userRules: {
account: [{ required: true, message: '请输入账号名', trigger: 'blur' }], account: [{ required: true, message: '请输入账号名', trigger: 'blur' }],
password: [{ required: true, validator: validateLoginPassword, trigger: 'blur' }],
realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }], realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }], phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
deptId: [{ required: true, message: '请选择所属组织', trigger: 'change' }], deptId: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
+14 -1
View File
@@ -91,7 +91,7 @@
<el-input <el-input
v-model="passwordForm.newPassword" v-model="passwordForm.newPassword"
type="password" type="password"
placeholder="请输入新密码" placeholder="大于8位,含字母、数字、特殊字符.!@#$%^&*"
show-password show-password
> >
<template #prefix> <template #prefix>
@@ -143,6 +143,7 @@
import { getUserInfo, updateInfo, updatePassword } from '@/api/system/user'; import { getUserInfo, updateInfo, updatePassword } from '@/api/system/user';
import md5 from 'js-md5'; import md5 from 'js-md5';
import { sensitive } from '@/utils/sensitive'; import { sensitive } from '@/utils/sensitive';
import { isValidLoginPassword, LOGIN_PASSWORD_RULE_MESSAGE } from '@/utils/validate';
import { import {
Plus, Plus,
Bell, Bell,
@@ -259,6 +260,18 @@ export default {
}, },
// //
submitPassword() { submitPassword() {
if (!this.passwordForm.newPassword || !this.passwordForm.newPassword1) {
this.$message.warning('请输入新密码和确认密码');
return;
}
if (this.passwordForm.newPassword !== this.passwordForm.newPassword1) {
this.$message.warning('两次输入的新密码不一致');
return;
}
if (!isValidLoginPassword(this.passwordForm.newPassword)) {
this.$message.warning(LOGIN_PASSWORD_RULE_MESSAGE);
return;
}
this.loading = true; this.loading = true;
updatePassword( updatePassword(
md5(this.passwordForm.oldPassword), md5(this.passwordForm.oldPassword),
+61 -21
View File
@@ -129,7 +129,7 @@
> >
<el-form <el-form
label-position="right" label-position="right"
label-width="calc(7em + 24px)" label-width="calc(4em + 12px)"
class="certification-audit archive-form" class="certification-audit archive-form"
> >
<section-card title="基础信息"> <section-card title="基础信息">
@@ -145,19 +145,25 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="外廓尺寸(毫米)"> <el-form-item label="外廓尺寸">
<div class="dimension-group"> <div class="dimension-group">
<div class="dimension-group__item"> <div class="dimension-group__item">
<span class="dimension-group__label"></span> <span class="dimension-group__label"></span>
<span class="certification-audit__value">{{ auditValue('outerLength') }}</span> <span class="certification-audit__value">
{{ auditValueWithUnit('outerLength', 'mm') }}
</span>
</div> </div>
<div class="dimension-group__item"> <div class="dimension-group__item">
<span class="dimension-group__label"></span> <span class="dimension-group__label"></span>
<span class="certification-audit__value">{{ auditValue('outerWidth') }}</span> <span class="certification-audit__value">
{{ auditValueWithUnit('outerWidth', 'mm') }}
</span>
</div> </div>
<div class="dimension-group__item"> <div class="dimension-group__item">
<span class="dimension-group__label"></span> <span class="dimension-group__label"></span>
<span class="certification-audit__value">{{ auditValue('outerHeight') }}</span> <span class="certification-audit__value">
{{ auditValueWithUnit('outerHeight', 'mm') }}
</span>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
@@ -165,13 +171,17 @@
</el-row> </el-row>
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :span="6"> <el-col :span="6">
<el-form-item label="核定载质量KG"> <el-form-item label="核定载质量">
<span class="certification-audit__value">{{ auditValue('approvedLoadKg') }}</span> <span class="certification-audit__value">
{{ auditValueWithUnit('approvedLoadKg', 'KG') }}
</span>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="准牵引总质量KG"> <el-form-item label="准牵引总质量">
<span class="certification-audit__value">{{ auditValue('tractionMassKg') }}</span> <span class="certification-audit__value">
{{ auditValueWithUnit('tractionMassKg', 'KG') }}
</span>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
@@ -357,7 +367,7 @@
v-if="showRejectReason" v-if="showRejectReason"
class="certification-audit__reason" class="certification-audit__reason"
label-position="right" label-position="right"
label-width="calc(7em + 24px)" label-width="calc(4em + 24px)"
> >
<el-form-item label="驳回原因" required> <el-form-item label="驳回原因" required>
<el-input <el-input
@@ -427,7 +437,7 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="外廓尺寸(毫米)"> <el-form-item label="外廓尺寸">
<div class="dimension-group"> <div class="dimension-group">
<div class="dimension-group__item"> <div class="dimension-group__item">
<span class="dimension-group__label"></span> <span class="dimension-group__label"></span>
@@ -436,7 +446,9 @@
inputmode="numeric" inputmode="numeric"
placeholder="请输入" placeholder="请输入"
@input="vehicleForm.outerLength = digitsOnly($event)" @input="vehicleForm.outerLength = digitsOnly($event)"
/> >
<template #suffix>mm</template>
</el-input>
</div> </div>
<div class="dimension-group__item"> <div class="dimension-group__item">
<span class="dimension-group__label"></span> <span class="dimension-group__label"></span>
@@ -445,7 +457,9 @@
inputmode="numeric" inputmode="numeric"
placeholder="请输入" placeholder="请输入"
@input="vehicleForm.outerWidth = digitsOnly($event)" @input="vehicleForm.outerWidth = digitsOnly($event)"
/> >
<template #suffix>mm</template>
</el-input>
</div> </div>
<div class="dimension-group__item"> <div class="dimension-group__item">
<span class="dimension-group__label"></span> <span class="dimension-group__label"></span>
@@ -454,7 +468,9 @@
inputmode="numeric" inputmode="numeric"
placeholder="请输入" placeholder="请输入"
@input="vehicleForm.outerHeight = digitsOnly($event)" @input="vehicleForm.outerHeight = digitsOnly($event)"
/> >
<template #suffix>mm</template>
</el-input>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
@@ -462,23 +478,27 @@
</el-row> </el-row>
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :span="6"> <el-col :span="6">
<el-form-item label="核定载质量KG" prop="approvedLoadKg"> <el-form-item label="核定载质量" prop="approvedLoadKg">
<el-input <el-input
v-model="vehicleForm.approvedLoadKg" v-model="vehicleForm.approvedLoadKg"
inputmode="numeric" inputmode="numeric"
placeholder="请输入" placeholder="请输入"
@input="vehicleForm.approvedLoadKg = digitsOnly($event)" @input="vehicleForm.approvedLoadKg = digitsOnly($event)"
/> >
<template #suffix>KG</template>
</el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="准牵引总质量KG" prop="tractionMassKg"> <el-form-item label="准牵引总质量" prop="tractionMassKg">
<el-input <el-input
v-model="vehicleForm.tractionMassKg" v-model="vehicleForm.tractionMassKg"
inputmode="numeric" inputmode="numeric"
placeholder="请输入" placeholder="请输入"
@input="vehicleForm.tractionMassKg = digitsOnly($event)" @input="vehicleForm.tractionMassKg = digitsOnly($event)"
/> >
<template #suffix>KG</template>
</el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
@@ -896,6 +916,7 @@ export default {
energyTypeOptions: ['柴油', '汽油', '新能源', '天然气'], energyTypeOptions: ['柴油', '汽油', '新能源', '天然气'],
baseRules: { baseRules: {
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }], organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
useDepartment: [{ required: true, message: '请选择使用部门', trigger: 'change' }],
plateNo: [ plateNo: [
{ required: true, message: '请输入车牌号', trigger: 'change' }, { required: true, message: '请输入车牌号', trigger: 'change' },
{ validator: this.validatePlateNo, trigger: 'change' }, { validator: this.validatePlateNo, trigger: 'change' },
@@ -986,6 +1007,9 @@ export default {
if (this.vehicleBox && !this.vehicleForm.id && !this.vehicleForm.organizationName) { if (this.vehicleBox && !this.vehicleForm.id && !this.vehicleForm.organizationName) {
this.vehicleForm.organizationName = this.getCurrentOrganizationName(); this.vehicleForm.organizationName = this.getCurrentOrganizationName();
} }
if (this.vehicleBox && !this.vehicleForm.useDepartment) {
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
}
}); });
}, },
flattenDeptOptions(tree = [], level = 0) { flattenDeptOptions(tree = [], level = 0) {
@@ -1061,6 +1085,7 @@ export default {
if (!row?.id) { if (!row?.id) {
this.vehicleForm = emptyForm(); this.vehicleForm = emptyForm();
this.vehicleForm.organizationName = this.getCurrentOrganizationName(); this.vehicleForm.organizationName = this.getCurrentOrganizationName();
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
this.syncPlateNo(false); this.syncPlateNo(false);
this.vehicleBox = true; this.vehicleBox = true;
return; return;
@@ -1070,6 +1095,9 @@ export default {
...emptyForm(), ...emptyForm(),
...(res.data.data || {}), ...(res.data.data || {}),
}; };
if (!this.vehicleForm.useDepartment) {
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
}
this.splitPlateNo(); this.splitPlateNo();
this.syncPlateNo(false); this.syncPlateNo(false);
this.vehicleBox = true; this.vehicleBox = true;
@@ -1102,6 +1130,10 @@ export default {
const value = this.certificationAuditForm[prop]; const value = this.certificationAuditForm[prop];
return value === undefined || value === null || value === '' ? '-' : value; return value === undefined || value === null || value === '' ? '-' : value;
}, },
auditValueWithUnit(prop, unit) {
const value = this.auditValue(prop);
return value === '-' ? value : `${value}${unit}`;
},
handleCertificationApprove() { handleCertificationApprove() {
auditCertification(this.certificationAuditForm.id, 1).then(() => { auditCertification(this.certificationAuditForm.id, 1).then(() => {
this.$message.success('认证已通过'); this.$message.success('认证已通过');
@@ -1693,11 +1725,19 @@ export default {
} }
} }
.certification-audit, .certification-audit {
:deep(.el-form-item__label) {
flex: 0 0 calc(4em + 12px) !important;
width: calc(4em + 12px) !important;
max-width: calc(4em + 12px);
}
}
.certification-audit__reason { .certification-audit__reason {
:deep(.el-form-item__label) { :deep(.el-form-item__label) {
flex: 0 0 calc(7em + 24px) !important; flex: 0 0 calc(4em + 24px) !important;
width: calc(7em + 24px) !important; width: calc(4em + 24px) !important;
max-width: calc(4em + 24px);
} }
} }
+272 -159
View File
@@ -144,9 +144,24 @@
> >
撤回 撤回
</el-link> </el-link>
<el-link
type="primary"
v-if="
hasPermission('customer_type_re_cert') &&
row.approvalStatus === 'reviewing' &&
row.reviewStatus !== '已完成'
"
@click="openReview(row)"
>
复评
</el-link>
<el-link <el-link
type="success" type="success"
v-if="hasPermission('customer_archive_approve') && row.approvalStatus === 'reviewing'" v-if="
hasPermission('customer_archive_approve') &&
row.approvalStatus === 'reviewing' &&
row.reviewStatus === '已完成'
"
@click="handleApprove(row)" @click="handleApprove(row)"
> >
审核通过 审核通过
@@ -517,7 +532,7 @@
<span>{{ formatScoreValue(row.selfScore) }}</span> <span>{{ formatScoreValue(row.selfScore) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="复评得分" min-width="145" align="center"> <el-table-column v-if="archiveForm.id" label="复评得分" min-width="145" align="center">
<template #default="{ row }"> <template #default="{ row }">
<span>{{ formatScoreValue(row.reviewScore) }}</span> <span>{{ formatScoreValue(row.reviewScore) }}</span>
</template> </template>
@@ -553,11 +568,18 @@
<span>{{ row.reviewStatus }}</span> <span>{{ row.reviewStatus }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="135" fixed="right" align="center"> <el-table-column label="操作" width="160" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)"> <el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)">
详情 详情
</el-link> </el-link>
<el-link
v-if="showScoreDelete"
type="danger"
@click="deleteScore(row.__rawIndex)"
>
删除
</el-link>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -624,6 +646,7 @@
<el-table-column label="开户人姓名" prop="accountHolderName" min-width="150" /> <el-table-column label="开户人姓名" prop="accountHolderName" min-width="150" />
<el-table-column label="收款账号" prop="bankAccount" min-width="180" /> <el-table-column label="收款账号" prop="bankAccount" min-width="180" />
<el-table-column label="开户行" prop="bankName" min-width="180" /> <el-table-column label="开户行" prop="bankName" min-width="180" />
<el-table-column label="联行号" prop="cnapsCode" min-width="160" />
<el-table-column label="备注" prop="remark" min-width="180" /> <el-table-column label="备注" prop="remark" min-width="180" />
<el-table-column <el-table-column
label="操作" label="操作"
@@ -875,43 +898,10 @@
/> />
</section-card> </section-card>
<el-dialog <change-record-detail-dialog
v-model="changeRecordDetailVisible" v-model="changeRecordDetailVisible"
title="变更记录详情" :rows="changeRecordDetailRows"
append-to-body
destroy-on-close
width="1100px"
top="10px"
class="change-record-detail-dialog"
>
<div v-if="changeRecordDetail" class="change-record-detail-meta">
<span>变更日期{{ changeRecordDetail.changeTime || '-' }}</span>
<span>变更账号{{ changeRecordDetail.changeUserName || '-' }}</span>
</div>
<el-table :data="changeRecordDetailRows" border :show-overflow-tooltip="false">
<el-table-column prop="field" label="变更字段" min-width="180" />
<el-table-column
prop="before"
label="变更前"
min-width="360"
class-name="change-record-detail-value"
/> />
<el-table-column
prop="after"
label="变更后"
min-width="500"
class-name="change-record-detail-value"
/>
</el-table>
<el-empty
v-if="!changeRecordDetailRows.length"
description="暂无变更内容"
:image-size="60"
/>
<template #footer>
<el-button type="primary" @click="changeRecordDetailVisible = false">关闭</el-button>
</template>
</el-dialog>
<div class="archive-form__footer" v-if="!isPublicViewPage"> <div class="archive-form__footer" v-if="!isPublicViewPage">
<!-- 次要独立页返回 / 只读关闭 / 弹窗取消 --> <!-- 次要独立页返回 / 只读关闭 / 弹窗取消 -->
@@ -1022,6 +1012,9 @@
<el-form-item label="开户行" prop="bankName"> <el-form-item label="开户行" prop="bankName">
<el-input v-model="receiptForm.bankName" maxlength="100" /> <el-input v-model="receiptForm.bankName" maxlength="100" />
</el-form-item> </el-form-item>
<el-form-item label="联行号" prop="cnapsCode">
<el-input v-model="receiptForm.cnapsCode" maxlength="32" />
</el-form-item>
<el-form-item label="收款账号" prop="bankAccount"> <el-form-item label="收款账号" prop="bankAccount">
<el-input v-model="receiptForm.bankAccount" maxlength="50" /> <el-input v-model="receiptForm.bankAccount" maxlength="50" />
</el-form-item> </el-form-item>
@@ -1139,17 +1132,18 @@
<el-input v-model="row.email" maxlength="100" :disabled="readonly" /> <el-input v-model="row.email" maxlength="100" :disabled="readonly" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="所属部门" min-width="200"> <el-table-column label="所属部门" min-width="220">
<template #default="{ row }"> <template #default="{ row }">
<el-input <el-input
v-if="!isInternalCustomer" v-if="!isInternalCustomer"
:model-value="row.deptNames" :model-value="row.deptNames"
disabled disabled
/> />
<el-select <el-cascader
v-else v-else
v-model="row.deptIdList" v-model="row.deptIdList"
multiple :options="deptTree"
:props="invoiceDeptCascaderProps"
collapse-tags collapse-tags
collapse-tags-tooltip collapse-tags-tooltip
filterable filterable
@@ -1157,14 +1151,7 @@
:disabled="readonly" :disabled="readonly"
placeholder="请选择所属部门" placeholder="请选择所属部门"
@change="() => handleInvoiceContactDeptChange(row)" @change="() => handleInvoiceContactDeptChange(row)"
>
<el-option
v-for="item in invoiceDeptOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/> />
</el-select>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="备注" min-width="160"> <el-table-column label="备注" min-width="160">
@@ -1319,7 +1306,25 @@
class="score-detail-table score-detail-table--inline" class="score-detail-table score-detail-table--inline"
max-height="520" max-height="520"
> >
<el-table-column label="评分项目" prop="itemName" width="150" align="center" /> <el-table-column
label="评分项目"
width="150"
align="center"
:show-overflow-tooltip="false"
>
<template #default="{ row: detail }">
<el-tooltip
:content="detail.itemName"
placement="top"
effect="dark"
append-to="body"
:show-after="200"
:disabled="!detail.itemName"
>
<span class="score-item-name">{{ detail.itemName }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="评分标准" min-width="430"> <el-table-column label="评分标准" min-width="430">
<template #default="{ row: detail }"> <template #default="{ row: detail }">
<div class="score-standard-cell"> <div class="score-standard-cell">
@@ -1329,6 +1334,7 @@
inputmode="decimal" inputmode="decimal"
:disabled="readonly" :disabled="readonly"
@input="value => handleScoreValueInput(detail, value)" @input="value => handleScoreValueInput(detail, value)"
@blur="handleScoreValueBlur(detail)"
> >
<template #append>{{ getScoreRule(detail)?.changeUnit || '' }}</template> <template #append>{{ getScoreRule(detail)?.changeUnit || '' }}</template>
</el-input> </el-input>
@@ -1358,13 +1364,15 @@
<span>{{ formatScoreValue(getSignedScore(detail, 'selfScore')) }}</span> <span>{{ formatScoreValue(getSignedScore(detail, 'selfScore')) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="复评得分" width="150" align="center"> <el-table-column v-if="!isNewScore" label="复评得分" width="150" align="center">
<template #default="{ row: detail }"> <template #default="{ row: detail }">
<el-input <el-input
v-model="detail.reviewScore" v-model="detail.reviewScore"
maxlength="10" maxlength="10"
inputmode="decimal" inputmode="decimal"
:disabled="readonly || !hasPermission('customer_type_re_cert')" :disabled="
(!reviewMode && readonly) || !hasPermission('customer_type_re_cert')
"
@input="value => handleScoreDetailScoreInput(detail, 'reviewScore', value)" @input="value => handleScoreDetailScoreInput(detail, 'reviewScore', value)"
> >
<template <template
@@ -1385,7 +1393,7 @@
<template #default="{ row }"> <template #default="{ row }">
<span class="score-category-table__name">{{ row.categoryName }}</span> <span class="score-category-table__name">{{ row.categoryName }}</span>
<span>自评{{ getSignedScore(row, 'selfScore') }}</span> <span>自评{{ getSignedScore(row, 'selfScore') }}</span>
<span>复评{{ getSignedScore(row, 'reviewScore') }}</span> <span v-if="!isNewScore">复评{{ getSignedScore(row, 'reviewScore') }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column width="60" align="center"> <el-table-column width="60" align="center">
@@ -1459,12 +1467,15 @@
<div class="score-detail-dialog__total"> <div class="score-detail-dialog__total">
<span>总分合计</span> <span>总分合计</span>
<span>自评{{ formatScoreValue(currentScore.selfScore) }}</span> <span>自评{{ formatScoreValue(currentScore.selfScore) }}</span>
<span>复评{{ formatScoreValue(currentScore.reviewScore) }}</span> <span v-if="!isNewScore">复评{{ formatScoreValue(currentScore.reviewScore) }}</span>
</div> </div>
<el-button @click="scoreBox = false">取消</el-button> <el-button @click="scoreBox = false">取消</el-button>
<el-button type="primary" plain v-if="!readonly" @click="saveScoreDetail">保存</el-button> <el-button v-if="isNewScore" type="primary" @click="submitSelfScore">提交自评</el-button>
<el-button type="primary" plain v-if="!readonly && !isNewScore" @click="saveScoreDetail">
保存
</el-button>
<el-button <el-button
v-if="!readonly && hasPermission('customer_type_re_cert')" v-if="!isNewScore && (!readonly || reviewMode) && hasPermission('customer_type_re_cert')"
type="primary" type="primary"
@click="confirmReviewScore" @click="confirmReviewScore"
> >
@@ -1500,7 +1511,7 @@ import {
getDetail as getCreditScoreQuantificationDetail, getDetail as getCreditScoreQuantificationDetail,
} from '@/api/vehicle/credit-score-quantification'; } from '@/api/vehicle/credit-score-quantification';
import { getDictionary } from '@/api/system/dictbiz'; import { getDictionary } from '@/api/system/dictbiz';
import { processDelete, processSubmit } from '@/api/system/business-process'; import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { getDeptTree } from '@/api/system/dept'; import { getDeptTree } from '@/api/system/dept';
import { getLazyTree } from '@/api/base/region'; import { getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common'; import { exportBlob } from '@/api/common';
@@ -1519,6 +1530,7 @@ import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
import SectionCard from '@/components/section-card/main.vue'; import SectionCard from '@/components/section-card/main.vue';
import PdfPreview from '@/components/pdf-preview/main.vue'; import PdfPreview from '@/components/pdf-preview/main.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
const createTimeRangeMap = { const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'], createTimeRange: ['createTimeStart', 'createTimeEnd'],
@@ -1534,11 +1546,18 @@ const qualificationViewerPlugins = [
export default { export default {
components: { components: {
SectionCard, SectionCard,
ChangeRecordDetailDialog,
ArrowRight, ArrowRight,
ElImageViewer, ElImageViewer,
OpenFileViewer, OpenFileViewer,
PdfPreview, PdfPreview,
}, },
props: {
embeddedPublicId: {
type: [String, Number],
default: '',
},
},
data() { data() {
const nonNegativeLabelMap = { const nonNegativeLabelMap = {
invoiceTaxRate: '开票税点', invoiceTaxRate: '开票税点',
@@ -1621,6 +1640,8 @@ export default {
receiptBox: false, receiptBox: false,
invoiceBox: false, invoiceBox: false,
readonly: false, readonly: false,
reviewMode: false,
listActivated: false,
originalAccessType: '', originalAccessType: '',
activeTab: 'scores', activeTab: 'scores',
archiveForm: this.emptyArchive(), archiveForm: this.emptyArchive(),
@@ -1779,6 +1800,7 @@ export default {
accountName: [{ required: true, message: '请输入收款单位名称', trigger: 'blur' }], accountName: [{ required: true, message: '请输入收款单位名称', trigger: 'blur' }],
accountHolderName: [{ required: true, message: '请输入开户人姓名', trigger: 'blur' }], accountHolderName: [{ required: true, message: '请输入开户人姓名', trigger: 'blur' }],
bankName: [{ required: true, message: '请输入开户行', trigger: 'blur' }], bankName: [{ required: true, message: '请输入开户行', trigger: 'blur' }],
cnapsCode: [{ required: true, message: '请输入联行号', trigger: 'blur' }],
bankAccount: [{ required: true, message: '请输入收款账号', trigger: 'blur' }], bankAccount: [{ required: true, message: '请输入收款账号', trigger: 'blur' }],
remark: [{ max: 200, message: '备注最多200个字符', trigger: 'blur' }], remark: [{ max: 200, message: '备注最多200个字符', trigger: 'blur' }],
}, },
@@ -1814,7 +1836,7 @@ export default {
editBtn: false, editBtn: false,
selection: true, selection: true,
dialogClickModal: false, dialogClickModal: false,
menuWidth: 280, menuWidth: 320,
column: [ column: [
{ {
label: '客商编号', label: '客商编号',
@@ -1994,7 +2016,7 @@ export default {
created() { created() {
if (this.isPublicViewPage) { if (this.isPublicViewPage) {
this.readonly = true; this.readonly = true;
const id = this.$route.query.id; const id = this.embeddedPublicId || this.$route.query.id;
if (!id) { if (!id) {
this.archiveBox = true; this.archiveBox = true;
this.$message.error('缺少客商ID'); this.$message.error('缺少客商ID');
@@ -2008,10 +2030,20 @@ export default {
this.initBusinessDictionaries(); this.initBusinessDictionaries();
this.initScoreQuantificationOptions(); this.initScoreQuantificationOptions();
if (this.isArchivePage) { if (this.isArchivePage) {
const readonly = this.$route.query.view === '1' || this.$route.query.view === 'true'; this.reviewMode = this.$route.query.review === '1';
const readonly =
this.$route.query.view === '1' || this.$route.query.view === 'true' || this.reviewMode;
this.openArchive(this.$route.query.id ? { id: this.$route.query.id } : null, readonly); this.openArchive(this.$route.query.id ? { id: this.$route.query.id } : null, readonly);
} }
}, },
activated() {
if (this.isArchivePage || this.isPublicViewPage) return;
if (!this.listActivated) {
this.listActivated = true;
return;
}
this.onLoad(this.page, this.query);
},
computed: { computed: {
...mapGetters(['permission', 'userInfo']), ...mapGetters(['permission', 'userInfo']),
isAdmin() { isAdmin() {
@@ -2029,7 +2061,9 @@ export default {
); );
}, },
isPublicViewPage() { isPublicViewPage() {
return this.$route.path === '/vehicle/customer-archive/public-view'; return (
this.$route.path === '/vehicle/customer-archive/public-view' || !!this.embeddedPublicId
);
}, },
archiveContainer() { archiveContainer() {
return 'div'; return 'div';
@@ -2055,6 +2089,18 @@ export default {
expandTrigger: 'click', expandTrigger: 'click',
}; };
}, },
//
invoiceDeptCascaderProps() {
return {
value: 'value',
label: 'label',
children: 'children',
multiple: true,
checkStrictly: true,
emitPath: false,
expandTrigger: 'click',
};
},
// archiveForm.deptId ID // archiveForm.deptId ID
// //
// deptTree // deptTree
@@ -2095,6 +2141,7 @@ export default {
return ids.join(','); return ids.join(',');
}, },
dialogTitle() { dialogTitle() {
if (this.reviewMode) return '客商复评';
if (this.readonly) return '查看客商档案'; if (this.readonly) return '查看客商档案';
return this.archiveForm.id ? '编辑客商档案' : '新增客商档案'; return this.archiveForm.id ? '编辑客商档案' : '新增客商档案';
}, },
@@ -2115,12 +2162,6 @@ export default {
isInternalCustomer() { isInternalCustomer() {
return this.archiveForm.customerKind === 'internal'; return this.archiveForm.customerKind === 'internal';
}, },
invoiceDeptOptions() {
return this.flattenDept(this.deptTree).map(item => ({
label: item.rawLabel,
value: String(item.value),
}));
},
isTemporaryToFormal() { isTemporaryToFormal() {
return ( return (
Boolean(this.archiveForm.id) && Boolean(this.archiveForm.id) &&
@@ -2138,6 +2179,14 @@ export default {
scoreList() { scoreList() {
return this.archiveForm.scores || []; return this.archiveForm.scores || [];
}, },
showScoreDelete() {
if (this.readonly) return false;
const status = this.archiveForm.approvalStatus || 'draft';
return status === 'draft' || status === 'rejected';
},
isNewScore() {
return this.scoreRecordIndex < 0 && !this.reviewMode;
},
scorePageCount() { scorePageCount() {
return Math.max(Math.ceil(this.scoreList.length / this.scorePage.pageSize), 1); return Math.max(Math.ceil(this.scoreList.length / this.scorePage.pageSize), 1);
}, },
@@ -2695,6 +2744,7 @@ export default {
accountName: '', accountName: '',
accountHolderName: '', accountHolderName: '',
bankName: '', bankName: '',
cnapsCode: '',
bankAccount: '', bankAccount: '',
registeredPhone: '', registeredPhone: '',
registeredAddress: '', registeredAddress: '',
@@ -3382,7 +3432,17 @@ export default {
this.invoiceForm.contacts.splice(index, 1); this.invoiceForm.contacts.splice(index, 1);
}, },
handleInvoiceContactDeptChange(row) { handleInvoiceContactDeptChange(row) {
const ids = Array.isArray(row.deptIdList) ? row.deptIdList.map(item => String(item)) : []; // emitPath=false id
const rawList = Array.isArray(row.deptIdList) ? row.deptIdList : [];
const ids = rawList
.map(item => {
if (Array.isArray(item)) {
return item.length ? String(item[item.length - 1]) : '';
}
return item !== undefined && item !== null && item !== '' ? String(item) : '';
})
.filter(Boolean);
row.deptIdList = ids;
row.deptIds = ids.join(','); row.deptIds = ids.join(',');
row.deptNames = ids.map(id => this.findDeptLabel(id)).filter(Boolean).join('、'); row.deptNames = ids.map(id => this.findDeptLabel(id)).filter(Boolean).join('、');
}, },
@@ -3948,6 +4008,12 @@ export default {
query: { id: row.id, name: '查看客商档案', view: '1' }, query: { id: row.id, name: '查看客商档案', view: '1' },
}); });
}, },
openReview(row) {
this.$router.push({
path: '/vehicle/customer-archive/form',
query: { id: row.id, name: '客商复评', review: '1' },
});
},
closeArchive() { closeArchive() {
if (this.isPublicViewPage) return; if (this.isPublicViewPage) return;
this.archiveBox = false; this.archiveBox = false;
@@ -3979,6 +4045,7 @@ export default {
if (this.isPublicViewPage) this.ensurePublicViewOptions(this.archiveForm); if (this.isPublicViewPage) this.ensurePublicViewOptions(this.archiveForm);
this.archiveBox = true; this.archiveBox = true;
this.loadChangeRecords(); this.loadChangeRecords();
if (this.reviewMode) this.openReviewScore();
}) })
.catch(() => { .catch(() => {
this.archiveBox = true; this.archiveBox = true;
@@ -4020,14 +4087,15 @@ export default {
this.invoiceForm = this.emptyInvoice(); this.invoiceForm = this.emptyInvoice();
this.$refs.archiveForm?.clearValidate(); this.$refs.archiveForm?.clearValidate();
}, },
// //
validateFormalForApproval(archive) { validateScoreForApproval(archive) {
if (archive.accessType !== 'formal') return true; const scores = archive.scores || [];
const hasCompletedScore = (archive.scores || []).some( if (scores.length > 1) {
item => item.selfStatus === '已完成' || item.reviewStatus === '已完成' || item.finalScore this.$message.warning('评分记录只能有一条');
); return false;
if (!hasCompletedScore) { }
this.$message.warning('正式客商提交审核前必须完成信用评分'); if (scores.length !== 1 || scores[0].selfStatus !== '已完成') {
this.$message.warning('提交审批前必须完成自评');
return false; return false;
} }
return true; return true;
@@ -4079,7 +4147,7 @@ export default {
.filter(item => item.contactName || item.contactPhone); .filter(item => item.contactName || item.contactPhone);
archive.receiptAccounts = (archive.receiptAccounts || []) archive.receiptAccounts = (archive.receiptAccounts || [])
.map(item => this.normalizeReceipt(item)) .map(item => this.normalizeReceipt(item))
.filter(item => item.accountName || item.accountHolderName || item.bankAccount); .filter(item => item.accountName || item.accountHolderName || item.bankAccount || item.cnapsCode);
archive.invoices = (archive.invoices || []) archive.invoices = (archive.invoices || [])
.map(item => { .map(item => {
const invoice = this.normalizeInvoice(item); const invoice = this.normalizeInvoice(item);
@@ -4137,7 +4205,7 @@ export default {
const archive = this.normalizeArchive(); const archive = this.normalizeArchive();
// //
archive.recordChange = true; archive.recordChange = true;
if (!this.validateFormalForApproval(archive)) return; if (!this.validateScoreForApproval(archive)) return;
submit(archive).then(res => { submit(archive).then(res => {
const data = res.data.data; const data = res.data.data;
const id = (data && typeof data === 'object' ? data.id : data) || this.archiveForm.id; const id = (data && typeof data === 'object' ? data.id : data) || this.archiveForm.id;
@@ -4165,44 +4233,31 @@ export default {
}); });
}, },
/** /**
* 列表 / 新增 / 编辑提交共用提交 MK 审核流并更新客商审批状态 * 列表 / 新增 / 编辑提交共用开启 MK 时提交审核流并更新客商审批状态
*/ */
submitCustomerApproval(id, fullName = '', approvalStatus = '') { submitCustomerApproval(id, fullName = '', approvalStatus = '') {
return this.submitMkApprovalFlow(id, fullName, approvalStatus).then(() => submitApproval(id)); return submitMkApprovalFlow({
bizType: 'customer-archive',
formInstanceId: id,
subjectName: fullName,
approvalStatus,
}).then(() => submitApproval(id));
}, },
/** openReviewScore() {
* 提交 MK 审核流templateCode 取业务字典 mk_template 提交客商审核流的键值 const scores = this.archiveForm.scores || [];
* 提交人手机号由后端从用户表读取真实值前端接口会脱敏 if (!scores.length) {
* MK 驳回后再提交时先删除旧流程再创建新流程 this.$message.warning('暂无评分记录,无法复评');
*/ return;
async submitMkApprovalFlow(formInstanceId, subjectName = '', approvalStatus = '') {
if (approvalStatus === 'rejected') {
await processDelete({ formInstanceId: String(formInstanceId) });
} }
const templateCode = await this.resolveMkTemplateCode('提交客商审核流'); this.$nextTick(() => this.openScore(scores[0], 0));
const subject = subjectName
? `客商准入审批:${subjectName}`
: `客商准入审批:${formInstanceId}`;
return processSubmit({
templateCode,
formInstanceId: String(formInstanceId),
subject,
});
},
async resolveMkTemplateCode(dictName = '提交客商审核流') {
const res = await getDictionary({ code: 'mk_template' });
const list = res?.data?.data || [];
const matched = list.find(item => String(item.dictValue || '').trim() === dictName);
const templateCode = matched?.dictKey;
if (!templateCode) {
this.$message.warning(`未配置业务字典 mk_template「${dictName}」,无法提交审核流`);
return Promise.reject(new Error(`未配置业务字典 mk_template「${dictName}`));
}
return String(templateCode);
}, },
addScore() { addScore() {
if ((this.archiveForm.scores || []).length) {
this.$message.warning('请勿重复添加');
return;
}
this.scoreRecordIndex = -1; this.scoreRecordIndex = -1;
this.currentScore = this.normalizeScore({}); this.currentScore = this.normalizeScore({ applyCreditLimit: '' });
this.scoreAttachmentFiles = []; this.scoreAttachmentFiles = [];
this.expandedScoreCategoryKeys = []; this.expandedScoreCategoryKeys = [];
this.scoreBox = true; this.scoreBox = true;
@@ -4284,7 +4339,7 @@ export default {
JSON.stringify( JSON.stringify(
(item.options || []).map(option => ({ (item.options || []).map(option => ({
label: option.label || option.optionName || option.value, label: option.label || option.optionName || option.value,
value: option.value || option.optionName || option.label, value: option.value ?? option.optionName ?? option.label,
score: option.score, score: option.score,
changeType: option.changeType, changeType: option.changeType,
changeValue: option.changeValue, changeValue: option.changeValue,
@@ -4374,20 +4429,21 @@ export default {
const isScoreOption = item.changeType || item.changeValue || item.changeUnit; const isScoreOption = item.changeType || item.changeValue || item.changeUnit;
const changeLabel = item.changeType === 'decrease' ? '每减少' : '每增加'; const changeLabel = item.changeType === 'decrease' ? '每减少' : '每增加';
const scoreLabel = item.scoreType === 'subtract' ? '减' : '加'; const scoreLabel = item.scoreType === 'subtract' ? '减' : '加';
const rawScore = item.score;
const generatedLabel = isScoreOption const generatedLabel = isScoreOption
? `${changeLabel}${item.changeValue || ''}${item.changeUnit || ''}${scoreLabel}${ ? `${changeLabel}${item.changeValue || ''}${item.changeUnit || ''}${scoreLabel}${
item.score || '' this.isBlankScore(rawScore) ? '' : rawScore
}` }`
: ''; : '';
const label = item.label || item.optionName || item.value || generatedLabel; const label = item.label || item.optionName || item.value || generatedLabel;
return { return {
...item, ...item,
label, label,
value: item.value || item.optionName || item.label || generatedLabel, value: item.value ?? item.optionName ?? item.label ?? generatedLabel,
score: score:
item.scoreType === 'subtract' && item.score !== undefined item.scoreType === 'subtract' && !this.isBlankScore(rawScore)
? -Math.abs(Number(item.score)) ? -Math.abs(Number(rawScore))
: item.score, : rawScore,
}; };
}); });
if (Array.isArray(value)) return normalize(value); if (Array.isArray(value)) return normalize(value);
@@ -4451,23 +4507,35 @@ export default {
const valid = increase ? input >= base : input <= base; const valid = increase ? input >= base : input <= base;
if (!valid) { if (!valid) {
detail.selfScore = ''; detail.selfScore = '';
this.$message.warning(`输入值${increase ? '不能小于' : '不能大于'}基准数值${base}`);
this.calculateScore(this.currentScore); this.calculateScore(this.currentScore);
return; return;
} }
detail.selfScore = this.getScoreTypeCalculatedScore(detail); detail.selfScore = this.getScoreTypeCalculatedScore(detail);
this.calculateScore(this.currentScore); this.calculateScore(this.currentScore);
}, },
handleScoreValueBlur(detail) {
const normalized = String(detail.scoreInput ?? '').trim();
if (!normalized || normalized === '-' || normalized === '.') return;
const rule = this.getScoreRule(detail);
const base = Number(detail.baseValue);
const input = Number(normalized);
if (!rule || !Number.isFinite(base) || !Number.isFinite(input)) return;
const increase = rule.changeType !== 'decrease';
const valid = increase ? input >= base : input <= base;
if (!valid) {
this.$message.warning(`输入值${increase ? '不能小于' : '不能大于'}基准数值${base}`);
}
},
scoreOptionChange(detail, optionValue) { scoreOptionChange(detail, optionValue) {
const option = this.parseScoreOptions(detail.optionsJson).find( const option = this.parseScoreOptions(detail.optionsJson).find(
item => item.value === optionValue item => item.value === optionValue
); );
const score = Number(option?.score || 0); const rawScore = option?.score;
detail.selfScore = score; detail.selfScore = this.isBlankScore(rawScore) ? 0 : Number(rawScore);
this.calculateScore(this.currentScore); this.calculateScore(this.currentScore);
}, },
handleScoreDetailScoreInput(detail, prop, value) { handleScoreDetailScoreInput(detail, prop, value) {
const normalized = String(value || '') const normalized = String(value ?? '')
.replace(/[^\d.]/g, '') .replace(/[^\d.]/g, '')
.replace(/^\./, '') .replace(/^\./, '')
.replace(/(\..*)\./g, '$1') .replace(/(\..*)\./g, '$1')
@@ -4599,8 +4667,7 @@ export default {
sumScore(plusDetails, 'reviewScore') - sumScore(plusDetails, 'reviewScore') -
sumScore(minusDetails, 'reviewScore'); sumScore(minusDetails, 'reviewScore');
const hasReviewScore = details.some( const hasReviewScore = details.some(
item => item => !this.isBlankScore(item.reviewScore)
item.reviewScore !== undefined && item.reviewScore !== null && item.reviewScore !== ''
); );
const finalScore = hasReviewScore ? reviewScore : selfScore; const finalScore = hasReviewScore ? reviewScore : selfScore;
const fullMark = this.getScoreFullMark(details); const fullMark = this.getScoreFullMark(details);
@@ -4620,13 +4687,17 @@ export default {
} }
return score; return score;
}, },
isBlankScore(value) {
return value === undefined || value === null || String(value).trim() === '';
},
hasIncompleteBasicScoreDetail(details = []) { hasIncompleteBasicScoreDetail(details = []) {
return this.getScoreCategoryDetailsByList(details, 'basic').some(item => { return this.getScoreCategoryDetailsByList(details, 'basic').some(item => {
if (this.isScoreTypeDetail(item)) { if (this.isScoreTypeDetail(item)) {
const input = String(item.scoreInput ?? '').trim(); const input = String(item.scoreInput ?? '').trim();
return !input || !Number.isFinite(Number(input)) || item.selfScore === ''; const inputBlank = input === '' || input === '-' || input === '.';
return inputBlank || !Number.isFinite(Number(input)) || this.isBlankScore(item.selfScore);
} }
return !item.selectedOption; return this.isBlankScore(item.selectedOption);
}); });
}, },
finishScore(score) { finishScore(score) {
@@ -4643,10 +4714,13 @@ export default {
score.reviewStatus = '已完成'; score.reviewStatus = '已完成';
this.$message.success('评分已完成'); this.$message.success('评分已完成');
}, },
saveScoreDetail() { saveScoreDetail(successMessage) {
if (!this.validateScoreForm(false)) return; if (!this.validateScoreForm(false)) return;
this.currentScore.attachments = JSON.parse(JSON.stringify(this.scoreAttachmentFiles || [])); this.currentScore.attachments = JSON.parse(JSON.stringify(this.scoreAttachmentFiles || []));
this.currentScore.proofAttachments = this.stringifyAttachments(this.scoreAttachmentFiles); this.currentScore.proofAttachments = this.stringifyAttachments(this.scoreAttachmentFiles);
this.currentScore.selfStatus = this.hasIncompleteBasicScoreDetail(this.currentScore.details)
? '未完成'
: '已完成';
this.calculateScore(this.currentScore); this.calculateScore(this.currentScore);
const score = JSON.parse(JSON.stringify(this.currentScore)); const score = JSON.parse(JSON.stringify(this.currentScore));
if (this.scoreRecordIndex > -1) { if (this.scoreRecordIndex > -1) {
@@ -4658,23 +4732,62 @@ export default {
this.syncArchiveCreditFromScores(); this.syncArchiveCreditFromScores();
this.activeTab = 'scores'; this.activeTab = 'scores';
this.scoreBox = false; this.scoreBox = false;
this.saveScoreArchive(); const pending = this.saveScoreArchive(successMessage);
}, if (this.reviewMode && successMessage && pending && pending.then) {
saveScoreArchive() { pending.then(() => this.closeArchive());
if (!this.archiveForm.id) {
this.$message.success('评分记录已添加,请保存客商档案后生效');
return;
} }
submit(this.normalizeArchive()).then(() => { return pending;
this.$message.success('评分记录已保存'); },
deleteScore(index) {
this.$confirm('确定删除该评分记录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
if (!Array.isArray(this.archiveForm.scores)) this.archiveForm.scores = [];
this.archiveForm.scores.splice(index, 1);
if (this.scoreRecordIndex === index) {
this.scoreBox = false;
} else if (this.scoreRecordIndex > index) {
this.scoreRecordIndex -= 1;
}
const latestScore = this.getLatestCreditScore(this.archiveForm.scores);
if (latestScore) {
this.syncArchiveCreditFromScores();
} else {
this.archiveForm.customerLevel = '';
this.archiveForm.maxCreditLimit = '';
this.archiveForm.applyCreditLimit = '';
}
this.handleScoreCurrentChange(this.scorePage.currentPage);
this.saveScoreArchive('评分记录已删除', '评分记录已删除,请保存客商档案后生效');
})
.catch(() => {});
},
saveScoreArchive(
successMessage = '评分记录已保存',
draftMessage = '评分记录已添加,请保存客商档案后生效'
) {
if (!this.archiveForm.id) {
this.$message.success(draftMessage);
return Promise.resolve();
}
return submit(this.normalizeArchive()).then(() => {
this.$message.success(successMessage);
}); });
}, },
submitSelfScore() {
if (!this.validateScoreForm(true)) return;
this.currentScore.selfStatus = '已完成';
this.currentScore.reviewStatus = '未完成';
this.saveScoreDetail('自评已提交');
},
confirmReviewScore() { confirmReviewScore() {
if (!this.validateScoreForm(true)) return; if (!this.validateScoreForm(true)) return;
this.currentScore.selfStatus = '已完成'; this.currentScore.selfStatus = '已完成';
this.currentScore.reviewStatus = '已完成'; this.currentScore.reviewStatus = '已完成';
this.saveScoreDetail(); this.saveScoreDetail('复评确认成功');
this.$message.success('复评确认成功');
}, },
validateScoreForm(requireComplete) { validateScoreForm(requireComplete) {
if (!this.currentScore.scoreDate) { if (!this.currentScore.scoreDate) {
@@ -4712,7 +4825,7 @@ export default {
if ( if (
item.reviewScore === undefined || item.reviewScore === undefined ||
item.reviewScore === null || item.reviewScore === null ||
item.reviewScore === '' String(item.reviewScore).trim() === ''
) { ) {
return false; return false;
} }
@@ -4729,7 +4842,7 @@ export default {
return false; return false;
} }
if (requireComplete && this.hasIncompleteBasicScoreDetail(this.currentScore.details)) { if (requireComplete && this.hasIncompleteBasicScoreDetail(this.currentScore.details)) {
this.$message.warning('请完整填写基础得分项评分明细'); this.$message.warning('请完整填写所有基础项目后再提交自评');
return false; return false;
} }
return true; return true;
@@ -4784,7 +4897,7 @@ export default {
getDetail(row.id).then(res => { getDetail(row.id).then(res => {
const archive = this.normalizeDetail(res.data.data); const archive = this.normalizeDetail(res.data.data);
archive.qualificationAttachments = archive.qualificationAttachments || ''; archive.qualificationAttachments = archive.qualificationAttachments || '';
if (!this.validateFormalForApproval(archive)) return; if (!this.validateScoreForApproval(archive)) return;
this.$confirm('是否提交客商准入审批?', '提示', { this.$confirm('是否提交客商准入审批?', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
@@ -5271,10 +5384,16 @@ export default {
} }
:deep(.el-table__body td) { :deep(.el-table__body td) {
//min-height: 112px;
color: #303133; color: #303133;
} }
:deep(.score-item-name) {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.el-select), :deep(.el-select),
:deep(.el-input), :deep(.el-input),
:deep(.el-select) { :deep(.el-select) {
@@ -5309,13 +5428,28 @@ export default {
.invoice-form { .invoice-form {
max-width: none; max-width: none;
:deep(.el-input),
:deep(.el-cascader),
:deep(.el-select) {
width: 100%;
}
:deep(.el-cascader .el-input) {
width: 100%;
}
} }
.invoice-contact-table { .invoice-contact-table {
width: 100%; width: 100%;
:deep(.el-input), :deep(.el-input),
:deep(.el-select) { :deep(.el-select),
:deep(.el-cascader) {
width: 100%;
}
:deep(.el-cascader .el-input) {
width: 100%; width: 100%;
} }
} }
@@ -5447,27 +5581,6 @@ export default {
word-break: break-word; word-break: break-word;
} }
:deep(.change-record-detail-dialog .el-dialog__body) {
max-height: 65vh;
overflow: auto;
}
:deep(.change-record-detail-dialog .change-record-detail-value .cell) {
white-space: pre-wrap;
word-break: break-word;
overflow: visible;
text-overflow: clip;
line-height: 1.6;
}
.change-record-detail-meta {
display: flex;
gap: 32px;
margin-bottom: 16px;
color: #606266;
font-size: 14px;
}
.customer-archive-page.is-public-view { .customer-archive-page.is-public-view {
:deep(.archive-page-form .archive-form__footer) { :deep(.archive-page-form .archive-form__footer) {
left: 0; left: 0;
+2 -2
View File
@@ -564,8 +564,8 @@ export default {
this.addressMapPickerVisible = true; this.addressMapPickerVisible = true;
} }
}, },
handleAddressMapConfirm(address) { handleAddressMapConfirm(payload) {
this.form.address = address; this.form.address = typeof payload === 'string' ? payload : payload?.address || '';
}, },
hasPermission(code) { hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false); return this.isAdmin || this.validData(this.permission[code], false);
+2 -2
View File
@@ -545,8 +545,8 @@ export default {
this.addressMapPickerVisible = true; this.addressMapPickerVisible = true;
} }
}, },
handleAddressMapConfirm(address) { handleAddressMapConfirm(payload) {
this.form.address = address; this.form.address = typeof payload === 'string' ? payload : payload?.address || '';
}, },
hasPermission(code) { hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false); return this.isAdmin || this.validData(this.permission[code], false);
+2 -2
View File
@@ -288,8 +288,8 @@ export default {
this.locationMapPickerVisible = true; this.locationMapPickerVisible = true;
} }
}, },
handleLocationMapConfirm(address) { handleLocationMapConfirm(payload) {
this.form.location = address; this.form.location = typeof payload === 'string' ? payload : payload?.address || '';
}, },
hasPermission(code) { hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false); return this.isAdmin || this.validData(this.permission[code], false);
+3 -1
View File
@@ -51,8 +51,10 @@ export default ({ mode, command }) => {
port: 2888, port: 2888,
proxy: { proxy: {
'/api': { '/api': {
// 本地网关默认 80 端口(blade-gateway bootstrap.yml);开发时需先启动网关及 blade-auth/blade-system 等服务
// 注意:本机 8080 一般是 Nacos,不是业务网关
target: 'http://localhost', target: 'http://localhost',
//target: 'https://saber3.bladex.cn/api', // target: 'http://172.16.203.228:8000', // 仅联调远程网关时启用(且不要 rewrite)
changeOrigin: true, changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''), rewrite: path => path.replace(/^\/api/, ''),
}, },
+7 -4
View File
@@ -570,19 +570,22 @@
- 功能点: - 功能点:
- 任意状态的运单均支持复制。 - 任意状态的运单均支持复制。
- 复制后生成新的草稿运单,运单号重新生成。 - 复制后运单号重新生成,业务状态与原运单保持一致
- 复制时保留运单基础信息、货物信息、承运信息和过程配置相关信息,状态与过程记录重新初始化。 - 复制时保留运单基础信息、货物信息、承运信息和过程配置相关信息;过程打卡记录重新初始化。
- 复制成功后跳转编辑页面 - 若原运单为已完成,复制保存后按合同系统计费规则自动生成对应应收、应付明细
- 复制成功后跳转编辑页面(独立表单模式)或刷新列表(弹窗模式)。
- 异常与边界: - 异常与边界:
- 原运单不存在时提示数据不存在。 - 原运单不存在时提示数据不存在。
- 复制后名称、编号或关联字段触发唯一性校验时应重新处理。 - 复制后名称、编号或关联字段触发唯一性校验时应重新处理。
- 复制失败时不影响原运单数据。 - 复制失败时不影响原运单数据。
- 合同未开启系统计费、无匹配计费方案或自有运输等场景下,按既有规则跳过对应应收/应付生成。
- 反例: - 反例:
- 不允许复制后沿用原运单号。 - 不允许复制后沿用原运单号。
- 不允许复制后直接生成进行中或已完成状态。 - 不允许复制后状态与原运单不一致
- 不允许复制后保留原过程节点完成记录。 - 不允许复制后保留原过程节点完成记录。
- 不允许已完成运单复制成功后遗漏按规则应生成的应收应付明细。
--- ---