|
|
|
|
@ -0,0 +1,919 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="statistics-container">
|
|
|
|
|
<div class="dashboard-container">
|
|
|
|
|
<!-- 筛选条件区域 -->
|
|
|
|
|
<div class="filter-section">
|
|
|
|
|
<div class="row">
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
<label class="filter-label">时间周期</label>
|
|
|
|
|
<el-select v-model="filterForm.timeRange" placeholder="请选择时间周期" @change="handleTimeRangeChange" style="width: 100%">
|
|
|
|
|
<el-option label="全周期" value="all"></el-option>
|
|
|
|
|
<el-option label="今年" value="thisYear"></el-option>
|
|
|
|
|
<el-option label="去年" value="lastYear"></el-option>
|
|
|
|
|
<el-option label="自定义时间段" value="custom"></el-option>
|
|
|
|
|
</el-select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
<label class="filter-label">自定义时间</label>
|
|
|
|
|
<div class="d-flex gap-2">
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="filterForm.startDate"
|
|
|
|
|
type="date"
|
|
|
|
|
placeholder="开始日期"
|
|
|
|
|
:disabled="filterForm.timeRange !== 'custom'"
|
|
|
|
|
style="width: 100%">
|
|
|
|
|
</el-date-picker>
|
|
|
|
|
<el-date-picker
|
|
|
|
|
v-model="filterForm.endDate"
|
|
|
|
|
type="date"
|
|
|
|
|
placeholder="结束日期"
|
|
|
|
|
:disabled="filterForm.timeRange !== 'custom'"
|
|
|
|
|
style="width: 100%">
|
|
|
|
|
</el-date-picker>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="row mt-3">
|
|
|
|
|
<div class="col-12">
|
|
|
|
|
<label class="filter-label">课程体系</label>
|
|
|
|
|
<div class="course-checkboxes">
|
|
|
|
|
<el-checkbox
|
|
|
|
|
v-model="filterForm.courseAll"
|
|
|
|
|
@change="handleCourseAllChange"
|
|
|
|
|
style="font-weight: bold; color: #0f4c75;">
|
|
|
|
|
全选
|
|
|
|
|
</el-checkbox>
|
|
|
|
|
<el-divider></el-divider>
|
|
|
|
|
<el-checkbox-group v-model="filterForm.selectedCourses" @change="handleCourseChange">
|
|
|
|
|
<el-checkbox
|
|
|
|
|
v-for="course in courseOptions"
|
|
|
|
|
:key="course.value"
|
|
|
|
|
:label="course.value">
|
|
|
|
|
{{ course.label }}
|
|
|
|
|
</el-checkbox>
|
|
|
|
|
</el-checkbox-group>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="row mt-4">
|
|
|
|
|
<div class="col-12 text-center">
|
|
|
|
|
<el-button type="primary" @click="filterStudentData" class="btn-filter">
|
|
|
|
|
<i class="el-icon-search"></i> 查询统计
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 学员统计卡片 -->
|
|
|
|
|
<div class="stats-container">
|
|
|
|
|
<div class="stats-card" :class="stat.cardClass" v-for="(stat, index) in studentStats" :key="index">
|
|
|
|
|
<div class="stats-icon">
|
|
|
|
|
<i :class="stat.icon"></i>
|
|
|
|
|
</div>
|
|
|
|
|
<h3>{{ stat.value }}</h3>
|
|
|
|
|
<p>{{ stat.label }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 课程分类明细统计表格 -->
|
|
|
|
|
<el-row>
|
|
|
|
|
<el-col :span="24">
|
|
|
|
|
<div class="table-header">
|
|
|
|
|
<h5 class="table-title">
|
|
|
|
|
<i class="el-icon-reading"></i> 课程分类明细统计
|
|
|
|
|
</h5>
|
|
|
|
|
<el-button type="success" @click="exportCourseData" class="btn-export">
|
|
|
|
|
<i class="el-icon-download"></i> 导出数据
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-table">
|
|
|
|
|
<el-table :data="courseDetailData" :span-method="objectSpanMethod" :header-cell-style="headerCellStyle">
|
|
|
|
|
<el-table-column prop="courseSystem" label="课程体系" width="200" align="center"></el-table-column>
|
|
|
|
|
<el-table-column prop="totalPeople" label="培养人数(未去重)" width="200" align="center"></el-table-column>
|
|
|
|
|
<el-table-column prop="uniquePeople" label="培养人数(课程体系内已去重)" width="280" align="center"></el-table-column>
|
|
|
|
|
<el-table-column prop="courseName" label="开课" min-width="200"></el-table-column>
|
|
|
|
|
<el-table-column prop="coursePeople" label="课程培养人数" width="150" align="center"></el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<!-- 区域明细统计表格 -->
|
|
|
|
|
<el-row style="margin-top: 30px;">
|
|
|
|
|
<el-col :span="24">
|
|
|
|
|
<div class="table-header">
|
|
|
|
|
<h5 class="table-title">
|
|
|
|
|
<i class="el-icon-location"></i> 区域明细统计
|
|
|
|
|
</h5>
|
|
|
|
|
<el-button type="success" @click="exportRegionData" class="btn-export">
|
|
|
|
|
<i class="el-icon-download"></i> 导出数据
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="detail-table">
|
|
|
|
|
<el-table :data="regionData" style="width: 100%" :header-cell-style="headerCellStyle">
|
|
|
|
|
<el-table-column prop="region" label="区域" width="200" align="center"></el-table-column>
|
|
|
|
|
<el-table-column prop="totalPeople" label="培养人数(未去重)" align="center"></el-table-column>
|
|
|
|
|
<el-table-column prop="uniquePeople" label="培养人数(已去重)" align="center"></el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import { index as courseTypeIndex } from '@/api/course/courseType.js'
|
|
|
|
|
import { courseChart } from '@/api/homeChart.js'
|
|
|
|
|
import * as XLSX from "xlsx";
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
name: 'Statistics',
|
|
|
|
|
components: {},
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
filterForm: {
|
|
|
|
|
timeRange: 'all',
|
|
|
|
|
startDate: '',
|
|
|
|
|
endDate: '',
|
|
|
|
|
courseAll: false,
|
|
|
|
|
selectedCourses: []
|
|
|
|
|
},
|
|
|
|
|
courseOptions: [],
|
|
|
|
|
courseTypeList: [], // 课程体系列表
|
|
|
|
|
studentStats: [
|
|
|
|
|
{
|
|
|
|
|
icon: 'el-icon-user-solid',
|
|
|
|
|
value: '1,247',
|
|
|
|
|
label: '报名人数(未去重)',
|
|
|
|
|
cardClass: 'student-card-1'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
icon: 'el-icon-s-check',
|
|
|
|
|
value: '1,156',
|
|
|
|
|
label: '培养人数(未去重)',
|
|
|
|
|
cardClass: 'student-card-2'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
icon: 'el-icon-s-custom',
|
|
|
|
|
value: '892',
|
|
|
|
|
label: '培养人数(已去重)',
|
|
|
|
|
cardClass: 'student-card-3'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
icon: 'el-icon-date',
|
|
|
|
|
value: '56',
|
|
|
|
|
label: '开课场次',
|
|
|
|
|
cardClass: 'student-card-4'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
icon: 'el-icon-c-scale-to-original',
|
|
|
|
|
value: '89',
|
|
|
|
|
label: '开课天数',
|
|
|
|
|
cardClass: 'student-card-5'
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
courseDetailData: [],
|
|
|
|
|
regionData: [
|
|
|
|
|
{ region: '吴中区', totalPeople: 125, uniquePeople: 98 },
|
|
|
|
|
{ region: '相城区', totalPeople: 82, uniquePeople: 65 },
|
|
|
|
|
{ region: '昆山市', totalPeople: 74, uniquePeople: 58 },
|
|
|
|
|
{ region: '太仓市', totalPeople: 74, uniquePeople: 62 },
|
|
|
|
|
{ region: '姑苏区', totalPeople: 65, uniquePeople: 52 }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
mounted() {
|
|
|
|
|
this.initDates()
|
|
|
|
|
this.getCourseTypeList()
|
|
|
|
|
// 默认加载全周期数据
|
|
|
|
|
this.getCourseChart()
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
async getCourseChart() {
|
|
|
|
|
try {
|
|
|
|
|
// 将选中的课程体系ID转换为逗号分隔的字符串
|
|
|
|
|
const courseTypeIds = this.filterForm.selectedCourses.length > 0
|
|
|
|
|
? this.filterForm.selectedCourses.join(',')
|
|
|
|
|
: ''
|
|
|
|
|
|
|
|
|
|
// 格式化日期参数
|
|
|
|
|
const startDate = this.formatDate(this.filterForm.startDate)
|
|
|
|
|
const endDate = this.formatDate(this.filterForm.endDate)
|
|
|
|
|
|
|
|
|
|
const res = await courseChart({
|
|
|
|
|
timeRange: this.filterForm.timeRange,
|
|
|
|
|
start_date: startDate,
|
|
|
|
|
end_date: endDate,
|
|
|
|
|
course_type_id: courseTypeIds
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
console.log('课程图表数据:', res)
|
|
|
|
|
console.log('日期参数:', { startDate, endDate })
|
|
|
|
|
|
|
|
|
|
if (res) {
|
|
|
|
|
// 处理返回的数据,更新统计卡片和表格
|
|
|
|
|
this.updateStatisticsData(res)
|
|
|
|
|
} else {
|
|
|
|
|
this.$message.error('获取课程图表数据失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取课程图表数据失败:', error)
|
|
|
|
|
this.$message.error('获取课程图表数据失败')
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 初始化日期
|
|
|
|
|
initDates() {
|
|
|
|
|
// 默认全周期,不设置具体日期
|
|
|
|
|
this.filterForm.startDate = ''
|
|
|
|
|
this.filterForm.endDate = ''
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 时间范围变化处理
|
|
|
|
|
handleTimeRangeChange(value) {
|
|
|
|
|
if (value === 'all') {
|
|
|
|
|
// 全周期:清空开始和结束日期
|
|
|
|
|
this.filterForm.startDate = ''
|
|
|
|
|
this.filterForm.endDate = ''
|
|
|
|
|
} else if (value === 'thisYear') {
|
|
|
|
|
// 今年:设置为今年第一天到今天
|
|
|
|
|
const today = new Date()
|
|
|
|
|
const thisYear = today.getFullYear()
|
|
|
|
|
this.filterForm.startDate = new Date(thisYear, 0, 1) // 今年1月1日
|
|
|
|
|
this.filterForm.endDate = today
|
|
|
|
|
} else if (value === 'lastYear') {
|
|
|
|
|
// 去年:设置为去年第一天到最后一天
|
|
|
|
|
const today = new Date()
|
|
|
|
|
const lastYear = today.getFullYear() - 1
|
|
|
|
|
this.filterForm.startDate = new Date(lastYear, 0, 1) // 去年1月1日
|
|
|
|
|
this.filterForm.endDate = new Date(lastYear, 11, 31) // 去年12月31日
|
|
|
|
|
} else if (value === 'custom') {
|
|
|
|
|
// 自定义:保持当前日期,让用户手动选择
|
|
|
|
|
// 如果当前没有设置日期,则设置为默认值
|
|
|
|
|
if (!this.filterForm.startDate || !this.filterForm.endDate) {
|
|
|
|
|
const today = new Date()
|
|
|
|
|
const thirtyDaysAgo = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000))
|
|
|
|
|
this.filterForm.startDate = thirtyDaysAgo
|
|
|
|
|
this.filterForm.endDate = today
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 格式化日期为YYYY-MM-DD格式
|
|
|
|
|
formatDate(date) {
|
|
|
|
|
if (!date) return ''
|
|
|
|
|
if (typeof date === 'string') return date
|
|
|
|
|
|
|
|
|
|
const year = date.getFullYear()
|
|
|
|
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
|
|
const day = String(date.getDate()).padStart(2, '0')
|
|
|
|
|
return `${year}-${month}-${day}`
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 全选课程处理
|
|
|
|
|
handleCourseAllChange(value) {
|
|
|
|
|
if (value) {
|
|
|
|
|
this.filterForm.selectedCourses = this.courseOptions.map(course => course.value)
|
|
|
|
|
} else {
|
|
|
|
|
this.filterForm.selectedCourses = []
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 课程选择变化处理
|
|
|
|
|
handleCourseChange(value) {
|
|
|
|
|
const allSelected = value.length === this.courseOptions.length
|
|
|
|
|
const noneSelected = value.length === 0
|
|
|
|
|
|
|
|
|
|
if (allSelected) {
|
|
|
|
|
this.filterForm.courseAll = true
|
|
|
|
|
} else if (noneSelected) {
|
|
|
|
|
this.filterForm.courseAll = false
|
|
|
|
|
} else {
|
|
|
|
|
this.filterForm.courseAll = false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 查询统计
|
|
|
|
|
filterStudentData() {
|
|
|
|
|
const params = {
|
|
|
|
|
timeRange: this.filterForm.timeRange,
|
|
|
|
|
startDate: this.filterForm.startDate,
|
|
|
|
|
endDate: this.filterForm.endDate,
|
|
|
|
|
selectedCourses: this.filterForm.selectedCourses
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('学员数据筛选:', params)
|
|
|
|
|
// 调用课程图表数据
|
|
|
|
|
this.getCourseChart()
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 导出课程分类明细数据
|
|
|
|
|
exportCourseData() {
|
|
|
|
|
if (!this.courseDetailData || this.courseDetailData.length === 0) {
|
|
|
|
|
this.$message.warning('暂无数据可导出')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 准备Excel数据
|
|
|
|
|
const excelData = [
|
|
|
|
|
['课程体系', '培养人数(未去重)', '培养人数(课程体系内已去重)', '开课', '课程培养人数']
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// 添加数据行,只导出合并后的数据(每行都显示完整信息)
|
|
|
|
|
this.courseDetailData.forEach(row => {
|
|
|
|
|
excelData.push([
|
|
|
|
|
row.courseSystem,
|
|
|
|
|
row.totalPeople,
|
|
|
|
|
row.uniquePeople,
|
|
|
|
|
row.courseName,
|
|
|
|
|
row.coursePeople
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 创建工作簿
|
|
|
|
|
const wb = XLSX.utils.book_new()
|
|
|
|
|
const ws = XLSX.utils.aoa_to_sheet(excelData)
|
|
|
|
|
|
|
|
|
|
// 设置列宽
|
|
|
|
|
const colWidths = [
|
|
|
|
|
{ wch: 20 }, // 课程体系
|
|
|
|
|
{ wch: 18 }, // 培养人数(未去重)
|
|
|
|
|
{ wch: 22 }, // 培养人数(课程体系内已去重)
|
|
|
|
|
{ wch: 30 }, // 开课
|
|
|
|
|
{ wch: 15 } // 课程培养人数
|
|
|
|
|
]
|
|
|
|
|
ws['!cols'] = colWidths
|
|
|
|
|
|
|
|
|
|
// 添加工作表到工作簿
|
|
|
|
|
XLSX.utils.book_append_sheet(wb, ws, '课程分类明细统计')
|
|
|
|
|
|
|
|
|
|
// 生成文件名
|
|
|
|
|
const timeRangeText = this.filterForm.timeRange === 'all' ? '全周期' :
|
|
|
|
|
this.filterForm.timeRange === 'thisYear' ? '今年' :
|
|
|
|
|
this.filterForm.timeRange === 'lastYear' ? '去年' : '自定义时间'
|
|
|
|
|
const fileName = `课程分类明细统计_${timeRangeText}_${new Date().toISOString().slice(0, 10)}.xlsx`
|
|
|
|
|
|
|
|
|
|
// 下载文件
|
|
|
|
|
XLSX.writeFile(wb, fileName)
|
|
|
|
|
|
|
|
|
|
this.$message.success('课程分类明细数据导出成功!')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('导出失败:', error)
|
|
|
|
|
this.$message.error('导出失败,请重试')
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 导出区域明细数据
|
|
|
|
|
exportRegionData() {
|
|
|
|
|
if (!this.regionData || this.regionData.length === 0) {
|
|
|
|
|
this.$message.warning('暂无数据可导出')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 准备Excel数据
|
|
|
|
|
const excelData = [
|
|
|
|
|
['区域', '培养人数(未去重)', '培养人数(已去重)']
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// 添加数据行
|
|
|
|
|
this.regionData.forEach(row => {
|
|
|
|
|
excelData.push([
|
|
|
|
|
row.region,
|
|
|
|
|
row.totalPeople,
|
|
|
|
|
row.uniquePeople
|
|
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 创建工作簿
|
|
|
|
|
const wb = XLSX.utils.book_new()
|
|
|
|
|
const ws = XLSX.utils.aoa_to_sheet(excelData)
|
|
|
|
|
|
|
|
|
|
// 设置列宽
|
|
|
|
|
const colWidths = [
|
|
|
|
|
{ wch: 20 }, // 区域
|
|
|
|
|
{ wch: 18 }, // 培养人数(未去重)
|
|
|
|
|
{ wch: 18 } // 培养人数(已去重)
|
|
|
|
|
]
|
|
|
|
|
ws['!cols'] = colWidths
|
|
|
|
|
|
|
|
|
|
// 添加工作表到工作簿
|
|
|
|
|
XLSX.utils.book_append_sheet(wb, ws, '区域明细统计')
|
|
|
|
|
|
|
|
|
|
// 生成文件名
|
|
|
|
|
const timeRangeText = this.filterForm.timeRange === 'all' ? '全周期' :
|
|
|
|
|
this.filterForm.timeRange === 'thisYear' ? '今年' :
|
|
|
|
|
this.filterForm.timeRange === 'lastYear' ? '去年' : '自定义时间'
|
|
|
|
|
const fileName = `区域明细统计_${timeRangeText}_${new Date().toISOString().slice(0, 10)}.xlsx`
|
|
|
|
|
|
|
|
|
|
// 下载文件
|
|
|
|
|
XLSX.writeFile(wb, fileName)
|
|
|
|
|
|
|
|
|
|
this.$message.success('区域明细数据导出成功!')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('导出失败:', error)
|
|
|
|
|
this.$message.error('导出失败,请重试')
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 更新统计数据
|
|
|
|
|
updateStatisticsData(data) {
|
|
|
|
|
// 根据API返回的数据更新统计卡片
|
|
|
|
|
if (data && data.list) {
|
|
|
|
|
// 更新报名人数(未去重)
|
|
|
|
|
this.studentStats[0].value = data.list.course_signs_total || '0'
|
|
|
|
|
|
|
|
|
|
// 更新培养人数(未去重)
|
|
|
|
|
this.studentStats[1].value = data.list.course_signs_pass || '0'
|
|
|
|
|
|
|
|
|
|
// 更新培养人数(已去重)
|
|
|
|
|
this.studentStats[2].value = data.list.course_signs_pass_unique || '0'
|
|
|
|
|
|
|
|
|
|
// 更新开课场次
|
|
|
|
|
this.studentStats[3].value = data.list.course_total || '0'
|
|
|
|
|
|
|
|
|
|
// 更新开课天数
|
|
|
|
|
this.studentStats[4].value = data.list.course_day_total || '0'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新课程分类明细统计表格
|
|
|
|
|
if (data && data.courseTypesSum && Array.isArray(data.courseTypesSum)) {
|
|
|
|
|
// 对数据进行分组和合并处理
|
|
|
|
|
const groupedData = {}
|
|
|
|
|
|
|
|
|
|
data.courseTypesSum.forEach(item => {
|
|
|
|
|
const courseType = item.course_type
|
|
|
|
|
if (!groupedData[courseType]) {
|
|
|
|
|
// 初始化该课程体系的数据
|
|
|
|
|
groupedData[courseType] = {
|
|
|
|
|
courseSystem: courseType,
|
|
|
|
|
totalPeople: item.course_type_signs_pass || 0,
|
|
|
|
|
uniquePeople: item.course_type_signs_pass_unique || 0,
|
|
|
|
|
courses: []
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 添加课程信息
|
|
|
|
|
groupedData[courseType].courses.push({
|
|
|
|
|
courseName: item.course_name || '',
|
|
|
|
|
coursePeople: item.course_signs_pass || 0
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 转换为表格需要的格式
|
|
|
|
|
this.courseDetailData = []
|
|
|
|
|
Object.values(groupedData).forEach(group => {
|
|
|
|
|
// 为每个课程体系创建多行数据
|
|
|
|
|
group.courses.forEach((course, index) => {
|
|
|
|
|
this.courseDetailData.push({
|
|
|
|
|
courseSystem: group.courseSystem,
|
|
|
|
|
totalPeople: group.totalPeople,
|
|
|
|
|
uniquePeople: group.uniquePeople,
|
|
|
|
|
courseName: course.courseName,
|
|
|
|
|
coursePeople: course.coursePeople,
|
|
|
|
|
isFirstRow: index === 0 // 标记是否为该课程体系的第一行
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新区域明细统计表格
|
|
|
|
|
if (data && data.areas && Array.isArray(data.areas)) {
|
|
|
|
|
this.regionData = data.areas.map(item => ({
|
|
|
|
|
region: item.value || '',
|
|
|
|
|
totalPeople: item.course_signs_pass || 0,
|
|
|
|
|
uniquePeople: item.course_signs_pass_unique || 0
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('统计数据已更新:', this.studentStats)
|
|
|
|
|
console.log('课程分类明细数据已更新:', this.courseDetailData)
|
|
|
|
|
console.log('区域明细数据已更新:', this.regionData)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 获取课程体系列表
|
|
|
|
|
async getCourseTypeList() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await courseTypeIndex({
|
|
|
|
|
page: 1,
|
|
|
|
|
page_size: 999
|
|
|
|
|
})
|
|
|
|
|
if (res && res.data) {
|
|
|
|
|
this.courseTypeList = res.data
|
|
|
|
|
this.courseOptions = this.courseTypeList.map(item => ({
|
|
|
|
|
label: item.name,
|
|
|
|
|
value: item.id
|
|
|
|
|
}))
|
|
|
|
|
} else {
|
|
|
|
|
this.$message.error('获取课程体系列表失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取课程体系列表失败:', error)
|
|
|
|
|
this.$message.error('获取课程体系列表失败')
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 表格合并行处理
|
|
|
|
|
objectSpanMethod({ row, column, rowIndex, columnIndex }) {
|
|
|
|
|
// 只对前3列进行合并:课程体系、培养人数(未去重)、培养人数(课程体系内已去重)
|
|
|
|
|
if (columnIndex === 0 || columnIndex === 1 || columnIndex === 2) {
|
|
|
|
|
// 计算当前行所属的课程体系
|
|
|
|
|
let currentCourseSystem = this.courseDetailData[rowIndex].courseSystem
|
|
|
|
|
|
|
|
|
|
// 计算该课程体系在表格中占用的行数
|
|
|
|
|
let rowspan = 1
|
|
|
|
|
let colspan = 1
|
|
|
|
|
|
|
|
|
|
// 向前查找相同课程体系的行数
|
|
|
|
|
for (let i = rowIndex - 1; i >= 0; i--) {
|
|
|
|
|
if (this.courseDetailData[i].courseSystem === currentCourseSystem) {
|
|
|
|
|
rowspan++
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 向后查找相同课程体系的行数
|
|
|
|
|
for (let i = rowIndex + 1; i < this.courseDetailData.length; i++) {
|
|
|
|
|
if (this.courseDetailData[i].courseSystem === currentCourseSystem) {
|
|
|
|
|
rowspan++
|
|
|
|
|
} else {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果是该课程体系的第一行,则合并显示
|
|
|
|
|
if (rowIndex === 0 || this.courseDetailData[rowIndex - 1].courseSystem !== currentCourseSystem) {
|
|
|
|
|
return { rowspan: rowspan, colspan: 1 }
|
|
|
|
|
} else {
|
|
|
|
|
// 不是第一行,则隐藏
|
|
|
|
|
return { rowspan: 0, colspan: 0 }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 其他列不合并
|
|
|
|
|
return { rowspan: 1, colspan: 1 }
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// 设置表头背景样式
|
|
|
|
|
headerCellStyle({ row, column, rowIndex, columnIndex }) {
|
|
|
|
|
return {
|
|
|
|
|
background: 'linear-gradient(135deg, #0f4c75 0%, #1e3c72 100%)',
|
|
|
|
|
color: 'white',
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
fontSize: '0.99rem',
|
|
|
|
|
textAlign: 'center'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
.statistics-container {
|
|
|
|
|
// background: linear-gradient(135deg, #0f4c75 0%, #1e3c72 50%, #2a5298 100%);
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
font-family: 'Microsoft YaHei', sans-serif;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.dashboard-container {
|
|
|
|
|
background: rgba(255, 255, 255, 0.98);
|
|
|
|
|
border-radius: 25px;
|
|
|
|
|
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
|
|
|
|
|
padding: 35px;
|
|
|
|
|
backdrop-filter: blur(10px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.main-title {
|
|
|
|
|
background: linear-gradient(135deg, #0f4c75 0%, #00a8ff 100%);
|
|
|
|
|
-webkit-background-clip: text;
|
|
|
|
|
-webkit-text-fill-color: transparent;
|
|
|
|
|
background-clip: text;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
font-size: 2.25rem;
|
|
|
|
|
margin-bottom: 40px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
|
|
|
|
i {
|
|
|
|
|
background: linear-gradient(135deg, #00a8ff 0%, #0097e6 100%);
|
|
|
|
|
-webkit-background-clip: text;
|
|
|
|
|
-webkit-text-fill-color: transparent;
|
|
|
|
|
background-clip: text;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-section {
|
|
|
|
|
background: linear-gradient(135deg, #f8f9fa 0%, #e3f2fd 100%);
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
padding: 25px;
|
|
|
|
|
margin-bottom: 35px;
|
|
|
|
|
border: 1px solid #e8f4fd;
|
|
|
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.08);
|
|
|
|
|
|
|
|
|
|
.row {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
margin: 0 -10px;
|
|
|
|
|
|
|
|
|
|
.col-md-6, .col-12 {
|
|
|
|
|
padding: 0 10px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.col-md-6 {
|
|
|
|
|
flex: 0 0 50%;
|
|
|
|
|
max-width: 50%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.col-12 {
|
|
|
|
|
flex: 0 0 100%;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mt-3 {
|
|
|
|
|
margin-top: 15px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mt-4 {
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.text-center {
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.d-flex {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-label {
|
|
|
|
|
color: #0f4c75;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
display: block;
|
|
|
|
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.course-checkboxes {
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
padding: 15px;
|
|
|
|
|
border: 1px solid #e9ecef;
|
|
|
|
|
|
|
|
|
|
.el-checkbox {
|
|
|
|
|
margin-right: 20px;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stats-container {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 20px;
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
padding: 0 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stats-card {
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
padding: 28px 20px;
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
|
|
|
|
transition: all 0.4s ease;
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-align: center;
|
|
|
|
|
height: 200px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
max-width: calc(20% - 16px);
|
|
|
|
|
|
|
|
|
|
&::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
height: 4px;
|
|
|
|
|
background: linear-gradient(90deg, #00a8ff, #0097e6);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: translateY(-8px);
|
|
|
|
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h3 {
|
|
|
|
|
font-size: 2.24rem;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p {
|
|
|
|
|
font-size: 1.08rem;
|
|
|
|
|
opacity: 0.95;
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
margin-bottom:15px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stats-icon {
|
|
|
|
|
font-size: 2.8rem;
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 学员维度卡片配色 */
|
|
|
|
|
.student-card-1 {
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.student-card-2 {
|
|
|
|
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.student-card-3 {
|
|
|
|
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.student-card-4 {
|
|
|
|
|
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.student-card-5 {
|
|
|
|
|
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-title {
|
|
|
|
|
color: #0f4c75;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: 1.17rem;
|
|
|
|
|
|
|
|
|
|
i {
|
|
|
|
|
color: #00a8ff;
|
|
|
|
|
margin-right: 10px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-table {
|
|
|
|
|
background: white;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
|
|
|
|
border: 1px solid #e8f4fd;
|
|
|
|
|
|
|
|
|
|
.el-table {
|
|
|
|
|
.el-table__header-wrapper {
|
|
|
|
|
.el-table__header {
|
|
|
|
|
th {
|
|
|
|
|
background: linear-gradient(135deg, #0f4c75 0%, #1e3c72 100%) !important;
|
|
|
|
|
color: white !important;
|
|
|
|
|
border: none !important;
|
|
|
|
|
font-weight: 600 !important;
|
|
|
|
|
padding: 18px 15px !important;
|
|
|
|
|
font-size: 0.99rem !important;
|
|
|
|
|
text-align: center !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-table__body-wrapper {
|
|
|
|
|
.el-table__body {
|
|
|
|
|
td {
|
|
|
|
|
vertical-align: middle !important;
|
|
|
|
|
border-color: #f1f5f9 !important;
|
|
|
|
|
padding: 15px !important;
|
|
|
|
|
color: #2c3e50 !important;
|
|
|
|
|
font-weight: 500 !important;
|
|
|
|
|
font-size: 0.9rem !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tr:hover {
|
|
|
|
|
background: linear-gradient(135deg, #f8f9fa 0%, #e3f2fd 100%) !important;
|
|
|
|
|
transform: scale(1.01);
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 强制覆盖Element UI的默认表头样式 */
|
|
|
|
|
.detail-table .el-table th.el-table__cell {
|
|
|
|
|
background: linear-gradient(135deg, #0f4c75 0%, #1e3c72 100%) !important;
|
|
|
|
|
color: white !important;
|
|
|
|
|
border: none !important;
|
|
|
|
|
font-weight: 600 !important;
|
|
|
|
|
padding: 18px 15px !important;
|
|
|
|
|
font-size: 0.99rem !important;
|
|
|
|
|
text-align: center !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-table .el-table td.el-table__cell {
|
|
|
|
|
vertical-align: middle !important;
|
|
|
|
|
border-color: #f1f5f9 !important;
|
|
|
|
|
padding: 15px !important;
|
|
|
|
|
color: #2c3e50 !important;
|
|
|
|
|
font-weight: 500 !important;
|
|
|
|
|
font-size: 0.9rem !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-table .el-table tbody tr:hover > td.el-table__cell {
|
|
|
|
|
background: linear-gradient(135deg, #f8f9fa 0%, #e3f2fd 100%) !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-filter {
|
|
|
|
|
background: linear-gradient(135deg, #00a8ff 0%, #0097e6 100%);
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 30px;
|
|
|
|
|
padding: 12px 30px;
|
|
|
|
|
color: white;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 0.99rem;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
box-shadow: 0 5px 15px rgba(0, 168, 255, 0.3);
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: translateY(-3px);
|
|
|
|
|
box-shadow: 0 8px 25px rgba(0, 168, 255, 0.4);
|
|
|
|
|
background: linear-gradient(135deg, #0097e6 0%, #0088d4 100%);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-export {
|
|
|
|
|
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
padding: 8px 20px;
|
|
|
|
|
color: white;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 0.9rem;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
box-shadow: 0 3px 10px rgba(40, 167, 69, 0.3);
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
box-shadow: 0 5px 15px rgba(40, 167, 69, 0.4);
|
|
|
|
|
background: linear-gradient(135deg, #20c997 0%, #17a2b8 100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i {
|
|
|
|
|
margin-right: 6px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 响应式布局 */
|
|
|
|
|
@media (max-width: 767.98px) {
|
|
|
|
|
.stats-container {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stats-card {
|
|
|
|
|
width: 100%;
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.table-header {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
|
|
|
|
.btn-export {
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|