tangweijie 230021a7b6 feat(dashboard): 优化风险趋势图Y轴动态缩放逻辑
根据数据范围自动计算合适的Y轴最大值和刻度间隔:
- 小于等于10:向上取整到整数
- 10-50:向上取整到10的倍数
- 50-100:向上取整到10的倍数
- 100-500:向上取整到50的倍数
- 500-1000:向上取整到100的倍数
- 大于1000:向上取整到500的倍数

自动计算刻度间隔,确保显示6-8个刻度,提升图表可读性
2026-01-23 11:38:08 +08:00

722 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="dash-entry-container">
<!-- 标题和帮助按钮 -->
<div class="entry-header">
<div class="entry-title">AI 心航360°</div>
<el-tooltip
effect="dark"
placement="bottom"
:show-after="200"
>
<template #content>
<div class="help-content">
<div class="help-item">
<strong>数据说明</strong>
<p>本页面展示的统计数据基于监区管理系统的实时数据统计</p>
</div>
<div class="help-item">
<strong>全部人员</strong>
<p>统计系统中所有在押罪犯的总人数</p>
</div>
<div class="help-item">
<strong>高危人员</strong>
<p>风险等级为"极高风险""高风险"的人员数量</p>
</div>
<div class="help-item">
<strong>预警人员</strong>
<p>风险等级为"中风险"且存在预警信息的人员数量</p>
</div>
<div class="help-item">
<strong>普通人员</strong>
<p>风险等级为"低风险""中风险"且无预警信息的人员数量</p>
</div>
<div class="help-item">
<strong>本月新增</strong>
<p>对比上月新增的罪犯数量正数表示增加负数表示减少</p>
</div>
<div class="help-item">
<strong>风险等级分布</strong>
<p>统计各风险等级人员占比数据来源于罪犯风险等级评估结果</p>
</div>
<div class="help-item">
<strong>风险趋势图</strong>
<p>展示近12个月各风险等级人员数量变化趋势</p>
</div>
<div class="help-item">
<strong>重点关注对象</strong>
<p>根据风险等级心理评估结果违纪记录等因素筛选出的需要重点关注的罪犯</p>
</div>
<div class="help-item">
<strong>数据更新频率</strong>
<p>统计数据每小时自动更新一次确保数据实时性</p>
</div>
<div class="help-item">
<strong>全景画像</strong>
<p>点击"全景画像"可查看该罪犯的完整详细信息包括基本信息风险评估消费记录奖惩记录等</p>
</div>
</div>
</template>
<div class="help-icon">
<Icon icon="ep:question-filled" :size="18" />
</div>
</el-tooltip>
</div>
<!-- 顶部四个数据卡片 -->
<div class="stats-cards">
<div v-for="(card, index) in statsCards" :key="index" class="stat-card">
<div class="stat-card-icon" :class="`icon-${card.type}`">
<Icon :icon="card.icon" :size="24" />
</div>
<div class="stat-card-content">
<div class="stat-card-title">{{ card.title }}</div>
<div class="stat-card-value">{{ card.value }}</div>
<div class="stat-card-subtitle">
{{ card.subtitle }}
<span class="trend-icon" :class="card.trend === 'up' ? 'trend-up' : 'trend-down'">{{
card.trend === 'up' ? '↑' : '↓'
}}</span>
</div>
</div>
</div>
</div>
<!-- 中间图表区域 -->
<div class="charts-section">
<!-- 左侧风险等级分布环形图 -->
<div class="chart-card">
<div class="chart-title">风险等级分布</div>
<div class="chart-content">
<EChart :options="riskDistributionOptions" :height="'200px'" />
</div>
</div>
<!-- 右侧风险趋势图折线图 -->
<div class="chart-card">
<div class="chart-title">风险趋势图</div>
<div class="chart-content">
<EChart :options="riskTrendOptions" :height="'200px'" />
</div>
</div>
</div>
<!-- 底部表格 -->
<div class="table-section">
<div class="table-title">重点关注对象列表</div>
<el-table :data="tableData.results" style="width: 100%" stripe v-loading="loading">
<el-table-column prop="name" label="姓名" width="`16.3%`" class-name="name-column">
<template #default="{ row }">
<span v-if="row.isNew" class="new-tag" :class="`tag-${row.riskLevelType}`">新增</span>
{{ row.name }}
</template>
</el-table-column>
<el-table-column prop="gender" label="性别" width="`10%`" />
<el-table-column prop="age" label="年龄" width="`10%`" />
<el-table-column prop="riskLevel" label="风险等级" width="`15%`">
<template #default="{ row }">
<span :class="`risk-level risk-${row.riskLevelType}`">
{{ row.riskLevel }}
</span>
</template>
</el-table-column>
<el-table-column prop="supervisionArea" label="监区" width="`15%`" />
<el-table-column prop="psychologicalRiskLevel" label="心理风险等级" width="`18.7%`" />
<el-table-column label="操作" width="`15%`">
<template #default="{ row }">
<el-link type="primary" @click="handleView(row)">全景画像</el-link>
</template>
</el-table-column>
</el-table>
<div class="table-pagination">
<span class="total-count">{{ tableData.total }}</span>
<el-pagination
v-model:current-page="tableData.current"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="tableData.total"
layout="sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { EChartsOption } from 'echarts'
// @ts-ignore
import EChart from '@/components/Echart/src/Echart.vue'
import { Icon } from '@/components/Icon'
import { useRouter } from 'vue-router'
import { AiDashEntryApi, type AiDashEntryStatisticsVO, type FocusPersonVO } from '@/api/prison/ai-dash-entry'
import { ElMessage } from 'element-plus'
defineOptions({ name: 'DashEntry' })
const router = useRouter()
const loading = ref(false)
// 统计数据
const statistics = ref<AiDashEntryStatisticsVO>({
totalCount: 0,
monthlyNewCount: 0,
monthlyChange: 0,
highRiskCount: 0,
highRiskMonthlyNew: 0,
highRiskMonthlyChange: 0,
warningCount: 0,
warningMonthlyNew: 0,
warningMonthlyChange: 0,
normalCount: 0,
normalMonthlyNew: 0,
normalMonthlyChange: 0,
riskDistribution: [],
riskTrendData: []
})
// 统计数据卡片
const statsCards = computed(() => [
{
title: '全部人员',
value: `${statistics.value.totalCount}`,
subtitle: `本月${statistics.value.monthlyNewCount}${statistics.value.monthlyChange >= 0 ? '+' : ''}${statistics.value.monthlyChange}`,
trend: statistics.value.monthlyChange >= 0 ? 'up' : 'down',
type: 'all',
icon: 'ep:user'
},
{
title: '高危人员',
value: `${statistics.value.highRiskCount}`,
subtitle: `本月${statistics.value.highRiskMonthlyNew}${statistics.value.highRiskMonthlyChange >= 0 ? '+' : ''}${statistics.value.highRiskMonthlyChange}`,
trend: statistics.value.highRiskMonthlyChange >= 0 ? 'up' : 'down',
type: 'high',
icon: 'ep:warning-filled'
},
{
title: '预警人员',
value: `${statistics.value.warningCount}`,
subtitle: `本月${statistics.value.warningMonthlyNew}${statistics.value.warningMonthlyChange >= 0 ? '+' : ''}${statistics.value.warningMonthlyChange}`,
trend: statistics.value.warningMonthlyChange >= 0 ? 'up' : 'down',
type: 'warning',
icon: 'ep:bell'
},
{
title: '普通人员',
value: `${statistics.value.normalCount}`,
subtitle: `本月${statistics.value.normalMonthlyNew}${statistics.value.normalMonthlyChange >= 0 ? '+' : ''}${statistics.value.normalMonthlyChange}`,
trend: statistics.value.normalMonthlyChange >= 0 ? 'up' : 'down',
type: 'normal',
icon: 'ep:user-filled'
}
])
// 风险等级分布图表配置(环形图)
const riskDistributionOptions = computed<EChartsOption>(() => {
const distribution = statistics.value.riskDistribution || []
const total = distribution.reduce((sum, item) => sum + item.value, 0)
const data = distribution.map(item => {
const percentage = total > 0 ? Math.round((item.value / total) * 100) : 0
return {
value: item.value,
name: `${item.name} (${percentage}%)`,
itemStyle: { color: item.color }
}
})
return {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
legend: {
show: true,
bottom: 0,
left: 'center',
itemGap: 20,
icon: 'circle',
textStyle: {
fontSize: 12
},
data: data.map(item => item.name)
},
series: [
{
name: '风险等级分布',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}\n{d}%'
},
emphasis: {
label: {
show: true,
fontSize: 12,
fontWeight: 'bold'
}
},
data: data
}
]
}
})
// 风险趋势图配置(折线图)
const riskTrendOptions = computed<EChartsOption>(() => {
const trendData = statistics.value.riskTrendData || []
const months = trendData.map(item => item.month)
const highRiskData = trendData.map(item => item.highRisk)
const warningData = trendData.map(item => item.warning)
const normalData = trendData.map(item => item.normal)
// 计算最大值用于设置Y轴范围
const allValues = [...highRiskData, ...warningData, ...normalData]
const maxValue = Math.max(...allValues, 10)
// 动态计算Y轴最大值根据数据范围自动向上取整到合适的量级
let yAxisMax: number
if (maxValue === 0) {
yAxisMax = 10
} else if (maxValue <= 10) {
yAxisMax = Math.ceil(maxValue)
} else if (maxValue <= 50) {
// 对于10-50的数据向上取整到10的倍数
yAxisMax = Math.ceil(maxValue / 10) * 10
} else if (maxValue <= 100) {
// 对于50-100的数据向上取整到10的倍数
yAxisMax = Math.ceil(maxValue / 10) * 10
} else if (maxValue <= 500) {
// 对于100-500的数据向上取整到50的倍数
yAxisMax = Math.ceil(maxValue / 50) * 50
} else if (maxValue <= 1000) {
// 对于500-1000的数据向上取整到100的倍数
yAxisMax = Math.ceil(maxValue / 100) * 100
} else {
// 对于大于1000的数据向上取整到500的倍数
yAxisMax = Math.ceil(maxValue / 500) * 500
}
// 计算合适的刻度间隔确保显示6-8个刻度
const interval = Math.max(Math.ceil(yAxisMax / 6), 1)
return {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['高危', '预警', '普通'],
bottom: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: months
},
yAxis: {
type: 'value',
min: 0,
max: yAxisMax,
interval: interval
},
series: [
{
name: '高危',
type: 'line',
data: highRiskData,
itemStyle: { color: '#ee6666' },
lineStyle: { color: '#ee6666' },
symbol: 'circle',
symbolSize: 6
},
{
name: '预警',
type: 'line',
data: warningData,
itemStyle: { color: '#fac858' },
lineStyle: { color: '#fac858', type: 'dashed' },
symbol: 'circle',
symbolSize: 6
},
{
name: '普通',
type: 'line',
data: normalData,
itemStyle: { color: '#666' },
lineStyle: { color: '#666' },
symbol: 'circle',
symbolSize: 6
}
]
}
})
// 表格数据
const tableData = ref<{
current: number
total: number
results: FocusPersonVO[]
}>({
current: 1,
total: 0,
results: []
})
// 分页
const pageSize = ref(10)
// 处理每页大小变化
const handleSizeChange = (val: number) => {
pageSize.value = val
tableData.value.current = 1
loadFocusPersonPage()
}
// 处理当前页变化
const handleCurrentChange = (val: number) => {
tableData.value.current = val
loadFocusPersonPage()
}
const handleView = (row: FocusPersonVO) => {
router.push({
path: '/prisoner/prisoner/dashboard',
query: {
prisonerId: row.id
}
})
}
// 加载统计数据
const loadStatistics = async () => {
try {
const data = await AiDashEntryApi.getStatistics()
if (data) {
statistics.value = data
}
} catch (error) {
console.error('加载统计数据失败:', error)
ElMessage.error('加载统计数据失败')
}
}
// 加载重点关注对象列表
const loadFocusPersonPage = async () => {
loading.value = true
try {
const data = await AiDashEntryApi.getFocusPersonPage({
pageNo: tableData.value.current,
pageSize: pageSize.value
})
if (data) {
tableData.value.results = data.list || []
tableData.value.total = data.total || 0
}
} catch (error) {
console.error('加载重点关注对象失败:', error)
ElMessage.error('加载重点关注对象列表失败')
} finally {
loading.value = false
}
}
onMounted(() => {
loadStatistics()
loadFocusPersonPage()
})
</script>
<style scoped lang="scss">
.dash-entry-container {
padding: 10px;
background: #fff;
min-height: 100vh;
width: 100%;
height: 100%;
overflow: scroll;
box-sizing: border-box;
}
.entry-title {
font-size: 24px;
margin-bottom: 10px;
text-align: center;
font-weight: 500;
color: #65CFE3;
}
.entry-header {
position: relative;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 10px;
padding: 0 50px;
.help-icon {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
color: #909399;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: #409EFF;
}
}
}
// 帮助提示内容样式
:deep(.el-tooltip__popper) {
max-width: 400px !important;
.help-content {
.help-item {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
strong {
display: block;
margin-bottom: 4px;
color: #303133;
font-size: 14px;
}
p {
margin: 0;
color: #606266;
font-size: 13px;
line-height: 1.6;
}
}
}
}
// 统计卡片区域
.stats-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-bottom: 15px;
width: 100%;
box-sizing: border-box;
}
.stat-card {
background: #ffffff;
border-radius: 8px;
padding: 10px 20px;
display: flex;
align-items: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
width: 100%;
box-sizing: border-box;
min-width: 0;
position: relative;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
.stat-card-icon {
position: absolute;
top: 10px;
right: 0;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
&.icon-all {
background: #e6f4ff;
color: #1890ff;
}
&.icon-high {
background: #fff1f0;
color: #ff4d4f;
}
&.icon-warning {
background: #fff7e6;
color: #faad14;
}
&.icon-normal {
background: #f6ffed;
color: #52c41a;
}
}
.stat-card-content {
flex: 1;
min-width: 0;
overflow: hidden;
}
.stat-card-title {
font-size: 14px;
color: #666;
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stat-card-value {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stat-card-subtitle {
font-size: 14px;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.trend-icon {
margin-left: 4px;
&.trend-up {
color: #52c41a;
}
&.trend-down {
color: #ff4d4f;
}
}
}
// 图表区域
.charts-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 15px;
}
.chart-card {
background: #ffffff;
border-radius: 8px;
padding: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.chart-title {
font-size: 15px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
}
.chart-content {
height: 200px;
}
// 表格区域
.table-section {
background: #ffffff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.table-title {
font-size: 15px;
font-weight: bold;
color: #333;
margin-bottom: 16px;
}
:deep(.name-column) {
padding-left: 32px;
}
.new-tag {
display: inline-block;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
padding: 2px 8px;
font-size: 13px;
border-radius: 4px;
&.tag-high {
color: #ff4d4f;
background: #fff1f0;
}
&.tag-warning {
color: #faad14;
background: #fff7e6;
}
&.tag-normal {
color: #666;
background: #f5f5f5;
}
}
.risk-level {
padding: 4px 8px;
border-radius: 4px;
font-size: 13px;
&.risk-high {
color: #ff4d4f;
background: #fff1f0;
}
&.risk-warning {
color: #faad14;
background: #fff7e6;
}
&.risk-normal {
color: #666;
background: #f5f5f5;
}
}
.table-pagination {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
.total-count {
color: #666;
font-size: 14px;
}
}
</style>