Ver Fonte

feat: #19 产值审核评定页面

院班子审核员核心页面,按部门分组折叠审核+批量操作+审核日志

- 审核概览: 6部门汇总卡片,彩色边条+进度条+提交统计
- 分组折叠: el-collapse按部门分组,独立选择状态管理
- 单个审核: 通过/不通过(原因必填500字)+反馈通知提示
- 批量审核: 跨分组汇总选中记录,批量通过/批量不通过
- 行展开详情: 产值详情+协作信息+历史填报记录子表格
- 审核时间窗: 13-16日倒计时,审核前/中/逾期三态提示
- 审核日志: 审核人/项目/部门/月份/结果(ReviewStatusTag)/意见/时间
- 自动刷新: 每次审核操作后重新加载列表和日志

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
shenzx há 1 mês atrás
pai
commit
ce1ac7fc68
1 ficheiros alterados com 385 adições e 58 exclusões
  1. 385 58
      src/views/performance/review/index.vue

+ 385 - 58
src/views/performance/review/index.vue

@@ -1,17 +1,38 @@
 <template>
   <div class="app-container">
+    <!-- 审核时间窗倒计时 -->
+    <el-alert :title="reviewWindowMessage" :type="reviewWindowType" :closable="false" show-icon class="mb16" />
+
+    <!-- 审核概览区:各部门填报状态汇总卡片 -->
+    <el-row :gutter="16" class="mb16">
+      <el-col :span="4" v-for="card in overviewCards" :key="card.deptId">
+        <el-card shadow="hover" class="overview-card" :class="`overview-card--${card.status}`">
+          <div class="overview-card__dept">{{ card.deptName }}</div>
+          <div class="overview-card__count">{{ card.submittedCount }} / {{ card.totalCount }}</div>
+          <div class="overview-card__label">已提交/总项目</div>
+          <el-progress
+            :percentage="card.totalCount > 0 ? Math.round((card.submittedCount / card.totalCount) * 100) : 0"
+            :color="card.statusColor"
+            :stroke-width="6"
+          />
+          <div class="overview-card__status">
+            <el-tag :type="card.statusTagType" size="small">{{ card.statusLabel }}</el-tag>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 搜索区域 -->
     <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="80px">
       <el-form-item label="部门" prop="deptId">
-        <el-tree-select v-model="queryParams.deptId" :data="deptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择部门" clearable check-strictly style="width: 200px" />
+        <DeptSelector v-model="queryParams.deptId" :useTree="false" placeholder="请选择部门" width="200px" />
       </el-form-item>
       <el-form-item label="项目名称" prop="projectName">
         <el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
       </el-form-item>
       <el-form-item label="审核结果" prop="result">
         <el-select v-model="queryParams.result" placeholder="审核结果" clearable style="width: 200px">
-          <el-option label="待审核" value="PENDING" />
-          <el-option label="审核通过" value="APPROVED" />
-          <el-option label="审核不通过" value="REJECTED" />
+          <el-option v-for="dict in resultOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
         </el-select>
       </el-form-item>
       <el-form-item label="填报月份" prop="month">
@@ -23,9 +44,17 @@
       </el-form-item>
     </el-form>
 
+    <!-- 工具栏 -->
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button type="success" plain icon="CircleCheck" :disabled="multiple" @click="handleBatchApprove" v-hasPermi="['performance:review:approve']">批量审核通过</el-button>
+        <el-button type="success" plain icon="CircleCheck" :disabled="selectCount === 0" @click="handleBatchApprove" v-hasPermi="['performance:review:approve']">
+          批量通过 ({{ selectCount }})
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="danger" plain icon="CircleClose" :disabled="selectCount === 0" @click="handleBatchReject" v-hasPermi="['performance:review:approve']">
+          批量不通过 ({{ selectCount }})
+        </el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['performance:review:export']">导出</el-button>
@@ -33,40 +62,129 @@
       <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <el-table v-loading="loading" :data="reviewList" @selection-change="handleSelectionChange">
