浏览代码

修复系统BUG

程健 1 周之前
父节点
当前提交
8a41c20b19

+ 10 - 5
src/views/contract/event.vue

@@ -184,7 +184,6 @@
             </el-table-column>
             <el-table-column fixed="right" header-align="center" label="操作" width="120">
               <template slot-scope="{ row }">
-                <el-button size="mini" type="text" @click.stop="handleEdit(row)">编辑</el-button>
                 <el-button size="mini" style="color: #f56c6c" type="text" @click.stop="handleCancelEvent(row)">
                   作废
                 </el-button>
@@ -351,13 +350,12 @@
             ref="addUploadRef"
             action=""
             :auto-upload="false"
-            :before-upload="beforeUpload"
             :file-list="addFileList"
             :limit="5"
             :multiple="true"
             :on-change="handleAddFileChange"
             :on-remove="handleAddFileRemove">
-            <el-button size="small" type="primary">选择文件</el-button>
+            <el-button size="small" type="primary" @click.native="handleUploadClick">选择文件</el-button>
             <div slot="tip" class="el-upload__tip">最多上传5个文件,单个文件不超过20MB</div>
           </el-upload>
         </el-form-item>
@@ -686,6 +684,7 @@
           if (contract) {
             this.fillAddFormFromContract(contract)
             this.determineEventTypeByContract(contract)
+            this.addContractList = [contract]
           }
         }
         this.addDialogVisible = true
@@ -811,6 +810,12 @@
         this.addFileList = fileList
         this.addUploadFiles = fileList.filter((f) => f.raw).map((f) => f.raw)
       },
+      handleUploadClick() {
+        const uploadRef = this.$refs.addUploadRef
+        if (uploadRef && uploadRef.$refs.input) {
+          uploadRef.$refs.input.click()
+        }
+      },
       beforeUpload(file) {
         if (file.size > 20 * 1024 * 1024) {
           this.$message.error('文件大小不能超过20MB')
@@ -889,12 +894,12 @@
         })
       },
       handleRowClick(row) {
-        this.detailData = { id: row.eventId }
+        this.detailData = { ...row, id: row.eventId }
         this.detailDialogType = row.eventType
         this.detailDialogVisible = true
       },
       handleEdit(row) {
-        this.editData = { id: row.eventId }
+        this.editData = { ...row, id: row.eventId }
         this.editDialogType = row.eventType
         this.editDialogVisible = true
       },

+ 121 - 2
src/views/devops/deliveryProject/components/DeliveryProjectEventEdit.vue

@@ -96,6 +96,54 @@
           <div slot="tip" class="el-upload__tip">最多上传5个文件,单个文件不超过20MB</div>
         </el-upload>
       </el-form-item>
+
+      <el-divider content-position="left">研发任务</el-divider>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="创建研发任务">
+            <el-select
+              v-model="form.createRdTask"
+              :disabled="hasRdTask"
+              placeholder="请选择"
+              style="width: 100%"
+              @change="handleCreateRdTaskChange">
+              <el-option v-if="!hasRdTask" label="否" value="false" />
+              <el-option label="是" value="true" />
+            </el-select>
+            <span v-if="hasRdTask" style="color: #909399; font-size: 12px; margin-left: 8px">已创建研发任务</span>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <template v-if="form.createRdTask === 'true' && !hasRdTask">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="负责人" prop="rdTaskOpsUserId">
+              <el-select
+                v-model="form.rdTaskOpsUserId"
+                clearable
+                filterable
+                :loading="rdUsersLoading"
+                placeholder="请选择负责人"
+                remote
+                :remote-method="remoteFetchRdUsers"
+                style="width: 100%"
+                @change="handleRdUserChange"
+                @visible-change="handleRdUserVisibleChange">
+                <el-option v-for="u in rdUserOptions" :key="u.value" :label="u.label" :value="u.value" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="任务类型" prop="rdTaskType">
+              <el-select v-model="form.rdTaskType" placeholder="请选择任务类型" style="width: 100%">
+                <el-option v-for="dict in rdTaskTypeOptions" :key="dict.key" :label="dict.value" :value="dict.key" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </template>
     </el-form>
 
     <div slot="footer">
