1302 lines
41 KiB
Vue
1302 lines
41 KiB
Vue
<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"
|
||
:before-open="beforeOpen"
|
||
@row-save="rowSave"
|
||
@row-update="rowUpdate"
|
||
@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="$refs.crud.rowAdd()"
|
||
>新建过程配置
|
||
</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 #menu-form-before="{ disabled }">
|
||
<template v-if="!dialogReadonly">
|
||
<el-button
|
||
plain
|
||
:loading="disabled"
|
||
:disabled="disabled"
|
||
@click="handleProcessConfigSave('draft')"
|
||
>
|
||
保存草稿
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:loading="disabled"
|
||
:disabled="disabled"
|
||
@click="handleProcessConfigSave('enabled')"
|
||
>
|
||
保存并启用
|
||
</el-button>
|
||
</template>
|
||
</template>
|
||
|
||
<template #status="{ row }">
|
||
<el-tag :type="statusTagType(row.status)">
|
||
{{ row.statusName || displayStatus(row.status) }}
|
||
</el-tag>
|
||
</template>
|
||
|
||
<template #includedNodes="{ row }">
|
||
<span class="process-config-page__node-value">
|
||
{{ formatProcessNodes(row.includedNodes) }}
|
||
</span>
|
||
</template>
|
||
|
||
<template #basicInfo-form>
|
||
<section-card title="基本信息">
|
||
<el-form
|
||
:model="form"
|
||
label-position="right"
|
||
label-width="calc(6em + 24px)"
|
||
>
|
||
<el-row :gutter="18">
|
||
<el-col :span="6">
|
||
<el-form-item label="配置名称" required>
|
||
<el-input
|
||
v-model="form.configName"
|
||
:disabled="dialogReadonly"
|
||
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"
|
||
:disabled="dialogReadonly"
|
||
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-page__finish-days">
|
||
<span>默认</span>
|
||
<el-input
|
||
v-model="form.defaultFinishDays"
|
||
:disabled="dialogReadonly"
|
||
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="4"
|
||
:disabled="dialogReadonly"
|
||
maxlength="500"
|
||
show-word-limit
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
</section-card>
|
||
</template>
|
||
|
||
<template #nodeConfigJson-form>
|
||
<div class="process-config-page__custom-form">
|
||
<section-card title="现行过程节点">
|
||
<div class="process-config-page__timeline" v-if="enabledNodes.length">
|
||
<div class="process-config-page__timeline-line"></div>
|
||
<div
|
||
v-for="node in enabledNodes"
|
||
:key="node.key"
|
||
class="process-config-page__timeline-item"
|
||
>
|
||
<span class="process-config-page__timeline-dot"></span>
|
||
<div class="process-config-page__timeline-name">{{ node.name }}</div>
|
||
<div class="process-config-page__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-page__node-table"
|
||
border
|
||
:data="nodeRows"
|
||
:span-method="nodeSpanMethod"
|
||
>
|
||
<el-table-column label="节点设置" width="136">
|
||
<template #default="{ row, $index }">
|
||
<div class="process-config-page__node-cell">
|
||
<el-checkbox
|
||
v-model="row.enabled"
|
||
:disabled="dialogReadonly"
|
||
@change="syncNodeFields"
|
||
/>
|
||
<span class="process-config-page__node-name">{{ row.name }}</span>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作要求" min-width="660">
|
||
<template #default="{ row }">
|
||
<div class="process-config-page__requirements">
|
||
<template v-if="row.key === 'accept'">
|
||
<div class="process-config-page__require-line">
|
||
<span class="process-config-page__require-label">是否确认</span>
|
||
<el-radio-group
|
||
v-model="row.confirmMode"
|
||
:disabled="dialogReadonly || !row.enabled"
|
||
@change="syncNodeFields"
|
||
>
|
||
<el-radio label="yes">是</el-radio>
|
||
<el-radio label="no_confirm_accept">无需确认接单</el-radio>
|
||
</el-radio-group>
|
||
</div>
|
||
<div class="process-config-page__require-line">
|
||
<span class="process-config-page__require-label">确认接单人</span>
|
||
<el-checkbox
|
||
v-model="row.confirmDriver"
|
||
:disabled="dialogReadonly || !row.enabled || row.confirmMode !== 'yes'"
|
||
@change="syncNodeFields"
|
||
>司机</el-checkbox
|
||
>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="row.key === 'return'">
|
||
<div class="process-config-page__require-line">
|
||
<span class="process-config-page__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-page__require-line">
|
||
<span class="process-config-page__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-page__require-grid">
|
||
<div class="process-config-page__require-line">
|
||
<span class="process-config-page__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-page__require-line" v-if="row.supportCargo">
|
||
<span class="process-config-page__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-page__require-line process-config-page__require-line--wide"
|
||
v-if="row.key === 'transit'"
|
||
>
|
||
<span class="process-config-page__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-page__require-line">
|
||
<span class="process-config-page__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-page__require-line" v-if="row.supportCargo">
|
||
<span class="process-config-page__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-page__require-line process-config-page__require-line--wide"
|
||
v-if="row.key === 'transit'"
|
||
>
|
||
<span class="process-config-page__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-page__attachments" v-if="row.supportVoucher">
|
||
<div class="process-config-page__require-line">
|
||
<span class="process-config-page__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-page__require-line">
|
||
<span class="process-config-page__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>
|
||
|
||
<template #menu="{ row, index }">
|
||
<el-link
|
||
type="primary"
|
||
v-if="hasPermission('process_config_view')"
|
||
@click="$refs.crud.rowView(row, index)"
|
||
>
|
||
查看
|
||
</el-link>
|
||
<el-link
|
||
type="primary"
|
||
v-if="canEdit(row)"
|
||
@click="$refs.crud.rowEdit(row, index)"
|
||
>
|
||
编辑
|
||
</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>
|
||
|
||
<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,
|
||
projectOptions: [],
|
||
projectLoading: false,
|
||
selectedProjectId: '',
|
||
nodeRows: [],
|
||
page: {
|
||
pageSize: 10,
|
||
pageSizes: [10, 20, 50, 100],
|
||
currentPage: 1,
|
||
total: 0,
|
||
},
|
||
selectionList: [],
|
||
submitStatus: 1,
|
||
};
|
||
},
|
||
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);
|
||
},
|
||
handleReturnConfirmChange(row) {
|
||
row.confirmInternal = row.confirmMode === 'yes';
|
||
this.syncNodeFields();
|
||
},
|
||
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;
|
||
},
|
||
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) {
|
||
return {
|
||
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: false,
|
||
confirmInternal: false,
|
||
...template,
|
||
};
|
||
},
|
||
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() {
|
||
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,
|
||
enabled: item.enabled,
|
||
confirmMode: item.confirmMode,
|
||
confirmDriver: item.confirmDriver,
|
||
confirmInternal: item.confirmInternal,
|
||
punch: item.punch,
|
||
location: item.location,
|
||
uploadCargo: item.uploadCargo,
|
||
cargoTypes: 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' ? '无需确认接单' : '是'}`
|
||
);
|
||
return result;
|
||
}
|
||
if (node.key === 'return') {
|
||
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 && node.uploadCargo && node.cargoTypes.length) {
|
||
result.push(`上传货量:${node.cargoTypes.join('、')}`);
|
||
}
|
||
if (node.supportVoucher && 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();
|
||
}
|
||
});
|
||
submitRow.defaultFinishDays = submitRow.defaultFinishDays
|
||
? Number(submitRow.defaultFinishDays)
|
||
: undefined;
|
||
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;
|
||
},
|
||
stopSubmitLoading(loading) {
|
||
if (typeof loading === 'function') {
|
||
loading();
|
||
}
|
||
},
|
||
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',
|
||
}
|
||
);
|
||
},
|
||
submitRow(row, done, loading, status = this.submitStatus) {
|
||
this.submitStatus = 1;
|
||
this.syncNodeFields();
|
||
const submitRow = this.normalizeRow({ ...row, status });
|
||
if (!this.validateRow(submitRow)) {
|
||
this.stopSubmitLoading(loading);
|
||
return;
|
||
}
|
||
this.checkEnabledProjectBeforeSubmit(submitRow)
|
||
.then(exists => {
|
||
if (exists) {
|
||
this.$message.warning('该项目已有启用状态的过程配置,不能重复添加');
|
||
throw { code: 'enabled-project-config' };
|
||
}
|
||
})
|
||
.then(() => this.confirmDuplicateVoucher())
|
||
.then(() => api.submit(submitRow))
|
||
.then(() => {
|
||
this.onLoad(this.page);
|
||
this.$message({ type: 'success', message: '操作成功!' });
|
||
done();
|
||
})
|
||
.catch(error => {
|
||
if (
|
||
error !== 'cancel' &&
|
||
error !== 'close' &&
|
||
error?.code !== 'enabled-project-config'
|
||
) {
|
||
window.console.log(error);
|
||
}
|
||
this.stopSubmitLoading(loading);
|
||
});
|
||
},
|
||
rowSave(row, done, loading) {
|
||
this.submitRow(row, done, loading);
|
||
},
|
||
rowUpdate(row, index, done, loading) {
|
||
this.submitRow(row, done, loading);
|
||
},
|
||
handleProcessConfigSave(mode) {
|
||
this.submitStatus = mode === 'draft' ? 2 : 1;
|
||
if (this.$refs.crud && typeof this.$refs.crud.rowSave === 'function') {
|
||
this.$refs.crud.rowSave();
|
||
}
|
||
},
|
||
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();
|
||
},
|
||
beforeOpen(done, type) {
|
||
this.dialogReadonly = type === 'view';
|
||
this.submitStatus = 1;
|
||
if (type === 'add') {
|
||
this.form = {
|
||
configName: '',
|
||
projectIds: '',
|
||
projectNames: '',
|
||
defaultFinishDays: '',
|
||
remark: '',
|
||
status: 0,
|
||
};
|
||
this.selectedProjectId = '';
|
||
this.resetNodeRows();
|
||
done();
|
||
return;
|
||
}
|
||
if (['edit', 'view'].includes(type)) {
|
||
api.getDetail(this.form.id).then(res => {
|
||
this.form = res.data.data || {};
|
||
this.selectedProjectId = this.form.projectIds ? String(this.form.projectIds) : '';
|
||
this.ensureSelectedProjectOption();
|
||
this.parseNodeConfig(this.form);
|
||
done();
|
||
});
|
||
return;
|
||
}
|
||
done();
|
||
},
|
||
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;
|
||
}
|
||
|
||
&__finish-days {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
.el-input {
|
||
width: 88px;
|
||
}
|
||
}
|
||
|
||
&__custom-form {
|
||
width: 100%;
|
||
}
|
||
|
||
&__timeline {
|
||
position: relative;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
min-height: 136px;
|
||
padding: 12px 28px 18px;
|
||
}
|
||
|
||
&__timeline-line {
|
||
position: absolute;
|
||
top: 22px;
|
||
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(.avue-crud__header) {
|
||
margin-top: 12px;
|
||
background: transparent;
|
||
}
|
||
|
||
:deep(.el-table) {
|
||
--el-table-border-color: #eff1f7;
|
||
}
|
||
|
||
:deep(.el-table__body tr:nth-child(even) > td.el-table__cell) {
|
||
background: #fafafa;
|
||
}
|
||
}
|
||
</style>
|