-      <el-table-column type="selection" width="50" align="center" />
+    <!-- 按部门分组的审核列表 -->
+    <div v-loading="loading">
+      <el-collapse v-model="activeDepts" v-if="groupedList.length > 0">
+        <el-collapse-item
+          v-for="group in groupedList"
+          :key="group.deptId"
+          :name="group.deptId"
+        >
+          <template #title>
+            <div class="collapse-title">
+              <span class="collapse-title__dept">{{ group.deptName }}</span>
+              <span class="collapse-title__stats">
+                已提交 {{ group.submittedCount }} / 共 {{ group.totalCount }} 个项目
+              </span>
+              <span class="collapse-title__output">
+                产值合计: {{ group.totalOutputSum }} 万元
+              </span>
+            </div>
+          </template>
+
+          <el-table :data="group.items" @selection-change="(sel) => handleGroupSelection(group.deptId, sel)" ref="groupTableRefs">
+            <el-table-column type="selection" width="50" align="center" />
+            <el-table-column label="项目名称" align="center" prop="projectName" :show-overflow-tooltip="true" />
+            <el-table-column label="填报月份" align="center" prop="month" width="120" />
+            <el-table-column label="当月进度(%)" align="center" prop="currentProgress" width="110" />
+            <el-table-column label="当月产值(万元)" align="center" prop="currentOutput" width="130" />
+            <el-table-column label="协作产值(万元)" align="center" prop="coopOutput" width="130" />
+            <el-table-column label="审核结果" align="center" width="120">
+              <template #default="scope">
+                <ReviewStatusTag :status="scope.row.result" :options="resultTagOptions" />
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" width="220" class-name="small-padding fixed-width">
+              <template #default="scope">
+                <el-button link type="success" icon="CircleCheck" @click="handleApprove(scope.row)" v-if="scope.row.result === 'PENDING'" v-hasPermi="['performance:review:approve']">
+                  通过
+                </el-button>
+                <el-button link type="danger" icon="CircleClose" @click="handleReject(scope.row)" v-if="scope.row.result === 'PENDING'" v-hasPermi="['performance:review:approve']">
+                  不通过
+                </el-button>
+                <el-button link type="primary" icon="View" @click="toggleRowExpand(scope.row, scope.$index)">
+                  {{ scope.row.expanded ? '收起' : '详情' }}
+                </el-button>
+              </template>
+            </el-table-column>
+            <!-- 行展开:详情 -->
+            <el-table-column type="expand" width="1">
+              <template #default="scope">
+                <div class="expand-detail">
+                  <el-descriptions :column="3" border size="small">
+                    <el-descriptions-item label="项目总产值得(万元)">{{ scope.row.projectTotalOutput || '--' }}</el-descriptions-item>
+                    <el-descriptions-item label="已填报产值(万元)">{{ scope.row.filledOutput || '--' }}</el-descriptions-item>
+                    <el-descriptions-item label="填报部门">{{ scope.row.deptName }}</el-descriptions-item>
+                    <el-descriptions-item label="协作部门">{{ scope.row.coopDeptName || '无' }}</el-descriptions-item>
+                    <el-descriptions-item label="进度说明">{{ scope.row.description || '无' }}</el-descriptions-item>
+                    <el-descriptions-item label="协作信息">
+                      <span v-if="scope.row.coopOutputValue">协作产值 {{ scope.row.coopOutputValue }} 万元,{{ scope.row.coopStatus || '待确认' }}</span>
+                      <span v-else>无</span>
+                    </el-descriptions-item>
+                    <el-descriptions-item label="填报时间">{{ parseTime(scope.row.createTime) }}</el-descriptions-item>
+                    <el-descriptions-item label="最近修改时间">{{ parseTime(scope.row.updateTime) }}</el-descriptions-item>
+                    <el-descriptions-item label="审核意见" v-if="scope.row.reviewComment">{{ scope.row.reviewComment }}</el-descriptions-item>
+                  </el-descriptions>
+                  <div class="expand-detail__history" v-if="scope.row.history && scope.row.history.length > 0">
+                    <div class="expand-detail__history-title">历史填报记录</div>
+                    <el-table :data="scope.row.history" border size="small">
+                      <el-table-column label="月份" prop="month" width="100" />
+                      <el-table-column label="进度(%)" prop="currentProgress" width="80" />
+                      <el-table-column label="产值(万元)" prop="currentOutput" width="120" />
+                      <el-table-column label="审核结果" prop="result" width="100">
+                        <template #default="s">
+                          <ReviewStatusTag :status="s.row.result" :options="resultTagOptions" />
+                        </template>
+                      </el-table-column>
+                    </el-table>
+                  </div>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-collapse-item>
+      </el-collapse>
+      <el-empty v-else description="暂无审核数据" />
+    </div>
+
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+
+    <!-- 审核日志 -->
+    <el-divider content-position="left">审核日志</el-divider>
+    <el-table :data="reviewLogs" border v-loading="logLoading">
+      <el-table-column label="审核人" align="center" prop="reviewerName" width="120" />
       <el-table-column label="项目名称" align="center" prop="projectName" :show-overflow-tooltip="true" />