@@ -112,8 +160,10 @@
   import attachmentUploadMixin from '@/mixins/attachmentUpload'
   import deliveryProjectApi from '@/api/devops/deliveryProject'
   import deliveryProjectEventApi from '@/api/devops/deliveryProjectEvent'
+  import userApi from '@/api/system/user'
   import store from '@/store'
   import { uploadRichtextImage } from '@/utils/richtextUpload'
+  import { DEVOPS_DEV_DEPT_ID } from '@/config/devops.config'
   import debounce from 'lodash/debounce'
 
   export default {
@@ -176,6 +226,10 @@
           feedbackReporter: '',
           onSite: '20',
           attachments: [],
+          createRdTask: 'false',
+          rdTaskOpsUserId: null,
+          rdTaskOpsUserName: '',
+          rdTaskType: '',
         },
         rules: {
           projectId: [{ required: true, message: '请选择所属项目', trigger: 'change' }],
@@ -183,12 +237,18 @@
           deliveryEventType: [{ required: true, message: '请选择事件类型', trigger: 'change' }],
           feedbackSource: [{ required: true, message: '请选择反馈来源', trigger: 'change' }],
           feedbackReporter: [{ required: true, message: '请输入反馈人', trigger: 'blur' }],
+          rdTaskOpsUserId: [{ required: true, message: '请选择负责人', trigger: 'change' }],
+          rdTaskType: [{ required: true, message: '请选择任务类型', trigger: 'change' }],
         },
         submitLoading: false,
         projectOptions: [],
         deliveryEventTypeOptions: [],
         feedbackSourceOptions: [],
         onSiteOptions: [],
+        rdUserOptions: [],
+        rdUsersLoading: false,
+        rdTaskTypeOptions: [],
+        hasRdTask: false, // 是否已关联研发任务(编辑时从后端获取)
       }
     },
     computed: {
@@ -232,11 +292,13 @@
           this.getDicts('delivery_event_type'),
           this.getDicts('feedback_source'),
           this.getDicts('sys_yes_no'),
+          this.getDicts('ops_task_type'),
         ])
-          .then(([eventType, feedbackSource, onSite]) => {
+          .then(([eventType, feedbackSource, onSite, rdTaskType]) => {
             this.deliveryEventTypeOptions = eventType.data.values || []
             this.feedbackSourceOptions = feedbackSource.data.values || []
             this.onSiteOptions = onSite.data.values || []
+            this.rdTaskTypeOptions = rdTaskType.data.values || []
           })
           .catch((err) => console.log(err))
       },
@@ -255,6 +317,10 @@
           feedbackReporter: data.feedbackReporter || data.feedback_reporter || '',
           onSite: data.onSite || data.on_site || '20',
           attachments: data.attachments || [],
+          createRdTask: 'false',
+          rdTaskOpsUserId: null,
+          rdTaskOpsUserName: '',
+          rdTaskType: '',
         }
       },
       normalizeProjectOption(project) {
@@ -315,6 +381,10 @@
       },
       initForm() {
         this.form = this.normalizeFormData(this.data)
+        this.hasRdTask = this.data?.hasRdTask || false
+        if (this.hasRdTask) {
+          this.form.createRdTask = 'false'
+        }
         this.searchProject('')
         this.initAttachmentFiles(this.form.attachments || [])
       },
@@ -332,7 +402,13 @@
           feedbackReporter: nickName || userName || '',
           onSite: '20',
           attachments: [],
+          createRdTask: 'false',
+          rdTaskOpsUserId: null,
+          rdTaskOpsUserName: '',
+          rdTaskType: '',
         }
