Răsfoiți Sursa

feat: #12 项目查询统计页面 - 决策看板

多维度查询+统计卡片+可视化图表完整决策支持页面

- 6维筛选: 项目名/部门/状态多选/类型/产值范围联合查询
- 6张统计卡片: 项目总数/在建/完工/总预估产值/总实际产值/项目均产值
- 3个图表: 柱状图(部门产值对比)/饼图(状态分布)/折线图(月度趋势多系列)
- 项目明细表: 名称/类型/部门/状态/产值+分页+Excel导出
- 统计入口: project/index.vue按钮路由跳转至独立stats页面
- 清理index.vue内联统计dialog死代码

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
shenzx 1 lună în urmă
părinte
comite
63ba6b9cb6

+ 14 - 0
src/router/index.js

@@ -223,6 +223,20 @@ export const performanceRoutes = [
         meta: { title: '绩效核算详情', activeMenu: '/performance/calculate' }
       }
     ]
+  },
+  {
+    path: '/performance/project-stats',
+    component: Layout,
+    hidden: true,
+    permissions: ['performance:project:stats'],
+    children: [
+      {
+        path: 'index',
+        component: () => import('@/views/performance/project/stats'),
+        name: 'ProjectStats',
+        meta: { title: '项目查询统计', activeMenu: '/performance/project' }
+      }
+    ]
   }
 ]
 

+ 3 - 121
src/views/performance/project/index.vue

@@ -55,34 +55,6 @@
       </right-toolbar>
     </el-row>
 
-    <!-- 统计卡片 -->
-    <el-row :gutter="16" class="mb8" v-if="showStats">
-      <el-col :span="6">
-        <el-card shadow="hover" class="stats-card">
-          <div class="stats-card__value">{{ stats.totalProjects }}</div>
-          <div class="stats-card__label">项目总数</div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover" class="stats-card stats-card--blue">
-          <div class="stats-card__value">{{ stats.totalEstimatedOutput }}</div>
-          <div class="stats-card__label">总预估产值(万元)</div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover" class="stats-card stats-card--green">
-          <div class="stats-card__value">{{ stats.totalActualOutput }}</div>
-          <div class="stats-card__label">总实际产值(万元)</div>
-        </el-card>
-      </el-col>
-      <el-col :span="6">
-        <el-card shadow="hover" class="stats-card stats-card--orange">
-          <div class="stats-card__value">{{ stats.completedProjects }}</div>
-          <div class="stats-card__label">完工项目数</div>
-        </el-card>
-      </el-col>
-    </el-row>
-
     <!-- 表格视图 -->
     <el-table v-if="viewMode === 'table'" v-loading="loading" :data="projectList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="50" align="center" />
@@ -268,28 +240,12 @@
 
     <!-- 导入对话框 -->
     <ExcelImportDialog ref="importRef" title="项目导入" :action="importAction" :templateAction="importTemplateAction" templateFileName="project_template" @success="getList" />
-
-    <!-- 统计对话框 -->
-    <el-dialog title="项目统计" v-model="statsOpen" width="900px" append-to-body>
-      <el-row :gutter="16" class="mb8">
-        <el-col :span="6" v-for="card in statsCards" :key="card.key">
-          <el-card shadow="hover">
-            <div class="stats-dialog-card">
-              <div class="stats-dialog-card__value">{{ card.value }}</div>
-              <div class="stats-dialog-card__label">{{ card.label }}</div>
-            </div>
-          </el-card>
-        </el-col>
-      </el-row>
-      <PerformanceChart type="bar" :xData="statsChartData.deptNames" :seriesData="statsChartData.series" title="各部门项目数量与产值对比" yName="数量/万元" height="300" />
-      <PerformanceChart type="pie" :xData="statsChartData.statusNames" :seriesData="statsChartData.statusSeries" title="项目状态分布" height="300" />
-    </el-dialog>
   </div>
 </template>
 
 <script setup name="PerformanceProject">
-import { listProject, getProject, addProject, updateProject, delProject, deptTreeSelect, changeProjectStatus, getProjectStats } from '@/api/performance/project'
-import { ProjectCard, ReviewStatusTag, PerformanceChart, DeptSelector } from '@/components/performance'
+import { listProject, getProject, addProject, updateProject, delProject, deptTreeSelect, changeProjectStatus } from '@/api/performance/project'
+import { ProjectCard, ReviewStatusTag, DeptSelector } from '@/components/performance'
 import ExcelImportDialog from '@/components/ExcelImportDialog'
 
 const { proxy } = getCurrentInstance()
@@ -369,8 +325,6 @@ const total = ref(0)
 const title = ref('')
 const deptOptions = ref([])
 const viewMode = ref('table')
