Merge remote-tracking branch 'origin/master'
# Conflicts: # src/option/business/shipping-template.js # src/option/business/transport-plan.js # src/option/business/waybill-manage.js # src/views/business/components/business-crud-page.vue # src/views/business/components/master-order-detail.vue # src/views/business/components/master-order-editor.vue # src/views/business/components/waybill-import-dialog.vue
This commit is contained in:
@@ -0,0 +1,76 @@
|
|||||||
|
# 风险处置后端接口契约
|
||||||
|
|
||||||
|
> 当前仓库为前端工程,无 Java 后端源码。本文件提供 BladeX 后端落库与接口实现契约,路径需放入 `blade-transport` 服务。
|
||||||
|
|
||||||
|
## Controller
|
||||||
|
|
||||||
|
```java
|
||||||
|
@RestController
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RequestMapping("/risk-disposal")
|
||||||
|
@Api(value = "风险处置", tags = "风险处置")
|
||||||
|
public class RiskDisposalController {
|
||||||
|
|
||||||
|
private final IRiskDisposalService riskDisposalService;
|
||||||
|
|
||||||
|
@GetMapping("/list")
|
||||||
|
public R<IPage<RiskDisposalVO>> list(RiskDisposalQuery query, Query page) {
|
||||||
|
IPage<RiskDisposalVO> pages = riskDisposalService.selectRiskDisposalPage(Condition.getPage(page), query);
|
||||||
|
return R.data(pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/detail")
|
||||||
|
public R<RiskDisposalVO> detail(Long id) {
|
||||||
|
return R.data(riskDisposalService.detail(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/dispose")
|
||||||
|
public R<Boolean> dispose(@Valid @RequestBody RiskDisposalDTO dto) {
|
||||||
|
return R.status(riskDisposalService.dispose(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/batch-dispose")
|
||||||
|
public R<Boolean> batchDispose(@Valid @RequestBody RiskDisposalBatchDTO dto) {
|
||||||
|
return R.status(riskDisposalService.batchDispose(dto));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 字段约定
|
||||||
|
|
||||||
|
- `riskLevel`: `low` 低,`medium` 中,`high` 高
|
||||||
|
- `disposalStatus`: `pending` 待处理,`processed` 已处理,`ignored` 已忽略
|
||||||
|
- `disposalMethod`: `process` 处理,`ignore` 忽略
|
||||||
|
- 列表默认按 `create_time DESC` 返回
|
||||||
|
- `riskNo` 由后端生成:`RK + yyyyMMdd + 3位流水号`
|
||||||
|
|
||||||
|
## 接口
|
||||||
|
|
||||||
|
- `GET /blade-transport/risk-disposal/list`
|
||||||
|
- `GET /blade-transport/risk-disposal/detail?id=`
|
||||||
|
- `POST /blade-transport/risk-disposal/dispose`
|
||||||
|
- `POST /blade-transport/risk-disposal/batch-dispose`
|
||||||
|
|
||||||
|
## DTO
|
||||||
|
|
||||||
|
```java
|
||||||
|
@Data
|
||||||
|
public class RiskDisposalDTO {
|
||||||
|
@NotNull(message = "风险ID不能为空")
|
||||||
|
private Long id;
|
||||||
|
@NotBlank(message = "请选择处理方式")
|
||||||
|
private String disposalMethod;
|
||||||
|
@Length(max = 500, message = "处置说明不能超过500个字符")
|
||||||
|
private String disposalRemark;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class RiskDisposalBatchDTO {
|
||||||
|
@NotEmpty(message = "风险ID不能为空")
|
||||||
|
private List<Long> ids;
|
||||||
|
@NotBlank(message = "请选择处理方式")
|
||||||
|
private String disposalMethod;
|
||||||
|
@Length(max = 500, message = "处置说明不能超过500个字符")
|
||||||
|
private String disposalRemark;
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 在途追踪后端接口契约
|
||||||
|
|
||||||
|
## 接口
|
||||||
|
|
||||||
|
- `GET /blade-transport/track-tracking/map`
|
||||||
|
- `GET /blade-transport/track-tracking/ship`
|
||||||
|
|
||||||
|
## 建议返回
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"siteName": "XX园区A门",
|
||||||
|
"address": "南宁市邕宁区龙岗大道21号",
|
||||||
|
"coordinates": "108.348610;22.721864",
|
||||||
|
"records": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- `map` 用于轨迹查询
|
||||||
|
- `ship` 用于船舶查询
|
||||||
|
- 当前前端先做高德地图占位,后端可先只返回基础信息和轨迹点数组
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
-- 风险处置模块初始化脚本
|
||||||
|
-- 父菜单:在途管理
|
||||||
|
|
||||||
|
INSERT INTO `blade_menu` (
|
||||||
|
`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`,
|
||||||
|
`remark`, `tenant_id`, `create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`,
|
||||||
|
`is_deleted`
|
||||||
|
) VALUES (
|
||||||
|
'1710000000000000001', 0, 'transit_manage', '在途管理', 'transit_manage', '/transit', 'el-icon-truck',
|
||||||
|
20, 1, 1, 1, '在途管理父菜单', '000000', 1, 1, NOW(), 1, NOW(), 1, 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `blade_menu` (
|
||||||
|
`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`,
|
||||||
|
`remark`, `tenant_id`, `create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`,
|
||||||
|
`is_deleted`
|
||||||
|
) VALUES (
|
||||||
|
'1710000000000000002', '1710000000000000001', 'risk_disposal', '风险处置', 'risk_disposal',
|
||||||
|
'/transit/risk-disposal', 'el-icon-warning-outline', 20, 1, 1, 1, '风险处置页面',
|
||||||
|
'000000', 1, 1, NOW(), 1, NOW(), 1, 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `blade_menu` (
|
||||||
|
`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`,
|
||||||
|
`remark`, `tenant_id`, `create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`,
|
||||||
|
`is_deleted`
|
||||||
|
) VALUES
|
||||||
|
('1710000000000000003', '1710000000000000002', 'risk_disposal_query', '查询', 'risk_disposal_query', '', '', 10, 2, 1, 1, '风险处置查询权限', '000000', 1, 1, NOW(), 1, NOW(), 1, 0),
|
||||||
|
('1710000000000000004', '1710000000000000002', 'risk_disposal_process', '处理', 'risk_disposal_process', '', '', 20, 2, 1, 1, '风险处置处理权限', '000000', 1, 1, NOW(), 1, NOW(), 1, 0),
|
||||||
|
('1710000000000000005', '1710000000000000002', 'risk_disposal_ignore', '忽略', 'risk_disposal_ignore', '', '', 30, 2, 1, 1, '风险处置忽略权限', '000000', 1, 1, NOW(), 1, NOW(), 1, 0),
|
||||||
|
('1710000000000000006', '1710000000000000002', 'risk_disposal_batch_process', '批量处理', 'risk_disposal_batch_process', '', '', 40, 2, 1, 1, '风险处置批量处理权限', '000000', 1, 1, NOW(), 1, NOW(), 1, 0),
|
||||||
|
('1710000000000000007', '1710000000000000002', 'risk_disposal_batch_ignore', '批量忽略', 'risk_disposal_batch_ignore', '', '', 50, 2, 1, 1, '风险处置批量忽略权限', '000000', 1, 1, NOW(), 1, NOW(), 1, 0),
|
||||||
|
('1710000000000000008', '1710000000000000002', 'risk_disposal_view', '查看', 'risk_disposal_view', '', '', 60, 2, 1, 1, '风险处置查看权限', '000000', 1, 1, NOW(), 1, NOW(), 1, 0);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `tms_risk_disposal` (
|
||||||
|
`id` bigint NOT NULL COMMENT '主键',
|
||||||
|
`tenant_id` varchar(12) DEFAULT '000000' COMMENT '租户ID',
|
||||||
|
`risk_no` varchar(30) NOT NULL COMMENT '风险编号',
|
||||||
|
`rule_name` varchar(50) NOT NULL COMMENT '规则名称',
|
||||||
|
`waybill_id` bigint DEFAULT NULL COMMENT '运单ID',
|
||||||
|
`waybill_no` varchar(30) DEFAULT NULL COMMENT '运单号',
|
||||||
|
`project_id` bigint DEFAULT NULL COMMENT '项目ID',
|
||||||
|
`project_name` varchar(50) DEFAULT NULL COMMENT '项目名称',
|
||||||
|
`risk_level` varchar(20) NOT NULL COMMENT '风险等级:low/medium/high',
|
||||||
|
`transport_type` varchar(20) DEFAULT NULL COMMENT '运输类型',
|
||||||
|
`risk_description` varchar(500) DEFAULT NULL COMMENT '风险描述',
|
||||||
|
`trigger_time` datetime DEFAULT NULL COMMENT '触发时间',
|
||||||
|
`disposal_status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT '处理状态:pending/processed/ignored',
|
||||||
|
`disposal_method` varchar(20) DEFAULT NULL COMMENT '处理方式:process/ignore',
|
||||||
|
`disposal_remark` varchar(500) DEFAULT NULL COMMENT '处置说明',
|
||||||
|
`dispose_user` bigint DEFAULT NULL COMMENT '处置人',
|
||||||
|
`dispose_user_name` varchar(50) DEFAULT NULL COMMENT '处置人名称',
|
||||||
|
`dispose_time` datetime DEFAULT NULL COMMENT '处置时间',
|
||||||
|
`create_user` bigint DEFAULT NULL COMMENT '创建人',
|
||||||
|
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
|
||||||
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`update_user` bigint DEFAULT NULL COMMENT '更新人',
|
||||||
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`status` int DEFAULT 1 COMMENT '状态',
|
||||||
|
`is_deleted` int DEFAULT 0 COMMENT '是否已删除',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_tms_risk_disposal_no` (`risk_no`),
|
||||||
|
KEY `idx_tms_risk_disposal_status` (`disposal_status`),
|
||||||
|
KEY `idx_tms_risk_disposal_level` (`risk_level`),
|
||||||
|
KEY `idx_tms_risk_disposal_waybill` (`waybill_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='风险处置';
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- 在途追踪模块初始化脚本
|
||||||
|
|
||||||
|
INSERT INTO `blade_menu` (
|
||||||
|
`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`,
|
||||||
|
`remark`, `tenant_id`, `create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`,
|
||||||
|
`is_deleted`
|
||||||
|
) VALUES (
|
||||||
|
'1720000000000000001', 0, 'transit_manage', '在途管理', 'transit_manage', '/transit', 'el-icon-truck',
|
||||||
|
20, 1, 1, 1, '在途管理父菜单', '000000', 1, 1, NOW(), 1, NOW(), 1, 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `blade_menu` (
|
||||||
|
`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`,
|
||||||
|
`remark`, `tenant_id`, `create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`,
|
||||||
|
`is_deleted`
|
||||||
|
) VALUES (
|
||||||
|
'1720000000000000002', '1720000000000000001', 'track_tracking', '在途追踪', 'track_tracking',
|
||||||
|
'/transit/track-tracking', 'el-icon-position', 10, 1, 1, 1, '在途追踪页面',
|
||||||
|
'000000', 1, 1, NOW(), 1, NOW(), 1, 0
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO `blade_menu` (
|
||||||
|
`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`,
|
||||||
|
`remark`, `tenant_id`, `create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`,
|
||||||
|
`is_deleted`
|
||||||
|
) VALUES
|
||||||
|
('1720000000000000003', '1720000000000000002', 'track_tracking_query', '查询', 'track_tracking_query', '', '', 10, 2, 1, 1, '在途追踪查询权限', '000000', 1, 1, NOW(), 1, NOW(), 1, 0),
|
||||||
|
('1720000000000000004', '1720000000000000002', 'track_tracking_ship_query', '船舶查询', 'track_tracking_ship_query', '', '', 20, 2, 1, 1, '在途追踪船舶查询权限', '000000', 1, 1, NOW(), 1, NOW(), 1, 0),
|
||||||
|
('1720000000000000005', '1720000000000000002', 'track_tracking_refresh', '刷新', 'track_tracking_refresh', '', '', 30, 2, 1, 1, '在途追踪刷新权限', '000000', 1, 1, NOW(), 1, NOW(), 1, 0);
|
||||||
@@ -1,9 +1,24 @@
|
|||||||
|
import request from '@/axios';
|
||||||
import { createCrudApi } from './common';
|
import { createCrudApi } from './common';
|
||||||
|
|
||||||
const api = createCrudApi('/blade-transport/process-config');
|
const api = createCrudApi('/blade-transport/process-config');
|
||||||
|
|
||||||
export const getList = api.getList;
|
export const getList = api.getList;
|
||||||
export const getDetail = api.getDetail;
|
export const getDetail = (id, waybillId) =>
|
||||||
|
request({
|
||||||
|
url: '/blade-transport/process-config/detail',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
...(waybillId === undefined ? {} : { waybillId }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const getVoucherImages = waybillId =>
|
||||||
|
request({
|
||||||
|
url: '/blade-transport/process-config/voucher-images',
|
||||||
|
method: 'get',
|
||||||
|
params: { waybillId },
|
||||||
|
});
|
||||||
export const submit = api.submit;
|
export const submit = api.submit;
|
||||||
export const remove = api.remove;
|
export const remove = api.remove;
|
||||||
export const copy = api.copy;
|
export const copy = api.copy;
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import request from '@/axios';
|
||||||
|
|
||||||
|
const baseUrl = '/blade-transport/voucher-manage';
|
||||||
|
|
||||||
|
export const getList = (current, size, params) =>
|
||||||
|
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
||||||
|
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
||||||
|
export const submit = data => request({ url: `${baseUrl}/submit`, method: 'post', data });
|
||||||
|
export const createUploadDraft = data =>
|
||||||
|
request({ url: `${baseUrl}/upload-draft`, method: 'post', data });
|
||||||
|
export const completeUploadFile = data =>
|
||||||
|
request({ url: `${baseUrl}/complete-upload-file`, method: 'post', data });
|
||||||
|
export const remove = id => request({ url: `${baseUrl}/remove`, method: 'post', params: { id } });
|
||||||
|
export const getWaybillBatches = (current, size, params) =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/waybill-batches`,
|
||||||
|
method: 'get',
|
||||||
|
params: { current, size, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileTaskBaseUrl = '/blade-file/fileTask';
|
||||||
|
export const queryFileTask = md5 =>
|
||||||
|
request({ url: `${fileTaskBaseUrl}/queryFileTask`, method: 'get', params: { md5 } });
|
||||||
|
export const getFileTask = id =>
|
||||||
|
request({ url: `${fileTaskBaseUrl}/queryFileTask`, method: 'get', params: { id } });
|
||||||
|
export const createFileTask = data =>
|
||||||
|
request({ url: `${fileTaskBaseUrl}/createFileTask`, method: 'post', data });
|
||||||
|
export const getPartUploadUrl = (id, partNumber) =>
|
||||||
|
request({ url: `${fileTaskBaseUrl}/getUploadUrl`, method: 'get', params: { id, partNumber } });
|
||||||
|
export const updateFileTask = data =>
|
||||||
|
request({ url: `${fileTaskBaseUrl}/updateFileTask`, method: 'post', data });
|
||||||
|
export const pauseFileTask = ids =>
|
||||||
|
request({ url: `${fileTaskBaseUrl}/updatePaused`, method: 'post', data: { ids } });
|
||||||
|
export const resumeFileTask = id =>
|
||||||
|
request({ url: `${fileTaskBaseUrl}/resume`, method: 'post', params: { id } });
|
||||||
|
export const getFileTaskList = (current, size, params) =>
|
||||||
|
request({ url: `${fileTaskBaseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
||||||
|
export const getFileTaskUrl = id =>
|
||||||
|
request({ url: `${fileTaskBaseUrl}/file-url`, method: 'get', params: { id } });
|
||||||
@@ -13,6 +13,12 @@ export const cancel = api.cancel;
|
|||||||
export const reassign = api.reassign;
|
export const reassign = api.reassign;
|
||||||
export const complete = api.complete;
|
export const complete = api.complete;
|
||||||
export const batchComplete = api.batchComplete;
|
export const batchComplete = api.batchComplete;
|
||||||
|
export const changeRoute = data =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/change-route`,
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
export const roadLoading = ids =>
|
export const roadLoading = ids =>
|
||||||
request({
|
request({
|
||||||
url: `${baseUrl}/road-loading`,
|
url: `${baseUrl}/road-loading`,
|
||||||
@@ -26,7 +32,6 @@ export const getImportBatches = params => request({ url: `${baseUrl}/import-batc
|
|||||||
export const getImportDetails = params => request({ url: `${baseUrl}/import-batch/details`, method: 'get', params });
|
export const getImportDetails = params => request({ url: `${baseUrl}/import-batch/details`, method: 'get', params });
|
||||||
export const removeImportBatches = ids => request({ url: `${baseUrl}/import-batch/remove`, method: 'post', params: { ids } });
|
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 getImportOptions = () => request({ url: `${baseUrl}/import-batch/options`, method: 'get' });
|
||||||
export const getImportContracts = projectId => request({ url: `${baseUrl}/import-batch/contracts`, method: 'get', params: { projectId } });
|
|
||||||
export const exportImportTemplate = () => request({ url: `${baseUrl}/import-batch/export-template`, method: 'get', responseType: 'blob' });
|
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 saveImportDraft = data => request({ url: `${baseUrl}/import-batch/draft`, method: 'post', data });
|
||||||
export const confirmImport = data => request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data });
|
export const confirmImport = data => request({ url: `${baseUrl}/import-batch/confirm`, method: 'post', data });
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import request from '@/axios';
|
||||||
|
|
||||||
|
const baseUrl = '/blade-transport/receivable-payable-detail';
|
||||||
|
|
||||||
|
export const getList = (current, size, params) =>
|
||||||
|
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
||||||
|
|
||||||
|
export const getFeeDetail = id =>
|
||||||
|
request({ url: `${baseUrl}/fee-detail`, method: 'get', params: { id } });
|
||||||
|
|
||||||
|
export const getChangeRecords = (current, size, params) =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/change-records`,
|
||||||
|
method: 'get',
|
||||||
|
params: { current, size, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateFee = data =>
|
||||||
|
request({ url: `${baseUrl}/update-fee`, method: 'post', data });
|
||||||
|
|
||||||
|
export const transferSettlement = data =>
|
||||||
|
request({ url: `${baseUrl}/transfer-settlement`, method: 'post', data });
|
||||||
|
|
||||||
|
export const getTransferCandidates = (current, size, params) =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/transfer-candidates`,
|
||||||
|
method: 'get',
|
||||||
|
params: { current, size, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getGenerateWaybills = (current, size, params) =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/generate-waybills`,
|
||||||
|
method: 'get',
|
||||||
|
params: { current, size, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getGeneratePreview = (current, size, params) =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/generate-preview`,
|
||||||
|
method: 'get',
|
||||||
|
params: { current, size, ...params },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const generateFee = data =>
|
||||||
|
request({ url: `${baseUrl}/generate-fee`, method: 'post', data });
|
||||||
|
|
||||||
|
export const exportDetail = params =>
|
||||||
|
request({
|
||||||
|
url: `${baseUrl}/export-receivable-payable-detail`,
|
||||||
|
method: 'get',
|
||||||
|
params,
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import request from '@/axios';
|
||||||
|
|
||||||
|
const baseUrl = '/blade-transport/exception-disposal';
|
||||||
|
|
||||||
|
export const getList = (current, size, params) =>
|
||||||
|
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
||||||
|
|
||||||
|
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
||||||
|
|
||||||
|
export const follow = data => request({ url: `${baseUrl}/follow`, method: 'post', data });
|
||||||
|
|
||||||
|
export const complete = data => request({ url: `${baseUrl}/complete`, method: 'post', data });
|
||||||
|
|
||||||
|
export const batchComplete = ids =>
|
||||||
|
request({ url: `${baseUrl}/batch-complete`, method: 'post', params: { ids } });
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import request from '@/axios';
|
||||||
|
|
||||||
|
const baseUrl = '/blade-transport/risk-disposal';
|
||||||
|
|
||||||
|
export const getList = (current, size, params) =>
|
||||||
|
request({ url: `${baseUrl}/list`, method: 'get', params: { current, size, ...params } });
|
||||||
|
|
||||||
|
export const getDetail = id => request({ url: `${baseUrl}/detail`, method: 'get', params: { id } });
|
||||||
|
|
||||||
|
export const dispose = data => request({ url: `${baseUrl}/dispose`, method: 'post', data });
|
||||||
|
|
||||||
|
export const batchDispose = data =>
|
||||||
|
request({ url: `${baseUrl}/batch-dispose`, method: 'post', data });
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import request from '@/axios';
|
||||||
|
|
||||||
|
const baseUrl = '/blade-transport/track-tracking';
|
||||||
|
|
||||||
|
export const getTrackingMap = params =>
|
||||||
|
request({ url: `${baseUrl}/map`, method: 'get', params });
|
||||||
|
|
||||||
|
export const getShipTracking = params =>
|
||||||
|
request({ url: `${baseUrl}/ship`, method: 'get', params });
|
||||||
@@ -122,8 +122,7 @@ export const createOption = () => ({
|
|||||||
maxlength: 20,
|
maxlength: 20,
|
||||||
formslot: true,
|
formslot: true,
|
||||||
rules: [
|
rules: [
|
||||||
{ max: 20, message: '联系方式不能超过20个字', trigger: 'blur' },
|
{ pattern: /^1[3-9]\d{9}$/, message: '联系方式格式不正确', trigger: 'blur' },
|
||||||
{ pattern: /^\d*$/, message: '联系方式只能输入数字', trigger: 'blur' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ export const planStatusOptions = [
|
|||||||
{ label: '待调度', value: 'waiting_dispatch' },
|
{ label: '待调度', value: 'waiting_dispatch' },
|
||||||
{ label: '调度中', value: 'dispatching' },
|
{ label: '调度中', value: 'dispatching' },
|
||||||
{ label: '已完成', value: 'completed' },
|
{ label: '已完成', value: 'completed' },
|
||||||
{ label: '已取消', value: 'cancelled' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const waybillStatusOptions = [
|
export const waybillStatusOptions = [
|
||||||
@@ -172,7 +171,7 @@ export const nonNegativeRule = label => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const phoneRule = label => ({
|
export const phoneRule = label => ({
|
||||||
pattern: /^(1\d{10}|0\d{2,3}-?\d{7,8})$/,
|
pattern: /^1[3-9]\d{9}$/,
|
||||||
message: `${label}格式不正确`,
|
message: `${label}格式不正确`,
|
||||||
trigger: 'blur',
|
trigger: 'blur',
|
||||||
});
|
});
|
||||||
@@ -209,6 +208,21 @@ export const createCrudOption = columns => ({
|
|||||||
column: columns,
|
column: columns,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const withSearchPlaceholders = columns =>
|
||||||
|
columns.map(column => {
|
||||||
|
if (!column.search || column.searchPlaceholder) return column;
|
||||||
|
if (column.type === 'date' || column.type === 'datetime' || column.searchRange) return column;
|
||||||
|
const isSelectSearch =
|
||||||
|
column.type === 'select' ||
|
||||||
|
column.searchType === 'select' ||
|
||||||
|
Boolean(column.dicData) ||
|
||||||
|
Boolean(column.dicUrl);
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
searchPlaceholder: isSelectSearch ? '请选择' : '请输入',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export const auditColumns = [
|
export const auditColumns = [
|
||||||
{
|
{
|
||||||
label: '创建人',
|
label: '创建人',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const transportTypeDict = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const carrierTypeOptions = ['承运商', '自运', '网签平台'];
|
export const carrierTypeOptions = ['承运商', '自运', '网货平台'];
|
||||||
|
|
||||||
export const loadingStatusOptions = [
|
export const loadingStatusOptions = [
|
||||||
{ label: '草稿', value: 'draft' },
|
{ label: '草稿', value: 'draft' },
|
||||||
@@ -28,6 +28,24 @@ export const loadingManageConfig = {
|
|||||||
permission: 'loading_manage',
|
permission: 'loading_manage',
|
||||||
exportUrl: '/blade-transport/loading-manage/export-loading-manage',
|
exportUrl: '/blade-transport/loading-manage/export-loading-manage',
|
||||||
exportName: '配载管理',
|
exportName: '配载管理',
|
||||||
|
exportColumns: [
|
||||||
|
{ prop: 'loadingNo', label: '配载单号' },
|
||||||
|
{ prop: 'loadingSubNos', label: '配载子单号' },
|
||||||
|
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' },
|
||||||
|
{ prop: 'driverName', label: '司机' },
|
||||||
|
{ prop: 'driverPhone', label: '联系电话' },
|
||||||
|
{ prop: 'carrierType', label: '承运类型' },
|
||||||
|
{ prop: 'departureAddress', label: '发货地' },
|
||||||
|
{ prop: 'transitAddress', label: '途经地' },
|
||||||
|
{ prop: 'arrivalAddress', label: '到货地' },
|
||||||
|
{ prop: 'carrierName', label: '承运商' },
|
||||||
|
{ prop: 'transportType', label: '运输方式' },
|
||||||
|
{ prop: 'dataSource', label: '数据来源' },
|
||||||
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
|
{ prop: 'updateTime', label: '更新时间' },
|
||||||
|
{ prop: 'currentProcessNode', label: '过程节点' },
|
||||||
|
{ prop: 'businessStatus', label: '状态' },
|
||||||
|
],
|
||||||
createText: '新建配载',
|
createText: '新建配载',
|
||||||
batchCompleteText: '批量完成',
|
batchCompleteText: '批量完成',
|
||||||
statusProp: 'businessStatus',
|
statusProp: 'businessStatus',
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { auditColumns, createCrudOption, selectRule, textRule, transportTypeOptions } from './common';
|
import {
|
||||||
|
auditColumns,
|
||||||
|
createCrudOption,
|
||||||
|
selectRule,
|
||||||
|
textRule,
|
||||||
|
transportTypeOptions,
|
||||||
|
withSearchPlaceholders,
|
||||||
|
} from './common';
|
||||||
|
|
||||||
const templateTypeOptions = [
|
const templateTypeOptions = [
|
||||||
{ label: '运输计划', value: '运输计划' },
|
{ label: '运输计划', value: '运输计划' },
|
||||||
@@ -8,10 +15,31 @@ const templateTypeOptions = [
|
|||||||
export const config = {
|
export const config = {
|
||||||
title: '发货模板',
|
title: '发货模板',
|
||||||
permission: 'shipping_template',
|
permission: 'shipping_template',
|
||||||
exportUrl: '/blade-transport/shipping-template/export-shipping-template',
|
tableColumnOrder: [
|
||||||
exportName: '发货模板',
|
'templateCode',
|
||||||
|
'templateName',
|
||||||
|
'templateType',
|
||||||
|
'transportType',
|
||||||
|
'createUserName',
|
||||||
|
'remark',
|
||||||
|
'updateTime',
|
||||||
|
'createTime',
|
||||||
|
],
|
||||||
|
exportColumns: [
|
||||||
|
{ prop: 'templateCode', label: '模板编号' },
|
||||||
|
{ prop: 'templateName', label: '模板名称' },
|
||||||
|
{ prop: 'templateType', label: '模板类型' },
|
||||||
|
{ prop: 'transportType', label: '运输方式' },
|
||||||
|
{ prop: 'createUserName', label: '创建人' },
|
||||||
|
{ prop: 'remark', label: '备注' },
|
||||||
|
{ prop: 'updateTime', label: '更新时间' },
|
||||||
|
{ prop: 'createTime', label: '创建时间' },
|
||||||
|
],
|
||||||
enableAllDept: false,
|
enableAllDept: false,
|
||||||
enableProjectSelect: true,
|
enableProjectSelect: true,
|
||||||
|
projectQueryParams: {
|
||||||
|
approvalStatuses: 'approved,change_approved',
|
||||||
|
},
|
||||||
excludeVoidedProjects: true,
|
excludeVoidedProjects: true,
|
||||||
enableAttachmentTable: true,
|
enableAttachmentTable: true,
|
||||||
enableTransportPlanForm: true,
|
enableTransportPlanForm: true,
|
||||||
@@ -41,178 +69,206 @@ export const config = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const option = {
|
export const option = {
|
||||||
...createCrudOption([
|
...createCrudOption(
|
||||||
{
|
withSearchPlaceholders([
|
||||||
label: '',
|
{
|
||||||
prop: 'templateInfoTitle',
|
label: '',
|
||||||
formslot: true,
|
prop: 'templateInfoTitle',
|
||||||
span: 24,
|
formslot: true,
|
||||||
order: 400,
|
span: 24,
|
||||||
hide: true,
|
order: 400,
|
||||||
labelWidth: 0,
|
hide: true,
|
||||||
},
|
labelWidth: 0,
|
||||||
{
|
},
|
||||||
label: '模板编号',
|
{
|
||||||
prop: 'templateCode',
|
label: '模板编号',
|
||||||
search: true,
|
prop: 'templateCode',
|
||||||
searchOrder: 4,
|
search: true,
|
||||||
span: 6,
|
searchOrder: 4,
|
||||||
order: 390,
|
span: 8,
|
||||||
minWidth: 170,
|
order: 390,
|
||||||
disabled: true,
|
minWidth: 170,
|
||||||
placeholder: '系统自动生成',
|
disabled: true,
|
||||||
},
|
placeholder: '系统自动生成',
|
||||||
{
|
display: true,
|
||||||
label: '模板名称',
|
},
|
||||||
prop: 'templateName',
|
{
|
||||||
search: true,
|
label: '模板名称',
|
||||||
searchOrder: 3,
|
prop: 'templateName',
|
||||||
span: 6,
|
search: true,
|
||||||
order: 380,
|
searchOrder: 3,
|
||||||
minWidth: 150,
|
span: 8,
|
||||||
rules: textRule('模板名称', 50, true),
|
order: 380,
|
||||||
},
|
minWidth: 150,
|
||||||
{
|
rules: textRule('模板名称', 50, true),
|
||||||
label: '模板类型',
|
display: true,
|
||||||
prop: 'templateType',
|
},
|
||||||
type: 'select',
|
{
|
||||||
search: true,
|
label: '模板类型',
|
||||||
searchOrder: 2,
|
prop: 'templateType',
|
||||||
span: 6,
|
type: 'select',
|
||||||
order: 370,
|
search: true,
|
||||||
dicData: templateTypeOptions,
|
searchOrder: 2,
|
||||||
minWidth: 120,
|
span: 8,
|
||||||
rules: selectRule('模板类型'),
|
order: 370,
|
||||||
},
|
dicData: templateTypeOptions,
|
||||||
{
|
minWidth: 120,
|
||||||
label: '备注',
|
rules: selectRule('模板类型'),
|
||||||
prop: 'remark',
|
display: true,
|
||||||
type: 'textarea',
|
},
|
||||||
minRows: 2,
|
{
|
||||||
span: 18,
|
label: '备注',
|
||||||
order: 360,
|
prop: 'remark',
|
||||||
minWidth: 180,
|
type: 'textarea',
|
||||||
maxlength: 200,
|
minRows: 3,
|
||||||
showWordLimit: true,
|
span: 24,
|
||||||
rules: textRule('备注', 200),
|
order: 360,
|
||||||
},
|
minWidth: 180,
|
||||||
{
|
maxlength: 200,
|
||||||
label: '',
|
showWordLimit: true,
|
||||||
prop: 'basicInfoTitle',
|
rules: textRule('备注', 200),
|
||||||
formslot: true,
|
display: true,
|
||||||
span: 24,
|
},
|
||||||
order: 350,
|
{
|
||||||
hide: true,
|
label: '',
|
||||||
labelWidth: 0,
|
prop: 'basicInfoTitle',
|
||||||
},
|
formslot: true,
|
||||||
{
|
span: 24,
|
||||||
label: '项目',
|
order: 350,
|
||||||
prop: 'projectName',
|
hide: true,
|
||||||
formslot: true,
|
labelWidth: 0,
|
||||||
search: true,
|
},
|
||||||
searchOrder: 1,
|
{
|
||||||
span: 6,
|
label: '项目',
|
||||||
order: 340,
|
prop: 'projectName',
|
||||||
minWidth: 150,
|
formslot: true,
|
||||||
rules: textRule('项目', 100, true),
|
search: true,
|
||||||
},
|
searchOrder: 1,
|
||||||
{
|
span: 8,
|
||||||
label: '客户合同',
|
order: 340,
|
||||||
prop: 'contractName',
|
minWidth: 150,
|
||||||
formslot: true,
|
rules: textRule('项目', 100, true),
|
||||||
span: 6,
|
display: true,
|
||||||
order: 330,
|
},
|
||||||
minWidth: 160,
|
{
|
||||||
rules: textRule('客户合同', 100, true),
|
label: '客户合同',
|
||||||
},
|
prop: 'contractName',
|
||||||
{
|
formslot: true,
|
||||||
label: '运输方式',
|
span: 8,
|
||||||
prop: 'transportType',
|
order: 330,
|
||||||
type: 'select',
|
minWidth: 160,
|
||||||
span: 6,
|
rules: textRule('客户合同', 100, true),
|
||||||
order: 320,
|
display: true,
|
||||||
dicData: transportTypeOptions,
|
},
|
||||||
minWidth: 130,
|
{
|
||||||
rules: selectRule('运输方式'),
|
label: '运输方式',
|
||||||
},
|
prop: 'transportType',
|
||||||
{
|
type: 'select',
|
||||||
label: '',
|
span: 8,
|
||||||
prop: 'shippingInfoTitle',
|
order: 320,
|
||||||
formslot: true,
|
dicData: transportTypeOptions,
|
||||||
span: 24,
|
minWidth: 130,
|
||||||
order: 310,
|
rules: selectRule('运输方式'),
|
||||||
hide: true,
|
display: true,
|
||||||
labelWidth: 0,
|
},
|
||||||
},
|
{
|
||||||
{
|
label: '备注',
|
||||||
label: '发货地址',
|
prop: 'basicRemark',
|
||||||
prop: 'departureAddress',
|
type: 'textarea',
|
||||||
formslot: true,
|
minRows: 3,
|
||||||
span: 24,
|
span: 24,
|
||||||
order: 300,
|
order: 315,
|
||||||
minWidth: 220,
|
minWidth: 180,
|
||||||
rules: textRule('发货地址', 255, true),
|
maxlength: 200,
|
||||||
},
|
showWordLimit: true,
|
||||||
{
|
rules: textRule('备注', 200),
|
||||||
label: '收货地址',
|
display: true,
|
||||||
prop: 'arrivalAddress',
|
},
|
||||||
formslot: true,
|
{
|
||||||
span: 24,
|
label: '',
|
||||||
order: 290,
|
prop: 'shippingInfoTitle',
|
||||||
minWidth: 220,
|
formslot: true,
|
||||||
rules: textRule('收货地址', 255, true),
|
span: 24,
|
||||||
},
|
order: 310,
|
||||||
{
|
hide: true,
|
||||||
label: '',
|
labelWidth: 0,
|
||||||
prop: 'goodsInfoTitle',
|
},
|
||||||
formslot: true,
|
{
|
||||||
span: 24,
|
label: '发货地址',
|
||||||
order: 280,
|
prop: 'departureAddress',
|
||||||
hide: true,
|
formslot: true,
|
||||||
labelWidth: 0,
|
span: 24,
|
||||||
},
|
order: 300,
|
||||||
{
|
minWidth: 220,
|
||||||
label: '',
|
rules: textRule('发货地址', 255, true),
|
||||||
prop: 'goodsJson',
|
display: true,
|
||||||
type: 'textarea',
|
},
|
||||||
formslot: true,
|
{
|
||||||
span: 24,
|
label: '收货地址',
|
||||||
order: 270,
|
prop: 'arrivalAddress',
|
||||||
hide: true,
|
formslot: true,
|
||||||
labelWidth: '0px',
|
span: 24,
|
||||||
className: 'business-crud-page__full-form-item',
|
order: 290,
|
||||||
},
|
minWidth: 220,
|
||||||
{ label: '运费信息', prop: 'freightJson', hide: true, display: false },
|
rules: textRule('收货地址', 255, true),
|
||||||
{
|
display: true,
|
||||||
label: '',
|
},
|
||||||
prop: 'attachmentTitle',
|
{
|
||||||
formslot: true,
|
label: '',
|
||||||
span: 24,
|
prop: 'goodsInfoTitle',
|
||||||
order: 260,
|
formslot: true,
|
||||||
hide: true,
|
span: 24,
|
||||||
labelWidth: 0,
|
order: 280,
|
||||||
},
|
hide: true,
|
||||||
{
|
labelWidth: 0,
|
||||||
label: '',
|
},
|
||||||
prop: 'attachmentsJson',
|
{
|
||||||
type: 'textarea',
|
label: '',
|
||||||
formslot: true,
|
prop: 'goodsJson',
|
||||||
span: 24,
|
type: 'textarea',
|
||||||
order: 250,
|
formslot: true,
|
||||||
hide: true,
|
span: 24,
|
||||||
labelWidth: '0px',
|
order: 270,
|
||||||
className: 'business-crud-page__full-form-item',
|
hide: true,
|
||||||
},
|
labelWidth: '0px',
|
||||||
{ label: '项目ID', prop: 'projectId', hide: true, display: false },
|
className: 'business-crud-page__full-form-item',
|
||||||
{ label: '客户合同ID', prop: 'contractId', hide: true, display: false },
|
},
|
||||||
{ label: '发货地', prop: 'departureName', hide: true, display: false },
|
{ label: '运费信息', prop: 'freightJson', hide: true, display: false },
|
||||||
{ label: '发货联系人', prop: 'departureContact', hide: true, display: false },
|
{
|
||||||
{ label: '发货联系方式', prop: 'departurePhone', hide: true, display: false },
|
label: '',
|
||||||
{ label: '收货地', prop: 'arrivalName', hide: true, display: false },
|
prop: 'attachmentTitle',
|
||||||
{ label: '收货联系人', prop: 'arrivalContact', hide: true, display: false },
|
formslot: true,
|
||||||
{ label: '收货联系方式', prop: 'arrivalPhone', hide: true, display: false },
|
span: 24,
|
||||||
{ label: '所属组织', prop: 'deptName', minWidth: 150, addDisplay: false, editDisplay: false },
|
order: 260,
|
||||||
...auditColumns,
|
hide: true,
|
||||||
]),
|
labelWidth: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
prop: 'attachmentsJson',
|
||||||
|
type: 'textarea',
|
||||||
|
formslot: true,
|
||||||
|
span: 24,
|
||||||
|
order: 250,
|
||||||
|
hide: true,
|
||||||
|
labelWidth: '0px',
|
||||||
|
className: 'business-crud-page__full-form-item',
|
||||||
|
},
|
||||||
|
{ label: '项目ID', prop: 'projectId', hide: true, display: false },
|
||||||
|
{ label: '客户合同ID', prop: 'contractId', hide: true, display: false },
|
||||||
|
{ label: '发货地', prop: 'departureName', hide: true, display: false },
|
||||||
|
{ label: '发货联系人', prop: 'departureContact', hide: true, display: false },
|
||||||
|
{ label: '发货联系方式', prop: 'departurePhone', hide: true, display: false },
|
||||||
|
{ label: '收货地', prop: 'arrivalName', hide: true, display: false },
|
||||||
|
{ label: '收货联系人', prop: 'arrivalContact', hide: true, display: false },
|
||||||
|
{ label: '收货联系方式', prop: 'arrivalPhone', hide: true, display: false },
|
||||||
|
{ label: '所属组织', prop: 'deptName', minWidth: 150, addDisplay: false, editDisplay: false, display: false },
|
||||||
|
...auditColumns.map(column => ({
|
||||||
|
...column,
|
||||||
|
display: false,
|
||||||
|
})),
|
||||||
|
])
|
||||||
|
),
|
||||||
dialogWidth: '96%',
|
dialogWidth: '96%',
|
||||||
|
dialogTop: '10px',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
planStatusOptions,
|
planStatusOptions,
|
||||||
selectRule,
|
selectRule,
|
||||||
textRule,
|
textRule,
|
||||||
|
withSearchPlaceholders,
|
||||||
} from './common';
|
} from './common';
|
||||||
|
|
||||||
const listDicFormatter = res => {
|
const listDicFormatter = res => {
|
||||||
@@ -104,8 +105,55 @@ const formatGoodsInfo = row => {
|
|||||||
return text || row.goodsInfo || '';
|
return text || row.goodsInfo || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDispatchProgress = row =>
|
const parseDispatchRows = row => {
|
||||||
row.dispatchProgressName || row.dispatchProgress || row.progress || '';
|
const sources = [
|
||||||
|
row.dispatchRows,
|
||||||
|
row.dispatchList,
|
||||||
|
row.dispatchRecords,
|
||||||
|
row.waybillList,
|
||||||
|
row.waybillRows,
|
||||||
|
];
|
||||||
|
for (const source of sources) {
|
||||||
|
const rows = parseGoodsRows(source);
|
||||||
|
if (rows.length) return rows;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDispatchQuantity = value => {
|
||||||
|
const number = Number(value || 0);
|
||||||
|
if (!Number.isFinite(number)) return '0';
|
||||||
|
const fixed = Math.round((number + Number.EPSILON) * 1000) / 1000;
|
||||||
|
return Number.isInteger(fixed) ? String(fixed) : String(fixed).replace(/\.?0+$/, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDispatchProgress = row => {
|
||||||
|
const goodsRows = parseGoodsRows(row.goodsJson);
|
||||||
|
const totalRows = goodsRows.length ? goodsRows : [row];
|
||||||
|
const dispatchRows = parseDispatchRows(row);
|
||||||
|
const summary = new Map();
|
||||||
|
const add = (item, field) => {
|
||||||
|
const unit = item.quantityUnit || item.goodsQuantityUnit || item.unit || '吨';
|
||||||
|
const quantity = Number(item.quantity || item.cargoQuantity || item.goodsQuantity || 0);
|
||||||
|
if (!summary.has(unit)) summary.set(unit, { total: 0, assigned: 0 });
|
||||||
|
if (Number.isFinite(quantity)) summary.get(unit)[field] += quantity;
|
||||||
|
};
|
||||||
|
totalRows.forEach(item => add(item, 'total'));
|
||||||
|
dispatchRows.forEach(item => add(item, 'assigned'));
|
||||||
|
|
||||||
|
if (!dispatchRows.length) {
|
||||||
|
const assigned = Number(row.dispatchedQuantity || row.dispatchQuantity || 0);
|
||||||
|
if (Number.isFinite(assigned)) {
|
||||||
|
const unit = row.quantityUnit || row.goodsQuantityUnit || row.unit || '吨';
|
||||||
|
if (!summary.has(unit)) summary.set(unit, { total: 0, assigned: 0 });
|
||||||
|
summary.get(unit).assigned = assigned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const lines = Array.from(summary.entries()).map(
|
||||||
|
([unit, item]) => `${formatDispatchQuantity(item.assigned)}/${formatDispatchQuantity(item.total)}${unit}`
|
||||||
|
);
|
||||||
|
return lines.length ? lines.join('\n') : row.dispatchProgressName || row.dispatchProgress || row.progress || '';
|
||||||
|
};
|
||||||
|
|
||||||
const auditColumn = prop => ({ ...auditColumns.find(item => item.prop === prop) });
|
const auditColumn = prop => ({ ...auditColumns.find(item => item.prop === prop) });
|
||||||
|
|
||||||
@@ -116,11 +164,15 @@ export const config = {
|
|||||||
exportName: '运输计划',
|
exportName: '运输计划',
|
||||||
enableAllDept: false,
|
enableAllDept: false,
|
||||||
enableProjectSelect: true,
|
enableProjectSelect: true,
|
||||||
|
projectQueryParams: {
|
||||||
|
approvalStatuses: 'approved,change_approved',
|
||||||
|
},
|
||||||
templateCreateTarget: 'transport-plan',
|
templateCreateTarget: 'transport-plan',
|
||||||
enableAttachmentTable: true,
|
enableAttachmentTable: true,
|
||||||
enableTransportPlanForm: true,
|
enableTransportPlanForm: true,
|
||||||
enableTransportPlanCreateActions: true,
|
enableTransportPlanCreateActions: true,
|
||||||
enableTransportPlanImport: true,
|
enableTransportPlanImport: true,
|
||||||
|
fixedTransportAddressType: true,
|
||||||
transportPlanImportTitle: '导入发货计划',
|
transportPlanImportTitle: '导入发货计划',
|
||||||
transportPlanTemplateUrl: '/blade-transport/transport-plan/export-template',
|
transportPlanTemplateUrl: '/blade-transport/transport-plan/export-template',
|
||||||
attachmentTitle: '附件',
|
attachmentTitle: '附件',
|
||||||
@@ -128,10 +180,10 @@ export const config = {
|
|||||||
planStartDateRange: ['planStartDateStart', 'planStartDateEnd'],
|
planStartDateRange: ['planStartDateStart', 'planStartDateEnd'],
|
||||||
planEndDateRange: ['planEndDateStart', 'planEndDateEnd'],
|
planEndDateRange: ['planEndDateStart', 'planEndDateEnd'],
|
||||||
},
|
},
|
||||||
actions: ['copy', 'cancel', 'complete'],
|
actions: ['copy', 'complete'],
|
||||||
statusProp: 'businessStatus',
|
statusProp: 'businessStatus',
|
||||||
statusTextProp: 'businessStatusName',
|
statusTextProp: 'businessStatusName',
|
||||||
deleteStatus: ['draft'],
|
deleteStatus: ['draft', 'waiting_dispatch'],
|
||||||
editStatus: ['draft', 'waiting_dispatch'],
|
editStatus: ['draft', 'waiting_dispatch'],
|
||||||
detailButton: true,
|
detailButton: true,
|
||||||
detailSections: [
|
detailSections: [
|
||||||
@@ -177,7 +229,7 @@ export const config = {
|
|||||||
action: 'dispatch',
|
action: 'dispatch',
|
||||||
label: '调度',
|
label: '调度',
|
||||||
statusProp: 'businessStatus',
|
statusProp: 'businessStatus',
|
||||||
status: ['waiting_dispatch'],
|
status: ['waiting_dispatch', 'dispatching'],
|
||||||
permission: 'transport_plan_edit',
|
permission: 'transport_plan_edit',
|
||||||
confirm: '确定调度该运输计划?',
|
confirm: '确定调度该运输计划?',
|
||||||
},
|
},
|
||||||
@@ -185,367 +237,370 @@ export const config = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const option = {
|
export const option = {
|
||||||
...createCrudOption([
|
...createCrudOption(
|
||||||
{
|
withSearchPlaceholders([
|
||||||
label: '',
|
{
|
||||||
prop: 'basicInfoTitle',
|
label: '',
|
||||||
formslot: true,
|
prop: 'basicInfoTitle',
|
||||||
span: 24,
|
formslot: true,
|
||||||
order: 400,
|
span: 24,
|
||||||
hide: true,
|
order: 400,
|
||||||
labelWidth: 0,
|
hide: true,
|
||||||
},
|
labelWidth: 0,
|
||||||
{
|
|
||||||
label: '计划单号',
|
|
||||||
prop: 'planNo',
|
|
||||||
search: true,
|
|
||||||
searchOrder: 13,
|
|
||||||
span: 6,
|
|
||||||
order: 370,
|
|
||||||
minWidth: 150,
|
|
||||||
disabled: true,
|
|
||||||
placeholder: '系统自动生成',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '计划名称',
|
|
||||||
prop: 'planName',
|
|
||||||
search: true,
|
|
||||||
searchOrder: 12,
|
|
||||||
span: 6,
|
|
||||||
order: 380,
|
|
||||||
minWidth: 150,
|
|
||||||
rules: textRule('计划名称', 50, true),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '运输方式',
|
|
||||||
prop: 'transportType',
|
|
||||||
type: 'select',
|
|
||||||
search: true,
|
|
||||||
searchOrder: 10,
|
|
||||||
span: 6,
|
|
||||||
order: 360,
|
|
||||||
dicUrl: '/blade-system/dict-biz/dictionary?code=transport_type',
|
|
||||||
props: {
|
|
||||||
label: 'dictValue',
|
|
||||||
value: 'dictKey',
|
|
||||||
},
|
},
|
||||||
minWidth: 130,
|
{
|
||||||
rules: selectRule('运输类型'),
|
label: '计划单号',
|
||||||
},
|
prop: 'planNo',
|
||||||
{
|
search: true,
|
||||||
label: '货物信息',
|
searchOrder: 13,
|
||||||
prop: 'goodsInfo',
|
span: 6,
|
||||||
formatter: row => formatGoodsInfo(row),
|
order: 370,
|
||||||
minWidth: 320,
|
minWidth: 150,
|
||||||
className: 'business-crud-page__goods-info-cell',
|
disabled: true,
|
||||||
overHidden: false,
|
placeholder: '系统自动生成',
|
||||||
showOverflowTooltip: false,
|
|
||||||
addDisplay: false,
|
|
||||||
editDisplay: false,
|
|
||||||
viewDisplay: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '调度进度',
|
|
||||||
prop: 'dispatchProgress',
|
|
||||||
formatter: row => formatDispatchProgress(row),
|
|
||||||
minWidth: 120,
|
|
||||||
addDisplay: false,
|
|
||||||
editDisplay: false,
|
|
||||||
viewDisplay: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '发货地址',
|
|
||||||
prop: 'departureAddress',
|
|
||||||
formslot: true,
|
|
||||||
search: true,
|
|
||||||
searchOrder: 7,
|
|
||||||
span: 24,
|
|
||||||
order: 310,
|
|
||||||
minWidth: 220,
|
|
||||||
rules: textRule('发货地址', 255, true),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '到货地址',
|
|
||||||
prop: 'arrivalAddress',
|
|
||||||
formslot: true,
|
|
||||||
search: true,
|
|
||||||
searchOrder: 6,
|
|
||||||
span: 24,
|
|
||||||
order: 300,
|
|
||||||
minWidth: 220,
|
|
||||||
rules: textRule('收货地址', 255, true),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '项目名称',
|
|
||||||
prop: 'projectName',
|
|
||||||
formslot: true,
|
|
||||||
search: true,
|
|
||||||
searchOrder: 11,
|
|
||||||
searchType: 'select',
|
|
||||||
span: 6,
|
|
||||||
order: 400,
|
|
||||||
dicUrl: '/blade-transport/project-apply/list?current=1&size=9999',
|
|
||||||
dicFormatter: listDicFormatter,
|
|
||||||
props: {
|
|
||||||
label: 'projectName',
|
|
||||||
value: 'projectName',
|
|
||||||
},
|
},
|
||||||
filterable: true,
|
{
|
||||||
minWidth: 150,
|
label: '计划名称',
|
||||||
rules: textRule('项目', 100, true),
|
prop: 'planName',
|
||||||
},
|
search: true,
|
||||||
{
|
searchOrder: 12,
|
||||||
label: '客户名称',
|
span: 6,
|
||||||
prop: 'customerName',
|
order: 380,
|
||||||
search: true,
|
minWidth: 150,
|
||||||
searchOrder: 5,
|
rules: textRule('计划名称', 50, true),
|
||||||
minWidth: 150,
|
},
|
||||||
addDisplay: false,
|
{
|
||||||
editDisplay: false,
|
label: '运输方式',
|
||||||
},
|
prop: 'transportType',
|
||||||
{
|
type: 'select',
|
||||||
label: '计划开始日期',
|
search: true,
|
||||||
prop: 'planStartDate',
|
searchOrder: 10,
|
||||||
sortable: true,
|
span: 6,
|
||||||
type: 'date',
|
order: 360,
|
||||||
format: 'YYYY-MM-DD',
|
dicUrl: '/blade-system/dict-biz/dictionary?code=transport_type',
|
||||||
valueFormat: 'YYYY-MM-DD',
|
props: {
|
||||||
span: 6,
|
label: 'dictValue',
|
||||||
order: 350,
|
value: 'dictKey',
|
||||||
minWidth: 130,
|
},
|
||||||
},
|
minWidth: 130,
|
||||||
{
|
rules: selectRule('运输类型'),
|
||||||
label: '计划结束日期',
|
},
|
||||||
prop: 'planEndDate',
|
{
|
||||||
sortable: true,
|
label: '货物信息',
|
||||||
type: 'date',
|
prop: 'goodsInfo',
|
||||||
format: 'YYYY-MM-DD',
|
formatter: row => formatGoodsInfo(row),
|
||||||
valueFormat: 'YYYY-MM-DD',
|
minWidth: 320,
|
||||||
span: 6,
|
className: 'business-crud-page__goods-info-cell',
|
||||||
order: 340,
|
overHidden: false,
|
||||||
minWidth: 130,
|
showOverflowTooltip: false,
|
||||||
},
|
addDisplay: false,
|
||||||
{
|
editDisplay: false,
|
||||||
label: '数据来源',
|
viewDisplay: false,
|
||||||
prop: 'dataSource',
|
},
|
||||||
type: 'select',
|
{
|
||||||
search: true,
|
label: '调度进度',
|
||||||
searchOrder: 1,
|
prop: 'dispatchProgress',
|
||||||
dicData: dataSourceOptions,
|
formatter: row => formatDispatchProgress(row),
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
addDisplay: false,
|
className: 'business-crud-page__dispatch-progress-cell',
|
||||||
editDisplay: false,
|
overHidden: false,
|
||||||
},
|
showOverflowTooltip: false,
|
||||||
{
|
addDisplay: false,
|
||||||
label: '货物类型',
|
editDisplay: false,
|
||||||
prop: 'cargoType',
|
viewDisplay: false,
|
||||||
search: true,
|
},
|
||||||
searchOrder: 8,
|
{
|
||||||
formatter: row => formatGoodsField(row, ['cargoType', 'goodsType']),
|
label: '发货地址',
|
||||||
minWidth: 130,
|
prop: 'departureAddress',
|
||||||
addDisplay: false,
|
formslot: true,
|
||||||
editDisplay: false,
|
search: true,
|
||||||
viewDisplay: false,
|
searchOrder: 7,
|
||||||
},
|
span: 24,
|
||||||
{
|
order: 310,
|
||||||
label: '货物名称',
|
minWidth: 220,
|
||||||
prop: 'cargoName',
|
rules: textRule('发货地址', 255, true),
|
||||||
search: true,
|
},
|
||||||
searchOrder: 9,
|
{
|
||||||
formatter: row => formatGoodsField(row, ['cargoName', 'goodsName']),
|
label: '到货地址',
|
||||||
minWidth: 150,
|
prop: 'arrivalAddress',
|
||||||
addDisplay: false,
|
formslot: true,
|
||||||
editDisplay: false,
|
search: true,
|
||||||
viewDisplay: false,
|
searchOrder: 6,
|
||||||
},
|
span: 24,
|
||||||
{
|
order: 300,
|
||||||
label: '备注',
|
minWidth: 220,
|
||||||
prop: 'remark',
|
rules: textRule('收货地址', 255, true),
|
||||||
type: 'textarea',
|
},
|
||||||
minRows: 2,
|
{
|
||||||
span: 24,
|
label: '项目名称',
|
||||||
order: 330,
|
prop: 'projectName',
|
||||||
minWidth: 180,
|
formslot: true,
|
||||||
maxlength: 200,
|
search: true,
|
||||||
showWordLimit: true,
|
searchOrder: 11,
|
||||||
rules: textRule('备注', 200),
|
searchType: 'select',
|
||||||
},
|
span: 6,
|
||||||
{
|
order: 400,
|
||||||
...auditColumn('updateUserName'),
|
dicUrl: '/blade-transport/project-apply/list?current=1&size=9999',
|
||||||
},
|
dicFormatter: listDicFormatter,
|
||||||
{
|
props: {
|
||||||
...auditColumn('createTime'),
|
label: 'projectName',
|
||||||
},
|
value: 'projectName',
|
||||||
{
|
},
|
||||||
...auditColumn('updateTime'),
|
filterable: true,
|
||||||
},
|
minWidth: 150,
|
||||||
{
|
rules: textRule('项目', 100, true),
|
||||||
label: '调度状态',
|
},
|
||||||
prop: 'businessStatus',
|
{
|
||||||
type: 'select',
|
label: '客户名称',
|
||||||
slot: true,
|
prop: 'customerName',
|
||||||
search: true,
|
search: true,
|
||||||
searchLabel: '状态',
|
searchOrder: 5,
|
||||||
searchOrder: 2,
|
minWidth: 150,
|
||||||
dicData: planStatusOptions,
|
addDisplay: false,
|
||||||
minWidth: 120,
|
editDisplay: false,
|
||||||
fixed: 'right',
|
},
|
||||||
addDisplay: false,
|
{
|
||||||
editDisplay: false,
|
label: '计划开始日期',
|
||||||
},
|
prop: 'planStartDate',
|
||||||
{
|
type: 'date',
|
||||||
label: '',
|
format: 'YYYY-MM-DD',
|
||||||
prop: 'shippingInfoTitle',
|
valueFormat: 'YYYY-MM-DD',
|
||||||
formslot: true,
|
span: 6,
|
||||||
span: 24,
|
order: 350,
|
||||||
order: 320,
|
minWidth: 150,
|
||||||
hide: true,
|
},
|
||||||
labelWidth: 0,
|
{
|
||||||
},
|
label: '计划结束日期',
|
||||||
{
|
prop: 'planEndDate',
|
||||||
label: '项目ID',
|
type: 'date',
|
||||||
prop: 'projectId',
|
format: 'YYYY-MM-DD',
|
||||||
hide: true,
|
valueFormat: 'YYYY-MM-DD',
|
||||||
display: false,
|
span: 6,
|
||||||
},
|
order: 340,
|
||||||
{
|
minWidth: 150,
|
||||||
label: '客户合同',
|
},
|
||||||
prop: 'contractName',
|
{
|
||||||
formslot: true,
|
label: '数据来源',
|
||||||
hide: true,
|
prop: 'dataSource',
|
||||||
span: 6,
|
type: 'select',
|
||||||
order: 390,
|
search: true,
|
||||||
minWidth: 160,
|
searchOrder: 1,
|
||||||
rules: textRule('客户合同', 100, true),
|
dicData: dataSourceOptions,
|
||||||
},
|
minWidth: 120,
|
||||||
{
|
addDisplay: false,
|
||||||
label: '客户合同ID',
|
editDisplay: false,
|
||||||
prop: 'contractId',
|
},
|
||||||
hide: true,
|
{
|
||||||
display: false,
|
label: '货物类型',
|
||||||
},
|
prop: 'cargoType',
|
||||||
{
|
search: true,
|
||||||
label: '发货地',
|
searchOrder: 8,
|
||||||
prop: 'departureName',
|
formatter: row => formatGoodsField(row, ['cargoType', 'goodsType']),
|
||||||
hide: true,
|
minWidth: 130,
|
||||||
display: false,
|
addDisplay: false,
|
||||||
minWidth: 140,
|
editDisplay: false,
|
||||||
rules: textRule('发货地', 100, true),
|
viewDisplay: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '收货地',
|
label: '货物名称',
|
||||||
prop: 'arrivalName',
|
prop: 'cargoName',
|
||||||
hide: true,
|
search: true,
|
||||||
display: false,
|
searchOrder: 9,
|
||||||
minWidth: 140,
|
formatter: row => formatGoodsField(row, ['cargoName', 'goodsName']),
|
||||||
rules: textRule('收货地', 100, true),
|
minWidth: 150,
|
||||||
},
|
addDisplay: false,
|
||||||
{
|
editDisplay: false,
|
||||||
label: '发货联系人',
|
viewDisplay: false,
|
||||||
prop: 'departureContact',
|
},
|
||||||
hide: true,
|
{
|
||||||
display: false,
|
label: '备注',
|
||||||
rules: textRule('发货联系人', 50),
|
prop: 'remark',
|
||||||
},
|
type: 'textarea',
|
||||||
{
|
minRows: 4,
|
||||||
label: '发货联系方式',
|
span: 24,
|
||||||
prop: 'departurePhone',
|
order: 330,
|
||||||
hide: true,
|
minWidth: 180,
|
||||||
display: false,
|
maxlength: 200,
|
||||||
rules: [phoneRule('发货联系方式'), ...textRule('发货联系方式', 50)],
|
showWordLimit: true,
|
||||||
},
|
rules: textRule('备注', 200),
|
||||||
{
|
},
|
||||||
label: '收货联系人',
|
{
|
||||||
prop: 'arrivalContact',
|
...auditColumn('updateUserName'),
|
||||||
hide: true,
|
},
|
||||||
display: false,
|
{
|
||||||
rules: textRule('收货联系人', 50),
|
...auditColumn('createTime'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '收货联系方式',
|
...auditColumn('updateTime'),
|
||||||
prop: 'arrivalPhone',
|
},
|
||||||
hide: true,
|
{
|
||||||
display: false,
|
label: '调度状态',
|
||||||
rules: [phoneRule('收货联系方式'), ...textRule('收货联系方式', 50)],
|
prop: 'businessStatus',
|
||||||
},
|
type: 'select',
|
||||||
{
|
slot: true,
|
||||||
label: '',
|
search: true,
|
||||||
prop: 'goodsInfoTitle',
|
searchLabel: '状态',
|
||||||
formslot: true,
|
searchOrder: 2,
|
||||||
span: 24,
|
dicData: planStatusOptions,
|
||||||
order: 280,
|
minWidth: 120,
|
||||||
hide: true,
|
fixed: 'right',
|
||||||
labelWidth: 0,
|
addDisplay: false,
|
||||||
},
|
editDisplay: false,
|
||||||
{
|
},
|
||||||
label: '计划开始日期',
|
{
|
||||||
prop: 'planStartDateRange',
|
label: '',
|
||||||
type: 'date',
|
prop: 'shippingInfoTitle',
|
||||||
format: 'YYYY-MM-DD',
|
formslot: true,
|
||||||
valueFormat: 'YYYY-MM-DD',
|
span: 24,
|
||||||
search: true,
|
order: 320,
|
||||||
searchRange: true,
|
hide: true,
|
||||||
searchOrder: 4,
|
labelWidth: 0,
|
||||||
hide: true,
|
},
|
||||||
addDisplay: false,
|
{
|
||||||
editDisplay: false,
|
label: '项目ID',
|
||||||
viewDisplay: false,
|
prop: 'projectId',
|
||||||
},
|
hide: true,
|
||||||
{
|
display: false,
|
||||||
label: '计划结束日期',
|
},
|
||||||
prop: 'planEndDateRange',
|
{
|
||||||
type: 'date',
|
label: '客户合同',
|
||||||
format: 'YYYY-MM-DD',
|
prop: 'contractName',
|
||||||
valueFormat: 'YYYY-MM-DD',
|
formslot: true,
|
||||||
search: true,
|
hide: true,
|
||||||
searchRange: true,
|
span: 6,
|
||||||
searchOrder: 3,
|
order: 390,
|
||||||
hide: true,
|
minWidth: 160,
|
||||||
addDisplay: false,
|
rules: textRule('客户合同', 100, true),
|
||||||
editDisplay: false,
|
},
|
||||||
viewDisplay: false,
|
{
|
||||||
},
|
label: '客户合同ID',
|
||||||
{
|
prop: 'contractId',
|
||||||
label: '所属组织',
|
hide: true,
|
||||||
prop: 'deptName',
|
display: false,
|
||||||
hide: true,
|
},
|
||||||
minWidth: 150,
|
{
|
||||||
addDisplay: false,
|
label: '发货地',
|
||||||
editDisplay: false,
|
prop: 'departureName',
|
||||||
},
|
hide: true,
|
||||||
{
|
display: false,
|
||||||
label: '',
|
minWidth: 140,
|
||||||
prop: 'goodsJson',
|
rules: textRule('发货地', 100, true),
|
||||||
type: 'textarea',
|
},
|
||||||
formslot: true,
|
{
|
||||||
minRows: 5,
|
label: '收货地',
|
||||||
span: 24,
|
prop: 'arrivalName',
|
||||||
order: 270,
|
hide: true,
|
||||||
hide: true,
|
display: false,
|
||||||
labelWidth: '0px',
|
minWidth: 140,
|
||||||
className: 'business-crud-page__full-form-item',
|
rules: textRule('收货地', 100, true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '',
|
label: '发货联系人',
|
||||||
prop: 'attachmentTitle',
|
prop: 'departureContact',
|
||||||
formslot: true,
|
hide: true,
|
||||||
span: 24,
|
display: false,
|
||||||
order: 265,
|
rules: textRule('发货联系人', 50),
|
||||||
hide: true,
|
},
|
||||||
labelWidth: 0,
|
{
|
||||||
},
|
label: '发货联系方式',
|
||||||
{
|
prop: 'departurePhone',
|
||||||
label: '',
|
hide: true,
|
||||||
prop: 'attachmentsJson',
|
display: false,
|
||||||
type: 'textarea',
|
rules: [phoneRule('发货联系方式'), ...textRule('发货联系方式', 50)],
|
||||||
formslot: true,
|
},
|
||||||
minRows: 2,
|
{
|
||||||
span: 24,
|
label: '收货联系人',
|
||||||
order: 260,
|
prop: 'arrivalContact',
|
||||||
hide: true,
|
hide: true,
|
||||||
labelWidth: '0px',
|
display: false,
|
||||||
className: 'business-crud-page__full-form-item',
|
rules: textRule('收货联系人', 50),
|
||||||
},
|
},
|
||||||
]),
|
{
|
||||||
|
label: '收货联系方式',
|
||||||
|
prop: 'arrivalPhone',
|
||||||
|
hide: true,
|
||||||
|
display: false,
|
||||||
|
rules: [phoneRule('收货联系方式'), ...textRule('收货联系方式', 50)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
prop: 'goodsInfoTitle',
|
||||||
|
formslot: true,
|
||||||
|
span: 24,
|
||||||
|
order: 280,
|
||||||
|
hide: true,
|
||||||
|
labelWidth: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '计划开始日期',
|
||||||
|
prop: 'planStartDateRange',
|
||||||
|
type: 'date',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
search: true,
|
||||||
|
searchRange: true,
|
||||||
|
searchOrder: 4,
|
||||||
|
hide: true,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
viewDisplay: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '计划结束日期',
|
||||||
|
prop: 'planEndDateRange',
|
||||||
|
type: 'date',
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
valueFormat: 'YYYY-MM-DD',
|
||||||
|
search: true,
|
||||||
|
searchRange: true,
|
||||||
|
searchOrder: 3,
|
||||||
|
hide: true,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
viewDisplay: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '所属组织',
|
||||||
|
prop: 'deptName',
|
||||||
|
hide: true,
|
||||||
|
minWidth: 150,
|
||||||
|
addDisplay: false,
|
||||||
|
editDisplay: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
prop: 'goodsJson',
|
||||||
|
type: 'textarea',
|
||||||
|
formslot: true,
|
||||||
|
minRows: 5,
|
||||||
|
span: 24,
|
||||||
|
order: 270,
|
||||||
|
hide: true,
|
||||||
|
labelWidth: '0px',
|
||||||
|
className: 'business-crud-page__full-form-item',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
prop: 'attachmentTitle',
|
||||||
|
formslot: true,
|
||||||
|
span: 24,
|
||||||
|
order: 265,
|
||||||
|
hide: true,
|
||||||
|
labelWidth: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
prop: 'attachmentsJson',
|
||||||
|
type: 'textarea',
|
||||||
|
formslot: true,
|
||||||
|
minRows: 3,
|
||||||
|
span: 24,
|
||||||
|
order: 260,
|
||||||
|
hide: true,
|
||||||
|
labelWidth: '0px',
|
||||||
|
className: 'business-crud-page__full-form-item',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
),
|
||||||
dialogWidth: '96%',
|
dialogWidth: '96%',
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
|||||||
|
export const settlementTypeOptions = [
|
||||||
|
{ label: '应收', value: 'receivable' },
|
||||||
|
{ label: '应付', value: 'payable' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const settlementStatusOptions = [
|
||||||
|
{ label: '待结算', value: 'pending' },
|
||||||
|
{ label: '已转预结算', value: 'pre_settled' },
|
||||||
|
{ label: '已转正式结算', value: 'formal_settled' },
|
||||||
|
{ label: '已关闭', value: 'closed' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const searchFields = [
|
||||||
|
{ label: '单据号', prop: 'documentNo', type: 'input' },
|
||||||
|
{ label: '生成日期', prop: 'generateDateRange', type: 'daterange' },
|
||||||
|
{ label: '客户名称', prop: 'customerName', type: 'input' },
|
||||||
|
{ label: '所属组织', prop: 'deptName', type: 'input' },
|
||||||
|
{ label: '项目名称', prop: 'projectName', type: 'input' },
|
||||||
|
{ label: '货物类型', prop: 'cargoType', type: 'input' },
|
||||||
|
{ label: '货物名称', prop: 'cargoName', type: 'input' },
|
||||||
|
{ label: '合同编号', prop: 'contractNo', type: 'input' },
|
||||||
|
{ label: '合同名称', prop: 'contractName', type: 'input' },
|
||||||
|
{ label: '预结算单号', prop: 'preSettlementNo', type: 'input' },
|
||||||
|
{ label: '正式结算单号', prop: 'formalSettlementNo', type: 'input' },
|
||||||
|
{ label: '批次号', prop: 'batchNo', type: 'input' },
|
||||||
|
{ label: '车号', prop: 'vehicleNo', type: 'input' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const tableColumns = [
|
||||||
|
{ label: '单据号', prop: 'documentNo', minWidth: 180, link: true },
|
||||||
|
{ label: '项目名称', prop: 'projectName', minWidth: 120 },
|
||||||
|
{ label: '所属组织', prop: 'deptName', minWidth: 120 },
|
||||||
|
{ label: '费用日期', prop: 'feeDate', minWidth: 120 },
|
||||||
|
{ label: '客商名称', prop: 'customerName', minWidth: 130 },
|
||||||
|
{ label: '合同编号', prop: 'contractNo', minWidth: 140 },
|
||||||
|
{ label: '合同名称', prop: 'contractName', minWidth: 140 },
|
||||||
|
{ label: '来源', prop: 'sourceType', minWidth: 110 },
|
||||||
|
{ label: '预结算单号', prop: 'preSettlementNo', minWidth: 140, link: true },
|
||||||
|
{ label: '正式结算单号', prop: 'formalSettlementNo', minWidth: 140, link: true },
|
||||||
|
{ label: '运单号', prop: 'waybillNo', minWidth: 140 },
|
||||||
|
{ label: '车号', prop: 'vehicleNo', minWidth: 120 },
|
||||||
|
{ label: '运输类型', prop: 'transportType', minWidth: 120 },
|
||||||
|
{ label: '货物名称', prop: 'cargoName', minWidth: 130 },
|
||||||
|
{ label: '货物类型', prop: 'cargoType', minWidth: 120 },
|
||||||
|
{ label: '运输总量', prop: 'transportQuantity', minWidth: 120, align: 'right' },
|
||||||
|
{ label: '里程(KM)', prop: 'mileage', minWidth: 120, align: 'right' },
|
||||||
|
{ label: '批次号', prop: 'batchNo', minWidth: 120 },
|
||||||
|
{ label: '运输单价', prop: 'unitPriceText', minWidth: 120, align: 'right' },
|
||||||
|
{ label: '运输费', prop: 'freightAmountText', minWidth: 120, align: 'right' },
|
||||||
|
{ label: '其它费用', prop: 'otherFeeAmountText', minWidth: 120, align: 'right' },
|
||||||
|
{ label: '费用合计', prop: 'totalAmountText', minWidth: 130, align: 'right' },
|
||||||
|
{ label: '状态', prop: 'settlementStatusName', minWidth: 120 },
|
||||||
|
{ label: '创建人', prop: 'createUserName', minWidth: 120 },
|
||||||
|
{ label: '创建时间', prop: 'createTime', minWidth: 170 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const feeDetailBaseColumns = [
|
||||||
|
{ label: '货物名称', prop: 'cargoName', minWidth: 140 },
|
||||||
|
{ label: '货物类型', prop: 'cargoType', minWidth: 130 },
|
||||||
|
{ label: '规格', prop: 'specification', minWidth: 120 },
|
||||||
|
{ label: '型号', prop: 'model', minWidth: 120 },
|
||||||
|
{ label: '运费计费要素', prop: 'billingFactor', minWidth: 130 },
|
||||||
|
{ label: '运费计费类型', prop: 'billingType', minWidth: 130 },
|
||||||
|
{ label: '运输量', prop: 'transportQuantityText', minWidth: 120 },
|
||||||
|
{ label: '运费计算单位', prop: 'priceUnit', minWidth: 130 },
|
||||||
|
{ label: '运输单价', prop: 'unitPrice', minWidth: 120, align: 'right' },
|
||||||
|
{ label: '里程(KM)', prop: 'mileage', minWidth: 120, align: 'right' },
|
||||||
|
{ label: '运输费', prop: 'freightAmount', minWidth: 120, align: 'right' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const feeDetailTailColumns = [
|
||||||
|
{ label: '原总金额', prop: 'originalAmountText', minWidth: 120, align: 'right' },
|
||||||
|
{ label: '调整金额', prop: 'adjustAmountText', minWidth: 120, align: 'right' },
|
||||||
|
{ label: '调整后总金额', prop: 'afterAmountText', minWidth: 140, align: 'right' },
|
||||||
|
{ label: '备注', prop: 'remark', minWidth: 160 },
|
||||||
|
{ label: '最后录入人', prop: 'updateUserName', minWidth: 120 },
|
||||||
|
{ label: '最后录入时间', prop: 'updateTime', minWidth: 170 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const changeRecordColumns = [
|
||||||
|
{ label: '行号', prop: 'lineNo', minWidth: 120 },
|
||||||
|
{ label: '货物名称', prop: 'cargoName', minWidth: 140 },
|
||||||
|
{ label: '变更内容', prop: 'changeContent', minWidth: 260 },
|
||||||
|
{ label: '调整人', prop: 'adjustUserName', minWidth: 140 },
|
||||||
|
{ label: '调整原因', prop: 'adjustReason', minWidth: 180 },
|
||||||
|
{ label: '调整时间', prop: 'adjustTime', minWidth: 170 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const updateFeeFormFields = [
|
||||||
|
{ label: '更新范围', prop: 'contractId', required: true },
|
||||||
|
{ label: '合同计费方案', prop: 'billingPlanId' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const transferSearchFields = [
|
||||||
|
{ label: '合同名称', prop: 'contractName', type: 'input' },
|
||||||
|
{ label: '批次号', prop: 'batchNo', type: 'input' },
|
||||||
|
{ label: '生成日期', prop: 'generateDateRange', type: 'daterange' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const generateSearchFields = [
|
||||||
|
{ label: '运单合同', prop: 'contractId', required: true },
|
||||||
|
{ label: '计费方案', prop: 'billingPlanId', required: true },
|
||||||
|
{ label: '批次号', prop: 'batchNo' },
|
||||||
|
{ label: '运单完成日期', prop: 'finishDateRange', type: 'daterange' },
|
||||||
|
];
|
||||||
|
|
||||||
|
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: 'driverName', minWidth: 120 },
|
||||||
|
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
|
||||||
|
{ label: '运输类型', prop: 'transportType', minWidth: 130 },
|
||||||
|
{ label: '承运类型', prop: 'carrierType', minWidth: 130 },
|
||||||
|
{ label: '货物信息', prop: 'cargoInfo', minWidth: 160 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const generatePreviewColumns = [
|
||||||
|
{ label: '客商名称', prop: 'customerName', minWidth: 160 },
|
||||||
|
{ label: '运单号', prop: 'waybillNo', minWidth: 160 },
|
||||||
|
{ label: '车号', prop: 'vehicleNo', minWidth: 140 },
|
||||||
|
{ label: '货物名称', prop: 'cargoName', minWidth: 140 },
|
||||||
|
{ label: '货物类型', prop: 'cargoType', minWidth: 140 },
|
||||||
|
{ label: '规格', prop: 'specification', minWidth: 140 },
|
||||||
|
];
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
export const exceptionCategoryOptions = [
|
||||||
|
{
|
||||||
|
label: '交通异常',
|
||||||
|
value: 'traffic',
|
||||||
|
children: [
|
||||||
|
{ label: '道路拥堵', value: '道路拥堵' },
|
||||||
|
{ label: '交通事故', value: '交通事故' },
|
||||||
|
{ label: '道路封闭', value: '道路封闭' },
|
||||||
|
{ label: '天气影响', value: '天气影响' },
|
||||||
|
{ label: '其他', value: '交通异常-其他' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '车辆异常',
|
||||||
|
value: 'vehicle',
|
||||||
|
children: [
|
||||||
|
{ label: '车辆故障维修', value: '车辆故障维修' },
|
||||||
|
{ label: '车辆事故', value: '车辆事故' },
|
||||||
|
{ label: '爆胎', value: '爆胎' },
|
||||||
|
{ label: '设备异常', value: '设备异常' },
|
||||||
|
{ label: '缺油缺电', value: '缺油缺电' },
|
||||||
|
{ label: '其他', value: '车辆异常-其他' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '货物异常',
|
||||||
|
value: 'cargo',
|
||||||
|
children: [
|
||||||
|
{ label: '货损', value: '货损' },
|
||||||
|
{ label: '货差', value: '货差' },
|
||||||
|
{ label: '货物丢失', value: '货物丢失' },
|
||||||
|
{ label: '包装异常', value: '包装异常' },
|
||||||
|
{ label: '其他', value: '货物异常-其他' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '其他异常',
|
||||||
|
value: 'other',
|
||||||
|
children: [
|
||||||
|
{ label: '变更路线地址', value: '变更路线地址' },
|
||||||
|
{ label: '其他', value: '其他异常-其他' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const disposalStatusOptions = [
|
||||||
|
{ label: '待处理', value: 'pending' },
|
||||||
|
{ label: '处理中', value: 'processing' },
|
||||||
|
{ label: '已完成', value: 'completed' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const searchFields = [
|
||||||
|
{ label: '运单号', prop: 'waybillNo', type: 'input' },
|
||||||
|
{ label: '车辆', prop: 'vehicleNo', type: 'input' },
|
||||||
|
{ label: '上报人', prop: 'reporterName', type: 'input' },
|
||||||
|
{ label: '上报时间', prop: 'reportTimeRange', type: 'daterange' },
|
||||||
|
{ label: '项目', prop: 'projectName', type: 'input' },
|
||||||
|
{ label: '承运商', prop: 'carrierName', type: 'input' },
|
||||||
|
{ label: '异常类型', prop: 'exceptionType', type: 'select', options: exceptionCategoryOptions },
|
||||||
|
{ label: '状态', prop: 'disposalStatus', type: 'select', options: disposalStatusOptions },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const tableColumns = [
|
||||||
|
{ label: '运单号', prop: 'waybillNo', minWidth: 160, link: true },
|
||||||
|
{ label: '项目', prop: 'projectName', minWidth: 160 },
|
||||||
|
{ label: '车辆', prop: 'vehicleNo', minWidth: 150, link: true },
|
||||||
|
{ label: '上报人', prop: 'reporterName', minWidth: 120 },
|
||||||
|
{ label: '异常类型', prop: 'exceptionTypeName', minWidth: 160 },
|
||||||
|
{ label: '上报说明', prop: 'reportDescription', minWidth: 180 },
|
||||||
|
{ label: '承运商', prop: 'carrierName', minWidth: 160 },
|
||||||
|
{ label: '最新跟进说明', prop: 'latestFollowContent', minWidth: 200 },
|
||||||
|
{ label: '状态', prop: 'disposalStatusName', minWidth: 120 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const detailFields = [
|
||||||
|
{ label: '配载单号/总单号', prop: 'loadingOrMasterNo', link: true },
|
||||||
|
{ label: '运单号', prop: 'waybillNo', link: true },
|
||||||
|
{ label: '项目', prop: 'projectName', link: true },
|
||||||
|
{ label: '车牌号', prop: 'vehicleNo', link: true },
|
||||||
|
{ label: '上报人', prop: 'reporterName' },
|
||||||
|
{ label: '承运商', prop: 'carrierName' },
|
||||||
|
{ label: '异常类型', prop: 'exceptionTypeName' },
|
||||||
|
{ label: '异常原因', prop: 'exceptionReason' },
|
||||||
|
{ label: '上报说明', prop: 'reportDescription', span: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const followColumns = [
|
||||||
|
{ label: '跟进说明', prop: 'followContent', minWidth: 260 },
|
||||||
|
{ label: '跟进人', prop: 'followUserName', minWidth: 130 },
|
||||||
|
{ label: '跟进时间', prop: 'followTime', minWidth: 170 },
|
||||||
|
];
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
export const riskLevelOptions = [
|
||||||
|
{ label: '低', value: 'low', tagType: 'success' },
|
||||||
|
{ label: '中', value: 'medium', tagType: 'warning' },
|
||||||
|
{ label: '高', value: 'high', tagType: 'danger' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const disposalStatusOptions = [
|
||||||
|
{ label: '待处理', value: 'pending', tagType: 'warning' },
|
||||||
|
{ label: '已处理', value: 'processed', tagType: 'success' },
|
||||||
|
{ label: '已忽略', value: 'ignored', tagType: 'info' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const disposalMethodOptions = [
|
||||||
|
{ label: '处理', value: 'process' },
|
||||||
|
{ label: '忽略', value: 'ignore' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const searchFields = [
|
||||||
|
{ label: '规则名称', prop: 'ruleName', type: 'input', placeholder: '请输入' },
|
||||||
|
{ label: '风险等级', prop: 'riskLevel', type: 'select', placeholder: '全部', options: riskLevelOptions },
|
||||||
|
{
|
||||||
|
label: '处理状态',
|
||||||
|
prop: 'disposalStatus',
|
||||||
|
type: 'select',
|
||||||
|
placeholder: '全部',
|
||||||
|
options: disposalStatusOptions,
|
||||||
|
},
|
||||||
|
{ label: '组织', prop: 'deptName', type: 'input', placeholder: '默认展示当前组织,可搜索' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const tableColumns = [
|
||||||
|
{ label: '风险编号', prop: 'riskNo', minWidth: 210 },
|
||||||
|
{ label: '规则名称', prop: 'ruleName', minWidth: 210 },
|
||||||
|
{ label: '运单号', prop: 'waybillNo', minWidth: 220, link: true },
|
||||||
|
{ label: '项目名称', prop: 'projectName', minWidth: 160 },
|
||||||
|
{ label: '风险等级', prop: 'riskLevelName', minWidth: 130, tag: true },
|
||||||
|
{ label: '运输类型', prop: 'transportType', minWidth: 130 },
|
||||||
|
{ label: '风险描述', prop: 'riskDescription', minWidth: 310 },
|
||||||
|
{ label: '处理状态', prop: 'disposalStatusName', minWidth: 130, tag: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const detailFields = [
|
||||||
|
{ label: '风险编号', prop: 'riskNo' },
|
||||||
|
{ label: '规则名称', prop: 'ruleName' },
|
||||||
|
{ label: '风险等级', prop: 'riskLevelName', tag: true },
|
||||||
|
{ label: '运单号', prop: 'waybillNo', link: true },
|
||||||
|
{ label: '项目名称', prop: 'projectName' },
|
||||||
|
{ label: '触发时间', prop: 'triggerTime' },
|
||||||
|
{ label: '风险描述', prop: 'riskDescription', span: 3 },
|
||||||
|
];
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export const queryTypeOptions = [
|
||||||
|
{ label: '轨迹查询', value: 'track' },
|
||||||
|
{ label: '船舶查询', value: 'ship' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mapInfoFields = [
|
||||||
|
{ label: '站点名称', prop: 'siteName' },
|
||||||
|
{ label: '地址', prop: 'address' },
|
||||||
|
{ label: '经纬度', prop: 'coordinates' },
|
||||||
|
];
|
||||||
@@ -27,7 +27,7 @@ export function isEmail(s) {
|
|||||||
* @param {*} s
|
* @param {*} s
|
||||||
*/
|
*/
|
||||||
export function isMobile(s) {
|
export function isMobile(s) {
|
||||||
return /^1[0-9]{10}$/.test(s);
|
return /^1[3-9]\d{9}$/.test(String(s || '').trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ import { createOption } from '@/option/base/common-address';
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
import { getToken } from '@/utils/auth';
|
import { getToken } from '@/utils/auth';
|
||||||
|
import { isMobile } from '@/utils/validate';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
import 'nprogress/nprogress.css';
|
import 'nprogress/nprogress.css';
|
||||||
|
|
||||||
@@ -830,12 +831,8 @@ export default {
|
|||||||
this.$message.warning('联系人不能超过50个字');
|
this.$message.warning('联系人不能超过50个字');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (row.contactPhone && row.contactPhone.length > 20) {
|
if (row.contactPhone && !isMobile(row.contactPhone)) {
|
||||||
this.$message.warning('联系方式不能超过20个字');
|
this.$message.warning('联系方式格式不正确');
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (row.contactPhone && !/^\d+$/.test(row.contactPhone)) {
|
|
||||||
this.$message.warning('联系方式只能输入数字');
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (row.remark && row.remark.length > 200) {
|
if (row.remark && row.remark.length > 200) {
|
||||||
|
|||||||
@@ -322,6 +322,7 @@ import { config, excelOption, option } from '@/option/business/common-route';
|
|||||||
import { getToken } from '@/utils/auth';
|
import { getToken } from '@/utils/auth';
|
||||||
import { openImportDialog } from '@/utils/import-excel';
|
import { openImportDialog } from '@/utils/import-excel';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
|
import { isMobile } from '@/utils/validate';
|
||||||
import { List, Search } from '@element-plus/icons-vue';
|
import { List, Search } from '@element-plus/icons-vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
@@ -789,6 +790,14 @@ export default {
|
|||||||
this.$message.warning('收货地址不能超过255个字符');
|
this.$message.warning('收货地址不能超过255个字符');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const invalidPhone = [
|
||||||
|
[row.departurePhone, '发货联系方式'],
|
||||||
|
[row.arrivalPhone, '收货联系方式'],
|
||||||
|
].find(([value]) => value && !isMobile(value));
|
||||||
|
if (invalidPhone) {
|
||||||
|
this.$message.warning(`${invalidPhone[1]}格式不正确`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (row.remark && row.remark.length > 500) {
|
if (row.remark && row.remark.length > 500) {
|
||||||
this.$message.warning('备注不能超过500个字符');
|
this.$message.warning('备注不能超过500个字符');
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,19 +2,24 @@
|
|||||||
<div v-loading="loading" class="master-detail">
|
<div v-loading="loading" class="master-detail">
|
||||||
<section v-if="master" class="detail-overview">
|
<section v-if="master" class="detail-overview">
|
||||||
<div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)">{{ statusName(master.businessStatus) }}</el-tag></div><el-button @click="$emit('back')">返回</el-button></div>
|
<div class="detail-heading"><div><h2>多联总单详情 <span>|</span> {{ master.masterNo }}</h2><el-tag :type="statusType(master.businessStatus)">{{ statusName(master.businessStatus) }}</el-tag></div><el-button @click="$emit('back')">返回</el-button></div>
|
||||||
<dl class="detail-meta"><div><dt>客户</dt><dd>{{ master.customerName || '-' }}</dd></div><div><dt>合同编号</dt><dd>{{ master.contractNo || '-' }}</dd></div><div><dt>项目</dt><dd>{{ master.projectName || '-' }}</dd></div><div><dt>货物名称</dt><dd>{{ goodsNames }}</dd></div><div><dt>货物类型</dt><dd>{{ goodsTypes }}</dd></div><div><dt>运输周期</dt><dd>{{ dateRange }}</dd></div></dl>
|
<dl class="detail-meta"><div><dt>客户</dt><dd>{{ master.customerName || '-' }}</dd></div><div><dt>合同编号</dt><dd>{{ master.contractNo || '-' }}</dd></div><div><dt>项目</dt><dd>{{ master.projectName || '-' }}</dd></div><div class="detail-meta__attachments"><dt>附件</dt><dd><template v-if="attachments.length"><el-link v-for="file in attachments" :key="file.url || file.link || file.name || file.originalName" type="primary" @click="previewAttachment(file)">{{ attachmentName(file) }}</el-link></template><span v-else>-</span></dd></div></dl>
|
||||||
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ route.departureName || '-' }}</strong><small>{{ route.departureAddress || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ route.arrivalName || '-' }}</strong><small>{{ route.arrivalAddress || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }}</small></div></template></div>
|
<div class="route-map"><template v-for="(route, index) in segments" :key="route.segmentNo"><div class="route-map__node"><span :class="['route-badge', index ? 'middle' : 'start']">{{ index ? '经' : '起' }}</span><strong>{{ route.departureName || '-' }}</strong><small>{{ route.departureAddress || '-' }}</small><small class="route-map__progress">{{ index ? `已到达 ${quantity(segments[index - 1].arrivedQuantity)}/${quantity(master.totalQuantity)}` : `已调度 ${quantity(route.dispatchedQuantity)}/${quantity(master.totalQuantity)}` }}</small><small v-if="index" class="route-map__progress">已调度 {{ quantity(route.dispatchedQuantity) }}/{{ quantity(master.totalQuantity) }}</small></div><div class="route-map__line"></div><div v-if="index === segments.length - 1" class="route-map__node"><span class="route-badge end">终</span><strong>{{ route.arrivalName || '-' }}</strong><small>{{ route.arrivalAddress || '-' }}</small><small class="route-map__progress">已到达 {{ quantity(route.arrivedQuantity) }}/{{ quantity(master.totalQuantity) }}</small></div></template></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<el-dialog v-model="documentPreviewVisible" :title="previewFile.name || '附件预览'" append-to-body destroy-on-close width="90%" top="4vh">
|
||||||
|
<open-file-viewer v-if="documentPreviewVisible && previewFile.url" :file="previewFile.url" :file-name="previewFile.name" :mime-type="previewFile.mimeType" width="100%" height="72vh" fit="contain" theme="auto" locale="zh-CN" :toolbar="viewerToolbar" :plugins="viewerPlugins" />
|
||||||
|
</el-dialog>
|
||||||
|
<el-image-viewer v-if="imagePreviewVisible" :url-list="imagePreviewUrls" :initial-index="imagePreviewIndex" @close="imagePreviewVisible = false" />
|
||||||
|
|
||||||
<section v-if="master" class="execution-detail-card">
|
<section v-if="master" class="execution-detail-card">
|
||||||
<div class="execution-detail-heading"><h3>分段执行明细</h3></div>
|
<div class="execution-detail-heading"><h3>分段执行明细</h3></div>
|
||||||
<section v-for="(route, index) in segments" :key="route.segmentNo" class="segment-detail">
|
<section v-for="(route, index) in segments" :key="route.segmentNo" class="segment-detail">
|
||||||
<header @click="toggleSegment(route.segmentNo)"><h3><span class="segment-index">{{ segmentNumber(route, index) }}</span><span>{{ route.departureName || '-' }} → {{ route.arrivalName || '-' }}</span></h3><div class="segment-detail__header-right"><span>已调度 <em>{{ quantity(route.dispatchedQuantity) }}</em> / {{ quantity(master.totalQuantity) }},已到达 <em>{{ quantity(route.arrivedQuantity) }}</em> / {{ quantity(master.totalQuantity) }}</span><el-tooltip :content="isSegmentExpanded(route.segmentNo) ? '折叠' : '展开'" placement="top"><el-button circle :icon="isSegmentExpanded(route.segmentNo) ? ArrowUp : ArrowDown" @click.stop="toggleSegment(route.segmentNo)" /></el-tooltip></div></header>
|
<header @click="toggleSegment(route.segmentNo)"><h3><span class="segment-index">{{ segmentNumber(route, index) }}</span><span class="segment-detail__transport-type">{{ segmentTransportType(route) || '-' }}</span><span class="segment-detail__progress-bar"><span :style="{ width: `${segmentProgress(route)}%` }"></span></span><span>{{ route.departureName || '-' }} → {{ route.arrivalName || '-' }}</span><span class="segment-detail__progress">已调度 <em>{{ quantity(route.dispatchedQuantity) }}</em> / {{ quantity(plannedQuantity(route)) }} {{ plannedUnit(route) }}</span></h3><div class="segment-detail__header-right"><el-tooltip :content="isSegmentExpanded(route.segmentNo) ? '折叠' : '展开'" placement="top"><el-button circle :icon="isSegmentExpanded(route.segmentNo) ? ArrowUp : ArrowDown" @click.stop="toggleSegment(route.segmentNo)" /></el-tooltip></div></header>
|
||||||
<div v-show="isSegmentExpanded(route.segmentNo)" class="segment-execution">
|
<div v-show="isSegmentExpanded(route.segmentNo)" class="segment-execution">
|
||||||
<div class="segment-stats"><div><span>总量</span><strong>{{ quantity(master.totalQuantity) }}<small>吨</small></strong></div><div><span>已调度</span><strong class="dispatched">{{ quantity(route.dispatchedQuantity) }}<small>吨</small></strong></div><div><span>剩余</span><strong class="remaining">{{ quantity(remainingQuantity(route)) }}<small>吨</small></strong></div></div>
|
<div class="segment-stats"><div><span>总量</span><strong>{{ quantity(master.totalQuantity) }}<small>吨</small></strong></div><div><span>已调度</span><strong class="dispatched">{{ quantity(route.dispatchedQuantity) }}<small>吨</small></strong></div><div><span>剩余</span><strong class="remaining">{{ quantity(remainingQuantity(route)) }}<small>吨</small></strong></div></div>
|
||||||
<el-table :data="route.waybills || []" border class="waybill-table"><el-table-column label="运单号" min-width="180"><template #default="{ row }"><el-link type="primary" @click="openWaybill(row)">{{ row.waybillNo }}</el-link></template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="190" /><el-table-column label="司机/车牌" min-width="180"><template #default="{ row }">{{ driverVehicle(row) }}</template></el-table-column><el-table-column prop="cargoType" label="货物类型" min-width="120" /><el-table-column label="数量(吨)" width="130"><template #default="{ row }">{{ quantity(row.quantity) }}</template></el-table-column><el-table-column label="状态" width="130"><template #default="{ row }"><el-tag :type="waybillStatusType(row.businessStatus)" size="small">{{ waybillStatusName(row.businessStatus) }}</el-tag></template></el-table-column><el-table-column prop="createTime" label="创建时间" min-width="180" /></el-table>
|
<el-table :data="route.waybills || []" border class="waybill-table"><el-table-column label="运单号" min-width="180"><template #default="{ row }"><el-link type="primary" @click="openWaybill(row)">{{ row.waybillNo }}</el-link></template></el-table-column><el-table-column prop="carrierName" label="承运商" min-width="190" /><el-table-column label="司机/车牌" min-width="180"><template #default="{ row }">{{ driverVehicle(row) }}</template></el-table-column><el-table-column prop="cargoType" label="货物类型" min-width="120" /><el-table-column label="数量(吨)" width="130"><template #default="{ row }">{{ quantity(row.quantity) }}</template></el-table-column><el-table-column label="状态" width="130"><template #default="{ row }"><el-tag :type="waybillStatusType(row.businessStatus)" size="small">{{ waybillStatusName(row.businessStatus) }}</el-tag></template></el-table-column><el-table-column prop="createTime" label="创建时间" min-width="180" /></el-table>
|
||||||
<el-empty v-if="!(route.waybills || []).length" description="暂无运单明细" :image-size="56" />
|
<el-empty v-if="!(route.waybills || []).length" description="暂无运单明细" :image-size="56" />
|
||||||
<div v-if="(route.transportPlans || []).length" class="transport-plan-detail"><h4>关联计划单</h4><el-table :data="route.transportPlans" border><el-table-column label="计划单号" min-width="160"><template #default="{ row }"><el-link type="primary" @click="openPlan(row)">{{ row.planNo }}</el-link></template></el-table-column><el-table-column prop="planName" label="计划名称" min-width="180" /><el-table-column prop="transportType" label="运输类型" width="130" /><el-table-column prop="planStartDate" label="计划开始日期" width="130" /><el-table-column prop="planEndDate" label="计划结束日期" width="130" /><el-table-column prop="businessStatus" label="状态" width="110" /></el-table></div>
|
<div v-if="(route.transportPlans || []).length" class="transport-plan-detail"><h4>关联计划单</h4><el-table :data="route.transportPlans" border><el-table-column label="计划单号" min-width="160"><template #default="{ row }"><el-link type="primary" @click="openPlan(row)">{{ row.planNo }}</el-link></template></el-table-column><el-table-column prop="planName" label="计划名称" min-width="180" /><el-table-column prop="transportType" label="运输方式" width="130" /><el-table-column prop="planStartDate" label="计划开始日期" width="130" /><el-table-column prop="planEndDate" label="计划结束日期" width="130" /><el-table-column prop="businessStatus" label="状态" width="110" /></el-table></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
@@ -29,13 +34,19 @@
|
|||||||
<script>
|
<script>
|
||||||
import * as api from '@/api/business/master-order';
|
import * as api from '@/api/business/master-order';
|
||||||
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
||||||
import SectionCard from '@/components/section-card/main.vue';
|
import { ElImageViewer } from 'element-plus';
|
||||||
|
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||||
|
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||||||
|
import '@open-file-viewer/core/style.css';
|
||||||
|
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||||
|
|
||||||
|
const viewerPlugins = [imagePlugin(), pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }), officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }), textPlugin(), fallbackPlugin()];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { SectionCard },
|
components: { ElImageViewer, OpenFileViewer },
|
||||||
props: { id: [String, Number] },
|
props: { id: [String, Number] },
|
||||||
emits: ['back'],
|
emits: ['back'],
|
||||||
data() { return { loading: false, master: null, expandedSegments: {}, ArrowDown, ArrowUp }; },
|
data() { return { loading: false, master: null, expandedSegments: {}, ArrowDown, ArrowUp, imagePreviewVisible: false, imagePreviewUrls: [], imagePreviewIndex: 0, documentPreviewVisible: false, previewFile: {}, viewerPlugins, viewerToolbar: { download: true, fullscreen: true, print: true, rotate: true, zoom: true } }; },
|
||||||
computed: {
|
computed: {
|
||||||
segments() {
|
segments() {
|
||||||
const routes = this.master?.routeProgress || this.master?.routes || [];
|
const routes = this.master?.routeProgress || this.master?.routes || [];
|
||||||
@@ -44,6 +55,7 @@ export default {
|
|||||||
return { ...route, departureName: previous?.departureName, departureAddress: previous?.departureAddress, arrivalName: route.departureName || this.master?.arrivalName, arrivalAddress: route.departureAddress || this.master?.arrivalAddress };
|
return { ...route, departureName: previous?.departureName, departureAddress: previous?.departureAddress, arrivalName: route.departureName || this.master?.arrivalName, arrivalAddress: route.departureAddress || this.master?.arrivalAddress };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
attachments() { const value = this.master?.attachmentsJson; if (Array.isArray(value)) return value; if (!value) return []; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : []; } catch (error) { return []; } },
|
||||||
goodsNames() { return (this.master?.goods || []).map(item => item.cargoName).filter(Boolean).join('、') || '-'; },
|
goodsNames() { return (this.master?.goods || []).map(item => item.cargoName).filter(Boolean).join('、') || '-'; },
|
||||||
goodsTypes() { return [...new Set((this.master?.goods || []).map(item => item.cargoType).filter(Boolean))].join('、') || '-'; },
|
goodsTypes() { return [...new Set((this.master?.goods || []).map(item => item.cargoType).filter(Boolean))].join('、') || '-'; },
|
||||||
dateRange() { return this.master?.planStartTime && this.master?.planEndTime ? `${String(this.master.planStartTime).slice(0, 10)} ~ ${String(this.master.planEndTime).slice(0, 10)}` : '-'; },
|
dateRange() { return this.master?.planStartTime && this.master?.planEndTime ? `${String(this.master.planStartTime).slice(0, 10)} ~ ${String(this.master.planEndTime).slice(0, 10)}` : '-'; },
|
||||||
@@ -65,6 +77,26 @@ export default {
|
|||||||
isSegmentExpanded(segmentNo) { return Boolean(this.expandedSegments[segmentNo]); },
|
isSegmentExpanded(segmentNo) { return Boolean(this.expandedSegments[segmentNo]); },
|
||||||
toggleSegment(segmentNo) { this.expandedSegments = { ...this.expandedSegments, [segmentNo]: !this.isSegmentExpanded(segmentNo) }; },
|
toggleSegment(segmentNo) { this.expandedSegments = { ...this.expandedSegments, [segmentNo]: !this.isSegmentExpanded(segmentNo) }; },
|
||||||
remainingQuantity(route) { return Math.max(0, Number(this.master?.totalQuantity || 0) - Number(route.dispatchedQuantity || 0)); },
|
remainingQuantity(route) { return Math.max(0, Number(this.master?.totalQuantity || 0) - Number(route.dispatchedQuantity || 0)); },
|
||||||
|
segmentTransportType(route = {}) { return route.transportTypeName || route.transportType || ''; },
|
||||||
|
plannedQuantity(route = {}) { return route.planQuantity || route.totalQuantity || this.master?.totalQuantity || 0; },
|
||||||
|
plannedUnit(route = {}) { return route.quantityUnit || this.master?.goods?.[0]?.quantityUnit || '吨'; },
|
||||||
|
segmentProgress(route = {}) { const planned = Number(this.plannedQuantity(route)); return planned > 0 ? Math.min(100, Math.max(0, (Number(route.dispatchedQuantity || 0) / planned) * 100)) : 0; },
|
||||||
|
attachmentUrl(file = {}) { return file.url || file.link || file.fileUrl || file.downloadUrl || file.domain || ''; },
|
||||||
|
attachmentName(file = {}) { return file.originalName || file.name || file.fileName || '附件'; },
|
||||||
|
attachmentExtension(file = {}) { const source = this.attachmentName(file).split('?')[0]; const index = source.lastIndexOf('.'); return index > -1 ? source.slice(index + 1).toLowerCase() : ''; },
|
||||||
|
isImageAttachment(file) { return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(file)); },
|
||||||
|
previewAttachment(file) {
|
||||||
|
const url = this.attachmentUrl(file);
|
||||||
|
if (!url) return this.$message.warning('附件地址为空,无法预览');
|
||||||
|
if (this.isImageAttachment(file)) {
|
||||||
|
this.imagePreviewUrls = this.attachments.filter(item => this.isImageAttachment(item)).map(item => this.attachmentUrl(item)).filter(Boolean);
|
||||||
|
this.imagePreviewIndex = Math.max(this.imagePreviewUrls.indexOf(url), 0);
|
||||||
|
this.imagePreviewVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.previewFile = { name: this.attachmentName(file), url, mimeType: file.mimeType || file.contentType || '' };
|
||||||
|
this.documentPreviewVisible = true;
|
||||||
|
},
|
||||||
driverVehicle(row) { return [row.driverName, row.vehicleNo].filter(Boolean).join(' / ') || '-'; },
|
driverVehicle(row) { return [row.driverName, row.vehicleNo].filter(Boolean).join(' / ') || '-'; },
|
||||||
openWaybill(row) { this.$router.push({ path: '/business/waybill-manage', query: { detailId: row.id } }); },
|
openWaybill(row) { this.$router.push({ path: '/business/waybill-manage', query: { detailId: row.id } }); },
|
||||||
openPlan(row) { this.$router.push({ path: '/business/transport-plan', query: { detailId: row.id } }); },
|
openPlan(row) { this.$router.push({ path: '/business/transport-plan', query: { detailId: row.id } }); },
|
||||||
@@ -80,14 +112,18 @@ export default {
|
|||||||
.master-detail { padding-bottom: 20px; color: #303133; }
|
.master-detail { padding-bottom: 20px; color: #303133; }
|
||||||
.detail-overview, .execution-detail-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
.detail-overview, .execution-detail-card { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
||||||
.detail-heading { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
|
.detail-heading { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; h2 { display: inline-block; margin: 0 16px 0 0; font-size: 20px; } h2 span { margin: 0 8px; color: #909399; font-weight: 400; } }
|
||||||
.detail-meta { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
|
.detail-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 24px; margin: 0; padding: 0 24px 20px; dt { margin-bottom: 6px; color: #909399; font-size: 13px; } dd { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 0; color: #409eff; font-size: 14px; word-break: break-all; } }
|
||||||
.route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; }
|
.route-map { display: flex; align-items: flex-start; padding: 16px 24px 20px; border-top: 1px solid #eff1f7; overflow-x: auto; }
|
||||||
.route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
|
.route-map__node { display: grid; flex: 0 0 150px; justify-items: center; gap: 6px; text-align: center; strong { font-size: 16px; white-space: nowrap; } small { color: #606266; white-space: nowrap; } }
|
||||||
.route-map__progress { color: #409eff !important; }
|
.route-map__progress { color: #409eff !important; }
|
||||||
.route-badge { display: inline-flex; width: 32px; height: 32px; align-items: center; justify-content: center; border-radius: 4px; background: #67c23a; color: #fff; font-weight: 600; &.start { background: #409eff; } &.end { background: #e6a23c; } }
|
.route-badge { display: inline-flex; width: 32px; height: 32px; align-items: center; justify-content: center; border-radius: 4px; background: #67c23a; color: #fff; font-weight: 600; &.start { background: #409eff; } &.end { background: #e6a23c; } }
|
||||||
.route-map__line { flex: 1 0 64px; min-width: 64px; height: 32px; border-bottom: 2px solid #606266; }
|
.route-map__line { flex: 1 0 64px; min-width: 64px; height: 32px; border-bottom: 2px solid #606266; }
|
||||||
.execution-detail-heading { padding: 20px 24px 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
.execution-detail-heading { padding: 20px 24px 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
||||||
.segment-detail { padding: 16px 24px; border-top: 1px solid #eff1f7; header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; cursor: pointer; h3 { display: flex; align-items: center; gap: 12px; margin: 0; font-size: 16px; } em { color: #409eff; font-style: normal; } } }
|
.segment-detail { padding: 16px 24px; border-top: 1px solid #eff1f7; header { display: flex; align-items: center; justify-content: space-between; margin: -16px -24px 8px; padding: 12px 24px; cursor: pointer; background: #fafbfc; h3 { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 12px; margin: 0; font-size: 16px; } em { color: #409eff; font-style: normal; } } }
|
||||||
|
.segment-detail__progress { color: #606266; font-size: 14px; font-weight: 400; }
|
||||||
|
.segment-detail__transport-type { color: #409eff; font-size: 14px; font-weight: 500; }
|
||||||
|
.segment-detail__progress-bar { display: inline-flex; width: 72px; height: 6px; overflow: hidden; border-radius: 3px; background: #e4e7ed; }
|
||||||
|
.segment-detail__progress-bar span { display: block; height: 100%; background: #409eff; transition: width 0.2s ease; }
|
||||||
.segment-detail__header-right { display: flex; align-items: center; gap: 12px; }
|
.segment-detail__header-right { display: flex; align-items: center; gap: 12px; }
|
||||||
.segment-index { display: inline-flex; width: 36px; height: 36px; align-items: center; justify-content: center; border-radius: 50%; background: #2088ee; color: #fff; font-size: 20px; font-weight: 500; }
|
.segment-index { display: inline-flex; width: 36px; height: 36px; align-items: center; justify-content: center; border-radius: 50%; background: #2088ee; color: #fff; font-size: 20px; font-weight: 500; }
|
||||||
.segment-execution { padding-top: 12px; }
|
.segment-execution { padding-top: 12px; }
|
||||||
|
|||||||
@@ -41,13 +41,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<el-checkbox v-model="route.selected" :label="`${route.segmentNo}:${route.departureName} → ${route.arrivalName}`" />
|
<el-checkbox v-model="route.selected" :label="`${route.segmentNo}:${route.departureName} → ${route.arrivalName}`" />
|
||||||
</div>
|
</div>
|
||||||
<div>总量 {{ formatQuantity(totalQuantity) }},已调度 <em>{{ formatQuantity(route.dispatchedQuantity) }}</em>,剩余 <em>{{ formatQuantity(remaining(route)) }}</em></div>
|
<div>总量 {{ formatQuantity(totalQuantity) }},已调度 <em>{{ formatQuantity(dispatchedQuantity(route)) }}</em>,剩余 <em>{{ formatQuantity(remaining(route)) }}</em></div>
|
||||||
</header>
|
</header>
|
||||||
<div v-show="route.selected" class="segment-content">
|
<div v-show="route.selected" class="segment-content">
|
||||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form">
|
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form">
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="24">
|
<el-col :span="8">
|
||||||
<el-form-item label="运输类型" required class="transport-type-field"><el-input :model-value="route.transportType" readonly /></el-form-item>
|
<el-form-item label="运输方式" required class="transport-type-field"><el-input :model-value="route.transportType" readonly /></el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col v-if="isRoad(route)" :span="8">
|
<el-col v-if="isRoad(route)" :span="8">
|
||||||
<el-form-item label="单据类型" required>
|
<el-form-item label="单据类型" required>
|
||||||
@@ -56,13 +56,13 @@
|
|||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="12">
|
||||||
<el-form-item label="发货地址" required><el-input :model-value="addressText(route.departureName, route.departureAddress)" readonly /></el-form-item>
|
<el-form-item label="发货地址" required><el-input :model-value="addressText(route.departureName, route.departureAddress)" readonly /></el-form-item>
|
||||||
</el-col>
|
</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.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="联系方式"><el-input v-model="route.departurePhone" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col :span="4"><el-form-item :label="dateStartLabel(route)" required><el-date-picker v-model="route.estimatedStartTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></el-col>
|
<el-col :span="4"><el-form-item :label="dateStartLabel(route)" required><el-date-picker v-model="route.estimatedStartTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" /></el-form-item></el-col>
|
||||||
<el-col :span="8">
|
<el-col :span="12">
|
||||||
<el-form-item label="收货地址" required><el-input :model-value="addressText(route.arrivalName, route.arrivalAddress)" readonly /></el-form-item>
|
<el-form-item label="收货地址" required><el-input :model-value="addressText(route.arrivalName, route.arrivalAddress)" readonly /></el-form-item>
|
||||||
</el-col>
|
</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.arrivalContact" placeholder="请输入" /></el-form-item></el-col>
|
||||||
@@ -74,10 +74,10 @@
|
|||||||
<div class="goods-heading"><h3>货物信息</h3><div><el-link type="primary" @click="addGoods(route)">新增货物</el-link></div></div>
|
<div class="goods-heading"><h3>货物信息</h3><div><el-link type="primary" @click="addGoods(route)">新增货物</el-link></div></div>
|
||||||
<el-table :data="route.goods" border class="goods-table">
|
<el-table :data="route.goods" border class="goods-table">
|
||||||
<el-table-column type="index" label="序号" width="64" />
|
<el-table-column type="index" label="序号" width="64" />
|
||||||
<el-table-column label="货物名称" min-width="180"><template #default="{ row }"><el-select v-model="row.sourceIndex" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoods" :key="item.sourceIndex" :label="item.label" :value="item.sourceIndex" /></el-select></template></el-table-column>
|
<el-table-column label="货物类型" min-width="180"><template #default="{ row }"><el-cascader v-model="row.cargoTypePath" :options="cargoTypeOptions" :props="cargoTypeCascaderProps" placeholder="请选择货物类型" clearable filterable @change="value => handleCargoTypeChange(route, row, value)" /></template></el-table-column>
|
||||||
<el-table-column label="货物类型" min-width="130"><template #default="{ row }"><el-input :model-value="row.cargoType" readonly /></template></el-table-column>
|
<el-table-column label="货物名称" min-width="180"><template #default="{ row }"><el-select v-model="row.sourceIndex" placeholder="请选择总单货物" @change="selectGoods(route, row)"><el-option v-for="item in masterGoodsByCargoType(row)" :key="item.sourceIndex" :label="item.cargoName" :value="item.sourceIndex" /></el-select></template></el-table-column>
|
||||||
<el-table-column label="剩余数量" width="120"><template #default="{ row }">{{ formatQuantity(row.remainingQuantity) }}</template></el-table-column>
|
<el-table-column prop="remainingQuantity" column-key="remainingQuantity" label="剩余数量" width="150" class-name="goods-table__remaining"><template #default="{ row }">{{ formatQuantity(goodsRemainingQuantity(route, row)) }}</template></el-table-column>
|
||||||
<el-table-column label="本次数量" width="160"><template #default="{ row }"><el-input :model-value="row.dispatchQuantity" inputmode="decimal" placeholder="请输入" @input="value => handleDispatchQuantityInput(row, value)" /></template></el-table-column>
|
<el-table-column prop="dispatchQuantity" column-key="dispatchQuantity" label="本次数量" width="200" class-name="goods-table__dispatch"><template #default="{ row }"><el-input :model-value="row.dispatchQuantity" inputmode="decimal" placeholder="请输入" @input="value => handleDispatchQuantityInput(route, row, value)" /></template></el-table-column>
|
||||||
<el-table-column prop="quantityUnit" label="数量单位" width="110" />
|
<el-table-column prop="quantityUnit" label="数量单位" width="110" />
|
||||||
<el-table-column prop="packageType" label="包装" min-width="110" />
|
<el-table-column prop="packageType" label="包装" min-width="110" />
|
||||||
<el-table-column prop="brand" label="品牌" min-width="110" />
|
<el-table-column prop="brand" label="品牌" min-width="110" />
|
||||||
@@ -89,11 +89,31 @@
|
|||||||
|
|
||||||
<div class="freight-heading"><h3>运费信息</h3></div>
|
<div class="freight-heading"><h3>运费信息</h3></div>
|
||||||
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form freight-form">
|
<el-form :model="route" label-position="right" label-width="auto" class="dispatch-form freight-form">
|
||||||
<el-row :gutter="16">
|
<el-row v-for="(item, index) in route.freightItems" :key="item.sourceIndex" :gutter="16">
|
||||||
<el-col :span="6"><el-form-item label="单价"><el-input :model-value="route.unitPrice" inputmode="decimal" placeholder="请输入" @input="value => handleUnitPriceInput(route, value)" /></el-form-item></el-col>
|
<el-col :span="6">
|
||||||
<el-col :span="6"><el-form-item label="计价单位"><el-select v-model="route.priceUnit"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /></el-select></el-form-item></el-col>
|
<el-form-item :label="`单价${index + 1}`" required>
|
||||||
<el-col :span="6"><el-form-item label="重量合计"><el-input :model-value="formatQuantity(dispatchWeight(route))" readonly><template #append>吨</template></el-input></el-form-item></el-col>
|
<el-input :model-value="item.unitPrice" inputmode="decimal" placeholder="请输入" @input="value => handleFreightUnitPriceInput(item, value)">
|
||||||
<el-col :span="6"><el-form-item label="运费"><el-input :model-value="formatAmount(freightAmount(route))" readonly><template #append>元</template></el-input></el-form-item></el-col>
|
<template #append><el-select v-model="item.priceUnit" class="freight-unit-select"><el-option label="元/吨" value="元/吨" /><el-option label="元/件" value="元/件" /><el-option label="元/方" value="元/方" /></el-select></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item :label="`数量${index + 1}`" required>
|
||||||
|
<el-input :model-value="formatQuantity(item.quantity)" readonly>
|
||||||
|
<template #append><el-select v-model="item.quantityUnit" class="freight-unit-select" @change="value => handleFreightQuantityUnitChange(route, item, value)"><el-option v-for="unit in quantityUnitOptions" :key="unit" :label="unit" :value="unit" /></el-select></template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-form-item :label="`运费${index + 1}`">
|
||||||
|
<el-input :model-value="formatAmount(freightAmount(item))" readonly><template #suffix>{{ currencyLabel(route.currency) }}</template></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row v-if="route.freightItems.length" :gutter="16">
|
||||||
|
<el-col :span="6"><el-form-item label="运费合计"><el-input :model-value="formatAmount(freightTotal(route))" readonly><template #suffix>{{ currencyLabel(route.currency) }}</template></el-input></el-form-item></el-col>
|
||||||
|
<el-col :span="6"><el-form-item label="其他费用合计"><el-input :model-value="route.otherFeeTotal" inputmode="decimal" placeholder="请输入" @input="value => handleOtherFeeTotalInput(route, value)" /></el-form-item></el-col>
|
||||||
|
<el-col :span="6"><el-form-item label="币种"><el-select v-model="route.currency"><el-option label="人民币" value="CNY" /><el-option label="美元" value="USD" /><el-option label="欧元" value="EUR" /></el-select></el-form-item></el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
@@ -108,7 +128,7 @@
|
|||||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="挂车车牌号" required><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="挂车车牌号" required><el-input v-model="route.trailerVehicleNo" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人" required><el-input v-model="route.escortName" placeholder="请输入" /></el-form-item></el-col>
|
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人" required><el-input v-model="route.escortName" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号" required><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
|
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="押运人手机号" required><el-input v-model="route.escortPhone" placeholder="请输入" /></el-form-item></el-col>
|
||||||
<el-col v-if="route.carrierType !== '承运商'" :span="6"><el-form-item label="里程(km)" required><el-input :model-value="route.mileage" inputmode="decimal" placeholder="请输入" @input="value => handleMileageInput(route, value)" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="里程(km)" required><el-input :model-value="route.mileage" inputmode="numeric" maxlength="10" placeholder="请输入" @input="value => handleMileageInput(route, value)" /></el-form-item></el-col>
|
||||||
<el-col :span="6"><el-form-item label="备注"><el-input v-model="route.remark" maxlength="200" show-word-limit placeholder="请输入" /></el-form-item></el-col>
|
<el-col :span="6"><el-form-item label="备注"><el-input v-model="route.remark" maxlength="200" show-word-limit placeholder="请输入" /></el-form-item></el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -135,6 +155,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="pendingExpanded" class="pending-list-spacer" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -142,6 +163,7 @@
|
|||||||
import * as api from '@/api/business/master-order';
|
import * as api from '@/api/business/master-order';
|
||||||
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
import { getList as getCustomerList } from '@/api/vehicle/customer-archive';
|
||||||
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||||
|
import { isMobile } from '@/utils/validate';
|
||||||
|
|
||||||
const unwrapRecords = res => {
|
const unwrapRecords = res => {
|
||||||
const data = res?.data || res;
|
const data = res?.data || res;
|
||||||
@@ -156,7 +178,7 @@ export default {
|
|||||||
props: { id: [String, Number] },
|
props: { id: [String, Number] },
|
||||||
emits: ['back'],
|
emits: ['back'],
|
||||||
data() {
|
data() {
|
||||||
return { loading: false, submitting: false, master: null, routes: [], pending: [], pendingExpanded: false, editingId: null, driverInputs: {}, carrierOptions: [], driverOptions: [], carrierLoading: false, driverLoading: false };
|
return { loading: false, submitting: false, master: null, routes: [], pending: [], pendingExpanded: false, editingId: null, driverInputs: {}, carrierOptions: [], driverOptions: [], carrierLoading: false, driverLoading: false, cargoTypeOptions: [], cargoTypeCascaderProps: { label: 'cargoName', value: 'id', children: 'children', emitPath: true }, quantityUnitOptions: ['吨', '千克', '立方米', '件', '车', '箱', '托盘'] };
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
totalQuantity() { return (this.master?.goods || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); },
|
totalQuantity() { return (this.master?.goods || []).reduce((sum, item) => sum + Number(item.quantity || 0), 0); },
|
||||||
@@ -186,60 +208,285 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const res = await api.getDetail(this.id);
|
const res = await api.getDetail(this.id);
|
||||||
this.master = res.data?.data || res.data || res;
|
this.master = res.data?.data || res.data || res;
|
||||||
this.routes = (this.master.routes || []).map((node, index) => this.createRoute(node, index));
|
this.cargoTypeOptions = this.buildMasterCargoTypeOptions(this.master.goods || []);
|
||||||
|
this.routes = this.dispatchRouteNodes().map((node, index) => this.createRoute(node, index));
|
||||||
} finally { this.loading = false; }
|
} finally { this.loading = false; }
|
||||||
},
|
},
|
||||||
|
dispatchRouteNodes() {
|
||||||
|
const routes = Array.isArray(this.master?.routes) ? this.master.routes : [];
|
||||||
|
if (routes.length) return routes;
|
||||||
|
return [{
|
||||||
|
segmentNo: '段1',
|
||||||
|
transportType: this.master?.transportType || '',
|
||||||
|
departureName: this.master?.arrivalName || '',
|
||||||
|
departureAddress: this.master?.arrivalAddress || '',
|
||||||
|
departureContact: this.master?.arrivalContact || '',
|
||||||
|
departurePhone: this.master?.arrivalPhone || '',
|
||||||
|
}];
|
||||||
|
},
|
||||||
createRoute(node, index) {
|
createRoute(node, index) {
|
||||||
const previous = index ? this.master.routes[index - 1] : this.master;
|
const routeNodes = this.dispatchRouteNodes();
|
||||||
return {
|
const previous = index ? routeNodes[index - 1] : this.master;
|
||||||
|
const route = {
|
||||||
...node,
|
...node,
|
||||||
selected: false,
|
selected: false,
|
||||||
documentType: '运单',
|
documentType: '运单',
|
||||||
carrierType: '承运商',
|
carrierType: '承运商',
|
||||||
unitPrice: undefined,
|
currency: 'CNY',
|
||||||
priceUnit: '元/吨',
|
otherFeeTotal: '',
|
||||||
|
freightItems: [],
|
||||||
departureName: previous.departureName || '', departureAddress: previous.departureAddress || '', departureContact: previous.departureContact || '', departurePhone: previous.departurePhone || '',
|
departureName: previous.departureName || '', departureAddress: previous.departureAddress || '', departureContact: previous.departureContact || '', departurePhone: previous.departurePhone || '',
|
||||||
arrivalName: node.departureName || this.master.arrivalName || '', arrivalAddress: node.departureAddress || this.master.arrivalAddress || '', arrivalContact: node.departureContact || '', arrivalPhone: node.departurePhone || '',
|
arrivalName: node.departureName || this.master.arrivalName || '', arrivalAddress: node.departureAddress || this.master.arrivalAddress || '', arrivalContact: node.departureContact || '', arrivalPhone: node.departurePhone || '',
|
||||||
goods: [],
|
goods: (this.master.goods || []).map((goods, sourceIndex) => this.createGoodsRow(goods, sourceIndex)),
|
||||||
};
|
};
|
||||||
|
this.syncFreightItems(route);
|
||||||
|
return route;
|
||||||
|
},
|
||||||
|
buildMasterCargoTypeOptions(goods = []) {
|
||||||
|
return [...new Set(goods.map(item => item.cargoType).filter(Boolean))].map(cargoType => ({
|
||||||
|
id: cargoType,
|
||||||
|
cargoName: cargoType,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
masterGoodsByCargoType(row = {}) {
|
||||||
|
return this.masterGoods.filter(item => !row.cargoType || item.cargoType === row.cargoType);
|
||||||
|
},
|
||||||
|
findCargoTypeByPath(path = []) {
|
||||||
|
let options = this.cargoTypeOptions;
|
||||||
|
let selected;
|
||||||
|
(path || []).forEach(id => {
|
||||||
|
selected = (options || []).find(item => String(item.id) === String(id));
|
||||||
|
options = selected?.children || [];
|
||||||
|
});
|
||||||
|
return selected;
|
||||||
|
},
|
||||||
|
findCargoTypePath(cargoType, options = this.cargoTypeOptions, parentPath = []) {
|
||||||
|
for (const option of options || []) {
|
||||||
|
const path = [...parentPath, option.id];
|
||||||
|
if (option.cargoName === cargoType) return path;
|
||||||
|
if (option.children?.length) {
|
||||||
|
const result = this.findCargoTypePath(cargoType, option.children, path);
|
||||||
|
if (result.length) return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
handleCargoTypeChange(route, row, value) {
|
||||||
|
const path = Array.isArray(value) ? value : [];
|
||||||
|
const cargoType = this.findCargoTypeByPath(path);
|
||||||
|
row.cargoTypePath = path;
|
||||||
|
row.cargoType = cargoType?.cargoName || '';
|
||||||
|
const firstGoods = this.masterGoodsByCargoType(row)[0];
|
||||||
|
if (firstGoods) {
|
||||||
|
row.sourceIndex = firstGoods.sourceIndex;
|
||||||
|
this.selectGoods(route, row);
|
||||||
|
}
|
||||||
|
else Object.assign(row, { sourceIndex: undefined, cargoName: '', quantity: '', quantityUnit: '' });
|
||||||
|
this.syncFreightItems(route);
|
||||||
},
|
},
|
||||||
isRoad(route) { return String(route.transportType || '').includes('公路'); },
|
isRoad(route) { return String(route.transportType || '').includes('公路'); },
|
||||||
remaining(route) { return Math.max(0, this.totalQuantity - Number(route.dispatchedQuantity || 0)); },
|
dispatchedQuantity(route) {
|
||||||
|
return this.quantityNumber(route.dispatchedQuantity) + this.pendingSegmentQuantity(route);
|
||||||
|
},
|
||||||
|
remaining(route) {
|
||||||
|
return Math.max(
|
||||||
|
0,
|
||||||
|
this.totalQuantity -
|
||||||
|
this.dispatchedQuantity(route)
|
||||||
|
);
|
||||||
|
},
|
||||||
formatQuantity(value) { const number = Number(value || 0); return Number.isInteger(number) ? number : number.toFixed(3).replace(/\.?0+$/, ''); },
|
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); },
|
formatAmount(value) { return Number(value || 0).toFixed(2); },
|
||||||
dispatchWeight(route) { return (route.goods || []).reduce((sum, item) => sum + Number(item.dispatchQuantity || 0), 0); },
|
dispatchWeight(route) { return (route.goods || []).reduce((sum, item) => sum + Number(item.dispatchQuantity || 0), 0); },
|
||||||
freightAmount(route) { return Number(route.unitPrice || 0) * this.dispatchWeight(route); },
|
freightAmount(item = {}) { return Number(item.unitPrice || 0) * Number(item.quantity || 0); },
|
||||||
handleDispatchQuantityInput(row, value) {
|
freightTotal(route) {
|
||||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
const goodsFreight = (route.freightItems || []).reduce(
|
||||||
row.dispatchQuantity = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 3)}` : integer;
|
(sum, item) => sum + this.freightAmount(item),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return goodsFreight + Number(route.otherFeeTotal || 0);
|
||||||
},
|
},
|
||||||
handleUnitPriceInput(route, value) {
|
buildFreightJson(route) {
|
||||||
|
return JSON.stringify({
|
||||||
|
currency: route.currency || 'CNY',
|
||||||
|
totalFreightAmount: this.freightTotal(route),
|
||||||
|
otherFreightAmount: route.otherFeeTotal || '',
|
||||||
|
freightItems: (route.freightItems || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
freightAmount: this.freightAmount(item),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
currencyLabel(value) { return ({ CNY: '人民币', USD: '美元', EUR: '欧元' })[value] || value || '人民币'; },
|
||||||
|
quantityNumber(value) { return Number(value || 0); },
|
||||||
|
goodsKey(goods = {}, includeUnit = true) {
|
||||||
|
return [
|
||||||
|
goods.cargoName || '',
|
||||||
|
goods.cargoType || '',
|
||||||
|
includeUnit ? goods.quantityUnit || '' : '',
|
||||||
|
].join('\u0000');
|
||||||
|
},
|
||||||
|
masterGoodsIndex(goods = {}) {
|
||||||
|
if (goods.sourceIndex !== undefined && goods.sourceIndex !== null && goods.sourceIndex !== '') {
|
||||||
|
return Number(goods.sourceIndex);
|
||||||
|
}
|
||||||
|
return (this.master?.goods || []).findIndex(item =>
|
||||||
|
item.cargoName === goods.cargoName &&
|
||||||
|
item.cargoType === goods.cargoType &&
|
||||||
|
(!goods.quantityUnit || !item.quantityUnit || item.quantityUnit === goods.quantityUnit)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isSameGoods(left = {}, right = {}) {
|
||||||
|
const leftIndex = this.masterGoodsIndex(left);
|
||||||
|
const rightIndex = this.masterGoodsIndex(right);
|
||||||
|
if (leftIndex >= 0 && rightIndex >= 0) return leftIndex === rightIndex;
|
||||||
|
return left.cargoName === right.cargoName && left.cargoType === right.cargoType && left.quantityUnit === right.quantityUnit;
|
||||||
|
},
|
||||||
|
dispatchedGoodsQuantity(route, row) {
|
||||||
|
const dispatchedGoods = route.dispatchedGoods || {};
|
||||||
|
return this.quantityNumber(
|
||||||
|
dispatchedGoods[this.goodsKey(row)] ?? dispatchedGoods[this.goodsKey(row, false)]
|
||||||
|
);
|
||||||
|
},
|
||||||
|
baseGoodsRemainingQuantity(route, row) {
|
||||||
|
return Math.max(0, this.quantityNumber(row.quantity) - this.dispatchedGoodsQuantity(route, row));
|
||||||
|
},
|
||||||
|
pendingSegmentQuantity(route) {
|
||||||
|
return this.pending.reduce((sum, item) => {
|
||||||
|
if (!this.isSameRouteSegment(route, item)) return sum;
|
||||||
|
return sum + this.quantityNumber(item.quantity);
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
pendingGoodsQuantity(route, row) {
|
||||||
|
const goodsIndex = this.masterGoodsIndex(row);
|
||||||
|
return this.pending.reduce((sum, item) => {
|
||||||
|
if (!this.isSameRouteSegment(route, item)) return sum;
|
||||||
|
const pendingGoodsIndex = this.masterGoodsIndex(item);
|
||||||
|
if (
|
||||||
|
(goodsIndex >= 0 && pendingGoodsIndex >= 0 && pendingGoodsIndex !== goodsIndex) ||
|
||||||
|
(goodsIndex < 0 && !this.isSameGoods(item, row))
|
||||||
|
) {
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
return sum + this.quantityNumber(item.quantity);
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
isSameRouteSegment(route = {}, item = {}) {
|
||||||
|
const routeSegment = route.segmentNo || route.relationNo || '';
|
||||||
|
const itemSegment = item.segmentNo || item.relationNo || '';
|
||||||
|
return String(routeSegment) === String(itemSegment);
|
||||||
|
},
|
||||||
|
editingGoodsQuantity(route, row) {
|
||||||
|
return (route.goods || []).reduce((sum, item) => {
|
||||||
|
if (item === row || !this.isSameGoods(item, row)) return sum;
|
||||||
|
return sum + this.quantityNumber(item.dispatchQuantity);
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
goodsRemainingQuantity(route, row) {
|
||||||
|
return Math.max(
|
||||||
|
0,
|
||||||
|
this.baseGoodsRemainingQuantity(route, row) -
|
||||||
|
this.pendingGoodsQuantity(route, row) -
|
||||||
|
this.editingGoodsQuantity(route, row)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
availableDispatchQuantity(route, row) { return this.goodsRemainingQuantity(route, row); },
|
||||||
|
handleDispatchQuantityInput(route, row, value) {
|
||||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||||
route.unitPrice = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
const nextValue = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 3)}` : integer;
|
||||||
|
const availableQuantity = this.availableDispatchQuantity(route, row);
|
||||||
|
if (Number(nextValue || 0) > availableQuantity) {
|
||||||
|
row.dispatchQuantity = this.formatQuantity(availableQuantity);
|
||||||
|
this.syncFreightItems(route);
|
||||||
|
this.$message.warning('本次数量不能超过剩余数量');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row.dispatchQuantity = nextValue;
|
||||||
|
this.syncFreightItems(route);
|
||||||
|
},
|
||||||
|
handleFreightUnitPriceInput(item, value) {
|
||||||
|
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||||
|
item.unitPrice = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||||
|
},
|
||||||
|
handleOtherFeeTotalInput(route, value) {
|
||||||
|
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||||
|
route.otherFeeTotal = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||||
|
},
|
||||||
|
validateFreightItems(route) {
|
||||||
|
for (const [index, item] of (route.freightItems || []).entries()) {
|
||||||
|
const fields = [
|
||||||
|
['unitPrice', '单价'],
|
||||||
|
['priceUnit', '单价单位'],
|
||||||
|
['quantity', '数量'],
|
||||||
|
['quantityUnit', '数量单位'],
|
||||||
|
];
|
||||||
|
const emptyField = fields.find(
|
||||||
|
([prop]) => item[prop] === undefined || item[prop] === null || item[prop] === ''
|
||||||
|
);
|
||||||
|
if (emptyField) {
|
||||||
|
this.$message.warning(`第${index + 1}条货物的${emptyField[1]}不能为空`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
handleFreightQuantityUnitChange(route, freightItem, value) {
|
||||||
|
const goods = (route.goods || []).find(item => String(item.sourceIndex) === String(freightItem.sourceIndex));
|
||||||
|
if (goods) goods.quantityUnit = value;
|
||||||
},
|
},
|
||||||
handleMileageInput(route, value) {
|
handleMileageInput(route, value) {
|
||||||
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
route.mileage = String(value || '').replace(/\D/g, '').slice(0, 10);
|
||||||
route.mileage = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
},
|
||||||
|
normalizeMileage(value) {
|
||||||
|
if (value === undefined || value === null || value === '') return '';
|
||||||
|
const mileage = Number(value);
|
||||||
|
return Number.isFinite(mileage) && mileage >= 0 ? String(Math.trunc(mileage)) : '';
|
||||||
},
|
},
|
||||||
addressText(name, address) { return [name, address].filter(Boolean).join(' / ') || '-'; },
|
addressText(name, address) { return [name, address].filter(Boolean).join(' / ') || '-'; },
|
||||||
contactText(name, phone) { return [name, phone].filter(Boolean).join(' - ') || '-'; },
|
contactText(name, phone) { return [name, phone].filter(Boolean).join(' - ') || '-'; },
|
||||||
dateStartLabel(route) { return route.documentType === '计划单' ? '计划开始日期' : '预计发货日期'; },
|
dateStartLabel(route) { return route.documentType === '计划单' ? '计划开始日期' : '预计发货日期'; },
|
||||||
dateEndLabel(route) { return route.documentType === '计划单' ? '计划结束日期' : '预计完成日期'; },
|
dateEndLabel(route) { return route.documentType === '计划单' ? '计划结束日期' : '预计完成日期'; },
|
||||||
createGoodsRow(goods = {}, sourceIndex, dispatchedGoods = {}) {
|
createGoodsRow(goods = {}, sourceIndex) {
|
||||||
return {
|
return {
|
||||||
...goods,
|
...goods,
|
||||||
sourceIndex,
|
sourceIndex,
|
||||||
remainingQuantity: Math.max(0, Number(goods.quantity || 0) - Number(dispatchedGoods[`${goods.cargoName || ''}\u0000${goods.cargoType || ''}`] || 0)),
|
cargoTypePath: this.findCargoTypePath(goods.cargoType),
|
||||||
dispatchQuantity: undefined,
|
dispatchQuantity: undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
addGoods(route) { route.goods.push(this.createGoodsRow()); },
|
addGoods(route) {
|
||||||
|
const firstGoods = this.masterGoods[0];
|
||||||
|
route.goods.push(
|
||||||
|
firstGoods
|
||||||
|
? this.createGoodsRow(firstGoods, firstGoods.sourceIndex)
|
||||||
|
: this.createGoodsRow()
|
||||||
|
);
|
||||||
|
this.syncFreightItems(route);
|
||||||
|
},
|
||||||
selectGoods(route, row) {
|
selectGoods(route, row) {
|
||||||
const source = (this.master.goods || [])[Number(row.sourceIndex)];
|
const source = (this.master.goods || [])[Number(row.sourceIndex)];
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
Object.assign(row, this.createGoodsRow(source, Number(row.sourceIndex), route.dispatchedGoods || {}));
|
Object.assign(row, this.createGoodsRow(source, Number(row.sourceIndex)));
|
||||||
|
this.syncFreightItems(route);
|
||||||
|
},
|
||||||
|
removeGoods(route, index) { route.goods.splice(index, 1); this.syncFreightItems(route); },
|
||||||
|
syncFreightItems(route) {
|
||||||
|
const oldItems = route.freightItems || [];
|
||||||
|
route.freightItems = (route.goods || [])
|
||||||
|
.filter(item => item.sourceIndex !== undefined && item.sourceIndex !== '')
|
||||||
|
.map(item => {
|
||||||
|
const old = oldItems.find(entry => String(entry.sourceIndex) === String(item.sourceIndex)) || {};
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
sourceIndex: item.sourceIndex,
|
||||||
|
cargoName: item.cargoName || '',
|
||||||
|
quantity: item.dispatchQuantity || '',
|
||||||
|
quantityUnit: item.quantityUnit || '',
|
||||||
|
unitPrice: old.unitPrice || '',
|
||||||
|
priceUnit: old.priceUnit || `元/${item.quantityUnit || '吨'}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
removeGoods(route, index) { route.goods.splice(index, 1); },
|
|
||||||
handleCarrierTypeChange(route, value) {
|
handleCarrierTypeChange(route, value) {
|
||||||
route.carrierType = value;
|
route.carrierType = value;
|
||||||
if (value === '承运商') {
|
if (value === '承运商') {
|
||||||
@@ -294,11 +541,14 @@ export default {
|
|||||||
addToPending(route, continueDispatch) {
|
addToPending(route, continueDispatch) {
|
||||||
const goods = route.goods.filter(item => Number(item.dispatchQuantity) > 0);
|
const goods = route.goods.filter(item => Number(item.dispatchQuantity) > 0);
|
||||||
if (!goods.length) return this.$message.warning('请填写本次数量');
|
if (!goods.length) return this.$message.warning('请填写本次数量');
|
||||||
if (goods.some(item => Number(item.dispatchQuantity) > Number(item.remainingQuantity))) return this.$message.warning('本次数量不能超过该货物的可调度数量');
|
if (goods.some(item => Number(item.dispatchQuantity) > this.availableDispatchQuantity(route, item))) return this.$message.warning('本次数量不能超过剩余数量');
|
||||||
|
this.syncFreightItems(route);
|
||||||
|
if (!this.validateFreightItems(route)) return;
|
||||||
if (!route.estimatedStartTime) return this.$message.warning(`请选择${this.dateStartLabel(route)}`);
|
if (!route.estimatedStartTime) return this.$message.warning(`请选择${this.dateStartLabel(route)}`);
|
||||||
if (!route.estimatedEndTime) return this.$message.warning(`请选择${this.dateEndLabel(route)}`);
|
if (!route.estimatedEndTime) return this.$message.warning(`请选择${this.dateEndLabel(route)}`);
|
||||||
|
if (!this.validateRoutePhones(route)) return;
|
||||||
if (route.documentType === '运单') {
|
if (route.documentType === '运单') {
|
||||||
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo)) return this.$message.warning('请填写承运商和车牌号');
|
if (route.carrierType === '承运商' && (!route.carrierName || !route.vehicleNo || !route.mileage)) return this.$message.warning('请填写承运商、车牌号和里程');
|
||||||
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo || !route.trailerVehicleNo || !route.escortName || !route.escortPhone || route.mileage === undefined || route.mileage === null || route.mileage === '')) return this.$message.warning('请补全自运或网货平台的车辆与人员信息');
|
if (route.carrierType !== '承运商' && (!route.driverName || !route.driverPhone || !route.vehicleNo || !route.trailerVehicleNo || !route.escortName || !route.escortPhone || route.mileage === undefined || route.mileage === null || route.mileage === '')) return this.$message.warning('请补全自运或网货平台的车辆与人员信息');
|
||||||
}
|
}
|
||||||
const batchNo = `${route.segmentNo}-${Date.now()}`;
|
const batchNo = `${route.segmentNo}-${Date.now()}`;
|
||||||
@@ -306,11 +556,11 @@ export default {
|
|||||||
id: `${route.segmentNo}-${item.cargoName}-${Date.now()}-${Math.random()}`,
|
id: `${route.segmentNo}-${item.cargoName}-${Date.now()}-${Math.random()}`,
|
||||||
batchNo,
|
batchNo,
|
||||||
segmentNo: route.segmentNo, relationNo: route.segmentNo, documentType: route.documentType, transportType: route.transportType, carrierType: route.carrierType,
|
segmentNo: route.segmentNo, relationNo: route.segmentNo, documentType: route.documentType, transportType: route.transportType, carrierType: route.carrierType,
|
||||||
carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, trailerVehicleNo: route.trailerVehicleNo, escortName: route.escortName, escortPhone: route.escortPhone, mileage: route.mileage, unitPrice: route.unitPrice, priceUnit: route.priceUnit, remark: route.remark,
|
carrierName: route.carrierName, driverName: route.driverName, driverPhone: route.driverPhone, vehicleNo: route.vehicleNo, trailerVehicleNo: route.trailerVehicleNo, escortName: route.escortName, escortPhone: route.escortPhone, mileage: this.normalizeMileage(route.mileage), unitPrice: this.freightItemsForGoods(route, item).unitPrice || '', priceUnit: this.freightItemsForGoods(route, item).priceUnit || '', currency: route.currency || 'CNY', freightAmount: this.freightAmount(this.freightItemsForGoods(route, item)), freightTotal: this.freightTotal(route), otherFeeTotal: route.otherFeeTotal || '', freightJson: this.buildFreightJson(route), remark: route.remark,
|
||||||
departureName: route.departureName, departureAddress: route.departureAddress, departureContact: route.departureContact, departurePhone: route.departurePhone,
|
departureName: route.departureName, departureAddress: route.departureAddress, departureContact: route.departureContact, departurePhone: route.departurePhone,
|
||||||
arrivalName: route.arrivalName, arrivalAddress: route.arrivalAddress, arrivalContact: route.arrivalContact, arrivalPhone: route.arrivalPhone,
|
arrivalName: route.arrivalName, arrivalAddress: route.arrivalAddress, arrivalContact: route.arrivalContact, arrivalPhone: route.arrivalPhone,
|
||||||
estimatedStartTime: route.estimatedStartTime, estimatedEndTime: route.estimatedEndTime,
|
estimatedStartTime: route.estimatedStartTime, estimatedEndTime: route.estimatedEndTime,
|
||||||
cargoName: item.cargoName, cargoType: item.cargoType, quantity: item.dispatchQuantity, quantityUnit: item.quantityUnit, packageType: item.packageType, brand: item.brand, specification: item.specification, model: item.model, materialCode: item.materialCode,
|
sourceIndex: this.masterGoodsIndex(item), cargoName: item.cargoName, cargoType: item.cargoType, quantity: item.dispatchQuantity, quantityUnit: item.quantityUnit, packageType: item.packageType, brand: item.brand, specification: item.specification, model: item.model, materialCode: item.materialCode,
|
||||||
}));
|
}));
|
||||||
this.editingId = null;
|
this.editingId = null;
|
||||||
this.pendingExpanded = true;
|
this.pendingExpanded = true;
|
||||||
@@ -327,20 +577,38 @@ export default {
|
|||||||
const sourceIndex = (this.master.goods || []).findIndex(goods => goods.cargoName === item.cargoName && goods.cargoType === item.cargoType);
|
const sourceIndex = (this.master.goods || []).findIndex(goods => goods.cargoName === item.cargoName && goods.cargoType === item.cargoType);
|
||||||
let goods = route.goods.find(goodsItem => goodsItem.cargoName === item.cargoName && goodsItem.cargoType === item.cargoType);
|
let goods = route.goods.find(goodsItem => goodsItem.cargoName === item.cargoName && goodsItem.cargoType === item.cargoType);
|
||||||
if (!goods && sourceIndex >= 0) {
|
if (!goods && sourceIndex >= 0) {
|
||||||
goods = this.createGoodsRow(this.master.goods[sourceIndex], sourceIndex, route.dispatchedGoods || {});
|
goods = this.createGoodsRow(this.master.goods[sourceIndex], sourceIndex);
|
||||||
route.goods.push(goods);
|
route.goods.push(goods);
|
||||||
}
|
}
|
||||||
if (goods) goods.dispatchQuantity = item.quantity;
|
if (goods) goods.dispatchQuantity = item.quantity;
|
||||||
Object.assign(route, item);
|
Object.assign(route, item);
|
||||||
|
this.syncFreightItems(route);
|
||||||
|
const freightItem = this.freightItemsForGoods(route, goods);
|
||||||
|
if (freightItem) { freightItem.unitPrice = item.unitPrice || ''; freightItem.priceUnit = item.priceUnit || freightItem.priceUnit; }
|
||||||
this.editingId = item.id;
|
this.editingId = item.id;
|
||||||
this.removePending(item.id);
|
this.removePending(item.id);
|
||||||
this.$nextTick(() => document.querySelector(`[data-segment="${item.segmentNo}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
|
this.$nextTick(() => document.querySelector(`[data-segment="${item.segmentNo}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
|
||||||
},
|
},
|
||||||
|
freightItemsForGoods(route, goods) {
|
||||||
|
return (route.freightItems || []).find(item => String(item.sourceIndex) === String(goods?.sourceIndex)) || {};
|
||||||
|
},
|
||||||
removePending(id) { this.pending = this.pending.filter(item => item.id !== id); if (this.editingId === id) this.editingId = null; },
|
removePending(id) { this.pending = this.pending.filter(item => item.id !== id); if (this.editingId === id) this.editingId = null; },
|
||||||
|
validateRoutePhones(route = {}) {
|
||||||
|
const invalidPhone = [
|
||||||
|
[route.departurePhone, '发货联系方式'],
|
||||||
|
[route.arrivalPhone, '收货联系方式'],
|
||||||
|
[route.driverPhone, '手机号'],
|
||||||
|
[route.escortPhone, '押运人手机号'],
|
||||||
|
].find(([value]) => String(value || '').trim() && !isMobile(value));
|
||||||
|
if (!invalidPhone) return true;
|
||||||
|
this.$message.warning(`${invalidPhone[1]}格式不正确`);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
async submit() {
|
async submit() {
|
||||||
if (!this.pending.length) return this.$message.warning('请加入待提交调度清单');
|
if (!this.pending.length) return this.$message.warning('请加入待提交调度清单');
|
||||||
|
if (this.pending.some(item => !this.validateRoutePhones(item))) return;
|
||||||
this.submitting = true;
|
this.submitting = true;
|
||||||
try { await api.dispatch({ id: this.master.id, dispatches: this.pending.map(({ id, ...item }) => item) }); this.$message.success('调度成功'); this.$emit('back'); } finally { this.submitting = false; }
|
try { await api.dispatch({ id: this.master.id, dispatches: this.pending.map(({ id, ...item }) => ({ ...item, mileage: this.normalizeMileage(item.mileage) })) }); this.$message.success('调度成功'); this.$emit('back'); } finally { this.submitting = false; }
|
||||||
},
|
},
|
||||||
statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
|
statusName(value) { return ({ waiting_dispatch: '待调度', dispatching: '调度中', completed: '调度完成', closed: '调度关闭' })[value] || value || '-'; },
|
||||||
statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; },
|
statusType(value) { return ({ waiting_dispatch: 'warning', dispatching: 'success', completed: 'primary', closed: 'danger' })[value] || 'info'; },
|
||||||
@@ -350,6 +618,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.master-dispatch { min-height: 100%; padding-bottom: 124px; background: #fff; color: #303133; }
|
.master-dispatch { min-height: 100%; padding-bottom: 124px; background: #fff; color: #303133; }
|
||||||
|
.pending-list-spacer { height: min(436px, calc(100vh - 130px)); }
|
||||||
.master-overview, .dispatch-section { margin-bottom: 8px; border: 1px solid #eff1f7; background: #fff; }
|
.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; }
|
.overview-heading, .segment-header, .pending-bar { display: flex; align-items: center; justify-content: space-between; }
|
||||||
.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-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; } }
|
||||||
@@ -365,7 +634,8 @@ export default {
|
|||||||
.transport-type-field :deep(.el-input) { width: 240px; }
|
.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; } }
|
.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; } }
|
.freight-heading { margin: 16px 0 12px; h3 { margin: 0; padding-left: 12px; border-left: 4px solid #409eff; font-size: 16px; } }
|
||||||
.goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } }
|
.goods-table { :deep(th.el-table__cell), :deep(td.el-table__cell) { border-color: #eff1f7; } :deep(.el-table__row--striped td.el-table__cell) { background: #fafafa; } :deep(.goods-table__remaining .cell) { white-space: nowrap; } :deep(.goods-table__dispatch .el-input) { width: 100%; min-width: 0; } }
|
||||||
|
.freight-form { :deep(.freight-unit-select) { width: 92px; } }
|
||||||
.carrier-type-form { margin-top: 16px; }
|
.carrier-type-form { margin-top: 16px; }
|
||||||
.carrier-form { padding-top: 8px; }
|
.carrier-form { padding-top: 8px; }
|
||||||
.segment-actions { display: flex; justify-content: flex-end; gap: 12px; padding-top: 4px; }
|
.segment-actions { display: flex; justify-content: flex-end; gap: 12px; padding-top: 4px; }
|
||||||
|
|||||||
@@ -49,13 +49,16 @@
|
|||||||
<section>
|
<section>
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h3>收发货信息</h3>
|
<h3>收发货信息</h3>
|
||||||
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
|
<div class="section-head__actions">
|
||||||
|
<el-link type="primary" @click="addRoute">新增途经点</el-link>
|
||||||
|
<el-link type="primary" @click="openRouteDialog">选择线路</el-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-form :model="form" label-position="right" label-width="auto"
|
<el-form :model="form" label-position="right" label-width="auto"
|
||||||
><div class="route-steps">
|
><div class="route-steps">
|
||||||
<el-steps direction="vertical" :active="form.routes.length + 1">
|
<el-steps direction="vertical" :active="form.routes.length + 1">
|
||||||
<el-step>
|
<el-step>
|
||||||
<template #icon><span class="route-step-icon">起</span></template>
|
<template #icon><span class="route-step-icon route-step-icon--start">起</span></template>
|
||||||
<template #description>
|
<template #description>
|
||||||
<div class="route-node-form">
|
<div class="route-node-form">
|
||||||
<el-form-item label="发货地址" required class="route-address-form-item"
|
<el-form-item label="发货地址" required class="route-address-form-item"
|
||||||
@@ -80,27 +83,27 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-step>
|
</el-step>
|
||||||
<el-step v-for="(route, index) in form.routes" :key="index">
|
<el-step v-for="(route, index) in form.routes" :key="index">
|
||||||
<template #icon><span class="route-step-icon">经</span></template>
|
<template #icon><span class="route-step-icon route-step-icon--middle">经</span></template>
|
||||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ index + 1 }}</span><span class="route-title-path">{{ getRouteTitle(index).replace(`段${index + 1}:`, '') }}</span><div class="route-transport-control"><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></template>
|
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ index + 1 }}</span><span class="route-title-path">{{ getRouteTitle(index).replace(`段${index + 1}:`, '') }}</span><div class="route-transport-control"><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></template>
|
||||||
<template #description>
|
<template #description>
|
||||||
<div class="route-node-form">
|
<div class="route-node-form">
|
||||||
<el-form-item label="途经地" required class="route-address-form-item"
|
<el-form-item label="途经地" required class="route-address-form-item"
|
||||||
><div class="route-address-inputs"
|
><div class="route-address-inputs"
|
||||||
><el-input v-model="route.departureName" class="route-address-name" readonly :disabled="!route.transportType" placeholder="请先选择运输类型" @click="openAddressMap(`route-${index}`)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType" placeholder="请先选择运输类型" @click="openAddressMap(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
><el-input v-model="route.departureName" class="route-address-name" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" @click="openAddressMap(`route-${index}`)" /><el-input v-model="route.departureAddress" class="route-address-detail" readonly :disabled="!route.transportType" placeholder="请先选择运输方式" @click="openAddressMap(`route-${index}`)" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!route.transportType" @click="openCommonAddress(`route-${index}`)" /></el-tooltip></div
|
||||||
></el-form-item>
|
></el-form-item>
|
||||||
<el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item>
|
<el-form-item label="联系人"><el-input v-model="route.departureContact" placeholder="请输入" /></el-form-item>
|
||||||
<el-form-item label="联系方式"><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 v-if="form.routes.length > 1" 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="联系方式"><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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-step>
|
</el-step>
|
||||||
<el-step>
|
<el-step>
|
||||||
<template #icon><span class="route-step-icon">终</span></template>
|
<template #icon><span class="route-step-icon route-step-icon--end">终</span></template>
|
||||||
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ form.routes.length + 1 }}</span><span class="route-title-path">{{ getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '') }}</span><div class="route-transport-control"><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></template>
|
<template #title><div class="route-step-title"><span class="route-segment-label">段{{ form.routes.length + 1 }}</span><span class="route-title-path">{{ getFinalRouteTitle().replace(`段${form.routes.length + 1}:`, '') }}</span><div class="route-transport-control"><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></template>
|
||||||
<template #description>
|
<template #description>
|
||||||
<div class="route-node-form">
|
<div class="route-node-form">
|
||||||
<el-form-item label="收货地址" required class="route-address-form-item"
|
<el-form-item label="收货地址" required class="route-address-form-item"
|
||||||
><div class="route-address-inputs"
|
><div class="route-address-inputs"
|
||||||
><el-input v-model="form.arrivalName" class="route-address-name" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输类型" @click="openAddressMap('arrival')" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输类型" @click="openAddressMap('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
><el-input v-model="form.arrivalName" class="route-address-name" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" @click="openAddressMap('arrival')" /><el-input v-model="form.arrivalAddress" class="route-address-detail" readonly :disabled="!form.finalTransportType" placeholder="请先选择运输方式" @click="openAddressMap('arrival')" /><el-tooltip content="选择常用地址" placement="top"><el-button class="address-picker-button" type="primary" link :icon="List" :disabled="!form.finalTransportType" @click="openCommonAddress('arrival')" /></el-tooltip></div
|
||||||
></el-form-item>
|
></el-form-item>
|
||||||
<el-form-item label="联系人"
|
<el-form-item label="联系人"
|
||||||
><el-input v-model="form.arrivalContact" placeholder="请输入" /></el-form-item>
|
><el-input v-model="form.arrivalContact" placeholder="请输入" /></el-form-item>
|
||||||
@@ -125,8 +128,8 @@
|
|||||||
<div class="goods-heading">
|
<div class="goods-heading">
|
||||||
<h3>货物信息</h3>
|
<h3>货物信息</h3>
|
||||||
<div class="goods-toolbar">
|
<div class="goods-toolbar">
|
||||||
<el-link type="primary" @click="cargoImportVisible = true">导入货物</el-link>
|
<el-link type="primary" @click="cargoImportVisible = true">导入货物</el-link>
|
||||||
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
|
<el-link type="primary" @click="openCommonCargoDialog">常用货物</el-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="form.goods" border
|
<el-table :data="form.goods" border
|
||||||
@@ -137,30 +140,39 @@
|
|||||||
><el-select
|
><el-select
|
||||||
v-model="row.cargoName"
|
v-model="row.cargoName"
|
||||||
filterable
|
filterable
|
||||||
remote
|
allow-create
|
||||||
|
default-first-option
|
||||||
clearable
|
clearable
|
||||||
placeholder="请输入并选择"
|
placeholder="请选择或输入"
|
||||||
:loading="cargoNameLoading"
|
:loading="isCargoOptionsLoading(row.cargoTypePath)"
|
||||||
:remote-method="searchCargoOptions"
|
@visible-change="visible => visible && loadCargoOptionsByType(row.cargoTypePath)"
|
||||||
@visible-change="loadCargoOptions"
|
|
||||||
@change="selectCargoName(row, $event)"
|
@change="selectCargoName(row, $event)"
|
||||||
><el-option
|
><el-option
|
||||||
v-for="cargo in cargoOptions"
|
v-for="cargo in getCargoOptionsByType(row.cargoTypePath)"
|
||||||
:key="cargo.id"
|
:key="cargo.id"
|
||||||
:label="cargo.cargoName"
|
:label="cargo.cargoName"
|
||||||
:value="cargo.cargoName" /></el-select
|
:value="cargo.cargoName" /></el-select
|
||||||
></template></el-table-column
|
></template></el-table-column
|
||||||
><el-table-column label="货物类型" min-width="130"
|
><el-table-column label="货物类型" min-width="130"
|
||||||
><template #default="{ row }"
|
><template #default="{ row }"
|
||||||
><el-input
|
><el-cascader
|
||||||
v-model="row.cargoType"
|
v-model="row.cargoTypePath"
|
||||||
placeholder="从货物类型获取" /></template></el-table-column
|
:options="cargoTypeOptions"
|
||||||
><el-table-column label="数量" width="104" class-name="master-editor__quantity-cell"
|
:props="cargoTypeCascaderProps"
|
||||||
|
:placeholder="row.cargoType || '请选择'"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
: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"
|
||||||
><template #default="{ row }"
|
><template #default="{ row }"
|
||||||
><el-input-number
|
><el-input
|
||||||
v-model="row.quantity"
|
v-model="row.quantity"
|
||||||
:min="0"
|
inputmode="decimal"
|
||||||
controls-position="right" /></template></el-table-column
|
placeholder="请输入"
|
||||||
|
@input="value => handleQuantityInput(row, value)" /></template></el-table-column
|
||||||
><el-table-column label="数量单位" width="120"
|
><el-table-column label="数量单位" width="120"
|
||||||
><template #default="{ row }"
|
><template #default="{ row }"
|
||||||
><el-select v-model="row.quantityUnit"
|
><el-select v-model="row.quantityUnit"
|
||||||
@@ -200,14 +212,85 @@
|
|||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h3>附件</h3>
|
<h3>附件</h3>
|
||||||
<el-upload
|
<div class="master-editor__attachment">
|
||||||
multiple
|
<div class="master-editor__attachment-head">
|
||||||
action="/api/blade-resource/oss/endpoint/put-file"
|
<vehicle-attachment-upload
|
||||||
:headers="uploadHeaders"
|
v-model="attachmentRows"
|
||||||
:on-success="attach"
|
:file-types="attachmentFileTypes"
|
||||||
><el-button type="primary" plain>上传附件</el-button></el-upload
|
:max-size="500"
|
||||||
>
|
:show-tip="false"
|
||||||
|
:show-file-list="false"
|
||||||
|
button-text="上传附件"
|
||||||
|
@change="handleAttachmentChange"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!attachmentRows.length"
|
||||||
|
@click="handleAttachmentBatchDownload"
|
||||||
|
>
|
||||||
|
批量下载
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="attachmentRows"
|
||||||
|
border
|
||||||
|
class="master-editor__attachment-table"
|
||||||
|
@selection-change="handleAttachmentSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
|
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||||
|
<el-table-column label="文件名" min-width="240" align="center" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link type="primary" @click="previewAttachment(row)">
|
||||||
|
{{ attachmentName(row) }}
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="文件大小" width="120" align="center">
|
||||||
|
<template #default="{ row }">{{ formatFileSize(row.size) }}</template>
|
||||||
|
</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-link type="danger" @click="removeAttachment($index)">删除</el-link>
|
||||||
|
<el-link type="primary" @click="downloadAttachment(row)">下载</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<el-dialog
|
||||||
|
v-model="attachmentDocumentPreviewVisible"
|
||||||
|
:title="attachmentPreviewFile.name || '附件预览'"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
width="90%"
|
||||||
|
top="4vh"
|
||||||
|
class="master-editor__attachment-viewer-dialog"
|
||||||
|
>
|
||||||
|
<open-file-viewer
|
||||||
|
v-if="attachmentDocumentPreviewVisible && attachmentPreviewFile.url"
|
||||||
|
:file="attachmentPreviewFile.url"
|
||||||
|
:file-name="attachmentPreviewFile.name"
|
||||||
|
:mime-type="attachmentPreviewFile.mimeType"
|
||||||
|
width="100%"
|
||||||
|
height="72vh"
|
||||||
|
fit="contain"
|
||||||
|
theme="auto"
|
||||||
|
locale="zh-CN"
|
||||||
|
:toolbar="attachmentViewerToolbar"
|
||||||
|
:plugins="attachmentViewerPlugins"
|
||||||
|
@unsupported="handleAttachmentPreviewUnsupported"
|
||||||
|
@error="handleAttachmentPreviewError"
|
||||||
|
/>
|
||||||
|
</el-dialog>
|
||||||
|
<el-image-viewer
|
||||||
|
v-if="attachmentImagePreviewVisible"
|
||||||
|
:url-list="attachmentImagePreviewUrls"
|
||||||
|
:initial-index="attachmentImagePreviewIndex"
|
||||||
|
@close="attachmentImagePreviewVisible = false"
|
||||||
|
/>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="addressDialogVisible"
|
v-model="addressDialogVisible"
|
||||||
title="选择常用地址"
|
title="选择常用地址"
|
||||||
@@ -400,20 +483,38 @@ import { getList as getCommonAddressList } from '@/api/base/common-address';
|
|||||||
import { getList as getAirportMasterList } from '@/api/base/airport-master';
|
import { getList as getAirportMasterList } from '@/api/base/airport-master';
|
||||||
import { getList as getPortTerminalList } from '@/api/base/port-terminal';
|
import { getList as getPortTerminalList } from '@/api/base/port-terminal';
|
||||||
import { getList as getRailwayStationList } from '@/api/base/railway-station';
|
import { getList as getRailwayStationList } from '@/api/base/railway-station';
|
||||||
|
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||||
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||||
import { exportBlob, importBlob } from '@/api/common';
|
import { exportBlob, importBlob } from '@/api/common';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadFileByUrl, downloadXls } from '@/utils/util';
|
||||||
import { getUploadHeaders } from '@/utils/upload';
|
import { List, Search } from '@element-plus/icons-vue';
|
||||||
import { List, Location, Search } from '@element-plus/icons-vue';
|
import { ElImageViewer } from 'element-plus';
|
||||||
|
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||||
|
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||||||
|
import '@open-file-viewer/core/style.css';
|
||||||
|
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
|
||||||
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
|
||||||
const quantityUnitOptions = ['吨', '件', '方'];
|
const quantityUnitOptions = ['吨', '件', '方'];
|
||||||
|
const attachmentViewerPlugins = [
|
||||||
|
imagePlugin(),
|
||||||
|
pdfPlugin({ workerSrc: pdfWorkerSrc, useFetchData: true }),
|
||||||
|
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
|
||||||
|
textPlugin(),
|
||||||
|
fallbackPlugin(),
|
||||||
|
];
|
||||||
let amapLoader;
|
let amapLoader;
|
||||||
import { getList as getCommonRouteList } from '@/api/business/common-route';
|
import { getList as getCommonRouteList } from '@/api/business/common-route';
|
||||||
import * as api from '@/api/business/master-order';
|
import * as api from '@/api/business/master-order';
|
||||||
import { formatUpdateUserName } from '@/utils/audit';
|
import { formatUpdateUserName } from '@/utils/audit';
|
||||||
|
import { isMobile } from '@/utils/validate';
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
ElImageViewer,
|
||||||
|
OpenFileViewer,
|
||||||
|
},
|
||||||
props: { id: [String, Number] },
|
props: { id: [String, Number] },
|
||||||
emits: ['back', 'dispatch'],
|
emits: ['back', 'dispatch'],
|
||||||
data() {
|
data() {
|
||||||
@@ -451,11 +552,44 @@ export default {
|
|||||||
commonCargoLoading: false,
|
commonCargoLoading: false,
|
||||||
commonCargoRows: [],
|
commonCargoRows: [],
|
||||||
commonCargoPage: { current: 1, size: 10, total: 0 },
|
commonCargoPage: { current: 1, size: 10, total: 0 },
|
||||||
cargoNameLoading: false,
|
cargoTypeLoading: false,
|
||||||
cargoOptions: [],
|
cargoTypeOptions: [],
|
||||||
|
cargoTypeFlatOptions: [],
|
||||||
|
cargoOptionsMap: {},
|
||||||
|
cargoOptionsLoadingMap: {},
|
||||||
cargoImportVisible: false,
|
cargoImportVisible: false,
|
||||||
cargoImportLoading: false,
|
cargoImportLoading: false,
|
||||||
uploadHeaders: getUploadHeaders(),
|
attachmentRows: [],
|
||||||
|
selectedAttachmentRows: [],
|
||||||
|
attachmentImagePreviewVisible: false,
|
||||||
|
attachmentImagePreviewUrls: [],
|
||||||
|
attachmentImagePreviewIndex: 0,
|
||||||
|
attachmentDocumentPreviewVisible: false,
|
||||||
|
attachmentPreviewFile: {},
|
||||||
|
attachmentViewerPlugins,
|
||||||
|
attachmentViewerToolbar: {
|
||||||
|
download: true,
|
||||||
|
fullscreen: true,
|
||||||
|
print: true,
|
||||||
|
rotate: true,
|
||||||
|
zoom: true,
|
||||||
|
},
|
||||||
|
attachmentFileTypes: [
|
||||||
|
'pdf',
|
||||||
|
'bmp',
|
||||||
|
'jpeg',
|
||||||
|
'png',
|
||||||
|
'jpg',
|
||||||
|
'doc',
|
||||||
|
'docx',
|
||||||
|
'ppt',
|
||||||
|
'pptx',
|
||||||
|
'xlsx',
|
||||||
|
'xls',
|
||||||
|
'eml',
|
||||||
|
'msg',
|
||||||
|
'zip',
|
||||||
|
],
|
||||||
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
transports: ['公路运输', '铁路运输', '水路运输', '航空运输'],
|
||||||
quantityUnitOptions,
|
quantityUnitOptions,
|
||||||
form: {
|
form: {
|
||||||
@@ -471,6 +605,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters(['userInfo']),
|
||||||
lastRouteName() {
|
lastRouteName() {
|
||||||
return this.form.routes[this.form.routes.length - 1]?.departureName || '途经地';
|
return this.form.routes[this.form.routes.length - 1]?.departureName || '途经地';
|
||||||
},
|
},
|
||||||
@@ -481,11 +616,19 @@ export default {
|
|||||||
air: '空港机场',
|
air: '空港机场',
|
||||||
}[this.addressTransportMode(this.addressTarget)] || '站点';
|
}[this.addressTransportMode(this.addressTarget)] || '站点';
|
||||||
},
|
},
|
||||||
|
cargoTypeCascaderProps() {
|
||||||
|
return {
|
||||||
|
value: 'id',
|
||||||
|
label: 'label',
|
||||||
|
children: 'children',
|
||||||
|
emitPath: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
await this.loadProjects();
|
await Promise.all([this.loadProjects(), this.loadCargoTypeOptions()]);
|
||||||
if (this.id) {
|
if (this.id) {
|
||||||
await this.loadDetail(this.id);
|
await this.loadDetail(this.id);
|
||||||
}
|
}
|
||||||
@@ -513,10 +656,24 @@ export default {
|
|||||||
? finalSegment.transportType
|
? finalSegment.transportType
|
||||||
: detail.finalTransportType || '',
|
: detail.finalTransportType || '',
|
||||||
goods: detail.goods || [{ quantityUnit: quantityUnitOptions[0] }],
|
goods: detail.goods || [{ quantityUnit: quantityUnitOptions[0] }],
|
||||||
|
attachmentsJson: detail.attachmentsJson || '[]',
|
||||||
transportOrganizationType: '多式联运',
|
transportOrganizationType: '多式联运',
|
||||||
};
|
};
|
||||||
|
this.restoreGoodsCargoTypePaths();
|
||||||
|
this.attachmentRows = this.parseJsonArray(this.form.attachmentsJson);
|
||||||
|
this.selectedAttachmentRows = [];
|
||||||
if (this.form.projectId) await this.loadContracts(this.form.projectId, false);
|
if (this.form.projectId) await this.loadContracts(this.form.projectId, false);
|
||||||
},
|
},
|
||||||
|
parseJsonArray(value) {
|
||||||
|
if (!value) return [];
|
||||||
|
if (Array.isArray(value)) return value;
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(value);
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
getRouteTitle(index) {
|
getRouteTitle(index) {
|
||||||
const departureName =
|
const departureName =
|
||||||
index === 0
|
index === 0
|
||||||
@@ -826,31 +983,117 @@ export default {
|
|||||||
this.commonCargoLoading = false;
|
this.commonCargoLoading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async loadCargoOptions(visible) {
|
async loadCargoTypeOptions() {
|
||||||
if (!visible || this.cargoOptions.length) return;
|
if (this.cargoTypeOptions.length || this.cargoTypeLoading) return;
|
||||||
await this.searchCargoOptions('');
|
this.cargoTypeLoading = true;
|
||||||
},
|
|
||||||
async searchCargoOptions(keyword) {
|
|
||||||
this.cargoNameLoading = true;
|
|
||||||
try {
|
try {
|
||||||
const response = await getCommonCargoList(1, 50, {
|
const response = await getCargoTypeList(1, 9999, { allDept: 0 });
|
||||||
allDept: 0,
|
const rows = this.responseRecords(response);
|
||||||
cargoName: keyword || undefined,
|
this.cargoTypeOptions = this.buildCargoTypeTree(rows);
|
||||||
});
|
|
||||||
const data = response?.data?.data || response?.data || response || {};
|
|
||||||
this.cargoOptions = data.records || [];
|
|
||||||
} finally {
|
} finally {
|
||||||
this.cargoNameLoading = false;
|
this.cargoTypeLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildCargoTypeTree(rows = []) {
|
||||||
|
const nodes = new Map();
|
||||||
|
rows.forEach(row => {
|
||||||
|
const id = String(row.id);
|
||||||
|
nodes.set(id, {
|
||||||
|
...row,
|
||||||
|
id,
|
||||||
|
label: row.cargoName || row.cargoTypeName || row.name || row.cargoCode || '',
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const roots = [];
|
||||||
|
nodes.forEach(node => {
|
||||||
|
const parentId = node.parentId == null ? '' : String(node.parentId);
|
||||||
|
if (parentId && parentId !== '0' && nodes.has(parentId)) {
|
||||||
|
nodes.get(parentId).children.push(node);
|
||||||
|
} else {
|
||||||
|
roots.push(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.cargoTypeFlatOptions = [];
|
||||||
|
const normalize = (node, path = []) => {
|
||||||
|
const nextPath = [...path, node.id];
|
||||||
|
const children = node.children.map(child => normalize(child, nextPath));
|
||||||
|
const normalized = { ...node, children };
|
||||||
|
this.cargoTypeFlatOptions.push({ ...normalized, path: nextPath });
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
return roots.map(node => normalize(node));
|
||||||
|
},
|
||||||
|
getCargoTypeByPath(path = []) {
|
||||||
|
const normalizedPath = Array.isArray(path) ? path.map(value => String(value)) : [];
|
||||||
|
const id = normalizedPath.at(-1);
|
||||||
|
return this.cargoTypeFlatOptions.find(item => item.id === id) || null;
|
||||||
|
},
|
||||||
|
getCargoOptionsKey(path = []) {
|
||||||
|
const cargoType = this.getCargoTypeByPath(path);
|
||||||
|
return String(cargoType?.cargoCode || cargoType?.code || cargoType?.id || '');
|
||||||
|
},
|
||||||
|
getCargoOptionsByType(path = []) {
|
||||||
|
const key = this.getCargoOptionsKey(path);
|
||||||
|
return key ? this.cargoOptionsMap[key] || [] : [];
|
||||||
|
},
|
||||||
|
isCargoOptionsLoading(path = []) {
|
||||||
|
const key = this.getCargoOptionsKey(path);
|
||||||
|
return key ? Boolean(this.cargoOptionsLoadingMap[key]) : false;
|
||||||
|
},
|
||||||
|
async loadCargoOptionsByType(path = []) {
|
||||||
|
const cargoType = this.getCargoTypeByPath(path);
|
||||||
|
const key = this.getCargoOptionsKey(path);
|
||||||
|
if (!key || !cargoType) return [];
|
||||||
|
if (this.cargoOptionsMap[key] || this.cargoOptionsLoadingMap[key]) {
|
||||||
|
return this.cargoOptionsMap[key] || [];
|
||||||
|
}
|
||||||
|
this.cargoOptionsLoadingMap = { ...this.cargoOptionsLoadingMap, [key]: true };
|
||||||
|
try {
|
||||||
|
const response = await getCommonCargoList(1, 500, {
|
||||||
|
allDept: 0,
|
||||||
|
secondCargoTypeName: cargoType.label,
|
||||||
|
secondCargoTypeCode: cargoType.cargoCode || cargoType.code || '',
|
||||||
|
});
|
||||||
|
const options = this.responseRecords(response);
|
||||||
|
this.cargoOptionsMap = { ...this.cargoOptionsMap, [key]: options };
|
||||||
|
return options;
|
||||||
|
} finally {
|
||||||
|
this.cargoOptionsLoadingMap = { ...this.cargoOptionsLoadingMap, [key]: false };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectCargoName(goods, cargoName) {
|
selectCargoName(goods, cargoName) {
|
||||||
if (!cargoName) return;
|
if (!cargoName) return;
|
||||||
const cargo = this.cargoOptions.find(item => item.cargoName === cargoName);
|
const cargo = this.getCargoOptionsByType(goods.cargoTypePath).find(
|
||||||
|
item => item.cargoName === cargoName
|
||||||
|
);
|
||||||
if (!cargo) return;
|
if (!cargo) return;
|
||||||
Object.assign(goods, this.normalizeCargoRow(cargo));
|
Object.assign(goods, this.normalizeCargoRow(cargo, goods.cargoTypePath));
|
||||||
|
},
|
||||||
|
handleCargoTypeChange(goods, value) {
|
||||||
|
const path = Array.isArray(value) ? value.map(item => String(item)) : [];
|
||||||
|
const cargoType = this.getCargoTypeByPath(path);
|
||||||
|
goods.cargoTypePath = path;
|
||||||
|
goods.cargoType = cargoType?.label || '';
|
||||||
|
goods.cargoTypeCode = cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
|
||||||
|
goods.cargoName = '';
|
||||||
|
goods.specification = '';
|
||||||
|
goods.model = '';
|
||||||
|
this.loadCargoOptionsByType(path);
|
||||||
|
},
|
||||||
|
restoreGoodsCargoTypePaths() {
|
||||||
|
this.form.goods.forEach(goods => {
|
||||||
|
if (Array.isArray(goods.cargoTypePath) && goods.cargoTypePath.length) return;
|
||||||
|
const cargoType = this.cargoTypeFlatOptions.find(
|
||||||
|
item =>
|
||||||
|
String(item.cargoCode || item.code || '') === String(goods.cargoTypeCode || '') ||
|
||||||
|
item.label === goods.cargoType
|
||||||
|
);
|
||||||
|
if (cargoType) goods.cargoTypePath = cargoType.path;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
selectCommonCargo(row) {
|
selectCommonCargo(row) {
|
||||||
const cargo = this.normalizeCargoRow(row);
|
const cargo = this.normalizeCargoRow(row, this.resolveCargoTypePath(row));
|
||||||
const firstGoods = this.form.goods[0];
|
const firstGoods = this.form.goods[0];
|
||||||
const isFirstGoodsEmpty =
|
const isFirstGoodsEmpty =
|
||||||
firstGoods &&
|
firstGoods &&
|
||||||
@@ -872,12 +1115,35 @@ export default {
|
|||||||
}
|
}
|
||||||
this.commonCargoVisible = false;
|
this.commonCargoVisible = false;
|
||||||
},
|
},
|
||||||
normalizeCargoRow(row = {}) {
|
resolveCargoTypePath(row = {}) {
|
||||||
|
return (
|
||||||
|
this.cargoTypeFlatOptions.find(
|
||||||
|
item =>
|
||||||
|
String(item.cargoCode || item.code || '') ===
|
||||||
|
String(row.secondCargoTypeCode || row.cargoTypeCode || '') ||
|
||||||
|
item.label === (row.secondCargoTypeName || row.cargoType || row.goodsType)
|
||||||
|
)?.path || []
|
||||||
|
);
|
||||||
|
},
|
||||||
|
normalizeCargoRow(row = {}, cargoTypePath = this.resolveCargoTypePath(row)) {
|
||||||
|
const cargoType = this.getCargoTypeByPath(cargoTypePath);
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
cargoName: row.cargoName || row.goodsName || '',
|
cargoName: row.cargoName || row.goodsName || '',
|
||||||
cargoType:
|
cargoType:
|
||||||
row.cargoType || row.goodsType || row.secondCargoTypeName || row.firstCargoTypeName || '',
|
cargoType?.label ||
|
||||||
|
row.cargoType ||
|
||||||
|
row.goodsType ||
|
||||||
|
row.secondCargoTypeName ||
|
||||||
|
row.firstCargoTypeName ||
|
||||||
|
'',
|
||||||
|
cargoTypeCode:
|
||||||
|
cargoType?.cargoCode ||
|
||||||
|
cargoType?.code ||
|
||||||
|
row.cargoTypeCode ||
|
||||||
|
row.secondCargoTypeCode ||
|
||||||
|
'',
|
||||||
|
cargoTypePath,
|
||||||
quantity: row.quantity || '',
|
quantity: row.quantity || '',
|
||||||
quantityUnit: row.quantityUnit || quantityUnitOptions[0],
|
quantityUnit: row.quantityUnit || quantityUnitOptions[0],
|
||||||
packageType: row.packageType || row.package || '',
|
packageType: row.packageType || row.package || '',
|
||||||
@@ -980,7 +1246,7 @@ export default {
|
|||||||
this.form.routes.push(this.createRoute());
|
this.form.routes.push(this.createRoute());
|
||||||
},
|
},
|
||||||
removeRoute(index) {
|
removeRoute(index) {
|
||||||
if (this.form.routes.length > 1) this.form.routes.splice(index, 1);
|
this.form.routes.splice(index, 1);
|
||||||
},
|
},
|
||||||
addGoods(index) {
|
addGoods(index) {
|
||||||
this.form.goods.splice(index + 1, 0, { quantityUnit: quantityUnitOptions[0] });
|
this.form.goods.splice(index + 1, 0, { quantityUnit: quantityUnitOptions[0] });
|
||||||
@@ -988,12 +1254,114 @@ export default {
|
|||||||
removeGoods(index) {
|
removeGoods(index) {
|
||||||
if (this.form.goods.length > 1) this.form.goods.splice(index, 1);
|
if (this.form.goods.length > 1) this.form.goods.splice(index, 1);
|
||||||
},
|
},
|
||||||
attach(response) {
|
handleQuantityInput(row, value) {
|
||||||
const list = JSON.parse(this.form.attachmentsJson || '[]');
|
const text = String(value || '').replace(/[^\d.]/g, '');
|
||||||
list.push(response.data || response);
|
const [integer = '', ...decimals] = text.split('.');
|
||||||
this.form.attachmentsJson = JSON.stringify(list);
|
row.quantity = decimals.length ? `${integer}.${decimals.join('').slice(0, 3)}` : integer;
|
||||||
|
},
|
||||||
|
syncAttachmentsJson() {
|
||||||
|
this.form.attachmentsJson = JSON.stringify(this.attachmentRows);
|
||||||
|
},
|
||||||
|
handleAttachmentChange(list) {
|
||||||
|
const userName = this.userInfo?.realName || this.userInfo?.userName || '';
|
||||||
|
const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
this.attachmentRows = (list || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
description: item.description || '',
|
||||||
|
uploadUserName: item.uploadUserName || userName,
|
||||||
|
uploadTime: item.uploadTime || uploadTime,
|
||||||
|
}));
|
||||||
|
this.selectedAttachmentRows = [];
|
||||||
|
this.syncAttachmentsJson();
|
||||||
|
},
|
||||||
|
handleAttachmentSelectionChange(rows) {
|
||||||
|
this.selectedAttachmentRows = rows || [];
|
||||||
|
},
|
||||||
|
removeAttachment(index) {
|
||||||
|
this.attachmentRows.splice(index, 1);
|
||||||
|
this.selectedAttachmentRows = this.selectedAttachmentRows.filter(item =>
|
||||||
|
this.attachmentRows.includes(item)
|
||||||
|
);
|
||||||
|
this.syncAttachmentsJson();
|
||||||
|
},
|
||||||
|
formatFileSize(size) {
|
||||||
|
const number = Number(size);
|
||||||
|
if (!number) return '';
|
||||||
|
if (number < 1024) return `${number}B`;
|
||||||
|
if (number < 1024 * 1024) return `${(number / 1024).toFixed(1)}KB`;
|
||||||
|
return `${(number / 1024 / 1024).toFixed(1)}MB`;
|
||||||
|
},
|
||||||
|
attachmentUrl(row = {}) {
|
||||||
|
return row.url || row.link || row.fileUrl || row.domain || '';
|
||||||
|
},
|
||||||
|
attachmentName(row = {}) {
|
||||||
|
return row.originalName || row.name || row.fileName || '附件';
|
||||||
|
},
|
||||||
|
attachmentExtension(row = {}) {
|
||||||
|
const source = String(this.attachmentName(row) || this.attachmentUrl(row)).split('?')[0];
|
||||||
|
const index = source.lastIndexOf('.');
|
||||||
|
return index > -1 ? source.slice(index + 1).toLowerCase() : '';
|
||||||
|
},
|
||||||
|
isAttachmentImage(row) {
|
||||||
|
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(this.attachmentExtension(row));
|
||||||
|
},
|
||||||
|
previewAttachment(row) {
|
||||||
|
const url = this.attachmentUrl(row);
|
||||||
|
if (!url) {
|
||||||
|
this.$message.warning('附件地址为空,无法预览');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.isAttachmentImage(row)) {
|
||||||
|
this.attachmentImagePreviewUrls = this.attachmentRows
|
||||||
|
.filter(item => this.isAttachmentImage(item) && this.attachmentUrl(item))
|
||||||
|
.map(item => this.attachmentUrl(item));
|
||||||
|
this.attachmentImagePreviewIndex = Math.max(this.attachmentImagePreviewUrls.indexOf(url), 0);
|
||||||
|
this.attachmentImagePreviewVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.attachmentPreviewFile = {
|
||||||
|
name: this.attachmentName(row),
|
||||||
|
url,
|
||||||
|
mimeType: row.mimeType || row.contentType || '',
|
||||||
|
};
|
||||||
|
this.attachmentDocumentPreviewVisible = true;
|
||||||
|
},
|
||||||
|
handleAttachmentPreviewUnsupported() {
|
||||||
|
this.$message.warning('当前文件暂不支持在线预览');
|
||||||
|
},
|
||||||
|
handleAttachmentPreviewError() {
|
||||||
|
this.$message.error('附件预览失败');
|
||||||
|
},
|
||||||
|
downloadAttachment(row) {
|
||||||
|
const url = this.attachmentUrl(row);
|
||||||
|
if (!url) {
|
||||||
|
this.$message.warning('附件地址为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
downloadFileByUrl(url, this.attachmentName(row));
|
||||||
|
},
|
||||||
|
handleAttachmentBatchDownload() {
|
||||||
|
const rows = this.selectedAttachmentRows.length
|
||||||
|
? this.selectedAttachmentRows
|
||||||
|
: this.attachmentRows;
|
||||||
|
rows.forEach(row => this.downloadAttachment(row));
|
||||||
},
|
},
|
||||||
async save(draft = false) {
|
async save(draft = false) {
|
||||||
|
const phoneFields = [
|
||||||
|
[this.form.departurePhone, '发货联系方式'],
|
||||||
|
...this.form.routes.map((route, index) => [
|
||||||
|
route.departurePhone,
|
||||||
|
`第${index + 1}个途经地联系方式`,
|
||||||
|
]),
|
||||||
|
[this.form.arrivalPhone, '收货联系方式'],
|
||||||
|
];
|
||||||
|
const invalidPhone = phoneFields.find(
|
||||||
|
([value]) => String(value || '').trim() && !isMobile(value)
|
||||||
|
);
|
||||||
|
if (invalidPhone) {
|
||||||
|
this.$message.warning(`${invalidPhone[1]}格式不正确`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!draft) {
|
if (!draft) {
|
||||||
await this.$refs.form.validate();
|
await this.$refs.form.validate();
|
||||||
if (!this.form.departureAddress || !this.form.arrivalAddress)
|
if (!this.form.departureAddress || !this.form.arrivalAddress)
|
||||||
@@ -1001,14 +1369,14 @@ export default {
|
|||||||
if (!this.form.goods.some(item => item.cargoType && item.quantity > 0 && item.quantityUnit))
|
if (!this.form.goods.some(item => item.cargoType && item.quantity > 0 && item.quantityUnit))
|
||||||
return this.$message.warning('请至少填写一条有效货物');
|
return this.$message.warning('请至少填写一条有效货物');
|
||||||
if (
|
if (
|
||||||
!this.form.routes.length ||
|
|
||||||
this.form.routes.some(item => !item.departureName || !item.transportType) ||
|
this.form.routes.some(item => !item.departureName || !item.transportType) ||
|
||||||
!this.form.finalTransportType
|
!this.form.finalTransportType
|
||||||
)
|
)
|
||||||
return this.$message.warning('请完整填写途经地及运输类型');
|
return this.$message.warning('请完整填写途经地及运输方式');
|
||||||
}
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
...this.form,
|
...this.form,
|
||||||
|
attachmentsJson: JSON.stringify(this.attachmentRows),
|
||||||
routes: [
|
routes: [
|
||||||
...this.form.routes.map((route, index) => ({ ...route, segmentNo: `段${index + 1}` })),
|
...this.form.routes.map((route, index) => ({ ...route, segmentNo: `段${index + 1}` })),
|
||||||
{
|
{
|
||||||
@@ -1024,26 +1392,35 @@ export default {
|
|||||||
delete payload.finalTransportType;
|
delete payload.finalTransportType;
|
||||||
const res = await (draft ? api.saveDraft(payload) : api.submit(payload));
|
const res = await (draft ? api.saveDraft(payload) : api.submit(payload));
|
||||||
this.$message.success('保存成功');
|
this.$message.success('保存成功');
|
||||||
return res.data || res;
|
// 兼容 Axios 响应及 BladeX 常见的 data.data 两层响应结构。
|
||||||
|
return res?.data?.data || res?.data || res;
|
||||||
},
|
},
|
||||||
async saveDraft() {
|
async saveDraft() {
|
||||||
await this.save(true);
|
const result = await this.save(true);
|
||||||
|
if (result === false) return;
|
||||||
this.$emit('back');
|
this.$emit('back');
|
||||||
},
|
},
|
||||||
async confirmCreate() {
|
async confirmCreate() {
|
||||||
await this.save();
|
const result = await this.save();
|
||||||
|
if (result === false) return;
|
||||||
this.$emit('back');
|
this.$emit('back');
|
||||||
},
|
},
|
||||||
async createAndDispatch() {
|
async createAndDispatch() {
|
||||||
const data = await this.save();
|
const data = await this.save();
|
||||||
this.$emit('dispatch', data.id);
|
if (data === false) return;
|
||||||
|
const id = data?.id || data?.masterOrderId || data?.masterId;
|
||||||
|
if (!id) {
|
||||||
|
this.$message.error('保存成功但未获取到总单ID,无法进入调度');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$emit('dispatch', id);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.master-editor section {
|
.master-editor section {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: 1px solid #eff1f7;
|
border: 1px solid #eff1f7;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -1054,22 +1431,21 @@ export default {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.el-form-item {
|
.el-form-item {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.goods-heading {
|
.goods-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
flex-direction: row;
|
justify-content: flex-start;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
h3 {
|
h3 {
|
||||||
margin-right: auto;
|
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.goods-toolbar {
|
.goods-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-left: auto;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.goods-actions {
|
.goods-actions {
|
||||||
@@ -1077,18 +1453,45 @@ export default {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
:deep(.master-editor__quantity-cell .el-input-number) {
|
.master-editor__attachment {
|
||||||
width: 80px;
|
width: 100%;
|
||||||
|
}
|
||||||
|
.master-editor__attachment-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.master-editor__attachment-table {
|
||||||
|
width: 100%;
|
||||||
|
:deep(.el-table__header th) {
|
||||||
|
background: #f5f7fa;
|
||||||
|
color: #303133;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
:deep(.el-table__cell) {
|
||||||
|
border-color: #eff1f7;
|
||||||
|
}
|
||||||
|
:deep(.el-table__row:nth-child(even) td.el-table__cell) {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.master-editor__quantity-cell .el-input) {
|
||||||
|
width: 156px;
|
||||||
}
|
}
|
||||||
.section-head {
|
.section-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 16px;
|
||||||
h3 {
|
h3 {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.section-head__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
.route-dialog__search {
|
.route-dialog__search {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
padding: 12px 12px 4px;
|
padding: 12px 12px 4px;
|
||||||
@@ -1139,6 +1542,12 @@ export default {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
&--middle {
|
||||||
|
background: #67c23a;
|
||||||
|
}
|
||||||
|
&--end {
|
||||||
|
background: #e6a23c;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.route-node-form {
|
.route-node-form {
|
||||||
|
|||||||
@@ -27,26 +27,31 @@
|
|||||||
|
|
||||||
<el-dialog v-model="detailVisible" title="导入运单详情" width="90%" append-to-body>
|
<el-dialog v-model="detailVisible" title="导入运单详情" width="90%" append-to-body>
|
||||||
<el-form :inline="true" :model="detailQuery"><el-form-item label="车牌号/船号"><el-input v-model="detailQuery.vehicleNo" clearable /></el-form-item><el-form-item label="货物名称"><el-input v-model="detailQuery.cargoName" clearable /></el-form-item><el-button type="primary" @click="loadDetails">查询</el-button></el-form>
|
<el-form :inline="true" :model="detailQuery"><el-form-item label="车牌号/船号"><el-input v-model="detailQuery.vehicleNo" clearable /></el-form-item><el-form-item label="货物名称"><el-input v-model="detailQuery.cargoName" clearable /></el-form-item><el-button type="primary" @click="loadDetails">查询</el-button></el-form>
|
||||||
<el-table :data="details" border><el-table-column type="index" label="序号" width="65" /><el-table-column prop="batchNo" label="运单批次号" min-width="140" /><el-table-column prop="originalNo" label="原始单号" min-width="130" /><el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" min-width="190" /><el-table-column prop="driverName" label="司机/船长" min-width="120" /><el-table-column prop="transportType" label="运输类型" width="110" /><el-table-column prop="cargoName" label="货物名称" width="130" /><el-table-column prop="cargoType" label="货物类型" width="120" /><el-table-column prop="quantity" label="重量" width="90" /><el-table-column prop="departureAddress" label="发货地址" min-width="220" show-overflow-tooltip /><el-table-column prop="departureContact" label="发货联系人" width="110" /><el-table-column prop="departurePhone" label="发货联系人电话" width="140" /><el-table-column prop="arrivalAddress" label="到货地址" min-width="220" show-overflow-tooltip /><el-table-column prop="arrivalContact" label="收货联系人" width="110" /><el-table-column prop="arrivalPhone" label="收货联系人电话" width="140" /><el-table-column prop="startDate" label="开始时间" width="160" sortable/><el-table-column prop="endDate" label="结束时间" width="160" sortable/><el-table-column prop="unitPrice" label="单价" width="90" /><el-table-column prop="freight" label="运费" width="100" /><el-table-column prop="otherFeeTotal" label="其他费用合计" width="120" /><el-table-column prop="freightTotal" label="运费合计" width="100" /><el-table-column prop="remark" label="备注" min-width="160" /><el-table-column label="操作" width="80">-</el-table-column></el-table>
|
<el-table :data="details" border><el-table-column type="index" label="序号" width="65" /><el-table-column prop="batchNo" label="运单批次号" min-width="140" /><el-table-column prop="originalNo" label="原始单号" min-width="130" /><el-table-column prop="vehicleNo" label="车牌号/航班号/船号/班列号" min-width="190" /><el-table-column prop="driverName" label="司机/船长" min-width="120" /><el-table-column prop="transportType" label="运输方式" width="110" /><el-table-column prop="cargoName" label="货物名称" width="130" /><el-table-column prop="cargoType" label="货物类型" width="120" /><el-table-column prop="quantity" label="重量" width="90" /><el-table-column prop="departureAddress" label="发货地址" min-width="220" show-overflow-tooltip /><el-table-column prop="departureContact" label="发货联系人" width="110" /><el-table-column prop="departurePhone" label="发货联系人电话" width="140" /><el-table-column prop="arrivalAddress" label="到货地址" min-width="220" show-overflow-tooltip /><el-table-column prop="arrivalContact" label="收货联系人" width="110" /><el-table-column prop="arrivalPhone" label="收货联系人电话" width="140" /><el-table-column prop="startDate" label="开始时间" width="160" sortable/><el-table-column prop="endDate" label="结束时间" width="160" sortable/><el-table-column prop="unitPrice" label="单价" width="90" /><el-table-column prop="freight" label="运费" width="100" /><el-table-column prop="otherFeeTotal" label="其他费用合计" width="120" /><el-table-column prop="freightTotal" label="运费合计" width="100" /><el-table-column prop="remark" label="备注" min-width="160" /><el-table-column label="操作" width="80">-</el-table-column></el-table>
|
||||||
<el-pagination v-model:current-page="detailPage.current" v-model:page-size="detailPage.size" layout="total, prev, pager, next, sizes" :page-sizes="[10, 20, 50]" :total="detailPage.total" @current-change="loadDetails" @size-change="loadDetails" />
|
<el-pagination v-model:current-page="detailPage.current" v-model:page-size="detailPage.size" layout="total, prev, pager, next, sizes" :page-sizes="[10, 20, 50]" :total="detailPage.total" @current-change="loadDetails" @size-change="loadDetails" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="createVisible" title="新建导入" width="85%" append-to-body destroy-on-close class="waybill-import-create dialog-form--carded">
|
<el-dialog v-model="createVisible" title="新建导入" width="85%" append-to-body destroy-on-close class="waybill-import-create">
|
||||||
<section-card title="导入信息">
|
<section class="waybill-import-create__form-panel">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="calc(6em + 24px)">
|
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="auto" class="archive-form">
|
||||||
<el-row :gutter="18"><el-col :span="6"><el-form-item label="项目" prop="projectId"><el-select v-model="form.projectId" filterable clearable placeholder="模糊查询后选择" @change="projectChange"><el-option v-for="item in projects" :key="item.id" :label="item.projectName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="客户" prop="customerName"><el-input v-model="form.customerName" disabled placeholder="自动带出" /></el-form-item></el-col><el-col :span="6"><el-form-item label="客户合同" prop="contractId"><el-select v-model="form.contractId" filterable clearable placeholder="自动带出"><el-option v-for="item in contracts" :key="item.id" :label="item.contractName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运类型" prop="carrierType"><el-select v-model="form.carrierType"><el-option label="承运商" value="承运商" /><el-option label="自运" value="自运" /><el-option label="网货平台" value="网货平台" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运商"><el-select v-model="form.carrierIds" multiple filterable placeholder="自动带出,多个需选择"><el-option v-for="item in carriers" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入状态" prop="status"><el-select v-model="form.status"><el-option label="完成" value="completed" /><el-option label="进行中" value="processing" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入类型" prop="importType"><el-select v-model="form.importType"><el-option label="运单" value="waybill" /><el-option label="结算单" value="settlement" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="发货计划"><el-select v-model="form.planId" clearable filterable placeholder="请选择"><el-option v-for="item in plans" :key="item.id" :label="item.planName" :value="item.id" /></el-select></el-form-item></el-col></el-row>
|
<el-row :gutter="20"><el-col :span="6"><el-form-item label="项目" prop="projectId"><el-select v-model="form.projectId" filterable clearable placeholder="模糊查询后选择" @change="projectChange"><el-option v-for="item in projects" :key="item.id" :label="item.projectName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="客户" prop="customerName"><el-input v-model="form.customerName" disabled placeholder="自动带出" /></el-form-item></el-col><el-col :span="6"><el-form-item label="客户合同" prop="contractId"><el-select v-model="form.contractId" filterable clearable placeholder="自动带出" @change="contractChange"><el-option v-for="item in contracts" :key="item.id" :label="item.contractName" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运类型" prop="carrierType"><el-select v-model="form.carrierType"><el-option label="承运商" value="承运商" /><el-option label="自运" value="自运" /><el-option label="网货平台" value="网货平台" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="承运商"><el-select v-model="form.carrierIds" multiple filterable placeholder="自动带出,多个需选择"><el-option v-for="item in carriers" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入状态" prop="status"><el-select v-model="form.status"><el-option label="完成" value="completed" /><el-option label="进行中" value="processing" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="导入类型" prop="importType"><el-select v-model="form.importType"><el-option label="运单" value="waybill" /><el-option label="结算单" value="settlement" /></el-select></el-form-item></el-col><el-col :span="6"><el-form-item label="计划名称"><el-select v-model="form.planId" clearable filterable placeholder="请选择"><el-option v-for="item in plans" :key="item.id" :label="item.planName" :value="item.id" /></el-select></el-form-item></el-col></el-row>
|
||||||
<el-form-item label="运单明细表" prop="file"><el-upload action="#" accept=".xls,.xlsx" :auto-upload="false" :limit="1" :file-list="files" :on-change="fileChange" :on-remove="fileRemove"><el-button type="primary">添加附件</el-button></el-upload><span class="waybill-import-create__file-tip">请上传运单明细表,仅支持 excel 格式</span><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item>
|
<el-form-item label="运单明细表" prop="file"><el-upload action="#" accept=".xls,.xlsx" :auto-upload="false" :limit="1" :file-list="files" :on-change="fileChange" :on-remove="fileRemove"><el-button type="primary">添加附件</el-button></el-upload><span class="waybill-import-create__file-tip">请上传运单明细表,仅支持 excel 格式</span><el-link type="primary" @click="downloadTemplate">下载模板</el-link></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div class="waybill-import-create__form-actions"><el-button @click="createVisible = false">关闭</el-button><el-button @click="saveDraft">保存草稿</el-button><el-button type="primary" @click="confirmImport">确认导入</el-button></div>
|
<div class="waybill-import-create__form-actions"><el-button @click="createVisible = false">关闭</el-button><el-button @click="saveDraft">保存草稿</el-button><el-button type="primary" @click="confirmImport">确认导入</el-button></div>
|
||||||
</section-card>
|
</section>
|
||||||
<section class="waybill-import-create__detail-panel" v-if="rows.length"><div class="waybill-import-create__detail-head"><div class="dialog-section-title">导入数据明细</div><el-button type="danger" plain :disabled="!rowSelection.length" @click="removeSelectedRows">批量删除</el-button></div><el-tabs v-model="previewTab"><el-tab-pane :label="`运单数(${rows.length})`" name="all" /><el-tab-pane :label="`疑似重复(${duplicateRows.length})`" name="duplicate" /></el-tabs><el-table :data="previewRows" border max-height="440" @selection-change="rowSelection = $event"><el-table-column type="selection" width="55" /><el-table-column type="index" label="序号" width="65" /><el-table-column v-for="column in detailColumns" :key="column.prop" :prop="column.prop" :label="column.label" min-width="140" show-overflow-tooltip /><el-table-column label="操作" width="180" fixed="right"><template #default="{ row }"><el-link type="primary" @click="cancelRow(row)">取消</el-link><el-link type="primary" @click="saveRow(row)">保存</el-link><el-link type="primary" @click="editRow(row)">编辑</el-link><el-link type="danger" @click="deleteRow(row)">删除</el-link></template></el-table-column></el-table></section>
|
<section class="waybill-import-create__detail-panel" v-if="rows.length"><div class="waybill-import-create__detail-head"><div class="dialog-section-title">导入数据明细</div><el-button type="danger" plain :disabled="!rowSelection.length" @click="removeSelectedRows">批量删除</el-button></div><el-tabs v-model="previewTab"><el-tab-pane :label="`运单数(${rows.length})`" name="all" /><el-tab-pane :label="`疑似重复(${duplicateRows.length})`" name="duplicate" /></el-tabs><el-table :data="previewRows" border max-height="440" @selection-change="rowSelection = $event"><el-table-column type="selection" width="55" /><el-table-column type="index" label="序号" width="65" /><el-table-column v-for="column in detailColumns" :key="column.prop" :prop="column.prop" :label="column.label" :min-width="column.minWidth || 150" show-overflow-tooltip><template #default="{ row }"><template v-if="row._editing"><el-select v-if="column.editor === 'transportType'" v-model="row.transportType" clearable filterable placeholder="请选择"><el-option v-for="item in transportTypeOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select><el-select v-else-if="column.editor === 'driver'" v-model="row.driverName" clearable filterable placeholder="请选择" @change="value => handleEditorChange(column, row, value)"><el-option v-for="item in driverOptions" :key="item.id || item.driverName || item.name" :label="item.driverName || item.name" :value="item.driverName || item.name" /></el-select><el-cascader v-else-if="column.editor === 'cargoType'" :model-value="resolveCargoTypePath(row)" :options="cargoTypeOptions" :props="{ label: 'cargoName', value: 'id', children: 'children', emitPath: true }" clearable filterable placeholder="请选择" @change="value => handleEditorChange(column, row, value)" /><el-select v-else-if="column.editor === 'cargoName'" v-model="row.cargoName" clearable filterable placeholder="请选择" @visible-change="visible => visible && loadCommonCargoOptions(row)" @change="value => handleEditorChange(column, row, value)"><el-option v-for="item in getCommonCargoOptions(row)" :key="item.id || item.cargoName || item.name" :label="formatCargoTypeLabel(item)" :value="formatCargoTypeLabel(item)" /></el-select><el-date-picker v-else-if="column.editor === 'datetime'" v-model="row[column.prop]" type="datetime" format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择" /><el-input v-else-if="column.editor === 'textarea'" v-model="row[column.prop]" type="textarea" :autosize="{ minRows: 1, maxRows: 3 }" :placeholder="`请输入${column.label}`" /><el-input v-else-if="column.editor === 'number'" :model-value="row[column.prop]" inputmode="decimal" :placeholder="`请输入${column.label}`" @input="value => updateEditorValue(column, row, value)" /><el-input v-else v-model="row[column.prop]" :placeholder="`请输入${column.label}`" /></template><span v-else>{{ formatCell(row, column) }}</span></template></el-table-column><el-table-column label="操作" width="180" 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></template></el-table-column></el-table></section>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive, ref } from 'vue';
|
import { computed, reactive, ref } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { getList as getCargoTypeList } from '@/api/base/cargo-type';
|
||||||
|
import { getList as getCommonCargoList } from '@/api/business/common-cargo';
|
||||||
|
import { getList as getContractList } from '@/api/business/contract-manage';
|
||||||
import { getList as getProjectList } from '@/api/business/project-apply';
|
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||||
|
import { getList as getDriverList } from '@/api/transportCapacity/driver';
|
||||||
|
import { getDictionary } from '@/api/system/dictbiz';
|
||||||
import * as api from '@/api/business/waybill-manage';
|
import * as api from '@/api/business/waybill-manage';
|
||||||
import { downloadXls } from '@/utils/util';
|
import { downloadXls } from '@/utils/util';
|
||||||
|
|
||||||
@@ -56,21 +61,162 @@ const visible = computed({ get: () => props.modelValue, set: value => emit('upda
|
|||||||
const createVisible = ref(false), detailVisible = ref(false), formRef = ref();
|
const createVisible = ref(false), detailVisible = ref(false), formRef = ref();
|
||||||
const query = reactive({ batchNo: '', carrierId: '', createUser: '', createTimeRange: [] });
|
const query = reactive({ batchNo: '', carrierId: '', createUser: '', createTimeRange: [] });
|
||||||
const detailQuery = reactive({ vehicleNo: '', cargoName: '' });
|
const detailQuery = reactive({ vehicleNo: '', cargoName: '' });
|
||||||
const form = reactive({ projectId: '', customerName: '', contractId: '', carrierType: '承运商', carrierIds: [], status: 'completed', importType: 'waybill', planId: '', file: null });
|
const form = reactive({ projectId: '', customerName: '', contractId: '', contractName: '', carrierType: '承运商', carrierIds: [], status: 'completed', importType: 'waybill', planId: '', file: null });
|
||||||
const rules = { projectId: [{ required: true, message: '请选择项目' }], customerName: [{ required: true, message: '客户不能为空' }], contractId: [{ required: true, message: '请选择客户合同' }], carrierType: [{ required: true, message: '请选择承运类型' }], status: [{ required: true, message: '请选择导入状态' }], importType: [{ required: true, message: '请选择导入类型' }], file: [{ required: true, message: '请上传明细表' }] };
|
const rules = { projectId: [{ required: true, message: '请选择项目' }], customerName: [{ required: true, message: '客户不能为空' }], contractId: [{ required: true, message: '请选择客户合同' }], carrierType: [{ required: true, message: '请选择承运类型' }], status: [{ required: true, message: '请选择导入状态' }], importType: [{ required: true, message: '请选择导入类型' }], file: [{ required: true, message: '请上传明细表' }] };
|
||||||
const batches = ref([]), details = ref([]), rows = ref([]), selected = ref([]), files = ref([]), projects = ref([]), contracts = ref([]), carriers = ref([]), creators = ref([]), plans = ref([]);
|
const batches = ref([]), details = ref([]), rows = ref([]), selected = ref([]), files = ref([]), projects = ref([]), contracts = ref([]), carriers = ref([]), creators = ref([]), plans = ref([]);
|
||||||
|
const transportTypeOptions = ref([]), cargoTypeOptions = ref([]), cargoTypeFlatOptions = ref([]), commonCargoOptionsMap = ref({}), commonCargoLoadingMap = ref({}), driverOptions = ref([]);
|
||||||
const rowSelection = ref([]), previewTab = ref('all');
|
const rowSelection = ref([]), previewTab = ref('all');
|
||||||
const duplicateRows = computed(() => rows.value.filter(row => row._duplicate));
|
const duplicateRows = computed(() => rows.value.filter(row => row._duplicate));
|
||||||
const previewRows = computed(() => previewTab.value === 'duplicate' ? duplicateRows.value : rows.value);
|
const previewRows = computed(() => previewTab.value === 'duplicate' ? duplicateRows.value : rows.value);
|
||||||
const page = reactive({ current: 1, size: 10, total: 0 }), detailPage = reactive({ current: 1, size: 10, total: 0 });
|
const page = reactive({ current: 1, size: 10, total: 0 }), detailPage = reactive({ current: 1, size: 10, total: 0 });
|
||||||
const detailColumns = [{ prop: 'batchNo', label: '运单批次号' }, { prop: 'originalNo', label: '原始单号' }, { prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' }, { prop: 'driverName', label: '司机/船长' }, { prop: 'transportType', label: '运输类型' }, { prop: 'cargoName', label: '货物名称' }, { prop: 'cargoType', label: '货物类型' }, { prop: 'quantity', label: '重量' }, { prop: 'departureAddress', label: '发货地址' }, { prop: 'departureContact', label: '发货联系人' }, { prop: 'departurePhone', label: '发货联系人电话' }, { prop: 'arrivalAddress', label: '到货地址' }, { prop: 'arrivalContact', label: '收货联系人' }, { prop: 'arrivalPhone', label: '收货联系人电话' }, { prop: 'startDate', label: '开始时间' }, { prop: 'endDate', label: '结束时间' }, { prop: 'unitPrice', label: '单价' }, { prop: 'freight', label: '运费' }, { prop: 'otherFeeTotal', label: '其他费用合计' }, { prop: 'freightTotal', label: '运费合计' }, { prop: 'remark', label: '备注' }];
|
const detailColumns = [
|
||||||
const requiredDetailFields = [{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' }, { prop: 'driverName', label: '司机/船长' }, { prop: 'transportType', label: '运输类型' }, { prop: 'cargoName', label: '货物名称' }, { prop: 'cargoType', label: '货物类型' }, { prop: 'departureAddress', label: '发货地址' }, { prop: 'departureContact', label: '发货联系人' }, { prop: 'departurePhone', label: '发货联系人电话' }, { prop: 'arrivalAddress', label: '到货地址' }, { prop: 'arrivalContact', label: '到货联系人' }, { prop: 'arrivalPhone', label: '收货联系人电话' }, { prop: 'startDate', label: '开始时间' }, { prop: 'endDate', label: '结束时间' }, { prop: 'unitPrice', label: '单价' }, { prop: 'freight', label: '运费' }];
|
{ prop: 'batchNo', label: '运单批次号', editor: 'input', minWidth: 150 },
|
||||||
|
{ prop: 'originalNo', label: '原始单号', editor: 'input', minWidth: 150 },
|
||||||
|
{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号', editor: 'input', minWidth: 220 },
|
||||||
|
{ prop: 'driverName', label: '司机/船长', editor: 'driver', minWidth: 170 },
|
||||||
|
{ prop: 'transportType', label: '运输方式', editor: 'transportType', minWidth: 150 },
|
||||||
|
{ prop: 'cargoType', label: '货物类型', editor: 'cargoType', minWidth: 220 },
|
||||||
|
{ prop: 'cargoName', label: '货物名称', editor: 'cargoName', minWidth: 220 },
|
||||||
|
{ prop: 'quantity', label: '重量', editor: 'number', minWidth: 130 },
|
||||||
|
{ prop: 'departureAddress', label: '发货地址', editor: 'textarea', minWidth: 260 },
|
||||||
|
{ prop: 'departureContact', label: '发货联系人', editor: 'input', minWidth: 140 },
|
||||||
|
{ prop: 'departurePhone', label: '发货联系人电话', editor: 'input', minWidth: 160 },
|
||||||
|
{ prop: 'arrivalAddress', label: '到货地址', editor: 'textarea', minWidth: 260 },
|
||||||
|
{ prop: 'arrivalContact', label: '收货联系人', editor: 'input', minWidth: 140 },
|
||||||
|
{ prop: 'arrivalPhone', label: '收货联系人电话', editor: 'input', minWidth: 160 },
|
||||||
|
{ prop: 'startDate', label: '开始时间', editor: 'datetime', minWidth: 190 },
|
||||||
|
{ prop: 'endDate', label: '结束时间', editor: 'datetime', minWidth: 190 },
|
||||||
|
{ prop: 'unitPrice', label: '单价', editor: 'number', minWidth: 130 },
|
||||||
|
{ prop: 'freight', label: '运费', editor: 'number', minWidth: 130 },
|
||||||
|
{ prop: 'otherFeeTotal', label: '其他费用合计', editor: 'number', minWidth: 150 },
|
||||||
|
{ prop: 'freightTotal', label: '运费合计', editor: 'number', minWidth: 130 },
|
||||||
|
{ prop: 'remark', label: '备注', editor: 'textarea', minWidth: 180 },
|
||||||
|
];
|
||||||
|
const requiredDetailFields = [{ prop: 'vehicleNo', label: '车牌号/航班号/船号/班列号' }, { prop: 'driverName', label: '司机/船长' }, { prop: 'transportType', label: '运输方式' }, { prop: 'cargoName', label: '货物名称' }, { prop: 'cargoType', label: '货物类型' }, { prop: 'departureAddress', label: '发货地址' }, { prop: 'departureContact', label: '发货联系人' }, { prop: 'departurePhone', label: '发货联系人电话' }, { prop: 'arrivalAddress', label: '到货地址' }, { prop: 'arrivalContact', label: '到货联系人' }, { prop: 'arrivalPhone', label: '收货联系人电话' }, { prop: 'startDate', label: '开始时间' }, { prop: 'endDate', label: '结束时间' }, { prop: 'unitPrice', label: '单价' }, { prop: 'freight', label: '运费' }];
|
||||||
|
const projectQueryParams = { approvalStatuses: 'approved,change_approved' };
|
||||||
|
const extractRecords = res => {
|
||||||
|
const data = res?.data || res;
|
||||||
|
if (Array.isArray(data)) return data;
|
||||||
|
if (Array.isArray(data?.records)) return data.records;
|
||||||
|
if (Array.isArray(data?.data)) return data.data;
|
||||||
|
if (Array.isArray(data?.data?.records)) return data.data.records;
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
const formatCargoTypeLabel = item => item?.cargoName || item?.name || item?.typeName || item?.label || '';
|
||||||
|
const buildCargoTypeTree = cargoTypes => {
|
||||||
|
const flatCargoTypes = [];
|
||||||
|
const collectCargoTypes = list => {
|
||||||
|
(list || []).forEach(item => {
|
||||||
|
flatCargoTypes.push({ ...item, children: undefined });
|
||||||
|
if (item.children?.length) collectCargoTypes(item.children);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
collectCargoTypes(cargoTypes);
|
||||||
|
const nodeMap = new Map();
|
||||||
|
const codeMap = new Map();
|
||||||
|
flatCargoTypes.forEach(item => {
|
||||||
|
const id = item.id ?? item.cargoCode ?? item.code;
|
||||||
|
if (id === undefined || id === null) return;
|
||||||
|
const node = {
|
||||||
|
...item,
|
||||||
|
id: String(id),
|
||||||
|
cargoName: formatCargoTypeLabel(item),
|
||||||
|
cargoCode: item.cargoCode || item.code || '',
|
||||||
|
parentId: item.parentId === undefined || item.parentId === null ? '' : String(item.parentId),
|
||||||
|
parentCargoCode: item.parentCargoCode || item.parentCode || '',
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
nodeMap.set(node.id, node);
|
||||||
|
if (node.cargoCode) codeMap.set(String(node.cargoCode), node);
|
||||||
|
});
|
||||||
|
const rootNodes = [];
|
||||||
|
nodeMap.forEach(node => {
|
||||||
|
const parent = nodeMap.get(node.parentId) || codeMap.get(String(node.parentCargoCode || '')) || codeMap.get(String(node.parentId || ''));
|
||||||
|
if (parent && parent !== node && Number(node.typeLevel) !== 1) parent.children.push(node);
|
||||||
|
else rootNodes.push(node);
|
||||||
|
});
|
||||||
|
const normalize = (cargoType, level = 1, parentPath = []) => {
|
||||||
|
if (level > 2) return null;
|
||||||
|
const id = String(cargoType.id);
|
||||||
|
const path = [...parentPath, id];
|
||||||
|
const children = level === 1 ? (cargoType.children || []).map(item => normalize(item, level + 1, path)).filter(Boolean) : [];
|
||||||
|
return { ...cargoType, id, cargoName: formatCargoTypeLabel(cargoType), path, leaf: level === 2, children: children.length ? children : undefined };
|
||||||
|
};
|
||||||
|
return rootNodes.map(item => normalize(item, 1)).filter(Boolean);
|
||||||
|
};
|
||||||
|
const flattenCargoTypeOptions = options => {
|
||||||
|
const result = [];
|
||||||
|
const collect = list => (list || []).forEach(item => {
|
||||||
|
result.push(item);
|
||||||
|
if (item.children?.length) collect(item.children);
|
||||||
|
});
|
||||||
|
collect(options);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
const getCargoTypePathLabels = path => {
|
||||||
|
let options = cargoTypeOptions.value;
|
||||||
|
const labels = [];
|
||||||
|
(path || []).forEach(value => {
|
||||||
|
const option = (options || []).find(item => String(item.id) === String(value));
|
||||||
|
if (!option) return;
|
||||||
|
labels.push(option.cargoName || '');
|
||||||
|
options = option.children || [];
|
||||||
|
});
|
||||||
|
return labels.filter(Boolean);
|
||||||
|
};
|
||||||
|
const findCargoTypeByPath = path => {
|
||||||
|
const id = Array.isArray(path) ? path[path.length - 1] : path;
|
||||||
|
return cargoTypeFlatOptions.value.find(item => String(item.id) === String(id));
|
||||||
|
};
|
||||||
|
const resolveCargoTypePath = row => {
|
||||||
|
if (Array.isArray(row.cargoTypePath) && row.cargoTypePath.length) return row.cargoTypePath;
|
||||||
|
const cargoType = cargoTypeFlatOptions.value.find(item => [item.cargoName, item.name, item.typeName, item.cargoCode].some(value => String(value || '') === String(row.cargoType || row.cargoTypeCode || '')));
|
||||||
|
return cargoType?.path || [];
|
||||||
|
};
|
||||||
|
const commonCargoKey = row => {
|
||||||
|
const path = resolveCargoTypePath(row);
|
||||||
|
const cargoType = findCargoTypeByPath(path);
|
||||||
|
return String(cargoType?.cargoCode || cargoType?.code || path.join('/'));
|
||||||
|
};
|
||||||
|
const getCommonCargoOptions = row => commonCargoOptionsMap.value[commonCargoKey(row)] || [];
|
||||||
|
const loadCommonCargoOptions = async row => {
|
||||||
|
const path = resolveCargoTypePath(row);
|
||||||
|
const cargoType = findCargoTypeByPath(path);
|
||||||
|
const key = commonCargoKey(row);
|
||||||
|
if (!key || commonCargoLoadingMap.value[key]) return;
|
||||||
|
commonCargoLoadingMap.value = { ...commonCargoLoadingMap.value, [key]: true };
|
||||||
|
try {
|
||||||
|
const labels = getCargoTypePathLabels(path);
|
||||||
|
const res = await getCommonCargoList(1, 500, {
|
||||||
|
secondCargoTypeName: labels[1] || row.cargoType || '',
|
||||||
|
secondCargoTypeCode: cargoType?.cargoCode || cargoType?.code || '',
|
||||||
|
allDept: 0,
|
||||||
|
});
|
||||||
|
commonCargoOptionsMap.value = { ...commonCargoOptionsMap.value, [key]: extractRecords(res) };
|
||||||
|
} finally {
|
||||||
|
commonCargoLoadingMap.value = { ...commonCargoLoadingMap.value, [key]: false };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
cargoTypeOptions.value = buildCargoTypeTree(extractRecords(cargoTypeRes));
|
||||||
|
cargoTypeFlatOptions.value = flattenCargoTypeOptions(cargoTypeOptions.value);
|
||||||
|
driverOptions.value = extractRecords(driverRes);
|
||||||
|
};
|
||||||
const loadBatches = async () => { const res = await api.getImportBatches({ ...query, current: page.current, size: page.size }); batches.value = res.data?.data?.records || []; page.total = res.data?.data?.total || 0; };
|
const loadBatches = async () => { const res = await api.getImportBatches({ ...query, current: page.current, size: page.size }); batches.value = res.data?.data?.records || []; page.total = res.data?.data?.total || 0; };
|
||||||
const resetQuery = () => { Object.assign(query, { batchNo: '', carrierId: '', createUser: '', createTimeRange: [] }); page.current = 1; loadBatches(); };
|
const resetQuery = () => { Object.assign(query, { batchNo: '', carrierId: '', createUser: '', createTimeRange: [] }); page.current = 1; loadBatches(); };
|
||||||
const loadDetails = async () => { const res = await api.getImportDetails({ batchId: detailQuery.batchId, ...detailQuery, current: detailPage.current, size: detailPage.size }); details.value = res.data?.data?.records || []; detailPage.total = res.data?.data?.total || 0; };
|
const loadDetails = async () => { const res = await api.getImportDetails({ batchId: detailQuery.batchId, ...detailQuery, current: detailPage.current, size: detailPage.size }); details.value = res.data?.data?.records || []; detailPage.total = res.data?.data?.total || 0; };
|
||||||
const openCreate = async () => {
|
const openCreate = async () => {
|
||||||
createVisible.value = true;
|
createVisible.value = true;
|
||||||
const projectRes = await getProjectList(1, 9999, {});
|
const [projectRes] = await Promise.all([getProjectList(1, 9999, projectQueryParams), loadEditorOptions()]);
|
||||||
projects.value = projectRes.data?.data?.records || [];
|
projects.value = projectRes.data?.data?.records || [];
|
||||||
api.getImportOptions?.().then(res => {
|
api.getImportOptions?.().then(res => {
|
||||||
const data = res.data?.data || {};
|
const data = res.data?.data || {};
|
||||||
@@ -84,7 +230,27 @@ const openDetail = row => { detailQuery.batchId = row.id; detailVisible.value =
|
|||||||
const editBatch = row => { openCreate(); Object.assign(form, row); };
|
const editBatch = row => { openCreate(); Object.assign(form, row); };
|
||||||
const removeBatches = () => api.removeImportBatches(selected.value.map(item => item.id).join(',')).then(loadBatches);
|
const removeBatches = () => api.removeImportBatches(selected.value.map(item => item.id).join(',')).then(loadBatches);
|
||||||
const removeBatch = row => api.removeImportBatches(row.id).then(loadBatches);
|
const removeBatch = row => api.removeImportBatches(row.id).then(loadBatches);
|
||||||
const projectChange = id => { const item = projects.value.find(row => row.id === id); form.customerName = item?.customerName || ''; api.getImportContracts?.(id).then(res => { contracts.value = res.data?.data || []; }); };
|
const projectChange = async id => {
|
||||||
|
const item = projects.value.find(row => row.id === id);
|
||||||
|
form.customerName = item?.customerName || item?.customerNames || item?.customer || '';
|
||||||
|
form.contractId = '';
|
||||||
|
form.contractName = '';
|
||||||
|
contracts.value = [];
|
||||||
|
if (!id) return;
|
||||||
|
const res = await getContractList(1, 9999, {
|
||||||
|
projectId: id,
|
||||||
|
projectName: item?.projectName,
|
||||||
|
});
|
||||||
|
contracts.value = res.data?.data?.records || [];
|
||||||
|
if (contracts.value.length) {
|
||||||
|
form.contractId = contracts.value[0].id;
|
||||||
|
form.contractName = contracts.value[0].contractName || '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const contractChange = id => {
|
||||||
|
const item = contracts.value.find(row => row.id === id);
|
||||||
|
form.contractName = item?.contractName || '';
|
||||||
|
};
|
||||||
const fileChange = async (file, list) => {
|
const fileChange = async (file, list) => {
|
||||||
files.value = list.slice(-1); form.file = file.raw;
|
files.value = list.slice(-1); form.file = file.raw;
|
||||||
const XLSX = await import('xlsx');
|
const XLSX = await import('xlsx');
|
||||||
@@ -96,6 +262,44 @@ const fileChange = async (file, list) => {
|
|||||||
rows.value.forEach(row => { row._duplicate = count.get(row._duplicateKey) > 1; });
|
rows.value.forEach(row => { row._duplicate = count.get(row._duplicateKey) > 1; });
|
||||||
};
|
};
|
||||||
const fileRemove = () => { files.value = []; form.file = null; };
|
const fileRemove = () => { files.value = []; form.file = null; };
|
||||||
|
const formatCell = (row, column) => row[column.prop] || '';
|
||||||
|
const updateEditorValue = (column, row, value) => {
|
||||||
|
if (column.editor === 'number') {
|
||||||
|
const [integer = '', decimal = ''] = String(value || '').replace(/[^\d.]/g, '').split('.');
|
||||||
|
row[column.prop] = decimal || String(value || '').includes('.') ? `${integer}.${decimal.slice(0, 2)}` : integer;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row[column.prop] = value;
|
||||||
|
};
|
||||||
|
const handleEditorChange = (column, row, value) => {
|
||||||
|
if (column.editor === 'driver') {
|
||||||
|
const driver = driverOptions.value.find(item => [item.driverName, item.name].some(name => String(name || '') === String(value || '')));
|
||||||
|
row.driverName = value || '';
|
||||||
|
if (driver) row.driverPhone = driver.mobile || driver.phone || driver.driverPhone || row.driverPhone || '';
|
||||||
|
}
|
||||||
|
if (column.editor === 'cargoType') {
|
||||||
|
const path = Array.isArray(value) ? value : [];
|
||||||
|
const cargoType = findCargoTypeByPath(path);
|
||||||
|
const labels = getCargoTypePathLabels(path);
|
||||||
|
row.cargoTypePath = path;
|
||||||
|
row.cargoType = labels.at(-1) || '';
|
||||||
|
row.cargoTypeCode = cargoType?.cargoCode || cargoType?.code || cargoType?.id || '';
|
||||||
|
row.cargoName = '';
|
||||||
|
row.specification = '';
|
||||||
|
row.model = '';
|
||||||
|
loadCommonCargoOptions(row);
|
||||||
|
}
|
||||||
|
if (column.editor === 'cargoName') {
|
||||||
|
const cargo = getCommonCargoOptions(row).find(item => formatCargoTypeLabel(item) === value);
|
||||||
|
row.cargoName = value || '';
|
||||||
|
if (cargo) {
|
||||||
|
row.specification = cargo.specification || cargo.spec || row.specification || '';
|
||||||
|
row.model = cargo.model || cargo.modelName || row.model || '';
|
||||||
|
row.packageType = cargo.packageType || row.packageType || '';
|
||||||
|
row.brand = cargo.brand || row.brand || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
const downloadTemplate = async () => {
|
const downloadTemplate = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.exportImportTemplate();
|
const res = await api.exportImportTemplate();
|
||||||
@@ -119,9 +323,9 @@ const validateImportRows = () => {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
const editRow = row => { row._editing = true; };
|
const editRow = row => { row._editSnapshot = JSON.parse(JSON.stringify(row)); row._editing = true; };
|
||||||
const saveRow = row => { row._editing = false; };
|
const saveRow = row => { row._editing = false; delete row._editSnapshot; };
|
||||||
const cancelRow = row => { row._cancelled = true; };
|
const cancelRow = row => { if (row._editSnapshot) Object.assign(row, row._editSnapshot); row._editing = false; delete row._editSnapshot; };
|
||||||
const deleteRow = row => { rows.value = rows.value.filter(item => item !== row); };
|
const deleteRow = row => { rows.value = rows.value.filter(item => item !== row); };
|
||||||
const removeSelectedRows = () => { const keys = new Set(rowSelection.value.map(row => row._key)); rows.value = rows.value.filter(row => !keys.has(row._key)); rowSelection.value = []; };
|
const removeSelectedRows = () => { const keys = new Set(rowSelection.value.map(row => row._key)); rows.value = rows.value.filter(row => !keys.has(row._key)); rowSelection.value = []; };
|
||||||
const saveDraft = () => api.saveImportDraft({ ...form, rows: rows.value });
|
const saveDraft = () => api.saveImportDraft({ ...form, rows: rows.value });
|
||||||
@@ -153,15 +357,22 @@ const confirmImport = async () => { await formRef.value?.validate(); if (!valida
|
|||||||
}
|
}
|
||||||
|
|
||||||
.waybill-import-create {
|
.waybill-import-create {
|
||||||
&__form-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
|
&__form-panel,
|
||||||
&__detail-panel {
|
&__detail-panel {
|
||||||
background: #fff;
|
border: 1px solid #eff1f7;
|
||||||
border-radius: 6px;
|
padding: 20px 24px;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
||||||
padding: 14px 16px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__detail-panel { margin-top: 12px; }
|
||||||
|
&__form-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 8px; }
|
||||||
&__detail-head { display: flex; align-items: center; gap: 24px; margin-bottom: 8px; }
|
&__detail-head { display: flex; align-items: center; gap: 24px; margin-bottom: 8px; }
|
||||||
&__file-tip { margin: 0 24px; color: #606266; }
|
&__file-tip { margin: 0 24px; color: #606266; }
|
||||||
|
|
||||||
|
:deep(.el-table .el-input),
|
||||||
|
:deep(.el-table .el-select),
|
||||||
|
:deep(.el-table .el-cascader),
|
||||||
|
:deep(.el-table .el-date-editor) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="运输类型">
|
<el-form-item label="运输方式">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="searchForm.transportType"
|
v-model="searchForm.transportType"
|
||||||
clearable
|
clearable
|
||||||
@@ -262,22 +262,20 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="配载子单号" prop="loadingSubNos" min-width="220">
|
<el-table-column label="配载子单号" prop="loadingSubNos" min-width="220">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="loading-manage-page__multi-line">
|
<span class="loading-manage-page__sub-nos">
|
||||||
<el-link
|
{{ splitText(row.loadingSubNos).join(',') || '-' }}
|
||||||
v-for="item in splitText(row.loadingSubNos)"
|
</span>
|
||||||
:key="item"
|
|
||||||
type="primary"
|
|
||||||
class="loading-manage-page__sub-link"
|
|
||||||
>{{ item }}</el-link>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
label="车牌号/航班号/船号/班列号"
|
|
||||||
prop="vehicleNo"
|
prop="vehicleNo"
|
||||||
min-width="210"
|
min-width="170"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
>
|
||||||
|
<template #header>
|
||||||
|
<span class="loading-manage-page__vehicle-header">车牌号/航班号/船号<br />班列号</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="司机" prop="driverName" min-width="120" show-overflow-tooltip />
|
<el-table-column label="司机" prop="driverName" min-width="120" show-overflow-tooltip />
|
||||||
<el-table-column label="联系电话" prop="driverPhone" min-width="140" show-overflow-tooltip />
|
<el-table-column label="联系电话" prop="driverPhone" min-width="140" show-overflow-tooltip />
|
||||||
<el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip />
|
<el-table-column label="承运类型" prop="carrierType" min-width="120" show-overflow-tooltip />
|
||||||
@@ -285,17 +283,32 @@
|
|||||||
label="发货地"
|
label="发货地"
|
||||||
prop="departureAddress"
|
prop="departureAddress"
|
||||||
min-width="180"
|
min-width="180"
|
||||||
show-overflow-tooltip
|
>
|
||||||
/>
|
<template #default="{ row }">
|
||||||
<el-table-column label="途经地" prop="transitAddress" min-width="190" show-overflow-tooltip />
|
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'departure') }}</div>
|
||||||
<el-table-column label="到货地" prop="arrivalAddress" min-width="180" show-overflow-tooltip />
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="途经地" prop="transitAddress" min-width="190">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'transit') }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="到货地" prop="arrivalAddress" min-width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="loading-manage-page__address-cell">{{ formatRouteAddress(row, 'arrival') }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip />
|
<el-table-column label="承运商" prop="carrierName" min-width="160" show-overflow-tooltip />
|
||||||
<el-table-column
|
<el-table-column
|
||||||
label="运输类型"
|
label="运输方式"
|
||||||
prop="transportType"
|
prop="transportType"
|
||||||
min-width="150"
|
min-width="150"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ transportTypeLabel(row.transportType) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="数据来源" prop="dataSource" min-width="130" show-overflow-tooltip />
|
<el-table-column label="数据来源" prop="dataSource" min-width="130" show-overflow-tooltip />
|
||||||
<el-table-column label="创建时间" prop="createTime" min-width="170" show-overflow-tooltip sortable/>
|
<el-table-column label="创建时间" prop="createTime" min-width="170" show-overflow-tooltip sortable/>
|
||||||
<el-table-column label="更新时间" prop="updateTime" min-width="170" show-overflow-tooltip sortable/>
|
<el-table-column label="更新时间" prop="updateTime" min-width="170" show-overflow-tooltip sortable/>
|
||||||
@@ -369,7 +382,7 @@
|
|||||||
</section-card>
|
</section-card>
|
||||||
<section-card title="货物信息"><el-table :data="cargoRows" border height="220"><el-table-column type="index" label="序号" width="70"/><el-table-column prop="projectName" label="项目名称" min-width="140"/><el-table-column prop="cargoName" label="货物名称" min-width="140"/><el-table-column prop="cargoType" label="货物类型" min-width="120"/><el-table-column prop="packageType" label="包装" min-width="100"/><el-table-column prop="weight" label="重量(吨)" min-width="110"/><el-table-column prop="volume" label="体积(方)" min-width="110"/><el-table-column prop="quantity" label="数量" min-width="90"/><el-table-column prop="materialCode" label="物料编码" min-width="120"/><el-table-column prop="equipmentCode" label="设备编码" min-width="120"/><el-table-column prop="brand" label="品牌" min-width="120"/><el-table-column prop="specificationModel" label="规格型号" min-width="140"/><el-table-column prop="remark" label="备注" min-width="160"/><el-table-column prop="unitPrice" label="货物单价(元)" min-width="140"/><el-table-column prop="planDate" label="计划日期" min-width="130"/></el-table></section-card>
|
<section-card title="货物信息"><el-table :data="cargoRows" border height="220"><el-table-column type="index" label="序号" width="70"/><el-table-column prop="projectName" label="项目名称" min-width="140"/><el-table-column prop="cargoName" label="货物名称" min-width="140"/><el-table-column prop="cargoType" label="货物类型" min-width="120"/><el-table-column prop="packageType" label="包装" min-width="100"/><el-table-column prop="weight" label="重量(吨)" min-width="110"/><el-table-column prop="volume" label="体积(方)" min-width="110"/><el-table-column prop="quantity" label="数量" min-width="90"/><el-table-column prop="materialCode" label="物料编码" min-width="120"/><el-table-column prop="equipmentCode" label="设备编码" min-width="120"/><el-table-column prop="brand" label="品牌" min-width="120"/><el-table-column prop="specificationModel" label="规格型号" min-width="140"/><el-table-column prop="remark" label="备注" min-width="160"/><el-table-column prop="unitPrice" label="货物单价(元)" min-width="140"/><el-table-column prop="planDate" label="计划日期" min-width="130"/></el-table></section-card>
|
||||||
<section-card title="关联运单信息"><el-table :data="selectedWaybillRows" border><el-table-column type="index" label="序号" width="70"/><el-table-column prop="waybillNo" label="运单号" min-width="150"><template #default="{ row }"><el-link type="primary">{{ row.waybillNo || '-' }}</el-link></template></el-table-column><el-table-column label="配载单号" min-width="150"><template #default>{{ dialogForm.loadingNo || '-' }}</template></el-table-column><el-table-column prop="projectName" label="项目名称" min-width="150"/><el-table-column prop="customerName" label="客户" min-width="140"/><el-table-column prop="departureAddress" label="发货地址" min-width="220" show-overflow-tooltip/><el-table-column prop="arrivalAddress" label="到货地址" min-width="220" show-overflow-tooltip/></el-table></section-card>
|
<section-card title="关联运单信息"><el-table :data="selectedWaybillRows" border><el-table-column type="index" label="序号" width="70"/><el-table-column prop="waybillNo" label="运单号" min-width="150"><template #default="{ row }"><el-link type="primary">{{ row.waybillNo || '-' }}</el-link></template></el-table-column><el-table-column label="配载单号" min-width="150"><template #default>{{ dialogForm.loadingNo || '-' }}</template></el-table-column><el-table-column prop="projectName" label="项目名称" min-width="150"/><el-table-column prop="customerName" label="客户" min-width="140"/><el-table-column prop="departureAddress" label="发货地址" min-width="220" show-overflow-tooltip/><el-table-column prop="arrivalAddress" label="到货地址" min-width="220" show-overflow-tooltip/></el-table></section-card>
|
||||||
<div class="loading-detail__bottom-grid"><section-card title="运输节点"><template #extra><div class="loading-detail__process-actions"><el-select v-model="processProjectId" filterable remote clearable placeholder="切换项目" :remote-method="loadProjectOptions" :loading="projectLoading" @visible-change="visible => visible && loadProjectOptions()" @change="handleProcessProjectChange"><el-option v-for="item in projectOptions" :key="item.id" :label="item.projectName || item.name" :value="item.id" /></el-select><el-button plain @click="handleProcessAction('supplement')">补录执行</el-button><el-button plain @click="handleProcessAction('batch')">批量补录</el-button></div></template><el-timeline v-if="processNodes.length"><el-timeline-item v-for="(item, index) in processNodes" :key="item.id || index" :timestamp="item.time || ''">{{ item.name || item.nodeName || item.label }}</el-timeline-item></el-timeline><el-empty v-else description="暂无运输节点" :image-size="50"/></section-card><section-card title="物流轨迹"><template #extra><div class="loading-detail__track-actions"><el-button plain @click="handleTrackAction('playback')">轨迹回放</el-button><el-button plain @click="handleTrackAction('locate')">实时定位</el-button></div></template><div ref="detailAmap" class="loading-detail__map"></div></section-card></div>
|
<div class="loading-detail__bottom-grid"><section-card title="运输节点"><template #extra><div class="loading-detail__process-actions"><el-select v-model="processProjectId" filterable placeholder="切换项目" @change="handleProcessProjectChange"><el-option v-for="item in processProjectOptions" :key="item.id" :label="item.projectName" :value="item.id" /></el-select><el-button plain @click="handleProcessAction('supplement')">补录执行</el-button><el-button plain @click="handleProcessAction('batch')">批量补录</el-button></div></template><el-timeline v-if="processNodes.length"><el-timeline-item v-for="(item, index) in processNodes" :key="item.id || index" :timestamp="item.time || ''">{{ item.name || item.nodeName || item.label }}</el-timeline-item></el-timeline><el-empty v-else description="暂无运输节点" :image-size="50"/></section-card><section-card title="物流轨迹"><template #extra><div class="loading-detail__track-actions"><el-button plain @click="handleTrackAction('playback')">轨迹回放</el-button><el-button plain @click="handleTrackAction('locate')">实时定位</el-button></div></template><div ref="detailAmap" class="loading-detail__map"></div></section-card></div>
|
||||||
</div>
|
</div>
|
||||||
<el-form
|
<el-form
|
||||||
v-else
|
v-else
|
||||||
@@ -384,7 +397,7 @@
|
|||||||
<template #extra>
|
<template #extra>
|
||||||
<el-link v-if="!dialogReadonly" type="primary" @click="openCandidateSearch">筛选</el-link>
|
<el-link v-if="!dialogReadonly" type="primary" @click="openCandidateSearch">筛选</el-link>
|
||||||
</template>
|
</template>
|
||||||
<div class="loading-manage-dialog__candidate-search" v-if="candidateSearchVisible">
|
<div class="loading-manage-dialog__candidate-search">
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="运单号">
|
<el-form-item label="运单号">
|
||||||
@@ -424,6 +437,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
|
<template v-if="candidateSearchVisible">
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="货物名称">
|
<el-form-item label="货物名称">
|
||||||
<el-input v-model="candidateQuery.cargoName" clearable placeholder="请输入" />
|
<el-input v-model="candidateQuery.cargoName" clearable placeholder="请输入" />
|
||||||
@@ -545,7 +559,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-form-item label="运输类型">
|
<el-form-item label="运输方式">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="candidateQuery.transportType"
|
v-model="candidateQuery.transportType"
|
||||||
clearable
|
clearable
|
||||||
@@ -562,14 +576,24 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="24" class="loading-manage-dialog__candidate-actions">
|
</template>
|
||||||
|
<el-col
|
||||||
|
:span="candidateSearchVisible ? 24 : 6"
|
||||||
|
class="loading-manage-dialog__candidate-actions"
|
||||||
|
>
|
||||||
<el-button type="primary" @click="handleCandidateSearch">查询</el-button>
|
<el-button type="primary" @click="handleCandidateSearch">查询</el-button>
|
||||||
<el-button @click="handleCandidateReset">重置</el-button>
|
<el-button @click="handleCandidateReset">重置</el-button>
|
||||||
<el-link type="primary" @click="candidateSearchVisible = false"
|
<el-link
|
||||||
>收起</el-link>
|
type="primary"
|
||||||
|
@click="candidateSearchVisible = !candidateSearchVisible"
|
||||||
|
>{{ candidateSearchVisible ? '收起' : '展开' }}</el-link>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
|
</section-card>
|
||||||
|
|
||||||
|
<section-card title="配载信息" class="loading-manage-dialog__load-info-card">
|
||||||
|
<section-card title="待配载运单">
|
||||||
<el-table
|
<el-table
|
||||||
ref="candidateTableRef"
|
ref="candidateTableRef"
|
||||||
v-loading="candidateLoading"
|
v-loading="candidateLoading"
|
||||||
@@ -601,10 +625,14 @@
|
|||||||
/>
|
/>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
prop="transportType"
|
prop="transportType"
|
||||||
label="运输类型"
|
label="运输方式"
|
||||||
min-width="130"
|
min-width="130"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ transportTypeLabel(row.transportType) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
prop="cargoName"
|
prop="cargoName"
|
||||||
label="货物名称"
|
label="货物名称"
|
||||||
@@ -736,9 +764,8 @@
|
|||||||
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
|
<el-table-column prop="cargoName" label="货物名称" min-width="140" />
|
||||||
<el-table-column prop="cargoType" label="货物类型" min-width="140" />
|
<el-table-column prop="cargoType" label="货物类型" min-width="140" />
|
||||||
<el-table-column prop="packageType" label="包装" min-width="100" />
|
<el-table-column prop="packageType" label="包装" min-width="100" />
|
||||||
<el-table-column prop="weight" label="重量(吨)" min-width="120" />
|
|
||||||
<el-table-column prop="volume" label="体积(方)" min-width="120" />
|
|
||||||
<el-table-column prop="quantity" label="数量" min-width="100" />
|
<el-table-column prop="quantity" label="数量" min-width="100" />
|
||||||
|
<el-table-column prop="quantityUnit" label="单位" min-width="100" />
|
||||||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||||||
<el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
|
<el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
|
||||||
<el-table-column prop="brand" label="品牌" min-width="120" />
|
<el-table-column prop="brand" label="品牌" min-width="120" />
|
||||||
@@ -759,7 +786,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="loading-manage-dialog__grid">
|
<div class="loading-manage-dialog__grid">
|
||||||
<el-form-item v-if="dialogForm.carrierType !== '自运'" label="承运商" required>
|
<el-form-item v-if="!['自运', '网货平台'].includes(dialogForm.carrierType)" label="承运商" required>
|
||||||
<el-select
|
<el-select
|
||||||
v-model="dialogForm.carrierName"
|
v-model="dialogForm.carrierName"
|
||||||
clearable
|
clearable
|
||||||
@@ -779,7 +806,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="司机" :required="dialogForm.carrierType === '自运'">
|
<el-form-item label="司机" :required="['自运', '网货平台'].includes(dialogForm.carrierType)">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="dialogForm.driverName"
|
v-model="dialogForm.driverName"
|
||||||
clearable
|
clearable
|
||||||
@@ -802,7 +829,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="手机号" :required="dialogForm.carrierType === '自运'">
|
<el-form-item label="手机号" :required="['自运', '网货平台'].includes(dialogForm.carrierType)">
|
||||||
<el-input v-model="dialogForm.driverPhone" clearable placeholder="请输入" />
|
<el-input v-model="dialogForm.driverPhone" clearable placeholder="请输入" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="车牌号" required>
|
<el-form-item label="车牌号" required>
|
||||||
@@ -855,6 +882,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</section-card>
|
</section-card>
|
||||||
|
</section-card>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -927,6 +955,7 @@ import {
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import { Location, Rank, Refresh, Setting } from '@element-plus/icons-vue';
|
import { Location, Rank, Refresh, Setting } from '@element-plus/icons-vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { isMobile } from '@/utils/validate';
|
||||||
|
|
||||||
const defaultTransportType = '';
|
const defaultTransportType = '';
|
||||||
const defaultCandidateTransportType = '';
|
const defaultCandidateTransportType = '';
|
||||||
@@ -1111,6 +1140,7 @@ export default {
|
|||||||
config: loadingManageConfig,
|
config: loadingManageConfig,
|
||||||
processNodes: [],
|
processNodes: [],
|
||||||
processProjectId: '',
|
processProjectId: '',
|
||||||
|
processProjectOptions: [],
|
||||||
routeChangeVisible: false,
|
routeChangeVisible: false,
|
||||||
routeChangeTab: 'change',
|
routeChangeTab: 'change',
|
||||||
routeChangeRows: [],
|
routeChangeRows: [],
|
||||||
@@ -1229,6 +1259,34 @@ export default {
|
|||||||
.map(item => item.trim())
|
.map(item => item.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
},
|
},
|
||||||
|
formatRouteAddress(row, type) {
|
||||||
|
const prefix = type === 'departure' ? 'departure' : type === 'arrival' ? 'arrival' : 'transit';
|
||||||
|
const values = [
|
||||||
|
row[`${prefix}RegionName`],
|
||||||
|
row[`${prefix}DistrictName`],
|
||||||
|
row[`${prefix}Address`],
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.flatMap(value => this.splitText(value))
|
||||||
|
.map(value => this.formatCityDistrict(value))
|
||||||
|
.filter(Boolean);
|
||||||
|
return [...new Set(values)].join(',') || '-';
|
||||||
|
},
|
||||||
|
formatCityDistrict(value) {
|
||||||
|
const text = String(value || '').trim();
|
||||||
|
if (!text) return '';
|
||||||
|
const cityIndex = text.indexOf('市');
|
||||||
|
const provinceIndex = Math.max(text.lastIndexOf('省', cityIndex), text.lastIndexOf('自治区', cityIndex));
|
||||||
|
const cityStart = provinceIndex > -1 ? provinceIndex + 1 : 0;
|
||||||
|
const districtStart = cityIndex > -1 ? cityIndex + 1 : cityStart;
|
||||||
|
const districtMatch = text
|
||||||
|
.slice(districtStart)
|
||||||
|
.match(/(?:自治县|自治旗|林区|矿区|新区|开发区|区|县|旗)/);
|
||||||
|
if (districtMatch) {
|
||||||
|
return text.slice(cityStart, districtStart + districtMatch.index + districtMatch[0].length);
|
||||||
|
}
|
||||||
|
return cityIndex > -1 ? text.slice(cityStart, cityIndex + 1) : text;
|
||||||
|
},
|
||||||
rowActions(row) {
|
rowActions(row) {
|
||||||
const status = row.businessStatus;
|
const status = row.businessStatus;
|
||||||
if (status === 'draft') {
|
if (status === 'draft') {
|
||||||
@@ -1286,12 +1344,11 @@ export default {
|
|||||||
...createDialogForm(),
|
...createDialogForm(),
|
||||||
...(res.data?.data || res.data || {}),
|
...(res.data?.data || res.data || {}),
|
||||||
};
|
};
|
||||||
this.processProjectId = this.dialogForm.projectId || '';
|
|
||||||
await this.loadProjectOptions(this.dialogForm.projectName || '');
|
|
||||||
await this.restoreWaybillRows(
|
await this.restoreWaybillRows(
|
||||||
this.dialogForm.waybillIdsJson,
|
this.dialogForm.waybillIdsJson,
|
||||||
this.dialogForm.loadingSubNos
|
this.dialogForm.loadingSubNos
|
||||||
);
|
);
|
||||||
|
this.syncProcessProjectOptions();
|
||||||
this.restoreRouteAndCargo();
|
this.restoreRouteAndCargo();
|
||||||
this.restoreRouteChangeRecords();
|
this.restoreRouteChangeRecords();
|
||||||
await this.loadProcessNodes();
|
await this.loadProcessNodes();
|
||||||
@@ -1326,6 +1383,7 @@ export default {
|
|||||||
this.candidateSearchVisible = false;
|
this.candidateSearchVisible = false;
|
||||||
this.processNodes = [];
|
this.processNodes = [];
|
||||||
this.processProjectId = '';
|
this.processProjectId = '';
|
||||||
|
this.processProjectOptions = [];
|
||||||
},
|
},
|
||||||
clearLoadingDialog() {
|
clearLoadingDialog() {
|
||||||
this.resetDialogData();
|
this.resetDialogData();
|
||||||
@@ -1355,13 +1413,62 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const rows = unwrapRecords(await getProcessConfigList(1, 20, { projectId: this.processProjectId, status: 1 }));
|
const projectId = String(this.processProjectId);
|
||||||
const config = rows.find(item => item.nodeConfigJson) || {};
|
const rows = unwrapRecords(
|
||||||
this.processNodes = parseJsonArray(config.nodeConfigJson);
|
await getProcessConfigList(1, 100, { projectIds: projectId, status: 1 })
|
||||||
|
);
|
||||||
|
const config =
|
||||||
|
rows.find(item => {
|
||||||
|
const projectIds = String(item.projectIds || '')
|
||||||
|
.split(',')
|
||||||
|
.map(value => value.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return projectIds.includes(projectId) && item.nodeConfigJson;
|
||||||
|
}) || {};
|
||||||
|
this.processNodes = this.parseProcessConfigNodes(config);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.processNodes = [];
|
this.processNodes = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
syncProcessProjectOptions() {
|
||||||
|
const projectMap = new Map();
|
||||||
|
this.selectedWaybillRows.forEach(row => {
|
||||||
|
const id = row.projectId;
|
||||||
|
if (!id || projectMap.has(String(id))) return;
|
||||||
|
projectMap.set(String(id), {
|
||||||
|
id,
|
||||||
|
projectName: row.projectName || row.projectNames || String(id),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!projectMap.size && this.dialogForm.projectId) {
|
||||||
|
projectMap.set(String(this.dialogForm.projectId), {
|
||||||
|
id: this.dialogForm.projectId,
|
||||||
|
projectName: this.dialogForm.projectName || String(this.dialogForm.projectId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.processProjectOptions = [...projectMap.values()];
|
||||||
|
this.processProjectId = this.processProjectOptions[0]?.id || '';
|
||||||
|
},
|
||||||
|
parseProcessConfigNodes(config = {}) {
|
||||||
|
let nodes = [];
|
||||||
|
try {
|
||||||
|
const parsed =
|
||||||
|
typeof config.nodeConfigJson === 'string'
|
||||||
|
? JSON.parse(config.nodeConfigJson)
|
||||||
|
: config.nodeConfigJson;
|
||||||
|
nodes = Array.isArray(parsed) ? parsed : parsed?.nodes || [];
|
||||||
|
} catch (error) {
|
||||||
|
nodes = [];
|
||||||
|
}
|
||||||
|
const includedNodes = String(config.includedNodes || '')
|
||||||
|
.split(',')
|
||||||
|
.map(value => value.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return nodes.filter(
|
||||||
|
node =>
|
||||||
|
node.enabled !== false && (!includedNodes.length || includedNodes.includes(node.name))
|
||||||
|
);
|
||||||
|
},
|
||||||
async handleProcessProjectChange() {
|
async handleProcessProjectChange() {
|
||||||
await this.loadProcessNodes();
|
await this.loadProcessNodes();
|
||||||
},
|
},
|
||||||
@@ -1605,7 +1712,7 @@ export default {
|
|||||||
this.dialogForm.loadingSubNos = rows
|
this.dialogForm.loadingSubNos = rows
|
||||||
.map(row => row.waybillNo)
|
.map(row => row.waybillNo)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n');
|
.join(',');
|
||||||
this.dialogForm.waybillIdsJson = JSON.stringify(rows.map(row => row.id).filter(Boolean));
|
this.dialogForm.waybillIdsJson = JSON.stringify(rows.map(row => row.id).filter(Boolean));
|
||||||
this.dialogForm.projectName = uniqueText(rows, 'projectName');
|
this.dialogForm.projectName = uniqueText(rows, 'projectName');
|
||||||
this.dialogForm.customerName = uniqueText(rows, 'customerName');
|
this.dialogForm.customerName = uniqueText(rows, 'customerName');
|
||||||
@@ -1697,10 +1804,21 @@ export default {
|
|||||||
weight: item.weight || item.cargoWeight || '',
|
weight: item.weight || item.cargoWeight || '',
|
||||||
volume: item.volume || item.cargoVolume || '',
|
volume: item.volume || item.cargoVolume || '',
|
||||||
quantity: item.quantity || item.cargoQuantity || waybill.quantity || '',
|
quantity: item.quantity || item.cargoQuantity || waybill.quantity || '',
|
||||||
|
quantityUnit:
|
||||||
|
item.quantityUnit ||
|
||||||
|
item.goodsQuantityUnit ||
|
||||||
|
item.cargoUnit ||
|
||||||
|
item.unit ||
|
||||||
|
waybill.quantityUnit ||
|
||||||
|
waybill.goodsQuantityUnit ||
|
||||||
|
waybill.cargoUnit ||
|
||||||
|
waybill.unit ||
|
||||||
|
'',
|
||||||
planDate: item.planDate || waybill.startDate || '',
|
planDate: item.planDate || waybill.startDate || '',
|
||||||
unitPrice: item.unitPrice || waybill.unitPrice || '',
|
unitPrice: item.unitPrice || waybill.unitPrice || '',
|
||||||
materialCode: item.materialCode || '',
|
materialCode: item.materialCode || '',
|
||||||
equipmentCode: item.equipmentCode || '',
|
equipmentCode:
|
||||||
|
item.equipmentCode || item.deviceCode || waybill.equipmentCode || waybill.deviceCode || '',
|
||||||
brand: item.brand || item.brandName || waybill.brand || '',
|
brand: item.brand || item.brandName || waybill.brand || '',
|
||||||
specificationModel:
|
specificationModel:
|
||||||
item.specificationModel || item.specModel || item.specification || waybill.specificationModel || '',
|
item.specificationModel || item.specModel || item.specification || waybill.specificationModel || '',
|
||||||
@@ -1736,7 +1854,15 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this.dialogForm.transportType) {
|
if (!this.dialogForm.transportType) {
|
||||||
ElMessage.warning('请选择运输类型');
|
ElMessage.warning('请选择运输方式');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const invalidPhone = [
|
||||||
|
[this.dialogForm.driverPhone, '手机号'],
|
||||||
|
[this.dialogForm.escortPhone, '押运人手机号'],
|
||||||
|
].find(([value]) => String(value || '').trim() && !isMobile(value));
|
||||||
|
if (invalidPhone) {
|
||||||
|
ElMessage.warning(`${invalidPhone[1]}格式不正确`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (draftMode) {
|
if (draftMode) {
|
||||||
@@ -1754,7 +1880,7 @@ export default {
|
|||||||
ElMessage.warning('请输入车牌号');
|
ElMessage.warning('请输入车牌号');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.dialogForm.carrierType === '自运') {
|
if (['自运', '网货平台'].includes(this.dialogForm.carrierType)) {
|
||||||
if (!this.dialogForm.driverName || !this.dialogForm.driverPhone) {
|
if (!this.dialogForm.driverName || !this.dialogForm.driverPhone) {
|
||||||
ElMessage.warning('请输入司机和司机手机号');
|
ElMessage.warning('请输入司机和司机手机号');
|
||||||
return false;
|
return false;
|
||||||
@@ -1855,6 +1981,7 @@ export default {
|
|||||||
{
|
{
|
||||||
...this.buildQueryParams(this.searchForm),
|
...this.buildQueryParams(this.searchForm),
|
||||||
ids,
|
ids,
|
||||||
|
exportColumns: JSON.stringify(loadingManageConfig.exportColumns),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
feedback: true,
|
feedback: true,
|
||||||
@@ -1864,7 +1991,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleCarrierTypeChange() {
|
handleCarrierTypeChange() {
|
||||||
if (this.dialogForm.carrierType === '自运') {
|
if (['自运', '网货平台'].includes(this.dialogForm.carrierType)) {
|
||||||
this.dialogForm.carrierName = '';
|
this.dialogForm.carrierName = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1991,17 +2118,27 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-manage-page__link,
|
.loading-manage-page__link {
|
||||||
.loading-manage-page__sub-link {
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-manage-page__multi-line {
|
.loading-manage-page__sub-nos {
|
||||||
display: flex;
|
display: block;
|
||||||
flex-direction: column;
|
overflow: hidden;
|
||||||
align-items: flex-start;
|
text-overflow: ellipsis;
|
||||||
line-height: 1.6;
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-manage-page__address-cell {
|
||||||
|
line-height: 20px;
|
||||||
|
white-space: pre-line;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-manage-page__vehicle-header {
|
||||||
|
line-height: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-manage-page__status {
|
.loading-manage-page__status {
|
||||||
@@ -2069,12 +2206,12 @@ export default {
|
|||||||
|
|
||||||
.loading-manage-dialog__grid {
|
.loading-manage-dialog__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
gap: 0 24px;
|
gap: 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-manage-dialog__span-2 {
|
.loading-manage-dialog__span-2 {
|
||||||
grid-column: span 2;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-manage-dialog__title-action {
|
.loading-manage-dialog__title-action {
|
||||||
@@ -2118,6 +2255,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
|
.loading-manage-dialog__grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.loading-manage-dialog__waybill-route-layout {
|
.loading-manage-dialog__waybill-route-layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -2233,5 +2374,14 @@ export default {
|
|||||||
}
|
}
|
||||||
.loading-route-change-dialog { .loading-route-change-dialog__original { color: #a8abb2; line-height: 20px; margin-bottom: 4px; }.loading-route-change-dialog__location { cursor: pointer; color: #909399; }.loading-route-change-dialog__hint { color: #909399; }.loading-route-change-dialog__route-list { display: flex; flex-direction: column; gap: 8px; }.loading-route-change-dialog__route-item { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto 24px; align-items: center; gap: 12px; padding: 10px 12px; background: #f5f6f7; cursor: move; }.loading-route-change-dialog__route-type { width: 30px; height: 30px; border-radius: 50%; background: #dcdfe6; text-align: center; line-height: 30px; }.loading-route-change-dialog__tags { display: flex; gap: 4px; }.loading-route-change-dialog__record-content { white-space: normal; overflow-wrap: anywhere; line-height: 22px; } }
|
.loading-route-change-dialog { .loading-route-change-dialog__original { color: #a8abb2; line-height: 20px; margin-bottom: 4px; }.loading-route-change-dialog__location { cursor: pointer; color: #909399; }.loading-route-change-dialog__hint { color: #909399; }.loading-route-change-dialog__route-list { display: flex; flex-direction: column; gap: 8px; }.loading-route-change-dialog__route-item { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto 24px; align-items: center; gap: 12px; padding: 10px 12px; background: #f5f6f7; cursor: move; }.loading-route-change-dialog__route-type { width: 30px; height: 30px; border-radius: 50%; background: #dcdfe6; text-align: center; line-height: 30px; }.loading-route-change-dialog__tags { display: flex; gap: 4px; }.loading-route-change-dialog__record-content { white-space: normal; overflow-wrap: anywhere; line-height: 22px; } }
|
||||||
.loading-common-address-dialog__toolbar { display: flex; gap: 8px; margin-bottom: 12px; }.loading-common-address-dialog__toolbar .el-input { flex: 1; }
|
.loading-common-address-dialog__toolbar { display: flex; gap: 8px; margin-bottom: 12px; }.loading-common-address-dialog__toolbar .el-input { flex: 1; }
|
||||||
@media (max-width: 900px) { .loading-detail__task-row, .loading-detail__bottom-grid { grid-template-columns: 1fr; } }
|
@media (max-width: 900px) {
|
||||||
|
.loading-manage-dialog__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-detail__task-row,
|
||||||
|
.loading-detail__bottom-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -421,12 +421,13 @@ export default {
|
|||||||
}
|
}
|
||||||
.master-card {
|
.master-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
border: 1px solid #eff1f7;
|
border: 1px solid #eff1f7;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
.card-info {
|
.card-info {
|
||||||
flex: 0 0 380px;
|
flex: 0 0 266px;
|
||||||
padding: 18px 20px;
|
padding: 18px 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #303133;
|
color: #303133;
|
||||||
@@ -446,13 +447,16 @@ export default {
|
|||||||
}
|
}
|
||||||
.status-tag {
|
.status-tag {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.route-progress {
|
.route-progress {
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow: auto;
|
min-width: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
min-height: 170px;
|
min-height: 170px;
|
||||||
padding: 22px 24px 18px;
|
padding: 22px 24px 18px;
|
||||||
border-right: 1px solid #eff1f7;
|
border-right: 1px solid #eff1f7;
|
||||||
@@ -529,14 +533,16 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.card-actions {
|
.card-actions {
|
||||||
width: 110px;
|
width: 88px;
|
||||||
padding: 16px;
|
padding: 16px 12px;
|
||||||
header {
|
header {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
.el-link {
|
.el-link {
|
||||||
margin: 0 8px 8px 0;
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 0 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,961 @@
|
|||||||
|
<template>
|
||||||
|
<basic-container class="voucher-manage-page">
|
||||||
|
<section class="voucher-manage-page__search">
|
||||||
|
<el-form :model="query" label-position="right" label-width="160px">
|
||||||
|
<div class="voucher-manage-page__search-grid">
|
||||||
|
<el-form-item label="凭证批次号"
|
||||||
|
><el-input v-model="query.voucherBatchNo" clearable placeholder="请输入"
|
||||||
|
/></el-form-item>
|
||||||
|
<el-form-item label="审核状态"
|
||||||
|
><el-select v-model="query.auditStatus" clearable placeholder="全部"
|
||||||
|
><el-option label="待审核" value="待审核" /><el-option
|
||||||
|
label="审核通过"
|
||||||
|
value="审核通过" /><el-option label="审核驳回" value="审核驳回" /></el-select
|
||||||
|
></el-form-item>
|
||||||
|
<el-form-item label="处理状态"
|
||||||
|
><el-select v-model="query.processStatus" clearable placeholder="全部"
|
||||||
|
><el-option label="上传中" value="上传中" /><el-option
|
||||||
|
label="处理中"
|
||||||
|
value="处理中" /><el-option label="处理完成" value="处理完成" /></el-select
|
||||||
|
></el-form-item>
|
||||||
|
<el-form-item label="运单批次号"
|
||||||
|
><el-input v-model="query.waybillBatchNo" clearable placeholder="请输入"
|
||||||
|
/></el-form-item>
|
||||||
|
<template v-if="searchExpanded">
|
||||||
|
<el-form-item label="上传来源"
|
||||||
|
><el-select v-model="query.uploadSource" clearable placeholder="全部"
|
||||||
|
><el-option label="内部" value="内部" /><el-option
|
||||||
|
label="承运商"
|
||||||
|
value="承运商" /></el-select
|
||||||
|
></el-form-item>
|
||||||
|
<el-form-item label="承运商"
|
||||||
|
><el-input v-model="query.carrierName" clearable placeholder="请输入"
|
||||||
|
/></el-form-item>
|
||||||
|
<el-form-item label="创建日期"
|
||||||
|
><el-date-picker
|
||||||
|
v-model="createRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="请选择"
|
||||||
|
end-placeholder="请选择"
|
||||||
|
/></el-form-item>
|
||||||
|
</template>
|
||||||
|
<div class="voucher-manage-page__search-actions">
|
||||||
|
<el-button type="primary" @click="search">查询</el-button
|
||||||
|
><el-button @click="reset">重置</el-button
|
||||||
|
><el-link type="primary" @click="searchExpanded = !searchExpanded">{{
|
||||||
|
searchExpanded ? '收起' : '展开'
|
||||||
|
}}</el-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<div class="voucher-manage-page__toolbar">
|
||||||
|
<el-button type="primary" @click="openUpload">批量导入凭证</el-button
|
||||||
|
><el-button plain @click="openProgress">上传进度</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table v-loading="loading" :data="rows" border class="voucher-manage-page__table">
|
||||||
|
<el-table-column type="selection" width="52" fixed="left" align="center" /><el-table-column
|
||||||
|
type="index"
|
||||||
|
label="序号"
|
||||||
|
width="58"
|
||||||
|
fixed="left"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column prop="voucherBatchNo" label="凭证批次号" min-width="150" fixed="left"
|
||||||
|
><template #default="{ row }"
|
||||||
|
><el-link class="voucher-manage-page__link" type="primary" @click="view(row)">{{
|
||||||
|
row.voucherBatchNo
|
||||||
|
}}</el-link></template
|
||||||
|
></el-table-column
|
||||||
|
>
|
||||||
|
<el-table-column prop="waybillBatchNo" label="运单批次号" min-width="170" /><el-table-column
|
||||||
|
prop="fileName"
|
||||||
|
label="文件名"
|
||||||
|
min-width="200"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
<el-table-column prop="uploadSource" label="上传来源" width="100" /><el-table-column
|
||||||
|
prop="carrierName"
|
||||||
|
label="承运商名称"
|
||||||
|
min-width="160"
|
||||||
|
><template #default="{ row }">{{ row.carrierName || '-' }}</template></el-table-column
|
||||||
|
>
|
||||||
|
<el-table-column prop="processStatus" label="处理状态" width="120" /><el-table-column
|
||||||
|
prop="voucherCount"
|
||||||
|
label="凭证数量"
|
||||||
|
width="100"
|
||||||
|
/><el-table-column
|
||||||
|
prop="relatedWaybillCount"
|
||||||
|
label="已关联运单"
|
||||||
|
width="115"
|
||||||
|
/><el-table-column prop="unRelatedWaybillCount" label="未关联运单" width="115" />
|
||||||
|
<el-table-column prop="status" label="状态" width="100"
|
||||||
|
><template #default="{ row }">{{
|
||||||
|
Number(row.status) === 1 ? '启用' : '停用'
|
||||||
|
}}</template></el-table-column
|
||||||
|
><el-table-column prop="createTime" label="创建时间" min-width="170" />
|
||||||
|
<el-table-column
|
||||||
|
prop="auditStatus"
|
||||||
|
label="审核状态"
|
||||||
|
width="120"
|
||||||
|
fixed="right"
|
||||||
|
align="center"
|
||||||
|
/><el-table-column label="操作" width="320" fixed="right" align="left"
|
||||||
|
><template #default="{ row }"
|
||||||
|
><div class="voucher-manage-page__actions">
|
||||||
|
<el-link
|
||||||
|
v-if="row.processStatus === '处理完成' && row.auditStatus === '待审核'"
|
||||||
|
type="primary"
|
||||||
|
>流程</el-link
|
||||||
|
><el-link v-if="row.processStatus === '处理完成'" type="primary" @click="view(row)"
|
||||||
|
>查看</el-link
|
||||||
|
><el-link
|
||||||
|
v-if="row.processStatus === '处理完成'"
|
||||||
|
type="primary"
|
||||||
|
@click="download(row)"
|
||||||
|
>下载</el-link
|
||||||
|
><el-link
|
||||||
|
v-if="row.processStatus !== '处理完成' || row.auditStatus === '待审核'"
|
||||||
|
type="primary"
|
||||||
|
@click="openUpload(row)"
|
||||||
|
>更换运单批次</el-link
|
||||||
|
><el-link
|
||||||
|
v-if="row.processStatus === '上传中' || row.auditStatus === '审核驳回'"
|
||||||
|
type="danger"
|
||||||
|
@click="removeRow(row)"
|
||||||
|
>删除</el-link
|
||||||
|
>
|
||||||
|
</div></template
|
||||||
|
></el-table-column
|
||||||
|
>
|
||||||
|
</el-table>
|
||||||
|
<div class="voucher-manage-page__pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page.current"
|
||||||
|
v-model:page-size="page.size"
|
||||||
|
:total="page.total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, prev, pager, next, sizes"
|
||||||
|
@current-change="load"
|
||||||
|
@size-change="load"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</basic-container>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="uploadVisible"
|
||||||
|
:title="editing.id ? '更换运单批次' : '批量导入凭证'"
|
||||||
|
width="1200px"
|
||||||
|
destroy-on-close
|
||||||
|
:before-close="beforeUploadDialogClose"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="uploadFormRef"
|
||||||
|
:model="editing"
|
||||||
|
:rules="rules"
|
||||||
|
label-position="right"
|
||||||
|
label-width="auto"
|
||||||
|
class="voucher-manage-page__upload-form"
|
||||||
|
>
|
||||||
|
<el-form-item label="项目名称" prop="projectId"
|
||||||
|
><el-select
|
||||||
|
v-model="editing.projectId"
|
||||||
|
class="voucher-manage-page__project-select"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="请选择"
|
||||||
|
@change="changeProject"
|
||||||
|
><el-option
|
||||||
|
v-for="item in projectOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.projectName"
|
||||||
|
:value="item.id" /></el-select
|
||||||
|
></el-form-item>
|
||||||
|
<el-form-item label="执行凭证" prop="fileUrl"
|
||||||
|
><el-upload
|
||||||
|
action="#"
|
||||||
|
accept=".zip,.7z"
|
||||||
|
:auto-upload="false"
|
||||||
|
:limit="1"
|
||||||
|
:on-change="uploadVoucher"
|
||||||
|
:on-remove="clearFile"
|
||||||
|
><el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="uploading"
|
||||||
|
:disabled="!editing.projectId || uploading"
|
||||||
|
>上传</el-button
|
||||||
|
><template #tip
|
||||||
|
><span class="voucher-manage-page__upload-tip"
|
||||||
|
>支持 zip、7z 格式,采用分片上传,上传中断后可在“上传进度”继续上传。</span
|
||||||
|
></template
|
||||||
|
></el-upload
|
||||||
|
><el-progress
|
||||||
|
v-if="uploading || editing.fileTaskId"
|
||||||
|
:percentage="uploadPercent"
|
||||||
|
:status="uploadPercent === 100 ? 'success' : undefined"
|
||||||
|
class="voucher-manage-page__upload-progress"
|
||||||
|
/></el-form-item>
|
||||||
|
<el-form-item label="关联运输批次" prop="waybillImportBatchIds"
|
||||||
|
><el-button type="primary" @click="openBatchDialog">选择</el-button></el-form-item
|
||||||
|
>
|
||||||
|
</el-form>
|
||||||
|
<el-table :data="selectedBatches" border
|
||||||
|
><el-table-column type="index" label="序号" width="80" /><el-table-column
|
||||||
|
prop="batchNo"
|
||||||
|
label="运输批次号"
|
||||||
|
min-width="220"
|
||||||
|
/><el-table-column prop="createTime" label="创建时间" min-width="170" /><el-table-column
|
||||||
|
prop="waybillCount"
|
||||||
|
label="运单数"
|
||||||
|
width="120"
|
||||||
|
/><el-table-column prop="createUserName" label="创建人" min-width="140" /><el-table-column
|
||||||
|
label="操作"
|
||||||
|
width="100"
|
||||||
|
><template #default="{ row }"
|
||||||
|
><el-link type="danger" @click="removeBatch(row)">删除</el-link></template
|
||||||
|
></el-table-column
|
||||||
|
></el-table
|
||||||
|
>
|
||||||
|
<template #footer
|
||||||
|
><el-button @click="closeUploadDialog">取消</el-button
|
||||||
|
><el-button type="primary" :loading="saving" @click="submitUpload"
|
||||||
|
>确认上传</el-button
|
||||||
|
></template
|
||||||
|
>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="batchVisible" title="关联运单批次" width="1200px" destroy-on-close>
|
||||||
|
<el-form
|
||||||
|
:model="batchQuery"
|
||||||
|
label-position="right"
|
||||||
|
label-width="160px"
|
||||||
|
class="voucher-manage-page__batch-search"
|
||||||
|
><el-row :gutter="20"
|
||||||
|
><el-col :span="12"
|
||||||
|
><el-form-item label="运单批次号"
|
||||||
|
><el-input
|
||||||
|
v-model="batchQuery.batchNo"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入" /></el-form-item></el-col
|
||||||
|
><el-col :span="12"
|
||||||
|
><el-form-item label="创建日期"
|
||||||
|
><el-date-picker
|
||||||
|
v-model="batchCreateRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
range-separator="-" /></el-form-item></el-col
|
||||||
|
><el-col :span="12"
|
||||||
|
><el-form-item label="创建人"
|
||||||
|
><el-input
|
||||||
|
v-model="batchQuery.createUser"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入" /></el-form-item></el-col
|
||||||
|
><el-col :span="12"
|
||||||
|
><el-form-item label="运单数"
|
||||||
|
><el-input-number v-model="batchQuery.waybillCount" :min="0" /></el-form-item></el-col
|
||||||
|
></el-row>
|
||||||
|
<div class="voucher-manage-page__search-actions">
|
||||||
|
<el-button type="primary" @click="loadBatches">查询</el-button
|
||||||
|
><el-button @click="resetBatchQuery">重置</el-button>
|
||||||
|
</div></el-form
|
||||||
|
>
|
||||||
|
<el-table
|
||||||
|
ref="batchTableRef"
|
||||||
|
:data="batchRows"
|
||||||
|
border
|
||||||
|
@selection-change="batchSelection = $event"
|
||||||
|
><el-table-column type="selection" width="56" /><el-table-column
|
||||||
|
type="index"
|
||||||
|
label="序号"
|
||||||
|
width="80" /><el-table-column
|
||||||
|
prop="batchNo"
|
||||||
|
label="运单批次号"
|
||||||
|
min-width="240" /><el-table-column
|
||||||
|
prop="createTime"
|
||||||
|
label="创建时间"
|
||||||
|
min-width="180" /><el-table-column label="运单数" width="120"
|
||||||
|
><template #default="{ row }">{{ row.waybillCount ?? '-' }}</template></el-table-column
|
||||||
|
><el-table-column prop="createUserName" label="创建人" min-width="140"
|
||||||
|
/></el-table>
|
||||||
|
<div class="voucher-manage-page__pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="batchPage.current"
|
||||||
|
v-model:page-size="batchPage.size"
|
||||||
|
:total="batchPage.total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, prev, pager, next, sizes"
|
||||||
|
@current-change="loadBatches"
|
||||||
|
@size-change="loadBatches"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template #footer
|
||||||
|
><el-button @click="batchVisible = false">取消</el-button
|
||||||
|
><el-button type="primary" @click="confirmBatches">确认</el-button></template
|
||||||
|
>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="progressVisible"
|
||||||
|
title="上传进度"
|
||||||
|
width="1200px"
|
||||||
|
destroy-on-close
|
||||||
|
:before-close="beforeProgressDialogClose"
|
||||||
|
@opened="loadProgress"
|
||||||
|
>
|
||||||
|
<section class="voucher-manage-page__progress-search">
|
||||||
|
<el-form :model="progressQuery" label-position="right" label-width="160px"
|
||||||
|
><div class="voucher-manage-page__search-grid">
|
||||||
|
<el-form-item label="凭证批次号"
|
||||||
|
><el-input
|
||||||
|
v-model="progressQuery.businessId"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入" /></el-form-item
|
||||||
|
><el-form-item label="文件名称"
|
||||||
|
><el-input
|
||||||
|
v-model="progressQuery.attachmentName"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入" /></el-form-item
|
||||||
|
><el-form-item label="上传状态"
|
||||||
|
><el-select v-model="progressQuery.status" clearable placeholder="全部"
|
||||||
|
><el-option label="正在上传" value="uploading" /><el-option
|
||||||
|
label="已暂停"
|
||||||
|
value="paused" /><el-option label="上传完成" value="completed" /><el-option
|
||||||
|
label="上传失败"
|
||||||
|
value="failed" /></el-select
|
||||||
|
></el-form-item>
|
||||||
|
<div class="voucher-manage-page__search-actions">
|
||||||
|
<el-button type="primary" @click="searchProgress">查询</el-button
|
||||||
|
><el-button @click="resetProgress">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div></el-form
|
||||||
|
>
|
||||||
|
</section>
|
||||||
|
<el-table :data="progressRows" border class="voucher-manage-page__table"
|
||||||
|
><el-table-column type="selection" width="52" /><el-table-column
|
||||||
|
type="index"
|
||||||
|
label="序号"
|
||||||
|
width="58"
|
||||||
|
/><el-table-column prop="businessId" label="凭证批次号" min-width="160" /><el-table-column
|
||||||
|
prop="attachmentName"
|
||||||
|
label="文件名称"
|
||||||
|
min-width="250"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/><el-table-column prop="suffix" label="文件类型" width="100" /><el-table-column
|
||||||
|
label="文件大小"
|
||||||
|
width="120"
|
||||||
|
><template #default="{ row }">{{ formatSize(row.size) }}</template></el-table-column
|
||||||
|
><el-table-column prop="createUserName" label="上传人" width="120" /><el-table-column
|
||||||
|
prop="createTime"
|
||||||
|
label="上传时间"
|
||||||
|
min-width="170"
|
||||||
|
/><el-table-column label="上传进度" min-width="220"
|
||||||
|
><template #default="{ row }"
|
||||||
|
><div class="voucher-manage-page__progress-cell">
|
||||||
|
<el-progress
|
||||||
|
:percentage="taskPercent(row)"
|
||||||
|
:stroke-width="10"
|
||||||
|
:show-text="false"
|
||||||
|
color="#409eff"
|
||||||
|
/><span class="voucher-manage-page__progress-value">{{ taskPercent(row) }}%</span>
|
||||||
|
</div></template
|
||||||
|
></el-table-column
|
||||||
|
><el-table-column label="上传状态" width="120"
|
||||||
|
><template #default="{ row }">{{ taskStatusName(row.status) }}</template></el-table-column
|
||||||
|
><el-table-column label="操作" width="160" fixed="right"
|
||||||
|
><template #default="{ row }"
|
||||||
|
><el-link
|
||||||
|
v-if="row.status === 'paused' || row.status === 'failed'"
|
||||||
|
type="primary"
|
||||||
|
@click="resumeTask(row)"
|
||||||
|
>继续上传</el-link
|
||||||
|
><el-link v-if="row.status === 'uploading'" type="danger" @click="pauseTask(row)"
|
||||||
|
>取消</el-link
|
||||||
|
></template
|
||||||
|
></el-table-column
|
||||||
|
></el-table
|
||||||
|
>
|
||||||
|
<div class="voucher-manage-page__pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="progressPage.current"
|
||||||
|
v-model:page-size="progressPage.size"
|
||||||
|
:total="progressPage.total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, prev, pager, next, sizes"
|
||||||
|
@current-change="loadProgress"
|
||||||
|
@size-change="loadProgress"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
<input
|
||||||
|
ref="resumeFileInput"
|
||||||
|
class="voucher-manage-page__hidden-file"
|
||||||
|
type="file"
|
||||||
|
accept=".zip,.7z"
|
||||||
|
@change="resumeFileSelected"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref, watch } from 'vue';
|
||||||
|
import md5 from 'js-md5';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { getList as getProjectList } from '@/api/business/project-apply';
|
||||||
|
import * as api from '@/api/business/voucher-manage';
|
||||||
|
|
||||||
|
const loading = ref(false),
|
||||||
|
rows = ref([]),
|
||||||
|
createRange = ref([]),
|
||||||
|
searchExpanded = ref(false),
|
||||||
|
uploadVisible = ref(false),
|
||||||
|
batchVisible = ref(false),
|
||||||
|
progressVisible = ref(false),
|
||||||
|
uploadFormRef = ref(),
|
||||||
|
uploading = ref(false),
|
||||||
|
saving = ref(false),
|
||||||
|
uploadPercent = ref(0),
|
||||||
|
resumeFileInput = ref(),
|
||||||
|
resumeTarget = ref(),
|
||||||
|
uploadAbortController = ref(),
|
||||||
|
uploadSession = ref(0),
|
||||||
|
closingUploadDialog = ref(false),
|
||||||
|
closingProgressDialog = ref(false),
|
||||||
|
cancelledTaskIds = ref(new Set()),
|
||||||
|
uploadCancelled = ref(false),
|
||||||
|
activeUploadTaskId = ref();
|
||||||
|
const query = reactive({
|
||||||
|
voucherBatchNo: '',
|
||||||
|
auditStatus: '',
|
||||||
|
processStatus: '',
|
||||||
|
waybillBatchNo: '',
|
||||||
|
uploadSource: '',
|
||||||
|
carrierName: '',
|
||||||
|
});
|
||||||
|
const page = reactive({ current: 1, size: 10, total: 0 });
|
||||||
|
const editing = reactive({
|
||||||
|
id: '',
|
||||||
|
voucherBatchNo: '',
|
||||||
|
projectId: '',
|
||||||
|
projectName: '',
|
||||||
|
fileName: '',
|
||||||
|
fileUrl: '',
|
||||||
|
fileTaskId: '',
|
||||||
|
waybillImportBatchIds: [],
|
||||||
|
});
|
||||||
|
const projectOptions = ref([]);
|
||||||
|
const selectedBatches = ref([]),
|
||||||
|
batchRows = ref([]),
|
||||||
|
batchSelection = ref([]),
|
||||||
|
batchCreateRange = ref([]);
|
||||||
|
const batchQuery = reactive({ batchNo: '', createUser: '', waybillCount: undefined });
|
||||||
|
const batchPage = reactive({ current: 1, size: 10, total: 0 });
|
||||||
|
const progressQuery = reactive({ businessId: '', attachmentName: '', status: '' });
|
||||||
|
const progressPage = reactive({ current: 1, size: 10, total: 0 });
|
||||||
|
const progressRows = ref([]);
|
||||||
|
const rules = {
|
||||||
|
projectId: [{ required: true, message: '请选择项目名称' }],
|
||||||
|
waybillImportBatchIds: [{ required: true, message: '请关联运输批次' }],
|
||||||
|
};
|
||||||
|
const load = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await api.getList(page.current, page.size, {
|
||||||
|
...query,
|
||||||
|
createTimeStart: createRange.value?.[0] ? `${createRange.value[0]} 00:00:00` : '',
|
||||||
|
createTimeEnd: createRange.value?.[1] ? `${createRange.value[1]} 23:59:59` : '',
|
||||||
|
});
|
||||||
|
const data = res.data?.data || {};
|
||||||
|
rows.value = data.records || [];
|
||||||
|
page.total = data.total || 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const search = () => {
|
||||||
|
page.current = 1;
|
||||||
|
load();
|
||||||
|
};
|
||||||
|
const reset = () => {
|
||||||
|
Object.assign(query, {
|
||||||
|
voucherBatchNo: '',
|
||||||
|
auditStatus: '',
|
||||||
|
processStatus: '',
|
||||||
|
waybillBatchNo: '',
|
||||||
|
uploadSource: '',
|
||||||
|
carrierName: '',
|
||||||
|
});
|
||||||
|
createRange.value = [];
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
const loadProjects = async () => {
|
||||||
|
const res = await getProjectList(1, 9999, { temporaryCreditLimitSelectable: true });
|
||||||
|
projectOptions.value = res.data?.data?.records || [];
|
||||||
|
};
|
||||||
|
const changeProject = projectId => {
|
||||||
|
editing.projectName = projectOptions.value.find(item => item.id === projectId)?.projectName || '';
|
||||||
|
};
|
||||||
|
const openUpload = async row => {
|
||||||
|
await loadProjects();
|
||||||
|
Object.assign(editing, {
|
||||||
|
id: row?.id || '',
|
||||||
|
voucherBatchNo: row?.voucherBatchNo || '',
|
||||||
|
projectId: row?.projectId || '',
|
||||||
|
projectName: row?.projectName || '',
|
||||||
|
fileName: row?.fileName || '',
|
||||||
|
fileUrl: row?.fileUrl || '',
|
||||||
|
fileTaskId: row?.fileTaskId || '',
|
||||||
|
waybillImportBatchIds: [],
|
||||||
|
});
|
||||||
|
uploadPercent.value = 0;
|
||||||
|
selectedBatches.value = [];
|
||||||
|
uploadVisible.value = true;
|
||||||
|
if (row?.waybillBatchNo) {
|
||||||
|
await loadBatches();
|
||||||
|
selectedBatches.value = batchRows.value.filter(item =>
|
||||||
|
row.waybillBatchNo.split(',').includes(item.batchNo)
|
||||||
|
);
|
||||||
|
editing.waybillImportBatchIds = selectedBatches.value.map(item => item.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const clearFile = () => Object.assign(editing, { fileName: '', fileUrl: '', fileTaskId: '' });
|
||||||
|
const isCurrentUploadTask = taskId =>
|
||||||
|
taskId && activeUploadTaskId.value && String(taskId) === String(activeUploadTaskId.value);
|
||||||
|
const stopCurrentUpload = async taskId => {
|
||||||
|
// 先使整个上传会话失效,阻止 MD5 计算、创建任务等异步流程返回后继续上传。
|
||||||
|
uploadCancelled.value = true;
|
||||||
|
uploadSession.value += 1;
|
||||||
|
uploadAbortController.value?.abort();
|
||||||
|
const currentTaskId = taskId || activeUploadTaskId.value || editing.fileTaskId;
|
||||||
|
if (!currentTaskId) return;
|
||||||
|
cancelledTaskIds.value.add(String(currentTaskId));
|
||||||
|
if (isCurrentUploadTask(currentTaskId)) {
|
||||||
|
uploading.value = false;
|
||||||
|
activeUploadTaskId.value = undefined;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const taskRes = await api.getFileTask(currentTaskId);
|
||||||
|
if (taskRes.data?.data?.status === 'completed') return;
|
||||||
|
await api.pauseFileTask([currentTaskId]);
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.message || '暂停上传失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const closeUploadDialog = async () => {
|
||||||
|
if (closingUploadDialog.value) return;
|
||||||
|
closingUploadDialog.value = true;
|
||||||
|
try {
|
||||||
|
await stopCurrentUpload(editing.fileTaskId);
|
||||||
|
uploadVisible.value = false;
|
||||||
|
} finally {
|
||||||
|
closingUploadDialog.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const beforeUploadDialogClose = async done => {
|
||||||
|
if (closingUploadDialog.value) {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closingUploadDialog.value = true;
|
||||||
|
try {
|
||||||
|
await stopCurrentUpload(editing.fileTaskId);
|
||||||
|
done();
|
||||||
|
} finally {
|
||||||
|
closingUploadDialog.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const beforeProgressDialogClose = async done => {
|
||||||
|
if (closingProgressDialog.value) return;
|
||||||
|
closingProgressDialog.value = true;
|
||||||
|
try {
|
||||||
|
if (uploading.value || activeUploadTaskId.value) {
|
||||||
|
await stopCurrentUpload(activeUploadTaskId.value);
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
} finally {
|
||||||
|
closingProgressDialog.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const ensureUploadSession = (sessionId, taskId) => {
|
||||||
|
if (
|
||||||
|
uploadCancelled.value ||
|
||||||
|
sessionId !== uploadSession.value ||
|
||||||
|
(taskId && cancelledTaskIds.value.has(String(taskId)))
|
||||||
|
) {
|
||||||
|
throw new DOMException('上传已暂停', 'AbortError');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const calculateMd5 = async file => {
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
return md5.base64(buffer);
|
||||||
|
};
|
||||||
|
const uploadParts = async (file, task, sessionId) => {
|
||||||
|
uploading.value = true;
|
||||||
|
activeUploadTaskId.value = task.id;
|
||||||
|
const abortController = new AbortController();
|
||||||
|
uploadAbortController.value = abortController;
|
||||||
|
const chunkSize = task.chunkSize;
|
||||||
|
try {
|
||||||
|
for (
|
||||||
|
let partNumber = Math.max(1, (task.currentIndex || 0) + 1);
|
||||||
|
partNumber <= task.chunkTotal;
|
||||||
|
partNumber += 1
|
||||||
|
) {
|
||||||
|
ensureUploadSession(sessionId, task.id);
|
||||||
|
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
|
||||||
|
const urlRes = await api.getPartUploadUrl(task.id, partNumber);
|
||||||
|
ensureUploadSession(sessionId, task.id);
|
||||||
|
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
|
||||||
|
const url = urlRes.data?.data;
|
||||||
|
const part = file.slice(
|
||||||
|
(partNumber - 1) * chunkSize,
|
||||||
|
Math.min(partNumber * chunkSize, file.size)
|
||||||
|
);
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: part,
|
||||||
|
signal: abortController.signal,
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error(`分片${partNumber}上传失败`);
|
||||||
|
const etag = String(response.headers.get('etag') || '').replaceAll('"', '');
|
||||||
|
if (!etag) throw new Error('MinIO 未返回分片 ETag,请确认跨域配置已暴露 ETag 响应头');
|
||||||
|
await api.updateFileTask({ id: task.id, partNumber, etag });
|
||||||
|
ensureUploadSession(sessionId, task.id);
|
||||||
|
if (abortController.signal.aborted) throw new DOMException('上传已暂停', 'AbortError');
|
||||||
|
uploadPercent.value = Math.round((partNumber / task.chunkTotal) * 100);
|
||||||
|
if (progressVisible.value) await loadProgress();
|
||||||
|
}
|
||||||
|
ensureUploadSession(sessionId, task.id);
|
||||||
|
const fileUrlRes = await api.getFileTaskUrl(task.id);
|
||||||
|
ensureUploadSession(sessionId, task.id);
|
||||||
|
editing.fileName = file.name;
|
||||||
|
editing.fileTaskId = task.id;
|
||||||
|
editing.fileUrl = fileUrlRes.data?.data || '';
|
||||||
|
await api.completeUploadFile({
|
||||||
|
voucherBatchNo: task.businessId,
|
||||||
|
fileTaskId: task.id,
|
||||||
|
fileName: file.name,
|
||||||
|
fileUrl: editing.fileUrl,
|
||||||
|
});
|
||||||
|
ElMessage.success('执行凭证上传完成');
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name !== 'AbortError')
|
||||||
|
ElMessage.error(error.message || '上传中断,可在上传进度中继续上传');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
uploading.value = false;
|
||||||
|
if (uploadAbortController.value === abortController) uploadAbortController.value = undefined;
|
||||||
|
if (activeUploadTaskId.value === task.id) activeUploadTaskId.value = undefined;
|
||||||
|
if (progressVisible.value) await loadProgress();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const ensureUploadDraft = async fileName => {
|
||||||
|
if (editing.id) return;
|
||||||
|
if (!editing.projectId) throw new Error('请先选择项目名称');
|
||||||
|
const res = await api.createUploadDraft({ projectId: editing.projectId, fileName });
|
||||||
|
const draft = res.data?.data;
|
||||||
|
if (!draft?.id || !draft?.voucherBatchNo) throw new Error('创建凭证上传记录失败');
|
||||||
|
Object.assign(editing, {
|
||||||
|
id: draft.id,
|
||||||
|
voucherBatchNo: draft.voucherBatchNo,
|
||||||
|
projectName: draft.projectName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const uploadVoucher = async file => {
|
||||||
|
const raw = file.raw;
|
||||||
|
if (!raw) return;
|
||||||
|
uploadCancelled.value = false;
|
||||||
|
const sessionId = uploadSession.value + 1;
|
||||||
|
uploadSession.value = sessionId;
|
||||||
|
try {
|
||||||
|
if (!resumeTarget.value) await ensureUploadDraft(raw.name);
|
||||||
|
ensureUploadSession(sessionId);
|
||||||
|
const md5 = await calculateMd5(raw);
|
||||||
|
ensureUploadSession(sessionId);
|
||||||
|
const resumeTask = resumeTarget.value;
|
||||||
|
let task;
|
||||||
|
if (resumeTask) {
|
||||||
|
if (
|
||||||
|
md5 !== resumeTask.md5 ||
|
||||||
|
raw.name !== resumeTask.attachmentName ||
|
||||||
|
raw.size !== Number(resumeTask.size)
|
||||||
|
) {
|
||||||
|
ElMessage.error('请选择与上传任务相同的文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const taskRes = await api.getFileTask(resumeTask.id);
|
||||||
|
ensureUploadSession(sessionId);
|
||||||
|
task = taskRes.data?.data;
|
||||||
|
if (!task || String(task.id) !== String(resumeTask.id)) {
|
||||||
|
ElMessage.error('上传任务不存在或已失效');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.resumeFileTask(task.id);
|
||||||
|
ensureUploadSession(sessionId);
|
||||||
|
task.status = 'uploading';
|
||||||
|
if (progressVisible.value) await loadProgress();
|
||||||
|
} else {
|
||||||
|
const existingRes = await api.queryFileTask(md5);
|
||||||
|
ensureUploadSession(sessionId);
|
||||||
|
task = existingRes.data?.data;
|
||||||
|
}
|
||||||
|
const chunkSize = 5 * 1024 * 1024;
|
||||||
|
if (
|
||||||
|
!task ||
|
||||||
|
task.status === 'completed' ||
|
||||||
|
(!resumeTask && task.businessId !== editing.voucherBatchNo)
|
||||||
|
) {
|
||||||
|
const createRes = await api.createFileTask({
|
||||||
|
md5,
|
||||||
|
attachmentName: raw.name,
|
||||||
|
size: raw.size,
|
||||||
|
chunkSize,
|
||||||
|
chunkTotal: Math.ceil(raw.size / chunkSize),
|
||||||
|
force: !!task,
|
||||||
|
businessType: 'voucher',
|
||||||
|
businessId: editing.voucherBatchNo,
|
||||||
|
});
|
||||||
|
ensureUploadSession(sessionId);
|
||||||
|
task = createRes.data?.data;
|
||||||
|
}
|
||||||
|
editing.fileName = raw.name;
|
||||||
|
editing.fileTaskId = task.id;
|
||||||
|
activeUploadTaskId.value = task.id;
|
||||||
|
cancelledTaskIds.value.delete(String(task.id));
|
||||||
|
resumeTarget.value = undefined;
|
||||||
|
await uploadParts(raw, task, sessionId);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name !== 'AbortError')
|
||||||
|
ElMessage.error(error.message || '上传中断,可在上传进度中继续上传');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
watch(uploadVisible, visible => {
|
||||||
|
// 兜底处理:右上角、遮罩、Esc 直接修改 v-model 时仍必须停止上传。
|
||||||
|
if (!visible) void stopCurrentUpload(editing.fileTaskId);
|
||||||
|
});
|
||||||
|
const openBatchDialog = async () => {
|
||||||
|
batchVisible.value = true;
|
||||||
|
await loadBatches();
|
||||||
|
};
|
||||||
|
const loadBatches = async () => {
|
||||||
|
const res = await api.getWaybillBatches(batchPage.current, batchPage.size, {
|
||||||
|
batchNo: batchQuery.batchNo,
|
||||||
|
createUser: batchQuery.createUser,
|
||||||
|
waybillCount: batchQuery.waybillCount,
|
||||||
|
createTimeStart: batchCreateRange.value?.[0] ? `${batchCreateRange.value[0]} 00:00:00` : '',
|
||||||
|
createTimeEnd: batchCreateRange.value?.[1] ? `${batchCreateRange.value[1]} 23:59:59` : '',
|
||||||
|
});
|
||||||
|
const data = res.data?.data || {};
|
||||||
|
batchRows.value = data.records || [];
|
||||||
|
batchPage.total = data.total || 0;
|
||||||
|
};
|
||||||
|
const resetBatchQuery = () => {
|
||||||
|
Object.assign(batchQuery, { batchNo: '', createUser: '', waybillCount: undefined });
|
||||||
|
batchCreateRange.value = [];
|
||||||
|
batchPage.current = 1;
|
||||||
|
loadBatches();
|
||||||
|
};
|
||||||
|
const confirmBatches = () => {
|
||||||
|
const batches = new Map();
|
||||||
|
batchSelection.value.forEach(item => {
|
||||||
|
if (!batches.has(item.batchNo)) batches.set(item.batchNo, item);
|
||||||
|
});
|
||||||
|
selectedBatches.value = [...batches.values()];
|
||||||
|
editing.waybillImportBatchIds = selectedBatches.value.map(item => item.id);
|
||||||
|
batchVisible.value = false;
|
||||||
|
};
|
||||||
|
const removeBatch = row => {
|
||||||
|
selectedBatches.value = selectedBatches.value.filter(item => item.id !== row.id);
|
||||||
|
editing.waybillImportBatchIds = selectedBatches.value.map(item => item.id);
|
||||||
|
};
|
||||||
|
const submitUpload = async () => {
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
if (uploading.value && editing.fileTaskId) {
|
||||||
|
await stopCurrentUpload(editing.fileTaskId);
|
||||||
|
}
|
||||||
|
const valid = await uploadFormRef.value.validate().catch(() => false);
|
||||||
|
if (!valid) return;
|
||||||
|
if (!editing.fileUrl && !editing.fileTaskId) {
|
||||||
|
ElMessage.warning('请先选择执行凭证');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.submit({
|
||||||
|
...editing,
|
||||||
|
waybillImportBatchIds: selectedBatches.value.map(item => item.id),
|
||||||
|
});
|
||||||
|
ElMessage.success('提交成功');
|
||||||
|
await closeUploadDialog();
|
||||||
|
load();
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const openProgress = () => {
|
||||||
|
progressVisible.value = true;
|
||||||
|
};
|
||||||
|
const loadProgress = async () => {
|
||||||
|
const res = await api.getFileTaskList(progressPage.current, progressPage.size, {
|
||||||
|
...progressQuery,
|
||||||
|
businessType: 'voucher',
|
||||||
|
});
|
||||||
|
const data = res.data?.data || {};
|
||||||
|
progressRows.value = data.records || [];
|
||||||
|
progressPage.total = data.total || 0;
|
||||||
|
};
|
||||||
|
const searchProgress = () => {
|
||||||
|
progressPage.current = 1;
|
||||||
|
loadProgress();
|
||||||
|
};
|
||||||
|
const resetProgress = () => {
|
||||||
|
Object.assign(progressQuery, { businessId: '', attachmentName: '', status: '' });
|
||||||
|
searchProgress();
|
||||||
|
};
|
||||||
|
const taskPercent = row =>
|
||||||
|
row.chunkTotal ? Math.round(((row.currentIndex || 0) / row.chunkTotal) * 100) : 0;
|
||||||
|
const taskStatusName = status =>
|
||||||
|
({ uploading: '正在上传', paused: '已暂停', completed: '上传完成', failed: '上传失败' }[status] ||
|
||||||
|
status);
|
||||||
|
const formatSize = size =>
|
||||||
|
size >= 1024 ** 3 ? `${(size / 1024 ** 3).toFixed(1)}G` : `${(size / 1024 ** 2).toFixed(1)}M`;
|
||||||
|
const pauseTask = async row => {
|
||||||
|
if (row.status === 'completed') return;
|
||||||
|
if (isCurrentUploadTask(row.id)) {
|
||||||
|
await stopCurrentUpload(row.id);
|
||||||
|
} else {
|
||||||
|
await api.pauseFileTask([row.id]);
|
||||||
|
cancelledTaskIds.value.add(String(row.id));
|
||||||
|
}
|
||||||
|
ElMessage.success('已取消上传');
|
||||||
|
loadProgress();
|
||||||
|
};
|
||||||
|
const resumeTask = row => {
|
||||||
|
resumeTarget.value = row;
|
||||||
|
activeUploadTaskId.value = row.id;
|
||||||
|
resumeFileInput.value?.click();
|
||||||
|
};
|
||||||
|
const resumeFileSelected = event => {
|
||||||
|
const [raw] = event.target.files || [];
|
||||||
|
if (raw) uploadVoucher({ raw });
|
||||||
|
event.target.value = '';
|
||||||
|
};
|
||||||
|
const removeRow = row =>
|
||||||
|
ElMessageBox.confirm(`确认删除凭证批次“${row.voucherBatchNo}”吗?`, '提示', {
|
||||||
|
type: 'warning',
|
||||||
|
}).then(async () => {
|
||||||
|
await api.remove(row.id);
|
||||||
|
ElMessage.success('删除成功');
|
||||||
|
load();
|
||||||
|
});
|
||||||
|
const view = row => ElMessage.info(`凭证批次:${row.voucherBatchNo}`);
|
||||||
|
const download = row => window.open(row.fileUrl, '_blank');
|
||||||
|
load();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.voucher-manage-page {
|
||||||
|
&__search {
|
||||||
|
padding: 12px 12px 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
&__search-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 8px 24px;
|
||||||
|
}
|
||||||
|
&__search-actions {
|
||||||
|
display: flex;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
&__toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
&__table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
&__link {
|
||||||
|
padding: 0;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px 12px;
|
||||||
|
}
|
||||||
|
&__pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 12px 0 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
&__project-select {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
&__hidden-file {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
&__upload-form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
&__upload-tip {
|
||||||
|
margin-left: 16px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
&__progress-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
:deep(.el-progress) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
:deep(.el-progress-bar__outer) {
|
||||||
|
background: #eff1f7;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__progress-value {
|
||||||
|
min-width: 38px;
|
||||||
|
color: #303133;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
&__batch-search {
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
:deep(.el-input),
|
||||||
|
:deep(.el-select),
|
||||||
|
:deep(.el-date-editor.el-input),
|
||||||
|
:deep(.el-date-editor.el-input__wrapper),
|
||||||
|
:deep(.el-date-editor--daterange) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
:deep(.voucher-manage-page__project-select) {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
:deep(.el-table) {
|
||||||
|
--el-table-border-color: #eff1f7;
|
||||||
|
--el-table-row-hover-bg-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
:deep(.el-table__body tr:nth-child(even) > td) {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<receivable-payable-detail settlement-type="payable" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ReceivablePayableDetail from './receivable-payable-detail.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PayableDetail',
|
||||||
|
components: { ReceivablePayableDetail },
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<receivable-payable-detail settlement-type="receivable" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ReceivablePayableDetail from './receivable-payable-detail.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ReceivableDetail',
|
||||||
|
components: { ReceivablePayableDetail },
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,938 @@
|
|||||||
|
<template>
|
||||||
|
<basic-container class="settlement-detail-page">
|
||||||
|
<section class="settlement-detail-page__search">
|
||||||
|
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
|
||||||
|
<div class="settlement-detail-page__search-grid">
|
||||||
|
<template v-for="field in visibleSearchFields" :key="field.prop">
|
||||||
|
<el-form-item :label="field.label">
|
||||||
|
<el-date-picker
|
||||||
|
v-if="field.type === 'daterange'"
|
||||||
|
v-model="query[field.prop]"
|
||||||
|
type="daterange"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
range-separator="~"
|
||||||
|
start-placeholder="年/月/日"
|
||||||
|
end-placeholder="年/月/日"
|
||||||
|
/>
|
||||||
|
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
|
</template>
|
||||||
|
<div class="settlement-detail-page__search-actions">
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
<el-button :icon="searchExpanded ? ArrowUp : ArrowDown" @click="toggleSearch" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settlement-detail-page__table-panel">
|
||||||
|
<div class="settlement-detail-page__toolbar">
|
||||||
|
<div class="settlement-detail-page__toolbar-left">
|
||||||
|
<el-button type="primary" @click="openGenerateDialog">生成费用</el-button>
|
||||||
|
<el-button type="primary" @click="openUpdateFeeDialog">更新费用</el-button>
|
||||||
|
<el-button type="primary" @click="openTransferDialog">批量转结算</el-button>
|
||||||
|
<el-button type="primary" plain @click="handleExport">导出</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="settlement-detail-page__toolbar-right">
|
||||||
|
<el-tooltip content="刷新" placement="top">
|
||||||
|
<el-button :icon="Refresh" text @click="loadTable" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="列设置" placement="top">
|
||||||
|
<el-button :icon="Setting" text @click="noop" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="rows"
|
||||||
|
border
|
||||||
|
class="settlement-detail-page__table"
|
||||||
|
@selection-change="selectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||||
|
<el-table-column type="index" label="序号" width="64" fixed="left" align="center" />
|
||||||
|
<el-table-column
|
||||||
|
v-for="column in tableColumns"
|
||||||
|
:key="column.prop"
|
||||||
|
:prop="column.prop"
|
||||||
|
:label="column.label"
|
||||||
|
:min-width="column.minWidth"
|
||||||
|
:align="column.align || 'center'"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link
|
||||||
|
v-if="column.link && row[column.prop]"
|
||||||
|
type="primary"
|
||||||
|
@click="openDetailDialog(row)"
|
||||||
|
>
|
||||||
|
{{ row[column.prop] }}
|
||||||
|
</el-link>
|
||||||
|
<span v-else>{{ row[column.prop] || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="settlement-detail-page__actions">
|
||||||
|
<el-link v-if="row.settlementStatus === 'pending'" type="primary" @click="openUpdateFeeDialog(row)">
|
||||||
|
调整
|
||||||
|
</el-link>
|
||||||
|
<el-link v-if="row.settlementStatus === 'pending'" type="primary" @click="closeRow(row)">
|
||||||
|
关闭
|
||||||
|
</el-link>
|
||||||
|
<el-link v-else type="primary" disabled>-</el-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="settlement-detail-page__pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page.current"
|
||||||
|
v-model:page-size="page.size"
|
||||||
|
:total="page.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadTable"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialog.visible"
|
||||||
|
:title="detailDialog.title"
|
||||||
|
width="92%"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
class="settlement-detail-dialog"
|
||||||
|
>
|
||||||
|
<el-tabs v-model="detailDialog.activeTab" @tab-change="handleDetailTabChange">
|
||||||
|
<el-tab-pane label="费用明细" name="fee">
|
||||||
|
<el-table v-loading="detailDialog.loading" :data="feeRows" border>
|
||||||
|
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||||
|
<el-table-column
|
||||||
|
v-for="column in feeDetailColumns"
|
||||||
|
:key="column.prop"
|
||||||
|
:prop="column.prop"
|
||||||
|
:label="column.label"
|
||||||
|
:min-width="column.minWidth"
|
||||||
|
:align="column.align || 'center'"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">{{ formatCell(row[column.prop]) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="变更记录" name="change">
|
||||||
|
<el-table v-loading="changeDialog.loading" :data="changeRows" border>
|
||||||
|
<el-table-column type="index" label="序号" width="64" align="center" />
|
||||||
|
<el-table-column
|
||||||
|
v-for="column in changeRecordColumns"
|
||||||
|
:key="column.prop"
|
||||||
|
:prop="column.prop"
|
||||||
|
:label="column.label"
|
||||||
|
:min-width="column.minWidth"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
</el-table>
|
||||||
|
<div class="settlement-detail-page__pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="changePage.current"
|
||||||
|
v-model:page-size="changePage.size"
|
||||||
|
:total="changePage.total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadChangeRecords"
|
||||||
|
@size-change="handleChangeSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="updateFeeDialog.visible" title="更新费用" width="620px" append-to-body>
|
||||||
|
<el-form
|
||||||
|
ref="updateFeeFormRef"
|
||||||
|
:model="updateFeeForm"
|
||||||
|
:rules="updateFeeRules"
|
||||||
|
label-position="right"
|
||||||
|
label-width="auto"
|
||||||
|
class="settlement-detail-page__dialog-form"
|
||||||
|
>
|
||||||
|
<el-form-item label="更新范围" prop="contractId">
|
||||||
|
<el-select
|
||||||
|
v-model="updateFeeForm.contractId"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="请选择合同"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in contractOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.contractName"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="合同计费方案" prop="billingPlanId">
|
||||||
|
<el-select v-model="updateFeeForm.billingPlanId" clearable filterable placeholder="请选择">
|
||||||
|
<el-option
|
||||||
|
v-for="item in billingPlanOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" :loading="updateFeeDialog.submitting" @click="confirmUpdateFee">
|
||||||
|
确认
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="updateFeeDialog.visible = false">取消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="transferDialog.visible" title="批量转结算" width="86%" append-to-body>
|
||||||
|
<div class="settlement-detail-page__transfer-form">
|
||||||
|
<el-form :model="transferQuery" inline label-position="right" label-width="120px">
|
||||||
|
<el-form-item label="转结算类型">
|
||||||
|
<el-radio-group v-model="transferForm.settlementBillType">
|
||||||
|
<el-radio label="pre">预结算单</el-radio>
|
||||||
|
<el-radio label="formal">正式结算单</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-form :model="transferQuery" inline label-position="right" label-width="120px">
|
||||||
|
<el-form-item
|
||||||
|
v-for="field in transferSearchFields"
|
||||||
|
:key="field.prop"
|
||||||
|
:label="field.label"
|
||||||
|
>
|
||||||
|
<el-date-picker
|
||||||
|
v-if="field.type === 'daterange'"
|
||||||
|
v-model="transferQuery[field.prop]"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="请选择"
|
||||||
|
end-placeholder="请选择"
|
||||||
|
/>
|
||||||
|
<el-input v-else v-model="transferQuery[field.prop]" clearable placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="loadTransferCandidates">查询</el-button>
|
||||||
|
<el-button type="primary" @click="confirmTransferSelection">确认选择</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
v-loading="transferDialog.loading"
|
||||||
|
:data="transferRows"
|
||||||
|
border
|
||||||
|
@selection-change="transferSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="52" align="center" />
|
||||||
|
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||||
|
<el-table-column
|
||||||
|
v-for="column in tableColumns.slice(0, 13)"
|
||||||
|
:key="column.prop"
|
||||||
|
:prop="column.prop"
|
||||||
|
:label="column.label"
|
||||||
|
:min-width="column.minWidth"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
</el-table>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" :loading="transferDialog.submitting" @click="submitTransfer">
|
||||||
|
确认
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="transferDialog.visible = false">取消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="generateDialog.visible" title="生成费用" width="88%" append-to-body>
|
||||||
|
<el-form :model="generateQuery" inline label-position="right" label-width="120px">
|
||||||
|
<el-form-item label="运单合同" required>
|
||||||
|
<el-select
|
||||||
|
v-model="generateQuery.contractId"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
placeholder="请选择合同"
|
||||||
|
@change="handleGenerateContractChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in contractOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.contractName"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="计费方案" required>
|
||||||
|
<el-select v-model="generateQuery.billingPlanId" filterable clearable placeholder="请选择">
|
||||||
|
<el-option
|
||||||
|
v-for="item in billingPlanOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="批次号">
|
||||||
|
<el-input v-model="generateQuery.batchNo" clearable placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="运单完成日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="generateQuery.finishDateRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="请选择"
|
||||||
|
end-placeholder="请选择"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="loadGenerateWaybills">查询</el-button>
|
||||||
|
<el-button @click="resetGenerateQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-table
|
||||||
|
v-loading="generateDialog.loading"
|
||||||
|
:data="generateRows"
|
||||||
|
border
|
||||||
|
@selection-change="generateSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="52" align="center" />
|
||||||
|
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||||
|
<el-table-column
|
||||||
|
v-for="column in generateWaybillColumns"
|
||||||
|
:key="column.prop"
|
||||||
|
:prop="column.prop"
|
||||||
|
:label="column.label"
|
||||||
|
:min-width="column.minWidth"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link v-if="column.link && row[column.prop]" type="primary">
|
||||||
|
{{ row[column.prop] }}
|
||||||
|
</el-link>
|
||||||
|
<span v-else>{{ row[column.prop] || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="settlement-detail-page__pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="generatePage.current"
|
||||||
|
v-model:page-size="generatePage.size"
|
||||||
|
:total="generatePage.total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadGenerateWaybills"
|
||||||
|
@size-change="handleGenerateSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="openGeneratePreview">生成费用</el-button>
|
||||||
|
<el-button @click="generateDialog.visible = false">取消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="previewDialog.visible" title="生成费用明细" width="88%" append-to-body>
|
||||||
|
<el-table v-loading="previewDialog.loading" :data="previewRows" border>
|
||||||
|
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||||
|
<el-table-column
|
||||||
|
v-for="column in previewColumns"
|
||||||
|
:key="column.prop"
|
||||||
|
:prop="column.prop"
|
||||||
|
:label="column.label"
|
||||||
|
:min-width="column.minWidth"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
</el-table>
|
||||||
|
<div class="settlement-detail-page__pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="previewPage.current"
|
||||||
|
v-model:page-size="previewPage.size"
|
||||||
|
:total="previewPage.total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadGeneratePreview"
|
||||||
|
@size-change="handlePreviewSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="generateDialog.visible = true">上一步</el-button>
|
||||||
|
<el-button type="primary" :loading="previewDialog.submitting" @click="submitGenerateFee">
|
||||||
|
确认
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="previewDialog.visible = false">取消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</basic-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ArrowDown, ArrowUp, Refresh, Setting } from '@element-plus/icons-vue';
|
||||||
|
import {
|
||||||
|
changeRecordColumns,
|
||||||
|
feeDetailBaseColumns,
|
||||||
|
feeDetailTailColumns,
|
||||||
|
generatePreviewColumns,
|
||||||
|
generateWaybillColumns,
|
||||||
|
searchFields,
|
||||||
|
settlementStatusOptions,
|
||||||
|
tableColumns,
|
||||||
|
transferSearchFields,
|
||||||
|
} from '@/option/settlement/receivable-payable-detail';
|
||||||
|
import * as api from '@/api/settlement/receivable-payable-detail';
|
||||||
|
import { getList as getContractList } from '@/api/business/contract-manage';
|
||||||
|
import { exportBlob } from '@/api/common';
|
||||||
|
import { downloadXls } from '@/utils/util';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
settlementType: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ArrowDown,
|
||||||
|
ArrowUp,
|
||||||
|
Refresh,
|
||||||
|
Setting,
|
||||||
|
searchFields,
|
||||||
|
tableColumns,
|
||||||
|
changeRecordColumns,
|
||||||
|
transferSearchFields,
|
||||||
|
generateWaybillColumns,
|
||||||
|
loading: false,
|
||||||
|
searchExpanded: false,
|
||||||
|
query: {},
|
||||||
|
rows: [],
|
||||||
|
selection: [],
|
||||||
|
page: { current: 1, size: 10, total: 0 },
|
||||||
|
detailDialog: { visible: false, title: '费用明细', activeTab: 'fee', row: null, loading: false },
|
||||||
|
feeRows: [],
|
||||||
|
dynamicFeeColumns: [],
|
||||||
|
changeRows: [],
|
||||||
|
changePage: { current: 1, size: 10, total: 0 },
|
||||||
|
changeDialog: { loading: false },
|
||||||
|
updateFeeDialog: { visible: false, submitting: false, row: null },
|
||||||
|
updateFeeForm: { contractId: '', billingPlanId: '' },
|
||||||
|
updateFeeRules: {
|
||||||
|
contractId: [{ required: true, message: '请选择更新范围', trigger: 'change' }],
|
||||||
|
},
|
||||||
|
contractOptions: [],
|
||||||
|
billingPlanOptions: [],
|
||||||
|
transferDialog: { visible: false, loading: false, submitting: false },
|
||||||
|
transferQuery: {},
|
||||||
|
transferForm: { settlementBillType: 'formal' },
|
||||||
|
transferRows: [],
|
||||||
|
transferSelection: [],
|
||||||
|
generateDialog: { visible: false, loading: false },
|
||||||
|
generateQuery: {},
|
||||||
|
generateRows: [],
|
||||||
|
generateSelection: [],
|
||||||
|
generatePage: { current: 1, size: 10, total: 0 },
|
||||||
|
previewDialog: { visible: false, loading: false, submitting: false },
|
||||||
|
previewRows: [],
|
||||||
|
previewPage: { current: 1, size: 10, total: 0 },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
settlementTypeLabel() {
|
||||||
|
if (this.settlementType === 'receivable') return '应收';
|
||||||
|
if (this.settlementType === 'payable') return '应付';
|
||||||
|
return '应收应付';
|
||||||
|
},
|
||||||
|
visibleSearchFields() {
|
||||||
|
return this.searchExpanded ? this.searchFields : this.searchFields.slice(0, 7);
|
||||||
|
},
|
||||||
|
feeDetailColumns() {
|
||||||
|
return [...feeDetailBaseColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
|
||||||
|
},
|
||||||
|
previewColumns() {
|
||||||
|
return [...generatePreviewColumns, ...this.dynamicFeeColumns, ...feeDetailTailColumns];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadContracts();
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadTable() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const params = this.buildRequestParams(
|
||||||
|
this.normalizeQuery(this.query, 'generateDateRange', 'generateStartDate', 'generateEndDate')
|
||||||
|
);
|
||||||
|
const res = await api.getList(this.page.current, this.page.size, params);
|
||||||
|
const data = this.unwrapPage(res);
|
||||||
|
this.rows = (data.records || []).map(this.decorateRow);
|
||||||
|
this.page.total = data.total || 0;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadContracts() {
|
||||||
|
const res = await getContractList(1, 999, { approvalStatus: 'approved' });
|
||||||
|
const data = this.unwrapPage(res);
|
||||||
|
this.contractOptions = data.records || [];
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.page.current = 1;
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
handleReset() {
|
||||||
|
this.query = {};
|
||||||
|
this.handleSearch();
|
||||||
|
},
|
||||||
|
toggleSearch() {
|
||||||
|
this.searchExpanded = !this.searchExpanded;
|
||||||
|
},
|
||||||
|
handleSizeChange() {
|
||||||
|
this.page.current = 1;
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
selectionChange(selection) {
|
||||||
|
this.selection = selection;
|
||||||
|
},
|
||||||
|
async openDetailDialog(row) {
|
||||||
|
this.detailDialog = { ...this.detailDialog, visible: true, row, title: row.documentNo, activeTab: 'fee' };
|
||||||
|
await this.loadFeeDetail();
|
||||||
|
},
|
||||||
|
async loadFeeDetail() {
|
||||||
|
if (!this.detailDialog.row) return;
|
||||||
|
this.detailDialog.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await api.getFeeDetail(this.detailDialog.row.id);
|
||||||
|
const data = res.data?.data || res.data || res || {};
|
||||||
|
this.dynamicFeeColumns = (data.feeItemNames || []).map((name, index) => ({
|
||||||
|
label: name,
|
||||||
|
prop: `feeItem${index}`,
|
||||||
|
minWidth: 130,
|
||||||
|
align: 'right',
|
||||||
|
}));
|
||||||
|
this.feeRows = (data.records || []).map(row => {
|
||||||
|
const dynamic = {};
|
||||||
|
(data.feeItemNames || []).forEach((name, index) => {
|
||||||
|
dynamic[`feeItem${index}`] = row.feeItems?.[name] || '';
|
||||||
|
});
|
||||||
|
return { ...row, ...dynamic };
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.detailDialog.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleDetailTabChange(name) {
|
||||||
|
if (name === 'change') this.loadChangeRecords();
|
||||||
|
},
|
||||||
|
async loadChangeRecords() {
|
||||||
|
if (!this.detailDialog.row) return;
|
||||||
|
this.changeDialog.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await api.getChangeRecords(this.changePage.current, this.changePage.size, {
|
||||||
|
detailId: this.detailDialog.row.id,
|
||||||
|
});
|
||||||
|
const data = this.unwrapPage(res);
|
||||||
|
this.changeRows = data.records || [];
|
||||||
|
this.changePage.total = data.total || 0;
|
||||||
|
} finally {
|
||||||
|
this.changeDialog.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleChangeSizeChange() {
|
||||||
|
this.changePage.current = 1;
|
||||||
|
this.loadChangeRecords();
|
||||||
|
},
|
||||||
|
openUpdateFeeDialog(row) {
|
||||||
|
this.updateFeeDialog = { ...this.updateFeeDialog, visible: true, row: row || null };
|
||||||
|
this.updateFeeForm = {
|
||||||
|
ids: row ? [row.id] : this.selection.map(item => item.id),
|
||||||
|
settlementType: this.settlementType || undefined,
|
||||||
|
contractId: row?.contractId || '',
|
||||||
|
billingPlanId: '',
|
||||||
|
};
|
||||||
|
this.syncBillingPlanOptions(this.updateFeeForm.contractId);
|
||||||
|
},
|
||||||
|
async confirmUpdateFee() {
|
||||||
|
await this.$refs.updateFeeFormRef.validate();
|
||||||
|
await this.$confirm(
|
||||||
|
'更新费用将使用最新的合同计费规则重新生成费用明细,确认更新费用?',
|
||||||
|
'确认提示',
|
||||||
|
{ type: 'warning' }
|
||||||
|
);
|
||||||
|
this.updateFeeDialog.submitting = true;
|
||||||
|
try {
|
||||||
|
await api.updateFee(this.updateFeeForm);
|
||||||
|
this.$message.success('更新成功');
|
||||||
|
this.updateFeeDialog.visible = false;
|
||||||
|
this.loadTable();
|
||||||
|
} finally {
|
||||||
|
this.updateFeeDialog.submitting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async closeRow(row) {
|
||||||
|
await this.$confirm('确认关闭该费用明细?', '提示', { type: 'warning' });
|
||||||
|
await api.updateFee({
|
||||||
|
ids: [row.id],
|
||||||
|
closeOnly: true,
|
||||||
|
settlementType: this.settlementType || undefined,
|
||||||
|
});
|
||||||
|
this.$message.success('关闭成功');
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
openTransferDialog() {
|
||||||
|
this.transferDialog.visible = true;
|
||||||
|
this.transferForm = { settlementBillType: 'formal' };
|
||||||
|
this.transferQuery = {};
|
||||||
|
this.transferRows = [];
|
||||||
|
this.transferSelection = [];
|
||||||
|
this.loadTransferCandidates();
|
||||||
|
},
|
||||||
|
async loadTransferCandidates() {
|
||||||
|
this.transferDialog.loading = true;
|
||||||
|
try {
|
||||||
|
const params = this.buildRequestParams(
|
||||||
|
this.normalizeQuery(this.transferQuery, 'generateDateRange', 'generateStartDate', 'generateEndDate')
|
||||||
|
);
|
||||||
|
const res = await api.getTransferCandidates(1, 50, {
|
||||||
|
...params,
|
||||||
|
settlementBillType: this.transferForm.settlementBillType,
|
||||||
|
});
|
||||||
|
const data = this.unwrapPage(res);
|
||||||
|
this.transferRows = (data.records || []).map(this.decorateRow);
|
||||||
|
} finally {
|
||||||
|
this.transferDialog.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmTransferSelection() {
|
||||||
|
if (!this.transferSelection.length) {
|
||||||
|
this.$message.warning('请选择需要转结算的明细');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$message.success(`已选择${this.transferSelection.length}条明细`);
|
||||||
|
},
|
||||||
|
transferSelectionChange(selection) {
|
||||||
|
this.transferSelection = selection;
|
||||||
|
},
|
||||||
|
async submitTransfer() {
|
||||||
|
if (!this.transferSelection.length) {
|
||||||
|
this.$message.warning('请选择需要转结算的明细');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.transferDialog.submitting = true;
|
||||||
|
try {
|
||||||
|
await api.transferSettlement({
|
||||||
|
settlementType: this.settlementType || undefined,
|
||||||
|
settlementBillType: this.transferForm.settlementBillType,
|
||||||
|
ids: this.transferSelection.map(item => item.id),
|
||||||
|
});
|
||||||
|
this.$message.success('转结算成功');
|
||||||
|
this.transferDialog.visible = false;
|
||||||
|
this.loadTable();
|
||||||
|
} finally {
|
||||||
|
this.transferDialog.submitting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openGenerateDialog() {
|
||||||
|
this.generateDialog.visible = true;
|
||||||
|
this.generateQuery = {};
|
||||||
|
this.generateRows = [];
|
||||||
|
this.generateSelection = [];
|
||||||
|
this.generatePage = { current: 1, size: 10, total: 0 };
|
||||||
|
},
|
||||||
|
handleGenerateContractChange(contractId) {
|
||||||
|
this.syncBillingPlanOptions(contractId);
|
||||||
|
this.generateQuery.billingPlanId = '';
|
||||||
|
},
|
||||||
|
async loadGenerateWaybills() {
|
||||||
|
if (!this.generateQuery.contractId || !this.generateQuery.billingPlanId) {
|
||||||
|
this.$message.warning('请选择运单合同和计费方案');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.generateDialog.loading = true;
|
||||||
|
try {
|
||||||
|
const params = this.buildRequestParams(
|
||||||
|
this.normalizeQuery(this.generateQuery, 'finishDateRange', 'finishStartDate', 'finishEndDate')
|
||||||
|
);
|
||||||
|
const res = await api.getGenerateWaybills(this.generatePage.current, this.generatePage.size, params);
|
||||||
|
const data = this.unwrapPage(res);
|
||||||
|
this.generateRows = data.records || [];
|
||||||
|
this.generatePage.total = data.total || 0;
|
||||||
|
} finally {
|
||||||
|
this.generateDialog.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleGenerateSizeChange() {
|
||||||
|
this.generatePage.current = 1;
|
||||||
|
this.loadGenerateWaybills();
|
||||||
|
},
|
||||||
|
resetGenerateQuery() {
|
||||||
|
this.generateQuery = {};
|
||||||
|
this.generateRows = [];
|
||||||
|
this.generateSelection = [];
|
||||||
|
this.generatePage = { current: 1, size: 10, total: 0 };
|
||||||
|
},
|
||||||
|
generateSelectionChange(selection) {
|
||||||
|
this.generateSelection = selection;
|
||||||
|
},
|
||||||
|
openGeneratePreview() {
|
||||||
|
if (!this.generateSelection.length) {
|
||||||
|
this.$message.warning('请选择需要生成费用的运单');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.previewDialog.visible = true;
|
||||||
|
this.generateDialog.visible = false;
|
||||||
|
this.previewPage.current = 1;
|
||||||
|
this.loadGeneratePreview();
|
||||||
|
},
|
||||||
|
async loadGeneratePreview() {
|
||||||
|
this.previewDialog.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await api.getGeneratePreview(this.previewPage.current, this.previewPage.size, {
|
||||||
|
settlementType: this.settlementType || undefined,
|
||||||
|
contractId: this.generateQuery.contractId,
|
||||||
|
billingPlanId: this.generateQuery.billingPlanId,
|
||||||
|
waybillIds: this.generateSelection.map(item => item.id).join(','),
|
||||||
|
});
|
||||||
|
const data = res.data?.data || res.data || res || {};
|
||||||
|
this.dynamicFeeColumns = (data.feeItemNames || []).map((name, index) => ({
|
||||||
|
label: name,
|
||||||
|
prop: `feeItem${index}`,
|
||||||
|
minWidth: 130,
|
||||||
|
align: 'right',
|
||||||
|
}));
|
||||||
|
this.previewRows = (data.records || []).map(row => {
|
||||||
|
const dynamic = {};
|
||||||
|
(data.feeItemNames || []).forEach((name, index) => {
|
||||||
|
dynamic[`feeItem${index}`] = row.feeItems?.[name] || '';
|
||||||
|
});
|
||||||
|
return { ...row, ...dynamic };
|
||||||
|
});
|
||||||
|
this.previewPage.total = data.total || 0;
|
||||||
|
} finally {
|
||||||
|
this.previewDialog.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handlePreviewSizeChange() {
|
||||||
|
this.previewPage.current = 1;
|
||||||
|
this.loadGeneratePreview();
|
||||||
|
},
|
||||||
|
async submitGenerateFee() {
|
||||||
|
this.previewDialog.submitting = true;
|
||||||
|
try {
|
||||||
|
await api.generateFee({
|
||||||
|
settlementType: this.settlementType || undefined,
|
||||||
|
contractId: this.generateQuery.contractId,
|
||||||
|
billingPlanId: this.generateQuery.billingPlanId,
|
||||||
|
waybillIds: this.generateSelection.map(item => item.id),
|
||||||
|
});
|
||||||
|
this.$message.success('生成成功');
|
||||||
|
this.previewDialog.visible = false;
|
||||||
|
this.loadTable();
|
||||||
|
} finally {
|
||||||
|
this.previewDialog.submitting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
exportBlob(
|
||||||
|
'/blade-transport/receivable-payable-detail/export-receivable-payable-detail',
|
||||||
|
this.buildRequestParams(
|
||||||
|
this.normalizeQuery(this.query, 'generateDateRange', 'generateStartDate', 'generateEndDate')
|
||||||
|
),
|
||||||
|
{ feedback: true }
|
||||||
|
).then(res => {
|
||||||
|
downloadXls(
|
||||||
|
res.data,
|
||||||
|
`${this.settlementTypeLabel}明细${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buildRequestParams(params = {}) {
|
||||||
|
return this.settlementType ? { ...params, settlementType: this.settlementType } : params;
|
||||||
|
},
|
||||||
|
syncBillingPlanOptions(contractId) {
|
||||||
|
const contract = this.contractOptions.find(item => String(item.id) === String(contractId));
|
||||||
|
const plans = this.parseJson(contract?.billingPlanJson);
|
||||||
|
this.billingPlanOptions = plans.length
|
||||||
|
? plans.map((item, index) => ({
|
||||||
|
id: item.id || item.planId || item.name || `plan-${index}`,
|
||||||
|
name: item.name || item.planName || item.billingPlanName || `计费方案${index + 1}`,
|
||||||
|
}))
|
||||||
|
: [{ id: 'default', name: '默认计费方案' }];
|
||||||
|
},
|
||||||
|
normalizeQuery(source, rangeProp, startProp, endProp) {
|
||||||
|
const params = { ...source };
|
||||||
|
const range = params[rangeProp];
|
||||||
|
delete params[rangeProp];
|
||||||
|
if (Array.isArray(range)) {
|
||||||
|
params[startProp] = range[0];
|
||||||
|
params[endProp] = range[1];
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
},
|
||||||
|
decorateRow(row) {
|
||||||
|
const currency = row.currency || 'RMB';
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
unitPriceText: this.money(row.unitPrice, currency),
|
||||||
|
freightAmountText: this.money(row.freightAmount, currency),
|
||||||
|
otherFeeAmountText: this.money(row.otherFeeAmount, currency),
|
||||||
|
totalAmountText: this.money(row.totalAmount, currency),
|
||||||
|
settlementStatusName:
|
||||||
|
settlementStatusOptions.find(item => item.value === row.settlementStatus)?.label ||
|
||||||
|
row.settlementStatus ||
|
||||||
|
'-',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
money(value, currency) {
|
||||||
|
if (value === null || value === undefined || value === '') return '-';
|
||||||
|
return `${Number(value).toFixed(2)} ${currency}`;
|
||||||
|
},
|
||||||
|
formatCell(value) {
|
||||||
|
return value === null || value === undefined || value === '' ? '-' : value;
|
||||||
|
},
|
||||||
|
parseJson(value) {
|
||||||
|
if (!value) return [];
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(value);
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unwrapPage(res) {
|
||||||
|
const data = res.data?.data || res.data || res || {};
|
||||||
|
return data.records ? data : { records: data.records || [], total: data.total || 0 };
|
||||||
|
},
|
||||||
|
noop() {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.settlement-detail-page {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-detail-page__search {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 12px 12px 4px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
:deep(.el-form-item__label) {
|
||||||
|
min-width: 160px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-detail-page__search-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(220px, 1fr));
|
||||||
|
gap: 8px 24px;
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input),
|
||||||
|
:deep(.el-select),
|
||||||
|
:deep(.el-date-editor) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-detail-page__search-actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-detail-page__table-panel {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-detail-page__toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-detail-page__toolbar-left,
|
||||||
|
.settlement-detail-page__toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-detail-page__table,
|
||||||
|
.settlement-detail-page :deep(.el-table) {
|
||||||
|
--el-table-border-color: #eff1f7;
|
||||||
|
|
||||||
|
:deep(th.el-table__cell) {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row:nth-child(even) td.el-table__cell) {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-detail-page__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-detail-page__pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-detail-page__dialog-form {
|
||||||
|
padding: 16px 20px 0;
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settlement-detail-page__transfer-form {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1600px) {
|
||||||
|
.settlement-detail-page__search-grid {
|
||||||
|
grid-template-columns: repeat(4, minmax(220px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
.settlement-detail-page__search-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,559 @@
|
|||||||
|
<template>
|
||||||
|
<basic-container class="exception-disposal-page">
|
||||||
|
<section class="exception-disposal-page__search">
|
||||||
|
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
|
||||||
|
<div class="exception-disposal-page__search-grid">
|
||||||
|
<el-form-item v-for="field in searchFields" :key="field.prop" :label="field.label">
|
||||||
|
<el-date-picker
|
||||||
|
v-if="field.type === 'daterange'"
|
||||||
|
v-model="query[field.prop]"
|
||||||
|
type="daterange"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
start-placeholder="请选择"
|
||||||
|
end-placeholder="请选择"
|
||||||
|
range-separator="-"
|
||||||
|
/>
|
||||||
|
<el-cascader
|
||||||
|
v-else-if="field.prop === 'exceptionType'"
|
||||||
|
v-model="query[field.prop]"
|
||||||
|
:options="field.options"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="全部"
|
||||||
|
:props="{ emitPath: false }"
|
||||||
|
/>
|
||||||
|
<el-select
|
||||||
|
v-else-if="field.type === 'select'"
|
||||||
|
v-model="query[field.prop]"
|
||||||
|
clearable
|
||||||
|
placeholder="全部"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in field.options"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
|
<div class="exception-disposal-page__search-actions">
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="exception-disposal-page__table-panel">
|
||||||
|
<div class="exception-disposal-page__toolbar">
|
||||||
|
<div>
|
||||||
|
<el-button
|
||||||
|
v-if="permission.exception_disposal_batch_complete"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
@click="handleBatchComplete"
|
||||||
|
>
|
||||||
|
批量完成
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="exception-disposal-page__toolbar-right">
|
||||||
|
<el-tooltip content="刷新" placement="top">
|
||||||
|
<el-button :icon="Refresh" text @click="loadTable" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="表格密度" placement="top">
|
||||||
|
<el-button :icon="Rank" text @click="noop" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="列设置" placement="top">
|
||||||
|
<el-button :icon="Setting" text @click="noop" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="rows"
|
||||||
|
border
|
||||||
|
class="exception-disposal-page__table"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="52" fixed="left" align="center" />
|
||||||
|
<el-table-column type="index" label="序号" width="72" fixed="left" align="center" />
|
||||||
|
<el-table-column
|
||||||
|
v-for="column in tableColumns"
|
||||||
|
:key="column.prop"
|
||||||
|
:prop="column.prop"
|
||||||
|
:label="column.label"
|
||||||
|
:min-width="column.minWidth"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link v-if="column.link && row[column.prop]" type="primary" @click="openWaybill(row)">
|
||||||
|
{{ row[column.prop] }}
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
v-else-if="column.prop === 'disposalStatusName'"
|
||||||
|
:type="statusLinkType(row.disposalStatus)"
|
||||||
|
@click="openDisposal(row, row.disposalStatus === 'completed' ? 'detail' : 'follow')"
|
||||||
|
>
|
||||||
|
{{ row[column.prop] || '-' }}
|
||||||
|
</el-link>
|
||||||
|
<span v-else>{{ row[column.prop] || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="exception-disposal-page__actions">
|
||||||
|
<template v-if="row.disposalStatus !== 'completed'">
|
||||||
|
<el-link
|
||||||
|
v-if="permission.exception_disposal_follow"
|
||||||
|
type="primary"
|
||||||
|
@click="openDisposal(row, 'follow')"
|
||||||
|
>
|
||||||
|
跟进
|
||||||
|
</el-link>
|
||||||
|
<el-link
|
||||||
|
v-if="permission.exception_disposal_complete"
|
||||||
|
type="primary"
|
||||||
|
@click="completeRow(row)"
|
||||||
|
>
|
||||||
|
完成
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
<el-link
|
||||||
|
v-else-if="permission.exception_disposal_view"
|
||||||
|
type="primary"
|
||||||
|
@click="openDisposal(row, 'detail')"
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="exception-disposal-page__pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page.current"
|
||||||
|
v-model:page-size="page.size"
|
||||||
|
:total="page.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadTable"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialog.visible"
|
||||||
|
:title="dialog.mode === 'detail' ? '异常详情' : '异常跟进'"
|
||||||
|
width="960px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<div v-loading="dialog.loading" class="exception-disposal-page__dialog">
|
||||||
|
<div class="dialog-section-title">异常信息</div>
|
||||||
|
<dl class="exception-disposal-page__detail-grid">
|
||||||
|
<template v-for="field in detailFields" :key="field.prop">
|
||||||
|
<div :class="{ 'is-wide': field.span === 2 }">
|
||||||
|
<dt>{{ field.label }}</dt>
|
||||||
|
<dd>
|
||||||
|
<el-link v-if="field.link && detail[field.prop]" type="primary">
|
||||||
|
{{ detail[field.prop] }}
|
||||||
|
</el-link>
|
||||||
|
<span v-else>{{ detail[field.prop] || '-' }}</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="dialog-section-title">现场照片</div>
|
||||||
|
<div class="exception-disposal-page__photos">
|
||||||
|
<el-image
|
||||||
|
v-for="(url, index) in photoUrls"
|
||||||
|
:key="url"
|
||||||
|
:src="url"
|
||||||
|
:preview-src-list="photoUrls"
|
||||||
|
:initial-index="index"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<el-empty v-if="!photoUrls.length" description="暂无现场照片" :image-size="56" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
v-if="dialog.mode === 'follow'"
|
||||||
|
ref="followFormRef"
|
||||||
|
:model="followForm"
|
||||||
|
:rules="followRules"
|
||||||
|
label-position="right"
|
||||||
|
label-width="auto"
|
||||||
|
class="exception-disposal-page__follow-form"
|
||||||
|
>
|
||||||
|
<el-form-item label="跟进说明" prop="followContent">
|
||||||
|
<el-input
|
||||||
|
v-model="followForm.followContent"
|
||||||
|
type="textarea"
|
||||||
|
maxlength="500"
|
||||||
|
show-word-limit
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<div class="dialog-section-title">历史跟进</div>
|
||||||
|
<el-table :data="detail.followRecords || []" border>
|
||||||
|
<el-table-column type="index" label="序号" width="70" align="center" />
|
||||||
|
<el-table-column
|
||||||
|
v-for="column in followColumns"
|
||||||
|
:key="column.prop"
|
||||||
|
:prop="column.prop"
|
||||||
|
:label="column.label"
|
||||||
|
:min-width="column.minWidth"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
/>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button
|
||||||
|
v-if="dialog.mode === 'follow' && permission.exception_disposal_follow"
|
||||||
|
type="primary"
|
||||||
|
:loading="dialog.submitting"
|
||||||
|
@click="submitFollow"
|
||||||
|
>
|
||||||
|
提交
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="dialog.mode === 'follow' && permission.exception_disposal_complete"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
@click="completeFromDialog"
|
||||||
|
>
|
||||||
|
完成
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="dialog.visible = false">取消</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</basic-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Rank, Refresh, Setting } from '@element-plus/icons-vue';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import {
|
||||||
|
detailFields,
|
||||||
|
disposalStatusOptions,
|
||||||
|
exceptionCategoryOptions,
|
||||||
|
followColumns,
|
||||||
|
searchFields,
|
||||||
|
tableColumns,
|
||||||
|
} from '@/option/transit/exception-disposal';
|
||||||
|
import * as api from '@/api/transit/exception-disposal';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
Rank,
|
||||||
|
Refresh,
|
||||||
|
Setting,
|
||||||
|
searchFields,
|
||||||
|
tableColumns,
|
||||||
|
detailFields,
|
||||||
|
followColumns,
|
||||||
|
loading: false,
|
||||||
|
query: {},
|
||||||
|
rows: [],
|
||||||
|
selection: [],
|
||||||
|
page: { current: 1, size: 10, total: 0 },
|
||||||
|
dialog: { visible: false, mode: 'follow', loading: false, submitting: false },
|
||||||
|
detail: {},
|
||||||
|
followForm: { followContent: '' },
|
||||||
|
followRules: {
|
||||||
|
followContent: [
|
||||||
|
{ required: true, message: '请输入跟进说明', trigger: 'blur' },
|
||||||
|
{ max: 500, message: '跟进说明不能超过500个字符', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['permission']),
|
||||||
|
photoUrls() {
|
||||||
|
return this.parsePhotos(this.detail.scenePhotos);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadTable() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await api.getList(this.page.current, this.page.size, this.buildQuery());
|
||||||
|
const data = this.unwrapPage(res);
|
||||||
|
this.rows = (data.records || []).map(this.decorateRow);
|
||||||
|
this.page.total = data.total || 0;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildQuery() {
|
||||||
|
const params = { ...this.query };
|
||||||
|
if (Array.isArray(params.reportTimeRange)) {
|
||||||
|
params.reportStartTime = params.reportTimeRange[0];
|
||||||
|
params.reportEndTime = params.reportTimeRange[1];
|
||||||
|
}
|
||||||
|
delete params.reportTimeRange;
|
||||||
|
return params;
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.page.current = 1;
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
handleReset() {
|
||||||
|
this.query = {};
|
||||||
|
this.handleSearch();
|
||||||
|
},
|
||||||
|
handleSizeChange() {
|
||||||
|
this.page.current = 1;
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
handleSelectionChange(selection) {
|
||||||
|
this.selection = selection;
|
||||||
|
},
|
||||||
|
async openDisposal(row, mode) {
|
||||||
|
this.dialog = { visible: true, mode, loading: true, submitting: false };
|
||||||
|
this.followForm = { followContent: '' };
|
||||||
|
try {
|
||||||
|
const res = await api.getDetail(row.id);
|
||||||
|
this.detail = this.decorateRow(res.data?.data || res.data || res || {});
|
||||||
|
} finally {
|
||||||
|
this.dialog.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submitFollow() {
|
||||||
|
await this.$refs.followFormRef.validate();
|
||||||
|
this.dialog.submitting = true;
|
||||||
|
try {
|
||||||
|
await api.follow({ id: this.detail.id, followContent: this.followForm.followContent });
|
||||||
|
this.$message.success('跟进成功');
|
||||||
|
this.dialog.visible = false;
|
||||||
|
this.loadTable();
|
||||||
|
} finally {
|
||||||
|
this.dialog.submitting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async completeRow(row) {
|
||||||
|
await this.$confirm('确认完成该异常处置?', '提示', { type: 'warning' });
|
||||||
|
await api.complete({ id: row.id });
|
||||||
|
this.$message.success('完成成功');
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
async completeFromDialog() {
|
||||||
|
await this.$confirm('确认完成该异常处置?', '提示', { type: 'warning' });
|
||||||
|
await api.complete({ id: this.detail.id });
|
||||||
|
this.$message.success('完成成功');
|
||||||
|
this.dialog.visible = false;
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
async handleBatchComplete() {
|
||||||
|
if (!this.selection.length) {
|
||||||
|
this.$message.warning('请选择需要完成的异常');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.$confirm('确认批量完成已选择的异常处置?', '提示', { type: 'warning' });
|
||||||
|
await api.batchComplete(this.selection.map(item => item.id).join(','));
|
||||||
|
this.$message.success('批量完成成功');
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
openWaybill(row) {
|
||||||
|
if (row.waybillId) {
|
||||||
|
this.$router.push({ path: '/business/waybill-manage', query: { detailId: row.waybillId } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decorateRow(row) {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
exceptionTypeName: this.exceptionName(row.exceptionType),
|
||||||
|
disposalStatusName:
|
||||||
|
disposalStatusOptions.find(item => item.value === row.disposalStatus)?.label ||
|
||||||
|
row.disposalStatus ||
|
||||||
|
'-',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
exceptionName(value) {
|
||||||
|
for (const group of exceptionCategoryOptions) {
|
||||||
|
const match = group.children.find(item => item.value === value);
|
||||||
|
if (match) return match.label;
|
||||||
|
}
|
||||||
|
return value || '-';
|
||||||
|
},
|
||||||
|
statusLinkType(status) {
|
||||||
|
return status === 'completed' ? 'info' : 'primary';
|
||||||
|
},
|
||||||
|
parsePhotos(value) {
|
||||||
|
if (!value) return [];
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
if (Array.isArray(parsed)) return parsed.filter(Boolean);
|
||||||
|
} catch (error) {
|
||||||
|
return String(value)
|
||||||
|
.split(',')
|
||||||
|
.map(item => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
unwrapPage(res) {
|
||||||
|
const data = res.data?.data || res.data || res || {};
|
||||||
|
return data.records ? data : { records: [], total: 0 };
|
||||||
|
},
|
||||||
|
noop() {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.exception-disposal-page {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exception-disposal-page__search {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 12px 12px 4px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.exception-disposal-page__search-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(260px, 1fr));
|
||||||
|
gap: 8px 36px;
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input),
|
||||||
|
:deep(.el-select),
|
||||||
|
:deep(.el-cascader),
|
||||||
|
:deep(.el-date-editor) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.exception-disposal-page__search-actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exception-disposal-page__table-panel {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exception-disposal-page__toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exception-disposal-page__toolbar-right,
|
||||||
|
.exception-disposal-page__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exception-disposal-page__table {
|
||||||
|
--el-table-border-color: #eff1f7;
|
||||||
|
|
||||||
|
:deep(th.el-table__cell) {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row:nth-child(even) td.el-table__cell) {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.exception-disposal-page__pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exception-disposal-page__dialog {
|
||||||
|
padding: 4px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-section-title {
|
||||||
|
margin: 12px 0;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 4px solid #409eff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exception-disposal-page__detail-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px 24px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 120px minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-wide {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
color: #606266;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin: 0 0 0 12px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.exception-disposal-page__photos {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 80px;
|
||||||
|
|
||||||
|
:deep(.el-image) {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
border: 1px solid #eff1f7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.exception-disposal-page__follow-form {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1200px) {
|
||||||
|
.exception-disposal-page__search-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 760px) {
|
||||||
|
.exception-disposal-page__search-grid,
|
||||||
|
.exception-disposal-page__detail-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,504 @@
|
|||||||
|
<template>
|
||||||
|
<basic-container class="risk-disposal-page">
|
||||||
|
<section class="risk-disposal-page__search">
|
||||||
|
<el-form :model="query" label-position="right" label-width="160px" @submit.prevent>
|
||||||
|
<div class="risk-disposal-page__search-grid">
|
||||||
|
<el-form-item v-for="field in searchFields" :key="field.prop" :label="field.label">
|
||||||
|
<el-select
|
||||||
|
v-if="field.type === 'select'"
|
||||||
|
v-model="query[field.prop]"
|
||||||
|
clearable
|
||||||
|
:placeholder="field.placeholder"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in field.options"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
v-else
|
||||||
|
v-model="query[field.prop]"
|
||||||
|
clearable
|
||||||
|
:placeholder="field.placeholder || '请输入'"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<div class="risk-disposal-page__search-actions">
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="risk-disposal-page__table-panel">
|
||||||
|
<div class="risk-disposal-page__toolbar">
|
||||||
|
<div class="risk-disposal-page__toolbar-left">
|
||||||
|
<el-button type="primary" plain @click="openBatchDialog('process')">批量处理</el-button>
|
||||||
|
<el-button type="primary" plain @click="openBatchDialog('ignore')">批量忽略</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="risk-disposal-page__toolbar-right">
|
||||||
|
<el-tooltip content="刷新" placement="top">
|
||||||
|
<el-button :icon="Refresh" text @click="loadTable" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="表格密度" placement="top">
|
||||||
|
<el-button :icon="Rank" text @click="noop" />
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="列设置" placement="top">
|
||||||
|
<el-button :icon="Setting" text @click="noop" />
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="rows"
|
||||||
|
border
|
||||||
|
class="risk-disposal-page__table"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="64" fixed="left" align="center" />
|
||||||
|
<el-table-column type="index" label="序号" width="96" fixed="left" align="center" />
|
||||||
|
<el-table-column
|
||||||
|
v-for="column in tableColumns"
|
||||||
|
:key="column.prop"
|
||||||
|
:prop="column.prop"
|
||||||
|
:label="column.label"
|
||||||
|
:min-width="column.minWidth"
|
||||||
|
align="center"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-link v-if="column.link && row[column.prop]" type="primary" @click="openWaybill(row)">
|
||||||
|
{{ row[column.prop] }}
|
||||||
|
</el-link>
|
||||||
|
<el-tag v-else-if="column.tag" :type="tagType(row, column.prop)" effect="plain">
|
||||||
|
{{ row[column.prop] || '-' }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else>{{ row[column.prop] || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="190" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="risk-disposal-page__actions">
|
||||||
|
<template v-if="row.disposalStatus === 'pending'">
|
||||||
|
<el-link type="primary" @click="openDisposal(row, 'process')">处理</el-link>
|
||||||
|
<el-link type="primary" @click="openDisposal(row, 'ignore')">忽略</el-link>
|
||||||
|
</template>
|
||||||
|
<el-link v-else type="primary" @click="openDetail(row)">查看</el-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="risk-disposal-page__pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="page.current"
|
||||||
|
v-model:page-size="page.size"
|
||||||
|
:total="page.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadTable"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialog.visible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="1100px"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
class="risk-disposal-dialog"
|
||||||
|
>
|
||||||
|
<div v-loading="dialog.loading" class="risk-disposal-page__dialog">
|
||||||
|
<div v-if="!dialog.batch" class="risk-disposal-page__risk-card">
|
||||||
|
<div
|
||||||
|
v-for="field in detailFields"
|
||||||
|
:key="field.prop"
|
||||||
|
:class="{ 'is-wide': field.span === 3 }"
|
||||||
|
class="risk-disposal-page__risk-item"
|
||||||
|
>
|
||||||
|
<div class="risk-disposal-page__risk-label">{{ field.label }}</div>
|
||||||
|
<div class="risk-disposal-page__risk-value">
|
||||||
|
<el-link v-if="field.link && detail[field.prop]" type="primary" @click="openWaybill(detail)">
|
||||||
|
{{ detail[field.prop] }}
|
||||||
|
</el-link>
|
||||||
|
<el-tag v-else-if="field.tag" :type="tagType(detail, field.prop)" effect="plain">
|
||||||
|
{{ detail[field.prop] || '-' }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else>{{ detail[field.prop] || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="disposeFormRef"
|
||||||
|
:model="disposeForm"
|
||||||
|
:rules="dialog.readonly ? {} : disposeRules"
|
||||||
|
label-position="right"
|
||||||
|
label-width="auto"
|
||||||
|
class="risk-disposal-page__dispose-form"
|
||||||
|
>
|
||||||
|
<el-form-item label="处理方式" prop="disposalMethod" required>
|
||||||
|
<el-radio-group v-model="disposeForm.disposalMethod" :disabled="dialog.readonly">
|
||||||
|
<el-radio
|
||||||
|
v-for="item in disposalMethodOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="处置说明" prop="disposalRemark">
|
||||||
|
<el-input
|
||||||
|
v-model="disposeForm.disposalRemark"
|
||||||
|
type="textarea"
|
||||||
|
maxlength="500"
|
||||||
|
:rows="4"
|
||||||
|
:disabled="dialog.readonly"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialog.visible = false">关闭</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="!dialog.readonly"
|
||||||
|
type="primary"
|
||||||
|
:loading="dialog.submitting"
|
||||||
|
@click="submitDisposal"
|
||||||
|
>
|
||||||
|
确认
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</basic-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Rank, Refresh, Setting } from '@element-plus/icons-vue';
|
||||||
|
import {
|
||||||
|
detailFields,
|
||||||
|
disposalMethodOptions,
|
||||||
|
disposalStatusOptions,
|
||||||
|
riskLevelOptions,
|
||||||
|
searchFields,
|
||||||
|
tableColumns,
|
||||||
|
} from '@/option/transit/risk-disposal';
|
||||||
|
import * as api from '@/api/transit/risk-disposal';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
Rank,
|
||||||
|
Refresh,
|
||||||
|
Setting,
|
||||||
|
searchFields,
|
||||||
|
tableColumns,
|
||||||
|
detailFields,
|
||||||
|
disposalMethodOptions,
|
||||||
|
loading: false,
|
||||||
|
query: {},
|
||||||
|
rows: [],
|
||||||
|
selection: [],
|
||||||
|
page: { current: 1, size: 10, total: 0 },
|
||||||
|
dialog: {
|
||||||
|
visible: false,
|
||||||
|
batch: false,
|
||||||
|
readonly: false,
|
||||||
|
loading: false,
|
||||||
|
submitting: false,
|
||||||
|
method: 'process',
|
||||||
|
},
|
||||||
|
detail: {},
|
||||||
|
disposeForm: {
|
||||||
|
disposalMethod: 'process',
|
||||||
|
disposalRemark: '',
|
||||||
|
},
|
||||||
|
disposeRules: {
|
||||||
|
disposalMethod: [{ required: true, message: '请选择处理方式', trigger: 'change' }],
|
||||||
|
disposalRemark: [
|
||||||
|
{ max: 500, message: '处置说明不能超过500个字符', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dialogTitle() {
|
||||||
|
if (this.dialog.readonly) return '风险详情';
|
||||||
|
return '风险处置';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadTable() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await api.getList(this.page.current, this.page.size, this.buildQuery());
|
||||||
|
const data = this.unwrapPage(res);
|
||||||
|
this.rows = (data.records || []).map(this.decorateRow);
|
||||||
|
this.page.total = data.total || 0;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildQuery() {
|
||||||
|
return { ...this.query };
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.page.current = 1;
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
handleReset() {
|
||||||
|
this.query = {};
|
||||||
|
this.handleSearch();
|
||||||
|
},
|
||||||
|
handleSizeChange() {
|
||||||
|
this.page.current = 1;
|
||||||
|
this.loadTable();
|
||||||
|
},
|
||||||
|
handleSelectionChange(selection) {
|
||||||
|
this.selection = selection;
|
||||||
|
},
|
||||||
|
async openDisposal(row, method) {
|
||||||
|
this.dialog = {
|
||||||
|
visible: true,
|
||||||
|
batch: false,
|
||||||
|
readonly: false,
|
||||||
|
loading: true,
|
||||||
|
submitting: false,
|
||||||
|
method,
|
||||||
|
};
|
||||||
|
this.disposeForm = { disposalMethod: method, disposalRemark: '' };
|
||||||
|
try {
|
||||||
|
const res = await api.getDetail(row.id);
|
||||||
|
this.detail = this.decorateRow(res.data?.data || res.data || res || row);
|
||||||
|
} finally {
|
||||||
|
this.dialog.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async openDetail(row) {
|
||||||
|
this.dialog = {
|
||||||
|
visible: true,
|
||||||
|
batch: false,
|
||||||
|
readonly: true,
|
||||||
|
loading: true,
|
||||||
|
submitting: false,
|
||||||
|
method: row.disposalMethod || 'process',
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const res = await api.getDetail(row.id);
|
||||||
|
this.detail = this.decorateRow(res.data?.data || res.data || res || row);
|
||||||
|
this.disposeForm = {
|
||||||
|
disposalMethod: this.detail.disposalMethod || this.detail.processMethod || 'process',
|
||||||
|
disposalRemark: this.detail.disposalRemark || this.detail.processRemark || '',
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
this.dialog.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openBatchDialog(method) {
|
||||||
|
const pendingRows = this.selection.filter(row => row.disposalStatus === 'pending');
|
||||||
|
if (!pendingRows.length) {
|
||||||
|
this.$message.warning('请选择待处理风险');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.dialog = {
|
||||||
|
visible: true,
|
||||||
|
batch: true,
|
||||||
|
readonly: false,
|
||||||
|
loading: false,
|
||||||
|
submitting: false,
|
||||||
|
method,
|
||||||
|
};
|
||||||
|
this.detail = {};
|
||||||
|
this.disposeForm = { disposalMethod: method, disposalRemark: '' };
|
||||||
|
},
|
||||||
|
async submitDisposal() {
|
||||||
|
await this.$refs.disposeFormRef.validate();
|
||||||
|
this.dialog.submitting = true;
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
disposalMethod: this.disposeForm.disposalMethod,
|
||||||
|
disposalRemark: this.disposeForm.disposalRemark,
|
||||||
|
};
|
||||||
|
if (this.dialog.batch) {
|
||||||
|
await api.batchDispose({
|
||||||
|
...payload,
|
||||||
|
ids: this.selection
|
||||||
|
.filter(row => row.disposalStatus === 'pending')
|
||||||
|
.map(row => row.id),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await api.dispose({ ...payload, id: this.detail.id });
|
||||||
|
}
|
||||||
|
this.$message.success(this.disposeForm.disposalMethod === 'ignore' ? '忽略成功' : '处理成功');
|
||||||
|
this.dialog.visible = false;
|
||||||
|
this.loadTable();
|
||||||
|
} finally {
|
||||||
|
this.dialog.submitting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openWaybill(row) {
|
||||||
|
if (row.waybillId) {
|
||||||
|
this.$router.push({ path: '/business/waybill-manage', query: { detailId: row.waybillId } });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decorateRow(row) {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
riskLevelName: this.optionLabel(riskLevelOptions, row.riskLevel) || row.riskLevelName,
|
||||||
|
disposalStatusName:
|
||||||
|
this.optionLabel(disposalStatusOptions, row.disposalStatus) || row.disposalStatusName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
optionLabel(options, value) {
|
||||||
|
return options.find(item => item.value === value)?.label || '';
|
||||||
|
},
|
||||||
|
tagType(row, prop) {
|
||||||
|
const options = prop === 'riskLevelName' ? riskLevelOptions : disposalStatusOptions;
|
||||||
|
const sourceProp = prop === 'riskLevelName' ? 'riskLevel' : 'disposalStatus';
|
||||||
|
return options.find(item => item.value === row[sourceProp])?.tagType || '';
|
||||||
|
},
|
||||||
|
unwrapPage(res) {
|
||||||
|
const data = res.data?.data || res.data || res || {};
|
||||||
|
return data.records ? data : { records: [], total: 0 };
|
||||||
|
},
|
||||||
|
noop() {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.risk-disposal-page {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__search {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 12px 12px 4px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__search-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(260px, 1fr));
|
||||||
|
gap: 8px 36px;
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input),
|
||||||
|
:deep(.el-select) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__search-actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__table-panel {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__toolbar-left,
|
||||||
|
.risk-disposal-page__toolbar-right,
|
||||||
|
.risk-disposal-page__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__table {
|
||||||
|
--el-table-border-color: #eff1f7;
|
||||||
|
|
||||||
|
:deep(th.el-table__cell) {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row:nth-child(even) td.el-table__cell) {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__dialog {
|
||||||
|
padding: 4px 36px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__risk-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 20px 48px;
|
||||||
|
margin: 0 0 72px;
|
||||||
|
padding: 14px 42px 12px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__risk-item.is-wide {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__risk-label {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__risk-value {
|
||||||
|
min-height: 24px;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-disposal-page__dispose-form {
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-textarea) {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1200px) {
|
||||||
|
.risk-disposal-page__search-grid,
|
||||||
|
.risk-disposal-page__risk-card {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.risk-disposal-page__search-grid,
|
||||||
|
.risk-disposal-page__risk-card {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
<template>
|
||||||
|
<basic-container class="track-tracking-page">
|
||||||
|
<section class="track-tracking-page__search">
|
||||||
|
<el-form :model="query" label-position="right" label-width="auto" @submit.prevent>
|
||||||
|
<div class="track-tracking-page__search-row">
|
||||||
|
<el-form-item label="查询类型" required>
|
||||||
|
<el-select v-model="query.queryType" placeholder="请选择">
|
||||||
|
<el-option
|
||||||
|
v-for="item in queryTypeOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="车牌号" required>
|
||||||
|
<el-input v-model="query.vehicleNo" clearable placeholder="请输入" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="时间范围" required>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="query.timeRange"
|
||||||
|
type="daterange"
|
||||||
|
range-separator="-"
|
||||||
|
start-placeholder="请选择"
|
||||||
|
end-placeholder="请选择"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-link class="track-tracking-page__ship-link" type="primary" @click="handleShipSearch">
|
||||||
|
船舶查询
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
|
<div class="track-tracking-page__search-actions">
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="track-tracking-page__content">
|
||||||
|
<div class="track-tracking-page__map-panel">
|
||||||
|
<div class="track-tracking-page__map-card">
|
||||||
|
<div class="track-tracking-page__map-info">
|
||||||
|
<div
|
||||||
|
v-for="field in mapInfoFields"
|
||||||
|
:key="field.prop"
|
||||||
|
class="track-tracking-page__map-info-item"
|
||||||
|
>
|
||||||
|
<div class="track-tracking-page__map-info-label">{{ field.label }}</div>
|
||||||
|
<div class="track-tracking-page__map-info-value">
|
||||||
|
{{ mapInfo[field.prop] || '-' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="track-tracking-page__map-placeholder">
|
||||||
|
<div class="track-tracking-page__map-placeholder-text">高德地图占位</div>
|
||||||
|
<div class="track-tracking-page__map-route">
|
||||||
|
<span class="track-tracking-page__map-point track-tracking-page__map-point--start" />
|
||||||
|
<span class="track-tracking-page__map-line" />
|
||||||
|
<span class="track-tracking-page__map-point track-tracking-page__map-point--mid" />
|
||||||
|
<span class="track-tracking-page__map-line track-tracking-page__map-line--short" />
|
||||||
|
<span class="track-tracking-page__map-point track-tracking-page__map-point--end" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="track-tracking-page__blank-panel" />
|
||||||
|
</section>
|
||||||
|
</basic-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getShipTracking, getTrackingMap } from '@/api/transit/track-tracking';
|
||||||
|
import { mapInfoFields, queryTypeOptions } from '@/option/transit/track-tracking';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
queryTypeOptions,
|
||||||
|
mapInfoFields,
|
||||||
|
query: {
|
||||||
|
queryType: 'track',
|
||||||
|
vehicleNo: '',
|
||||||
|
timeRange: [],
|
||||||
|
},
|
||||||
|
mapInfo: {
|
||||||
|
siteName: 'XX园区A门',
|
||||||
|
address: '南宁市邕宁区龙岗大道21号',
|
||||||
|
coordinates: '108.348610;22.721864',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadTracking();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadTracking() {
|
||||||
|
if (this.query.queryType === 'ship') {
|
||||||
|
await getShipTracking(this.buildQuery());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await getTrackingMap(this.buildQuery());
|
||||||
|
},
|
||||||
|
buildQuery() {
|
||||||
|
const params = { ...this.query };
|
||||||
|
if (Array.isArray(params.timeRange)) {
|
||||||
|
params.startTime = params.timeRange[0];
|
||||||
|
params.endTime = params.timeRange[1];
|
||||||
|
}
|
||||||
|
delete params.timeRange;
|
||||||
|
return params;
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.loadTracking();
|
||||||
|
},
|
||||||
|
handleReset() {
|
||||||
|
this.query = {
|
||||||
|
queryType: 'track',
|
||||||
|
vehicleNo: '',
|
||||||
|
timeRange: [],
|
||||||
|
};
|
||||||
|
this.loadTracking();
|
||||||
|
},
|
||||||
|
handleShipSearch() {
|
||||||
|
this.query.queryType = 'ship';
|
||||||
|
this.loadTracking();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.track-tracking-page {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__search {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 24px 48px 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__search-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 260px 320px 360px 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__ship-link {
|
||||||
|
justify-self: end;
|
||||||
|
align-self: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__search-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 36px;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(1100px, 1fr) 1fr;
|
||||||
|
gap: 32px;
|
||||||
|
align-items: start;
|
||||||
|
min-height: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-panel {
|
||||||
|
min-height: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-card {
|
||||||
|
position: relative;
|
||||||
|
min-height: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-info {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
left: 16px;
|
||||||
|
z-index: 2;
|
||||||
|
width: 320px;
|
||||||
|
padding: 24px 28px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-info-item + .track-tracking-page__map-info-item {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-info-label {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-info-value {
|
||||||
|
color: #303133;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-placeholder {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 640px;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
linear-gradient(0deg, rgba(255, 255, 255, 0.86), rgba(255, 255, 255, 0.86)),
|
||||||
|
radial-gradient(circle at 12% 30%, rgba(230, 230, 230, 0.9) 0, rgba(230, 230, 230, 0.9) 24%, transparent 24.5%),
|
||||||
|
radial-gradient(circle at 34% 18%, rgba(235, 235, 235, 0.95) 0, rgba(235, 235, 235, 0.95) 18%, transparent 18.5%),
|
||||||
|
radial-gradient(circle at 65% 46%, rgba(232, 232, 232, 0.9) 0, rgba(232, 232, 232, 0.9) 26%, transparent 26.5%),
|
||||||
|
linear-gradient(135deg, #fafafa 0%, #ececec 100%);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-placeholder-text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 1;
|
||||||
|
color: rgba(0, 0, 0, 0.28);
|
||||||
|
font-size: 18px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-route {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 78px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-line {
|
||||||
|
display: block;
|
||||||
|
flex: 1;
|
||||||
|
height: 4px;
|
||||||
|
background: #1f86ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-line--short {
|
||||||
|
flex: 0.42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-point {
|
||||||
|
display: block;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin: 0 -9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #67c23a;
|
||||||
|
box-shadow: 0 0 0 6px rgba(103, 194, 58, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-point--start {
|
||||||
|
margin-left: 76px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-point--mid {
|
||||||
|
background: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__map-point--end {
|
||||||
|
background: #ff6b6b;
|
||||||
|
box-shadow: 0 0 0 8px rgba(255, 107, 107, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__blank-panel {
|
||||||
|
min-height: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.track-tracking-page__search .el-form-item__label) {
|
||||||
|
min-width: 96px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.track-tracking-page__search .el-input),
|
||||||
|
:deep(.track-tracking-page__search .el-select),
|
||||||
|
:deep(.track-tracking-page__search .el-date-editor) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1400px) {
|
||||||
|
.track-tracking-page__search-row {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-tracking-page__content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -532,6 +532,7 @@
|
|||||||
<el-table :data="invoiceTableData" border class="invoice-table">
|
<el-table :data="invoiceTableData" border class="invoice-table">
|
||||||
<el-table-column label="序号" prop="__index" width="90" align="center" />
|
<el-table-column label="序号" prop="__index" width="90" align="center" />
|
||||||
<el-table-column label="收票方名称" prop="invoiceTitle" min-width="180" />
|
<el-table-column label="收票方名称" prop="invoiceTitle" min-width="180" />
|
||||||
|
<el-table-column label="发票类型" prop="invoiceType" min-width="150" />
|
||||||
<el-table-column label="纳税人识别号" prop="taxNo" min-width="180" />
|
<el-table-column label="纳税人识别号" prop="taxNo" min-width="180" />
|
||||||
<el-table-column label="开户行名称" prop="bankName" min-width="180" />
|
<el-table-column label="开户行名称" prop="bankName" min-width="180" />
|
||||||
<el-table-column label="注册电话" prop="registeredPhone" min-width="150" />
|
<el-table-column label="注册电话" prop="registeredPhone" min-width="150" />
|
||||||
@@ -892,6 +893,12 @@
|
|||||||
<el-form-item label="收票方名称" prop="invoiceTitle">
|
<el-form-item label="收票方名称" prop="invoiceTitle">
|
||||||
<el-input v-model="invoiceForm.invoiceTitle" maxlength="100" />
|
<el-input v-model="invoiceForm.invoiceTitle" maxlength="100" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="发票类型" prop="invoiceType">
|
||||||
|
<el-select v-model="invoiceForm.invoiceType" clearable placeholder="请选择">
|
||||||
|
<el-option label="增值税专用发票" value="增值税专用发票" />
|
||||||
|
<el-option label="普通发票" value="普通发票" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="开户行名称" prop="bankName">
|
<el-form-item label="开户行名称" prop="bankName">
|
||||||
<el-select v-model="invoiceForm.bankName" filterable clearable>
|
<el-select v-model="invoiceForm.bankName" filterable clearable>
|
||||||
<el-option
|
<el-option
|
||||||
@@ -919,14 +926,16 @@
|
|||||||
<div class="invoice-form__address">
|
<div class="invoice-form__address">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="invoiceForm.receiverRegionName"
|
v-model="invoiceForm.receiverRegionName"
|
||||||
|
readonly
|
||||||
maxlength="100"
|
maxlength="100"
|
||||||
placeholder="地址控件选择"
|
placeholder="点击后弹出地图组件"
|
||||||
|
@click="handleInvoiceReceiverMapPick"
|
||||||
/>
|
/>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="invoiceForm.receiverDetailAddress"
|
v-model="invoiceForm.receiverDetailAddress"
|
||||||
maxlength="255"
|
maxlength="255"
|
||||||
placeholder="请输入详细地址"
|
placeholder="请输入详细地址"
|
||||||
/>
|
><template #suffix><el-icon class="invoice-form__address-location" @click="handleInvoiceReceiverMapPick"><Location /></el-icon></template></el-input>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -1220,7 +1229,7 @@ import { downloadXls } from '@/utils/util';
|
|||||||
import { getToken } from '@/utils/auth';
|
import { getToken } from '@/utils/auth';
|
||||||
import { getUploadHeaders } from '@/utils/upload';
|
import { getUploadHeaders } from '@/utils/upload';
|
||||||
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
import { normalizeSearchRangeParams } from '@/utils/search-range';
|
||||||
import { ArrowRight } from '@element-plus/icons-vue';
|
import { ArrowRight, Location } from '@element-plus/icons-vue';
|
||||||
import { ElImageViewer } from 'element-plus';
|
import { ElImageViewer } from 'element-plus';
|
||||||
import { OpenFileViewer } from '@open-file-viewer/vue';
|
import { OpenFileViewer } from '@open-file-viewer/vue';
|
||||||
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
import { fallbackPlugin, imagePlugin, officePlugin, pdfPlugin, textPlugin } from '@open-file-viewer/core';
|
||||||
@@ -1248,6 +1257,7 @@ const qualificationViewerPlugins = [
|
|||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
|
Location,
|
||||||
ElImageViewer,
|
ElImageViewer,
|
||||||
OpenFileViewer,
|
OpenFileViewer,
|
||||||
},
|
},
|
||||||
@@ -1308,6 +1318,7 @@ export default {
|
|||||||
receiptBox: false,
|
receiptBox: false,
|
||||||
invoiceBox: false,
|
invoiceBox: false,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
|
originalAccessType: '',
|
||||||
activeTab: 'scores',
|
activeTab: 'scores',
|
||||||
archiveForm: this.emptyArchive(),
|
archiveForm: this.emptyArchive(),
|
||||||
currentScore: { details: [] },
|
currentScore: { details: [] },
|
||||||
@@ -1458,6 +1469,7 @@ export default {
|
|||||||
invoiceRules: {
|
invoiceRules: {
|
||||||
taxNo: [{ required: true, message: '请输入纳税人识别号', trigger: 'blur' }],
|
taxNo: [{ required: true, message: '请输入纳税人识别号', trigger: 'blur' }],
|
||||||
invoiceTitle: [{ required: true, message: '请输入收票方名称', trigger: 'blur' }],
|
invoiceTitle: [{ required: true, message: '请输入收票方名称', trigger: 'blur' }],
|
||||||
|
invoiceType: [{ required: true, message: '请选择发票类型', trigger: 'change' }],
|
||||||
registeredPhone: [{ required: true, message: '请输入注册电话', trigger: 'blur' }],
|
registeredPhone: [{ required: true, message: '请输入注册电话', trigger: 'blur' }],
|
||||||
bankName: [{ required: true, message: '请输入开户行名称', trigger: 'blur' }],
|
bankName: [{ required: true, message: '请输入开户行名称', trigger: 'blur' }],
|
||||||
registeredRegionName: [
|
registeredRegionName: [
|
||||||
@@ -1573,6 +1585,7 @@ export default {
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
search: true,
|
search: true,
|
||||||
searchOrder: 8,
|
searchOrder: 8,
|
||||||
|
searchPlaceholder: '请选择',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
dicData: [
|
dicData: [
|
||||||
{ label: '临时', value: 'temporary' },
|
{ label: '临时', value: 'temporary' },
|
||||||
@@ -1678,8 +1691,11 @@ export default {
|
|||||||
invoiceDialogTitle() {
|
invoiceDialogTitle() {
|
||||||
return this.invoiceIndex > -1 ? '编辑发票信息' : '新增发票信息';
|
return this.invoiceIndex > -1 ? '编辑发票信息' : '新增发票信息';
|
||||||
},
|
},
|
||||||
|
isTemporaryToFormal() {
|
||||||
|
return Boolean(this.archiveForm.id) && this.originalAccessType === 'temporary' && this.archiveForm.accessType === 'formal';
|
||||||
|
},
|
||||||
missingQualificationText() {
|
missingQualificationText() {
|
||||||
if (this.archiveForm.accessType !== 'formal') return '';
|
if (this.archiveForm.accessType !== 'formal' || this.isTemporaryToFormal) return '';
|
||||||
const types = this.qualificationFiles
|
const types = this.qualificationFiles
|
||||||
.filter(item => item.url)
|
.filter(item => item.url)
|
||||||
.map(item => item.type || item.name)
|
.map(item => item.type || item.name)
|
||||||
@@ -1909,6 +1925,7 @@ export default {
|
|||||||
emptyInvoice() {
|
emptyInvoice() {
|
||||||
return {
|
return {
|
||||||
invoiceTitle: '',
|
invoiceTitle: '',
|
||||||
|
invoiceType: '',
|
||||||
taxNo: '',
|
taxNo: '',
|
||||||
bankName: '',
|
bankName: '',
|
||||||
registeredPhone: '',
|
registeredPhone: '',
|
||||||
@@ -1938,27 +1955,12 @@ export default {
|
|||||||
value: item.rawLabel,
|
value: item.rawLabel,
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
this.applyDefaultDeptSearch(deptColumn);
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.deptSearchInitialized = true;
|
this.deptSearchInitialized = true;
|
||||||
this.$nextTick(() => this.onLoad(this.page, this.query));
|
this.$nextTick(() => this.onLoad(this.page, this.query));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
applyDefaultDeptSearch(deptColumn) {
|
|
||||||
if (this.isAdmin) return;
|
|
||||||
const currentDeptId = this.userInfo.deptId || this.userInfo.dept_id;
|
|
||||||
const currentDept = this.deptOptions.find(
|
|
||||||
item => String(item.value) === String(currentDeptId)
|
|
||||||
);
|
|
||||||
const deptName = currentDept?.rawLabel || this.userInfo.deptName || this.userInfo.dept_name;
|
|
||||||
if (!deptName) return;
|
|
||||||
deptColumn.searchValue = deptName;
|
|
||||||
this.query = { ...this.query, deptName };
|
|
||||||
if (this.$refs.crud?.searchForm) {
|
|
||||||
this.$refs.crud.searchForm.deptName = deptName;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
initRegionOptions() {
|
initRegionOptions() {
|
||||||
getLazyTree().then(res => {
|
getLazyTree().then(res => {
|
||||||
const regions = this.buildRegionTree(res.data.data || []);
|
const regions = this.buildRegionTree(res.data.data || []);
|
||||||
@@ -2220,13 +2222,11 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
formatInvoiceReceiverAddress(row = {}) {
|
formatInvoiceReceiverAddress(row = {}) {
|
||||||
return (
|
const address = [row.receiverRegionName, row.receiverDetailAddress]
|
||||||
row.receiverAddress ||
|
.map(item => String(item || '').trim())
|
||||||
[row.receiverRegionName, row.receiverDetailAddress]
|
.filter(Boolean)
|
||||||
.map(item => String(item || '').trim())
|
.join('');
|
||||||
.filter(Boolean)
|
return address || row.receiverAddress || '';
|
||||||
.join('')
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
normalizeContact(contact = {}) {
|
normalizeContact(contact = {}) {
|
||||||
const contactAddress =
|
const contactAddress =
|
||||||
@@ -2307,6 +2307,17 @@ export default {
|
|||||||
this.contactMapStatus = '可搜索地址或点击地图选点';
|
this.contactMapStatus = '可搜索地址或点击地图选点';
|
||||||
this.contactMapBox = true;
|
this.contactMapBox = true;
|
||||||
},
|
},
|
||||||
|
handleInvoiceReceiverMapPick() {
|
||||||
|
this.contactMapTarget = 'invoiceReceiver';
|
||||||
|
this.contactMapKeyword =
|
||||||
|
this.invoiceForm.receiverDetailAddress || this.invoiceForm.receiverRegionName || '';
|
||||||
|
this.contactMapSelected = this.buildContactMapSelection({
|
||||||
|
address: this.invoiceForm.receiverDetailAddress,
|
||||||
|
regionName: this.invoiceForm.receiverRegionName,
|
||||||
|
});
|
||||||
|
this.contactMapStatus = '可搜索地址或点击地图选点';
|
||||||
|
this.contactMapBox = true;
|
||||||
|
},
|
||||||
loadAmap() {
|
loadAmap() {
|
||||||
if (window.AMap && window.AMap.Map) {
|
if (window.AMap && window.AMap.Map) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -2462,6 +2473,18 @@ export default {
|
|||||||
this.contactMapBox = false;
|
this.contactMapBox = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.contactMapTarget === 'invoiceReceiver') {
|
||||||
|
this.invoiceForm.receiverRegionName =
|
||||||
|
this.contactMapSelected.regionName || this.invoiceForm.receiverRegionName;
|
||||||
|
this.invoiceForm.receiverDetailAddress = this.formatMapDetailAddress(
|
||||||
|
this.contactMapSelected.detailAddress || this.invoiceForm.receiverDetailAddress,
|
||||||
|
this.invoiceForm.receiverRegionName
|
||||||
|
);
|
||||||
|
this.invoiceForm.receiverAddress = this.formatInvoiceReceiverAddress(this.invoiceForm);
|
||||||
|
this.$refs.invoiceForm?.validateField('receiverRegionName');
|
||||||
|
this.contactMapBox = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.contactForm.detailAddress =
|
this.contactForm.detailAddress =
|
||||||
this.contactMapSelected.detailAddress || this.contactForm.detailAddress;
|
this.contactMapSelected.detailAddress || this.contactForm.detailAddress;
|
||||||
this.contactForm.regionName =
|
this.contactForm.regionName =
|
||||||
@@ -2948,6 +2971,7 @@ export default {
|
|||||||
if (row && row.id) {
|
if (row && row.id) {
|
||||||
getDetail(row.id).then(res => {
|
getDetail(row.id).then(res => {
|
||||||
this.archiveForm = this.normalizeDetail(res.data.data);
|
this.archiveForm = this.normalizeDetail(res.data.data);
|
||||||
|
this.originalAccessType = this.archiveForm.accessType || '';
|
||||||
this.qualificationFiles = this.parseAttachments(
|
this.qualificationFiles = this.parseAttachments(
|
||||||
this.archiveForm.qualificationAttachments
|
this.archiveForm.qualificationAttachments
|
||||||
);
|
);
|
||||||
@@ -2958,6 +2982,7 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.archiveForm = this.emptyArchive();
|
this.archiveForm = this.emptyArchive();
|
||||||
|
this.originalAccessType = '';
|
||||||
this.qualificationFiles = [];
|
this.qualificationFiles = [];
|
||||||
this.qualificationUploadFiles = [];
|
this.qualificationUploadFiles = [];
|
||||||
this.selectedQualificationFiles = [];
|
this.selectedQualificationFiles = [];
|
||||||
@@ -2966,6 +2991,7 @@ export default {
|
|||||||
resetArchive() {
|
resetArchive() {
|
||||||
this.readonly = false;
|
this.readonly = false;
|
||||||
this.archiveForm = this.emptyArchive();
|
this.archiveForm = this.emptyArchive();
|
||||||
|
this.originalAccessType = '';
|
||||||
this.qualificationFiles = [];
|
this.qualificationFiles = [];
|
||||||
this.qualificationUploadFiles = [];
|
this.qualificationUploadFiles = [];
|
||||||
this.selectedQualificationFiles = [];
|
this.selectedQualificationFiles = [];
|
||||||
@@ -2986,7 +3012,8 @@ export default {
|
|||||||
},
|
},
|
||||||
validateFormalForApproval(archive) {
|
validateFormalForApproval(archive) {
|
||||||
if (archive.accessType !== 'formal') return true;
|
if (archive.accessType !== 'formal') return true;
|
||||||
if (!archive.qualificationAttachments) {
|
const isTemporaryToFormal = Boolean(archive.id) && this.originalAccessType === 'temporary';
|
||||||
|
if (!isTemporaryToFormal && !archive.qualificationAttachments) {
|
||||||
this.$message.warning('正式客商提交审核前必须维护客商材料');
|
this.$message.warning('正式客商提交审核前必须维护客商材料');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3592,7 +3619,7 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
searchReset() {
|
searchReset() {
|
||||||
this.query = this.isAdmin ? {} : { deptName: this.getCurrentDeptName() };
|
this.query = {};
|
||||||
this.onLoad(this.page);
|
this.onLoad(this.page);
|
||||||
},
|
},
|
||||||
searchChange(params, done) {
|
searchChange(params, done) {
|
||||||
@@ -3661,13 +3688,6 @@ export default {
|
|||||||
buildQuery(params = {}) {
|
buildQuery(params = {}) {
|
||||||
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
|
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
|
||||||
},
|
},
|
||||||
getCurrentDeptName() {
|
|
||||||
const currentDeptId = this.userInfo.deptId || this.userInfo.dept_id;
|
|
||||||
const currentDept = this.deptOptions.find(
|
|
||||||
item => String(item.value) === String(currentDeptId)
|
|
||||||
);
|
|
||||||
return currentDept?.rawLabel || this.userInfo.deptName || this.userInfo.dept_name || '';
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -4029,6 +4049,11 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoice-form__address-location {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
.score-detail-table {
|
.score-detail-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user