+        this.hasRdTask = false
+        this.rdUserOptions = []
         this.searchProject('')
         this.resetAttachmentFiles()
         if (this.editor) {
@@ -348,15 +424,24 @@
         this.$refs.form.validate(async (valid) => {
           if (!valid) return
 
+          // 编辑模式下,如果已创建研发任务,禁止再次创建
+          if (this.form.id && this.hasRdTask && this.form.createRdTask === 'true') {
+            this.$message.warning('此事件已关联研发任务,无法再次创建')
+            return
+          }
+
           this.submitLoading = true
           try {
-            // 上传附件
             const uploadedAttachments = await this.uploadAttachments()
 
             const submitData = {
               ...this.form,
               projectId: this.form.projectId ? parseInt(this.form.projectId) : null,
               attachments: uploadedAttachments,
+              createRdTask: this.form.createRdTask === 'true',
+              rdTaskOpsUserId: this.form.createRdTask === 'true' ? this.form.rdTaskOpsUserId : null,
+              rdTaskOpsUserName: this.form.createRdTask === 'true' ? this.form.rdTaskOpsUserName : '',
+              rdTaskType: this.form.createRdTask === 'true' ? this.form.rdTaskType : '',
             }
 
             const api = this.form.id ? deliveryProjectEventApi.update : deliveryProjectEventApi.create
@@ -373,6 +458,40 @@
           }
         })
       },
+      handleCreateRdTaskChange(val) {
+        if (val !== 'true') {
+          this.form.rdTaskOpsUserId = null
+          this.form.rdTaskOpsUserName = ''
+          this.form.rdTaskType = ''
+        }
+      },
+      handleRdUserChange(userId) {
+        const user = this.rdUserOptions.find((u) => u.value === userId)
+        this.form.rdTaskOpsUserName = user ? user.label : ''
+      },
+      async remoteFetchRdUsers(query) {
+        this.rdUsersLoading = true
+        try {
+          const payload = { deptId: DEVOPS_DEV_DEPT_ID, pageNum: 1, pageSize: 999 }
+          if (query) payload.keyWords = query
+          const res = await userApi.getList(payload)
+          const list = res.data?.list || []
+          this.rdUserOptions = 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.rdUserOptions = []
+        } finally {
+          this.rdUsersLoading = false
+        }
+      },
+      handleRdUserVisibleChange(visible) {
+        if (visible && !this.rdUsersLoading && !this.rdUserOptions.length) {
+          this.remoteFetchRdUsers('')
+        }
+      },
     },
   }
 </script>

+ 12 - 0
src/views/devops/operation/components/OperationDetail.vue

@@ -471,6 +471,7 @@
             this.totalWorkHour = (this.data && this.data.totalWorkHour) || 0
             await this.getOptions()
             this.initDialog()
+            await this.fetchDetail()
             this.fetchRecordList()
             this.fetchAttachmentList()
           }
@@ -566,6 +567,17 @@
           this.runInitialAction()
         })
       },
