Browse Source

评估-三级校审问题功能提交

徐滕 1 week atrás
parent
commit
04aeadd8f9

+ 1 - 0
public/version.json

@@ -0,0 +1 @@
+{"version":"1793264570498"}

+ 66 - 0
src/api/program/CommonIssueService.js

@@ -0,0 +1,66 @@
+import request from '@/utils/httpRequest'
+import { ASSESS_PATH as prefix } from "../AppPath";
+
+export default class CommonIssueService {
+  list (params) {
+    return request({
+      url: prefix + '/program/commonIssue/list',
+      method: 'get',
+      params: params
+    })
+  }
+
+  queryById (id) {
+    return request({
+      url: prefix + '/program/commonIssue/queryById',
+      method: 'get',
+      params: { id: id }
+    })
+  }
+
+  save (inputForm) {
+    return request({
+      url: prefix + '/program/commonIssue/save',
+      method: 'post',
+      data: inputForm
+    })
+  }
+
+  delete (ids) {
+    return request({
+      url: prefix + '/program/commonIssue/delete',
+      method: 'delete',
+      params: { ids: ids }
+    })
+  }
+
+  getEnabledList (auditLevel) {
+    return request({
+      url: prefix + '/program/commonIssue/getEnabledList',
+      method: 'get',
+      params: { auditLevel: auditLevel }
+    })
+  }
+
+  getTreeData () {
+    return request({
+      url: prefix + '/program/commonIssue/getTreeData',
+      method: 'get'
+    })
+  }
+
+  getCategoryList () {
+    return request({
+      url: prefix + '/program/commonIssue/getCategoryList',
+      method: 'get'
+    })
+  }
+
+  updateStatus (ids, status) {
+    return request({
+      url: prefix + '/program/commonIssue/updateStatus',
+      method: 'post',
+      data: { ids: ids, status: status }
+    })
+  }
+}

+ 36 - 0
src/api/program/IssueRecordService.js

@@ -0,0 +1,36 @@
+import request from '@/utils/httpRequest'
+import { ASSESS_PATH as prefix } from "../AppPath";
+
+export default class IssueRecordService {
+  saveRecords (data) {
+    return request({
+      url: prefix + '/program/issueRecord/saveRecords',
+      method: 'post',
+      data: data
+    })
+  }
+
+  delete (ids) {
+    return request({
+      url: prefix + '/program/issueRecord/delete',
+      method: 'delete',
+      params: { ids: ids }
+    })
+  }
+
+  getReminders (targetUserId, auditLevel) {
+    return request({
+      url: prefix + '/program/issueRecord/getReminders',
+      method: 'get',
+      params: { targetUserId: targetUserId, auditLevel: auditLevel }
+    })
+  }
+
+  getRecordsByProjectId (projectId, auditLevel) {
+    return request({
+      url: prefix + '/program/issueRecord/getRecordsByProjectId',
+      method: 'get',
+      params: { projectId: projectId, auditLevel: auditLevel }
+    })
+  }
+}

+ 49 - 0
src/api/program/IssueStatisticsService.js

@@ -0,0 +1,49 @@
+import request from '@/utils/httpRequest'
+import { ASSESS_PATH as prefix } from "../AppPath";
+
+export default class IssueStatisticsService {
+  /**
+   * 汇总统计(按问题分组)
+   */
+  summary (params) {
+    return request({
+      url: prefix + '/program/issueStatistics/summary',
+      method: 'get',
+      params: params
+    })
+  }
+
+  /**
+   * 明细统计(展开行)
+   */
+  detail (params) {
+    return request({
+      url: prefix + '/program/issueStatistics/detail',
+      method: 'get',
+      params: params
+    })
+  }
+
+  /**
+   * 导出Excel
+   */
+  exportFile (params) {
+    return request({
+      url: prefix + '/program/issueStatistics/export',
+      method: 'get',
+      params: params,
+      responseType: 'blob'
+    })
+  }
+
+  /**
+   * 按校审级别分组统计(用于左侧树计数)
+   */
+  levelCounts (params) {
+    return request({
+      url: prefix + '/program/issueStatistics/levelCounts',
+      method: 'get',
+      params: params
+    })
+  }
+}

+ 150 - 0
src/views/cw/reimbursementApproval/info/ImportDialog.vue

@@ -0,0 +1,150 @@
+<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
+	<div>
+		<el-dialog :title="title" :close-on-click-modal="false" draggable width="1000px" height="500px" @close="close"
+			append-to-body v-model="visible">
+			<el-form :model="inputForm" ref="inputForm" label-width="135px" @submit.native.prevent>
+				<el-row :gutter="0">
+				</el-row>
+				<el-row :gutter="0">
+					<el-col :span="24">
+						<el-form-item label="附件上传:">
+							<el-upload ref="upload" :on-remove="handleRemove" action="" :limit="1" :auto-upload="false"
+								:on-change="beforeUploadDetail" :show-file-list="true" accept=".xlsx,.xls">
+								<el-button type="primary">点击上传Excel文件</el-button>
+							</el-upload>
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button type="warning" @click="downloadTpl()" plain>下载模板</el-button>
+					<el-button @click="close()" icon="el-icon-circle-close">关闭</el-button>
+					<el-button type="primary" icon="el-icon-circle-check" @click="doSubmit()">确定</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script>
+import ReimbursementApprovalService from '@/api/cw/reimbursementApproval/ReimbursementApprovalService'
+
+export default {
+	name: 'ImportDialog',
+	emits: ['refreshDataList', 'import-error'],
+	data() {
+		return {
+			visible: false,
+			title: '',
+			inputForm: {
+				file: []
+			},
+			reimbursementApprovalService: null,
+		}
+	},
+	created() {
+		this.reimbursementApprovalService = new ReimbursementApprovalService()
+	},
+	components: {},
+	methods: {
+		init() {
+			this.visible = true
+			this.title = '报销数据导入'
+			this.inputForm = {
+				file: []
+			}
+			this.$forceUpdate()
+		},
+		handleRemove() {
+			this.inputForm.file.pop()
+		},
+		beforeUploadDetail(file) {
+			this.inputForm.file.push(file)
+		},
+		// 下载模板
+		downloadTpl() {
+			const loading = this.$loading({
+				lock: true,
+				text: '模板下载中,请稍后',
+				spinner: 'el-icon-loading',
+				background: 'rgba(255, 255, 255, 0.3)'
+			});
+			this.reimbursementApprovalService.importTemplate().then((res) => {
+				this.$utils.downloadExcel(res, '报销导入模板.xlsx')
+				loading.close()
+			}).catch(function (err) {
+				loading.close()
+				if (err.response) {
+					console.log(err.response)
+				}
+			})
+		},
+		// 确定导入
+		doSubmit() {
+			if (this.inputForm.file.length == 0) {
+				this.$message.warning('未上传文件,无法提交导入')
+				return
+			}
+			const loading = this.$loading({
+				lock: true,
+				text: '导入中,请稍后',
+				spinner: 'el-icon-loading',
+				background: 'rgba(255, 255, 255, 0.3)'
+			});
+			const formBody = new FormData()
+			formBody.append('file', this.inputForm.file[0].raw)
+
+			this.reimbursementApprovalService.importFile(formBody).then((data) => {
+				loading.close()
+				console.log("导入返回数据:", data)
+
+				// 有错误文件 → 通知父页面处理
+				if (data && data.errorFilePath) {
+					this.$emit('import-error', data)
+					if (data.failCount > 0 && data.successCount > 0) {
+						this.$message.warning(`部分导入成功:成功 ${data.successCount} 条,失败 ${data.failCount} 条,请下载查看错误详情`)
+						this.$emit('refreshDataList')
+					} else if (data.failCount > 0) {
+						this.$message.warning(`导入全部失败,共 ${data.failCount} 条,请下载查看错误详情`)
+					}
+					this.close()
+					return
+				}
+
+				this.$message.success(`导入成功!共创建 ${data.successCount} 条报销单(${data.successDetailCount} 条明细)`)
+				this.close()
+				this.$emit('refreshDataList')
+			}).catch(err => {
+				loading.close()
+				// 优先取服务器返回的错误信息
+				let errMsg = (err.response && err.response.data) ? err.response.data : (err.message || '服务异常')
+				if (typeof errMsg === 'string' && errMsg.startsWith('错误: ')) {
+					errMsg = errMsg.substring(4)
+				}
+				this.$message.error('导入失败:' + errMsg)
+			})
+		},
+		close() {
+			this.visible = false
+			this.$refs.inputForm.resetFields()
+			this.$refs.upload.clearFiles()
+			this.inputForm = {
+				file: []
+			}
+		}
+	}
+}
+</script>
+
+<style scoped>
+/deep/ .el-input-number .el-input__inner {
+	text-align: left;
+}
+
+.el-row {
+	display: flex;
+	align-items: center;
+}
+</style>