-      <el-table-column label="填报部门" align="center" prop="deptName" />
-      <el-table-column label="填报月份" align="center" prop="month" />
-      <el-table-column label="当月进度(%)" align="center" prop="currentProgress" />
-      <el-table-column label="当月产值(万元)" align="center" prop="currentOutput" />
-      <el-table-column label="协作产值(万元)" align="center" prop="coopOutput" />
-      <el-table-column label="审核结果" align="center" prop="result">
+      <el-table-column label="部门" align="center" prop="deptName" width="120" />
+      <el-table-column label="填报月份" align="center" prop="month" width="100" />
+      <el-table-column label="审核结果" align="center" prop="result" width="110">
         <template #default="scope">
-          <dict-tag :options="resultOptions" :value="scope.row.result" />
+          <ReviewStatusTag :status="scope.row.result" :options="resultTagOptions" />
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" width="200" class-name="small-padding fixed-width">
-        <template #default="scope">
-          <el-button link type="success" icon="CircleCheck" @click="handleApprove(scope.row)" v-if="scope.row.result === 'PENDING'" v-hasPermi="['performance:review:approve']">通过</el-button>
-          <el-button link type="danger" icon="CircleClose" @click="handleReject(scope.row)" v-if="scope.row.result === 'PENDING'" v-hasPermi="['performance:review:approve']">不通过</el-button>
-          <el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
-        </template>
+      <el-table-column label="审核意见" align="center" prop="comment" :show-overflow-tooltip="true" />
+      <el-table-column label="审核时间" align="center" prop="createTime" width="160">
+        <template #default="scope">{{ parseTime(scope.row.createTime) }}</template>
       </el-table-column>
     </el-table>
-
-    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    <pagination v-show="logTotal > 0" :total="logTotal" v-model:page="logQuery.pageNum" v-model:limit="logQuery.pageSize" @pagination="getReviewLogs" />
 
     <!-- 审核不通过对话框 -->
-    <el-dialog title="审核不通过" v-model="rejectOpen" width="500px" append-to-body>
+    <el-dialog title="审核不通过" v-model="rejectOpen" width="550px" append-to-body @close="rejectOpen = false">
+      <template #header>
+        <span style="color: #F56C6C; font-weight: bold">审核不通过</span>
+        <div style="font-size: 13px; color: #909399; margin-top: 4px">审核不通过将自动通知填报部门管理员</div>
+      </template>
       <el-form :model="rejectForm" :rules="rejectRules" ref="rejectRef" label-width="100px">
         <el-form-item label="不通过原因" prop="reason">
