Sfoglia il codice sorgente

feat: 订单系统按照流程节点、负责人、业务订单号映射导入到“我的流程”

wuwenyi 2 settimane fa
parent
commit
a77444e2cb

+ 0 - 30
src/api/file.ts

@@ -6,33 +6,3 @@ export function uploadFile(file: File): Promise<string> {
   return request.post('/file/upload', formData)
 }
 
-/**
- * 解析 Excel 文件,返回表单数据
- * @param file Excel 文件
- * @param definitionId 流程定义 ID
- * @param mappings Excel 列名 -> 系统字段名 映射
- */
-export function parseExcel(file: File, definitionId: number, mappings: Record<string, string>): Promise<Record<string, unknown>> {
-  const formData = new FormData()
-  formData.append('file', file)
-  formData.append('definitionId', String(definitionId))
-  formData.append('mappings', JSON.stringify(mappings))
-  return request.post('/file/parse-excel', formData)
-}
-
-/**
- * 下载流程表单 Excel 模板
- */
-export async function downloadTemplate(definitionId: number): Promise<void> {
-  const blob = (await request.get(`/file/template/${definitionId}`, {
-    responseType: 'blob'
-  })) as unknown as Blob
-  const url = window.URL.createObjectURL(blob)
-  const link = document.createElement('a')
-  link.href = url
-  link.download = 'template.xlsx'
-  document.body.appendChild(link)
-  link.click()
-  document.body.removeChild(link)
-  window.URL.revokeObjectURL(url)
-}

+ 4 - 0
src/api/flow/instance.ts

@@ -7,6 +7,10 @@ export function listMyInstance(params?: PageQuery): Promise<PageResult<FlowInsta
   return request.get('/flow/instance/mine', { params })
 }
 