+ 1 - 1
src/views/flowable/task/TaskForm.vue

@@ -1,4 +1,4 @@
-<template>
+<template>
 	<div class="jp-center">
 		<h2 class="title">{{ title }}</h2>
 

+ 161 - 0
src/views/program/commonIssue/CommonIssueForm.vue

@@ -0,0 +1,161 @@
+<template>
+  <div>
+    <el-dialog :title="title" :close-on-click-modal="false" draggable
+               width="600px" @close="close" @keyup.enter.native="doSubmit"
+               v-model="visible">
+      <el-form size="large" :model="inputForm" ref="inputForm" v-loading="loading"
+               :class="method==='view'?'readonly':''" :disabled="method==='view'"
+               label-width="110px" @submit.native.prevent>
+        <el-row :gutter="15">
+          <el-col :span="24">
+            <el-form-item label="漏洞名称" prop="issueName"
+                          :rules="[{required: true, message:'漏洞名称不能为空', trigger:'blur'}]">
+              <el-input v-model="inputForm.issueName" placeholder="请输入漏洞名称" clearable></el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="分类" prop="category">
+              <el-input v-model="inputForm.category" placeholder="请输入分类(如:报告质量类、技术方法类)" clearable></el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="漏洞描述" prop="issueDesc">
+              <el-input type="textarea" :rows="3" v-model="inputForm.issueDesc" placeholder="请输入漏洞描述" maxlength="1000" show-word-limit></el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="整改建议" prop="suggestion">
+              <el-input type="textarea" :rows="3" v-model="inputForm.suggestion" placeholder="请输入整改建议(选填,为空则提醒时只展示名称和次数)" maxlength="1000" show-word-limit></el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-if="!isFromAudit">
+            <el-form-item label="校审级别" prop="auditLevel"
+                          :rules="[{required: true, message:'请选择校审级别', trigger:'change'}]">
+              <el-select v-model="inputForm.auditLevel" placeholder="请选择校审级别" style="width: 100%;">
+                <el-option label="一级校审" value="1"></el-option>
+                <el-option label="二级校审" value="2"></el-option>
+                <el-option label="三级校审" value="3"></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-else>
+            <el-form-item label="校审级别">
+              <el-input :value="auditLevelText" disabled></el-input>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="备注" prop="remarks">
+              <el-input type="textarea" :rows="2" v-model="inputForm.remarks" placeholder="备注" maxlength="1000"></el-input>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="close()" icon="el-icon-circle-close">关闭</el-button>
+          <el-button type="primary" v-if="method != 'view'" @click="doSubmit()"
+                     icon="el-icon-circle-check" v-noMoreClick>确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import CommonIssueService from '@/api/program/CommonIssueService'
+
+export default {
+  data () {
+    return {
+      title: '',
+      method: '',
+      visible: false,
+      loading: false,
+      isFromAudit: false,  // 是否从审核页面调用
+      inputForm: {
+        id: '',
+        issueName: '',
+        issueDesc: '',
+        suggestion: '',
+        category: '',
+        auditLevel: '',
+        threshold: 3,
+        resetMonths: null,
+        sortOrder: 0,
+        status: '1',
+        remarks: ''
+      }
+    }
+  },
+  commonIssueService: null,
+  created () { this.commonIssueService = new CommonIssueService() },
+  computed: {
+    auditLevelText() {
+      const levelMap = { '1': '一级校审', '2': '二级校审', '3': '三级校审' }
+      return levelMap[this.inputForm.auditLevel] || ''
+    }
+  },
+  methods: {
+    init (method, id, defaultAuditLevel) {
+      this.method = method
+      this.isFromAudit = !!defaultAuditLevel  // 如果有默认校审级别,说明是从审核页面调用
+      this.inputForm = {
+        id: '',
+        issueName: '',
+        issueDesc: '',
+        suggestion: '',
+        category: '',
+        auditLevel: defaultAuditLevel || '',
+        threshold: 3,
+        resetMonths: null,
+        sortOrder: 0,
+        status: '1',
+        remarks: ''
+      }
+      if (method === 'add') { this.title = '新增常见漏洞' }
+      else if (method === 'edit') { this.inputForm.id = id; this.title = '编辑常见漏洞' }
+      else if (method === 'view') { this.inputForm.id = id; this.title = '查看常见漏洞' }
+      this.visible = true
+      this.loading = false
+      this.$nextTick(() => {
+        this.$refs.inputForm.resetFields()
+        if (method === 'edit' || method === 'view') {
+          this.loading = true
+          this.commonIssueService.queryById(this.inputForm.id).then((data) => {
+            this.inputForm = this.recover(this.inputForm, data)
+            this.inputForm = JSON.parse(JSON.stringify(this.inputForm))
+            this.loading = false
+          })
+        }
+      })
+    },
+    doSubmit () {
+      this.$refs['inputForm'].validate((valid) => {
+        if (valid) {
+          this.loading = true
+          this.commonIssueService.save(this.inputForm).then((data) => {
+            this.close()
+            this.$message.success(data)
+            this.$emit('refreshDataList')
+            this.loading = false
+          }).catch(() => { this.loading = false })
+        }
+      })
+    },
+    close () {
+      this.$refs.inputForm.resetFields()
+      this.visible = false
+    }
+  }
+}
+</script>
+<style scoped>
+/* 确保 el-input-number 在表单中正确显示 */
+:deep(.el-form-item .el-input-number) {
+  width: 100% !important;
+}
+:deep(.el-form-item .el-input-number .el-input__inner) {
+  text-align: left;
+  width: 100%;
+}
+</style>

+ 244 - 0
src/views/program/commonIssue/CommonIssueList.vue