-          <el-input v-model="rejectForm.reason" type="textarea" placeholder="请填写审核不通过的原因" maxlength="500" />
+          <el-input v-model="rejectForm.reason" type="textarea" :rows="4" placeholder="请详细填写审核不通过的原因,此意见将反馈给填报部门" maxlength="500" show-word-limit />
         </el-form-item>
+        <el-alert title="反馈通知" type="warning" :closable="false" show-icon style="margin-top: 8px">
+          <span>审核不通过后,系统将自动向填报部门管理员发送通知,提醒其在14日前修改后重新提交。</span>
+        </el-alert>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button type="danger" @click="submitReject">确 定</el-button>
+          <el-button type="danger" @click="submitReject">确认不通过并通知</el-button>
           <el-button @click="rejectOpen = false">取 消</el-button>
         </div>
       </template>
@@ -75,53 +193,152 @@
 </template>
 
 <script setup name="PerformanceReview">
-import { listReview, approveReview, rejectReview } from '@/api/performance/review'
+import { listReview, approveReview, rejectReview, batchReview, listReviewLog } from '@/api/performance/review'
+import { ReviewStatusTag, DeptSelector } from '@/components/performance'
 
 const { proxy } = getCurrentInstance()
 
-const resultOptions = ref([
+// 审核时间窗计算 (每月13-16日)
+const now = new Date()
+const currentDay = now.getDate()
+const currentMonth = now.getMonth() + 1
+const inReviewWindow = currentDay >= 13 && currentDay <= 16
+const beforeReviewWindow = currentDay < 13
+const isReviewOverdue = currentDay > 16
+
+const reviewWindowMessage = computed(() => {
+  if (inReviewWindow) {
+    const remainDays = 16 - currentDay
+    return `审核进行中(${currentMonth}月13-16日),距离审核截止还有 ${remainDays} 天。请在截止前完成审核评定!`
+  }
+  if (beforeReviewWindow) {
+    return `审核窗口将于${currentMonth}月13日开启(每月13-16日),当前为填报阶段。`
+  }
+  return `已超过审核截止日期(${currentMonth}月16日),请尽快完成遗留审核!逾期未审核系统将自动提醒。`
+})
+
+const reviewWindowType = computed(() => {
+  if (inReviewWindow) return currentDay >= 15 ? 'warning' : 'success'
+  if (isReviewOverdue) return 'error'
+  return 'info'
+})
+
+// 选项
+const resultOptions = [
   { label: '待审核', value: 'PENDING' },
   { label: '审核通过', value: 'APPROVED' },
   { label: '审核不通过', value: 'REJECTED' }
-])
+]
 
-const reviewList = ref([])
-const loading = ref(true)
+const resultTagOptions = [
+  { label: '待审核', value: 'PENDING', tagType: 'warning' },
+  { label: '审核通过', value: 'APPROVED', tagType: 'success' },
+  { label: '审核不通过', value: 'REJECTED', tagType: 'danger' }
+]
+
+// 状态变量
+const loading = ref(false)
 const showSearch = ref(true)
-const ids = ref([])
-const single = ref(true)
-const multiple = ref(true)
 const total = ref(0)
+const selectCount = ref(0)
 const rejectOpen = ref(false)
