Files
tms-erp-web/src/views/business/process-config.vue
T
2026-09-13 01:01:24 +08:00

1342 lines
43 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container class="process-config-page">
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
v-model="form"
ref="crud"
:permission="permissionList"
@row-del="rowDel"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="primary"
icon="el-icon-plus"
v-if="hasPermission('process_config_add')"
@click="openConfig()"
>新建过程配置
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="config.exportUrl && hasPermission('process_config_export')"
@click="handleExport"
>批量导出
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="hasPermission('process_config_delete')"
@click="handleDelete"
>批量删除
</el-button>
</template>
<template #status="{ row }">
<el-tag :type="statusTagType(row.status)" class="status-text">
{{ row.statusName || displayStatus(row.status) }}
</el-tag>
</template>
<template #includedNodes="{ row }">
<span class="process-config-page__node-value">
{{ formatProcessNodes(row.includedNodes) }}
</span>
</template>
<template #menu="{ row, index }">
<el-link
type="primary"
v-if="hasPermission('process_config_view')"
@click="openConfig(row, true)"
>
查看
</el-link>
<el-link
type="primary"
v-if="canEdit(row)"
@click="openConfig(row)"
>
编辑
</el-link>
<el-link
type="primary"
v-if="hasPermission('process_config_copy')"
@click="handleCopy(row)"
>
复制
</el-link>
<el-link
type="primary"
v-if="canEnable(row)"
@click="handleAction('enable', row)"
>
启用
</el-link>
<el-link
type="primary"
v-if="canDisable(row)"
@click="handleAction('disable', row)"
>
停用
</el-link>
<el-link
type="danger"
v-if="canDelete(row)"
@click="rowDel(row)"
>
删除
</el-link>
</template>
</avue-crud>
<el-dialog
:title="dialogReadonly ? '查看过程配置' : form.id ? '编辑过程配置' : '新建过程配置'"
append-to-body
v-model="configBox"
width="90%"
top="4vh"
class="process-config-dialog"
:close-on-click-modal="false"
@closed="handleDialogClosed"
>
<div class="process-config-form">
<section-card title="基本信息">
<el-form
:model="form"
label-position="right"
label-width="calc(6em + 24px)"
:disabled="dialogReadonly"
>
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="配置名称" required>
<el-input
v-model="form.configName"
maxlength="100"
placeholder="请输入"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="项目" required>
<el-select
v-model="selectedProjectId"
clearable
filterable
remote
:remote-method="loadProjectOptions"
:loading="projectLoading"
placeholder="请选择"
style="width: 100%"
@change="handleProjectChange"
>
<el-option
v-for="item in projectOptions"
:key="item.id"
:label="formatProjectLabel(item)"
:value="String(item.id)"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="运输完成时限">
<div class="process-config-form__finish-days">
<span>默认</span>
<el-input
v-model="form.defaultFinishDays"
maxlength="3"
placeholder="请输入"
@input="handleFinishDaysInput"
/>
<span>天后完成运输</span>
</div>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
maxlength="500"
show-word-limit
placeholder="请输入"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</section-card>
<section-card title="现行过程节点">
<div class="process-config-form__timeline" v-if="enabledNodes.length">
<div class="process-config-form__timeline-line"></div>
<div
v-for="node in enabledNodes"
:key="node.key"
class="process-config-form__timeline-item"
>
<span class="process-config-form__timeline-dot"></span>
<div class="process-config-form__timeline-name">{{ node.name }}</div>
<div class="process-config-form__timeline-desc">
<div v-for="text in timelineDescriptions(node)" :key="text">{{ text }}</div>
</div>
</div>
</div>
<el-empty v-else description="请在节点设置中勾选过程节点" :image-size="72" />
</section-card>
<section-card title="节点设置">
<el-table
class="process-config-form__node-table"
border
:data="nodeRows"
:span-method="nodeSpanMethod"
>
<el-table-column label="节点设置" width="136">
<template #default="{ row }">
<div class="process-config-form__node-cell">
<el-checkbox
v-model="row.enabled"
:disabled="dialogReadonly"
@change="syncNodeFields"
/>
<span class="process-config-form__node-name">{{ row.name }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="操作要求" min-width="660">
<template #default="{ row }">
<div class="process-config-form__requirements">
<template v-if="row.key === 'accept'">
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">是否确认</span>
<el-radio-group
v-model="row.confirmMode"
:disabled="dialogReadonly || !row.enabled"
@change="handleAcceptConfirmChange(row)"
>
<el-radio label="yes"></el-radio>
<el-radio label="no_confirm_accept">无需确认接单</el-radio>
</el-radio-group>
</div>
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">确认接单人</span>
<el-checkbox v-model="row.confirmDriver" :disabled="true">司机</el-checkbox>
</div>
</template>
<template v-else-if="row.key === 'return'">
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">是否确认</span>
<el-radio-group
v-model="row.confirmMode"
:disabled="dialogReadonly || !row.enabled"
@change="handleReturnConfirmChange(row)"
>
<el-radio label="yes"></el-radio>
<el-radio label="no_confirm_complete">无需确认完成</el-radio>
</el-radio-group>
</div>
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">确认完成</span>
<el-checkbox
v-model="row.confirmInternal"
:disabled="dialogReadonly || !row.enabled || row.confirmMode !== 'yes'"
@change="syncNodeFields"
>内部人员</el-checkbox
>
</div>
</template>
<template v-else>
<div class="process-config-form__require-grid">
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">是否打卡</span>
<el-radio-group
v-model="row.punch"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</div>
<div class="process-config-form__require-line" v-if="row.supportCargo">
<span class="process-config-form__require-label">上传货量</span>
<el-radio-group
v-model="row.uploadCargo"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</div>
<div
class="process-config-form__require-line process-config-form__require-line--wide"
v-if="row.key === 'transit'"
>
<span class="process-config-form__require-label">打卡频次</span>
<el-input
v-model="row.frequencyDays"
:disabled="dialogReadonly || !row.enabled || !row.punch"
maxlength="3"
@input="value => handleFrequencyInput(row, value)"
/>
<span>天打卡1次</span>
</div>
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">打卡定位</span>
<el-radio-group
v-model="row.location"
:disabled="dialogReadonly || !row.enabled || !row.punch"
@change="syncNodeFields"
>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</div>
<div class="process-config-form__require-line" v-if="row.supportCargo">
<span class="process-config-form__require-label">上传货量类型</span>
<el-checkbox-group
v-model="row.cargoTypes"
:disabled="dialogReadonly || !row.enabled || !row.uploadCargo"
@change="syncNodeFields"
>
<el-checkbox label="重量">重量</el-checkbox>
<el-checkbox label="体积">体积</el-checkbox>
<el-checkbox label="数量">数量</el-checkbox>
</el-checkbox-group>
</div>
<div
class="process-config-form__require-line process-config-form__require-line--wide"
v-if="row.key === 'transit'"
>
<span class="process-config-form__require-label">打卡时段</span>
<el-time-picker
v-model="row.timeStart"
:disabled="dialogReadonly || !row.enabled || !row.punch"
format="HH:mm"
value-format="HH:mm"
placeholder="开始"
/>
<span>-</span>
<el-time-picker
v-model="row.timeEnd"
:disabled="dialogReadonly || !row.enabled || !row.punch"
format="HH:mm"
value-format="HH:mm"
placeholder="结束"
/>
</div>
</div>
</template>
</div>
</template>
</el-table-column>
<el-table-column label="附件要求" min-width="320">
<template #default="{ row }">
<div class="process-config-form__attachments" v-if="row.supportVoucher">
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">上传凭证</span>
<el-radio-group
v-model="row.uploadVoucher"
:disabled="dialogReadonly || !row.enabled"
@change="syncNodeFields"
>
<el-radio :label="true"></el-radio>
<el-radio :label="false"></el-radio>
</el-radio-group>
</div>
<div class="process-config-form__require-line">
<span class="process-config-form__require-label">凭证类型</span>
<el-checkbox-group
v-model="row.voucherTypes"
:disabled="dialogReadonly || !row.enabled || !row.uploadVoucher"
@change="syncNodeFields"
>
<el-checkbox v-for="item in row.voucherOptions" :key="item" :label="item">
{{ item }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</template>
</el-table-column>
</el-table>
</section-card>
</div>
<template #footer>
<el-button @click="configBox = false">{{ dialogReadonly ? '关闭' : '取消' }}</el-button>
<el-button
plain
:loading="submitting"
v-if="!dialogReadonly"
@click="submitConfig('draft')"
>
保存草稿
</el-button>
<el-button
type="primary"
:loading="submitting"
v-if="!dialogReadonly"
@click="submitConfig('enabled')"
>
保存并启用
</el-button>
</template>
</el-dialog>
<empty-pagination
:page="page"
@size-change="sizeChange"
@current-change="currentChange"
@load="onLoad(page, query)"
/>
</basic-container>
</template>
<script>
import * as api from '@/api/business/process-config';
import { getList as getProjectList } from '@/api/business/project-apply';
import { getDeptTree } from '@/api/system/dept';
import { exportBlob } from '@/api/common';
import { config, option } from '@/option/business/process-config';
import { getToken } from '@/utils/auth';
import { downloadXls } from '@/utils/util';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const NODE_TEMPLATES = [
{
key: 'accept',
name: '接单',
type: 'confirm',
supportVoucher: false,
},
{ key: 'arrive_scene', name: '到场', type: 'punch', enabled: false },
{
key: 'load',
name: '装货',
type: 'cargo',
enabled: false,
supportCargo: true,
supportVoucher: true,
voucherOptions: ['委托单', '发货单', '磅单', '装车照片'],
},
{
key: 'dispatch',
name: '发货',
type: 'cargo',
supportCargo: true,
supportVoucher: true,
voucherOptions: ['委托单', '发货单', '磅单', '装车照片'],
},
{
key: 'transit',
name: '在途',
type: 'transit',
enabled: false,
supportVoucher: true,
voucherOptions: ['货物照片'],
},
{
key: 'arrival',
name: '到货',
type: 'cargo',
supportCargo: true,
supportVoucher: true,
voucherOptions: ['磅单', '卸货照片', '签收单'],
},
{
key: 'unload',
name: '卸货',
type: 'cargo',
enabled: false,
supportCargo: true,
supportVoucher: true,
voucherOptions: ['磅单', '卸货照片', '签收单'],
},
{
key: 'sign',
name: '签收',
type: 'cargo',
enabled: false,
supportCargo: true,
supportVoucher: true,
voucherOptions: ['磅单', '签收单'],
},
{
key: 'return',
name: '回单',
type: 'confirm',
supportVoucher: true,
voucherOptions: ['磅单', '签收单'],
},
];
export default {
data() {
return {
api,
config,
option,
form: {},
query: {},
loading: true,
data: [],
dialogReadonly: false,
configBox: false,
submitting: false,
projectOptions: [],
projectLoading: false,
selectedProjectId: '',
nodeRows: [],
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
currentPage: 1,
total: 0,
},
selectionList: [],
};
},
computed: {
...mapGetters(['permission', 'userInfo']),
permissionList() {
return {
addBtn: this.hasPermission('process_config_add'),
};
},
isAdmin() {
const authority = this.userInfo && this.userInfo.authority;
return Array.isArray(authority)
? authority.includes('admin')
: String(authority || '').includes('admin');
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
enabledNodes() {
return this.nodeRows.filter(item => item.enabled);
},
},
created() {
this.resetNodeRows();
this.loadProjectOptions('');
this.applyDefaultDeptSearch();
this.initDeptOptions();
},
methods: {
hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
},
handleAcceptConfirmChange(row) {
row.confirmDriver = row.confirmMode === 'yes';
this.syncNodeFields();
},
handleReturnConfirmChange(row) {
row.confirmInternal = row.confirmMode === 'yes';
this.syncNodeFields();
},
enforceAcceptDriver(node) {
if (!node || (node.key !== 'accept' && node.name !== '接单')) return node;
node.confirmDriver = node.confirmMode === 'yes';
return node;
},
initDeptOptions() {
getDeptTree(this.userInfo.tenantId).then(res => {
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
if (deptColumn) {
deptColumn.dicData = this.flattenDept(res.data.data || []);
}
});
},
defaultDeptId() {
return (this.userInfo && (this.userInfo.deptId || this.userInfo.dept_id)) || '';
},
applyDefaultDeptSearch() {
const deptId = this.defaultDeptId();
const deptColumn = this.findColumn(this.option.column, 'searchDeptId');
if (!deptId || !deptColumn) return;
deptColumn.searchValue = deptId;
this.query = { ...this.query, deptId };
this.$nextTick(() => {
if (this.$refs.crud?.searchForm) {
this.$refs.crud.searchForm.searchDeptId = deptId;
}
});
},
flattenDept(tree, level = 0) {
return tree.flatMap(item => [
{
label: `${' '.repeat(level)}${item.title || item.deptName || item.name}`,
value: item.id,
},
...(item.children && item.children.length
? this.flattenDept(item.children, level + 1)
: []),
]);
},
displayStatus(status) {
const statusMap = {
0: '草稿',
1: '启用',
2: '停用',
};
return statusMap[status] || status || '未知';
},
formatProcessNodes(nodes) {
return String(nodes || '')
.split(/[,]/)
.map(item => item.trim())
.filter(Boolean)
.join('-');
},
statusTagType(status) {
if (status === 1) return 'success';
if (status === 2) return 'info';
return 'warning';
},
canEdit(row) {
return this.hasPermission('process_config_edit') && !row.readonly && !row.hasRelatedWaybill;
},
canDelete(row) {
return (
this.hasPermission('process_config_delete') && !row.readonly && [0, 2].includes(row.status)
);
},
canEnable(row) {
return (
this.hasPermission('process_config_enable') && !row.readonly && [0, 2].includes(row.status)
);
},
canDisable(row) {
return this.hasPermission('process_config_disable') && !row.readonly && row.status === 1;
},
cloneNode(template) {
const node = {
enabled: false,
punch: true,
location: true,
uploadCargo: true,
cargoTypes: [],
uploadVoucher: undefined,
voucherTypes: [],
frequencyDays: '1',
timeStart: '00:00',
timeEnd: '23:59',
supportCargo: false,
supportVoucher: false,
voucherOptions: [],
confirmMode: 'yes',
confirmDriver: true,
confirmInternal: false,
...template,
};
if ((node.key === 'accept' || node.name === '接单') && node.confirmMode === 'yes') {
node.confirmDriver = true;
} else if ((node.key === 'accept' || node.name === '接单') && node.confirmMode !== 'yes') {
node.confirmDriver = false;
}
return node;
},
resetNodeRows() {
this.nodeRows = NODE_TEMPLATES.map(item => this.cloneNode(item));
this.syncNodeFields();
},
parseNodeConfig(row = {}) {
let nodes = [];
if (row.nodeConfigJson) {
try {
const parsed = JSON.parse(row.nodeConfigJson);
nodes = Array.isArray(parsed) ? parsed : parsed.nodes || [];
} catch (error) {
nodes = [];
}
}
const includedNodes = String(row.includedNodes || '')
.split(',')
.map(item => item.trim())
.filter(Boolean);
this.nodeRows = NODE_TEMPLATES.map(template => {
const stored =
nodes.find(item => item.key === template.key || item.name === template.name) || {};
const node = this.cloneNode({ ...template, ...stored });
if (includedNodes.length) {
node.enabled = includedNodes.includes(node.name);
}
return node;
});
this.syncNodeFields();
},
nodeSpanMethod({ row, columnIndex }) {
if (!row.supportVoucher && columnIndex === 2) {
return [1, 1];
}
return [1, 1];
},
loadProjectOptions(keyword = '') {
this.projectLoading = true;
return getProjectList(1, 50, {
projectName: keyword,
approvalStatus: 'approved',
allDept: 0,
})
.then(res => {
const data = res.data.data || {};
this.projectOptions = data.records || [];
this.syncProjectSearchOptions();
this.ensureSelectedProjectOption();
})
.finally(() => {
this.projectLoading = false;
});
},
ensureSelectedProjectOption() {
if (!this.form.projectIds || !this.form.projectNames) return;
const exists = this.projectOptions.some(
item => String(item.id) === String(this.form.projectIds)
);
if (exists) return;
this.projectOptions = [
{
id: this.form.projectIds,
projectName: this.form.projectNames,
projectCode: this.form.projectCode,
},
...this.projectOptions,
];
this.syncProjectSearchOptions();
},
syncProjectSearchOptions() {
const column = this.option.column.find(item => item.prop === 'projectNames');
if (!column) return;
column.dicData = this.projectOptions.map(item => ({
label: this.formatProjectLabel(item),
value: item.projectName,
}));
},
formatProjectLabel(item) {
return item.projectCode ? `${item.projectName}${item.projectCode}` : item.projectName;
},
handleProjectChange(value) {
const project = this.projectOptions.find(item => String(item.id) === String(value));
this.form.projectIds = project ? String(project.id) : '';
this.form.projectNames = project ? project.projectName : '';
},
checkEnabledProjectConfig(projectId, excludeId = '') {
if (!projectId) return Promise.resolve(false);
return api
.getList(1, 100, {
projectIds: String(projectId),
status: 1,
})
.then(res => {
const data = res?.data?.data || {};
const records = Array.isArray(data) ? data : data.records || [];
return records.some(item => {
const projectIds = String(item.projectIds || '')
.split(/[,]/)
.map(value => value.trim())
.filter(Boolean);
return (
String(item.id || '') !== String(excludeId || '') &&
Number(item.status) === 1 &&
projectIds.includes(String(projectId))
);
});
});
},
checkEnabledProjectBeforeSubmit(row) {
if (row.id || !row.projectIds) return Promise.resolve(false);
return this.checkEnabledProjectConfig(row.projectIds);
},
handleFinishDaysInput(value) {
this.form.defaultFinishDays = String(value || '')
.replace(/\D/g, '')
.slice(0, 3);
},
handleFrequencyInput(row, value) {
row.frequencyDays = String(value || '')
.replace(/\D/g, '')
.slice(0, 3);
this.syncNodeFields();
},
syncNodeFields() {
this.nodeRows.forEach(item => this.enforceAcceptDriver(item));
const enabledNodes = this.nodeRows.filter(item => item.enabled);
this.form.includedNodes = enabledNodes.map(item => item.name).join(',');
this.form.nodeConfigJson = JSON.stringify(
this.nodeRows.map(item => ({
key: item.key,
name: item.name,
type: item.type,
enabled: item.enabled,
confirmMode: item.confirmMode,
confirmDriver: item.confirmDriver,
confirmInternal: item.confirmInternal,
punch: item.punch,
location: item.location,
// 在途不支持货量;其它节点按配置
uploadCargo: item.type === 'transit' || item.key === 'transit' ? false : item.uploadCargo,
cargoTypes:
item.type === 'transit' || item.key === 'transit'
? []
: item.uploadCargo
? item.cargoTypes
: [],
frequencyDays: item.frequencyDays,
timeStart: item.timeStart,
timeEnd: item.timeEnd,
uploadVoucher: item.uploadVoucher,
voucherTypes: item.uploadVoucher ? item.voucherTypes : [],
}))
);
},
timelineDescriptions(node) {
const result = [];
if (node.key === 'accept') {
result.push(
`是否接单:${node.confirmMode === 'no_confirm_accept' ? '无需确认接单' : '是'}`
);
result.push(
`确认接单人(司机):${node.confirmMode === 'yes' && node.confirmDriver ? '是' : '否'}`
);
return result;
}
if (node.key === 'return') {
result.push(
`是否确认:${node.confirmMode === 'no_confirm_complete' ? '无需确认完成' : '是'}`
);
result.push(
`确认完成(内部人员):${
node.confirmMode === 'yes' && node.confirmInternal ? '是' : '否'
}`
);
if (node.supportVoucher) {
result.push(`上传凭证:${node.uploadVoucher ? '是' : '否'}`);
if (node.uploadVoucher && node.voucherTypes.length) {
result.push(`凭证类型:${node.voucherTypes.join('、')}`);
}
}
return result;
}
if (node.type === 'transit') {
result.push(`打卡操作:${node.punch ? '是' : '否'}`);
result.push(`打卡定位:${node.location ? '是' : '否'}`);
if (node.punch) {
result.push(`打卡频次:每${node.frequencyDays || 1}天打卡1次`);
result.push(`打卡时段:${node.timeStart || '00:00'}-${node.timeEnd || '23:59'}`);
}
} else {
result.push(`打卡操作:${node.punch ? '是' : '否'}`);
result.push(`打卡定位:${node.location ? '是' : '否'}`);
}
if (node.supportCargo) {
result.push(`上传货量:${node.uploadCargo ? '是' : '否'}`);
if (node.uploadCargo && node.cargoTypes.length) {
result.push(`上传货量类型:${node.cargoTypes.join('、')}`);
}
}
if (node.supportVoucher) {
result.push(`上传凭证:${node.uploadVoucher ? '是' : '否'}`);
if (node.uploadVoucher && node.voucherTypes.length) {
result.push(`凭证类型:${node.voucherTypes.join('、')}`);
}
}
return result;
},
normalizeRow(row) {
const submitRow = { ...row };
Object.keys(submitRow).forEach(key => {
if (typeof submitRow[key] === 'string') {
submitRow[key] = submitRow[key].trim();
}
});
const defaultFinishDays = submitRow.defaultFinishDays;
submitRow.defaultFinishDays =
defaultFinishDays === undefined ||
defaultFinishDays === null ||
defaultFinishDays === '' ||
Number(defaultFinishDays) === -1
? ''
: Number(defaultFinishDays);
return submitRow;
},
validateRow(row) {
if (!row.configName) {
this.$message.warning('配置名称不能为空');
return false;
}
if (row.configName.length > 100) {
this.$message.warning('配置名称不能超过100个字符');
return false;
}
if (!row.projectNames) {
this.$message.warning('项目不能为空');
return false;
}
if (row.defaultFinishDays !== undefined && row.defaultFinishDays !== '') {
if (!Number.isInteger(Number(row.defaultFinishDays)) || Number(row.defaultFinishDays) < 0) {
this.$message.warning('完成天数不能小于0');
return false;
}
if (Number(row.defaultFinishDays) > 999) {
this.$message.warning('完成天数不能超过999');
return false;
}
}
if (row.remark && row.remark.length > 500) {
this.$message.warning('备注不能超过500个字符');
return false;
}
if (!this.enabledNodes.length) {
this.$message.warning('请至少选择一个过程节点');
return false;
}
const transit = this.nodeRows.find(item => item.key === 'transit');
if (transit && transit.enabled && transit.punch) {
if (!transit.frequencyDays || Number(transit.frequencyDays) < 1) {
this.$message.warning('请输入打卡频次');
return false;
}
if (!transit.timeStart || !transit.timeEnd) {
this.$message.warning('请选择打卡时段');
return false;
}
}
return true;
},
needsDuplicateVoucherConfirm() {
const signNode = this.nodeRows.find(item => item.key === 'sign');
const returnNode = this.nodeRows.find(item => item.key === 'return');
const hasSignVoucher =
signNode &&
signNode.enabled &&
signNode.uploadVoucher &&
signNode.voucherTypes.includes('签收单');
const hasReturnVoucher =
returnNode &&
returnNode.enabled &&
returnNode.uploadVoucher &&
returnNode.voucherTypes.includes('签收单');
return hasSignVoucher && hasReturnVoucher;
},
confirmDuplicateVoucher() {
if (!this.needsDuplicateVoucherConfirm()) {
return Promise.resolve();
}
return this.$confirm(
'签收与回单同时勾选了上传凭证,司机将需重复上传凭证,是否确认?',
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
);
},
openConfig(row, readonly = false) {
this.dialogReadonly = readonly || !!(row && row.hasRelatedWaybill);
if (!row || !row.id) {
this.form = {
configName: '',
projectIds: '',
projectNames: '',
defaultFinishDays: '',
remark: '',
status: 0,
};
this.selectedProjectId = '';
this.resetNodeRows();
this.configBox = true;
return;
}
api.getDetail(row.id).then(res => {
const detail = res.data.data || {};
this.dialogReadonly = readonly || !!detail.hasRelatedWaybill;
this.form = {
...detail,
defaultFinishDays:
Number(detail.defaultFinishDays) === -1 ? '' : detail.defaultFinishDays ?? '',
};
this.selectedProjectId = this.form.projectIds ? String(this.form.projectIds) : '';
this.ensureSelectedProjectOption();
this.parseNodeConfig(this.form);
this.configBox = true;
});
},
handleDialogClosed() {
this.submitting = false;
this.dialogReadonly = false;
},
submitConfig(mode) {
if (this.submitting) return;
if (this.form.id && this.form.hasRelatedWaybill) {
this.$message.warning('该项目已有运单,过程配置不可修改');
return;
}
this.syncNodeFields();
const submitRow = this.normalizeRow({
...this.form,
status: mode === 'draft' ? 2 : 1,
});
if (!this.validateRow(submitRow)) {
return;
}
this.submitting = true;
this.checkEnabledProjectBeforeSubmit(submitRow)
.then(exists => {
if (exists) {
this.$message.warning('该项目已有启用状态的过程配置,不能重复添加');
throw { code: 'enabled-project-config' };
}
})
.then(() => this.confirmDuplicateVoucher())
.then(() => api.submit(submitRow))
.then(() => {
this.configBox = false;
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
})
.catch(error => {
if (
error !== 'cancel' &&
error !== 'close' &&
error?.code !== 'enabled-project-config'
) {
window.console.log(error);
}
})
.finally(() => {
this.submitting = false;
});
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => api.remove(row.id))
.then(res => {
this.afterRemove(res.data.data);
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
const selection = this.selectionList.filter(item => this.canDelete(item));
if (!selection.length) {
this.$message.warning('所选数据当前状态不可删除');
return;
}
this.$confirm('确定将选择数据删除?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => api.remove(selection.map(item => item.id).join(',')))
.then(res => {
this.afterRemove(res.data.data);
});
},
afterRemove(result = {}) {
this.onLoad(this.page);
const successCount = result.successCount || 0;
const skippedCount = result.skippedCount || 0;
if (skippedCount) {
this.$message.warning(`成功删除${successCount}条,因引用跳过${skippedCount}条`);
} else {
this.$message({ type: 'success', message: '删除成功!' });
}
this.selectionClear();
},
searchReset() {
this.query = {};
this.applyDefaultDeptSearch();
this.page.currentPage = 1;
this.onLoad(this.page, this.query);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
if (this.$refs.crud) {
this.$refs.crud.toggleSelection();
}
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
this.loading = true;
api
.getList(page.currentPage, page.pageSize, {
...params,
...this.query,
allDept: this.isAdmin ? 1 : 0,
})
.then(res => {
const result = res.data.data || {};
this.page.total = result.total || 0;
this.data = result.records || [];
this.selectionClear();
})
.finally(() => {
this.loading = false;
});
},
buildExportParams() {
return {
...this.query,
allDept: this.isAdmin ? 1 : 0,
ids: this.ids,
[this.website.tokenHeader]: getToken(),
};
},
handleExport() {
this.$confirm(`是否导出${config.exportName}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(config.exportUrl, this.buildExportParams(), { feedback: true })
.then(res => {
downloadXls(
res.data,
`${config.exportName}${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
);
})
.finally(() => {
NProgress.done();
});
});
},
handleCopy(row) {
this.$confirm(`确定复制该${config.title}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => api.copy(row.id))
.then(() => {
this.$message.success('复制成功');
this.onLoad(this.page, this.query);
});
},
handleAction(action, row) {
const actionName = {
enable: '启用',
disable: '停用',
}[action];
this.$confirm(`确定${actionName}${config.title}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
if (action !== 'enable') return true;
return this.checkEnabledProjectConfig(row.projectIds, row.id)
.then(exists => {
if (exists) {
this.$message.warning('该项目已有其他启用状态的过程配置,不能重复启用');
return false;
}
return true;
})
.catch(error => {
window.console.log(error);
this.$message.error('检查项目过程配置失败,暂不能启用');
return false;
});
})
.then(canProceed => {
if (!canProceed) return null;
return api[action](row.id);
})
.then(result => {
if (!result) return;
this.$message.success(`${actionName}成功`);
this.onLoad(this.page, this.query);
});
},
},
};
</script>
<style lang="scss" scoped>
.process-config-page {
&__node-value {
white-space: normal;
word-break: break-all;
}
:deep(.avue-crud__header) {
margin-top: 12px;
background: transparent;
}
}
.process-config-form {
&__finish-days {
display: flex;
align-items: center;
gap: 8px;
.el-input {
width: 88px;
}
}
&__timeline {
position: relative;
display: flex;
justify-content: space-between;
min-height: 136px;
padding: 12px 28px 18px;
}
&__timeline-line {
position: absolute;
// 圆点中心位置 = padding-top(12) + dot 半径(7) = 19px
// 线 height 3px,因此 top 需为 19 - 1.5 = 17.5px,使线视觉中心与圆点中心对齐
top: 17.5px;
right: 58px;
left: 58px;
height: 3px;
background: #1e90ff;
}
&__timeline-item {
position: relative;
z-index: 1;
flex: 1;
text-align: center;
}
&__timeline-dot {
display: inline-block;
width: 14px;
height: 14px;
border-radius: 50%;
background: #1e90ff;
}
&__timeline-name {
margin-top: 14px;
color: #1688ff;
font-size: 18px;
font-weight: 600;
}
&__timeline-desc {
margin-top: 6px;
color: #303133;
line-height: 1.7;
white-space: nowrap;
}
&__node-table {
margin-top: 8px;
}
&__node-cell {
display: flex;
align-items: center;
gap: 12px;
}
&__node-name {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 64px;
height: 44px;
padding: 0 14px;
border-radius: 4px;
background: #f2f3f5;
color: #303133;
font-size: 16px;
font-weight: 600;
}
&__requirements,
&__attachments {
padding: 8px 0;
}
&__require-grid {
display: grid;
grid-template-columns: minmax(240px, 1fr) minmax(260px, 1fr);
gap: 12px 24px;
}
&__require-line {
display: flex;
align-items: center;
min-height: 30px;
gap: 10px;
.el-input {
width: 62px;
}
.el-time-editor.el-input {
width: 116px;
}
}
&__require-line--wide {
white-space: nowrap;
}
&__require-label {
flex: 0 0 96px;
color: #303133;
font-weight: 500;
text-align: right;
}
:deep(.el-table) {
--el-table-border-color: #eff1f7;
}
:deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
background: #fafafa;
}
}
</style>