-const showStats = ref(false)
-const statsOpen = ref(false)
 
 const columns = ref({
   projectName: { label: '项目名称', visible: true },
@@ -383,20 +337,6 @@ const columns = ref({
   status: { label: '项目状态', visible: true }
 })
 
-const stats = reactive({
-  totalProjects: 0,
-  totalEstimatedOutput: '--',
-  totalActualOutput: '--',
-  completedProjects: 0
-})
-
-const statsChartData = reactive({
-  deptNames: [],
-  series: [{ name: '项目数', data: [] }, { name: '产值(万元)', data: [] }],
-  statusNames: [],
-  statusSeries: [{ name: '项目数', data: [] }]
-})
-
 const data = reactive({
   form: {
     projectId: undefined,
@@ -445,13 +385,6 @@ const computedRiskOutput = computed(() => {
 const importAction = '/performance/project/importData'
 const importTemplateAction = '/performance/project/importTemplate'
 
-const statsCards = computed(() => [
-  { key: 'total', label: '项目总数', value: stats.totalProjects },
-  { key: 'estimated', label: '总预估产值(万元)', value: stats.totalEstimatedOutput },
-  { key: 'actual', label: '总实际产值(万元)', value: stats.totalActualOutput },
-  { key: 'completed', label: '完工项目数', value: stats.completedProjects }
-])
-
 // 数据加载
 function getList() {
   loading.value = true
@@ -468,23 +401,6 @@ function getDeptTree() {
   })
 }
 
-function getStats() {
-  getProjectStats(queryParams.value).then(res => {
-    const d = res.data || {}
-    stats.totalProjects = d.totalProjects || 0
-    stats.totalEstimatedOutput = d.totalEstimatedOutput || '--'
-    stats.totalActualOutput = d.totalActualOutput || '--'
-    stats.completedProjects = d.completedProjects || 0
-    statsChartData.deptNames = d.deptNames || []
-    statsChartData.series = [
-      { name: '项目数', data: d.deptProjectCounts || [], color: '#409EFF' },
-      { name: '产值(万元)', data: d.deptOutputValues || [], color: '#67C23A' }
-    ]
-    statsChartData.statusNames = d.statusNames || []
-    statsChartData.statusSeries = [{ name: '项目数', data: d.statusCounts || [] }]
-  })
-}
-
 // 搜索与重置
 function handleQuery() {
   queryParams.value.pageNum = 1
@@ -590,11 +506,7 @@ function handleExport() {
 }
 
 function handleStats() {
-  showStats.value = !showStats.value
-  if (showStats.value) {
-    getStats()
-    statsOpen.value = true
-  }
+  router.push('/performance/project-stats/index')
 }
 
 function submitForm() {
@@ -635,36 +547,6 @@ onMounted(() => {
   margin-bottom: 16px;
 }
 
-.stats-card {
-  text-align: center;
-  &__value {
-    font-size: 28px;
-    font-weight: bold;
-    margin-bottom: 4px;
-  }
-  &__label {
-    font-size: 13px;
-    color: #909399;
-  }
-  &--blue .stats-card__value { color: #409EFF; }
-  &--green .stats-card__value { color: #67C23A; }
-  &--orange .stats-card__value { color: #E6A23C; }
-}
-
-.stats-dialog-card {
-  text-align: center;
-  &__value {
-    font-size: 24px;
-    font-weight: bold;
-    color: #409EFF;
-  }
-  &__label {
-    font-size: 13px;
-    color: #909399;
-    margin-top: 4px;
-  }
-}
-
 .computed-output {
   font-size: 16px;
   font-weight: bold;

+ 297 - 0
src/views/performance/project/stats.vue

@@ -0,0 +1,297 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索区域 -->
+    <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="80px" class="search-form">
+      <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="deptId">
+        <DeptSelector v-model="queryParams.deptId" :useTree="false" placeholder="请选择部门" width="200px" />
+      </el-form-item>
+      <el-form-item label="项目状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="项目状态" clearable multiple collapse-tags style="width: 200px">
+          <el-option v-for="dict in projectStatusOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="项目类型" prop="projectType">
+        <el-select v-model="queryParams.projectType" placeholder="项目类型" clearable style="width: 150px">
+          <el-option v-for="dict in projectTypeOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="产值范围" prop="outputRange">
+        <el-input-number v-model="queryParams.outputMin" :precision="2" :min="0" placeholder="最小" style="width: 120px" />
+        <span style="margin: 0 8px">—</span>
+        <el-input-number v-model="queryParams.outputMax" :precision="2" :min="0" placeholder="最大" style="width: 120px" />
+        <span style="margin-left: 4px; color: #909399">万元</span>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 统计卡片 -->
+    <el-row :gutter="16" class="mb16">
+      <el-col :span="4" v-for="(card, idx) in statsCards" :key="card.key">
+        <el-card shadow="hover" class="stats-card" :class="`stats-card--color${idx}`">
+          <div class="stats-card__icon">{{ card.icon }}</div>
+          <div class="stats-card__value">{{ card.value }}</div>
+          <div class="stats-card__label">{{ card.label }}</div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 图表区域 -->
+    <el-row :gutter="20" class="mb16">
+      <el-col :span="16">
+        <el-card shadow="hover">
+          <PerformanceChart
+            type="bar"
+            :xData="barChart.deptNames"
+            :seriesData="barChart.series"
+            title="各部门产值与项目统计"
+            yName="数量/万元"
+            :height="340"
+          />
+        </el-card>
+      </el-col>
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <PerformanceChart
+            type="pie"
+            :xData="pieChart.labels"
+            :seriesData="pieChart.series"
+            title="项目状态分布"
+            :height="340"
+          />
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="20" class="mb16">
+      <el-col :span="24">
+        <el-card shadow="hover">
+          <PerformanceChart
+            type="line"
+            :xData="lineChart.months"
+            :seriesData="lineChart.series"
+            title="部门产值月度趋势"
+            yName="万元"
+            :height="320"
+            :smooth="true"
+          />
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 工具栏 + 项目明细表格 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-tag type="info" effect="plain">项目明细 ({{ total }})</el-tag>
+      </el-col>
+      <el-col :span="1.5" :offset="18">
+        <el-button type="warning" plain icon="Download" size="small" @click="handleExport">导出Excel</el-button>
+      </el-col>
+    </el-row>
+
+    <el-table v-loading="loading" :data="projectList" border stripe>
+      <el-table-column label="项目名称" align="center" prop="projectName" :show-overflow-tooltip="true" min-width="180" />
+      <el-table-column label="项目类型" align="center" width="90">
+        <template #default="scope">{{ typeMap[scope.row.projectType] || '--' }}</template>
+      </el-table-column>
+      <el-table-column label="牵头部门" align="center" prop="deptName" width="140" />
+      <el-table-column label="项目状态" align="center" width="140">
+        <template #default="scope">
+          <ReviewStatusTag :status="scope.row.status" :options="statusTagOptions" />
+        </template>
+      </el-table-column>
+      <el-table-column label="合同额(万元)" align="center" prop="contractAmount" width="120" />
+      <el-table-column label="预估产值(万元)" align="center" prop="estimatedOutput" width="120" />
+      <el-table-column label="实际产值(万元)" align="center" prop="actualOutput" width="120">
+        <template #default="scope">
+          <span :style="{ fontWeight: 'bold', color: (scope.row.actualOutput || 0) > 0 ? '#67C23A' : '#909399' }">
+            {{ scope.row.actualOutput || '--' }}
+          </span>
+        </template>
+      </el-table-column>
+      <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="handleQuery" />
+  </div>
+</template>
+
+<script setup name="ProjectStats">
+import { listProject, getProjectStats } from '@/api/performance/project'
+import { PerformanceChart, ReviewStatusTag, DeptSelector } from '@/components/performance'
+
+const { proxy } = getCurrentInstance()
+
+const projectStatusOptions = [
+  { label: '已完成招投标在建', value: 'BIDDING_IN_PROGRESS' },
+  { label: '前期策划但未开工', value: 'PLANNING_NOT_STARTED' },
+  { label: '前期策划并且同步开工', value: 'PLANNING_IN_PROGRESS' },
+  { label: '完工', value: 'COMPLETED' },
+  { label: '作废', value: 'CANCELLED' }
+]
+
+const statusTagOptions = [
+  { label: '已完成招投标在建', value: 'BIDDING_IN_PROGRESS', tagType: 'warning' },
+  { label: '前期策划但未开工', value: 'PLANNING_NOT_STARTED', tagType: 'info' },
+  { label: '前期策划并且同步开工', value: 'PLANNING_IN_PROGRESS', tagType: '' },
+  { label: '完工', value: 'COMPLETED', tagType: 'success' },
+  { label: '作废', value: 'CANCELLED', tagType: 'danger' }
+]
+
+const projectTypeOptions = [
+  { label: '工程项目', value: 'ENGINEERING' },
+  { label: '技术服务', value: 'TECHNICAL_SERVICE' },
+  { label: '咨询服务', value: 'CONSULTING' },
+  { label: '其他', value: 'OTHER' }
+]
+
+const typeMap = { 'ENGINEERING': '工程项目', 'TECHNICAL_SERVICE': '技术服务', 'CONSULTING': '咨询服务', 'OTHER': '其他' }
+
+const iconMap = ['🏢', '📊', '✅', '💰', '📈', '🎯']
+
+const projectList = ref([])
+const loading = ref(false)
+const total = ref(0)
+
+const statsCards = ref([
+  { key: 'totalProjects', icon: '📋', label: '项目总数', value: '--' },
+  { key: 'inProgressProjects', icon: '🔨', label: '在建项目', value: '--' },
+  { key: 'completedProjects', icon: '✅', label: '完工项目', value: '--' },
+  { key: 'totalEstimatedOutput', icon: '📊', label: '总预估产值(万元)', value: '--' },
+  { key: 'totalActualOutput', icon: '💰', label: '总实际产值(万元)', value: '--' },
+  { key: 'avgOutput', icon: '📈', label: '项目均产值(万元)', value: '--' }
+])
+
+const barChart = reactive({
+  deptNames: [],
+  series: [{ name: '项目数', data: [], color: '#409EFF' }, { name: '产值(万元)', data: [], color: '#67C23A' }]
+})
+
+const pieChart = reactive({
+  labels: [],
+  series: [{ name: '项目数', data: [] }]
+})
+
+const lineChart = reactive({
+  months: [],
+  series: []
+})
+
+const data = reactive({
+  queryParams: {
+    pageNum: 1,
+    pageSize: 15,
+    projectName: undefined,
+    deptId: undefined,
+    status: undefined,
+    projectType: undefined,
+    outputMin: undefined,
+    outputMax: undefined
+  }
+})
+
+const { queryParams } = toRefs(data)
+
+function handleQuery() {
+  loading.value = true
+  const params = { ...queryParams.value }
+  // 处理多选状态转逗号分隔
+  if (Array.isArray(params.status)) {
+    params.status = params.status.join(',')
+  }
+
+  listProject(params).then(res => {
+    projectList.value = res.rows || []
+    total.value = res.total || 0
+    loading.value = false
+  })
+
+  getProjectStats(params).then(res => {
+    const d = res.data || {}
+    statsCards.value = [
+      { key: 'totalProjects', icon: '📋', label: '项目总数', value: d.totalProjects || 0 },
+      { key: 'inProgressProjects', icon: '🔨', label: '在建项目', value: d.inProgressProjects || 0 },
+      { key: 'completedProjects', icon: '✅', label: '完工项目', value: d.completedProjects || 0 },
+      { key: 'totalEstimatedOutput', icon: '📊', label: '总预估产值(万元)', value: d.totalEstimatedOutput || '--' },
+      { key: 'totalActualOutput', icon: '💰', label: '总实际产值(万元)', value: d.totalActualOutput || '--' },
+      { key: 'avgOutput', icon: '📈', label: '项目均产值(万元)', value: d.avgOutput || '--' }
+    ]
+
+    barChart.deptNames = d.deptNames || []
+    barChart.series = [
+      { name: '项目数', data: d.deptProjectCounts || [], color: '#409EFF' },
+      { name: '产值(万元)', data: d.deptOutputValues || [], color: '#67C23A' }
+    ]
+
+    pieChart.labels = d.statusNames || []
+    pieChart.series = [{ name: '项目数', data: d.statusCounts || [] }]
+
+    lineChart.months = d.trendMonths || []
+    lineChart.series = (d.trendSeries || []).map(s => ({
+      name: s.deptName,
+      data: s.data || [],
+      smooth: true
+    }))
+  })
+}
+
+function resetQuery() {
+  proxy.resetForm('queryRef')
+  handleQuery()
+}
+
+function handleExport() {
+  proxy.download('/performance/project/export', { ...queryParams.value }, `project_stats_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+  handleQuery()
+})
+</script>
+
+<style lang="scss" scoped>
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+
+.mb16 {
+  margin-bottom: 16px;
+}
+
+.stats-card {
+  text-align: center;
+  border-top: 3px solid #DCDFE6;
+
+  &--color0 { border-top-color: #409EFF; }
+  &--color1 { border-top-color: #E6A23C; }
+  &--color2 { border-top-color: #67C23A; }
+  &--color3 { border-top-color: #909399; }
+  &--color4 { border-top-color: #F56C6C; }
+  &--color5 { border-top-color: #409EFF; }
+
+  &__icon {
+    font-size: 24px;
+  }
+
+  &__value {
+    font-size: 22px;
+    font-weight: bold;
+    color: #303133;
+    margin: 6px 0;
+  }
+
+  &__label {
+    font-size: 12px;
+    color: #909399;
+  }
+}
+</style>