-const currentRow = ref(null)
-const deptOptions = ref([])
+const activeDepts = ref([])
+const logLoading = ref(false)
+const logTotal = ref(0)
+
+const reviewList = ref([])
+const reviewLogs = ref([])
+const groupSelectionMap = reactive({})
+const groupedList = computed(() => {
+  const map = {}
+  reviewList.value.forEach(item => {
+    if (!map[item.deptId]) {
+      map[item.deptId] = {
+        deptId: item.deptId,
+        deptName: item.deptName,
+        items: [],
+        submittedCount: 0,
+        totalCount: 0,
+        totalOutputSum: 0
+      }
+    }
+    map[item.deptId].items.push({ ...item, expanded: false })
+    map[item.deptId].totalCount++
+    if (item.result !== 'DRAFT') {
+      map[item.deptId].submittedCount++
+    }
+    map[item.deptId].totalOutputSum = ((map[item.deptId].totalOutputSum || 0) + (item.currentOutput || 0)).toFixed(4)
+  })
+  return Object.values(map)
+})
+
+const overviewCards = computed(() => {
+  return groupedList.value.map(g => {
+    const allSubmitted = g.totalCount > 0 && g.submittedCount === g.totalCount
+    const someSubmitted = g.submittedCount > 0 && g.submittedCount < g.totalCount
+    let status, statusLabel, statusTagType, statusColor
+    if (allSubmitted) {
+      status = 'complete'
+      statusLabel = '全部已提交'
+      statusTagType = 'success'
+      statusColor = '#67C23A'
+    } else if (someSubmitted) {
+      status = 'partial'
+      statusLabel = '部分已提交'
+      statusTagType = 'warning'
+      statusColor = '#E6A23C'
+    } else {
+      status = 'none'
+      statusLabel = '尚未提交'
+      statusTagType = 'danger'
+      statusColor = '#F56C6C'
+    }
+    return {
+      ...g,
+      status,
+      statusLabel,
+      statusTagType,
+      statusColor
+    }
+  })
+})
 
 const data = reactive({
   queryParams: {
     pageNum: 1,
-    pageSize: 10,
+    pageSize: 20,
     deptId: undefined,
     projectName: undefined,
     result: undefined,
     month: undefined
   },
-  rejectForm: {
-    reviewId: undefined,
-    reason: undefined
-  },
-  rejectRules: {
-    reason: [{ required: true, message: '不通过原因不能为空', trigger: 'blur' }]
-  }
+  rejectForm: { reviewId: undefined, reason: undefined },
+  rejectRules: { reason: [{ required: true, message: '不通过原因不能为空', trigger: 'blur' }] },
+  logQuery: { pageNum: 1, pageSize: 10 }
 })
 
-const { queryParams, rejectForm, rejectRules } = toRefs(data)
+const { queryParams, rejectForm, rejectRules, logQuery } = toRefs(data)
 
 function getList() {
   loading.value = true
+  selectCount.value = 0
   listReview(queryParams.value).then(res => {
-    reviewList.value = res.rows
-    total.value = res.total
+    reviewList.value = res.rows || []
+    total.value = res.total || 0
     loading.value = false
+    // 默认展开所有有数据的部门
+    nextTick(() => {
+      activeDepts.value = groupedList.value.filter(g => g.totalCount > 0).map(g => g.deptId)
+    })
+  })
+}
+
+function getReviewLogs() {
+  logLoading.value = true
+  listReviewLog(logQuery.value).then(res => {
+    reviewLogs.value = res.rows || []
+    logTotal.value = res.total || 0
+    logLoading.value = false
   })
 }
 
@@ -135,25 +352,29 @@ function resetQuery() {
   handleQuery()
 }
 