+export function listInstance(params?: PageQuery & { applicantName?: string; processName?: string; currentNodeName?: string }): Promise<PageResult<FlowInstance>> {
+  return request.get('/flow/instance/page', { params })
+}
+
 export function startInstance(processDefinitionId: number, title?: string, formData?: Record<string, unknown>, attachmentUrls?: string): Promise<void> {
   return request.post('/flow/instance/start', {
     processDefinitionId,

+ 5 - 1
src/api/flow/task.ts

@@ -1,5 +1,5 @@
 import request from '@/api/request'
-import type { FlowTask, ApprovalAction } from '@/types/flow'
+import type { FlowTask, ApprovalAction, NextNode } from '@/types/flow'
 import type { PageQuery, PageResult } from '@/types/api'
 
 export function listTodo(params?: PageQuery): Promise<PageResult<FlowTask>> {
@@ -53,3 +53,7 @@ export function listCc(params?: PageQuery): Promise<PageResult<FlowTask>> {
 export function readCc(taskId: number): Promise<void> {
   return request.post(`/flow/task/cc/${taskId}/read`)
 }
+
+export function listNextNodes(taskId: number): Promise<NextNode[]> {
+  return request.get(`/flow/task/${taskId}/next-nodes`)
+}

+ 11 - 3
src/components/FlowFormFields/index.vue

@@ -92,7 +92,8 @@
 import { computed, watch } from 'vue'
 import { ElMessage } from 'element-plus'
 import type { FormField, FormFieldOption, FlowDefinition } from '@/types/flow'
-import { parseExcel, downloadTemplate } from '@/api/file'
+import { downloadTemplate as downloadFlowTemplate } from '@/api/flow/import'
+import request from '@/api/request'
 
 const props = defineProps<{
   definition: FlowDefinition | null | undefined
@@ -153,7 +154,7 @@ function validate(): boolean {
 async function handleDownloadTemplate() {
   if (!props.definition?.id) return
   try {
-    await downloadTemplate(props.definition.id)
+    await downloadFlowTemplate(props.definition.id)
   } catch (e: any) {
     ElMessage.error(e?.message || '模板下载失败')
   }
@@ -180,7 +181,14 @@ async function handleExcelUpload(options: any) {
       mappings[field.label] = field.name
       mappings[`${field.label}(${field.name})`] = field.name
     }
-    const data = await parseExcel(options.file, props.definition.id, mappings)
+    const formData2 = new FormData()
+    formData2.append('file', options.file)
+    formData2.append('definitionId', String(props.definition.id))
+    formData2.append('mappings', JSON.stringify(mappings))
+    const res = await request.post('/flow/import/parse', formData2, {
+      headers: { 'Content-Type': 'multipart/form-data' }
+    })
+    const data = res as unknown as Record<string, unknown>
     formData.value = { ...formData.value, ...data }
     ElMessage.success('Excel 数据已填充')
     options.onSuccess?.()

+ 8 - 2
src/router/index.ts

@@ -146,6 +146,12 @@ const router = createRouter({
           name: 'FlowExecute',
           component: () => import('@/views/flow/execute/index.vue'),
           meta: { title: '流程执行', icon: 'Pointer' }
+        },
+        {
+          path: 'all',
+          name: 'AllInstance',
+          component: () => import('@/views/flow/instance/all.vue'),
+          meta: { title: '流程明细', icon: 'Document' }
         }
       ]
     }
@@ -158,8 +164,8 @@ const publicPaths = new Set(['/login'])
 const permissionPaths: Record<string, Set<string>> = {
   common_user: new Set(['/', '/dashboard', '/login', '/profile', '/approval/todo', '/approval/handled', '/approval/mine', '/approval/execute', '/approval/cc']),
   dept_manager: new Set(['/', '/dashboard', '/login', '/profile', '/system/user', '/system/role', '/approval/todo', '/approval/handled', '/approval/mine', '/approval/execute', '/approval/cc']),
-  flow_manager: new Set(['/', '/dashboard', '/login', '/profile', '/flow/designer', '/flow/definition', '/approval/todo', '/approval/handled', '/approval/mine', '/approval/execute', '/approval/cc', '/analysis']),
-  super_admin: new Set(['/', '/dashboard', '/login', '/profile', '/system/user', '/system/role', '/flow/designer', '/flow/definition', '/approval/todo', '/approval/handled', '/approval/mine', '/approval/execute', '/approval/cc', '/analysis'])
+  flow_manager: new Set(['/', '/dashboard', '/login', '/profile', '/flow/designer', '/flow/definition', '/flow/instance/all', '/approval/todo', '/approval/handled', '/approval/mine', '/approval/execute', '/approval/cc', '/analysis']),
+  super_admin: new Set(['/', '/dashboard', '/login', '/profile', '/system/user', '/system/role', '/flow/designer', '/flow/definition', '/flow/instance/all', '/approval/todo', '/approval/handled', '/approval/mine', '/approval/execute', '/approval/cc', '/analysis'])
 }
 
 function isPathAllowed(allowedSet: Set<string>, path: string): boolean {

+ 10 - 0
src/types/flow.ts

@@ -23,6 +23,9 @@ export interface FlowDefinition {
   status: number // 0-草稿 1-已发布 2-停用
   formSchema?: string
   formFields?: FormField[]
+  nodeFieldName?: string
+  ownerFieldName?: string
+  bizFieldName?: string
   flowJson?: string
   createTime?: string
   updateTime?: string
@@ -33,6 +36,7 @@ export interface FlowInstance {
   definitionId: number
   definitionName?: string
   title?: string
+  bizValue?: string
   instanceNo?: string
   businessKey?: string
   status: number // 0-待接收, 1-运行中, 2-已通过, 3-已拒绝, 4-已回退, 5-已完成, 6-已撤回, 7-已终止
@@ -84,6 +88,12 @@ export interface FlowEdgeConfig {
   properties?: Record<string, unknown>
 }
 
+export interface NextNode {
+  nodeId: string
+  nodeName: string
+  nodeType: string
+}
+
 export interface ApprovalAction {
   action: 'pass' | 'reject' | 'rollback' | 'transfer'
   comment?: string

+ 74 - 30
src/views/analysis/index.vue

@@ -136,55 +136,63 @@
       </el-col>
     </el-row>
 
-    <el-card shadow="hover" class="detail-card" v-loading="loadingStuck">
+    <el-card shadow="hover" class="detail-card" v-loading="loadingInstanceList">
       <template #header>
         <div class="detail-header">
-          <span>节点下钻明细(卡住流程)</span>
+          <span>流程明细</span>
           <div class="detail-actions">
-            <el-input-number
-              v-model="stuckQuery.minStayMinutes"
-              :min="0"
-              placeholder="最小停留分钟"
-              controls-position="right"
-              style="width: 160px"
-              @change="loadStuck(1)"
-            />
-            <el-button type="primary" @click="loadStuck(1)">查询</el-button>
+            <el-select v-model="instanceQuery.processName" placeholder="流程名称" clearable style="width: 160px;" @change="loadInstanceList">
+              <el-option v-for="def in definitionList" :key="def.id" :label="def.name" :value="def.name" />
+            </el-select>
+            <el-input v-model="instanceQuery.applicantName" placeholder="归属用户" clearable style="width: 140px; margin-left: 8px;" @change="loadInstanceList" />
+            <el-select v-model="instanceQuery.currentNodeName" placeholder="当前节点" clearable style="width: 140px; margin-left: 8px;" @change="loadInstanceList">
+              <el-option v-for="node in currentNodeOptions" :key="node" :label="node" :value="node" />
+            </el-select>
+            <el-button type="primary" @click="loadInstanceList" style="margin-left: 8px;">查询</el-button>
           </div>
         </div>
       </template>
 
-      <el-table :data="stuckList" stripe>
-        <el-table-column prop="instanceNo" label="实例编号" min-width="160" show-overflow-tooltip />
-        <el-table-column prop="title" label="标题" min-width="180" show-overflow-tooltip />
-        <el-table-column prop="processName" label="流程" min-width="140" show-overflow-tooltip />
-        <el-table-column prop="nodeName" label="当前节点" min-width="140" show-overflow-tooltip />
-        <el-table-column prop="applicantName" label="申请人" min-width="100" />
-        <el-table-column prop="stayMinutes" label="已停留" min-width="120">
+      <el-table :data="instanceList" stripe>
+        <el-table-column prop="definitionName" label="流程名称" min-width="140" show-overflow-tooltip />
+        <el-table-column label="业务编号" min-width="140">
           <template #default="{ row }">
-            <el-tag :type="row.stayMinutes > 1440 ? 'danger' : row.stayMinutes > 240 ? 'warning' : 'info'">
-              {{ formatMinutes(row.stayMinutes) }}
-            </el-tag>
+            {{ row.bizValue || row.instanceNo || '-' }}
           </template>
         </el-table-column>
-        <el-table-column prop="taskCreateTime" label="任务到达时间" min-width="160" />
-        <el-table-column label="操作" width="100" fixed="right">
+        <el-table-column prop="applicantName" label="发起人" min-width="100" />
+        <el-table-column prop="status" label="状态" min-width="90">
           <template #default="{ row }">
-            <el-link type="primary" @click="goInstance(row.instanceId)">查看</el-link>
+            <el-tag :type="instanceStatusTagType(row.status)">{{ instanceStatusText(row.status) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="currentNodeName" label="当前节点" min-width="120">
+          <template #default="{ row }">
+            {{ row.currentNodeName || row.currentNode || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="startTime" label="发起时间" min-width="160" />
+        <el-table-column label="操作" min-width="80" fixed="right">
+          <template #default="{ row }">
+            <el-button link type="primary" size="small" @click="openInstanceDetail(row.id)">详情</el-button>
           </template>
         </el-table-column>
       </el-table>
 
       <el-pagination
-        v-model:current-page="stuckQuery.pageNum"
-        v-model:page-size="stuckQuery.pageSize"
-        :total="stuckTotal"
-        layout="total, sizes, prev, pager, next"
+        v-model:current-page="instanceQuery.pageNum"
+        v-model:page-size="instanceQuery.pageSize"
+        :total="instanceTotal"
         :page-sizes="[10, 20, 50]"
-        @size-change="loadStuck(1)"
-        @current-change="loadStuck"
+        layout="total, sizes, prev, pager, next, jumper"
+        class="pagination"
+        @size-change="loadInstanceList"
+        @current-change="loadInstanceList"
       />
     </el-card>
+
+    <InstanceDetail v-model="instanceDetailVisible" :instance-id="instanceDetailId" />
+
   </div>
 </template>
 
@@ -206,6 +214,10 @@ import {
   getTrend
 } from '@/api/analysis'
 import { listEnabled } from '@/api/flow/definition'
+import { listInstance } from '@/api/flow/instance'
+import { instanceStatusText, instanceStatusTagType } from '@/utils/flow'
+import InstanceDetail from '@/views/flow/execute/InstanceDetail.vue'
+import type { FlowInstance } from '@/types/flow'
 import type {
   ProcessEfficiency,
   NodeStayStat,
@@ -616,9 +628,41 @@ function buildNodeOption(list: NodeStayStat[]) {
   }
 }
 
+// 流程明细
+const instanceQuery = reactive({ pageNum: 1, pageSize: 10, processName: '', applicantName: '', currentNodeName: '' })
+const instanceList = ref<FlowInstance[]>([])
+const instanceTotal = ref(0)
+const loadingInstanceList = ref(false)
+const definitionList = ref<FlowDefinition[]>([])
+const currentNodeOptions = ref<string[]>([])
+
+async function loadDefinitionsFull() {
+  try { definitionList.value = await listEnabled() } catch { /* ignore */ }
+}
+
+const instanceDetailVisible = ref(false)
+const instanceDetailId = ref(0)
+function openInstanceDetail(id: number) { instanceDetailId.value = id; instanceDetailVisible.value = true }
+
+async function loadInstanceList() {
+  loadingInstanceList.value = true
+  try {
+    const res = await listInstance({
+      pageNum: instanceQuery.pageNum, pageSize: instanceQuery.pageSize,
+      processName: instanceQuery.processName || undefined,
+      applicantName: instanceQuery.applicantName || undefined,
+      currentNodeName: instanceQuery.currentNodeName || undefined
+    })
+    instanceList.value = res.list
+    instanceTotal.value = res.total
+  } finally { loadingInstanceList.value = false }
+}
+
 onMounted(() => {
   loadDefinitions()
   loadAll()
+  loadDefinitionsFull()
+  loadInstanceList()
 })
 </script>
 

+ 23 - 1
src/views/flow/definition/index.vue

@@ -90,7 +90,7 @@
 
     <!-- 编辑流程 - 基本信息弹窗 -->
     <el-dialog v-model="editDialogVisible" title="编辑流程信息" width="700px">
-      <el-form ref="editFormRef" :model="editForm" :rules="editFormRules" label-width="100px">
+      <el-form ref="editFormRef" :model="editForm" :rules="editFormRules" label-width="120px">
         <el-form-item label="流程编码">
           <el-input v-model="editForm.code" disabled />
         </el-form-item>
@@ -103,6 +103,27 @@
         <el-form-item label="描述">
           <el-input v-model="editForm.description" type="textarea" />
         </el-form-item>
+        <el-form-item>
+          <template #label>
+            节点名称字段
+            <el-tooltip placement="top" content="导入流程时,按此字段的值匹配对应的流程节点" raw-content>
+              <el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
+            </el-tooltip>
+          </template>
+          <el-input v-model="editForm.nodeFieldName" placeholder="如:节点名称,对应 Excel 中的列名" />
+        </el-form-item>
+        <el-form-item>
+          <template #label>
+            归属人字段
+            <el-tooltip placement="top" content="导入流程时,按此字段的值匹配 sys_user 表中的用户,匹配成功则该流程属于该用户" raw-content>
+              <el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
+            </el-tooltip>
+          </template>
+          <el-input v-model="editForm.ownerFieldName" placeholder="如:发起人,对应 Excel 中的列名" />
+        </el-form-item>
+        <el-form-item label="业务编号字段">
+          <el-input v-model="editForm.bizFieldName" placeholder="如:customerName,列表中显示为该字段的值" />
+        </el-form-item>
       </el-form>
 
       <div class="form-fields-section">
@@ -205,6 +226,7 @@
 import { ref, reactive, onMounted } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { QuestionFilled } from '@element-plus/icons-vue'
 import type { FormInstance, FormRules } from 'element-plus'
 import { listDefinition, getDefinition, updateDefinition, deleteDefinition, publishDefinition, stopDefinition, enableDefinition } from '@/api/flow/definition'
 import type { FlowDefinition, FormField, FormFieldOption } from '@/types/flow'

+ 20 - 4
src/views/flow/designer/index.vue

@@ -88,8 +88,25 @@
           </el-form-item>
         </template>
         <template v-if="selectedNode.type === 'condition-node'">
-          <el-form-item label="条件表达式">
-            <el-input v-model="nodeProps.condition" type="textarea" placeholder="输入条件表达式" />
+          <el-form-item>
+            <template #label>
+              条件表达式
+              <el-tooltip placement="top" effect="dark" raw-content>
+                <template #content>
+                  <div style="line-height: 1.8; max-width: 320px;">
+                    <div style="margin-bottom: 6px; font-weight: bold;">格式:字段名 运算符 值</div>
+                    <div>运算符:<code>==</code>&nbsp;<code>!=</code>&nbsp;<code>&gt;</code>&nbsp;<code>&lt;</code>&nbsp;<code>&gt;=</code>&nbsp;<code>&lt;=</code></div>
+                    <div style="margin-top: 4px;">示例:</div>
+                    <div><code>quantity &gt;= 10</code>&nbsp;&nbsp;数量 ≥ 10</div>
+                    <div><code>tradeTerms == "FOB"</code>&nbsp;&nbsp;贸易条款为FOB</div>
+                    <div><code>destCountry == "USA"</code>&nbsp;&nbsp;目的国为USA</div>
+                    <div style="margin-top: 4px; color: #e6a23c;">注意:条件不满足时走默认分支(无条件的连线)</div>
+                  </div>
+                </template>
+                <el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
+              </el-tooltip>
+            </template>
+            <el-input v-model="nodeProps.condition" type="textarea" placeholder="如: quantity >= 10" />
           </el-form-item>
         </template>
         <template v-if="selectedNode.type === 'cc-node'">
@@ -237,7 +254,7 @@ import LogicFlow, { RectNode, RectNodeModel, CircleNode, CircleNodeModel, Polygo
 import { Menu, Snapshot } from '@logicflow/extension'
 import '@logicflow/core/dist/style/index.css'
 import '@logicflow/extension/lib/style/index.css'
-import { VideoPlay, User, Message, Operation, CircleCheck } from '@element-plus/icons-vue'
+import { VideoPlay, User, Message, Operation, CircleCheck, QuestionFilled } from '@element-plus/icons-vue'
 import { ElMessage } from 'element-plus'
 import type { FormInstance, FormRules } from 'element-plus'
 import { listRole } from '@/api/system/role'
@@ -676,7 +693,6 @@ function validateFlow(graphData: any): string | null {
   if (startNodes.length === 0) return '流程图缺少开始节点'
   if (endNodes.length === 0) return '流程图缺少结束节点'
   if (startNodes.length > 1) return '流程图只能有一个开始节点'
-  if (endNodes.length > 1) return '流程图只能有一个结束节点'
   // 检查审批节点是否选择了审批角色
   for (const node of nodes) {
     if (node.type === 'approval-node' || node.type === 'approval') {

+ 44 - 240
src/views/flow/execute/index.vue

@@ -8,16 +8,11 @@
       </template>
 
       <el-tabs v-model="activeTab">
-        <el-tab-pane label="发起流程" name="start">
+        <el-tab-pane label="发起流程" name="start" lazy>
           <div v-if="loadingDefinitions" v-loading="true" style="height: 200px;" />
           <el-empty v-else-if="definitionList.length === 0" description="暂无可用的流程" />
           <div v-else class="definition-list">
-            <el-card
-              v-for="def in definitionList"
-              :key="def.id"
-              class="definition-card"
-              shadow="hover"
-            >
+            <el-card v-for="def in definitionList" :key="def.id" class="definition-card" shadow="hover">
               <div class="def-header">
                 <el-tag type="success" size="small">已发布</el-tag>
                 <span class="def-name">{{ def.name }}</span>
@@ -31,83 +26,21 @@
             </el-card>
           </div>
         </el-tab-pane>
-
-        <el-tab-pane label="我发起的流程" name="mine">
-          <el-table v-loading="loadingInstances" :data="instanceList" border>
-            <el-table-column type="index" label="序号" width="60" />
-            <el-table-column prop="definitionName" label="流程名称" />
-            <el-table-column prop="title" label="标题" />
-            <el-table-column prop="status" label="状态" width="100">
-              <template #default="{ row }">
-                <el-tag :type="instanceStatusTagType(row.status)">
-                  {{ instanceStatusText(row.status) }}
-                </el-tag>
-              </template>
-            </el-table-column>
-            <el-table-column prop="currentNodeName" label="当前节点">
-              <template #default="{ row }">
-                {{ row.currentNodeName || row.currentNode || '-' }}
-              </template>
-            </el-table-column>
-            <el-table-column prop="startTime" label="发起时间" />
-            <el-table-column label="操作" width="120">
-              <template #default="{ row }">
-                <el-button link type="primary" @click="handleView(row)">查看详情</el-button>
-              </template>
-            </el-table-column>
-          </el-table>
-          <el-pagination
-            v-model:current-page="queryParams.pageNum"
-            v-model:page-size="queryParams.pageSize"
-            :total="total"
-            :page-sizes="[10, 20, 50]"
-            layout="total, sizes, prev, pager, next, jumper"
-            class="pagination"
-            @current-change="loadInstances"
-            @size-change="loadInstances"
-          />
-        </el-tab-pane>
       </el-tabs>
     </el-card>
 
-    <!-- 发起流程弹窗 -->
     <el-dialog v-model="startDialogVisible" title="发起流程" width="640px">
       <el-form ref="startFormRef" :model="startForm" :rules="startFormRules" label-width="80px">
-        <el-form-item v-show="false">
-          <el-input v-model="startForm.processDefinitionId" />
-        </el-form-item>
-        <el-form-item label="流程名称">
-          <el-input v-model="startForm.definitionName" disabled />
-        </el-form-item>
-        <el-form-item label="标题" prop="title">
-          <el-input v-model="startForm.title" placeholder="请输入流程标题" />
-        </el-form-item>
+        <el-form-item v-show="false"><el-input v-model="startForm.processDefinitionId" /></el-form-item>
+        <el-form-item label="流程名称"><el-input v-model="startForm.definitionName" disabled /></el-form-item>
+        <el-form-item label="标题" prop="title"><el-input v-model="startForm.title" placeholder="请输入流程标题" /></el-form-item>
         <el-form-item label="表单数据">
-          <FlowFormFields v-if="hasFormFields" ref="flowFormFieldsRef" v-model="dynamicFormData" :definition="selectedDefinition" />
+          <FlowFormFields v-if="hasFormFields" v-model="dynamicFormData" :definition="selectedDefinition" />
           <el-input v-else v-model="startForm.formData" type="textarea" :rows="4" placeholder="请输入表单数据,格式:每行一个字段,如 amount=1000" />
         </el-form-item>
         <el-form-item label="附件">
-          <el-upload
-            v-model:file-list="attachmentList"
-            action="#"
-            :http-request="handleUpload"
-            :before-upload="beforeFileUpload"
-            :before-remove="() => true"
-            :limit="5"
-            multiple
-          >
+          <el-upload v-model:file-list="attachmentList" action="#" :http-request="handleUpload" :before-upload="beforeFileUpload" :before-remove="() => true" :limit="5" multiple>
             <el-button type="primary" size="small">上传附件</el-button>
-            <template #tip>
-              <div class="el-upload__tip">支持 Excel、图片等常见格式</div>
-            </template>
-            <template #file="{ file }">
-              <div class="upload-file-item">
-                <el-icon><Document /></el-icon>
-                <el-link type="primary" @click="openPreview(getUploadUrl(file))">
-                  {{ file.name }}
-                </el-link>
-              </div>
-            </template>
           </el-upload>
         </el-form-item>
       </el-form>
@@ -117,15 +50,7 @@
       </template>
     </el-dialog>
 
-    <!-- 流程详情弹窗 -->
-    <InstanceDetail
-      v-model="detailVisible"
-      :instance-id="currentInstanceId"
-      @approved="loadInstances"
-    />
-
-    <!-- 附件预览 -->
-    <FilePreview v-model="previewVisible" :url="previewUrl" />
+    <InstanceDetail v-model="detailVisible" :instance-id="currentInstanceId" />
   </div>
 </template>
 
@@ -133,201 +58,80 @@
 import { ref, reactive, computed, onMounted } from 'vue'
 import { ElMessage } from 'element-plus'
 import type { FormInstance, FormRules } from 'element-plus'
-import { Document } from '@element-plus/icons-vue'
 import { listEnabled } from '@/api/flow/definition'
-import { listMyInstance, startInstance } from '@/api/flow/instance'
+import { startInstance } from '@/api/flow/instance'
 import { uploadFile } from '@/api/file'
-import { beforeFileUpload, collectAttachmentUrls, getUploadUrl } from '@/utils/file'
-import { instanceStatusText, instanceStatusTagType } from '@/utils/flow'
-import type { FlowDefinition, FlowInstance } from '@/types/flow'
+import { beforeFileUpload, collectAttachmentUrls } from '@/utils/file'
+import type { FlowDefinition } from '@/types/flow'
 import InstanceDetail from './InstanceDetail.vue'
 import FlowFormFields from '@/components/FlowFormFields/index.vue'
-import FilePreview from '@/components/FilePreview/index.vue'
 
 const activeTab = ref('start')
 const loadingDefinitions = ref(false)
-const loadingInstances = ref(false)
 const submitting = ref(false)
 const definitionList = ref<FlowDefinition[]>([])
-const instanceList = ref<FlowInstance[]>([])
-const total = ref(0)
-const queryParams = reactive({ pageNum: 1, pageSize: 10 })
-
 const startDialogVisible = ref(false)
 const startFormRef = ref<FormInstance>()
-const startForm = ref({
-  processDefinitionId: 0 as number,
-  definitionName: '',
-  title: '',
-  formData: ''
-})
+const startForm = ref({ processDefinitionId: 0 as number, definitionName: '', title: '', formData: '' })
 const dynamicFormData = ref<Record<string, unknown>>({})
 const selectedDefinition = ref<FlowDefinition | null>(null)
-const flowFormFieldsRef = ref<any>(null)
-
+const attachmentList = ref<any[]>([])
+const startFormRules: FormRules = { title: [{ required: true, message: '请输入标题', trigger: 'blur' }] }
 const hasFormFields = computed(() => {
-  const schema = selectedDefinition.value?.formSchema
-  if (!schema) return false
-  try {
-    const parsed = JSON.parse(schema)
-    return Array.isArray(parsed) && parsed.length > 0
-  } catch {
-    return false
-  }
+  const s = selectedDefinition.value?.formSchema
+  if (!s) return false
+  try { const a = JSON.parse(s); return Array.isArray(a) && a.length > 0 } catch { return false }
 })
-const attachmentList = ref<any[]>([])
-const startFormRules: FormRules = {
-  title: [{ required: true, message: '请输入标题', trigger: 'blur' }]
-}
-
 const detailVisible = ref(false)
 const currentInstanceId = ref(0)
-const previewVisible = ref(false)
-const previewUrl = ref('')
-
-function openPreview(url: string) {
-  if (!url) return
-  previewUrl.value = url
-  previewVisible.value = true
-}
-
-function handleFilePreview(file: any) {
-  const url = getUploadUrl(file)
-  if (url) openPreview(url)
-}
 
 async function loadDefinitions() {
   loadingDefinitions.value = true
-  try {
-    const res = await listEnabled()
-    definitionList.value = res
-  } finally {
-    loadingDefinitions.value = false
-  }
-}
-
-async function loadInstances() {
-  loadingInstances.value = true
-  try {
-    const res = await listMyInstance(queryParams)
-    instanceList.value = res.list
-    total.value = res.total
-  } finally {
-    loadingInstances.value = false
-  }
+  try { definitionList.value = await listEnabled() } finally { loadingDefinitions.value = false }
 }
 
 function handleStart(def: FlowDefinition) {
-  startForm.value.processDefinitionId = def.id
-  startForm.value.definitionName = def.name
-  startForm.value.title = ''
-  startForm.value.formData = ''
-  dynamicFormData.value = {}
-  selectedDefinition.value = def
-  attachmentList.value = []
+  startForm.value.processDefinitionId = def.id; startForm.value.definitionName = def.name
+  startForm.value.title = ''; startForm.value.formData = ''; dynamicFormData.value = {}
+  selectedDefinition.value = def; attachmentList.value = []
   startDialogVisible.value = true
 }
 
 async function handleUpload(options: any) {
-  try {
-    const res = await uploadFile(options.file)
-    options.onSuccess(res)
-  } catch (e) {
-    options.onError(e)
-  }
+  try { const r = await uploadFile(options.file); options.onSuccess(r) } catch (e) { options.onError(e) }
 }
 
 async function submitStart() {
-  const valid = await startFormRef.value?.validate().catch(() => false)
-  if (!valid) return
-  // 校验动态表单字段;无动态字段时视为校验通过
-  const dynamicValid = flowFormFieldsRef.value?.validate() ?? true
-  if (dynamicValid === false) return
+  const v = await startFormRef.value?.validate().catch(() => false)
+  if (!v) return
   submitting.value = true
   try {
-    let formData: Record<string, unknown> | undefined
-    if (selectedDefinition.value?.formSchema) {
-      formData = { ...dynamicFormData.value }
-    } else if (startForm.value.formData.trim()) {
-      // 兼容旧版无表单字段配置的流程
-      formData = {}
-      startForm.value.formData.split('\n').forEach(line => {
-        const idx = line.indexOf('=')
-        if (idx > 0) {
-          const key = line.substring(0, idx).trim()
-          const value = line.substring(idx + 1).trim()
-          if (key) {
-            const num = Number(value)
-            formData![key] = !isNaN(num) && value !== '' ? num : value
-          }
+    let fd: Record<string, unknown> | undefined
+    if (selectedDefinition.value?.formSchema) { fd = { ...dynamicFormData.value } }
+    else if (startForm.value.formData.trim()) {
+      fd = {}
+      startForm.value.formData.split('\n').forEach(l => {
+        const i = l.indexOf('='); if (i > 0) {
+          const k = l.substring(0, i).trim(), val = l.substring(i + 1).trim()
+          if (k) { const n = Number(val); fd![k] = !isNaN(n) && val !== '' ? n : val }
         }
       })
     }
-    const attachmentUrls = attachmentList.value.length > 0
-      ? collectAttachmentUrls(attachmentList.value)
-      : undefined
-    await startInstance(startForm.value.processDefinitionId, startForm.value.title || undefined, formData, attachmentUrls)
-    ElMessage.success('流程发起成功')
-    startDialogVisible.value = false
-    activeTab.value = 'mine'
-    loadInstances()
-  } finally {
-    submitting.value = false
-  }
-}
-
-function handleView(row: FlowInstance) {
-  currentInstanceId.value = row.id
-  detailVisible.value = true
+    const au = attachmentList.value.length > 0 ? collectAttachmentUrls(attachmentList.value) : undefined
+    await startInstance(startForm.value.processDefinitionId, startForm.value.title || undefined, fd, au)
+    ElMessage.success('流程发起成功'); startDialogVisible.value = false
+  } finally { submitting.value = false }
 }
 
-onMounted(() => {
-  loadDefinitions()
-  loadInstances()
-})
+onMounted(loadDefinitions)
 </script>
 
 <style scoped>
-.card-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-.definition-list {
-  display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
-  gap: 16px;
-}
-.definition-card {
-  cursor: default;
-}
-.def-header {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  margin-bottom: 12px;
-}
-.def-name {
-  font-size: 16px;
-  font-weight: bold;
-  color: #303133;
-}
-.def-info {
-  font-size: 13px;
-  color: #606266;
-  margin-bottom: 12px;
-}
-.def-info p {
-  margin: 4px 0;
-}
-.pagination {
-  margin-top: 20px;
-  justify-content: flex-end;
-}
-.upload-file-item {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-  padding: 4px 0;
-}
+.card-header { display: flex; justify-content: space-between; align-items: center; }
+.definition-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
+.definition-card { cursor: default; }
+.def-header { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
+.def-name { font-size: 16px; font-weight: bold; color: #303133; }
+.def-info { font-size: 13px; color: #606266; margin-bottom: 12px; }
+.def-info p { margin: 4px 0; }
 </style>

+ 120 - 35
src/views/flow/instance/mine.vue

@@ -9,28 +9,30 @@
 
       <el-form :inline="true" :model="queryParams" class="search-form">
         <el-form-item label="流程名称">
-          <el-input v-model="queryParams.processName" placeholder="请输入流程名称" clearable />
+          <el-select v-model="queryParams.processName" placeholder="全部" clearable @change="onProcessNameChange" style="width: 180px;">
+            <el-option v-for="def in definitionList" :key="def.id" :label="def.name" :value="def.name" />
+          </el-select>
         </el-form-item>
-        <el-form-item label="状态">
-          <el-select v-model="queryParams.status" placeholder="全部" clearable>
-            <el-option
-              v-for="opt in statusOptions"
-              :key="opt.value"
-              :label="opt.label"
-              :value="opt.value"
-            />
+        <el-form-item label="当前节点">
+          <el-select v-model="queryParams.currentNodeName" placeholder="全部" clearable :disabled="!queryParams.processName" style="width: 180px;">
+            <el-option v-for="node in currentNodeOptions" :key="node" :label="node" :value="node" />
           </el-select>
         </el-form-item>
         <el-form-item>
           <el-button type="primary" @click="handleQuery">查询</el-button>
           <el-button @click="resetQuery">重置</el-button>
+          <el-button type="success" @click="importDialogVisible = true">导入流程</el-button>
         </el-form-item>
       </el-form>
 
       <el-table v-loading="loading" :data="tableData" border>
         <el-table-column type="index" label="序号" width="60" />
         <el-table-column prop="definitionName" label="流程名称" />
-        <el-table-column prop="businessKey" label="业务编号" />
+        <el-table-column label="业务编号">
+          <template #default="{ row }">
+            {{ row.bizValue || row.instanceNo || '-' }}
+          </template>
+        </el-table-column>
         <el-table-column prop="status" label="状态" width="100">
           <template #default="{ row }">
             <el-tag :type="instanceStatusTagType(row.status)">
@@ -67,15 +69,46 @@
         @size-change="loadData"
       />
     </el-card>
+
+    <!-- 导入流程弹窗 -->
+    <el-dialog v-model="importDialogVisible" title="导入流程" width="550px">
+      <el-form label-width="100px">
+        <el-form-item label="选择流程">
+          <el-select v-model="importDefinitionId" placeholder="请选择要导入的流程" style="width: 100%">
+            <el-option v-for="def in definitionList" :key="def.id" :label="def.name" :value="def.id" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div class="import-buttons" style="margin-left: 100px; margin-bottom: 16px;">
+        <el-button type="primary" :disabled="!importDefinitionId" @click="downloadTemplate">下载Excel模板</el-button>
+        <el-upload
+          :show-file-list="false"
+          :before-upload="handleImportFlow"
+          accept=".xlsx,.xls"
+          action="#"
+          style="display: inline-block; margin-left: 12px;"
+        >
+          <el-button type="success" :disabled="!importDefinitionId" :loading="importing">导入流程</el-button>
+        </el-upload>
+      </div>
+      <el-alert v-if="importDefinitionId" type="info" :closable="false">
+        <template #title>操作说明:先下载模板,填写数据后点击「导入流程」上传。</template>
+      </el-alert>
+      <template #footer>
+        <el-button @click="importDialogVisible = false">关闭</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref, reactive, onMounted } from 'vue'
 import { listMyInstance, revokeInstance, deleteInstance } from '@/api/flow/instance'
+import { listEnabled, getDefinition } from '@/api/flow/definition'
 import InstanceDetail from '@/views/flow/execute/InstanceDetail.vue'
 import { instanceStatusText, instanceStatusTagType } from '@/utils/flow'
-import type { FlowInstance } from '@/types/flow'
+import { downloadTemplate as downloadFlowTemplate, importFlow } from '@/api/flow/import'
+import type { FlowInstance, FlowDefinition } from '@/types/flow'
 import { ElMessage, ElMessageBox } from 'element-plus'
 
 const loading = ref(false)
@@ -83,24 +116,31 @@ const tableData = ref<FlowInstance[]>([])
 const total = ref(0)
 const detailVisible = ref(false)
 const selectedInstanceId = ref<number>(0)
-const statusOptions = [
-  { label: '待接收', value: 0 },
-  { label: '运行中', value: 1 },
-  { label: '已通过', value: 2 },
-  { label: '已拒绝', value: 3 },
-  { label: '已回退', value: 4 },
-  { label: '已完成', value: 5 },
-  { label: '已撤回', value: 6 },
-  { label: '已终止', value: 7 }
-]
+const definitionList = ref<FlowDefinition[]>([])
+const currentNodeOptions = ref<string[]>([])
 
 const queryParams = reactive({
   pageNum: 1,
   pageSize: 10,
   processName: undefined as string | undefined,
-  status: undefined as number | undefined
+  currentNodeName: undefined as string | undefined
 })
 
+function onProcessNameChange() {
+  queryParams.currentNodeName = undefined
+  const def = definitionList.value.find(d => d.name === queryParams.processName)
+  if (def?.flowJson) {
+    try {
+      const model = JSON.parse(def.flowJson)
+      currentNodeOptions.value = (model.nodes || [])
+        .filter((n: any) => n.type !== 'start' && n.type !== 'end' && n.type !== 'condition' && n.type !== 'cc')
+        .map((n: any) => n.name)
+    } catch { currentNodeOptions.value = [] }
+  } else {
+    currentNodeOptions.value = []
+  }
+}
+
 function handleDetail(row: FlowInstance) {
   selectedInstanceId.value = row.id
   detailVisible.value = true
@@ -135,7 +175,8 @@ function handleQuery() {
 
 function resetQuery() {
   queryParams.processName = undefined
-  queryParams.status = undefined
+  queryParams.currentNodeName = undefined
+  currentNodeOptions.value = []
   queryParams.pageNum = 1
   loadData()
 }
@@ -151,20 +192,64 @@ async function loadData() {
   }
 }
 
-onMounted(loadData)
-</script>
+async function loadDefinitions() {
+  try {
+    definitionList.value = await listEnabled()
+  } catch { /* ignore */ }
+}
 
-<style scoped>
-.card-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
+// 导入流程
+const importDialogVisible = ref(false)
+const importDefinitionId = ref<number | null>(null)
+const importing = ref(false)
+
+async function downloadTemplate() {
+  if (!importDefinitionId.value) return
+  try {
+    await downloadFlowTemplate(importDefinitionId.value)
+  } catch {
+    ElMessage.error({ message: '下载模板失败', duration: 3000 })
+  }
 }
-.pagination {
-  margin-top: 20px;
-  justify-content: flex-end;
+
+async function handleImportFlow(file: any) {
+  if (!importDefinitionId.value) return false
+  importing.value = true
+  try {
+    const def = await getDefinition(importDefinitionId.value)
+    const mappings = buildMappings(def)
+    const res = await importFlow(file, importDefinitionId.value, mappings)
+    ElMessage.success({ message: `导入成功:新增 ${res.successCount} 条,更新 ${res.updateCount} 条`, duration: 3000 })
+    importDialogVisible.value = false
+    loadData()
+  } catch (e: any) {
+    ElMessage.error({ message: e?.message || '导入失败', duration: 3000 })
+  } finally {
+    importing.value = false
+  }
+  return false
 }
-.search-form {
-  margin-bottom: 20px;
+
+function buildMappings(def: any): Record<string, string> {
+  const map: Record<string, string> = {}
+  if (def.formSchema) {
+    try {
+      for (const f of JSON.parse(def.formSchema)) {
+        if (f.label && f.name) map[f.label] = f.name
+      }
+    } catch { /* ignore */ }
+  }
+  return map
 }
+
+onMounted(() => {
+  loadDefinitions()
+  loadData()
+})
+</script>
+
+<style scoped>
+.card-header { display: flex; justify-content: space-between; align-items: center; }
+.pagination { margin-top: 20px; justify-content: flex-end; }
+.search-form { margin-bottom: 20px; }
 </style>

+ 16 - 2
src/views/flow/task/todo.vue

@@ -147,6 +147,11 @@
           class="batch-alert"
         />
         <el-form :model="form" label-width="80px">
+          <el-form-item v-if="!batchMode && form.action === 'pass' && nextNodes.length > 0" label="选择分支">
+            <el-select v-model="form.targetNodeId" placeholder="请选择下一节点" style="width: 100%">
+              <el-option v-for="n in nextNodes" :key="n.nodeId" :label="n.nodeName" :value="n.nodeId" />
+            </el-select>
+          </el-form-item>
           <el-form-item v-if="!batchMode && form.action === 'transfer'" label="转办人">
             <el-select v-model="form.transferTo" filterable placeholder="请选择转办人" style="width: 100%">
               <el-option v-for="u in userList" :key="u.id" :label="u.realName || u.username" :value="u.id" />
@@ -216,13 +221,13 @@
 import { ref, reactive, onMounted, computed } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Check, Close, Switch, Plus, View } from '@element-plus/icons-vue'
-import { listTodo, approveTask, rejectTask, transferTask, addSignTask, batchApproveTask, batchRejectTask } from '@/api/flow/task'
+import { listTodo, approveTask, rejectTask, transferTask, addSignTask, batchApproveTask, batchRejectTask, listNextNodes } from '@/api/flow/task'
 import { listUsersByRole } from '@/api/system/role'
 import { listUser } from '@/api/system/user'
 import { uploadFile } from '@/api/file'
 import { getInstanceAttachments } from '@/api/flow/instance'
 import { beforeFileUpload, getUploadUrl, getFileName } from '@/utils/file'
-import type { FlowTask, ApprovalAction, Attachment } from '@/types/flow'
+import type { FlowTask, ApprovalAction, Attachment, NextNode } from '@/types/flow'
 import { useUserStore } from '@/stores/user-store'
 import type { User } from '@/types/system'
 import InstanceDetail from '@/views/flow/execute/InstanceDetail.vue'
@@ -271,6 +276,7 @@ const filteredTableData = computed(() => {
 })
 
 const userList = ref<User[]>([])
+const nextNodes = ref<NextNode[]>([])
 
 // 选择相关
 const selectedIds = ref<number[]>([])
@@ -348,6 +354,9 @@ function buildActionData(): ApprovalAction {
   if (form.action === 'transfer' && form.transferTo) {
     data.transferToUserId = Number(form.transferTo)
   }
+  if (form.action === 'pass' && form.targetNodeId) {
+    data.targetNodeId = form.targetNodeId
+  }
   if (attachmentList.value.length > 0) {
     data.attachmentUrls = JSON.stringify(attachmentList.value.map((f: any) => f.response || f.url).filter(Boolean))
   }
@@ -359,6 +368,11 @@ async function openQuickApprove(row: FlowTask, action: 'pass' | 'reject') {
   currentTask.value = row
   form.action = action
   batchMode.value = false
+  if (action === 'pass') {
+    try { nextNodes.value = await listNextNodes(row.id) } catch { nextNodes.value = [] }
+  } else {
+    nextNodes.value = []
+  }
   await loadPreviousAttachments(row.instanceId)
   approveDrawerVisible.value = true
 }

+ 16 - 2
src/views/workbench/index.vue

@@ -187,6 +187,11 @@
           class="batch-alert"
         />
         <el-form :model="form" label-width="80px">
+          <el-form-item v-if="form.action === 'pass' && nextNodes.length > 0" label="选择分支">
+            <el-select v-model="form.targetNodeId" placeholder="请选择下一节点" style="width: 100%">
+              <el-option v-for="n in nextNodes" :key="n.nodeId" :label="n.nodeName" :value="n.nodeId" />
+            </el-select>
+          </el-form-item>
           <el-form-item v-if="form.action === 'transfer'" label="转办人">
             <el-select v-model="form.transferTo" filterable placeholder="请选择转办人" style="width: 100%">
               <el-option v-for="u in userList" :key="u.id" :label="u.realName || u.username" :value="u.id" />
@@ -253,7 +258,7 @@ import { ref, reactive, onMounted, computed } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import { Check, Close, Switch, Plus, View } from '@element-plus/icons-vue'
-import { listTodo, approveTask, rejectTask, transferTask, addSignTask } from '@/api/flow/task'
+import { listTodo, approveTask, rejectTask, transferTask, addSignTask, listNextNodes } from '@/api/flow/task'
 import { listHandled, listCc } from '@/api/flow/task'
 import { listMyInstance, getInstanceAttachments } from '@/api/flow/instance'
 import { listUser } from '@/api/system/user'
@@ -261,7 +266,7 @@ import { listUsersByRole } from '@/api/system/role'
 import { uploadFile } from '@/api/file'
 import { todoCount } from '@/api/flow/task'
 import { beforeFileUpload, getUploadUrl } from '@/utils/file'
-import type { FlowTask, ApprovalAction, FlowInstance, Attachment } from '@/types/flow'
+import type { FlowTask, ApprovalAction, FlowInstance, Attachment, NextNode } from '@/types/flow'
 import type { User } from '@/types/system'
 import { instanceStatusText, instanceStatusTagType } from '@/utils/flow'
 import InstanceDetail from '@/views/flow/execute/InstanceDetail.vue'
@@ -279,6 +284,7 @@ const ccList = ref<FlowTask[]>([])
 const mineList = ref<FlowInstance[]>([])
 const total = ref(0)
 const userList = ref<User[]>([])
+const nextNodes = ref<NextNode[]>([])
 
 const queryParams = reactive({
   pageNum: 1,
@@ -384,6 +390,9 @@ function buildActionData(): ApprovalAction {
   if (form.action === 'transfer' && form.transferTo) {
     data.transferToUserId = Number(form.transferTo)
   }
+  if (form.action === 'pass' && form.targetNodeId) {
+    data.targetNodeId = form.targetNodeId
+  }
   if (attachmentList.value.length > 0) {
     data.attachmentUrls = JSON.stringify(attachmentList.value.map((f: any) => f.response || f.url).filter(Boolean))
   }
@@ -394,6 +403,11 @@ async function openQuickApprove(row: FlowTask, action: 'pass' | 'reject') {
   resetApproveForm()
   currentTask.value = row
   form.action = action
+  if (action === 'pass') {
+    try { nextNodes.value = await listNextNodes(row.id) } catch { nextNodes.value = [] }
+  } else {
+    nextNodes.value = []
+  }
   await loadPreviousAttachments(row.instanceId)
   approveDrawerVisible.value = true
 }