+      async fetchDetail() {
+        if (!this.data || !this.data.id) return
+        try {
+          const res = await operationEventApi.getDetail({ id: this.data.id })
+          if (res.code === 200 && res.data) {
+            Object.assign(this.data, res.data)
+          }
+        } catch (error) {
+          console.error('获取运维事件详情失败:', error)
+        }
+      },
       runInitialAction() {
         if (!this.detailAction || this.readOnly) return
         const actionMap = {

+ 163 - 2
src/views/devops/operationHistory/index.vue

@@ -38,12 +38,20 @@
                 @keyup.enter.native="queryData" />
             </el-form-item>
             <el-form-item prop="opsUserName">
-              <el-input
+              <el-select
                 v-model="queryForm.opsUserName"
                 clearable
+                filterable
+                :loading="opsUserLoading"
                 placeholder="处理人"
                 size="small"
-                @keyup.enter.native="queryData" />
+                style="width: 140px">
+                <el-option
+                  v-for="user in opsUserOptions"
+                  :key="user.userId || user.user_id || user.id"
+                  :label="user.nickName || user.nick_name || user.userName || user.user_name"
+                  :value="user.nickName || user.nick_name || user.userName || user.user_name" />
+              </el-select>
             </el-form-item>
             <el-form-item prop="dateRange">
               <el-date-picker
@@ -77,6 +85,9 @@
               @click="toggleSidebarFilters">
               <i class="el-icon-search" />
             </button>
+            <button class="sidebar-action-btn" title="查看全部项目" type="button" @click="handleViewAllProjects">
+              <i class="el-icon-menu" />
+            </button>
           </div>
           <div class="sidebar-actions">
             <button
@@ -99,6 +110,21 @@
               prefix-icon="el-icon-search"
               size="small"
               @input="onProjectSearchDebounced" />
+            <el-select
+              v-model="opsManagerUserId"
+              class="project-search"
+              clearable
+              filterable
+              :loading="opsManagerLoading"
+              placeholder="运维负责人"
+              size="small"
+              @change="handleOpsManagerFilterChange">
+              <el-option
+                v-for="user in opsManagerOptions"
+                :key="user.userId || user.user_id || user.id"
+                :label="user.nickName || user.nick_name || user.userName || user.user_name"
+                :value="user.userId || user.user_id || user.id" />
+            </el-select>
           </div>
           <div ref="projectList" class="project-list">
             <div
@@ -248,6 +274,54 @@
       :mode="'view'"
       :read-only="true"
       :visible.sync="detailVisible" />
+
+    <el-dialog
+      :close-on-click-modal="false"
+      title="全部项目列表"
+      top="3vh"
+      :visible.sync="projectListDialogVisible"
+      width="1000px">
+      <div class="project-list-dialog-body">
+        <el-table v-loading="projectListDialogLoading" border :data="allProjects" max-height="65vh">
+          <el-table-column align="center" label="序号" type="index" width="60" />
+          <el-table-column align="center" label="合同编号" min-width="130" prop="contractNo" show-overflow-tooltip />
+          <el-table-column align="center" label="项目名称" min-width="150" prop="projectName" show-overflow-tooltip />
+          <el-table-column align="center" label="产品线" min-width="100" show-overflow-tooltip>
+            <template #default="{ row }">
+              {{ getProductLineLabel(row.productLine) }}
+            </template>
+          </el-table-column>
+          <el-table-column align="center" label="项目状态" min-width="90" show-overflow-tooltip>
+            <template #default="{ row }">
+              {{ selectDictLabel(projectStatusOptions, row.projectStatus) }}
+            </template>
+          </el-table-column>
+          <el-table-column align="center" label="运维负责人" min-width="110" show-overflow-tooltip>
+            <template #default="{ row }">{{ row.attribute3 || '-' }}</template>
+          </el-table-column>
+          <el-table-column
+            align="center"
+            label="销售负责人"
+            min-width="100"
+            prop="salesUserName"
+            show-overflow-tooltip />
+          <el-table-column
+            align="center"
+            label="交付负责人"
+            min-width="100"
+            prop="deliveryUserName"
+            show-overflow-tooltip />
+        </el-table>
+        <el-pagination
+          v-if="projectListDialogTotal > 20"
+          background
+          :current-page.sync="projectListDialogPage"
+          layout="total, prev, pager, next"
+          :page-size="20"
+          :total="projectListDialogTotal"
+          @current-change="fetchAllProjects" />
+      </div>
+    </el-dialog>
   </div>
 </template>
 
@@ -257,6 +331,7 @@
   import debounce from 'lodash/debounce'
   import deliveryProjectApi from '@/api/devops/deliveryProject'
   import operationEventApi from '@/api/operation/operationEvent'
+  import userApi from '@/api/system/user'
   import dictApi from '@/api/system/dict'
   import OperationDetail from '@/views/devops/operation/components/OperationDetail'
 
@@ -290,6 +365,16 @@
         projectHasMore: true,
         sidebarCollapsed: false,
         showSidebarFilters: true,
+        opsUserOptions: [],
+        opsUserLoading: false,
+        opsManagerUserId: '',
+        opsManagerOptions: [],
+        opsManagerLoading: false,
+        projectListDialogVisible: false,
+        projectListDialogLoading: false,
+        allProjects: [],
+        projectListDialogTotal: 0,
+        projectListDialogPage: 1,
         queryForm: {
           scopeType: 'my',
           includeClosed: false,
@@ -319,6 +404,8 @@
         this.projectHasMore = true
         this.fetchProjects()
       }, 300)
+      this.fetchOpsUsers()
+      this.fetchOpsManagerUsers()
     },
     activated() {
       if (this.hasLoaded) {
@@ -454,6 +541,10 @@
             params.keyWords = keyword
           }
 
+          if (this.opsManagerUserId) {
+            params.opsManagerUserId = parseInt(this.opsManagerUserId)
+          }
+
           const res = await deliveryProjectApi.getList(params)
           const list = (res.data && res.data.list ? res.data.list : [])
             .map(this.normalizeProject)
@@ -557,6 +648,11 @@
           pageNum: 1,
           pageSize: 10,
         }
