Merge remote-tracking branch 'refs/remotes/origin/master' into feature/bfi-api-rate

# Conflicts:
#	pnpm-lock.yaml
#	src/axios.js
#	src/views/settlement/formal-settlement.vue
#	src/views/vehicle/customer-archive.vue
This commit is contained in:
刘泉佳
2026-10-01 13:46:44 +08:00
139 changed files with 10731 additions and 2593 deletions
+11
View File
@@ -0,0 +1,11 @@
# 2026-09-30
## /wel/index 首页重构为运营数据看板(按蓝湖效果图)
- 新增依赖:echarts ^6.1.0(corepack pnpm add echarts)。
- 重构 `src/views/wel/index.vue`:改为「核心经营指标 / 运输结构 / 业务单元情况 / 项目情况 / 财务情况 / 车队与线路 / 车务管理 / 底部占比与趋势」8 大板块 + 版权页脚,删除原欢迎横幅/快捷入口。
- 新增文件:
- `src/views/wel/chart-options.js`:排行条形/盈亏双向/环形/分组柱/面积折线/堆叠柱 6 类 option 生成器(fmt 千分位、红涨绿跌 rich label)。
- `src/views/wel/dashboard.scss`:看板公共样式,各组件 scoped `@import '../dashboard.scss'`(子目录注意路径)。
- `src/views/wel/components/`:wel-chart.vue(ECharts 封装:ResizeObserver + activated resize + dispose)、wel-filters.vue(统计时间/条件筛选 mock)、rank-panel.vue(排行/盈亏通用面板)、section-kpi / section-structure / section-business / section-project / section-finance / section-fleet / section-vehicle / section-summary.vue。
- 数据全部为 Mock,组件内已注释标注对接点;蓝湖红字标注的指标口径未定,待后端统计接口(估 12~18 个)确定后替换。
- 自检:dev server(2889) 下所有新文件及 `?vue&type=style&...&lang.scss` 均 200;`/node_modules/.vite/deps/echarts.js` 200。vite build 因 facelogin.vue 沙箱中断不可用,未跑。
+1
View File
@@ -24,6 +24,7 @@
"crypto-js": "^4.1.1",
"dayjs": "^1.10.6",
"disable-devtool": "^0.3.8",
"echarts": "^6.1.0",
"element-plus": "^2.14.2",
"exceljs": "^4.4.0",
"highlight.js": "^11.9.0",
+4264
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -1,5 +1,10 @@
packages:
- '.'
allowBuilds:
'@parcel/watcher': set this to true or false
es5-ext: set this to true or false
esbuild: set this to true or false
patchedDependencies:
'@smallwei/avue': patches/@smallwei__avue.patch
+12
View File
@@ -22,6 +22,18 @@ export const getVoucherImages = (waybillId, params = {}) =>
...params,
},
});
export const getVoucherFolders = waybillId =>
request({
url: '/blade-transport/process-config/voucher-folders',
method: 'get',
params: { waybillId },
});
export const getVoucherFolderDetail = (voucherId, plateNo) =>
request({
url: '/blade-transport/process-config/voucher-folder-detail',
method: 'get',
params: { voucherId, plateNo },
});
export const submit = api.submit;
export const remove = api.remove;
export const copy = api.copy;
+13 -3
View File
@@ -67,6 +67,16 @@ export const getImportDetails = params => request({ url: `${baseUrl}/import-batc
export const removeImportBatches = ids => request({ url: `${baseUrl}/import-batch/remove`, method: 'post', params: { ids } });
export const getImportOptions = () => request({ url: `${baseUrl}/import-batch/options`, method: 'get' });
export const exportImportTemplate = () => request({ url: `${baseUrl}/import-batch/export-template`, method: 'get', responseType: 'blob' });
export const saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data });
export const validateImport = data => request({ url: `${baseUrl}/import-batch/validate`, method: 'post', data, responseType: 'blob' });
export const confirmImport = data => request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data });
export const saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data, timeout: 120000 });
export const validateImport = data =>
request({ url: `${baseUrl}/import-batch/validate`, method: 'post', data, timeout: 120000 });
export const exportImportFailures = data =>
request({
url: `${baseUrl}/import-batch/export-failures`,
method: 'post',
data,
responseType: 'blob',
timeout: 120000,
});
export const confirmImport = data =>
request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data, timeout: 120000 });
+30 -2
View File
@@ -6,8 +6,9 @@ import request from '@/axios';
* @param {string} data.templateCode 模板编码
* @param {string} data.submitIdentity 提交人(手机号)
* @param {string} data.loginName 登录账号(手机号)
* @param {string} data.formInstanceId 业务表单实例 id
* @param {string} data.bizId 业务主键(后端生成/复用 formInstanceId)
* @param {string} [data.subject] 流程标题
* @param {string} [data.bizType] 业务类型
*/
export const processSubmit = data => {
return request({
@@ -20,7 +21,7 @@ export const processSubmit = data => {
/**
* 调用 MK processDelete 删除审核流(驳回后重新提交前使用)
* @param {Object} data
* @param {string} data.formInstanceId 业务表单实例 id
* @param {string} data.bizId 业务主键
*/
export const processDelete = data => {
return request({
@@ -29,3 +30,30 @@ export const processDelete = data => {
data,
});
};
/**
* 查询 MK 流程实例是否仍存在(有数据则无需删除/重新提交)
* @param {Object} params
* @param {string} params.bizId 业务主键
*/
export const hasMkProcessInstance = params => {
return request({
url: '/blade-system/businessProcess/hasMkProcessInstance',
method: 'get',
params,
});
};
/**
* 获取 MK 审批页跳转链接(后端按 thirdParty.mk 配置拼接,含 SSO)
* @param {Object} params
* @param {string} [params.bizId] 业务 id(如客商 id)
* @param {string} [params.processInstanceId] 流程实例 id
*/
export const getMKApprovalUrl = params => {
return request({
url: '/blade-system/businessProcess/getMKApprovalUrl',
method: 'get',
params,
});
};
+55 -11
View File
@@ -70,11 +70,44 @@ const redirectToLogin = () => {
store.dispatch('FedLogOut').then(() => router.push({ path: '/login' }));
};
/**
* 统一弹出错误提示并返回带标记的 Error。
* `_notified` 标记用于让业务层(批量导入等)识别「拦截器已经提示过」,
* 避免同一条错误被弹两次。
*/
const notifiedError = message => {
ElMessage({
message: message,
type: 'error',
});
const error = new Error(message);
error._notified = true;
return error;
};
axios.defaults.timeout = 10000;
//返回其他状态码
axios.defaults.validateStatus = function (status) {
return status >= 200 && status <= 500; // 默认的
};
/**
* 判断响应是否为 JSON。
* 用于 blob 请求(导入/导出)的兜底:后端在业务校验失败时返回的是 JSON
* (Content-Type: application/json),而不是文件流,需要解析后才能取到 msg。
*/
const isJsonContentType = res => {
const headers = res.headers || {};
const contentType = String(headers['content-type'] || res.data?.type || '').toLowerCase();
return contentType.includes('application/json') || contentType.includes('text/json');
};
/**
* 判断响应体是否已是解析好的对象(无需再解析)。
* 注意 Blob 也是 object,必须排除,否则会把待解析的文件流误判为已完成。
*/
const isPlainObject = value =>
value !== null && typeof value === 'object' && !(value instanceof Blob);
//跨域请求,允许保存cookie
axios.defaults.withCredentials = true;
// NProgress Configuration
@@ -140,7 +173,7 @@ axios.interceptors.request.use(
//http response拦截
axios.interceptors.response.use(
res => {
async res => {
NProgress.done();
//获取配置信息
const config = res.config;
@@ -149,6 +182,22 @@ axios.interceptors.response.use(
if (cryptoData) {
res.data = JSON.parse(crypto.decryptAES(res.data, crypto.aesKey));
}
// blob 请求(文件下载、导入)的响应体是 Blob,直接读 res.data.code / res.data.msg
// 拿不到任何字段:状态码会退回 res.status、提示文案会落到「系统错误」兜底,
// 用户就会以为系统崩了——而后端其实返回了明确的业务提示(如「未读取到数据行…」)。
// 这里按 Content-Type 判断:JSON 才解析回对象;真·文件流(Excel 等)保持原样不动。
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
if (isJsonContentType(res) && !isPlainObject(res.data)) {
try {
const text = res.data instanceof Blob ? await res.data.text() : res.data;
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
if (parsed && typeof parsed === 'object') {
res.data = parsed;
}
} catch (ignored) {
// 声明是 JSON 却解析失败:保持原样,交给下游按文件处理
}
}
//获取状态信息
const status = res.data.error_code || res.data.code || res.status;
const statusWhiteList = website.statusWhiteList || [];
@@ -204,20 +253,12 @@ axios.interceptors.response.use(
// OAuth2 业务错误码提示
if (status > 2000 && !validatenull(res.data.error_description)) {
ElMessage({
message: message,
type: 'error',
});
return Promise.reject(new Error(message));
return Promise.reject(notifiedError(message));
}
// 其余非 200 响应统一提示
if (status !== 200) {
ElMessage({
message: message,
type: 'error',
});
return Promise.reject(new Error(message));
return Promise.reject(notifiedError(message));
}
return res;
},
@@ -232,6 +273,9 @@ axios.interceptors.response.use(
});
}
return Promise.reject(new Error(error));
// 保留原始 axios 错误对象:下游(如导入失败明细解析)需要 error.response
// 读取响应体。若包装成 new Error(error),response 会丢失、message 退化成
// "[object Object]",用户只能看到无意义的通用提示。
}
);
+38 -21
View File
@@ -24,10 +24,10 @@
</template>
<script>
import { normalizeMapSearchResults } from '@/utils/map-search';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
const AMAP_KEY = '13dcb15bc29bfe9c432d17ecd808b812';
const AMAP_SECURITY_CODE = 'f9d036ef29e376e15d309e0de5c0f7ab';
let amapLoader;
export default {
@@ -90,10 +90,24 @@ export default {
}
},
methods: {
hasCoordinateValue(value) {
return value !== '' && value !== null && value !== undefined;
},
buildInitialSelection() {
// Number('') === 0,空字符串不能当有效坐标,否则会落到 [0,0](非洲附近)
if (!this.hasCoordinateValue(this.longitude) || !this.hasCoordinateValue(this.latitude)) {
return {};
}
const longitude = Number(this.longitude);
const latitude = Number(this.latitude);
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) {
if (
!Number.isFinite(longitude) ||
!Number.isFinite(latitude) ||
longitude < -180 ||
longitude > 180 ||
latitude < -90 ||
latitude > 90
) {
return {};
}
return {
@@ -131,22 +145,24 @@ export default {
this.$nextTick(() => {
if (!this.$refs.map) return resolve(null);
const initial = this.buildInitialSelection();
const center =
Number.isFinite(initial.longitude) && Number.isFinite(initial.latitude)
? [initial.longitude, initial.latitude]
: [116.40769, 39.89945];
const hasPoint =
Number.isFinite(initial.longitude) && Number.isFinite(initial.latitude);
const center = hasPoint
? [initial.longitude, initial.latitude]
: [116.40769, 39.89945];
const zoom = hasPoint ? 14 : 11;
if (!this.amap) {
this.amap = new window.AMap.Map(this.$refs.map, {
center,
zoom: initial.longitude ? 14 : 11,
zoom,
});
this.amap.on('click', event => this.pickPoint(event.lnglat));
} else {
this.amap.resize();
this.clearMarker();
this.amap.setZoomAndCenter(initial.longitude ? 14 : 11, center);
this.amap.setZoomAndCenter(zoom, center);
}
if (initial.longitude) {
if (hasPoint) {
const point = new window.AMap.LngLat(initial.longitude, initial.latitude);
this.renderMarker(point);
this.selected = { ...initial };
@@ -208,18 +224,17 @@ export default {
this.searchResults = [];
this.searching = true;
this.initMap()
.then(map => (map ? this.runGeocode('location', keyword) : null))
.then(result => {
if (!result) return;
const geocodes = result.geocodes || (result.location ? [result] : []);
this.searchResults = normalizeMapSearchResults(geocodes, keyword);
const point = this.searchResults[0]?.location || result.location;
if (!point) {
.then(map => (map ? searchAmapPlaceText(keyword) : null))
.then(pois => {
if (!pois) return;
this.searchResults = normalizeMapSearchResults(pois, keyword);
const first = this.searchResults[0];
if (!first?.location) {
throw new Error('未找到匹配地址');
}
return this.$nextTick().then(() => {
this.amap?.resize();
this.pickPoint(point, this.searchResults[0]?.address || keyword);
this.pickPoint(first.location, first.address || first.name || keyword);
});
})
.catch(() => {
@@ -236,8 +251,10 @@ export default {
}
},
pickPoint(point, fallbackAddress = '') {
const longitude = typeof point.getLng === 'function' ? point.getLng() : point.lng;
const latitude = typeof point.getLat === 'function' ? point.getLat() : point.lat;
const longitude =
typeof point.getLng === 'function' ? point.getLng() : point.lng ?? point.longitude;
const latitude =
typeof point.getLat === 'function' ? point.getLat() : point.lat ?? point.latitude;
if (!Number.isFinite(Number(longitude)) || !Number.isFinite(Number(latitude))) {
this.$message.warning('选点坐标无效');
return;
+7
View File
@@ -146,6 +146,13 @@ export const createOption = () => ({
showWordLimit: true,
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '更新时间',
prop: 'updateTime',
+143 -139
View File
@@ -35,26 +35,7 @@ export const formatCargoValue = value => {
};
export const option = createCrudOption([
{
label: '一级货物类型',
prop: 'firstCargoTypeName',
formslot: true,
order: 60,
span: 12,
minWidth: 150,
editDisabled: true,
rules: [{ required: true, message: '请选择一级货物类型', trigger: 'change' }],
},
{
label: '二级货物类型',
prop: 'secondCargoTypeName',
formslot: true,
order: 59,
span: 12,
minWidth: 150,
editDisabled: true,
rules: [{ required: true, message: '请选择二级货物类型', trigger: 'change' }],
},
// ---------- 列表字段(顺序即表格展示顺序) ----------
{
label: '货物名称',
prop: 'cargoName',
@@ -79,12 +60,24 @@ export const option = createCrudOption([
],
},
{
label: '一级货物类型编码',
prop: 'firstCargoTypeCode',
hide: true,
display: false,
addDisplay: false,
label: '一级货物类型',
prop: 'firstCargoTypeName',
formslot: true,
order: 60,
span: 12,
minWidth: 150,
editDisabled: true,
rules: [{ required: true, message: '请选择一级货物类型', trigger: 'change' }],
},
{
label: '二级货物类型',
prop: 'secondCargoTypeName',
formslot: true,
order: 59,
span: 12,
minWidth: 150,
editDisabled: true,
rules: [{ required: true, message: '请选择二级货物类型', trigger: 'change' }],
},
{
label: '二级货物类型编码',
@@ -92,9 +85,132 @@ export const option = createCrudOption([
display: false,
addDisplay: false,
editDisabled: true,
minWidth: 180,
minWidth: 160,
},
// 搜索字段与表格、表单字段独立,避免筛选配置影响数据展示和编辑布局。
{
label: '包装',
prop: 'packageType',
type: 'select',
dicData: packageOptions,
order: 39,
span: 12,
minWidth: 100,
},
{
label: '品牌',
prop: 'brand',
search: false,
order: 40,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('品牌', 50),
},
{
label: '规格',
prop: 'specification',
order: 29,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('规格', 50),
},
{
label: '型号',
prop: 'model',
order: 19,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('型号', 50, true),
},
{
label: '单价',
prop: 'cargoValue',
formslot: true,
order: 30,
formatter: row => formatCargoValue(row.cargoValue),
minWidth: 120,
rules: [
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
return;
}
if (!/^\d+(\.\d{1,2})?$/.test(String(value))) {
callback(new Error('单价只能输入数字,最多保留2位小数'));
return;
}
callback();
},
trigger: 'blur',
},
],
},
{
label: '计价单位',
prop: 'priceUnit',
type: 'select',
dicData: [],
display: false,
minWidth: 120,
},
{
label: '尺寸',
prop: 'sizeText',
minWidth: 120,
addDisplay: false,
editDisplay: false,
rules: textRule('尺寸', 100),
},
{
label: '说明',
prop: 'descriptionOne',
order: 20,
span: 12,
minWidth: 160,
placeholder: '请输入',
rules: textRule('说明', 100),
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
order: 10,
minRows: 2,
span: 24,
maxlength: 200,
showWordLimit: true,
placeholder: '请输入',
rules: textRule('备注', 200),
},
{
label: '组织',
prop: 'deptName',
minWidth: 150,
addDisplay: false,
editDisplay: false,
},
...auditColumns
.filter(column => ['createUserName', 'createTime', 'updateTime'].includes(column.prop))
.sort(
(first, second) =>
['createUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
['createUserName', 'createTime', 'updateTime'].indexOf(second.prop)
),
// ---------- 表单隐藏字段 ----------
{
label: '一级货物类型编码',
prop: 'firstCargoTypeCode',
hide: true,
display: false,
addDisplay: false,
editDisabled: true,
},
// ---------- 搜索字段(与表格、表单独立) ----------
{
label: '货物名称',
prop: 'searchCargoName',
@@ -186,118 +302,6 @@ export const option = createCrudOption([
editDisplay: false,
viewDisplay: false,
},
{
label: '包装',
prop: 'packageType',
type: 'select',
dicData: packageOptions,
order: 39,
span: 12,
minWidth: 100,
},
{
label: '品牌',
prop: 'brand',
search: false,
order: 40,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('品牌', 50),
},
{
label: '规格',
prop: 'specification',
order: 29,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('规格', 50),
},
{
label: '货值',
prop: 'cargoValue',
formslot: true,
order: 30,
formatter: row => formatCargoValue(row.cargoValue),
minWidth: 120,
rules: [
{
validator: (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
return;
}
if (!/^\d+(\.\d{1,2})?$/.test(String(value))) {
callback(new Error('货值只能输入数字,最多保留2位小数'));
return;
}
callback();
},
trigger: 'blur',
},
],
},
{
label: '计价单位',
prop: 'priceUnit',
type: 'select',
dicData: [],
display: false,
minWidth: 120,
},
{
label: '型号',
prop: 'model',
order: 19,
span: 12,
minWidth: 120,
placeholder: '请输入',
rules: textRule('型号', 50, true),
},
{
label: '尺寸',
prop: 'sizeText',
minWidth: 120,
addDisplay: false,
editDisplay: false,
rules: textRule('尺寸', 100),
},
{
label: '说明',
prop: 'descriptionOne',
order: 20,
span: 12,
minWidth: 160,
placeholder: '请输入',
rules: textRule('说明', 100),
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
order: 10,
minRows: 2,
span: 24,
maxlength: 200,
showWordLimit: true,
placeholder: '请输入',
rules: textRule('备注', 200),
},
{
label: '组织',
prop: 'deptName',
minWidth: 150,
addDisplay: false,
editDisplay: false,
},
...auditColumns
.filter(column => ['updateTime', 'createTime'].includes(column.prop))
.sort(
(first, second) =>
['updateTime', 'createTime'].indexOf(first.prop) -
['updateTime', 'createTime'].indexOf(second.prop)
),
]);
// 搜索栏标签统一加宽,给较长的货物类型字段预留足够展示空间。
+2 -3
View File
@@ -281,14 +281,13 @@ export const option = {
filterable: true,
},
...auditColumns
.filter(column => column.prop !== 'createUserName')
.map(column =>
['createTime', 'updateTime'].includes(column.prop) ? { ...column, minWidth: 190 } : column
)
.sort(
(first, second) =>
['updateUserName', 'updateTime', 'createTime'].indexOf(first.prop) -
['updateUserName', 'updateTime', 'createTime'].indexOf(second.prop)
['createUserName', 'updateUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
['createUserName', 'updateUserName', 'createTime', 'updateTime'].indexOf(second.prop)
),
]),
dialogCustomClass: 'avue-dialog--common-route',
+10 -3
View File
@@ -114,7 +114,7 @@ export const config = {
operations: [
{
action: 'flow',
label: '查看流程',
label: '流程',
statusProp: 'approvalStatus',
status: ['reviewing', 'change_reviewing'],
stage: ['temporary', 'formal'],
@@ -206,7 +206,7 @@ export const config = {
},
{
action: 'flow',
label: '查看流程',
label: '流程',
statusProp: 'approvalStatus',
status: ['approved', 'change_approved'],
stage: ['formal', 'terminated'],
@@ -532,6 +532,13 @@ export const option = createCrudOption([
order: 30,
minWidth: 110,
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '创建时间',
prop: 'createTime',
@@ -730,7 +737,7 @@ export const option = createCrudOption([
labelWidth: 0,
},
...auditColumns
.filter(column => column.prop !== 'createTime')
.filter(column => !['createTime', 'createUserName'].includes(column.prop))
.map(column => ({ ...column, hide: true })),
]);
+2 -1
View File
@@ -31,7 +31,7 @@ export const loadingManageConfig = {
exportColumns: [
{ prop: 'loadingNo', label: '配载单号' },
{ prop: 'loadingSubNos', label: '配载子单号' },
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号' },
{ prop: 'driverName', label: '司机' },
{ prop: 'driverPhone', label: '联系电话' },
{ prop: 'carrierType', label: '承运类型' },
@@ -41,6 +41,7 @@ export const loadingManageConfig = {
{ prop: 'carrierName', label: '承运商' },
{ prop: 'transportType', label: '运输方式' },
{ prop: 'dataSource', label: '数据来源' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
{ prop: 'updateTime', label: '更新时间' },
{ prop: 'currentProcessNode', label: '过程节点' },
+3 -3
View File
@@ -112,12 +112,12 @@ export const option = {
editDisplay: false,
},
...auditColumns
.filter(column => ['updateTime', 'createTime'].includes(column.prop))
.filter(column => ['createUserName', 'createTime', 'updateTime'].includes(column.prop))
.map(column => ({ ...column, minWidth: 190 }))
.sort(
(first, second) =>
['updateTime', 'createTime'].indexOf(first.prop) -
['updateTime', 'createTime'].indexOf(second.prop)
['createUserName', 'createTime', 'updateTime'].indexOf(first.prop) -
['createUserName', 'createTime', 'updateTime'].indexOf(second.prop)
),
{
label: '项目ID集合',
+17 -3
View File
@@ -57,16 +57,24 @@ export const config = {
action: 'submitApproval',
label: '提交',
statusProp: 'approvalStatus',
status: ['draft'],
status: ['draft', 'rejected', 'withdrawn'],
permission: 'project_apply_submit',
},
{
action: 'withdraw',
label: '撤回',
statusProp: 'approvalStatus',
status: ['reviewing'],
status: ['draft', 'reviewing'],
permission: 'project_apply_withdraw',
},
{
action: 'mkAudit',
label: '流程',
statusProp: 'approvalStatus',
status: ['reviewing', 'change_reviewing'],
permission: 'project_apply_approve',
mkOnly: true,
},
{
action: 'approve',
label: '通过',
@@ -74,6 +82,7 @@ export const config = {
statusProp: 'approvalStatus',
status: ['reviewing', 'change_reviewing'],
permission: 'project_apply_approve',
localOnly: true,
},
{
action: 'reject',
@@ -82,6 +91,7 @@ export const config = {
statusProp: 'approvalStatus',
status: ['reviewing', 'change_reviewing'],
permission: 'project_apply_reject',
localOnly: true,
},
{
action: 'startChange',
@@ -442,5 +452,9 @@ export const option = createCrudOption([
span: 24,
hide: true,
},
...auditColumns.map(column => ({ ...column, hide: true })),
...auditColumns.map(column =>
['createUserName', 'createTime'].includes(column.prop)
? column
: { ...column, hide: true }
),
]);
@@ -332,7 +332,11 @@ export const option = {
addDisplay: false,
editDisplay: false,
},
...auditColumns.map(column => ({ ...column, hide: true })),
...auditColumns.map(column =>
['createUserName', 'createTime'].includes(column.prop)
? column
: { ...column, hide: true }
),
])),
dialogWidth: 1100,
menuFixed: 'right',
+5 -1
View File
@@ -249,9 +249,10 @@ export const config = {
title: '其它信息',
fields: [
['remark', '备注'],
['createUserName', '创建人'],
['createTime', '创建时间'],
['updateUserName', '更新人'],
['updateTime', '更新时间'],
['createTime', '创建时间'],
],
},
],
@@ -457,6 +458,9 @@ export const option = {
showWordLimit: true,
rules: textRule('备注', 200),
},
{
...auditColumn('createUserName'),
},
{
...auditColumn('updateUserName'),
},
+18 -2
View File
@@ -565,10 +565,10 @@ export const option = {
display: false,
},
{
label: '车牌号/航班号/船号/班列号',
label: '车牌号/船班号/船次号/班次号',
prop: 'vehicleNo',
search: true,
searchLabel: '车/船/航班/班列',
searchLabel: '车牌号/船班号/船次号/班次号',
searchOrder: 13,
minWidth: 210,
display: false,
@@ -738,6 +738,19 @@ export const option = {
span: 6,
order: 870,
minWidth: 150,
formatter: row => {
const value = row?.planName;
if (
value === undefined ||
value === null ||
value === '' ||
String(value) === '-1' ||
Number(value) === -1
) {
return '';
}
return value;
},
},
{
label: '货物类型',
@@ -870,6 +883,9 @@ export const option = {
viewDisplay: false,
display: false,
},
{
...auditColumn('createUserName'),
},
{
...auditColumn('createTime'),
},
@@ -106,7 +106,7 @@ export const generateWaybillColumns = [
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
{ label: '项目名称', prop: 'projectName', minWidth: 160 },
{ label: '客户/委托方', prop: 'customerName', minWidth: 160 },
{ label: '车牌号/航班号/船号/班列号', prop: 'vehicleNo', minWidth: 190 },
{ label: '车牌号/船班号/船次号/班次号', prop: 'vehicleNo', minWidth: 190 },
{ label: '司机', prop: 'driverName', minWidth: 120 },
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
{ label: '运输类型', prop: 'transportType', minWidth: 130 },
+17
View File
@@ -125,6 +125,23 @@ export const option = {
slot: true,
minWidth: 160,
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 170,
addDisplay: false,
editDisplay: false,
},
{
label: '准驾车型',
prop: 'drivingType',
@@ -91,6 +91,23 @@ export const option = {
minWidth: 140,
align: 'center',
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 170,
addDisplay: false,
editDisplay: false,
},
{
label: '',
prop: 'expireStatus',
@@ -151,6 +151,23 @@ export const option = {
minWidth: 130,
align: 'center',
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 170,
addDisplay: false,
editDisplay: false,
},
{
label: '',
prop: 'expireStatus',
+16 -1
View File
@@ -1,7 +1,7 @@
import { formatUpdateUserName } from '@/utils/audit';
const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
callback();
} else if (Number(value) < 0) {
callback(new Error('不能小于 0'));
@@ -10,6 +10,9 @@ const validateNonNegative = (rule, value, callback) => {
}
};
const formatEmptySentinel = (row, value) =>
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
export const vehicleTypeDic = [
{ label: '车辆', value: '车辆' },
{ label: '船舶', value: '船舶' },
@@ -135,6 +138,7 @@ export const option = {
minWidth: 140,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -146,6 +150,7 @@ export const option = {
minWidth: 140,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -204,6 +209,14 @@ export const option = {
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -281,6 +294,8 @@ export const exportColumns = [
{ prop: 'insuranceClaimAmount', label: '保险理赔金额' },
{ prop: 'accidentReasonDamage', label: '事故原因及损坏情况' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+15 -1
View File
@@ -1,7 +1,7 @@
import { formatUpdateUserName } from '@/utils/audit';
const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
callback();
} else if (Number(value) < 0) {
callback(new Error('不能小于 0'));
@@ -10,6 +10,9 @@ const validateNonNegative = (rule, value, callback) => {
}
};
const formatEmptySentinel = (row, value) =>
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
export const vehicleTypeDic = [
{ label: '车辆', value: '车辆' },
{ label: '船舶', value: '船舶' },
@@ -159,6 +162,7 @@ export const option = {
minWidth: 100,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [
{ required: true, message: '请输入费用', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
@@ -206,6 +210,14 @@ export const option = {
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -282,6 +294,8 @@ export const exportColumns = [
{ prop: 'fee', label: '费用(元)' },
{ prop: 'assessmentUnit', label: '评定(复核)单位' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+11
View File
@@ -147,6 +147,15 @@ export const option = {
viewDisplay: false,
minWidth: 170,
},
{
label: '创建人',
prop: 'createUserName',
display: false,
addDisplay: false,
editDisplay: false,
viewDisplay: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -224,6 +233,8 @@ export const exportColumns = [
{ prop: 'specificationModel', label: '规格型号' },
{ prop: 'remark', label: '备注' },
{ prop: 'originalEquipmentNo', label: '原厂设备号' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+10
View File
@@ -172,6 +172,14 @@ export const option = {
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -218,6 +226,8 @@ export const exportColumns = [
{ prop: 'exitStation', label: '出口站' },
{ prop: 'dataSource', label: '数据来源' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+15 -1
View File
@@ -1,7 +1,7 @@
import { formatUpdateUserName } from '@/utils/audit';
const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
callback();
} else if (Number(value) < 0) {
callback(new Error('不能小于 0'));
@@ -141,6 +141,7 @@ export const option = {
formslot: true,
append: '元',
minWidth: 120,
formatter: (row, value) => (value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value),
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -150,6 +151,7 @@ export const option = {
formslot: true,
append: '元',
minWidth: 120,
formatter: (row, value) => (value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value),
rules: [
{ required: true, message: '请输入保费', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
@@ -159,6 +161,7 @@ export const option = {
label: '发票号',
prop: 'invoiceNo',
minWidth: 150,
formatter: (row, value) => (value === undefined || value === null || value === '' || String(value) === '-1' ? '' : value),
rules: [{ max: 80, message: '最多 80 个字符', trigger: 'blur' }],
},
{
@@ -168,6 +171,7 @@ export const option = {
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
minWidth: 120,
formatter: (row, value) => (value === undefined || value === null || value === '' || String(value) === '-1' ? '' : value),
},
{
label: '备注',
@@ -178,6 +182,14 @@ export const option = {
hide: true,
rules: [{ max: 200, message: '备注最多 200 个字', trigger: 'blur' }],
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -256,6 +268,8 @@ export const exportColumns = [
{ prop: 'invoiceNo', label: '发票号' },
{ prop: 'invoiceDate', label: '开票日期' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+18 -1
View File
@@ -1,6 +1,7 @@
import { formatUpdateUserName } from '@/utils/audit';
const isEmpty = value => value === undefined || value === null || value === '';
const isEmpty = value =>
value === undefined || value === null || value === '' || Number(value) === -1;
const validateNonNegative = (rule, value, callback) => {
if (isEmpty(value)) {
@@ -12,6 +13,8 @@ const validateNonNegative = (rule, value, callback) => {
}
};
const formatEmptySentinel = (row, value) => (isEmpty(value) ? '' : value);
export const option = {
height: 'auto',
calcHeight: 32,
@@ -60,6 +63,7 @@ export const option = {
append: 'km',
minWidth: 210,
slot: true,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -70,6 +74,7 @@ export const option = {
append: 'km',
minWidth: 210,
slot: true,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -80,6 +85,7 @@ export const option = {
append: 'km',
minWidth: 170,
slot: true,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -90,6 +96,7 @@ export const option = {
append: 'km',
minWidth: 170,
slot: true,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -133,6 +140,14 @@ export const option = {
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -208,6 +223,8 @@ export const exportColumns = [
{ prop: 'monthlyMileage', label: '本月行驶里程数' },
{ prop: 'totalMileage', label: '累计行驶里程数' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+18 -1
View File
@@ -1,6 +1,7 @@
import { formatUpdateUserName } from '@/utils/audit';
const isEmpty = value => value === undefined || value === null || value === '';
const isEmpty = value =>
value === undefined || value === null || value === '' || Number(value) === -1;
const validateNonNegative = (rule, value, callback) => {
if (isEmpty(value)) {
@@ -12,6 +13,8 @@ const validateNonNegative = (rule, value, callback) => {
}
};
const formatEmptySentinel = (row, value) => (isEmpty(value) ? '' : value);
export const vehicleTypeDic = [
{ label: '车辆', value: '车辆' },
{ label: '船舶', value: '船舶' },
@@ -124,6 +127,7 @@ export const option = {
type: 'input',
formslot: true,
minWidth: 110,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -133,6 +137,7 @@ export const option = {
formslot: true,
append: '元/升',
minWidth: 120,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -141,6 +146,7 @@ export const option = {
type: 'input',
formslot: true,
minWidth: 130,
formatter: formatEmptySentinel,
rules: [
{ required: true, message: '请输入交易金额', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
@@ -152,6 +158,7 @@ export const option = {
type: 'input',
formslot: true,
minWidth: 110,
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -225,6 +232,14 @@ export const option = {
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -304,6 +319,8 @@ export const exportColumns = [
{ prop: 'balance', label: '余额' },
{ prop: 'station', label: '站点' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
@@ -165,6 +165,14 @@ export const option = {
editDisplay: false,
viewDisplay: false,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -209,6 +217,8 @@ export const exportColumns = [
{ prop: 'amount', label: '金额' },
{ prop: 'dataSource', label: '数据来源' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+17 -2
View File
@@ -1,7 +1,7 @@
import { formatUpdateUserName } from '@/utils/audit';
const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
callback();
} else if (Number(value) < 0) {
callback(new Error('不能小于 0'));
@@ -11,7 +11,7 @@ const validateNonNegative = (rule, value, callback) => {
};
const validatePositiveInteger = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
callback();
} else if (!Number.isInteger(Number(value)) || Number(value) <= 0) {
callback(new Error('换胎数量必须为正整数'));
@@ -20,6 +20,9 @@ const validatePositiveInteger = (rule, value, callback) => {
}
};
const formatEmptySentinel = (row, value) =>
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
export const option = {
height: 'auto',
calcHeight: 32,
@@ -98,6 +101,7 @@ export const option = {
minWidth: 110,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [{ validator: validatePositiveInteger, trigger: 'blur' }],
},
{
@@ -108,6 +112,7 @@ export const option = {
minWidth: 120,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [
{ required: true, message: '请输入换胎费用', trigger: 'blur' },
{ validator: validateNonNegative, trigger: 'blur' },
@@ -142,6 +147,14 @@ export const option = {
slot: true,
formslot: true,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -217,6 +230,8 @@ export const exportColumns = [
{ prop: 'replacementCost', label: '换胎费用' },
{ prop: 'replacementDescription', label: '换胎说明' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
@@ -97,6 +97,14 @@ export const option = {
slot: true,
formslot: true,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -169,6 +177,8 @@ export const exportColumns = [
{ prop: 'changeItem', label: '变更事项' },
{ prop: 'changeContent', label: '变更内容' },
{ prop: 'remark', label: '备注' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+17 -2
View File
@@ -1,7 +1,7 @@
import { formatUpdateUserName } from '@/utils/audit';
const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
callback();
} else if (Number(value) < 0) {
callback(new Error('不能小于 0'));
@@ -11,7 +11,7 @@ const validateNonNegative = (rule, value, callback) => {
};
const validateDeductPoints = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
callback();
} else if (!Number.isInteger(Number(value)) || Number(value) < 0 || Number(value) > 15) {
callback(new Error('被扣分数范围为 0-15 分'));
@@ -20,6 +20,9 @@ const validateDeductPoints = (rule, value, callback) => {
}
};
const formatEmptySentinel = (row, value) =>
value === undefined || value === null || value === '' || Number(value) === -1 ? '' : value;
export const vehicleTypeDic = [
{ label: '车辆', value: '车辆' },
{ label: '船舶', value: '船舶' },
@@ -150,6 +153,7 @@ export const option = {
minWidth: 100,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [{ validator: validateNonNegative, trigger: 'blur' }],
},
{
@@ -160,6 +164,7 @@ export const option = {
minWidth: 110,
span: 12,
placeholder: '请输入',
formatter: formatEmptySentinel,
rules: [{ validator: validateDeductPoints, trigger: 'blur' }],
},
{
@@ -216,6 +221,14 @@ export const option = {
slot: true,
formslot: true,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
@@ -294,6 +307,8 @@ export const exportColumns = [
{ prop: 'processStatus', label: '状态' },
{ prop: 'processDescription', label: '过程描述' },
{ prop: 'processResult', label: '处理结果' },
{ prop: 'createUserName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' },
];
export const excelOption = {
+33 -11
View File
@@ -106,17 +106,6 @@ export default [
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: '查看预结算信息',
@@ -150,6 +139,39 @@ export default [
bizType: 'payment-application',
},
},
{
path: '/payment/invoice-application/public-view',
name: '查看开票申请信息',
component: () => import('@/views/payment/invoice-application-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'invoice-application',
},
},
{
path: '/payment/invoice-receipt/public-view',
name: '查看收票登记信息',
component: () => import('@/views/payment/invoice-receipt-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'invoice-receipt',
},
},
{
path: '/payment/bill-payment/public-view',
name: '查看汇票付款信息',
component: () => import('@/views/payment/bill-payment-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'bill-payment',
},
},
{
path: '/',
name: '主页',
+4
View File
@@ -1,8 +1,12 @@
import { isAdministrator as checkAdministrator } from '@/utils/auth';
const getters = {
tag: state => state.tags.tag,
language: state => state.common.language,
setting: state => state.common.setting,
userInfo: state => state.user.userInfo,
// 超级管理员:角色别名中包含 administrator
isAdministrator: state => checkAdministrator(state.user.userInfo),
colorName: state => state.common.colorName,
themeName: state => state.common.themeName,
isMacOs: (state, getters) => getters.themeName === 'mac-os',
+24 -1
View File
@@ -6,8 +6,31 @@ const RefreshTokenKey = website.refreshTokenKey;
const SessionId = 'JSESSIONID';
const UserId = 'b-user-id';
/** 超级管理员角色别名 */
export const ADMINISTRATOR_ROLE_ALIAS = 'administrator';
/**
* 是否超级管理员:角色别名中包含 administrator 即为超级管理员
*
* @param {object|string|string[]} [userInfoOrAuthority] 用户信息,或 authority / role_name 字符串/数组
* @returns {boolean}
*/
export function isAdministrator(userInfoOrAuthority) {
let authority = userInfoOrAuthority;
if (authority && typeof authority === 'object' && !Array.isArray(authority)) {
authority = authority.authority ?? authority.role_name ?? authority.roleName ?? '';
}
const roles = Array.isArray(authority)
? authority
: String(authority || '')
.split(/[,,]/)
.map(role => role.trim())
.filter(Boolean);
return roles.some(role => String(role).trim() === ADMINISTRATOR_ROLE_ALIAS);
}
export function getToken() {
return Cookies.get(TokenKey);
return Cookies.get(TokenKey);
}
export function setToken(token) {
+33 -1
View File
@@ -28,6 +28,34 @@ const parseBlobJson = async blob => {
return text ? JSON.parse(text) : {};
};
/**
* 解析导入失败时的错误提示文案。
*
* 背景:导入接口统一用 `responseType: 'blob'` 请求,因此**非 2xx 响应体也是 Blob**。
* axios 的错误分支不做解析,直接 reject,`error.message` 只有
* 「Request failed with status code 400」这类通用文案(或全局兜底的「系统错误」),
* 用户会误以为系统崩了——而后端其实已返回了明确的中文业务提示(如「导入数据不能为空」)。
*
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
*/
const resolveErrorMessage = async error => {
const blob = error?.response?.data ?? error?.data;
if (blob instanceof Blob) {
try {
const result = await parseBlobJson(blob);
if (result?.msg) return result.msg;
} catch (ignored) {
// 响应体不是 JSON(如网关返回的 HTML 错误页),退回通用文案
}
}
const fallback = error?.response?.data?.msg || error?.message;
// axios 的英文通用文案对用户没有意义,统一换成中文提示
if (!fallback || /Request failed with status code/i.test(fallback)) {
return '导入失败,请检查导入文件后重试';
}
return fallback;
};
/**
* 加工后端返回的导入失败明细 Excel。
* 若传入 failDetailDecorator(函数),则动态加载 exceljs 读取 blob 流,
@@ -125,7 +153,11 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
option.onSuccess(res);
}
} catch (error) {
vm.$message.error(error.message || '导入失败');
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
// 只保留一次清晰的中文提示。
if (!error?._notified) {
vm.$message.error(await resolveErrorMessage(error));
}
if (typeof option.onError === 'function') {
option.onError(error);
}
+138 -15
View File
@@ -1,6 +1,59 @@
/** 地点搜索 v5/place/text 使用的「Web 服务」类型 Key */
export const AMAP_WEB_SERVICE_KEY = 'b43016a0d1f5b0c32211560392059708';
/**
* 将高德 Geocoder 结果规范化为地图选址浮层列表数据。
* @param {Array|Object} geocodesOrResult geocodes 数组,或含 geocodes/location 的结果对象
* 高德地点关键词搜索(Web 服务 v5)。
* @see https://restapi.amap.com/v5/place/text
* @param {string} keyword 地点关键字
* @param {{ key?: string, pageSize?: number, pageNum?: number, region?: string, cityLimit?: boolean }} options
* @returns {Promise<Array>}
*/
export function searchAmapPlaceText(keyword, options = {}) {
const key = options.key || AMAP_WEB_SERVICE_KEY;
const pageSize = options.pageSize || 20;
const pageNum = options.pageNum || 1;
if (!key) {
return Promise.reject(new Error('高德地图 Key 未配置'));
}
const trimmed = String(keyword || '').trim();
if (!trimmed) {
return Promise.reject(new Error('请输入地址关键词'));
}
const params = new URLSearchParams({
key,
keywords: trimmed,
page_size: String(pageSize),
page_num: String(pageNum),
show_fields: 'photos',
});
if (options.region) {
params.set('region', options.region);
}
if (options.cityLimit != null) {
params.set('city_limit', options.cityLimit ? 'true' : 'false');
}
return fetch(`https://restapi.amap.com/v5/place/text?${params.toString()}`)
.then(response => {
if (!response.ok) {
throw new Error('高德地点搜索请求失败');
}
return response.json();
})
.then(data => {
if (String(data?.status) === '1') {
return Array.isArray(data.pois) ? data.pois : [];
}
const error = new Error(data?.info || '高德地点搜索失败');
error.code = data?.infocode;
throw error;
});
}
/**
* 将高德 Geocoder / place/text 结果规范化为地图选址浮层列表数据。
* @param {Array|Object} geocodesOrResult geocodes/pois 数组,或含 geocodes/pois/location 的结果对象
* @param {string} keyword 搜索关键词兜底文案
* @returns {Array<{id: string|number, name: string, address: string, location: any, photo: string}>}
*/
@@ -10,25 +63,95 @@ export function normalizeMapSearchResults(geocodesOrResult, keyword = '') {
if (Array.isArray(geocodesOrResult)) {
list = geocodesOrResult;
} else if (geocodesOrResult && typeof geocodesOrResult === 'object') {
if (Array.isArray(geocodesOrResult.geocodes)) {
if (Array.isArray(geocodesOrResult.pois)) {
list = geocodesOrResult.pois;
} else if (Array.isArray(geocodesOrResult.geocodes)) {
list = geocodesOrResult.geocodes;
} else if (geocodesOrResult.location) {
list = [geocodesOrResult];
}
}
return list.map((item, index) => {
const buildingName = pickNamedField(item?.building);
const neighborhoodName = pickNamedField(item?.neighborhood);
const address = item?.formattedAddress || item?.address || fallback || '-';
return {
id: item?.id || index,
name: buildingName || neighborhoodName || address,
address,
location: item?.location,
photo: item?.photo || item?.image || '',
};
});
return list
.map((item, index) => {
if (!item) return null;
if (isPlacePoi(item)) {
const address = buildPoiAddress(item) || fallback || '-';
return {
id: item.id || index,
name: String(item.name || '').trim() || address,
address,
location: parseMapLocation(item.location),
photo: pickPoiPhoto(item),
};
}
const buildingName = pickNamedField(item?.building);
const neighborhoodName = pickNamedField(item?.neighborhood);
const address = item?.formattedAddress || item?.address || fallback || '-';
return {
id: item?.id || index,
name: buildingName || neighborhoodName || address,
address,
location: parseMapLocation(item?.location),
photo: item?.photo || item?.image || '',
};
})
.filter(item => item && item.location);
}
export function parseMapLocation(location) {
if (!location) return null;
if (typeof location.getLng === 'function' && typeof location.getLat === 'function') {
return location;
}
if (typeof location === 'string') {
const parts = location.split(',');
const lng = Number(parts[0]);
const lat = Number(parts[1]);
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null;
return { lng, lat };
}
if (Number.isFinite(Number(location.lng)) && Number.isFinite(Number(location.lat))) {
return { lng: Number(location.lng), lat: Number(location.lat) };
}
if (
Number.isFinite(Number(location.longitude)) &&
Number.isFinite(Number(location.latitude))
) {
return { lng: Number(location.longitude), lat: Number(location.latitude) };
}
return null;
}
function isPlacePoi(item) {
return Boolean(item && item.name && item.location && (item.id || item.typecode || item.adname));
}
function buildPoiAddress(item) {
const detail = String(item.address || '').trim();
const region = [item.pname, item.cityname, item.adname]
.map(value => String(value || '').trim())
.filter(Boolean);
// 直辖市等场景省市名可能重复
const uniqueRegion = region.filter((value, index) => region.indexOf(value) === index);
const prefix = uniqueRegion.join('');
if (prefix && detail) {
if (detail.startsWith(prefix)) return detail;
return `${prefix}${detail}`;
}
return detail || prefix || String(item.name || '').trim();
}
function pickPoiPhoto(item) {
const photos = item?.photos;
if (Array.isArray(photos) && photos.length) {
return photos[0]?.url || '';
}
if (photos && typeof photos === 'object') {
return photos.url || '';
}
return item?.photo || item?.image || '';
}
function pickNamedField(value) {
+65 -10
View File
@@ -1,5 +1,5 @@
import { getDictionary } from '@/api/system/dictbiz';
import { processDelete, processSubmit } from '@/api/system/business-process';
import { hasMkProcessInstance, processDelete, processSubmit } from '@/api/system/business-process';
import { ElMessage } from 'element-plus';
export const MK_BIZ = {
@@ -7,36 +7,55 @@ export const MK_BIZ = {
dictName: '提交客商审核流',
subjectPrefix: '客商准入审批',
rejected: ['rejected'],
approved: ['approved'],
},
'project-apply': {
dictName: '提交项目审核流',
subjectPrefix: '项目审批',
rejected: ['rejected', 'change_rejected', 'withdrawn'],
approved: ['approved', 'change_approved'],
},
'contract-manage': {
dictName: '提交合同审核流',
subjectPrefix: '合同审批',
rejected: ['rejected', 'change_rejected', 'withdrawn'],
},
'waybill-manage': {
dictName: '提交运单审核流',
subjectPrefix: '运单审批',
rejected: ['rejected', 'returned'],
approved: ['approved', 'change_approved'],
},
'pre-settlement': {
dictName: '提交预结算审核流',
subjectPrefix: '预结算审批',
rejected: ['returned'],
approved: ['approved'],
},
'formal-settlement': {
dictName: '提交正式结算审核流',
subjectPrefix: '正式结算审批',
rejected: ['returned'],
approved: ['approved'],
},
'payment-application': {
dictName: '提交付款审核流',
subjectPrefix: '付款审批',
rejected: ['returned'],
approved: ['approved'],
},
'invoice-application': {
dictName: '提交开票审核流',
subjectPrefix: '开票审批',
rejected: ['returned'],
approved: ['approved'],
},
'invoice-receipt': {
dictName: '提交收票审核流',
subjectPrefix: '收票审批',
rejected: ['returned'],
approved: ['approved'],
},
'bill-payment': {
dictName: '提交汇票付款审核流',
subjectPrefix: '汇票付款审批',
rejected: ['returned'],
approved: ['approved'],
},
};
@@ -53,6 +72,12 @@ export function isMkEnabled(list = []) {
return String(matched?.dictKey ?? '').trim() === '1';
}
/** 加载业务字典并判断是否开启 MK */
export async function loadMkEnabled() {
const list = await loadMkTemplateList();
return isMkEnabled(list);
}
export async function resolveMkTemplateCode(dictName, list) {
const dictList = list || (await loadMkTemplateList());
const matched = dictList.find(item => String(item.dictValue || '').trim() === dictName);
@@ -64,9 +89,26 @@ export async function resolveMkTemplateCode(dictName, list) {
return String(templateCode);
}
/**
* 是否属于「审核通过后再提交」需要新建表单实例的状态
*/
export function isMkApprovedStatus(bizType, approvalStatus) {
const conf = MK_BIZ[bizType];
if (!conf) return false;
return (conf.approved || []).includes(approvalStatus);
}
/**
* 提交 MK 审核流
* @param {Object} options
* @param {string} options.bizType 业务类型
* @param {string|number} options.bizId 业务主键(后端据此生成/复用 formInstanceId)
* @param {string} [options.subjectName] 标题后缀
* @param {string} [options.approvalStatus] 提交前的审批状态(须在业务 submit 改状态前取值;通过态强制新建实例)
*/
export async function submitMkApprovalFlow({
bizType,
formInstanceId,
bizId,
subjectName = '',
approvalStatus = '',
}) {
@@ -74,21 +116,34 @@ export async function submitMkApprovalFlow({
if (!conf) {
return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`));
}
if (bizId == null || bizId === '') {
return Promise.reject(new Error('业务id不能为空'));
}
const list = await loadMkTemplateList();
if (!isMkEnabled(list)) {
return;
}
const bizIdText = String(bizId);
const forceNewFormInstance = isMkApprovedStatus(bizType, approvalStatus);
// 审核通过后再提交必须新建流程;其余场景已有有效实例则跳过
if (!forceNewFormInstance) {
const existsRes = await hasMkProcessInstance({ bizId: bizIdText });
if (existsRes?.data?.data) {
return;
}
}
if ((conf.rejected || []).includes(approvalStatus)) {
await processDelete({ formInstanceId: String(formInstanceId) });
await processDelete({ bizId: bizIdText });
}
const templateCode = await resolveMkTemplateCode(conf.dictName, list);
const subject = subjectName
? `${conf.subjectPrefix}:${subjectName}`
: `${conf.subjectPrefix}:${formInstanceId}`;
: `${conf.subjectPrefix}:${bizIdText}`;
return processSubmit({
templateCode,
formInstanceId: String(formInstanceId),
bizId: bizIdText,
subject,
bizType,
forceNewFormInstance,
});
}
+5 -5
View File
@@ -24,7 +24,7 @@
type="danger"
icon="el-icon-delete"
plain
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
@click="handleDelete"
>删除
</el-button>
@@ -43,19 +43,19 @@
<template #menu="{ row }">
<el-link
type="primary"
v-if="row.status === -1 && userInfo.authority.includes('admin')"
v-if="row.status === -1 && isAdministrator"
@click.stop="handleEnable(row)"
>启用
</el-link>
<el-link
type="primary"
v-if="row.status === 1 && userInfo.authority.includes('admin')"
v-if="row.status === 1 && isAdministrator"
@click.stop="handleDisable(row)"
>禁用
</el-link>
<el-link
type="primary"
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
@click.stop="handleRowLog(row)"
>日志
</el-link>
@@ -522,7 +522,7 @@ sortable: true, span: 24, width: 180 },
};
},
computed: {
...mapGetters(['userInfo']),
...mapGetters(['userInfo', 'isAdministrator']),
ids() {
let ids = [];
this.selectionList.forEach(ele => {
+5 -5
View File
@@ -24,7 +24,7 @@
type="danger"
icon="el-icon-delete"
plain
v-if="userInfo.authority.includes('administrator')"
v-if="isAdministrator"
@click="handleDelete"
>删除
</el-button>
@@ -54,19 +54,19 @@
<template #menu="{ row }">
<el-link
type="primary"
v-if="row.status !== 1 && userInfo.authority.includes('administrator')"
v-if="row.status !== 1 && isAdministrator"
@click.stop="handleEnable(row)"
>启用
</el-link>
<el-link
type="primary"
v-if="row.status === 1 && userInfo.authority.includes('administrator')"
v-if="row.status === 1 && isAdministrator"
@click.stop="handleDisable(row)"
>禁用
</el-link>
<el-link
type="primary"
v-if="userInfo.authority.includes('administrator')"
v-if="isAdministrator"
@click.stop="handleRowLog(row)"
>日志
</el-link>
@@ -401,7 +401,7 @@ sortable: true, span: 24, width: 170 },
};
},
computed: {
...mapGetters(['userInfo']),
...mapGetters(['userInfo', 'isAdministrator']),
ids() {
let ids = [];
this.selectionList.forEach(ele => {
+3 -3
View File
@@ -31,7 +31,7 @@
<el-button
icon="el-icon-setting"
@click="handleRole"
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
plain
>权限设置
</el-button>
@@ -39,7 +39,7 @@
<template #menu="{ row }">
<el-link
type="primary"
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
style="border: 0; background-color: transparent !important"
@click.stop="handleRowRole(row)"
>权限设置
@@ -354,7 +354,7 @@ export default {
};
},
computed: {
...mapGetters(['userInfo', 'permission']),
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
permissionList() {
return {
addBtn: this.validData(this.permission.role_add, false),
+9 -5
View File
@@ -150,7 +150,7 @@ import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { getToken, isAdministrator } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import NProgress from 'nprogress';
@@ -460,6 +460,13 @@ export default {
showWordLimit: true,
rules: [{ max: 200, message: '备注不能超过200字', trigger: 'blur' }],
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
minWidth: 120,
},
{
label: '更新人',
prop: 'updateUserName',
@@ -548,10 +555,7 @@ export default {
};
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
ids() {
let ids = [];
+30 -29
View File
@@ -182,8 +182,8 @@
<el-input
v-model="form.regionName"
readonly
disabled
placeholder="行政区划自动带出"
:disabled="dialogReadonly"
/>
<el-input
v-model="form.detailAddress"
@@ -269,8 +269,8 @@ import { exportBlob } from '@/api/common';
import { createOption } from '@/option/base/common-address';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { getToken } from '@/utils/auth';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { getToken, isAdministrator } from '@/utils/auth';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { isMobile } from '@/utils/validate';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -324,16 +324,16 @@ export default {
};
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
},
created() {
if (this.isAdmin) {
this.allDept = 1;
}
this.initDeptTree();
},
methods: {
@@ -1112,31 +1112,32 @@ export default {
this.$message.warning('请输入地址关键词');
return;
}
if (this.mapLoading) return;
this.mapSearchResults = [];
this.mapLoading = true;
this.loadAmap()
.then(() => {
this.mapLoading = true;
this.ensureAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
this.mapSearchResults = normalizeMapSearchResults(result, keyword);
const point = this.resolveMapPoint(result);
if (!point) {
this.mapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
return;
}
this.pickMapPoint(point, keyword);
})
.catch(() => {
this.mapStatus = '地图搜索失败';
this.$message.error('地图搜索失败,请稍后重试');
})
.finally(() => {
this.mapLoading = false;
});
.then(() => searchAmapPlaceText(keyword))
.then(pois => {
this.mapSearchResults = normalizeMapSearchResults(pois, keyword);
const first = this.mapSearchResults[0];
if (!first?.location) {
this.mapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
return;
}
return this.$nextTick().then(() => {
if (typeof this.amap?.resize === 'function') {
this.amap.resize();
}
this.pickMapPoint(first.location, first.address || first.name || keyword);
});
})
.catch(() => {
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
this.mapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
})
.finally(() => {
this.mapLoading = false;
});
},
selectMapSearchResult(item) {
+7
View File
@@ -238,6 +238,13 @@ export default {
value: 'value',
},
},
{
label: '创建人',
prop: 'createUserName',
minWidth: 120,
addDisplay: false,
editDisplay: false,
},
{
label: '更新人',
prop: 'updateUserName',
+2 -1
View File
@@ -143,6 +143,7 @@
import { mapGetters } from 'vuex';
import { getDetail, getList, remove, submit } from '@/api/base/insurance-ocr-template';
import SectionCard from '@/components/section-card/main.vue';
import { isAdministrator } from '@/utils/auth';
const insuranceFieldKeys = [
'保险类型',
@@ -203,7 +204,7 @@ export default {
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
return String(this.userInfo.authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
selectionIds() {
return this.selectionList.map(item => item.id).join(',');
+2 -1
View File
@@ -82,6 +82,7 @@
import { mapGetters } from 'vuex';
import { changeStatus, getDetail, getList, remove, submit } from '@/api/base/measurement-unit';
import { createOption } from '@/option/base/measurement-unit';
import { isAdministrator } from '@/utils/auth';
export default {
data() {
@@ -111,7 +112,7 @@ export default {
return this.selectionList.map(item => item.id).join(',');
},
isAdmin() {
return String(this.userInfo?.authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
},
methods: {
+9 -5
View File
@@ -270,7 +270,7 @@ import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { getToken, isAdministrator } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import NProgress from 'nprogress';
@@ -621,6 +621,13 @@ export default {
showWordLimit: true,
rules: [{ max: 200, message: '备注不能超过200个字', trigger: 'blur' }],
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
minWidth: 120,
},
{
label: '更新人',
prop: 'updateUserName',
@@ -708,10 +715,7 @@ export default {
};
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
isParentRegionLocked() {
return this.form.category === '码头';
+9 -5
View File
@@ -150,7 +150,7 @@ import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
import { openImportDialog } from '@/utils/import-excel';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { getToken, isAdministrator } from '@/utils/auth';
import { getCoordinateValidationMessage, normalizeCoordinateInput } from '@/utils/coordinate';
import AddressMapPicker from '@/components/address-map-picker/main.vue';
import NProgress from 'nprogress';
@@ -433,6 +433,13 @@ export default {
showWordLimit: true,
rules: [{ max: 200, message: '备注不能超过200字', trigger: 'blur' }],
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
minWidth: 120,
},
{
label: '更新人',
prop: 'updateUserName',
@@ -519,10 +526,7 @@ export default {
};
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
ids() {
let ids = [];
+54 -41
View File
@@ -26,6 +26,7 @@
</el-form-item>
<el-form-item label="区域层级">
<el-select v-model="searchForm.regionLevel" clearable placeholder="请选择" style="width: 200px;">
<el-option label="国家" :value="0" />
<el-option label="省份" :value="1" />
<el-option label="省市" :value="2" />
<el-option label="区县" :value="3" />
@@ -42,6 +43,13 @@
</section-card>
<el-button-group>
<el-button
v-if="permission.region_add"
type="primary"
icon="el-icon-plus"
@click="addCountry"
>新增国家
</el-button>
<el-button
v-if="permission.region_add"
type="primary"
@@ -112,14 +120,7 @@
placeholder="请输入国家编码,如 +86"
v-model="regionForm.code"
/>
<el-input
v-else-if="isProvinceRegion"
placeholder="请输入省级区划编号"
v-model="regionForm.subCode"
/>
<el-input v-else placeholder="请输入 区划子编号" v-model="regionForm.subCode">
<template #prepend>{{ regionForm.parentCode }}</template>
</el-input>
<el-input v-else placeholder="请输入区划编号" v-model="regionForm.subCode" />
</template>
</avue-form>
<el-dialog title="行政区划数据导入" append-to-body v-model="excelBox" width="555px">
@@ -253,9 +254,10 @@ export default {
dicUrl: '/blade-system/dict/dictionary?code=region',
dicFormatter: response => {
const list = Array.isArray(response) ? response : response?.data;
return (Array.isArray(list) ? list : [])
.filter(item => Number(item.dictKey) !== 0)
.map(item => ({ ...item, dictKey: Number(item.dictKey) }));
return (Array.isArray(list) ? list : []).map(item => ({
...item,
dictKey: Number(item.dictKey),
}));
},
props: {
label: 'dictValue',
@@ -410,9 +412,6 @@ export default {
isCountryRegion() {
return Number(this.regionForm.regionLevel) === 0;
},
isProvinceRegion() {
return Number(this.regionForm.regionLevel) === 1;
},
isEditRegion() {
return !validatenull(this.regionForm.originalCode);
},
@@ -476,10 +475,7 @@ export default {
getDetail(this.treeCode).then(res => {
this.regionForm = res.data.data;
this.regionForm.originalCode = this.regionForm.code;
this.regionForm.subCode =
this.regionForm.parentCode === this.topCode
? this.regionForm.code
: this.regionForm.code.replace(this.regionForm.parentCode, '');
this.regionForm.subCode = this.regionForm.code;
this.loadParentOptions(this.regionForm.parentCode, this.regionForm.parentName);
});
},
@@ -500,25 +496,31 @@ export default {
};
},
addChildren() {
if (validatenull(this.regionForm.code) || validatenull(this.regionForm.name)) {
const current = this.regionForm || {};
const parentCode = current.originalCode || current.code;
if (validatenull(parentCode) || validatenull(current.name)) {
this.$message.warning('请先选择一项区划');
return;
}
if (Number(this.regionForm.regionLevel) >= 3) {
if (Number(current.regionLevel) >= 3) {
this.$message.warning('区县已是末级,不能新增下级');
return;
}
const parentCode = this.regionForm.code;
const parentName = this.regionForm.name;
const parentGroupCode = this.regionForm.parentCode || this.topCode;
const parentName = current.name;
const parentGroupCode = current.parentCode || this.topCode;
this.setRegionColumnDisplay('code', true);
this.regionForm.parentCode = parentCode;
this.regionForm.parentName = parentName;
this.regionForm.code = '';
this.regionForm.subCode = '';
this.regionForm.originalCode = '';
this.regionForm.name = '';
this.regionForm.regionLevel = Number(this.regionForm.regionLevel) + 1;
this.regionForm = {
parentCode,
parentName,
code: '',
subCode: '',
originalCode: '',
name: '',
regionLevel: Number(current.regionLevel) + 1,
sort: 1,
remark: '',
status: 1,
};
this.loadParentOptionsByGroup(parentGroupCode, parentCode, parentName);
},
setRegionColumnDisplay(prop, display) {
@@ -536,8 +538,7 @@ export default {
if (this.isCountryRegion) {
return;
}
const parentCode = this.isProvinceRegion ? '' : this.regionForm.parentCode || '';
this.regionForm.code = parentCode + (this.regionForm.subCode || '');
this.regionForm.code = this.regionForm.subCode || '';
},
getRootParentOptions() {
return [
@@ -586,16 +587,28 @@ export default {
this.parentOptions = this.ensureSelectedParent(options, selectedCode, selectedName);
});
},
handleSubmit(form, done) {
if (Number(form.regionLevel) === 0) {
form.parentCode = this.topCode;
form.ancestors = this.topCode;
form.subCode = form.code;
} else {
const parentCode = Number(form.regionLevel) === 1 ? '' : form.parentCode;
form.code = parentCode + form.subCode;
buildRegionPayload(form) {
const source = validatenull(this.regionForm) ? form || {} : this.regionForm;
const regionLevel = Number(source.regionLevel);
const isNew = validatenull(source.originalCode);
const payload = {
code: regionLevel === 0 ? source.code : source.subCode,
parentCode: regionLevel === 0 ? this.topCode : source.parentCode,
name: source.name,
regionLevel,
sort: source.sort,
remark: source.remark,
status: source.status == null || source.status === '' ? 1 : source.status,
};
if (!isNew) {
payload.originalCode = source.originalCode;
payload.dataSource = source.dataSource;
}
submit(form).then(
return payload;
},
handleSubmit(form, done) {
const payload = this.buildRegionPayload(form);
submit(payload).then(
() => {
this.$message({
type: 'success',
+61 -13
View File
@@ -204,7 +204,7 @@ import { exportBlob } from '@/api/common';
import { getDeptTree } from '@/api/system/dept';
import { config, excelOption, formatCargoValue, option } from '@/option/business/common-cargo';
import { getDictionary } from '@/api/system/dictbiz';
import { getToken } from '@/utils/auth';
import { getToken, isAdministrator } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
import { downloadXls } from '@/utils/util';
import { mapGetters } from 'vuex';
@@ -250,16 +250,14 @@ export default {
};
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
},
created() {
this.applyDefaultDeptSearch();
this.initDeptTree();
this.loadPriceUnitOptions();
},
@@ -273,6 +271,26 @@ export default {
if (Array.isArray(data)) return data;
return (data && data.records) || [];
},
defaultDeptId() {
// 用户可能绑定多个组织(逗号分隔),搜索下拉是单选,只取当前会话第一个组织 ID。
const raw = (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
return String(raw)
.split(',')
.map(item => item.trim())
.filter(Boolean)[0] || '';
},
applyDefaultDeptSearch() {
const deptId = this.defaultDeptId();
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
if (!deptId || !deptColumn) return;
deptColumn.searchValue = deptId;
this.query = { ...this.query, deptId };
this.$nextTick(() => {
if (this.$refs.crud?.searchForm) {
this.$refs.crud.searchForm.searchDeptId = deptId;
}
});
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
this.deptOptions = this.flattenDept(res.data.data || []);
@@ -280,6 +298,18 @@ export default {
if (deptColumn) {
deptColumn.dicData = this.deptOptions;
}
// dicData 异步到位后再回填,避免下拉选项为空时清空已选组织,导致请求丢失 deptId。
const deptId = String(this.query.deptId || this.defaultDeptId() || '');
if (!deptId) return;
if (deptColumn) {
deptColumn.searchValue = deptId;
}
this.query = { ...this.query, deptId };
this.$nextTick(() => {
if (this.$refs.crud?.searchForm) {
this.$refs.crud.searchForm.searchDeptId = deptId;
}
});
});
},
loadPriceUnitOptions() {
@@ -307,9 +337,13 @@ export default {
flattenDept(tree, level = 0) {
const result = [];
tree.forEach(item => {
// 雪花 ID 必须保持字符串,避免 JS Number 精度丢失导致筛错组织。
const id =
item.id === undefined || item.id === null || item.id === '' ? '' : String(item.id);
if (!id) return;
result.push({
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
value: item.id,
value: id,
});
if (item.children && item.children.length) {
result.push(...this.flattenDept(item.children, level + 1));
@@ -317,6 +351,18 @@ export default {
});
return result;
},
normalizeListQuery(params = {}) {
const query = { ...params, ...this.query };
const rawDeptId = query.deptId ?? query.searchDeptId;
delete query.searchDeptId;
if (rawDeptId !== undefined && rawDeptId !== null && String(rawDeptId).trim() !== '') {
query.deptId = String(rawDeptId).split(',')[0].trim();
} else {
delete query.deptId;
}
query.allDept = this.isAdmin ? 1 : 0;
return query;
},
normalizeCargoTypeOptions(list = []) {
return list
.map(item => ({
@@ -485,7 +531,7 @@ export default {
return false;
}
if (row.cargoValue && !/^\d+(\.\d{1,2})?$/.test(String(row.cargoValue))) {
this.$message.warning('货值只能输入数字,最多保留2位小数');
this.$message.warning('单价只能输入数字,最多保留2位小数');
return false;
}
if (!row.model) {
@@ -619,13 +665,14 @@ export default {
},
searchReset() {
this.query = {};
this.applyDefaultDeptSearch();
this.page.currentPage = 1;
this.onLoad(this.page);
this.onLoad(this.page, this.query);
},
searchChange(params, done) {
this.query = params;
this.query = this.normalizeListQuery(params);
this.page.currentPage = 1;
this.onLoad(this.page, params);
this.onLoad(this.page, this.query);
done();
},
selectionChange(list) {
@@ -648,8 +695,10 @@ export default {
},
onLoad(page, params = {}) {
this.loading = true;
const query = this.normalizeListQuery(params);
this.query = query;
api
.getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: 0 })
.getList(page.currentPage, page.pageSize, query)
.then(res => {
const result = res.data.data;
this.page.total = result.total;
@@ -662,8 +711,7 @@ export default {
},
buildExportParams() {
return {
...this.query,
allDept: 0,
...this.normalizeListQuery(this.query),
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
+26 -21
View File
@@ -321,9 +321,9 @@ import { exportBlob } from '@/api/common';
import { getDeptTree } from '@/api/system/dept';
import { addressTypeOptions } from '@/option/base/common-address';
import { config, excelOption, option } from '@/option/business/common-route';
import { getToken } from '@/utils/auth';
import { getToken, isAdministrator } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
import { List, Search } from '@element-plus/icons-vue';
@@ -390,10 +390,7 @@ export default {
};
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
ids() {
return this.selectionList.map(item => item.id).join(',');
@@ -490,7 +487,7 @@ export default {
this.addressLoading = true;
getCommonAddressList(this.addressPage.currentPage, this.addressPage.pageSize, {
...this.addressQuery,
allDept: 0,
allDept: this.isAdmin ? 1 : 0,
})
.then(res => {
const data = res.data.data || {};
@@ -610,25 +607,29 @@ export default {
this.$message.warning('请输入地址关键词');
return;
}
if (this.routeMapLoading) return;
this.routeMapSearchResults = [];
this.routeMapLoading = true;
this.loadAmap()
.then(() => {
this.routeMapLoading = true;
return this.ensureRouteAmapGeocoder();
})
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
this.routeMapSearchResults = normalizeMapSearchResults(result, keyword);
const point = this.resolveMapPoint(result);
if (!point) {
.then(() => searchAmapPlaceText(keyword))
.then(pois => {
this.routeMapSearchResults = normalizeMapSearchResults(pois, keyword);
const first = this.routeMapSearchResults[0];
if (!first?.location) {
this.routeMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
return;
}
this.pickRouteMapPoint(point, keyword);
return this.$nextTick().then(() => {
if (typeof this.routeAmap?.resize === 'function') {
this.routeAmap.resize();
}
this.pickRouteMapPoint(first.location, first.address || first.name || keyword);
});
})
.catch(() => {
this.routeMapStatus = '地图搜索失败';
this.$message.error('地图搜索失败,请稍后重试');
this.routeMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
})
.finally(() => {
this.routeMapLoading = false;
@@ -1024,7 +1025,11 @@ export default {
onLoad(page, params = {}) {
this.loading = true;
api
.getList(page.currentPage, page.pageSize, { ...params, ...this.query, allDept: 0 })
.getList(page.currentPage, page.pageSize, {
...params,
...this.query,
allDept: this.isAdmin ? 1 : 0,
})
.then(res => {
const result = res.data.data;
this.page.total = result.total;
@@ -1038,7 +1043,7 @@ export default {
buildExportParams() {
return {
...this.query,
allDept: 0,
allDept: this.isAdmin ? 1 : 0,
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
@@ -2423,7 +2423,7 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" width="200" />
<el-table-column prop="vehicleNo" label="车牌号/船班号/船次号/班次号" width="200" />
<el-table-column prop="driverName" label="司机" width="128" />
<el-table-column prop="carrierName" label="承运商" width="192" />
<el-table-column label="运输方式" width="192">
@@ -2909,14 +2909,15 @@
原:{{ row.originalDepartureAddress || '-' }}
</div>
<el-input
v-model="row.departureAddress"
placeholder="请选择或输入地址"
@input="updateWaybillRouteChangeNodes"
:model-value="row.departureAddress"
readonly
placeholder="请点击地图选点"
@click="openWaybillRouteChangeMapDialog(row, 'departure')"
>
<template #suffix>
<el-icon
class="business-crud-page__route-change-location"
@click="openWaybillCommonAddressDialog(row, 'departure')"
@click.stop="openWaybillRouteChangeMapDialog(row, 'departure')"
>
<Location />
</el-icon>
@@ -2930,14 +2931,15 @@
原:{{ row.originalArrivalAddress || '-' }}
</div>
<el-input
v-model="row.arrivalAddress"
placeholder="请选择或输入地址"
@input="updateWaybillRouteChangeNodes"
:model-value="row.arrivalAddress"
readonly
placeholder="请点击地图选点"
@click="openWaybillRouteChangeMapDialog(row, 'arrival')"
>
<template #suffix>
<el-icon
class="business-crud-page__route-change-location"
@click="openWaybillCommonAddressDialog(row, 'arrival')"
@click.stop="openWaybillRouteChangeMapDialog(row, 'arrival')"
>
<Location />
</el-icon>
@@ -2993,49 +2995,6 @@
</template>
</el-dialog>
<el-dialog
v-model="waybillCommonAddressBox"
title="选择常用地址"
append-to-body
width="900px"
class="business-crud-page__common-address-dialog"
>
<div class="business-crud-page__common-address-toolbar">
<el-input
v-model="waybillCommonAddressQuery.addressName"
clearable
placeholder="请输入地址名称"
@keyup.enter="loadWaybillCommonAddresses"
/>
<el-button type="primary" @click="loadWaybillCommonAddresses">查询</el-button>
</div>
<el-table
v-loading="waybillCommonAddressLoading"
:data="waybillCommonAddressRows"
border
@row-dblclick="selectWaybillCommonAddress"
>
<el-table-column prop="addressName" label="地址名称" min-width="180" />
<el-table-column prop="regionName" label="区域" min-width="180" />
<el-table-column prop="detailAddress" label="详细地址" min-width="260" />
<el-table-column label="操作" width="90">
<template #default="{ row }">
<el-link type="primary" @click="selectWaybillCommonAddress(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-pagination
v-model:current-page="waybillCommonAddressPage.currentPage"
v-model:page-size="waybillCommonAddressPage.pageSize"
layout="total, prev, pager, next"
:total="waybillCommonAddressPage.total"
@current-change="loadWaybillCommonAddresses"
/>
<el-button @click="waybillCommonAddressBox = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog
v-if="config.enableTransportPlanImport"
v-model="transportPlanImportBox"
@@ -3514,7 +3473,7 @@
</el-table-column>
<el-table-column
prop="vehicleNo"
label="车牌号/航班号/船号/班列号"
label="车牌号/船班号/船次号/班次号"
min-width="200"
align="center"
show-overflow-tooltip
@@ -4169,7 +4128,7 @@
</el-form-item>
<el-form-item
v-if="dispatchItemForm.carrierType"
:label="dispatchIsNonRoadTransport ? '船/航/班列号' : '车牌号'"
:label="dispatchIsNonRoadTransport ? '船班号/船次号/班次号' : '车牌号'"
required
>
<el-input v-model="dispatchItemForm.vehicleNo" placeholder="请输入" />
@@ -4982,6 +4941,7 @@
v-model="transportMapBox"
width="920px"
@opened="initTransportAmap"
@closed="waybillRouteChangeMapTarget = null"
>
<div class="business-crud-page__map-toolbar">
<el-input
@@ -5010,7 +4970,7 @@
>
</div>
<template #footer>
<el-button @click="transportMapBox = false">取消</el-button>
<el-button @click="closeTransportMapDialog">取消</el-button>
<el-button
type="primary"
:disabled="!transportMapSelected.longitude"
@@ -5281,9 +5241,9 @@ import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { getToken, isAdministrator } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
@@ -5633,18 +5593,7 @@ export default {
},
],
},
waybillCommonAddressBox: false,
waybillCommonAddressLoading: false,
waybillCommonAddressQuery: {
addressName: '',
},
waybillCommonAddressRows: [],
waybillCommonAddressTarget: null,
waybillCommonAddressPage: {
pageSize: 10,
currentPage: 1,
total: 0,
},
waybillRouteChangeMapTarget: null,
transportPlanImportBox: false,
transportPlanImportLoading: false,
transportPlanImportFiles: [],
@@ -6075,7 +6024,7 @@ export default {
return this.config.permission === 'waybill_manage' && !this.isRoadTransport;
},
transportVehicleNoLabel() {
return this.isRoadTransport ? '车牌号' : '船/航/班列号';
return this.isRoadTransport ? '车牌号' : '船班号/船次号/班次号';
},
transportPlanImportDuplicateCount() {
return this.transportPlanImportRows.filter(row => row._duplicate).length;
@@ -6399,10 +6348,7 @@ export default {
return this.currencyRemark(currency);
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
canViewAllDept() {
return this.isAdmin;
@@ -7121,7 +7067,7 @@ export default {
return String(row.carrierTypeName || row.carrierType || '').trim() === '承运商';
},
waybillVehicleNoLabel(row = {}) {
return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号';
return this.waybillIsRoadTransport(row) ? '车牌号' : '船班号/船次号/班次号';
},
waybillUnitPrice(row = {}) {
const price = row.unitPrice || row.price || '';
@@ -7440,43 +7386,22 @@ export default {
updateWaybillRouteChangeNodes() {
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
},
openWaybillCommonAddressDialog(row, field) {
this.waybillCommonAddressTarget = { row, field };
this.waybillCommonAddressQuery = { addressName: '' };
this.waybillCommonAddressPage.currentPage = 1;
this.waybillCommonAddressBox = true;
this.loadWaybillCommonAddresses();
},
loadWaybillCommonAddresses() {
this.waybillCommonAddressLoading = true;
getCommonAddressList(
this.waybillCommonAddressPage.currentPage,
this.waybillCommonAddressPage.pageSize,
{
...this.waybillCommonAddressQuery,
allDept: 0,
}
)
.then(res => {
const data = res.data?.data || {};
this.waybillCommonAddressRows = data.records || [];
this.waybillCommonAddressPage.total = data.total || 0;
})
.finally(() => {
this.waybillCommonAddressLoading = false;
});
},
selectWaybillCommonAddress(address) {
const target = this.waybillCommonAddressTarget;
if (!target) return;
target.row[`${target.field}Address`] =
[address.regionName, address.detailAddress || address.addressName]
.filter(Boolean)
.join('') ||
address.addressName ||
'';
this.updateWaybillRouteChangeNodes();
this.waybillCommonAddressBox = false;
openWaybillRouteChangeMapDialog(row, field) {
if (!row || !field) return;
this.waybillRouteChangeMapTarget = { row, field };
this.transportMapTarget = field;
this.transportMapKeyword = row[`${field}Address`] || row[`${field}Name`] || '';
this.transportMapSelected = this.buildTransportMapSelection({
lng: row[`${field}Longitude`],
lat: row[`${field}Latitude`],
address: row[`${field}Address`],
regionName: row[`${field}Name`],
regionCode: row[`${field}RegionCode`],
});
this.transportMapStatus = this.transportMapSelected.longitude
? '已加载当前选点,可重新选点'
: '可搜索地址或点击地图选点';
this.transportMapBox = true;
},
async confirmWaybillRouteChange() {
const row = this.waybillRouteChangeRows[0] || {};
@@ -9491,7 +9416,7 @@ export default {
}
} else {
const invalidLengthField = [
['vehicleNo', '船/航/班列号', 30],
['vehicleNo', '船班号/船次号/班次号', 30],
['captainName', '船长', 20],
['cabinNo', '舱位', 30],
['containerNo', '箱号', 30],
@@ -10201,10 +10126,15 @@ export default {
this.$refs.crud?.clearValidate?.([`${prefix}Address`]);
});
},
closeTransportMapDialog() {
this.transportMapBox = false;
this.waybillRouteChangeMapTarget = null;
},
openTransportMapDialog(target) {
if (this.dialogReadonly) return;
if (!this.ensureTransportTypeBeforeAddress()) return;
const prefix = target === 'departure' ? 'departure' : 'arrival';
this.waybillRouteChangeMapTarget = null;
this.transportMapTarget = prefix;
this.transportMapKeyword = this.form[`${prefix}Address`] || this.form[`${prefix}Name`] || '';
this.transportMapSelected = this.buildTransportMapSelection({
@@ -10281,31 +10211,32 @@ export default {
this.$message.warning('请输入地址关键词');
return;
}
if (this.transportMapLoading) return;
this.transportMapSearchResults = [];
this.transportMapLoading = true;
this.loadAmap()
.then(() => {
this.transportMapLoading = true;
this.ensureTransportAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
const point = this.resolveMapPoint(result);
if (!point) {
this.transportMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
return;
}
this.pickTransportMapPoint(point, keyword);
})
.catch(() => {
this.transportMapStatus = '地图搜索失败';
this.$message.error('地图搜索失败,请稍后重试');
})
.finally(() => {
this.transportMapLoading = false;
});
.then(() => searchAmapPlaceText(keyword))
.then(pois => {
this.transportMapSearchResults = normalizeMapSearchResults(pois, keyword);
const first = this.transportMapSearchResults[0];
if (!first?.location) {
this.transportMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
return;
}
return this.$nextTick().then(() => {
if (typeof this.transportAmap?.resize === 'function') {
this.transportAmap.resize();
}
this.pickTransportMapPoint(first.location, first.address || first.name || keyword);
});
})
.catch(() => {
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
this.transportMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
})
.finally(() => {
this.transportMapLoading = false;
});
},
selectTransportMapSearchResult(item) {
@@ -10363,6 +10294,23 @@ export default {
this.$message.warning('请先搜索或点击地图完成选点');
return;
}
if (this.waybillRouteChangeMapTarget) {
const { row, field } = this.waybillRouteChangeMapTarget;
row[`${field}Address`] =
this.transportMapSelected.detailAddress || row[`${field}Address`] || '';
if (this.transportMapSelected.regionName) {
row[`${field}Name`] = this.transportMapSelected.regionName;
}
row[`${field}Longitude`] = this.transportMapSelected.longitude;
row[`${field}Latitude`] = this.transportMapSelected.latitude;
if (this.transportMapSelected.regionCode) {
row[`${field}RegionCode`] = this.transportMapSelected.regionCode;
}
this.updateWaybillRouteChangeNodes();
this.waybillRouteChangeMapTarget = null;
this.transportMapBox = false;
return;
}
const prefix = this.transportMapTarget;
this.form[`${prefix}Address`] =
this.transportMapSelected.detailAddress || this.form[`${prefix}Address`];
@@ -13278,7 +13226,7 @@ export default {
[
'vehicleNo',
['water', 'rail', 'air'].includes(this.resolveTransportMode(row.transportType))
? '船/航/班列号'
? '船班号/船次号/班次号'
: '车牌号',
],
];
@@ -13358,7 +13306,7 @@ export default {
['escortPhone', '押运人手机号', 20],
]
: [
['vehicleNo', '船/航/班列号', 30],
['vehicleNo', '船班号/船次号/班次号', 30],
['captainName', '船长', 20],
['cabinNo', '舱位', 30],
['containerNo', '箱号', 30],
@@ -15914,16 +15862,6 @@ export default {
white-space: pre-wrap;
}
&__common-address-toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
.el-input {
width: 260px;
}
}
@media (max-width: 1200px) {
&__waybill-summary-grid {
grid-template-columns: repeat(4, minmax(130px, 1fr));
@@ -79,14 +79,21 @@
<section v-if="master" class="master-goods-card">
<div class="master-goods-card__heading"><h3>货物信息</h3></div>
<el-table :data="master.goods || []" border>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="cargoName" label="货物名称" min-width="150" />
<el-table-column prop="cargoType" label="类型" min-width="130" />
<el-table-column prop="cargoType" min-width="130">
<template #header
><span>货物类型<span class="required-asterisk">*</span></span></template
>
</el-table-column>
<el-table-column prop="packageType" label="包装" min-width="120" />
<el-table-column label="数量" min-width="100"><template #default="{ row }">{{ row.quantity ?? '-' }}{{ row.quantityUnit ? ` ${row.quantityUnit}` : '' }}</template></el-table-column>
<el-table-column prop="quantity" label="数量" min-width="100" />
<el-table-column prop="quantityUnit" label="数量单位" width="100" />
<el-table-column prop="brand" label="品牌" min-width="120" />
<el-table-column prop="specification" label="规格" min-width="150" />
<el-table-column prop="model" label="型号" min-width="150" />
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
<el-table-column prop="specification" label="规格" min-width="120" />
<el-table-column prop="model" label="型号" min-width="120" />
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
<el-table-column label="操作" width="100" align="center" fixed="right" />
</el-table>
<el-empty v-if="!(master.goods || []).length" description="暂无货物信息" :image-size="56" />
</section>
@@ -344,5 +351,7 @@ export default {
.transport-plan-detail { margin-top: 20px; h4 { margin: 0 0 12px; padding-left: 12px; border-left: 4px solid #409eff; font-size: 15px; } }
.master-goods-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
.master-goods-card__heading { padding: 20px 24px 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
.master-goods-card :deep(.el-table) { margin: 0 24px 20px; width: auto; }
.required-asterisk { margin-left: 4px; color: #f56c6c; }
@media screen and (max-width: 1200px) { .detail-meta { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
</style>
@@ -51,9 +51,29 @@
<section v-for="(route, index) in routes" :key="route.segmentNo" class="dispatch-section" :data-segment="route.segmentNo">
<header class="segment-header">
<div>
<el-checkbox v-model="route.selected">
<el-tooltip
v-if="!canDispatchSegment(route)"
content="需等上一段运单完成到达后才能调度"
placement="top"
>
<span class="segment-checkbox-wrap">
<el-checkbox :model-value="false" disabled>
<span class="segment-checkbox-label">
<span class="segment-checkbox-no">{{ route.segmentNo }}:</span>
<span class="segment-checkbox-path"
>{{ dispatchLocationName(route, 'departure') }} → {{ dispatchLocationName(route, 'arrival') }}</span
>
</span>
</el-checkbox>
</span>
</el-tooltip>
<el-checkbox
v-else
v-model="route.selected"
@change="value => handleSegmentSelect(route, value)"
>
<span class="segment-checkbox-label">
<span>{{ route.segmentNo }}:</span>
<span class="segment-checkbox-no">{{ route.segmentNo }}:</span>
<el-tooltip
:content="`${dispatchLocationName(route, 'departure')} → ${dispatchLocationName(route, 'arrival')}`"
placement="top"
@@ -65,7 +85,15 @@
</span>
</el-checkbox>
</div>
<div>总量 {{ formatQuantity(totalQuantity) }} {{ quantityUnitLabel }},已调度 <em>{{ formatQuantity(dispatchedQuantity(route)) }} {{ quantityUnitLabel }}</em>,剩余 <em>{{ formatQuantity(remaining(route)) }} {{ quantityUnitLabel }}</em></div>
<div>
总量 {{ formatQuantity(totalQuantity) }} {{ quantityUnitLabel }},已调度
<em>{{ formatQuantity(dispatchedQuantity(route)) }} {{ quantityUnitLabel }}</em>,剩余
<em>{{ formatQuantity(remaining(route)) }} {{ quantityUnitLabel }}</em>
<template v-if="index > 0">
,上一段已到达
<em>{{ formatQuantity(previousArrivedQuantity(route)) }} {{ quantityUnitLabel }}</em>
</template>
</div>
</header>
<div v-show="route.selected" class="segment-content">
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form">
@@ -78,16 +106,28 @@
<el-col :span="12">
<el-form-item label="发货地址" required><el-input :model-value="addressText(route.departureName, route.departureAddress)" disabled /></el-form-item>
</el-col>
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.departurePhone" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="4"
><el-form-item label="联系人" label-width="68px" class="dispatch-form__compact-label"
><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item
></el-col>
<el-col :span="4"
><el-form-item label="联系方式" label-width="80px" class="dispatch-form__compact-label"
><el-input v-model="route.departurePhone" placeholder="请输入" /></el-form-item
></el-col>
<el-col :span="4"><el-form-item :label="dateStartLabel(route)"><el-date-picker v-model="route.estimatedStartTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="收货地址" required><el-input :model-value="addressText(route.arrivalName, route.arrivalAddress)" disabled /></el-form-item>
</el-col>
<el-col :span="4"><el-form-item label="联系人"><el-input v-model="route.arrivalContact" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="4"><el-form-item label="联系方式"><el-input v-model="route.arrivalPhone" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="4"
><el-form-item label="联系人" label-width="68px" class="dispatch-form__compact-label"
><el-input v-model="route.arrivalContact" placeholder="请输入" /></el-form-item
></el-col>
<el-col :span="4"
><el-form-item label="联系方式" label-width="80px" class="dispatch-form__compact-label"
><el-input v-model="route.arrivalPhone" placeholder="请输入" /></el-form-item
></el-col>
<el-col :span="4"><el-form-item :label="dateEndLabel(route)"><el-date-picker v-model="route.estimatedEndTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></el-col>
</el-row>
</el-form>
@@ -156,16 +196,59 @@
<el-row :gutter="16">
<template v-if="isRoad(route)">
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType !== '自运'"><el-select :model-value="route.carrierType !== '自运' ? route.carrierContractId : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType !== '自运' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="司机" :required="route.carrierType !== '承运商'"><el-autocomplete :ref="element => setDriverInput(route.segmentNo, element)" v-model="route.driverName" :debounce="300" :fetch-suggestions="(query, cb) => fetchDriverSuggestions(route, query, cb)" clearable placeholder="请输入司机" :loading="driverLoading" @select="item => handleDriverSuggestionSelect(route, item)" /></el-form-item></el-col>
<el-col :span="6"><el-form-item :label="route.carrierType === '承运商' ? '手机号' : '司机手机号'" :required="route.carrierType !== '承运商'"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="车牌号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="挂车车牌号"><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人"><el-autocomplete v-model="route.escortName" :debounce="300" :fetch-suggestions="(query, cb) => fetchEscortSuggestions(route, query, cb)" clearable placeholder="请输入" :loading="escortLoading" @select="item => handleEscortSuggestionSelect(route, item)" /></el-form-item></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号"><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"
><el-form-item label="司机" :required="route.carrierType !== '承运商'"
><el-autocomplete
:ref="element => setDriverInput(route.segmentNo, element)"
v-model="route.driverName"
clearable
trigger-on-focus
:debounce="300"
:fetch-suggestions="fetchDriverSuggestions"
:loading="driverLoading"
placeholder="请输入司机"
@mousedown="() => handleDriverFocus(route)"
@focus="() => handleDriverFocus(route)"
@select="handleDriverSuggestionSelect"
/></el-form-item
></el-col>
<el-col :span="6"
><el-form-item
:label="route.carrierType === '承运商' ? '手机号' : '司机手机号'"
:required="route.carrierType !== '承运商'"
><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item
></el-col>
<el-col :span="6"
><el-form-item label="车牌号" required
><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item
></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"
><el-form-item label="挂车车牌号"
><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item
></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"
><el-form-item label="押运人"
><el-autocomplete
v-model="route.escortName"
clearable
trigger-on-focus
:debounce="300"
:fetch-suggestions="fetchEscortSuggestions"
:loading="escortLoading"
placeholder="请输入"
@mousedown="() => handleDriverFocus(route)"
@focus="() => handleDriverFocus(route)"
@select="handleEscortSuggestionSelect"
/></el-form-item
></el-col>
<el-col v-if="route.carrierType !== '承运商'" :span="6"
><el-form-item label="押运人手机号"
><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item
></el-col>
</template>
<template v-else>
<el-col :span="6"><el-form-item label="承运商" :required="route.carrierType !== '自运'"><el-select :model-value="route.carrierType !== '自运' ? route.carrierContractId : route.carrierName" filterable clearable placeholder="请选择" :loading="carrierLoading" @change="value => handleCarrierContractChange(route, value)"><el-option v-for="item in carrierOptions" :key="carrierOptionValue(item)" :label="carrierOptionLabel(item)" :value="route.carrierType !== '自运' ? carrierOptionValue(item) : item.carrierName" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="船/航/班列号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="船班号/船次号/班次号" required><el-input v-model="route.vehicleNo" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="船长"><el-input v-model="route.captainName" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="联系电话"><el-input v-model="route.driverPhone" placeholder="请输入" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="箱号"><el-input v-model="route.containerNo" placeholder="请输入" /></el-form-item></el-col>
@@ -249,7 +332,23 @@ export default {
props: { id: [String, Number] },
emits: ['back'],
data() {
return { loading: false, submitting: false, master: null, routes: [], pending: [], pendingExpanded: false, editingId: null, driverInputs: {}, carrierOptions: [], driverOptions: [], carrierLoading: false, driverLoading: false, escortLoading: false, quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'] };
return {
loading: false,
submitting: false,
master: null,
routes: [],
pending: [],
pendingExpanded: false,
editingId: null,
driverInputs: {},
activeSuggestRoute: null,
carrierOptions: [],
driverOptions: [],
carrierLoading: false,
driverLoading: false,
escortLoading: false,
quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'],
};
},
computed: {
totalQuantity() { return (this.master?.goods || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); },
@@ -453,12 +552,29 @@ export default {
dispatchedQuantity(route) {
return this.quantityNumber(route.dispatchedQuantity) + this.pendingSegmentQuantity(route);
},
previousArrivedQuantity(route) {
const routeIndex = this.routes.indexOf(route);
if (routeIndex <= 0) return null;
return this.quantityNumber(this.routes[routeIndex - 1]?.arrivedQuantity);
},
canDispatchSegment(route) {
const previousArrived = this.previousArrivedQuantity(route);
if (previousArrived === null) return true;
return previousArrived > 0;
},
remaining(route) {
return Math.max(
0,
this.totalQuantity -
this.dispatchedQuantity(route)
);
const masterRemaining = Math.max(0, this.totalQuantity - this.dispatchedQuantity(route));
const previousArrived = this.previousArrivedQuantity(route);
if (previousArrived === null) return masterRemaining;
return Math.max(0, Math.min(masterRemaining, previousArrived - this.dispatchedQuantity(route)));
},
handleSegmentSelect(route, value) {
if (value && !this.canDispatchSegment(route)) {
route.selected = false;
this.$message.warning(
`${route.segmentNo}需等上一段运单完成到达后才能调度(上一段已到达 ${this.formatQuantity(this.previousArrivedQuantity(route))} ${this.quantityUnitLabel})`
);
}
},
formatQuantity(value) { const number = Number(value || 0); return Number.isInteger(number) ? number : number.toFixed(3).replace(/\.?0+$/, ''); },
formatAmount(value) { return Number(value || 0).toFixed(2); },
@@ -518,7 +634,7 @@ export default {
})),
});
},
currencyLabel(value) { return ({ CNY: '人民币', RMB: '人民币', USD: '美元', EUR: '欧元' })[value] || value || '人民币'; },
currencyLabel(value) { return ({ CNY: '元', RMB: '元', USD: '美元', EUR: '欧元' })[value] || value || '元'; },
quantityNumber(value) { return Number(value || 0); },
goodsKey(goods = {}, includeUnit = true) {
return [
@@ -563,13 +679,14 @@ export default {
}, 0);
},
validatePreviousSegmentCompletedQuantity(route, goods) {
const routeIndex = this.routes.indexOf(route);
if (routeIndex <= 0) return true;
const previousCompletedQuantity = this.quantityNumber(
this.routes[routeIndex - 1]?.arrivedQuantity
);
const previousCompletedQuantity = this.previousArrivedQuantity(route);
if (previousCompletedQuantity === null) return true;
if (previousCompletedQuantity <= 0) {
this.$message.warning(`${route.segmentNo}需等上一段运单完成到达后才能调度`);
return false;
}
const currentDispatchQuantity = (goods || []).reduce(
(sum, item) => sum + this.quantityNumber(item.dispatchQuantity),
(sum, item) => sum + this.quantityNumber(item.dispatchQuantity ?? item.quantity),
0
);
const dispatchTotal =
@@ -578,7 +695,7 @@ export default {
currentDispatchQuantity;
if (dispatchTotal <= previousCompletedQuantity) return true;
this.$message.warning(
`${route.segmentNo}调度总量不能大于上一段已完成数量(${this.formatQuantity(previousCompletedQuantity)} ${this.quantityUnitLabel})`
`${route.segmentNo}调度总量不能大于上一段已到达数量(${this.formatQuantity(previousCompletedQuantity)} ${this.quantityUnitLabel})`
);
return false;
},
@@ -836,6 +953,9 @@ export default {
route.carrierContractId = '';
});
},
handleDriverFocus(route) {
this.activeSuggestRoute = route;
},
async loadDriverOptions(route = {}, keyword = '') {
const carrier = this.getRouteCarrierFilter(route);
if (!carrier.carrierId && !carrier.carrierName) {
@@ -853,35 +973,50 @@ export default {
this.driverLoading = false;
}
},
fetchDriverSuggestions(route, queryString, callback) {
fetchDriverSuggestions(queryString, callback) {
const route = this.activeSuggestRoute || {};
const carrier = this.getRouteCarrierFilter(route);
const keyword = String(queryString || '').trim();
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
if (!keyword) this.$message.warning('请先选择承运商');
return;
}
this.loadDriverOptions(route, String(queryString || '').trim())
.then(() =>
this.driverLoading = true;
fetchDriversByCarrierOrganizations(
{ size: 20, ...(keyword ? { driverName: keyword } : {}), posts: '司机' },
carrier
)
.then(records => {
this.driverOptions = records;
callback(
this.driverOptions.map(item => ({
records.map(item => ({
...item,
value: this.driverOptionLabel(item),
}))
)
)
.catch(() => callback([]));
);
})
.catch(() => callback([]))
.finally(() => {
this.driverLoading = false;
});
},
handleDriverSuggestionSelect(route, item = {}) {
handleDriverSuggestionSelect(item = {}) {
const route = this.activeSuggestRoute;
if (!route) return;
route.driverName = this.driverOptionLabel(item);
route.driverId = item.id || item.driverId || '';
route.driverPhone = item.mobile || item.driverPhone || item.phone || route.driverPhone || '';
const drivingVehicle = String(item.drivingVehicle || '').trim();
if (drivingVehicle) route.vehicleNo = drivingVehicle;
},
fetchEscortSuggestions(route, queryString, callback) {
fetchEscortSuggestions(queryString, callback) {
const route = this.activeSuggestRoute || {};
const keyword = String(queryString || '').trim();
const carrier = this.getRouteCarrierFilter(route);
if (!carrier.carrierId && !carrier.carrierName) {
callback([]);
if (!keyword) this.$message.warning('请先选择承运商');
return;
}
this.escortLoading = true;
@@ -902,7 +1037,9 @@ export default {
this.escortLoading = false;
});
},
handleEscortSuggestionSelect(route, item = {}) {
handleEscortSuggestionSelect(item = {}) {
const route = this.activeSuggestRoute;
if (!route) return;
route.escortName = this.driverOptionLabel(item);
const phone = item.mobile || item.driverPhone || item.phone || '';
if (phone) route.escortPhone = phone;
@@ -927,6 +1064,10 @@ export default {
addToPending(route, continueDispatch) {
const goods = route.goods.filter(item => Number(item.dispatchQuantity) > 0);
if (!goods.length) return this.$message.warning('请填写本次数量');
if (!this.canDispatchSegment(route)) {
return this.$message.warning(`${route.segmentNo}需等上一段运单完成到达后才能调度`);
}
if (!this.validatePreviousSegmentCompletedQuantity(route, goods)) return;
if (!this.validateDispatchGoods(route, goods)) return;
this.syncFreightItems(route);
if (!this.validateFreightItems(route)) return;
@@ -1041,9 +1182,11 @@ export default {
.pending-list-spacer { height: min(436px, calc(100vh - 130px)); }
.master-overview, .dispatch-section { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
.overview-heading, .segment-header, .pending-bar { display: flex; align-items: center; justify-content: space-between; }
.segment-header :deep(.el-checkbox.is-checked .el-checkbox__label) { font-size: 16px; font-weight: 700; }
.segment-checkbox-label { display: inline-flex; align-items: center; min-width: 0; }
.segment-checkbox-path { display: inline-block; white-space: nowrap; }
.segment-checkbox-wrap { display: inline-flex; max-width: 100%; }
.segment-checkbox-label { display: inline-flex; align-items: center; min-width: 0; font-size: 14px; font-weight: 400; color: #303133; }
/* 仅「段X:」加粗加大,冒号右侧路线保持常规字号 */
.segment-checkbox-no { font-size: 16px; font-weight: 700; color: #303133; }
.segment-checkbox-path { display: inline-block; white-space: nowrap; font-size: 14px; font-weight: 400; }
.overview-heading { padding: 18px 24px 12px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
.overview-content { display: grid; grid-template-columns: minmax(400px, .9fr) minmax(620px, 1.6fr); gap: 28px; padding: 8px 24px 20px; }
.overview-meta { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 24px; margin: 0; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
@@ -1055,6 +1198,12 @@ export default {
.segment-header { min-height: 46px; padding: 0 20px; border-bottom: 1px solid #eff1f7; font-weight: 600; em { color: #409eff; font-style: normal; } }
.segment-content { padding: 16px 24px; }
.dispatch-form { :deep(.el-form-item) { margin-bottom: 14px; } :deep(.el-input), :deep(.el-select), :deep(.el-date-editor), :deep(.el-input-number) { width: 100%; } :deep(.el-input-group__append .el-select) { width: auto; } }
/* 短标签按自身宽度,避免被「预计发货日期」等长标签把左侧撑开 */
.dispatch-form__compact-label {
:deep(.el-form-item__label) {
justify-content: flex-end;
}
}
.transport-type-field :deep(.el-input) { width: 240px; }
.goods-heading { display: flex; align-items: center; justify-content: space-between; margin: 0 -24px 12px; padding: 14px 24px; border-top: 1px solid #eff1f7; border-bottom: 1px solid #eff1f7; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } span { color: #909399; font-size: 13px; } }
.freight-heading { margin: 16px 0 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
@@ -9,6 +9,7 @@
><el-select
v-model="form.projectId"
filterable
:disabled="isBasicRouteReadonly"
placeholder="请输入并选择"
@change="projectChange"
><el-option
@@ -20,7 +21,7 @@
><el-form-item label="客户合同" prop="contractName"
><el-select
v-model="form.contractId"
:disabled="!form.projectId"
:disabled="isBasicRouteReadonly || !form.projectId"
placeholder="选择项目后带出"
@change="contractChange"
><el-option
@@ -41,6 +42,7 @@
><el-form-item label="备注"
><el-input
v-model="form.remark"
:disabled="isBasicRouteReadonly"
maxlength="200"
show-word-limit
placeholder="请输入" /></el-form-item></el-col></el-row
@@ -49,7 +51,7 @@
<section>
<div class="section-head">
<h3>收发货信息</h3>
<div class="section-head__actions">
<div v-if="!isBasicRouteReadonly" class="section-head__actions">
<el-link type="primary" @click="addRoute">新增途经点</el-link>
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
</div>
@@ -62,13 +64,13 @@
<div class="route-node-form">
<el-form-item label="发货地" required class="route-address-form-item"
><div class="route-address-inputs"
><el-input v-if="isNonRoadAddress('departure')" v-model="form.departureName" class="route-address-name" readonly placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths.departure" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('departure', value)" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly placeholder="请输入详细地址" @click="handleAddressDetailClick('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" @click="openCommonAddress('departure')" /></el-tooltip></div
><el-input v-if="isNonRoadAddress('departure')" v-model="form.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly" placeholder="请选择站点" /><el-cascader v-else v-model="addressRegionPaths.departure" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('departure', value)" /><el-input v-model="form.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly" placeholder="请输入详细地址" @click="handleAddressDetailClick('departure')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly" @click="openCommonAddress('departure')" /></el-tooltip></div
></el-form-item>
<el-form-item label="联系人" class="route-contact-form-item"
><el-input v-model="form.departureContact" placeholder="请输入"
><el-input v-model="form.departureContact" :disabled="isBasicRouteReadonly" placeholder="请输入"
/></el-form-item>
<el-form-item label="联系方式" class="route-phone-form-item"
><el-input v-model="form.departurePhone" placeholder="请输入"
><el-input v-model="form.departurePhone" :disabled="isBasicRouteReadonly" placeholder="请输入"
/></el-form-item>
<el-form-item label="计划开始" class="route-date-form-item"
><el-date-picker
@@ -76,6 +78,7 @@
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="isBasicRouteReadonly"
placeholder="请选择"
/></el-form-item>
</div>
@@ -83,7 +86,7 @@
</div>
<template v-for="(route, index) in form.routes" :key="index">
<div class="route-flow-segment">
<div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(index + 1) }} 段{{ index + 1 }}</span><el-tooltip :content="getRouteTitle(index).replace(`段${index + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getRouteTitle(index), index + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="route.transportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
<div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(index + 1) }} 段{{ index + 1 }}</span><el-tooltip :content="getRouteTitle(index).replace(`段${index + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getRouteTitle(index), index + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="route.transportType" :disabled="isBasicRouteReadonly" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
</div>
<div class="route-flow-row">
<span class="route-flow-marker route-flow-marker--middle">经</span>
@@ -91,16 +94,16 @@
<div class="route-node-form">
<el-form-item label="途经地" required class="route-address-form-item"
><div class="route-address-inputs"
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
><el-input v-if="isNonRoadAddress(`route-${index}`)" v-model="route.departureName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths[`route-${index}`]" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange(`route-${index}`, value)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !route.transportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
></el-form-item>
<el-form-item label="联系人" class="route-contact-form-item"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item>
<el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" placeholder="请输入" /><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></div></el-form-item>
<el-form-item label="联系人" class="route-contact-form-item"><el-input v-model="route.departureContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
<el-form-item label="联系方式" class="route-phone-form-item"><div class="route-contact-actions"><el-input v-model="route.departurePhone" :disabled="isBasicRouteReadonly" placeholder="请输入" /><template v-if="!isBasicRouteReadonly"><el-tooltip v-if="index === form.routes.length - 1" content="新增途经点" placement="top"><el-button class="route-action-button" type="primary" link @click="addRoute"><el-icon><CirclePlus /></el-icon></el-button></el-tooltip><el-tooltip content="删除途经点" placement="top"><el-button class="route-action-button" type="danger" link @click="removeRoute(index)"><el-icon><Remove /></el-icon></el-button></el-tooltip></template></div></el-form-item>
</div>
</div>
</div>
</template>
<div class="route-flow-segment">
<div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(form.routes.length + 1) }} 段{{ form.routes.length + 1 }}</span><el-tooltip :content="getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getFinalRouteTitle(), form.routes.length + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="form.finalTransportType" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
<div class="route-step-title"><span class="route-segment-label">{{ circledSegmentNumber(form.routes.length + 1) }} 段{{ form.routes.length + 1 }}</span><el-tooltip :content="getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '')" placement="top"><span class="route-title-path">{{ formatRouteTitle(getFinalRouteTitle(), form.routes.length + 1) }}</span></el-tooltip><div class="route-transport-control"><span>运输方式<span class="required-asterisk">*</span></span><el-select v-model="form.finalTransportType" :disabled="isBasicRouteReadonly" placeholder="请选择"><el-option v-for="type in transports" :key="type" :label="type" :value="type" /></el-select></div></div>
</div>
<div class="route-flow-row">
<span class="route-flow-marker route-flow-marker--end">终</span>
@@ -108,18 +111,19 @@
<div class="route-node-form">
<el-form-item label="收货地" required class="route-address-form-item"
><div class="route-address-inputs"
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="!form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
><el-input v-if="isNonRoadAddress('arrival')" v-model="form.arrivalName" class="route-address-name" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" /><el-cascader v-else v-model="addressRegionPaths.arrival" class="route-address-name" :options="regionOptions" :props="regionCascaderProps" :loading="regionLoading" :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请选择省市区" clearable filterable @visible-change="visible => visible && loadRegionOptions()" @change="value => handleAddressRegionChange('arrival', value)" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="isBasicRouteReadonly || !form.finalTransportType" placeholder="请先选择运输方式" @click="handleAddressDetailClick('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="OfficeBuilding" :disabled="isBasicRouteReadonly || !form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
></el-form-item>
<el-form-item label="联系人" class="route-contact-form-item"
><el-input v-model="form.arrivalContact" placeholder="请输入" /></el-form-item>
><el-input v-model="form.arrivalContact" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
<el-form-item label="联系方式" class="route-phone-form-item"
><el-input v-model="form.arrivalPhone" placeholder="请输入" /></el-form-item>
><el-input v-model="form.arrivalPhone" :disabled="isBasicRouteReadonly" placeholder="请输入" /></el-form-item>
<el-form-item label="计划结束" class="route-date-form-item"
><el-date-picker
v-model="form.planEndTime"
type="date"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
:disabled="isBasicRouteReadonly"
placeholder="请选择"
/></el-form-item>
</div>
@@ -144,6 +148,7 @@
><el-autocomplete
v-model="row.cargoName"
clearable
:disabled="isGoodsCategoryLocked(row)"
placeholder="请输入货物名称"
:debounce="300"
:fetch-suggestions="
@@ -168,22 +173,29 @@
:placeholder="row.cargoType || '请选择'"
clearable
filterable
:disabled="isGoodsCategoryLocked(row)"
:loading="cargoTypeLoading"
@visible-change="visible => visible && loadCargoTypeOptions()"
@change="value => handleCargoTypeChange(row, value)"
/></template></el-table-column
><el-table-column label="数量" width="180" class-name="master-editor__quantity-cell"
><el-table-column label="数量" width="200" class-name="master-editor__quantity-cell"
><template #header><span>数量<span class="required-asterisk">*</span></span></template>
<template #default="{ row }"
><el-input
v-model="row.quantity"
inputmode="decimal"
placeholder="请输入"
@input="value => handleQuantityInput(row, value)" /></template></el-table-column
><div class="master-editor__quantity-field"
><el-input
v-model="row.quantity"
inputmode="decimal"
placeholder="请输入"
@input="value => handleQuantityInput(row, value)"
@blur="() => enforceMinDispatchedQuantity(row)"
/><small v-if="dispatchedQuantityOf(row) > 0" class="master-editor__quantity-hint"
>已调度 {{ formatDispatchedQuantity(dispatchedQuantityOf(row)) }},不可低于此值</small
></div
></template></el-table-column
><el-table-column label="数量单位" width="120"
><template #header><span>数量单位<span class="required-asterisk">*</span></span></template>
<template #default="{ row }"
><el-select v-model="row.quantityUnit"
><el-select v-model="row.quantityUnit" :disabled="isGoodsCategoryLocked(row)"
><el-option v-for="unit in quantityUnitOptions" :key="unit" :label="unit" :value="unit" /></el-select></template></el-table-column
><el-table-column label="包装" width="130"
><template #default="{ row }"
@@ -218,7 +230,7 @@
>新增</el-link
><el-link
type="danger"
:disabled="form.goods.length === 1"
:disabled="form.goods.length === 1 || isGoodsCategoryLocked(form.goods[$index])"
@click="removeGoods($index)"
>删除</el-link
></div
@@ -272,10 +284,9 @@
</el-table-column>
<el-table-column prop="uploadUserName" label="上传人" width="140" align="center" />
<el-table-column prop="uploadTime" label="上传时间" width="170" align="center" sortable />
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="{ row, $index }">
<el-table-column label="操作" width="80" align="center" fixed="right">
<template #default="{ $index }">
<el-link type="danger" @click="removeAttachment($index)">删除</el-link>
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
</template>
</el-table-column>
</el-table>
@@ -601,7 +612,7 @@ let amapLoader;
import { getList as getCommonRouteList } from '@/api/business/common-route';
import * as api from '@/api/business/master-order';
import { formatUpdateUserName } from '@/utils/audit';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { isMobile } from '@/utils/validate';
export default {
components: {
@@ -707,6 +718,31 @@ export default {
},
computed: {
...mapGetters(['userInfo']),
// 非草稿编辑:基本信息与收发货路线只读
isBasicRouteReadonly() {
return Boolean(this.id) && Boolean(this.form.businessStatus) && this.form.businessStatus !== 'draft';
},
// 品类 → 已调度量(各分段取最大值;优先用详情返回的 dispatchedGoods)
dispatchedGoodsMap() {
const map = {};
const source = this.form.dispatchedGoods;
if (source && typeof source === 'object' && !Array.isArray(source)) {
Object.entries(source).forEach(([key, qty]) => {
const number = Number(qty);
if (Number.isFinite(number)) map[key] = number;
});
return map;
}
(this.form.routeProgress || []).forEach(route => {
const goods = route?.dispatchedGoods || {};
Object.entries(goods).forEach(([key, qty]) => {
const number = Number(qty);
if (!Number.isFinite(number)) return;
map[key] = Math.max(map[key] || 0, number);
});
});
return map;
},
lastRouteName() {
return this.form.routes[this.form.routes.length - 1]?.departureName || '途经地';
},
@@ -1105,6 +1141,7 @@ export default {
return row.regionCode || row.districtCode || row.areaCode || row.adcode || '';
},
openCommonAddress(target) {
if (this.isBasicRouteReadonly) return;
this.addressTarget = target;
if (this.addressTransportMode(target) !== 'road') {
this.stationQuery = {};
@@ -1210,6 +1247,7 @@ export default {
this.loadRegionOptions().then(() => this.syncAddressRegionPath(this.addressTarget));
},
handleAddressDetailClick(target) {
if (this.isBasicRouteReadonly) return;
if (this.addressTransportMode(target) !== 'road') {
this.openCommonAddress(target);
return;
@@ -1217,6 +1255,7 @@ export default {
this.openAddressMap(target);
},
openAddressMap(target) {
if (this.isBasicRouteReadonly) return;
if (this.addressTransportMode(target) !== 'road') {
this.$message.info('非公路运输地址不能使用地图选择');
return;
@@ -1274,24 +1313,31 @@ export default {
.catch(() => this.$message.error('高德地图组件加载失败'));
},
searchAddressMap() {
if (!this.mapKeyword.trim()) return this.$message.warning('请输入地址关键词');
const keyword = String(this.mapKeyword || '').trim();
if (!keyword) return this.$message.warning('请输入地址关键词');
if (this.mapLoading) return;
this.mapSearchResults = [];
this.mapLoading = true;
this.loadAmap()
.then(() => this.ensureAddressGeocoder())
.then(
() =>
new Promise((resolve, reject) =>
this.addressMapGeocoder.getLocation(this.mapKeyword, (status, result) =>
status === 'complete' ? resolve(result) : reject(result)
)
)
)
.then(result => {
this.mapSearchResults = normalizeMapSearchResults(result, this.mapKeyword);
const point = this.mapSearchResults[0]?.location;
if (!point) return this.$message.warning('地图搜索无匹配地址');
this.pickAddressMap(point, this.mapKeyword);
.then(() => searchAmapPlaceText(keyword))
.then(pois => {
this.mapSearchResults = normalizeMapSearchResults(pois, keyword);
const first = this.mapSearchResults[0];
if (!first?.location) {
this.mapStatus = '未找到匹配地址';
return this.$message.warning('地图搜索无匹配地址');
}
return this.$nextTick().then(() => {
this.pickAddressMap(first.location, first.address || first.name || keyword);
});
})
.catch(() => this.$message.error('地图搜索失败'));
.catch(() => {
this.mapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
})
.finally(() => {
this.mapLoading = false;
});
},
selectAddressMapSearchResult(item) {
if (item && item.location) {
@@ -1657,6 +1703,7 @@ export default {
});
},
openRouteDialog() {
if (this.isBasicRouteReadonly) return;
this.routeQuery = {};
this.routePage.current = 1;
this.routeDialogVisible = true;
@@ -1714,12 +1761,28 @@ export default {
return { segmentNo: `段${index + 1}`, transportType: '' };
},
addRoute() {
if (this.isBasicRouteReadonly) return;
this.form.routes.push(this.createRoute());
},
removeRoute(index) {
if (this.isBasicRouteReadonly) return;
this.form.routes.splice(index, 1);
this.syncAddressRegionPaths();
},
goodsKey(row = {}) {
return `${row.cargoName || ''}\u0000${row.cargoType || ''}`;
},
dispatchedQuantityOf(row = {}) {
return Number(this.dispatchedGoodsMap[this.goodsKey(row)] || 0);
},
formatDispatchedQuantity(value) {
const number = Number(value);
if (!Number.isFinite(number)) return '0';
return String(number);
},
isGoodsCategoryLocked(row = {}) {
return this.dispatchedQuantityOf(row) > 0;
},
addGoods(index) {
this.form.goods.splice(index + 1, 0, {
quantityUnit: quantityUnitOptions[0],
@@ -1729,6 +1792,13 @@ export default {
});
},
removeGoods(index) {
const row = this.form.goods[index];
if (this.isGoodsCategoryLocked(row)) {
this.$message.warning(
`货物【${row.cargoName || ''}】已有调度量,不能删除`
);
return;
}
if (this.form.goods.length > 1) this.form.goods.splice(index, 1);
},
handleQuantityInput(row, value) {
@@ -1736,6 +1806,42 @@ export default {
const [integer = '', ...decimals] = text.split('.');
row.quantity = decimals.length ? `${integer}.${decimals.join('').slice(0, 3)}` : integer;
},
enforceMinDispatchedQuantity(row = {}) {
const min = this.dispatchedQuantityOf(row);
if (min <= 0) return true;
const current = Number(row.quantity);
if (!Number.isFinite(current) || current < min) {
row.quantity = String(min);
this.$message.warning(
`货物【${row.cargoName || ''}】数量不能低于已调度量(${this.formatDispatchedQuantity(min)})`
);
return false;
}
return true;
},
validateGoodsDispatchedQuantity() {
const totals = {};
(this.form.goods || []).forEach(row => {
const key = this.goodsKey(row);
totals[key] = (totals[key] || 0) + (Number(row.quantity) || 0);
});
for (const [key, qty] of Object.entries(this.dispatchedGoodsMap)) {
const dispatched = Number(qty) || 0;
if (dispatched <= 0) continue;
const cargoName = String(key).split('\u0000')[0] || '未知货物';
if (!(key in totals)) {
this.$message.warning(`货物【${cargoName}】已有调度量,不能删除或变更品类`);
return false;
}
if ((totals[key] || 0) < dispatched) {
this.$message.warning(
`货物【${cargoName}】数量不能低于已调度量(${this.formatDispatchedQuantity(dispatched)})`
);
return false;
}
}
return true;
},
syncAttachmentsJson() {
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
},
@@ -1858,6 +1964,7 @@ export default {
return this.$message.warning(`请完整填写第${invalidRouteIndex + 1}段途经地及运输方式`);
if (!this.form.finalTransportType) return this.$message.warning('请填写最后一段运输方式');
}
if (!this.validateGoodsDispatchedQuantity()) return false;
const payload = {
...this.form,
planStartTime: this.normalizeDateTimeValue(this.form.planStartTime),
@@ -1943,6 +2050,16 @@ export default {
margin-bottom: 14px;
}
}
.master-editor__quantity-field {
display: flex;
flex-direction: column;
gap: 4px;
}
.master-editor__quantity-hint {
color: #e6a23c;
font-size: 12px;
line-height: 1.2;
}
.goods-heading {
display: flex;
margin-bottom: 8px;
@@ -1098,7 +1098,7 @@ import {
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { getDictionary } from '@/api/system/dictbiz';
import { packageOptions } from '@/option/business/common';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
import { Location } from '@element-plus/icons-vue';
@@ -1115,6 +1115,7 @@ import '@open-file-viewer/core/style.css';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
import { h, resolveComponent } from 'vue';
import { mapGetters } from 'vuex';
import { isAdministrator } from '@/utils/auth';
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
@@ -1361,7 +1362,7 @@ export default {
},
isAdmin() {
return (
String(this.userInfo?.authority || '').includes('admin') ||
isAdministrator(this.userInfo) ||
this.userInfo?.authority?.includes?.('admin')
);
},
@@ -2611,21 +2612,22 @@ export default {
searchTransportMapKeyword() {
const keyword = String(this.transportMapKeyword || '').trim();
if (!keyword) return this.$message.warning('请输入地址关键词');
if (this.transportMapLoading) return;
this.transportMapSearchResults = [];
this.transportMapLoading = true;
this.loadAmap()
.then(() => this.ensureTransportMapGeocoder())
.then(() => this.runTransportMapGeocode('location', keyword))
.then(result => {
this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
const point = this.resolveTransportMapPoint(result);
if (!point) throw new Error('未找到匹配地址');
.then(() => searchAmapPlaceText(keyword))
.then(pois => {
this.transportMapSearchResults = normalizeMapSearchResults(pois, keyword);
const first = this.transportMapSearchResults[0];
if (!first?.location) throw new Error('未找到匹配地址');
return this.$nextTick().then(() => {
this.transportMapInstance?.resize();
return this.pickMapPoint(point, keyword);
return this.pickMapPoint(first.location, first.address || first.name || keyword);
});
})
.catch(() => {
this.transportMapStatus = '地图搜索失败';
this.transportMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
})
.finally(() => {
@@ -756,7 +756,7 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" width="200" />
<el-table-column prop="vehicleNo" label="车牌号/船班号/船次号/班次号" width="200" />
<el-table-column prop="driverName" label="司机" width="128" />
<el-table-column prop="carrierName" label="承运商" width="192" />
<el-table-column label="运输方式" width="192">
@@ -1401,7 +1401,7 @@
</el-table-column>
<el-table-column
prop="vehicleNo"
label="车牌号/航班号/船号/班列号"
label="车牌号/船班号/船次号/班次号"
min-width="200"
align="center"
show-overflow-tooltip
@@ -2097,7 +2097,7 @@
</el-form-item>
<el-form-item
v-if="dispatchItemForm.carrierType"
:label="dispatchIsNonRoadTransport ? '船/航/班列号' : '车牌号'"
:label="dispatchIsNonRoadTransport ? '船班号/船次号/班次号' : '车牌号'"
required
>
<el-input v-model="dispatchItemForm.vehicleNo" placeholder="请输入" />
@@ -2740,8 +2740,8 @@ import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit';
import { getToken } from '@/utils/auth';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { getToken, isAdministrator } from '@/utils/auth';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
@@ -3610,10 +3610,7 @@ export default {
return this.currencyRemark(value);
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
canCreate() {
return this.hasPermission(`${this.config.permission}_add`);
@@ -5771,31 +5768,32 @@ export default {
this.$message.warning('请输入地址关键词');
return;
}
if (this.transportMapLoading) return;
this.transportMapSearchResults = [];
this.transportMapLoading = true;
this.loadAmap()
.then(() => {
this.transportMapLoading = true;
this.ensureTransportAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
const point = this.resolveMapPoint(result);
if (!point) {
this.transportMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
return;
}
this.pickTransportMapPoint(point, keyword);
})
.catch(() => {
this.transportMapStatus = '地图搜索失败';
this.$message.error('地图搜索失败,请稍后重试');
})
.finally(() => {
this.transportMapLoading = false;
});
.then(() => searchAmapPlaceText(keyword))
.then(pois => {
this.transportMapSearchResults = normalizeMapSearchResults(pois, keyword);
const first = this.transportMapSearchResults[0];
if (!first?.location) {
this.transportMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
return;
}
return this.$nextTick().then(() => {
if (typeof this.transportAmap?.resize === 'function') {
this.transportAmap.resize();
}
this.pickTransportMapPoint(first.location, first.address || first.name || keyword);
});
})
.catch(() => {
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
this.transportMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
})
.finally(() => {
this.transportMapLoading = false;
});
},
selectTransportMapSearchResult(item) {
@@ -8123,7 +8121,7 @@ export default {
[
'vehicleNo',
['water', 'rail', 'air'].includes(this.resolveTransportMode(row.transportType))
? '船/航/班列号'
? '船班号/船次号/班次号'
: '车牌号',
],
];
@@ -8204,7 +8202,7 @@ export default {
['escortPhone', '押运人手机号', 20],
]
: [
['vehicleNo', '船/航/班列号', 30],
['vehicleNo', '船班号/船次号/班次号', 30],
['captainName', '船长', 20],
['cabinNo', '舱位', 30],
['containerNo', '箱号', 30],
@@ -0,0 +1,274 @@
<template>
<div class="voucher-folder-page voucher-folder-page--embedded">
<section class="voucher-folder-page__search">
<el-form :model="query" label-position="right" label-width="160px">
<div class="voucher-folder-page__search-grid">
<el-form-item label="车牌号">
<el-input v-model="query.plateNo" clearable placeholder="请输入" />
</el-form-item>
<el-form-item label="是否关联运单">
<el-select v-model="query.matched" clearable placeholder="全部">
<el-option label="是" :value="1" />
<el-option label="否" :value="0" />
</el-select>
</el-form-item>
<div class="voucher-folder-page__search-actions">
<el-button type="primary" @click="search">查询</el-button>
<el-button @click="reset">重置</el-button>
</div>
</div>
</el-form>
</section>
<section class="voucher-folder-page__table-wrap">
<div class="voucher-folder-page__title">{{ batchTitle }}</div>
<el-table v-loading="loading" :data="pageRows" border class="voucher-folder-page__table">
<el-table-column type="index" label="序号" width="70" :index="rowIndex" />
<el-table-column prop="voucherNo" label="执行凭证号" min-width="220">
<template #default="{ row }">
<el-link type="primary" @click="openViewer(row)">{{ row.voucherNo }}</el-link>
</template>
</el-table-column>
<el-table-column prop="plateNo" label="车牌号" min-width="150" />
<el-table-column prop="folderName" label="文件夹名称" min-width="160" />
<el-table-column prop="matched" label="是否关联运单" width="130">
<template #default="{ row }">{{ row.matched === 1 ? '是' : '否' }}</template>
</el-table-column>
<el-table-column prop="processStatus" label="处理状态" width="130" />
<el-table-column prop="createTime" label="创建时间" min-width="180" />
<el-table-column prop="updateTime" label="更新时间" min-width="180" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<div class="voucher-folder-page__actions">
<el-link type="primary" @click="openViewer(row)">查看</el-link>
</div>
</template>
</el-table-column>
</el-table>
<div class="voucher-folder-page__pagination">
<el-pagination
v-model:current-page="page.current"
v-model:page-size="page.size"
:total="filteredRows.length"
:page-sizes="[10, 20, 50]"
layout="total, prev, pager, next, sizes"
@size-change="page.current = 1"
/>
</div>
</section>
<el-dialog v-model="viewerVisible" title="执行凭证详情" width="960px" destroy-on-close>
<div v-loading="viewerLoading" class="voucher-folder-page__viewer">
<div class="voucher-folder-page__viewer-info">
<span>执行凭证号:{{ viewer.voucherNo || '-' }}</span>
<span>文件夹名称:{{ viewer.folderName || '-' }}</span>
<span>是否关联运单:{{ viewer.matched === 1 ? '是' : '否' }}</span>
<span>
配载单号:
<el-link v-if="viewer.loadingNo" type="primary" @click="goToLoading(viewer.loadingNo)">
{{ viewer.loadingNo }}
</el-link>
<template v-else>-</template>
</span>
</div>
<div v-if="viewerFiles.length" class="voucher-folder-page__images">
<el-image
v-for="(file, index) in viewerFiles"
:key="file.id || file.url || index"
:src="file.url"
:preview-src-list="viewerFiles.map(item => item.url).filter(Boolean)"
:initial-index="index"
fit="cover"
/>
</div>
<el-empty v-else description="暂无凭证文件" />
</div>
</el-dialog>
</div>
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import { getVoucherFolderDetail, getVoucherFolders } from '@/api/business/process-config';
const props = defineProps({
waybillId: {
type: [String, Number],
default: '',
},
});
const router = useRouter();
const loading = ref(false);
const rows = ref([]);
const page = reactive({ current: 1, size: 10 });
const query = reactive({ plateNo: '', matched: undefined });
const appliedQuery = reactive({ plateNo: '', matched: undefined });
const viewerVisible = ref(false);
const viewerLoading = ref(false);
const viewer = ref({});
const viewerFiles = ref([]);
const batchTitle = computed(() => {
const batchNos = [...new Set(rows.value.map(item => item.voucherBatchNo).filter(Boolean))];
return batchNos.length === 1 ? batchNos[0] : '执行凭证详情';
});
const filteredRows = computed(() => {
const plateNo = String(appliedQuery.plateNo || '').trim();
return rows.value.filter(item => {
const plateMatched = !plateNo || String(item.plateNo || '').includes(plateNo);
const relatedMatched =
appliedQuery.matched === undefined || appliedQuery.matched === null || appliedQuery.matched === ''
? true
: item.matched === appliedQuery.matched;
return plateMatched && relatedMatched;
});
});
const pageRows = computed(() => {
const start = (page.current - 1) * page.size;
return filteredRows.value.slice(start, start + page.size);
});
const rowIndex = index => (page.current - 1) * page.size + index + 1;
const load = async () => {
if (!props.waybillId) {
rows.value = [];
return;
}
loading.value = true;
try {
const res = await getVoucherFolders(props.waybillId);
rows.value = res.data?.data || [];
} catch (error) {
rows.value = [];
ElMessage.error(error.message || '获取执行凭证失败');
} finally {
loading.value = false;
}
};
const search = () => {
appliedQuery.plateNo = query.plateNo;
appliedQuery.matched = query.matched;
page.current = 1;
};
const reset = () => {
query.plateNo = '';
query.matched = undefined;
search();
};
const openViewer = async row => {
viewerVisible.value = true;
viewerLoading.value = true;
viewer.value = row;
viewerFiles.value = [];
try {
const res = await getVoucherFolderDetail(row.voucherId, row.plateNo);
viewer.value = res.data?.data || row;
viewerFiles.value = viewer.value.files || [];
} catch (error) {
ElMessage.error(error.message || '获取凭证详情失败');
} finally {
viewerLoading.value = false;
}
};
const goToLoading = loadingNo => {
router.push({ path: '/business/loading-manage', query: { loadingNo } });
};
watch(
() => props.waybillId,
() => {
page.current = 1;
load();
},
{ immediate: true }
);
</script>
<style scoped lang="scss">
.voucher-folder-page {
&__search {
padding: 12px 12px 4px;
margin-bottom: 8px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
&__search-grid {
display: grid;
grid-template-columns: 360px 360px 1fr;
gap: 8px 24px;
}
&__search-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
&__table-wrap {
padding: 12px;
background: #fff;
}
&__title {
padding-bottom: 12px;
color: #303133;
font-size: 16px;
font-weight: 600;
}
&__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
&__pagination {
display: flex;
justify-content: flex-end;
padding-top: 12px;
}
&__viewer {
min-height: 360px;
}
&__viewer-info {
display: flex;
flex-wrap: wrap;
gap: 28px;
padding-bottom: 20px;
color: #303133;
}
&__images {
display: grid;
grid-template-columns: repeat(5, 140px);
gap: 20px;
}
&__images :deep(.el-image) {
width: 140px;
height: 100px;
border: 6px solid #cfcfcf;
cursor: zoom-in;
}
:deep(.el-table) {
--el-table-border-color: #eff1f7;
}
:deep(.el-table__body tr:nth-child(even) > td) {
background: #fafafa;
}
:deep(.el-form-item) {
margin-bottom: 8px;
}
:deep(.el-form-item__label) {
white-space: nowrap;
}
:deep(.el-input),
:deep(.el-select) {
width: 100%;
}
}
@media (max-width: 900px) {
.voucher-folder-page__search-grid {
grid-template-columns: 1fr 1fr;
}
.voucher-folder-page__search-actions {
grid-column: 1 / -1;
}
}
</style>
@@ -202,7 +202,7 @@
<section-card class="waybill-search">
<el-form :inline="true" :model="detailQuery"
>
<el-form-item label="车牌号/船号"
<el-form-item label="车牌号/船班号/船次号/班次号"
><el-input v-model="detailQuery.vehicleNo" clearable /></el-form-item
><el-form-item label="司机/船长姓名"
><el-input v-model="detailQuery.driverName" clearable placeholder="请输入" /></el-form-item
@@ -219,12 +219,14 @@
prop="loadingIdentifier"
label="配载标识号"
min-width="130"
/><el-table-column
>
<template #default="{ row }">{{ row.loadingIdentifier || row.loadingNo || '' }}</template>
</el-table-column><el-table-column
prop="vehicleNo"
label="车牌号/航班号/船号/班列号"
label="车牌号/船班号/船次号/班次号"
min-width="190"
/><el-table-column prop="transportType" label="运输方式" width="110">
<template #default="{ row }">{{ transportTypeLabel(row.transportType) }}</template>
<template #default="{ row }">{{ transportTypeLabel(row) }}</template>
</el-table-column>
<el-table-column
prop="driverName"
@@ -334,6 +336,7 @@
label-width="auto"
class="archive-form"
>
<div class="dialog-section-title">项目信息</div>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="项目" prop="projectId">
@@ -444,16 +447,21 @@
</el-form-item>
</el-col>
</el-row>
<div class="dialog-section-title waybill-import-create__import-title">导入运单明细</div>
<p v-if="!projectInfoReady" class="waybill-import-create__import-hint">
请先选择项目、客户合同和承运信息,再导入运单明细
</p>
<el-form-item label="运单明细表" prop="file"
><el-upload
action="#"
accept=".xls,.xlsx"
:auto-upload="false"
:limit="1"
:disabled="!projectInfoReady"
:file-list="files"
:on-change="fileChange"
:on-remove="fileRemove"
><el-button type="primary">添加附件</el-button></el-upload
><el-button type="primary" :disabled="!projectInfoReady">导入</el-button></el-upload
><span class="waybill-import-create__file-tip">请上传运单明细表,仅支持 excel 格式;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法</span
><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item
>
@@ -479,6 +487,7 @@
:data="previewRows"
border
max-height="440"
:row-class-name="({ row }) => (row._duplicate ? 'is-duplicate-row' : '')"
@selection-change="rowSelection = $event"
><el-table-column type="selection" width="55" /><el-table-column
type="index"
@@ -564,14 +573,18 @@
/><span v-else>{{ formatCell(row, column) }}</span></template
><span v-else>{{ formatCell(row, column) }}</span></template
></el-table-column
><el-table-column label="操作" width="180" fixed="right"
><el-table-column label="操作" width="220" fixed="right"
><template #default="{ row }"
><template v-if="row._editing"
><el-link type="primary" @click="saveRow(row)">保存</el-link
><el-link type="primary" @click="cancelRow(row)">取消</el-link></template
><template v-else
><el-link type="primary" @click="editRow(row)">编辑</el-link
><el-link type="danger" @click="deleteRow(row)">删除</el-link></template
><div class="waybill-import-create__row-actions"
><template v-if="row._editing"
><el-link type="primary" @click="saveRow(row)">保存</el-link
><el-link type="primary" @click="cancelRow(row)">取消</el-link></template
><template v-else
><el-link v-if="row._duplicate" type="primary" @click="confirmDuplicateRow(row)"
>确认</el-link
><el-link type="primary" @click="editRow(row)">编辑</el-link
><el-link type="danger" @click="deleteRow(row)">删除</el-link></template
></div
></template
></el-table-column
></el-table
@@ -579,6 +592,7 @@
</section>
<div v-if="createPage" class="waybill-import-create__footer">
<el-button @click="closeCreate">取消</el-button>
<el-button @click="saveDraft">保存草稿</el-button>
<el-button type="primary" @click="confirmImport">确认导入</el-button>
</div>
</component>
@@ -586,7 +600,7 @@
<script setup>
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import dayjs from 'dayjs';
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
@@ -601,6 +615,7 @@ import SectionCard from '@/components/section-card/main.vue';
const props = defineProps({ modelValue: Boolean, standalone: Boolean, createPage: Boolean });
const emit = defineEmits(['update:modelValue', 'closed']);
const route = useRoute();
const router = useRouter();
const visible = computed({
get: () => props.modelValue,
@@ -656,10 +671,10 @@ const createDefaultForm = () => ({
file: null,
});
const form = reactive(createDefaultForm());
// 批量导入状态仅保留草稿与导入完成两种,与后端 importStatus 取值一致。
// 导入状态与运单业务状态一致:进行中、已完成。
const importStatusOptions = [
{ label: '进行中', value: 'draft' },
{ label: '导入完成', value: 'completed' },
{ label: '进行中', value: 'running' },
{ label: '已完成', value: 'completed' },
];
// 编辑草稿时明细已入库,此时不再强制重新上传附件。
const validateImportFile = (rule, value, callback) =>
@@ -705,6 +720,21 @@ const carrierContractsLoaded = ref(false),
hasCarrierContracts = ref(null);
const carrierTypes = ['承运商', '自运'];
const isSelfOperated = computed(() => form.carrierType === '自运');
const projectInfoReady = computed(() => {
if (
!form.projectId ||
!String(form.customerName || '').trim() ||
!form.contractId ||
!form.carrierType ||
!form.status ||
!form.importType
) {
return false;
}
return isSelfOperated.value
? Boolean(String(form.carrierName || '').trim())
: Boolean(form.carrierContractId);
});
// 项目下没有承运商合同时只允许自运,与运单管理的承运类型收窄规则一致。
const carrierTypeOptions = computed(() =>
carrierContractsLoaded.value && hasCarrierContracts.value === false ? ['自运'] : carrierTypes
@@ -733,7 +763,7 @@ const detailColumns = [
{ prop: 'batchNo', label: '运单批次号', minWidth: 150 },
{ prop: 'originalNo', label: '原始单号', editor: 'input', minWidth: 150 },
{ prop: 'loadingIdentifier', label: '配载标识号', editor: 'input', minWidth: 150 },
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号', editor: 'input', minWidth: 220 },
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号', editor: 'input', minWidth: 220 },
{ prop: 'transportType', label: '运输方式', editor: 'transportType', minWidth: 150 },
{ prop: 'driverName', label: '司机/船长姓名', editor: 'driver', minWidth: 170 },
{ prop: 'driverPhone', label: '司机/船长手机号', editor: 'input', minWidth: 150 },
@@ -763,7 +793,7 @@ const detailColumns = [
{ prop: 'waybillIdentifier', label: '同一运单标识号', editor: 'input', minWidth: 150 },
];
const requiredDetailFields = [
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
{ prop: 'vehicleNo', label: '车牌号/船班号/船次号/班次号' },
{ prop: 'transportType', label: '运输方式' },
{ prop: 'departureAddress', label: '发货地址' },
{ prop: 'arrivalAddress', label: '到货地址' },
@@ -976,33 +1006,78 @@ const loadCommonCargoOptions = async row => {
commonCargoLoadingMap.value = { ...commonCargoLoadingMap.value, [key]: false };
}
};
const TRANSPORT_TYPE_DEFAULTS = [
{ label: '公路运输', value: 'road', dictKey: 'road', dictValue: '公路运输' },
{ label: '铁路运输', value: 'railway', dictKey: 'railway', dictValue: '铁路运输' },
{ label: '水路运输', value: 'river', dictKey: 'river', dictValue: '水路运输' },
{ label: '航空运输', value: 'air', dictKey: 'air', dictValue: '航空运输' },
];
const readDictionaryList = res => {
const payload = res?.data?.data ?? res?.data ?? res;
if (Array.isArray(payload)) return payload;
if (Array.isArray(payload?.records)) return payload.records;
if (Array.isArray(payload?.data)) return payload.data;
return [];
};
const normalizeTransportTypeOptions = list => {
const options = [];
const append = item => {
if (!item || typeof item !== 'object') return;
const dictKey = String(item.dictKey ?? item.value ?? '').trim();
const dictValue = String(item.dictValue ?? item.label ?? '').trim();
if (dictKey === '-1') {
(item.children || []).forEach(append);
return;
}
if (dictKey || dictValue) {
options.push({
label: dictValue || dictKey,
value: dictKey || dictValue,
dictKey,
dictValue,
});
}
(item.children || []).forEach(append);
};
(list || []).forEach(append);
return options.length ? options : TRANSPORT_TYPE_DEFAULTS;
};
const applyTransportTypeOptions = res => {
transportTypeOptions.value = normalizeTransportTypeOptions(readDictionaryList(res));
};
const loadEditorOptions = async () => {
const [dictRes, cargoTypeRes, driverRes] = await Promise.all([
getDictionary({ code: 'transport_type' }),
getCargoTypeList(1, 9999),
getDriverList(1, 9999, {}),
]);
transportTypeOptions.value = extractRecords(dictRes).map(item => ({
label: item.dictValue || item.label,
value: item.dictKey || item.value,
}));
applyTransportTypeOptions(dictRes);
cargoTypeOptions.value = buildCargoTypeTree(extractRecords(cargoTypeRes));
cargoTypeFlatOptions.value = flattenCargoTypeOptions(cargoTypeOptions.value);
driverOptions.value = extractRecords(driverRes);
};
const ensureTransportTypeOptions = async () => {
if (transportTypeOptions.value.length) return;
const dictRes = await getDictionary({ code: 'transport_type' });
transportTypeOptions.value = extractRecords(dictRes).map(item => ({
label: item.dictValue || item.label,
value: item.dictKey || item.value,
}));
try {
applyTransportTypeOptions(await getDictionary({ code: 'transport_type' }));
} catch (error) {
transportTypeOptions.value = TRANSPORT_TYPE_DEFAULTS;
}
};
const transportTypeLabel = value => {
if (value === null || value === undefined || value === '') return '-';
return (
transportTypeOptions.value.find(item => String(item.value) === String(value))?.label || value
const source = value && typeof value === 'object' ? value : null;
const stored = source ? source.transportType : value;
const named = String(source?.transportTypeName || '').trim();
const text = String(stored ?? '').trim();
if (named && named !== text) return named;
if (!text && !named) return '-';
const target = (named || text).toLowerCase();
const matched = transportTypeOptions.value.find(item =>
[item.value, item.dictKey, item.label, item.dictValue].some(
candidate => String(candidate ?? '').trim().toLowerCase() === target
)
);
return matched?.dictValue || matched?.label || named || text || '-';
};
const loadBatches = async () => {
const res = await api.getImportBatches({ ...query, current: page.current, size: page.size });
@@ -1011,7 +1086,7 @@ const loadBatches = async () => {
};
onMounted(() => {
if (props.standalone) loadBatches();
if (props.createPage) openCreate();
if (props.createPage) openCreatePage();
});
watch(
() => props.modelValue,
@@ -1034,16 +1109,18 @@ const loadDetails = async () => {
details.value = res.data?.data?.records || [];
detailPage.total = res.data?.data?.total || 0;
};
const openCreate = async () => {
const openCreate = () => prepareCreateForm(true);
const prepareCreateForm = async generateCode => {
resetCreateForm();
createVisible.value = true;
const [projectRes, , optionRes, batchNo] = await Promise.all([
const requests = [
getProjectList(1, 9999, projectQueryParams),
loadEditorOptions(),
api.getImportOptions?.(),
generateBatchNo(),
]);
form.batchNo = batchNo;
];
if (generateCode) requests.push(generateBatchNo());
const [projectRes, , optionRes, batchNo] = await Promise.all(requests);
if (generateCode) form.batchNo = batchNo || '';
projects.value = projectRes.data?.data?.records || [];
const data = optionRes?.data?.data || {};
queryCarriers.value = data.carriers || [];
@@ -1082,27 +1159,78 @@ const loadBatchRows = async batchId => {
_key: `${id || 'row'}-${index}`,
batchNo: item.batchNo || form.batchNo,
}));
refreshDuplicateFlags();
rowSelection.value = [];
previewTab.value = 'all';
};
const editBatch = async row => {
await openCreate();
const editBatch = row => {
router.push({
path: '/business/waybill-import/form',
query: { id: row.id, batchNo: row.batchNo || '' },
});
};
const openCreatePage = async () => {
const batchId = route.query.id;
if (!batchId) {
await openCreate();
return;
}
const res = await api.getImportBatches({
id: batchId,
batchNo: route.query.batchNo || '',
current: 1,
size: 20,
});
const batch = (res.data?.data?.records || []).find(item => String(item.id) === String(batchId));
if (!batch) {
ElMessage.warning('未找到导入批次');
router.push({ path: '/business/waybill-import' });
return;
}
await fillEditForm(batch);
};
const fillEditForm = async row => {
await prepareCreateForm(false);
const snapshot = { ...row };
const savedCarrierId = String(snapshot.carrierIds || snapshot.carrierId || '')
.split(',')
.map(item => item.trim())
.find(Boolean);
if (snapshot.projectId) await projectChange(snapshot.projectId);
const project = projects.value.find(item => String(item.id) === String(snapshot.projectId));
Object.assign(form, snapshot, {
projectId: snapshot.projectId || '',
id: snapshot.id || '',
batchNo: snapshot.batchNo || '',
projectId: project?.id || snapshot.projectId || '',
projectName: snapshot.projectName || form.projectName,
// 列表返回的是 importStatus,表单沿用 status 字段提交给后端。
status: snapshot.importStatus || 'completed',
status:
snapshot.importStatus === 'running' || snapshot.importStatus === 'completed'
? snapshot.importStatus
: 'completed',
file: null,
carrierIds: [],
});
const plan = plans.value.find(item => String(item.id) === String(snapshot.planId));
// 接口数字 null 会序列化为 -1,未选计划时回填为空
form.planId =
plan && Number(plan.id) !== -1 && Number(plan.id) > 0
? plan.id
: normalizeOptionalPlanId(snapshot.planId) || '';
const contract = contracts.value.find(item => String(item.id) === String(snapshot.contractId));
form.contractId = contract?.id || '';
form.carrierType = snapshot.carrierType || '承运商';
contractChange(form.contractId);
syncCarrierOptions();
const carrierOption =
form.carrierType === '自运'
? null
: carrierOptions.value.find(item => String(item.id) === String(savedCarrierId || '')) ||
carrierOptions.value.find(item => item.carrierName === snapshot.carrierName);
const carrierValue =
form.carrierType === '自运' ? snapshot.carrierName : snapshot.carrierContractId;
form.carrierType === '自运'
? snapshot.carrierName
: snapshot.carrierContractId || carrierOption?.value;
if (carrierValue) carrierChange(carrierValue);
await loadBatchRows(snapshot.id);
};
@@ -1170,6 +1298,12 @@ const carrierChange = value => {
if (!isSelfOperated.value) form.carrierContractId = item.carrierContractId || '';
};
const fileChange = async (file, list) => {
if (!projectInfoReady.value) {
ElMessage.warning('请先选择项目、客户合同和承运信息,再导入运单明细');
files.value = [];
form.file = null;
return;
}
if (file.raw?.size > 50 * 1024 * 1024) {
ElMessage.warning('单个文件大小不能超过50M');
files.value = [];
@@ -1190,7 +1324,7 @@ const fileChange = async (file, list) => {
const keyMap = {
originalNo: ['原始单号'],
loadingIdentifier: ['配载标识号'],
vehicleNo: ['车牌号/航班号/船号/班列号'],
vehicleNo: ['车牌号/船班号/船次号/班次号', '车牌号/航班号/船号/班列号'],
transportType: ['运输方式', '运输类型'],
driverName: ['司机/船长姓名', '司机/船长'],
driverPhone: ['司机/船长手机号'],
@@ -1227,7 +1361,6 @@ const fileChange = async (file, list) => {
const text = String(value ?? '').trim();
return text.slice(0, 10);
};
const count = new Map();
// 先解析数据,但不填充到 rows.value
const parsedRows = source.map((item, index) => {
const row = { _key: `${Date.now()}-${index}`, ...item, batchNo: form.batchNo };
@@ -1241,14 +1374,9 @@ const fileChange = async (file, list) => {
row.actualEndDate = normalizeImportDate(row.actualEndDate);
row.planStartDate = normalizeImportDate(row.planStartDate);
row.planEndDate = normalizeImportDate(row.planEndDate);
const duplicateKey = JSON.stringify(Object.keys(keyMap).map(key => row[key]));
count.set(duplicateKey, (count.get(duplicateKey) || 0) + 1);
row._duplicateKey = duplicateKey;
return row;
});
parsedRows.forEach(row => {
row._duplicate = count.get(row._duplicateKey) > 1;
});
refreshDuplicateFlags(parsedRows);
// 调用后端校验接口,校验通过才填充表格
const isValid = await performValidation(parsedRows);
@@ -1268,67 +1396,175 @@ const fileRemove = () => {
form.file = null;
};
const performValidation = async (parsedRows) => {
/** 运单批量导入每次请求最多提交行数,避免网关 body 超限。 */
const IMPORT_BATCH_SIZE = 100;
const importFieldText = value => String(value ?? '').trim();
/** 同一运单批次号下,全部导入字段一致时标记为疑似重复。已在线确认的行不再标为重复。 */
const refreshDuplicateFlags = (list = rows.value) => {
const groups = new Map();
list.forEach(row => {
const signature = detailColumns.map(column => importFieldText(row[column.prop])).join('\u0001');
const batchNo = importFieldText(row.batchNo || form.batchNo);
if (row._duplicateConfirmed && row._duplicateSignature !== signature) {
row._duplicateConfirmed = false;
}
row._duplicateSignature = signature;
const groupKey = `${batchNo}\u0002${signature}`;
const group = groups.get(groupKey) || [];
group.push(row);
groups.set(groupKey, group);
});
groups.forEach(group => {
const duplicated = group.length > 1;
group.forEach(row => {
row._duplicate = duplicated && !row._duplicateConfirmed;
});
});
};
const confirmDuplicateRow = row => {
row._duplicateConfirmed = true;
refreshDuplicateFlags();
ElMessage.success('已确认,该行将按正常数据导入');
};
const stripInternalRowFields = row => {
const { _key, _duplicate, _duplicateKey, _duplicateConfirmed, _duplicateSignature, _editing, _editSnapshot, ...data } = row;
return data;
};
/**
* 按配载标识号 / 同一运单标识号尽量同组,再按 IMPORT_BATCH_SIZE 切分。
* 返回 [{ rows, globalIndexes }],globalIndexes 与 rows 一一对应。
*/
const chunkImportRows = sourceRows => {
if (!sourceRows?.length) return [];
const groups = [];
const groupIndexByKey = new Map();
sourceRows.forEach((row, globalIndex) => {
const loadingIdentifier = String(row.loadingIdentifier || '').trim();
const waybillIdentifier = String(row.waybillIdentifier || '').trim();
let groupKey = '';
if (loadingIdentifier) groupKey = `L:${loadingIdentifier}`;
else if (waybillIdentifier) groupKey = `W:${waybillIdentifier}`;
else groupKey = `R:${globalIndex}`;
let groupPos = groupIndexByKey.get(groupKey);
if (groupPos == null) {
groupPos = groups.length;
groupIndexByKey.set(groupKey, groupPos);
groups.push({ rows: [], globalIndexes: [] });
}
groups[groupPos].rows.push(row);
groups[groupPos].globalIndexes.push(globalIndex);
});
const chunks = [];
let current = { rows: [], globalIndexes: [] };
const flush = () => {
if (!current.rows.length) return;
chunks.push(current);
current = { rows: [], globalIndexes: [] };
};
groups.forEach(group => {
// 单组超过上限时整组单独成批,保证跨行一致性校验与多货物合并完整。
if (group.rows.length > IMPORT_BATCH_SIZE) {
flush();
chunks.push(group);
return;
}
if (current.rows.length + group.rows.length > IMPORT_BATCH_SIZE) {
flush();
}
current.rows.push(...group.rows);
current.globalIndexes.push(...group.globalIndexes);
});
flush();
return chunks;
};
const downloadImportFailures = async failureRows => {
if (!failureRows.length) return;
const response = await api.exportImportFailures({
rows: failureRows.map(row => stripInternalRowFields(row)),
});
const blob = response.data || response;
downloadXls(blob, `运单导入失败明细_${dayjs().format('YYYYMMDDHHmmss')}.xlsx`);
};
/** 按每批 100 条请求校验,全部批次结束后再汇总失败行。 */
const collectImportValidationErrors = async rowList => {
const errorMap = new Map();
for (const chunk of chunkImportRows(rowList)) {
const response = await api.validateImport(buildImportPayload(form.status, chunk.rows));
const result = response?.data?.data ?? response?.data ?? response;
if (result?.success === false || (Array.isArray(result?.errors) && result.errors.length)) {
(result.errors || []).forEach(error => {
const localIndex = Number(error.rowIndex);
if (Number.isNaN(localIndex) || localIndex < 0 || localIndex >= chunk.globalIndexes.length) {
return;
}
errorMap.set(chunk.globalIndexes[localIndex], error.errorMessage || '校验失败');
});
}
}
return errorMap;
};
const exportCollectedImportFailures = async (rowList, errorMap, exportAll = false) => {
const indexedRows = exportAll
? rowList.map((row, globalIndex) => [globalIndex, errorMap.get(globalIndex) || ''])
: [...errorMap.entries()].sort((left, right) => left[0] - right[0]);
const failureRows = indexedRows.map(([globalIndex, errorMessage]) => ({
...stripInternalRowFields(rowList[globalIndex]),
errorMessage,
}));
await downloadImportFailures(failureRows);
ElMessage.warning('数据校验失败,已自动下载错误明细表,请修正后重新上传');
};
const performValidation = async parsedRows => {
if (!parsedRows || !parsedRows.length) return false;
try {
// 构建校验请求参数
const payload = {
...form,
rows: parsedRows.map(row => {
const { _key, _duplicate, _duplicateKey, _editing, _editSnapshot, ...data } = row;
return data;
})
};
const chunks = chunkImportRows(parsedRows);
const errorMap = new Map();
// 调用后端校验接口
const response = await api.validateImport(payload);
// 检查响应类型
const blob = response.data || response;
// 如果是 Blob,需要检查其 MIME 类型
if (blob instanceof Blob) {
// Excel 文件的 MIME 类型
if (blob.type.includes('application/vnd.ms-excel') ||
blob.type.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')) {
// 校验失败,下载错误明细
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `运单导入失败明细_${dayjs().format('YYYYMMDDHHmmss')}.xlsx`;
link.click();
window.URL.revokeObjectURL(url);
ElMessage.warning('数据校验失败,已自动下载错误明细表,请修正后重新上传');
return false;
} else if (blob.type.includes('application/json')) {
// 可能是 JSON 响应被当作 Blob,需要解析
const text = await blob.text();
const json = JSON.parse(text);
if (json.success) {
ElMessage.success('数据校验通过');
return true;
} else {
ElMessage.error(json.msg || '校验失败');
return false;
}
} else {
// 其他类型,默认认为是校验通过
ElMessage.success('数据校验通过');
return true;
for (const chunk of chunks) {
const payload = {
...form,
rows: chunk.rows.map(stripInternalRowFields),
};
const response = await api.validateImport(payload);
const result = response?.data?.data ?? response?.data ?? response;
if (result?.success === false || (Array.isArray(result?.errors) && result.errors.length)) {
(result.errors || []).forEach(error => {
const localIndex = Number(error.rowIndex);
if (Number.isNaN(localIndex) || localIndex < 0 || localIndex >= chunk.globalIndexes.length) {
return;
}
const globalIndex = chunk.globalIndexes[localIndex];
errorMap.set(globalIndex, error.errorMessage || '校验失败');
});
}
} else {
// 不是 Blob,直接判断
ElMessage.success('数据校验通过');
return true;
}
if (errorMap.size) {
await exportCollectedImportFailures(parsedRows, errorMap);
return false;
}
ElMessage.success('数据校验通过');
return true;
} catch (error) {
console.error('校验失败:', error);
ElMessage.error('校验接口调用失败');
ElMessage.error(error?.message || '校验接口调用失败');
return false;
}
};
const formatCell = (row, column) => row[column.prop] || '';
const formatCell = (row, column) =>
column.prop === 'transportType' ? transportTypeLabel(row) : row[column.prop] || '';
const updateEditorValue = (column, row, value) => {
if (column.editor === 'number') {
const [integer = '', decimal = ''] = String(value || '')
@@ -1403,6 +1639,7 @@ const editRow = row => {
const saveRow = row => {
row._editing = false;
delete row._editSnapshot;
refreshDuplicateFlags();
};
const cancelRow = row => {
if (row._editSnapshot) Object.assign(row, row._editSnapshot);
@@ -1411,11 +1648,13 @@ const cancelRow = row => {
};
const deleteRow = row => {
rows.value = rows.value.filter(item => item !== row);
refreshDuplicateFlags();
};
const removeSelectedRows = () => {
const keys = new Set(rowSelection.value.map(row => row._key));
rows.value = rows.value.filter(row => !keys.has(row._key));
rowSelection.value = [];
refreshDuplicateFlags();
};
const firstNotEmpty = (...values) => values.find(value => String(value ?? '').trim()) ?? '';
const normalizeOptionalImportNumber = (value, emptySentinels = []) => {
@@ -1442,9 +1681,22 @@ const normalizeImportRow = row => ({
row['*运输方式']
),
});
const buildImportPayload = (status = form.status) => {
const normalizeOptionalPlanId = value => {
if (value === undefined || value === null || value === '') return null;
return Number(value) === -1 || Number(value) <= 0 ? null : value;
};
const buildImportPayload = (status = form.status, rowList = rows.value, extra = {}) => {
const { file, ...batch } = form;
return { ...batch, status, rows: rows.value.map(normalizeImportRow) };
const planId = normalizeOptionalPlanId(batch.planId);
return {
...batch,
planId,
// 未选计划时不带占位名称,避免落库/回显 -1
planName: planId ? batch.planName || undefined : null,
status,
rows: rowList.map(normalizeImportRow).map(stripInternalRowFields),
...extra,
};
};
// 独立页面下返回批次列表路由,弹窗形态下关闭新建面板并刷新列表。
const backToList = () => {
@@ -1454,22 +1706,72 @@ const backToList = () => {
// 保存草稿:批次与已解析明细一并入库,成功后返回批次列表。
const saveDraft = async () => {
await formRef.value?.validateField(draftRequiredFields);
await api.saveImportDraft(buildImportPayload('draft'));
const chunks = chunkImportRows(rows.value);
if (!chunks.length) {
ElMessage.warning('请上传至少一条运单明细');
return;
}
// 草稿编辑时首批带原 id 覆盖;后续批 append,避免整包超限。
let batchId = form.id || '';
for (let i = 0; i < chunks.length; i++) {
const append = i > 0;
const payload = buildImportPayload('draft', chunks[i].rows, {
id: append ? batchId : batchId || undefined,
append,
});
const response = await api.saveImportDraft(payload);
const batch = response?.data?.data ?? response?.data ?? response;
batchId = batch?.id || batchId;
}
if (batchId) form.id = batchId;
ElMessage.success('保存草稿成功');
backToList();
};
const confirmImport = async () => {
// 导入状态选择草稿时不生成正式运单,等同于保存草稿。
if (form.status === 'draft') return saveDraft();
await formRef.value?.validate();
if (!validateImportRows()) return;
const count = rows.value.filter(row => row._duplicate).length;
if (count) await ElMessageBox.confirm(`当前导入有${count}条重复数据,是否确认导入?`, '提示');
if (form.importType === 'settlement')
if (count) await ElMessageBox.confirm(`当前导入有${count}条疑似重复数据,是否确认导入?`, '提示');
if (form.importType === 'settlement' && form.status === 'completed')
await ElMessageBox.confirm('确认后将同时生成运单、应收应付明细、计结算单,是否继续?', '提示');
await api.confirmImport(buildImportPayload());
ElMessage.success('导入成功');
backToList();
const chunks = chunkImportRows(rows.value);
let errorMap;
try {
errorMap = await collectImportValidationErrors(rows.value);
} catch (error) {
console.error('校验失败:', error);
ElMessage.error(error?.message || '校验接口调用失败');
return;
}
if (errorMap.size) {
await exportCollectedImportFailures(rows.value, errorMap, true);
return;
}
// 校验全部通过后才分批落库,避免前面批次已入库、后面批次校验失败。
const draftBatchId = form.id || '';
let batchId = '';
try {
for (let i = 0; i < chunks.length; i++) {
const append = i > 0;
const payload = buildImportPayload(form.status, chunks[i].rows, {
id: append ? batchId : draftBatchId || undefined,
append,
});
const response = await api.confirmImport(payload);
const batch = response?.data?.data ?? response?.data ?? response;
batchId = batch?.id || batchId;
}
ElMessage.success('导入成功');
backToList();
} catch (error) {
console.error('确认导入失败:', error);
ElMessage.error(
batchId
? `部分数据可能已导入(批次已创建),请到批次详情核对后重试:${error?.message || '导入失败'}`
: error?.message || '导入失败'
);
}
};
</script>
@@ -1562,6 +1864,15 @@ const confirmImport = async () => {
&__detail-panel {
margin-top: 12px;
}
&__import-title {
margin-top: 8px;
}
&__import-hint {
margin: 0 0 12px;
color: #909399;
font-size: 13px;
line-height: 20px;
}
&__form-actions {
display: flex;
justify-content: flex-end;
@@ -1574,6 +1885,14 @@ const confirmImport = async () => {
gap: 24px;
margin-bottom: 8px;
}
&__row-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
:deep(.is-duplicate-row > td.el-table__cell) {
background: #fff7e8;
}
&__file-tip {
margin: 0 24px;
color: #606266;
@@ -492,6 +492,7 @@
v-model="row.quantityUnit"
clearable
:disabled="dialogReadonly"
@change="handleTransportCargoQuantityUnitChange"
@visible-change="visible => visible && loadTaskQuantityUnitOptions()"
>
<el-option
@@ -1091,20 +1092,6 @@
</template>
</el-input>
</el-form-item>
<el-form-item label="数量合计">
<el-input :model-value="taskFullFreightGroupQuantity(group)" disabled>
<template #append>
<el-select
:model-value="group.quantityUnit"
class="waybill-manage-page__append-select waybill-manage-page__append-select--wide"
placeholder="单位"
disabled
>
<el-option :label="fixedQuantityUnit" :value="fixedQuantityUnit" />
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item label="运费">
<el-input
:model-value="taskFullFreightGroupAmount(group)"
@@ -1117,11 +1104,6 @@
</el-input>
</el-form-item>
</template>
<el-form-item label="运费合计">
<el-input :model-value="taskFullFreightTotal" disabled>
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
</el-input>
</el-form-item>
<el-form-item label="其他运费合计">
<el-input
v-model="form.otherFeeTotal"
@@ -1133,6 +1115,11 @@
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
</el-input>
</el-form-item>
<el-form-item label="运费合计">
<el-input :model-value="taskFullFreightTotal" disabled>
<template #suffix>{{ taskFreightCurrencyLabel }}</template>
</el-input>
</el-form-item>
</el-form>
</div>
</div>
@@ -1317,7 +1304,7 @@
@change="handleTaskCargoTypeChange"
/>
</el-form-item>
<el-form-item label="货物名称">
<el-form-item label="货物名称" required>
<el-autocomplete
v-model="form.cargoName"
placeholder="请输入货物名称"
@@ -1347,7 +1334,7 @@
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="型号">
<el-form-item label="型号" required>
<el-input
v-model="form.model"
placeholder="选择货物自动回填"
@@ -1355,7 +1342,7 @@
:disabled="dialogReadonly"
/>
</el-form-item>
<el-form-item label="数量">
<el-form-item label="数量" required>
<el-input
v-model="form.quantity"
placeholder="请输入数量"
@@ -1802,14 +1789,14 @@
:class="{ 'is-plan': item[0] === 'planName' }"
>
<span class="waybill-manage-page__waybill-label">{{ item[1] }}</span>
<el-link
<span
v-if="canOpenWaybillDetailSummaryLink(item[0])"
type="primary"
class="waybill-manage-page__waybill-value is-link"
role="link"
@click="handleWaybillDetailSummaryClick(item[0])"
>
{{ formatDetailValue(detailRow, item[0]) }}
</el-link>
</span>
<span
v-else
:class="['waybill-manage-page__waybill-value', { 'is-link': item[2] }]"
@@ -1962,7 +1949,7 @@
>
</div>
<div class="waybill-manage-page__waybill-detail-field">
<span>航班号/船次号/班列号</span>
<span>船班号/船次号/班次号</span>
<strong>{{ detailRow.vehicleNo || '-' }}</strong>
</div>
<div class="waybill-manage-page__waybill-detail-field">
@@ -2162,8 +2149,12 @@
label="批量补录"
name="batchSupplement"
>
<voucher-execution-detail
v-if="waybillIsCarrier(detailRow)"
:waybill-id="detailRow.id"
/>
<div
v-if="waybillVoucherFolderView"
v-else-if="waybillVoucherFolderView"
class="waybill-manage-page__voucher-folder-toolbar"
>
<el-button text type="primary" @click="backToWaybillVoucherFolders">
@@ -2172,6 +2163,7 @@
<span>{{ waybillVoucherFolderName(waybillVoucherFolder) }}</span>
</div>
<div
v-if="!waybillIsCarrier(detailRow)"
v-loading="waybillVoucherImagesLoading"
class="waybill-manage-page__voucher-image-grid"
>
@@ -2356,7 +2348,7 @@
</div>
</template>
</div>
<div v-if="detailPageLocked && !isPublicWaybillView" class="waybill-manage-page__detail-footer">
<div v-if="detailPageLocked" class="waybill-manage-page__detail-footer">
<el-button type="primary" @click="closeDetail">关闭</el-button>
</div>
<template v-if="!detailPageLocked" #footer>
@@ -2421,14 +2413,15 @@
原:{{ row.originalDepartureAddress || '-' }}
</div>
<el-input
v-model="row.departureAddress"
placeholder="请选择或输入地址"
@input="updateWaybillRouteChangeNodes"
:model-value="row.departureAddress"
readonly
placeholder="请点击地图选点"
@click="openWaybillRouteChangeMapDialog(row, 'departure')"
>
<template #suffix>
<el-icon
class="waybill-manage-page__route-change-location"
@click="openWaybillCommonAddressDialog(row, 'departure')"
@click.stop="openWaybillRouteChangeMapDialog(row, 'departure')"
>
<Location />
</el-icon>
@@ -2442,14 +2435,15 @@
原:{{ row.originalArrivalAddress || '-' }}
</div>
<el-input
v-model="row.arrivalAddress"
placeholder="请选择或输入地址"
@input="updateWaybillRouteChangeNodes"
:model-value="row.arrivalAddress"
readonly
placeholder="请点击地图选点"
@click="openWaybillRouteChangeMapDialog(row, 'arrival')"
>
<template #suffix>
<el-icon
class="waybill-manage-page__route-change-location"
@click="openWaybillCommonAddressDialog(row, 'arrival')"
@click.stop="openWaybillRouteChangeMapDialog(row, 'arrival')"
>
<Location />
</el-icon>
@@ -2505,49 +2499,6 @@
</template>
</el-dialog>
<el-dialog
v-model="waybillCommonAddressBox"
title="选择常用地址"
append-to-body
width="900px"
class="waybill-manage-page__common-address-dialog"
>
<div class="waybill-manage-page__common-address-toolbar">
<el-input
v-model="waybillCommonAddressQuery.addressName"
clearable
placeholder="请输入地址名称"
@keyup.enter="loadWaybillCommonAddresses"
/>
<el-button type="primary" @click="loadWaybillCommonAddresses">查询</el-button>
</div>
<el-table
v-loading="waybillCommonAddressLoading"
:data="waybillCommonAddressRows"
border
@row-dblclick="selectWaybillCommonAddress"
>
<el-table-column prop="addressName" label="地址名称" min-width="180" />
<el-table-column prop="regionName" label="区域" min-width="180" />
<el-table-column prop="detailAddress" label="详细地址" min-width="260" />
<el-table-column label="操作" width="90">
<template #default="{ row }">
<el-link type="primary" @click="selectWaybillCommonAddress(row)">选择</el-link>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-pagination
v-model:current-page="waybillCommonAddressPage.currentPage"
v-model:page-size="waybillCommonAddressPage.pageSize"
layout="total, prev, pager, next"
:total="waybillCommonAddressPage.total"
@current-change="loadWaybillCommonAddresses"
/>
<el-button @click="waybillCommonAddressBox = false">关闭</el-button>
</template>
</el-dialog>
<flow-design
v-if="website.design.designMode"
is-dialog
@@ -2858,6 +2809,7 @@
v-model="transportMapBox"
width="920px"
@opened="initTransportAmap"
@closed="waybillRouteChangeMapTarget = null"
>
<div class="waybill-manage-page__map-toolbar">
<el-input
@@ -2886,7 +2838,7 @@
>
</div>
<template #footer>
<el-button @click="transportMapBox = false">取消</el-button>
<el-button @click="closeTransportMapDialog">取消</el-button>
<el-button
type="primary"
:disabled="!transportMapSelected.longitude"
@@ -3135,7 +3087,7 @@ import {
getVoucherImages as getProcessConfigVoucherImages,
} from '@/api/business/process-config';
import { getPunchRecords as getWaybillPunchRecords, locateVehicle, trackVehicle } from '@/api/business/waybill-manage';
import { getMkPublicDetail } from '@/api/mk-process';
import VoucherExecutionDetail from './voucher-execution-detail.vue';
import { getList as getDriverList } from '@/api/transportCapacity/driver';
import {
fetchDriversByCarrierOrganizations,
@@ -3145,10 +3097,9 @@ import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { addressTypeOptions } from '@/option/base/common-address';
import { packageOptions } from '@/option/business/common';
import { formatUpdateUserName } from '@/utils/audit';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { getToken } from '@/utils/auth';
import { getToken, isAdministrator } from '@/utils/auth';
import { openImportDialog } from '@/utils/import-excel';
import { normalizeMapSearchResults } from '@/utils/map-search';
import { normalizeMapSearchResults, searchAmapPlaceText } from '@/utils/map-search';
import { applyTableMenuWidth } from '@/utils/table-menu';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { isMobile } from '@/utils/validate';
@@ -3173,9 +3124,11 @@ const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let amapLoader;
const standaloneWaybillFormRoute = '/business/waybill-manage/form';
const standaloneWaybillFormPaths = ['/business/waybill-manage', standaloneWaybillFormRoute];
const standaloneWaybillListRoute = '/business/waybill-manage';
const standaloneWaybillFormPaths = [standaloneWaybillListRoute, standaloneWaybillFormRoute];
const standaloneWaybillDetailRoute = '/business/waybill-manage/detail';
const loadingCreateWaybillsStorageKey = 'loading-manage-create-waybills';
const waybillManageListRefreshKey = 'waybill-manage-list-refresh';
const attachmentViewerPlugins = [
imagePlugin(),
@@ -3330,6 +3283,7 @@ export default {
WarnTriangleFilled,
ElImageViewer,
OpenFileViewer,
VoucherExecutionDetail,
},
name: 'WaybillManagePage',
props: {
@@ -3471,18 +3425,7 @@ export default {
},
],
},
waybillCommonAddressBox: false,
waybillCommonAddressLoading: false,
waybillCommonAddressQuery: {
addressName: '',
},
waybillCommonAddressRows: [],
waybillCommonAddressTarget: null,
waybillCommonAddressPage: {
pageSize: 10,
currentPage: 1,
total: 0,
},
waybillRouteChangeMapTarget: null,
draftSaveLoading: false,
crudDialogType: '',
defaultDialogButtons: null,
@@ -3662,7 +3605,6 @@ export default {
this.formPageLocked = this.isStandaloneWaybillFormPage;
this.detailPageLocked = this.isStandaloneWaybillDetailPage;
this.formModeLocked = this.$route.query.mode || '';
if (this.isPublicWaybillView) return;
this.setupSearchAutocompletes();
if (this.config.enableAllDept && this.isAdmin) {
this.allDept = 1;
@@ -3692,6 +3634,14 @@ export default {
deactivated() {
this.closeInnerDialogs();
},
activated() {
// 列表实例被 keep-alive 重新激活时,按标记拉取最新数据
if (this.formPageLocked || this.detailPageLocked) return;
if (sessionStorage.getItem(waybillManageListRefreshKey)) {
sessionStorage.removeItem(waybillManageListRefreshKey);
this.onLoad(this.page, this.query);
}
},
beforeUnmount() {
this.closeInnerDialogs();
},
@@ -3727,14 +3677,8 @@ export default {
isStandaloneWaybillFormPage() {
return this.isStandaloneWaybillPage && ['add', 'edit'].includes(this.$route.query.mode);
},
isPublicWaybillView() {
return this.$route.path === '/business/waybill-manage/public-view';
},
isStandaloneWaybillDetailPage() {
return (
(this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute) ||
this.isPublicWaybillView
);
return this.standaloneDetailPage && this.$route.path === standaloneWaybillDetailRoute;
},
// 容器形态读实例创建时锁定的标志,不随 $route 翻转(详见 data 中 formPageLocked 的说明)
// 当前路由是否仍属于本实例(tab.js 按 fullPath 建实例,一个实例只对应一个 path)。
@@ -3794,7 +3738,7 @@ export default {
return !this.isRoadTransport;
},
transportVehicleNoLabel() {
return this.isRoadTransport ? '车牌号' : '船/航/班列号';
return this.isRoadTransport ? '车牌号' : '船班号/船次号/班次号';
},
actionButtonLikeSearch() {
return this.config.actionButtonLikeSearch === true;
@@ -3863,9 +3807,6 @@ export default {
if (!total) return '';
return Number.isInteger(total) ? String(total) : String(Number(total.toFixed(3)));
},
fixedQuantityUnit() {
return WAYBILL_QUANTITY_UNIT;
},
taskFreightAmount() {
const quantity = Number(this.taskCargoQuantityTotal || this.form.quantity || 0);
const unitPrice = Number(this.form.unitPrice || 0);
@@ -3896,10 +3837,7 @@ export default {
return this.currencyRemark(currency);
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
canViewAllDept() {
return this.isAdmin;
@@ -3999,7 +3937,19 @@ export default {
},
watch: {
$route(to, from) {
if (!this.isStandaloneWaybillFormPage) return;
// 列表实例:从表单/详情返回时刷新,避免 keep-alive 仍显示旧数据
if (!this.formPageLocked && !this.detailPageLocked) {
if (
to.path === standaloneWaybillListRoute &&
(from?.path?.includes('/waybill-manage/form') ||
from?.path?.includes('/waybill-manage/detail'))
) {
sessionStorage.removeItem(waybillManageListRefreshKey);
this.onLoad(this.page, this.query);
}
return;
}
if (!this.formPageLocked || !this.isStandaloneWaybillFormPage) return;
if (
to.query.mode !== from?.query?.mode ||
String(to.query.id || '') !== String(from?.query?.id || '')
@@ -4038,7 +3988,7 @@ export default {
this.attachmentDocumentPreviewVisible = false;
this.attachmentImagePreviewVisible = false;
this.waybillRouteChangeBox = false;
this.waybillCommonAddressBox = false;
this.waybillRouteChangeMapTarget = null;
this.flowBox = false;
this.transportRouteBox = false;
this.transportAddressBox = false;
@@ -4505,6 +4455,20 @@ export default {
if (prop === 'feeGenerationMode') {
return row[prop] === 'manual' ? '账单导入生成' : '系统生成';
}
// 计划未选时接口可能回传 planId=-1 / planName=-1,详情统一显示为空
if (prop === 'planName' || prop === 'planId') {
const planName = row.planName;
if (
planName === undefined ||
planName === null ||
planName === '' ||
String(planName) === '-1' ||
Number(planName) === -1
) {
return '-';
}
return planName;
}
const column = this.findColumn(this.option.column, prop);
if (column?.formatter) return column.formatter(row, {}, row[prop]);
if (column?.dicData) {
@@ -4512,6 +4476,15 @@ export default {
if (item) return item.label;
}
const value = row[prop];
if (
value === undefined ||
value === null ||
value === '' ||
Number(value) === -1 ||
String(value) === '-1'
) {
return '-';
}
if (value && typeof value === 'object') return JSON.stringify(value);
if (typeof value === 'string' && /^[\[{]/.test(value.trim())) {
try {
@@ -4521,7 +4494,7 @@ export default {
// 非 JSON 文本按原值展示。
}
}
return value === undefined || value === null || value === '' ? '-' : value;
return value;
},
waybillTransportMode(row = {}) {
return (
@@ -4595,7 +4568,7 @@ export default {
return text.slice(0, 10);
},
waybillVehicleNoLabel(row = {}) {
return this.waybillIsRoadTransport(row) ? '车牌号' : '船/航/班列号';
return this.waybillIsRoadTransport(row) ? '车牌号' : '船班号/船次号/班次号';
},
waybillUnitPrice(row = {}) {
const price = row.unitPrice || row.price || '';
@@ -4655,19 +4628,14 @@ export default {
this.waybillVoucherFolder = null;
this.waybillVoucherFolderView = false;
this.waybillVoucherImagesLoaded = false;
const request = this.isPublicWaybillView
? getMkPublicDetail('waybill-manage', row.id)
: typeof this.api.getDetail === 'function'
const request =
typeof this.api.getDetail === 'function'
? this.api.getDetail(row.id)
: Promise.resolve({ data: { data: row } });
request
.then(res => {
const detail = res?.data?.data || res?.data || row;
this.detailRow = detail;
if (this.isPublicWaybillView) {
this.applyFormDetail(detail);
return;
}
this.loadWaybillPunchRecords();
this.loadWaybillVoucherImages();
if (detail.contractId) {
@@ -4708,8 +4676,9 @@ export default {
this.waybillTrackInfo = null;
this.waybillTrackHint = '';
if (this.isStandaloneWaybillDetailPage) {
this.markWaybillListNeedRefresh();
this.$router.$avueRouter?.closeTag?.();
this.$router.push({ path: '/business/waybill-manage', query: {} });
this.$router.push({ path: standaloneWaybillListRoute, query: {} });
return;
}
this.detailBox = false;
@@ -4742,7 +4711,6 @@ export default {
return row.id || row.customerId || '';
},
canOpenWaybillDetailSummaryLink(prop) {
if (this.isPublicWaybillView) return false;
const row = this.detailRow || {};
const value = this.formatDetailValue(row, prop);
if (!value || value === '-') return false;
@@ -5015,43 +4983,22 @@ export default {
updateWaybillRouteChangeNodes() {
this.waybillRouteChangeNodes = this.buildWaybillRouteNodes(this.waybillRouteChangeRows);
},
openWaybillCommonAddressDialog(row, field) {
this.waybillCommonAddressTarget = { row, field };
this.waybillCommonAddressQuery = { addressName: '' };
this.waybillCommonAddressPage.currentPage = 1;
this.waybillCommonAddressBox = true;
this.loadWaybillCommonAddresses();
},
loadWaybillCommonAddresses() {
this.waybillCommonAddressLoading = true;
getCommonAddressList(
this.waybillCommonAddressPage.currentPage,
this.waybillCommonAddressPage.pageSize,
{
...this.waybillCommonAddressQuery,
allDept: 0,
}
)
.then(res => {
const data = res.data?.data || {};
this.waybillCommonAddressRows = data.records || [];
this.waybillCommonAddressPage.total = data.total || 0;
})
.finally(() => {
this.waybillCommonAddressLoading = false;
});
},
selectWaybillCommonAddress(address) {
const target = this.waybillCommonAddressTarget;
if (!target) return;
target.row[`${target.field}Address`] =
[address.regionName, address.detailAddress || address.addressName]
.filter(Boolean)
.join('') ||
address.addressName ||
'';
this.updateWaybillRouteChangeNodes();
this.waybillCommonAddressBox = false;
openWaybillRouteChangeMapDialog(row, field) {
if (!row || !field) return;
this.waybillRouteChangeMapTarget = { row, field };
this.transportMapTarget = field;
this.transportMapKeyword = row[`${field}Address`] || row[`${field}Name`] || '';
this.transportMapSelected = this.buildTransportMapSelection({
lng: row[`${field}Longitude`],
lat: row[`${field}Latitude`],
address: row[`${field}Address`],
regionName: row[`${field}Name`],
regionCode: row[`${field}RegionCode`],
});
this.transportMapStatus = this.transportMapSelected.longitude
? '已加载当前选点,可重新选点'
: '可搜索地址或点击地图选点';
this.transportMapBox = true;
},
async confirmWaybillRouteChange() {
const row = this.waybillRouteChangeRows[0] || {};
@@ -5438,6 +5385,12 @@ export default {
if (this.config.saveAsDraft && saveAsDraft) {
submitRow[this.config.draftStatusProp || 'approvalStatus'] =
this.config.draftStatus || 'draft';
} else if (
this.config.draftStatusProp === 'businessStatus' &&
submitRow.businessStatus === (this.config.draftStatus || 'draft')
) {
// 正式提交:清空草稿态,由后端按过程配置校正为待执行/进行中
submitRow.businessStatus = '';
}
delete submitRow.basicInfoTitle;
delete submitRow.templateInfoTitle;
@@ -5455,6 +5408,26 @@ export default {
submitRow[key] = submitRow[key].trim();
}
});
// 未选计划时清空 -1 占位,避免落库后编辑/查看回显 -1
if (
submitRow.planId === undefined ||
submitRow.planId === null ||
submitRow.planId === '' ||
Number(submitRow.planId) === -1 ||
Number(submitRow.planId) <= 0
) {
submitRow.planId = null;
if (
!submitRow.planName ||
String(submitRow.planName) === '-1' ||
Number(submitRow.planName) === -1
) {
submitRow.planName = null;
}
}
if (String(submitRow.planName) === '-1' || Number(submitRow.planName) === -1) {
submitRow.planName = null;
}
return submitRow;
},
validateMobileFields(fields = []) {
@@ -5664,22 +5637,11 @@ export default {
}
this.getSaveRequest()(submitRow).then(
res => {
const data = res.data?.data || {};
const id = data.id || submitRow.id;
const after = id
? submitMkApprovalFlow({
bizType: 'waybill-manage',
formInstanceId: id,
subjectName: data.waybillNo || submitRow.waybillNo || '',
approvalStatus: data.approvalStatus || submitRow.approvalStatus || '',
})
: Promise.resolve();
return after.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '提交成功!' });
done();
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
});
this.markWaybillListNeedRefresh();
this.onLoad(this.page);
this.$message({ type: 'success', message: '提交成功!' });
done();
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
},
error => {
window.console.log(error);
@@ -5698,26 +5660,14 @@ export default {
? this.api.updateAttachments
: this.getSaveRequest();
request(submitRow).then(
res => {
() => {
const skipMk = this.attachmentUploadMode;
const data = res.data?.data || {};
const id = data.id || submitRow.id;
const after =
skipMk || !id
? Promise.resolve()
: submitMkApprovalFlow({
bizType: 'waybill-manage',
formInstanceId: id,
subjectName: data.waybillNo || submitRow.waybillNo || '',
approvalStatus: data.approvalStatus || submitRow.approvalStatus || '',
});
return after.then(() => {
this.attachmentUploadMode = false;
this.onLoad(this.page);
this.$message({ type: 'success', message: skipMk ? '操作成功!' : '提交成功!' });
done();
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
});
this.attachmentUploadMode = false;
this.markWaybillListNeedRefresh();
this.onLoad(this.page);
this.$message({ type: 'success', message: skipMk ? '操作成功!' : '提交成功!' });
done();
if (this.isStandaloneWaybillPage) this.closeCrudDialog();
},
error => {
this.attachmentUploadMode = false;
@@ -5729,6 +5679,9 @@ export default {
getSaveRequest() {
return this.api.submit;
},
markWaybillListNeedRefresh() {
sessionStorage.setItem(waybillManageListRefreshKey, '1');
},
handleDraftSave() {
const request =
typeof this.api.saveDraft === 'function' ? this.api.saveDraft : this.api.submit;
@@ -5739,6 +5692,7 @@ export default {
request(submitRow)
.then(() => {
this.$message.success(`${this.config.draftSaveText || '保存'}成功`);
this.markWaybillListNeedRefresh();
this.onLoad(this.page, this.query);
this.closeCrudDialog();
})
@@ -5748,8 +5702,10 @@ export default {
},
closeCrudDialog() {
if (this.isStandaloneWaybillPage) {
// 关闭独立表单/详情页时标记列表需刷新(含直接点关闭未保存的场景)
this.markWaybillListNeedRefresh();
this.$router.$avueRouter?.closeTag?.();
this.$router.push({ path: '/business/waybill-manage', query: {} });
this.$router.push({ path: standaloneWaybillListRoute, query: {} });
return;
}
const crud = this.$refs.crud;
@@ -6371,14 +6327,11 @@ export default {
if (!rows.length) return false;
const quantityUnits = rows.map(row => String(row.quantityUnit || '').trim());
if (quantityUnits.some(unit => !unit) || new Set(quantityUnits).size !== 1) return false;
const quantityUnit = quantityUnits[0];
return rows.every(row => {
const priceUnit = String(row.priceUnit || '').trim();
const separatorIndex = Math.max(priceUnit.lastIndexOf('/'), priceUnit.lastIndexOf('/'));
const priceQuantityUnit =
separatorIndex >= 0 ? priceUnit.slice(separatorIndex + 1).trim() : priceUnit;
return !this.isBillingFieldEmpty(row.unitPrice) && priceQuantityUnit === quantityUnit;
});
const expectedPriceUnit = this.resolvePriceUnitByQuantityUnit(quantityUnits[0]);
if (!expectedPriceUnit) return false;
return rows.every(
row => !this.isBillingFieldEmpty(row.unitPrice) && String(row.priceUnit || '').trim() === expectedPriceUnit
);
},
taskFullFreightQuantity(index) {
return this.formatTaskFullFreightNumber(this.transportCargoRows[index]?.quantity || 0);
@@ -6394,10 +6347,6 @@ export default {
Number(cargo.unitPrice || 0) * Number(cargo.quantity || 0)
);
},
taskFullFreightGroupQuantity(group = {}) {
const total = (group.rows || []).reduce((sum, cargo) => sum + Number(cargo.quantity || 0), 0);
return this.formatTaskFullFreightNumber(total);
},
taskFullFreightGroupUnitPrice(group = {}) {
const prices = (group.rows || [])
.map(cargo => String(cargo.unitPrice ?? '').trim())
@@ -6406,7 +6355,11 @@ export default {
return prices[0];
},
taskFullFreightGroupPriceUnit(group = {}) {
return group.rows?.[0]?.priceUnit || (group.quantityUnit ? `元/${group.quantityUnit}` : '');
return (
group.rows?.[0]?.priceUnit ||
this.resolvePriceUnitByQuantityUnit(group.rows?.[0]?.quantityUnit) ||
''
);
},
taskFullFreightGroupAmount(group = {}) {
const total = (group.rows || []).reduce(
@@ -7137,6 +7090,11 @@ export default {
requiredFields.push(['driverName', '司机']);
requiredFields.push(['driverPhone', '手机号']);
}
if (!this.isTaskFullMode) {
requiredFields.push(['cargoName', '货物名称']);
requiredFields.push(['model', '型号']);
requiredFields.push(['quantity', '数量']);
}
const emptyField = requiredFields.find(([prop]) => this.isBillingFieldEmpty(row[prop]));
if (emptyField) {
this.$message.warning(`请选择或输入${emptyField[1]}`);
@@ -7171,7 +7129,7 @@ export default {
}
} else {
const invalidLengthField = [
['vehicleNo', '船/航/班列号', 30],
['vehicleNo', '船班号/船次号/班次号', 30],
['captainName', '船长', 20],
['cabinNo', '舱位', 30],
['containerNo', '箱号', 30],
@@ -7429,6 +7387,24 @@ export default {
parts.length > 1 ? `${parts[0]}.${parts.slice(1).join('').slice(0, 3)}` : parts[0];
this.syncTransportCargoJson();
},
resolvePriceUnitByQuantityUnit(quantityUnit) {
return (
{
吨: '元/吨',
方: '元/立方',
件: '元/件',
}[String(quantityUnit || '').trim()] || ''
);
},
handleTransportCargoQuantityUnitChange(quantityUnit) {
const priceUnit = this.resolvePriceUnitByQuantityUnit(quantityUnit);
if (!priceUnit) return;
(this.transportCargoRows || []).forEach(cargo => {
cargo.priceUnit = priceUnit;
});
this.form.priceUnit = priceUnit;
this.syncTransportCargoJson();
},
handleTransportCargoTypeChange(row, value) {
const path = this.normalizeCargoTypePath(value);
const cargoType = this.findCargoTypeByPath(path);
@@ -7861,10 +7837,15 @@ export default {
this.$refs.crud?.clearValidate?.([`${prefix}Address`]);
});
},
closeTransportMapDialog() {
this.transportMapBox = false;
this.waybillRouteChangeMapTarget = null;
},
openTransportMapDialog(target) {
if (this.dialogReadonly) return;
if (!this.ensureTransportTypeBeforeAddress()) return;
const prefix = target === 'departure' ? 'departure' : 'arrival';
this.waybillRouteChangeMapTarget = null;
this.transportMapTarget = prefix;
this.transportMapKeyword = this.form[`${prefix}Address`] || this.form[`${prefix}Name`] || '';
this.transportMapSelected = this.buildTransportMapSelection({
@@ -7941,31 +7922,32 @@ export default {
this.$message.warning('请输入地址关键词');
return;
}
if (this.transportMapLoading) return;
this.transportMapSearchResults = [];
this.transportMapLoading = true;
this.loadAmap()
.then(() => {
this.transportMapLoading = true;
this.ensureTransportAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
this.transportMapSearchResults = normalizeMapSearchResults(result, keyword);
const point = this.resolveMapPoint(result);
if (!point) {
this.transportMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
return;
}
this.pickTransportMapPoint(point, keyword);
})
.catch(() => {
this.transportMapStatus = '地图搜索失败';
this.$message.error('地图搜索失败,请稍后重试');
})
.finally(() => {
this.transportMapLoading = false;
});
.then(() => searchAmapPlaceText(keyword))
.then(pois => {
this.transportMapSearchResults = normalizeMapSearchResults(pois, keyword);
const first = this.transportMapSearchResults[0];
if (!first?.location) {
this.transportMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
return;
}
return this.$nextTick().then(() => {
if (typeof this.transportAmap?.resize === 'function') {
this.transportAmap.resize();
}
this.pickTransportMapPoint(first.location, first.address || first.name || keyword);
});
})
.catch(() => {
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
this.transportMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
})
.finally(() => {
this.transportMapLoading = false;
});
},
selectTransportMapSearchResult(item) {
@@ -8023,6 +8005,23 @@ export default {
this.$message.warning('请先搜索或点击地图完成选点');
return;
}
if (this.waybillRouteChangeMapTarget) {
const { row, field } = this.waybillRouteChangeMapTarget;
row[`${field}Address`] =
this.transportMapSelected.detailAddress || row[`${field}Address`] || '';
if (this.transportMapSelected.regionName) {
row[`${field}Name`] = this.transportMapSelected.regionName;
}
row[`${field}Longitude`] = this.transportMapSelected.longitude;
row[`${field}Latitude`] = this.transportMapSelected.latitude;
if (this.transportMapSelected.regionCode) {
row[`${field}RegionCode`] = this.transportMapSelected.regionCode;
}
this.updateWaybillRouteChangeNodes();
this.waybillRouteChangeMapTarget = null;
this.transportMapBox = false;
return;
}
const prefix = this.transportMapTarget;
this.form[`${prefix}Address`] =
this.transportMapSelected.detailAddress || this.form[`${prefix}Address`];
@@ -9750,10 +9749,16 @@ export default {
}
&--road {
grid-template-columns: repeat(3, minmax(0, 1fr));
column-gap: 56px;
grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: 24px;
row-gap: 16px;
:deep(.el-form-item__content > .el-input),
:deep(.el-form-item__content > .el-select) {
width: 100%;
max-width: 100%;
}
:deep(.waybill-manage-page__append-select) {
width: 112px;
}
@@ -10107,6 +10112,10 @@ export default {
font-weight: 500;
vertical-align: baseline;
}
&.is-link[role='link'] {
cursor: pointer;
}
}
&__waybill-route {
@@ -678,7 +678,7 @@ export default {
removeCustomPeriodRow(index) { if (index <= 0) return; const periods = [...(this.settlementRule.customPeriods || [])]; periods.splice(index, 1); this.settlementRule.customPeriods = normalizeCustomPeriods(periods); },
validateCustomPeriods(periods = [], label) { const rows = normalizeCustomPeriods(periods); if (!rows.length) { this.$message.warning(`${label}:请至少配置一段自定义周期`); return false; } for (let index = 0; index < rows.length; index += 1) { const row = rows[index]; const startDay = Number(row.startDay); const endDay = Number(row.endDay); if (!Number.isFinite(startDay) || startDay < 1 || startDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间开始日`); return false; } if (!Number.isFinite(endDay) || endDay < 1 || endDay > 31) { this.$message.warning(`${label}:请选择第${index + 1}行运单区间结束日`); return false; } if (endDay < startDay) { this.$message.warning(`${label}:第${index + 1}行结束日不能早于开始日`); return false; } if (index > 0 && startDay !== Number(rows[index - 1].endDay) + 1) { this.$message.warning(`${label}:自定义多周期区间必须连续,不允许重叠或存在日期缺口`); return false; } } return true; },
validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; },
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', formInstanceId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected' }); this.$message.success('变更已提交'); this.$router.back(); },
async submit() { await this.$refs.formRef.validate(); const total = this.paymentRatioRows.reduce((sum, row) => sum + Number(row.ratioLimit || 0), 0); if (this.paymentRatioRows.length && Math.abs(total - 100) > 0.0001) { this.$message.warning('付款比例上限合计必须等于100%'); return; } if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule }; else this.formalSettlementConfig = { ...this.settlementRule }; if (!this.validateSettlementRule(this.preSettlementConfig, '预结算配置') || !this.validateSettlementRule(this.formalSettlementConfig, '正式结算配置')) return; const settlementRule = { preSettlementConfig: this.preSettlementConfig, formalSettlementConfig: this.formalSettlementConfig }; await api.submitChange({ ...this.form, settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB', copyCount: normalizeOptionalPositiveInteger(this.form.copyCount), invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle), paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays), contractAmount: normalizeOptionalAmount(this.form.contractAmount), startDate: this.period[0], endDate: this.period[1], feeGenerationMode: this.feeGenerationMode, billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0, billingPlanJson: JSON.stringify(this.plans), settlementRuleJson: JSON.stringify(settlementRule), preSettlementConfigJson: JSON.stringify(this.preSettlementConfig), formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig), paymentRatioJson: JSON.stringify(this.paymentRatioRows), contractFileJson: JSON.stringify(this.contractFileRows), attachmentsJson: JSON.stringify(this.attachments), changeContent: this.form.changeContent, changeReason: this.form.changeReason, changeAttachmentsJson: JSON.stringify(this.changeMaterials) }); await submitMkApprovalFlow({ bizType: 'contract-manage', bizId: this.form.id, subjectName: this.form.contractName || '', approvalStatus: this.form.approvalStatus || 'change_rejected' }); this.$message.success('变更已提交'); this.$router.back(); },
},
};
</script>
+53 -22
View File
@@ -1017,7 +1017,7 @@ import { getDeptTree } from '@/api/system/dept';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
import { config, option } from '@/option/business/contract-manage';
import { getToken } from '@/utils/auth';
import { getToken, isAdministrator } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import BillingPlanEditor from './components/billing-plan-editor.vue';
@@ -1034,6 +1034,11 @@ import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-fil
import '@open-file-viewer/core/style.css';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
const contractManageListRoute = '/business/contract-manage';
const contractManageFormRoute = '/business/contract-manage/form';
const contractManageDetailRoute = '/business/contract-manage/detail';
const contractManageListRefreshKey = 'contract-manage-list-refresh';
const clone = value => JSON.parse(JSON.stringify(value));
const normalizeOptionalInteger = (value, emptyValue = null) => {
if (value === undefined || value === null || value === '' || Number(value) < 0) {
@@ -1296,6 +1301,9 @@ export default {
flowBox: false,
flowUrl: '',
processInstanceId: '',
// 页面形态在实例创建时锁定,避免 keep-alive 缓存后随全局 $route 翻转
formPageLocked: false,
detailPageLocked: false,
};
},
computed: {
@@ -1304,24 +1312,19 @@ export default {
return { addBtn: this.canCreate };
},
isAdmin() {
const authority = this.userInfo?.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
canCreate() {
return this.hasPermission(`${this.config.permission}_add`);
},
isListPage() {
return !this.isFormPage && !this.isDetailPage;
return !this.formPageLocked && !this.detailPageLocked && !this.isPublicViewPage;
},
isFormPage() {
return this.$route.path === '/business/contract-manage/form';
return this.formPageLocked;
},
isDetailPage() {
return (
this.$route.path === '/business/contract-manage/detail' || this.isPublicViewPage
);
return this.detailPageLocked || this.isPublicViewPage;
},
isPublicViewPage() {
return this.$route.path === '/business/contract-manage/public-view';
@@ -1484,9 +1487,21 @@ export default {
},
},
watch: {
'$route.fullPath'() {
if (this.isFormPage) this.initFormPage();
if (this.isDetailPage) this.initDetailPage();
$route(to, from) {
if (this.isListPage) {
if (
to.path === contractManageListRoute &&
(from?.path?.includes('/contract-manage/form') ||
from?.path?.includes('/contract-manage/detail') ||
from?.path?.includes('/contract-manage/change'))
) {
sessionStorage.removeItem(contractManageListRefreshKey);
this.onLoad(this.page, this.query);
}
return;
}
if (this.formPageLocked && to.path === contractManageFormRoute) this.initFormPage();
if (this.detailPageLocked && to.path === contractManageDetailRoute) this.initDetailPage();
},
settlementConfigTab(tab, oldTab) {
if (tab === oldTab) return;
@@ -1498,6 +1513,9 @@ export default {
},
},
created() {
this.formPageLocked = this.$route.path === contractManageFormRoute;
this.detailPageLocked =
this.$route.path === contractManageDetailRoute || this.isPublicViewPage;
if (!this.isPublicViewPage) {
this.loadProjectOptions();
this.loadSettlementDictionaries();
@@ -1506,7 +1524,17 @@ export default {
if (this.isFormPage) this.initFormPage();
if (this.isDetailPage) this.initDetailPage();
},
activated() {
if (!this.isListPage) return;
if (sessionStorage.getItem(contractManageListRefreshKey)) {
sessionStorage.removeItem(contractManageListRefreshKey);
this.onLoad(this.page, this.query);
}
},
methods: {
markContractManageListNeedRefresh() {
sessionStorage.setItem(contractManageListRefreshKey, '1');
},
buildTableOption() {
return {
...option,
@@ -1579,7 +1607,7 @@ export default {
});
},
normalizeSearch(params = {}) {
const query = { ...params };
const query = { ...params, allDept: this.isAdmin ? 1 : 0 };
if (query.organizationName) {
const id = Array.isArray(query.organizationName)
? query.organizationName.at(-1)
@@ -1648,7 +1676,7 @@ export default {
},
openForm(mode, row = {}) {
this.$router.push({
path: '/business/contract-manage/form',
path: contractManageFormRoute,
query:
mode === 'edit'
? { mode, id: row.id, name: '编辑合同管理' }
@@ -1741,8 +1769,9 @@ export default {
this.$nextTick(() => this.$refs.contractForm?.clearValidate());
},
closeForm() {
this.markContractManageListNeedRefresh();
this.$router.$avueRouter?.closeTag?.();
this.$router.push('/business/contract-manage');
this.$router.push(contractManageListRoute);
},
validateBase(action) {
if (action === 'temporary') {
@@ -1840,6 +1869,7 @@ export default {
submitCreate(action) {
if (this.submitAction || !this.validateBase(action)) return;
this.submitAction = action;
const previousApprovalStatus = this.form.approvalStatus || '';
const submit = {
...this.normalizeSubmit(),
contractStage: action === 'formal' ? 'temporary' : 'draft',
@@ -1858,9 +1888,9 @@ export default {
).then(() =>
submitMkApprovalFlow({
bizType: 'contract-manage',
formInstanceId: id,
bizId: id,
subjectName: saved.contractName || submit.contractName || '',
approvalStatus: saved.approvalStatus || submit.approvalStatus || '',
approvalStatus: previousApprovalStatus || saved.approvalStatus || submit.approvalStatus || '',
})
);
}
@@ -1896,7 +1926,7 @@ export default {
}
return submitMkApprovalFlow({
bizType: 'contract-manage',
formInstanceId: payload.id || this.form.id,
bizId: payload.id || this.form.id,
subjectName: payload.contractName || this.form.contractName || '',
approvalStatus: this.form.approvalStatus || 'change_rejected',
}).then(() => {
@@ -2532,8 +2562,9 @@ export default {
});
},
closeDetail() {
this.markContractManageListNeedRefresh();
this.$router.$avueRouter?.closeTag?.();
this.$router.push('/business/contract-manage');
this.$router.push(contractManageListRoute);
},
applyDetailState(detail = {}) {
this.detailRow = {
@@ -2842,7 +2873,7 @@ export default {
? request().then(() =>
submitMkApprovalFlow({
bizType: 'contract-manage',
formInstanceId: row.id,
bizId: row.id,
subjectName: row.contractName || '',
approvalStatus: row.approvalStatus || '',
})
@@ -2876,7 +2907,7 @@ export default {
},
handleCopy(row) {
this.$router.push({
path: '/business/contract-manage/form',
path: contractManageFormRoute,
query: { mode: 'add', copyId: row.id, name: '新增合同管理' },
});
},
File diff suppressed because it is too large Load Diff
+2
View File
@@ -70,6 +70,8 @@
<p class="project-name">{{ row.projectName || '-' }}</p>
<p>{{ timeRange(row) }}</p>
<p>{{ goodsSummary(row) }}</p>
<p>创建人:{{ row.createUserName || '-' }}</p>
<p>创建时间:{{ row.createTime || '-' }}</p>
</section>
<section class="route-progress">
<div class="route-track">
+3 -6
View File
@@ -416,7 +416,7 @@ import { getList as getProjectList } from '@/api/business/project-apply';
import { getDeptTree } from '@/api/system/dept';
import { exportBlob } from '@/api/common';
import { config, option } from '@/option/business/process-config';
import { getToken } from '@/utils/auth';
import { getToken, isAdministrator } from '@/utils/auth';
import { downloadXls } from '@/utils/util';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
@@ -524,10 +524,7 @@ export default {
};
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
ids() {
return this.selectionList.map(item => item.id).join(',');
@@ -696,7 +693,7 @@ export default {
return getProjectList(1, 50, {
projectName: keyword,
approvalStatus: 'approved',
allDept: 0,
allDept: this.isAdmin ? 1 : 0,
})
.then(res => {
const data = res.data.data || {};
+78 -14
View File
@@ -911,9 +911,10 @@ import { getDeptTree, getPlatformCompanySelect } from '@/api/system/dept';
import { getDictionary as getSystemDictionary } from '@/api/system/dict';
import { getDictionary as getBizDictionary } from '@/api/system/dictbiz';
import { getList as getUserList } from '@/api/system/user';
import { getToken } from '@/utils/auth';
import { getToken, isAdministrator } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import PdfPreview from '@/components/pdf-preview/main.vue';
@@ -1008,6 +1009,10 @@ const attachmentViewerPlugins = [
fallbackPlugin(),
];
const projectApplyListRoute = '/business/project-apply';
const projectApplyFormRoute = '/business/project-apply/form';
const projectApplyListRefreshKey = 'project-apply-list-refresh';
const otherAttachmentType = { label: '其他附件', value: '其他附件' };
const normalProjectAttachmentTypeOptions = [
@@ -1113,6 +1118,7 @@ export default {
// 否则 isProjectFormPage 变 false 后容器会从 div 切成 el-dialog(append-to-body),
// 盖住后续打开的页面(如客商档案)。
isFormPageInstance: false,
mkEnabled: false,
projectBox: false,
dialogType: 'add',
dialogReadonly: false,
@@ -1280,10 +1286,7 @@ export default {
};
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
canViewAllDept() {
return this.isAdmin;
@@ -1358,16 +1361,26 @@ export default {
},
},
created() {
if (this.isAdmin) {
this.allDept = 1;
}
if (this.isPublicViewPage) {
this.isFormPageInstance = true;
this.openPublicProjectForm();
return;
}
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
this.loadDeptOptions();
this.loadCargoTypeOptions();
this.loadTransportTypeOptions();
this.loadSettlementModeOptions();
if (this.$route.path === '/business/project-apply/form') {
if (this.$route.path === projectApplyFormRoute) {
this.isFormPageInstance = true;
this.openProjectFormPage();
}
@@ -1377,10 +1390,32 @@ export default {
deactivated() {
this.closeInnerDialogs();
},
activated() {
if (this.isFormPageInstance || this.isPublicViewPage) return;
if (sessionStorage.getItem(projectApplyListRefreshKey)) {
sessionStorage.removeItem(projectApplyListRefreshKey);
this.onLoad(this.page, this.query);
}
},
beforeUnmount() {
this.closeInnerDialogs();
},
watch: {
$route(to, from) {
if (this.isFormPageInstance || this.isPublicViewPage) return;
if (
to.path === projectApplyListRoute &&
from?.path?.includes('/project-apply/form')
) {
sessionStorage.removeItem(projectApplyListRefreshKey);
this.onLoad(this.page, this.query);
}
},
},
methods: {
markProjectApplyListNeedRefresh() {
sessionStorage.setItem(projectApplyListRefreshKey, '1');
},
closeInnerDialogs() {
this.changeRecordDetailVisible = false;
this.attachmentDocumentPreviewVisible = false;
@@ -1506,7 +1541,9 @@ export default {
},
closeProjectForm() {
if (this.isProjectFormPage) {
this.$router.push('/business/project-apply');
this.markProjectApplyListNeedRefresh();
this.$router.$avueRouter?.closeTag?.();
this.$router.push(projectApplyListRoute);
} else {
this.projectBox = false;
}
@@ -1535,7 +1572,11 @@ export default {
);
},
customOperations(row) {
return (this.config.operations || []).filter(operation => this.canOperation(operation, row));
return (this.config.operations || []).filter(operation => {
if (operation.mkOnly && !this.mkEnabled) return false;
if (operation.localOnly && this.mkEnabled) return false;
return this.canOperation(operation, row);
});
},
canOperation(operation, row) {
const permission = operation.permission || `${this.config.permission}_${operation.action}`;
@@ -1661,6 +1702,10 @@ export default {
this.openProjectDialog('change', row);
return;
}
if (operation.action === 'mkAudit') {
this.handleMkAudit(row);
return;
}
const run = value => {
const args = operation.prompt ? [row.id, value] : [row.id];
const request = () => this.api[operation.action](...args);
@@ -1668,7 +1713,7 @@ export default {
operation.action === 'submitApproval'
? submitMkApprovalFlow({
bizType: 'project-apply',
formInstanceId: row.id,
bizId: row.id,
subjectName: row.projectName || '',
approvalStatus: row.approvalStatus || '',
}).then(() => request())
@@ -1697,6 +1742,24 @@ export default {
type: 'warning',
}).then(() => run());
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning('项目ID为空,无法打开审核页');
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
buildExportParams() {
return {
...this.query,
@@ -1736,7 +1799,7 @@ export default {
else if (type === 'view') query.name = `查看${this.config.title}`;
else if (type === 'majorSupplement') query.name = '重大项目补录';
else if (type === 'change') query.name = '项目变更';
this.$router.push({ path: '/business/project-apply/form', query });
this.$router.push({ path: projectApplyFormRoute, query });
},
resetProjectDialog() {
this.$refs.projectForm?.clearValidate();
@@ -1844,7 +1907,8 @@ export default {
const data = res.data?.data || {};
const id = data.id || this.form.id;
const name = data.projectName || this.form.projectName || '';
const status = data.approvalStatus || this.form.approvalStatus || '';
// 必须用提交前状态:业务 submit 后可能已变成 reviewing,导致无法识别「通过后再提交」
const status = this.form.approvalStatus || data.approvalStatus || '';
if (!id) {
this.$message.success('操作成功!');
this.closeProjectForm();
@@ -1853,7 +1917,7 @@ export default {
}
return submitMkApprovalFlow({
bizType: 'project-apply',
formInstanceId: id,
bizId: id,
subjectName: name,
approvalStatus: status,
}).then(() => {
@@ -1924,7 +1988,7 @@ export default {
const id = this.form.id;
return submitMkApprovalFlow({
bizType: 'project-apply',
formInstanceId: id,
bizId: id,
subjectName: this.form.projectName || '',
approvalStatus: this.form.approvalStatus || 'change_rejected',
}).then(() => {
@@ -309,7 +309,7 @@ import {
getList as getProjectList,
} from '@/api/business/project-apply';
import { config, option } from '@/option/business/temporary-credit-limit';
import { getToken } from '@/utils/auth';
import { getToken, isAdministrator } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { ElImageViewer } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
@@ -423,10 +423,7 @@ export default {
return String(width);
},
isAdmin() {
const authority = this.userInfo?.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
canCreate() {
return this.hasPermission(`${this.config.permission}_add`);
@@ -537,7 +534,11 @@ export default {
onLoad(page, params = {}) {
this.loading = true;
this.api
.getList(page.currentPage, page.pageSize, { ...params, ...this.query })
.getList(page.currentPage, page.pageSize, {
...params,
...this.query,
allDept: this.isAdmin ? 1 : 0,
})
.then(res => {
const result = res.data?.data || {};
this.page.total = result.total || 0;
@@ -942,7 +943,7 @@ export default {
NProgress.start();
exportBlob(
this.config.exportUrl,
{ ...this.query, ids: this.ids, [this.website.tokenHeader]: getToken() },
{ ...this.query, allDept: this.isAdmin ? 1 : 0, ids: this.ids, [this.website.tokenHeader]: getToken() },
{ feedback: true }
)
.then(res => {
+2 -3
View File
@@ -45,6 +45,7 @@ import { useStore } from 'vuex';
import { ElMessage, ElMessageBox } from 'element-plus';
import md5 from 'js-md5';
import * as api from '@/api/business/voucher-manage';
import { isAdministrator } from '@/utils/auth';
const route = useRoute();
const router = useRouter();
@@ -52,9 +53,7 @@ const store = useStore();
const voucherId = route.query.id;
const isSuperAdmin = computed(() => {
const authority = store.getters.userInfo?.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator({ authority });
});
const loading = ref(false), rows = ref([]), voucher = ref({ voucherBatchNo: route.query.batchNo || '' });
const page = reactive({ current: 1, size: 10, total: 0 });
+4 -2
View File
@@ -101,7 +101,8 @@
prop="relatedWaybillCount"
label="凭证已关联数量"
width="125"
/><el-table-column prop="unRelatedWaybillCount" label="凭证未关联数量" width="125" />
/> <el-table-column prop="unRelatedWaybillCount" label="凭证未关联数量" width="125" />
<el-table-column prop="createUserName" label="创建人" min-width="120" />
<el-table-column prop="createTime" label="创建时间" min-width="170" />
<el-table-column
prop="auditStatus"
@@ -504,6 +505,7 @@
import { computed, reactive, ref, watch, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import { isAdministrator as checkAdministrator } from '@/utils/auth';
import md5 from 'js-md5';
import { ElMessage, ElMessageBox } from 'element-plus';
import { WarnTriangleFilled } from '@element-plus/icons-vue';
@@ -517,7 +519,7 @@ const store = useStore();
// 权限判断
const permission = computed(() => store.getters.permission);
const userInfo = computed(() => store.getters.userInfo);
const isAdmin = computed(() => userInfo.value?.authority?.includes('admin'));
const isAdmin = computed(() => checkAdministrator(userInfo.value));
const hasPermission = code => {
if (isAdmin.value) return true;
return permission.value?.[code] === true;
+6 -1
View File
@@ -1,12 +1,17 @@
<template>
<basic-container class="waybill-import-form-page-container">
<div class="waybill-import-form-page-title">新建导入</div>
<div class="waybill-import-form-page-title">{{ pageTitle }}</div>
<waybill-import-dialog create-page />
</basic-container>
</template>
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import WaybillImportDialog from './components/waybill-import-dialog.vue';
const route = useRoute();
const pageTitle = computed(() => (route.query.id ? '编辑导入' : '新建导入'));
</script>
<style scoped lang="scss">
@@ -1,38 +0,0 @@
<template>
<mk-public-shell biz-type="waybill-manage" :get-form="getForm">
<waybill-manage-page
ref="page"
:api="api"
:config="config"
:crud-option="option"
:detail-id="detailId"
standalone-detail-page
/>
</mk-public-shell>
</template>
<script>
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
import WaybillManagePage from '@/views/business/components/waybill-manage-page.vue';
import * as api from '@/api/business/waybill-manage';
import { config, option } from '@/option/business/waybill-manage';
export default {
name: 'WaybillManagePublicView',
components: { MkPublicShell, WaybillManagePage },
data() {
return { api, config, option };
},
computed: {
detailId() {
return this.$route.query.id || '';
},
},
methods: {
getForm() {
const row = this.$refs.page?.detailRow || {};
return { ...row, subject: row.waybillNo || '' };
},
},
};
</script>
-12
View File
@@ -42,17 +42,6 @@ const FIELD_MAP = {
{ label: '乙方', prop: 'partyB' },
{ label: '项目名称', prop: 'projectName', span: 2 },
],
'waybill-manage': [
{ label: '运单号', prop: 'waybillNo' },
{ label: '项目', prop: 'projectName' },
{ label: '客户合同', prop: 'contractName' },
{ label: '客户名称', prop: 'customerName' },
{ label: '货物名称', prop: 'cargoName' },
{ label: '承运商', prop: 'carrierName' },
{ label: '当前过程节点', prop: 'currentProcessNode' },
{ label: '发货地', prop: 'departureName' },
{ label: '收货地', prop: 'arrivalName' },
],
'pre-settlement': [
{ label: '预结算单号', prop: 'preSettlementNo' },
{ label: '合同名称', prop: 'contractName' },
@@ -88,7 +77,6 @@ const FIELD_MAP = {
const TITLE_MAP = {
'project-apply': '查看项目信息',
'contract-manage': '查看合同信息',
'waybill-manage': '查看运单信息',
'pre-settlement': '查看预结算信息',
'formal-settlement': '查看正式结算信息',
'payment-application': '查看付款申请信息',
+27 -2
View File
@@ -78,7 +78,7 @@
<vehicle-attachment-table v-model="form.attachments" :readonly="readonly" :max-size="10" />
</section-card>
<div class="bill-payment-form-page__actions">
<div v-if="!isPublicViewPage" class="bill-payment-form-page__actions">
<template v-if="!readonly">
<el-button type="primary" :loading="saving" :disabled="submitting" @click="saveDraft">
保存
@@ -139,6 +139,8 @@ import { mapGetters } from 'vuex';
import * as api from '@/api/payment/billPayment';
import * as billLedgerApi from '@/api/payment/billLedger';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { getMkPublicDetail } from '@/api/mk-process';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
const emptyForm = () => ({
id: null,
@@ -194,7 +196,10 @@ export default {
return this.$route.query.id || '';
},
readonly() {
return this.$route.query.mode === 'view';
return this.$route.query.mode === 'view' || this.isPublicViewPage;
},
isPublicViewPage() {
return this.$route.path === '/payment/bill-payment/public-view';
},
},
created() {
@@ -215,6 +220,10 @@ export default {
}
},
async initialize() {
if (this.isPublicViewPage) {
await this.loadPublicDetail();
return;
}
if (this.recordId) {
const data = this.unwrapData(await api.getDetail(this.recordId));
this.form = {
@@ -228,6 +237,15 @@ export default {
this.form.deptName = this.userInfo?.deptName || this.userInfo?.dept_name || '';
this.form.paymentDate = this.$dayjs().format('YYYY-MM-DD');
},
async loadPublicDetail() {
if (!this.recordId) return;
const data = this.unwrapData(await getMkPublicDetail('bill-payment', this.recordId));
this.form = {
...emptyForm(),
...data,
attachments: this.parse(data.attachmentsJson),
};
},
validateUsedAmount(rule, value, callback) {
const amount = Number(value);
const balance = Number(this.form.availableBalance || 0);
@@ -323,7 +341,14 @@ export default {
try {
const id = this.unwrapData(await api.save(this.payload()));
this.form.id = id;
const previousApprovalStatus = this.form.approvalStatus || '';
await api.submit({ id: this.form.id });
await submitMkApprovalFlow({
bizType: 'bill-payment',
bizId: this.form.id,
subjectName: this.form.paymentNo || this.form.billNo || '',
approvalStatus: previousApprovalStatus,
});
this.$message.success('提交成功');
this.goBack();
} finally {
@@ -0,0 +1,21 @@
<template>
<mk-public-shell biz-type="bill-payment" :get-form="getForm">
<bill-payment-form ref="page" />
</mk-public-shell>
</template>
<script>
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
import BillPaymentForm from '@/views/payment/bill-payment-form.vue';
export default {
name: 'BillPaymentPublicView',
components: { MkPublicShell, BillPaymentForm },
methods: {
getForm() {
const form = this.$refs.page?.form || {};
return { ...form, subject: form.paymentNo || form.billNo || '' };
},
},
};
</script>
+42 -21
View File
@@ -10,38 +10,26 @@
:rows="rows"
:loading="loading"
:page="page"
:mk-enabled="mkEnabled"
@add="openCreate"
@view="openView"
@edit="openEdit"
@delete="handleDelete"
@flow="openFlow"
@submit="handleSubmit"
@mk-audit="handleMkAudit"
@approve="handleApprove"
@return="handleReturn"
@void="handleVoid"
@page-change="handlePageChange"
@size-change="handleSizeChange"
/>
<el-dialog v-model="flowDialog.visible" title="审批流程" width="620px" append-to-body>
<el-descriptions :column="1" border>
<el-descriptions-item label="单据号">
{{ flowDialog.row.paymentNo || '-' }}
</el-descriptions-item>
<el-descriptions-item label="单据状态">
{{ flowDialog.row.approvalStatusName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="当前节点">
{{ flowDialog.row.currentNode || '-' }}
</el-descriptions-item>
<el-descriptions-item label="当前处理人">
{{ flowDialog.row.currentProcessor || '-' }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</basic-container>
</template>
<script>
import * as api from '@/api/payment/billPayment';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import BillPaymentSearch from './components/bill-payment-search.vue';
import BillPaymentTable from './components/bill-payment-table.vue';
@@ -62,11 +50,18 @@ export default {
rows: [],
loading: false,
page: { current: 1, size: 10, total: 0 },
flowDialog: { visible: false, row: {} },
mkEnabled: false,
};
},
mounted() {
this.loadTable();
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
},
methods: {
unwrapData(response) {
@@ -125,9 +120,35 @@ export default {
if (this.rows.length === 1 && this.page.current > 1) this.page.current -= 1;
this.loadTable();
},
openFlow(row) {
this.flowDialog.row = row;
this.flowDialog.visible = true;
async handleSubmit(row) {
const previousApprovalStatus = row.approvalStatus || '';
await api.submit({ id: row.id });
await submitMkApprovalFlow({
bizType: 'bill-payment',
bizId: row.id,
subjectName: row.paymentNo || row.billNo || '',
approvalStatus: previousApprovalStatus,
});
this.$message.success('提交成功');
this.loadTable();
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning('汇票付款ID为空,无法打开审核页');
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
async handleApprove(row) {
await api.approve({ id: row.id });
@@ -50,19 +50,40 @@
>编辑</el-link
>
<el-link
v-if="row.approvalStatus !== 'draft'"
v-if="
['draft', 'returned'].includes(row.approvalStatus) &&
hasPermission('bill_payment_submit')
"
type="primary"
@click="$emit('flow', row)"
@click="$emit('submit', row)"
>提交</el-link
>
<el-link
v-if="
mkEnabled &&
row.approvalStatus === 'reviewing' &&
hasPermission('bill_payment_approve')
"
type="primary"
@click="$emit('mk-audit', row)"
>流程</el-link
>
<el-link
v-if="row.approvalStatus === 'reviewing' && hasPermission('bill_payment_approve')"
v-if="
!mkEnabled &&
row.approvalStatus === 'reviewing' &&
hasPermission('bill_payment_approve')
"
type="primary"
@click="$emit('approve', row)"
>通过</el-link
>
<el-link
v-if="row.approvalStatus === 'reviewing' && hasPermission('bill_payment_approve')"
v-if="
!mkEnabled &&
row.approvalStatus === 'reviewing' &&
hasPermission('bill_payment_approve')
"
type="danger"
@click="$emit('return', row)"
>驳回</el-link
@@ -94,6 +115,7 @@
<script>
import { mapGetters } from 'vuex';
import { billPaymentTableColumns } from '@/option/payment/billPayment';
import { isAdministrator } from '@/utils/auth';
export default {
name: 'BillPaymentTable',
@@ -101,13 +123,15 @@ export default {
rows: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
page: { type: Object, required: true },
mkEnabled: { type: Boolean, default: false },
},
emits: [
'add',
'view',
'edit',
'delete',
'flow',
'submit',
'mk-audit',
'approve',
'return',
'void',
@@ -120,7 +144,7 @@ export default {
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
return String(this.userInfo?.authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
},
methods: {
+33 -8
View File
@@ -405,7 +405,7 @@
</el-form-item>
</section-card>
<div class="invoice-form-page__actions">
<div v-if="!isPublicViewPage" class="invoice-form-page__actions">
<el-button @click="goBack">取消</el-button>
<el-button v-if="!readonly" type="primary" plain @click="syncReferenceData">同步</el-button>
<el-button v-if="!readonly && canSave" type="primary" plain @click="saveDraft"
@@ -491,6 +491,8 @@ import { getList as getReceivablePayableDetailList } from '@/api/settlement/rece
import { getDictionary } from '@/api/system/dictbiz';
import { invoiceApplicationDetailColumns } from '@/option/payment/invoiceApplication';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { getMkPublicDetail } from '@/api/mk-process';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
let localId = 0;
const emptyLine = () => ({
@@ -584,7 +586,10 @@ export default {
return this.$route.query.id || '';
},
readonly() {
return this.$route.query.mode === 'view';
return this.$route.query.mode === 'view' || this.isPublicViewPage;
},
isPublicViewPage() {
return this.$route.path === '/payment/invoice-application/public-view';
},
canSave() {
const code = this.recordId ? 'invoice_application_edit' : 'invoice_application_add';
@@ -656,6 +661,10 @@ export default {
.filter(Boolean);
},
async initialize() {
if (this.isPublicViewPage) {
await this.loadPublicDetail();
return;
}
await Promise.all([this.loadInvoiceItems(), this.loadTransportTypeOptions()]);
const userInfo = this.$store.getters.userInfo || {};
if (!this.recordId) {
@@ -663,6 +672,21 @@ export default {
return;
}
const data = this.unwrapData(await api.getDetail(this.recordId));
this.applyDetail(data);
if (this.settlementIds.length) {
await Promise.all([this.loadReceiverInformation(), this.loadSettlementDetails(false)]);
}
if (this.readonly)
this.$nextTick(() => this.$refs.formRef?.clearValidate('departmentEmails'));
this.$nextTick(this.restoreDetailSelection);
},
async loadPublicDetail() {
if (!this.recordId) return;
const data = this.unwrapData(await getMkPublicDetail('invoice-application', this.recordId));
this.applyDetail(data);
this.$nextTick(() => this.$refs.formRef?.clearValidate('departmentEmails'));
},
applyDetail(data) {
this.form = {
...emptyForm(),
...data,
@@ -683,12 +707,6 @@ export default {
sheet.lines.forEach(line => this.syncInvoiceItemLine(line, false))
);
this.selectedDetailIds = (data.details || []).map(item => item.formalSettlementDetailId);
if (this.settlementIds.length) {
await Promise.all([this.loadReceiverInformation(), this.loadSettlementDetails(false)]);
}
if (this.readonly)
this.$nextTick(() => this.$refs.formRef?.clearValidate('departmentEmails'));
this.$nextTick(this.restoreDetailSelection);
},
async loadInvoiceItems() {
const data = this.unwrapData(await getInvoiceItems());
@@ -1183,7 +1201,14 @@ export default {
}
const saved = await this.saveDraft();
if (!saved) return;
const previousApprovalStatus = this.form.approvalStatus || '';
await api.submit({ id: this.form.id });
await submitMkApprovalFlow({
bizType: 'invoice-application',
bizId: this.form.id,
subjectName: this.form.applicationNo || '',
approvalStatus: previousApprovalStatus,
});
this.$message.success('提交成功');
this.goBack();
},
@@ -0,0 +1,21 @@
<template>
<mk-public-shell biz-type="invoice-application" :get-form="getForm">
<invoice-application-form ref="page" />
</mk-public-shell>
</template>
<script>
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
import InvoiceApplicationForm from '@/views/payment/invoice-application-form.vue';
export default {
name: 'InvoiceApplicationPublicView',
components: { MkPublicShell, InvoiceApplicationForm },
methods: {
getForm() {
const form = this.$refs.page?.form || {};
return { ...form, subject: form.applicationNo || form.projectName || '' };
},
},
};
</script>
+47 -67
View File
@@ -152,14 +152,20 @@
>提交</el-link
>
<el-link
v-if="hasPermission('invoice_application_view') && row.approvalStatus !== 'draft'"
v-if="
mkEnabled &&
hasPermission('invoice_application_approve') &&
row.approvalStatus === 'reviewing'
"
type="primary"
@click="openFlow(row)"
@click="handleMkAudit(row)"
>流程</el-link
>
<el-link
v-if="
hasPermission('invoice_application_approve') && row.approvalStatus === 'reviewing'
!mkEnabled &&
hasPermission('invoice_application_approve') &&
row.approvalStatus === 'reviewing'
"
type="primary"
@click="handleApprove(row)"
@@ -167,7 +173,9 @@
>
<el-link
v-if="
hasPermission('invoice_application_approve') && row.approvalStatus === 'reviewing'
!mkEnabled &&
hasPermission('invoice_application_approve') &&
row.approvalStatus === 'reviewing'
"
type="danger"
@click="handleReturn(row)"
@@ -198,42 +206,6 @@
/>
</div>
</section>
<el-dialog v-model="flowDialog.visible" title="审批流程" width="900px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="开票申请单号">
{{ flowDialog.row.applicationNo || '-' }}
</el-descriptions-item>
<el-descriptions-item label="状态">
{{ flowDialog.row.approvalStatusName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="当前节点">
{{ flowDialog.row.currentNode || '-' }}
</el-descriptions-item>
<el-descriptions-item label="当前处理人">
{{ flowDialog.row.currentProcessor || '-' }}
</el-descriptions-item>
</el-descriptions>
<el-table
v-loading="flowDialog.loading"
:data="flowDialog.records"
border
class="invoice-page__flow-table"
>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="actionName" label="操作" min-width="110" align="center" />
<el-table-column label="原状态" min-width="100" align="center">
<template #default="{ row }">{{ statusName(row.fromStatus) }}</template>
</el-table-column>
<el-table-column label="新状态" min-width="100" align="center">
<template #default="{ row }">{{ statusName(row.toStatus) }}</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作人" min-width="110" align="center" />
<el-table-column prop="createTime" label="操作时间" min-width="170" align="center" />
<el-table-column prop="reason" label="原因" min-width="160" show-overflow-tooltip />
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" />
</el-table>
</el-dialog>
</basic-container>
</template>
@@ -245,6 +217,8 @@ import * as api from '@/api/payment/invoiceApplication';
import * as formalSettlementApi from '@/api/settlement/formalSettlement';
import { invoiceApplicationTableColumns } from '@/option/payment/invoiceApplication';
import organizationSearch from '@/mixins/organization-search';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
const emptyQuery = () => ({
applicationNo: '',
@@ -273,7 +247,7 @@ export default {
loading: false,
batchSyncing: false,
page: { current: 1, size: 10, total: 0 },
flowDialog: { visible: false, loading: false, row: {}, records: [] },
mkEnabled: false,
};
},
computed: {
@@ -282,6 +256,13 @@ export default {
mounted() {
this.loadOrganizationOptions();
this.loadTable();
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
},
methods: {
hasPermission(code) {
@@ -358,19 +339,6 @@ export default {
this.$message.error('正式结算单详情加载失败');
}
},
async openFlow(row) {
this.flowDialog.row = { ...row };
this.flowDialog.records = [];
this.flowDialog.visible = true;
this.flowDialog.loading = true;
try {
const detail = this.unwrapData(await api.getDetail(row.id));
this.flowDialog.row = { ...row, ...detail };
this.flowDialog.records = detail.records || [];
} finally {
this.flowDialog.loading = false;
}
},
async handleDelete(row) {
await this.$confirm('确认删除该开票申请?', '提示', { type: 'warning' });
await api.remove(row.id);
@@ -378,10 +346,35 @@ export default {
this.loadTable();
},
async handleSubmit(row) {
const previousApprovalStatus = row.approvalStatus || '';
await api.submit({ id: row.id });
await submitMkApprovalFlow({
bizType: 'invoice-application',
bizId: row.id,
subjectName: row.applicationNo || '',
approvalStatus: previousApprovalStatus,
});
this.$message.success('提交成功');
this.loadTable();
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning('开票申请ID为空,无法打开审核页');
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
async handleApprove(row) {
await this.$confirm('确认审批通过该开票申请?', '提示', { type: 'warning' });
await api.approve({ id: row.id });
@@ -455,19 +448,6 @@ export default {
''
);
},
statusName(status) {
return (
{
draft: '草稿',
reviewing: '审批中',
approved: '审批通过',
returned: '已驳回',
voided: '已作废',
}[status] ||
status ||
'-'
);
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
+27 -3
View File
@@ -134,7 +134,7 @@
</el-form-item>
</section-card>
<div class="receipt-form-page__actions">
<div v-if="!isPublicViewPage" class="receipt-form-page__actions">
<el-button @click="goBack">取消</el-button>
<el-button v-if="!readonly" type="primary" plain @click="syncReferenceData">同步</el-button>
<el-button v-if="!readonly && canSave" type="primary" plain @click="saveDraft"
@@ -212,6 +212,8 @@ import { mapGetters } from 'vuex';
import * as api from '@/api/payment/invoiceReceipt';
import KingdeeInvoicePoolDialog from '@/components/kingdee-invoice-pool-dialog/main.vue';
import VehicleAttachmentTable from '@/components/vehicle-attachment-table/main.vue';
import { getMkPublicDetail } from '@/api/mk-process';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
const emptyForm = () => ({
id: null,
@@ -274,7 +276,10 @@ export default {
return this.$route.query.id || '';
},
readonly() {
return this.$route.query.mode === 'view';
return this.$route.query.mode === 'view' || this.isPublicViewPage;
},
isPublicViewPage() {
return this.$route.path === '/payment/invoice-receipt/public-view';
},
canSave() {
const code = this.recordId ? 'invoice_receipt_edit' : 'invoice_receipt_add';
@@ -323,6 +328,10 @@ export default {
.filter(Boolean);
},
async initialize() {
if (this.isPublicViewPage) {
await this.loadPublicDetail();
return;
}
const userInfo = this.$store.getters.userInfo || {};
const userEmail = userInfo.email || '';
this.departmentEmailOptions = userEmail ? [userEmail] : [];
@@ -331,6 +340,15 @@ export default {
return;
}
const data = this.unwrapData(await api.getDetail(this.recordId));
this.applyDetail(data);
if (this.settlementIds.length) await this.loadReferenceInformation();
},
async loadPublicDetail() {
if (!this.recordId) return;
const data = this.unwrapData(await getMkPublicDetail('invoice-receipt', this.recordId));
this.applyDetail(data);
},
applyDetail(data) {
this.form = {
...emptyForm(),
...data,
@@ -342,7 +360,6 @@ export default {
})),
attachments: this.parse(data.attachmentsJson),
};
if (this.settlementIds.length) await this.loadReferenceInformation();
},
validateInvoice(rule, value, callback) {
if (!value) {
@@ -541,7 +558,14 @@ export default {
async submitForm() {
const saved = await this.saveDraft();
if (!saved) return;
const previousApprovalStatus = this.form.approvalStatus || '';
await api.submit({ id: this.form.id });
await submitMkApprovalFlow({
bizType: 'invoice-receipt',
bizId: this.form.id,
subjectName: this.form.invoiceNo || '',
approvalStatus: previousApprovalStatus,
});
this.$message.success('提交成功');
this.goBack();
},
@@ -0,0 +1,21 @@
<template>
<mk-public-shell biz-type="invoice-receipt" :get-form="getForm">
<invoice-receipt-form ref="page" />
</mk-public-shell>
</template>
<script>
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
import InvoiceReceiptForm from '@/views/payment/invoice-receipt-form.vue';
export default {
name: 'InvoiceReceiptPublicView',
components: { MkPublicShell, InvoiceReceiptForm },
methods: {
getForm() {
const form = this.$refs.page?.form || {};
return { ...form, subject: form.invoiceNo || form.projectName || '' };
},
},
};
</script>
+47 -67
View File
@@ -147,14 +147,20 @@
>提交</el-link
>
<el-link
v-if="hasPermission('invoice_receipt_view') && row.approvalStatus !== 'draft'"
v-if="
mkEnabled &&
hasPermission('invoice_receipt_approve') &&
row.approvalStatus === 'reviewing'
"
type="primary"
@click="openFlow(row)"
@click="handleMkAudit(row)"
>流程</el-link
>
<el-link
v-if="
hasPermission('invoice_receipt_approve') && row.approvalStatus === 'reviewing'
!mkEnabled &&
hasPermission('invoice_receipt_approve') &&
row.approvalStatus === 'reviewing'
"
type="primary"
@click="handleApprove(row)"
@@ -162,7 +168,9 @@
>
<el-link
v-if="
hasPermission('invoice_receipt_approve') && row.approvalStatus === 'reviewing'
!mkEnabled &&
hasPermission('invoice_receipt_approve') &&
row.approvalStatus === 'reviewing'
"
type="danger"
@click="handleReturn(row)"
@@ -191,42 +199,6 @@
/>
</div>
</section>
<el-dialog v-model="flowDialog.visible" title="审批流程" width="900px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="发票号码">
{{ flowDialog.row.invoiceNo || '-' }}
</el-descriptions-item>
<el-descriptions-item label="审核状态">
{{ flowDialog.row.approvalStatusName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="当前节点">
{{ flowDialog.row.currentNode || '-' }}
</el-descriptions-item>
<el-descriptions-item label="当前处理人">
{{ flowDialog.row.currentProcessor || '-' }}
</el-descriptions-item>
</el-descriptions>
<el-table
v-loading="flowDialog.loading"
:data="flowDialog.records"
border
class="receipt-page__flow-table"
>
<el-table-column type="index" label="序号" width="64" align="center" />
<el-table-column prop="actionName" label="操作" min-width="110" align="center" />
<el-table-column label="原状态" min-width="100" align="center">
<template #default="{ row }">{{ statusName(row.fromStatus) }}</template>
</el-table-column>
<el-table-column label="新状态" min-width="100" align="center">
<template #default="{ row }">{{ statusName(row.toStatus) }}</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作人" min-width="110" align="center" />
<el-table-column prop="createTime" label="操作时间" min-width="170" align="center" />
<el-table-column prop="reason" label="原因" min-width="160" show-overflow-tooltip />
<el-table-column prop="kingdeeBillNo" label="金蝶单据号" min-width="150" />
</el-table>
</el-dialog>
</basic-container>
</template>
@@ -237,6 +209,8 @@ import * as XLSX from 'xlsx';
import * as api from '@/api/payment/invoiceReceipt';
import { invoiceReceiptTableColumns } from '@/option/payment/invoiceReceipt';
import organizationSearch from '@/mixins/organization-search';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
const emptyQuery = () => ({
invoiceNo: '',
@@ -266,7 +240,7 @@ export default {
loading: false,
pushLoading: false,
page: { current: 1, size: 10, total: 0 },
flowDialog: { visible: false, loading: false, row: {}, records: [] },
mkEnabled: false,
};
},
computed: {
@@ -275,6 +249,13 @@ export default {
mounted() {
this.loadOrganizationOptions();
this.loadTable();
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
},
methods: {
hasPermission(code) {
@@ -323,19 +304,6 @@ export default {
query: { mode: 'view', id: row.id },
});
},
async openFlow(row) {
this.flowDialog.row = { ...row };
this.flowDialog.records = [];
this.flowDialog.visible = true;
this.flowDialog.loading = true;
try {
const detail = this.unwrapData(await api.getDetail(row.id));
this.flowDialog.row = { ...row, ...detail };
this.flowDialog.records = detail.records || [];
} finally {
this.flowDialog.loading = false;
}
},
async handleDelete(row) {
await this.$confirm('确认删除该收票登记?', '提示', { type: 'warning' });
await api.remove(row.id);
@@ -343,10 +311,35 @@ export default {
this.loadTable();
},
async handleSubmit(row) {
const previousApprovalStatus = row.approvalStatus || '';
await api.submit({ id: row.id });
await submitMkApprovalFlow({
bizType: 'invoice-receipt',
bizId: row.id,
subjectName: row.invoiceNo || '',
approvalStatus: previousApprovalStatus,
});
this.$message.success('提交成功');
this.loadTable();
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning('收票登记ID为空,无法打开审核页');
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
async handleApprove(row) {
await this.$confirm('确认审批通过该收票登记?', '提示', { type: 'warning' });
await api.approve({ id: row.id });
@@ -418,19 +411,6 @@ export default {
''
);
},
statusName(status) {
return (
{
draft: '草稿',
reviewing: '审批中',
approved: '审批通过',
returned: '已驳回',
voided: '已作废',
}[status] ||
status ||
'-'
);
},
displayValue(value) {
return value === null || value === undefined || value === '' ? '-' : value;
},
@@ -2672,12 +2672,13 @@ export default {
// 提交前静默保存,避免同时弹出「保存成功」与材料缺失 toast
const saved = await this.saveDraft({ showSuccess: false });
if (!saved) return;
const previousApprovalStatus = this.form.approvalStatus || '';
await api.submit({ id: this.form.id });
await submitMkApprovalFlow({
bizType: 'payment-application',
formInstanceId: this.form.id,
bizId: this.form.id,
subjectName: this.form.paymentNo || '',
approvalStatus: this.form.approvalStatus || '',
approvalStatus: previousApprovalStatus,
});
this.$message.success('提交成功');
this.goBack();
+51 -33
View File
@@ -160,14 +160,29 @@
>编辑</el-link
>
<el-link
v-if="hasPermission('payment_application_submit') && row.approvalStatus === 'draft'"
v-if="
hasPermission('payment_application_submit') &&
['draft', 'returned'].includes(row.approvalStatus)
"
type="primary"
@click="handleSubmit(row)"
>提交</el-link
>
<el-link
v-if="
hasPermission('payment_application_approve') && row.approvalStatus === 'reviewing'
mkEnabled &&
hasPermission('payment_application_approve') &&
row.approvalStatus === 'reviewing'
"
type="primary"
@click="handleMkAudit(row)"
>流程</el-link
>
<el-link
v-if="
!mkEnabled &&
hasPermission('payment_application_approve') &&
row.approvalStatus === 'reviewing'
"
type="primary"
@click="handleApprove(row)"
@@ -175,18 +190,14 @@
>
<el-link
v-if="
hasPermission('payment_application_approve') && row.approvalStatus === 'reviewing'
!mkEnabled &&
hasPermission('payment_application_approve') &&
row.approvalStatus === 'reviewing'
"
type="danger"
@click="handleReturn(row)"
>驳回</el-link
>
<el-link
v-if="hasPermission('payment_application_view') && row.approvalStatus !== 'draft'"
type="primary"
@click="openFlow(row)"
>流程</el-link
>
<el-link
v-if="
hasPermission('payment_application_void') && row.approvalStatus === 'approved'
@@ -217,22 +228,6 @@
/>
</div>
</section>
<el-dialog v-model="flowDialog.visible" title="审批流程" width="620px" append-to-body>
<el-descriptions :column="1" border>
<el-descriptions-item label="付款申请单号">
{{ flowDialog.row.paymentNo || '-' }}
</el-descriptions-item>
<el-descriptions-item label="审核状态">
{{ flowDialog.row.approvalStatusName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="当前节点">
{{ flowDialog.row.currentNode || '-' }}
</el-descriptions-item>
<el-descriptions-item label="当前处理人">
{{ flowDialog.row.currentProcessor || '-' }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</basic-container>
</template>
@@ -242,7 +237,8 @@ import { mapGetters } from 'vuex';
import * as api from '@/api/payment/paymentApplication';
import { paymentApplicationTableColumns } from '@/option/payment/paymentApplication';
import organizationSearch from '@/mixins/organization-search';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
const emptyQuery = () => ({
paymentNo: '',
@@ -276,7 +272,7 @@ export default {
syncLoading: false,
resultSyncing: false,
page: { current: 1, size: 10, total: 0 },
flowDialog: { visible: false, row: {} },
mkEnabled: false,
};
},
computed: {
@@ -288,6 +284,13 @@ export default {
mounted() {
this.loadOrganizationOptions();
this.loadTable();
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
},
methods: {
hasPermission(code) {
@@ -372,12 +375,13 @@ export default {
this.loadTable();
},
async handleSubmit(row) {
const previousApprovalStatus = row.approvalStatus || '';
await api.submit({ id: row.id });
await submitMkApprovalFlow({
bizType: 'payment-application',
formInstanceId: row.id,
bizId: row.id,
subjectName: row.paymentNo || '',
approvalStatus: row.approvalStatus || '',
approvalStatus: previousApprovalStatus,
});
this.$message.success('提交成功');
this.loadTable();
@@ -387,16 +391,30 @@ export default {
this.$message.success('审批通过');
this.loadTable();
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning('付款申请ID为空,无法打开审核页');
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
async handleReturn(row) {
const { value } = await this.$prompt('请输入驳回原因', '审批驳回');
await api.returnBill({ id: row.id, reason: value });
this.$message.success('已驳回');
this.loadTable();
},
openFlow(row) {
this.flowDialog.row = row;
this.flowDialog.visible = true;
},
async handleVoid(row) {
const { value } = await this.$prompt('请输入作废原因', '作废付款申请');
await api.voidBill({ id: row.id, reason: value });
+2 -4
View File
@@ -157,6 +157,7 @@ import { mapGetters } from 'vuex';
import * as api from '@/api/payment/receiptClaimRecord';
import { receiptClaimRecordTableColumns } from '@/option/payment/receiptClaimRecord';
import SectionCard from '@/components/section-card/main.vue';
import { isAdministrator } from '@/utils/auth';
const emptyQuery = () => ({
receiptNoticeNo: '',
@@ -195,10 +196,7 @@ export default {
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo?.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
},
mounted() {
+2 -4
View File
@@ -271,6 +271,7 @@ import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import * as api from '@/api/payment/receiptFlow';
import { receiptFlowTableColumns } from '@/option/payment/receiptFlow';
import { isAdministrator } from '@/utils/auth';
const emptyQuery = () => ({
receiptNoticeNo: '',
@@ -312,10 +313,7 @@ export default {
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo?.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
},
mounted() {
+3 -3
View File
@@ -26,13 +26,13 @@
<el-link
type="primary"
@click.stop="handleDesign(scope.row.name)"
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
>设计
</el-link>
<el-link
type="primary"
@click.stop="handlePreview(scope.row.name)"
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
>预览
</el-link>
</template>
@@ -97,7 +97,7 @@ export default {
};
},
computed: {
...mapGetters(['userInfo', 'permission']),
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
permissionList() {
return {
addBtn: false,
+2 -2
View File
@@ -33,7 +33,7 @@
<template #menu="scope">
<el-link
type="primary"
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
@click="handleDebug(scope.row)"
>调试
</el-link>
@@ -292,7 +292,7 @@ export default {
},
},
computed: {
...mapGetters(['userInfo', 'permission']),
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
permissionList() {
return {
addBtn: this.validData(this.permission.oss_add),
+2 -2
View File
@@ -33,7 +33,7 @@
<template #menu="scope">
<el-link
type="primary"
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
@click="handleDebug(scope.row)"
>调试
</el-link>
@@ -313,7 +313,7 @@ export default {
},
},
computed: {
...mapGetters(['userInfo', 'permission']),
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
permissionList() {
return {
addBtn: this.validData(this.permission.sms_add, false),
@@ -136,6 +136,7 @@ export default {
syncLoading: { type: Boolean, default: false },
page: { type: Object, required: true },
hasPermission: { type: Function, required: true },
mkEnabled: { type: Boolean, default: false },
},
emits: [
'create',
@@ -195,7 +196,10 @@ export default {
actions.push({ type: 'delete', label: '删除', danger: true });
if (this.hasPermission('formal_settlement_submit') && this.isEditable(row))
actions.push({ type: 'submit', label: '提交' });
if (this.hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing') {
if (this.mkEnabled && this.hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing') {
actions.push({ type: 'mkAudit', label: '流程' });
}
if (!this.mkEnabled && this.hasPermission('formal_settlement_approve') && row.approvalStatus === 'reviewing') {
actions.push({ type: 'approve', label: '通过' });
actions.push({ type: 'return', label: '驳回', danger: true });
}
@@ -966,6 +966,7 @@ import {
import { downloadFileByUrl } from '@/utils/util';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import * as XLSX from 'xlsx';
import { isAdministrator } from '@/utils/auth';
export default {
name: 'PreSettlementEditor',
@@ -1164,10 +1165,7 @@ export default {
return !this.readonly && ['draft', 'returned'].includes(this.form.approvalStatus);
},
isAdmin() {
const authority = this.userInfo?.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
canAdjustDetail() {
return this.editable && this.hasPermission('pre_settlement_adjust');
@@ -1611,12 +1609,13 @@ export default {
await this.loadDetail();
await this.persistPendingAdjustments();
if (shouldSubmit) {
const previousApprovalStatus = this.form.approvalStatus || '';
await submit({ id: this.form.id });
await submitMkApprovalFlow({
bizType: 'pre-settlement',
formInstanceId: this.form.id,
bizId: this.form.id,
subjectName: this.form.preSettlementNo || this.form.contractName || '',
approvalStatus: this.form.approvalStatus || '',
approvalStatus: previousApprovalStatus,
});
this.$message.success('审批流程已发起');
this.visible = false;
@@ -127,6 +127,13 @@
</div>
<el-button @click="resetInternalQuery">重置</el-button>
<el-button type="primary" @click="internalFilterTick++">查询</el-button>
<el-button
type="primary"
plain
:disabled="!filteredInternalRows.length"
@click="exportInternalDetails"
>导出</el-button
>
</div>
<el-table
:data="filteredInternalRows"
@@ -616,6 +623,7 @@
<script>
import { mapGetters } from 'vuex';
import * as XLSX from 'xlsx';
import * as api from '@/api/settlement/transportReconciliation';
import * as formalSettlementApi from '@/api/settlement/formalSettlement';
import { getDictionary } from '@/api/system/dictbiz';
@@ -1807,6 +1815,63 @@ export default {
formatNumber(value) {
return Number(value || 0).toFixed(2);
},
formatExportMoney(value) {
if (value === null || value === undefined || value === '' || Number(value) === -1) {
return '';
}
return Number(value || 0).toFixed(2);
},
formatInternalExportCell(row, column) {
if (column.prop === 'matchResult') return this.matchName(row.matchResult);
if (column.prop === 'updateResult') return this.updateName(row.updateResult);
if (column.prop === 'matchedExternalLineNo') {
return this.hasMatchedExternal(row)
? this.formatMatchedExternalLineNo(row.matchedExternalLineNo)
: '';
}
if (column.prop === 'transportType') return this.transportTypeLabel(row.transportType);
if (column.prop === 'transportQuantity') {
return row.transportQuantity === null ||
row.transportQuantity === undefined ||
row.transportQuantity === ''
? ''
: Number(row.transportQuantity || 0).toFixed(2);
}
if (column.feeItemName) {
return this.formatExportMoney(this.getFeeItemAmount(row, column.feeItemName));
}
if (column.prop === 'mileage') {
return Number(row.mileage) === -1 ? '' : this.formatExportMoney(row.mileage);
}
if (column.prop === 'actualDepartureTime' || column.prop === 'actualCompletionTime') {
const text = this.formatDate(row[column.prop]);
return text === '-' ? '' : text;
}
if (column.money || column.number) return this.formatExportMoney(row[column.prop]);
const value = row[column.prop];
return value === null || value === undefined || value === '' ? '' : value;
},
exportInternalDetails() {
if (!this.filteredInternalRows.length) {
this.$message.warning('暂无可导出的内部账单明细');
return;
}
const rows = this.filteredInternalRows.map((row, index) => {
const result = { 序号: index + 1 };
this.internalTableColumns.forEach(column => {
result[column.label] = this.formatInternalExportCell(row, column);
});
return result;
});
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(rows), '内部账单明细');
XLSX.writeFile(
workbook,
`${this.form.reconciliationNo || '运输对账'}内部账单明细${this.$dayjs().format(
'YYYY-MM-DD HH-mm-ss'
)}.xlsx`
);
},
},
};
</script>
+34 -3
View File
@@ -65,6 +65,7 @@
:page="page"
:has-permission="hasPermissionFn"
:sync-loading="syncLoading"
:mk-enabled="mkEnabled"
@create="openCreate"
@payment="openPaymentDialog"
@sync="handleSync"
@@ -84,6 +85,7 @@
:page="page"
:has-permission="hasPermissionFn"
:sync-loading="syncLoading"
:mk-enabled="mkEnabled"
@create="openCreate"
@payment="openPaymentDialog"
@sync="handleSync"
@@ -184,7 +186,8 @@ import organizationSearch from '@/mixins/organization-search';
import { formalSettlementTableColumns } from '@/option/settlement/formalSettlementTable';
import FormalSettlementEditor from './components/formal-settlement-editor.vue';
import FormalSettlementTablePanel from './components/formal-settlement-table-panel.vue';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { submitMkApprovalFlow, loadMkEnabled } from '@/utils/mk-approval';
import { getMKApprovalUrl } from '@/api/system/business-process';
const emptyQuery = () => ({
formalSettlementNo: '',
@@ -227,6 +230,7 @@ export default {
rows: [],
form: { appliedAmount: 0, remark: '' },
},
mkEnabled: false,
};
},
computed: {
@@ -250,6 +254,13 @@ export default {
this.loadOrganizationOptions();
this.loadTable();
this.openRouteDetail();
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
},
methods: {
hasPermission(code) {
@@ -293,6 +304,7 @@ export default {
edit: () => this.openEdit(row),
delete: () => this.handleDelete(row),
submit: () => this.handleSubmit(row),
mkAudit: () => this.handleMkAudit(row),
approve: () => this.handleApprove(row),
return: () => this.handleReturn(row),
void: () => this.handleVoid(row),
@@ -340,16 +352,35 @@ export default {
},
async handleSubmit(row) {
await this.$confirm('提交后来源预结算将保持锁定,确认提交?', '提示', { type: 'warning' });
const previousApprovalStatus = row.approvalStatus || '';
await api.submit({ id: row.id });
await submitMkApprovalFlow({
bizType: 'formal-settlement',
formInstanceId: row.id,
bizId: row.id,
subjectName: row.formalSettlementNo || '',
approvalStatus: row.approvalStatus || '',
approvalStatus: previousApprovalStatus,
});
this.$message.success('提交成功');
this.loadTable();
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning('正式结算单ID为空,无法打开审核页');
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
async handleApprove(row) {
await this.$confirm(
'审核通过后正式结算生效,金额只能通过结算调整单处理,确认通过?',
+68 -32
View File
@@ -172,21 +172,43 @@
删除
</el-link>
<el-link
v-if="hasPermission('pre_settlement_view') && row.approvalStatus !== 'draft'"
v-if="
hasPermission('pre_settlement_submit') &&
['draft', 'returned'].includes(row.approvalStatus)
"
type="primary"
@click="openFlow(row)"
@click="handleSubmit(row)"
>
提交
</el-link>
<el-link
v-if="
mkEnabled &&
hasPermission('pre_settlement_approve') &&
row.approvalStatus === 'reviewing'
"
type="primary"
@click="handleMkAudit(row)"
>
流程
</el-link>
<el-link
v-if="hasPermission('pre_settlement_approve') && row.approvalStatus === 'reviewing'"
v-if="
!mkEnabled &&
hasPermission('pre_settlement_approve') &&
row.approvalStatus === 'reviewing'
"
type="primary"
@click="handleApprove(row)"
>
通过
</el-link>
<el-link
v-if="hasPermission('pre_settlement_approve') && row.approvalStatus === 'reviewing'"
v-if="
!mkEnabled &&
hasPermission('pre_settlement_approve') &&
row.approvalStatus === 'reviewing'
"
type="danger"
@click="handleReturn(row)"
>
@@ -259,23 +281,6 @@
</el-button>
</template>
</el-dialog>
<el-dialog v-model="flowDialog.visible" title="审批流程" width="620px" append-to-body>
<el-descriptions :column="1" border>
<el-descriptions-item label="预结算单号">
{{ flowDialog.row.preSettlementNo || '-' }}
</el-descriptions-item>
<el-descriptions-item label="审核状态">
{{ flowDialog.row.approvalStatusName || '-' }}
</el-descriptions-item>
<el-descriptions-item label="当前节点">
{{ flowDialog.row.currentNode || '-' }}
</el-descriptions-item>
<el-descriptions-item label="当前处理人">
{{ flowDialog.row.currentProcessor || '-' }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</basic-container>
</template>
@@ -290,6 +295,7 @@ import {
getPrintTemplates,
remove,
returnBill,
submit,
voidBill,
} from '@/api/settlement/preSettlement';
import { preSettlementSearchFields } from '@/option/settlement/preSettlementSearch';
@@ -297,7 +303,10 @@ import organizationSearch from '@/mixins/organization-search';
import { preSettlementTableColumns } from '@/option/settlement/preSettlementTable';
import { createSettlementTransfer } from '@/utils/settlement-transfer';
import { downloadFile } from '@/utils/util';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import PreSettlementEditor from './components/pre-settlement-editor.vue';
import { isAdministrator } from '@/utils/auth';
const emptyQuery = () => ({
preSettlementNo: '',
@@ -345,19 +354,13 @@ export default {
templates: [],
template: '',
},
flowDialog: {
visible: false,
row: {},
},
mkEnabled: false,
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo?.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
visibleSearchFields() {
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
@@ -372,6 +375,13 @@ export default {
this.loadOrganizationOptions();
this.loadTable();
this.openRouteDetail();
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
})
.catch(() => {
this.mkEnabled = false;
});
},
methods: {
hasPermission(code) {
@@ -628,9 +638,35 @@ export default {
const { data } = await exportList(params);
downloadFile(data, `预结算单${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`);
},
openFlow(row) {
this.flowDialog.row = row;
this.flowDialog.visible = true;
async handleSubmit(row) {
const previousApprovalStatus = row.approvalStatus || '';
await submit({ id: row.id });
await submitMkApprovalFlow({
bizType: 'pre-settlement',
bizId: row.id,
subjectName: row.preSettlementNo || '',
approvalStatus: previousApprovalStatus,
});
this.$message.success('提交成功');
this.loadTable();
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning('预结算单ID为空,无法打开审核页');
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
async handleApprove(row) {
await this.$confirm('确认审批通过该预结算单?', '提示', { type: 'warning' });
@@ -172,6 +172,7 @@ import { settlementAdjustmentSearchFields } from '@/option/settlement/settlement
import organizationSearch from '@/mixins/organization-search';
import { settlementAdjustmentTableColumns } from '@/option/settlement/settlementAdjustmentTable';
import SettlementAdjustmentEditor from './components/settlement-adjustment-editor.vue';
import { isAdministrator } from '@/utils/auth';
const emptyQuery = () => ({
adjustmentNo: '',
@@ -207,7 +208,7 @@ export default {
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 4);
},
isAdmin() {
return String(this.userInfo?.authority || '').includes('admin');
return isAdministrator(this.userInfo);
},
},
created() {
+185 -38
View File
@@ -34,7 +34,7 @@
type="primary"
plain
:loading="oaSyncLoading"
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
@click="handleOaOrgSync"
>自动同步组织
</el-button>
@@ -43,12 +43,12 @@
<el-link
type="primary"
@click.stop="handleAdd(scope.row, scope.index)"
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
>新增子项
</el-link>
</template>
<template #deptCategory="{ row }">
<el-tag>{{ getDeptCategoryLabel(row.deptCategory) }}</el-tag>
<el-tag>{{ row.deptCategoryName || getDeptCategoryLabel(row.deptCategory) }}</el-tag>
</template>
<template #status="{ row }">
<el-tag :type="Number(row.status) === 1 ? 'success' : 'info'" class="status-text">
@@ -181,18 +181,21 @@ import {
clearNonTopDept,
} from '@/api/system/dept';
import { getLeaderList } from '@/api/system/user';
import { getDictionary } from '@/api/system/dictbiz';
import { getList as getCustomerArchiveList } from '@/api/vehicle/customer-archive';
import { mapGetters } from 'vuex';
import website from '@/config/website';
import func from '@/utils/func';
const escapeRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const ORGANIZATION_TYPE_DICT_CODE = 'organization_type';
export default {
data() {
return {
form: {},
leaderOptions: [],
organizationTypeOptions: [],
parentDeptCode: '',
parentDept: null,
carrierDialogVisible: false,
@@ -315,6 +318,31 @@ export default {
},
],
},
{
label: '金蝶部门代码',
prop: 'kingdeeDeptCode',
minWidth: 140,
order: 65,
maxlength: 64,
showWordLimit: true,
rules: [
{
validator: (rule, value, callback) => {
const code = String(value || '').trim();
if (!code) {
callback();
return;
}
if (code.length > 64) {
callback(new Error('金蝶部门代码不能超过64个字符'));
return;
}
callback();
},
trigger: 'blur',
},
],
},
{
label: '组织简称',
prop: 'shortName',
@@ -336,14 +364,7 @@ export default {
{
label: '组织类型',
type: 'select',
dicData: [
{ label: '一级集团', value: 1 },
{ label: '二级事业部', value: 2 },
{ label: '部门', value: 3 },
{ label: '三级分公司', value: 4 },
{ label: '物流园区', value: 5 },
{ label: '承运商', value: 6 },
],
dicData: [],
dataType: 'number',
minWidth: 120,
order: 90,
@@ -486,9 +507,10 @@ export default {
// ignore storage errors
}
this.loadLeaderOptions();
this.loadOrganizationTypeOptions();
},
computed: {
...mapGetters(['userInfo', 'permission']),
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
permissionList() {
return {
addBtn: this.validData(this.permission.dept_add, false),
@@ -725,38 +747,154 @@ export default {
});
});
},
loadOrganizationTypeOptions() {
return getDictionary({ code: ORGANIZATION_TYPE_DICT_CODE }).then(res => {
const list = res.data.data || [];
this.organizationTypeOptions = list
.map(item => this.normalizeOrganizationTypeOption(item))
.filter(Boolean);
this.resetDeptCategoryOptions();
if (list.length && !this.organizationTypeOptions.length) {
this.$message.warning('组织类型字典 dictKey 须为数字 1~6,当前无法解析,请检查业务字典配置');
}
// 字典异步返回后触发表格组织类型标签重绘
if (Array.isArray(this.data) && this.data.length) {
this.data = [...this.data];
}
});
},
/**
* 业务字典项转下拉选项。后端 deptCategory 为 Integer,value 必须始终为数字。
* dictKey 为纯数字时直接使用;若配成中文则回退用 sort(1~6)作为层级码。
*/
normalizeOrganizationTypeOption(item) {
const label = item?.dictValue ?? item?.label;
if (label === undefined || label === null || String(label).trim() === '') {
return null;
}
const rawKey = item?.dictKey ?? item?.value;
const keyText = rawKey === undefined || rawKey === null ? '' : String(rawKey).trim();
const keyNumber = Number(keyText);
const isPureNumber = keyText !== '' && !Number.isNaN(keyNumber) && String(keyNumber) === keyText;
if (isPureNumber) {
return {
label: String(label),
value: keyNumber,
};
}
// 兼容 dictKey 误配成中文:用排序号作为 1~6 层级码,保证下拉有数据且可提交 Integer
const sortNumber = Number(item?.sort);
if (!Number.isNaN(sortNumber) && sortNumber >= 1 && sortNumber <= 6) {
return {
label: String(label),
value: sortNumber,
};
}
return null;
},
/**
* 提交前将组织类型规范为 Integer;空状态字段去掉,避免 "" 反序列化失败。
*/
prepareDeptSubmitRow(row) {
const category = this.resolveDeptCategoryValue(row.deptCategory);
if (category === null) {
return false;
}
row.deptCategory = category;
if (row.status === '' || row.status === null || row.status === undefined) {
delete row.status;
} else {
row.status = Number(row.status);
}
return true;
},
resolveDeptCategoryValue(value) {
if (value === null || value === undefined || value === '') {
this.$message.warning('请选择组织类型');
return null;
}
const asNumber = Number(value);
if (!Number.isNaN(asNumber) && String(asNumber) === String(value).trim()) {
return asNumber;
}
// 容错:表单里残留了中文标签时,按字典项 label 反查数字编码
const matched = this.organizationTypeOptions.find(
item => String(item.label) === String(value) || this.isSameDeptCategory(item.value, value)
);
if (matched && typeof matched.value === 'number' && !Number.isNaN(matched.value)) {
return matched.value;
}
this.$message.warning('组织类型无效,请重新选择(字典键须为数字 1~6)');
return null;
},
resetDeptCategoryOptions() {
const categoryColumn = this.findColumn(this.option.column, 'deptCategory');
if (!categoryColumn) {
return;
}
categoryColumn.dicData = [...this.organizationTypeOptions];
},
isSameDeptCategory(left, right) {
if (
left === null ||
left === undefined ||
left === '' ||
right === null ||
right === undefined ||
right === ''
) {
return false;
}
if (String(left) === String(right)) {
return true;
}
const leftNumber = Number(left);
const rightNumber = Number(right);
return !Number.isNaN(leftNumber) && !Number.isNaN(rightNumber) && leftNumber === rightNumber;
},
isCarrierDeptCategory(value) {
if (Number(value) === 6) {
return true;
}
const matched = this.organizationTypeOptions.find(item => this.isSameDeptCategory(item.value, value));
const text = matched?.label || value;
return String(text).includes('承运商');
},
filterOrganizationTypeOptions(values) {
const valueSet = new Set(values.map(item => String(item)));
const filtered = this.organizationTypeOptions.filter(item => valueSet.has(String(item.value)));
// 字典 key 非数字时无法按历史层级码过滤,回退展示全部,避免下拉无选项
return filtered.length ? filtered : [...this.organizationTypeOptions];
},
updateDeptCategoryOptions() {
const categoryColumn = this.findColumn(this.option.column, 'deptCategory');
const parent = this.parentDept;
if (!parent) return;
if (!parent || !categoryColumn) {
return;
}
let dicData = [];
if (this.isRootDept(parent)) {
dicData =
parent.deptName === '外部组织'
? [{ label: '承运商', value: 6 }]
: [{ label: '集团', value: 1 }];
? this.filterOrganizationTypeOptions([6])
: this.filterOrganizationTypeOptions([1]);
} else if (Number(parent.deptCategory) === 1) {
dicData = [
{ label: '事业部', value: 2 },
{ label: '部门', value: 3 },
];
dicData = this.filterOrganizationTypeOptions([2, 3]);
} else if ([2, 3].includes(Number(parent.deptCategory))) {
dicData = [
{ label: '分公司', value: 4 },
{ label: '物流园区', value: 5 },
{ label: '承运商', value: 6 },
];
dicData = this.filterOrganizationTypeOptions([4, 5, 6]);
} else {
dicData = [...this.organizationTypeOptions];
}
categoryColumn.dicData = dicData;
if (!dicData.some(item => item.value === Number(this.form.deptCategory))) {
if (!dicData.some(item => this.isSameDeptCategory(item.value, this.form.deptCategory))) {
this.form.deptCategory = dicData[0]?.value;
}
if (Number(this.form.deptCategory) === 6) {
if (this.isCarrierDeptCategory(this.form.deptCategory)) {
this.openCarrierDialog();
}
},
handleDeptCategoryChange(value) {
if (Number(value) === 6) {
if (this.isCarrierDeptCategory(value)) {
this.openCarrierDialog();
}
},
@@ -816,15 +954,13 @@ export default {
});
},
getDeptCategoryLabel(deptCategory) {
const categoryMap = {
1: '集团',
2: '事业部',
3: '部门',
4: '分公司',
5: '物流园区',
6: '承运商',
};
return categoryMap[Number(deptCategory)] || '-';
if (deptCategory === null || deptCategory === undefined || deptCategory === '') {
return '-';
}
const matched = this.organizationTypeOptions.find(item =>
this.isSameDeptCategory(item.value, deptCategory)
);
return matched?.label || String(deptCategory);
},
getLeaderValue(leaderId, field) {
// func.split 空值时返回 '',需兼容为数组,避免表格 formatter 渲染崩溃导致列错位
@@ -861,9 +997,14 @@ export default {
rowSave(row, done, loading) {
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
row.deptCode = String(row.deptCode || '').trim();
row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim();
row.leaderId = func.join(row.leaderId);
this.normalizeTopParent(row);
if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) {
if (!this.prepareDeptSubmitRow(row)) {
loading();
return;
}
if (this.isCarrierDeptCategory(row.deptCategory) && !row.carrierCustomerId) {
this.$message.warning('请选择承运商');
loading();
return;
@@ -891,9 +1032,14 @@ export default {
rowUpdate(row, index, done, loading) {
row.tenantId = row.tenantId || this.userInfo.tenantId || website.tenantId;
row.deptCode = String(row.deptCode || '').trim();
row.kingdeeDeptCode = String(row.kingdeeDeptCode || '').trim();
row.leaderId = func.join(row.leaderId);
this.normalizeTopParent(row);
if (Number(row.deptCategory) === 6 && !row.carrierCustomerId) {
if (!this.prepareDeptSubmitRow(row)) {
loading();
return;
}
if (this.isCarrierDeptCategory(row.deptCategory) && !row.carrierCustomerId) {
this.$message.warning('请选择承运商');
loading();
return;
@@ -1026,6 +1172,7 @@ export default {
const column = this.findColumn(this.option.column, 'parentId');
column.value = '';
column.addDisabled = false;
this.resetDeptCategoryOptions();
done();
},
currentChange(currentPage) {
+3 -3
View File
@@ -35,7 +35,7 @@
<el-link
type="primary"
@click.stop="handleRowClick(scope.row)"
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
>字典配置
</el-link>
</template>
@@ -81,7 +81,7 @@
<el-link
type="primary"
@click.stop="handleAdd(scope.row, scope.index)"
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
>新增子项
</el-link>
</template>
@@ -137,7 +137,7 @@ export default {
};
},
computed: {
...mapGetters(['userInfo', 'permission']),
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
permissionList() {
return {
addBtn: this.validData(this.permission.dict_add, false),
+3 -3
View File
@@ -35,7 +35,7 @@
<el-link
type="primary"
@click.stop="handleRowClick(scope.row)"
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
>字典配置
</el-link>
</template>
@@ -81,7 +81,7 @@
<el-link
type="primary"
@click.stop="handleAdd(scope.row, scope.index)"
v-if="userInfo.authority.includes('admin')"
v-if="isAdministrator"
>新增子项
</el-link>
</template>
@@ -137,7 +137,7 @@ export default {
};
},
computed: {
...mapGetters(['userInfo', 'permission']),
...mapGetters(['userInfo', 'permission', 'isAdministrator']),
permissionList() {
return {
addBtn: this.validData(this.permission.dictbiz_add, false),

Some files were not shown because too many files have changed in this diff Show More