9 Commits

Author SHA1 Message Date
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
weicw1996 815810f841 批量导入上传提示注明日期宽容写法
8 个 CRUD 导入上传 tip 与运单导入弹窗、运输计划导入页的提示文案
统一补充「日期支持 2026-08-02、2026-8-2、2026/8/2 等写法」,
配合后端宽容日期解析(weicw/tms-erp#1)。
2026-09-21 19:08:56 +08:00
weicw1996 4e04cdbc5b 统一运输方式兜底文案为公路运输
去掉计划单调度、运输计划展示中的「公路整车」旧文案
2026-09-21 19:08:55 +08:00
b2894lxlx 57368f0179 调试mk 2026-09-21 11:14:51 +08:00
b2894lxlx 7078de6385 调试mk 2026-09-20 18:12:51 +08:00
b2894lxlx 4af9afda61 调试mk 2026-09-20 13:17:05 +08:00
67 changed files with 4734 additions and 964 deletions
+19
View File
@@ -33,6 +33,25 @@ export const getPunchRecords = waybillId =>
method: 'get',
params: { waybillId },
});
/** 运单车辆实时定位 */
export const locateVehicle = id =>
request({
url: `${baseUrl}/locate`,
method: 'post',
params: { id },
timeout: 60000,
});
/** 运单车辆历史轨迹 */
export const trackVehicle = (id, startDate, endDate) =>
request({
url: `${baseUrl}/track`,
method: 'post',
params: { id, startDate, endDate },
timeout: 60000,
});
export const roadLoading = ids =>
request({
url: `${baseUrl}/road-loading`,
+23
View File
@@ -0,0 +1,23 @@
import request from '@/axios';
export const getMkPublicDetail = (bizType, id) => {
return request({
url: `/blade-transport/mk-process/public/${bizType}/detail`,
method: 'get',
meta: {
isToken: false,
},
params: { id },
});
};
export const postMkPublicProcessMessage = (bizType, data) => {
return request({
url: `/blade-transport/mk-process/public/${bizType}/process-message`,
method: 'post',
meta: {
isToken: false,
},
data,
});
};
+1 -1
View File
@@ -24,7 +24,7 @@ export const syncKingdeeBatch = ids =>
export const paymentTypeOptions = [
{ label: '项目预付', value: 'project_advance' },
{ label: '进度预付', value: 'progress_advance' },
{ label: '结算付款', value: 'settlement_payment' },
{ label: '尾款付款', value: 'settlement_payment' },
];
export const approvalStatusOptions = [
{ label: '草稿', value: 'draft' },
+31
View File
@@ -0,0 +1,31 @@
import request from '@/axios';
/**
* 调用 MK processSubmit 提交审核流
* @param {Object} data
* @param {string} data.templateCode 模板编码
* @param {string} data.submitIdentity 提交人(手机号)
* @param {string} data.loginName 登录账号(手机号)
* @param {string} data.formInstanceId 业务表单实例 id
* @param {string} [data.subject] 流程标题
*/
export const processSubmit = data => {
return request({
url: '/blade-system/businessProcess/processSubmit',
method: 'post',
data,
});
};
/**
* 调用 MK processDelete 删除审核流(驳回后重新提交前使用)
* @param {Object} data
* @param {string} data.formInstanceId 业务表单实例 id
*/
export const processDelete = data => {
return request({
url: '/blade-system/businessProcess/processDelete',
method: 'post',
data,
});
};
+35
View File
@@ -48,6 +48,41 @@ 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) => {
return request({
url: '/blade-system/dept/sync-oa-company',
method: 'post',
params: {
current,
size,
},
timeout: 60000,
signal,
});
};
export const syncOaDepartment = (current = 1, size = 20, signal) => {
return request({
url: '/blade-system/dept/sync-oa-department',
method: 'post',
params: {
current,
size,
},
timeout: 60000,
signal,
});
};
export const update = row => {
return request({
url: '/blade-system/dept/submit',
+39
View File
@@ -22,6 +22,19 @@ export const getDetail = id => {
});
};
export const getPublicDetail = id => {
return request({
url: '/blade-transport/customer-archive/public/detail',
method: 'get',
meta: {
isToken: false,
},
params: {
id,
},
});
};
export const getChangeRecordList = (customerId, current, size) => {
return request({
url: '/blade-transport/customer-archive/change-record/list',
@@ -34,6 +47,32 @@ export const getChangeRecordList = (customerId, current, size) => {
});
};
export const getPublicChangeRecordList = (customerId, current, size) => {
return request({
url: '/blade-transport/customer-archive/public/change-record/list',
method: 'get',
meta: {
isToken: false,
},
params: {
customerId,
current,
size,
},
});
};
export const postPublicProcessMessage = data => {
return request({
url: '/blade-transport/customer-archive/public/process-message',
method: 'post',
meta: {
isToken: false,
},
data,
});
};
export const submit = row => {
return request({
url: '/blade-transport/customer-archive/submit',
+47 -5
View File
@@ -42,6 +42,14 @@ export default {
type: String,
default: '',
},
longitude: {
type: [String, Number],
default: '',
},
latitude: {
type: [String, Number],
default: '',
},
},
emits: ['update:modelValue', 'confirm'],
data() {
@@ -66,9 +74,11 @@ export default {
this.visible = value;
if (value) {
this.keyword = this.address || '';
this.selected = {};
this.status = '可搜索地址或点击地图选点';
this.searchResults = [];
this.selected = this.buildInitialSelection();
this.status = this.selected.longitude
? this.selected.address || '已选点,可确认回填'
: '可搜索地址或点击地图选点';
}
},
},
@@ -82,6 +92,18 @@ export default {
}
},
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() {
if (window.AMap && window.AMap.Map) {
return Promise.resolve();
@@ -110,15 +132,27 @@ export default {
return new Promise(resolve => {
this.$nextTick(() => {
if (!this.$refs.map) return resolve(null);
const initial = this.buildInitialSelection();
const center =
Number.isFinite(initial.longitude) && Number.isFinite(initial.latitude)
? [initial.longitude, initial.latitude]
: [116.40769, 39.89945];
if (!this.amap) {
this.amap = new window.AMap.Map(this.$refs.map, {
center: [116.40769, 39.89945],
zoom: 11,
center,
zoom: initial.longitude ? 14 : 11,
});
this.amap.on('click', event => this.pickPoint(event.lnglat));
} else {
this.amap.resize();
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);
});
@@ -254,11 +288,19 @@ export default {
}
},
confirm() {
if (!this.selected.longitude || !this.selected.latitude) {
this.$message.warning('请先搜索或点击地图完成选点');
return;
}
if (!this.selected.address) {
this.$message.warning('请等待地址解析完成后再确认');
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;
},
},
@@ -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>
+15
View File
@@ -38,6 +38,21 @@ export const createOption = () => ({
menuWidth: 240,
menuFixed: 'right',
column: [
{
label: '计量单位编码',
prop: 'unitCode',
minWidth: 150,
span: 24,
search: true,
searchOrder: 4,
searchSpan: 6,
maxlength: 50,
showWordLimit: true,
rules: [
{ required: true, message: '请输入计量单位编码', trigger: 'blur' },
{ max: 50, message: '计量单位编码不能超过50个字', trigger: 'blur' },
],
},
{
label: '计量单位',
prop: 'unitName',
@@ -334,7 +334,7 @@ export const option = {
},
...auditColumns.map(column => ({ ...column, hide: true })),
])),
dialogWidth: '96%',
dialogWidth: '1100px',
menuFixed: 'right',
menuWidth: 320,
index: false,
+3 -2
View File
@@ -5,6 +5,7 @@ export const GRANT_TYPE_DIC = [
{ label: '社交登录', value: 'social' },
{ label: '客户端凭证', value: 'client_credentials' },
{ label: '刷新令牌', value: 'refresh_token' },
{ label: '退出登录', value: 'logout' },
];
export const authLogOption = {
@@ -38,7 +39,7 @@ export const authLogOption = {
prop: 'realName',
},
{
label: '授权类型',
label: '操作类型',
prop: 'grantType',
type: 'select',
search: true,
@@ -80,7 +81,7 @@ export const authLogOption = {
width: 120,
},
{
label: '登录时间',
label: '操作时间',
prop: 'loginTime',
sortable: true,
span: 24,
+2 -5
View File
@@ -1,12 +1,9 @@
import { getDeptLazyTree } from '@/api/system/dept';
import { validateLoginPassword } from '@/utils/validate';
export const userOption = safe => {
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请输入密码'));
} else {
callback();
}
validateLoginPassword(rule, value, callback);
};
const validatePass2 = (rule, value, callback) => {
if (value === '') {
+3 -1
View File
@@ -24,6 +24,7 @@
-->
<userLogin v-if="activeName === 'user'"></userLogin>
<registerLogin v-else-if="activeName === 'register'"></registerLogin>
<!-- 暂时默认账号密码登录IAM 选择入口先隐藏
<div v-else class="iam-login">
<el-button type="primary" class="login-submit" @click.prevent="handleIamLogin">
IAM统一身份认证
@@ -32,6 +33,7 @@
账号密码登录
</el-button>
</div>
-->
</div>
</div>
</div>
@@ -61,7 +63,7 @@ export default {
return {
website: website,
time: '',
activeName: 'iam',
activeName: 'user',
socialForm: {
tenantId: '000000',
source: '',
+76
View File
@@ -74,6 +74,82 @@ export default [
isAuth: false,
},
},
{
path: '/vehicle/customer-archive/public-view',
name: '查看客商信息',
component: () => import('@/views/vehicle/customer-archive-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
},
},
{
path: '/business/project-apply/public-view',
name: '查看项目信息',
component: () => import('@/views/business/project-apply-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'project-apply',
},
},
{
path: '/business/contract-manage/public-view',
name: '查看合同信息',
component: () => import('@/views/business/contract-manage-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'contract-manage',
},
},
{
path: '/business/waybill-manage/public-view',
name: '查看运单信息',
component: () => import('@/views/business/waybill-manage-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'waybill-manage',
},
},
{
path: '/settlement/pre-settlement/public-view',
name: '查看预结算信息',
component: () => import('@/views/settlement/pre-settlement-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'pre-settlement',
},
},
{
path: '/settlement/formal-settlement/public-view',
name: '查看正式结算信息',
component: () => import('@/views/settlement/formal-settlement-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'formal-settlement',
},
},
{
path: '/payment/payment-application/public-view',
name: '查看付款申请信息',
component: () => import('@/views/payment/payment-application-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'payment-application',
},
},
{
path: '/',
name: '主页',
+94
View File
@@ -0,0 +1,94 @@
import { getDictionary } from '@/api/system/dictbiz';
import { processDelete, processSubmit } from '@/api/system/business-process';
import { ElMessage } from 'element-plus';
export const MK_BIZ = {
'customer-archive': {
dictName: '提交客商审核流',
subjectPrefix: '客商准入审批',
rejected: ['rejected'],
},
'project-apply': {
dictName: '提交项目审核流',
subjectPrefix: '项目审批',
rejected: ['rejected', 'change_rejected', 'withdrawn'],
},
'contract-manage': {
dictName: '提交合同审核流',
subjectPrefix: '合同审批',
rejected: ['rejected', 'change_rejected', 'withdrawn'],
},
'waybill-manage': {
dictName: '提交运单审核流',
subjectPrefix: '运单审批',
rejected: ['rejected', 'returned'],
},
'pre-settlement': {
dictName: '提交预结算审核流',
subjectPrefix: '预结算审批',
rejected: ['returned'],
},
'formal-settlement': {
dictName: '提交正式结算审核流',
subjectPrefix: '正式结算审批',
rejected: ['returned'],
},
'payment-application': {
dictName: '提交付款审核流',
subjectPrefix: '付款审批',
rejected: ['returned'],
},
};
const MK_SWITCH_NAME = '开启MK';
async function loadMkTemplateList() {
const res = await getDictionary({ code: 'mk_template' });
return res?.data?.data || [];
}
/** 业务字典 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;
if (!templateCode) {
ElMessage.warning(`未配置业务字典 mk_template「${dictName}」,无法提交审核流`);
return Promise.reject(new Error(`未配置业务字典 mk_template「${dictName}`));
}
return String(templateCode);
}
export async function submitMkApprovalFlow({
bizType,
formInstanceId,
subjectName = '',
approvalStatus = '',
}) {
const conf = MK_BIZ[bizType];
if (!conf) {
return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`));
}
const list = await loadMkTemplateList();
if (!isMkEnabled(list)) {
return;
}
if ((conf.rejected || []).includes(approvalStatus)) {
await processDelete({ formInstanceId: String(formInstanceId) });
}
const templateCode = await resolveMkTemplateCode(conf.dictName, list);
const subject = subjectName
? `${conf.subjectPrefix}${subjectName}`
: `${conf.subjectPrefix}${formInstanceId}`;
return processSubmit({
templateCode,
formInstanceId: String(formInstanceId),
subject,
bizType,
});
}
+28
View File
@@ -283,3 +283,31 @@ export function validatejson(val) {
// 非对象、非数组、非字符串,或者字符串不是 JSON
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();
}
+58 -4
View File
@@ -94,6 +94,17 @@
/>
</el-select>
</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>
<el-input
v-model="form.longitude"
@@ -121,6 +132,13 @@
</template>
</avue-form>
</el-dialog>
<address-map-picker
v-model="addressMapPickerVisible"
:address="form.detailAddress"
:longitude="form.longitude"
:latitude="form.latitude"
@confirm="handleAddressMapConfirm"
/>
</basic-container>
</template>
@@ -134,6 +152,7 @@ import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -181,6 +200,9 @@ const addExcelRequiredHeaderMarks = async blob => {
};
export default {
components: {
AddressMapPicker,
},
data() {
const validateIataCode = (rule, value, callback) => {
if (!/^[A-Z]{3}$/.test(String(value || '').toUpperCase())) {
@@ -220,6 +242,7 @@ export default {
loading: true,
data: [],
excelBox: false,
addressMapPickerVisible: false,
excelForm: {},
provinceOptions: [],
cityOptions: [],
@@ -368,15 +391,14 @@ export default {
{
label: '详细地址',
prop: 'detailAddress',
type: 'textarea',
minRows: 2,
formslot: true,
span: 24,
minWidth: 220,
overHidden: false,
maxlength: 255,
showWordLimit: true,
rules: [
{ required: true, message: '请输入详细地址', trigger: 'blur' },
{ required: true, message: '请选择详细地址', trigger: 'change' },
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
],
},
@@ -504,7 +526,7 @@ export default {
propsHttp: {
res: 'data',
},
tip: '请上传 .xls,.xlsx 标准格式文件',
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
accept: '.xls,.xlsx',
action: '/blade-system/airport-master/import-airport-master',
},
@@ -669,6 +691,29 @@ export default {
handleCoordinateInput(prop, 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) {
row.code = row.iataCode ? `JC-${row.iataCode}` : row.code;
row.regionCode = String(row.regionCode || '').trim();
@@ -917,4 +962,13 @@ export default {
white-space: nowrap;
}
}
.detail-address-input {
cursor: pointer;
:deep(.el-input__wrapper),
:deep(.el-input__inner) {
cursor: pointer;
}
}
</style>
+1 -1
View File
@@ -274,7 +274,7 @@ export default {
res: 'data',
},
headers: getUploadHeaders(),
tip: '请上传 .xls,.xlsx 标准格式文件',
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
action: '/blade-system/cargo-type/import-cargo-type',
},
{
+1 -1
View File
@@ -326,7 +326,7 @@ export default {
propsHttp: {
res: 'data',
},
tip: '请上传 .xls,.xlsx 标准格式文件',
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
action: '/blade-system/currency/import-currency',
},
{
+1
View File
@@ -119,6 +119,7 @@ export default {
return this.isAdmin || this.permission?.[code] === true;
},
normalizeRow(row) {
row.unitCode = String(row.unitCode || '').trim();
row.unitName = String(row.unitName || '').trim();
row.dimension = String(row.dimension || '').trim();
row.remark = String(row.remark || '').trim();
+43 -6
View File
@@ -194,10 +194,11 @@
<el-input
v-model="form.detailAddress"
class="detail-address-input"
clearable
readonly
maxlength="255"
show-word-limit
placeholder="请输入"
placeholder="点击选择地图地址"
@click="openAddressMapPicker"
/>
</template>
<template #remark-form>
@@ -244,6 +245,13 @@
</template>
</avue-form>
</el-dialog>
<address-map-picker
v-model="addressMapPickerVisible"
:address="form.detailAddress"
:longitude="form.longitude"
:latitude="form.latitude"
@confirm="handleAddressMapConfirm"
/>
</basic-container>
</template>
@@ -264,12 +272,16 @@ import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const DEFAULT_COUNTRY_CODE = '+86';
const newLocal = '请选择类型';
export default {
components: {
AddressMapPicker,
},
data() {
const validateCode = (rule, value, callback) => {
const code = String(value || '').toUpperCase();
@@ -315,6 +327,7 @@ export default {
loading: true,
data: [],
excelBox: false,
addressMapPickerVisible: false,
excelForm: {},
portOptions: [],
countryOptions: [],
@@ -572,11 +585,11 @@ export default {
minWidth: 220,
overHidden: false,
order: 85,
placeholder: '请输入',
placeholder: '点击选择地图地址',
maxlength: 255,
showWordLimit: true,
rules: [
{ required: true, message: '请输入详细地址', trigger: 'blur' },
{ required: true, message: '请选择详细地址', trigger: 'change' },
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
],
},
@@ -673,7 +686,7 @@ export default {
propsHttp: {
res: 'data',
},
tip: '请上传 .xls,.xlsx 标准格式文件',
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
accept: '.xls,.xlsx',
action: '/blade-system/port-terminal/import-port-terminal',
},
@@ -1040,6 +1053,21 @@ export default {
handleCoordinateInput(prop, 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) {
if (!country) {
return Promise.resolve('');
@@ -1107,7 +1135,7 @@ export default {
return false;
}
if (isBlank(row.detailAddress)) {
this.$message.warning('请输入详细地址');
this.$message.warning('请选择详细地址');
return false;
}
if (isBlank(row.longitude)) {
@@ -1399,6 +1427,15 @@ export default {
line-height: 20px;
padding: 4px 0;
}
.detail-address-input {
cursor: pointer;
:deep(.el-input__wrapper),
:deep(.el-input__inner) {
cursor: pointer;
}
}
</style>
<style lang="scss">
+57 -5
View File
@@ -94,6 +94,17 @@
/>
</el-select>
</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>
<el-input
v-model="form.longitude"
@@ -121,6 +132,13 @@
</template>
</avue-form>
</el-dialog>
<address-map-picker
v-model="addressMapPickerVisible"
:address="form.detailAddress"
:longitude="form.longitude"
:latitude="form.latitude"
@confirm="handleAddressMapConfirm"
/>
</basic-container>
</template>
@@ -134,12 +152,16 @@ import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const DEFAULT_COUNTRY_CODE = '+86';
export default {
components: {
AddressMapPicker,
},
data() {
const validateTmisCode = (rule, value, callback) => {
if (!/^\d{5}$/.test(value || '')) {
@@ -185,6 +207,7 @@ export default {
loading: true,
data: [],
excelBox: false,
addressMapPickerVisible: false,
excelForm: {},
provinceOptions: [],
cityOptions: [],
@@ -340,15 +363,14 @@ export default {
{
label: '详细地址',
prop: 'detailAddress',
type: 'textarea',
minRows: 2,
formslot: true,
span: 24,
minWidth: 220,
overHidden: false,
maxlength: 255,
showWordLimit: true,
rules: [
{ required: true, message: '请输入详细地址', trigger: 'blur' },
{ required: true, message: '请选择详细地址', trigger: 'change' },
{ max: 255, message: '详细地址不能超过255字', trigger: 'blur' },
],
},
@@ -475,7 +497,7 @@ export default {
propsHttp: {
res: 'data',
},
tip: '请上传 .xls,.xlsx 标准格式文件',
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
accept: '.xls,.xlsx',
action: '/blade-system/railway-station/import-railway-station',
},
@@ -652,6 +674,29 @@ export default {
handleCoordinateInput(prop, 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) {
row.code = row.tmisCode ? `TL${row.tmisCode}` : row.code;
row.regionCode = String(row.regionCode || '').trim();
@@ -910,9 +955,16 @@ export default {
:deep(.el-table td .cell) {
white-space: nowrap;
}
}
.detail-address-input {
cursor: pointer;
:deep(.el-input__wrapper),
:deep(.el-input__inner) {
cursor: pointer;
}
}
</style>
<style lang="scss">
+1 -1
View File
@@ -309,7 +309,7 @@ export default {
propsHttp: {
res: 'data',
},
tip: '请上传 .xls,.xlsx 标准格式文件',
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
action: '/blade-system/region/import-region',
},
{
@@ -157,12 +157,19 @@
<el-table-column label="计费单位" width="150"
><template #default="{ row }"
><span v-if="readonly">{{ displayValue(row.billingUnit) }}</span
><el-select v-else v-model="row.billingUnit" clearable filterable :loading="unitLoading"
><el-select
v-else
v-model="row.billingUnit"
clearable
filterable
:disabled="!canSelectBillingUnit(row)"
:loading="unitLoading"
:placeholder="billingUnitPlaceholder(row)"
><el-option
v-for="item in unitOptions"
:key="item.id || item.dictKey || item.dictValue"
:label="item.dictValue"
:value="item.dictValue" /></el-select></template
v-for="item in unitOptionsFor(row)"
:key="item.id || item.value"
:label="item.label"
:value="item.value" /></el-select></template
></el-table-column>
<el-table-column label="单价" width="180"
><template #default="{ row }"
@@ -367,12 +374,26 @@
<script>
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
import { getList as getFeeItemList } from '@/api/base/fee-item';
import { getList as getMeasurementUnitList } from '@/api/base/measurement-unit';
import { getLazyTree as getRegionLazyTree } from '@/api/base/region';
import { InfoFilled } from '@element-plus/icons-vue';
import { getDictionary } from '@/api/system/dictbiz';
import SectionCard from '@/components/section-card/main.vue';
const clone = value => JSON.parse(JSON.stringify(value));
/** 计费要素与计量单位维度的对应关系(这些要素可选计费单位) */
const BILLING_ELEMENT_DIMENSION_MAP = {
按重量: '重量',
按体积: '体积',
按数量: '数量',
};
const BILLING_UNIT_SELECTABLE_ELEMENTS = Object.keys(BILLING_ELEMENT_DIMENSION_MAP);
/** 固定计费单位(不可编辑) */
const FIXED_BILLING_UNIT_MAP = {
按里程: '公里',
'按吨·公里': '吨公里',
'固定金额(整单一口价)': '单',
};
const defaultRule = () => ({
feeType: '',
feeItem: '',
@@ -417,11 +438,11 @@ export default {
feeItems: {},
feeItemLoadingMap: {},
unitOptions: [],
measurementUnits: [],
unitLoading: false,
billingElements: [
'按重量',
'按体积',
'按车辆',
'按里程',
'按吨·公里',
'固定金额(整单一口价)',
@@ -430,7 +451,6 @@ export default {
typeMap: {
按重量: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
按体积: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
按车辆: ['固定单价'],
按里程: ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
'按吨·公里': ['固定单价', '区间单价', '阶梯单价', '区间阶梯一口价'],
'固定金额(整单一口价)': ['固定一口价'],
@@ -545,6 +565,7 @@ export default {
next.limitRanges = next.limitRanges.map(item => ({ ...item, minimumBillingWeight: '' }));
this.syncLegacyLimit(next);
next.matchCondition = { ...defaultRule().matchCondition, ...(rule.matchCondition || {}) };
this.syncBillingUnit(next);
return next;
},
normalizeRanges(row) {
@@ -614,15 +635,76 @@ export default {
.finally(() => {
this.feeCategoryLoading = false;
});
this.loadUnitOptions();
},
loadUnitOptions() {
this.unitLoading = true;
getDictionary({ code: 'unit_fee' })
.then(res => {
Promise.all([
getDictionary({ code: 'unit_fee' }).then(res => {
this.unitOptions = res.data?.data || [];
})
.finally(() => {
}),
getMeasurementUnitList(1, 9999, { status: 1 }).then(res => {
const data = res?.data?.data || res?.data || {};
const records = Array.isArray(data) ? data : data.records || [];
this.measurementUnits = records.filter(
item => item.status === undefined || item.status === null || Number(item.status) === 1
);
}),
]).finally(() => {
this.unitLoading = false;
});
},
measurementDimension(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) {
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);
if (dimension) {
return this.measurementUnits
.filter(item => String(item.dimension || '').trim() === dimension)
.map(item => ({
id: item.id,
label: item.unitName,
value: item.unitName,
}))
.filter(item => item.value);
}
return [];
},
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);
if (!options.some(item => String(item.value) === String(row.billingUnit || ''))) {
row.billingUnit = '';
}
},
feeTypeKey(row) {
const option = this.feeCategories.find(
item =>
@@ -699,7 +781,14 @@ export default {
return this.typeMap[row.billingElement] || [];
},
handleElementChange(row) {
if (!this.billingTypes(row).includes(row.billingType)) row.billingType = '';
const billingTypes = this.billingTypes(row);
if (!billingTypes.includes(row.billingType)) {
row.billingType =
row.billingElement === '固定金额(整单一口价)' && billingTypes.includes('固定一口价')
? '固定一口价'
: '';
}
this.syncBillingUnit(row);
if (!this.canEditMinimum(row)) {
row.minimumBillingWeight = '';
row.limitRanges = (row.limitRanges || []).map(item => ({
@@ -831,7 +920,6 @@ export default {
['taxRate', '税率'],
['billingElement', '计费要素'],
['billingType', '计费类型'],
['billingUnit', '计费单位'],
];
for (const [i, row] of this.draft.rules.entries()) {
const empty = required.find(
@@ -841,6 +929,15 @@ export default {
this.$message.warning(`${i + 1}${empty[1]}不能为空`);
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();
if (feeItemSet.has(feeItem)) {
this.$message.warning(`费用项“${feeItem}”不能重复`);
@@ -1899,11 +1899,11 @@
label-width="auto"
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
v-model="settlementRuleForm.billStartDate"
type="date"
placeholder="请选择账单起始日"
placeholder="请选择账单起始日"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="dialogReadonly"
@@ -3374,7 +3374,7 @@
{{ dispatchDialogStatusText }}
</el-tag>
<el-tag type="primary" effect="light">
{{ formatDispatchTransportType(dispatchRow.transportType) || '公路整车' }}
{{ formatDispatchTransportType(dispatchRow.transportType) || '公路运输' }}
</el-tag>
</div>
</div>
@@ -7784,8 +7784,8 @@ export default {
const rule = this.normalizeSettlementRule(source);
this.settlementRuleErrors = {};
if (this.isBillingFieldEmpty(rule.billStartDate)) {
this.settlementRuleErrors = { billStartDate: '请选择账单起始日' };
this.$message.warning(`${label}${label ? '' : ''}请选择账单起始日`);
this.settlementRuleErrors = { billStartDate: '请选择账单起始日' };
this.$message.warning(`${label}${label ? '' : ''}请选择账单起始日`);
return false;
}
if (this.isBillingFieldEmpty(rule.settlementType)) {
@@ -103,8 +103,13 @@
<div class="contract-attachment-upload-dialog__body">
<div class="contract-attachment-upload-dialog__field">
<span class="contract-attachment-upload-dialog__label">附件位置</span>
<el-select :model-value="attachmentLocation" disabled style="width: 100%">
<el-option :label="attachmentLocation" :value="attachmentLocation" />
<el-select v-model="uploadDialogLocation" placeholder="请选择附件位置" style="width: 100%">
<el-option
v-for="item in resolvedLocationOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</div>
<div class="contract-attachment-upload-dialog__field">
@@ -273,15 +278,17 @@ export default {
markApprovedOnUpload: Boolean,
useUploadDialog: Boolean,
attachmentLocation: { type: String, default: '合同文件' },
attachmentLocationOptions: { type: Array, default: null },
preview: { type: Function, default: null },
},
emits: ['update:rows'],
emits: ['update:rows', 'upload-to-location'],
data() {
return {
selected: [],
attachmentFileTypes,
contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions],
uploadDialogVisible: false,
uploadDialogLocation: '合同文件',
uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions),
uploadDialogFiles: [],
uploadDialogFileList: [],
@@ -300,6 +307,11 @@ export default {
defaultAttachmentType() {
return resolveOtherAttachmentType(this.contractAttachmentTypeOptions);
},
resolvedLocationOptions() {
const options = (this.attachmentLocationOptions || []).filter(Boolean);
if (options.length) return options;
return ['合同文件', '其它附件'];
},
},
created() {
this.loadAttachmentTypeOptions();
@@ -340,6 +352,10 @@ export default {
this.update([...this.rows]);
},
openUploadDialog() {
const options = this.resolvedLocationOptions;
this.uploadDialogLocation = options.includes(this.attachmentLocation)
? this.attachmentLocation
: options[0] || this.attachmentLocation;
this.uploadDialogType = this.defaultAttachmentType;
this.uploadDialogFiles = [];
this.uploadDialogFileList = [];
@@ -348,6 +364,7 @@ export default {
resetUploadDialog() {
this.uploadDialogFiles = [];
this.uploadDialogFileList = [];
this.uploadDialogLocation = this.attachmentLocation;
this.uploadDialogType = this.defaultAttachmentType;
},
beforeUpload(file) {
@@ -416,6 +433,10 @@ export default {
this.$message.warning('请先上传附件');
return;
}
if (!this.uploadDialogLocation) {
this.$message.warning('请选择附件位置');
return;
}
if (!this.uploadDialogType) {
this.$message.warning('请选择附件类型');
return;
@@ -430,7 +451,14 @@ export default {
uploadTime: row.uploadTime || uploadTime,
...(this.markApprovedOnUpload ? { approved: true } : {}),
}));
if (this.uploadDialogLocation === this.attachmentLocation) {
this.update([...(this.rows || []), ...appended]);
} else {
this.$emit('upload-to-location', {
location: this.uploadDialogLocation,
rows: appended,
});
}
this.uploadDialogVisible = false;
},
handleChange(rows) {
@@ -1,7 +1,17 @@
<template>
<div v-loading="loading" class="master-detail">
<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">
<div>
<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)}` : '-'; },
},
async mounted() {
if (!this.id) return;
this.loading = true;
try {
const res = await api.getDetail(this.id);
@@ -282,8 +293,36 @@ export default {
<style scoped lang="scss">
.master-detail { padding-bottom: 20px; color: #303133; }
.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; } }
.transport-flow-tag { margin-left: 8px; }
.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;
}
: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; } }
.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; } }
@@ -295,6 +295,7 @@ export default {
},
methods: {
async load() {
if (!this.id) return;
this.loading = true;
try {
const res = await api.getDetail(this.id);
@@ -1231,7 +1231,7 @@
{{ dispatchDialogStatusText }}
</el-tag>
<el-tag type="primary" effect="light">
{{ formatDispatchTransportType(dispatchRow.transportType) || '公路整车' }}
{{ formatDispatchTransportType(dispatchRow.transportType) || '公路运输' }}
</el-tag>
</div>
</div>
@@ -113,6 +113,12 @@
label="导入方式"
width="100"
/>
<el-table-column
v-if="columnVisible.statusName"
prop="statusName"
label="状态"
width="100"
/>
<el-table-column
v-if="columnVisible.createUserName"
prop="createUserName"
@@ -121,12 +127,6 @@
>
<template #default="{ row }">{{ row.createUserName || '-' }}</template>
</el-table-column>
<el-table-column
v-if="columnVisible.statusName"
prop="statusName"
label="状态"
width="100"
/>
<el-table-column
v-if="columnVisible.createTime"
prop="createTime"
@@ -454,7 +454,7 @@
:on-change="fileChange"
:on-remove="fileRemove"
><el-button type="primary">添加附件</el-button></el-upload
><span class="waybill-import-create__file-tip">请上传运单明细表仅支持 excel 格式</span
><span class="waybill-import-create__file-tip">请上传运单明细表仅支持 excel 格式日期支持 2026-08-022026-8-22026/8/2 等写法</span
><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item
>
</el-form>
@@ -621,8 +621,8 @@ const columnOptions = [
{ prop: 'carrierType', label: '承运类型' },
{ prop: 'waybillCount', label: '运单数' },
{ prop: 'importTypeName', label: '导入方式' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'statusName', label: '状态' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
{ prop: 'updateTime', label: '更新时间' },
];
@@ -1774,12 +1774,17 @@
<div class="waybill-manage-page__waybill-heading">
<strong>运单详情</strong>
<span>{{ detailRow.waybillNo || '-' }}</span>
<span class="detail-status-text status-text-success">{{
displayStatus(detailRow, 'businessStatus')
}}</span>
<span class="detail-status-text detail-transport-type">
{{ getTransportTypeLabel(waybillTransportMode(detailRow)) || '-' }}
</span>
<el-tag :type="statusTagType(detailRow.businessStatus)">
{{ displayStatus(detailRow, 'businessStatus') }}
</el-tag>
<el-tag type="primary">
{{
detailRow.transportTypeName ||
getTransportTypeLabel(detailRow.transportType) ||
waybillTransportMode(detailRow) ||
'-'
}}
</el-tag>
<el-link
v-if="detailRow.id"
class="waybill-manage-page__waybill-route-change-link"
@@ -1927,7 +1932,8 @@
v-if="!waybillIsCarrier(detailRow)"
class="waybill-manage-page__waybill-detail-field"
>
<span>里程(km)</span><strong>{{ detailRow.mileage || '-' }}</strong>
<span>里程(km)</span
><strong>{{ normalizeMileageValue(detailRow.mileage) || '-' }}</strong>
</div>
</template>
<template v-else-if="waybillIsNonRoadTransport(detailRow)">
@@ -2183,7 +2189,11 @@
/>
</div>
</el-tab-pane>
<el-tab-pane label="司机上传" name="driverUpload">
<el-tab-pane
v-if="waybillDriverUploads.length"
label="司机上传"
name="driverUpload"
>
<div
v-loading="waybillPunchRecordsLoading"
class="waybill-manage-page__driver-upload-grid"
@@ -2200,11 +2210,6 @@
{{ photo.label || '凭证' }}
</div>
</div>
<el-empty
v-if="!waybillPunchRecordsLoading && !waybillDriverUploads.length"
description="暂无司机上传数据"
:image-size="50"
/>
</div>
</el-tab-pane>
</el-tabs>
@@ -2212,18 +2217,110 @@
<section-card title="物流轨迹">
<template #extra>
<div class="waybill-manage-page__waybill-track-actions">
<el-button plain @click="handleWaybillTrackAction('playback')"
<el-button
plain
:type="waybillTrackMode === 'playback' ? 'primary' : undefined"
:loading="waybillTrackLoading"
@click="handleWaybillTrackAction('playback')"
>轨迹回放</el-button
>
<el-button plain @click="handleWaybillTrackAction('locate')">实时定位</el-button>
<el-button
plain
:type="waybillTrackMode === 'locate' ? 'primary' : undefined"
:loading="waybillLocateLoading"
@click="handleWaybillTrackAction('locate')"
>实时定位</el-button
>
</div>
</template>
<div class="waybill-manage-page__waybill-map-empty">暂无数据</div>
<div
v-if="waybillTrackMode === 'playback'"
class="waybill-manage-page__waybill-track-toolbar"
>
<span>时间段</span>
<el-date-picker
v-model="waybillTrackDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
:clearable="false"
:disabled="waybillTrackLoading"
/>
<el-button
type="primary"
:loading="waybillTrackLoading"
@click="loadWaybillTrack"
>查询</el-button
>
<span class="waybill-manage-page__waybill-track-total"
>轨迹点 {{ waybillTrackInfo?.total || 0 }} </span
>
</div>
<div
v-if="waybillTrackMode === 'locate'"
class="waybill-manage-page__waybill-locate"
>
<div v-if="waybillLocateInfo" class="waybill-manage-page__waybill-locate-meta">
<div>
<span>车牌号</span><strong>{{ waybillLocateInfo.vehicleNo || '-' }}</strong>
</div>
<div>
<span>定位时间</span><strong>{{ waybillLocateInfo.locateTime || '-' }}</strong>
</div>
<div>
<span>速度</span><strong>{{ waybillLocateInfo.speed || '-' }}</strong>
</div>
<div>
<span>方向</span><strong>{{ waybillLocateInfo.direction || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-locate-address">
<span>地址</span><strong>{{ waybillLocateInfo.address || '-' }}</strong>
</div>
<div>
<span>坐标</span
><strong
>{{
waybillLocateInfo.longitude != null && waybillLocateInfo.latitude != null
? `${waybillLocateInfo.longitude}, ${waybillLocateInfo.latitude}`
: '-'
}}
</strong>
</div>
</div>
<div
ref="waybillLocateMap"
class="waybill-manage-page__waybill-locate-map"
></div>
<div
v-if="!waybillLocateInfo"
class="waybill-manage-page__waybill-map-tip"
>
{{ waybillLocateHint || '暂无数据' }}
</div>
</div>
<div
v-else-if="waybillTrackMode === 'playback'"
class="waybill-manage-page__waybill-locate"
>
<div
ref="waybillTrackMap"
class="waybill-manage-page__waybill-locate-map waybill-manage-page__waybill-locate-map--track"
></div>
<div
v-if="!(waybillTrackInfo?.points || []).length"
class="waybill-manage-page__waybill-map-tip"
>
{{ waybillTrackHint || '暂无数据' }}
</div>
</div>
<div v-else class="waybill-manage-page__waybill-map-empty">暂无数据</div>
</section-card>
</div>
</template>
</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>
</div>
<template v-if="!detailPageLocked" #footer>
@@ -3029,13 +3126,15 @@ import {
getList as getProcessConfigList,
getVoucherImages as getProcessConfigVoucherImages,
} from '@/api/business/process-config';
import { getPunchRecords as getWaybillPunchRecords } 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 { getDictionary } from '@/api/system/dictbiz';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { getToken } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
import { applyTableMenuWidth } from '@/utils/table-menu';
@@ -3292,6 +3391,21 @@ export default {
waybillRouteChangeRecords: [],
waybillRouteChangeDragIndex: -1,
waybillRouteChangeSaving: false,
waybillLocateLoading: false,
waybillLocateInfo: null,
waybillLocateHint: '',
waybillLocateMapInstance: null,
waybillLocateMarker: null,
waybillLocateInfoWindow: null,
waybillTrackMode: '',
waybillTrackLoading: false,
waybillTrackInfo: null,
waybillTrackHint: '',
waybillTrackDateRange: [],
waybillTrackMapInstance: null,
waybillTrackPolyline: null,
waybillTrackStartMarker: null,
waybillTrackEndMarker: null,
mileageDialog: {
visible: false,
submitting: false,
@@ -3538,6 +3652,7 @@ export default {
this.formPageLocked = this.isStandaloneWaybillFormPage;
this.detailPageLocked = this.isStandaloneWaybillDetailPage;
this.formModeLocked = this.$route.query.mode || '';
if (this.isPublicWaybillView) return;
if (this.config.enableAllDept && this.isAdmin) {
this.allDept = 1;
}
@@ -3601,8 +3716,14 @@ export default {
isStandaloneWaybillFormPage() {
return this.isStandaloneWaybillPage && ['add', 'edit'].includes(this.$route.query.mode);
},
isPublicWaybillView() {
return this.$route.path === '/business/waybill-manage/public-view';
},
isStandaloneWaybillDetailPage() {
return this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute;
return (
(this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute) ||
this.isPublicWaybillView
);
},
// $route data formPageLocked
// tab.js fullPath path
@@ -4483,14 +4604,19 @@ export default {
this.waybillVoucherFolder = null;
this.waybillVoucherFolderView = false;
this.waybillVoucherImagesLoaded = false;
const request =
typeof this.api.getDetail === 'function'
const request = this.isPublicWaybillView
? getMkPublicDetail('waybill-manage', row.id)
: typeof this.api.getDetail === 'function'
? this.api.getDetail(row.id)
: Promise.resolve({ data: { data: row } });
request
.then(res => {
const detail = res?.data?.data || res?.data || row;
this.detailRow = detail;
if (this.isPublicWaybillView) {
this.applyFormDetail(detail);
return;
}
this.loadWaybillPunchRecords();
this.loadWaybillVoucherImages();
if (detail.contractId) {
@@ -4523,6 +4649,13 @@ export default {
});
},
closeDetail() {
this.destroyWaybillLocateMap();
this.destroyWaybillTrackMap();
this.waybillTrackMode = '';
this.waybillLocateInfo = null;
this.waybillLocateHint = '';
this.waybillTrackInfo = null;
this.waybillTrackHint = '';
if (this.isStandaloneWaybillDetailPage) {
this.$router.$avueRouter?.closeTag?.();
this.$router.push({ path: '/business/waybill-manage', query: {} });
@@ -4867,7 +5000,262 @@ export default {
}
},
handleWaybillTrackAction(action) {
this.$message.info(action === 'playback' ? '暂无可回放的物流轨迹' : '暂无车辆实时定位数据');
if (action === 'playback') {
this.destroyWaybillLocateMap();
this.waybillTrackMode = 'playback';
this.waybillLocateInfo = null;
this.waybillLocateHint = '';
if (!Array.isArray(this.waybillTrackDateRange) || this.waybillTrackDateRange.length !== 2) {
this.waybillTrackDateRange = this.buildDefaultTrackDateRange();
}
this.loadWaybillTrack();
return;
}
this.destroyWaybillTrackMap();
this.waybillTrackMode = 'locate';
this.waybillTrackInfo = null;
this.waybillTrackHint = '';
this.loadWaybillLocate();
},
buildDefaultTrackDateRange() {
const pad = value => String(value).padStart(2, '0');
const formatDate = date =>
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
const today = new Date();
const yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1);
return [formatDate(yesterday), formatDate(today)];
},
destroyWaybillLocateMap() {
if (this.waybillLocateMarker) {
this.waybillLocateMarker.setMap(null);
this.waybillLocateMarker = null;
}
if (this.waybillLocateInfoWindow) {
this.waybillLocateInfoWindow.close();
this.waybillLocateInfoWindow = null;
}
if (this.waybillLocateMapInstance) {
this.waybillLocateMapInstance.destroy();
this.waybillLocateMapInstance = null;
}
},
destroyWaybillTrackMap() {
if (this.waybillTrackPolyline) {
this.waybillTrackPolyline.setMap(null);
this.waybillTrackPolyline = null;
}
if (this.waybillTrackStartMarker) {
this.waybillTrackStartMarker.setMap(null);
this.waybillTrackStartMarker = null;
}
if (this.waybillTrackEndMarker) {
this.waybillTrackEndMarker.setMap(null);
this.waybillTrackEndMarker = null;
}
if (this.waybillTrackMapInstance) {
this.waybillTrackMapInstance.destroy();
this.waybillTrackMapInstance = null;
}
},
async loadWaybillLocate() {
const waybillId = this.detailRow?.id;
if (!waybillId) {
this.$message.warning('运单信息不完整');
return;
}
if (!this.detailRow.vehicleNo) {
this.$message.warning('运单未绑定车牌号,无法实时定位');
return;
}
this.waybillLocateLoading = true;
this.waybillLocateHint = '正在获取车辆实时定位...';
this.waybillLocateInfo = null;
try {
const locateApi =
typeof this.api.locateVehicle === 'function' ? this.api.locateVehicle : locateVehicle;
const res = await locateApi(waybillId);
const data = res?.data?.data || null;
if (!data || data.longitude == null || data.latitude == null) {
this.waybillLocateInfo = null;
this.waybillLocateHint = '暂无车辆实时定位数据';
this.$message.warning('暂无车辆实时定位数据');
return;
}
this.waybillLocateInfo = data;
this.waybillLocateHint = '';
await this.$nextTick();
await this.renderWaybillLocateMap(data);
} catch (error) {
this.waybillLocateInfo = null;
this.waybillLocateHint = '实时定位获取失败';
this.$message.error(error?.message || '实时定位获取失败');
} finally {
this.waybillLocateLoading = false;
}
},
async loadWaybillTrack() {
const waybillId = this.detailRow?.id;
if (!waybillId) {
this.$message.warning('运单信息不完整');
return;
}
if (!this.detailRow.vehicleNo) {
this.$message.warning('运单未绑定车牌号,无法查询历史轨迹');
return;
}
if (!Array.isArray(this.waybillTrackDateRange) || this.waybillTrackDateRange.length !== 2) {
this.waybillTrackDateRange = this.buildDefaultTrackDateRange();
}
const [startDate, endDate] = this.waybillTrackDateRange;
this.waybillTrackLoading = true;
this.waybillTrackHint = '正在获取历史轨迹...';
this.destroyWaybillTrackMap();
try {
const trackApi =
typeof this.api.trackVehicle === 'function' ? this.api.trackVehicle : trackVehicle;
const res = await trackApi(waybillId, startDate, endDate);
const data = res?.data?.data || null;
const points = Array.isArray(data?.points) ? data.points : [];
if (!data || points.length === 0) {
this.waybillTrackInfo = data || { total: 0, points: [] };
this.waybillTrackHint = '暂无可回放的物流轨迹';
this.$message.warning('暂无可回放的物流轨迹');
return;
}
this.waybillTrackInfo = data;
this.waybillTrackHint = '';
await this.$nextTick();
await this.renderWaybillTrackMap(points);
} catch (error) {
this.waybillTrackInfo = null;
this.waybillTrackHint = '历史轨迹获取失败';
this.$message.error(error?.message || '历史轨迹获取失败');
} finally {
this.waybillTrackLoading = false;
}
},
async renderWaybillLocateMap(locateInfo) {
const longitude = Number(locateInfo?.longitude);
const latitude = Number(locateInfo?.latitude);
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
this.$message.warning('定位坐标无效,无法在地图上展示');
return;
}
try {
await this.loadAmap();
await this.$nextTick();
const container = this.$refs.waybillLocateMap;
if (!container || !window.AMap) {
this.$message.warning('高德地图容器未就绪');
return;
}
this.destroyWaybillLocateMap();
this.waybillLocateMapInstance = new window.AMap.Map(container, {
zoom: 15,
center: [longitude, latitude],
viewMode: '2D',
resizeEnable: true,
});
const content = [
`<div style="padding:4px 2px;line-height:1.6;font-size:12px;">`,
`<div><b>${locateInfo.vehicleNo || '车辆位置'}</b></div>`,
locateInfo.locateTime ? `<div>时间:${locateInfo.locateTime}</div>` : '',
locateInfo.address ? `<div>地址:${locateInfo.address}</div>` : '',
`<div>坐标:${longitude}, ${latitude}</div>`,
`</div>`,
].join('');
this.waybillLocateInfoWindow = new window.AMap.InfoWindow({
content,
offset: new window.AMap.Pixel(0, -30),
});
this.waybillLocateMarker = new window.AMap.Marker({
position: [longitude, latitude],
title: locateInfo.vehicleNo || '车辆位置',
anchor: 'bottom-center',
});
this.waybillLocateMarker.on('click', () => {
this.waybillLocateInfoWindow.open(this.waybillLocateMapInstance, [longitude, latitude]);
});
this.waybillLocateMapInstance.add(this.waybillLocateMarker);
this.waybillLocateInfoWindow.open(this.waybillLocateMapInstance, [longitude, latitude]);
this.waybillLocateMapInstance.setFitView([this.waybillLocateMarker], false, [40, 40, 40, 40]);
setTimeout(() => {
this.waybillLocateMapInstance?.resize?.();
}, 80);
} catch (error) {
window.console.warn('实时定位地图渲染失败', error);
this.$message.error('高德地图渲染失败,请稍后重试');
}
},
async renderWaybillTrackMap(points) {
const path = (points || [])
.map(item => [Number(item.longitude), Number(item.latitude)])
.filter(item => Number.isFinite(item[0]) && Number.isFinite(item[1]));
if (!path.length) {
this.$message.warning('轨迹坐标无效,无法在地图上展示');
return;
}
try {
await this.loadAmap();
await this.$nextTick();
const container = this.$refs.waybillTrackMap;
if (!container || !window.AMap) {
this.$message.warning('高德地图容器未就绪');
return;
}
this.destroyWaybillTrackMap();
this.waybillTrackMapInstance = new window.AMap.Map(container, {
zoom: 12,
center: path[0],
viewMode: '2D',
resizeEnable: true,
});
this.waybillTrackPolyline = new window.AMap.Polyline({
path,
strokeColor: '#409eff',
strokeWeight: 6,
strokeOpacity: 0.9,
lineJoin: 'round',
lineCap: 'round',
showDir: path.length > 1,
});
this.waybillTrackStartMarker = new window.AMap.Marker({
position: path[0],
title: '起点',
anchor: 'bottom-center',
label: {
content: '起',
direction: 'top',
offset: new window.AMap.Pixel(0, -6),
},
});
this.waybillTrackEndMarker = new window.AMap.Marker({
position: path[path.length - 1],
title: '终点',
anchor: 'bottom-center',
label: {
content: '终',
direction: 'top',
offset: new window.AMap.Pixel(0, -6),
},
});
this.waybillTrackMapInstance.add([
this.waybillTrackPolyline,
this.waybillTrackStartMarker,
this.waybillTrackEndMarker,
]);
this.waybillTrackMapInstance.setFitView(
[this.waybillTrackPolyline, this.waybillTrackStartMarker, this.waybillTrackEndMarker],
false,
[48, 48, 48, 48]
);
setTimeout(() => {
this.waybillTrackMapInstance?.resize?.();
}, 80);
} catch (error) {
window.console.warn('历史轨迹地图渲染失败', error);
this.$message.error('高德地图渲染失败,请稍后重试');
}
},
displayStatus(row, prop) {
const formatStatus = this.config.formatStatus;
@@ -4881,7 +5269,9 @@ export default {
: defaultText;
},
statusTagType(status) {
if ([1, 'pending', 'processing', 'approved', 'change_approved'].includes(status)) {
if (
[1, 'pending', 'processing', 'running', 'approved', 'change_approved'].includes(status)
) {
return 'success';
}
if ([2, 'cancelled', 'withdrawn', 'draft'].includes(status)) {
@@ -5152,11 +5542,23 @@ export default {
return;
}
this.getSaveRequest()(submitRow).then(
() => {
res => {
const data = res.data?.data || {};
const id = data.id || submitRow.id;
const after = id
? submitMkApprovalFlow({
bizType: 'waybill-manage',
formInstanceId: id,
subjectName: data.waybillNo || submitRow.waybillNo || '',
approvalStatus: data.approvalStatus || submitRow.approvalStatus || '',
})
: Promise.resolve();
return after.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
this.$message({ type: 'success', message: '提交成功!' });
done();
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
});
},
error => {
window.console.log(error);
@@ -5175,12 +5577,26 @@ export default {
? this.api.updateAttachments
: this.getSaveRequest();
request(submitRow).then(
() => {
res => {
const skipMk = this.attachmentUploadMode;
const data = res.data?.data || {};
const id = data.id || submitRow.id;
const after =
skipMk || !id
? Promise.resolve()
: submitMkApprovalFlow({
bizType: 'waybill-manage',
formInstanceId: id,
subjectName: data.waybillNo || submitRow.waybillNo || '',
approvalStatus: data.approvalStatus || submitRow.approvalStatus || '',
});
return after.then(() => {
this.attachmentUploadMode = false;
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
this.$message({ type: 'success', message: skipMk ? '操作成功!' : '提交成功!' });
done();
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
});
},
error => {
this.attachmentUploadMode = false;
@@ -8164,7 +8580,7 @@ export default {
};
this.mileageForm = {
id: row.id,
mileage: row.mileage === null || row.mileage === undefined ? '' : String(row.mileage),
mileage: this.normalizeMileageValue(row.mileage),
mileageRemark: row.mileageRemark || '',
};
this.$nextTick(() => this.$refs.mileageFormRef?.clearValidate());
@@ -9353,6 +9769,15 @@ export default {
strong {
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 {
@@ -9553,6 +9978,81 @@ export default {
border: 1px solid #eff1f7;
}
&__waybill-locate {
position: relative;
display: flex;
flex-direction: column;
gap: 10px;
}
&__waybill-locate-meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px 16px;
padding: 10px 12px;
border: 1px solid #eff1f7;
background: #fafafa;
color: #606266;
font-size: 13px;
span {
margin-right: 8px;
color: #909399;
}
strong {
color: #303133;
font-weight: 600;
}
}
&__waybill-locate-address {
grid-column: 1 / -1;
}
&__waybill-locate-map {
width: 100%;
min-height: 320px;
height: 320px;
border: 1px solid #eff1f7;
background: #f5f7fa;
}
&__waybill-locate-map--track {
min-height: 420px;
height: 420px;
}
&__waybill-map-tip {
position: absolute;
left: 50%;
top: 50%;
z-index: 2;
transform: translate(-50%, -50%);
padding: 8px 14px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
color: #909399;
font-size: 13px;
text-align: center;
pointer-events: none;
}
&__waybill-track-toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin-bottom: 10px;
color: #606266;
font-size: 13px;
}
&__waybill-track-total {
color: #909399;
}
&__waybill-track-actions {
display: flex;
gap: 8px;
+71 -11
View File
@@ -70,7 +70,7 @@
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="YYYY-MM-DD"
placeholder="请输入"
/>
<span></span>
<el-date-picker
@@ -78,7 +78,7 @@
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="YYYY-MM-DD"
placeholder="请输入"
/>
</div>
</el-form-item>
@@ -92,12 +92,12 @@
</el-input>
</el-form-item>
<el-form-item label="合同金额">
<el-input-number
v-model="form.contractAmount"
:min="0"
:precision="2"
:controls="false"
<el-input
:model-value="contractAmountDisplay"
placeholder="请输入"
clearable
@input="handleContractAmountInput"
@clear="form.contractAmount = null"
/>
</el-form-item>
<el-form-item label="是否范本">
@@ -190,9 +190,11 @@
description
attachment-type
use-upload-dialog
:attachment-location-options="changeAttachmentLocationOptions"
:rows="contractFileRows"
:preview="previewAttachment"
@update:rows="contractFileRows = $event"
@upload-to-location="handleAttachmentUploadToLocation"
/>
<section class="change-section">
@@ -218,7 +220,7 @@
</el-radio-group>
</div>
<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 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>
@@ -258,9 +260,11 @@
attachment-type
use-upload-dialog
attachment-location="其它附件"
:attachment-location-options="changeAttachmentLocationOptions"
:rows="attachments"
:preview="previewAttachment"
@update:rows="attachments = $event"
@upload-to-location="handleAttachmentUploadToLocation"
/>
<section class="change-section change-reason-section">
@@ -283,9 +287,11 @@
attachment-type
use-upload-dialog
attachment-location="变更材料"
:attachment-location-options="changeAttachmentLocationOptions"
:rows="changeMaterials"
:preview="previewAttachment"
@update:rows="changeMaterials = $event"
@upload-to-location="handleAttachmentUploadToLocation"
/>
<div class="page-footer">
<el-button @click="$router.back()">取消</el-button>
@@ -302,6 +308,7 @@
<script>
import * as api from '@/api/business/contract-manage';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import BillingPlanEditor from './components/billing-plan-editor.vue';
import ContractAttachmentSection, {
normalizeContractFileRows,
@@ -342,7 +349,15 @@ const normalizeOptionalPositiveInteger = value => {
return Number.isInteger(number) && number > 0 ? number : null;
};
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);
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : null;
};
@@ -420,6 +435,22 @@ export default {
pageTitle() {
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() {
return this.contractCategoryDictOptions.length
? this.contractCategoryDictOptions
@@ -463,6 +494,13 @@ export default {
},
},
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() {
Promise.all([
getSystemDictionary({ code: 'currency_type' }),
@@ -618,6 +656,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() : ''; },
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; },
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('当前文件暂不支持在线预览'); },
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`; },
@@ -628,8 +680,8 @@ export default {
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); },
validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; },
validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; },
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); this.$message.success('变更已提交'); this.$router.back(); },
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(); },
},
};
</script>
@@ -653,6 +705,14 @@ export default {
.contract-basic-section :deep(.el-input),
.contract-basic-section :deep(.el-select),
.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::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; }
@@ -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>
+152 -94
View File
@@ -222,7 +222,7 @@
Number(detailSettlementRule.autoGenerate) === 1 ? '开启' : '关闭'
}}</el-descriptions-item>
<template v-if="Number(detailSettlementRule.autoGenerate) === 1">
<el-descriptions-item label="账单起始日">{{
<el-descriptions-item label="账单起始日">{{
displayValue(detailSettlementRule.billStartDate)
}}</el-descriptions-item>
<el-descriptions-item label="结算类型">{{
@@ -244,7 +244,7 @@
}}</el-descriptions-item
>
<el-descriptions-item
v-if="detailSettlementRule.settlementType === '固定天数周期结算'"
v-if="isFixedCycleSettlementType(detailSettlementRule.settlementType)"
label="周期天数"
>{{
detailObjectUnitValue(detailSettlementRule, 'cycleDays', '天')
@@ -345,7 +345,7 @@
</section>
</div>
<div class="contract-manage-page__footer">
<el-button @click="closeDetail">关闭</el-button>
<el-button v-if="!isPublicViewPage" @click="closeDetail">关闭</el-button>
</div>
</template>
@@ -478,7 +478,7 @@
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="YYYY-MM-DD"
placeholder="请输入"
/>
<span></span>
<el-date-picker
@@ -486,7 +486,7 @@
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="YYYY-MM-DD"
placeholder="请输入"
/>
</div>
</el-form-item>
@@ -498,12 +498,12 @@
><template #suffix></template></el-input>
</el-form-item>
<el-form-item label="合同金额">
<el-input-number
v-model="form.contractAmount"
:min="0"
:precision="2"
:controls="false"
<el-input
:model-value="contractAmountDisplay"
placeholder="请输入"
clearable
@input="handleContractAmountInput"
@clear="form.contractAmount = null"
/>
</el-form-item>
<el-form-item label="是否范本">
@@ -593,11 +593,13 @@
description
attachment-type
use-upload-dialog
:attachment-location-options="contractAttachmentLocationOptions"
:lock-approved="isAttachmentUploadMode || hasApprovedContractFiles"
:mark-approved-on-upload="isAttachmentUploadMode"
:rows="contractFileRows"
:preview="previewAttachment"
@update:rows="contractFileRows = $event"
@upload-to-location="handleAttachmentUploadToLocation"
/>
<section class="contract-manage-form__section contract-manage-form__section--panel">
@@ -646,7 +648,7 @@
content="开启时,系统根据配置规则归集运单,定时生成结算单"
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-radio :label="0">关闭</el-radio>
</el-radio-group>
@@ -655,13 +657,13 @@
v-if="settlementRuleEnabled"
class="contract-manage-form__grid contract-manage-form__settlement-grid"
>
<el-form-item label="账单起始日" required>
<el-form-item label="账单起始日" required>
<el-date-picker
v-model="settlementRuleForm.billStartDate"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
placeholder="请选择账单起始日"
placeholder="请选择账单起始日"
/>
</el-form-item>
<el-form-item label="结算类型" required>
@@ -813,10 +815,12 @@
attachment-type
use-upload-dialog
attachment-location="其它附件"
:attachment-location-options="contractAttachmentLocationOptions"
:readonly="isAttachmentUploadMode"
:rows="attachmentRows"
:preview="previewAttachment"
@update:rows="attachmentRows = $event"
@upload-to-location="handleAttachmentUploadToLocation"
/>
<section
@@ -962,44 +966,10 @@
@close="attachmentImagePreviewVisible = false"
/>
<el-dialog
<change-record-detail-dialog
v-model="detailChangeRecordVisible"
title="变更记录详情"
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"
:rows="detailChangeRecordDetailRows"
/>
<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
v-model="detailBillingPlanBox"
@@ -1027,6 +997,7 @@ import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import { exportBlob } from '@/api/common';
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 getCustomerArchiveList } from '@/api/vehicle/customer-archive';
import { getDeptTree } from '@/api/system/dept';
@@ -1035,7 +1006,9 @@ import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
import { config, option } from '@/option/business/contract-manage';
import { getToken } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import BillingPlanEditor from './components/billing-plan-editor.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
import ContractAttachmentSection, {
attachmentName as sharedAttachmentName,
attachmentUrl as sharedAttachmentUrl,
@@ -1057,7 +1030,15 @@ const normalizeOptionalInteger = (value, emptyValue = null) => {
return Number.isFinite(number) ? Math.trunc(number) : emptyValue;
};
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);
return Number.isFinite(number) && number >= 0 ? Number(number.toFixed(2)) : emptyValue;
};
@@ -1099,7 +1080,7 @@ const defaultForm = () => ({
settlementCurrency: '',
invoiceCycle: '',
paymentDays: '',
contractAmount: '',
contractAmount: null,
templateFlag: 0,
originalContractNo: '',
electronicSealFlag: 0,
@@ -1180,7 +1161,14 @@ const attachmentViewerPlugins = [
export default {
name: 'ContractManage',
components: { ContractAttachmentSection, BillingPlanEditor, ElImageViewer, OpenFileViewer, PdfPreview },
components: {
ContractAttachmentSection,
BillingPlanEditor,
ChangeRecordDetailDialog,
ElImageViewer,
OpenFileViewer,
PdfPreview,
},
data() {
return {
api,
@@ -1271,7 +1259,7 @@ export default {
formalSettlementRuleForm: defaultSettlementRule(),
paymentRatioRows: [],
changeRecordRows: [],
settlementTypeOptions: ['结', '日结', '周结', '月结', '固定天数周期结算'],
settlementTypeOptions: ['结', '日结', '周结', '月结', '固定天数周期'],
billCycleTypeOptions: ['固定截单日', '自然月', '自定义多周期'],
detailLoading: false,
detailRow: {},
@@ -1315,7 +1303,12 @@ export default {
return this.$route.path === '/business/contract-manage/form';
},
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() {
return this.$route.query.mode === 'edit' ? 'edit' : 'add';
@@ -1330,6 +1323,23 @@ export default {
isAttachmentUploadMode() {
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() {
return (this.contractFileRows || []).some(
row =>
@@ -1411,7 +1421,7 @@ export default {
return this.showBillCycleType && this.settlementRuleForm.billCycleType === '自定义多周期';
},
showCycleDays() {
return this.settlementRuleForm.settlementType === '固定天数周期结算';
return this.isFixedCycleSettlementType(this.settlementRuleForm.settlementType);
},
billCutoffDayOptions() {
return Array.from({ length: 31 }, (_, index) => ({
@@ -1472,9 +1482,11 @@ export default {
},
},
created() {
if (!this.isPublicViewPage) {
this.loadProjectOptions();
this.loadSettlementDictionaries();
this.loadOrganizationOptions();
}
if (this.isFormPage) this.initFormPage();
if (this.isDetailPage) this.initDetailPage();
},
@@ -1674,7 +1686,7 @@ export default {
copyCount: normalizeOptionalInteger(detail.copyCount, ''),
invoiceCycle: normalizeOptionalInteger(detail.invoiceCycle, ''),
paymentDays: normalizeOptionalInteger(detail.paymentDays, ''),
contractAmount: normalizeOptionalAmount(detail.contractAmount, ''),
contractAmount: normalizeOptionalAmount(detail.contractAmount, null),
archiveStatus: detail.archiveStatus || '未归档',
feeGenerationMode:
detail.feeGenerationMode || (Number(detail.billingEnabled) === 0 ? 'manual' : 'system'),
@@ -1824,9 +1836,17 @@ export default {
const id = saved.id || this.form.id;
if (action === 'temporary' || action === 'formal') {
if (!id) throw new Error('合同保存成功但未返回主键,无法提交合同');
return action === 'temporary'
return (action === 'temporary'
? this.api.toTemporary(id)
: this.api.submitFormal(id);
: this.api.submitFormal(id)
).then(() =>
submitMkApprovalFlow({
bizType: 'contract-manage',
formInstanceId: id,
subjectName: saved.contractName || submit.contractName || '',
approvalStatus: saved.approvalStatus || submit.approvalStatus || '',
})
);
}
return null;
})
@@ -1853,8 +1873,20 @@ export default {
: this.api.submit(payload);
request
.then(() => {
this.$message.success(isChangeRejected ? '已重新提交变更审批' : '修改成功');
if (!isChangeRejected) {
this.$message.success('修改成功');
this.closeForm();
return;
}
return submitMkApprovalFlow({
bizType: 'contract-manage',
formInstanceId: payload.id || this.form.id,
subjectName: payload.contractName || this.form.contractName || '',
approvalStatus: this.form.approvalStatus || 'change_rejected',
}).then(() => {
this.$message.success('已重新提交变更审批');
this.closeForm();
});
})
.finally(() => {
this.submitAction = '';
@@ -2097,6 +2129,13 @@ export default {
integerInput(prop, value) {
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) {
const normalized = String(value || '').replace(/\D/g, '').replace(/^0+/, '');
this.form[prop] = normalized;
@@ -2127,6 +2166,16 @@ export default {
};
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() {
this.$message.warning('当前文件暂不支持在线预览');
},
@@ -2185,8 +2234,12 @@ export default {
}
return true;
},
isFixedCycleSettlementType(value) {
return value === '按固定天数周期' || value === '固定天数周期结算';
},
normalizeSettlementRule(rule = {}) {
const next = { ...defaultSettlementRule(), ...rule };
if (next.settlementType === '固定天数周期结算') next.settlementType = '按固定天数周期';
if (next.settlementType !== '月结') {
next.billCycleType = '';
next.billCutoffDay = '';
@@ -2201,7 +2254,7 @@ export default {
} else {
next.customPeriods = [];
}
if (next.settlementType !== '固定天数周期结算') next.cycleDays = '';
if (!this.isFixedCycleSettlementType(next.settlementType)) next.cycleDays = '';
return next;
},
syncSettlementConfig() {
@@ -2224,7 +2277,7 @@ export default {
this.settlementRuleForm.billCycleType = '';
this.settlementRuleForm.billCutoffDay = '';
this.settlementRuleForm.customPeriods = [];
if (value !== '固定天数周期结算') this.settlementRuleForm.cycleDays = '';
if (!this.isFixedCycleSettlementType(value)) this.settlementRuleForm.cycleDays = '';
},
handleBillCycleTypeChange(value) {
if (value !== '固定截单日') this.settlementRuleForm.billCutoffDay = '';
@@ -2315,7 +2368,7 @@ export default {
validateSettlementRule(rule, label) {
if (Number(rule.autoGenerate) !== 1) return true;
if (!rule.billStartDate || !rule.settlementType) {
this.$message.warning(`${label}:请完整填写账单起始日和结算类型`);
this.$message.warning(`${label}:请完整填写账单起始日和结算类型`);
return false;
}
if (rule.settlementType === '月结' && !rule.billCycleType) {
@@ -2333,7 +2386,7 @@ export default {
if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') {
return this.validateCustomPeriods(rule.customPeriods, label);
}
if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) {
if (this.isFixedCycleSettlementType(rule.settlementType) && !rule.cycleDays) {
this.$message.warning(`${label}:请选择周期天数`);
return false;
}
@@ -2385,8 +2438,10 @@ export default {
if (!id) return;
this.detailLoading = true;
this.applyDetailState({});
this.api
.getDetail(id)
const request = this.isPublicViewPage
? getMkPublicDetail('contract-manage', id)
: this.api.getDetail(id);
request
.then(res => {
this.applyDetailState(res.data?.data || {});
})
@@ -2555,7 +2610,7 @@ export default {
.join('')}`
);
}
if (config.billStartDate) parts.push(`账单起始日${config.billStartDate}`);
if (config.billStartDate) parts.push(`账单起始日:${config.billStartDate}`);
return parts.join('');
};
if (normalized.preSettlementConfig || normalized.formalSettlementConfig) {
@@ -2651,6 +2706,15 @@ export default {
},
detailValue(prop) {
const value = this.detailRow[prop];
if (prop === 'contractAmount') {
return value === undefined ||
value === null ||
value === '' ||
Number(value) === -1 ||
String(value) === '-1'
? '-'
: value;
}
if (
prop === 'contractStage' ||
prop === 'approvalStatus' ||
@@ -2698,7 +2762,19 @@ export default {
}
const run = value => {
const args = operation.prompt ? [row.id, value] : [row.id];
this.api[operation.action](...args).then(() => {
const request = () => this.api[operation.action](...args);
const afterMk =
operation.action === 'submitFormal'
? request().then(() =>
submitMkApprovalFlow({
bizType: 'contract-manage',
formInstanceId: row.id,
subjectName: row.contractName || '',
approvalStatus: row.approvalStatus || '',
})
)
: request();
afterMk.then(() => {
this.$message.success(operation.successMessage || `${operation.label}成功`);
this.onLoad(this.page, this.query);
});
@@ -2867,25 +2943,6 @@ export default {
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) {
left: 60px;
}
@@ -2985,13 +3042,6 @@ export default {
align-items: center;
gap: 20px;
margin-bottom: 16px;
.settlement-switch-tip {
margin: 0 4px;
color: #a8abb2;
cursor: help;
font-size: 14px;
}
}
&__settlement-grid {
@@ -3049,11 +3099,19 @@ export default {
min-width: 0;
}
:deep(.el-input-number) {
width: 360px;
max-width: 100%;
.el-input__inner {
text-align: left;
}
}
:deep(.el-input),
:deep(.el-select),
:deep(.el-cascader),
:deep(.el-tree-select),
:deep(.el-input-number),
:deep(.el-date-editor) {
width: 360px;
max-width: 100%;
+16 -12
View File
@@ -395,14 +395,8 @@
<div class="loading-detail__summary">
<div class="loading-detail__heading">配载单详情</div>
<div>{{ dialogForm.loadingNo || '-' }}</div>
<div>
<el-tag :type="detailStatusType" class="status-text">{{
statusText(dialogForm)
}}</el-tag>
</div>
<div>
<el-tag type="warning">{{ transportTypeLabel(dialogForm.transportType) }}</el-tag>
</div>
<el-tag :type="detailStatusType">{{ statusText(dialogForm) }}</el-tag>
<el-tag type="primary">{{ transportTypeLabel(dialogForm.transportType) }}</el-tag>
</div>
<div class="loading-detail__task-row">
<div class="loading-detail__field">
@@ -1683,11 +1677,12 @@ export default {
const typeMap = {
pending: 'success',
running: 'success',
completed: 'info',
processing: 'success',
completed: 'primary',
cancelled: 'info',
draft: 'warning',
draft: 'info',
};
return typeMap[this.dialogForm.businessStatus] || 'info';
return typeMap[this.dialogForm.businessStatus] || 'warning';
},
taskSummary() {
return (
@@ -3604,8 +3599,17 @@ export default {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 24px;
gap: 12px;
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;
+11 -1
View File
@@ -132,7 +132,7 @@
/>
</template>
<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"
><span>{{ confirm.message }}</span
><template #footer
@@ -153,6 +153,10 @@ export default {
components: { MasterOrderEditor, MasterOrderDispatch, MasterOrderDetail },
data() {
return {
// keep-alive $route
// query.mode / query.id
//mode=add id id
routePathLocked: this.$route.path,
searchExpanded: false,
loading: false,
records: [],
@@ -180,10 +184,15 @@ export default {
};
},
computed: {
isOwnedRoute() {
return this.$route.path === this.routePathLocked;
},
mode() {
if (!this.isOwnedRoute) return 'list';
return this.$route.query.mode || 'list';
},
routeId() {
if (!this.isOwnedRoute) return '';
return this.$route.query.id;
},
masterEditorTitle() {
@@ -194,6 +203,7 @@ export default {
'$route.query': {
immediate: true,
handler() {
if (!this.isOwnedRoute) return;
this.syncTagTitle();
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>
+201 -64
View File
@@ -1,5 +1,5 @@
<template>
<basic-container class="project-apply-page">
<basic-container class="project-apply-page" :class="{ 'is-public-view': isPublicViewPage }">
<avue-crud
v-if="!isProjectFormPage"
:option="tableOption"
@@ -196,7 +196,7 @@
<el-form-item label="项目编号" prop="projectCode">
<el-input
v-model="form.projectCode"
placeholder="请输入"
:placeholder="dialogType === 'add' ? '若不填写,系统自动生成' : '请输入'"
:disabled="dialogType === 'edit'"
/>
</el-form-item>
@@ -703,6 +703,7 @@
label="变更内容"
min-width="180"
align="center"
:show-overflow-tooltip="false"
>
<template #default="{ row }">
<el-tooltip placement="top" :show-after="200">
@@ -730,43 +731,10 @@
</el-table-column>
</el-table>
<el-dialog
<change-record-detail-dialog
v-model="changeRecordDetailVisible"
title="变更记录详情"
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"
:rows="changeRecordDetailRows"
/>
<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">
<div class="dialog-section-title">变更原因</div>
@@ -784,6 +752,7 @@
</el-form>
<div
v-if="!isPublicViewPage"
class="project-apply-dialog__footer"
:class="{ 'project-apply-dialog__footer--change': isChangeDialog }"
>
@@ -909,12 +878,29 @@
/>
</div>
</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>
</template>
<script>
import { exportBlob } from '@/api/common';
import { defineAsyncComponent } from 'vue';
import * as api from '@/api/business/project-apply';
import { getMkPublicDetail } from '@/api/mk-process';
import {
getList as getCustomerArchiveList,
getDetail as getCustomerArchiveDetail,
@@ -925,9 +911,11 @@ import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
import { getList as getUserList } from '@/api/system/user';
import { getToken } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import PdfPreview from '@/components/pdf-preview/main.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
import {
fallbackPlugin,
imagePlugin,
@@ -1044,7 +1032,10 @@ const majorProjectAttachmentTypeOptions = [
].map(item => ({ label: item, value: item })).concat(otherAttachmentType);
export default {
name: 'ProjectApply',
components: {
CustomerArchive: defineAsyncComponent(() => import('@/views/vehicle/customer-archive.vue')),
ChangeRecordDetailDialog,
ElImageViewer,
OpenFileViewer,
PdfPreview,
@@ -1116,6 +1107,10 @@ export default {
total: 0,
},
selectionList: [],
// keep-alive $route
// isProjectFormPage false div el-dialogappend-to-body
//
isFormPageInstance: false,
projectBox: false,
dialogType: 'add',
dialogReadonly: false,
@@ -1234,6 +1229,8 @@ export default {
changeRecordDetail: null,
changeRecordDetailRows: [],
userBox: false,
publicCustomerVisible: false,
publicCustomerId: '',
userPickType: '',
userLoading: false,
userData: [],
@@ -1308,13 +1305,17 @@ export default {
return ['add', 'majorSupplement'].includes(this.dialogType);
},
isProjectFormPage() {
return this.$route.path === '/business/project-apply/form';
return this.isFormPageInstance;
},
isPublicViewPage() {
return this.$route.path === '/business/project-apply/public-view';
},
projectFormContainer() {
return this.isProjectFormPage ? 'div' : 'el-dialog';
// keep-alive
return this.isFormPageInstance ? 'div' : 'el-dialog';
},
projectFormContainerProps() {
if (this.isProjectFormPage) return { class: 'project-apply-page-form' };
if (this.isFormPageInstance) return { class: 'project-apply-page-form' };
return {
modelValue: this.projectBox,
title: this.projectDialogTitle,
@@ -1355,15 +1356,36 @@ export default {
},
},
created() {
if (this.isPublicViewPage) {
this.isFormPageInstance = true;
this.openPublicProjectForm();
return;
}
this.loadDeptOptions();
this.loadCargoTypeOptions();
this.loadTransportTypeOptions();
this.loadSettlementModeOptions();
if (this.isProjectFormPage) {
if (this.$route.path === '/business/project-apply/form') {
this.isFormPageInstance = true;
this.openProjectFormPage();
}
},
// keep-alive append-to-body
// Teleport DOM
deactivated() {
this.closeInnerDialogs();
},
beforeUnmount() {
this.closeInnerDialogs();
},
methods: {
closeInnerDialogs() {
this.changeRecordDetailVisible = false;
this.attachmentDocumentPreviewVisible = false;
this.attachmentImagePreviewVisible = false;
this.userBox = false;
this.publicCustomerVisible = false;
},
buildTableOption() {
return {
...option,
@@ -1385,6 +1407,75 @@ export default {
hasPermission(code) {
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() {
const type = this.$route.query.mode || 'add';
const id = this.$route.query.id;
@@ -1399,6 +1490,7 @@ export default {
this.fillDefaultUsers();
return;
}
if (!id) return;
this.api.getDetail(id).then(res => {
const detail = res.data.data || {};
const isChange = type === 'change';
@@ -1569,7 +1661,17 @@ export default {
}
const run = value => {
const args = operation.prompt ? [row.id, value] : [row.id];
this.api[operation.action](...args).then(res => {
const request = () => this.api[operation.action](...args);
const afterMk =
operation.action === 'submitApproval'
? submitMkApprovalFlow({
bizType: 'project-apply',
formInstanceId: row.id,
subjectName: row.projectName || '',
approvalStatus: row.approvalStatus || '',
}).then(() => request())
: request();
afterMk.then(res => {
if (res.data?.success === false || res.data?.data === false) {
this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`);
return;
@@ -1737,9 +1839,26 @@ export default {
this.$message.error(res.data?.msg || '保存失败,请联系管理员');
return;
}
const data = res.data?.data || {};
const id = data.id || this.form.id;
const name = data.projectName || this.form.projectName || '';
const status = data.approvalStatus || this.form.approvalStatus || '';
if (!id) {
this.$message.success('操作成功!');
this.closeProjectForm();
this.onLoad(this.page, this.query);
return;
}
return submitMkApprovalFlow({
bizType: 'project-apply',
formInstanceId: id,
subjectName: name,
approvalStatus: status,
}).then(() => {
this.$message.success('提交成功!');
this.closeProjectForm();
this.onLoad(this.page, this.query);
});
},
error => {
window.console.log(error);
@@ -1807,9 +1926,23 @@ export default {
);
return;
}
this.$message.success(`${needSubmit ? '提交' : '保存'}成功!`);
if (!needSubmit) {
this.$message.success('保存成功!');
this.closeProjectForm();
this.onLoad(this.page, this.query);
return;
}
const id = this.form.id;
return submitMkApprovalFlow({
bizType: 'project-apply',
formInstanceId: id,
subjectName: this.form.projectName || '',
approvalStatus: this.form.approvalStatus || 'change_rejected',
}).then(() => {
this.$message.success('提交成功!');
this.closeProjectForm();
this.onLoad(this.page, this.query);
});
})
.finally(() => {
this.submitLoading = false;
@@ -1950,6 +2083,11 @@ export default {
this.$message.warning('客商档案 ID 为空,无法打开');
return;
}
if (this.isPublicViewPage) {
this.publicCustomerId = String(id);
this.publicCustomerVisible = true;
return;
}
this.$router.push({
path: '/vehicle/customer-archive/form',
query: { id: String(id), name: '查看客商档案', view: '1' },
@@ -2532,6 +2670,12 @@ export default {
this.$message.warning('项目ID为空,无法查看变更详情');
return;
}
if (this.isPublicViewPage) {
this.changeRecordDetail = { ...row };
this.changeRecordDetailRows = this.buildChangeRecordDetailRows(this.changeRecordDetail);
this.changeRecordDetailVisible = true;
return;
}
try {
if (!this.transportTypeOptions.length) {
await this.loadTransportTypeOptions();
@@ -2588,6 +2732,12 @@ export default {
</script>
<style lang="scss" scoped>
.project-apply-page.is-public-view {
:deep(.el-link) {
pointer-events: auto;
}
}
.project-apply-form {
padding: 0;
@@ -2820,27 +2970,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) {
max-height: 76vh;
overflow-y: auto;
@@ -2957,3 +3086,11 @@ export default {
}
}
</style>
<style lang="scss">
.project-apply-public-customer-dialog .el-dialog__body {
max-height: 78vh;
overflow: auto;
padding-top: 8px;
}
</style>
+98 -20
View File
@@ -99,13 +99,25 @@
<el-table :data="attachmentRows" border @selection-change="selectedAttachments = $event">
<el-table-column type="selection" width="55" 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 }">
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
</template>
</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">
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column>
@@ -172,7 +184,7 @@
title="查看临时额度申请"
append-to-body
destroy-on-close
width="96%"
width="1100px"
class="temporary-credit-limit-dialog temporary-credit-limit-detail-dialog"
>
<div v-loading="detailLoading" class="business-crud-page__detail-content">
@@ -204,13 +216,16 @@
</div>
<el-table :data="attachmentRows" empty-text="暂无附件">
<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 }">
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
</template>
</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">
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column>
@@ -595,7 +610,12 @@ export default {
return value;
},
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.attachmentRows = this.parseJsonArray(detail.attachmentsJson);
this.selectedAttachments = [];
@@ -605,6 +625,9 @@ export default {
const { projectQuotaInfoTitle, temporaryCreditInfoTitle, ...payload } = row || {};
return {
...payload,
projectFundLimit: this.toOptionalAmount(payload.projectFundLimit),
usedFundLimit: this.toOptionalAmount(payload.usedFundLimit),
remainingFundLimit: this.toOptionalAmount(payload.remainingFundLimit),
applyLimit: payload.applyLimit === '' ? '' : Number(payload.applyLimit),
attachmentsJson: JSON.stringify(this.attachmentRows),
};
@@ -750,8 +773,8 @@ export default {
projectName: project.projectName || '',
projectCode: project.projectCode || '',
undertakeDeptName: project.undertakeDeptName || '',
projectFundLimit: project.fundLimit || project.projectFundLimit || 0,
usedFundLimit: project.usedFundLimit || 0,
projectFundLimit: this.blankSentinelAmount(project.fundLimit ?? project.projectFundLimit),
usedFundLimit: this.blankSentinelAmount(project.usedFundLimit),
});
this.form.remainingFundLimit = this.calculateRemainingFundLimit();
getProjectDetail(project.id).then(res => {
@@ -759,18 +782,32 @@ export default {
Object.assign(this.form, {
projectCode: detail.projectCode || this.form.projectCode,
undertakeDeptName: detail.undertakeDeptName || this.form.undertakeDeptName,
projectFundLimit:
detail.fundLimit ?? detail.projectFundLimit ?? this.form.projectFundLimit,
usedFundLimit: detail.usedFundLimit ?? this.form.usedFundLimit,
projectFundLimit: this.blankSentinelAmount(
detail.fundLimit ?? detail.projectFundLimit ?? this.form.projectFundLimit
),
usedFundLimit: this.blankSentinelAmount(
detail.usedFundLimit ?? this.form.usedFundLimit
),
});
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() {
const total = Number(this.form.projectFundLimit);
const used = Number(this.form.usedFundLimit);
if (!Number.isFinite(total) || !Number.isFinite(used)) return '';
return Math.round((total - used + Number.EPSILON) * 100) / 100;
const total = this.blankSentinelAmount(this.form.projectFundLimit);
const used = this.blankSentinelAmount(this.form.usedFundLimit);
if (total === '' || used === '') return '';
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) {
const text = String(value || '').replace(/[^\d.]/g, '');
@@ -783,24 +820,37 @@ export default {
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
this.attachmentRows = (list || []).map(item => ({
...item,
description: item.description || '',
uploadUserName: item.uploadUserName || uploadUserName,
uploadTime: item.uploadTime || uploadTime,
}));
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
this.syncAttachmentsJson();
},
removeAttachment(index) {
this.attachmentRows.splice(index, 1);
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
this.syncAttachmentsJson();
},
syncAttachmentsJson() {
this.form.attachmentsJson = JSON.stringify(this.attachmentRows || []);
},
parseJsonArray(value) {
if (Array.isArray(value)) return value;
if (!value) return [];
let list = [];
if (Array.isArray(value)) {
list = value;
} else if (!value) {
return [];
} else {
try {
const data = JSON.parse(value);
return Array.isArray(data) ? data : [];
list = Array.isArray(data) ? data : [];
} catch (error) {
return [];
}
}
return list.map(item => ({
...item,
description: item?.description || '',
}));
},
attachmentName(row = {}) {
return row.originalName || row.name || row.fileName || '附件';
@@ -907,6 +957,7 @@ export default {
.temporary-credit-limit-page {
&__field {
width: 100%;
max-width: 240px;
}
&__attachment-head {
@@ -993,7 +1044,7 @@ export default {
align-items: center;
gap: 8px;
width: 100%;
padding: 14px 16px 4px;
padding: 14px 0 4px;
margin-bottom: 0;
color: #303133;
font-size: 15px;
@@ -1086,9 +1137,36 @@ export default {
background: transparent !important;
box-shadow: none !important;
margin: 0 !important;
padding: 0 !important;
padding: 0 16px 8px !important;
border-radius: 0 !important;
}
// /
&: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 > .el-textarea,
.temporary-credit-limit-page__field {
width: 100%;
max-width: 240px;
}
.el-form-item__content > .el-textarea {
max-width: 100%;
}
.el-date-editor.el-input,
.el-date-editor.el-input__wrapper {
width: 100%;
max-width: 240px;
}
}
}
.temporary-credit-limit-dialog .business-crud-page__detail-content .el-descriptions__label {
@@ -11,7 +11,7 @@
{{ planData.businessStatusName || '-' }}
</span>
<span class="detail-status-text detail-transport-type">
{{ planData.transportTypeName || '公路整车' }}
{{ planData.transportTypeName || '公路运输' }}
</span>
</div>
<div class="transport-plan-dispatch-page__summary-grid">
+1 -1
View File
@@ -76,7 +76,7 @@
<el-button type="primary">添加附件</el-button>
</el-upload>
<span class="transport-plan-import-page__file-tip">
请上传计划明细表仅支持 Excel 格式
请上传计划明细表仅支持 Excel 格式日期支持 2026-08-022026-8-22026/8/2 等写法
</span>
<el-link type="primary" @click="handleTemplateDownload">下载模板</el-link>
</el-form-item>
@@ -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>
+271
View File
@@ -0,0 +1,271 @@
<template>
<div ref="page" class="mk-public-biz-view">
<basic-container>
<div class="mk-public-biz-view__title">{{ pageTitle }}</div>
<el-descriptions v-if="detail" :column="2" border>
<el-descriptions-item
v-for="item in visibleFields"
:key="item.prop"
:label="item.label"
:span="item.span || 1"
>
{{ displayValue(detail[item.prop]) }}
</el-descriptions-item>
</el-descriptions>
<el-empty v-else description="暂无数据" />
</basic-container>
</div>
</template>
<script>
import { getMkPublicDetail, postMkPublicProcessMessage } from '@/api/mk-process';
const FIELD_MAP = {
'project-apply': [
{ label: '申请单号', prop: 'applyNo' },
{ label: '项目编号', prop: 'projectCode' },
{ label: '项目名称', prop: 'projectName', span: 2 },
{ label: '审核状态', prop: 'approvalStatusName' },
{ label: '当前节点', prop: 'currentNode' },
{ label: '当前处理人', prop: 'currentProcessor' },
{ label: '所属组织', prop: 'deptName' },
{ label: '备注', prop: 'remark', span: 2 },
],
'contract-manage': [
{ label: '合同编号', prop: 'contractNo' },
{ label: '合同名称', prop: 'contractName', span: 2 },
{ label: '合同类别', prop: 'contractCategory' },
{ label: '审核状态', prop: 'approvalStatusName' },
{ label: '当前节点', prop: 'currentNode' },
{ label: '当前处理人', prop: 'currentProcessor' },
{ label: '甲方', prop: 'partyA' },
{ label: '乙方', prop: 'partyB' },
{ label: '项目名称', prop: 'projectName', span: 2 },
],
'waybill-manage': [
{ label: '运单号', prop: 'waybillNo' },
{ label: '项目', prop: 'projectName' },
{ label: '客户合同', prop: 'contractName' },
{ label: '客户名称', prop: 'customerName' },
{ label: '货物名称', prop: 'cargoName' },
{ label: '承运商', prop: 'carrierName' },
{ label: '当前过程节点', prop: 'currentProcessNode' },
{ label: '发货地', prop: 'departureName' },
{ label: '收货地', prop: 'arrivalName' },
],
'pre-settlement': [
{ label: '预结算单号', prop: 'preSettlementNo' },
{ label: '合同名称', prop: 'contractName' },
{ label: '项目名称', prop: 'projectName' },
{ label: '结算金额', prop: 'settlementAmount' },
{ label: '审核状态', prop: 'approvalStatusName' },
{ label: '当前节点', prop: 'currentNode' },
{ label: '当前处理人', prop: 'currentProcessor' },
{ label: '备注', prop: 'remark', span: 2 },
],
'formal-settlement': [
{ label: '正式结算单号', prop: 'formalSettlementNo' },
{ label: '预结算单号', prop: 'preSettlementNos' },
{ label: '项目名称', prop: 'projectName' },
{ label: '合同名称', prop: 'contractName' },
{ label: '结算金额', prop: 'settlementAmount' },
{ label: '审核状态', prop: 'approvalStatusName' },
{ label: '当前节点', prop: 'currentNode' },
{ label: '当前处理人', prop: 'currentProcessor' },
],
'payment-application': [
{ label: '付款申请号', prop: 'paymentNo' },
{ label: '付款类型', prop: 'paymentTypeName' },
{ label: '收款方', prop: 'payeeName' },
{ label: '项目名称', prop: 'projectName' },
{ label: '申请金额', prop: 'applyAmount' },
{ label: '审核状态', prop: 'approvalStatusName' },
{ label: '当前节点', prop: 'currentNode' },
{ label: '当前处理人', prop: 'currentProcessor' },
],
};
const TITLE_MAP = {
'project-apply': '查看项目信息',
'contract-manage': '查看合同信息',
'waybill-manage': '查看运单信息',
'pre-settlement': '查看预结算信息',
'formal-settlement': '查看正式结算信息',
'payment-application': '查看付款申请信息',
};
export default {
name: 'MkPublicBizView',
data() {
return {
detail: null,
};
},
computed: {
bizType() {
return this.$route.meta.bizType || this.$route.query.bizType || '';
},
pageTitle() {
return TITLE_MAP[this.bizType] || '查看详情';
},
visibleFields() {
return FIELD_MAP[this.bizType] || [];
},
},
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.loadDetail();
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: {
loadDetail() {
const id = this.getFormId();
if (!id || !this.bizType) return;
getMkPublicDetail(this.bizType, id).then(res => {
this.detail = res.data?.data || null;
this.handleIframeHeight();
});
},
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) {
const payload = { status, formValues: formValues || {}, formData };
postMkPublicProcessMessage(this.bizType, payload).catch(error => {
console.error('公开页提交流程数据失败:', error);
});
},
buildFormData() {
const detail = this.detail || {};
return {
...detail,
id: this.getFormId() || detail.id,
subject: this.pageTitle,
formInstanceId: this.getFormId(),
};
},
getFormId() {
return this.$route.query.id || this.detail?.id || '';
},
getLoginName(formValues = {}) {
return (
formValues.loginName ||
this.$route.query.loginName ||
this.$route.query.submitIdentity ||
''
);
},
displayValue(value) {
if (value === null || value === undefined || value === '') return '-';
if (Array.isArray(value)) return value.join('、') || '-';
if (typeof value === 'object') return JSON.stringify(value);
return value;
},
},
};
</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-biz-view {
min-height: 100%;
padding: 12px 0 24px;
box-sizing: border-box;
background: #f0f2f5;
:deep(.basic-container) {
padding: 0 12px;
}
&__title {
position: relative;
padding-left: 12px;
margin: 8px 0 16px;
font-size: 16px;
font-weight: 600;
line-height: 22px;
&::before {
content: '';
position: absolute;
left: 0;
top: 2px;
width: 4px;
height: 18px;
background: #409eff;
}
}
}
</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>
+8 -1
View File
@@ -233,6 +233,7 @@ import { mapGetters } from 'vuex';
import * as api from '@/api/payment/paymentApplication';
import { paymentApplicationTableColumns } from '@/option/payment/paymentApplication';
import organizationSearch from '@/mixins/organization-search';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
const emptyQuery = () => ({
paymentNo: '',
@@ -325,7 +326,7 @@ export default {
openProjectAdvance() {
this.$router.push({
path: '/payment/payment-application/form',
query: { mode: 'add', paymentType: 'project_advance' },
query: { mode: 'add', paymentType: 'progress_advance' },
});
},
openEdit(row) {
@@ -361,6 +362,12 @@ export default {
},
async handleSubmit(row) {
await api.submit({ id: row.id });
await submitMkApprovalFlow({
bizType: 'payment-application',
formInstanceId: row.id,
subjectName: row.paymentNo || '',
approvalStatus: row.approvalStatus || '',
});
this.$message.success('提交成功');
this.loadTable();
},
@@ -211,8 +211,15 @@
</el-button>
</el-form-item>
</el-form>
<el-table v-show="!detailCollapsed" :data="filteredDetails" border>
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table v-show="!detailCollapsed" :data="pagedDetails" border>
<el-table-column
type="index"
label="序号"
width="64"
fixed="left"
align="center"
:index="detailIndexMethod"
/>
<el-table-column
v-for="column in detailTableColumns"
:key="column.prop"
@@ -245,6 +252,16 @@
</template>
</el-table-column>
</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>
@@ -623,7 +640,7 @@
>提交</el-button
>
</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
v-if="editable && !recordId"
@@ -929,6 +946,7 @@
</template>
<script>
import { getMkPublicDetail } from '@/api/mk-process';
import {
adjustDetail,
getCandidateDetails,
@@ -977,6 +995,10 @@ export default {
type: Object,
default: null,
},
publicMode: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue', 'success'],
data() {
@@ -1088,6 +1110,10 @@ export default {
batchNo: '',
cargoName: '',
},
detailPage: {
current: 1,
size: 10,
},
};
},
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() {
const missing = this.getMissingAttachmentTypes();
return missing.length ? `未上传:${missing.join('、')}` : '';
@@ -1159,6 +1192,16 @@ export default {
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) {
this.form.settlementAmount = Number(value || 0).toFixed(2);
this.form.localSettlementAmount = (
@@ -1173,7 +1216,9 @@ export default {
},
methods: {
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);
this.form = {
...createFormalSettlementForm(),
@@ -1185,12 +1230,13 @@ export default {
};
this.sources = data.sources || [];
this.details = data.details || [];
this.detailPage.current = 1;
this.summaryFees = data.summaryFees || [];
//
this.detailFeeSnapshots = {};
this.pendingAdjustments = {};
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.adjustments = data.adjustments || [];
@@ -1237,7 +1283,9 @@ export default {
rule: null,
};
this.detailCollapsed = false;
this.detailPage = { current: 1, size: 10 };
this.resetDetailQuery(false);
if (!this.publicMode) {
await Promise.all([
this.loadAllContracts(),
this.loadFeeOptions(),
@@ -1245,6 +1293,7 @@ export default {
this.loadAttachmentTypeOptions(),
this.loadTransportTypeOptions(),
]);
}
if (!this.recordId) {
this.form.exchangeRateDate = this.$dayjs().format('YYYY-MM-DD');
if (this.initialData) await this.applyInitialData();
@@ -1328,6 +1377,7 @@ export default {
settlementAmountTax:
row.settlementAmountTax ?? row.totalAmount ?? row.afterAmount ?? row.settlementAmount,
}));
this.detailPage.current = 1;
this.form.sourceDetailIds = this.details.map(item => item.sourceDetailId);
await this.refreshSummaryFees();
},
@@ -1373,6 +1423,7 @@ export default {
sourcePreSettlementId: settlement.id,
}))
);
this.detailPage.current = 1;
const summaryMap = new Map();
settlements
.flatMap(settlement => settlement.summaryFees || [])
@@ -1601,6 +1652,7 @@ export default {
...this.details.filter(item => item.formalSettlementId),
...existing.values(),
];
this.detailPage.current = 1;
this.form.sourceDetailIds = [...existing.values()].map(item => item.sourceDetailId);
this.detailCandidate.loading = true;
try {
@@ -1819,6 +1871,9 @@ export default {
return sourceDetailId ? `src:${sourceDetailId}` : '';
},
async loadDetailFeeRows(detail) {
if (this.publicMode) {
return [this.normalizeAdjustRow(detail)];
}
if (detail.formalSettlementId && detail.id) {
const rows = this.unwrapData(await getDetailFees(detail.id)) || [];
return rows.map(item => this.normalizeAdjustRow(item));
@@ -1920,12 +1975,19 @@ export default {
},
handleDetailQuery() {
this.appliedDetailQuery = { ...this.detailQuery };
this.detailPage.current = 1;
},
resetDetailQuery(apply = true) {
this.detailQuery = { documentNo: '', waybillNo: '', batchNo: '', cargoName: '' };
if (apply) this.handleDetailQuery();
else this.appliedDetailQuery = { ...this.detailQuery };
},
handleDetailPageSizeChange() {
this.detailPage.current = 1;
},
detailIndexMethod(index) {
return (this.detailPage.current - 1) * this.detailPage.size + index + 1;
},
exportDetails() {
if (!this.details.length) return this.$message.warning('暂无可导出的结算明细');
const rows = this.details.map((row, index) => {
@@ -66,6 +66,12 @@
<span v-else-if="field.prop === 'settlementType'" class="form-readonly">
{{ settlementTypeName }}
</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">
{{ formatMoney(form[field.prop], moneyCurrency(field.prop)) }}
</span>
@@ -184,7 +190,6 @@
</template>
<el-form
:model="detailQuery"
inline
label-position="right"
label-width="auto"
class="pre-settlement-editor__detail-filter"
@@ -228,8 +233,15 @@
</el-form-item>
</el-form>
<div v-show="!detailCollapsed">
<el-table :data="filteredDetails" border class="pre-settlement-editor__detail-table">
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table :data="pagedDetails" border class="pre-settlement-editor__detail-table">
<el-table-column
type="index"
label="序号"
width="64"
fixed="left"
align="center"
:index="detailIndexMethod"
/>
<el-table-column
v-for="column in visibleDetailColumns"
:key="column.prop"
@@ -300,6 +312,16 @@
</template>
</el-table-column>
</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>
</section-card>
@@ -313,13 +335,43 @@
<el-table :data="attachments" border @selection-change="handleAttachmentSelectionChange">
<el-table-column type="selection" width="55" 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 }">
<el-link type="primary" @click="previewAttachment(row)">
{{ attachmentName(row) }}
</el-link>
</template>
</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">
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
</el-table-column>
@@ -425,7 +477,7 @@
提交
</el-button>
</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 v-if="editable" type="primary" plain :loading="saving" @click="saveDraft(false)"
>保存</el-button
@@ -681,8 +733,6 @@
append-to-body
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-column type="index" label="序号" width="64" fixed="left" align="center" />
<el-table-column prop="cargoName" label="货物名称" min-width="140" align="center" />
@@ -780,35 +830,16 @@
class="pre-settlement-editor__adjust-reason"
>
<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>
</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>
<el-button @click="adjustDialog.visible = false">取消</el-button>
<el-button
@@ -822,35 +853,10 @@
</template>
</el-dialog>
<el-dialog
<change-record-detail-dialog
v-model="adjustChangeRecordVisible"
title="变更记录详情"
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"
:rows="adjustChangeRecordDetailRows"
/>
<template #footer>
<el-button type="primary" @click="adjustChangeRecordVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog
v-model="billingRuleDialog.visible"
@@ -922,6 +928,8 @@
import { h } from 'vue';
import { mapGetters } from 'vuex';
import { InfoFilled } from '@element-plus/icons-vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
import { getMkPublicDetail } from '@/api/mk-process';
import {
adjustDetail,
getCandidateDetails,
@@ -955,11 +963,12 @@ import {
settlementSummaryColumns,
} from '@/option/settlement/preSettlementTable';
import { downloadFileByUrl } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import * as XLSX from 'xlsx';
export default {
name: 'PreSettlementEditor',
components: { InfoFilled },
components: { InfoFilled, ChangeRecordDetailDialog },
props: {
modelValue: {
type: Boolean,
@@ -981,6 +990,10 @@ export default {
type: Object,
default: null,
},
publicMode: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue', 'success'],
data() {
@@ -1053,9 +1066,14 @@ export default {
batchNo: '',
cargoName: '',
},
detailPage: {
current: 1,
size: 10,
},
advances: [],
changeRecords: [],
attachments: [],
attachmentTypeOptions: [],
selectedAttachmentRows: [],
candidateDialog: {
visible: false,
@@ -1078,7 +1096,6 @@ export default {
loading: false,
saving: false,
readonly: false,
activeTab: 'adjust',
detailId: '',
sourceDetailId: '',
detailLineNo: '',
@@ -1205,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() {
const names = new Set();
this.adjustRows.forEach(row => {
@@ -1231,6 +1255,16 @@ export default {
this.form.settlementAmount = Number(value || 0).toFixed(2);
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: {
hasPermission(code) {
@@ -1238,9 +1272,12 @@ export default {
},
async initialize() {
this.resetEditor();
if (!this.publicMode) {
await this.loadFeeOptions();
await this.loadFeeCategoryOptions();
await this.loadTransportTypeOptions();
await this.loadAttachmentTypeOptions();
}
if (this.recordId) await this.loadDetail();
else if (this.initialData) await this.applyInitialData();
},
@@ -1280,6 +1317,7 @@ export default {
localCurrency: first.localCurrency || 'RMB',
});
this.details = rows.map(row => this.normalizeSourceDetail(row));
this.detailPage.current = 1;
this.loading = true;
try {
await this.ensureSourceFeeSnapshots();
@@ -1341,6 +1379,7 @@ export default {
cargoName: '',
};
this.appliedDetailQuery = { ...this.detailQuery };
this.detailPage = { current: 1, size: 10 };
this.advances = [];
this.changeRecords = [];
this.attachments = [];
@@ -1349,7 +1388,6 @@ export default {
this.pendingAdjustments = {};
this.sourceFeeSnapshots = {};
this.detailFeeCache = {};
this.adjustDialog.activeTab = 'adjust';
this.adjustDialog.detailId = '';
this.adjustDialog.sourceDetailId = '';
this.adjustDialog.detailLineNo = '';
@@ -1369,11 +1407,14 @@ export default {
async loadDetail() {
this.loading = true;
try {
const { data } = await getDetail(this.form.id || this.recordId);
const detail = data?.data || {};
const response = this.publicMode
? 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.summaryFees = (detail.summaryFees || []).map(row => ({ ...row }));
this.details = (detail.details || []).map(row => this.normalizeSourceDetail(row));
this.detailPage.current = 1;
this.advances = (detail.advances || []).map(row => ({
...row,
createUserName: row.createUserName || detail.createUserName,
@@ -1381,6 +1422,7 @@ export default {
}));
this.changeRecords = detail.changeRecords || [];
this.attachments = this.parseAttachments(detail.attachmentsJson);
this.sortAttachments();
this.selectedAttachmentRows = [];
if (
detail.contractId &&
@@ -1398,8 +1440,15 @@ export default {
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();
}
} finally {
this.loading = false;
}
@@ -1553,12 +1602,19 @@ export default {
await this.persistPendingAdjustments();
if (shouldSubmit) {
await submit({ id: this.form.id });
await submitMkApprovalFlow({
bizType: 'pre-settlement',
formInstanceId: this.form.id,
subjectName: this.form.preSettlementNo || this.form.contractName || '',
approvalStatus: this.form.approvalStatus || '',
});
this.$message.success('审批流程已发起');
this.visible = false;
this.$emit('success', this.form.id);
return;
}
this.$message.success('保存成功');
this.visible = false;
this.$emit('success', this.form.id);
} finally {
this[stateKey] = false;
@@ -1888,6 +1944,7 @@ export default {
},
handleDetailQuery() {
this.appliedDetailQuery = { ...this.detailQuery };
this.detailPage.current = 1;
},
resetDetailQuery() {
this.detailQuery = {
@@ -1897,6 +1954,13 @@ export default {
cargoName: '',
};
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 = []) {
const rawFeeItems =
@@ -2019,7 +2083,6 @@ export default {
this.adjustDialog.visible = true;
this.adjustDialog.loading = true;
this.adjustDialog.readonly = readonly;
this.adjustDialog.activeTab = 'adjust';
this.adjustDialog.detailId = detailRow.id;
this.adjustDialog.detailLineNo = detailRow.lineNo;
this.adjustDialog.sourceDetailId =
@@ -2035,6 +2098,11 @@ export default {
this.adjustRows = this.cloneAdjustRows(pending.rows);
return;
}
if (this.publicMode) {
const cachedFees = this.detailFeeCache[detailRow.id] || [];
this.adjustRows = cachedFees.map(item => this.normalizeAdjustRow(item));
return;
}
if (sourceDetailId) {
if (!this.sourceFeeSnapshots[sourceDetailId]) {
this.sourceFeeSnapshots[sourceDetailId] = await this.loadSourceFeeRows(sourceDetailId);
@@ -2042,6 +2110,11 @@ export default {
this.adjustRows = this.cloneAdjustRows(this.sourceFeeSnapshots[sourceDetailId]);
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 rows = response.data?.data || response.data || [];
this.adjustRows = rows.map(item => this.normalizeAdjustRow(item));
@@ -2230,10 +2303,6 @@ export default {
return String(name || '').includes('运费') || String(name || '').includes('运输费');
},
async saveAdjustment() {
if (!this.adjustDialog.reason.trim()) {
this.$message.warning('请输入调整原因');
return;
}
if (this.adjustRows.some(row => row.calculating)) {
this.$message.warning('费用正在重新计算,请稍候');
return;
@@ -2412,13 +2481,92 @@ export default {
handleAttachmentChange(list) {
const uploadUserName = this.userInfo?.realName || this.userInfo?.userName || '';
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,
uploadUserName: file.uploadUserName || uploadUserName,
uploadTime: file.uploadTime || uploadTime,
}));
type: existing?.type || file.type || this.resolveAttachmentType(originalName),
description: existing?.description || file.description || '',
uploadUserName: existing?.uploadUserName || file.uploadUserName || uploadUserName,
uploadTime: existing?.uploadTime || file.uploadTime || uploadTime,
};
});
this.sortAttachments();
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) {
this.selectedAttachmentRows = rows || [];
},
@@ -2462,13 +2610,25 @@ export default {
},
parseAttachments(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
let list = [];
if (Array.isArray(value)) {
list = value;
} else {
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [];
list = Array.isArray(parsed) ? parsed : [];
} catch (error) {
return [];
}
}
return list.map(file => {
const originalName = this.attachmentName(file);
return {
...file,
type: file?.type || this.resolveAttachmentType(originalName),
description: file?.description || '',
};
});
},
parseFeeItems(value) {
if (!value) return {};
@@ -2505,6 +2665,11 @@ export default {
if (!Number.isFinite(amount)) return '-';
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) {
const value = row.transportQuantity;
if (value === undefined || value === null || value === '') return '-';
@@ -2655,22 +2820,43 @@ export default {
}
&__detail-filter {
display: flex;
flex-wrap: wrap;
gap: 0 24px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)) auto;
align-items: center;
gap: 8px 16px;
margin-bottom: 12px;
padding: 12px 12px 4px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
:deep(.el-form-item) {
display: flex;
min-width: 0;
margin-right: 0;
margin-bottom: 0;
}
:deep(.el-form-item__content) {
flex: 1;
min-width: 0;
}
:deep(.el-input) {
width: 220px;
width: 100%;
}
}
&__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 {
@@ -2783,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) {
padding: 12px 16px;
}
@@ -56,6 +56,7 @@ export default {
methods: {
goBack() {
removeSettlementTransfer(this.$route.query.transferToken);
this.$router.$avueRouter.closeTag();
this.$router.push('/settlement/formal-settlement');
},
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>
@@ -182,6 +182,7 @@ import organizationSearch from '@/mixins/organization-search';
import { formalSettlementTableColumns } from '@/option/settlement/formalSettlementTable';
import FormalSettlementEditor from './components/formal-settlement-editor.vue';
import FormalSettlementTablePanel from './components/formal-settlement-table-panel.vue';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
const emptyQuery = () => ({
formalSettlementNo: '',
@@ -337,6 +338,12 @@ export default {
async handleSubmit(row) {
await this.$confirm('提交后来源预结算将保持锁定,确认提交?', '提示', { type: 'warning' });
await api.submit({ id: row.id });
await submitMkApprovalFlow({
bizType: 'formal-settlement',
formInstanceId: row.id,
subjectName: row.formalSettlementNo || '',
approvalStatus: row.approvalStatus || '',
});
this.$message.success('提交成功');
this.loadTable();
},
@@ -50,6 +50,7 @@ export default {
},
goBack() {
removeSettlementTransfer(this.$route.query.transferToken);
this.$router.$avueRouter.closeTag();
this.$router.push('/settlement/pre-settlement');
},
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',
query: {
mode: 'add',
paymentType: 'progress_advance',
transferToken,
preSettlementIds: sourcePreSettlements
.map(row => row.preSettlementId)
@@ -183,6 +183,7 @@
title="变更记录详情"
width="88%"
append-to-body
align-center
>
<el-table v-loading="changeRecordsDialog.loading" :data="changeRows" border>
<el-table-column type="index" label="序号" width="64" align="center" />
@@ -1153,8 +1154,7 @@ export default {
minWidth: 130,
align: 'right',
};
const dynamicColumns = this.isPayable
? [
const dynamicColumns = [
{
label: '运输费',
prop: 'tableFreightAmount',
@@ -1163,17 +1163,6 @@ export default {
align: 'right',
},
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];
columns.splice(totalIndex, 0, ...dynamicColumns);
@@ -1209,10 +1198,24 @@ export default {
await this.loadTable();
await this.openRouteDetail();
},
// keep-alive teleport/append-to-body
deactivated() {
this.closeInnerDialogs();
},
beforeUnmount() {
this.clearAdjustCalculations();
this.closeInnerDialogs();
},
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) {
return (
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 data = this.unwrapPage(res);
this.rows = (data.records || []).map(this.decorateRow);
this.tableFeeItemNames = this.isPayable ? [] : this.collectFeeItemNames(this.rows);
this.tableFeeItemNames = [];
this.page.total = data.total || 0;
} finally {
this.loading = false;
@@ -1738,7 +1741,7 @@ export default {
});
const adjusted = {
...item,
transportQuantity: Number(item.transportQuantity || 0),
transportQuantity: this.normalizeAdjustTransportQuantity(item),
mileage:
item.mileage === null || item.mileage === undefined || Number(item.mileage) === -1
? null
@@ -1767,6 +1770,21 @@ export default {
feeSourceLabel(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) {
return ADJUST_BILLING_TYPE_MAP[row.billingFactor] || [];
},
@@ -1894,7 +1912,12 @@ export default {
model: row.model,
billingFactor: row.billingFactor,
billingType: row.billingType,
transportQuantity: row.transportQuantity,
transportQuantity:
row.transportQuantity === '' ||
row.transportQuantity === null ||
row.transportQuantity === undefined
? null
: Number(row.transportQuantity),
priceUnit: row.priceUnit,
unitPrice: row.unitPrice,
mileage: row.mileage,
+308 -8
View File
@@ -30,6 +30,14 @@
@click="handleDelete"
>删除
</el-button>
<el-button
type="primary"
plain
:loading="oaSyncLoading"
v-if="userInfo.authority.includes('admin')"
@click="handleOaOrgSync"
>自动同步组织
</el-button>
</template>
<template #menu="scope">
<el-link
@@ -116,6 +124,47 @@
/>
</div>
</el-dialog>
<el-dialog
v-model="oaSyncVisible"
width="480px"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="!oaSyncRunning"
:show-close="!oaSyncRunning"
class="oa-sync-dialog"
@close="closeOaSyncDialog"
>
<template #header>
<span class="dialog-title">{{ oaSyncDialogTitle }}</span>
</template>
<div class="oa-sync-body">
<el-progress
:percentage="oaSyncPercent"
:status="oaSyncProgressStatus"
:stroke-width="12"
/>
<div class="oa-sync-meta" v-if="oaSyncStage === 'clear'">当前阶段{{ oaSyncStageLabel }}</div>
<div class="oa-sync-meta" v-else>
当前阶段{{ oaSyncStageLabel }} {{ oaSyncProgress.current || 0 }} / {{ oaSyncTotalPage }}
</div>
<div class="oa-sync-stats">
<div class="oa-sync-stat">
<span class="oa-sync-stat__label">同步成功</span>
<span class="oa-sync-stat__value oa-sync-stat__value--success">{{ oaSyncProgress.synced }}</span>
</div>
<div class="oa-sync-stat">
<span class="oa-sync-stat__label">跳过</span>
<span class="oa-sync-stat__value oa-sync-stat__value--skip">{{ oaSyncProgress.skipped }}</span>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button v-if="oaSyncRunning" @click="cancelOaSync">取消</el-button>
<el-button v-else type="primary" @click="closeOaSyncDialog">关闭</el-button>
</span>
</template>
</el-dialog>
</basic-container>
</template>
@@ -127,6 +176,9 @@ import {
add,
getDept,
getDeptTree,
syncOaCompany,
syncOaDepartment,
clearNonTopDept,
} from '@/api/system/dept';
import { getLeaderList } from '@/api/system/user';
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
@@ -159,6 +211,21 @@ export default {
selectionList: [],
query: {},
loading: true,
oaSyncLoading: false,
oaSyncVisible: false,
oaSyncRunning: false,
oaSyncCancelled: false,
oaSyncStatus: 'running',
oaSyncStage: 'company',
oaSyncClearNonTop: false,
oaSyncController: null,
oaSyncProgress: {
current: 0,
size: 20,
total: 0,
synced: 0,
skipped: 0,
},
parentId: 0,
page: {
pageSize: 10,
@@ -223,8 +290,9 @@ export default {
return;
}
const parentId = this.normalizeParentId(this.form?.parentId);
//
if (!parentId) {
callback(new Error('请先选择上级组织'));
callback();
return;
}
if (!this.parentDept || String(this.parentDept.id) !== String(parentId)) {
@@ -365,13 +433,6 @@ export default {
props: {
label: 'title',
},
rules: [
{
required: true,
message: '请选择上级组织',
trigger: 'click',
},
],
},
{
label: '所属租户',
@@ -443,8 +504,177 @@ export default {
});
return ids.join(',');
},
oaSyncDialogTitle() {
if (this.oaSyncStatus === 'done') {
return '同步完成';
}
if (this.oaSyncStatus === 'cancelled') {
return '已取消同步';
}
if (this.oaSyncStatus === 'error') {
return '同步失败';
}
return '自动同步组织';
},
oaSyncStageLabel() {
if (this.oaSyncStage === 'clear') {
return '清除非顶级组织';
}
return this.oaSyncStage === 'department' ? '同步部门' : '同步公司';
},
oaSyncTotalPage() {
const total = Number(this.oaSyncProgress.total) || 0;
const size = Number(this.oaSyncProgress.size) || 20;
if (total <= 0) {
return this.oaSyncProgress.current || 0;
}
return Math.max(1, Math.ceil(total / size));
},
oaSyncPercent() {
if (this.oaSyncStatus === 'done') {
return 100;
}
const totalPage = this.oaSyncTotalPage;
const current = Number(this.oaSyncProgress.current) || 0;
const stageBase = this.oaSyncStage === 'department' ? 50 : 0;
if (totalPage <= 0) {
return stageBase;
}
const stagePercent = Math.min(50, Math.round((current / totalPage) * 50));
return Math.min(99, stageBase + stagePercent);
},
oaSyncProgressStatus() {
if (this.oaSyncStatus === 'done') {
return 'success';
}
if (this.oaSyncStatus === 'error') {
return 'exception';
}
return undefined;
},
},
methods: {
handleOaOrgSync() {
this.$confirm(
'是否清除非顶级组织?选择“是”将先删除顶级以外的组织后再同步;选择“否”则直接同步。',
'提示',
{
confirmButtonText: '是',
cancelButtonText: '否',
distinguishCancelAndClose: true,
closeOnClickModal: false,
type: 'warning',
}
)
.then(() => {
this.startOaOrgSync(true);
})
.catch(action => {
if (action === 'cancel') {
this.startOaOrgSync(false);
}
});
},
startOaOrgSync(clearNonTop) {
this.oaSyncLoading = true;
this.oaSyncVisible = true;
this.oaSyncRunning = true;
this.oaSyncCancelled = false;
this.oaSyncStatus = 'running';
this.oaSyncClearNonTop = !!clearNonTop;
this.oaSyncStage = clearNonTop ? 'clear' : 'company';
this.oaSyncController = new AbortController();
this.oaSyncProgress = {
current: 0,
size: 20,
total: 0,
synced: 0,
skipped: 0,
};
this.runOaOrgSyncPages();
},
isOaSyncCanceledError(error) {
return (
this.oaSyncCancelled ||
error?.code === 'ERR_CANCELED' ||
error?.name === 'CanceledError' ||
error?.name === 'AbortError'
);
},
async runOaOrgSyncStage(syncApi) {
const size = 20;
let current = 1;
while (!this.oaSyncCancelled) {
const res = await syncApi(current, size, this.oaSyncController?.signal);
const page = res?.data?.data || {};
this.oaSyncProgress.current = page.current || current;
this.oaSyncProgress.size = page.size || size;
this.oaSyncProgress.total = page.total || 0;
this.oaSyncProgress.synced += page.syncedCount || 0;
this.oaSyncProgress.skipped += page.skippedCount || 0;
if (page.finished) {
break;
}
current += 1;
}
},
async runOaOrgSyncPages() {
try {
if (this.oaSyncClearNonTop) {
this.oaSyncStage = 'clear';
await clearNonTopDept(this.oaSyncController?.signal);
if (this.oaSyncCancelled) {
this.oaSyncStatus = 'cancelled';
return;
}
}
this.oaSyncStage = 'company';
await this.runOaOrgSyncStage(syncOaCompany);
if (this.oaSyncCancelled) {
this.oaSyncStatus = 'cancelled';
return;
}
this.oaSyncStage = 'department';
this.oaSyncProgress.current = 0;
this.oaSyncProgress.total = 0;
await this.runOaOrgSyncStage(syncOaDepartment);
if (this.oaSyncCancelled) {
this.oaSyncStatus = 'cancelled';
return;
}
this.oaSyncStatus = 'done';
} catch (error) {
if (this.isOaSyncCanceledError(error)) {
this.oaSyncStatus = 'cancelled';
} else {
this.oaSyncStatus = 'error';
this.$message.error(error?.message || 'OA组织同步失败');
}
} finally {
this.oaSyncRunning = false;
this.oaSyncLoading = false;
if (this.oaSyncStatus === 'done' || this.oaSyncStatus === 'cancelled') {
this.parentId = 0;
this.data = [];
this.$refs.crud?.refreshTable?.();
this.onLoad(this.page, this.query);
}
}
},
cancelOaSync() {
if (!this.oaSyncRunning) {
return;
}
this.oaSyncCancelled = true;
this.oaSyncController?.abort();
},
closeOaSyncDialog() {
if (this.oaSyncRunning) {
this.cancelOaSync();
return;
}
this.oaSyncVisible = false;
},
initData(tenantId) {
getDeptTree(tenantId).then(res => {
const column = this.findColumn(this.option.column, 'parentId');
@@ -612,6 +842,11 @@ export default {
.filter(Boolean);
return result.length ? result.join('、') : '-';
},
normalizeTopParent(row) {
if (!this.normalizeParentId(row?.parentId)) {
row.parentId = 0;
}
},
isRootDept(row) {
return row && (row.parentId === 0 || String(row.parentId) === '0');
},
@@ -627,6 +862,7 @@ export default {
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
row.deptCode = String(row.deptCode || '').trim();
row.leaderId = func.join(row.leaderId);
this.normalizeTopParent(row);
if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) {
this.$message.warning('请选择承运商');
loading();
@@ -656,6 +892,7 @@ export default {
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
row.deptCode = String(row.deptCode || '').trim();
row.leaderId = func.join(row.leaderId);
this.normalizeTopParent(row);
if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) {
this.$message.warning('请选择承运商');
loading();
@@ -865,3 +1102,66 @@ export default {
color: var(--el-color-danger-light-3);
}
</style>
<style>
.oa-sync-dialog .dialog-title {
display: inline-flex;
align-items: center;
font-size: 16px;
font-weight: 600;
}
.oa-sync-dialog .dialog-title::before {
width: 4px;
height: 18px;
margin-right: 8px;
background: #409eff;
content: '';
}
.oa-sync-body {
padding: 8px 4px 0;
}
.oa-sync-meta {
margin-top: 12px;
color: #606266;
font-size: 13px;
}
.oa-sync-stats {
display: flex;
gap: 16px;
margin-top: 16px;
}
.oa-sync-stat {
flex: 1;
padding: 12px 16px;
background: #fafafa;
border: 1px solid #eff1f7;
border-radius: 4px;
}
.oa-sync-stat__label {
display: block;
color: #909399;
font-size: 12px;
}
.oa-sync-stat__value {
display: block;
margin-top: 6px;
font-size: 22px;
font-weight: 600;
line-height: 1.2;
}
.oa-sync-stat__value--success {
color: #409eff;
}
.oa-sync-stat__value--skip {
color: #909399;
}
</style>
+27 -9
View File
@@ -198,6 +198,17 @@
<el-form ref="userFormRef" :model="form" :rules="userRules" label-position="right" label-width="calc(6em + 24px)">
<el-row :gutter="18">
<el-col :span="6"><el-form-item label="账号名" prop="account"><el-input v-model="form.account" /></el-form-item></el-col>
<el-col v-if="userDialogMode === 'add'" :span="6">
<el-form-item label="登录密码" prop="password">
<el-input
v-model="form.password"
type="password"
show-password
autocomplete="new-password"
placeholder="大于8位,含字母、数字、特殊字符.!@#$%^&*"
/>
</el-form-item>
</el-col>
<el-col :span="6"><el-form-item label="姓名" prop="realName"><el-input v-model="form.realName" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="手机号" prop="phone"><el-input v-model="form.phone" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item></el-col>
@@ -321,7 +332,12 @@
</el-col>
<el-col :span="24">
<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-col>
<el-col :span="24">
@@ -413,6 +429,7 @@ import { mapGetters } from 'vuex';
import { h } from 'vue';
import { getToken } from '@/utils/auth';
import { downloadXls } from '@/utils/util';
import { validateLoginPassword } from '@/utils/validate';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import func from '@/utils/func';
@@ -476,8 +493,7 @@ export default {
passwordForm: {},
passwordRules: {
password: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 32, message: '密码长度在6到32个字符', trigger: 'blur' },
{ required: true, validator: validateLoginPassword, trigger: 'blur' },
],
password2: [
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
@@ -486,6 +502,7 @@ export default {
},
userRules: {
account: [{ required: true, message: '请输入账号名', trigger: 'blur' }],
password: [{ required: true, validator: validateLoginPassword, trigger: 'blur' }],
realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
deptId: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
@@ -544,19 +561,16 @@ export default {
}
return undefined;
},
// checkStrictly false
// checkStrictly true
deptCascaderProps() {
return {
label: 'title',
value: 'id',
children: 'children',
emitPath: false,
checkStrictly: false,
checkStrictly: true,
expandTrigger: 'click',
leaf: (data, node) => {
// children children
return !data.children || data.children.length === 0;
},
leaf: data => !data.children || data.children.length === 0,
};
},
// form.deptId join id
@@ -835,6 +849,7 @@ export default {
if (mode === 'add') {
this.form = {
account: '',
password: '',
realName: '',
phone: '',
email: '',
@@ -890,6 +905,9 @@ export default {
roleId: func.join(this.form.roleId),
leaderId: func.join(this.form.leaderId),
};
if (this.userDialogMode !== 'add') {
delete row.password;
}
const request = this.userDialogMode === 'add' ? add(row) : update(this.sensitiveManager.getSubmitData(row));
request.then(() => {
this.userDialog = false;
+14 -1
View File
@@ -91,7 +91,7 @@
<el-input
v-model="passwordForm.newPassword"
type="password"
placeholder="请输入新密码"
placeholder="大于8位,含字母、数字、特殊字符.!@#$%^&*"
show-password
>
<template #prefix>
@@ -143,6 +143,7 @@
import { getUserInfo, updateInfo, updatePassword } from '@/api/system/user';
import md5 from 'js-md5';
import { sensitive } from '@/utils/sensitive';
import { isValidLoginPassword, LOGIN_PASSWORD_RULE_MESSAGE } from '@/utils/validate';
import {
Plus,
Bell,
@@ -259,6 +260,18 @@ export default {
},
//
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;
updatePassword(
md5(this.passwordForm.oldPassword),
+61 -21
View File
@@ -129,7 +129,7 @@
>
<el-form
label-position="right"
label-width="calc(7em + 24px)"
label-width="calc(4em + 12px)"
class="certification-audit archive-form"
>
<section-card title="基础信息">
@@ -145,19 +145,25 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="外廓尺寸(毫米)">
<el-form-item label="外廓尺寸">
<div class="dimension-group">
<div class="dimension-group__item">
<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 class="dimension-group__item">
<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 class="dimension-group__item">
<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>
</el-form-item>
@@ -165,13 +171,17 @@
</el-row>
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="核定载质量KG">
<span class="certification-audit__value">{{ auditValue('approvedLoadKg') }}</span>
<el-form-item label="核定载质量">
<span class="certification-audit__value">
{{ auditValueWithUnit('approvedLoadKg', 'KG') }}
</span>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="准牵引总质量KG">
<span class="certification-audit__value">{{ auditValue('tractionMassKg') }}</span>
<el-form-item label="准牵引总质量">
<span class="certification-audit__value">
{{ auditValueWithUnit('tractionMassKg', 'KG') }}
</span>
</el-form-item>
</el-col>
<el-col :span="6">
@@ -357,7 +367,7 @@
v-if="showRejectReason"
class="certification-audit__reason"
label-position="right"
label-width="calc(7em + 24px)"
label-width="calc(4em + 24px)"
>
<el-form-item label="驳回原因" required>
<el-input
@@ -427,7 +437,7 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="外廓尺寸(毫米)">
<el-form-item label="外廓尺寸">
<div class="dimension-group">
<div class="dimension-group__item">
<span class="dimension-group__label"></span>
@@ -436,7 +446,9 @@
inputmode="numeric"
placeholder="请输入"
@input="vehicleForm.outerLength = digitsOnly($event)"
/>
>
<template #suffix>mm</template>
</el-input>
</div>
<div class="dimension-group__item">
<span class="dimension-group__label"></span>
@@ -445,7 +457,9 @@
inputmode="numeric"
placeholder="请输入"
@input="vehicleForm.outerWidth = digitsOnly($event)"
/>
>
<template #suffix>mm</template>
</el-input>
</div>
<div class="dimension-group__item">
<span class="dimension-group__label"></span>
@@ -454,7 +468,9 @@
inputmode="numeric"
placeholder="请输入"
@input="vehicleForm.outerHeight = digitsOnly($event)"
/>
>
<template #suffix>mm</template>
</el-input>
</div>
</div>
</el-form-item>
@@ -462,23 +478,27 @@
</el-row>
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="核定载质量KG" prop="approvedLoadKg">
<el-form-item label="核定载质量" prop="approvedLoadKg">
<el-input
v-model="vehicleForm.approvedLoadKg"
inputmode="numeric"
placeholder="请输入"
@input="vehicleForm.approvedLoadKg = digitsOnly($event)"
/>
>
<template #suffix>KG</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="准牵引总质量KG" prop="tractionMassKg">
<el-form-item label="准牵引总质量" prop="tractionMassKg">
<el-input
v-model="vehicleForm.tractionMassKg"
inputmode="numeric"
placeholder="请输入"
@input="vehicleForm.tractionMassKg = digitsOnly($event)"
/>
>
<template #suffix>KG</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="6">
@@ -896,6 +916,7 @@ export default {
energyTypeOptions: ['柴油', '汽油', '新能源', '天然气'],
baseRules: {
organizationName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
useDepartment: [{ required: true, message: '请选择使用部门', trigger: 'change' }],
plateNo: [
{ required: true, message: '请输入车牌号', trigger: 'change' },
{ validator: this.validatePlateNo, trigger: 'change' },
@@ -986,6 +1007,9 @@ export default {
if (this.vehicleBox && !this.vehicleForm.id && !this.vehicleForm.organizationName) {
this.vehicleForm.organizationName = this.getCurrentOrganizationName();
}
if (this.vehicleBox && !this.vehicleForm.useDepartment) {
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
}
});
},
flattenDeptOptions(tree = [], level = 0) {
@@ -1061,6 +1085,7 @@ export default {
if (!row?.id) {
this.vehicleForm = emptyForm();
this.vehicleForm.organizationName = this.getCurrentOrganizationName();
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
this.syncPlateNo(false);
this.vehicleBox = true;
return;
@@ -1070,6 +1095,9 @@ export default {
...emptyForm(),
...(res.data.data || {}),
};
if (!this.vehicleForm.useDepartment) {
this.vehicleForm.useDepartment = this.getCurrentOrganizationName();
}
this.splitPlateNo();
this.syncPlateNo(false);
this.vehicleBox = true;
@@ -1102,6 +1130,10 @@ export default {
const value = this.certificationAuditForm[prop];
return value === undefined || value === null || value === '' ? '-' : value;
},
auditValueWithUnit(prop, unit) {
const value = this.auditValue(prop);
return value === '-' ? value : `${value}${unit}`;
},
handleCertificationApprove() {
auditCertification(this.certificationAuditForm.id, 1).then(() => {
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 {
:deep(.el-form-item__label) {
flex: 0 0 calc(7em + 24px) !important;
width: calc(7em + 24px) !important;
flex: 0 0 calc(4em + 24px) !important;
width: calc(4em + 24px) !important;
max-width: calc(4em + 24px);
}
}
@@ -0,0 +1,168 @@
<template>
<div ref="page" class="customer-archive-public-view">
<customer-archive ref="archive" />
</div>
</template>
<script>
import CustomerArchive from './customer-archive.vue';
import { postPublicProcessMessage } from '@/api/vehicle/customer-archive';
export default {
name: 'CustomerArchivePublicView',
components: {
CustomerArchive,
},
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(() => {
var tbody = document.body;
var height = tbody.clientHeight;
// postMessage
window.parent.postMessage({ height: height }, '*');
});
},
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;
console.log('客商公开页收到流程消息:', data);
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) {
const payload = {
status,
formValues: formValues || {},
formData,
};
console.log('客商公开页提交流程数据:', payload);
postPublicProcessMessage(payload).catch(error => {
console.error('客商公开页提交流程数据失败:', error);
});
},
buildFormData() {
const archive = this.getArchiveForm();
return {
...archive,
subject: archive.fullName || archive.shortName || '',
formInstanceId: this.getFormId(),
};
},
getArchiveForm() {
const archive = this.$refs.archive && this.$refs.archive.archiveForm;
if (!archive) return {};
try {
return JSON.parse(JSON.stringify(archive));
} catch (error) {
return { ...archive };
}
},
getFormId() {
return this.$route.query.id || this.getArchiveForm().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>
.customer-archive-public-view {
min-height: 100%;
padding: 12px 0 24px;
box-sizing: border-box;
background: #f0f2f5;
:deep(.basic-container) {
padding: 0 12px;
}
:deep(.archive-page-form .archive-form__footer) {
left: 0;
}
}
</style>
+336 -112
View File
@@ -1,5 +1,5 @@
<template>
<basic-container class="customer-archive-page">
<basic-container class="customer-archive-page" :class="{ 'is-public-view': isPublicViewPage }">
<avue-crud
v-if="!isArchivePage"
:option="option"
@@ -144,9 +144,24 @@
>
撤回
</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
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)"
>
审核通过
@@ -517,7 +532,7 @@
<span>{{ formatScoreValue(row.selfScore) }}</span>
</template>
</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 }">
<span>{{ formatScoreValue(row.reviewScore) }}</span>
</template>
@@ -553,11 +568,18 @@
<span>{{ row.reviewStatus }}</span>
</template>
</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 }">
<el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)">
详情
</el-link>
<el-link
v-if="showScoreDelete"
type="danger"
@click="deleteScore(row.__rawIndex)"
>
删除
</el-link>
</template>
</el-table-column>
</el-table>
@@ -624,6 +646,7 @@
<el-table-column label="开户人姓名" prop="accountHolderName" min-width="150" />
<el-table-column label="收款账号" prop="bankAccount" 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="操作"
@@ -875,45 +898,12 @@
/>
</section-card>
<el-dialog
<change-record-detail-dialog
v-model="changeRecordDetailVisible"
title="变更记录详情"
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"
:rows="changeRecordDetailRows"
/>
<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">
<div class="archive-form__footer" v-if="!isPublicViewPage">
<!-- 次要独立页返回 / 只读关闭 / 弹窗取消 -->
<el-button @click="closeArchive">{{ readonly ? '关闭' : '取消' }}</el-button>
<el-button type="primary" plain v-if="!readonly" @click="saveArchive">保存</el-button>
@@ -1022,6 +1012,9 @@
<el-form-item label="开户行" prop="bankName">
<el-input v-model="receiptForm.bankName" maxlength="100" />
</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-input v-model="receiptForm.bankAccount" maxlength="50" />
</el-form-item>
@@ -1319,7 +1312,25 @@
class="score-detail-table score-detail-table--inline"
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">
<template #default="{ row: detail }">
<div class="score-standard-cell">
@@ -1329,6 +1340,7 @@
inputmode="decimal"
:disabled="readonly"
@input="value => handleScoreValueInput(detail, value)"
@blur="handleScoreValueBlur(detail)"
>
<template #append>{{ getScoreRule(detail)?.changeUnit || '' }}</template>
</el-input>
@@ -1358,13 +1370,15 @@
<span>{{ formatScoreValue(getSignedScore(detail, 'selfScore')) }}</span>
</template>
</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 }">
<el-input
v-model="detail.reviewScore"
maxlength="10"
inputmode="decimal"
:disabled="readonly || !hasPermission('customer_type_re_cert')"
:disabled="
(!reviewMode && readonly) || !hasPermission('customer_type_re_cert')
"
@input="value => handleScoreDetailScoreInput(detail, 'reviewScore', value)"
>
<template
@@ -1385,7 +1399,7 @@
<template #default="{ row }">
<span class="score-category-table__name">{{ row.categoryName }}</span>
<span>自评{{ getSignedScore(row, 'selfScore') }}</span>
<span>复评{{ getSignedScore(row, 'reviewScore') }}</span>
<span v-if="!isNewScore">复评{{ getSignedScore(row, 'reviewScore') }}</span>
</template>
</el-table-column>
<el-table-column width="60" align="center">
@@ -1459,12 +1473,15 @@
<div class="score-detail-dialog__total">
<span>总分合计</span>
<span>自评{{ formatScoreValue(currentScore.selfScore) }}</span>
<span>复评{{ formatScoreValue(currentScore.reviewScore) }}</span>
<span v-if="!isNewScore">复评{{ formatScoreValue(currentScore.reviewScore) }}</span>
</div>
<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
v-if="!readonly && hasPermission('customer_type_re_cert')"
v-if="!isNewScore && (!readonly || reviewMode) && hasPermission('customer_type_re_cert')"
type="primary"
@click="confirmReviewScore"
>
@@ -1482,7 +1499,9 @@ import { ElCascader } from 'element-plus';
import {
getList,
getDetail,
getPublicDetail,
getChangeRecordList,
getPublicChangeRecordList,
submit,
submitApproval,
withdrawApproval,
@@ -1498,6 +1517,7 @@ import {
getDetail as getCreditScoreQuantificationDetail,
} from '@/api/vehicle/credit-score-quantification';
import { getDictionary } from '@/api/system/dictbiz';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { getDeptTree } from '@/api/system/dept';
import { getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
@@ -1516,6 +1536,7 @@ import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import SectionCard from '@/components/section-card/main.vue';
import PdfPreview from '@/components/pdf-preview/main.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'],
@@ -1531,11 +1552,18 @@ const qualificationViewerPlugins = [
export default {
components: {
SectionCard,
ChangeRecordDetailDialog,
ArrowRight,
ElImageViewer,
OpenFileViewer,
PdfPreview,
},
props: {
embeddedPublicId: {
type: [String, Number],
default: '',
},
},
data() {
const nonNegativeLabelMap = {
invoiceTaxRate: '开票税点',
@@ -1618,6 +1646,8 @@ export default {
receiptBox: false,
invoiceBox: false,
readonly: false,
reviewMode: false,
listActivated: false,
originalAccessType: '',
activeTab: 'scores',
archiveForm: this.emptyArchive(),
@@ -1776,6 +1806,7 @@ export default {
accountName: [{ required: true, message: '请输入收款单位名称', trigger: 'blur' }],
accountHolderName: [{ required: true, message: '请输入开户人姓名', trigger: 'blur' }],
bankName: [{ required: true, message: '请输入开户行', trigger: 'blur' }],
cnapsCode: [{ required: true, message: '请输入联行号', trigger: 'blur' }],
bankAccount: [{ required: true, message: '请输入收款账号', trigger: 'blur' }],
remark: [{ max: 200, message: '备注最多200个字符', trigger: 'blur' }],
},
@@ -1811,7 +1842,7 @@ export default {
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 280,
menuWidth: 320,
column: [
{
label: '客商编号',
@@ -1989,15 +2020,36 @@ export default {
};
},
created() {
if (this.isPublicViewPage) {
this.readonly = true;
const id = this.embeddedPublicId || this.$route.query.id;
if (!id) {
this.archiveBox = true;
this.$message.error('缺少客商ID');
return;
}
this.openArchive({ id }, true);
return;
}
this.initDeptTree();
this.initRegionOptions();
this.initBusinessDictionaries();
this.initScoreQuantificationOptions();
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);
}
},
activated() {
if (this.isArchivePage || this.isPublicViewPage) return;
if (!this.listActivated) {
this.listActivated = true;
return;
}
this.onLoad(this.page, this.query);
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
@@ -2010,7 +2062,14 @@ export default {
};
},
isArchivePage() {
return this.$route.path === '/vehicle/customer-archive/form';
return (
this.$route.path === '/vehicle/customer-archive/form' || this.isPublicViewPage
);
},
isPublicViewPage() {
return (
this.$route.path === '/vehicle/customer-archive/public-view' || !!this.embeddedPublicId
);
},
archiveContainer() {
return 'div';
@@ -2076,6 +2135,7 @@ export default {
return ids.join(',');
},
dialogTitle() {
if (this.reviewMode) return '客商复评';
if (this.readonly) return '查看客商档案';
return this.archiveForm.id ? '编辑客商档案' : '新增客商档案';
},
@@ -2119,6 +2179,14 @@ export default {
scoreList() {
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() {
return Math.max(Math.ceil(this.scoreList.length / this.scorePage.pageSize), 1);
},
@@ -2190,6 +2258,45 @@ export default {
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
},
mergeSelectOptions(target, values) {
const list = Array.isArray(values) ? values : values ? [values] : [];
list.forEach(value => {
if (value === undefined || value === null || value === '') return;
if (!this[target].some(item => String(item.value) === String(value))) {
this[target].push({ label: String(value), value });
}
});
},
ensurePublicViewOptions(archive = {}) {
this.mergeSelectOptions('customerNatureOptions', archive.customerNature);
this.mergeSelectOptions('customerTypeOptions', archive.customerType);
this.mergeSelectOptions('businessScopeOptions', archive.businessScope);
this.mergeSelectOptions(
'qualificationTypeOptions',
(this.qualificationFiles || []).map(item => item.type)
);
const deptId = Array.isArray(archive.deptId) ? archive.deptId[0] : archive.deptId;
if (deptId && archive.deptName) {
this.deptTree = [
{
label: archive.deptName,
value: String(deptId),
children: [],
},
];
this.deptOptions = this.flattenDept(this.deptTree);
}
(archive.scores || []).forEach(score => {
if (!score.quantificationId) return;
const id = String(score.quantificationId);
if (!this.scoreQuantificationOptions.some(item => String(item.id) === id)) {
this.scoreQuantificationOptions.push({
id,
name: score.quantificationName || score.name || id,
});
}
});
},
// tooltip
checkOverflow(refName, flag) {
const inst = this.$refs[refName];
@@ -2278,7 +2385,8 @@ export default {
loadChangeRecords() {
if (!this.archiveForm.id) return;
const page = this.changeRecordPage;
getChangeRecordList(this.archiveForm.id, page.currentPage, page.pageSize).then(res => {
const requestList = this.isPublicViewPage ? getPublicChangeRecordList : getChangeRecordList;
requestList(this.archiveForm.id, page.currentPage, page.pageSize).then(res => {
const data = res.data.data || {};
page.records = this.replaceNegativeOneWithBlank(data.records || []);
page.total = Number(data.total || 0);
@@ -2636,6 +2744,7 @@ export default {
accountName: '',
accountHolderName: '',
bankName: '',
cnapsCode: '',
bankAccount: '',
registeredPhone: '',
registeredAddress: '',
@@ -3889,7 +3998,16 @@ export default {
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() {
if (this.isPublicViewPage) return;
this.archiveBox = false;
this.$router.$avueRouter?.closeTag?.();
this.$router.push('/vehicle/customer-archive');
},
openArchive(row, readonly = false) {
@@ -3898,7 +4016,9 @@ export default {
this.resetDetailPagination();
this.resetChangeRecordPage();
if (row && row.id) {
getDetail(row.id).then(res => {
const requestDetail = this.isPublicViewPage ? getPublicDetail : getDetail;
requestDetail(row.id)
.then(res => {
const archive = this.normalizeDetail(res.data.data);
this.archiveForm = readonly ? this.replaceNegativeOneWithBlank(archive) : archive;
this.originalAccessType = this.archiveForm.accessType || '';
@@ -3912,8 +4032,14 @@ export default {
this.qualificationUploadFiles = [];
this.ocrQualificationUploadFiles = [];
this.selectedQualificationFiles = [];
if (this.isPublicViewPage) this.ensurePublicViewOptions(this.archiveForm);
this.archiveBox = true;
this.loadChangeRecords();
if (this.reviewMode) this.openReviewScore();
})
.catch(() => {
this.archiveBox = true;
if (this.isPublicViewPage) this.$message.error('客商信息加载失败');
});
return;
}
@@ -3951,14 +4077,15 @@ export default {
this.invoiceForm = this.emptyInvoice();
this.$refs.archiveForm?.clearValidate();
},
//
validateFormalForApproval(archive) {
if (archive.accessType !== 'formal') return true;
const hasCompletedScore = (archive.scores || []).some(
item => item.selfStatus === '已完成' || item.reviewStatus === '已完成' || item.finalScore
);
if (!hasCompletedScore) {
this.$message.warning('正式客商提交审核前必须完成信用评分');
//
validateScoreForApproval(archive) {
const scores = archive.scores || [];
if (scores.length > 1) {
this.$message.warning('评分记录只能有一条');
return false;
}
if (scores.length !== 1 || scores[0].selfStatus !== '已完成') {
this.$message.warning('提交审批前必须完成自评');
return false;
}
return true;
@@ -4010,7 +4137,7 @@ export default {
.filter(item => item.contactName || item.contactPhone);
archive.receiptAccounts = (archive.receiptAccounts || [])
.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 || [])
.map(item => {
const invoice = this.normalizeInvoice(item);
@@ -4068,7 +4195,7 @@ export default {
const archive = this.normalizeArchive();
//
archive.recordChange = true;
if (!this.validateFormalForApproval(archive)) return;
if (!this.validateScoreForApproval(archive)) return;
submit(archive).then(res => {
const data = res.data.data;
const id = (data && typeof data === 'object' ? data.id : data) || this.archiveForm.id;
@@ -4078,7 +4205,16 @@ export default {
this.$message({ type: 'success', message: '保存成功,请在列表提交审批' });
return;
}
submitApproval(id).then(() => {
const fullName =
(data && typeof data === 'object' ? data.fullName : '') ||
archive.fullName ||
this.archiveForm.fullName ||
'';
this.submitCustomerApproval(
id,
fullName,
archive.approvalStatus || this.archiveForm.approvalStatus
).then(() => {
this.closeArchive();
this.onLoad(this.page);
this.$message({ type: 'success', message: '提交成功!' });
@@ -4086,9 +4222,32 @@ export default {
});
});
},
/**
* 列表 / 新增 / 编辑提交共用开启 MK 时提交审核流并更新客商审批状态
*/
submitCustomerApproval(id, fullName = '', approvalStatus = '') {
return submitMkApprovalFlow({
bizType: 'customer-archive',
formInstanceId: id,
subjectName: fullName,
approvalStatus,
}).then(() => submitApproval(id));
},
openReviewScore() {
const scores = this.archiveForm.scores || [];
if (!scores.length) {
this.$message.warning('暂无评分记录,无法复评');
return;
}
this.$nextTick(() => this.openScore(scores[0], 0));
},
addScore() {
if ((this.archiveForm.scores || []).length) {
this.$message.warning('请勿重复添加');
return;
}
this.scoreRecordIndex = -1;
this.currentScore = this.normalizeScore({});
this.currentScore = this.normalizeScore({ applyCreditLimit: '' });
this.scoreAttachmentFiles = [];
this.expandedScoreCategoryKeys = [];
this.scoreBox = true;
@@ -4170,7 +4329,7 @@ export default {
JSON.stringify(
(item.options || []).map(option => ({
label: option.label || option.optionName || option.value,
value: option.value || option.optionName || option.label,
value: option.value ?? option.optionName ?? option.label,
score: option.score,
changeType: option.changeType,
changeValue: option.changeValue,
@@ -4260,20 +4419,21 @@ export default {
const isScoreOption = item.changeType || item.changeValue || item.changeUnit;
const changeLabel = item.changeType === 'decrease' ? '每减少' : '每增加';
const scoreLabel = item.scoreType === 'subtract' ? '减' : '加';
const rawScore = item.score;
const generatedLabel = isScoreOption
? `${changeLabel}${item.changeValue || ''}${item.changeUnit || ''}${scoreLabel}${
item.score || ''
this.isBlankScore(rawScore) ? '' : rawScore
}`
: '';
const label = item.label || item.optionName || item.value || generatedLabel;
return {
...item,
label,
value: item.value || item.optionName || item.label || generatedLabel,
value: item.value ?? item.optionName ?? item.label ?? generatedLabel,
score:
item.scoreType === 'subtract' && item.score !== undefined
? -Math.abs(Number(item.score))
: item.score,
item.scoreType === 'subtract' && !this.isBlankScore(rawScore)
? -Math.abs(Number(rawScore))
: rawScore,
};
});
if (Array.isArray(value)) return normalize(value);
@@ -4337,23 +4497,35 @@ export default {
const valid = increase ? input >= base : input <= base;
if (!valid) {
detail.selfScore = '';
this.$message.warning(`输入值${increase ? '不能小于' : '不能大于'}基准数值${base}`);
this.calculateScore(this.currentScore);
return;
}
detail.selfScore = this.getScoreTypeCalculatedScore(detail);
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) {
const option = this.parseScoreOptions(detail.optionsJson).find(
item => item.value === optionValue
);
const score = Number(option?.score || 0);
detail.selfScore = score;
const rawScore = option?.score;
detail.selfScore = this.isBlankScore(rawScore) ? 0 : Number(rawScore);
this.calculateScore(this.currentScore);
},
handleScoreDetailScoreInput(detail, prop, value) {
const normalized = String(value || '')
const normalized = String(value ?? '')
.replace(/[^\d.]/g, '')
.replace(/^\./, '')
.replace(/(\..*)\./g, '$1')
@@ -4485,8 +4657,7 @@ export default {
sumScore(plusDetails, 'reviewScore') -
sumScore(minusDetails, 'reviewScore');
const hasReviewScore = details.some(
item =>
item.reviewScore !== undefined && item.reviewScore !== null && item.reviewScore !== ''
item => !this.isBlankScore(item.reviewScore)
);
const finalScore = hasReviewScore ? reviewScore : selfScore;
const fullMark = this.getScoreFullMark(details);
@@ -4506,13 +4677,17 @@ export default {
}
return score;
},
isBlankScore(value) {
return value === undefined || value === null || String(value).trim() === '';
},
hasIncompleteBasicScoreDetail(details = []) {
return this.getScoreCategoryDetailsByList(details, 'basic').some(item => {
if (this.isScoreTypeDetail(item)) {
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) {
@@ -4529,10 +4704,13 @@ export default {
score.reviewStatus = '已完成';
this.$message.success('评分已完成');
},
saveScoreDetail() {
saveScoreDetail(successMessage) {
if (!this.validateScoreForm(false)) return;
this.currentScore.attachments = JSON.parse(JSON.stringify(this.scoreAttachmentFiles || []));
this.currentScore.proofAttachments = this.stringifyAttachments(this.scoreAttachmentFiles);
this.currentScore.selfStatus = this.hasIncompleteBasicScoreDetail(this.currentScore.details)
? '未完成'
: '已完成';
this.calculateScore(this.currentScore);
const score = JSON.parse(JSON.stringify(this.currentScore));
if (this.scoreRecordIndex > -1) {
@@ -4544,23 +4722,62 @@ export default {
this.syncArchiveCreditFromScores();
this.activeTab = 'scores';
this.scoreBox = false;
this.saveScoreArchive();
},
saveScoreArchive() {
if (!this.archiveForm.id) {
this.$message.success('评分记录已添加,请保存客商档案后生效');
return;
const pending = this.saveScoreArchive(successMessage);
if (this.reviewMode && successMessage && pending && pending.then) {
pending.then(() => this.closeArchive());
}
submit(this.normalizeArchive()).then(() => {
this.$message.success('评分记录已保存');
return pending;
},
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() {
if (!this.validateScoreForm(true)) return;
this.currentScore.selfStatus = '已完成';
this.currentScore.reviewStatus = '已完成';
this.saveScoreDetail();
this.$message.success('复评确认成功');
this.saveScoreDetail('复评确认成功');
},
validateScoreForm(requireComplete) {
if (!this.currentScore.scoreDate) {
@@ -4598,7 +4815,7 @@ export default {
if (
item.reviewScore === undefined ||
item.reviewScore === null ||
item.reviewScore === ''
String(item.reviewScore).trim() === ''
) {
return false;
}
@@ -4615,7 +4832,7 @@ export default {
return false;
}
if (requireComplete && this.hasIncompleteBasicScoreDetail(this.currentScore.details)) {
this.$message.warning('请完整填写基础得分项评分明细');
this.$message.warning('请完整填写所有基础项目后再提交自评');
return false;
}
return true;
@@ -4670,13 +4887,19 @@ export default {
getDetail(row.id).then(res => {
const archive = this.normalizeDetail(res.data.data);
archive.qualificationAttachments = archive.qualificationAttachments || '';
if (!this.validateFormalForApproval(archive)) return;
if (!this.validateScoreForApproval(archive)) return;
this.$confirm('是否提交客商准入审批?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => submitApproval(row.id))
.then(() =>
this.submitCustomerApproval(
row.id,
archive.fullName || row.fullName,
archive.approvalStatus || row.approvalStatus
)
)
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '提交成功!' });
@@ -5151,10 +5374,16 @@ export default {
}
:deep(.el-table__body td) {
//min-height: 112px;
color: #303133;
}
:deep(.score-item-name) {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.el-select),
:deep(.el-input),
:deep(.el-select) {
@@ -5189,6 +5418,16 @@ export default {
.invoice-form {
max-width: none;
:deep(.el-input),
:deep(.el-cascader),
:deep(.el-select) {
width: 100%;
}
:deep(.el-cascader .el-input) {
width: 100%;
}
}
.invoice-contact-table {
@@ -5327,25 +5566,10 @@ export default {
word-break: break-word;
}
:deep(.change-record-detail-dialog .el-dialog__body) {
max-height: 65vh;
overflow: auto;
.customer-archive-page.is-public-view {
:deep(.archive-page-form .archive-form__footer) {
left: 0;
}
: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;
}
.archive-page-form {
+3 -3
View File
@@ -516,7 +516,7 @@ export default {
propsHttp: {
res: 'data',
},
tip: '请上传 .xls,.xlsx 标准格式文件',
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
action: '/blade-transport/maintenance-plan/import-maintenance-plan',
},
{
@@ -564,8 +564,8 @@ export default {
this.addressMapPickerVisible = true;
}
},
handleAddressMapConfirm(address) {
this.form.address = address;
handleAddressMapConfirm(payload) {
this.form.address = typeof payload === 'string' ? payload : payload?.address || '';
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
+3 -3
View File
@@ -489,7 +489,7 @@ export default {
propsHttp: {
res: 'data',
},
tip: '请上传 .xls,.xlsx 标准格式文件',
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
action: '/blade-transport/maintenance-record/import-maintenance-record',
},
{
@@ -545,8 +545,8 @@ export default {
this.addressMapPickerVisible = true;
}
},
handleAddressMapConfirm(address) {
this.form.address = address;
handleAddressMapConfirm(payload) {
this.form.address = typeof payload === 'string' ? payload : payload?.address || '';
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
+2 -2
View File
@@ -288,8 +288,8 @@ export default {
this.locationMapPickerVisible = true;
}
},
handleLocationMapConfirm(address) {
this.form.location = address;
handleLocationMapConfirm(payload) {
this.form.location = typeof payload === 'string' ? payload : payload?.address || '';
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
+13 -13
View File
@@ -47,26 +47,26 @@ export default ({ mode, command }) => {
__VUE_I18N_LEGACY_API__: true,
__INTLIFY_PROD_DEVTOOLS__: false,
},
// server: {
// port: 2888,
// proxy: {
// '/api': {
// target: 'http://localhost',
// //target: 'https://saber3.bladex.cn/api',
// changeOrigin: true,
// rewrite: path => path.replace(/^\/api/, ''),
// },
// },
// },
server: {
port: 2889,
port: 2888,
proxy: {
'/api': {
target: 'http://172.16.203.228:8000',
target: 'http://localhost',
//target: 'https://saber3.bladex.cn/api',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
// server: {
// port: 2889,
// proxy: {
// '/api': {
// target: 'http://172.16.203.228:8000',
// changeOrigin: true,
// },
// },
// },
resolve: {
alias: {
'~': resolve(__dirname, './'),
+7 -4
View File
@@ -570,19 +570,22 @@
- 功能点:
- 任意状态的运单均支持复制。
- 复制后生成新的草稿运单,运单号重新生成。
- 复制时保留运单基础信息、货物信息、承运信息和过程配置相关信息,状态与过程记录重新初始化。
- 复制成功后跳转编辑页面
- 复制后运单号重新生成,业务状态与原运单保持一致
- 复制时保留运单基础信息、货物信息、承运信息和过程配置相关信息;过程打卡记录重新初始化。
- 若原运单为已完成,复制保存后按合同系统计费规则自动生成对应应收、应付明细
- 复制成功后跳转编辑页面(独立表单模式)或刷新列表(弹窗模式)。
- 异常与边界:
- 原运单不存在时提示数据不存在。
- 复制后名称、编号或关联字段触发唯一性校验时应重新处理。
- 复制失败时不影响原运单数据。
- 合同未开启系统计费、无匹配计费方案或自有运输等场景下,按既有规则跳过对应应收/应付生成。
- 反例:
- 不允许复制后沿用原运单号。
- 不允许复制后直接生成进行中或已完成状态。
- 不允许复制后状态与原运单不一致
- 不允许复制后保留原过程节点完成记录。
- 不允许已完成运单复制成功后遗漏按规则应生成的应收应付明细。
---