新增分析:实现管理驾驶舱聚合接口与可视化
This commit is contained in:
1 parent
992791b7bd
commit
f17526b409
23 files changed
+3077
-23
No files matched your search
@@ -0,0 +1,43 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取管理驾驶舱宏观概览
|
||||
export function getDashboardOverview() {
|
||||
return request({
|
||||
url: '/train/analysis/dashboard/overview',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取管理驾驶舱图表数据
|
||||
export function getDashboardCharts() {
|
||||
return request({
|
||||
url: '/train/analysis/dashboard/charts',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取考勤专题分析汇总 KPI
|
||||
export function getAttendanceSummary(params) {
|
||||
return request({
|
||||
url: '/train/analysis/attendance/summary',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取考勤专题分析图表集合
|
||||
export function getAttendanceCharts(params) {
|
||||
return request({
|
||||
url: '/train/analysis/attendance/charts',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取指定学员 360 客观全景画像
|
||||
export function getStudent360(studentId) {
|
||||
return request({
|
||||
url: '/train/analysis/portrait/student-360/' + studentId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -4,29 +4,7 @@
|
||||
<div v-if="loadingRoles" v-loading="true" style="height: 300px;" />
|
||||
|
||||
<!-- 1. 管理员优先展示管理端综合驾驶舱 (最高优先级: admin / train_admin) -->
|
||||
<div v-else-if="isAdmin" class="app-container home">
|
||||
<el-row :gutter="20">
|
||||
<el-col :sm="24" :lg="14" style="padding-left: 20px;">
|
||||
<el-card class="box-card">
|
||||
<div slot="header" class="clearfix">
|
||||
<span style="font-weight: bold; font-size: 16px;">SYFJGYD学员管理系统</span>
|
||||
</div>
|
||||
<div class="text item" style="margin-bottom: 16px; font-size: 14px; color: #606266; line-height: 1.8;">
|
||||
<p>欢迎使用SYFJGYD学员管理系统。本平台提供培训计划编排、学员主档与参训管理、智能排课排程、电子班牌联动、教学设施与实训场地调度,以及全流程多维学习数据分析等核心功能。</p>
|
||||
</div>
|
||||
<div class="text item" style="margin-bottom: 20px; text-align: center">
|
||||
<img src="@/assets/images/version.png" alt="系统架构" style="max-width: 100%; border-radius: 4px;">
|
||||
</div>
|
||||
<el-divider style="margin: 15px 0;" />
|
||||
<div class="text item" style="display: flex; justify-content: space-between; color: #909399; font-size: 13px;">
|
||||
<span>系统版本:V1.0</span>
|
||||
<span>版权所有:中国科学院沈阳计算技术研究所有限公司</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-divider />
|
||||
</div>
|
||||
<admin-dashboard v-else-if="isAdmin" />
|
||||
|
||||
<!-- 2. 异常检测:同时拥有教师与学员互斥角色,展示受控安全告警 -->
|
||||
<div v-else-if="hasRoleConflict" class="app-container">
|
||||
@@ -59,12 +37,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AdminDashboard from '@/views/train/analysis/dashboard/index'
|
||||
import TeacherDashboard from '@/views/train/me/teacher/index'
|
||||
import StudentDashboard from '@/views/train/me/student/index'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: {
|
||||
AdminDashboard,
|
||||
TeacherDashboard,
|
||||
StudentDashboard
|
||||
},
|
||||
|
||||
@@ -0,0 +1,655 @@
|
||||
<template>
|
||||
<div class="dashboard-editor-container">
|
||||
<!-- 1. 顶部标题与数据更新状态 -->
|
||||
<div class="dashboard-header">
|
||||
<div class="header-title">
|
||||
<h2>SYFJGYD学员管理系统 · 管理驾驶舱</h2>
|
||||
<span class="subtitle">全校培训教学运行、排课调度、学员考勤与生活保障综合宏观指挥大盘</span>
|
||||
</div>
|
||||
<div class="header-action">
|
||||
<el-button icon="el-icon-refresh" size="mini" type="primary" plain :loading="loading" @click="refreshAll">刷新数据</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 核心 KPI 卡片 (8项客观清晰指标) -->
|
||||
<el-row :gutter="16" class="kpi-row" v-loading="loadingOverview">
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">学员总数</div>
|
||||
<div class="kpi-value text-primary">{{ overview.studentTotal || 0 }}</div>
|
||||
<div class="kpi-sub">主档档案正常</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">教师总数</div>
|
||||
<div class="kpi-value text-success">{{ overview.teacherTotal || 0 }}</div>
|
||||
<div class="kpi-sub">专兼职讲师</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">课程总数</div>
|
||||
<div class="kpi-value text-info">{{ overview.courseTotal || 0 }}</div>
|
||||
<div class="kpi-sub">标准课程库</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">培训计划</div>
|
||||
<div class="kpi-value text-warning">{{ overview.planTotal || 0 }}</div>
|
||||
<div class="kpi-sub">全周期班级数</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">累计排课</div>
|
||||
<div class="kpi-value">{{ overview.lessonTotal || 0 }}</div>
|
||||
<div class="kpi-sub">课次课时调度</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">累计参训</div>
|
||||
<div class="kpi-value">{{ overview.enrollmentTotal || 0 }}</div>
|
||||
<div class="kpi-sub">人次总规模</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">已报到人数</div>
|
||||
<div class="kpi-value text-success">{{ overview.reportTotal || 0 }}</div>
|
||||
<div class="kpi-sub">核验确认人次</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6" :md="3">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">当前在宿</div>
|
||||
<div class="kpi-value text-primary">{{ overview.accommodationCurrent || 0 }}</div>
|
||||
<div class="kpi-sub">宿舍在住学员</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 3. 计划状态与业务异常提醒条 -->
|
||||
<el-row :gutter="16" style="margin-top: 16px;">
|
||||
<!-- 计划运行状态分布 -->
|
||||
<el-col :xs="24" :sm="12" :md="10">
|
||||
<el-card shadow="never" class="status-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-date" style="color: #409EFF; margin-right: 6px;"></i>开班计划运行状态</span>
|
||||
</div>
|
||||
<div class="plan-status-tags" v-loading="loadingOverview">
|
||||
<div class="status-tag-item">
|
||||
<span class="dot dot-info"></span>
|
||||
<span class="label">未开始:</span>
|
||||
<span class="count">{{ planStatus.notStarted || 0 }}</span> 个
|
||||
</div>
|
||||
<div class="status-tag-item">
|
||||
<span class="dot dot-success"></span>
|
||||
<span class="label">进行中:</span>
|
||||
<span class="count">{{ planStatus.ongoing || 0 }}</span> 个
|
||||
</div>
|
||||
<div class="status-tag-item">
|
||||
<span class="dot dot-ended"></span>
|
||||
<span class="label">已结束:</span>
|
||||
<span class="count">{{ planStatus.ended || 0 }}</span> 个
|
||||
</div>
|
||||
<div class="status-tag-item">
|
||||
<span class="dot dot-danger"></span>
|
||||
<span class="label">已取消:</span>
|
||||
<span class="count">{{ planStatus.cancelled || 0 }}</span> 个
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 业务待办与异常提醒 -->
|
||||
<el-col :xs="24" :sm="12" :md="14">
|
||||
<el-card shadow="never" class="status-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-warning-outline" style="color: #E6A23C; margin-right: 6px;"></i>业务运行待办与治理提醒</span>
|
||||
<span class="tip-text">点击可直接跳转业务处理</span>
|
||||
</div>
|
||||
<div class="alert-grid" v-loading="loadingOverview">
|
||||
<div class="alert-item clickable" @click="goTo('/train/analysis/attendance')">
|
||||
<div class="alert-icon bg-danger"><i class="el-icon-time" /></div>
|
||||
<div class="alert-content">
|
||||
<div class="alert-num text-danger">{{ overview.abnormalAttendanceCount || 0 }}</div>
|
||||
<div class="alert-title">异常考勤人次</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert-item clickable" @click="goTo('/train/report')">
|
||||
<div class="alert-icon bg-warning"><i class="el-icon-document-checked" /></div>
|
||||
<div class="alert-content">
|
||||
<div class="alert-num text-warning">{{ overview.pendingReportCount || 0 }}</div>
|
||||
<div class="alert-title">待报到参训学员</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert-item clickable" @click="goTo('/train/accommodation')">
|
||||
<div class="alert-icon bg-primary"><i class="el-icon-office-building" /></div>
|
||||
<div class="alert-content">
|
||||
<div class="alert-num text-primary">{{ overview.preallocatedAccommodationCount || 0 }}</div>
|
||||
<div class="alert-title">住宿预分配待入住</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert-item clickable" @click="goTo('/train/extra')">
|
||||
<div class="alert-icon bg-danger"><i class="el-icon-key" /></div>
|
||||
<div class="alert-content">
|
||||
<div class="alert-num text-danger">{{ overview.abnormalAuthCount || 0 }}</div>
|
||||
<div class="alert-title">门禁授权异常数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 4. 图表行 1:趋势与班级对比 -->
|
||||
<el-row :gutter="16" style="margin-top: 16px;">
|
||||
<!-- 近30天考勤走势 -->
|
||||
<el-col :xs="24" :sm="24" :lg="14">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-data-line" style="color: #409EFF; margin-right: 6px;"></i>近 30 天考勤打卡情况走势</span>
|
||||
<span class="tip-text">正常签到 vs 异常考勤人次</span>
|
||||
</div>
|
||||
<div v-loading="loadingCharts" class="chart-wrapper">
|
||||
<div ref="trendChart" style="height: 320px; width: 100%;" />
|
||||
<div v-if="!loadingCharts && (!charts.trendDates || charts.trendDates.length === 0)" class="empty-chart">
|
||||
<el-empty description="近 30 天暂无考勤打卡流水" :image-size="70" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 培训计划参训/报到对比 -->
|
||||
<el-col :xs="24" :sm="24" :lg="10">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-s-data" style="color: #67C23A; margin-right: 6px;"></i>培训计划参训 / 报到对比 (Top 10)</span>
|
||||
</div>
|
||||
<div v-loading="loadingCharts" class="chart-wrapper">
|
||||
<div ref="planCompChart" style="height: 320px; width: 100%;" />
|
||||
<div v-if="!loadingCharts && (!charts.planComparisonNames || charts.planComparisonNames.length === 0)" class="empty-chart">
|
||||
<el-empty description="暂无培训计划数据" :image-size="70" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 5. 图表行 2:状态构成与场地使用 -->
|
||||
<el-row :gutter="16" style="margin-top: 16px; margin-bottom: 24px;">
|
||||
<!-- 全校考勤状态构成 -->
|
||||
<el-col :xs="24" :sm="24" :lg="10">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-pie-chart" style="color: #E6A23C; margin-right: 6px;"></i>全校考勤状态构成分布</span>
|
||||
<span class="tip-text">含无签到记录独立统计</span>
|
||||
</div>
|
||||
<div v-loading="loadingCharts" class="chart-wrapper">
|
||||
<div ref="statusChart" style="height: 300px; width: 100%;" />
|
||||
<div v-if="!loadingCharts && (!charts.attendanceStatusDistribution || charts.attendanceStatusDistribution.length === 0)" class="empty-chart">
|
||||
<el-empty description="暂无考勤状态数据" :image-size="70" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 教学场地使用频次 Top 5 -->
|
||||
<el-col :xs="24" :sm="24" :lg="14">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-school" style="color: #909399; margin-right: 6px;"></i>教学场地使用频次与累计课时 Top 5</span>
|
||||
<span class="tip-text">依排课记录客观统计,不推算无分母利用率</span>
|
||||
</div>
|
||||
<div v-loading="loadingCharts" class="chart-wrapper">
|
||||
<div ref="venueChart" style="height: 300px; width: 100%;" />
|
||||
<div v-if="!loadingCharts && (!charts.venueUsageTop5 || charts.venueUsageTop5.length === 0)" class="empty-chart">
|
||||
<el-empty description="暂无排课场地使用记录" :image-size="70" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getDashboardOverview, getDashboardCharts } from '@/api/train/analysis'
|
||||
|
||||
export default {
|
||||
name: 'AdminDashboard',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
loadingOverview: false,
|
||||
loadingCharts: false,
|
||||
overview: {},
|
||||
charts: {},
|
||||
trendChartInstance: null,
|
||||
planCompChartInstance: null,
|
||||
statusChartInstance: null,
|
||||
venueChartInstance: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
planStatus() {
|
||||
return this.overview.planStatusCounts || {}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.refreshAll()
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
this.disposeCharts()
|
||||
},
|
||||
methods: {
|
||||
refreshAll() {
|
||||
this.fetchOverview()
|
||||
this.fetchCharts()
|
||||
},
|
||||
fetchOverview() {
|
||||
this.loadingOverview = true
|
||||
getDashboardOverview().then(res => {
|
||||
if (res.code === 200) {
|
||||
this.overview = res.data || {}
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loadingOverview = false
|
||||
})
|
||||
},
|
||||
fetchCharts() {
|
||||
this.loadingCharts = true
|
||||
getDashboardCharts().then(res => {
|
||||
if (res.code === 200) {
|
||||
this.charts = res.data || {}
|
||||
this.$nextTick(() => {
|
||||
this.renderTrendChart()
|
||||
this.renderPlanCompChart()
|
||||
this.renderStatusChart()
|
||||
this.renderVenueChart()
|
||||
})
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loadingCharts = false
|
||||
})
|
||||
},
|
||||
renderTrendChart() {
|
||||
if (!this.$refs.trendChart) return
|
||||
if (!this.trendChartInstance) {
|
||||
this.trendChartInstance = this.$echarts.init(this.$refs.trendChart)
|
||||
}
|
||||
const dates = this.charts.trendDates || []
|
||||
const normal = this.charts.trendNormalCounts || []
|
||||
const abnormal = this.charts.trendAbnormalCounts || []
|
||||
|
||||
const option = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['正常签到人次', '异常考勤人次'], top: 0 },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: dates,
|
||||
axisLine: { lineStyle: { color: '#909399' } }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
minInterval: 1,
|
||||
splitLine: { lineStyle: { type: 'dashed' } }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '正常签到人次',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: normal,
|
||||
itemStyle: { color: '#67C23A' },
|
||||
areaStyle: {
|
||||
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(103,194,58,0.3)' },
|
||||
{ offset: 1, color: 'rgba(103,194,58,0.02)' }
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '异常考勤人次',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: abnormal,
|
||||
itemStyle: { color: '#F56C6C' },
|
||||
areaStyle: {
|
||||
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(245,108,108,0.3)' },
|
||||
{ offset: 1, color: 'rgba(245,108,108,0.02)' }
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
this.trendChartInstance.setOption(option)
|
||||
},
|
||||
renderPlanCompChart() {
|
||||
if (!this.$refs.planCompChart) return
|
||||
if (!this.planCompChartInstance) {
|
||||
this.planCompChartInstance = this.$echarts.init(this.$refs.planCompChart)
|
||||
}
|
||||
const names = this.charts.planComparisonNames || []
|
||||
const enrolled = this.charts.planEnrolledCounts || []
|
||||
const reported = this.charts.planReportedCounts || []
|
||||
|
||||
const option = {
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
legend: { data: ['参训人数', '报到人数'], top: 0 },
|
||||
grid: { left: '3%', right: '4%', bottom: '8%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: names,
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 20,
|
||||
formatter: val => val.length > 7 ? val.substring(0, 7) + '...' : val
|
||||
}
|
||||
},
|
||||
yAxis: { type: 'value', minInterval: 1 },
|
||||
series: [
|
||||
{
|
||||
name: '参训人数',
|
||||
type: 'bar',
|
||||
data: enrolled,
|
||||
itemStyle: { color: '#409EFF' }
|
||||
},
|
||||
{
|
||||
name: '报到人数',
|
||||
type: 'bar',
|
||||
data: reported,
|
||||
itemStyle: { color: '#67C23A' }
|
||||
}
|
||||
]
|
||||
}
|
||||
this.planCompChartInstance.setOption(option)
|
||||
},
|
||||
renderStatusChart() {
|
||||
if (!this.$refs.statusChart) return
|
||||
if (!this.statusChartInstance) {
|
||||
this.statusChartInstance = this.$echarts.init(this.$refs.statusChart)
|
||||
}
|
||||
const rawList = this.charts.attendanceStatusDistribution || []
|
||||
const data = rawList.map(item => ({ name: item.name, value: item.value }))
|
||||
|
||||
const option = {
|
||||
tooltip: { trigger: 'item', formatter: '{b}: {c} 人次 ({d}%)' },
|
||||
legend: { bottom: 0, left: 'center' },
|
||||
color: ['#67C23A', '#E6A23C', '#F56C6C', '#409EFF', '#909399', '#C0C4CC'],
|
||||
series: [
|
||||
{
|
||||
name: '考勤状态',
|
||||
type: 'pie',
|
||||
radius: ['45%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: false, position: 'center' },
|
||||
emphasis: {
|
||||
label: { show: true, fontSize: 14, fontWeight: 'bold' }
|
||||
},
|
||||
data: data
|
||||
}
|
||||
]
|
||||
}
|
||||
this.statusChartInstance.setOption(option)
|
||||
},
|
||||
renderVenueChart() {
|
||||
if (!this.$refs.venueChart) return
|
||||
if (!this.venueChartInstance) {
|
||||
this.venueChartInstance = this.$echarts.init(this.$refs.venueChart)
|
||||
}
|
||||
const raw = this.charts.venueUsageTop5 || []
|
||||
const names = raw.map(i => i.venueName).reverse()
|
||||
const counts = raw.map(i => i.usageCount).reverse()
|
||||
const hours = raw.map(i => i.totalHours).reverse()
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
formatter: params => {
|
||||
const name = params[0].name
|
||||
let str = `<b>${name}</b><br/>`
|
||||
params.forEach(p => {
|
||||
str += `${p.seriesName}: ${p.value} ${p.seriesName.includes('课时') ? '小时' : '次'}<br/>`
|
||||
})
|
||||
return str
|
||||
}
|
||||
},
|
||||
legend: { data: ['使用频次', '累计课时(小时)'], top: 0 },
|
||||
grid: { left: '3%', right: '5%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'value', minInterval: 1 },
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: names,
|
||||
axisLabel: {
|
||||
formatter: val => val.length > 8 ? val.substring(0, 8) + '...' : val
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '使用频次',
|
||||
type: 'bar',
|
||||
data: counts,
|
||||
itemStyle: { color: '#36cbcb' }
|
||||
},
|
||||
{
|
||||
name: '累计课时(小时)',
|
||||
type: 'bar',
|
||||
data: hours,
|
||||
itemStyle: { color: '#4ecb73' }
|
||||
}
|
||||
]
|
||||
}
|
||||
this.venueChartInstance.setOption(option)
|
||||
},
|
||||
handleResize() {
|
||||
if (this.trendChartInstance) this.trendChartInstance.resize()
|
||||
if (this.planCompChartInstance) this.planCompChartInstance.resize()
|
||||
if (this.statusChartInstance) this.statusChartInstance.resize()
|
||||
if (this.venueChartInstance) this.venueChartInstance.resize()
|
||||
},
|
||||
disposeCharts() {
|
||||
if (this.trendChartInstance) { this.trendChartInstance.dispose(); this.trendChartInstance = null }
|
||||
if (this.planCompChartInstance) { this.planCompChartInstance.dispose(); this.planCompChartInstance = null }
|
||||
if (this.statusChartInstance) { this.statusChartInstance.dispose(); this.statusChartInstance = null }
|
||||
if (this.venueChartInstance) { this.venueChartInstance.dispose(); this.venueChartInstance = null }
|
||||
},
|
||||
goTo(path) {
|
||||
if (path) this.$router.push(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dashboard-editor-container {
|
||||
padding: 16px 20px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
padding: 16px 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
border-left: 4px solid #409EFF;
|
||||
|
||||
.header-title h2 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 18px;
|
||||
color: #303133;
|
||||
}
|
||||
.header-title .subtitle {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-row {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 14px 16px;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.kpi-label {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.kpi-value {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.kpi-sub {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.text-primary { color: #409EFF !important; }
|
||||
.text-success { color: #67C23A !important; }
|
||||
.text-warning { color: #E6A23C !important; }
|
||||
.text-danger { color: #F56C6C !important; }
|
||||
.text-info { color: #909399 !important; }
|
||||
|
||||
.status-card {
|
||||
min-height: 140px;
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
.tip-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plan-status-tags {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 10px 0;
|
||||
.status-tag-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
&.dot-info { background: #909399; }
|
||||
&.dot-success { background: #67C23A; }
|
||||
&.dot-ended { background: #409EFF; }
|
||||
&.dot-danger { background: #F56C6C; }
|
||||
}
|
||||
.count {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
.alert-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fafafa;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
transition: all 0.2s;
|
||||
&.clickable:hover {
|
||||
cursor: pointer;
|
||||
background: #f0f7ff;
|
||||
border-color: #b3d8ff;
|
||||
}
|
||||
.alert-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
margin-right: 10px;
|
||||
&.bg-danger { background: #F56C6C; }
|
||||
&.bg-warning { background: #E6A23C; }
|
||||
&.bg-primary { background: #409EFF; }
|
||||
}
|
||||
.alert-content {
|
||||
.alert-num {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.alert-title {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
.tip-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
.empty-chart {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user