feat(web-admin): add dedicated ETC vehicle recharge pages

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-12 19:03:00 +08:00
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>