+        this.opsManagerUserId = ''
+        this.projectPageNum = 1
+        this.projects = []
+        this.projectHasMore = true
+        this.fetchProjects()
         this.fetchData()
       },
       toggleSidebar() {
@@ -635,6 +731,71 @@
           }
         }
       },
+      async fetchOpsUsers() {
+        this.opsUserLoading = true
+        try {
+          const res = await userApi.getUsersByRoleKeys(['OperationsEngineer'], [])
+          const list = res.data?.list || res.data || []
+          this.opsUserOptions = Array.isArray(list) ? list : []
+        } catch (error) {
+          console.error('获取处理人列表失败:', error)
+          this.opsUserOptions = []
+        } finally {
+          this.opsUserLoading = false
+        }
+      },
+      async fetchOpsManagerUsers() {
+        this.opsManagerLoading = true
+        try {
+          const res = await userApi.getUsersByRoleKeys(['OperationsEngineer'], [])
+          const list = res.data?.list || res.data || []
+          this.opsManagerOptions = Array.isArray(list) ? list : []
+        } catch (error) {
+          console.error('获取运维负责人列表失败:', error)
+          this.opsManagerOptions = []
+        } finally {
+          this.opsManagerLoading = false
+        }
+      },
+      handleOpsManagerFilterChange() {
+        this.projectPageNum = 1
+        this.projects = []
+        this.projectHasMore = true
+        this.fetchProjects()
+      },
+      async handleViewAllProjects() {
+        this.projectListDialogVisible = true
+        this.projectListDialogPage = 1
+        await this.fetchAllProjects()
+      },
+      async fetchAllProjects() {
+        this.projectListDialogLoading = true
+        try {
+          const params = {
+            pageNum: this.projectListDialogPage,
+            pageSize: 20,
+            productLine: '10,20,30,40,50,60',
+            sortField: 'contract_no',
+            sortOrder: 'desc',
+            projectStatus: '50',
+          }
+          const keyword = this.projectSearch.trim()
+          if (keyword) {
+            params.keyWords = keyword
+          }
+          if (this.opsManagerUserId) {
+            params.opsManagerUserId = parseInt(this.opsManagerUserId)
+          }
+          const res = await deliveryProjectApi.getList(params)
+          this.allProjects = res.data?.list || []
+          this.projectListDialogTotal = res.data?.total || 0
+        } catch (error) {
+          console.error('获取全部项目列表失败:', error)
+          this.allProjects = []
+        } finally {
+          this.projectListDialogLoading = false
+        }
+      },
     },
   }
 </script>

+ 67 - 23
src/views/devops/project/index.vue

@@ -4,15 +4,14 @@
     <div class="query-form-container">
       <div class="query-form-basic">
         <el-form class="query-form-fields" :inline="true" :model="queryForm" size="small">
-          <el-form-item>
-            <el-radio-group v-model="queryForm.projectStatusType" size="small">
-              <el-radio-button label="delivery">交付</el-radio-button>
-              <el-radio-button label="operation">运维</el-radio-button>
-            </el-radio-group>
-          </el-form-item>
           <el-form-item label="项目名称">
             <el-input v-model="queryForm.projectName" clearable placeholder="请输入" style="width: 180px" />
           </el-form-item>
