Explorar el Código

feat: 完成 5 项前端优化

- Element Select 宽度自适应(用户/流程状态筛选)
- 我的待办实例编号列移至最后一列
- 转办/加签选人改为按节点审批角色过滤
- 流程执行页卡片增加眼睛图标查看流程详情
- 流程分析看板移除近 30 天趋势图表
ye-zhaojia hace 1 semana
padre
commit
5d944a8e7f

+ 6 - 0
src/api/flow/task.ts

@@ -1,6 +1,7 @@
 import request from '@/api/request'
 import type { FlowTask, ApprovalAction, NextNode } from '@/types/flow'
 import type { PageQuery, PageResult } from '@/types/api'
+import type { User } from '@/types/system'
 
 export function listTodo(params?: PageQuery): Promise<PageResult<FlowTask>> {
   return request.get('/flow/task/todo', { params })
@@ -57,3 +58,8 @@ export function readCc(taskId: number): Promise<void> {
 export function listNextNodes(taskId: number): Promise<NextNode[]> {
   return request.get(`/flow/task/${taskId}/next-nodes`)
 }
+
+/** 获取可转办/加签的用户列表(按当前节点审批角色过滤) */
+export function getTransferableUsers(taskId: number): Promise<User[]> {
+  return request.get(`/flow/task/${taskId}/transferable-users`)
+}

+ 3 - 92
src/views/analysis/index.vue

@@ -84,7 +84,7 @@
       </el-col>
     </el-row>
 
-    <!-- 状态分布 + 趋势 -->
+    <!-- 状态分布 -->
     <el-row :gutter="16" class="chart-row">
       <el-col :span="12">
         <el-card shadow="hover" v-loading="loadingStatus">
@@ -95,15 +95,6 @@
           <el-empty v-else description="暂无数据" />
         </el-card>
       </el-col>
-      <el-col :span="12">
-        <el-card shadow="hover" v-loading="loadingTrend">
-          <template #header>
-            <span>近30天趋势</span>
-          </template>
-          <v-chart v-if="trendChartOption" :option="trendChartOption" autoresize style="height: 320px" />
-          <el-empty v-else description="暂无数据" />
-        </el-card>
-      </el-col>
     </el-row>
 
     <!-- 效率 + 节点停留 -->
@@ -219,7 +210,7 @@ import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import { use } from 'echarts/core'
 import { CanvasRenderer } from 'echarts/renderers'
-import { BarChart, PieChart, LineChart } from 'echarts/charts'
+import { BarChart, PieChart } from 'echarts/charts'
 import { GridComponent, TooltipComponent, LegendComponent, TitleComponent, DataZoomComponent } from 'echarts/components'
 import VChart from 'vue-echarts'
 import {
@@ -228,7 +219,6 @@ import {
   getStuckInstances,
   getOverview,
   getStatusDistribution,
-  getTrend,
   getNodeCount
 } from '@/api/analysis'
 import { listEnabled } from '@/api/flow/definition'
@@ -241,8 +231,7 @@ import type {
   NodeStayStat,
   StuckInstance,
   AnalysisOverview,
-  StatusDistribution,
-  Trend
+  StatusDistribution
 } from '@/types/analysis'
 import type { FlowDefinition } from '@/types/flow'
 
@@ -250,7 +239,6 @@ use([
   CanvasRenderer,
   BarChart,
   PieChart,
-  LineChart,
   GridComponent,
   TooltipComponent,
   LegendComponent,
@@ -292,10 +280,6 @@ const loadingStatus = ref(false)
 const statusList = ref<StatusDistribution[]>([])
 const statusChartOption = computed(() => buildStatusOption(statusList.value))
 
-const loadingTrend = ref(false)
-const trendList = ref<Trend[]>([])
-const trendChartOption = computed(() => buildTrendOption(trendList.value))
-
 const loadingEfficiency = ref(false)
 const efficiencyList = ref<ProcessEfficiency[]>([])
 const efficiencyChartOption = computed(() => buildEfficiencyOption(efficiencyList.value))
@@ -370,15 +354,6 @@ async function loadStatusDistribution() {
   }
 }
 
-async function loadTrend() {
-  loadingTrend.value = true
-  try {
-    trendList.value = await getTrend(query.processDefinitionId)
-  } finally {
-    loadingTrend.value = false
-  }
-}
-
 async function loadEfficiency() {
   loadingEfficiency.value = true
   try {
@@ -427,7 +402,6 @@ async function loadAll() {
     await Promise.all([
       loadOverview(),
       loadStatusDistribution(),
-      loadTrend(),
       loadEfficiency(),
       loadNodes()
     ])
@@ -496,69 +470,6 @@ function buildStatusOption(list: StatusDistribution[]) {
   }
 }
 
-function buildTrendOption(list: Trend[]) {
-  if (!list.length) return null
-  const dates = list.map((i) => i.date)
-  return {
-    color: colorPalette,
-    tooltip: {
-      trigger: 'axis',
-      formatter: (params: any[]) => {
-        let html = params[0]?.axisValue + '<br/>'
-        params.forEach((p) => {
-          html += `${p.marker} ${p.seriesName}:${p.value} 个<br/>`
-        })
-        return html
-      }
-    },
-    legend: {
-      data: ['发起数', '完成数', '拒绝数'],
-      bottom: 0
-    },
-    grid: { left: '3%', right: '4%', bottom: '12%', top: '10%', containLabel: true },
-    xAxis: {
-      type: 'category',
-      boundaryGap: false,
-      data: dates,
-      axisLabel: { rotate: 30 }
-    },
-    yAxis: { type: 'value', name: '个' },
-    series: [
-      {
-        name: '发起数',
-        type: 'line',
-        smooth: true,
-        data: list.map((i) => i.startedCount),
-        areaStyle: {
-          color: {
-            type: 'linear',
-            x: 0,
-            y: 0,
-            x2: 0,
-            y2: 1,
-            colorStops: [
-              { offset: 0, color: 'rgba(84,112,198,0.4)' },
-              { offset: 1, color: 'rgba(84,112,198,0.05)' }
-            ]
-          }
-        }
-      },
-      {
-        name: '完成数',
-        type: 'line',
-        smooth: true,
-        data: list.map((i) => i.completedCount)
-      },
-      {
-        name: '拒绝数',
-        type: 'line',
-        smooth: true,
-        data: list.map((i) => i.rejectedCount)
-      }
-    ]
-  }
-}
-
 function buildEfficiencyOption(list: ProcessEfficiency[]) {
   if (!list.length) return null
   return {

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

@@ -16,7 +16,7 @@
           <el-input v-model="queryParams.name" placeholder="请输入流程名称" clearable />
         </el-form-item>
         <el-form-item label="状态">
-          <el-select v-model="queryParams.status" placeholder="全部" clearable>
+          <el-select v-model="queryParams.status" placeholder="全部" clearable style="width: auto; min-width: 120px;">
             <el-option label="草稿" :value="0" />
             <el-option label="已发布" :value="1" />
             <el-option label="已停用" :value="2" />

+ 12 - 9
src/views/flow/execute/InstanceDetail.vue

@@ -213,8 +213,7 @@
 import { ref, reactive, computed, watch } from 'vue'
 import { ElMessage } from 'element-plus'
 import { getProgress, getInstanceAttachments } from '@/api/flow/instance'
-import { approveTask, rejectTask, returnTask, transferTask, addSignTask } from '@/api/flow/task'
-import { listUser } from '@/api/system/user'
+import { approveTask, rejectTask, returnTask, transferTask, addSignTask, getTransferableUsers } from '@/api/flow/task'
 import { uploadFile } from '@/api/file'
 import { beforeFileUpload, getFileUrl, getFileName, getUploadUrl, parseAttachments, collectAttachmentUrls } from '@/utils/file'
 import { instanceStatusText, instanceStatusTagType, taskStatusText, taskStatusType, recordActionText, recordActionType } from '@/utils/flow'
@@ -270,11 +269,15 @@ const rollbackNodeOptions = computed<NodeProgress[]>(() => {
 
 const userList = ref<User[]>([])
 
-async function loadUsers() {
+async function loadTransferableUsers() {
+  const task = myPendingTask.value
+  if (!task) {
+    userList.value = []
+    return
+  }
   try {
-    // 静默加载用户列表,避免无权限用户在查看审批详情时被弹窗打扰
-    const res = await listUser({ pageNum: 1, pageSize: 9999 }, { silent: true })
-    userList.value = res.list
+    // 按当前节点审批角色过滤可选用户,已自动排除当前登录人
+    userList.value = await getTransferableUsers(task.id)
   } catch {
     userList.value = []
   }
@@ -464,10 +467,10 @@ watch(() => props.instanceId, (id) => {
   }
 })
 
-watch(() => props.modelValue, (val) => {
+watch(() => props.modelValue, async (val) => {
   if (val && props.instanceId) {
-    loadProgress()
-    loadUsers()
+    await loadProgress()
+    await loadTransferableUsers()
     loadAttachments()
   }
 })

+ 122 - 2
src/views/flow/execute/index.vue

@@ -22,7 +22,10 @@
                 <p v-if="def.category"><strong>分类:</strong>{{ def.category }}</p>
                 <p v-if="def.description"><strong>描述:</strong>{{ def.description }}</p>
               </div>
-              <el-button type="primary" size="small" @click="handleStart(def)">发起流程</el-button>
+              <div class="def-actions">
+                <el-button type="primary" size="small" @click="handleStart(def)">发起流程</el-button>
+                <el-button type="info" size="small" :icon="View" @click="openPreview(def)">查看详情</el-button>
+              </div>
             </el-card>
           </div>
         </el-tab-pane>
@@ -50,6 +53,60 @@
       </template>
     </el-dialog>
 
+    <!-- 流程详情预览弹窗 -->
+    <el-dialog v-model="previewVisible" title="流程详情" width="640px">
+      <div v-if="previewDefinition" class="preview-content">
+        <div class="preview-section">
+          <h4>基本信息</h4>
+          <p><strong>流程名称:</strong>{{ previewDefinition.name }}</p>
+          <p><strong>流程编码:</strong>{{ previewDefinition.code }}</p>
+          <p v-if="previewDefinition.category"><strong>分类:</strong>{{ previewDefinition.category }}</p>
+          <p v-if="previewDefinition.description"><strong>描述:</strong>{{ previewDefinition.description }}</p>
+        </div>
+
+        <div v-if="previewFormFields.length > 0" class="preview-section">
+          <h4>表单字段</h4>
+          <el-table :data="previewFormFields" size="small" border>
+            <el-table-column prop="label" label="显示名" min-width="120" />
+            <el-table-column prop="name" label="字段名" min-width="120" />
+            <el-table-column label="类型" width="90">
+              <template #default="{ row }">
+                {{ fieldTypeText(row.type) }}
+              </template>
+            </el-table-column>
+            <el-table-column label="必填" width="70" align="center">
+              <template #default="{ row }">
+                <el-checkbox v-model="row.required" disabled />
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+
+        <div v-if="previewApprovalNodes.length > 0" class="preview-section">
+          <h4>审批节点</h4>
+          <el-timeline>
+            <el-timeline-item
+              v-for="(node, index) in previewApprovalNodes"
+              :key="node.id"
+              :type="node.type === 'cc-node' ? 'info' : 'primary'"
+            >
+              <div class="preview-node">
+                <span class="preview-node-index">{{ index + 1 }}</span>
+                <span class="preview-node-name">{{ node.name }}</span>
+                <el-tag size="small" :type="node.type === 'cc-node' ? 'info' : 'warning'">
+                  {{ node.type === 'cc-node' ? '抄送' : '审批' }}
+                </el-tag>
+              </div>
+            </el-timeline-item>
+          </el-timeline>
+        </div>
+      </div>
+      <template #footer>
+        <el-button @click="previewVisible = false">关闭</el-button>
+        <el-button type="primary" @click="handleStartFromPreview">发起流程</el-button>
+      </template>
+    </el-dialog>
+
     <InstanceDetail v-model="detailVisible" :instance-id="currentInstanceId" />
   </div>
 </template>
@@ -58,11 +115,12 @@
 import { ref, reactive, computed, onMounted } from 'vue'
 import { ElMessage } from 'element-plus'
 import type { FormInstance, FormRules } from 'element-plus'
+import { View } from '@element-plus/icons-vue'
 import { listEnabled } from '@/api/flow/definition'
 import { startInstance } from '@/api/flow/instance'
 import { uploadFile } from '@/api/file'
 import { beforeFileUpload, collectAttachmentUrls } from '@/utils/file'
-import type { FlowDefinition } from '@/types/flow'
+import type { FlowDefinition, FormField } from '@/types/flow'
 import InstanceDetail from './InstanceDetail.vue'
 import FlowFormFields from '@/components/FlowFormFields/index.vue'
 
@@ -84,6 +142,60 @@ const hasFormFields = computed(() => {
 })
 const detailVisible = ref(false)
 const currentInstanceId = ref(0)
+const previewVisible = ref(false)
+const previewDefinition = ref<FlowDefinition | null>(null)
+
+const previewFormFields = computed<FormField[]>(() => {
+  const schema = previewDefinition.value?.formSchema
+  if (!schema) return []
+  try {
+    const parsed = JSON.parse(schema)
+    return Array.isArray(parsed) ? parsed : []
+  } catch {
+    return []
+  }
+})
+
+interface PreviewNode {
+  id: string
+  type: string
+  name: string
+}
+
+const previewApprovalNodes = computed<PreviewNode[]>(() => {
+  const flowJson = previewDefinition.value?.flowJson
+  if (!flowJson) return []
+  try {
+    const model = JSON.parse(flowJson)
+    const nodes = Array.isArray(model?.nodes) ? model.nodes : []
+    return nodes.filter((n: any) => n?.type === 'approval-node' || n?.type === 'cc-node')
+      .map((n: any) => ({ id: n.id, type: n.type, name: n.name || '未命名节点' }))
+  } catch {
+    return []
+  }
+})
+
+function fieldTypeText(type?: string): string {
+  const map: Record<string, string> = {
+    text: '文本',
+    number: '数字',
+    date: '日期',
+    select: '下拉',
+    textarea: '多行文本'
+  }
+  return map[type || ''] || type || '-'
+}
+
+function openPreview(def: FlowDefinition) {
+  previewDefinition.value = def
+  previewVisible.value = true
+}
+
+function handleStartFromPreview() {
+  if (!previewDefinition.value) return
+  previewVisible.value = false
+  handleStart(previewDefinition.value)
+}
 
 async function loadDefinitions() {
   loadingDefinitions.value = true
@@ -134,4 +246,12 @@ onMounted(loadDefinitions)
 .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; }
+.def-actions { display: flex; gap: 8px; }
+.preview-content { max-height: 60vh; overflow-y: auto; padding-right: 8px; }
+.preview-section { margin-bottom: 20px; }
+.preview-section h4 { margin: 0 0 12px 0; font-size: 15px; color: #303133; border-left: 4px solid #409eff; padding-left: 8px; }
+.preview-section p { margin: 6px 0; color: #606266; font-size: 14px; }
+.preview-node { display: flex; align-items: center; gap: 8px; }
+.preview-node-index { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; background-color: #f0f2f5; color: #606266; font-size: 12px; }
+.preview-node-name { font-size: 14px; color: #303133; }
 </style>

+ 19 - 32
src/views/flow/task/todo.vue

@@ -80,12 +80,6 @@
             </template>
           </el-table-column>
 
-          <el-table-column label="实例编号" width="170" show-overflow-tooltip>
-            <template #default="{ row }">
-              <span class="text-muted">{{ row.instanceNo || '—' }}</span>
-            </template>
-          </el-table-column>
-
           <!-- 动态流程表单字段列 -->
           <el-table-column
             v-for="field in dynamicFields"
@@ -115,6 +109,12 @@
             </template>
           </el-table-column>
 
+          <el-table-column label="实例编号" width="170" show-overflow-tooltip>
+            <template #default="{ row }">
+              <span class="text-muted">{{ row.instanceNo || '—' }}</span>
+            </template>
+          </el-table-column>
+
           <el-table-column label="操作" width="220" fixed="right">
             <template #default="{ row }">
               <div class="operation-cell">
@@ -273,10 +273,8 @@
 import { ref, reactive, onMounted, computed } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { Check, Close, Switch, Plus, View, ArrowDown } from '@element-plus/icons-vue'
-import { listTodo, approveTask, rejectTask, transferTask, addSignTask, batchApproveTask, batchRejectTask, listNextNodes } from '@/api/flow/task'
+import { listTodo, approveTask, rejectTask, transferTask, addSignTask, batchApproveTask, batchRejectTask, listNextNodes, getTransferableUsers } from '@/api/flow/task'
 import { listEnabled } from '@/api/flow/definition'
-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'
@@ -498,17 +496,11 @@ async function openTransfer(row: FlowTask) {
   currentTask.value = row
   form.action = 'transfer'
   batchMode.value = false
-  // 转办选人:ROLE 任务查同角色用户,USER 任务查全部用户,过滤掉自己
-  if (row.assigneeType === 'ROLE' && row.assigneeId) {
-    try {
-      const users = await listUsersByRole(row.assigneeId)
-      userList.value = users.filter(u => u.id !== userStore.userInfo?.id)
-    } catch {
-      userList.value = []
-    }
-  } else {
-    await loadUsers()
-    userList.value = userList.value.filter(u => u.id !== userStore.userInfo?.id)
+  // 转办选人:按当前节点审批角色过滤可选用户,已自动排除当前登录人
+  try {
+    userList.value = await getTransferableUsers(row.id)
+  } catch {
+    userList.value = []
   }
   await loadPreviousAttachments(row.instanceId)
   approveDrawerVisible.value = true
@@ -610,9 +602,15 @@ const addSignTaskId = ref<number>(0)
 const addSignUserId = ref<number | undefined>(undefined)
 const addSignLoading = ref(false)
 
-function handleAddSign(row: FlowTask) {
+async function handleAddSign(row: FlowTask) {
   addSignTaskId.value = row.id
   addSignUserId.value = undefined
+  // 加签选人:按当前节点审批角色过滤可选用户,已自动排除当前登录人
+  try {
+    userList.value = await getTransferableUsers(row.id)
+  } catch {
+    userList.value = []
+  }
   addSignVisible.value = true
 }
 
@@ -661,16 +659,6 @@ async function loadData() {
   }
 }
 
-async function loadUsers() {
-  try {
-    // 静默加载用户列表,避免普通用户进入待办页时被提示“权限不足”
-    const res = await listUser({ pageNum: 1, pageSize: 9999 }, { silent: true })
-    userList.value = res.list
-  } catch {
-    userList.value = []
-  }
-}
-
 function handleQuery() {
   queryParams.pageNum = 1
   loadData()
@@ -693,7 +681,6 @@ async function loadDefinitions() {
 
 onMounted(() => {
   loadData()
-  loadUsers()
   loadDefinitions()
 })
 </script>

+ 1 - 1
src/views/system/user/index.vue

@@ -13,7 +13,7 @@
           <el-input v-model="queryParams.username" placeholder="请输入账号" clearable />
         </el-form-item>
         <el-form-item label="状态">
-          <el-select v-model="queryParams.status" placeholder="全部" clearable>
+          <el-select v-model="queryParams.status" placeholder="全部" clearable style="width: auto; min-width: 120px;">
             <el-option label="正常" :value="0" />
             <el-option label="禁用" :value="1" />
           </el-select>

+ 13 - 24
src/views/workbench/index.vue

@@ -258,11 +258,9 @@ 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, listNextNodes } from '@/api/flow/task'
+import { listTodo, approveTask, rejectTask, transferTask, addSignTask, listNextNodes, getTransferableUsers } from '@/api/flow/task'
 import { listHandled, listCc } from '@/api/flow/task'
 import { listMyInstance, getInstanceAttachments } from '@/api/flow/instance'
-import { listUser } from '@/api/system/user'
-import { listUsersByRole } from '@/api/system/role'
 import { uploadFile } from '@/api/file'
 import { todoCount } from '@/api/flow/task'
 import { beforeFileUpload, getUploadUrl } from '@/utils/file'
@@ -416,17 +414,11 @@ async function openTransfer(row: FlowTask) {
   resetApproveForm()
   currentTask.value = row
   form.action = 'transfer'
-  // 转办选人:ROLE 任务查同角色用户,USER 任务查全部用户,过滤掉自己
-  if (row.assigneeType === 'ROLE' && row.assigneeId) {
-    try {
-      const users = await listUsersByRole(row.assigneeId)
-      userList.value = users.filter(u => u.id !== userStore.userInfo?.id)
-    } catch {
-      userList.value = []
-    }
-  } else {
-    await loadUsers()
-    userList.value = userList.value.filter(u => u.id !== userStore.userInfo?.id)
+  // 转办选人:按当前节点审批角色过滤可选用户,已自动排除当前登录人
+  try {
+    userList.value = await getTransferableUsers(row.id)
+  } catch {
+    userList.value = []
   }
   await loadPreviousAttachments(row.instanceId)
   approveDrawerVisible.value = true
@@ -506,9 +498,15 @@ const addSignTaskId = ref<number>(0)
 const addSignUserId = ref<number | undefined>(undefined)
 const addSignLoading = ref(false)
 
-function handleAddSign(row: FlowTask) {
+async function handleAddSign(row: FlowTask) {
   addSignTaskId.value = row.id
   addSignUserId.value = undefined
+  // 加签选人:按当前节点审批角色过滤可选用户,已自动排除当前登录人
+  try {
+    userList.value = await getTransferableUsers(row.id)
+  } catch {
+    userList.value = []
+  }
   addSignVisible.value = true
 }
 
@@ -529,18 +527,9 @@ async function submitAddSign() {
   }
 }
 
-async function loadUsers() {
-  try {
-    // 静默加载用户列表,避免普通用户进入工作台时被提示“权限不足”
-    const res = await listUser({ pageNum: 1, pageSize: 9999 }, { silent: true })
-    userList.value = res.list
-  } catch { userList.value = [] }
-}
-
 onMounted(() => {
   loadData()
   loadStats()
-  loadUsers()
 })
 </script>