@@ -0,0 +1,244 @@
+<template>
+  <div class="page" style="display: flex; height: calc(100vh - 120px);">
+    <!-- 左侧树 -->
+    <div style="width: 220px; border-right: 1px solid #e8e8e8; padding: 10px 0; overflow-y: auto; flex-shrink: 0;">
+      <el-tree
+        :data="treeData"
+        :props="treeProps"
+        node-key="id"
+        highlight-current
+        default-expand-all
+        @node-click="handleNodeClick"
+        ref="tree"
+      >
+        <template #default="{ node, data }">
+          <span style="display: flex; justify-content: space-between; width: 100%; align-items: center;">
+            <span>{{ data.label }}</span>
+            <el-tag size="small" type="info" style="margin-left: 8px;">{{ data.count }}</el-tag>
+          </span>
+        </template>
+      </el-tree>
+    </div>
+
+    <!-- 右侧内容 -->
+    <div style="flex: 1; padding: 10px; overflow: hidden; display: flex; flex-direction: column;">
+      <!-- 搜索 -->
+      <el-form :inline="true" class="query-form" ref="searchForm" :model="searchForm"
+               @keyup.enter.native="refreshList()" @submit.native.prevent>
+        <el-form-item label="漏洞名称" prop="issueName">
+          <el-input v-model="searchForm.issueName" placeholder="请输入漏洞名称" clearable></el-input>
+        </el-form-item>
+        <el-form-item label="分类" prop="category">
+          <el-select v-model="searchForm.category" placeholder="请选择分类" clearable style="width:100%;">
+            <el-option v-for="cat in categoryList" :key="cat" :label="cat" :value="cat"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="searchForm.status" placeholder="请选择" clearable style="width:100%;">
+            <el-option label="启用" value="1"></el-option>
+            <el-option label="停用" value="0"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="refreshList()" icon="el-icon-search">查询</el-button>
+          <el-button @click="resetSearch()" icon="el-icon-refresh-right">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 表格 -->
+      <div class="jp-table" style="flex: 1; overflow: hidden;">
+        <vxe-toolbar :refresh="{query: refreshList}" custom>
+          <template #buttons>
+            <el-button v-if="hasPermission('program:commonIssue:add')"
+                       type="primary" icon="el-icon-plus" @click="add()">新增</el-button>
+            <el-button v-if="hasPermission('program:commonIssue:del')"
+                       type="danger" icon="el-icon-delete" @click="del()"
+                       :disabled="selectedRows.length === 0" plain>删除</el-button>
+            <el-button v-if="hasPermission('program:commonIssue:edit')"
+                       type="success" icon="el-icon-check" @click="batchStatus('1')"
+                       :disabled="selectedRows.length === 0" plain>启用</el-button>
+            <el-button v-if="hasPermission('program:commonIssue:edit')"
+                       type="warning" icon="el-icon-close" @click="batchStatus('0')"
+                       :disabled="selectedRows.length === 0" plain>停用</el-button>
+          </template>
+        </vxe-toolbar>
+        <div class="jp-table-body">
+          <vxe-table border="inner" auto-resize resizable height="auto"
+                     :loading="loading" size="small" ref="table"
+                     show-header-overflow show-overflow highlight-hover-row
+                     :data="dataList"
+                     :checkbox-config="{checkMethod: checkboxMethod}"
+                     @checkbox-change="onCheckboxChange"
+                     @checkbox-all="onCheckboxChange">
+            <vxe-column type="seq" width="60" title="序号"></vxe-column>
+            <vxe-column type="checkbox" width="50"></vxe-column>
+            <vxe-column field="issueName" title="漏洞名称" min-width="180"></vxe-column>
+            <vxe-column field="category" title="分类" width="120"></vxe-column>
+            <vxe-column field="auditLevel" title="校审级别" width="100">
+              <template #default="scope">
+                <span v-if="scope.row.auditLevel === '1'">一级校审</span>
+                <span v-else-if="scope.row.auditLevel === '2'">二级校审</span>
+                <span v-else-if="scope.row.auditLevel === '3'">三级校审</span>
+              </template>
+            </vxe-column>
+            <vxe-column field="threshold" title="提醒阈值" width="100"></vxe-column>
+            <vxe-column field="resetMonths" title="重置周期(月)" width="120">
+              <template #default="scope">
+                <span v-if="scope.row.resetMonths">{{ scope.row.resetMonths }}个月</span>
+                <span v-else>永不重置</span>
+              </template>
+            </vxe-column>
+            <vxe-column field="status" title="状态" width="100">
+              <template #default="scope">
+                <el-switch v-if="hasPermission('program:commonIssue:edit')"
+                           v-model="scope.row.status" active-value="1" inactive-value="0"
+                           @change="toggleStatus(scope.row)" />
+                <el-tag v-else :type="scope.row.status === '1' ? 'success' : 'danger'" size="small">
+                  {{ scope.row.status === '1' ? '启用' : '停用' }}
+                </el-tag>
+              </template>
+            </vxe-column>
+            <vxe-column field="sortOrder" title="排序" width="80"></vxe-column>
+            <vxe-column title="操作" width="200" fixed="right" align="center">
+              <template #default="scope">
+                <el-button v-if="hasPermission('program:commonIssue:edit')" text type="primary"
+                           @click="edit(scope.row.id)">编辑</el-button>
+                <el-button v-if="hasPermission('program:commonIssue:del')" text type="danger"
+                           @click="del(scope.row.id)">删除</el-button>
+              </template>
+            </vxe-column>
+          </vxe-table>
+        </div>
+      </div>
+    </div>
+
+    <!-- 表单弹窗 -->
+    <CommonIssueForm ref="commonIssueForm" @refreshDataList="refreshList"></CommonIssueForm>
+  </div>
+</template>
+
+<script>
+import CommonIssueService from '@/api/program/CommonIssueService'
+import CommonIssueForm from './CommonIssueForm'
+
+export default {
+  data () {
+    return {
+      searchForm: {
+        issueName: '',
+        category: '',
+        auditLevel: '',
+        status: ''
+      },
+      dataList: [],
+      selectedRows: [],
+      tablePage: { total: 0, currentPage: 1, pageSize: 10, orders: [] },
+      loading: false,
+      treeData: [],
+      treeProps: { children: 'children', label: 'label' },
+      currentLevel: '',
+      categoryList: []
+    }
+  },
+  commonIssueService: null,
+  created () {
+    this.commonIssueService = new CommonIssueService()
+  },
+  components: { CommonIssueForm },
+  mounted () {
+    this.loadTreeData()
+    this.loadCategoryList()
+  },
+  methods: {
+    loadTreeData () {
+      this.commonIssueService.getTreeData().then((data) => {
+        this.treeData = data || []
+        // 默认选中"全部"
+        this.$nextTick(() => {
+          this.$refs.tree.setCurrentKey('all')
+          this.currentLevel = ''
+          this.refreshList()
+        })
+      })
+    },
+    loadCategoryList () {
+      this.commonIssueService.getCategoryList().then((data) => {
+        this.categoryList = data || []
+      })
+    },
+    handleNodeClick (data) {
+      this.currentLevel = data.id === 'all' ? '' : data.id
+      this.searchForm.auditLevel = this.currentLevel
+      this.refreshList()
+    },
+    add () {
+      this.$refs.commonIssueForm.init('add', '', this.currentLevel)
+    },
+    edit (id) { this.$refs.commonIssueForm.init('edit', id) },
+    refreshList () {
+      this.loading = true
+      this.commonIssueService.list({
+        'current': this.tablePage.currentPage,
+        'size': this.tablePage.pageSize,
+        'orders': this.tablePage.orders,
+        ...this.searchForm,
+        auditLevel: this.currentLevel || this.searchForm.auditLevel
+      }).then((data) => {
+        this.dataList = data.records || []
+        this.tablePage.total = data.total || 0
+        this.loading = false
+        this.selectedRows = []
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    del (id) {
+      let ids = id || this.selectedRows.map(item => item.id).join(',')
+      this.$confirm(`确认删除选中的常见漏洞?`, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.commonIssueService.delete(ids).then((data) => {
+          this.$message.success(data)
+          this.refreshList()
+          this.loadTreeData()
+        })
+      })
+    },
+    batchStatus (status) {
+      let ids = this.selectedRows.map(item => item.id).join(',')
+      let msg = status === '1' ? '确认启用选中的漏洞?' : '确认停用选中的漏洞?'
+      this.$confirm(msg, '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.commonIssueService.updateStatus(ids, status).then((data) => {
+          this.$message.success(data)
+          this.refreshList()
+        })
+      })
+    },
+    toggleStatus (row) {
+      this.commonIssueService.updateStatus(row.id, row.status).then((data) => {
+        this.$message.success(data)
+        this.loadTreeData()
+      }).catch(() => {
+        row.status = row.status === '1' ? '0' : '1'
+      })
+    },
+    resetSearch () {
+      this.$refs.searchForm.resetFields()
+      this.searchForm.auditLevel = this.currentLevel
+      this.refreshList()
+    },
+    checkboxMethod ({ row }) {
+      return true
+    },
+    onCheckboxChange () {
+      this.selectedRows = this.$refs.table ? this.$refs.table.getCheckboxRecords() : []
+    }
+  }
+}
+</script>

+ 432 - 0
src/views/program/issueStatistics/IssueStatisticsList.vue

