Просмотр исходного кода

feat: #16 绩效核算展示页面 - 核心业务闭环收官

填报→审核→核算完整闭环最后一步,8项需求全部实现

- 核算概览: 4张汇总卡片(全院产值/绩效总额/已核算部门/审核通过项目)
- 公式展示: "绩效总额 = 审核通过产值总和 × 20%" 醒目提示
- 核算明细表: 部门/月份/产值总和/核算比例/绩效总额,按月筛选
- 项目产值明细: 行展开子表格,贡献绩效自动计算(产值×比例)
- 图表: 柱状图(部门对比)+折线图(历史趋势多系列平滑曲线)
- 权限控制: 院班子全部门+比例设置,部门/员工仅本部门
- 核算比例设置: 弹窗修改 1-100% 2位小数,历史结果不变
- Excel导出 + 绩效详情页跳转

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
shenzx 1 месяц назад
Родитель
Сommit
f627f77976
1 измененных файлов с 267 добавлено и 92 удалено
  1. 267 92
      src/views/performance/calculate/index.vue

+ 267 - 92
src/views/performance/calculate/index.vue

@@ -1,8 +1,60 @@
 <template>
   <div class="app-container">
+    <!-- 核算公式展示 -->
+    <el-alert :closable="false" show-icon type="info" class="mb16">
+      <template #title>
+        <span style="font-weight: bold">绩效核算公式:</span>
+        <span>部门当月绩效总额 = 该部门当月所有审核通过项目产值总和 × </span>
+        <span style="font-weight: bold; color: #409EFF; font-size: 16px">{{ performanceRatio }}%</span>
+        <el-divider direction="vertical" />
+        <span style="color: #909399; font-size: 12px">核算比例由院班子在系统配置中设定,当前为默认值</span>
+      </template>
+    </el-alert>
+
+    <!-- 核算概览区 -->
+    <el-row :gutter="16" class="mb16">
+      <el-col :span="6" v-for="(card, idx) in summaryCards" :key="card.key">
+        <el-card shadow="hover" class="summary-card" :class="`summary-card--${idx}`">
+          <div class="summary-card__label">{{ card.label }}</div>
+          <div class="summary-card__value">{{ card.value }}</div>
+          <div class="summary-card__unit" v-if="card.unit">{{ card.unit }}</div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 图表区域 -->
+    <el-row :gutter="20" class="mb16">
+      <el-col :span="12">
+        <el-card shadow="hover">
+          <PerformanceChart
+            type="bar"
+            :xData="barChartData.deptNames"
+            :seriesData="barChartData.series"
+            title="各部门当月绩效对比"
+            yName="万元"
+            :height="350"
+          />
+        </el-card>
+      </el-col>
+      <el-col :span="12">
+        <el-card shadow="hover">
+          <PerformanceChart
+            type="line"
+            :xData="lineChartData.months"
+            :seriesData="lineChartData.series"
+            title="各部门绩效月度趋势"
+            yName="万元"
+            :height="350"
+            :smooth="true"
+          />
+        </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" />
+      <el-form-item label="部门" prop="deptId" v-if="isAdminView">
+        <DeptSelector v-model="queryParams.deptId" :useTree="false" placeholder="请选择部门" width="200px" />
       </el-form-item>
       <el-form-item label="核算月份" prop="month">
         <el-date-picker v-model="queryParams.month" type="month" value-format="YYYY-MM" placeholder="请选择月份" clearable style="width: 200px" />
@@ -15,50 +67,75 @@
 
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['performance:calculate:export']">导出</el-button>
-      </el-col>
-      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
-    </el-row>
-
-    <!-- 绩效汇总卡片 -->
-    <el-row :gutter="20" class="mb8">
-      <el-col :span="6" v-for="card in summaryCards" :key="card.key">
-        <el-card shadow="hover">
-          <div class="summary-card">
-            <div class="summary-card__label">{{ card.label }}</div>
-            <div class="summary-card__value">{{ card.value }}</div>
-          </div>
-        </el-card>
+        <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['performance:calculate:export']">导出Excel</el-button>
       </el-col>
