- 重命名文件: ruoyi.js common.js, ruoyi.scss common.scss - 重命名组件: RuoYi/ Common/ - 创建新类: XinliConfig.java (替代RuoYiConfig.java) - 更新所有导入语句和引用 (50+ 处) - 更新配置前缀: ruoyi xinli - 更新Swagger文档标题和描述 - 更新许可证版权信息 - 移除所有RuoYi文档链接和示例代码
792 lines
27 KiB
Vue
792 lines
27 KiB
Vue
<template>
|
||
<div class="assessment-analysis">
|
||
<el-tabs v-model="activeTab" type="border-card">
|
||
<el-tab-pane label="全局概览" name="overview">
|
||
<div class="tab-pane-body" v-loading="loading">
|
||
<el-card class="filter-card" shadow="never">
|
||
<el-form :inline="true" size="small">
|
||
<el-form-item label="测评量表">
|
||
<el-select v-model="queryParams.scaleId" clearable filterable placeholder="全部量表" style="width: 220px">
|
||
<el-option
|
||
v-for="item in scaleOptions"
|
||
:key="item.scaleId"
|
||
:label="item.scaleName"
|
||
:value="item.scaleId"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="测评时间">
|
||
<el-date-picker
|
||
v-model="queryParams.dateRange"
|
||
type="daterange"
|
||
unlink-panels
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
value-format="yyyy-MM-dd"
|
||
range-separator="至"
|
||
style="width: 320px"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
|
||
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<el-row :gutter="16" class="overview-row">
|
||
<el-col v-for="card in overviewCards" :key="card.key" :xs="12" :sm="6" :md="4">
|
||
<el-card class="stat-card" shadow="hover">
|
||
<div class="stat-title">{{ card.label }}</div>
|
||
<div class="stat-value">{{ formatNumber(card.value) }}</div>
|
||
<div class="stat-desc">{{ card.desc }}</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="16" class="chart-row">
|
||
<el-col :md="24" :sm="24">
|
||
<el-card shadow="always" class="chart-card">
|
||
<div slot="header" class="chart-card__title">测评状态分布</div>
|
||
<div ref="statusPie" class="chart-container"></div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="16" class="chart-row">
|
||
<el-col :md="24" :sm="24">
|
||
<el-card shadow="always" class="chart-card">
|
||
<div slot="header" class="chart-card__title">近期待完成趋势</div>
|
||
<div ref="trendChart" class="chart-container trend-chart"></div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="16" class="detail-row">
|
||
<el-col :md="12" :sm="24">
|
||
<el-card shadow="never">
|
||
<div slot="header" class="detail-title">量表参与 TOP10</div>
|
||
<el-table
|
||
:data="analytics.scaleDistribution"
|
||
size="small"
|
||
height="300"
|
||
:empty-text="emptyText"
|
||
border
|
||
>
|
||
<el-table-column type="index" label="#" width="60" />
|
||
<el-table-column prop="name" label="量表" show-overflow-tooltip />
|
||
<el-table-column prop="value" label="参与次数" width="120" align="right" />
|
||
</el-table>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :md="12" :sm="24">
|
||
<el-card shadow="never">
|
||
<div slot="header" class="detail-title">得分区间分布</div>
|
||
<el-table
|
||
:data="analytics.scoreRangeDistribution"
|
||
size="small"
|
||
height="300"
|
||
:empty-text="emptyText"
|
||
border
|
||
>
|
||
<el-table-column prop="name" label="区间" />
|
||
<el-table-column prop="value" label="人数" width="120" align="right" />
|
||
</el-table>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane label="学员视角" name="student">
|
||
<div class="tab-pane-body" v-loading="studentLoading">
|
||
<el-card class="filter-card" shadow="never">
|
||
<el-form :inline="true" size="small">
|
||
<el-form-item label="选择学员">
|
||
<el-select
|
||
v-model="selectedStudentId"
|
||
style="width: 280px"
|
||
clearable
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
:remote-method="loadStudentOptions"
|
||
:loading="studentOptionsLoading"
|
||
placeholder="输入姓名或账号搜索"
|
||
@change="handleStudentChange"
|
||
>
|
||
<el-option
|
||
v-for="item in studentOptions"
|
||
:key="item.userId"
|
||
:label="buildStudentLabel(item)"
|
||
:value="item.userId"
|
||
>
|
||
<div class="student-option">
|
||
<span class="name">{{ item.nickName || item.userName }}</span>
|
||
<span class="dept">{{ item.deptName || '未分配单位' }}</span>
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" icon="el-icon-search" :disabled="!selectedStudentId" @click="fetchUserSummary">
|
||
载入
|
||
</el-button>
|
||
<el-button icon="el-icon-refresh" @click="resetStudentSelection">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<template v-if="studentSummary">
|
||
<el-card shadow="never" class="student-info-card">
|
||
<el-descriptions size="small" :column="3" border>
|
||
<el-descriptions-item label="罪犯姓名">{{ studentSummary.nickName || studentSummary.userName || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="信息编号">{{ studentSummary.infoNumber || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="监狱">{{ studentSummary.prisonName || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="监区">{{ studentSummary.prisonAreaName || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="民族">{{ studentSummary.nation || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="文化程度">{{ studentSummary.educationLevel || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="罪名">{{ studentSummary.crimeName || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="刑期">{{ studentSummary.sentenceTerm || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="状态">{{ studentSummary.custodyStatus || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="刑期起日">{{ studentSummary.sentenceStartDate || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="刑期止日">{{ studentSummary.sentenceEndDate || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="入监时间">{{ studentSummary.entryDate || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="量表数量">{{ (studentSummary.scales || []).length }}</el-descriptions-item>
|
||
<el-descriptions-item label="测评次数">{{ studentSummary.totalAssessments || 0 }}</el-descriptions-item>
|
||
<el-descriptions-item label="原账号">{{ studentSummary.loginAccount || '-' }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
</el-card>
|
||
|
||
<el-collapse v-model="openedScalePanels" accordion class="scale-collapse">
|
||
<el-collapse-item
|
||
v-for="scale in studentScales"
|
||
:key="scale.scaleId"
|
||
:name="scale.scaleId"
|
||
:title="scale.scaleName + '(' + scale.attempts.length + '次)'"
|
||
>
|
||
<div class="scale-attempts">
|
||
<el-radio-group v-model="scale.selectedAssessmentId" size="mini" class="attempt-radio-group">
|
||
<el-radio-button
|
||
v-for="attempt in scale.attempts"
|
||
:key="attempt.assessmentId"
|
||
:label="attempt.assessmentId"
|
||
>
|
||
{{ attempt.submitTime ? formatDateTime(attempt.submitTime) : '未提交' }}
|
||
</el-radio-button>
|
||
</el-radio-group>
|
||
|
||
<div class="attempt-detail" v-if="getSelectedAttempt(scale)">
|
||
<div class="attempt-detail__row">
|
||
<div class="attempt-field">
|
||
<span>提交时间</span>
|
||
<strong>{{ formatDateTime(getSelectedAttempt(scale).submitTime) }}</strong>
|
||
</div>
|
||
<div class="attempt-field">
|
||
<span>总分</span>
|
||
<strong>{{ getSelectedAttempt(scale).totalScore != null ? getSelectedAttempt(scale).totalScore : '-' }}</strong>
|
||
</div>
|
||
<div class="attempt-field">
|
||
<span>状态</span>
|
||
<el-tag size="small" :type="statusTagType(getSelectedAttempt(scale).status)">
|
||
{{ statusLabel(getSelectedAttempt(scale).status) }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
<div class="attempt-summary">
|
||
<div class="summary-title">报告摘要</div>
|
||
<div class="summary-content">{{ getSelectedAttempt(scale).reportSummary || '暂无报告摘要' }}</div>
|
||
</div>
|
||
<el-button
|
||
type="primary"
|
||
size="mini"
|
||
:disabled="!getSelectedAttempt(scale).reportId"
|
||
@click="navigateReport(getSelectedAttempt(scale))"
|
||
>
|
||
查看报告
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</el-collapse-item>
|
||
</el-collapse>
|
||
</template>
|
||
<el-empty v-else description="请选择学员查看测评详情"></el-empty>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane label="量表 / 单位" name="scaleDept">
|
||
<div class="tab-pane-body" v-loading="scaleStatsLoading">
|
||
<el-card class="filter-card" shadow="never">
|
||
<el-form :inline="true" size="small">
|
||
<el-form-item label="量表">
|
||
<el-select v-model="scaleDeptForm.scaleId" filterable placeholder="请选择量表" style="width: 220px">
|
||
<el-option
|
||
v-for="item in scaleOptions"
|
||
:key="item.scaleId"
|
||
:label="item.scaleName"
|
||
:value="item.scaleId"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="监区">
|
||
<Treeselect
|
||
v-model="scaleDeptForm.deptIds"
|
||
:options="deptOptions"
|
||
:multiple="true"
|
||
:clearable="true"
|
||
placeholder="可多选监区单位"
|
||
style="width: 260px"
|
||
value-consists-of="LEAF_PRIORITY"
|
||
></Treeselect>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" icon="el-icon-data-analysis" @click="handleScaleDeptQuery">统计</el-button>
|
||
<el-button icon="el-icon-refresh" @click="resetScaleDeptForm">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<template v-if="scaleStats && (scaleStats.totalTargets || scaleStats.measuredCount)">
|
||
<el-row :gutter="16" class="overview-row">
|
||
<el-col :md="6" :sm="12">
|
||
<el-card class="stat-card">
|
||
<div class="stat-title">目标人数</div>
|
||
<div class="stat-value">{{ formatNumber(scaleStats.totalTargets || 0) }}</div>
|
||
<div class="stat-desc">选择单位内的全部受测人</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :md="6" :sm="12">
|
||
<el-card class="stat-card">
|
||
<div class="stat-title">已测评</div>
|
||
<div class="stat-value">{{ formatNumber(scaleStats.measuredCount || 0) }}</div>
|
||
<div class="stat-desc">至少完成一次该量表</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :md="6" :sm="12">
|
||
<el-card class="stat-card">
|
||
<div class="stat-title">未测评</div>
|
||
<div class="stat-value">{{ formatNumber(scaleStats.unmeasuredCount || 0) }}</div>
|
||
<div class="stat-desc">尚未参与或未提交</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :md="6" :sm="12">
|
||
<el-card class="stat-card">
|
||
<div class="stat-title">完成率</div>
|
||
<div class="stat-value">{{ formatPercent(scaleStats.completionRate) }}</div>
|
||
<div class="stat-desc">已测评 / 目标人数</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="16" class="chart-row">
|
||
<el-col :md="12" :sm="24">
|
||
<el-card shadow="always" class="chart-card">
|
||
<div slot="header" class="chart-card__title">完成情况</div>
|
||
<div ref="scaleCompletionPie" class="chart-container"></div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :md="12" :sm="24">
|
||
<el-card shadow="always" class="chart-card">
|
||
<div slot="header" class="chart-card__title">报告结果分布</div>
|
||
<div ref="scaleResultPie" class="chart-container"></div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
</template>
|
||
<el-empty v-else description="请选择量表与单位后统计"></el-empty>
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import * as echarts from 'echarts'
|
||
require('echarts/theme/macarons')
|
||
import Treeselect from '@riophae/vue-treeselect'
|
||
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
|
||
import { parseTime } from '@/utils/common'
|
||
import {
|
||
getAssessmentAnalytics,
|
||
getStudentOptions,
|
||
getUserAssessmentSummary,
|
||
getScaleDeptStats
|
||
} from '@/api/psychology/assessment'
|
||
import { listScale } from '@/api/psychology/scale'
|
||
import { getCustodyTreeOptions, findCustodyLabel } from '@/utils/custodyArea'
|
||
|
||
export default {
|
||
name: 'AssessmentAnalysis',
|
||
components: { Treeselect },
|
||
data() {
|
||
return {
|
||
activeTab: 'overview',
|
||
loading: false,
|
||
studentLoading: false,
|
||
scaleStatsLoading: false,
|
||
queryParams: {
|
||
scaleId: undefined,
|
||
dateRange: []
|
||
},
|
||
scaleOptions: [],
|
||
deptOptions: JSON.parse(JSON.stringify(getCustodyTreeOptions())),
|
||
analytics: this.createEmptyAnalytics(),
|
||
charts: {},
|
||
resizeHandler: null,
|
||
emptyText: '暂无数据',
|
||
studentOptions: [],
|
||
studentOptionsLoading: false,
|
||
selectedStudentId: undefined,
|
||
studentSummary: null,
|
||
studentScales: [],
|
||
openedScalePanels: [],
|
||
scaleDeptForm: {
|
||
scaleId: undefined,
|
||
deptIds: []
|
||
},
|
||
scaleStats: this.createEmptyScaleStats()
|
||
}
|
||
},
|
||
computed: {
|
||
overviewCards() {
|
||
const overview = this.analytics.overview || {}
|
||
return [
|
||
{ key: 'total', label: '测评总数', value: overview.totalAssessments || 0, desc: '所有已发起测评' },
|
||
{ key: 'completed', label: '已完成', value: overview.completedAssessments || 0, desc: '提交并生成结果' },
|
||
{ key: 'inProgress', label: '进行中', value: overview.inProgressAssessments || 0, desc: '仍在答题' },
|
||
{ key: 'paused', label: '已暂停', value: overview.pausedAssessments || 0, desc: '等待恢复' },
|
||
{ key: 'invalid', label: '已作废', value: overview.invalidAssessments || 0, desc: '无效测评' },
|
||
{ key: 'reports', label: '报告数量', value: overview.generatedReports || 0, desc: '已生成报告' },
|
||
{ key: 'participants', label: '覆盖人数', value: overview.uniqueParticipants || 0, desc: '参测人员数' }
|
||
]
|
||
}
|
||
},
|
||
watch: {
|
||
activeTab(val) {
|
||
if (val === 'overview') {
|
||
this.$nextTick(() => this.renderCharts())
|
||
}
|
||
if (val === 'scaleDept') {
|
||
this.$nextTick(() => this.renderScaleDeptCharts())
|
||
}
|
||
}
|
||
},
|
||
created() {
|
||
this.fetchScaleOptions()
|
||
this.loadStudentOptions('')
|
||
},
|
||
mounted() {
|
||
this.resizeHandler = () => {
|
||
Object.values(this.charts).forEach((chart) => {
|
||
if (chart) {
|
||
chart.resize()
|
||
}
|
||
})
|
||
}
|
||
window.addEventListener('resize', this.resizeHandler)
|
||
this.loadAnalytics()
|
||
},
|
||
beforeDestroy() {
|
||
window.removeEventListener('resize', this.resizeHandler)
|
||
this.disposeCharts()
|
||
},
|
||
methods: {
|
||
createEmptyAnalytics() {
|
||
return {
|
||
overview: {},
|
||
statusDistribution: [],
|
||
scaleDistribution: [],
|
||
scoreRangeDistribution: [],
|
||
monthlyTrend: []
|
||
}
|
||
},
|
||
createEmptyScaleStats() {
|
||
return {
|
||
totalTargets: 0,
|
||
measuredCount: 0,
|
||
unmeasuredCount: 0,
|
||
completionRate: 0,
|
||
resultDistribution: []
|
||
}
|
||
},
|
||
formatNumber(value) {
|
||
if (!value) {
|
||
return '0'
|
||
}
|
||
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||
},
|
||
formatPercent(value) {
|
||
if (!value) {
|
||
return '0%'
|
||
}
|
||
return `${parseFloat(value).toFixed(2)}%`
|
||
},
|
||
formatDateTime(value) {
|
||
return value ? parseTime(value) : '-'
|
||
},
|
||
buildQueryParams() {
|
||
const params = {}
|
||
if (this.queryParams.scaleId) {
|
||
params.scaleId = this.queryParams.scaleId
|
||
}
|
||
if (this.queryParams.dateRange && this.queryParams.dateRange.length === 2) {
|
||
params.startDate = this.queryParams.dateRange[0]
|
||
params.endDate = this.queryParams.dateRange[1]
|
||
}
|
||
return params
|
||
},
|
||
handleQuery() {
|
||
this.loadAnalytics()
|
||
},
|
||
resetQuery() {
|
||
this.queryParams.scaleId = undefined
|
||
this.queryParams.dateRange = []
|
||
this.handleQuery()
|
||
},
|
||
fetchScaleOptions() {
|
||
listScale({ pageNum: 1, pageSize: 999 }).then((res) => {
|
||
this.scaleOptions = res.rows || []
|
||
})
|
||
},
|
||
loadAnalytics() {
|
||
this.loading = true
|
||
const params = this.buildQueryParams()
|
||
getAssessmentAnalytics(params)
|
||
.then((res) => {
|
||
const data = res.data || this.createEmptyAnalytics()
|
||
this.analytics = Object.assign(this.createEmptyAnalytics(), data)
|
||
this.$nextTick(() => {
|
||
this.renderCharts()
|
||
})
|
||
})
|
||
.finally(() => {
|
||
this.loading = false
|
||
})
|
||
},
|
||
loadStudentOptions(query) {
|
||
this.studentOptionsLoading = true
|
||
getStudentOptions({ keyword: query, limit: 20 })
|
||
.then((res) => {
|
||
this.studentOptions = res.data || []
|
||
})
|
||
.finally(() => {
|
||
this.studentOptionsLoading = false
|
||
})
|
||
},
|
||
handleStudentChange() {
|
||
if (!this.selectedStudentId) {
|
||
this.studentSummary = null
|
||
this.studentScales = []
|
||
this.openedScalePanels = []
|
||
}
|
||
},
|
||
resetStudentSelection() {
|
||
this.selectedStudentId = undefined
|
||
this.studentSummary = null
|
||
this.studentScales = []
|
||
this.openedScalePanels = []
|
||
},
|
||
fetchUserSummary() {
|
||
if (!this.selectedStudentId) {
|
||
return
|
||
}
|
||
this.studentLoading = true
|
||
getUserAssessmentSummary(this.selectedStudentId)
|
||
.then((res) => {
|
||
this.studentSummary = res.data || null
|
||
const scales = (this.studentSummary && this.studentSummary.scales) ? this.studentSummary.scales : []
|
||
this.studentScales = scales.map((scale) => ({
|
||
...scale,
|
||
selectedAssessmentId:
|
||
scale.latestAssessmentId || (scale.attempts && scale.attempts.length ? scale.attempts[0].assessmentId : undefined)
|
||
}))
|
||
this.openedScalePanels = this.studentScales.map((item) => item.scaleId)
|
||
})
|
||
.finally(() => {
|
||
this.studentLoading = false
|
||
})
|
||
},
|
||
statusLabel(status) {
|
||
const map = {
|
||
0: '进行中',
|
||
1: '已完成',
|
||
2: '已作废',
|
||
3: '已暂停'
|
||
}
|
||
return map[status] || '未知'
|
||
},
|
||
statusTagType(status) {
|
||
const map = {
|
||
1: 'success',
|
||
0: 'warning',
|
||
3: 'info',
|
||
2: 'danger'
|
||
}
|
||
return map[status] || 'info'
|
||
},
|
||
getSelectedAttempt(scale) {
|
||
if (!scale || !scale.attempts) {
|
||
return null
|
||
}
|
||
return scale.attempts.find((item) => item.assessmentId === scale.selectedAssessmentId) || scale.attempts[0]
|
||
},
|
||
navigateReport(attempt) {
|
||
if (!attempt || !attempt.reportId) {
|
||
this.$modal.msgWarning('当前测评尚未生成报告')
|
||
return
|
||
}
|
||
this.$router.push({ path: '/psychology/assessment/report', query: { assessmentId: attempt.assessmentId } })
|
||
},
|
||
buildStudentLabel(option) {
|
||
if (!option) {
|
||
return ''
|
||
}
|
||
const name = option.nickName || option.userName || ''
|
||
const dept = option.deptName ? ` - ${option.deptName}` : ''
|
||
return name + dept
|
||
},
|
||
handleScaleDeptQuery() {
|
||
if (!this.scaleDeptForm.scaleId) {
|
||
this.$message.warning('请选择量表')
|
||
return
|
||
}
|
||
if (!this.scaleDeptForm.deptIds || this.scaleDeptForm.deptIds.length === 0) {
|
||
this.$message.warning('请至少选择一个监区')
|
||
return
|
||
}
|
||
this.scaleStatsLoading = true
|
||
getScaleDeptStats(this.scaleDeptForm)
|
||
.then((res) => {
|
||
this.scaleStats = Object.assign(this.createEmptyScaleStats(), res.data || {})
|
||
this.$nextTick(() => {
|
||
this.renderScaleDeptCharts()
|
||
})
|
||
})
|
||
.finally(() => {
|
||
this.scaleStatsLoading = false
|
||
})
|
||
},
|
||
resetScaleDeptForm() {
|
||
this.scaleDeptForm.scaleId = undefined
|
||
this.scaleDeptForm.deptIds = []
|
||
this.scaleStats = this.createEmptyScaleStats()
|
||
this.$nextTick(() => this.renderScaleDeptCharts())
|
||
},
|
||
renderCharts() {
|
||
this.renderPie('statusPie', this.analytics.statusDistribution, '测评状态')
|
||
this.renderTrendChart(this.analytics.monthlyTrend, 'trendChart')
|
||
},
|
||
renderScaleDeptCharts() {
|
||
const completionData = [
|
||
{ name: '已测评', value: this.scaleStats.measuredCount || 0 },
|
||
{ name: '未测评', value: this.scaleStats.unmeasuredCount || 0 }
|
||
]
|
||
this.renderPie('scaleCompletionPie', completionData, '完成情况')
|
||
this.renderPie(
|
||
'scaleResultPie',
|
||
this.scaleStats.resultDistribution && this.scaleStats.resultDistribution.length
|
||
? this.scaleStats.resultDistribution
|
||
: [{ name: '暂无数据', value: 0 }],
|
||
'报告结果'
|
||
)
|
||
},
|
||
ensureChartInstance(refName) {
|
||
const dom = this.$refs[refName]
|
||
if (!dom) {
|
||
return null
|
||
}
|
||
if (this.charts[refName]) {
|
||
return this.charts[refName]
|
||
}
|
||
this.charts[refName] = echarts.init(dom, 'macarons')
|
||
return this.charts[refName]
|
||
},
|
||
renderPie(refName, sourceData, title) {
|
||
const chart = this.ensureChartInstance(refName)
|
||
if (!chart) {
|
||
return
|
||
}
|
||
const data = sourceData && sourceData.length > 0 ? sourceData : [{ name: '暂无数据', value: 0 }]
|
||
chart.setOption({
|
||
tooltip: {
|
||
trigger: 'item',
|
||
formatter: '{b} : {c} ({d}%)'
|
||
},
|
||
legend: {
|
||
bottom: 0,
|
||
left: 'center',
|
||
data: data.map((item) => item.name)
|
||
},
|
||
series: [
|
||
{
|
||
name: title,
|
||
type: 'pie',
|
||
radius: ['35%', '70%'],
|
||
center: ['50%', '45%'],
|
||
roseType: 'radius',
|
||
data: data,
|
||
label: {
|
||
formatter: '{b}\n{d}%'
|
||
}
|
||
}
|
||
]
|
||
})
|
||
},
|
||
renderTrendChart(sourceData, refName) {
|
||
const chart = this.ensureChartInstance(refName)
|
||
if (!chart) {
|
||
return
|
||
}
|
||
const list = sourceData && sourceData.length > 0 ? sourceData : [{ label: '暂无数据', value: 0 }]
|
||
chart.setOption({
|
||
tooltip: {
|
||
trigger: 'axis'
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '5%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: list.map((item) => item.label)
|
||
},
|
||
yAxis: {
|
||
type: 'value'
|
||
},
|
||
series: [
|
||
{
|
||
name: '测评次数',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: list.map((item) => item.value),
|
||
areaStyle: {
|
||
opacity: 0.15
|
||
},
|
||
lineStyle: {
|
||
width: 3
|
||
},
|
||
itemStyle: {
|
||
color: '#5B8FF9'
|
||
}
|
||
}
|
||
]
|
||
})
|
||
},
|
||
disposeCharts() {
|
||
Object.keys(this.charts).forEach((key) => {
|
||
if (this.charts[key]) {
|
||
this.charts[key].dispose()
|
||
this.charts[key] = null
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.assessment-analysis {
|
||
.tab-pane-body {
|
||
padding: 12px;
|
||
}
|
||
.filter-card {
|
||
margin-bottom: 16px;
|
||
}
|
||
.overview-row {
|
||
margin-bottom: 16px;
|
||
}
|
||
.chart-row {
|
||
margin-bottom: 16px;
|
||
}
|
||
.stat-card {
|
||
text-align: left;
|
||
margin-bottom: 16px;
|
||
.stat-title {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
}
|
||
.stat-value {
|
||
font-size: 26px;
|
||
font-weight: 600;
|
||
margin: 8px 0;
|
||
color: #303133;
|
||
}
|
||
.stat-desc {
|
||
font-size: 12px;
|
||
color: #c0c4cc;
|
||
}
|
||
}
|
||
.chart-card {
|
||
height: 380px;
|
||
.chart-container {
|
||
width: 100%;
|
||
height: 320px;
|
||
}
|
||
.trend-chart {
|
||
height: 320px;
|
||
}
|
||
}
|
||
.detail-title {
|
||
font-weight: 600;
|
||
}
|
||
.student-info-card {
|
||
margin-bottom: 16px;
|
||
}
|
||
.scale-collapse {
|
||
background: #fff;
|
||
}
|
||
.scale-attempts {
|
||
.attempt-radio-group {
|
||
margin-bottom: 12px;
|
||
}
|
||
}
|
||
.attempt-detail {
|
||
border: 1px dashed #ebeef5;
|
||
padding: 12px;
|
||
border-radius: 4px;
|
||
background: #fafafa;
|
||
.attempt-detail__row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 12px;
|
||
.attempt-field {
|
||
width: 220px;
|
||
margin-right: 24px;
|
||
span {
|
||
color: #909399;
|
||
display: block;
|
||
}
|
||
strong {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
}
|
||
.attempt-summary {
|
||
margin-bottom: 12px;
|
||
.summary-title {
|
||
font-weight: 600;
|
||
margin-bottom: 6px;
|
||
}
|
||
.summary-content {
|
||
color: #606266;
|
||
}
|
||
}
|
||
}
|
||
.student-option {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
.name {
|
||
font-weight: 500;
|
||
}
|
||
.dept {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
|