1130 lines
35 KiB
Vue
1130 lines
35 KiB
Vue
<template>
|
||
<view class="page" v-if="!isH5">
|
||
<view class="card">
|
||
<view class="filters">
|
||
<view class="chip" :class="{ active: range === '7d' }" @click="setRange('7d')">近7天</view>
|
||
<view class="chip" :class="{ active: range === '1m' }" @click="setRange('1m')">近1月</view>
|
||
<view class="chip" :class="{ active: range === '3m' }" @click="setRange('3m')">近3月</view>
|
||
<view class="chip" :class="{ active: range === 'all' }" @click="setRange('all')">全部时间</view>
|
||
</view>
|
||
<view class="filters" style="margin-top: 14rpx;">
|
||
<picker mode="selector" :range="prisonAreaOptions" :value="selectedPrisonAreaIndex" @change="onPrisonAreaChange">
|
||
<view class="chip" :class="{ active: selectedPrisonAreaIndex !== 0 }">{{ selectedPrisonAreaLabel }}</view>
|
||
</picker>
|
||
<view v-if="selectedPrisonAreaIndex !== 0" class="chip" @click="clearPrisonArea">清除</view>
|
||
</view>
|
||
<view v-if="errorMsg" class="error">{{ errorMsg }}</view>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<view class="card-title">概览指标</view>
|
||
<view class="stats">
|
||
<view class="stat">
|
||
<view class="num">{{ displayedOverview.totalAssessments }}</view>
|
||
<view class="lab">测评总数</view>
|
||
</view>
|
||
<view class="stat">
|
||
<view class="num">{{ displayedOverview.completedAssessments }}</view>
|
||
<view class="lab">已完成</view>
|
||
</view>
|
||
<view class="stat">
|
||
<view class="num">{{ displayedOverview.uniqueParticipants }}</view>
|
||
<view class="lab">参与人数</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<view class="card-title">趋势图(折线)</view>
|
||
<view class="chart-box">
|
||
<qiun-data-charts type="line" :opts="chartOpts" :chartData="lineChartData" canvasId="lineChart" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<view class="card-title">对比图(柱状)</view>
|
||
<view class="chart-box">
|
||
<qiun-data-charts type="column" :opts="chartOpts" :chartData="barChartData" canvasId="barChart" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card">
|
||
<view class="card-title">占比图(环形)</view>
|
||
<view class="chart-box">
|
||
<qiun-data-charts type="ring" :opts="ringOpts" :chartData="ringChartData" canvasId="ringChart" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="panel">
|
||
<view class="card-title">热力图(监区×风险等级)</view>
|
||
<view v-if="!isH5" class="chart-box heat">
|
||
<qiun-data-charts type="demotype" :echartsApp="true" :eopts="heatEopts" :opts="chartOpts" :chartData="heatChartData" canvasId="heatChart" :canvas2d="true" />
|
||
</view>
|
||
<view v-else class="state">
|
||
<uni-icons type="info" size="34" color="#CBD5E1"></uni-icons>
|
||
<view class="state-text">H5 暂不支持热力图展示</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="panel">
|
||
<view class="card-title">监区统计</view>
|
||
<view v-if="loading" class="state">
|
||
<uni-icons type="spinner-cycle" size="28" color="#94A3B8"></uni-icons>
|
||
<view class="state-text">加载中...</view>
|
||
</view>
|
||
<view v-else>
|
||
<view v-if="displayedDeptRows.length === 0" class="state">
|
||
<uni-icons type="home" size="34" color="#CBD5E1"></uni-icons>
|
||
<view class="state-text">暂无数据</view>
|
||
</view>
|
||
<view v-else class="list">
|
||
<view class="item" v-for="(row, idx) in displayedDeptRows" :key="idx">
|
||
<view class="item-title">{{ row.deptName || ('部门#' + row.deptId) }}</view>
|
||
<view class="item-desc">
|
||
测评 {{ row.totalAssessments }} · 完成 {{ row.completedAssessments }} · 报告 {{ row.generatedReports || 0 }} · 参与 {{ row.uniqueParticipants }}
|
||
</view>
|
||
<view class="subblock" v-if="riskPairs(row).length">
|
||
<view class="sub-title">风险等级占比</view>
|
||
<view class="chips">
|
||
<view
|
||
class="chip risk"
|
||
:class="riskClass(item.level)"
|
||
v-for="(item, rIdx) in riskPairs(row)"
|
||
:key="rIdx"
|
||
>
|
||
{{ riskLabel(item.level) }} {{ item.count }} ({{ formatPercent(item.percent) }})
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="subblock" v-if="(row.topProblems || []).length">
|
||
<view class="sub-title">重点心理问题 Top{{ (row.topProblems || []).length }}</view>
|
||
<view class="problems">
|
||
<view class="problem" v-for="(p, pIdx) in row.topProblems" :key="pIdx">
|
||
<view class="problem-name">{{ p.problemName }}</view>
|
||
<view class="problem-count">{{ p.userCount }}人</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-else class="page big">
|
||
<view class="big-top">
|
||
<view class="big-brand">
|
||
<view class="big-title">心理数据研判平台</view>
|
||
<view class="big-sub">数据概览 · 监区看板</view>
|
||
</view>
|
||
<view class="big-top-actions">
|
||
<view class="big-chip" :class="{ active: range === '7d' }" @click="setRange('7d')">近7天</view>
|
||
<view class="big-chip" :class="{ active: range === '1m' }" @click="setRange('1m')">近1月</view>
|
||
<view class="big-chip" :class="{ active: range === '3m' }" @click="setRange('3m')">近3月</view>
|
||
<view class="big-chip" :class="{ active: range === 'all' }" @click="setRange('all')">全部</view>
|
||
<picker mode="selector" :range="prisonAreaOptions" :value="selectedPrisonAreaIndex" @change="onPrisonAreaChange">
|
||
<view class="big-chip" :class="{ active: selectedPrisonAreaIndex !== 0 }">{{ selectedPrisonAreaLabel }}</view>
|
||
</picker>
|
||
<view v-if="selectedPrisonAreaIndex !== 0" class="big-chip" @click="clearPrisonArea">清除</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="errorMsg" class="big-error">{{ errorMsg }}</view>
|
||
|
||
<view class="big-grid">
|
||
<view class="big-col">
|
||
<view class="big-panel">
|
||
<view class="big-panel-title">报告占比</view>
|
||
<view class="big-chart">
|
||
<qiun-data-charts type="ring" :opts="ringOpts" :chartData="ringChartData" canvasId="ringChartBig" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="big-panel">
|
||
<view class="big-panel-title">测评趋势</view>
|
||
<view class="big-chart">
|
||
<qiun-data-charts type="line" :opts="chartOpts" :chartData="lineChartData" canvasId="lineChartBig" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="big-panel">
|
||
<view class="big-panel-title">概览指标</view>
|
||
<view class="big-kpis">
|
||
<view class="big-kpi">
|
||
<view class="big-kpi-num">{{ displayedOverview.totalAssessments }}</view>
|
||
<view class="big-kpi-lab">测评总数</view>
|
||
</view>
|
||
<view class="big-kpi">
|
||
<view class="big-kpi-num">{{ displayedOverview.completedAssessments }}</view>
|
||
<view class="big-kpi-lab">已完成</view>
|
||
</view>
|
||
<view class="big-kpi">
|
||
<view class="big-kpi-num">{{ displayedOverview.uniqueParticipants }}</view>
|
||
<view class="big-kpi-lab">参与人数</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="big-center">
|
||
<view class="big-hero">
|
||
<view class="big-hero-glow"></view>
|
||
<view class="big-hero-core">
|
||
<view class="big-hero-title">快速导航</view>
|
||
<view class="big-nav">
|
||
<view class="big-nav-item" @tap="goWarning">
|
||
<uni-icons type="notification" size="24" color="#74d8ff"></uni-icons>
|
||
<view class="big-nav-text">预警中心</view>
|
||
</view>
|
||
<view class="big-nav-item" @tap="goProfile">
|
||
<uni-icons type="person" size="24" color="#74d8ff"></uni-icons>
|
||
<view class="big-nav-text">个体画像</view>
|
||
</view>
|
||
<view class="big-nav-item" @tap="goComprehensive">
|
||
<uni-icons type="paperplane" size="24" color="#74d8ff"></uni-icons>
|
||
<view class="big-nav-text">综合报告</view>
|
||
</view>
|
||
<view class="big-nav-item" @tap="goChartTemplates">
|
||
<uni-icons type="bars" size="24" color="#74d8ff"></uni-icons>
|
||
<view class="big-nav-text">自定义图表</view>
|
||
</view>
|
||
<view class="big-nav-item" @tap="goInterventionTasks">
|
||
<uni-icons type="flag" size="24" color="#74d8ff"></uni-icons>
|
||
<view class="big-nav-text">干预任务</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="big-col">
|
||
<view class="big-panel">
|
||
<view class="big-panel-title">报告对比</view>
|
||
<view class="big-chart">
|
||
<qiun-data-charts type="column" :opts="chartOpts" :chartData="barChartData" canvasId="barChartBig" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="big-panel">
|
||
<view class="big-panel-title">监区统计</view>
|
||
<view v-if="loading" class="big-state">
|
||
<uni-icons type="spinner-cycle" size="28" color="#74d8ff"></uni-icons>
|
||
<view class="big-state-text">加载中...</view>
|
||
</view>
|
||
<view v-else>
|
||
<view v-if="displayedDeptRows.length === 0" class="big-state">
|
||
<uni-icons type="info" size="30" color="#74d8ff"></uni-icons>
|
||
<view class="big-state-text">暂无数据</view>
|
||
</view>
|
||
<view v-else class="big-list">
|
||
<view class="big-item" v-for="(row, idx) in displayedDeptRows" :key="idx">
|
||
<view class="big-item-title">{{ row.deptName || ('部门#' + row.deptId) }}</view>
|
||
<view class="big-item-desc">测评 {{ row.totalAssessments }} · 完成 {{ row.completedAssessments }} · 报告 {{ row.generatedReports || 0 }} · 参与 {{ row.uniqueParticipants }}</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { request } from '../../utils/request'
|
||
import QiunDataCharts from '../../uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
|
||
import UniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue'
|
||
|
||
export default {
|
||
components: {
|
||
QiunDataCharts,
|
||
UniIcons
|
||
},
|
||
data() {
|
||
return {
|
||
range: '7d',
|
||
isH5: false,
|
||
loading: false,
|
||
errorMsg: '',
|
||
selectedPrisonAreaIndex: 0,
|
||
selectedPrisonArea: '',
|
||
prisonAreas: [],
|
||
overview: {
|
||
totalAssessments: 0,
|
||
completedAssessments: 0,
|
||
uniqueParticipants: 0
|
||
},
|
||
deptStats: [],
|
||
deptOverview: [],
|
||
analytics: null,
|
||
lineEopts: {},
|
||
barEopts: {},
|
||
ringEopts: {},
|
||
heatEopts: {},
|
||
chartOpts: {
|
||
timing: 'easeOut',
|
||
duration: 800,
|
||
yAxis: {
|
||
min: 0,
|
||
splitNumber: 5,
|
||
format: (val) => {
|
||
const n = Number(val)
|
||
if (!isFinite(n)) return '0'
|
||
return String(Math.round(n))
|
||
}
|
||
}
|
||
},
|
||
ringOpts: {
|
||
timing: 'easeOut',
|
||
duration: 800,
|
||
title: { name: '预警报告占比' },
|
||
extra: { ring: { ringWidth: 18, offsetAngle: -90 } }
|
||
},
|
||
emptyChartData: { categories: [' '], series: [{ name: ' ', data: [0] }] },
|
||
lineChartData: { categories: [' '], series: [{ name: '测评次数', data: [0] }] },
|
||
barChartData: { categories: [' '], series: [{ name: '报告数', data: [0] }] },
|
||
ringChartData: { series: [{ data: [{ name: '正常报告', value: 0 }, { name: '预警报告', value: 0 }] }] },
|
||
heatChartData: { categories: [' '], series: [{ name: ' ', type: 'heatmap', data: [[0, 0, 0]] }] }
|
||
}
|
||
},
|
||
computed: {
|
||
prisonAreaOptions() {
|
||
if (this.prisonAreas && this.prisonAreas.length) {
|
||
return ['全部监区'].concat(this.prisonAreas)
|
||
}
|
||
const rows = this.deptRows || []
|
||
const names = rows.map((r) => (r && r.deptName) ? r.deptName : String((r && r.deptId) || ''))
|
||
const uniq = []
|
||
names.forEach((n) => {
|
||
if (!n) return
|
||
if (uniq.indexOf(n) === -1) uniq.push(n)
|
||
})
|
||
return ['全部监区'].concat(uniq)
|
||
},
|
||
selectedPrisonAreaLabel() {
|
||
if (this.selectedPrisonAreaIndex === 0) return '全部监区'
|
||
return this.prisonAreaOptions[this.selectedPrisonAreaIndex] || '全部监区'
|
||
},
|
||
deptRows() {
|
||
const overviewRows = (this.deptOverview && this.deptOverview.length) ? this.deptOverview : []
|
||
// deptOverview 接口在前端请求时可能带 topN,导致未返回所选监区。
|
||
// 若用户已选择监区且 overviewRows 不包含该监区,则回退使用全量 deptStats。
|
||
if (overviewRows.length) {
|
||
if (!this.selectedPrisonArea) {
|
||
return overviewRows
|
||
}
|
||
const sel = String(this.selectedPrisonArea || '').trim()
|
||
const hit = overviewRows.some((r) => r && String(r.deptName || '').trim() === sel)
|
||
if (hit) {
|
||
return overviewRows
|
||
}
|
||
}
|
||
return (this.deptStats || []).map((row) => {
|
||
const total = row && row.totalAssessments ? Number(row.totalAssessments) : 0
|
||
const completed = row && row.completedAssessments ? Number(row.completedAssessments) : 0
|
||
const rate = total > 0 ? (completed * 100.0) / total : 0
|
||
return Object.assign({}, row, { completionRate: Math.round(rate * 100) / 100 })
|
||
})
|
||
},
|
||
selectedDeptRow() {
|
||
if (!this.selectedPrisonArea) return null
|
||
const rows = this.deptRows || []
|
||
const sel = String(this.selectedPrisonArea || '').trim()
|
||
return rows.find((r) => r && (String(r.deptName || '').trim() === sel)) || null
|
||
},
|
||
displayedDeptRows() {
|
||
if (!this.selectedPrisonArea) return this.deptRows || []
|
||
const sel = String(this.selectedPrisonArea || '').trim()
|
||
return (this.deptRows || []).filter((r) => r && String(r.deptName || '').trim() === sel)
|
||
},
|
||
displayedOverview() {
|
||
const rows = this.displayedDeptRows || []
|
||
if (this.selectedPrisonArea) {
|
||
const o = (this.analytics && this.analytics.overview) ? this.analytics.overview : null
|
||
if (o) {
|
||
return o
|
||
}
|
||
if (rows.length === 1) {
|
||
return rows[0] || this.overview
|
||
}
|
||
}
|
||
return this.overview
|
||
}
|
||
},
|
||
onLoad() {
|
||
try {
|
||
const info = uni.getSystemInfoSync()
|
||
this.isH5 = info && info.uniPlatform === 'web'
|
||
} catch (e) {
|
||
this.isH5 = false
|
||
}
|
||
this.fetchPrisonAreas().finally(() => {
|
||
this.fetchAll()
|
||
})
|
||
},
|
||
methods: {
|
||
fetchPrisonAreas() {
|
||
return request({
|
||
url: '/psychology/profile/student/list',
|
||
method: 'GET',
|
||
data: {
|
||
pageNum: 1,
|
||
pageSize: 200
|
||
}
|
||
}).then((res) => {
|
||
const data = res && res.data ? res.data : null
|
||
if (!data || data.code !== 200) {
|
||
return
|
||
}
|
||
const rows = data.rows || []
|
||
const uniq = []
|
||
rows.forEach((r) => {
|
||
const v = r && r.prisonArea ? String(r.prisonArea).trim() : ''
|
||
if (!v) return
|
||
if (uniq.indexOf(v) === -1) uniq.push(v)
|
||
})
|
||
this.prisonAreas = uniq
|
||
}).catch(() => {
|
||
// ignore
|
||
})
|
||
},
|
||
onPrisonAreaChange(e) {
|
||
const idx = e && e.detail ? Number(e.detail.value) : 0
|
||
this.selectedPrisonAreaIndex = isNaN(idx) ? 0 : idx
|
||
if (this.selectedPrisonAreaIndex === 0) {
|
||
this.selectedPrisonArea = ''
|
||
this.fetchAll()
|
||
return
|
||
}
|
||
this.selectedPrisonArea = this.prisonAreaOptions[this.selectedPrisonAreaIndex] || ''
|
||
this.fetchAll()
|
||
},
|
||
clearPrisonArea() {
|
||
this.selectedPrisonAreaIndex = 0
|
||
this.selectedPrisonArea = ''
|
||
this.fetchAll()
|
||
},
|
||
setRange(r) {
|
||
if (this.loading) return
|
||
this.range = r
|
||
this.fetchAll()
|
||
},
|
||
formatDate(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}`
|
||
},
|
||
getDateRange() {
|
||
if (this.range === 'all') {
|
||
return {
|
||
startDate: '',
|
||
endDate: ''
|
||
}
|
||
}
|
||
const end = new Date()
|
||
const start = new Date()
|
||
if (this.range === '7d') start.setDate(end.getDate() - 7)
|
||
if (this.range === '1m') start.setMonth(end.getMonth() - 1)
|
||
if (this.range === '3m') start.setMonth(end.getMonth() - 3)
|
||
return {
|
||
startDate: this.formatDate(start),
|
||
endDate: this.formatDate(end)
|
||
}
|
||
},
|
||
buildQuery({ startDate, endDate, prisonArea }) {
|
||
const qs = []
|
||
if (startDate) qs.push(`startDate=${encodeURIComponent(startDate)}`)
|
||
if (endDate) qs.push(`endDate=${encodeURIComponent(endDate)}`)
|
||
if (prisonArea) qs.push(`prisonArea=${encodeURIComponent(prisonArea)}`)
|
||
return qs.length ? `?${qs.join('&')}` : ''
|
||
},
|
||
fetchAll() {
|
||
this.errorMsg = ''
|
||
this.loading = true
|
||
const { startDate, endDate } = this.getDateRange()
|
||
const prisonArea = this.selectedPrisonArea ? String(this.selectedPrisonArea).trim() : ''
|
||
const query = this.buildQuery({ startDate, endDate, prisonArea })
|
||
Promise.all([
|
||
request({ url: `/psychology/assessment/analytics${query}`, method: 'GET' }),
|
||
request({ url: `/psychology/assessment/deptStats${query}`, method: 'GET' }),
|
||
request({ url: `/psychology/assessment/deptOverview${query}${query ? '&' : '?'}topN=5`, method: 'GET' })
|
||
]).then(([aRes, dRes, oRes]) => {
|
||
this.loading = false
|
||
const aData = aRes && aRes.data ? aRes.data : null
|
||
const dData = dRes && dRes.data ? dRes.data : null
|
||
const oData = oRes && oRes.data ? oRes.data : null
|
||
if (!aData || aData.code !== 200) {
|
||
this.errorMsg = (aData && aData.msg) ? aData.msg : '概览加载失败'
|
||
return
|
||
}
|
||
this.analytics = aData.data || null
|
||
this.overview = (aData.data && aData.data.overview) ? aData.data.overview : this.overview
|
||
if (dData && dData.code === 200) {
|
||
this.deptStats = dData.data || []
|
||
} else {
|
||
this.deptStats = []
|
||
this.errorMsg = (dData && dData.msg) ? dData.msg : (this.errorMsg || '监区统计加载失败')
|
||
}
|
||
if (oData && oData.code === 200) {
|
||
this.deptOverview = (oData.data || []).map((row) => {
|
||
const merged = Object.assign({}, row)
|
||
if (!merged.totalAssessments || !merged.completedAssessments || !merged.uniqueParticipants) {
|
||
const fallback = (this.deptStats || []).find((x) => (x && row) && (x.deptId === row.deptId || x.deptName === row.deptName))
|
||
if (fallback) {
|
||
merged.totalAssessments = merged.totalAssessments || fallback.totalAssessments
|
||
merged.completedAssessments = merged.completedAssessments || fallback.completedAssessments
|
||
merged.uniqueParticipants = merged.uniqueParticipants || fallback.uniqueParticipants
|
||
}
|
||
}
|
||
return merged
|
||
})
|
||
} else {
|
||
this.deptOverview = []
|
||
this.errorMsg = (oData && oData.msg) ? oData.msg : (this.errorMsg || '监区概览加载失败')
|
||
}
|
||
|
||
this.buildCharts()
|
||
}).catch((e) => {
|
||
this.loading = false
|
||
this.errorMsg = e && e.message ? e.message : '网络错误'
|
||
})
|
||
},
|
||
formatPercent(value) {
|
||
const num = Number(value || 0)
|
||
return `${num.toFixed(2)}%`
|
||
},
|
||
buildCharts() {
|
||
this.lineChartData = this.buildTrendLineChartData()
|
||
this.barChartData = this.buildDeptCompareBarChartData()
|
||
this.ringChartData = this.buildRiskRingChartData()
|
||
this.syncRingCenterText()
|
||
this.lineEopts = this.buildTrendLineEopts()
|
||
this.barEopts = this.buildDeptCompareBarEopts()
|
||
this.ringEopts = this.buildRiskRingEopts()
|
||
this.heatEopts = this.buildRiskHeatmapEopts()
|
||
},
|
||
syncRingCenterText() {
|
||
const overview = this.displayedOverview || this.overview || {}
|
||
const total = Number(overview.generatedReports || 0)
|
||
const warning = Number(overview.warningReports || 0)
|
||
const pct = total > 0 ? Math.round((warning * 10000) / total) / 100 : 0
|
||
this.ringOpts = Object.assign({}, this.ringOpts, {
|
||
title: Object.assign({}, (this.ringOpts && this.ringOpts.title) ? this.ringOpts.title : {}, { name: '预警报告占比' }),
|
||
subtitle: { name: `${pct}%` }
|
||
})
|
||
},
|
||
buildTrendLineChartData() {
|
||
const list = (this.analytics && this.analytics.monthlyTrend) ? this.analytics.monthlyTrend : []
|
||
const categories = (list && list.length) ? list.map((it) => it.label) : ['暂无数据']
|
||
const data = (list && list.length) ? list.map((it) => Number(it.value || 0)) : [0]
|
||
return { categories, series: [{ name: '测评次数', data }] }
|
||
},
|
||
buildDeptCompareBarChartData() {
|
||
const rows = this.displayedDeptRows || []
|
||
const categories = rows.length ? rows.map((r) => r.deptName || String(r.deptId || '')) : ['暂无数据']
|
||
const data = rows.length ? rows.map((r) => Number(r.generatedReports || 0)) : [0]
|
||
return { categories, series: [{ name: '报告数', data }] }
|
||
},
|
||
buildRiskRingChartData() {
|
||
const overview = this.displayedOverview || this.overview || {}
|
||
const totalReports = Number(overview.generatedReports || 0)
|
||
const warningReports = Number(overview.warningReports || 0)
|
||
const normalReports = Math.max(totalReports - warningReports, 0)
|
||
const data = [
|
||
{ name: '预警报告', value: warningReports },
|
||
{ name: '正常报告', value: normalReports }
|
||
].filter((it) => it.value > 0)
|
||
return { series: [{ data: data.length ? data : [{ name: '暂无数据', value: 0 }] }] }
|
||
},
|
||
buildTrendLineEopts() {
|
||
const list = (this.analytics && this.analytics.monthlyTrend) ? this.analytics.monthlyTrend : []
|
||
const x = (list && list.length) ? list.map((it) => it.label) : ['暂无数据']
|
||
const y = (list && list.length) ? list.map((it) => Number(it.value || 0)) : [0]
|
||
return {
|
||
tooltip: { trigger: 'axis' },
|
||
grid: { left: 40, right: 18, top: 24, bottom: 40 },
|
||
xAxis: { type: 'category', data: x, boundaryGap: false, axisLabel: { color: '#6B7280' } },
|
||
yAxis: { type: 'value', axisLabel: { color: '#6B7280' }, splitLine: { lineStyle: { color: 'rgba(15,23,42,0.08)' } } },
|
||
series: [{
|
||
name: '测评次数',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: y,
|
||
areaStyle: { opacity: 0.12 },
|
||
lineStyle: { width: 3, color: '#2B6BFF' },
|
||
itemStyle: { color: '#2B6BFF' }
|
||
}]
|
||
}
|
||
},
|
||
buildDeptCompareBarEopts() {
|
||
const rows = this.displayedDeptRows || []
|
||
const x = rows.map((r) => r.deptName || String(r.deptId || ''))
|
||
const v = rows.map((r) => Number(r.generatedReports || 0))
|
||
return {
|
||
tooltip: { trigger: 'axis' },
|
||
grid: { left: 40, right: 18, top: 24, bottom: 80 },
|
||
xAxis: { type: 'category', data: x.length ? x : ['暂无数据'], axisLabel: { color: '#6B7280', rotate: 35 } },
|
||
yAxis: { type: 'value', axisLabel: { color: '#6B7280' }, splitLine: { lineStyle: { color: 'rgba(15,23,42,0.08)' } } },
|
||
series: [{ name: '报告数', type: 'bar', data: v.length ? v : [0], barWidth: '50%', itemStyle: { color: '#7C5CFF', borderRadius: [8, 8, 0, 0] } }]
|
||
}
|
||
},
|
||
buildRiskRingEopts() {
|
||
const overview = this.displayedOverview || this.overview || {}
|
||
const totalReports = Number(overview.generatedReports || 0)
|
||
const warningReports = Number(overview.warningReports || 0)
|
||
const normalReports = Math.max(totalReports - warningReports, 0)
|
||
const data = [
|
||
{ name: '预警报告', value: warningReports },
|
||
{ name: '正常报告', value: normalReports }
|
||
].filter((it) => it.value > 0)
|
||
return {
|
||
tooltip: { trigger: 'item' },
|
||
legend: { bottom: 0, left: 'center', textStyle: { color: '#4B5563' } },
|
||
series: [{
|
||
name: '预警报告占比',
|
||
type: 'pie',
|
||
radius: ['40%', '70%'],
|
||
center: ['50%', '45%'],
|
||
data: data.length ? data : [{ name: '暂无数据', value: 0 }],
|
||
label: { formatter: '{b}\n{d}%' }
|
||
}]
|
||
}
|
||
},
|
||
buildRiskHeatmapEopts() {
|
||
const rows = this.displayedDeptRows || []
|
||
const depts = rows.map((r) => r.deptName || String(r.deptId || ''))
|
||
const levels = ['严重', '高', '中', '低', '无']
|
||
const keyMap = { '严重': 'critical', '高': 'high', '中': 'medium', '低': 'low', '无': 'none' }
|
||
const data = []
|
||
rows.forEach((r, i) => {
|
||
const m = (r && r.riskLevelCounts) ? r.riskLevelCounts : {}
|
||
levels.forEach((lv, j) => {
|
||
const k = keyMap[lv]
|
||
data.push([i, j, Number(m[k] || 0)])
|
||
})
|
||
})
|
||
const maxVal = data.reduce((mx, it) => Math.max(mx, Number(it[2] || 0)), 0)
|
||
return {
|
||
tooltip: {
|
||
position: 'top',
|
||
formatter: (p) => {
|
||
const v = p && p.value ? p.value : []
|
||
const dept = depts[v[0]] || ''
|
||
const lvl = levels[v[1]] || ''
|
||
return `${dept}<br/>${lvl}:${v[2]}`
|
||
}
|
||
},
|
||
grid: { top: 10, left: 70, right: 18, bottom: 60 },
|
||
xAxis: { type: 'category', data: depts.length ? depts : ['暂无数据'], axisLabel: { color: '#6B7280', rotate: 35 } },
|
||
yAxis: { type: 'category', data: levels, axisLabel: { color: '#6B7280' } },
|
||
visualMap: {
|
||
min: 0,
|
||
max: maxVal || 1,
|
||
calculable: false,
|
||
orient: 'horizontal',
|
||
left: 'center',
|
||
bottom: 10,
|
||
inRange: { color: ['#F3F4F6', '#A5B4FC', '#7C5CFF', '#2B6BFF'] }
|
||
},
|
||
series: [{
|
||
name: '风险热力',
|
||
type: 'heatmap',
|
||
data,
|
||
emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.25)' } },
|
||
label: { show: false }
|
||
}]
|
||
}
|
||
},
|
||
riskLabel(level) {
|
||
const map = {
|
||
critical: '严重',
|
||
high: '高',
|
||
medium: '中',
|
||
low: '低',
|
||
none: '无'
|
||
}
|
||
return map[level] || level || '未知'
|
||
},
|
||
riskClass(level) {
|
||
const map = {
|
||
critical: 'critical',
|
||
high: 'high',
|
||
medium: 'medium',
|
||
low: 'low',
|
||
none: 'none'
|
||
}
|
||
return map[level] || 'none'
|
||
},
|
||
riskPairs(row) {
|
||
const counts = (row && row.riskLevelCounts) ? row.riskLevelCounts : null
|
||
if (!counts) return []
|
||
const entries = Object.keys(counts).map((k) => ({ level: k, count: Number(counts[k] || 0) }))
|
||
const total = entries.reduce((sum, it) => sum + it.count, 0)
|
||
return entries
|
||
.filter((it) => it.count > 0)
|
||
.sort((a, b) => b.count - a.count)
|
||
.map((it) => Object.assign({}, it, { percent: total > 0 ? (it.count * 100.0) / total : 0 }))
|
||
},
|
||
goWarning() {
|
||
uni.navigateTo({
|
||
url: '/pages/warning/index'
|
||
})
|
||
},
|
||
goProfile() {
|
||
uni.navigateTo({
|
||
url: '/pages/profile/index'
|
||
})
|
||
},
|
||
goComprehensive() {
|
||
uni.navigateTo({
|
||
url: '/pages/comprehensive/index'
|
||
})
|
||
},
|
||
goChartTemplates() {
|
||
uni.navigateTo({
|
||
url: '/pages/chart/templates'
|
||
})
|
||
},
|
||
goInterventionTasks() {
|
||
uni.navigateTo({
|
||
url: '/pages/interventionTask/index'
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.page {
|
||
min-height: 100vh;
|
||
padding: 24rpx 24rpx 120rpx;
|
||
box-sizing: border-box;
|
||
background: #F4F6FB;
|
||
}
|
||
.card,
|
||
.panel {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 18rpx;
|
||
padding: 24rpx;
|
||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||
box-shadow: 0 10rpx 22rpx rgba(15, 23, 42, 0.05);
|
||
margin-bottom: 18rpx;
|
||
}
|
||
.card-title {
|
||
font-size: 28rpx;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
margin-bottom: 14rpx;
|
||
}
|
||
.filters {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
}
|
||
.chip {
|
||
padding: 14rpx 22rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 26rpx;
|
||
line-height: 36rpx;
|
||
min-height: 60rpx;
|
||
box-sizing: border-box;
|
||
color: #64748B;
|
||
background: rgba(15, 23, 42, 0.04);
|
||
margin-right: 16rpx;
|
||
margin-bottom: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.chip.active {
|
||
color: #1677ff;
|
||
background: rgba(22, 119, 255, 0.12);
|
||
}
|
||
.stats {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
.stat {
|
||
width: 32%;
|
||
background: rgba(15, 23, 42, 0.04);
|
||
border-radius: 16rpx;
|
||
padding: 18rpx;
|
||
box-sizing: border-box;
|
||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||
}
|
||
.num {
|
||
font-size: 36rpx;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
}
|
||
.lab {
|
||
margin-top: 8rpx;
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
}
|
||
.list {
|
||
margin-top: 8rpx;
|
||
}
|
||
.item {
|
||
padding: 18rpx;
|
||
border-radius: 18rpx;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||
margin-bottom: 14rpx;
|
||
}
|
||
.item-title {
|
||
font-size: 26rpx;
|
||
font-weight: 700;
|
||
color: #111827;
|
||
}
|
||
.item-desc {
|
||
margin-top: 8rpx;
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
}
|
||
.subblock {
|
||
margin-top: 12rpx;
|
||
}
|
||
.sub-title {
|
||
font-size: 22rpx;
|
||
color: #64748B;
|
||
font-weight: 700;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
.chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
}
|
||
.chip.risk {
|
||
padding: 8rpx 14rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 22rpx;
|
||
margin-right: 12rpx;
|
||
margin-bottom: 10rpx;
|
||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||
background: rgba(15, 23, 42, 0.04);
|
||
color: #64748B;
|
||
}
|
||
.chip.risk.critical {
|
||
background: rgba(245, 108, 108, 0.12);
|
||
border-color: rgba(245, 108, 108, 0.25);
|
||
color: #F56C6C;
|
||
}
|
||
.chip.risk.high {
|
||
background: rgba(230, 162, 60, 0.12);
|
||
border-color: rgba(230, 162, 60, 0.25);
|
||
color: #E6A23C;
|
||
}
|
||
.chip.risk.medium {
|
||
background: rgba(64, 158, 255, 0.12);
|
||
border-color: rgba(64, 158, 255, 0.25);
|
||
color: #409EFF;
|
||
}
|
||
.chip.risk.low {
|
||
background: rgba(103, 194, 58, 0.12);
|
||
border-color: rgba(103, 194, 58, 0.25);
|
||
color: #67C23A;
|
||
}
|
||
.chip.risk.none {
|
||
background: rgba(144, 147, 153, 0.10);
|
||
border-color: rgba(144, 147, 153, 0.20);
|
||
color: #909399;
|
||
}
|
||
.problems {
|
||
margin-top: 6rpx;
|
||
}
|
||
.problem {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 10rpx 0;
|
||
border-bottom: 1px dashed #EBEEF5;
|
||
}
|
||
.problem:last-child {
|
||
border-bottom: none;
|
||
}
|
||
.problem-name {
|
||
font-size: 22rpx;
|
||
color: #111827;
|
||
}
|
||
.problem-count {
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
}
|
||
.chart-box { height: 320rpx; }
|
||
.chart-box.heat { height: 420rpx; }
|
||
.state {
|
||
height: 360rpx;
|
||
border-radius: 20rpx;
|
||
background: rgba(255, 255, 255, 0.85);
|
||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12rpx;
|
||
}
|
||
.state-text {
|
||
font-size: 24rpx;
|
||
color: #94A3B8;
|
||
font-weight: 700;
|
||
}
|
||
.error {
|
||
margin-top: 14rpx;
|
||
font-size: 24rpx;
|
||
color: #ff4d4f;
|
||
line-height: 36rpx;
|
||
}
|
||
|
||
.page.big {
|
||
padding: 22rpx 18rpx 40rpx;
|
||
background-image:
|
||
radial-gradient(1200rpx 600rpx at 50% 20%, rgba(43, 107, 255, 0.30) 0%, rgba(11, 19, 41, 0.0) 60%),
|
||
linear-gradient(180deg, rgba(5, 11, 24, 0.90) 0%, rgba(8, 20, 45, 0.85) 40%, rgba(6, 16, 40, 0.92) 100%),
|
||
url('/static/bg.png');
|
||
background-size: auto, auto, cover;
|
||
background-position: center, center, center;
|
||
background-repeat: no-repeat, no-repeat, no-repeat;
|
||
color: rgba(235, 248, 255, 0.92);
|
||
}
|
||
.big-top {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
padding: 10rpx 10rpx 16rpx;
|
||
border-bottom: 1px solid rgba(116, 216, 255, 0.18);
|
||
background: linear-gradient(90deg, rgba(17, 30, 58, 0.55) 0%, rgba(7, 13, 28, 0.15) 40%, rgba(17, 30, 58, 0.55) 100%);
|
||
border-radius: 14rpx;
|
||
}
|
||
.big-title {
|
||
font-size: 34rpx;
|
||
font-weight: 800;
|
||
letter-spacing: 2rpx;
|
||
color: #c9f2ff;
|
||
text-shadow: 0 0 16rpx rgba(116, 216, 255, 0.35);
|
||
}
|
||
.big-sub {
|
||
margin-top: 6rpx;
|
||
font-size: 22rpx;
|
||
color: rgba(201, 242, 255, 0.65);
|
||
}
|
||
.big-top-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-end;
|
||
gap: 10rpx;
|
||
max-width: 520rpx;
|
||
}
|
||
.big-chip {
|
||
padding: 12rpx 16rpx;
|
||
border-radius: 999rpx;
|
||
font-size: 22rpx;
|
||
line-height: 30rpx;
|
||
color: rgba(201, 242, 255, 0.70);
|
||
border: 1px solid rgba(116, 216, 255, 0.20);
|
||
background: rgba(10, 18, 38, 0.65);
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
.big-chip.active {
|
||
color: #0b1226;
|
||
background: linear-gradient(90deg, rgba(116, 216, 255, 0.95) 0%, rgba(43, 107, 255, 0.90) 100%);
|
||
border-color: rgba(116, 216, 255, 0.5);
|
||
}
|
||
.big-error {
|
||
margin-top: 12rpx;
|
||
padding: 14rpx 16rpx;
|
||
border-radius: 14rpx;
|
||
border: 1px solid rgba(255, 77, 79, 0.35);
|
||
background: rgba(255, 77, 79, 0.10);
|
||
font-size: 24rpx;
|
||
color: rgba(255, 220, 220, 0.92);
|
||
}
|
||
.big-grid {
|
||
margin-top: 18rpx;
|
||
display: flex;
|
||
gap: 14rpx;
|
||
}
|
||
.big-col {
|
||
width: 28%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14rpx;
|
||
}
|
||
.big-center {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.big-panel {
|
||
position: relative;
|
||
border-radius: 16rpx;
|
||
padding: 16rpx 16rpx 12rpx;
|
||
border: 1px solid rgba(116, 216, 255, 0.22);
|
||
background: linear-gradient(180deg, rgba(10, 18, 38, 0.75) 0%, rgba(5, 10, 22, 0.55) 100%);
|
||
box-shadow: 0 12rpx 24rpx rgba(0, 0, 0, 0.35);
|
||
overflow: hidden;
|
||
}
|
||
.big-panel:before {
|
||
content: '';
|
||
position: absolute;
|
||
left: -40rpx;
|
||
top: -40rpx;
|
||
width: 180rpx;
|
||
height: 180rpx;
|
||
background: radial-gradient(circle, rgba(116, 216, 255, 0.25) 0%, rgba(116, 216, 255, 0.0) 70%);
|
||
}
|
||
.big-panel-title {
|
||
position: relative;
|
||
font-size: 24rpx;
|
||
font-weight: 800;
|
||
color: rgba(201, 242, 255, 0.92);
|
||
padding-left: 14rpx;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
.big-panel-title:before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 10rpx;
|
||
width: 6rpx;
|
||
height: 100rpx;
|
||
border-radius: 6rpx;
|
||
background: linear-gradient(180deg, rgba(116, 216, 255, 0.95) 0%, rgba(43, 107, 255, 0.85) 100%);
|
||
}
|
||
.big-chart {
|
||
height: 260rpx;
|
||
}
|
||
.big-kpis {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 10rpx;
|
||
}
|
||
.big-kpi {
|
||
flex: 1;
|
||
min-width: 0;
|
||
border-radius: 14rpx;
|
||
padding: 14rpx 12rpx;
|
||
border: 1px solid rgba(116, 216, 255, 0.18);
|
||
background: rgba(7, 13, 28, 0.45);
|
||
}
|
||
.big-kpi-num {
|
||
font-size: 34rpx;
|
||
font-weight: 900;
|
||
color: #74d8ff;
|
||
text-shadow: 0 0 14rpx rgba(116, 216, 255, 0.35);
|
||
}
|
||
.big-kpi-lab {
|
||
margin-top: 8rpx;
|
||
font-size: 20rpx;
|
||
color: rgba(201, 242, 255, 0.65);
|
||
}
|
||
.big-hero {
|
||
position: relative;
|
||
height: 100%;
|
||
min-height: 820rpx;
|
||
border-radius: 18rpx;
|
||
border: 1px solid rgba(116, 216, 255, 0.26);
|
||
background: radial-gradient(600rpx 420rpx at 50% 35%, rgba(116, 216, 255, 0.20) 0%, rgba(116, 216, 255, 0.0) 70%),
|
||
linear-gradient(180deg, rgba(10, 18, 38, 0.55) 0%, rgba(5, 10, 22, 0.35) 100%);
|
||
box-shadow: 0 18rpx 36rpx rgba(0, 0, 0, 0.45);
|
||
overflow: hidden;
|
||
}
|
||
.big-hero-glow {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 42%;
|
||
width: 520rpx;
|
||
height: 520rpx;
|
||
transform: translate(-50%, -50%);
|
||
border-radius: 999rpx;
|
||
border: 1px solid rgba(116, 216, 255, 0.28);
|
||
box-shadow: 0 0 40rpx rgba(116, 216, 255, 0.22), inset 0 0 30rpx rgba(43, 107, 255, 0.18);
|
||
background: radial-gradient(circle, rgba(43, 107, 255, 0.20) 0%, rgba(43, 107, 255, 0.0) 65%);
|
||
opacity: 0.9;
|
||
}
|
||
.big-hero-core {
|
||
position: relative;
|
||
padding: 22rpx;
|
||
}
|
||
.big-hero-title {
|
||
font-size: 26rpx;
|
||
font-weight: 900;
|
||
color: rgba(201, 242, 255, 0.92);
|
||
margin-bottom: 18rpx;
|
||
}
|
||
.big-nav {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
}
|
||
.big-nav-item {
|
||
width: calc(33.33% - 11rpx);
|
||
border-radius: 16rpx;
|
||
border: 1px solid rgba(116, 216, 255, 0.22);
|
||
background: rgba(7, 13, 28, 0.35);
|
||
padding: 18rpx 12rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
.big-nav-item:active {
|
||
transform: scale(0.98);
|
||
border-color: rgba(116, 216, 255, 0.45);
|
||
}
|
||
.big-nav-text {
|
||
font-size: 22rpx;
|
||
font-weight: 800;
|
||
color: rgba(201, 242, 255, 0.86);
|
||
}
|
||
.big-state {
|
||
height: 240rpx;
|
||
border-radius: 16rpx;
|
||
border: 1px dashed rgba(116, 216, 255, 0.22);
|
||
background: rgba(7, 13, 28, 0.30);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12rpx;
|
||
}
|
||
.big-state-text {
|
||
font-size: 22rpx;
|
||
color: rgba(201, 242, 255, 0.70);
|
||
font-weight: 800;
|
||
}
|
||
.big-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10rpx;
|
||
}
|
||
.big-item {
|
||
border-radius: 14rpx;
|
||
border: 1px solid rgba(116, 216, 255, 0.18);
|
||
background: rgba(7, 13, 28, 0.35);
|
||
padding: 12rpx 12rpx;
|
||
}
|
||
.big-item-title {
|
||
font-size: 22rpx;
|
||
font-weight: 900;
|
||
color: rgba(201, 242, 255, 0.92);
|
||
}
|
||
.big-item-desc {
|
||
margin-top: 6rpx;
|
||
font-size: 20rpx;
|
||
color: rgba(201, 242, 255, 0.65);
|
||
line-height: 30rpx;
|
||
}
|
||
|
||
@media (max-width: 980px) {
|
||
.big-grid {
|
||
flex-direction: column;
|
||
}
|
||
.big-col {
|
||
width: 100%;
|
||
}
|
||
.big-hero {
|
||
min-height: 520rpx;
|
||
}
|
||
}
|
||
</style>
|