新增分析:实现考勤专题统计与异常排行
This commit is contained in:
1 parent
f17526b409
commit
d0ae543a4d
1 file changed
+593
@@ -0,0 +1,593 @@
|
||||
<template>
|
||||
<div class="app-container attendance-analysis-container">
|
||||
<!-- 1. 顶部筛选区 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form :model="queryParams" ref="queryForm" :inline="true" size="small" label-width="78px">
|
||||
<el-form-item label="起止时间" prop="dateRange">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
:picker-options="pickerOptions"
|
||||
@change="handleDateRangeChange"
|
||||
style="width: 240px;"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="培训计划" prop="planId">
|
||||
<el-select v-model="queryParams.planId" placeholder="全部培训计划" clearable filterable style="width: 220px;">
|
||||
<el-option
|
||||
v-for="item in planOptions"
|
||||
:key="item.planId"
|
||||
:label="item.planName"
|
||||
:value="item.planId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属课程" prop="courseId">
|
||||
<el-select v-model="queryParams.courseId" placeholder="全部课程" clearable filterable style="width: 200px;">
|
||||
<el-option
|
||||
v-for="item in courseOptions"
|
||||
:key="item.cId"
|
||||
:label="item.cName"
|
||||
:value="item.cId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询分析</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 2. 统计指标卡片 (带口径待确认警示) -->
|
||||
<el-row :gutter="16" class="mt16" v-loading="loadingSummary">
|
||||
<el-col :xs="12" :sm="12" :md="5">
|
||||
<div class="kpi-box">
|
||||
<div class="kpi-title">累计已开课次</div>
|
||||
<div class="kpi-num">{{ summary.lessonTotal || 0 }} <span class="unit">节</span></div>
|
||||
<div class="kpi-desc">范围内实际调度课次</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="5">
|
||||
<div class="kpi-box">
|
||||
<div class="kpi-title">应到人次</div>
|
||||
<div class="kpi-num">{{ summary.expectedAttendTotal || 0 }} <span class="unit">人次</span></div>
|
||||
<div class="kpi-desc">班级参训基准总负荷</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="5">
|
||||
<div class="kpi-box">
|
||||
<div class="kpi-title">正常出勤人次</div>
|
||||
<div class="kpi-num text-success">{{ summary.normalAttendTotal || 0 }} <span class="unit">人次</span></div>
|
||||
<div class="kpi-desc">按时到课核验记录</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="4">
|
||||
<div class="kpi-box">
|
||||
<div class="kpi-title">异常考勤人次</div>
|
||||
<div class="kpi-num text-danger">{{ summary.abnormalAttendTotal || 0 }} <span class="unit">人次</span></div>
|
||||
<div class="kpi-desc">缺勤/迟到/早退/请假/无打卡</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="24" :md="5">
|
||||
<div class="kpi-box rate-box">
|
||||
<div class="kpi-title">
|
||||
综合出勤率
|
||||
<el-tooltip :content="summary.metricDefinitionNotice || '出勤率分母采用开课所属班级参训学员应到人次,业务标准待甲方确认'" placement="top">
|
||||
<i class="el-icon-question" style="color: #E6A23C; cursor: pointer; margin-left: 4px;" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div class="kpi-num text-primary">{{ summary.attendanceRate != null ? summary.attendanceRate : 0 }}%</div>
|
||||
<div class="kpi-desc">正常人次 / 应到总人次</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 业务口径状态警示条 -->
|
||||
<el-alert
|
||||
:title="summary.metricDefinitionNotice"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="mt16"
|
||||
/>
|
||||
|
||||
<!-- 3. 图表区 1:趋势折线图 & 状态环形图 -->
|
||||
<el-row :gutter="16" class="mt16">
|
||||
<el-col :xs="24" :sm="24" :lg="15">
|
||||
<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>每日考勤打卡趋势走势 (Daily Attendance Trend)</span>
|
||||
</div>
|
||||
<div v-loading="loadingCharts" class="chart-wrapper">
|
||||
<div ref="dailyTrendChart" style="height: 320px; width: 100%;" />
|
||||
<div v-if="!loadingCharts && (!charts.dailyTrend || charts.dailyTrend.length === 0)" class="empty-chart">
|
||||
<el-empty description="所选筛选范围内暂无每日打卡数据" :image-size="70" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="24" :lg="9">
|
||||
<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>
|
||||
</div>
|
||||
<div v-loading="loadingCharts" class="chart-wrapper">
|
||||
<div ref="statusDistChart" style="height: 320px; width: 100%;" />
|
||||
<div v-if="!loadingCharts && (!charts.statusDistribution || charts.statusDistribution.length === 0)" class="empty-chart">
|
||||
<el-empty description="暂无考勤状态分布" :image-size="70" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 4. 图表区 2:培训计划对比 & 课程对比 -->
|
||||
<el-row :gutter="16" class="mt16">
|
||||
<el-col :xs="24" :sm="24" :lg="12">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-s-cooperation" style="color: #409EFF; margin-right: 6px;"></i>培训计划出勤对比 (Top 10)</span>
|
||||
</div>
|
||||
<div v-loading="loadingCharts" class="chart-wrapper">
|
||||
<div ref="planCompChart" style="height: 300px; width: 100%;" />
|
||||
<div v-if="!loadingCharts && (!charts.planComparison || charts.planComparison.length === 0)" class="empty-chart">
|
||||
<el-empty description="暂无班级考勤对比数据" :image-size="70" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="24" :lg="12">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-notebook-2" style="color: #67C23A; margin-right: 6px;"></i>课程考勤对比 (Top 10)</span>
|
||||
</div>
|
||||
<div v-loading="loadingCharts" class="chart-wrapper">
|
||||
<div ref="courseCompChart" style="height: 300px; width: 100%;" />
|
||||
<div v-if="!loadingCharts && (!charts.courseComparison || charts.courseComparison.length === 0)" class="empty-chart">
|
||||
<el-empty description="暂无课程考勤对比数据" :image-size="70" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 5. 异常学员 Top 10 列表与柱图穿透 -->
|
||||
<el-card shadow="never" class="mt16" v-loading="loadingCharts">
|
||||
<div slot="header" class="card-header">
|
||||
<span><i class="el-icon-user" style="color: #F56C6C; margin-right: 6px;"></i>考勤异常重点学员排行 Top 10 (异常出勤频次预警)</span>
|
||||
<span class="tip-text">点击学员或操作栏可直接穿透至【学员360画像】</span>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :sm="24" :lg="10">
|
||||
<div ref="abnormalChart" style="height: 340px; width: 100%;" />
|
||||
<div v-if="!loadingCharts && (!charts.abnormalStudentTop10 || charts.abnormalStudentTop10.length === 0)" class="empty-chart">
|
||||
<el-empty description="当前无异常考勤学员" :image-size="70" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="24" :lg="14">
|
||||
<el-table :data="charts.abnormalStudentTop10 || []" border size="mini" stripe style="width: 100%; margin-top: 10px;">
|
||||
<el-table-column type="index" label="排名" width="50" align="center" />
|
||||
<el-table-column prop="studentId" label="学号" width="110" align="center" />
|
||||
<el-table-column label="姓名" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-link type="primary" :underline="false" @click="drillToPortrait(scope.row)">
|
||||
{{ scope.row.studentName }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="planName" label="所属培训班" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="absentCount" label="缺勤" width="60" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span :class="scope.row.absentCount > 0 ? 'text-danger font-bold' : ''">{{ scope.row.absentCount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="lateCount" label="迟到" width="60" align="center" />
|
||||
<el-table-column prop="leaveCount" label="请假" width="60" align="center" />
|
||||
<el-table-column prop="totalAbnormalCount" label="总异常次数" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag size="mini" type="danger">{{ scope.row.totalAbnormalCount }} 次</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" icon="el-icon-view" @click="drillToPortrait(scope.row)">
|
||||
画像
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAttendanceSummary, getAttendanceCharts } from '@/api/train/analysis'
|
||||
import { listPlan } from '@/api/train/plan'
|
||||
import { listCourse } from '@/api/train/course'
|
||||
|
||||
export default {
|
||||
name: 'AttendanceAnalysis',
|
||||
data() {
|
||||
return {
|
||||
dateRange: [],
|
||||
queryParams: {
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
planId: '',
|
||||
courseId: ''
|
||||
},
|
||||
planOptions: [],
|
||||
courseOptions: [],
|
||||
summary: {},
|
||||
charts: {},
|
||||
loadingSummary: false,
|
||||
loadingCharts: false,
|
||||
pickerOptions: {
|
||||
shortcuts: [
|
||||
{
|
||||
text: '最近30天',
|
||||
onClick(picker) {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
|
||||
picker.$emit('pick', [start, end])
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '最近90天',
|
||||
onClick(picker) {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90)
|
||||
picker.$emit('pick', [start, end])
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '最近1年',
|
||||
onClick(picker) {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 365)
|
||||
picker.$emit('pick', [start, end])
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
dailyTrendInstance: null,
|
||||
statusDistInstance: null,
|
||||
planCompInstance: null,
|
||||
courseCompInstance: null,
|
||||
abnormalInstance: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initDefaultDates()
|
||||
this.loadFilterOptions()
|
||||
this.handleQuery()
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
this.disposeCharts()
|
||||
},
|
||||
methods: {
|
||||
initDefaultDates() {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
|
||||
const format = d => {
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
}
|
||||
this.dateRange = [format(start), format(end)]
|
||||
this.queryParams.startDate = this.dateRange[0]
|
||||
this.queryParams.endDate = this.dateRange[1]
|
||||
},
|
||||
loadFilterOptions() {
|
||||
listPlan({ pageNum: 1, pageSize: 100 }).then(res => {
|
||||
this.planOptions = res.rows || []
|
||||
})
|
||||
listCourse({ pageNum: 1, pageSize: 100 }).then(res => {
|
||||
this.courseOptions = res.rows || []
|
||||
})
|
||||
},
|
||||
handleDateRangeChange(val) {
|
||||
if (val && val.length === 2) {
|
||||
this.queryParams.startDate = val[0]
|
||||
this.queryParams.endDate = val[1]
|
||||
} else {
|
||||
this.queryParams.startDate = ''
|
||||
this.queryParams.endDate = ''
|
||||
}
|
||||
},
|
||||
handleQuery() {
|
||||
this.fetchSummary()
|
||||
this.fetchCharts()
|
||||
},
|
||||
resetQuery() {
|
||||
this.initDefaultDates()
|
||||
this.queryParams.planId = ''
|
||||
this.queryParams.courseId = ''
|
||||
this.handleQuery()
|
||||
},
|
||||
fetchSummary() {
|
||||
this.loadingSummary = true
|
||||
getAttendanceSummary(this.queryParams).then(res => {
|
||||
if (res.code === 200) {
|
||||
this.summary = res.data || {}
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loadingSummary = false
|
||||
})
|
||||
},
|
||||
fetchCharts() {
|
||||
this.loadingCharts = true
|
||||
getAttendanceCharts(this.queryParams).then(res => {
|
||||
if (res.code === 200) {
|
||||
this.charts = res.data || {}
|
||||
this.$nextTick(() => {
|
||||
this.renderDailyTrend()
|
||||
this.renderStatusDist()
|
||||
this.renderPlanComp()
|
||||
this.renderCourseComp()
|
||||
this.renderAbnormalChart()
|
||||
})
|
||||
}
|
||||
}).finally(() => {
|
||||
this.loadingCharts = false
|
||||
})
|
||||
},
|
||||
renderDailyTrend() {
|
||||
if (!this.$refs.dailyTrendChart) return
|
||||
if (!this.dailyTrendInstance) {
|
||||
this.dailyTrendInstance = this.$echarts.init(this.$refs.dailyTrendChart)
|
||||
}
|
||||
const raw = this.charts.dailyTrend || []
|
||||
const dates = raw.map(i => i.date)
|
||||
const normal = raw.map(i => i.normalCount)
|
||||
const abnormal = raw.map(i => i.abnormalCount)
|
||||
|
||||
const option = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['正常出勤人次', '异常出勤人次'], top: 0 },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'category', data: dates },
|
||||
yAxis: { type: 'value', minInterval: 1 },
|
||||
series: [
|
||||
{
|
||||
name: '正常出勤人次',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: normal,
|
||||
itemStyle: { color: '#67C23A' }
|
||||
},
|
||||
{
|
||||
name: '异常出勤人次',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: abnormal,
|
||||
itemStyle: { color: '#F56C6C' }
|
||||
}
|
||||
]
|
||||
}
|
||||
this.dailyTrendInstance.setOption(option)
|
||||
},
|
||||
renderStatusDist() {
|
||||
if (!this.$refs.statusDistChart) return
|
||||
if (!this.statusDistInstance) {
|
||||
this.statusDistInstance = this.$echarts.init(this.$refs.statusDistChart)
|
||||
}
|
||||
const raw = this.charts.statusDistribution || []
|
||||
const data = raw.map(i => ({ name: i.name, value: i.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 },
|
||||
data: data
|
||||
}
|
||||
]
|
||||
}
|
||||
this.statusDistInstance.setOption(option)
|
||||
},
|
||||
renderPlanComp() {
|
||||
if (!this.$refs.planCompChart) return
|
||||
if (!this.planCompInstance) {
|
||||
this.planCompInstance = this.$echarts.init(this.$refs.planCompChart)
|
||||
}
|
||||
const raw = this.charts.planComparison || []
|
||||
const names = raw.map(i => i.name).reverse()
|
||||
const normal = raw.map(i => i.normalCount).reverse()
|
||||
const abnormal = raw.map(i => i.abnormalCount).reverse()
|
||||
|
||||
const option = {
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
legend: { data: ['正常出勤', '异常考勤'], top: 0 },
|
||||
grid: { left: '3%', right: '4%', 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', stack: 'total', data: normal, itemStyle: { color: '#67C23A' } },
|
||||
{ name: '异常考勤', type: 'bar', stack: 'total', data: abnormal, itemStyle: { color: '#F56C6C' } }
|
||||
]
|
||||
}
|
||||
this.planCompInstance.setOption(option)
|
||||
},
|
||||
renderCourseComp() {
|
||||
if (!this.$refs.courseCompChart) return
|
||||
if (!this.courseCompInstance) {
|
||||
this.courseCompInstance = this.$echarts.init(this.$refs.courseCompChart)
|
||||
}
|
||||
const raw = this.charts.courseComparison || []
|
||||
const names = raw.map(i => i.name).reverse()
|
||||
const normal = raw.map(i => i.normalCount).reverse()
|
||||
const abnormal = raw.map(i => i.abnormalCount).reverse()
|
||||
|
||||
const option = {
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
legend: { data: ['正常出勤', '异常考勤'], top: 0 },
|
||||
grid: { left: '3%', right: '4%', 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', stack: 'total', data: normal, itemStyle: { color: '#409EFF' } },
|
||||
{ name: '异常考勤', type: 'bar', stack: 'total', data: abnormal, itemStyle: { color: '#E6A23C' } }
|
||||
]
|
||||
}
|
||||
this.courseCompInstance.setOption(option)
|
||||
},
|
||||
renderAbnormalChart() {
|
||||
if (!this.$refs.abnormalChart) return
|
||||
if (!this.abnormalInstance) {
|
||||
this.abnormalInstance = this.$echarts.init(this.$refs.abnormalChart)
|
||||
}
|
||||
const raw = this.charts.abnormalStudentTop10 || []
|
||||
const names = raw.map(i => i.studentName).reverse()
|
||||
const counts = raw.map(i => i.totalAbnormalCount).reverse()
|
||||
|
||||
const option = {
|
||||
title: { text: '异常频次分布', textStyle: { fontSize: 13, color: '#606266' } },
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
grid: { left: '3%', right: '6%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'value', minInterval: 1 },
|
||||
yAxis: { type: 'category', data: names },
|
||||
series: [
|
||||
{
|
||||
name: '总异常人次',
|
||||
type: 'bar',
|
||||
data: counts,
|
||||
itemStyle: { color: '#F56C6C' },
|
||||
label: { show: true, position: 'right' }
|
||||
}
|
||||
]
|
||||
}
|
||||
this.abnormalInstance.setOption(option)
|
||||
},
|
||||
handleResize() {
|
||||
if (this.dailyTrendInstance) this.dailyTrendInstance.resize()
|
||||
if (this.statusDistInstance) this.statusDistInstance.resize()
|
||||
if (this.planCompInstance) this.planCompInstance.resize()
|
||||
if (this.courseCompInstance) this.courseCompInstance.resize()
|
||||
if (this.abnormalInstance) this.abnormalInstance.resize()
|
||||
},
|
||||
disposeCharts() {
|
||||
if (this.dailyTrendInstance) { this.dailyTrendInstance.dispose(); this.dailyTrendInstance = null }
|
||||
if (this.statusDistInstance) { this.statusDistInstance.dispose(); this.statusDistInstance = null }
|
||||
if (this.planCompInstance) { this.planCompInstance.dispose(); this.planCompInstance = null }
|
||||
if (this.courseCompInstance) { this.courseCompInstance.dispose(); this.courseCompInstance = null }
|
||||
if (this.abnormalInstance) { this.abnormalInstance.dispose(); this.abnormalInstance = null }
|
||||
},
|
||||
drillToPortrait(row) {
|
||||
if (row && row.studentId) {
|
||||
this.$router.push({
|
||||
path: '/train/portrait',
|
||||
query: { studentId: row.studentId, planId: row.planId }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.attendance-analysis-container {
|
||||
background-color: #f0f2f5;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.mt16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.kpi-box {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 14px 16px;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.kpi-title {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.kpi-num {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 6px;
|
||||
.unit {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
.kpi-desc {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.rate-box {
|
||||
border-top: 3px solid #409EFF;
|
||||
}
|
||||
|
||||
.text-primary { color: #409EFF !important; }
|
||||
.text-success { color: #67C23A !important; }
|
||||
.text-danger { color: #F56C6C !important; }
|
||||
.font-bold { font-weight: bold; }
|
||||
|
||||
.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