|
|
@@ -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>
|