@@ -0,0 +1,432 @@
+<template>
+	<div class="page" style="display: flex; height: calc(100vh - 120px);">
+		<!-- 左侧校审级别树 -->
+		<div style="width: 220px; border-right: 1px solid #e8e8e8; padding: 10px 0; overflow-y: auto; flex-shrink: 0;">
+			<el-tree
+				:data="treeData"
+				:props="treeProps"
+				node-key="id"
+				highlight-current
+				default-expand-all
+				@node-click="handleNodeClick"
+				ref="tree">
+				<template #default="{ node, data }">
+					<span style="display: flex; justify-content: space-between; width: 100%; align-items: center;">
+						<span>{{ data.label }}</span>
+						<el-tag size="small" type="info" style="margin-left: 8px;">{{ data.count }}</el-tag>
+					</span>
+				</template>
+			</el-tree>
+		</div>
+
+		<!-- 右侧内容 -->
+		<div style="flex: 1; padding: 10px; overflow: hidden; display: flex; flex-direction: column;">
+			<!-- 筛选条件 -->
+			<el-form :inline="true" class="query-form" ref="searchForm" :model="searchForm"
+			         @keyup.enter.native="refreshList" @submit.native.prevent>
+				<el-form-item label="年度" prop="year">
+					<el-select v-model="searchForm.year" placeholder="请选择年度" clearable style="width: 120px" @change="onYearChange">
+						<el-option v-for="y in yearOptions" :key="y" :label="y + '年'" :value="y"></el-option>
+					</el-select>
+				</el-form-item>
+				<el-form-item label="时间范围" prop="dateRange">
+					<el-date-picker
+						v-model="searchForm.dateRange"
+						type="daterange"
+						range-separator="至"
+						start-placeholder="开始日期"
+						end-placeholder="结束日期"
+						value-format="YYYY-MM-DD"
+						style="width: 260px"
+						@change="onDateRangeChange">
+					</el-date-picker>
+				</el-form-item>
+				<el-form-item label="问题" prop="issueId">
+					<el-select v-model="searchForm.issueId" placeholder="全部问题" clearable filterable style="width: 200px">
+						<el-option v-for="item in issueOptions" :key="item.id" :label="item.issueName" :value="item.id"></el-option>
+					</el-select>
+				</el-form-item>
+				<el-form-item>
+					<el-button type="primary" @click="refreshList" icon="el-icon-search">查询</el-button>
+					<el-button @click="resetSearch" icon="el-icon-refresh-right">重置</el-button>
+				</el-form-item>
+			</el-form>
+
+			<!-- 汇总表格 -->
+			<div class="jp-table" style="flex: 1; overflow: hidden;">
+				<vxe-toolbar ref="toolbarRef" :refresh="{ query: refreshList }" export>
+				</vxe-toolbar>
+				<div class="jp-table-body">
+					<vxe-table
+						border="inner"
+						auto-resize
+						resizable
+						height="auto"
+						:loading="loading"
+						ref="summaryTable"
+						show-header-overflow
+						show-overflow
+						highlight-hover-row
+						:data="summaryList"
+						:expand-config="{ trigger: 'row', lazy: true, loadMethod: loadDetail }"
+						:export-config="{
+							remote: true,
+							filename: exportFilename,
+							sheetName: '常见问题统计',
+							exportMethod: exportMethod,
+							types: ['xlsx'],
+							modes: ['all']
+						}">
+						<vxe-column type="expand" width="60">
+							<template #content="{ row }">
+								<div style="padding: 15px 20px;">
+									<template v-if="personnelMap[row.issueId] && personnelMap[row.issueId].length > 0">
+										<!-- 人员汇总表 -->
+										<vxe-table
+											border="inner"
+											size="small"
+											:data="personnelMap[row.issueId]"
+											show-overflow
+											:row-config="{ isHover: true }">
+											<vxe-column type="seq" width="50" title="序号"></vxe-column>
+											<vxe-column min-width="100" title="涉及人员" field="targetUserName">
+												<template #default="{ row: person }">
+													<el-button text type="primary" size="small" @click="togglePersonProjects(row.issueId, person.targetUserId)">
+														<span :style="{ display: 'inline-block', transform: expandedPersons[row.issueId + '_' + person.targetUserId] ? 'rotate(90deg)' : '', transition: 'transform 0.2s', marginRight: '4px', fontSize: '12px' }">▶</span>
+														{{ person.targetUserName }}
+													</el-button>
+												</template>
+											</vxe-column>
+											<vxe-column width="100" title="涉及项目数" field="projectCount" align="center">
+												<template #default="{ row: person }">
+													<el-tag size="small" type="warning" effect="plain">{{ person.projectCount }}</el-tag>
+												</template>
+											</vxe-column>
+											<vxe-column width="100" title="标记次数" field="markCount" align="center">
+												<template #default="{ row: person }">
+													<el-tag size="small" type="danger" effect="plain">{{ person.markCount }}</el-tag>
+												</template>
+											</vxe-column>
+										</vxe-table>
+										<!-- 点击人员后显示的项目明细表 -->
+										<div v-for="person in personnelMap[row.issueId]" :key="person.targetUserId">
+											<div v-if="expandedPersons[row.issueId + '_' + person.targetUserId]" style="margin-top: 10px; margin-bottom: 15px;">
+												<div style="font-size: 13px; color: #606266; margin-bottom: 6px; padding-left: 10px; border-left: 3px solid #409eff;">
+													{{ person.targetUserName }} 涉及的项目({{ person.projects.length }} 条记录)
+												</div>
+												<vxe-table
+													border="inner"
+													size="mini"
+													:data="person.projects"
+													show-overflow
+													style="margin-left: 10px;">
+													<vxe-column type="seq" width="50" title="序号"></vxe-column>
+													<vxe-column min-width="180" title="项目名称" field="projectName" align="center">
+														<template #default="{ row: proj }">
+															<el-link type="primary" :underline="false" @click="viewProject(proj)">{{ proj.projectName }}</el-link>
+														</template>
+													</vxe-column>
+													<vxe-column min-width="120" title="报告号" field="reportNo" align="center"></vxe-column>
+													<vxe-column min-width="150" title="委托方" field="clientName" align="center"></vxe-column>
+													<vxe-column width="80" title="校审级别" field="auditLevel" align="center">
+														<template #default="{ row: proj }">
+															{{ auditLevelMap[proj.auditLevel] || proj.auditLevel }}
+														</template>
+													</vxe-column>
+													<vxe-column width="80" title="审核人" field="markUserName" align="center"></vxe-column>
+													<vxe-column width="130" title="标记时间" field="markTime" align="center">
+														<template #default="{ row: proj }">
+															{{ proj.markTime ? moment(proj.markTime).format('YYYY-MM-DD HH:mm') : '' }}
+														</template>
+													</vxe-column>
+												</vxe-table>
+											</div>
+										</div>
+									</template>
+									<div v-else-if="detailLoadingMap[row.issueId]" style="text-align: center; padding: 20px; color: #909399;">加载中...</div>
+									<div v-else style="text-align: center; padding: 20px; color: #909399;">暂无数据</div>
+								</div>
+							</template>
+						</vxe-column>
+						<vxe-column type="seq" width="60" title="序号"></vxe-column>
+						<vxe-column min-width="200" title="问题名称" field="issueName" align="center"></vxe-column>
+						<vxe-column min-width="120" title="分类" field="category" align="center"></vxe-column>
+						<vxe-column width="100" title="标记次数" field="markCount" align="center" sortable>
+							<template #default="{ row }">
+								<el-tag type="danger" effect="plain">{{ row.markCount }}</el-tag>
+							</template>
+						</vxe-column>
+						<vxe-column width="100" title="涉及项目" field="projectCount" align="center" sortable></vxe-column>
+						<vxe-column width="100" title="涉及人数" field="targetUserCount" align="center" sortable></vxe-column>
+					</vxe-table>
+				</div>
+			</div>
+		</div>
+
+		<!-- 项目详情弹窗 -->
+		<ProjectForm ref="projectForm"></ProjectForm>
+	</div>
+</template>
+
+<script>
+import IssueStatisticsService from '@/api/program/IssueStatisticsService'
+import CommonIssueService from '@/api/program/CommonIssueService'
+import ProjectForm from '@/views/program/registered/ProjectForm'
+
+export default {
+	name: 'IssueStatisticsList',
+	components: { ProjectForm },
+	data () {
+		return {
+			searchForm: {
+				year: String(new Date().getFullYear()),
+				dateRange: null,
+				auditLevel: '',
+				issueId: ''
+			},
+			treeData: [],
+			treeProps: { children: 'children', label: 'label' },
+			issueOptions: [],
+			summaryList: [],
+			detailMap: {},
+			detailLoadingMap: {},
+			personnelMap: {},
+			expandedPersons: {},
+			loading: false,
+			issueStatisticsService: null,
+			commonIssueService: null
+		}
+	},
+	created () {
+		this.issueStatisticsService = new IssueStatisticsService()
+		this.commonIssueService = new CommonIssueService()
+	},
+	computed: {
+		yearOptions () {
+			const currentYear = new Date().getFullYear()
+			const years = []
+			for (let y = currentYear; y >= currentYear - 5; y--) {
+				years.push(String(y))
+			}
+			return years
+		},
+		auditLevelMap () {
+			return { '1': '一审', '2': '二审', '3': '三审' }
+		},
+		exportFilename () {
+			const parts = []
+			if (this.searchForm.year) parts.push(this.searchForm.year + '年')
+			if (this.searchForm.dateRange && this.searchForm.dateRange.length === 2) {
+				parts.push(this.searchForm.dateRange[0] + '~' + this.searchForm.dateRange[1])
+			}
+			const levelLabel = this.auditLevelMap[this.searchForm.auditLevel] || '全部'
+			return (parts.length > 0 ? parts.join('') : '全部') + levelLabel + '常见问题统计'
+		}
+	},
+	mounted () {
+		this.$nextTick(() => {
+			const $table = this.$refs.summaryTable
+			const $toolbar = this.$refs.toolbarRef
+			if ($table && $toolbar) {
+				$table.connect($toolbar)
+			}
+		})
+		this.loadIssueOptions()
+		this.refreshList()
+	},
+	activated () {
+		this.refreshList()
+	},
+	methods: {
+		/** 加载问题下拉 */
+		loadIssueOptions () {
+			this.commonIssueService.getEnabledList().then(data => {
+				this.issueOptions = data || []
+			}).catch(() => {
+				this.issueOptions = []
+			})
+		},
+		/** 年度切换时自动更新日期范围 */
+		onYearChange (year) {
+			if (year) {
+				this.searchForm.dateRange = [year + '-01-01', year + '-12-31']
+			} else {
+				this.searchForm.dateRange = null
+			}
+		},
+		/** 手动修改日期范围 */
+		onDateRangeChange () {
+			// 年度保留不动,筛选条件取交集
+		},
+		/** 构建请求参数(不含 auditLevel,用于获取全量数据构建树) */
+		buildParamsNoAudit () {
+			const params = {}
+			if (this.searchForm.dateRange && this.searchForm.dateRange.length === 2) {
+				params.startDate = this.searchForm.dateRange[0] + ' 00:00:00'
+				params.endDate = this.searchForm.dateRange[1] + ' 23:59:59'
+			}
+			if (this.searchForm.issueId) params.issueId = this.searchForm.issueId
+			return params
+		},
+		/** 构建请求参数(含 auditLevel,用于右侧表格过滤) */
+		buildParamsWithAudit () {
+			const params = this.buildParamsNoAudit()
+			if (this.searchForm.auditLevel) params.auditLevel = this.searchForm.auditLevel
+			return params
+		},
+		/** 查询汇总列表 */
+		refreshList () {
+			this.loading = true
+			this.detailMap = {}
+			this.personnelMap = {}
+			this.expandedPersons = {}
+			const params = this.buildParamsWithAudit()
+			this.issueStatisticsService.summary(params).then(data => {
+				this.summaryList = data || []
+				this.loadTreeCounts()
+				this.loading = false
+			}).catch(() => {
+				this.summaryList = []
+				this.loading = false
+			})
+		},
+		/** 加载左侧树的各级别计数 */
+		loadTreeCounts () {
+			const params = this.buildParamsNoAudit()
+			this.issueStatisticsService.levelCounts(params).then(data => {
+				const levelMap = {}
+				;(data || []).forEach(item => {
+					levelMap[item.auditLevel] = item.count
+				})
+				const totalCount = Object.values(levelMap).reduce((sum, c) => sum + c, 0)
+				this.treeData = [
+					{ id: 'all', label: '全部', count: totalCount, children: [
+						{ id: '1', label: '一级校审', count: levelMap['1'] || 0 },
+						{ id: '2', label: '二级校审', count: levelMap['2'] || 0 },
+						{ id: '3', label: '三级校审', count: levelMap['3'] || 0 }
+					]}
+				]
+				this.$nextTick(() => {
+					if (this.$refs.tree) {
+						this.$refs.tree.setCurrentKey('all')
+					}
+				})
+			}).catch(() => {
+				const totalCount = (this.summaryList || []).reduce((sum, item) => sum + (item.markCount || 0), 0)
+				this.treeData = [
+					{ id: 'all', label: '全部', count: totalCount, children: [
+						{ id: '1', label: '一级校审', count: 0 },
+						{ id: '2', label: '二级校审', count: 0 },
+						{ id: '3', label: '三级校审', count: 0 }
+					]}
+				]
+			})
+		},
+		/** 点击树节点筛选 */
+		handleNodeClick (data) {
+			if (data.id === 'all') {
+				this.searchForm.auditLevel = ''
+			} else {
+				this.searchForm.auditLevel = data.id
+			}
+			this.loading = true
+			this.detailMap = {}
+			this.personnelMap = {}
+			this.expandedPersons = {}
+			const params = this.buildParamsWithAudit()
+			this.issueStatisticsService.summary(params).then(data => {
+				this.summaryList = data || []
+				this.loading = false
+			}).catch(() => {
+				this.summaryList = []
+				this.loading = false
+			})
+		},
+		/** 展开行时懒加载明细并构建人员汇总 */
+		loadDetail ({ row }) {
+			if (this.detailMap[row.issueId]) {
+				return Promise.resolve()
+			}
+			this.detailLoadingMap[row.issueId] = true
+			const params = this.buildParamsWithAudit()
+			params.issueId = row.issueId
+			return this.issueStatisticsService.detail(params).then(data => {
+				const records = data || []
+				this.detailMap[row.issueId] = records
+				// 按人员分组
+				const personMap = {}
+				records.forEach(record => {
+					const uid = record.targetUserId || record.targetUserName
+					if (!personMap[uid]) {
+						personMap[uid] = {
+							targetUserId: record.targetUserId,
+							targetUserName: record.targetUserName,
+							markCount: 0,
+							projects: []
+						}
+					}
+					personMap[uid].markCount++
+					personMap[uid].projects.push({
+						projectId: record.projectId,
+						projectName: record.projectName,
+						reportNo: record.reportNo,
+						clientName: record.clientName,
+						auditLevel: record.auditLevel,
+						markUserName: record.markUserName,
+						markTime: record.markTime,
+						auditId1: record.auditId1,
+						auditId2: record.auditId2,
+						auditId3: record.auditId3,
+						archiveId: record.archiveId
+					})
+				})
+				const personnelList = Object.values(personMap).map(p => ({
+					...p,
+					projectCount: [...new Set(p.projects.map(pr => pr.projectId))].length
+				}))
+				personnelList.sort((a, b) => b.markCount - a.markCount)
+				this.personnelMap[row.issueId] = personnelList
+				this.detailLoadingMap[row.issueId] = false
+			}).catch(() => {
+				this.detailLoadingMap[row.issueId] = false
+			})
+		},
+		/** 切换人员项目展开/收起 */
+		togglePersonProjects (issueId, targetUserId) {
+			const key = issueId + '_' + targetUserId
+			this.expandedPersons[key] = !this.expandedPersons[key]
+		},
+		/** 点击查看项目详情 */
+		viewProject (proj) {
+			this.$refs.projectForm.init('view', proj.projectId, proj.auditId1, proj.auditId2, proj.auditId3, proj.archiveId)
+		},
+		/** 导出Excel(vxe-table remote export) */
+		exportMethod ({ options }) {
+			const params = this.buildParamsWithAudit()
+			params.filename = options.filename
+			params.sheetName = options.sheetName
+			return this.issueStatisticsService.exportFile(params).then(res => {
+				this.$utils.downloadExcel(res, options.filename)
+			}).catch(err => {
+				console.error('导出失败', err)
+			})
+		},
+		/** 重置筛选 */
+		resetSearch () {
+			this.searchForm = {
+				year: String(new Date().getFullYear()),
+				dateRange: [new Date().getFullYear() + '-01-01', new Date().getFullYear() + '-12-31'],
+				auditLevel: '',
+				issueId: ''
+			}
+			this.refreshList()
+		}
+	}
+}
+</script>
+
+<style scoped>
+.jp-table {
+	margin-top: 10px;
+}
+</style>