+          <el-form-item label="项目状态">
+            <el-select v-model="queryForm.projectStatus" clearable placeholder="请选择" style="width: 150px">
+              <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="计划交付时间">
             <el-date-picker
               v-model="queryForm.planDeliveryTimeRange"
@@ -70,6 +69,15 @@
         </div>
         <div v-if="sidebarCollapsed" class="sidebar-collapsed-label">人员列表</div>
         <template v-if="!sidebarCollapsed">
+          <!-- 页签切换 -->
+          <div class="person-tabs">
+            <div :class="['person-tab', { active: personTab === 'delivery' }]" @click="switchPersonTab('delivery')">
+              交付
+            </div>
+            <div :class="['person-tab', { active: personTab === 'operation' }]" @click="switchPersonTab('operation')">
+              运维
+            </div>
+          </div>
           <!-- 人员搜索 -->
           <el-input
             v-model="personSearch"
@@ -239,12 +247,14 @@
           contractNo: '',
           projectName: '',
           productLine: '',
-          projectStatusType: 'delivery',
+          projectStatus: '',
           deliveryUserId: null,
+          opsManagerUserId: null,
           salesUserId: null,
           planDeliveryTimeRange: [],
         },
         personSearch: '',
+        personTab: 'delivery',
         selectedPerson: 'all',
         allPersonOption: { userId: 'all', userName: '全部人员', nickName: '全部人员' },
         personList: [],
@@ -303,16 +313,13 @@
         }
         delete params.planDeliveryTimeRange
 
-        // 根据项目状态类型设置对应的状态列表
-        if (params.projectStatusType === 'delivery') {
-          params.projectStatusList = [10, 20, 30, 40]
-        } else if (params.projectStatusType === 'operation') {
-          params.projectStatusList = [50]
-        }
-        delete params.projectStatusType
-
+        // 根据页签类型设置对应的人员ID过滤
         if (this.selectedPerson !== 'all') {
-          params.deliveryUserId = parseInt(this.selectedPerson)
+          if (this.personTab === 'delivery') {
+            params.deliveryUserId = parseInt(this.selectedPerson)
+          } else if (this.personTab === 'operation') {
+            params.opsManagerUserId = parseInt(this.selectedPerson)
+          }
         }
 
         projectInventoryApi
@@ -338,8 +345,9 @@
           contractNo: '',
           projectName: '',
           productLine: '',
-          projectStatusType: 'delivery',
+          projectStatus: '',
           deliveryUserId: null,
+          opsManagerUserId: null,
           salesUserId: null,
           planDeliveryTimeRange: [],
         }
@@ -358,6 +366,12 @@
       toggleSidebar() {
         this.sidebarCollapsed = !this.sidebarCollapsed
       },
+      switchPersonTab(tab) {
+        this.personTab = tab
+        this.selectedPerson = 'all'
+        this.queryForm.pageNum = 1
+        this.loadData()
+      },
       selectPerson(userId) {
         this.selectedPerson = userId
         this.queryForm.pageNum = 1
@@ -379,14 +393,16 @@
           contractNo: this.queryForm.contractNo,
           projectName: this.queryForm.projectName,
           productLine: this.queryForm.productLine,
-          deliveryUserId: this.selectedPerson !== 'all' ? parseInt(this.selectedPerson) : 0,
+          projectStatus: this.queryForm.projectStatus,
         }
 
