feat(web-admin): add dedicated ETC vehicle recharge pages
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
95a9c04fa6
commit
345149dd6b
5 files changed
+3983
No files matched your search
@@ -0,0 +1,134 @@
|
||||
import type { ColumnFilterDef } from '@/composables/useClientColumnFilters'
|
||||
|
||||
const RECHARGE_TYPE_OPTIONS = [{ value: 'etc', label: 'ETC' }]
|
||||
|
||||
const RECHARGE_STATUS_OPTIONS = [
|
||||
{ value: 'draft', label: '草稿' },
|
||||
{ value: 'in_approval', label: '审批中' },
|
||||
{ value: 'rejected', label: '已驳回' },
|
||||
{ value: 'disbursed', label: '已发放' },
|
||||
{ value: 'cancelled', label: '已作废' }
|
||||
]
|
||||
|
||||
/** ETC 申请列表列头漏斗(服务端)。 */
|
||||
export const plVehicleRechargeApplyColumnFilterDefs: ColumnFilterDef[] = [
|
||||
{ prop: 'applyNo', label: '申请单号', type: 'text', queryKey: 'applyNo' },
|
||||
{ prop: 'licensePlate', label: '车牌号', type: 'text', queryKey: 'licensePlate' },
|
||||
{
|
||||
prop: 'rechargeType',
|
||||
label: '充值类型',
|
||||
type: 'enum',
|
||||
queryKey: 'rechargeType',
|
||||
enumOptions: RECHARGE_TYPE_OPTIONS
|
||||
},
|
||||
{
|
||||
prop: 'applyDate',
|
||||
label: '申请日期',
|
||||
type: 'datetime',
|
||||
queryKeyFrom: 'applyDateFrom',
|
||||
queryKeyTo: 'applyDateTo'
|
||||
},
|
||||
{ prop: 'odometerKm', label: '期初公里数', type: 'number', queryKeyPrefix: 'odometerKm' },
|
||||
{ prop: 'applicantName', label: '申请人', type: 'text', queryKey: 'applicantName' },
|
||||
{ prop: 'remark', label: '备注', type: 'text', queryKey: 'remark' },
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
type: 'enum',
|
||||
queryKey: 'status',
|
||||
enumOptions: RECHARGE_STATUS_OPTIONS
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '创建时间',
|
||||
type: 'datetime',
|
||||
queryKeyFrom: 'createTimeFrom',
|
||||
queryKeyTo: 'createTimeTo'
|
||||
}
|
||||
]
|
||||
|
||||
/** ETC 审批列表列头漏斗(服务端;在申请 defs 基础上增加当前节点)。 */
|
||||
export const plVehicleRechargeApprovalColumnFilterDefs: ColumnFilterDef[] = [
|
||||
...plVehicleRechargeApplyColumnFilterDefs.filter((d) => d.prop !== 'createTime'),
|
||||
{ prop: 'currentNodeName', label: '当前节点', type: 'text', queryKey: 'currentNodeName' },
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '创建时间',
|
||||
type: 'datetime',
|
||||
queryKeyFrom: 'createTimeFrom',
|
||||
queryKeyTo: 'createTimeTo'
|
||||
}
|
||||
]
|
||||
|
||||
/** ETC 发放看板列头漏斗(服务端)。 */
|
||||
export const plVehicleRechargeDisburseColumnFilterDefs: ColumnFilterDef[] = [
|
||||
{ prop: 'applyNo', label: '申请单号', type: 'text', queryKey: 'applyNo' },
|
||||
{
|
||||
prop: 'applyDate',
|
||||
label: '申请日期',
|
||||
type: 'datetime',
|
||||
queryKeyFrom: 'applyDateFrom',
|
||||
queryKeyTo: 'applyDateTo'
|
||||
},
|
||||
{ prop: 'applicantName', label: '申请人', type: 'text', queryKey: 'applicantName' },
|
||||
{ prop: 'odometerKm', label: '期初公里数', type: 'number', queryKeyPrefix: 'odometerKm' },
|
||||
{
|
||||
prop: 'applyStatus',
|
||||
label: '申请状态',
|
||||
type: 'enum',
|
||||
queryKey: 'applyStatus',
|
||||
enumOptions: RECHARGE_STATUS_OPTIONS
|
||||
},
|
||||
{ prop: 'licensePlate', label: '车牌号', type: 'text', queryKey: 'licensePlate' },
|
||||
{
|
||||
prop: 'rechargeType',
|
||||
label: '充值类型',
|
||||
type: 'enum',
|
||||
queryKey: 'rechargeType',
|
||||
enumOptions: RECHARGE_TYPE_OPTIONS
|
||||
},
|
||||
{ prop: 'rechargeOrderNo', label: '充值订单号', type: 'text', queryKey: 'rechargeOrderNo' },
|
||||
{
|
||||
prop: 'rechargeDate',
|
||||
label: '充值日期',
|
||||
type: 'datetime',
|
||||
queryKeyFrom: 'rechargeDateFrom',
|
||||
queryKeyTo: 'rechargeDateTo'
|
||||
},
|
||||
{ prop: 'cardNo', label: '卡号', type: 'text', queryKey: 'cardNo' },
|
||||
{ prop: 'gasStation', label: '加油站/渠道', type: 'text', queryKey: 'gasStation' },
|
||||
{ prop: 'amount', label: '金额(元)', type: 'number', queryKeyPrefix: 'amount' },
|
||||
{
|
||||
prop: 'recordCreateByName',
|
||||
label: '登记人',
|
||||
type: 'text',
|
||||
queryKey: 'recordCreateByName'
|
||||
}
|
||||
]
|
||||
|
||||
/** ETC 审批人设置(客户端)。与油卡共用同一套节点筛选定义。 */
|
||||
export const plVehicleRechargeWorkflowColumnFilterDefs: ColumnFilterDef[] = [
|
||||
{ prop: 'nodeName', label: '节点名称', type: 'text' },
|
||||
{
|
||||
prop: 'actionType',
|
||||
label: '节点动作',
|
||||
type: 'enum',
|
||||
enumOptions: [
|
||||
{ value: 'approve', label: '审批' },
|
||||
{ value: 'disburse', label: '发放登记' }
|
||||
],
|
||||
getValue: (row) => row.actionType || row.action_type || 'approve'
|
||||
},
|
||||
{ prop: 'nodeOrder', label: '节点顺序', type: 'number' },
|
||||
{ prop: 'approverName', label: '审批人', type: 'text' },
|
||||
{
|
||||
prop: 'configStatus',
|
||||
label: '配置状态',
|
||||
type: 'enum',
|
||||
enumOptions: [
|
||||
{ value: 'configured', label: '已配置' },
|
||||
{ value: 'unconfigured', label: '未配置' }
|
||||
],
|
||||
getValue: (row) => ((row.approverId ?? row.approver_id) ? 'configured' : 'unconfigured')
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,835 @@
|
||||
<!-- ETC充值审批(待办/已办) -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<PlPlanTypeTabs
|
||||
:model-value="activeTab"
|
||||
:tabs="APPROVAL_TABS"
|
||||
@change="handleTabChange"
|
||||
/>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<span class="list-toolbar-selected">已选 {{ selectedRows.length }} 项</span>
|
||||
<el-button
|
||||
v-if="hasActiveFilters"
|
||||
class="btn-ghost"
|
||||
@click="clearAllFilters"
|
||||
>
|
||||
<el-icon><Filter /></el-icon>清空列过滤
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="activeTab === 'pending'"
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:disabled="!canApproveSelected"
|
||||
@click="openHandleDialog(selectedRows[0])"
|
||||
>
|
||||
<el-icon><Check /></el-icon>处理
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="selectedRows.length !== 1" @click="openView(selectedRows[0])">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<ListTable
|
||||
table-key="pl-etc-vehicle-recharge-approval"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
@selection-change="(rows) => (selectedRows = rows)"
|
||||
@row-dblclick="openView"
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" />
|
||||
<el-table-column type="index" label="序号" width="68" align="center" />
|
||||
<el-table-column prop="applyNo" label="申请单号" min-width="160" show-overflow-tooltip>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">申请单号</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="applyNo"
|
||||
label="申请单号"
|
||||
type="text"
|
||||
:is-active="isFilterActive('applyNo')"
|
||||
:model-value="filters.applyNo || null"
|
||||
@update:model-value="(v) => setFilterState('applyNo', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openView(row)">{{ row.applyNo }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="160" show-overflow-tooltip align="center">
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">车牌号</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="licensePlate"
|
||||
label="车牌号"
|
||||
type="text"
|
||||
:is-active="isFilterActive('licensePlate')"
|
||||
:model-value="filters.licensePlate || null"
|
||||
@update:model-value="(v) => setFilterState('licensePlate', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
{{ formatPlVehiclePlateDisplay(row.licensePlate, undefined, vehicleTrailerLookup) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rechargeType" label="充值类型" min-width="100" align="center">
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">充值类型</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="rechargeType"
|
||||
label="充值类型"
|
||||
type="enum"
|
||||
:enum-options="enumOptionsFor('rechargeType')"
|
||||
:is-active="isFilterActive('rechargeType')"
|
||||
:model-value="filters.rechargeType || null"
|
||||
@update:model-value="(v) => setFilterState('rechargeType', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlRechargeTypeTagType(row.rechargeType)" size="small">
|
||||
{{ getPlRechargeTypeLabel(row.rechargeType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applyDate" label="申请日期" min-width="120" align="center">
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">申请日期</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="applyDate"
|
||||
label="申请日期"
|
||||
type="datetime"
|
||||
:is-active="isFilterActive('applyDate')"
|
||||
:model-value="filters.applyDate || null"
|
||||
@update:model-value="(v) => setFilterState('applyDate', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="odometerKm" label="期初公里数" min-width="110" align="center">
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">期初公里数</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="odometerKm"
|
||||
label="期初公里数"
|
||||
type="number"
|
||||
:is-active="isFilterActive('odometerKm')"
|
||||
:model-value="filters.odometerKm || null"
|
||||
@update:model-value="(v) => setFilterState('odometerKm', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">{{ row.odometerKm ?? '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center">
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">申请人</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="applicantName"
|
||||
label="申请人"
|
||||
type="text"
|
||||
:is-active="isFilterActive('applicantName')"
|
||||
:model-value="filters.applicantName || null"
|
||||
@update:model-value="(v) => setFilterState('applicantName', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="attachmentUrls" label="附件" min-width="220" align="left" header-align="left">
|
||||
<template #default="{ row }">
|
||||
<PlPlanDocumentAttachmentCell
|
||||
:attachments="toRechargeAttachmentItems(row.attachmentUrls)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
min-width="160"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">备注</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="remark"
|
||||
label="备注"
|
||||
type="text"
|
||||
:is-active="isFilterActive('remark')"
|
||||
:model-value="filters.remark || null"
|
||||
@update:model-value="(v) => setFilterState('remark', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="currentNodeName" label="当前节点" min-width="120" show-overflow-tooltip>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">当前节点</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="currentNodeName"
|
||||
label="当前节点"
|
||||
type="text"
|
||||
:is-active="isFilterActive('currentNodeName')"
|
||||
:model-value="filters.currentNodeName || null"
|
||||
@update:model-value="(v) => setFilterState('currentNodeName', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" min-width="100" align="center">
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">状态</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="status"
|
||||
label="状态"
|
||||
type="enum"
|
||||
:enum-options="enumOptionsFor('status')"
|
||||
:is-active="isFilterActive('status')"
|
||||
:model-value="filters.status || null"
|
||||
@update:model-value="(v) => setFilterState('status', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlRechargeApplyStatusTagType(row.status)" size="small">
|
||||
{{ getPlRechargeApplyStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center">
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">创建时间</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
type="datetime"
|
||||
:is-active="isFilterActive('createTime')"
|
||||
:model-value="filters.createTime || null"
|
||||
@update:model-value="(v) => setFilterState('createTime', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">{{ formatPlDateTime(row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pager.page"
|
||||
v-model:page-size="pager.pageSize"
|
||||
:total="pager.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewDetail?.applyNo || '申请详情'"
|
||||
:sections="viewSections"
|
||||
width="960px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
>
|
||||
<template v-if="activeTab === 'pending' && viewDetail" #hero-actions>
|
||||
<el-button type="primary" @click="openHandleDialog(viewDetail)">
|
||||
<el-icon><Check /></el-icon>处理
|
||||
</el-button>
|
||||
</template>
|
||||
<template #approval-flow>
|
||||
<BxApprovalFlowTabs
|
||||
section-title="审批记录"
|
||||
:nodes="viewApprovalFlowNodes"
|
||||
:history="viewApprovalHistoryItems"
|
||||
/>
|
||||
</template>
|
||||
<template #apply-attachments>
|
||||
<AttachmentUploadField
|
||||
:model-value="viewDetail?.attachmentUrls || ''"
|
||||
disabled
|
||||
compact
|
||||
/>
|
||||
</template>
|
||||
<template #record-attachments>
|
||||
<AttachmentUploadField
|
||||
:model-value="viewDetail?.record?.attachmentUrls || ''"
|
||||
disabled
|
||||
compact
|
||||
/>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="handleVisible"
|
||||
:title="handleDetail?.applyNo || '审批处理'"
|
||||
width="1280px"
|
||||
align-center
|
||||
class="dialog-form-layout"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form label-position="top" class="dialog-form">
|
||||
<!-- 上方:审批信息 -->
|
||||
<div class="handle-section">
|
||||
<div class="handle-section__title">审批信息</div>
|
||||
<div class="form-grid-4">
|
||||
<el-form-item label="当前节点">
|
||||
<el-input :model-value="handleDetail?.currentNodeName || '—'" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="节点动作">
|
||||
<el-input :model-value="isDisburseNode ? '发放登记' : '审批'" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="isDisburseNode">
|
||||
<el-form-item label="充值日期" required>
|
||||
<el-date-picker
|
||||
v-model="disburseForm.rechargeDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值金额" required>
|
||||
<el-input-number
|
||||
v-model="disburseForm.amount"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="卡号">
|
||||
<el-input v-model="disburseForm.cardNo" placeholder="卡号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="加油站/渠道">
|
||||
<el-select
|
||||
v-model="disburseForm.gasStation"
|
||||
allow-create
|
||||
filterable
|
||||
clearable
|
||||
placeholder="请选择或输入"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="s in PL_GAS_STATION_PRESETS" :key="s" :label="s" :value="s" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-half">
|
||||
<el-input v-model="opinion" type="textarea" :rows="2" placeholder="选填;驳回时请填写原因" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发放附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="disburseAttachmentRef"
|
||||
v-model="disburseForm.attachmentUrls"
|
||||
folder="pl/vehicle-recharge/record"
|
||||
accept="image/*,.pdf"
|
||||
:limit="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item v-else label="备注" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="opinion"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="默认同意;驳回时请说明原因"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 充值申请详情(只读信息元素,对齐查看弹窗) -->
|
||||
<div class="handle-section">
|
||||
<div class="handle-section__title">ETC充值申请详情</div>
|
||||
<ViewKvGrid :fields="handleApplyDetailFields" :columns="4" />
|
||||
<div class="handle-apply-attachments">
|
||||
<div class="handle-apply-attachments__label">附件</div>
|
||||
<AttachmentUploadField
|
||||
:model-value="handleDetail?.attachmentUrls || ''"
|
||||
disabled
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批记录 -->
|
||||
<div class="handle-section">
|
||||
<BxApprovalFlowTabs
|
||||
section-title="审批记录"
|
||||
:nodes="handleApprovalFlowNodes"
|
||||
:history="handleApprovalHistoryItems"
|
||||
/>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="handleVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="danger" class="btn-ghost" :loading="submitting" @click="handleReject">
|
||||
<el-icon><CloseBold /></el-icon>驳回
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!isDisburseNode"
|
||||
type="primary"
|
||||
class="btn-main"
|
||||
:loading="submitting"
|
||||
@click="handleApprove"
|
||||
>
|
||||
<el-icon><Check /></el-icon>通过
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="primary"
|
||||
class="btn-main"
|
||||
:loading="submitting"
|
||||
@click="handleDisburse"
|
||||
>
|
||||
<el-icon><Check /></el-icon>确认发放
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import mittBus from '@/utils/sys/mittBus'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { Check, Close, CloseBold, Filter, View } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import PlPlanDocumentAttachmentCell from '@/components/business/pl-plan-document-attachment-cell/index.vue'
|
||||
import PlPlanTypeTabs from '@/components/business/pl-plan-type-tabs/index.vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import ViewKvGrid from '@/components/business/system-view-dialog/ViewKvGrid.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
|
||||
import { useServerColumnFilters } from '@/composables/useServerColumnFilters'
|
||||
import { plVehicleRechargeApprovalColumnFilterDefs } from '@/views/pl/etc-vehicle-recharge-apply/columnFilterDefs'
|
||||
import {
|
||||
vehicleApi,
|
||||
vehicleRechargeApplyApi,
|
||||
vehicleRechargeApprovalApi,
|
||||
type PlanDocumentAttachmentItem,
|
||||
type Vehicle,
|
||||
type VehicleRechargeApply
|
||||
} from '@/api/pl'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
|
||||
import {
|
||||
buildVehicleTrailerLookup,
|
||||
formatPlDateTime,
|
||||
formatPlVehiclePlateDisplay,
|
||||
getPlRechargeApplyStatusLabel,
|
||||
getPlRechargeApplyStatusTagType,
|
||||
getPlRechargeTypeLabel,
|
||||
getPlRechargeTypeTagType,
|
||||
mapVehicleRechargeApprovalFlowToNodes,
|
||||
PL_GAS_STATION_PRESETS
|
||||
} from '@/utils/pl-display'
|
||||
import { refreshMenuBadges } from '@/utils/menu-badge-registry'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
|
||||
const {
|
||||
filters,
|
||||
hasActiveFilters,
|
||||
setFilterState,
|
||||
clearAllFilters,
|
||||
isFilterActive,
|
||||
toQueryParams
|
||||
} = useServerColumnFilters(plVehicleRechargeApprovalColumnFilterDefs, {
|
||||
onChange: () => {
|
||||
pager.page = 1
|
||||
fetchData()
|
||||
}
|
||||
})
|
||||
|
||||
function enumOptionsFor(prop: string) {
|
||||
const def = plVehicleRechargeApprovalColumnFilterDefs.find((d) => d.prop === prop)
|
||||
return (def?.enumOptions || []).map((o) => ({ ...o, count: 0 }))
|
||||
}
|
||||
|
||||
const APPROVAL_TABS = [
|
||||
{ label: '待我审批', name: 'pending' },
|
||||
{ label: '我已办', name: 'done' }
|
||||
]
|
||||
|
||||
const menuStore = useMenuStore()
|
||||
|
||||
async function refreshListAndBadges() {
|
||||
await fetchData()
|
||||
void refreshMenuBadges(menuStore.menuList)
|
||||
}
|
||||
|
||||
const DEFAULT_APPROVAL_OPINION = '同意'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const activeTab = ref<'pending' | 'done'>('pending')
|
||||
const tableData = ref<VehicleRechargeApply[]>([])
|
||||
const selectedRows = ref<VehicleRechargeApply[]>([])
|
||||
const pager = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const vehicleTrailerLookup = computed(() => buildVehicleTrailerLookup(vehicleOptions.value))
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewDetail = ref<VehicleRechargeApply | null>(null)
|
||||
const handleVisible = ref(false)
|
||||
const handleDetail = ref<VehicleRechargeApply | null>(null)
|
||||
let viewRequestId = 0
|
||||
let handleRequestId = 0
|
||||
watch(viewVisible, (visible) => {
|
||||
if (!visible) viewRequestId += 1
|
||||
}, { flush: 'sync' })
|
||||
watch(handleVisible, (visible) => {
|
||||
if (!visible) handleRequestId += 1
|
||||
}, { flush: 'sync' })
|
||||
const opinion = ref(DEFAULT_APPROVAL_OPINION)
|
||||
const disburseForm = reactive({
|
||||
rechargeDate: '',
|
||||
amount: null as number | null,
|
||||
cardNo: '',
|
||||
gasStation: '',
|
||||
attachmentUrls: ''
|
||||
})
|
||||
const disburseAttachmentRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const canApproveSelected = computed(
|
||||
() => activeTab.value === 'pending' && selectedRows.value.length === 1
|
||||
)
|
||||
const isDisburseNode = computed(
|
||||
() => (handleDetail.value?.currentNodeActionType || 'approve') === 'disburse'
|
||||
)
|
||||
|
||||
const viewApprovalFlowNodes = computed(() =>
|
||||
mapVehicleRechargeApprovalFlowToNodes(viewDetail.value?.approvalFlow || [])
|
||||
)
|
||||
const viewApprovalHistoryItems = computed(() =>
|
||||
mapVehicleRechargeApprovalFlowToNodes(viewDetail.value?.approvalHistory || [], {
|
||||
withRoundPrefix: true
|
||||
})
|
||||
)
|
||||
|
||||
const handleApprovalFlowNodes = computed(() =>
|
||||
mapVehicleRechargeApprovalFlowToNodes(handleDetail.value?.approvalFlow || [])
|
||||
)
|
||||
const handleApprovalHistoryItems = computed(() =>
|
||||
mapVehicleRechargeApprovalFlowToNodes(handleDetail.value?.approvalHistory || [], {
|
||||
withRoundPrefix: true
|
||||
})
|
||||
)
|
||||
|
||||
const handleApplyDetailFields = computed((): ViewField[] => {
|
||||
const d = handleDetail.value
|
||||
if (!d) return []
|
||||
return [
|
||||
{ label: '申请单号', value: d.applyNo },
|
||||
{ label: '申请人', value: d.applicantName || '—' },
|
||||
{
|
||||
label: '车牌号',
|
||||
value: formatPlVehiclePlateDisplay(d.licensePlate, undefined, vehicleTrailerLookup.value)
|
||||
},
|
||||
{
|
||||
label: '充值类型',
|
||||
value: getPlRechargeTypeLabel(d.rechargeType),
|
||||
tagType: getPlRechargeTypeTagType(d.rechargeType)
|
||||
},
|
||||
{ label: '申请日期', value: d.applyDate },
|
||||
{ label: '期初公里数', value: d.odometerKm != null ? String(d.odometerKm) : '—' },
|
||||
{
|
||||
label: '状态',
|
||||
value: getPlRechargeApplyStatusLabel(d.status),
|
||||
tagType: getPlRechargeApplyStatusTagType(d.status)
|
||||
},
|
||||
{ label: '创建时间', value: formatPlDateTime(d.createTime) },
|
||||
{ label: '备注', value: d.remark || '—', span: true }
|
||||
]
|
||||
})
|
||||
|
||||
function toRechargeAttachmentItems(raw?: string | null): PlanDocumentAttachmentItem[] {
|
||||
return String(raw || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.map((url, index) => ({
|
||||
url,
|
||||
label: `附件${index + 1}`
|
||||
}))
|
||||
}
|
||||
|
||||
function resetOpinionForDetail(detail: VehicleRechargeApply | null) {
|
||||
const disburse = (detail?.currentNodeActionType || 'approve') === 'disburse'
|
||||
opinion.value = disburse ? '' : DEFAULT_APPROVAL_OPINION
|
||||
}
|
||||
|
||||
const viewSections = computed(() => {
|
||||
const d = viewDetail.value
|
||||
if (!d) return []
|
||||
const record = d.record
|
||||
const sections: Array<Record<string, unknown>> = [
|
||||
{
|
||||
title: '申请信息',
|
||||
fields: [
|
||||
{ label: '申请单号', value: d.applyNo },
|
||||
{
|
||||
label: '车牌号',
|
||||
value: formatPlVehiclePlateDisplay(d.licensePlate, undefined, vehicleTrailerLookup.value)
|
||||
},
|
||||
{
|
||||
label: '充值类型',
|
||||
value: getPlRechargeTypeLabel(d.rechargeType),
|
||||
tagType: getPlRechargeTypeTagType(d.rechargeType)
|
||||
},
|
||||
{ label: '申请日期', value: d.applyDate },
|
||||
{ label: '期初公里数', value: d.odometerKm != null ? String(d.odometerKm) : '—' },
|
||||
{ label: '申请人', value: d.applicantName || '—' },
|
||||
{
|
||||
label: '状态',
|
||||
value: getPlRechargeApplyStatusLabel(d.status),
|
||||
tagType: getPlRechargeApplyStatusTagType(d.status)
|
||||
},
|
||||
{ label: '当前节点', value: d.currentNodeName || '—' },
|
||||
{ label: '备注', value: d.remark || '—', span: 2 as const }
|
||||
]
|
||||
},
|
||||
{ title: '申请附件', fields: [], slot: 'apply-attachments' },
|
||||
{ title: '', fields: [], slot: 'approval-flow' }
|
||||
]
|
||||
if (record) {
|
||||
sections.push({
|
||||
title: '发放信息',
|
||||
fields: [
|
||||
{ label: '充值订单号', value: record.rechargeOrderNo || '—', long: false },
|
||||
{ label: '充值日期', value: record.rechargeDate || '—' },
|
||||
{
|
||||
label: '充值金额',
|
||||
value: record.amount != null ? `${Number(record.amount).toFixed(2)} 元` : '—'
|
||||
},
|
||||
{ label: '卡号', value: record.cardNo || '—', long: false, fullSpan: false },
|
||||
{ label: '加油站/渠道', value: record.gasStation || '—' },
|
||||
{ label: '发放人', value: record.createByName || '—' },
|
||||
{ label: '备注', value: record.remark || '—', span: true }
|
||||
],
|
||||
slotTitle: '发放附件',
|
||||
slot: 'record-attachments'
|
||||
})
|
||||
} else {
|
||||
sections.push({
|
||||
title: '发放信息',
|
||||
fields: [{ label: '发放信息', value: '暂无', span: true }]
|
||||
})
|
||||
}
|
||||
return sections
|
||||
})
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
try {
|
||||
vehicleOptions.value = await fetchAllPagedRecords<Vehicle>(async (page, pageSize) => {
|
||||
const res = await vehicleApi.list({ page, pageSize })
|
||||
return pickBxPageResult<Vehicle>(res)
|
||||
})
|
||||
} catch {
|
||||
vehicleOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const api = activeTab.value === 'pending' ? vehicleRechargeApprovalApi.pending : vehicleRechargeApprovalApi.done
|
||||
const params: Record<string, any> = {
|
||||
page: pager.page,
|
||||
pageSize: pager.pageSize,
|
||||
...toQueryParams()
|
||||
}
|
||||
delete params.rechargeTypeNe
|
||||
delete params.rechargeTypeIn
|
||||
params.rechargeType = 'etc'
|
||||
const res = await api(params)
|
||||
const data: any = (res as any)?.data ?? res
|
||||
tableData.value = data?.items || []
|
||||
pager.total = data?.total || 0
|
||||
mittBus.emit('rechargeSummaryRefresh')
|
||||
selectedRows.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleTabChange(name: string) {
|
||||
activeTab.value = name === 'done' ? 'done' : 'pending'
|
||||
pager.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
async function openView(row: VehicleRechargeApply) {
|
||||
const requestId = ++viewRequestId
|
||||
viewDetail.value = row
|
||||
viewVisible.value = true
|
||||
try {
|
||||
const res = await vehicleRechargeApplyApi.get(row.id)
|
||||
if (requestId === viewRequestId && viewVisible.value) viewDetail.value = (res as any)?.data ?? res
|
||||
} catch {
|
||||
// keep list row as fallback
|
||||
}
|
||||
}
|
||||
|
||||
async function openHandleDialog(row: VehicleRechargeApply) {
|
||||
const requestId = ++handleRequestId
|
||||
handleDetail.value = row
|
||||
resetOpinionForDetail(row)
|
||||
disburseForm.rechargeDate = new Date().toISOString().slice(0, 10)
|
||||
disburseForm.amount = null
|
||||
disburseForm.cardNo = ''
|
||||
disburseForm.gasStation = ''
|
||||
disburseForm.attachmentUrls = ''
|
||||
handleVisible.value = true
|
||||
try {
|
||||
const res = await vehicleRechargeApplyApi.get(row.id)
|
||||
if (requestId !== handleRequestId || !handleVisible.value) return
|
||||
handleDetail.value = (res as any)?.data ?? res
|
||||
resetOpinionForDetail(handleDetail.value)
|
||||
} catch {
|
||||
// keep list row as fallback
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApprove() {
|
||||
if (!handleDetail.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
await vehicleRechargeApprovalApi.approve(handleDetail.value.id, {
|
||||
opinion: opinion.value.trim() || DEFAULT_APPROVAL_OPINION
|
||||
})
|
||||
ElMessage.success('已通过')
|
||||
handleVisible.value = false
|
||||
viewVisible.value = false
|
||||
await refreshListAndBadges()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReject() {
|
||||
if (!handleDetail.value) return
|
||||
const text = opinion.value.trim()
|
||||
if (!text || text === DEFAULT_APPROVAL_OPINION) {
|
||||
ElMessage.warning('驳回时请填写备注')
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定驳回该申请吗?', '提示', { type: 'warning' })
|
||||
submitting.value = true
|
||||
try {
|
||||
await vehicleRechargeApprovalApi.reject(handleDetail.value.id, { opinion: text })
|
||||
ElMessage.success('已驳回')
|
||||
handleVisible.value = false
|
||||
viewVisible.value = false
|
||||
await refreshListAndBadges()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDisburse() {
|
||||
if (!handleDetail.value) return
|
||||
if (!disburseForm.rechargeDate || !disburseForm.amount || disburseForm.amount <= 0) {
|
||||
ElMessage.warning('请填写充值日期和金额')
|
||||
return
|
||||
}
|
||||
if (disburseAttachmentRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await vehicleRechargeApprovalApi.disburse(handleDetail.value.id, {
|
||||
applyId: handleDetail.value.id,
|
||||
licensePlate: handleDetail.value.licensePlate,
|
||||
rechargeType: 'etc',
|
||||
rechargeDate: disburseForm.rechargeDate,
|
||||
amount: disburseForm.amount,
|
||||
cardNo: disburseForm.cardNo || undefined,
|
||||
gasStation: disburseForm.gasStation || undefined,
|
||||
attachmentUrls: disburseForm.attachmentUrls || undefined
|
||||
})
|
||||
ElMessage.success('发放登记成功')
|
||||
handleVisible.value = false
|
||||
viewVisible.value = false
|
||||
await refreshListAndBadges()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadVehicleOptions()
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.handle-section {
|
||||
& + & {
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #1e293b);
|
||||
line-height: 1.4;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
margin-right: 8px;
|
||||
vertical-align: -2px;
|
||||
border-radius: 2px;
|
||||
background: var(--primary, #10b981);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.handle-apply-attachments {
|
||||
margin-top: 14px;
|
||||
|
||||
&__label {
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
/* 四列栅格中占两列(如:加油站右侧的备注) */
|
||||
.form-grid-4 > .form-col-span-half {
|
||||
grid-column: span 2;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,663 @@
|
||||
<!-- ETC充值审批人设置 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="wf-info-bar">
|
||||
<span><b>流程名称:</b>{{ wfDef.wfName || '-' }}</span>
|
||||
<span>
|
||||
<b>状态:</b>
|
||||
<el-tag :type="statusTagType(wfDef.status)" size="small">
|
||||
{{ statusLabel(wfDef.status) }}
|
||||
</el-tag>
|
||||
</span>
|
||||
<span class="form-tip">ETC 与油卡共用同一套审批人配置</span>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<ListQuickSearchInput
|
||||
v-model="quickSearchKeyword"
|
||||
placeholder="节点名称/审批人"
|
||||
@search="handleQuickSearch"
|
||||
@clear="handleQuickSearchClear"
|
||||
/>
|
||||
<el-button class="btn-main" type="primary" @click="openAddDialog">
|
||||
<el-icon><Plus /></el-icon>新增节点
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
@click="handleToolbarEdit"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length !== 1 || !canDeleteByCreator(selectedRows[0])"
|
||||
:title="selectedRows.length === 1 && !canDeleteByCreator(selectedRows[0]) ? CREATOR_DELETE_TIP : ''"
|
||||
:loading="loadingMap.rowDelete"
|
||||
@click="handleToolbarDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="selectedRows.length !== 1" @click="handleToolbarMoveUp">
|
||||
<el-icon><ArrowUp /></el-icon>上移
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="selectedRows.length !== 1" @click="handleToolbarMoveDown">
|
||||
<el-icon><ArrowDown /></el-icon>下移
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="loadingMap.save" @click="saveNodes">
|
||||
<el-icon><Upload /></el-icon>保存配置
|
||||
</el-button>
|
||||
|
||||
<ListToolbarEndActions
|
||||
:loading="loading"
|
||||
:column-defs="listColumnDefs"
|
||||
v-model:visible-columns="visibleColumns"
|
||||
v-model:check-all="checkAll"
|
||||
:is-indeterminate="isIndeterminate"
|
||||
@refresh="loadWfDef"
|
||||
@reset-columns="resetToDefault"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="list-table-viewport-anchor">
|
||||
<ListTable
|
||||
table-key="pl-etc-vehicle-recharge-workflow"
|
||||
class="list-table"
|
||||
:data="filteredNodes"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openEditDialog"
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" header-align="center" />
|
||||
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
prop="nodeName"
|
||||
label="节点名称"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">节点名称</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="nodeName"
|
||||
label="节点名称"
|
||||
type="text"
|
||||
:is-active="isFilterActive('nodeName')"
|
||||
:model-value="filters.nodeName || null"
|
||||
@update:model-value="(v) => setFilterState('nodeName', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="actionType"
|
||||
label="节点动作"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">节点动作</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="actionType"
|
||||
label="节点动作"
|
||||
type="enum"
|
||||
:enum-options="collectUniqueOptions('actionType')"
|
||||
:is-active="isFilterActive('actionType')"
|
||||
:model-value="filters.actionType || null"
|
||||
@update:model-value="(v) => setFilterState('actionType', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="(row.actionType || row.action_type || 'approve') === 'disburse' ? 'success' : 'primary'"
|
||||
size="small"
|
||||
>
|
||||
{{ (row.actionType || row.action_type || 'approve') === 'disburse' ? '发放登记' : '审批' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('nodeOrder')"
|
||||
prop="nodeOrder"
|
||||
label="节点顺序"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">节点顺序</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="nodeOrder"
|
||||
label="节点顺序"
|
||||
type="number"
|
||||
:is-active="isFilterActive('nodeOrder')"
|
||||
:model-value="filters.nodeOrder || null"
|
||||
@update:model-value="(v) => setFilterState('nodeOrder', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('approverName')"
|
||||
prop="approverName"
|
||||
label="审批人"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">审批人</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="approverName"
|
||||
label="审批人"
|
||||
type="text"
|
||||
:is-active="isFilterActive('approverName')"
|
||||
:model-value="filters.approverName || null"
|
||||
@update:model-value="(v) => setFilterState('approverName', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('configStatus')"
|
||||
prop="configStatus"
|
||||
label="配置状态"
|
||||
min-width="110"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #header>
|
||||
<div class="col-header-with-filter">
|
||||
<span class="col-header-with-filter__label">配置状态</span>
|
||||
<ColumnHeaderFilter
|
||||
prop="configStatus"
|
||||
label="配置状态"
|
||||
type="enum"
|
||||
:enum-options="collectUniqueOptions('configStatus')"
|
||||
:is-active="isFilterActive('configStatus')"
|
||||
:model-value="filters.configStatus || null"
|
||||
@update:model-value="(v) => setFilterState('configStatus', v)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="(row.approverId ?? row.approver_id) ? 'success' : 'warning'" size="small">
|
||||
{{ (row.approverId ?? row.approver_id) ? '已配置' : '未配置' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</ListTable>
|
||||
</div>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<div class="list-pager-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogs.addNode"
|
||||
title="新增节点"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="addFormRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
class="bx-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="节点名称" prop="nodeName">
|
||||
<el-input v-model="formData.nodeName" placeholder="请输入节点名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="节点顺序">
|
||||
<el-input-number v-model="formData.nodeOrder" controls-position="right" :min="1" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="节点动作" prop="actionType">
|
||||
<el-select v-model="formData.actionType" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="审批" value="approve" />
|
||||
<el-option label="发放登记" value="disburse" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="审批人" style="grid-column: 1 / -1">
|
||||
<el-select
|
||||
v-model="formData.approverId"
|
||||
placeholder="请选择审批人"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="handleApproverChange"
|
||||
>
|
||||
<el-option v-for="u in userOptions" :key="u.id" :label="u.displayName" :value="u.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dialogs.addNode = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loadingMap.addNodeSubmit" @click="submitAddNode">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogs.editNode"
|
||||
title="编辑节点"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="editFormRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
class="bx-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="节点名称" prop="nodeName">
|
||||
<el-input v-model="formData.nodeName" placeholder="请输入节点名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="节点顺序">
|
||||
<el-input-number v-model="formData.nodeOrder" controls-position="right" :min="1" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="节点动作" prop="actionType">
|
||||
<el-select v-model="formData.actionType" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="审批" value="approve" />
|
||||
<el-option label="发放登记" value="disburse" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="审批人" style="grid-column: 1 / -1">
|
||||
<el-select
|
||||
v-model="formData.approverId"
|
||||
placeholder="请选择审批人"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="handleApproverChange"
|
||||
>
|
||||
<el-option v-for="u in userOptions" :key="u.id" :label="u.displayName" :value="u.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dialogs.editNode = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loadingMap.editNodeSubmit" @click="submitEditNode">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import { useClientColumnFilters } from '@/composables/useClientColumnFilters'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
EditPen,
|
||||
Delete,
|
||||
Close,
|
||||
Check,
|
||||
Upload,
|
||||
ArrowUp,
|
||||
ArrowDown
|
||||
} from '@element-plus/icons-vue'
|
||||
import { workflowApi } from '@/api/bx'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import { statusLabel, statusTagType } from '@/utils/bx/format'
|
||||
import { plVehicleRechargeWorkflowColumnFilterDefs } from '@/views/pl/etc-vehicle-recharge-apply/columnFilterDefs'
|
||||
|
||||
type DialogKey = 'addNode' | 'editNode'
|
||||
|
||||
const loadingMap = reactive<Record<string, boolean>>({})
|
||||
const dialogs = reactive<Record<DialogKey, boolean>>({
|
||||
addNode: false,
|
||||
editNode: false
|
||||
})
|
||||
|
||||
const quickSearchKeyword = ref('')
|
||||
const appliedQuickSearch = ref('')
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'nodeOrder', label: '节点顺序' },
|
||||
{ prop: 'approverName', label: '审批人' },
|
||||
{ prop: 'configStatus', label: '配置状态' }
|
||||
]
|
||||
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('pl-etc-vehicle-recharge-workflow-list-columns', listColumnDefs)
|
||||
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref<Record<string, any>[]>([])
|
||||
const wfDef = ref<Record<string, any>>({})
|
||||
const allNodes = computed(() => wfDef.value.nodes || [])
|
||||
const quickFilteredNodes = computed(() => {
|
||||
const kw = appliedQuickSearch.value.trim().toLowerCase()
|
||||
if (!kw) return allNodes.value
|
||||
return allNodes.value.filter(
|
||||
(n: any) =>
|
||||
String(n.nodeName || '')
|
||||
.toLowerCase()
|
||||
.includes(kw) ||
|
||||
String(n.approverName || '')
|
||||
.toLowerCase()
|
||||
.includes(kw)
|
||||
)
|
||||
})
|
||||
const {
|
||||
filters,
|
||||
filteredRows: filteredNodes,
|
||||
setFilterState,
|
||||
isFilterActive,
|
||||
collectUniqueOptions
|
||||
} = useClientColumnFilters(quickFilteredNodes, plVehicleRechargeWorkflowColumnFilterDefs)
|
||||
const nodes = allNodes
|
||||
const userOptions = ref<
|
||||
{ id: string; realName: string; username: string; roles: string; displayName: string }[]
|
||||
>([])
|
||||
|
||||
const formData = reactive({
|
||||
nodeName: '',
|
||||
nodeOrder: 1,
|
||||
actionType: 'approve' as 'approve' | 'disburse',
|
||||
approverId: null as string | null,
|
||||
approverName: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
nodeName: [{ required: true, message: '节点名称不能为空', trigger: 'blur' }],
|
||||
actionType: [{ required: true, message: '请选择节点动作', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const addFormRef = ref<FormInstance>()
|
||||
const editFormRef = ref<FormInstance>()
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const data: any = (res as any)?.data ?? res ?? []
|
||||
userOptions.value = (Array.isArray(data) ? data : []).map((u: any) => {
|
||||
const name = u.label || u.nickName || ''
|
||||
const account = u.userNo || ''
|
||||
return {
|
||||
id: String(u.value ?? u.id ?? ''),
|
||||
realName: name,
|
||||
username: account,
|
||||
roles: '',
|
||||
displayName: account ? `${name}(${account})` : name
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
userOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWfDef() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await workflowApi.getByBizType('vehicle_recharge')
|
||||
wfDef.value = res.data ?? {}
|
||||
} catch {
|
||||
wfDef.value = {}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadWfDef()
|
||||
loadUsers()
|
||||
})
|
||||
|
||||
function onSelectionChange(rows: Record<string, any>[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function handleQuickSearch() {
|
||||
appliedQuickSearch.value = quickSearchKeyword.value.trim()
|
||||
}
|
||||
|
||||
function handleQuickSearchClear() {
|
||||
quickSearchKeyword.value = ''
|
||||
appliedQuickSearch.value = ''
|
||||
}
|
||||
|
||||
function handleToolbarEdit() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请先勾选一条节点')
|
||||
return
|
||||
}
|
||||
openEditDialog(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleToolbarDelete() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请先勾选一条节点')
|
||||
return
|
||||
}
|
||||
deleteNode(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleToolbarMoveUp() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
moveUp(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleToolbarMoveDown() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
moveDown(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function buildNodePayload(list: Record<string, any>[]) {
|
||||
return list.map((n, i) => ({
|
||||
id: n.id != null ? String(n.id) : undefined,
|
||||
nodeName: n.nodeName,
|
||||
nodeOrder: i + 1,
|
||||
actionType: n.actionType ?? n.action_type ?? 'approve',
|
||||
approverId: n.approverId ?? n.approver_id ?? null,
|
||||
approverName: n.approverName ?? n.approver_name ?? ''
|
||||
}))
|
||||
}
|
||||
|
||||
async function saveNodes() {
|
||||
loadingMap.save = true
|
||||
try {
|
||||
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(nodes.value))
|
||||
ElMessage.success('保存成功')
|
||||
await loadWfDef()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '保存失败')
|
||||
} finally {
|
||||
loadingMap.save = false
|
||||
}
|
||||
}
|
||||
|
||||
function openAddDialog() {
|
||||
Object.assign(formData, {
|
||||
nodeName: '',
|
||||
nodeOrder: nodes.value.length + 1,
|
||||
actionType: 'approve',
|
||||
approverId: null,
|
||||
approverName: ''
|
||||
})
|
||||
dialogs.addNode = true
|
||||
}
|
||||
|
||||
function openEditDialog(node: Record<string, any>) {
|
||||
Object.assign(formData, {
|
||||
nodeName: node.nodeName,
|
||||
nodeOrder: node.nodeOrder,
|
||||
actionType: (node.actionType ?? node.action_type ?? 'approve') as 'approve' | 'disburse',
|
||||
approverId: node.approverId ?? node.approver_id ?? null,
|
||||
approverName: node.approverName ?? node.approver_name ?? ''
|
||||
})
|
||||
dialogs.editNode = true
|
||||
}
|
||||
|
||||
function handleApproverChange(userId: string | null) {
|
||||
if (userId == null) {
|
||||
formData.approverName = ''
|
||||
return
|
||||
}
|
||||
const found = userOptions.value.find((u) => u.id === userId)
|
||||
formData.approverName = found ? found.realName : ''
|
||||
}
|
||||
|
||||
async function submitAddNode() {
|
||||
const valid = await addFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
loadingMap.addNodeSubmit = true
|
||||
try {
|
||||
const newNodes = [
|
||||
...nodes.value,
|
||||
{
|
||||
nodeName: formData.nodeName,
|
||||
nodeOrder: formData.nodeOrder,
|
||||
actionType: formData.actionType,
|
||||
approverId: formData.approverId,
|
||||
approverName: formData.approverName
|
||||
}
|
||||
]
|
||||
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(newNodes))
|
||||
ElMessage.success('新增成功')
|
||||
dialogs.addNode = false
|
||||
await loadWfDef()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '新增失败')
|
||||
} finally {
|
||||
loadingMap.addNodeSubmit = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitEditNode() {
|
||||
const valid = await editFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
loadingMap.editNodeSubmit = true
|
||||
try {
|
||||
const newNodes = nodes.value.map((n: Record<string, any>) =>
|
||||
n.nodeOrder === formData.nodeOrder
|
||||
? {
|
||||
...n,
|
||||
nodeName: formData.nodeName,
|
||||
actionType: formData.actionType,
|
||||
approverId: formData.approverId,
|
||||
approverName: formData.approverName
|
||||
}
|
||||
: n
|
||||
)
|
||||
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(newNodes))
|
||||
ElMessage.success('编辑成功')
|
||||
dialogs.editNode = false
|
||||
await loadWfDef()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '编辑失败')
|
||||
} finally {
|
||||
loadingMap.editNodeSubmit = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNode(node: Record<string, any>) {
|
||||
if (!canDeleteByCreator(node)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确定要删除节点「${node.nodeName}」吗?`, '删除确认', {
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
loadingMap.rowDelete = true
|
||||
try {
|
||||
const newNodes = nodes.value.filter((n: Record<string, any>) => n.id !== node.id)
|
||||
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(newNodes))
|
||||
ElMessage.success('删除成功')
|
||||
await loadWfDef()
|
||||
} catch (e: any) {
|
||||
if (e !== 'cancel') ElMessage.error(e.message || '删除失败')
|
||||
} finally {
|
||||
loadingMap.rowDelete = false
|
||||
}
|
||||
}
|
||||
|
||||
async function moveUp(node: Record<string, any>) {
|
||||
const idx = nodes.value.findIndex((n: Record<string, any>) => n.id === node.id)
|
||||
if (idx <= 0) return
|
||||
const arr = [...nodes.value]
|
||||
const temp = arr[idx]
|
||||
arr[idx] = arr[idx - 1]
|
||||
arr[idx - 1] = temp
|
||||
arr.forEach((n: Record<string, any>, i: number) => (n.nodeOrder = i + 1))
|
||||
try {
|
||||
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(arr))
|
||||
ElMessage.success('上移成功')
|
||||
await loadWfDef()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '上移失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function moveDown(node: Record<string, any>) {
|
||||
const arr = [...nodes.value]
|
||||
const idx = arr.findIndex((n: Record<string, any>) => n.id === node.id)
|
||||
if (idx < 0 || idx >= arr.length - 1) return
|
||||
const temp = arr[idx]
|
||||
arr[idx] = arr[idx + 1]
|
||||
arr[idx + 1] = temp
|
||||
arr.forEach((n: Record<string, any>, i: number) => (n.nodeOrder = i + 1))
|
||||
try {
|
||||
await workflowApi.saveNodes(wfDef.value.id, buildNodePayload(arr))
|
||||
ElMessage.success('下移成功')
|
||||
await loadWfDef()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || '下移失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in new issue
Block a user