Browse Source

feat: refine devops delivery and software workflows

程健 4 months ago
parent
commit
7e2771dfa9

+ 2 - 2
.env.development

@@ -15,12 +15,12 @@ VUE_APP_WEBSOCKET_URL=ws://39.105.105.147:9188/ws
 # GateWay地址
 VUE_APP_MicroSrvProxy_API=http://192.168.0.221:9189/api/
 # 登录验证微服务名称
-VUE_APP_AdminPath=dashoo.opms.admin-0.0.1
+VUE_APP_AdminPath=dashoo.opms.admin-0.0.1-cj
 # 业务接口微服务名称
 VUE_APP_ParentPath=dashoo.opms.parent-0.0.2-cj
 
 # 文件上传
-VUE_APP_UPLOAD_WEED='/api/weedfs/'
+VUE_APP_UPLOAD_WEED='/dir/'
 # 文件一步上传
 VUE_APP_UPLOAD_FILE_WEED=/weedfs/
 VUE_APP_RICHTEXT_UPLOAD_API=/weed_filer/

+ 10 - 0
src/api/devops/opsEventTask.js

@@ -77,4 +77,14 @@ export default {
   addRecord(data) {
     return micro_request.postRequest(basePath, 'OpsEventTask', 'AddRecord', data)
   },
+
+  // 添加工时登记
+  addWorkHour(data) {
+    return micro_request.postRequest(basePath, 'OpsEventTask', 'AddWorkHour', data)
+  },
+
+  // 获取工时登记列表
+  getWorkHourList(taskId) {
+    return micro_request.postRequest(basePath, 'OpsEventTask', 'GetWorkHourList', { taskId })
+  },
 }

+ 7 - 0
src/api/system/dict.js

@@ -9,6 +9,13 @@ export default {
     })
   },
 