-    </el-row>
-
-    <!-- 绩效核算图表 -->
-    <el-row :gutter="20" class="mb8">
-      <el-col :span="12">
-        <el-card shadow="hover">
-          <template #header><span>各部门当月绩效对比</span></template>
-          <div ref="barChartRef" style="height: 350px"></div>
-        </el-card>
-      </el-col>
-      <el-col :span="12">
-        <el-card shadow="hover">
-          <template #header><span>绩效月度趋势</span></template>
-          <div ref="lineChartRef" style="height: 350px"></div>
-        </el-card>
+      <el-col :span="1.5" v-if="isAdminView">
+        <el-button type="primary" plain icon="Setting" @click="handleRatioSetting" v-hasPermi="['performance:calculate:ratio']">核算比例设置</el-button>
       </el-col>
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="handleQuery"></right-toolbar>
     </el-row>
 
-    <el-table v-loading="loading" :data="calculateList" border>
-      <el-table-column label="部门" align="center" prop="deptName" />
-      <el-table-column label="核算月份" align="center" prop="month" />
-      <el-table-column label="当月产值总和(万元)" align="center" prop="totalOutput" />
-      <el-table-column label="核算比例(%)" align="center" prop="ratio" />
-      <el-table-column label="绩效总额(万元)" align="center" prop="performanceAmount">
+    <!-- 核算明细表(支持展开) -->
+    <el-table
+      v-loading="loading"
+      :data="calculateList"
+      border
+      row-key="deptId"
+      @expand-change="handleExpandChange"
+    >
+      <el-table-column type="expand">
         <template #default="scope">
