Pārlūkot izejas kodu

feat: add vehicle recharge approval workflow and pending UI

Wire configurable workflow config, approval todo with approve/reject/disburse, apply submit and history, and block disburse board for in-approval rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 1 mēnesi atpakaļ
vecāks
revīzija
7b99040f9c

+ 44 - 2
web-admin/src/api/pl/index.ts

@@ -1687,6 +1687,19 @@ export const menuBadgeApi = {
 // ETC/加油充值 API
 // =============================================================================
 
+export interface VehicleRechargeApproval {
+  id: string
+  applyId: string
+  approvalRound: number
+  nodeId?: string
+  nodeName?: string
+  approverId: string
+  approverName: string
+  approvalStatus: 'approved' | 'rejected' | 'disbursed' | string
+  approvalOpinion?: string
+  approvalTime: string
+}
+
 export interface VehicleRechargeApply {
   id: string
   applyNo: string
@@ -1697,7 +1710,14 @@ export interface VehicleRechargeApply {
   attachmentUrls?: string
   applicantId: string
   applicantName?: string
-  status: 'pending' | 'disbursed' | 'cancelled'
+  status: 'draft' | 'pending' | 'in_approval' | 'rejected' | 'disbursed' | 'cancelled' | string
+  wfId?: string
+  currentNode?: string
+  currentNodeName?: string
+  currentNodeActionType?: 'approve' | 'disburse' | string
+  approvalRound?: number
+  approvals?: VehicleRechargeApproval[]
+  record?: VehicleRechargeRecord | null
   remark?: string
   createTime?: string
   updateTime?: string
@@ -1751,7 +1771,7 @@ export interface VehicleRechargeDisburseBoardItem {
   applyDate: string
   odometerKm?: number | null
   applicantName?: string
-  applyStatus: 'pending' | 'disbursed' | 'cancelled'
+  applyStatus: 'draft' | 'pending' | 'in_approval' | 'rejected' | 'disbursed' | 'cancelled' | string
   applyRemark?: string
   recordId?: string
   rechargeOrderNo?: string
@@ -1780,6 +1800,8 @@ export const vehicleRechargeApplyApi = {
     request.post<VehicleRechargeApply>('/transport/vehicle-recharge-apply/', data),
   update: (id: string, data: Partial<VehicleRechargeApply>) =>
     request.put<VehicleRechargeApply>(`/transport/vehicle-recharge-apply/${id}`, data),
+  submit: (id: string) =>
+    request.post<VehicleRechargeApply>(`/transport/vehicle-recharge-apply/${id}/submit`),
   cancel: (id: string) =>
     request.post<VehicleRechargeApply>(`/transport/vehicle-recharge-apply/${id}/cancel`),
   delete: (ids: string) => request.delete(`/transport/vehicle-recharge-apply/${ids}`),
@@ -1787,6 +1809,26 @@ export const vehicleRechargeApplyApi = {
     request.get<VehicleRechargeApplyOption[]>('/transport/vehicle-recharge-apply/pending-options')
 }
 
+export const vehicleRechargeApprovalApi = {
+  pending: (params?: Record<string, unknown>) =>
+    request.get<VehicleRechargeApplyListResponse>('/transport/vehicle-recharge-approval/pending', {
+      params
+    }),
+  done: (params?: Record<string, unknown>) =>
+    request.get<VehicleRechargeApplyListResponse>('/transport/vehicle-recharge-approval/done', {
+      params
+    }),
+  approve: (id: string, data?: { opinion?: string }) =>
+    request.post<VehicleRechargeApply>(
+      `/transport/vehicle-recharge-approval/${id}/approve`,
+      data || {}
+    ),
+  reject: (id: string, data: { opinion?: string }) =>
+    request.post<VehicleRechargeApply>(`/transport/vehicle-recharge-approval/${id}/reject`, data),
+  disburse: (id: string, data: Partial<VehicleRechargeRecord>) =>
+    request.post<VehicleRechargeApply>(`/transport/vehicle-recharge-approval/${id}/disburse`, data)
+}
+
 export const vehicleRechargeRecordApi = {
   disburseBoard: (params?: Record<string, unknown>) =>
     request.get<VehicleRechargeDisburseBoardListResponse>(

+ 28 - 0
web-admin/src/router/modules/pl.ts

@@ -119,6 +119,13 @@ export const plRoutes: AppRouteRecord[] = [
             component: '/pl/vehicle-recharge-apply/index',
             meta: { title: 'ETC/加油充值申请', keepAlive: false, menuCode: 'pl:vehicle-recharge-apply:list' }
           },
+          {
+            id: 'PL_VEH_RECHARGE_APPROVAL',
+            name: 'PlVehicleRechargeApproval',
+            path: 'vehicle-recharge-approval',
+            component: '/pl/vehicle-recharge-approval/index',
+            meta: { title: '充值审批', keepAlive: false, menuCode: 'pl:vehicle-recharge-approval:list' }
+          },
           {
             id: 'PL_VEH_RECHARGE_RECORD',
             name: 'PlVehicleRechargeRecord',
@@ -126,6 +133,13 @@ export const plRoutes: AppRouteRecord[] = [
             component: '/pl/vehicle-recharge-record/index',
             meta: { title: 'ETC/加油充值发放登记', keepAlive: false, menuCode: 'pl:vehicle-recharge-record:list' }
           },
+          {
+            id: 'PL_VEH_RECHARGE_WF',
+            name: 'PlVehicleRechargeWorkflow',
+            path: 'vehicle-recharge-workflow',
+            component: '/pl/vehicle-recharge-workflow/index',
+            meta: { title: '充值审批流程', keepAlive: false, menuCode: 'pl:vehicle-recharge-workflow:list' }
+          },
           {
             id: 'PL_VEH_MAINT_APPLY',
             name: 'PlVehicleMaintenanceApply',
@@ -412,6 +426,20 @@ export const plRoutes: AppRouteRecord[] = [
         redirect: '/pl/vehicle-mgmt/vehicle-recharge-record',
         meta: { title: 'ETC/加油充值发放登记', isHide: true }
       },
+      {
+        id: 'PL_REDIRECT_VEH_RECHARGE_APPROVAL',
+        name: 'PlVehicleRechargeApprovalRedirect',
+        path: 'vehicle-recharge-approval',
+        redirect: '/pl/vehicle-mgmt/vehicle-recharge-approval',
+        meta: { title: '充值审批', isHide: true }
+      },
+      {
+        id: 'PL_REDIRECT_VEH_RECHARGE_WORKFLOW',
+        name: 'PlVehicleRechargeWorkflowRedirect',
+        path: 'vehicle-recharge-workflow',
+        redirect: '/pl/vehicle-mgmt/vehicle-recharge-workflow',
+        meta: { title: '充值审批流程配置', isHide: true }
+      },
       {
         id: 'PL_REDIRECT_VEH_MAINT_APPLY',
         name: 'PlVehicleMaintenanceApplyRedirect',

+ 24 - 0
web-admin/src/utils/pl-display.ts

@@ -637,7 +637,10 @@ export function buildFuelRegisterCopyText(row: {
 
 export function getPlRechargeApplyStatusLabel(status: string) {
   const map: Record<string, string> = {
+    draft: '草稿',
     pending: '待发放',
+    in_approval: '审批中',
+    rejected: '已驳回',
     disbursed: '已发放',
     cancelled: '已作废'
   }
@@ -646,13 +649,34 @@ export function getPlRechargeApplyStatusLabel(status: string) {
 
 export function getPlRechargeApplyStatusTagType(status: string) {
   const map: Record<string, string> = {
+    draft: 'info',
     pending: 'warning',
+    in_approval: 'warning',
+    rejected: 'danger',
     disbursed: 'success',
     cancelled: 'info'
   }
   return map[status] || 'info'
 }
 
+export function getPlRechargeApprovalStatusLabel(status: string) {
+  const map: Record<string, string> = {
+    approved: '通过',
+    rejected: '驳回',
+    disbursed: '已发放'
+  }
+  return map[status] || status
+}
+
+export function getPlRechargeApprovalStatusTagType(status: string) {
+  const map: Record<string, string> = {
+    approved: 'success',
+    rejected: 'danger',
+    disbursed: 'primary'
+  }
+  return map[status] || 'info'
+}
+
 export const PL_GAS_STATION_PRESETS = ['中石化', '线上etc-app', '百链', '现金转账', '其他']
 
 export const PL_MAINTENANCE_TYPE_PRESETS = ['保养', '维修', '年检']

+ 2 - 0
web-admin/src/utils/router/menu-icon.ts

@@ -145,7 +145,9 @@ export const SIDEBAR_MENU_ICON_BY_CODE: Record<string, string> = {
   'pl:freight-disburse:list': 'ri:bank-card-line',
   'pl:transport-address:list': 'ri:map-pin-line',
   'pl:vehicle-recharge-apply:list': 'ri:gas-station-line',
+  'pl:vehicle-recharge-approval:list': 'ri:checkbox-circle-line',
   'pl:vehicle-recharge-record:list': 'ri:hand-coin-line',
+  'pl:vehicle-recharge-workflow:list': 'ri:flow-chart',
   'pl:vehicle-maintenance-apply:list': 'ri:tools-line',
   'pl:vehicle-maintenance-process:list': 'ri:settings-3-line',
   'pl:vehicle-urea-apply:list': 'ri:drop-line',

+ 3 - 1
web-admin/src/views/pl/vehicle-recharge-apply/columnFilterDefs.ts

@@ -6,7 +6,9 @@ const RECHARGE_TYPE_OPTIONS = [
 ]
 
 const RECHARGE_STATUS_OPTIONS = [
-  { value: 'pending', label: '待发放' },
+  { value: 'draft', label: '草稿' },
+  { value: 'in_approval', label: '审批中' },
+  { value: 'rejected', label: '已驳回' },
   { value: 'disbursed', label: '已发放' },
   { value: 'cancelled', label: '已作废' }
 ]

+ 80 - 5
web-admin/src/views/pl/vehicle-recharge-apply/index.vue

@@ -20,7 +20,19 @@
           </el-button>
           <el-button
             class="btn-ghost"
-            :disabled="selectedRows.length !== 1 || selectedRows[0].status !== 'pending'"
+            :disabled="
+              selectedRows.length !== 1 || !['draft', 'rejected'].includes(selectedRows[0].status)
+            "
+            :loading="selectedRows.length === 1 && workflowSubmitLoadingMap[selectedRows[0].id]"
+            @click="handleToolbarWorkflowSubmit"
+          >
+            <el-icon><Upload /></el-icon>提交
+          </el-button>
+          <el-button
+            class="btn-ghost"
+            :disabled="
+              selectedRows.length !== 1 || !['draft', 'rejected'].includes(selectedRows[0].status)
+            "
             @click="handleToolbarEdit"
           >
             <el-icon><Edit /></el-icon>编辑
@@ -28,7 +40,9 @@
           <el-button
             class="btn-ghost"
             type="warning"
-            :disabled="selectedRows.length !== 1 || selectedRows[0].status !== 'pending'"
+            :disabled="
+              selectedRows.length !== 1 || !['draft', 'rejected'].includes(selectedRows[0].status)
+            "
             :loading="selectedRows.length === 1 && cancelLoadingMap[selectedRows[0].id]"
             @click="handleToolbarCancel"
           >
@@ -338,7 +352,14 @@
       modal-class="order-module-dialog-mask"
       append-to-body
     >
-      <template v-if="viewingRecord?.status === 'pending'" #hero-actions>
+      <template v-if="viewingRecord && ['draft', 'rejected'].includes(viewingRecord.status)" #hero-actions>
+        <el-button
+          class="btn-ghost"
+          :loading="workflowSubmitLoadingMap[viewingRecord!.id]"
+          @click="handleWorkflowSubmit(viewingRecord!)"
+        >
+          <el-icon><Upload /></el-icon>提交
+        </el-button>
         <el-button class="btn-main" type="primary" @click="handleEdit(viewingRecord!)">
           <el-icon><Edit /></el-icon>编辑
         </el-button>
@@ -371,7 +392,8 @@ import {
   Close,
   Edit,
   Filter,
-  Plus
+  Plus,
+  Upload
 } from '@element-plus/icons-vue'
 import {
   vehicleApi,
@@ -391,6 +413,8 @@ import {
   formatPlVehiclePlateDisplay,
   getPlRechargeApplyStatusLabel,
   getPlRechargeApplyStatusTagType,
+  getPlRechargeApprovalStatusLabel,
+  getPlRechargeApprovalStatusTagType,
   getPlRechargeTypeLabel,
   getPlRechargeTypeTagType
 } from '@/utils/pl-display'
@@ -491,6 +515,7 @@ const userOptions = ref<
 const loading = ref(false)
 const submitLoading = ref(false)
 const cancelLoadingMap = reactive<Record<string, boolean>>({})
+const workflowSubmitLoadingMap = reactive<Record<string, boolean>>({})
 const tableData = ref<VehicleRechargeApply[]>([])
 const selectedRows = ref<VehicleRechargeApply[]>([])
 
@@ -564,6 +589,28 @@ async function refreshViewIfOpen(editedId?: string) {
 const viewSections = computed<ViewSection[]>(() => {
   const d = viewDetail.value
   if (!d) return []
+  const approvals = d.approvals || []
+  const approvalFields =
+    approvals.length > 0
+      ? approvals.map((a) => ({
+          label: `第${a.approvalRound}轮 · ${a.nodeName || '节点'}`,
+          value: `${getPlRechargeApprovalStatusLabel(a.approvalStatus)} / ${a.approverName} / ${a.approvalTime}${
+            a.approvalOpinion ? ` / ${a.approvalOpinion}` : ''
+          }`,
+          tagType: getPlRechargeApprovalStatusTagType(a.approvalStatus),
+          span: 2 as const
+        }))
+      : [{ label: '审核记录', value: '暂无', span: 2 as const }]
+  const record = d.record
+  const recordFields = record
+    ? [
+        { label: '充值订单号', value: record.rechargeOrderNo || '—' },
+        { label: '充值日期', value: record.rechargeDate || '—' },
+        { label: '金额', value: record.amount != null ? String(record.amount) : '—' },
+        { label: '卡号', value: record.cardNo || '—' },
+        { label: '加油站/渠道', value: record.gasStation || '—', span: 2 as const }
+      ]
+    : [{ label: '发放信息', value: '暂无', span: 2 as const }]
   return [
     {
       title: '申请信息',
@@ -583,9 +630,12 @@ const viewSections = computed<ViewSection[]>(() => {
           value: getPlRechargeApplyStatusLabel(d.status),
           tagType: getPlRechargeApplyStatusTagType(d.status)
         },
+        { label: '当前节点', value: d.currentNodeName || '—' },
         { label: '备注', value: d.remark || '—', span: 2 }
       ]
-    }
+    },
+    { title: '审核记录', fields: approvalFields },
+    { title: '发放信息', fields: recordFields }
   ]
 })
 
@@ -713,6 +763,31 @@ async function handleView(row: VehicleRechargeApply) {
   }
 }
 
+function handleToolbarWorkflowSubmit() {
+  if (selectedRows.value.length !== 1) {
+    ElMessage.warning('请先勾选一条申请')
+    return
+  }
+  handleWorkflowSubmit(selectedRows.value[0])
+}
+
+async function handleWorkflowSubmit(row: VehicleRechargeApply) {
+  if (!['draft', 'rejected'].includes(row.status)) {
+    ElMessage.warning('仅草稿或已驳回的申请可提交')
+    return
+  }
+  await ElMessageBox.confirm('确定提交该申请进入审批吗?', '提示', { type: 'warning' })
+  workflowSubmitLoadingMap[row.id] = true
+  try {
+    await vehicleRechargeApplyApi.submit(row.id)
+    ElMessage.success('已提交审批')
+    await fetchData()
+    await refreshViewIfOpen(row.id)
+  } finally {
+    workflowSubmitLoadingMap[row.id] = false
+  }
+}
+
 function handleToolbarEdit() {
   if (selectedRows.value.length !== 1) {
     ElMessage.warning('请先勾选一条申请')

+ 383 - 0
web-admin/src/views/pl/vehicle-recharge-approval/index.vue

@@ -0,0 +1,383 @@
+<!-- 充值审批(待办/已办) -->
+<template>
+  <div class="page-container">
+    <section class="panel">
+      <div class="list-toolbar">
+        <div class="list-toolbar-row">
+          <span class="list-toolbar-selected">已选 {{ selectedRows.length }} 项</span>
+          <el-radio-group v-model="activeTab" @change="handleTabChange">
+            <el-radio-button value="pending">待我审批</el-radio-button>
+            <el-radio-button value="done">我已办</el-radio-button>
+          </el-radio-group>
+          <el-button
+            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
+          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 #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="110" />
+          <el-table-column prop="rechargeType" label="充值类型" min-width="100" align="center">
+            <template #default="{ row }">
+              <el-tag :type="getPlRechargeTypeTagType(row.rechargeType)" size="small">
+                {{ getPlRechargeTypeLabel(row.rechargeType) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="currentNodeName" label="当前节点" min-width="120" show-overflow-tooltip />
+          <el-table-column prop="status" label="状态" min-width="100" align="center">
+            <template #default="{ row }">
+              <el-tag :type="getPlRechargeApplyStatusTagType(row.status)" size="small">
+                {{ getPlRechargeApplyStatusLabel(row.status) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="applicantName" label="申请人" min-width="100" />
+          <el-table-column prop="applyDate" label="申请日期" min-width="120" />
+        </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"
+    >
+      <template v-if="activeTab === 'pending' && viewDetail" #hero-actions>
+        <el-button type="primary" @click="openHandleDialog(viewDetail)">
+          <el-icon><Check /></el-icon>处理
+        </el-button>
+      </template>
+    </SystemViewDialog>
+
+    <el-dialog
+      v-model="handleVisible"
+      :title="handleDetail?.applyNo || '审批处理'"
+      width="720px"
+      align-center
+      class="dialog-form-layout"
+      :close-on-click-modal="false"
+      destroy-on-close
+    >
+      <el-form label-position="top" class="dialog-form">
+        <div class="form-grid-2">
+          <el-form-item label="车牌号">
+            <el-input :model-value="handleDetail?.licensePlate" disabled />
+          </el-form-item>
+          <el-form-item label="当前节点">
+            <el-input :model-value="handleDetail?.currentNodeName || '—'" disabled />
+          </el-form-item>
+          <el-form-item v-if="!isDisburseNode" label="审批意见" class="form-col-span-2">
+            <el-input v-model="opinion" type="textarea" :rows="3" placeholder="选填;驳回建议填写原因" />
+          </el-form-item>
+        </div>
+
+        <template v-if="isDisburseNode">
+          <el-divider content-position="left">发放登记</el-divider>
+          <div class="form-grid-2">
+            <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-2">
+              <el-input v-model="opinion" type="textarea" :rows="2" placeholder="驳回时填写" />
+            </el-form-item>
+          </div>
+        </template>
+      </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 { computed, onMounted, reactive, ref } from 'vue'
+  import { Check, Close, CloseBold, View } from '@element-plus/icons-vue'
+  import { ElMessage, ElMessageBox } from 'element-plus'
+  import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
+  import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
+  import {
+    vehicleRechargeApplyApi,
+    vehicleRechargeApprovalApi,
+    type VehicleRechargeApply
+  } from '@/api/pl'
+  import {
+    getPlRechargeApplyStatusLabel,
+    getPlRechargeApplyStatusTagType,
+    getPlRechargeApprovalStatusLabel,
+    getPlRechargeApprovalStatusTagType,
+    getPlRechargeTypeLabel,
+    getPlRechargeTypeTagType,
+    PL_GAS_STATION_PRESETS
+  } from '@/utils/pl-display'
+
+  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 viewVisible = ref(false)
+  const viewDetail = ref<VehicleRechargeApply | null>(null)
+  const handleVisible = ref(false)
+  const handleDetail = ref<VehicleRechargeApply | null>(null)
+  const opinion = ref('')
+  const disburseForm = reactive({
+    rechargeDate: '',
+    amount: null as number | null,
+    cardNo: '',
+    gasStation: ''
+  })
+
+  const canApproveSelected = computed(
+    () => activeTab.value === 'pending' && selectedRows.value.length === 1
+  )
+  const isDisburseNode = computed(
+    () => (handleDetail.value?.currentNodeActionType || 'approve') === 'disburse'
+  )
+
+  const viewSections = computed(() => {
+    const d = viewDetail.value
+    if (!d) return []
+    const approvals = d.approvals || []
+    const approvalLines = approvals.length
+      ? approvals.map((a) => ({
+          label: `第${a.approvalRound}轮 · ${a.nodeName || '节点'}`,
+          value: `${getPlRechargeApprovalStatusLabel(a.approvalStatus)} / ${a.approverName} / ${a.approvalTime}${
+            a.approvalOpinion ? ` / ${a.approvalOpinion}` : ''
+          }`,
+          tagType: getPlRechargeApprovalStatusTagType(a.approvalStatus)
+        }))
+      : [{ label: '审核记录', value: '暂无' }]
+    const record = d.record
+    const recordFields = record
+      ? [
+          { label: '充值订单号', value: record.rechargeOrderNo || '—' },
+          { label: '充值日期', value: record.rechargeDate || '—' },
+          { label: '金额', value: record.amount != null ? String(record.amount) : '—' },
+          { label: '卡号', value: record.cardNo || '—' },
+          { label: '加油站/渠道', value: record.gasStation || '—' }
+        ]
+      : [{ label: '发放信息', value: '暂无' }]
+    return [
+      {
+        title: '申请信息',
+        fields: [
+          { label: '申请单号', value: d.applyNo },
+          { label: '车牌号', value: d.licensePlate },
+          {
+            label: '充值类型',
+            value: getPlRechargeTypeLabel(d.rechargeType),
+            tagType: getPlRechargeTypeTagType(d.rechargeType)
+          },
+          {
+            label: '状态',
+            value: getPlRechargeApplyStatusLabel(d.status),
+            tagType: getPlRechargeApplyStatusTagType(d.status)
+          },
+          { label: '申请人', value: d.applicantName || '—' },
+          { label: '申请日期', value: d.applyDate },
+          { label: '当前节点', value: d.currentNodeName || '—' },
+          { label: '备注', value: d.remark || '—' }
+        ]
+      },
+      { title: '审核记录', fields: approvalLines },
+      { title: '发放信息', fields: recordFields }
+    ]
+  })
+
+  async function fetchData() {
+    loading.value = true
+    try {
+      const api = activeTab.value === 'pending' ? vehicleRechargeApprovalApi.pending : vehicleRechargeApprovalApi.done
+      const res = await api({ page: pager.page, pageSize: pager.pageSize })
+      const data: any = (res as any)?.data ?? res
+      tableData.value = data?.items || []
+      pager.total = data?.total || 0
+      selectedRows.value = []
+    } finally {
+      loading.value = false
+    }
+  }
+
+  function handleTabChange() {
+    pager.page = 1
+    fetchData()
+  }
+
+  async function openView(row: VehicleRechargeApply) {
+    viewVisible.value = true
+    viewDetail.value = row
+    try {
+      const res = await vehicleRechargeApplyApi.get(row.id)
+      viewDetail.value = (res as any)?.data ?? res
+    } catch {
+      // keep list row as fallback
+    }
+  }
+
+  async function openHandleDialog(row: VehicleRechargeApply) {
+    handleDetail.value = row
+    opinion.value = ''
+    disburseForm.rechargeDate = new Date().toISOString().slice(0, 10)
+    disburseForm.amount = null
+    disburseForm.cardNo = ''
+    disburseForm.gasStation = ''
+    handleVisible.value = true
+    try {
+      const res = await vehicleRechargeApplyApi.get(row.id)
+      handleDetail.value = (res as any)?.data ?? res
+    } 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 || undefined })
+      ElMessage.success('已通过')
+      handleVisible.value = false
+      viewVisible.value = false
+      await fetchData()
+    } finally {
+      submitting.value = false
+    }
+  }
+
+  async function handleReject() {
+    if (!handleDetail.value) return
+    if (!opinion.value.trim()) {
+      ElMessage.warning('请填写驳回意见')
+      return
+    }
+    await ElMessageBox.confirm('确定驳回该申请吗?', '提示', { type: 'warning' })
+    submitting.value = true
+    try {
+      await vehicleRechargeApprovalApi.reject(handleDetail.value.id, { opinion: opinion.value })
+      ElMessage.success('已驳回')
+      handleVisible.value = false
+      viewVisible.value = false
+      await fetchData()
+    } finally {
+      submitting.value = false
+    }
+  }
+
+  async function handleDisburse() {
+    if (!handleDetail.value) return
+    if (!disburseForm.rechargeDate || !disburseForm.amount || disburseForm.amount <= 0) {
+      ElMessage.warning('请填写充值日期和金额')
+      return
+    }
+    submitting.value = true
+    try {
+      await vehicleRechargeApprovalApi.disburse(handleDetail.value.id, {
+        applyId: handleDetail.value.id,
+        licensePlate: handleDetail.value.licensePlate,
+        rechargeType: handleDetail.value.rechargeType,
+        rechargeDate: disburseForm.rechargeDate,
+        amount: disburseForm.amount,
+        cardNo: disburseForm.cardNo || undefined,
+        gasStation: disburseForm.gasStation || undefined
+      })
+      ElMessage.success('发放登记成功')
+      handleVisible.value = false
+      viewVisible.value = false
+      await fetchData()
+    } finally {
+      submitting.value = false
+    }
+  }
+
+  onMounted(fetchData)
+</script>

+ 10 - 1
web-admin/src/views/pl/vehicle-recharge-record/index.vue

@@ -20,7 +20,12 @@
           </el-button>
           <el-button
             class="btn-ghost"
-            :disabled="selectedRows.length !== 1 || selectedRows[0].applyStatus !== 'pending'"
+            :disabled="
+              selectedRows.length !== 1 ||
+              ['in_approval', 'disbursed', 'cancelled', 'rejected', 'draft'].includes(
+                selectedRows[0].applyStatus
+              )
+            "
             @click="handleToolbarDisburse"
           >
             <el-icon><Money /></el-icon>发放
@@ -790,6 +795,10 @@ function resetForm() {
 }
 
 function handleDisburse(row: VehicleRechargeDisburseBoardItem) {
+  if (row.applyStatus === 'in_approval') {
+    ElMessage.warning('审批中的申请请到「充值审批」菜单完成发放登记')
+    return
+  }
   resetForm()
   dialogMode.value = 'disburse'
   dialogTitle.value = '发放登记'

+ 592 - 0
web-admin/src/views/pl/vehicle-recharge-workflow/index.vue

@@ -0,0 +1,592 @@
+<!-- 工作流配置(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>
+      </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 ref="wfReimburseTableWrapRef" class="list-table-viewport-anchor">
+        <ListTable
+          ref="wfReimburseTableRef"
+          table-key="pl-vehicle-recharge-workflow"
+          class="list-table"
+          :height="tableHeight"
+          :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
+          />
+          <el-table-column
+            prop="actionType"
+            label="节点动作"
+            min-width="120"
+            align="center"
+            header-align="center"
+          >
+            <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"
+          />
+          <el-table-column
+            v-if="isColumnVisible('approverName')"
+            prop="approverName"
+            label="审批人"
+            min-width="120"
+            align="center"
+            header-align="center"
+            show-overflow-tooltip
+          />
+          <el-table-column
+            v-if="isColumnVisible('configStatus')"
+            label="配置状态"
+            min-width="110"
+            align="center"
+            header-align="center"
+          >
+            <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 ref="wfReimbursePagerRef" 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, nextTick } from 'vue'
+import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
+import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
+import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
+import { useListTableViewportHeight } from '@/composables/useListTableViewportHeight'
+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'
+
+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-vehicle-recharge-workflow-list-columns', listColumnDefs)
+
+const wfReimburseTableWrapRef = ref<HTMLElement | null>(null)
+const wfReimbursePagerRef = ref<HTMLElement | null>(null)
+const wfReimburseTableRef = ref<any>(null)
+const { tableHeight, recalc: recalcTableHeight } = useListTableViewportHeight({
+  tableEl: wfReimburseTableWrapRef,
+  pagerEl: wfReimbursePagerRef,
+  tableRef: wfReimburseTableRef
+})
+const loading = ref(false)
+const selectedRows = ref<Record<string, any>[]>([])
+const wfDef = ref<Record<string, any>>({})
+const allNodes = computed(() => wfDef.value.nodes || [])
+const filteredNodes = 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 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()
+  nextTick(() => recalcTableHeight())
+})
+
+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) => ({
+    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>