+  // 批量获取多个字典信息
+  getDictDataByTypes(dictTypes) {
+    return micro_request.postRequest(basePath, 'Dict', 'GetDictDataByTypes', {
+      dictTypes: dictTypes,
+    })
+  },
+
   // 字典类型
   getDictTypeList(query) {
     return micro_request.postRequest(basePath, 'Dict', 'GetDictTypeList', query)

+ 47 - 0
src/config/devopsTagTypes.js

@@ -0,0 +1,47 @@
+/**
+ * DevOps 模块 Tag 类型映射
+ * 将字典值映射为 Element UI el-tag 的 type 属性(info/warning/danger/success/primary)
+ * 这些属于 UI 呈现逻辑,不适合存入数据字典
+ */
+
+export const taskStatusTagTypes = {
+  10: 'info',
+  20: 'primary',
+  25: 'warning',
+  30: 'success',
+  70: 'danger',
+  90: 'info',
+}
+
+export const priorityTagTypes = {
+  10: 'danger',
+  20: 'warning',
+  30: 'primary',
+  40: 'info',
+}
+
+export const projectStatusTagTypes = {
+  10: 'info',
+  20: 'warning',
+  30: 'danger',
+  40: 'success',
+  50: 'success',
+  90: 'info',
+}
+
+export const deliveryEventStatusTagTypes = {
+  10: 'info',
+  20: 'warning',
+  30: 'success',
+  90: 'info',
+}
+
+export const opsPriorityTagTypes = {
+  P1: 'danger',
+  P2: 'warning',
+  P3: 'success',
+}
+
+export function getTagType(tagMap, value, fallback = '') {
+  return tagMap[String(value)] || fallback
+}

+ 11 - 1
src/main.js

@@ -17,13 +17,23 @@ import 'vxe-table/lib/style.css'
 
 import { parseTime, translateDataToTree, resetForm, formatPrice, selectDictLabel } from '@/utils'
 import dictApi from '@/api/system/dict'
+import { getCachedDict, setCachedDict } from '@/utils/dictCache'
 
 import PostComment from '@/components/postComments/index.js'
 
 Vue.prototype.$PostComment = PostComment
 Vue.prototype.parseTime = parseTime
 Vue.prototype.translateDataToTree = translateDataToTree
-Vue.prototype.getDicts = dictApi.getDictDataByType
+Vue.prototype.getDicts = function (dictType) {
+  const cached = getCachedDict(dictType)
+  if (cached) {
+    return Promise.resolve(cached)
+  }
+  return dictApi.getDictDataByType(dictType).then((res) => {
+    setCachedDict(dictType, res)
+    return res
+  })
+}
 Vue.prototype.resetForm = resetForm
 Vue.prototype.selectDictLabel = selectDictLabel
 Vue.prototype.formatPrice = formatPrice

+ 29 - 0
src/utils/dictCache.js

@@ -0,0 +1,29 @@
+const CACHE_PREFIX = 'opms_dict_'
+const CACHE_TTL = 30 * 60 * 1000
+
+export function getCachedDict(dictType) {
+  try {
+    const raw = localStorage.getItem(CACHE_PREFIX + dictType)
+    if (!raw) return null
+    const { data, timestamp } = JSON.parse(raw)
+    if (Date.now() - timestamp < CACHE_TTL) {
+      return data
+    }
+  } catch (_) {
+    /* corrupt entry — treat as miss */
+  }
+  return null
+}
+
+export function setCachedDict(dictType, data) {
+  try {
+    localStorage.setItem(CACHE_PREFIX + dictType, JSON.stringify({ data, timestamp: Date.now() }))
+  } catch (_) {
+    /* quota exceeded — silently skip */
+  }
+}
+
+export function clearDictCache() {
+  const keys = Object.keys(localStorage).filter((k) => k.startsWith(CACHE_PREFIX))
+  keys.forEach((k) => localStorage.removeItem(k))
+}

+ 30 - 35
src/views/contract/components/Edit.vue

@@ -248,7 +248,7 @@
         <el-col class="proj-col" :span="12">
           <el-upload
             ref="uploadRef"
-            action="#"
+            :action="uploadFileUrl"
             :before-upload="beforeAvatarUpload"
             :http-request="uploadrequest"
             multiple>
@@ -327,7 +327,6 @@
   import enclosureApi from '@/api/contract/enclosure'
   import businessApi from '@/api/proj/business'
   import asyncUploadFile from '@/utils/uploadajax'
-  import axios from 'axios'
   import ProductTable from './ProductTable'
   import SelectBusiness from '@/components/select/SelectBusiness'
   import SelectDistributor from '@/components/select/SelectDistributor'
@@ -445,6 +444,7 @@
         businessUserQueryParams: {}, //查询客户签约人参数
         // 附件相关
         fileList: [],
+        uploadFileUrl: process.env.VUE_APP_UPLOAD_FILE_WEED,
         fileSettings: {
           fileSize: 52428800,
           fileTypes: '.pdf,.doc,.docx,.zip,.xls,.xlsx,.rar,.jpg,.jpeg,.gif,.png,.jfif,.txt,.mp4',
@@ -698,43 +698,38 @@
         return true
       },
       // 上传附件
-      uploadrequest(option) {
-        let _this = this
-        let url = process.env.VUE_APP_UPLOAD_WEED
-        axios
-          .post(url)
-          .then(function (res) {
-            if (res.data && res.data.fid && res.data.fid !== '') {
-              option.action = `${process.env.VUE_APP_PROTOCOL}${res.data.publicUrl}/${res.data.fid}`
-              let file_name = option.file.name
-              let index = file_name.lastIndexOf('.')
-              let file_extend = ''
-              if (index > 0) {
-                file_extend = file_name.substr(index + 1)
-              }
-              asyncUploadFile(option).then(() => {
-                let enclosureItem = {
-                  fileName: file_name,
-                  fileUrl: `${process.env.VUE_APP_PROTOCOL}${res.data.publicUrl}/${res.data.fid}`,
-                  size: option.file.size.toString(),
-                  fileType: file_extend,
-                }
-                _this.enclosureData.push(enclosureItem)
-                _this.$message.success('上传成功')
-              })
-            } else {
-              _this.$message({
-                type: 'warning',
-                message: '未上传成功!请刷新界面重新上传!',
-              })
-            }
-          })
-          .catch(function () {
-            _this.$message({
+      async uploadrequest(option) {
+        option.action = this.uploadFileUrl
+        try {
+          const res = await asyncUploadFile(option)
+          const uploadRes = typeof res === 'string' ? JSON.parse(res) : res
+          if (!uploadRes || uploadRes.Code !== 200 || !uploadRes.Data) {
+            this.$message({
               type: 'warning',
               message: '未上传成功!请重新上传!',
             })
+            return
+          }
+          let file_name = option.file.name
+          let index = file_name.lastIndexOf('.')
+          let file_extend = ''
+          if (index > 0) {
+            file_extend = file_name.substr(index + 1)
+          }
+          let enclosureItem = {
+            fileName: file_name,
+            fileUrl: uploadRes.Data,
+            size: option.file.size.toString(),
+            fileType: file_extend,
+          }
+          this.enclosureData.push(enclosureItem)
+          this.$message.success('上传成功')
+        } catch (error) {
+          this.$message({
+            type: 'warning',
+            message: '未上传成功!请重新上传!',
           })
+        }
       },
       // 删除附件
       handleDelEnclosure(row) {

+ 21 - 50
src/views/devops/components/ProjectInfoDialog.vue

@@ -81,15 +81,15 @@
           <el-col :span="12">
             <div class="info-item">
               <span class="info-label">项目状态:</span>
-              <el-tag size="small" :type="getStatusType(projectData.projectStatus)">
-                {{ getStatusLabel(projectData.projectStatus) }}
+              <el-tag size="small" :type="getProjectStatusTagType(projectData.projectStatus)">
+                {{ selectDictLabel(projectStatusOptions, projectData.projectStatus) }}
               </el-tag>
             </div>
           </el-col>
           <el-col :span="12">
             <div class="info-item">
               <span class="info-label">交付节点:</span>
-              <span class="info-value">{{ getDeliveryNodeLabel(projectData.deliveryNode) }}</span>
+              <span class="info-value">{{ selectDictLabel(deliveryNodeOptions, projectData.deliveryNode) }}</span>
             </div>
           </el-col>
         </el-row>
@@ -184,6 +184,7 @@
 <script>
   import deliveryProjectApi from '@/api/devops/deliveryProject'
   import { parseTime } from '@/utils'
+  import { projectStatusTagTypes, getTagType } from '@/config/devopsTagTypes'
 
   export default {
     name: 'ProjectInfoDialog',
@@ -202,6 +203,8 @@
         loading: false,
         projectData: {},
         productLineDict: [],
+        projectStatusOptions: [],
+        deliveryNodeOptions: [],
         // 编辑状态
         editingPlanDelivery: false,
         editingPlanAccept: false,
@@ -282,14 +285,23 @@
       },
     },
     mounted() {
-      this.loadProductLineDict()
+      this.getOptions()
     },
     methods: {
-      // 加载产品线字典
-      loadProductLineDict() {
-        this.getDicts('sys_product_line')
-          .then((res) => {
-            this.productLineDict = res.data.values || []
+      getTagType,
+      getProjectStatusTagType(status) {
+        return getTagType(projectStatusTagTypes, status, 'info')
+      },
+      getOptions() {
+        Promise.all([
+          this.getDicts('sys_product_line'),
+          this.getDicts('delivery_project_status'),
+          this.getDicts('delivery_node'),
+        ])
+          .then(([productLine, projectStatus, deliveryNode]) => {
+            this.productLineDict = productLine.data.values || []
+            this.projectStatusOptions = projectStatus.data.values || []
+            this.deliveryNodeOptions = deliveryNode.data.values || []
           })
           .catch((err) => console.log(err))
       },
@@ -499,47 +511,6 @@
         return label || productLine || '-'
       },
 
-      // 获取状态标签
-      getStatusLabel(status) {
-        const map = {
-          10: '待交付',
-          20: '交付中',
-          30: '暂停',
-          40: '交付完成',
-          50: '已验收',
-          90: '作废',
-        }
-        return map[status] || status || '-'
-      },
-
-      // 获取状态样式
-      getStatusType(status) {
-        const map = {
-          10: 'info',
-          20: 'primary',
-          30: 'warning',
-          40: 'success',
-          50: 'success',
-          90: 'danger',
-        }
-        return map[status] || 'info'
-      },
-
-      // 获取交付节点标签
-      getDeliveryNodeLabel(node) {
-        const map = {
-          '05': '已指派',
-          10: '内部启动会',
-          15: '外部启动会',
-          20: '制定计划',
-          30: '项目实施',
-          40: '完成部署',
-          50: '试运行',
-          60: '交付完成',
-        }
-        return map[node] || node || '-'
-      },
-
       handleClose() {
         this.projectData = {}
         this.resetEditState()

+ 37 - 38
src/views/devops/deliveryHardware/components/DeliveryHardwareEventDetail.vue

@@ -47,7 +47,9 @@
             <i class="el-icon-s-promotion" />
           </div>
           <div class="card-content">
-            <div class="card-value">{{ (detailData && getStatusLabel(detailData.deliveryEventStatus)) || '-' }}</div>
+            <div class="card-value">
+              {{ (detailData && selectDictLabel(deliveryEventStatusOptions, detailData.deliveryEventStatus)) || '-' }}
+            </div>
             <div class="card-label">当前状态</div>
           </div>
         </div>
@@ -56,7 +58,9 @@
             <i class="el-icon-warning" />
           </div>
           <div class="card-content">
-            <div class="card-value">{{ (detailData && getEventTypeLabel(detailData.deliveryEventType)) || '-' }}</div>
+            <div class="card-value">
+              {{ (detailData && selectDictLabel(deliveryEventTypeOptions, detailData.deliveryEventType)) || '-' }}
+            </div>
             <div class="card-label">事件类型</div>
           </div>
         </div>
@@ -89,13 +93,13 @@
               <div class="property-item">
                 <span class="property-label">事件类型</span>
                 <span class="property-value">
-                  {{ (detailData && getEventTypeLabel(detailData.deliveryEventType)) || '-' }}
+                  {{ (detailData && selectDictLabel(deliveryEventTypeOptions, detailData.deliveryEventType)) || '-' }}
                 </span>
               </div>
               <div class="property-item">
                 <span class="property-label">反馈来源</span>
                 <span class="property-value">
-                  {{ (detailData && getFeedbackSourceLabel(detailData.feedbackSource)) || '-' }}
+                  {{ (detailData && selectDictLabel(feedbackSourceOptions, detailData.feedbackSource)) || '-' }}
                 </span>
               </div>
               <div class="property-item">
@@ -133,8 +137,11 @@
             <el-form ref="processForm" label-width="90px" :model="processForm" :rules="processRules" size="small">
               <el-form-item label="处理结果" prop="deliveryEventResult">
                 <el-select v-model="processForm.deliveryEventResult" placeholder="请选择处理结果" style="width: 100%">
-                  <el-option label="已解决" value="10" />
-                  <el-option label="未解决" value="30" />
+                  <el-option
+                    v-for="dict in deliveryEventResultOptions"
+                    :key="dict.key"
+                    :label="dict.value"
+                    :value="dict.key" />
                 </el-select>
               </el-form-item>
               <el-form-item label="处理方案" prop="completeDesc">
@@ -280,8 +287,11 @@
       <el-form ref="closeEventForm" label-width="100px" :model="closeEventForm" :rules="closeEventRules" size="small">
         <el-form-item label="解决状态" prop="deliveryEventResult">
           <el-select v-model="closeEventForm.deliveryEventResult" placeholder="请选择解决状态" style="width: 100%">
-            <el-option label="已解决" value="10" />
-            <el-option label="未解决" value="30" />
+            <el-option
+              v-for="dict in deliveryEventResultOptions"
+              :key="dict.key"
+              :label="dict.value"
+              :value="dict.key" />
           </el-select>
         </el-form-item>
         <el-form-item label="关闭原因" prop="closeReason">
@@ -394,6 +404,10 @@
         closeEventFileList: [],
         closeEventUploadFiles: [],
         productLineDict: [],
+        deliveryEventTypeOptions: [],
+        deliveryEventStatusOptions: [],
+        deliveryEventResultOptions: [],
+        feedbackSourceOptions: [],
       }
     },
     computed: {
@@ -441,7 +455,7 @@
       },
     },
     created() {
-      this.loadProductLineDict()
+      this.getOptions()
     },
     beforeDestroy() {
       if (this.quickEditor) {
@@ -451,10 +465,20 @@
     },
     methods: {
       sanitizeHtml,
-      loadProductLineDict() {
-        this.getDicts('sys_product_line')
-          .then((res) => {
-            this.productLineDict = res.data.values || []
+      getOptions() {
+        Promise.all([
+          this.getDicts('sys_product_line'),
+          this.getDicts('delivery_event_type'),
+          this.getDicts('delivery_event_status'),
+          this.getDicts('delivery_event_result'),
+          this.getDicts('feedback_source'),
+        ])
+          .then(([productLine, eventType, eventStatus, eventResult, feedbackSource]) => {
+            this.productLineDict = productLine.data.values || []
+            this.deliveryEventTypeOptions = eventType.data.values || []
+            this.deliveryEventStatusOptions = eventStatus.data.values || []
+            this.deliveryEventResultOptions = eventResult.data.values || []
+            this.feedbackSourceOptions = feedbackSource.data.values || []
           })
           .catch((err) => console.log(err))
       },
@@ -767,31 +791,6 @@
         this.processFileList = []
         this.processUploadFiles = []
       },
-      getStatusLabel(status) {
-        const map = {
-          10: '待处理',
-          20: '处理中',
-          30: '已解决',
-          40: '已关闭',
-        }
-        return map[status] || status
-      },
-      getEventTypeLabel(type) {
-        // 硬件交付模块只支持硬件发货和硬件安装
-        const map = {
-          40: '硬件发货',
-          41: '硬件安装',
-        }
-        return map[type] || type
-      },
-      getFeedbackSourceLabel(source) {
-        const map = {
-          10: '客户',
-          20: '销售',
-          30: '交付',
-        }
-        return map[source] || source
-      },
       getProductLineLabel(productLine) {
         const label = this.selectDictLabel(this.productLineDict, productLine)
         return label || productLine || '-'

+ 26 - 8
src/views/devops/deliveryHardware/components/DeliveryHardwareEventEdit.vue

@@ -49,18 +49,18 @@
         <el-col :span="12">
           <el-form-item label="事件类型" prop="deliveryEventType">
             <el-select v-model="form.deliveryEventType" placeholder="请选择事件类型" style="width: 100%">
-              <!-- 硬件交付模块只支持硬件发货和硬件安装 -->
-              <el-option label="硬件发货" value="40" />
-              <el-option label="硬件安装" value="41" />
+              <el-option
+                v-for="dict in deliveryEventTypeOptions"
+                :key="dict.key"
+                :label="dict.value"
+                :value="dict.key" />
             </el-select>
           </el-form-item>
         </el-col>
         <el-col :span="12">
           <el-form-item label="反馈来源" prop="feedbackSource">
             <el-select v-model="form.feedbackSource" placeholder="请选择反馈来源" style="width: 100%">
-              <el-option label="客户" value="10" />
-              <el-option label="销售" value="20" />
-              <el-option label="交付" value="30" />
+              <el-option v-for="dict in feedbackSourceOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
             </el-select>
           </el-form-item>
         </el-col>
@@ -75,8 +75,7 @@
         <el-col :span="12">
           <el-form-item label="是否现场" prop="onSite">
             <el-select v-model="form.onSite" placeholder="请选择" style="width: 100%">
-              <el-option label="是" value="10" />
-              <el-option label="否" value="20" />
+              <el-option v-for="dict in onSiteOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
             </el-select>
           </el-form-item>
         </el-col>
@@ -187,6 +186,9 @@
         fileList: [],
         uploadFiles: [],
         projectOptions: [],
+        deliveryEventTypeOptions: [],
+        feedbackSourceOptions: [],
+        onSiteOptions: [],
       }
     },
     computed: {
@@ -210,6 +212,7 @@
       },
     },
     created() {
+      this.getOptions()
       this.remoteSearchProject = debounce((query) => {
         this.searchAllProjectsByStatus(query)
       }, 300)
@@ -224,6 +227,21 @@
       }
     },
     methods: {
+      getOptions() {
+        Promise.all([
+          this.getDicts('delivery_event_type'),
+          this.getDicts('feedback_source'),
+          this.getDicts('sys_yes_no'),
+        ])
+          .then(([eventType, feedbackSource, onSite]) => {
+            this.deliveryEventTypeOptions = (eventType.data.values || []).filter((item) =>
+              ['40', '41'].includes(String(item.key))
+            )
+            this.feedbackSourceOptions = feedbackSource.data.values || []
+            this.onSiteOptions = onSite.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
       onEditorCreated(editor) {
         this.editor = editor
       },

+ 51 - 79
src/views/devops/deliveryHardware/index.vue

@@ -14,9 +14,11 @@
               multiple
               placeholder="请选择"
               style="width: 140px">
-              <el-option label="待处理" value="10" />
-              <el-option label="处理中" value="20" />
-              <el-option label="已关闭" value="30" />
+              <el-option
+                v-for="dict in deliveryEventStatusOptions"
+                :key="dict.key"
+                :label="dict.value"
+                :value="dict.key" />
             </el-select>
           </el-form-item>
           <el-form-item label="反馈人">
@@ -93,7 +95,7 @@
                   <span class="project-line-tag">{{ getProductLineLabel(project.productLine) }}</span>
                 </div>
                 <span :class="['project-status-tag', 'project-status-tag--' + project.status]">
-                  {{ getStatusLabel(project.status) }}
+                  {{ selectDictLabel(projectStatusOptions, project.status) }}
                 </span>
               </div>
               <div class="project-card-title" :title="project.name">{{ project.name }}</div>
@@ -144,7 +146,7 @@
             <el-table-column label="事件类型" prop="deliveryEventType" sortable="custom" width="140">
               <template slot-scope="{ row }">
                 <el-tag size="small" type="info">
-                  {{ getDeliveryEventTypeLabel(row.deliveryEventType || row.delivery_event_type) }}
+                  {{ selectDictLabel(deliveryEventTypeOptions, row.deliveryEventType || row.delivery_event_type) }}
                 </el-tag>
               </template>
             </el-table-column>
@@ -152,14 +154,16 @@
               <template slot-scope="{ row }">
                 <el-tag
                   size="small"
-                  :type="getDeliveryEventStatusType(row.deliveryEventStatus || row.delivery_event_status)">
-                  {{ getDeliveryEventStatusLabel(row.deliveryEventStatus || row.delivery_event_status) }}
+                  :type="getDeliveryEventStatusTagType(row.deliveryEventStatus || row.delivery_event_status)">
+                  {{
+                    selectDictLabel(deliveryEventStatusOptions, row.deliveryEventStatus || row.delivery_event_status)
+                  }}
                 </el-tag>
               </template>
             </el-table-column>
             <el-table-column label="事件结果" prop="deliveryEventResult" sortable="custom" width="120">
               <template slot-scope="{ row }">
-                {{ getDeliveryEventResultLabel(row.deliveryEventResult || row.delivery_event_result) }}
+                {{ selectDictLabel(deliveryEventResultOptions, row.deliveryEventResult || row.delivery_event_result) }}
               </template>
             </el-table-column>
             <el-table-column label="负责人" prop="opsUserName" show-overflow-tooltip sortable="custom" width="130">
@@ -179,7 +183,7 @@
             </el-table-column>
             <el-table-column label="是否现场" width="90">
               <template slot-scope="{ row }">
-                {{ getOnSiteLabel(row.onSite || row.on_site) }}
+                {{ selectDictLabel(onSiteOptions, row.onSite || row.on_site) }}
               </template>
             </el-table-column>
             <el-table-column label="反馈人" prop="feedbackReporter" show-overflow-tooltip sortable="custom" width="130">
@@ -194,7 +198,7 @@
             </el-table-column>
             <el-table-column label="反馈来源" width="100">
               <template slot-scope="{ row }">
-                {{ getFeedbackSourceLabel(row.feedbackSource || row.feedback_source) }}
+                {{ selectDictLabel(feedbackSourceOptions, row.feedbackSource || row.feedback_source) }}
               </template>
             </el-table-column>
             <el-table-column fixed="right" header-align="center" label="操作" width="160">
@@ -262,8 +266,11 @@
       <el-form ref="closeEventForm" label-width="100px" :model="closeEventForm" :rules="closeEventRules" size="small">
         <el-form-item label="解决状态" prop="deliveryEventResult">
           <el-select v-model="closeEventForm.deliveryEventResult" placeholder="请选择解决状态" style="width: 100%">
-            <el-option label="已解决" value="10" />
-            <el-option label="未解决" value="30" />
+            <el-option
+              v-for="dict in deliveryEventResultOptions"
+              :key="dict.key"
+              :label="dict.value"
+              :value="dict.key" />
           </el-select>
         </el-form-item>
         <el-form-item label="关闭原因" prop="closeReason">
@@ -299,6 +306,8 @@
   import { parseTime } from '@/utils'
   import { uploadFileToRichtextServer } from '@/utils/richtextUpload'
   import { escapeHtml, sanitizeHtml } from '@/utils/safeHtml'
+  import { deliveryEventStatusTagTypes, getTagType } from '@/config/devopsTagTypes'
+  import dictApi from '@/api/system/dict'
   import store from '@/store'
 
   export default {
@@ -332,6 +341,12 @@
         editDialogVisible: false,
         editData: null,
         productLineDict: [],
+        projectStatusOptions: [],
+        deliveryEventTypeOptions: [],
+        deliveryEventStatusOptions: [],
+        deliveryEventResultOptions: [],
+        feedbackSourceOptions: [],
+        onSiteOptions: [],
         detailVisible: false,
         detailMode: 'view',
         currentRow: null,
@@ -368,17 +383,37 @@
       },
     },
     created() {
-      this.loadProductLineDict()
+      this.getOptions()
       this.fetchData()
     },
     methods: {
+      getTagType,
+      getDeliveryEventStatusTagType(status) {
+        return getTagType(deliveryEventStatusTagTypes, status, 'info')
+      },
       parseTime,
       sanitizeHtml,
 
-      loadProductLineDict() {
-        this.getDicts('sys_product_line')
+      getOptions() {
+        dictApi
+          .getDictDataByTypes([
+            'sys_product_line',
+            'delivery_project_status',
+            'delivery_event_type',
+            'delivery_event_status',
+            'delivery_event_result',
+            'feedback_source',
+            'sys_yes_no',
+          ])
           .then((res) => {
-            this.productLineDict = res.data.values || []
+            const dicts = res.data || {}
+            this.productLineDict = (dicts.sys_product_line && dicts.sys_product_line.values) || []
+            this.projectStatusOptions = (dicts.delivery_project_status && dicts.delivery_project_status.values) || []
+            this.deliveryEventTypeOptions = (dicts.delivery_event_type && dicts.delivery_event_type.values) || []
+            this.deliveryEventStatusOptions = (dicts.delivery_event_status && dicts.delivery_event_status.values) || []
+            this.deliveryEventResultOptions = (dicts.delivery_event_result && dicts.delivery_event_result.values) || []
+            this.feedbackSourceOptions = (dicts.feedback_source && dicts.feedback_source.values) || []
+            this.onSiteOptions = (dicts.sys_yes_no && dicts.sys_yes_no.values) || []
           })
           .catch((err) => console.log(err))
       },
@@ -639,74 +674,11 @@
         return time ? parseTime(time, '{y}-{m}-{d} {h}:{i}') : '-'
       },
 
-      getDeliveryEventTypeLabel(type) {
-        const map = {
-          40: '硬件发货',
-          41: '硬件安装',
-        }
-        return map[type] || type || '-'
-      },
-
-      getDeliveryEventStatusLabel(status) {
-        const map = {
-          10: '待处理',
-          20: '处理中',
-          30: '已关闭',
-        }
-        return map[status] || status || '-'
-      },
-
-      getDeliveryEventStatusType(status) {
-        const map = {
-          10: 'info',
-          20: 'warning',
-          30: 'success',
-        }
-        return map[status] || 'info'
-      },
-
-      getDeliveryEventResultLabel(result) {
-        const map = {
-          10: '已解决',
-          30: '未解决',
-        }
-        return map[result] || result || '-'
-      },
-
-      getFeedbackSourceLabel(source) {
-        const map = {
-          10: '客户',
-          20: '销售',
-          30: '交付',
-        }
-        return map[source] || source || '-'
-      },
-
-      getOnSiteLabel(onSite) {
-        const map = {
-          10: '是',
-          20: '否',
-        }
-        return map[onSite] || onSite || '-'
-      },
-
       getProductLineLabel(productLine) {
         const label = this.selectDictLabel(this.productLineDict, productLine)
         return label || productLine || '-'
       },
 
-      getStatusLabel(status) {
-        const map = {
-          10: '待交付',
-          20: '交付中',
-          30: '暂停',
-          40: '交付完成',
-          50: '验收',
-          90: '作废',
-        }
-        return map[status] || status
-      },
-
       handleRefreshEvent() {
         this.fetchEventData()
       },

+ 39 - 61
src/views/devops/deliveryProject/components/DeliveryProjectEventDetail.vue

@@ -47,7 +47,9 @@
             <i class="el-icon-s-promotion" />
           </div>
           <div class="card-content">
-            <div class="card-value">{{ (detailData && getStatusLabel(detailData.deliveryEventStatus)) || '-' }}</div>
+            <div class="card-value">
+              {{ (detailData && selectDictLabel(deliveryEventStatusOptions, detailData.deliveryEventStatus)) || '-' }}
+            </div>
             <div class="card-label">当前状态</div>
           </div>
         </div>
@@ -56,7 +58,9 @@
             <i class="el-icon-warning" />
           </div>
           <div class="card-content">
-            <div class="card-value">{{ (detailData && getEventTypeLabel(detailData.deliveryEventType)) || '-' }}</div>
+            <div class="card-value">
+              {{ (detailData && selectDictLabel(deliveryEventTypeOptions, detailData.deliveryEventType)) || '-' }}
+            </div>
             <div class="card-label">事件类型</div>
           </div>
         </div>
@@ -107,13 +111,13 @@
               <div class="property-item">
                 <span class="property-label">事件类型</span>
                 <span class="property-value">
-                  {{ (detailData && getEventTypeLabel(detailData.deliveryEventType)) || '-' }}
+                  {{ (detailData && selectDictLabel(deliveryEventTypeOptions, detailData.deliveryEventType)) || '-' }}
                 </span>
               </div>
               <div class="property-item">
                 <span class="property-label">反馈来源</span>
                 <span class="property-value">
-                  {{ (detailData && getFeedbackSourceLabel(detailData.feedbackSource)) || '-' }}
+                  {{ (detailData && selectDictLabel(feedbackSourceOptions, detailData.feedbackSource)) || '-' }}
                 </span>
               </div>
               <div class="property-item">
@@ -130,7 +134,9 @@
               </div>
               <div class="property-item">
                 <span class="property-label">是否现场</span>
-                <span class="property-value">{{ (detailData && getOnSiteLabel(detailData.onSite)) || '-' }}</span>
+                <span class="property-value">
+                  {{ (detailData && selectDictLabel(onSiteOptions, detailData.onSite)) || '-' }}
+                </span>
               </div>
               <div class="property-item">
                 <span class="property-label">完成时间</span>
@@ -277,14 +283,16 @@
       <el-form ref="closeEventForm" label-width="100px" :model="closeEventForm" :rules="closeEventRules" size="small">
         <el-form-item label="处理结果" prop="deliveryEventResult">
           <el-select v-model="closeEventForm.deliveryEventResult" placeholder="请选择处理结果" style="width: 100%">
-            <el-option label="已解决" value="10" />
-            <el-option label="未解决" value="30" />
+            <el-option
+              v-for="dict in deliveryEventResultOptions"
+              :key="dict.key"
+              :label="dict.value"
+              :value="dict.key" />
           </el-select>
         </el-form-item>
         <el-form-item label="是否现场" prop="onSite">
           <el-select v-model="closeEventForm.onSite" placeholder="请选择是否现场" style="width: 100%">
-            <el-option label="是" value="10" />
-            <el-option label="否" value="20" />
+            <el-option v-for="dict in onSiteOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
           </el-select>
         </el-form-item>
         <el-form-item label="完成时间" prop="completeTime">
@@ -410,6 +418,11 @@
         closeEventFileList: [],
         closeEventUploadFiles: [],
         productLineDict: [],
+        deliveryEventTypeOptions: [],
+        deliveryEventStatusOptions: [],
+        deliveryEventResultOptions: [],
+        feedbackSourceOptions: [],
+        onSiteOptions: [],
       }
     },
     computed: {
@@ -457,7 +470,7 @@
       },
     },
     created() {
-      this.loadProductLineDict()
+      this.getOptions()
     },
     beforeDestroy() {
       if (this.quickEditor) {
@@ -467,10 +480,22 @@
     },
     methods: {
       sanitizeHtml,
-      loadProductLineDict() {
-        this.getDicts('sys_product_line')
-          .then((res) => {
-            this.productLineDict = res.data.values || []
+      getOptions() {
+        Promise.all([
+          this.getDicts('sys_product_line'),
+          this.getDicts('delivery_event_type'),
+          this.getDicts('delivery_event_status'),
+          this.getDicts('delivery_event_result'),
+          this.getDicts('feedback_source'),
+          this.getDicts('sys_yes_no'),
+        ])
+          .then(([productLine, eventType, eventStatus, eventResult, feedbackSource, onSite]) => {
+            this.productLineDict = productLine.data.values || []
+            this.deliveryEventTypeOptions = eventType.data.values || []
+            this.deliveryEventStatusOptions = eventStatus.data.values || []
+            this.deliveryEventResultOptions = eventResult.data.values || []
+            this.feedbackSourceOptions = feedbackSource.data.values || []
+            this.onSiteOptions = onSite.data.values || []
           })
           .catch((err) => console.log(err))
       },
@@ -669,57 +694,10 @@
           this.quickEditor.clear()
         }
       },
-      getStatusLabel(status) {
-        const map = {
-          10: '待处理',
-          20: '处理中',
-          30: '已关闭',
-          90: '已作废',
-        }
-        return map[status] || status
-      },
-      getEventTypeLabel(type) {
-        const map = {
-          10: '内部启动会',
-          15: '外部启动会',
-          20: '交付计划',
-          30: '需求评审',
-          31: '需求沟通',
-          32: '功能调整',
-          33: '二开需求',
-          35: '系统缺陷',
-          37: '接口联调',
-          38: '系统发版',
-          39: '软件部署',
-          40: '硬件发货',
-          41: '硬件安装',
-          42: '硬件验收',
-          50: '试运行',
-          55: '系统培训',
-          60: '验收汇报',
-          99: '其他',
-        }
-        return map[type] || type
-      },
-      getFeedbackSourceLabel(source) {
-        const map = {
-          10: '客户',
-          20: '销售',
-          30: '交付',
-        }
-        return map[source] || source
-      },
       getProductLineLabel(productLine) {
         const label = this.selectDictLabel(this.productLineDict, productLine)
         return label || productLine || '-'
       },
-      getOnSiteLabel(onSite) {
-        const map = {
-          10: '是',
-          20: '否',
-        }
-        return map[onSite] || '-'
-      },
       formatTime(time) {
         return time ? parseTime(time, '{y}-{m}-{d} {h}:{i}') : '-'
       },

+ 24 - 15
src/views/devops/deliveryProject/components/DeliveryProjectEventEdit.vue

@@ -49,25 +49,18 @@
         <el-col :span="12">
           <el-form-item label="事件类型" prop="deliveryEventType">
             <el-select v-model="form.deliveryEventType" placeholder="请选择事件类型" style="width: 100%">
-              <el-option label="内部启动会" value="10" />
-              <el-option label="外部启动会" value="15" />
-              <el-option label="交付计划" value="20" />
-              <el-option label="需求沟通" value="31" />
-              <el-option label="功能调整" value="32" />
-              <el-option label="二开需求" value="33" />
-              <el-option label="系统缺陷" value="35" />
-              <el-option label="试运行" value="50" />
-              <el-option label="系统培训" value="55" />
-              <el-option label="验收汇报" value="60" />
+              <el-option
+                v-for="dict in deliveryEventTypeOptions"
+                :key="dict.key"
+                :label="dict.value"
+                :value="dict.key" />
             </el-select>
           </el-form-item>
         </el-col>
         <el-col :span="12">
           <el-form-item label="反馈来源" prop="feedbackSource">
             <el-select v-model="form.feedbackSource" placeholder="请选择反馈来源" style="width: 100%">
-              <el-option label="客户" value="10" />
-              <el-option label="销售" value="20" />
-              <el-option label="交付" value="30" />
+              <el-option v-for="dict in feedbackSourceOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
             </el-select>
           </el-form-item>
         </el-col>
@@ -82,8 +75,7 @@
         <el-col :span="12">
           <el-form-item label="是否现场" prop="onSite">
             <el-select v-model="form.onSite" placeholder="请选择" style="width: 100%">
-              <el-option label="是" value="10" />
-              <el-option label="否" value="20" />
+              <el-option v-for="dict in onSiteOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
             </el-select>
           </el-form-item>
         </el-col>
@@ -194,6 +186,9 @@
         },
         submitLoading: false,
         projectOptions: [],
+        deliveryEventTypeOptions: [],
+        feedbackSourceOptions: [],
+        onSiteOptions: [],
       }
     },
     computed: {
@@ -217,6 +212,7 @@
       },
     },
     created() {
+      this.getOptions()
       this.remoteSearchProject = debounce((query) => {
         this.searchProject(query)
       }, 300)
@@ -231,6 +227,19 @@
       }
     },
     methods: {
+      getOptions() {
+        Promise.all([
+          this.getDicts('delivery_event_type'),
+          this.getDicts('feedback_source'),
+          this.getDicts('sys_yes_no'),
+        ])
+          .then(([eventType, feedbackSource, onSite]) => {
+            this.deliveryEventTypeOptions = eventType.data.values || []
+            this.feedbackSourceOptions = feedbackSource.data.values || []
+            this.onSiteOptions = onSite.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
       onEditorCreated(editor) {
         this.editor = editor
       },

+ 219 - 149
src/views/devops/deliveryProject/index.vue

@@ -14,16 +14,32 @@
               multiple
               placeholder="请选择"
               style="width: 140px">
-              <el-option label="待处理" value="10" />
-              <el-option label="处理中" value="20" />
-              <el-option label="已关闭" value="30" />
+              <el-option
+                v-for="dict in deliveryEventStatusOptions"
+                :key="dict.key"
+                :label="dict.value"
+                :value="dict.key" />
             </el-select>
           </el-form-item>
           <el-form-item label="反馈人">
             <el-input v-model="queryForm.feedbackReporter" clearable placeholder="请输入" style="width: 120px" />
           </el-form-item>
           <el-form-item label="负责人">
-            <el-input v-model="queryForm.opsUserName" clearable placeholder="请输入" style="width: 120px" />
+            <el-select
+              v-model="queryForm.opsUserName"
+              class="query-select--owner"
+              clearable
+              collapse-tags
+              filterable
+              :loading="opsUsersLoading"
+              multiple
+              placeholder="请选择"
+              remote
+              :remote-method="remoteFetchOpsUsers"
+              reserve-keyword
+              @visible-change="handleOpsUserVisibleChange">
+              <el-option v-for="u in opsUserOptions" :key="u.value" :label="u.label" :value="u.label" />
+            </el-select>
           </el-form-item>
           <el-form-item label="反馈时间">
             <el-date-picker
@@ -58,24 +74,11 @@
               multiple
               placeholder="请选择"
               style="width: 160px">
-              <el-option label="内部启动会" value="10" />
-              <el-option label="外部启动会" value="15" />
-              <el-option label="交付计划" value="20" />
-              <el-option label="需求评审" value="30" />
-              <el-option label="需求沟通" value="31" />
-              <el-option label="功能调整" value="32" />
-              <el-option label="二开需求" value="33" />
-              <el-option label="系统缺陷" value="35" />
-              <el-option label="接口联调" value="37" />
-              <el-option label="系统发版" value="38" />
-              <el-option label="软件部署" value="39" />
-              <el-option label="硬件发货" value="40" />
-              <el-option label="硬件安装" value="41" />
-              <el-option label="硬件验收" value="42" />
-              <el-option label="试运行" value="50" />
-              <el-option label="系统培训" value="55" />
-              <el-option label="验收汇报" value="60" />
-              <el-option label="其他" value="99" />
+              <el-option
+                v-for="dict in deliveryEventTypeOptions"
+                :key="dict.key"
+                :label="dict.value"
+                :value="dict.key" />
             </el-select>
           </el-form-item>
           <el-form-item label="事件结果">
@@ -85,8 +88,11 @@
               multiple
               placeholder="请选择"
               style="width: 140px">
-              <el-option label="已解决" value="10" />
-              <el-option label="未解决" value="30" />
+              <el-option
+                v-for="dict in deliveryEventResultOptions"
+                :key="dict.key"
+                :label="dict.value"
+                :value="dict.key" />
             </el-select>
           </el-form-item>
           <el-form-item label="事件描述">
@@ -173,7 +179,7 @@
                   <span class="project-line-tag">{{ getProductLineLabel(project.productLine) }}</span>
                 </div>
                 <span :class="['project-status-tag', 'project-status-tag--' + project.status]">
-                  {{ getStatusLabel(project.status) }}
+                  {{ selectDictLabel(projectStatusOptions, project.status) }}
                 </span>
               </div>
               <div class="project-card-title" :title="project.name">{{ project.name }}</div>
@@ -216,8 +222,7 @@
             height="100%"
             stripe
             style="width: 100%"
-            @row-click="handleRowClick"
-            @sort-change="handleSortChange">
+            @row-click="handleRowClick">
             <el-table-column align="center" type="index" width="50" />
             <el-table-column label="事件标题" min-width="320" show-overflow-tooltip>
               <template slot-scope="{ row }">
@@ -226,33 +231,51 @@
                 </span>
               </template>
             </el-table-column>
-            <el-table-column label="事件类型" prop="deliveryEventType" sortable="custom" width="140">
+            <el-table-column
+              label="事件类型"
+              :render-header="renderSortableHeader('事件类型', 'deliveryEventType')"
+              width="140">
               <template slot-scope="{ row }">
                 <el-tag size="small" type="info">
-                  {{ getDeliveryEventTypeLabel(row.deliveryEventType || row.delivery_event_type) }}
+                  {{ selectDictLabel(deliveryEventTypeOptions, row.deliveryEventType || row.delivery_event_type) }}
                 </el-tag>
               </template>
             </el-table-column>
-            <el-table-column label="事件状态" prop="deliveryEventStatus" sortable="custom" width="120">
+            <el-table-column
+              label="事件状态"
+              :render-header="renderSortableHeader('事件状态', 'deliveryEventStatus')"
+              width="120">
               <template slot-scope="{ row }">
                 <el-tag
                   size="small"
-                  :type="getDeliveryEventStatusType(row.deliveryEventStatus || row.delivery_event_status)">
-                  {{ getDeliveryEventStatusLabel(row.deliveryEventStatus || row.delivery_event_status) }}
+                  :type="getDeliveryEventStatusTagType(row.deliveryEventStatus || row.delivery_event_status)">
+                  {{
+                    selectDictLabel(deliveryEventStatusOptions, row.deliveryEventStatus || row.delivery_event_status)
+                  }}
                 </el-tag>
               </template>
             </el-table-column>
-            <el-table-column label="事件结果" prop="deliveryEventResult" sortable="custom" width="120">
+            <el-table-column
+              label="事件结果"
+              :render-header="renderSortableHeader('事件结果', 'deliveryEventResult')"
+              width="120">
               <template slot-scope="{ row }">
-                {{ getDeliveryEventResultLabel(row.deliveryEventResult || row.delivery_event_result) }}
+                {{ selectDictLabel(deliveryEventResultOptions, row.deliveryEventResult || row.delivery_event_result) }}
               </template>
             </el-table-column>
-            <el-table-column label="负责人" prop="opsUserName" show-overflow-tooltip sortable="custom" width="130">
+            <el-table-column
+              label="负责人"
+              :render-header="renderSortableHeader('负责人', 'opsUserName')"
+              show-overflow-tooltip
+              width="130">
               <template slot-scope="{ row }">
                 {{ row.opsUserName || row.ops_user_name || '-' }}
               </template>
             </el-table-column>
-            <el-table-column label="处理时间" prop="completeTime" sortable="custom" width="180">
+            <el-table-column
+              label="处理时间"
+              :render-header="renderSortableHeader('处理时间', 'completeTime')"
+              width="180">
               <template slot-scope="{ row }">
                 {{ formatEventTime(row.completeTime || row.complete_time) }}
               </template>
@@ -264,22 +287,29 @@
             </el-table-column>
             <el-table-column label="是否现场" width="90">
               <template slot-scope="{ row }">
-                {{ getOnSiteLabel(row.onSite || row.on_site) }}
+                {{ selectDictLabel(onSiteOptions, row.onSite || row.on_site) }}
               </template>
             </el-table-column>
-            <el-table-column label="反馈人" prop="feedbackReporter" show-overflow-tooltip sortable="custom" width="130">
+            <el-table-column
+              label="反馈人"
+              :render-header="renderSortableHeader('反馈人', 'feedbackReporter')"
+              show-overflow-tooltip
+              width="130">
               <template slot-scope="{ row }">
                 {{ row.feedbackReporter || row.feedback_reporter || '-' }}
               </template>
             </el-table-column>
-            <el-table-column label="反馈时间" prop="feedbackDate" sortable="custom" width="180">
+            <el-table-column
+              label="反馈时间"
+              :render-header="renderSortableHeader('反馈时间', 'feedbackDate')"
+              width="180">
               <template slot-scope="{ row }">
                 {{ formatEventTime(row.feedbackDate || row.feedback_date) }}
               </template>
             </el-table-column>
             <el-table-column label="反馈来源" width="100">
               <template slot-scope="{ row }">
-                {{ getFeedbackSourceLabel(row.feedbackSource || row.feedback_source) }}
+                {{ selectDictLabel(feedbackSourceOptions, row.feedbackSource || row.feedback_source) }}
               </template>
             </el-table-column>
             <el-table-column fixed="right" header-align="center" label="操作" width="160">
@@ -387,6 +417,8 @@
 <script>
   import deliveryProjectApi from '@/api/devops/deliveryProject'
   import deliveryProjectEventApi from '@/api/devops/deliveryProjectEvent'
+  import userApi from '@/api/system/user'
+  import { DEVOPS_DEV_DEPT_ID } from '@/config/devops.config'
   import DeliveryProjectEventEdit from './components/DeliveryProjectEventEdit'
   import DeliveryProjectEventDetail from './components/DeliveryProjectEventDetail'
   import DeliveryProjectAssign from './components/DeliveryProjectAssign'
@@ -394,6 +426,8 @@
   import { parseTime } from '@/utils'
   import { uploadFileToRichtextServer } from '@/utils/richtextUpload'
   import { sanitizeHtml } from '@/utils/safeHtml'
+  import { deliveryEventStatusTagTypes, getTagType } from '@/config/devopsTagTypes'
+  import dictApi from '@/api/system/dict'
 
   export default {
     name: 'DeliveryProject',
@@ -416,6 +450,8 @@
           sortFields: [],
         },
         showAdvanced: false,
+        opsUserOptions: [],
+        opsUsersLoading: false,
         tableData: [],
         total: 0,
         loading: false,
@@ -429,6 +465,12 @@
         editDialogVisible: false,
         editData: null,
         productLineDict: [],
+        projectStatusOptions: [],
+        deliveryEventTypeOptions: [],
+        deliveryEventStatusOptions: [],
+        deliveryEventResultOptions: [],
+        feedbackSourceOptions: [],
+        onSiteOptions: [],
         detailVisible: false,
         detailMode: 'view',
         currentRow: null,
@@ -498,17 +540,37 @@
       },
     },
     created() {
-      this.loadProductLineDict()
+      this.getOptions()
       this.fetchData()
     },
     methods: {
+      getTagType,
+      getDeliveryEventStatusTagType(status) {
+        return getTagType(deliveryEventStatusTagTypes, status, 'info')
+      },
       parseTime,
       sanitizeHtml,
 
-      loadProductLineDict() {
-        this.getDicts('sys_product_line')
+      getOptions() {
+        dictApi
+          .getDictDataByTypes([
+            'sys_product_line',
+            'delivery_project_status',
+            'delivery_event_type',
+            'delivery_event_status',
+            'delivery_event_result',
+            'feedback_source',
+            'sys_yes_no',
+          ])
           .then((res) => {
-            this.productLineDict = res.data.values || []
+            const dicts = res.data || {}
+            this.productLineDict = (dicts.sys_product_line && dicts.sys_product_line.values) || []
+            this.projectStatusOptions = (dicts.delivery_project_status && dicts.delivery_project_status.values) || []
+            this.deliveryEventTypeOptions = (dicts.delivery_event_type && dicts.delivery_event_type.values) || []
+            this.deliveryEventStatusOptions = (dicts.delivery_event_status && dicts.delivery_event_status.values) || []
+            this.deliveryEventResultOptions = (dicts.delivery_event_result && dicts.delivery_event_result.values) || []
+            this.feedbackSourceOptions = (dicts.feedback_source && dicts.feedback_source.values) || []
+            this.onSiteOptions = (dicts.sys_yes_no && dicts.sys_yes_no.values) || []
           })
           .catch((err) => console.log(err))
       },
@@ -516,18 +578,22 @@
       // 获取项目列表
       async fetchProjectList() {
         try {
-          const params = { pageNum: 1, pageSize: 999 }
+          const params = {
+            pageNum: 1,
+            pageSize: 999,
+            productLine: '10,20,30,40,50,60',
+            sortField: 'contract_no',
+            sortOrder: 'desc',
+          }
           // 状态筛选
           if (this.projectStatusFilter) {
-            // 转换逻辑值为实际状态码
             const statusMap = {
-              pending: '10', // 待分配
-              delivering: '20,30,40', // 交付中
-              delivered: '50', // 已验收
+              pending: '10',
+              delivering: '20,30,40',
+              delivered: '50',
             }
             params.projectStatus = statusMap[this.projectStatusFilter] || this.projectStatusFilter
           } else {
-            // 全部:排除90(作废)
             params.projectStatus = '10,20,30,40,50'
           }
           const res = await deliveryProjectApi.getList(params)
@@ -542,6 +608,7 @@
               deliveryUserId: item.deliveryUserId || item.delivery_user_id,
               status: String(item.projectStatus || item.project_status),
             }))
+            projectList.sort((a, b) => (b.contractNo || '').localeCompare(a.contractNo || ''))
             this.projects = [{ id: '', name: '全部' }, ...projectList]
           }
         } catch (error) {
@@ -620,7 +687,7 @@
           deliveryEventType: [],
           deliveryEventStatus: [],
           deliveryEventResult: [],
-          opsUserName: '',
+          opsUserName: [],
           feedbackReporter: '',
           feedbackDateRange: [],
           completeDateRange: [],
@@ -869,92 +936,12 @@
       formatEventTime(time) {
         return time ? parseTime(time, '{y}-{m}-{d} {h}:{i}') : '-'
       },
-      getDeliveryEventTypeLabel(type) {
-        const map = {
-          10: '内部启动会',
-          15: '外部启动会',
-          20: '交付计划',
-          30: '需求评审',
-          31: '需求沟通',
-          32: '功能调整',
-          33: '二开需求',
-          35: '系统缺陷',
-          37: '接口联调',
-          38: '系统发版',
-          39: '软件部署',
-          40: '硬件发货',
-          41: '硬件安装',
-          42: '硬件验收',
-          50: '试运行',
-          55: '系统培训',
-          60: '验收汇报',
-          99: '其他',
-        }
-        return map[type] || type || '-'
-      },
-
-      getDeliveryEventStatusLabel(status) {
-        const map = {
-          10: '待处理',
-          20: '处理中',
-          30: '已关闭',
-        }
-        return map[status] || status || '-'
-      },
-
-      getDeliveryEventStatusType(status) {
-        const map = {
-          10: 'info',
-          20: 'warning',
-          30: 'success',
-        }
-        return map[status] || 'info'
-      },
-
-      getDeliveryEventResultLabel(result) {
-        const map = {
-          10: '已解决',
-          30: '未解决',
-        }
-        return map[result] || result || '-'
-      },
-
-      getFeedbackSourceLabel(source) {
-        const map = {
-          10: '客户',
-          20: '销售',
-          30: '交付',
-        }
-        return map[source] || source || '-'
-      },
-
-      getOnSiteLabel(onSite) {
-        const map = {
-          10: '是',
-          20: '否',
-        }
-        return map[onSite] || onSite || '-'
-      },
-
       // 产品线标签
       getProductLineLabel(productLine) {
         const label = this.selectDictLabel(this.productLineDict, productLine)
         return label || productLine || '-'
       },
 
-      // 状态标签
-      getStatusLabel(status) {
-        const map = {
-          10: '待分配',
-          20: '交付中',
-          30: '暂停',
-          40: '交付完成',
-          50: '已验收',
-          90: '作废',
-        }
-        return map[status] || status
-      },
-
       // 状态样式
       getStatusType(status) {
         const map = {
@@ -978,28 +965,67 @@
         this.fetchEventData()
       },
 
-      // 处理表格排序变化
-      handleSortChange({ prop, order }) {
-        const sortItem = { field: prop, order: order === 'ascending' ? 'asc' : 'desc' }
-
-        // 检查是否已存在该字段排序
-        const existingIndex = this.queryForm.sortFields.findIndex((s) => s.field === prop)
-
-        if (existingIndex > -1) {
-          if (order) {
-            // 更新排序
-            this.queryForm.sortFields[existingIndex] = sortItem
-          } else {
-            // 移除排序
-            this.queryForm.sortFields.splice(existingIndex, 1)
-          }
-        } else if (order) {
-          // 添加新排序
-          this.queryForm.sortFields.push(sortItem)
+      // 远程搜索负责人
+      async remoteFetchOpsUsers(search) {
+        this.opsUsersLoading = true
+        try {
+          const payload = { deptId: DEVOPS_DEV_DEPT_ID, pageNum: 1, pageSize: 999 }
+          if (search) payload.keyWords = search
+          const res = await userApi.getList(payload)
+          const list = res.data?.list || []
+          this.opsUserOptions = list.map((u) => ({
+            value: u.userId ?? u.user_id ?? u.id ?? null,
+            label: u.nickName ?? u.nick_name ?? u.name ?? '',
+          }))
+        } catch (error) {
+          console.error('获取负责人列表失败:', error)
+          this.opsUserOptions = []
+        } finally {
+          this.opsUsersLoading = false
+        }
+      },
+      handleOpsUserVisibleChange(visible) {
+        if (visible && !this.opsUsersLoading && !this.opsUserOptions.length) {
+          this.remoteFetchOpsUsers('')
         }
+      },
 
+      // 多列排序:点击表头切换 无→升序→降序→无
+      toggleSort(field) {
+        const idx = this.queryForm.sortFields.findIndex((s) => s.field === field)
+        if (idx === -1) {
+          if (this.queryForm.sortFields.length >= 3) return
+          this.queryForm.sortFields.push({ field, order: 'asc' })
+        } else if (this.queryForm.sortFields[idx].order === 'asc') {
+          this.queryForm.sortFields[idx].order = 'desc'
+        } else {
+          this.queryForm.sortFields.splice(idx, 1)
+        }
         this.fetchEventData()
       },
+      getSortState(field) {
+        const sf = this.queryForm.sortFields.find((s) => s.field === field)
+        return sf ? sf.order : ''
+      },
+      getSortPriority(field) {
+        const idx = this.queryForm.sortFields.findIndex((s) => s.field === field)
+        return idx === -1 ? '' : String(idx + 1)
+      },
+      renderSortableHeader(label, field) {
+        const vm = this
+        return function (h) {
+          const state = vm.getSortState(field)
+          const prio = vm.getSortPriority(field)
+          return h('div', { class: 'sortable-header', on: { click: () => vm.toggleSort(field) } }, [
+            h('span', { class: 'sortable-header-label' }, label),
+            h('span', { class: 'sort-arrows' }, [
+              h('i', { class: ['el-icon-caret-top', { 'sort-active': state === 'asc' }] }),
+              h('i', { class: ['el-icon-caret-bottom', { 'sort-active': state === 'desc' }] }),
+            ]),
+            prio ? h('span', { class: 'sort-priority' }, prio) : null,
+          ])
+        }
+      },
 
       // 项目状态筛选变化
       handleProjectStatusChange() {
@@ -1605,4 +1631,48 @@
     font-size: 12px;
     color: #409eff;
   }
+
+  ::v-deep .sortable-header {
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+    cursor: pointer;
+    user-select: none;
+    white-space: nowrap;
+
+    .sort-arrows {
+      display: inline-flex;
+      flex-direction: column;
+      line-height: 1;
+
+      i {
+        font-size: 10px;
+        color: #c0c4cc;
+        transition: color 0.2s;
+
+        &.sort-active {
+          color: #409eff;
+        }
+      }
+    }
+
+    .sort-priority {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      width: 16px;
+      height: 16px;
+      border-radius: 50%;
+      background: #409eff;
+      color: #fff;
+      font-size: 10px;
+      font-weight: 700;
+      line-height: 1;
+      margin-left: 2px;
+    }
+  }
+
+  .query-select--owner {
+    width: 180px;
+  }
 </style>

+ 25 - 32
src/views/devops/operation/components/OperationDetail.vue

@@ -62,7 +62,7 @@
             <i class="el-icon-s-promotion" />
           </div>
           <div class="card-content">
-            <div class="card-value">{{ getStatusLabel(data.eventStatus) }}</div>
+            <div class="card-value">{{ selectDictLabel(eventStatusOptions, data.eventStatus) }}</div>
             <div class="card-label">当前状态</div>
           </div>
         </div>
@@ -71,7 +71,7 @@
             <i class="el-icon-warning" />
           </div>
           <div class="card-content">
-            <div class="card-value">{{ data.priorityLevel }}</div>
+            <div class="card-value">{{ selectDictLabel(priorityLevelOptions, data.priorityLevel) }}</div>
             <div class="card-label">优先级</div>
           </div>
         </div>
@@ -107,11 +107,11 @@
               </div>
               <div class="property-item">
                 <span class="property-label">事件类型</span>
-                <span class="property-value">{{ getEventTypeLabel(data.eventType) }}</span>
+                <span class="property-value">{{ selectDictLabel(eventTypeOptions, data.eventType) }}</span>
               </div>
               <div class="property-item">
                 <span class="property-label">优先级</span>
-                <span class="property-value">{{ data.priorityLevel }}</span>
+                <span class="property-value">{{ selectDictLabel(priorityLevelOptions, data.priorityLevel) }}</span>
               </div>
               <div class="property-item">
                 <span class="property-label">合同编号</span>
@@ -226,7 +226,7 @@
       <el-form label-width="80px">
         <el-form-item label="解决状态">
           <el-select v-model="closeForm.handleResult" style="width: 100%">
-            <el-option v-for="item in handleResultOptions" :key="item.value" :label="item.label" :value="item.value" />
+            <el-option v-for="dict in handleResultOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
           </el-select>
         </el-form-item>
         <el-form-item label="关闭原因">
@@ -343,17 +343,17 @@
           handleResult: '10',
           handleContent: '',
         },
-        handleResultOptions: [
-          { value: '10', label: '已解决' },
-          { value: '20', label: '部分解决' },
-          { value: '30', label: '未解决' },
-        ],
+        handleResultOptions: [],
         detailAction: '',
+        eventStatusOptions: [],
+        eventTypeOptions: [],
+        priorityLevelOptions: [],
       }
     },
     watch: {
       visible(val) {
         if (val) {
+          this.getOptions()
           this.initDialog()
           this.fetchRecordList()
           this.fetchAttachmentList()
@@ -391,6 +391,21 @@
     },
     methods: {
       sanitizeHtml,
+      getOptions() {
+        Promise.all([
+          this.getDicts('ops_event_status'),
+          this.getDicts('ops_event_type'),
+          this.getDicts('ops_priority_level'),
+          this.getDicts('ops_handle_result'),
+        ])
+          .then(([eventStatus, eventType, priorityLevel, handleResult]) => {
+            this.eventStatusOptions = eventStatus.data.values || []
+            this.eventTypeOptions = eventType.data.values || []
+            this.priorityLevelOptions = priorityLevel.data.values || []
+            this.handleResultOptions = handleResult.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
       onEditorCreated(editor) {
         this.editor = editor
       },
@@ -533,28 +548,6 @@
         }
         this.$emit('update:visible', false)
       },
-      getStatusLabel(status) {
-        const map = {
-          10: '待处理',
-          20: '处理中(重点)',
-          30: '处理中(普通)',
-          40: '转研发',
-          70: '挂起',
-          80: '已关闭',
-        }
-        return map[status] || status
-      },
-      getEventTypeLabel(type) {
-        const map = {
-          10: '操作咨询',
-          20: '数据处理',
-          30: '系统BUG',
-          40: '功能调整',
-          50: '二开需求',
-          90: '其他问题',
-        }
-        return map[type] || type
-      },
       formatTime(time) {
         return time ? parseTime(time, '{y}-{m}-{d} {h}:{i}') : '-'
       },

+ 16 - 14
src/views/devops/operation/components/OperationEdit.vue

@@ -31,21 +31,14 @@
         <el-col :span="12">
           <el-form-item label="事件类型" prop="eventType">
             <el-select v-model="form.eventType" placeholder="请选择事件类型" style="width: 100%">
-              <el-option label="操作咨询" value="10" />
-              <el-option label="数据处理" value="20" />
-              <el-option label="系统BUG" value="30" />
-              <el-option label="功能调整" value="40" />
-              <el-option label="二开需求" value="50" />
-              <el-option label="其他问题" value="90" />
+              <el-option v-for="dict in eventTypeOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
             </el-select>
           </el-form-item>
         </el-col>
         <el-col :span="12">
           <el-form-item label="优先级" prop="priorityLevel">
             <el-select v-model="form.priorityLevel" placeholder="请选择优先级" style="width: 100%">
-              <el-option label="P1 紧急" value="P1" />
-              <el-option label="P2 一般" value="P2" />
-              <el-option label="P3 低优" value="P3" />
+              <el-option v-for="dict in priorityLevelOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
             </el-select>
           </el-form-item>
         </el-col>
@@ -92,9 +85,7 @@
         <el-col :span="12">
           <el-form-item label="反馈来源" prop="feedbackSource">
             <el-select v-model="form.feedbackSource" placeholder="请选择反馈来源" style="width: 100%">
-              <el-option label="客户" value="10" />
-              <el-option label="销售" value="20" />
-              <el-option label="交付" value="30" />
+              <el-option v-for="dict in feedbackSourceOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
             </el-select>
           </el-form-item>
         </el-col>
@@ -208,6 +199,9 @@
         uploadFiles: [],
         contractRequestSeq: 0,
         productLineOptions: [],
+        eventTypeOptions: [],
+        priorityLevelOptions: [],
+        feedbackSourceOptions: [],
       }
     },
     computed: {
@@ -245,9 +239,17 @@
     },
     methods: {
       getOptions() {
-        Promise.all([this.getDicts('sys_product_line')])
-          .then(([productLine]) => {
+        Promise.all([
+          this.getDicts('sys_product_line'),
+          this.getDicts('ops_event_type'),
+          this.getDicts('ops_priority_level'),
+          this.getDicts('feedback_source'),
+        ])
+          .then(([productLine, eventType, priorityLevel, feedbackSource]) => {
             this.productLineOptions = productLine.data.values || []
+            this.eventTypeOptions = eventType.data.values || []
+            this.priorityLevelOptions = priorityLevel.data.values || []
+            this.feedbackSourceOptions = feedbackSource.data.values || []
           })
           .catch((err) => console.log(err))
       },

+ 19 - 22
src/views/devops/operation/index.vue

@@ -56,8 +56,8 @@
             @dragstart="handleDragStart(item, $event)">
             <div class="card-header">
               <span class="card-title">{{ item.eventTitle }}</span>
-              <el-tag size="mini" :type="getPriorityType(item.priorityLevel)">
-                {{ item.priorityLevel }}
+              <el-tag size="mini" :type="getOpsPriorityTagType(item.priorityLevel)">
+                {{ selectDictLabel(priorityLevelOptions, item.priorityLevel) }}
               </el-tag>
             </div>
             <div class="card-body">
@@ -80,7 +80,7 @@
               <div class="card-tags">
                 <el-tag v-if="item.isBig === '10'" size="mini" type="danger">重点项目</el-tag>
                 <el-tag v-if="item.isOps === '10'" size="mini" type="success">运维期</el-tag>
-                <el-tag size="mini" type="warning">{{ getEventTypeLabel(item.eventType) }}</el-tag>
+                <el-tag size="mini" type="warning">{{ selectDictLabel(eventTypeOptions, item.eventType) }}</el-tag>
               </div>
             </div>
             <div class="card-footer">
@@ -130,6 +130,7 @@
   import operationEventApi from '@/api/operation/operationEvent'
   import { parseTime } from '@/utils'
   import to from 'await-to-js'
+  import { opsPriorityTagTypes, getTagType } from '@/config/devopsTagTypes'
 
   export default {
     name: 'Operation',
@@ -156,6 +157,8 @@
         detailAction: '',
         currentRow: null,
         draggingItem: null,
+        eventTypeOptions: [],
+        priorityLevelOptions: [],
       }
     },
     watch: {
@@ -164,9 +167,22 @@
       },
     },
     created() {
+      this.getOptions()
       this.fetchData()
     },
     methods: {
+      getTagType,
+      getOpsPriorityTagType(level) {
+        return getTagType(opsPriorityTagTypes, level, 'success')
+      },
+      getOptions() {
+        Promise.all([this.getDicts('ops_event_type'), this.getDicts('ops_priority_level')])
+          .then(([eventType, priorityLevel]) => {
+            this.eventTypeOptions = eventType.data.values || []
+            this.priorityLevelOptions = priorityLevel.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
       async fetchData() {
         try {
           const res = await operationEventApi.getKanbanData(this.queryForm)
@@ -270,25 +286,6 @@
         }
         this.draggingItem = null
       },
-      getPriorityType(priority) {
-        const map = {
-          P1: 'danger',
-          P2: 'warning',
-          P3: 'success',
-        }
-        return map[priority] || 'info'
-      },
-      getEventTypeLabel(type) {
-        const map = {
-          10: '操作咨询',
-          20: '数据处理',
-          30: '系统BUG',
-          40: '功能调整',
-          50: '二开需求',
-          90: '其他问题',
-        }
-        return map[type] || type
-      },
       formatTime(time) {
         return time ? parseTime(time, '{y}-{m}-{d}') : '-'
       },

+ 24 - 25
src/views/devops/operationHistory/index.vue

@@ -85,13 +85,17 @@
         <el-table-column align="center" label="处理人" min-width="100" prop="opsUserName" show-overflow-tooltip />
         <el-table-column align="center" label="事件类型" min-width="90" prop="eventType" show-overflow-tooltip>
           <template #default="{ row }">
-            {{ getEventTypeLabel(row.eventType) }}
+            {{ selectDictLabel(eventTypeOptions, row.eventType) }}
+          </template>
+        </el-table-column>
+        <el-table-column align="center" label="优先级" min-width="80" prop="priorityLevel" show-overflow-tooltip>
+          <template #default="{ row }">
+            {{ selectDictLabel(priorityLevelOptions, row.priorityLevel) }}
           </template>
         </el-table-column>
-        <el-table-column align="center" label="优先级" min-width="80" prop="priorityLevel" show-overflow-tooltip />
         <el-table-column align="center" label="事件状态" min-width="100" prop="eventStatus" show-overflow-tooltip>
           <template #default="{ row }">
-            {{ getStatusLabel(row.eventStatus) }}
+            {{ selectDictLabel(eventStatusOptions, row.eventStatus) }}
           </template>
         </el-table-column>
         <el-table-column align="center" label="关闭时间" min-width="110" prop="completeTime" show-overflow-tooltip>
@@ -149,6 +153,9 @@
         tableLayoutTimers: [],
         tableLayoutRaf: null,
         hasLoaded: false,
+        eventTypeOptions: [],
+        eventStatusOptions: [],
+        priorityLevelOptions: [],
         queryForm: {
           scopeType: 'my',
           includeClosed: false,
@@ -173,6 +180,7 @@
       window.removeEventListener('resize', this.scheduleTableLayout)
     },
     mounted() {
+      this.getOptions()
       this.fetchData()
       this.scheduleTableLayout()
     },
@@ -181,6 +189,19 @@
       window.removeEventListener('resize', this.scheduleTableLayout)
     },
     methods: {
+      getOptions() {
+        Promise.all([
+          this.getDicts('ops_event_type'),
+          this.getDicts('ops_event_status'),
+          this.getDicts('ops_priority_level'),
+        ])
+          .then(([eventType, eventStatus, priorityLevel]) => {
+            this.eventTypeOptions = eventType.data.values || []
+            this.eventStatusOptions = eventStatus.data.values || []
+            this.priorityLevelOptions = priorityLevel.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
       clearTableLayoutTasks() {
         this.tableLayoutTimers.forEach((timer) => clearTimeout(timer))
         this.tableLayoutTimers = []
@@ -279,28 +300,6 @@
         this.currentRow = row
         this.detailVisible = true
       },
-      getStatusLabel(status) {
-        const map = {
-          10: '待处理',
-          20: '处理中(重点)',
-          30: '处理中(普通)',
-          40: '转研发',
-          70: '挂起',
-          80: '已关闭',
-        }
-        return map[status] || status
-      },
-      getEventTypeLabel(type) {
-        const map = {
-          10: '操作咨询',
-          20: '数据处理',
-          30: '系统BUG',
-          40: '功能调整',
-          50: '二开需求',
-          90: '其他问题',
-        }
-        return map[type] || type
-      },
       async handleExport() {
         const params = {
           scopeType: this.queryForm.scopeType,

+ 22 - 40
src/views/devops/project/index.vue

@@ -9,11 +9,7 @@
           </el-form-item>
           <el-form-item label="项目状态">
             <el-select v-model="queryForm.projectStatus" clearable placeholder="请选择" style="width: 120px">
-              <el-option
-                v-for="item in projectStatusOptions"
-                :key="item.dictValue"
-                :label="item.dictLabel"
-                :value="item.dictValue" />
+              <el-option v-for="item in projectStatusOptions" :key="item.key" :label="item.value" :value="item.key" />
             </el-select>
           </el-form-item>
           <el-form-item label="计划交付时间">
@@ -46,11 +42,7 @@
           </el-form-item>
           <el-form-item label="产品线">
             <el-select v-model="queryForm.productLine" clearable placeholder="请选择" style="width: 130px">
-              <el-option
-                v-for="item in productLineOptions"
-                :key="item.dictValue"
-                :label="item.dictLabel"
-                :value="item.dictValue" />
+              <el-option v-for="item in productLineOptions" :key="item.key" :label="item.value" :value="item.key" />
             </el-select>
           </el-form-item>
         </el-form>
@@ -228,6 +220,7 @@
   import projectInventoryApi from '@/api/devops/projectInventory'
   import { parseTime } from '@/utils'
   import to from 'await-to-js'
+  import { projectStatusTagTypes, getTagType } from '@/config/devopsTagTypes'
 
   export default {
     name: 'ProjectInventory',
@@ -278,22 +271,19 @@
       this.loadData()
     },
     methods: {
+      getTagType,
       initDicts() {
-        projectInventoryApi.getProductLines().then((res) => {
-          if (res.code === 0 || res.code === 200) {
-            this.productLineOptions = res.data || []
-          }
-        })
-        projectInventoryApi.getProjectStatusList().then((res) => {
-          if (res.code === 0 || res.code === 200) {
-            this.projectStatusOptions = res.data || []
-          }
-        })
-        projectInventoryApi.getDeliveryNodes().then((res) => {
-          if (res.code === 0 || res.code === 200) {
-            this.deliveryNodeOptions = res.data || []
-          }
-        })
+        Promise.all([
+          this.getDicts('sys_product_line'),
+          this.getDicts('delivery_project_status'),
+          this.getDicts('delivery_node'),
+        ])
+          .then(([productLine, projectStatus, deliveryNode]) => {
+            this.productLineOptions = productLine.data.values || []
+            this.projectStatusOptions = projectStatus.data.values || []
+            this.deliveryNodeOptions = deliveryNode.data.values || []
+          })
+          .catch((err) => console.log(err))
       },
       loadPersonList() {
         projectInventoryApi.getProjectManagers('all').then((res) => {
@@ -429,27 +419,19 @@
         return '¥' + parseFloat(amount).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
       },
       getProductLineLabel(value) {
-        const item = this.productLineOptions.find((opt) => opt.dictValue === value)
-        return item ? item.dictLabel : value || '-'
+        const label = this.selectDictLabel(this.productLineOptions, value)
+        return label || value || '-'
       },
       getProjectStatusLabel(value) {
-        const item = this.projectStatusOptions.find((opt) => opt.dictValue === value)
-        return item ? item.dictLabel : value || '-'
+        const label = this.selectDictLabel(this.projectStatusOptions, value)
+        return label || value || '-'
       },
       getProjectStatusType(value) {
-        const statusMap = {
-          10: 'info',
-          20: 'warning',
-          30: 'danger',
-          40: 'success',
-          50: 'success',
-          90: 'info',
-        }
-        return statusMap[value] || ''
+        return getTagType(projectStatusTagTypes, value, 'info')
       },
       getDeliveryNodeLabel(value) {
-        const item = this.deliveryNodeOptions.find((opt) => opt.dictValue === value)
-        return item ? item.dictLabel : value || '-'
+        const label = this.selectDictLabel(this.deliveryNodeOptions, value)
+        return label || value || '-'
       },
     },
   }

+ 20 - 38
src/views/devops/software/components/ReleaseTaskListDialog.vue

@@ -11,13 +11,13 @@
       <el-table-column label="任务标题" min-width="200" prop="taskTitle" show-overflow-tooltip />
       <el-table-column label="任务类型" width="100">
         <template slot-scope="{ row }">
-          <el-tag size="small" type="info">{{ getTaskTypeLabel(row.taskType) }}</el-tag>
+          <el-tag size="small" type="info">{{ selectDictLabel(taskTypeOptions, row.taskType) }}</el-tag>
         </template>
       </el-table-column>
       <el-table-column label="任务状态" width="100">
         <template slot-scope="{ row }">
-          <el-tag size="small" :type="getTaskStatusType(row.taskStatus)">
-            {{ getTaskStatusLabel(row.taskStatus) }}
+          <el-tag size="small" :type="getTaskStatusTagType(row.taskStatus)">
+            {{ selectDictLabel(taskStatusOptions, row.taskStatus) }}
           </el-tag>
         </template>
       </el-table-column>
@@ -33,6 +33,7 @@
 
 <script>
   import opsEventTaskApi from '@/api/devops/opsEventTask'
+  import { taskStatusTagTypes, getTagType } from '@/config/devopsTagTypes'
 
   export default {
     name: 'ReleaseTaskListDialog',
@@ -50,6 +51,8 @@
       return {
         loading: false,
         taskList: [],
+        taskTypeOptions: [],
+        taskStatusOptions: [],
       }
     },
     watch: {
@@ -59,7 +62,21 @@
         }
       },
     },
+    created() {
+      this.getOptions()
+    },
     methods: {
+      getTaskStatusTagType(status) {
+        return getTagType(taskStatusTagTypes, status, 'info')
+      },
+      getOptions() {
+        Promise.all([this.getDicts('ops_task_type'), this.getDicts('ops_task_status')])
+          .then(([taskType, taskStatus]) => {
+            this.taskTypeOptions = taskType.data.values || []
+            this.taskStatusOptions = taskStatus.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
       async fetchTaskList() {
         this.loading = true
         try {
@@ -83,41 +100,6 @@
         this.$emit('update:visible', false)
         this.taskList = []
       },
-      // 任务类型标签
-      getTaskTypeLabel(type) {
-        const map = {
-          10: '需求评审',
-          20: '功能开发',
-          30: '功能测试',
-          35: 'BUG',
-          40: '系统发版',
-        }
-        return map[type] || '-'
-      },
-      // 任务状态标签
-      getTaskStatusLabel(status) {
-        const map = {
-          10: '待处理',
-          20: '处理中',
-          25: '暂停',
-          30: '已完成',
-          70: '阻塞',
-          90: '作废',
-        }
-        return map[status] || '-'
-      },
-      // 任务状态类型
-      getTaskStatusType(status) {
-        const map = {
-          10: 'info',
-          20: 'warning',
-          25: '',
-          30: 'success',
-          70: 'danger',
-          90: 'info',
-        }
-        return map[status] || ''
-      },
     },
   }
 </script>

+ 338 - 129
src/views/devops/software/components/TaskDetailDialog.vue

@@ -21,6 +21,10 @@
               提交
             </el-button>
           </template>
+          <template v-else-if="isEditMode">
+            <el-button size="small" @click="handleClose">取消</el-button>
+            <el-button :loading="submitLoading" size="small" type="primary" @click="handleEditSubmit">保存</el-button>
+          </template>
           <el-button v-else size="small" @click="handleClose">关闭</el-button>
         </div>
       </div>
@@ -44,7 +48,9 @@
             <i class="el-icon-s-promotion" />
           </div>
           <div class="card-content">
-            <div class="card-value">{{ (detailData && getTaskStatusLabel(detailData.taskStatus)) || '-' }}</div>
+            <div class="card-value">
+              {{ (detailData && selectDictLabel(taskStatusOptions, detailData.taskStatus)) || '-' }}
+            </div>
             <div class="card-label">当前状态</div>
           </div>
         </div>
@@ -53,7 +59,9 @@
             <i class="el-icon-warning" />
           </div>
           <div class="card-content">
-            <div class="card-value">{{ (detailData && getTaskTypeLabel(detailData.taskType)) || '-' }}</div>
+            <div class="card-value">
+              {{ (detailData && selectDictLabel(taskTypeOptions, detailData.taskType)) || '-' }}
+            </div>
             <div class="card-label">任务类型</div>
           </div>
         </div>
@@ -90,13 +98,29 @@
           <div class="task-desc-header">
             <div class="section-title">任务描述</div>
             <el-button
+              v-if="!isEditMode"
               class="task-desc-action"
               icon="el-icon-full-screen"
               size="mini"
               type="text"
               @click="toggleTaskDescExpanded" />
           </div>
-          <div class="task-desc-wrapper">
+          <div v-if="isEditMode" class="task-desc-wrapper task-desc-wrapper--edit">
+            <div style="border: 1px solid #dcdfe6; border-radius: 4px">
+              <Toolbar
+                :default-config="descToolbarConfig"
+                :editor="descEditor"
+                :mode="descEditorMode"
+                style="border-bottom: 1px solid #dcdfe6" />
+              <Editor
+                v-model="scheduleForm.taskDesc"
+                :default-config="descEditorConfig"
+                :mode="descEditorMode"
+                style="height: 200px; overflow-y: hidden"
+                @onCreated="onDescEditorCreated" />
+            </div>
+          </div>
+          <div v-else class="task-desc-wrapper">
             <div class="task-desc" v-html="sanitizeHtml(detailData && detailData.taskDesc)"></div>
           </div>
 
@@ -105,13 +129,13 @@
             <div class="section-title">属性信息</div>
             <!-- 排期模式显示可编辑表单 -->
             <el-form
-              v-if="isScheduleMode"
+              v-if="isScheduleMode || isEditMode"
               ref="scheduleForm"
               class="property-form"
               label-position="right"
               label-width="70px"
               :model="scheduleForm"
-              :rules="scheduleRules"
+              :rules="isEditMode ? editRules : scheduleRules"
               size="small">
               <div class="property-grid">
                 <div v-if="!isReleaseTask" class="property-item">
@@ -120,20 +144,8 @@
                 </div>
                 <div class="property-item">
                   <span class="property-label">任务类型</span>
-                  <span class="property-value">{{ (detailData && getTaskTypeLabel(detailData.taskType)) || '-' }}</span>
-                </div>
-                <!-- 发版类型显示发布版本 -->
-                <div v-if="isReleaseTask" class="property-item">
-                  <span class="property-label">发布版本</span>
-                  <span class="property-value release-version">
-                    {{ (detailData && detailData.releaseVersion) || '-' }}
-                    <el-tooltip v-if="detailData && detailData.releaseVersion" content="查看关联任务" placement="top">
-                      <el-button
-                        class="view-release-btn"
-                        icon="el-icon-view"
-                        type="text"
-                        @click="handleViewReleaseDetail" />
-                    </el-tooltip>
+                  <span class="property-value">
+                    {{ (detailData && selectDictLabel(taskTypeOptions, detailData.taskType)) || '-' }}
                   </span>
                 </div>
                 <div class="property-item">
@@ -156,24 +168,20 @@
                     </el-select>
                   </el-form-item>
                 </div>
-                <div class="property-item">
+                <div class="property-item estimate-workhour-item">
                   <el-form-item label="预估工时" prop="estimateWorkHour" style="margin-bottom: 0">
-                    <el-input-number
-                      v-model="scheduleForm.estimateWorkHour"
-                      controls-position="right"
-                      :min="0"
-                      :precision="1"
-                      :step="0.5"
-                      style="width: 100%" />
+                    <div class="estimate-workhour-wrapper">
+                      <el-input-number
+                        v-model="scheduleForm.estimateWorkHour"
+                        class="estimate-input"
+                        controls-position="right"
+                        :min="0"
+                        :precision="1" />
+                      <el-button class="estimate-btn" size="mini" @click="addEstimateHour(0.5)">+0.5</el-button>
+                      <el-button class="estimate-btn" size="mini" @click="addEstimateHour(2)">+2</el-button>
+                    </div>
                   </el-form-item>
                 </div>
-                <!-- BUG类型显示缺陷类型 -->
-                <div v-if="isBugTask" class="property-item">
-                  <span class="property-label">缺陷类型</span>
-                  <span class="property-value">
-                    {{ (detailData && getDefectTypeLabel(detailData.defectType)) || '-' }}
-                  </span>
-                </div>
                 <div class="property-item">
                   <el-form-item label="计划开始" prop="planStartTime" style="margin-bottom: 0">
                     <el-date-picker
@@ -196,6 +204,28 @@
                       value-format="yyyy-MM-dd HH:mm:ss" />
                   </el-form-item>
                 </div>
+                <!-- 动态显示的属性 -->
+                <!-- 发版类型显示发布版本 -->
+                <div v-if="isReleaseTask" class="property-item">
+                  <span class="property-label">发布版本</span>
+                  <span class="property-value release-version">
+                    {{ (detailData && detailData.releaseVersion) || '-' }}
+                    <el-tooltip v-if="detailData && detailData.releaseVersion" content="查看关联任务" placement="top">
+                      <el-button
+                        class="view-release-btn"
+                        icon="el-icon-view"
+                        type="text"
+                        @click="handleViewReleaseDetail" />
+                    </el-tooltip>
+                  </span>
+                </div>
+                <!-- BUG类型显示缺陷类型 -->
+                <div v-if="isBugTask" class="property-item">
+                  <span class="property-label">缺陷类型</span>
+                  <span class="property-value">
+                    {{ (detailData && selectDictLabel(defectTypeOptions, detailData.defectType)) || '-' }}
+                  </span>
+                </div>
                 <div class="property-item">
                   <span class="property-label">创建人</span>
                   <span class="property-value">{{ (detailData && detailData.createdName) || '-' }}</span>
@@ -214,27 +244,15 @@
               </div>
               <div class="property-item">
                 <span class="property-label">任务类型</span>
-                <span class="property-value">{{ (detailData && getTaskTypeLabel(detailData.taskType)) || '-' }}</span>
-              </div>
-              <!-- 发版类型显示发布版本 -->
-              <div v-if="isReleaseTask" class="property-item">
-                <span class="property-label">发布版本</span>
-                <span class="property-value release-version">
-                  {{ (detailData && detailData.releaseVersion) || '-' }}
-                  <el-tooltip v-if="detailData && detailData.releaseVersion" content="查看关联任务" placement="top">
-                    <el-button
-                      class="view-release-btn"
-                      icon="el-icon-view"
-                      type="text"
-                      @click="handleViewReleaseDetail" />
-                  </el-tooltip>
+                <span class="property-value">
+                  {{ (detailData && selectDictLabel(taskTypeOptions, detailData.taskType)) || '-' }}
                 </span>
               </div>
               <div class="property-item">
                 <span class="property-label">优先级</span>
                 <span class="property-value">
-                  <el-tag size="mini" :type="getPriorityType(detailData && detailData.priority)">
-                    {{ (detailData && getPriorityLabel(detailData.priority)) || '-' }}
+                  <el-tag size="mini" :type="getPriorityTagType(detailData && detailData.priority)">
+                    {{ (detailData && selectDictLabel(priorityOptions, detailData.priority)) || '-' }}
                   </el-tag>
                 </span>
               </div>
@@ -242,13 +260,6 @@
                 <span class="property-label">负责人</span>
                 <span class="property-value">{{ (detailData && detailData.opsUserName) || '-' }}</span>
               </div>
-              <!-- BUG类型显示缺陷类型 -->
-              <div v-if="isBugTask" class="property-item">
-                <span class="property-label">缺陷类型</span>
-                <span class="property-value">
-                  {{ (detailData && getDefectTypeLabel(detailData.defectType)) || '-' }}
-                </span>
-              </div>
               <div class="property-item">
                 <span class="property-label">计划开始</span>
                 <span class="property-value">{{ (detailData && formatDate(detailData.planStartTime)) || '-' }}</span>
@@ -265,8 +276,37 @@
               </div>
               <div class="property-item">
                 <span class="property-label">实际工时</span>
-                <span class="property-value">
+                <span class="property-value work-hour-value">
                   {{ detailData && detailData.actualWorkHour ? detailData.actualWorkHour + 'h' : '-' }}
+                  <el-tooltip content="查看工时登记" placement="top">
+                    <i class="el-icon-warning-outline work-hour-info-icon" @click.stop="handleViewWorkHourList" />
+                  </el-tooltip>
+                </span>
+              </div>
+              <div class="property-item">
+                <span class="property-label">完成日期</span>
+                <span class="property-value">{{ (detailData && formatDate(detailData.completeTime)) || '-' }}</span>
+              </div>
+              <!-- 动态显示的属性(换行展示) -->
+              <!-- 发版类型显示发布版本 -->
+              <div v-if="isReleaseTask" class="property-item property-item--full">
+                <span class="property-label">发布版本</span>
+                <span class="property-value release-version">
+                  {{ (detailData && detailData.releaseVersion) || '-' }}
+                  <el-tooltip v-if="detailData && detailData.releaseVersion" content="查看关联任务" placement="top">
+                    <el-button
+                      class="view-release-btn"
+                      icon="el-icon-view"
+                      type="text"
+                      @click="handleViewReleaseDetail" />
+                  </el-tooltip>
+                </span>
+              </div>
+              <!-- BUG类型显示缺陷类型 -->
+              <div v-if="isBugTask" class="property-item property-item--full">
+                <span class="property-label">缺陷类型</span>
+                <span class="property-value">
+                  {{ (detailData && selectDictLabel(defectTypeOptions, detailData.defectType)) || '-' }}
                 </span>
               </div>
               <div class="property-item">
@@ -369,13 +409,13 @@
                 <div v-for="(task, index) in relatedTaskList" :key="index" class="related-task-item">
                   <div class="related-task-header">
                     <span class="related-task-no">{{ task.taskNo }}</span>
-                    <el-tag size="mini" :type="getTaskStatusType(task.taskStatus)">
-                      {{ getTaskStatusLabel(task.taskStatus) }}
+                    <el-tag size="mini" :type="getTaskStatusTagType(task.taskStatus)">
+                      {{ selectDictLabel(taskStatusOptions, task.taskStatus) }}
                     </el-tag>
                   </div>
                   <div class="related-task-title">{{ task.taskTitle }}</div>
                   <div class="related-task-meta">
-                    <span>{{ getTaskTypeLabel(task.taskType) }}</span>
+                    <span>{{ selectDictLabel(taskTypeOptions, task.taskType) }}</span>
                     <span v-if="task.opsUserName">| {{ task.opsUserName }}</span>
                   </div>
                 </div>
@@ -391,6 +431,12 @@
       :release-task-id="detailData && detailData.id"
       :visible.sync="releaseDialogVisible"
       @close="releaseDialogVisible = false" />
+
+    <!-- 工时登记历史弹窗 -->
+    <work-hour-list-dialog
+      :task-id="detailData && detailData.id"
+      :visible.sync="workHourListDialogVisible"
+      @close="workHourListDialogVisible = false" />
   </div>
 </template>
 
@@ -399,15 +445,17 @@
   import opsEventTaskApi from '@/api/devops/opsEventTask'
   import userApi from '@/api/system/user'
   import { parseTime } from '@/utils'
-  import { uploadFileToRichtextServer } from '@/utils/richtextUpload'
+  import { uploadFileToRichtextServer, uploadRichtextImage } from '@/utils/richtextUpload'
   import { openSafeUrl, sanitizeHtml } from '@/utils/safeHtml'
   import { DEVOPS_DEV_DEPT_ID } from '@/config/devops.config'
+  import { taskStatusTagTypes, priorityTagTypes, getTagType } from '@/config/devopsTagTypes'
   import debounce from 'lodash/debounce'
   import ReleaseTaskListDialog from './ReleaseTaskListDialog.vue'
+  import WorkHourListDialog from './WorkHourListDialog.vue'
 
   export default {
     name: 'TaskDetailDialog',
-    components: { Editor, Toolbar, ReleaseTaskListDialog },
+    components: { Editor, Toolbar, ReleaseTaskListDialog, WorkHourListDialog },
     props: {
       visible: {
         type: Boolean,
@@ -429,10 +477,41 @@
         attachmentList: [],
         relatedTaskList: [],
         releaseDialogVisible: false,
+        workHourListDialogVisible: false,
         // 快速登记相关
         showQuickInput: false,
         quickEditor: null,
         editorMode: 'default',
+        // 描述富文本编辑器
+        descEditor: null,
+        descEditorMode: 'default',
+        descToolbarConfig: {
+          toolbarKeys: [
+            'bold',
+            'italic',
+            'underline',
+            'through',
+            '|',
+            'color',
+            'bgColor',
+            '|',
+            'bulletedList',
+            'numberedList',
+            '|',
+            'uploadImage',
+            '|',
+            'undo',
+            'redo',
+          ],
+        },
+        descEditorConfig: {
+          placeholder: '请输入任务描述...',
+          MENU_CONF: {
+            uploadImage: {
+              customUpload: this.handleDescImageUpload,
+            },
+          },
+        },
         quickToolbarConfig: {
           toolbarKeys: ['uploadImage', '|', 'bold', 'italic', 'underline', '|', 'bulletedList', 'numberedList'],
         },
@@ -451,6 +530,7 @@
         isTaskDescExpanded: false,
         // 排期表单相关
         scheduleForm: {
+          taskDesc: '',
           opsUserId: null,
           opsUserName: '',
           planStartTime: null,
@@ -466,12 +546,20 @@
           planEndTime: [{ required: true, message: '请选择计划结束时间', trigger: 'change' }],
           estimateWorkHour: [{ required: true, type: 'number', message: '请输入预估工作量', trigger: 'blur' }],
         },
+        editRules: {},
+        taskTypeOptions: [],
+        taskStatusOptions: [],
+        priorityOptions: [],
+        defectTypeOptions: [],
       }
     },
     computed: {
       isScheduleMode() {
         return this.mode === 'schedule'
       },
+      isEditMode() {
+        return this.mode === 'edit'
+      },
       isBugTask() {
         return this.detailData && String(this.detailData.taskType) === '35'
       },
@@ -491,7 +579,7 @@
         }
       },
       mode() {
-        if (this.visible && this.isScheduleMode) {
+        if (this.visible && (this.isScheduleMode || this.isEditMode)) {
           this.initScheduleForm()
         }
       },
@@ -504,8 +592,13 @@
         this.quickEditor.destroy()
         this.quickEditor = null
       }
+      if (this.descEditor) {
+        this.descEditor.destroy()
+        this.descEditor = null
+      }
     },
     created() {
+      this.getOptions()
       this.remoteFetchUserList = debounce((query) => {
         this.fetchUserList(query)
       }, 300)
@@ -515,6 +608,28 @@
       }
     },
     methods: {
+      getTagType,
+      getPriorityTagType(priority) {
+        return getTagType(priorityTagTypes, priority, 'info')
+      },
+      getTaskStatusTagType(status) {
+        return getTagType(taskStatusTagTypes, status, 'info')
+      },
+      getOptions() {
+        Promise.all([
+          this.getDicts('ops_task_type'),
+          this.getDicts('ops_task_status'),
+          this.getDicts('ops_priority'),
+          this.getDicts('ops_defect_type'),
+        ])
+          .then(([taskType, taskStatus, priority, defectType]) => {
+            this.taskTypeOptions = taskType.data.values || []
+            this.taskStatusOptions = taskStatus.data.values || []
+            this.priorityOptions = priority.data.values || []
+            this.defectTypeOptions = defectType.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
       initDialogData() {
         this.isTaskDescExpanded = false
         this.activeTab = 'record'
@@ -526,14 +641,15 @@
         } else {
           this.relatedTaskList = []
         }
-        // 排期模式下初始化表单数据
-        if (this.isScheduleMode) {
+        // 排期/编辑模式下初始化表单数据
+        if (this.isScheduleMode || this.isEditMode) {
           this.initScheduleForm()
         }
       },
       initScheduleForm() {
         const opsUserId = this.detailData.opsUserId ? Number(this.detailData.opsUserId) : null
         this.scheduleForm = {
+          taskDesc: this.detailData.taskDesc || '',
           opsUserId,
           opsUserName: this.detailData.opsUserName || '',
           planStartTime: this.detailData.planStartTime || null,
@@ -567,6 +683,7 @@
         this.isTaskDescExpanded = false
         // 重置排期表单
         this.scheduleForm = {
+          taskDesc: '',
           opsUserId: null,
           opsUserName: '',
           planStartTime: null,
@@ -595,6 +712,10 @@
         const user = this.userOptions.find((u) => u.value === val)
         this.scheduleForm.opsUserName = user ? user.label : ''
       },
+      addEstimateHour(hours) {
+        const current = Number(this.scheduleForm.estimateWorkHour) || 0
+        this.scheduleForm.estimateWorkHour = Math.round((current + hours) * 10) / 10
+      },
       handleScheduleSubmit() {
         this.$refs.scheduleForm.validate(async (valid) => {
           if (!valid) return
@@ -623,12 +744,104 @@
           }
         })
       },
+      async handleEditSubmit() {
+        if (!this.detailData || !this.detailData.id) {
+          this.$message.error('任务ID不能为空')
+          return
+        }
+        this.submitLoading = true
+
+        // 保存旧值用于变更记录(仅对比非描述字段)
+        const oldData = {
+          opsUserName: this.detailData.opsUserName || '',
+          opsUserId: this.detailData.opsUserId || '',
+          planStartTime: this.detailData.planStartTime || '',
+          planEndTime: this.detailData.planEndTime || '',
+          estimateWorkHour:
+            this.detailData.estimateWorkHour !== undefined && this.detailData.estimateWorkHour !== null
+              ? Number(this.detailData.estimateWorkHour)
+              : null,
+        }
+
+        try {
+          const payload = {
+            id: this.detailData.id,
+            taskDesc: this.scheduleForm.taskDesc,
+            opsUserId: this.scheduleForm.opsUserId,
+            opsUserName: this.scheduleForm.opsUserName,
+            planStartTime: this.scheduleForm.planStartTime ? this.scheduleForm.planStartTime.substring(0, 10) : null,
+            planEndTime: this.scheduleForm.planEndTime ? this.scheduleForm.planEndTime.substring(0, 10) : null,
+            estimateWorkHour: this.scheduleForm.estimateWorkHour,
+          }
+          await opsEventTaskApi.update(payload)
+
+          // 构建变更记录(仅记录执行人/计划开始/计划结束/预估工时,不记录描述)
+          const changes = this.buildChangeRecords(oldData)
+          if (changes.length) {
+            await opsEventTaskApi.addRecord({
+              taskId: this.detailData.id,
+              handleContent: changes.join('<br>'),
+            })
+          }
+
+          this.$message.success('保存成功')
+          this.$emit('refresh')
+          this.handleClose()
+        } catch (err) {
+          this.$message.error('保存失败: ' + (err.message || err))
+        } finally {
+          this.submitLoading = false
+        }
+      },
+      buildChangeRecords(oldData) {
+        const changes = []
+        const newOpsUserName = this.scheduleForm.opsUserName || ''
+        const newPlanStart = this.scheduleForm.planStartTime || ''
+        const newPlanEnd = this.scheduleForm.planEndTime || ''
+        const newEstimateHour =
+          this.scheduleForm.estimateWorkHour !== undefined && this.scheduleForm.estimateWorkHour !== null
+            ? Number(this.scheduleForm.estimateWorkHour)
+            : null
+
+        if (String(oldData.opsUserId) !== String(this.scheduleForm.opsUserId || '')) {
+          changes.push(`变更执行人:${oldData.opsUserName || '-'} → ${newOpsUserName || '-'}`)
+        }
+        if (oldData.planStartTime !== newPlanStart) {
+          changes.push(`变更计划开始时间:${this.formatDate(oldData.planStartTime)} → ${this.formatDate(newPlanStart)}`)
+        }
+        if (oldData.planEndTime !== newPlanEnd) {
+          changes.push(`变更计划结束时间:${this.formatDate(oldData.planEndTime)} → ${this.formatDate(newPlanEnd)}`)
+        }
+        if (oldData.estimateWorkHour !== newEstimateHour) {
+          changes.push(
+            `变更预估工时:${oldData.estimateWorkHour !== null ? oldData.estimateWorkHour + 'h' : '-'} → ${
+              newEstimateHour !== null ? newEstimateHour + 'h' : '-'
+            }`
+          )
+        }
+        return changes
+      },
       handleViewReleaseDetail() {
         this.releaseDialogVisible = true
       },
+      handleViewWorkHourList() {
+        this.workHourListDialogVisible = true
+      },
       onQuickEditorCreated(editor) {
         this.quickEditor = editor
       },
+      // 描述富文本编辑器
+      onDescEditorCreated(editor) {
+        this.descEditor = editor
+      },
+      async handleDescImageUpload(file, insertFn) {
+        try {
+          await uploadRichtextImage(file, insertFn)
+        } catch (err) {
+          this.$message.error('图片上传失败')
+          console.error(err)
+        }
+      },
       async handleQuickRecordImageUpload(file, insertFn) {
         try {
           const result = await uploadFileToRichtextServer(file)
@@ -771,69 +984,6 @@
       downloadFile(file) {
         openSafeUrl(file && file.fileUrl, this.$message)
       },
-      // 任务类型标签
-      getTaskTypeLabel(type) {
-        const map = {
-          10: '需求评审',
-          20: '功能开发',
-          30: '功能测试',
-          35: 'BUG',
-          40: '系统发版',
-        }
-        return map[type] || '-'
-      },
-      // 优先级标签
-      getPriorityLabel(priority) {
-        const map = {
-          10: '紧急',
-          20: '高',
-          30: '中',
-          40: '低',
-        }
-        return map[priority] || '-'
-      },
-      // 优先级类型
-      getPriorityType(priority) {
-        const map = {
-          10: 'danger',
-          20: 'warning',
-          30: '',
-          40: 'info',
-        }
-        return map[priority] || ''
-      },
-      // 任务状态标签
-      getTaskStatusLabel(status) {
-        const map = {
-          10: '待处理',
-          20: '处理中',
-          25: '暂停',
-          30: '已完成',
-          70: '阻塞',
-          90: '作废',
-        }
-        return map[status] || '-'
-      },
-      // 任务状态类型
-      getTaskStatusType(status) {
-        const map = {
-          10: 'info',
-          20: 'warning',
-          25: '',
-          30: 'success',
-          70: 'danger',
-          90: 'info',
-        }
-        return map[status] || ''
-      },
-      // 缺陷类型标签
-      getDefectTypeLabel(type) {
-        const map = {
-          10: '前端',
-          20: '后端',
-        }
-        return map[type] || '-'
-      },
       formatTime(time) {
         return time ? parseTime(time, '{y}-{m}-{d} {h}:{i}') : '-'
       },
@@ -1038,6 +1188,13 @@
         overflow-x: hidden;
         padding: 12px;
 
+        &.task-desc-wrapper--edit {
+          height: auto;
+          background: transparent;
+          padding: 0;
+          overflow: visible;
+        }
+
         &.task-desc-wrapper--fullscreen {
           flex: 1;
           height: auto;
@@ -1075,6 +1232,10 @@
             align-items: center;
             min-width: 0;
 
+            &.property-item--full {
+              grid-column: 1 / -1;
+            }
+
             .property-label {
               width: 70px;
               font-size: 13px;
@@ -1142,6 +1303,45 @@
                 }
               }
             }
+
+            .work-hour-info-icon {
+              margin-left: 6px;
+              font-size: 16px;
+              color: #e6a23c;
+              cursor: pointer;
+              vertical-align: middle;
+
+              &:hover {
+                color: #f56c6c;
+              }
+            }
+          }
+
+          // 预估工时输入区域样式优化
+          .estimate-workhour-item {
+            .estimate-workhour-wrapper {
+              display: flex;
+              gap: 4px;
+              align-items: center;
+              width: 100%;
+
+              .estimate-input {
+                width: 90px;
+                flex-shrink: 0;
+
+                ::v-deep .el-input__inner {
+                  padding-left: 8px;
+                  padding-right: 40px;
+                  text-align: left;
+                }
+              }
+
+              .estimate-btn {
+                padding: 7px 10px;
+                font-size: 12px;
+                flex-shrink: 0;
+              }
+            }
           }
         }
       }
@@ -1506,6 +1706,15 @@
         box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
       }
     }
+
+    .estimate-btn {
+      padding: 0 4px;
+      font-size: 12px;
+      line-height: 1;
+      min-width: auto;
+      height: 22px;
+      flex-shrink: 0;
+    }
   }
 </style>
 

+ 16 - 11
src/views/devops/software/components/TaskEditDialog.vue

@@ -72,11 +72,7 @@
               placeholder="请选择任务类型"
               style="width: 100%"
               @change="handleTaskTypeChange">
-              <el-option label="需求评审" value="10" />
-              <el-option label="功能开发" value="20" />
-              <el-option label="功能测试" value="30" />
-              <el-option label="BUG" value="35" />
-              <el-option label="系统发版" value="40" />
+              <el-option v-for="dict in taskTypeOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
             </el-select>
           </el-form-item>
         </el-col>
@@ -87,10 +83,7 @@
           <!-- 优先级 -->
           <el-form-item label="优先级" prop="priority">
             <el-select v-model="form.priority" placeholder="请选择优先级" style="width: 100%">
-              <el-option label="紧急" value="10" />
-              <el-option label="高" value="20" />
-              <el-option label="中" value="30" />
-              <el-option label="低" value="40" />
+              <el-option v-for="dict in priorityOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
             </el-select>
           </el-form-item>
         </el-col>
@@ -154,8 +147,7 @@
         <el-col v-if="isBugType" :span="12">
           <el-form-item label="缺陷类型" prop="defectType">
             <el-select v-model="form.defectType" placeholder="请选择缺陷类型" style="width: 100%">
-              <el-option label="前端" value="10" />
-              <el-option label="后端" value="20" />
+              <el-option v-for="dict in defectTypeOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
             </el-select>
           </el-form-item>
         </el-col>
@@ -227,6 +219,9 @@
         loadingUsers: false,
         projectOptions: [],
         userOptions: [],
+        taskTypeOptions: [],
+        priorityOptions: [],
+        defectTypeOptions: [],
         editor: null,
         mode: 'default',
         toolbarConfig: {
@@ -327,6 +322,7 @@
       },
     },
     created() {
+      this.getOptions()
       this.remoteSearchProject = debounce((query) => {
         this.searchAllProjectsByStatus(query)
       }, 300)
@@ -347,6 +343,15 @@
       }
     },
     methods: {
+      getOptions() {
+        Promise.all([this.getDicts('ops_task_type'), this.getDicts('ops_priority'), this.getDicts('ops_defect_type')])
+          .then(([taskType, priority, defectType]) => {
+            this.taskTypeOptions = taskType.data.values || []
+            this.priorityOptions = priority.data.values || []
+            this.defectTypeOptions = defectType.data.values || []
+          })
+          .catch((err) => console.log(err))
+      },
       clearDynamicFieldValidate() {
         this.$nextTick(() => {
           if (this.$refs.taskForm) {

+ 142 - 0
src/views/devops/software/components/WorkHourDialog.vue

@@ -0,0 +1,142 @@
+<template>
+  <el-dialog
+    :close-on-click-modal="false"
+    :title="'工时登记'"
+    :visible="visible"
+    width="480px"
+    @close="handleClose"
+    @update:visible="$emit('update:visible', $event)">
+    <el-form ref="form" class="work-hour-dialog-form" label-width="90px" :model="form" :rules="rules">
+      <el-form-item label="工作日期" prop="workDate">
+        <el-date-picker
+          v-model="form.workDate"
+          format="yyyy-MM-dd"
+          placeholder="请选择日期"
+          style="width: 100%"
+          type="date"
+          value-format="yyyy-MM-dd" />
+      </el-form-item>
+      <el-form-item label="实际工时" prop="actualHour">
+        <div class="actual-hour-row">
+          <el-input-number
+            v-model="form.actualHour"
+            controls-position="right"
+            :min="0"
+            :precision="1"
+            style="width: 120px" />
+          <el-button size="mini" @click="addActualHour(0.5)">+0.5</el-button>
+          <el-button size="mini" @click="addActualHour(2)">+2</el-button>
+        </div>
+      </el-form-item>
+      <el-form-item label="工作进展" prop="remark">
+        <el-input v-model="form.remark" placeholder="请输入工作进展" :rows="3" type="textarea" />
+      </el-form-item>
+    </el-form>
+    <span slot="footer" class="dialog-footer">
+      <el-button @click="handleClose">取消</el-button>
+      <el-button type="primary" @click="handleSubmit">提交</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+  import opsEventTaskApi from '@/api/devops/opsEventTask'
+  import { parseTime } from '@/utils'
+
+  export default {
+    name: 'WorkHourDialog',
+    props: {
+      visible: {
+        type: Boolean,
+        required: true,
+      },
+      taskId: {
+        type: [Number, String],
+        default: null,
+      },
+    },
+    data() {
+      return {
+        form: {
+          workDate: parseTime(new Date(), '{y}-{m}-{d}'),
+          actualHour: null,
+          remark: '',
+        },
+        rules: {
+          workDate: [{ required: true, message: '请选择工作日期', trigger: 'change' }],
+          actualHour: [{ required: true, type: 'number', message: '请输入实际工时', trigger: 'blur' }],
+          remark: [
+            { required: true, message: '请输入工作进展', trigger: 'blur' },
+            { max: 500, message: '长度不能超过500字符', trigger: 'blur' },
+          ],
+        },
+      }
+    },
+    watch: {
+      visible(val) {
+        if (val) {
+          this.resetForm()
+        }
+      },
+    },
+    methods: {
+      resetForm() {
+        this.form = {
+          workDate: parseTime(new Date(), '{y}-{m}-{d}'),
+          actualHour: null,
+          remark: '',
+        }
+        this.$nextTick(() => {
+          this.$refs.form && this.$refs.form.clearValidate()
+        })
+      },
+      addActualHour(hours) {
+        const current = Number(this.form.actualHour) || 0
+        this.form.actualHour = Math.round((current + hours) * 10) / 10
+      },
+      handleSubmit() {
+        this.$refs.form.validate(async (valid) => {
+          if (!valid) return
+          if (!this.taskId) {
+            this.$message.error('任务ID不能为空')
+            return
+          }
+          try {
+            const payload = {
+              taskId: this.taskId,
+              workDate: this.form.workDate,
+              actualHour: this.form.actualHour,
+              remark: this.form.remark,
+            }
+            await opsEventTaskApi.addWorkHour(payload)
+            this.$message.success('工时登记成功')
+            this.$emit('refresh')
+            this.handleClose()
+          } catch (err) {
+            console.error(err)
+            this.$message.error('工时登记失败,请重试')
+          }
+        })
+      },
+      handleClose() {
+        this.$emit('update:visible', false)
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .work-hour-dialog-form {
+    padding: 0 20px;
+  }
+
+  .actual-hour-row {
+    display: flex;
+    gap: 6px;
+    align-items: center;
+
+    .el-button--mini {
+      flex-shrink: 0;
+    }
+  }
+</style>

+ 101 - 0
src/views/devops/software/components/WorkHourListDialog.vue

@@ -0,0 +1,101 @@
+<template>
+  <el-dialog :close-on-click-modal="false" title="工时登记记录" :visible="visible" width="640px" @close="handleClose">
+    <el-table v-loading="loading" border :data="list" size="small" style="width: 100%">
+      <el-table-column align="center" label="工作日期" prop="workDate" width="110">
+        <template slot-scope="{ row }">
+          {{ formatDate(row.workDate) }}
+        </template>
+      </el-table-column>
+      <el-table-column align="right" label="实际工时" prop="actualHour" width="100">
+        <template slot-scope="{ row }">
+          {{ row.actualHour ? row.actualHour + 'h' : '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="工作进展" min-width="160" prop="remark" show-overflow-tooltip>
+        <template slot-scope="{ row }">
+          {{ row.remark || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="登记人" prop="createdName" width="100">
+        <template slot-scope="{ row }">
+          {{ row.createdName || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="登记时间" prop="createdTime" width="150">
+        <template slot-scope="{ row }">
+          {{ formatTime(row.createdTime) }}
+        </template>
+      </el-table-column>
+    </el-table>
+    <div v-if="!loading && list.length === 0" class="empty-hint">暂无工时登记记录</div>
+    <span slot="footer" class="dialog-footer">
+      <el-button size="small" @click="handleClose">关闭</el-button>
+    </span>
+  </el-dialog>
+</template>
+
+<script>
+  import opsEventTaskApi from '@/api/devops/opsEventTask'
+  import { parseTime } from '@/utils'
+
+  export default {
+    name: 'WorkHourListDialog',
+    props: {
+      taskId: {
+        type: [Number, String],
+        default: null,
+      },
+      visible: {
+        type: Boolean,
+        default: false,
+      },
+    },
+    data() {
+      return {
+        list: [],
+        loading: false,
+      }
+    },
+    watch: {
+      visible(val) {
+        if (val && this.taskId) {
+          this.fetchList()
+        }
+      },
+    },
+    methods: {
+      async fetchList() {
+        this.loading = true
+        try {
+          const res = await opsEventTaskApi.getWorkHourList(this.taskId)
+          if (res.code === 200) {
+            this.list = res.data?.list || []
+          }
+        } catch (err) {
+          this.list = []
+        } finally {
+          this.loading = false
+        }
+      },
+      formatDate(time) {
+        return time ? parseTime(time, '{y}-{m}-{d}') : '-'
+      },
+      formatTime(time) {
+        return time ? parseTime(time, '{y}-{m}-{d} {h}:{i}') : '-'
+      },
+      handleClose() {
+        this.$emit('update:visible', false)
+        this.$emit('close')
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .empty-hint {
+    text-align: center;
+    color: #909399;
+    font-size: 13px;
+    padding: 32px 0;
+  }
+</style>

+ 219 - 113
src/views/devops/software/index.vue

@@ -18,10 +18,7 @@
               collapse-tags
               multiple
               placeholder="请选择">
-              <el-option label="需求评审" value="10" />
-              <el-option label="功能开发" value="20" />
-              <el-option label="功能测试" value="30" />
-              <el-option label="系统发版" value="40" />
+              <el-option v-for="dict in taskTypeOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
             </el-select>
           </el-form-item>
           <el-form-item label="任务状态">
@@ -31,20 +28,25 @@
               collapse-tags
               multiple
               placeholder="请选择">
-              <el-option label="待处理" value="10" />
-              <el-option label="处理中" value="20" />
-              <el-option label="暂停" value="25" />
-              <el-option label="已完成" value="30" />
-              <el-option label="阻塞" value="70" />
-              <el-option label="作废" value="90" />
+              <el-option v-for="dict in taskStatusOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
             </el-select>
           </el-form-item>
           <el-form-item label="负责人">
-            <el-input
+            <el-select
               v-model="queryForm.opsUserName"
-              class="query-input query-input--compact"
+              class="query-input query-input--owner"
               clearable
-              placeholder="请输入" />
+              collapse-tags
+              filterable
+              :loading="opsUsersLoading"
+              multiple
+              placeholder="请选择"
+              remote
+              :remote-method="remoteFetchOpsUsers"
+              reserve-keyword
+              @visible-change="handleOpsUserVisibleChange">
+              <el-option v-for="u in opsUserOptions" :key="u.value" :label="u.label" :value="u.label" />
+            </el-select>
           </el-form-item>
           <el-form-item label="计划结束">
             <el-date-picker
@@ -161,7 +163,7 @@
                   <span class="project-line-tag">{{ getProductLineLabel(project.productLine) }}</span>
                 </div>
                 <span :class="['project-status-tag', 'project-status-tag--' + project.status]">
-                  {{ getStatusLabel(project.status) }}
+                  {{ selectDictLabel(projectStatusOptions, project.status) }}
                 </span>
               </div>
               <div class="project-card-title" :title="project.name">{{ project.name }}</div>
@@ -200,8 +202,7 @@
             height="100%"
             stripe
             style="width: 100%"
-            @row-click="handleRowClick"
-            @sort-change="handleSortChange">
+            @row-click="handleRowClick">
             <el-table-column align="center" type="index" width="50" />
             <el-table-column v-if="isColumnVisible('taskNo')" label="任务编号" show-overflow-tooltip width="140">
               <template slot-scope="{ row }">
@@ -212,9 +213,8 @@
               v-if="isColumnVisible('taskTitle')"
               label="任务标题"
               min-width="200"
-              prop="taskTitle"
-              show-overflow-tooltip
-              sortable="custom">
+              :render-header="renderSortableHeader('任务标题', 'taskTitle')"
+              show-overflow-tooltip>
               <template slot-scope="{ row }">
                 <span class="task-title-text">{{ row.taskTitle || '-' }}</span>
               </template>
@@ -222,9 +222,8 @@
             <el-table-column
               v-if="isColumnVisible('functionName')"
               label="功能模块"
-              prop="functionName"
+              :render-header="renderSortableHeader('功能模块', 'functionName')"
               show-overflow-tooltip
-              sortable="custom"
               width="160">
               <template slot-scope="{ row }">
                 {{ row.functionName || '-' }}
@@ -233,45 +232,41 @@
             <el-table-column
               v-if="isColumnVisible('taskType')"
               label="任务类型"
-              prop="taskType"
-              sortable="custom"
+              :render-header="renderSortableHeader('任务类型', 'taskType')"
               width="116">
               <template slot-scope="{ row }">
                 <el-tag size="small" type="info">
-                  {{ getTaskTypeLabel(row.taskType) }}
+                  {{ selectDictLabel(taskTypeOptions, row.taskType) }}
                 </el-tag>
               </template>
             </el-table-column>
             <el-table-column
               v-if="isColumnVisible('taskStatus')"
               label="任务状态"
-              prop="taskStatus"
-              sortable="custom"
+              :render-header="renderSortableHeader('任务状态', 'taskStatus')"
               width="116">
               <template slot-scope="{ row }">
-                <el-tag size="small" :type="getTaskStatusType(row.taskStatus)">
-                  {{ getTaskStatusLabel(row.taskStatus) }}
+                <el-tag size="small" :type="getTaskStatusTagType(row.taskStatus)">
+                  {{ selectDictLabel(taskStatusOptions, row.taskStatus) }}
                 </el-tag>
               </template>
             </el-table-column>
             <el-table-column
               v-if="isColumnVisible('priority')"
               label="优先级"
-              prop="priority"
-              sortable="custom"
+              :render-header="renderSortableHeader('优先级', 'priority')"
               width="104">
               <template slot-scope="{ row }">
-                <el-tag size="small" :type="getPriorityType(row.priority)">
-                  {{ getPriorityLabel(row.priority) }}
+                <el-tag size="small" :type="getPriorityTagType(row.priority)">
+                  {{ selectDictLabel(priorityOptions, row.priority) }}
                 </el-tag>
               </template>
             </el-table-column>
             <el-table-column
               v-if="isColumnVisible('opsUserName')"
               label="负责人"
-              prop="opsUserName"
+              :render-header="renderSortableHeader('负责人', 'opsUserName')"
               show-overflow-tooltip
-              sortable="custom"
               width="120">
               <template slot-scope="{ row }">
                 {{ row.opsUserName || '-' }}
@@ -280,28 +275,25 @@
             <el-table-column
               v-if="isColumnVisible('planStartTime')"
               label="计划开始时间"
-              prop="planStartTime"
-              sortable="custom"
+              :render-header="renderSortableHeader('计划开始时间', 'planStartTime')"
               width="170">
               <template slot-scope="{ row }">
-                {{ formatTime(row.planStartTime) }}
+                {{ row.planStartTime ? parseTime(row.planStartTime, '{y}-{m}-{d}') : '-' }}
               </template>
             </el-table-column>
             <el-table-column
               v-if="isColumnVisible('planEndTime')"
               label="计划结束时间"
-              prop="planEndTime"
-              sortable="custom"
+              :render-header="renderSortableHeader('计划结束时间', 'planEndTime')"
               width="170">
               <template slot-scope="{ row }">
-                {{ formatTime(row.planEndTime) }}
+                {{ row.planEndTime ? parseTime(row.planEndTime, '{y}-{m}-{d}') : '-' }}
               </template>
             </el-table-column>
             <el-table-column
               v-if="isColumnVisible('completeTime')"
               label="完成时间"
-              prop="completeTime"
-              sortable="custom"
+              :render-header="renderSortableHeader('完成时间', 'completeTime')"
               width="170">
               <template slot-scope="{ row }">
                 {{ formatTime(row.completeTime) }}
@@ -329,15 +321,14 @@
             </el-table-column>
             <el-table-column v-if="isColumnVisible('defectType')" label="缺陷类型" width="100">
               <template slot-scope="{ row }">
-                {{ getDefectTypeLabel(row.defectType) }}
+                {{ selectDictLabel(defectTypeOptions, row.defectType) }}
               </template>
             </el-table-column>
             <el-table-column
               v-if="isColumnVisible('releaseVersion')"
               label="发布版本"
-              prop="releaseVersion"
+              :render-header="renderSortableHeader('发布版本', 'releaseVersion')"
               show-overflow-tooltip
-              sortable="custom"
               width="160">
               <template slot-scope="{ row }">
                 <div class="release-version-cell">
@@ -363,8 +354,7 @@
             <el-table-column
               v-if="isColumnVisible('createdTime')"
               label="创建时间"
-              prop="createdTime"
-              sortable="custom"
+              :render-header="renderSortableHeader('创建时间', 'createdTime')"
               width="170">
               <template slot-scope="{ row }">
                 {{ formatTime(row.createdTime) }}
@@ -490,6 +480,12 @@
       :visible.sync="cancelDialogVisible"
       @refresh="fetchData" />
 
+    <work-hour-dialog
+      v-if="workHourDialogVisible"
+      :task-id="currentTaskId"
+      :visible.sync="workHourDialogVisible"
+      @refresh="fetchData" />
+
     <release-complete-dialog
       v-if="releaseCompleteDialogVisible"
       :project-id="currentRow ? currentRow.projectId : null"
@@ -514,6 +510,9 @@
 <script>
   import opsEventTaskApi from '@/api/devops/opsEventTask'
   import deliveryProjectApi from '@/api/devops/deliveryProject'
+  import userApi from '@/api/system/user'
+  import dictApi from '@/api/system/dict'
+  import { DEVOPS_DEV_DEPT_ID } from '@/config/devops.config'
   import TaskEditDialog from './components/TaskEditDialog'
   import TaskDetailDialog from './components/TaskDetailDialog'
   import CompleteDialog from './components/CompleteDialog'
@@ -521,8 +520,10 @@
   import PauseDialog from './components/PauseDialog'
   import BlockDialog from './components/BlockDialog'
   import CancelDialog from './components/CancelDialog'
+  import WorkHourDialog from './components/WorkHourDialog'
   import ProjectInfoDialog from '../components/ProjectInfoDialog'
   import { parseTime } from '@/utils'
+  import { taskStatusTagTypes, priorityTagTypes, projectStatusTagTypes, getTagType } from '@/config/devopsTagTypes'
 
   const COLUMN_STORAGE_KEY = 'opms-devops-software-visible-columns'
   const TABLE_COLUMN_OPTIONS = [
@@ -555,6 +556,7 @@
       PauseDialog,
       BlockDialog,
       CancelDialog,
+      WorkHourDialog,
       ProjectInfoDialog,
     },
     data() {
@@ -566,14 +568,16 @@
           taskTitle: '',
           taskType: [],
           taskStatus: [],
-          opsUserName: '',
+          opsUserName: [],
           planEndDateRange: [],
           createdTimeRange: [],
           completeTimeRange: [],
         },
-        // 排序
-        sortField: '',
-        sortOrder: '',
+        // 多列排序
+        sortFields: [],
+        // 负责人下拉
+        opsUserOptions: [],
+        opsUsersLoading: false,
         columnOptions: TABLE_COLUMN_OPTIONS,
         visibleColumnKeys: TABLE_COLUMN_OPTIONS.map((column) => column.key),
         lastVisibleColumnKeys: TABLE_COLUMN_OPTIONS.map((column) => column.key),
@@ -588,6 +592,11 @@
         selectedProject: '',
         projects: [{ id: '', name: '全部' }],
         productLineOptions: [],
+        taskTypeOptions: [],
+        taskStatusOptions: [],
+        priorityOptions: [],
+        defectTypeOptions: [],
+        projectStatusOptions: [],
         // 弹窗控制
         editDialogVisible: false,
         detailDialogVisible: false,
@@ -597,6 +606,7 @@
         pauseDialogVisible: false,
         blockDialogVisible: false,
         cancelDialogVisible: false,
+        workHourDialogVisible: false,
         currentRow: null,
         currentTaskId: null,
         editData: null,
@@ -655,19 +665,29 @@
     mounted() {
       this.initVisibleColumns()
       this.getOptions()
+      this.remoteFetchOpsUsers('')
       this.fetchProjects()
-      // 设置默认负责人为当前登录人
-      const currentUserName = this.$store.getters['user/nickName'] || this.$store.getters['user/username']
-      if (currentUserName) {
-        this.queryForm.opsUserName = currentUserName
-      }
       this.fetchData()
     },
     methods: {
       getOptions() {
-        Promise.all([this.getDicts('sys_product_line')])
-          .then(([productLine]) => {
-            this.productLineOptions = productLine.data.values || []
+        dictApi
+          .getDictDataByTypes([
+            'sys_product_line',
+            'ops_task_type',
+            'ops_task_status',
+            'ops_priority',
+            'ops_defect_type',
+            'delivery_project_status',
+          ])
+          .then((res) => {
+            const dicts = res.data || {}
+            this.productLineOptions = (dicts.sys_product_line && dicts.sys_product_line.values) || []
+            this.taskTypeOptions = (dicts.ops_task_type && dicts.ops_task_type.values) || []
+            this.taskStatusOptions = (dicts.ops_task_status && dicts.ops_task_status.values) || []
+            this.priorityOptions = (dicts.ops_priority && dicts.ops_priority.values) || []
+            this.defectTypeOptions = (dicts.ops_defect_type && dicts.ops_defect_type.values) || []
+            this.projectStatusOptions = (dicts.delivery_project_status && dicts.delivery_project_status.values) || []
           })
           .catch((err) => console.log(err))
       },
@@ -707,9 +727,9 @@
         this.lastVisibleColumnKeys = [...validColumnKeys]
         this.persistVisibleColumns()
 
-        if (this.sortField && !validColumnKeys.includes(this.sortField)) {
-          this.sortField = ''
-          this.sortOrder = ''
+        const removed = this.sortFields.filter((s) => !validColumnKeys.includes(s.field))
+        if (removed.length) {
+          this.sortFields = this.sortFields.filter((s) => validColumnKeys.includes(s.field))
           this.$nextTick(() => {
             this.$refs.taskTable && this.$refs.taskTable.clearSort()
           })
@@ -728,7 +748,13 @@
       // 获取项目列表
       async fetchProjects() {
         try {
-          const params = { pageNum: 1, pageSize: 999 }
+          const params = {
+            pageNum: 1,
+            pageSize: 999,
+            productLine: '10,20,30',
+            sortField: 'contract_no',
+            sortOrder: 'desc',
+          }
           if (this.projectStatusFilter) {
             const statusMap = {
               undelivered: '10,20,30,40',
@@ -759,6 +785,8 @@
             deliveryOwner: item.deliveryUserName || item.delivery_user_name || '',
             status: String(item.projectStatus || item.project_status || ''),
           }))
+          // 按合同编号倒序排列(兜底)
+          list.sort((a, b) => (b.contractNo || '').localeCompare(a.contractNo || ''))
           this.projects = [{ id: '', name: '全部' }, ...list]
         } catch (error) {
           console.error('获取项目列表失败', error)
@@ -786,7 +814,7 @@
             completeTimeStart: this.queryForm.completeTimeRange?.[0] || '',
             completeTimeEnd: this.queryForm.completeTimeRange?.[1] || '',
             // 排序
-            sortFields: this.sortField && this.sortOrder ? [{ field: this.sortField, order: this.sortOrder }] : [],
+            sortFields: this.sortFields,
           }
           const res = await opsEventTaskApi.getList(params)
           this.tableData = res.data?.list || []
@@ -798,6 +826,30 @@
           this.loading = false
         }
       },
+      // 远程搜索负责人
+      async remoteFetchOpsUsers(search) {
+        this.opsUsersLoading = true
+        try {
+          const payload = { deptId: DEVOPS_DEV_DEPT_ID, pageNum: 1, pageSize: 999 }
+          if (search) payload.keyWords = search
+          const res = await userApi.getList(payload)
+          const list = res.data?.list || []
+          this.opsUserOptions = list.map((u) => ({
+            value: u.userId ?? u.user_id ?? u.id ?? null,
+            label: u.nickName ?? u.nick_name ?? u.name ?? '',
+          }))
+        } catch (error) {
+          console.error('获取负责人列表失败:', error)
+          this.opsUserOptions = []
+        } finally {
+          this.opsUsersLoading = false
+        }
+      },
+      handleOpsUserVisibleChange(visible) {
+        if (visible && !this.opsUsersLoading && !this.opsUserOptions.length) {
+          this.remoteFetchOpsUsers('')
+        }
+      },
       // 查询
       handleSearch() {
         this.queryForm.pageNum = 1
@@ -812,11 +864,12 @@
           taskTitle: '',
           taskType: [],
           taskStatus: [],
-          opsUserName: '',
+          opsUserName: [],
           planEndDateRange: [],
           createdTimeRange: [],
           completeTimeRange: [],
         }
+        this.sortFields = []
         this.selectedProject = ''
         this.fetchData()
       },
@@ -830,12 +883,46 @@
         this.queryForm.pageNum = 1
         this.fetchData()
       },
-      // 排序
-      handleSortChange({ prop, order }) {
-        this.sortField = prop || ''
-        this.sortOrder = order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : ''
+      // 多列排序:点击表头切换 无→升序→降序→无
+      toggleSort(field) {
+        const idx = this.sortFields.findIndex((s) => s.field === field)
+        if (idx === -1) {
+          if (this.sortFields.length >= 3) return
+          this.sortFields.push({ field, order: 'asc' })
+        } else if (this.sortFields[idx].order === 'asc') {
+          this.sortFields[idx].order = 'desc'
+        } else {
+          this.sortFields.splice(idx, 1)
+        }
         this.fetchData()
       },
+      getSortState(field) {
+        const sf = this.sortFields.find((s) => s.field === field)
+        return sf ? sf.order : ''
+      },
+      getSortPriority(field) {
+        const idx = this.sortFields.findIndex((s) => s.field === field)
+        return idx === -1 ? '' : String(idx + 1)
+      },
+      getColumnSortLabel(field) {
+        const option = this.columnOptions.find((c) => c.key === field)
+        return option ? option.label : field
+      },
+      renderSortableHeader(label, field) {
+        const vm = this
+        return function (h) {
+          const state = vm.getSortState(field)
+          const prio = vm.getSortPriority(field)
+          return h('div', { class: 'sortable-header', on: { click: () => vm.toggleSort(field) } }, [
+            h('span', { class: 'sortable-header-label' }, label),
+            h('span', { class: 'sort-arrows' }, [
+              h('i', { class: ['el-icon-caret-top', { 'sort-active': state === 'asc' }] }),
+              h('i', { class: ['el-icon-caret-bottom', { 'sort-active': state === 'desc' }] }),
+            ]),
+            prio ? h('span', { class: 'sort-priority' }, prio) : null,
+          ])
+        }
+      },
       // 侧边栏
       toggleSidebar() {
         this.sidebarCollapsed = !this.sidebarCollapsed
@@ -869,37 +956,32 @@
       getRowActions(row) {
         const status = String(row.taskStatus)
         const taskType = String(row.taskType)
-        const editAction = { key: 'edit', label: '编辑' }
         const actionMap = {
-          10: [editAction, { key: 'schedule', label: '排期' }],
+          10: [{ key: 'schedule', label: '排期' }],
           20: [
-            editAction,
             ...(taskType === '30'
               ? [
                   { key: 'pass', label: '通过' },
                   { key: 'fail', label: '不通过', tone: 'warning' },
                 ]
               : [{ key: 'complete', label: '完成' }]),
+            { key: 'workHour', label: '工时' },
             { key: 'pause', label: '暂停' },
             { key: 'block', label: '阻塞', tone: 'warning' },
             { key: 'cancel', label: '作废', tone: 'danger' },
           ],
           25: [
-            editAction,
             { key: 'start', label: '开始' },
             { key: 'block', label: '阻塞', tone: 'warning' },
             { key: 'cancel', label: '作废', tone: 'danger' },
           ],
           70: [
-            editAction,
             { key: 'start', label: '开始' },
             { key: 'pause', label: '暂停' },
             { key: 'cancel', label: '作废', tone: 'danger' },
           ],
-          30: [editAction],
-          90: [editAction],
         }
-        return actionMap[status] || [editAction]
+        return actionMap[status] || []
       },
       getPrimaryActions(row) {
         return this.getRowActions(row).slice(0, 3)
@@ -977,6 +1059,7 @@
           edit: this.handleEdit,
           schedule: this.handleSchedule,
           complete: this.handleComplete,
+          workHour: this.handleWorkHour,
           pass: () => {
             this.handleComplete(row, 'pass')
           }, // 通过按钮传参数
@@ -1000,10 +1083,10 @@
           this.failTaskRow = null
         }
       },
-      // 点击行显示详情
+      // 点击行显示详情(支持编辑)
       handleRowClick(row) {
         this.detailData = this.normalizeTaskDetail(row)
-        this.detailDialogMode = 'view'
+        this.detailDialogMode = 'edit'
         this.detailDialogVisible = true
       },
       // 查看发布版本关联任务详情
@@ -1089,6 +1172,11 @@
         this.currentTaskId = row.id
         this.pauseDialogVisible = true
       },
+      handleWorkHour(row) {
+        this.currentRow = row
+        this.currentTaskId = row.id
+        this.workHourDialogVisible = true
+      },
       handleBlock(row) {
         this.currentRow = row
         this.currentTaskId = row.id
@@ -1111,19 +1199,7 @@
           }
         }
       },
-      // 辅助方法
-      getTaskTypeLabel(type) {
-        const map = { 10: '需求评审', 20: '功能开发', 30: '功能测试', 40: '系统发版' }
-        return map[type] || type
-      },
-      getTaskStatusLabel(status) {
-        const map = { 10: '待处理', 20: '处理中', 25: '暂停', 30: '已完成', 70: '阻塞', 90: '作废' }
-        return map[status] || status
-      },
-      getTaskStatusType(status) {
-        const map = { 10: 'info', 20: 'primary', 25: 'warning', 30: 'success', 70: 'danger', 90: 'info' }
-        return map[status] || 'info'
-      },
+      // 辅助方法 — 标签映射已改用 getDicts + selectDictLabel,保留 badge class 映射(UI 呈现)
       getTaskStatusBadgeClass(status) {
         const map = {
           10: 'data-badge--pending',
@@ -1135,14 +1211,6 @@
         }
         return map[status] || 'data-badge--neutral'
       },
-      getPriorityLabel(priority) {
-        const map = { 10: '紧急', 20: '高', 30: '中', 40: '低' }
-        return map[priority] || priority
-      },
-      getPriorityType(priority) {
-        const map = { 10: 'danger', 20: 'warning', 30: 'primary', 40: 'info' }
-        return map[priority] || 'info'
-      },
       getPriorityBadgeClass(priority) {
         const map = {
           10: 'data-badge--urgent',
@@ -1152,20 +1220,14 @@
         }
         return map[priority] || 'data-badge--neutral'
       },
-      getDefectTypeLabel(type) {
-        const map = { 10: '前端', 20: '后端' }
-        return map[type] || '-'
+      getTaskStatusTagType(status) {
+        return getTagType(taskStatusTagTypes, status, 'info')
       },
-      getStatusLabel(status) {
-        const map = {
-          10: '待交付',
-          20: '交付中',
-          30: '暂停',
-          40: '交付完成',
-          50: '验收',
-          90: '作废',
-        }
-        return map[status] || status || '-'
+      getPriorityTagType(priority) {
+        return getTagType(priorityTagTypes, priority, 'info')
+      },
+      getProjectStatusTagType(status) {
+        return getTagType(projectStatusTagTypes, status, 'info')
       },
       formatTime(time) {
         return time ? parseTime(time, '{y}-{m}-{d} {h}:{i}') : '-'
@@ -1300,6 +1362,46 @@
     }
   }
 
+  ::v-deep .sortable-header {
+    display: inline-flex;
+    align-items: center;
+    gap: 4px;
+    cursor: pointer;
+    user-select: none;
+    white-space: nowrap;
+
+    .sort-arrows {
+      display: inline-flex;
+      flex-direction: column;
+      line-height: 1;
+
+      i {
+        font-size: 10px;
+        color: #c0c4cc;
+        transition: color 0.2s;
+
+        &.sort-active {
+          color: #409eff;
+        }
+      }
+    }
+
+    .sort-priority {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      width: 16px;
+      height: 16px;
+      border-radius: 50%;
+      background: #409eff;
+      color: #fff;
+      font-size: 10px;
+      font-weight: 700;
+      line-height: 1;
+      margin-left: 2px;
+    }
+  }
+
   .query-input {
     ::v-deep .el-input__inner,
     ::v-deep .el-range-input,
@@ -1316,6 +1418,10 @@
     width: 112px;
   }
 
+  .query-input--owner {
+    width: 180px;
+  }
+
   .query-input--date-range {
     width: 256px;
   }