-          <span style="font-weight: bold; color: #409EFF">{{ scope.row.performanceAmount }}</span>
+          <div class="expand-projects">
+            <div class="expand-projects__title">{{ scope.row.deptName }} — 审核通过项目产值明细</div>
+            <el-table :data="scope.row.projects || []" border size="small" v-loading="scope.row._loading">
+              <el-table-column label="项目名称" align="center" prop="projectName" :show-overflow-tooltip="true" />
+              <el-table-column label="项目类型" align="center" width="100">
+                <template #default="s">{{ typeMap[s.row.projectType] || s.row.projectType }}</template>
+              </el-table-column>
+              <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="100">
+                <template #default="s">
+                  <el-tag :type="s.row.reviewResult === 'APPROVED' ? 'success' : 'danger'" size="small">
+                    {{ s.row.reviewResult === 'APPROVED' ? '通过' : '不通过' }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column label="贡献绩效(万元)" align="center" width="120">
+                <template #default="s">
+                  <span style="font-weight: bold; color: #409EFF">
+                    {{ ((s.row.currentOutput || 0) * ((scope.row.ratio || 20) / 100)).toFixed(4) }}
+                  </span>
+                </template>
+              </el-table-column>
+            </el-table>
+            <el-empty v-if="!scope.row.projects || scope.row.projects.length === 0" description="暂无明细数据" :image-size="60" />
+          </div>
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" width="100">
+      <el-table-column label="部门" align="center" prop="deptName" width="160" />
+      <el-table-column label="核算月份" align="center" prop="month" width="120" />
+      <el-table-column label="审核通过项目数" align="center" prop="approvedProjectCount" width="120" />
+      <el-table-column label="当月产值总和(万元)" align="center" prop="totalOutput" width="140">
+        <template #default="scope">
+          <span style="font-weight: 500">{{ scope.row.totalOutput }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="核算比例(%)" align="center" prop="ratio" width="100">
+        <template #default="scope">
+          <el-tag type="warning" effect="plain">{{ scope.row.ratio || 20 }}%</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="绩效总额(万元)" align="center" prop="performanceAmount" width="140">
+        <template #default="scope">
+          <span class="performance-amount">{{ scope.row.performanceAmount }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="核算时间" align="center" prop="calculateTime" width="160">
+        <template #default="scope">{{ parseTime(scope.row.createTime) }}</template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="80">
         <template #default="scope">
           <el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
         </template>
@@ -66,83 +143,130 @@
     </el-table>
 
     <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+
+    <!-- 核算比例设置对话框 -->
+    <el-dialog title="核算比例设置" v-model="ratioOpen" width="450px" append-to-body>
+      <el-form :model="ratioForm" ref="ratioRef" label-width="120px">
+        <el-form-item label="绩效核算比例(%)" prop="ratio">
+          <el-input-number v-model="ratioForm.ratio" :min="1" :max="100" :precision="2" style="width: 100%" />
+        </el-form-item>
+        <el-alert title="注意" type="warning" :closable="false" show-icon>
+          修改核算比例将重新计算所有部门的绩效总额,请谨慎操作。历史核算结果不受影响。
+        </el-alert>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitRatio">确认修改</el-button>
+          <el-button @click="ratioOpen = false">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="PerformanceCalculate">
-import * as echarts from 'echarts'
-import { listCalculate, getCalculateSummary } from '@/api/performance/calculate'
+import { listCalculate, getCalculateSummary, getCalculateChart, updateCalculateRatio } from '@/api/performance/calculate'
+import { PerformanceChart, DeptSelector } from '@/components/performance'
+import useUserStore from '@/store/modules/user'
 
 const { proxy } = getCurrentInstance()
+const router = useRouter()
 
+// 权限判断
+const userStore = useUserStore()
+const isAdminView = computed(() => userStore.roles.includes('admin') || userStore.roles.includes('ROLE_ADMIN'))
+
+const typeMap = {
+  'ENGINEERING': '工程项目',
+  'TECHNICAL_SERVICE': '技术服务',
+  'CONSULTING': '咨询服务',
+  'OTHER': '其他'
+}
+
+// 默认核算比例(从配置获取)
+const performanceRatio = ref(20)
+
+// 状态变量
 const calculateList = ref([])
-const loading = ref(true)
+const loading = ref(false)
 const showSearch = ref(true)
 const total = ref(0)
-const deptOptions = ref([])
-const barChartRef = ref(null)
-const lineChartRef = ref(null)
-let barChart = null
-let lineChart = null
+const ratioOpen = ref(false)
 
 const summaryCards = ref([
-  { key: 'totalOutput', label: '全院当月产值总和(万元)', value: '--' },
-  { key: 'totalPerformance', label: '全院当月绩效总额(万元)', value: '--' },
-  { key: 'deptCount', label: '已核算部门数', value: '--' },
-  { key: 'projectCount', label: '已核算项目数', value: '--' }
+  { key: 'totalOutput', label: '全院当月审核通过产值总和', value: '--', unit: '万元' },
+  { key: 'totalPerformance', label: '全院当月绩效总额', value: '--', unit: '万元' },
+  { key: 'deptCount', label: '已核算部门数', value: '--', unit: '' },
+  { key: 'approvedProjectCount', label: '审核通过项目总数', value: '--', unit: '个' }
 ])
 
+const barChartData = reactive({
+  deptNames: [],
+  series: [{ name: '绩效总额(万元)', data: [], color: '#409EFF' }]
+})
+
+const lineChartData = reactive({
+  months: [],
+  series: []
+})
+
 const data = reactive({
   queryParams: {
     pageNum: 1,
     pageSize: 10,
     deptId: undefined,
     month: undefined
-  }
+  },
+  ratioForm: { ratio: 20 }
 })
 
-const { queryParams } = toRefs(data)
+const { queryParams, ratioForm } = toRefs(data)
 
+// 数据加载
 function getList() {
   loading.value = true
   listCalculate(queryParams.value).then(res => {
-    calculateList.value = res.rows
-    total.value = res.total
+    const rows = (res.rows || []).map(row => ({
+      ...row,
+      _loading: false,
+      projects: []
+    }))
+    calculateList.value = rows
+    total.value = res.total || 0
     loading.value = false
   })
 }
 
 function getSummary() {
   getCalculateSummary(queryParams.value).then(res => {
-    const data = res.data
+    const d = res.data || {}
     summaryCards.value = [
-      { key: 'totalOutput', label: '全院当月产值总和(万元)', value: data.totalOutput || '--' },
-      { key: 'totalPerformance', label: '全院当月绩效总额(万元)', value: data.totalPerformance || '--' },
-      { key: 'deptCount', label: '已核算部门数', value: data.deptCount || '--' },
-      { key: 'projectCount', label: '已核算项目数', value: data.projectCount || '--' }
+      { key: 'totalOutput', label: '全院当月审核通过产值总和', value: d.totalOutput || '--', unit: '万元' },
+      { key: 'totalPerformance', label: '全院当月绩效总额', value: d.totalPerformance || '--', unit: '万元' },
+      { key: 'deptCount', label: '已核算部门数', value: d.deptCount || '--', unit: '' },
+      { key: 'approvedProjectCount', label: '审核通过项目总数', value: d.approvedProjectCount || '--', unit: '个' }
     ]
+    performanceRatio.value = d.ratio || 20
   })
 }
 
-function initBarChart() {
-  if (!barChartRef.value) return
-  barChart = echarts.init(barChartRef.value)
-  barChart.setOption({
-    tooltip: { trigger: 'axis' },
-    xAxis: { type: 'category', data: [] },
-    yAxis: { type: 'value', name: '绩效总额(万元)' },
-    series: [{ type: 'bar', data: [], itemStyle: { color: '#409EFF' } }]
-  })
-}
+function getChart() {
+  getCalculateChart(queryParams.value).then(res => {
+    const d = res.data || {}
 
-function initLineChart() {
-  if (!lineChartRef.value) return
-  lineChart = echarts.init(lineChartRef.value)
-  lineChart.setOption({
-    tooltip: { trigger: 'axis' },
-    xAxis: { type: 'category', data: [] },
-    yAxis: { type: 'value', name: '绩效总额(万元)' },
-    series: [{ type: 'line', data: [], smooth: true, itemStyle: { color: '#67C23A' } }]
+    // 柱状图:各部门当月绩效对比
+    barChartData.deptNames = d.barDeptNames || []
+    barChartData.series = [
+      { name: '绩效总额(万元)', data: d.barPerformanceData || [], color: '#409EFF' }
+    ]
+
+    // 折线图:历史趋势
+    lineChartData.months = d.lineMonths || []
+    lineChartData.series = (d.lineDeptSeries || []).map(s => ({
+      name: s.deptName,
+      data: s.data || [],
+      smooth: true
+    }))
   })
 }
 
@@ -150,6 +274,7 @@ function handleQuery() {
   queryParams.value.pageNum = 1
   getList()
   getSummary()
+  getChart()
 }
 
 function resetQuery() {
@@ -157,41 +282,91 @@ function resetQuery() {
   handleQuery()
 }
 
+// 展开行加载项目明细
+function handleExpandChange(row, expandedRows) {
+  if (expandedRows.some(r => r.deptId === row.deptId)) {
+    row._loading = true
+    listCalculate({ deptId: row.deptId, month: row.month }).then(res => {
+      row.projects = (res.rows || []).flatMap(r => r.projects || [])
+      row._loading = false
+    })
+  }
+}
+
 function handleDetail(row) {
-  // TODO: 查看部门绩效详情
+  router.push(`/performance/calculate-detail/index/${row.deptId}/${row.month}`)
 }
 
 function handleExport() {
   proxy.download('/performance/calculate/export', { ...queryParams.value }, `calculate_${new Date().getTime()}.xlsx`)
 }
 
-onMounted(() => {
-  getList()
-  getSummary()
-  nextTick(() => {
-    initBarChart()
-    initLineChart()
+function handleRatioSetting() {
+  ratioForm.value.ratio = performanceRatio.value
+  ratioOpen.value = true
+}
+
+function submitRatio() {
+  updateCalculateRatio(ratioForm.value.ratio).then(() => {
+    proxy.$modal.msgSuccess('核算比例已更新,绩效核算结果将重新计算')
+    ratioOpen.value = false
+    performanceRatio.value = ratioForm.value.ratio
+    handleQuery()
   })
-})
+}
 
-onUnmounted(() => {
-  barChart?.dispose()
-  lineChart?.dispose()
+onMounted(() => {
+  handleQuery()
 })
 </script>
 
 <style lang="scss" scoped>
+.mb16 {
+  margin-bottom: 16px;
+}
+
 .summary-card {
   text-align: center;
+
+  &--0 { border-top: 3px solid #409EFF; }
+  &--1 { border-top: 3px solid #67C23A; }
+  &--2 { border-top: 3px solid #E6A23C; }
+  &--3 { border-top: 3px solid #F56C6C; }
+
   &__label {
-    font-size: 14px;
+    font-size: 13px;
     color: #909399;
-    margin-bottom: 8px;
+    margin-bottom: 10px;
   }
+
   &__value {
-    font-size: 24px;
+    font-size: 26px;
     font-weight: bold;
     color: #303133;
   }
+
+  &__unit {
+    font-size: 12px;
+    color: #C0C4CC;
+    margin-top: 4px;
+  }
+}
+
+.performance-amount {
+  font-weight: bold;
+  color: #409EFF;
+  font-size: 15px;
+}
+
+.expand-projects {
+  padding: 16px 20px;
+  background: #F5F7FA;
+
+  &__title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #303133;
+    margin-bottom: 10px;
+  }
 }
 </style>