-        // 根据项目状态类型设置对应的状态列表
-        if (this.queryForm.projectStatusType === 'delivery') {
-          params.projectStatusList = [10, 20, 30, 40]
-        } else if (this.queryForm.projectStatusType === 'operation') {
-          params.projectStatusList = [50]
+        // 根据页签类型设置对应的人员ID过滤
+        if (this.selectedPerson !== 'all') {
+          if (this.personTab === 'delivery') {
+            params.deliveryUserId = parseInt(this.selectedPerson)
+          } else if (this.personTab === 'operation') {
+            params.opsManagerUserId = parseInt(this.selectedPerson)
+          }
         }
 
         if (this.queryForm.planDeliveryTimeRange && this.queryForm.planDeliveryTimeRange.length === 2) {
@@ -609,6 +625,34 @@
           text-align: center;
         }
 
+        .person-tabs {
+          display: flex;
+          gap: 8px;
+          margin-bottom: 8px;
+
+          .person-tab {
+            flex: 1;
+            text-align: center;
+            padding: 8px 12px;
+            border-radius: 4px;
+            background: #f5f7fa;
+            cursor: pointer;
+            font-size: 14px;
+            color: #606266;
+            transition: all 0.2s ease;
+
+            &:hover {
+              background: #ecf5ff;
+              color: #409eff;
+            }
+
+            &.active {
+              background: #409eff;
+              color: #fff;
+            }
+          }
+        }
+
         .person-search {
           margin-bottom: 8px;
         }

+ 5 - 1
src/views/devops/software/dashboard.vue

@@ -221,11 +221,15 @@
       },
       flatTaskList() {
         if (!this.dashboardData || !this.dashboardData.days) return []
+        const seen = new Set()
         const allItems = []
         this.dashboardData.days.forEach((day) => {
           if (day.tasks) {
             day.tasks.forEach((task) => {
-              allItems.push({ ...task })
+              if (!seen.has(task.id)) {
+                seen.add(task.id)
+                allItems.push({ ...task })
+              }
             })
           }
         })

+ 84 - 22
src/views/devops/software/index.vue

@@ -73,6 +73,16 @@
 
       <div v-show="showAdvanced" class="query-form-advanced">
         <el-form :inline="true" :model="queryForm" size="small">
+          <el-form-item label="计划开始">
+            <el-date-picker
+              v-model="queryForm.planStartDateRange"
+              class="query-input query-input--date-range"
+              end-placeholder="结束"
+              range-separator="至"
+              start-placeholder="开始"
+              type="daterange"
+              value-format="yyyy-MM-dd" />
+          </el-form-item>
           <el-form-item label="创建日期">
             <el-date-picker
               v-model="queryForm.createdTimeRange"
@@ -595,6 +605,7 @@
           taskType: [],
           taskStatus: [],
           opsUserName: [],
+          planStartDateRange: this.getDefaultPlanStartDateRange(),
           planEndDateRange: [],
           createdTimeRange: [],
           completeTimeRange: [],
@@ -742,6 +753,15 @@
       }
     },
     methods: {
+      // 获取默认计划开始时间范围(前2周至后2周)
+      getDefaultPlanStartDateRange() {
+        const now = new Date()
+        const startDate = new Date(now)
+        startDate.setDate(now.getDate() - 14)
+        const endDate = new Date(now)
+        endDate.setDate(now.getDate() + 14)
+        return [parseTime(startDate, '{y}-{m}-{d}'), parseTime(endDate, '{y}-{m}-{d}')]
+      },
       getOptions() {
         dictApi
           .getDictDataByTypes([
@@ -918,6 +938,9 @@
             taskType: this.queryForm.taskType,
             taskStatus: this.queryForm.taskStatus,
             opsUserName: this.queryForm.opsUserName,
+            // 计划开始日期范围
+            planStartDateStart: this.queryForm.planStartDateRange?.[0] || '',
+            planStartDateEnd: this.queryForm.planStartDateRange?.[1] || '',
             // 计划结束日期范围
             planEndDateStart: this.queryForm.planEndDateRange?.[0] || '',
             planEndDateEnd: this.queryForm.planEndDateRange?.[1] || '',
@@ -931,9 +954,9 @@
             // 排序字段
             sortFields: this.sortFields,
           }
-          // 如果选了产品线,收集项目 ID
-          if (this.queryForm.productLine) {
-            params.projectIds = this.projects.filter((p) => p.id !== '').map((p) => parseInt(p.id))
+          // 产品线传参:仅 projectId = 0(全部)时发送,后端关联项目表过滤
+          if (this.queryForm.productLine && !this.selectedProject) {
+            params.productLine = this.queryForm.productLine
           }
           const res = await opsEventTaskApi.getList(params)
           this.tableData = res.data?.list || []
@@ -943,6 +966,9 @@
           console.error(error)
         } finally {
           this.loading = false
+          this.$nextTick(() => {
+            this.$refs.taskTable && this.$refs.taskTable.doLayout()
+          })
         }
       },
       // 远程搜索负责人
@@ -969,13 +995,15 @@
           this.remoteFetchOpsUsers('')
         }
       },