+ 112 - 95
src/views/program/registered/ProjectList.vue

@@ -788,7 +788,8 @@ export default {
 						formTitle: processTitle,
 						businessId: 'false',
 						isShow: false,
-						routePath: '/program/registered/ProjectList'
+						routePath: '/program/registered/ProjectList',
+							skipRejectConfirm: 'true'  // 评估项目校审跳过驳回确认对话框
 					}
 				})
 			})
@@ -823,7 +824,8 @@ export default {
 						businessId: row.id,
 						isShow: 'false',
 						status: status,
-						routePath: '/program/registered/ProjectList'
+						routePath: '/program/registered/ProjectList',
+							skipRejectConfirm: 'true'  // 评估项目校审跳过驳回确认对话框
 					}
 				})
 			})
@@ -893,36 +895,40 @@ export default {
 		},
 		// 发起一级校审
 		firstAuditPush(row) {
-			// 读取流程表单
-			let title = `发起流程【评估-项目一级校审】`
-			let processTitle = `${this.userName} 在 ${this.moment(new Date()).format('YYYY-MM-DD HH:mm')} 发起了[评估-项目一级校审]`
-			let status = 'startAndHold'
-			if (row.status1 === '3') {
-				status = 'startAndClose'
-			} else if (row.status1 === '4') {
-				status = 'reapplyFlag'
-			}
-			taskService.getTaskDef({
-				procDefId: this.processDefinitionId1,
-				businessId: row.auditId1,
-				businessTable: 'program_audit'
-			}).then((data) => {
-				this.$router.push({
-					path: '/flowable/task/TaskForm',
-					query: {
-						...pick(data, 'formType', 'formUrl', 'procDefKey', 'taskDefKey', 'procInsId', 'procDefId', 'taskId', 'status', 'title'),
-						procDefId: this.processDefinitionId1,
-						procDefKey: this.procDefKey1,
-						title: title,
-						formType: data.formType,
-						formUrl: data.formUrl,
-						formTitle: processTitle,
-						businessTable: 'program_audit',
-						businessId: row.auditId1,
-						isShow: 'false',
-						status: status,
-						routePath: '/program/registered/ProjectList'
-					}
+			// 先检查漏洞提醒
+			this.checkAndShowReminders('1', () => {
+				// 读取流程表单
+				let title = `发起流程【评估-项目一级校审】`
+				let processTitle = `${this.userName} 在 ${this.moment(new Date()).format('YYYY-MM-DD HH:mm')} 发起了[评估-项目一级校审]`
+				let status = 'startAndHold'
+				if (row.status1 === '3') {
+					status = 'startAndClose'
+				} else if (row.status1 === '4') {
+					status = 'reapplyFlag'
+				}
+				taskService.getTaskDef({
+					procDefId: this.processDefinitionId1,
+					businessId: row.auditId1,
+					businessTable: 'program_audit'
+				}).then((data) => {
+					this.$router.push({
+						path: '/flowable/task/TaskForm',
+						query: {
+							...pick(data, 'formType', 'formUrl', 'procDefKey', 'taskDefKey', 'procInsId', 'procDefId', 'taskId', 'status', 'title'),
+							procDefId: this.processDefinitionId1,
+							procDefKey: this.procDefKey1,
+							title: title,
+							formType: data.formType,
+							formUrl: data.formUrl,
+							formTitle: processTitle,
+							businessTable: 'program_audit',
+							businessId: row.auditId1,
+							isShow: 'false',
+							status: status,
+							routePath: '/program/registered/ProjectList',
+							skipRejectConfirm: 'true'  // 评估项目校审跳过驳回确认对话框
+						}
+					})
 				})
 			})
 		},
@@ -988,36 +994,40 @@ export default {
 
 		// 发起二级校审
 		secondAuditPush(row) {
-			// 读取流程表单
-			let title = `发起流程【评估-项目二级校审】`
-			let processTitle = `${this.userName} 在 ${this.moment(new Date()).format('YYYY-MM-DD HH:mm')} 发起了[评估-项目二级校审]`
-			let status = 'startAndHold'
-			if (row.status2 === '3') {
-				status = 'startAndClose'
-			} else if (row.status2 === '4') {
-				status = 'reapplyFlag'
-			}
-			taskService.getTaskDef({
-				procDefId: this.processDefinitionId2,
-				businessId: row.auditId2,
-				businessTable: 'program_audit'
-			}).then((data) => {
-				this.$router.push({
-					path: '/flowable/task/TaskForm',
-					query: {
-						...pick(data, 'formType', 'formUrl', 'procDefKey', 'taskDefKey', 'procInsId', 'procDefId', 'taskId', 'status', 'title'),
-						procDefId: this.processDefinitionId2,
-						procDefKey: this.procDefKey2,
-						title: title,
-						formType: data.formType,
-						formUrl: data.formUrl,
-						formTitle: processTitle,
-						businessTable: 'program_audit',
-						businessId: row.auditId2,
-						isShow: 'false',
-						status: status,
-						routePath: '/program/registered/ProjectList'
-					}
+			// 先检查漏洞提醒
+			this.checkAndShowReminders('2', () => {
+				// 读取流程表单
+				let title = `发起流程【评估-项目二级校审】`
+				let processTitle = `${this.userName} 在 ${this.moment(new Date()).format('YYYY-MM-DD HH:mm')} 发起了[评估-项目二级校审]`
+				let status = 'startAndHold'
+				if (row.status2 === '3') {
+					status = 'startAndClose'
+				} else if (row.status2 === '4') {
+					status = 'reapplyFlag'
+				}
+				taskService.getTaskDef({
+					procDefId: this.processDefinitionId2,
+					businessId: row.auditId2,
+					businessTable: 'program_audit'
+				}).then((data) => {
+					this.$router.push({
+						path: '/flowable/task/TaskForm',
+						query: {
+							...pick(data, 'formType', 'formUrl', 'procDefKey', 'taskDefKey', 'procInsId', 'procDefId', 'taskId', 'status', 'title'),
+							procDefId: this.processDefinitionId2,
+							procDefKey: this.procDefKey2,
+							title: title,
+							formType: data.formType,
+							formUrl: data.formUrl,
+							formTitle: processTitle,
+							businessTable: 'program_audit',
+							businessId: row.auditId2,
+							isShow: 'false',
+							status: status,
+							routePath: '/program/registered/ProjectList',
+							skipRejectConfirm: 'true'  // 评估项目校审跳过驳回确认对话框
+						}
+					})
 				})
 			})
 		},
@@ -1082,36 +1092,40 @@ export default {
 		},
 		// 发起三级校审
 		thirdAuditPush(row) {
-			// 读取流程表单
-			let title = `发起流程【评估-项目三级校审】`
-			let processTitle = `${this.userName} 在 ${this.moment(new Date()).format('YYYY-MM-DD HH:mm')} 发起了[评估-项目三级校审]`
-			let status = 'startAndHold'
-			if (row.status3 === '3') {
-				status = 'startAndClose'
-			} else if (row.status3 === '4') {
-				status = 'reapplyFlag'
-			}
-			taskService.getTaskDef({
-				procDefId: this.processDefinitionId3,
-				businessId: row.auditId3,
-				businessTable: 'program_audit'
-			}).then((data) => {
-				this.$router.push({
-					path: '/flowable/task/TaskForm',
-					query: {
-						...pick(data, 'formType', 'formUrl', 'procDefKey', 'taskDefKey', 'procInsId', 'procDefId', 'taskId', 'status', 'title'),
-						procDefId: this.processDefinitionId3,
-						procDefKey: this.procDefKey3,
-						title: title,
-						formType: data.formType,
-						formUrl: data.formUrl,
-						formTitle: processTitle,
-						businessTable: 'program_audit',
-						businessId: row.auditId3,
-						isShow: 'false',
-						status: status,
-						routePath: '/program/registered/ProjectList'
-					}
+			// 先检查漏洞提醒
+			this.checkAndShowReminders('3', () => {
+				// 读取流程表单
+				let title = `发起流程【评估-项目三级校审】`
+				let processTitle = `${this.userName} 在 ${this.moment(new Date()).format('YYYY-MM-DD HH:mm')} 发起了[评估-项目三级校审]`
+				let status = 'startAndHold'
+				if (row.status3 === '3') {
+					status = 'startAndClose'
+				} else if (row.status3 === '4') {
+					status = 'reapplyFlag'
+				}
+				taskService.getTaskDef({
+					procDefId: this.processDefinitionId3,
+					businessId: row.auditId3,
+					businessTable: 'program_audit'
+				}).then((data) => {
+					this.$router.push({
+						path: '/flowable/task/TaskForm',
+						query: {
+							...pick(data, 'formType', 'formUrl', 'procDefKey', 'taskDefKey', 'procInsId', 'procDefId', 'taskId', 'status', 'title'),
+							procDefId: this.processDefinitionId3,
+							procDefKey: this.procDefKey3,
+							title: title,
+							formType: data.formType,
+							formUrl: data.formUrl,
+							formTitle: processTitle,
+							businessTable: 'program_audit',
+							businessId: row.auditId3,
+							isShow: 'false',
+							status: status,
+							routePath: '/program/registered/ProjectList',
+							skipRejectConfirm: 'true'  // 评估项目校审跳过驳回确认对话框
+						}
+					})
 				})
 			})
 		},
@@ -1203,7 +1217,8 @@ export default {
 						businessId: row.archiveId,
 						isShow: 'false',
 						status: status,
-						routePath: '/program/registered/ProjectList'
+						routePath: '/program/registered/ProjectList',
+							skipRejectConfirm: 'true'  // 评估项目校审跳过驳回确认对话框
 					}
 				})
 			})
@@ -1424,7 +1439,8 @@ export default {
 						businessId: row.id,
 						isShow: 'false',
 						status: status,
-						routePath: '/program/registered/ProjectList'
+						routePath: '/program/registered/ProjectList',
+							skipRejectConfirm: 'true'  // 评估项目校审跳过驳回确认对话框
 					}
 				})
 			})
@@ -1890,7 +1906,8 @@ export default {
 						businessId: row.id,
 						isShow: 'false',
 						status: status,
-						routePath: '/program/registered/ProjectList'
+						routePath: '/program/registered/ProjectList',
+							skipRejectConfirm: 'true'  // 评估项目校审跳过驳回确认对话框
 					}
 				})
 			})

+ 285 - 53
src/views/program/registered/ProjectThreeAuditForm.vue

@@ -109,6 +109,17 @@
             二级校审详情
           </el-link>
         </el-divider>
+        <!-- 超阈值漏洞提醒(灰色字体,不明显) -->
+        <div v-if="overThresholdIssues.length > 0" style="padding: 8px 15px; margin-bottom: 15px; margin-left: 125px; background-color: #fafafa; border-radius: 4px;">
+          <div style="font-size: 13px; color: #909399; line-height: 1.6;">
+            <i class="el-icon-info" style="margin-right: 4px;"></i>
+            <span style="font-weight: 500;">常见漏洞提醒:</span>
+            <span v-for="(issue, index) in overThresholdIssues" :key="issue.issueId">
+              <span v-if="index > 0">;</span>
+              {{ issue.issueName }}(已标记{{ issue.markCount }}次)
+            </span>
+          </div>
+        </div>
         <el-row>
           <el-col :span="24">
             <el-form-item label="明细表" prop="detailOpinion">
@@ -159,9 +170,67 @@
           </el-col>
         </el-row>
 
+        <!-- 问题记录展示区域(被驳回人查看,非审核模式下显示) -->
+        <div v-if="status !== 'audit' && issueRecords.length > 0" style="background-color: #fef0f0; padding: 20px; margin: 0 -20px 20px -20px; border-radius: 4px; border-left: 4px solid #F56C6C;">
+          <el-divider content-position="left" style="margin: 0 0 10px 0;">
+            <i class="el-icon-warning" style="color: #F56C6C;"></i>
+            <span style="font-weight: bold; color: #F56C6C; font-size: 15px;">校审问题记录</span>
+          </el-divider>
+          <el-row>
+            <el-col :span="24">
+              <el-form-item label="问题记录">
+                <div style="width: 100%;">
+                  <div v-for="(records, category) in groupedIssueRecords" :key="category" style="margin-bottom: 12px;">
+                    <div style="color: #303133; margin-bottom: 6px; font-size: 14px; font-weight: bold;">{{ category }}</div>
+                    <div v-for="record in records" :key="record.id" style="margin-bottom: 8px; padding-left: 30px;">
+                      <div style="font-size: 14px; color: #303133; line-height: 1.5;">
+                        <i class="el-icon-minus" style="color: #F56C6C; margin-right: 4px;"></i>
+                        {{ record.issueName }}
+                      </div>
+                      <div v-if="record.issueDesc" style="font-size: 13px; color: #606266; margin-top: 2px; padding-left: 16px;">
+                        问题描述:{{ record.issueDesc }}
+                      </div>
+                      <div v-if="record.suggestion" style="font-size: 13px; color: #67C23A; margin-top: 2px; padding-left: 16px;">
+                        整改建议:{{ record.suggestion }}
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
+
+        <!-- 问题标记区域(仅在审核模式下显示) -->
+        <div v-if="status === 'audit'" style="background-color: #f5f7fa; padding: 20px; margin: 0 -20px; border-radius: 4px;">
+          <el-divider content-position="left" style="margin: 0 0 10px 0;">
+            <i class="el-icon-warning" style="color: #E6A23C;"></i>
+            <span style="font-weight: bold; color: #F56C6C; font-size: 15px;">问题标记(选填,驳回时记录)</span>
+            <el-button type="primary" size="mini" icon="el-icon-plus" @click="openAddIssueDialog" style="margin-left: 15px;">添加常见问题</el-button>
+          </el-divider>
+          <el-row>
+            <el-col :span="24">
+              <el-form-item label="常见问题" prop="commonIssues">
+                <div v-if="commonIssueList.length > 0" style="width: 100%;">
+                  <div v-for="(issues, category) in groupedIssues" :key="category" style="margin-bottom: 8px;">
+                    <div style="font-weight: bold; color: #303133; margin-bottom: 4px; font-size: 14px;">{{ category }}</div>
+                    <el-checkbox-group v-model="selectedIssueIds">
+                      <el-checkbox v-for="issue in issues" :key="issue.id" :label="issue.id" style="display: block; margin-left: 0;">
+                        {{ issue.issueName }}
+                        <el-tooltip v-if="issue.issueDesc" :content="issue.issueDesc" placement="top">
+                          <i class="el-icon-info" style="color: #909399; cursor: help; margin-left: 4px;"></i>
+                        </el-tooltip>
+                      </el-checkbox>
+                    </el-checkbox-group>
+                  </div>
+                </div>
+                <div v-else style="color: #909399; font-size: 14px;">暂无常见问题字典,请点击上方按钮添加</div>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
 
       </el-form>
-      <!--        附件-->
       <UpLoadComponent ref="uploadComponent"></UpLoadComponent>
 
     </div>
@@ -171,6 +240,9 @@
     <user-select2 ref="userSelect2" @doSubmit="selectUser2"></user-select2>
     <user-select3 ref="userSelect3" @doSubmit="selectUser3"></user-select3>
 
+    <!-- 添加自定义漏洞对话框 -->
+    <CommonIssueForm ref="commonIssueForm" @refreshDataList="onIssueAdded"></CommonIssueForm>
+
   </div>
 </template>
 
@@ -186,6 +258,9 @@ import UserSelect1 from '@/views/utils/UserTreeSelect'
 import UserSelect2 from '@/views/utils/UserTreeSelect'
 import UserSelect3 from '@/views/utils/UserTreeSelect'
 import taskService from "@/api/flowable/taskService.js";
+import CommonIssueService from '@/api/program/CommonIssueService'
+import IssueRecordService from '@/api/program/IssueRecordService'
+import CommonIssueForm from '@/views/program/commonIssue/CommonIssueForm'
 
 export default {
   props: {
@@ -237,12 +312,23 @@ export default {
       programId: '',
       supervisorList: [],
       keyWatch: '',
-      tabName: ''
+      tabName: '',
+      // 漏洞标记相关
+      commonIssueList: [],
+      selectedIssueIds: [],
+      // 问题记录(被驳回人查看)
+      issueRecords: [],
+      // 超阈值漏洞提醒
+      overThresholdIssues: []
     }
   },
   programProjectListInfoService: null,
+  commonIssueService: null,
+  issueRecordService: null,
   created() {
     this.programProjectListInfoService = new ProgramProjectListInfoService()
+    this.commonIssueService = new CommonIssueService()
+    this.issueRecordService = new IssueRecordService()
   },
   computed: {
     bus: {
@@ -253,6 +339,45 @@ export default {
       set(val) {
         this.businessId = val
       }
+    },
+    groupedIssues() {
+      const groups = {}
+      this.commonIssueList.forEach(issue => {
+        const cat = issue.category || '其他'
+        if (!groups[cat]) groups[cat] = []
+        groups[cat].push(issue)
+      })
+      // 将"其他"分类放在最后
+      const sortedGroups = {}
+      const keys = Object.keys(groups).sort((a, b) => {
+        if (a === '其他') return 1
+        if (b === '其他') return -1
+        return a.localeCompare(b, 'zh')
+      })
+      keys.forEach(key => {
+        sortedGroups[key] = groups[key]
+      })
+      return sortedGroups
+    },
+    // 按分类分组的问题记录
+    groupedIssueRecords() {
+      const groups = {}
+      this.issueRecords.forEach(record => {
+        const cat = record.category || '其他'
+        if (!groups[cat]) groups[cat] = []
+        groups[cat].push(record)
+      })
+      // 将"其他"分类放在最后
+      const sortedGroups = {}
+      const keys = Object.keys(groups).sort((a, b) => {
+        if (a === '其他') return 1
+        if (b === '其他') return -1
+        return a.localeCompare(b, 'zh')
+      })
+      keys.forEach(key => {
+        sortedGroups[key] = groups[key]
+      })
+      return sortedGroups
     }
   },
   watch: {
@@ -286,7 +411,8 @@ export default {
     ProjectThreeAuditFormDialog,
     UserSelect1,
     UserSelect2,
-    UserSelect3
+    UserSelect3,
+    CommonIssueForm
   },
   methods: {
     getKeyWatch(keyWatch) {
@@ -387,6 +513,47 @@ export default {
           } else {
             this.$refs.uploadComponent.newUpload('view', this.inputForm.workAttachmentDtoList, 'program_audit', 500, null, true, this.$store.state.user.name)
           }
+          // 加载常见漏洞列表
+          console.log('=== 初始化:加载常见漏洞列表 ===')
+          console.log('auditLevel:', this.inputForm.auditLevel)
+          if (this.commonJS.isNotEmpty(this.inputForm.auditLevel)) {
+            this.commonIssueService.getEnabledList(this.inputForm.auditLevel).then((data) => {
+              console.log('常见漏洞列表加载完成,数量:', data ? data.length : 0)
+              this.commonIssueList = data || []
+              this.selectedIssueIds = []
+            }).catch(err => {
+              console.error('加载常见漏洞列表失败:', err)
+            })
+          } else {
+            console.log('auditLevel 为空,跳过加载常见漏洞列表')
+          }
+          // 加载问题标记记录(被驳回人查看)
+          console.log('=== 初始化:加载问题标记记录 ===')
+          console.log('inputForm.programId:', this.inputForm.programId)
+          console.log('inputForm.auditLevel:', this.inputForm.auditLevel)
+          if (this.commonJS.isNotEmpty(this.inputForm.programId) && this.commonJS.isNotEmpty(this.inputForm.auditLevel)) {
+            this.issueRecordService.getRecordsByProjectId(this.inputForm.programId, this.inputForm.auditLevel).then((data) => {
+              console.log('问题标记记录加载完成,数量:', data ? data.length : 0)
+              this.issueRecords = data || []
+            }).catch(err => {
+              console.error('加载问题标记记录失败:', err)
+            })
+          } else {
+            console.log('programId 或 auditLevel 为空,跳过加载问题标记记录')
+          }
+          // 加载超阈值漏洞提醒(当前登录人)
+          console.log('=== 初始化:加载超阈值漏洞提醒 ===')
+          const currentUserId = this.$store.state.user.id
+          if (this.commonJS.isNotEmpty(currentUserId) && this.commonJS.isNotEmpty(this.inputForm.auditLevel)) {
+            this.issueRecordService.getReminders(currentUserId, this.inputForm.auditLevel).then((data) => {
+              console.log('超阈值漏洞提醒加载完成,数量:', data ? data.length : 0)
+              this.overThresholdIssues = data || []
+            }).catch(err => {
+              console.error('加载超阈值漏洞提醒失败:', err)
+            })
+          } else {
+            console.log('userId 或 auditLevel 为空,跳过加载超阈值漏洞提醒')
+          }
           this.loading = false
         })
       })
@@ -530,44 +697,33 @@ export default {
       })
     },
     async updateStatusById(type, callback) {
+      console.log('=== ProjectThreeAuditForm.updateStatusById 被调用 ===')
+      console.log('type:', type)
+      console.log('callback 类型:', typeof callback)
       this.loading = true
       if (await this.$refs.uploadComponent.checkProgress()) {
+        console.log('上传进行中,中断操作')
         this.loading = false
         throw new Error()
       } else {
+        console.log('上传检查通过')
         if (type === 'reject' || type === 'reback') {
+          console.log('进入 reject/reback 分支')
           this.programProjectListInfoService.findByIdAudit(this.inputForm.id).then((data) => {
-            if (data.status !== '2') { // status的值不等于“审核中”,就弹出提示
+            console.log('findByIdAudit 返回数据:', data)
+            console.log('当前状态 data.status:', data.status)
+            if (data.status !== '2') { // status的值不等于”审核中”,就弹出提示
               //如果数据状态出现错误,会先去判断当前节点审核人是否是本人
               taskService.getTaskAuditUsersByprocInstId(this.$route.query.procInsId).then(res => {
-                let userInfo = this.$TOOL.data.get("USER_INFO");
+                let userInfo = this.$TOOL.data.get('USER_INFO');
                 // 如果审核人是本人的话,则跳过数据状态的限制,可再次审批
                 if (res.includes(userInfo.id)) {
                   if (type === 'reject') {
-                    // 驳回
-                    this.inputForm.status = '4'
-                    this.loading = true
-                    this.inputForm.workAttachmentDtoList = this.$refs.uploadComponent.getDataList()
-                    if (this.commonJS.isNotEmpty(this.inputForm.auditPeopleList)) {
-                      this.inputForm.programAuditAssessPeopleDtoList = []
-                      this.inputForm.auditPeopleList.split(',').forEach(item => {
-                        let i = { assessPeople: item, auditId: this.inputForm.id }
-                        this.inputForm.programAuditAssessPeopleDtoList.push(i)
-                      })
-                    } else {
-                      this.inputForm.programAuditAssessPeopleDtoList = []
-                    }
-                    this.programProjectListInfoService.saveFormThree(this.inputForm).then((data) => {
-                      this.loading = false
-                      let param = { status: this.inputForm.status, id: this.inputForm.id }
-                      this.loading = true
-                      this.programProjectListInfoService.updateStatusByAuditId(param).then(() => {
-                        this.loading = false
-                        callback()
-                      })
-                    }).catch(() => {
-                      this.loading = false
-                    })
+                  if (type === 'reject') {
+                    // 驳回 - 直接执行驳回逻辑,保存漏洞标记记录
+                    console.log('=== 执行驳回逻辑 ===')
+                    this.doReject(callback)
+                  }
                   }
                   if (type === 'reback') {
                     this.inputForm.status = '3'
@@ -588,31 +744,13 @@ export default {
                 this.$message.error('任务数据已发生改变或不存在,请在待办任务中确认此任务是否存在');
               });
             } else {
+              console.log('=== 状态为审核中(2),进入 else 分支 ===')
               if (type === 'reject') {
-                // 驳回
-                this.inputForm.status = '4'
-                this.loading = true
-                this.inputForm.workAttachmentDtoList = this.$refs.uploadComponent.getDataList()
-                if (this.commonJS.isNotEmpty(this.inputForm.auditPeopleList)) {
-                  this.inputForm.programAuditAssessPeopleDtoList = []
-                  this.inputForm.auditPeopleList.split(',').forEach(item => {
-                    let i = { assessPeople: item, auditId: this.inputForm.id }
-                    this.inputForm.programAuditAssessPeopleDtoList.push(i)
-                  })
-                } else {
-                  this.inputForm.programAuditAssessPeopleDtoList = []
-                }
-                this.programProjectListInfoService.saveFormThree(this.inputForm).then((data) => {
-                  this.loading = false
-                  let param = { status: this.inputForm.status, id: this.inputForm.id }
-                  this.loading = true
-                  this.programProjectListInfoService.updateStatusByAuditId(param).then(() => {
-                    this.loading = false
-                    callback()
-                  })
-                }).catch(() => {
-                  this.loading = false
-                })
+              if (type === 'reject') {
+                // 驳回 - 直接执行驳回逻辑,保存漏洞标记记录
+                console.log('=== 执行驳回逻辑 (status=2) ===')
+                this.doReject(callback)
+              }
               }
               if (type === 'reback') {
                 this.inputForm.status = '3'
@@ -791,6 +929,85 @@ export default {
       this.inputForm.auditPeopleName = user[0].name
       this.inputForm.auditPeople = user[0].id
     },
+    // 打开添加自定义漏洞对话框
+    openAddIssueDialog() {
+      this.$refs.commonIssueForm.init('add', null, this.inputForm.auditLevel)
+    },
+    // 漏洞添加成功后的回调
+    onIssueAdded() {
+      console.log('=== onIssueAdded 被调用 ===')
+      console.log('当前 auditLevel:', this.inputForm.auditLevel)
+      // 重新加载常见漏洞列表
+      if (this.commonJS.isNotEmpty(this.inputForm.auditLevel)) {
+        this.commonIssueService.getEnabledList(this.inputForm.auditLevel).then((data) => {
+          console.log('getEnabledList 返回数据:', data)
+          console.log('数据数量:', data ? data.length : 0)
+          if (data && data.length > 0) {
+            console.log('第一条数据:', data[0])
+          }
+          this.commonIssueList = data || []
+          this.selectedIssueIds = []
+          console.log('commonIssueList 更新后:', this.commonIssueList)
+        }).catch(err => {
+          console.error('重新加载常见漏洞列表失败:', err)
+        })
+      } else {
+        console.log('auditLevel 为空,跳过重新加载')
+      }
+    },
+    // 执行驳回逻辑
+    doReject(callback) {
+      this.inputForm.status = '4'
+      this.loading = true
+      this.inputForm.workAttachmentDtoList = this.$refs.uploadComponent.getDataList()
+      if (this.commonJS.isNotEmpty(this.inputForm.auditPeopleList)) {
+        this.inputForm.programAuditAssessPeopleDtoList = []
+        this.inputForm.auditPeopleList.split(',').forEach(item => {
+          let i = { assessPeople: item, auditId: this.inputForm.id }
+          this.inputForm.programAuditAssessPeopleDtoList.push(i)
+        })
+      } else {
+        this.inputForm.programAuditAssessPeopleDtoList = []
+      }
+      this.programProjectListInfoService.saveFormThree(this.inputForm).then((data) => {
+        this.loading = false
+        let param = { status: this.inputForm.status, id: this.inputForm.id }
+        this.loading = true
+        this.programProjectListInfoService.updateStatusByAuditId(param).then(() => {
+          this.loading = false
+          // 保存漏洞标记记录
+          this.saveIssueRecords(callback)
+        })
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    // 保存漏洞标记记录
+    saveIssueRecords(callback) {
+      const targetUserId = this.inputForm.programProjectListInfo.createBy || this.inputForm.programProjectListInfo.projectManager
+
+      if (!this.selectedIssueIds || this.selectedIssueIds.length === 0) {
+        // 没有选择任何漏洞,直接执行回调
+        if (callback) callback()
+        return
+      }
+
+      // 保存漏洞标记记录
+      this.issueRecordService.saveRecords({
+        projectId: this.inputForm.programId,
+        auditId: this.inputForm.id,
+        auditLevel: this.inputForm.auditLevel,
+        issueIds: this.selectedIssueIds,
+        targetUserId: targetUserId
+      }).then(() => {
+        console.log('漏洞标记记录保存成功')
+        if (callback) callback()
+      }).catch((err) => {
+        console.error('漏洞标记记录保存失败:', err)
+        // 即使失败也继续执行驳回
+        if (callback) callback()
+      })
+    },
   }
 }
 </script>
@@ -807,4 +1024,19 @@ export default {
   font-size: 16px;
   font-weight: bold;
 }
+
+/* 问题标记区域样式调整 */
+/deep/ .el-checkbox-group {
+  line-height: 1.4;
+}
+
+/deep/ .el-checkbox {
+  margin-bottom: 0;
+  margin-right: 0;
+  line-height: 1.4;
+}
+
+/deep/ .el-checkbox + .el-checkbox {
+  margin-left: 0;
+}
 </style>