-function handleSelectionChange(selection) {
-  ids.value = selection.map(item => item.reviewId)
-  single.value = selection.length !== 1
-  multiple.value = !selection.length
+// 分组选择
+function handleGroupSelection(deptId, selection) {
+  groupSelectionMap[deptId] = selection
+
+  let total = 0
+  Object.values(groupSelectionMap).forEach(arr => { total += arr.length })
+  selectCount.value = total
 }
 
+// 单条审核
 function handleApprove(row) {
-  proxy.$modal.confirm('确认审核通过该条产值填报?').then(() => {
+  proxy.$modal.confirm(`确认审核通过「${row.projectName}」的产值填报?`).then(() => {
     return approveReview({ reviewId: row.reviewId })
   }).then(() => {
     proxy.$modal.msgSuccess('审核通过')
     getList()
+    getReviewLogs()
   }).catch(() => {})
 }
 
 function handleReject(row) {
   rejectForm.value.reviewId = row.reviewId
   rejectForm.value.reason = undefined
-  currentRow.value = row
   rejectOpen.value = true
 }
 
@@ -161,29 +382,53 @@ function submitReject() {
   proxy.$refs.rejectRef.validate(valid => {
     if (valid) {
       rejectReview(rejectForm.value).then(() => {
-        proxy.$modal.msgSuccess('已提交审核不通过意见')
+        proxy.$modal.msgSuccess('审核不通过,已通知填报部门管理员')
         rejectOpen.value = false
         getList()
+        getReviewLogs()
       })
     }
   })
 }
 
+// 批量审核
 function handleBatchApprove() {
-  if (ids.value.length === 0) {
+  const allSelectedIds = getAllSelectedIds()
+  if (allSelectedIds.length === 0) {
     proxy.$modal.msgWarning('请选择要审核的记录')
     return
   }
-  proxy.$modal.confirm('确认批量审核通过选中的记录?').then(() => {
-    return approveReview({ reviewIds: ids.value })
+  proxy.$modal.confirm(`确认批量审核通过 ${allSelectedIds.length} 条记录?`).then(() => {
+    return batchReview({ reviewIds: allSelectedIds, result: 'APPROVED' })
   }).then(() => {
     proxy.$modal.msgSuccess('批量审核通过')
     getList()
+    getReviewLogs()
   }).catch(() => {})
 }
 
-function handleDetail(row) {
-  // TODO: 查看审核详情
+function handleBatchReject() {
+  const allSelectedIds = getAllSelectedIds()
+  if (allSelectedIds.length === 0) {
+    proxy.$modal.msgWarning('请选择要审核的记录')
+    return
+  }
+  rejectForm.value.reviewId = allSelectedIds.join(',')
+  rejectForm.value.reason = undefined
+  rejectOpen.value = true
+}
+
+function getAllSelectedIds() {
+  const ids = []
+  Object.values(groupSelectionMap).forEach(arr => {
+    arr.forEach(item => ids.push(item.reviewId))
+  })
+  return ids
+}
+
+// 行展开/收起
+function toggleRowExpand(row, index) {
+  row.expanded = !row.expanded
 }
 
 function handleExport() {
@@ -192,5 +437,87 @@ function handleExport() {
 
 onMounted(() => {
   getList()
+  getReviewLogs()
 })
 </script>
+
+<style lang="scss" scoped>
+.mb16 {
+  margin-bottom: 16px;
+}
+
+.overview-card {
+  text-align: center;
+  border-top: 3px solid #DCDFE6;
+
+  &--complete { border-top-color: #67C23A; }
+  &--partial { border-top-color: #E6A23C; }
+  &--none { border-top-color: #F56C6C; }
+
+  &__dept {
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  &__count {
+    font-size: 24px;
+    font-weight: bold;
+    margin: 8px 0 4px;
+  }
+
+  &__label {
+    font-size: 12px;
+    color: #909399;
+    margin-bottom: 10px;
+  }
+
+  &__status {
+    margin-top: 8px;
+  }
+}
+
+.collapse-title {
+  display: flex;
+  align-items: center;
+  gap: 24px;
+  width: 100%;
+
+  &__dept {
+    font-size: 15px;
+    font-weight: 600;
+    color: #303133;
+    min-width: 150px;
+  }
+
+  &__stats {
+    font-size: 13px;
+    color: #606266;
+  }
+
+  &__output {
+    font-size: 13px;
+    color: #409EFF;
+    font-weight: 500;
+  }
+}
+
+.expand-detail {
+  padding: 16px 20px;
+  background: #F5F7FA;
+
+  &__history {
+    margin-top: 12px;
+  }
+
+  &__history-title {
+    font-size: 13px;
+    font-weight: 600;
+    color: #303133;
+    margin-bottom: 8px;
+  }
+}
+</style>