-      // 产品线变更
+      // 产品线变更:刷新左侧项目列表,同时将选中置为"全部"并重新查询任务
       handleProductLineChange() {
         this.selectedProject = ''
         this.projectPageNum = 1
         this.projects = [{ id: '', name: '全部' }]
         this.projectHasMore = true
+        this.queryForm.pageNum = 1
         this.fetchProjects()
+        this.fetchData()
       },
       // 查询
       handleSearch() {
@@ -992,6 +1020,7 @@
           taskType: [],
           taskStatus: [],
           opsUserName: [],
+          planStartDateRange: this.getDefaultPlanStartDateRange(),
           planEndDateRange: [],
           createdTimeRange: [],
           completeTimeRange: [],
@@ -1227,24 +1256,56 @@
           this.failTaskRow = null
         }
       },
-      // 点击行显示详情(支持编辑)
-      handleRowClick(row) {
-        this.detailData = this.normalizeTaskDetail(row)
-        this.detailDialogMode = ['30', '90'].includes(String(row.taskStatus)) ? 'view' : 'edit'
-        this.detailDialogVisible = true
-      },
-      // 查看发布版本关联任务详情
-      handleViewReleaseDetail(row) {
-        this.detailData = this.normalizeTaskDetail(row)
-        this.detailDialogMode = 'view'
-        this.detailDialogVisible = true
-        // 触发详情弹窗中的查看关联任务方法
-        this.$nextTick(() => {
-          const detailDialog = this.$refs.taskDetailDialog
-          if (detailDialog) {
-            detailDialog.handleViewReleaseDetail()
+      // 点击行显示详情(支持编辑)- 调用后台API获取最新数据
+      async handleRowClick(row) {
+        this.loading = true
+        try {
+          const res = await opsEventTaskApi.getById(row.id)
+          if (res.code === 200 && res.data) {
+            // 优先使用 res.data.data(某些接口包装在data中),否则直接使用res.data
+            const taskData = res.data.data || res.data
+            this.detailData = this.normalizeTaskDetail(taskData)
+            this.detailDialogMode = ['30', '90'].includes(String(taskData.taskStatus || taskData.task_status))
+              ? 'view'
+              : 'edit'
+            this.detailDialogVisible = true
+          } else {
+            this.$message.error('获取任务详情失败')
           }
-        })
+        } catch (error) {
+          this.$message.error('获取任务详情失败')
+          console.error(error)
+        } finally {
+          this.loading = false
+        }
+      },
+      // 查看发布版本关联任务详情 - 调用后台API获取最新数据
+      async handleViewReleaseDetail(row) {
+        this.loading = true
+        try {
+          const res = await opsEventTaskApi.getById(row.id)
+          if (res.code === 200 && res.data) {
+            // 优先使用 res.data.data(某些接口包装在data中),否则直接使用res.data
+            const taskData = res.data.data || res.data
+            this.detailData = this.normalizeTaskDetail(taskData)
+            this.detailDialogMode = 'view'
+            this.detailDialogVisible = true
+            // 触发详情弹窗中的查看关联任务方法
+            this.$nextTick(() => {
+              const detailDialog = this.$refs.taskDetailDialog
+              if (detailDialog) {
+                detailDialog.handleViewReleaseDetail()
+              }
+            })
+          } else {
+            this.$message.error('获取任务详情失败')
+          }
+        } catch (error) {
+          this.$message.error('获取任务详情失败')
+          console.error(error)
+        } finally {
+          this.loading = false
+        }
       },
       // 操作
       handleAdd() {
@@ -2107,7 +2168,8 @@
     flex: 1;
     min-width: 0;
     min-height: 0;
-    overflow: hidden;
+    overflow-x: auto;
+    overflow-y: hidden;
   }
 
   .task-table {