Polish absence analysis demo experience
This commit is contained in:
@@ -2,10 +2,9 @@
|
||||
<div class="shell" :class="{ 'shell-collapsed': isSidebarCollapsed }">
|
||||
<aside class="shell-sidebar">
|
||||
<div class="brand-block">
|
||||
<div class="brand-mark">HR</div>
|
||||
<div class="brand-mark"></div>
|
||||
<div v-if="!isSidebarCollapsed">
|
||||
<div class="brand-title">企业缺勤分析台</div>
|
||||
<div class="brand-subtitle">Human Resource Insight Console</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,14 +29,14 @@
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/jdr-analysis">
|
||||
<el-icon class="nav-icon"><Reading /></el-icon>
|
||||
<span class="nav-label">JD-R分析</span>
|
||||
<span class="nav-label">理论分析</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
<div v-if="!isSidebarCollapsed" class="sidebar-note">
|
||||
<div class="sidebar-label">系统摘要</div>
|
||||
<p>面向企业管理场景的缺勤趋势、风险预测与群体画像展示。</p>
|
||||
<p>缺勤趋势、风险预测与群体画像。</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -56,8 +55,6 @@
|
||||
<el-button class="theme-toggle" @click="toggleTheme">
|
||||
{{ isDarkMode ? '浅色模式' : '深色模式' }}
|
||||
</el-button>
|
||||
<span class="topbar-badge">企业健康运营分析</span>
|
||||
<span class="topbar-badge topbar-badge-accent">可视化决策界面</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -81,23 +78,23 @@ const isDarkMode = ref(false)
|
||||
const metaMap = {
|
||||
'/dashboard': {
|
||||
title: '数据概览',
|
||||
subtitle: '从企业缺勤事件的总量、时序与结构分布切入,建立整体认知。'
|
||||
subtitle: '缺勤事件总量、趋势与结构分布'
|
||||
},
|
||||
'/analysis': {
|
||||
title: '影响因素',
|
||||
subtitle: '观察模型最关注的驱动因素,辅助解释缺勤风险的来源。'
|
||||
subtitle: '关键驱动因素与群体差异'
|
||||
},
|
||||
'/prediction': {
|
||||
title: '缺勤预测',
|
||||
subtitle: '围绕最核心的业务信号输入,快速获得缺勤时长与风险等级。'
|
||||
subtitle: '缺勤时长、风险等级与模型对比'
|
||||
},
|
||||
'/clustering': {
|
||||
title: '员工画像',
|
||||
subtitle: '通过聚类划分典型群体,为答辩演示提供更直观的人群视角。'
|
||||
subtitle: '典型群体划分与画像分析'
|
||||
},
|
||||
'/jdr-analysis': {
|
||||
title: 'JD-R理论分析',
|
||||
subtitle: '基于工作要求-资源理论的可解释分析,揭示缺勤的心理学驱动因素。'
|
||||
title: '理论分析',
|
||||
subtitle: '工作要求、资源支持与缺勤风险'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,8 +161,28 @@ watch(isDarkMode, value => {
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, #fef3c7, #fdba74);
|
||||
color: #7c2d12;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.brand-mark::before,
|
||||
.brand-mark::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-radius: 999px;
|
||||
background: rgba(124, 45, 18, 0.9);
|
||||
}
|
||||
|
||||
.brand-mark::before {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.brand-mark::after {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
right: 11px;
|
||||
bottom: 11px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
@@ -174,12 +191,6 @@ watch(isDarkMode, value => {
|
||||
color: var(--sidebar-text);
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--sidebar-text-subtle);
|
||||
}
|
||||
|
||||
.sidebar-panel,
|
||||
.sidebar-note {
|
||||
padding: 18px;
|
||||
@@ -296,19 +307,6 @@ watch(isDarkMode, value => {
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.topbar-badge {
|
||||
padding: 9px 14px;
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 999px;
|
||||
background: var(--surface);
|
||||
font-size: 12px;
|
||||
color: var(--brand-strong);
|
||||
}
|
||||
|
||||
.topbar-badge-accent {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ const routes = [
|
||||
path: '/jdr-analysis',
|
||||
name: 'JDRAnalysis',
|
||||
component: () => import('@/views/JDRAnalysis.vue'),
|
||||
meta: { title: 'JD-R理论分析' }
|
||||
meta: { title: '理论分析' }
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -104,14 +104,6 @@ a {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.page-eyebrow {
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 30px;
|
||||
@@ -168,21 +160,6 @@ a {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.soft-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
color: var(--brand-strong);
|
||||
background: rgba(15, 118, 110, 0.1);
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] .soft-tag {
|
||||
background: rgba(52, 211, 153, 0.14);
|
||||
}
|
||||
|
||||
.soft-grid {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<section class="page-hero cluster-hero">
|
||||
<div class="page-eyebrow">Clustering</div>
|
||||
<h1 class="page-title">员工画像与群体切片</h1>
|
||||
<p class="page-description">
|
||||
将员工划分为不同缺勤画像群体,通过雷达图和散点图形成直观的人群对比展示。
|
||||
基于缺勤行为、工作压力和基础属性划分典型员工群体。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -12,7 +11,7 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">群体雷达画像</h3>
|
||||
<p class="section-caption">以年龄、司龄、加班、通勤、BMI 和缺勤水平构建群体轮廓。</p>
|
||||
<p class="section-caption">年龄、司龄、加班、通勤、BMI 与缺勤水平</p>
|
||||
</div>
|
||||
<el-select v-model="nClusters" @change="loadData" class="cluster-select">
|
||||
<el-option :label="2" :value="2" />
|
||||
@@ -29,9 +28,8 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">聚类结果</h3>
|
||||
<p class="section-caption">便于答辩时逐个介绍群体特征。</p>
|
||||
<p class="section-caption">群体规模与主要特征</p>
|
||||
</div>
|
||||
<span class="soft-tag">Profiles</span>
|
||||
</div>
|
||||
<el-table :data="clusterData" stripe class="cluster-table">
|
||||
<el-table-column prop="name" label="群体名称" min-width="120" />
|
||||
@@ -39,7 +37,7 @@
|
||||
<el-table-column prop="percentage" label="占比(%)" width="90">
|
||||
<template #default="{ row }">{{ row.percentage }}%</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="说明" min-width="180" />
|
||||
<el-table-column prop="description" label="群体特征" min-width="180" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
@@ -48,9 +46,8 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">加班与缺勤散点图</h3>
|
||||
<p class="section-caption">展示各聚类在加班强度与缺勤水平上的位置差异。</p>
|
||||
<p class="section-caption">加班强度与缺勤水平分布</p>
|
||||
</div>
|
||||
<span class="soft-tag">Scatter</span>
|
||||
</div>
|
||||
<div ref="scatterChartRef" class="chart-frame"></div>
|
||||
</el-card>
|
||||
@@ -60,7 +57,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import request from '@/api/request'
|
||||
|
||||
@@ -68,19 +65,44 @@ const radarChartRef = ref(null)
|
||||
const scatterChartRef = ref(null)
|
||||
const nClusters = ref(3)
|
||||
const clusterData = ref([])
|
||||
const loadVersion = ref(0)
|
||||
let radarChart = null
|
||||
let scatterChart = null
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
radarChart?.dispose()
|
||||
scatterChart?.dispose()
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
await Promise.all([initRadarChart(), initScatterChart(), loadClusterResult()])
|
||||
const currentClusters = nClusters.value
|
||||
const version = ++loadVersion.value
|
||||
const [profile, scatter, result] = await Promise.all([
|
||||
request.get(`/cluster/profile?n_clusters=${currentClusters}`),
|
||||
request.get(`/cluster/scatter?n_clusters=${currentClusters}`),
|
||||
request.get(`/cluster/result?n_clusters=${currentClusters}`)
|
||||
])
|
||||
|
||||
if (version !== loadVersion.value || currentClusters !== nClusters.value) {
|
||||
return
|
||||
}
|
||||
|
||||
renderRadarChart(profile)
|
||||
renderScatterChart(scatter)
|
||||
clusterData.value = result.clusters
|
||||
}
|
||||
|
||||
async function initRadarChart() {
|
||||
const chart = echarts.init(radarChartRef.value)
|
||||
const data = await request.get(`/cluster/profile?n_clusters=${nClusters.value}`)
|
||||
chart.setOption({
|
||||
function renderRadarChart(data) {
|
||||
if (!radarChartRef.value) return
|
||||
if (!radarChart) {
|
||||
radarChart = echarts.init(radarChartRef.value)
|
||||
}
|
||||
radarChart.clear()
|
||||
radarChart.setOption({
|
||||
tooltip: {},
|
||||
legend: { top: 6, data: data.clusters.map(item => item.name) },
|
||||
radar: { indicator: data.dimensions.map(name => ({ name, max: 1 })), radius: '62%' },
|
||||
@@ -88,15 +110,18 @@ async function initRadarChart() {
|
||||
})
|
||||
}
|
||||
|
||||
async function initScatterChart() {
|
||||
const chart = echarts.init(scatterChartRef.value)
|
||||
const data = await request.get(`/cluster/scatter?n_clusters=${nClusters.value}`)
|
||||
function renderScatterChart(data) {
|
||||
if (!scatterChartRef.value) return
|
||||
if (!scatterChart) {
|
||||
scatterChart = echarts.init(scatterChartRef.value)
|
||||
}
|
||||
scatterChart.clear()
|
||||
const grouped = {}
|
||||
data.points.forEach(point => {
|
||||
if (!grouped[point.cluster_id]) grouped[point.cluster_id] = []
|
||||
grouped[point.cluster_id].push([point.x, point.y])
|
||||
})
|
||||
chart.setOption({
|
||||
scatterChart.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
grid: { left: 36, right: 18, top: 20, bottom: 36, containLabel: true },
|
||||
xAxis: { name: data.x_axis_name, splitLine: { lineStyle: { color: '#E5EBF2' } } },
|
||||
@@ -109,11 +134,6 @@ async function initScatterChart() {
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
async function loadClusterResult() {
|
||||
const data = await request.get(`/cluster/result?n_clusters=${nClusters.value}`)
|
||||
clusterData.value = data.clusters
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<section class="page-hero">
|
||||
<div class="page-eyebrow">Overview</div>
|
||||
<h1 class="page-title">企业缺勤全景概览</h1>
|
||||
<p class="page-description">
|
||||
通过总量、时序、结构分布三个层面快速识别缺勤风险的整体轮廓,适合作为答辩时的第一屏总览。
|
||||
汇总缺勤总量、趋势变化与结构分布。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -25,9 +24,8 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">月度缺勤事件趋势</h3>
|
||||
<p class="section-caption">观察不同月份的事件量与时长波动。</p>
|
||||
<p class="section-caption">月度事件量与时长波动</p>
|
||||
</div>
|
||||
<span class="soft-tag">Trend</span>
|
||||
</div>
|
||||
<div ref="trendChartRef" class="chart-frame"></div>
|
||||
</el-card>
|
||||
@@ -37,9 +35,8 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">星期分布</h3>
|
||||
<p class="section-caption">识别工作周内的缺勤集中区间。</p>
|
||||
<p class="section-caption">工作周缺勤分布</p>
|
||||
</div>
|
||||
<span class="soft-tag">Weekday</span>
|
||||
</div>
|
||||
<div ref="weekdayChartRef" class="chart-frame"></div>
|
||||
</el-card>
|
||||
@@ -52,9 +49,8 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">请假原因大类分布</h3>
|
||||
<p class="section-caption">呈现引发缺勤的主要业务原因结构。</p>
|
||||
<p class="section-caption">主要请假原因结构</p>
|
||||
</div>
|
||||
<span class="soft-tag">Reason Mix</span>
|
||||
</div>
|
||||
<div ref="reasonChartRef" class="chart-frame"></div>
|
||||
</el-card>
|
||||
@@ -64,9 +60,8 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">季节影响分布</h3>
|
||||
<p class="section-caption">展示季节变化与缺勤总量之间的关系。</p>
|
||||
<p class="section-caption">季节与缺勤总量</p>
|
||||
</div>
|
||||
<span class="soft-tag">Season</span>
|
||||
</div>
|
||||
<div ref="seasonChartRef" class="chart-frame"></div>
|
||||
</el-card>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<section class="page-hero analysis-hero">
|
||||
<div class="page-eyebrow">Analysis</div>
|
||||
<h1 class="page-title">缺勤驱动因素洞察</h1>
|
||||
<p class="page-description">
|
||||
将模型特征重要性、变量相关关系与群体差异放在同一界面展示,形成更完整的解释链路。
|
||||
汇总关键变量、相关关系与群体差异,定位缺勤风险的主要来源。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -12,9 +11,8 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">缺勤影响因素排序</h3>
|
||||
<p class="section-caption">用于展示模型最关注的驱动信号及其主次关系。</p>
|
||||
<p class="section-caption">模型特征重要性</p>
|
||||
</div>
|
||||
<span class="soft-tag">Importance</span>
|
||||
</div>
|
||||
<div ref="importanceChartRef" class="chart-frame"></div>
|
||||
</el-card>
|
||||
@@ -25,9 +23,8 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">核心特征相关性</h3>
|
||||
<p class="section-caption">帮助说明关键指标之间的联动关系。</p>
|
||||
<p class="section-caption">关键指标联动关系</p>
|
||||
</div>
|
||||
<span class="soft-tag">Correlation</span>
|
||||
</div>
|
||||
<div ref="correlationChartRef" class="chart-frame"></div>
|
||||
</el-card>
|
||||
@@ -37,7 +34,7 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">群体对比分析</h3>
|
||||
<p class="section-caption">从行业、排班和健康等维度比较平均缺勤时长。</p>
|
||||
<p class="section-caption">不同维度下的平均缺勤时长</p>
|
||||
</div>
|
||||
<el-select v-model="dimension" @change="loadComparison" class="dimension-select">
|
||||
<el-option label="所属行业" value="industry" />
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
<template>
|
||||
<div class="page-shell jdr-page">
|
||||
<section class="page-hero jdr-hero">
|
||||
<div class="page-eyebrow">JD-R Theory</div>
|
||||
<h1 class="page-title">JD-R 理论驱动的可解释分析</h1>
|
||||
<h1 class="page-title">JD-R 理论分析</h1>
|
||||
<p class="page-description">
|
||||
基于工作要求-资源模型,从心理学理论视角解析员工缺勤的深层驱动因素,提供可解释的干预建议。
|
||||
基于工作要求-资源模型,分析工作压力、资源支持与缺勤风险之间的关系。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<el-tabs v-model="activeTab" type="border-card" class="jdr-tabs">
|
||||
<!-- Tab 1: JD-R 维度分析 -->
|
||||
<el-tab-pane label="维度分析" name="dimensions">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>
|
||||
<h3 class="section-title">JD-R 维度雷达图</h3>
|
||||
<p class="section-caption">工作要求、工作资源与个人资源三维度均值对比</p>
|
||||
<h3 class="section-title">维度雷达图</h3>
|
||||
<p class="section-caption">工作要求、工作资源与个人资源</p>
|
||||
</template>
|
||||
<div v-if="dimensionData" ref="radarChartRef" style="height: 380px"></div>
|
||||
<el-empty v-else description="加载中..." />
|
||||
@@ -26,7 +24,7 @@
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>
|
||||
<h3 class="section-title">维度分布统计</h3>
|
||||
<p class="section-caption">各维度的均值、标准差与平衡度</p>
|
||||
<p class="section-caption">均值、标准差与平衡度</p>
|
||||
</template>
|
||||
<div v-if="dimensionData" class="dim-stats">
|
||||
<div v-for="(item, key) in dimensionLabels" :key="key" class="dim-stat-row">
|
||||
@@ -35,7 +33,7 @@
|
||||
<div class="dim-stat-sub">std: {{ dimensionData[key]?.std || '-' }}</div>
|
||||
</div>
|
||||
<div v-if="dimensionData?.balance" class="dim-stat-row dim-stat-balance">
|
||||
<div class="dim-stat-label">JD-R 平衡度</div>
|
||||
<div class="dim-stat-label">平衡度</div>
|
||||
<div class="dim-stat-value">{{ dimensionData.balance.mean }}</div>
|
||||
<div class="dim-stat-sub">正向比例: {{ dimensionData.balance.positive_ratio }}%</div>
|
||||
</div>
|
||||
@@ -46,14 +44,13 @@
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 2: 倦怠与投入 -->
|
||||
<el-tab-pane label="倦怠与投入" name="burnout">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>
|
||||
<h3 class="section-title">倦怠与投入分布</h3>
|
||||
<p class="section-caption">工作倦怠(1-7)和工作投入(1-7)的分布对比</p>
|
||||
<p class="section-caption">工作倦怠与工作投入分布</p>
|
||||
</template>
|
||||
<div v-if="burnoutData" ref="burnoutChartRef" style="height: 380px"></div>
|
||||
<el-empty v-else description="加载中..." />
|
||||
@@ -63,7 +60,7 @@
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>
|
||||
<h3 class="section-title">关键相关性</h3>
|
||||
<p class="section-caption">JD-R 维度与缺勤时长之间的关联强度</p>
|
||||
<p class="section-caption">理论维度与缺勤时长</p>
|
||||
</template>
|
||||
<div v-if="burnoutData" ref="corrChartRef" style="height: 380px"></div>
|
||||
<el-empty v-else description="加载中..." />
|
||||
@@ -72,14 +69,13 @@
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 3: 双路径分析 -->
|
||||
<el-tab-pane label="双路径分析" name="path">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :lg="14">
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>
|
||||
<h3 class="section-title">JD-R 双路径理论模型</h3>
|
||||
<p class="section-caption">健康损伤路径(需求→倦怠→缺勤)与激励路径(资源→投入→低缺勤)</p>
|
||||
<h3 class="section-title">双路径理论模型</h3>
|
||||
<p class="section-caption">健康损伤路径与激励路径</p>
|
||||
</template>
|
||||
<div class="path-diagram">
|
||||
<div class="path-flow">
|
||||
@@ -122,7 +118,7 @@
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>
|
||||
<h3 class="section-title">风险等级分布</h3>
|
||||
<p class="section-caption">全员缺勤风险等级统计</p>
|
||||
<p class="section-caption">全员风险等级统计</p>
|
||||
</template>
|
||||
<div v-if="riskData" ref="riskChartRef" style="height: 320px"></div>
|
||||
<el-empty v-else description="加载中..." />
|
||||
@@ -131,22 +127,21 @@
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 4: SHAP 解释 -->
|
||||
<el-tab-pane label="SHAP 解释" name="shap">
|
||||
<el-tab-pane label="特征贡献" name="shap">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xs="24" :lg="14">
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="section-heading" style="margin-bottom:0">
|
||||
<div>
|
||||
<h3 class="section-title">全局特征重要性 (SHAP)</h3>
|
||||
<p class="section-caption">按 SHAP 值排列的特征贡献度</p>
|
||||
<h3 class="section-title">全局特征重要性</h3>
|
||||
<p class="section-caption">特征贡献排序</p>
|
||||
</div>
|
||||
<el-select v-model="shapModel" size="small" style="width: 160px" @change="loadShapGlobal">
|
||||
<el-option label="随机森林" value="random_forest" />
|
||||
<el-option label="XGBoost" value="xgboost" />
|
||||
<el-option label="LightGBM" value="lightgbm" />
|
||||
<el-option label="GBDT" value="gradient_boosting" />
|
||||
<el-option label="增强树模型一" value="xgboost" />
|
||||
<el-option label="增强树模型二" value="lightgbm" />
|
||||
<el-option label="梯度提升树" value="gradient_boosting" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
@@ -158,7 +153,7 @@
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>
|
||||
<h3 class="section-title">维度贡献占比</h3>
|
||||
<p class="section-caption">按 JD-R 理论维度聚合的 SHAP 贡献</p>
|
||||
<p class="section-caption">理论维度聚合贡献</p>
|
||||
</template>
|
||||
<div v-if="shapGlobalData" ref="shapDimPieRef" style="height: 420px"></div>
|
||||
<el-empty v-else description="加载中..." />
|
||||
@@ -170,7 +165,7 @@
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>
|
||||
<h3 class="section-title">特征依赖图</h3>
|
||||
<p class="section-caption">选择特征查看其取值与 SHAP 值的关系</p>
|
||||
<p class="section-caption">特征取值与贡献关系</p>
|
||||
</template>
|
||||
<div style="margin-bottom: 12px">
|
||||
<el-select v-model="dependenceFeature" size="small" style="width: 200px" @change="loadDependence">
|
||||
@@ -185,7 +180,7 @@
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>
|
||||
<h3 class="section-title">特征交互强度</h3>
|
||||
<p class="section-caption">Top 特征对的交互效应</p>
|
||||
<p class="section-caption">主要特征对交互效应</p>
|
||||
</template>
|
||||
<div v-if="shapGlobalData" ref="shapInteractionRef" style="height: 320px"></div>
|
||||
<el-empty v-else description="加载中..." />
|
||||
@@ -239,6 +234,18 @@ function getOrCreateChart(el) {
|
||||
return chart
|
||||
}
|
||||
|
||||
function clearShapViews() {
|
||||
shapGlobalData.value = null
|
||||
shapTopFeatures.value = []
|
||||
const refs = [shapGlobalRef.value, shapDimPieRef.value, shapDependenceRef.value, shapInteractionRef.value]
|
||||
refs.forEach(el => {
|
||||
const chart = getOrCreateChart(el)
|
||||
if (chart) {
|
||||
chart.clear()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 延迟渲染:等待 Vue 将 v-if 的 DOM 插入到页面
|
||||
function scheduleRender(fn) {
|
||||
requestAnimationFrame(() => {
|
||||
@@ -265,7 +272,7 @@ function renderRadarChart() {
|
||||
{ name: '工作要求', max: 10 },
|
||||
{ name: '工作资源', max: 5 },
|
||||
{ name: '个人资源', max: 5 },
|
||||
{ name: 'JD-R平衡度', max: 5 },
|
||||
{ name: '平衡度', max: 5 },
|
||||
]
|
||||
const values = [
|
||||
dims.demands?.mean || 0,
|
||||
@@ -281,7 +288,7 @@ function renderRadarChart() {
|
||||
type: 'radar',
|
||||
data: [{
|
||||
value: values,
|
||||
name: 'JD-R 维度均值',
|
||||
name: '维度均值',
|
||||
areaStyle: { color: 'rgba(15, 118, 110, 0.2)' },
|
||||
lineStyle: { color: '#0f766e', width: 2 },
|
||||
itemStyle: { color: '#0f766e' },
|
||||
@@ -386,12 +393,16 @@ function renderRiskChart() {
|
||||
// ── Tab 4: SHAP ──
|
||||
async function loadShapGlobal() {
|
||||
if (activeTab.value !== 'shap') return
|
||||
clearShapViews()
|
||||
try {
|
||||
const data = await getGlobalImportance(shapModel.value)
|
||||
if (data.error) { ElMessage.error(data.error); return }
|
||||
if (data.error) {
|
||||
ElMessage.error(data.error)
|
||||
return
|
||||
}
|
||||
shapGlobalData.value = data
|
||||
shapTopFeatures.value = data.top_features || []
|
||||
if (shapTopFeatures.value.length && !dependenceFeature.value) {
|
||||
if (shapTopFeatures.value.length) {
|
||||
dependenceFeature.value = shapTopFeatures.value[0].name
|
||||
}
|
||||
scheduleRender(() => {
|
||||
@@ -401,7 +412,7 @@ async function loadShapGlobal() {
|
||||
loadInteractions()
|
||||
})
|
||||
} catch (e) {
|
||||
ElMessage.error('加载 SHAP 数据失败')
|
||||
ElMessage.error('加载特征贡献数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,7 +433,7 @@ function renderShapGlobalChart() {
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
grid: { left: 140, right: 30, top: 10, bottom: 30 },
|
||||
xAxis: { type: 'value', name: 'Mean |SHAP|' },
|
||||
xAxis: { type: 'value', name: '平均贡献值' },
|
||||
yAxis: { type: 'category', data: features.map(f => f.name_cn) },
|
||||
series: [{
|
||||
type: 'bar', data: features.map(f => ({
|
||||
@@ -465,17 +476,22 @@ async function loadDependence() {
|
||||
if (!dependenceFeature.value) return
|
||||
try {
|
||||
const data = await getDependence(dependenceFeature.value, shapModel.value)
|
||||
if (data.error) return
|
||||
if (data.error) {
|
||||
const chart = getOrCreateChart(shapDependenceRef.value)
|
||||
if (chart) chart.clear()
|
||||
ElMessage.error(data.error)
|
||||
return
|
||||
}
|
||||
await nextTick()
|
||||
const chart = getOrCreateChart(shapDependenceRef.value)
|
||||
if (!chart) return
|
||||
|
||||
const points = data.values.map((v, i) => [v, data.shap_values[i]])
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'item', formatter: (p) => `值: ${p.data[0].toFixed(2)}<br/>SHAP: ${p.data[1].toFixed(4)}` },
|
||||
tooltip: { trigger: 'item', formatter: (p) => `值: ${p.data[0].toFixed(2)}<br/>贡献值: ${p.data[1].toFixed(4)}` },
|
||||
grid: { left: 60, right: 20, top: 20, bottom: 40 },
|
||||
xAxis: { type: 'value', name: data.feature_cn },
|
||||
yAxis: { type: 'value', name: 'SHAP value' },
|
||||
yAxis: { type: 'value', name: '贡献值' },
|
||||
series: [{
|
||||
type: 'scatter', data: points, symbolSize: 5,
|
||||
itemStyle: { color: '#0f766e', opacity: 0.6 },
|
||||
@@ -488,7 +504,12 @@ async function loadInteractions() {
|
||||
if (activeTab.value !== 'shap') return
|
||||
try {
|
||||
const data = await getInteractions(shapModel.value, 10)
|
||||
if (data.error || !data.top_interactions) return
|
||||
if (data.error || !data.top_interactions) {
|
||||
const chart = getOrCreateChart(shapInteractionRef.value)
|
||||
if (chart) chart.clear()
|
||||
if (data.error) ElMessage.error(data.error)
|
||||
return
|
||||
}
|
||||
await nextTick()
|
||||
const chart = getOrCreateChart(shapInteractionRef.value)
|
||||
if (!chart) return
|
||||
@@ -498,7 +519,7 @@ async function loadInteractions() {
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
grid: { left: 160, right: 20, top: 10, bottom: 30 },
|
||||
xAxis: { type: 'value', name: '交互强度' },
|
||||
yAxis: { type: 'category', data: interactions.map(i => `${i.feature_1_cn} x ${i.feature_2_cn}`) },
|
||||
yAxis: { type: 'category', data: interactions.map(i => `${i.feature_1_cn}与${i.feature_2_cn}`) },
|
||||
series: [{
|
||||
type: 'bar', data: interactions.map(i => i.strength),
|
||||
itemStyle: { color: '#f59e0b' },
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<div class="page-shell prediction">
|
||||
<section class="page-hero prediction-hero">
|
||||
<div class="page-eyebrow">Prediction</div>
|
||||
<h1 class="page-title">核心因子驱动的缺勤预测</h1>
|
||||
<p class="page-description">
|
||||
仅保留对结果最关键的输入项,让演示流程更聚焦,也让答辩老师更容易理解模型的业务逻辑。
|
||||
基于缺勤事件、工作压力、家庭负担与岗位背景,评估缺勤时长和风险等级。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -15,12 +14,12 @@
|
||||
<div class="section-heading" style="margin-bottom: 0">
|
||||
<div>
|
||||
<h3 class="section-title">中国企业缺勤风险输入</h3>
|
||||
<p class="section-caption">使用卡片分区组织核心因子,演示时更清晰。</p>
|
||||
<p class="section-caption">核心字段分区录入</p>
|
||||
</div>
|
||||
<el-button size="small" @click="resetForm">重置</el-button>
|
||||
</div>
|
||||
<div class="form-tip">
|
||||
系统会自动补齐企业背景、健康生活与组织属性等次级信息,页面仅保留对预测结果影响最大的核心字段。
|
||||
后端将结合默认企业背景、健康生活与组织属性完成特征补齐。
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
@@ -28,9 +27,8 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">缺勤事件核心信息</h3>
|
||||
<p class="section-caption">决定本次缺勤时长的直接事件属性。</p>
|
||||
<p class="section-caption">请假类型、时间与证明信息</p>
|
||||
</div>
|
||||
<span class="soft-tag">Event</span>
|
||||
</div>
|
||||
<el-form :model="form" label-width="118px" size="small">
|
||||
<el-row :gutter="18">
|
||||
@@ -86,9 +84,8 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">工作压力与排班</h3>
|
||||
<p class="section-caption">体现通勤、加班和排班对缺勤的影响。</p>
|
||||
<p class="section-caption">加班、通勤、班次与健康状态</p>
|
||||
</div>
|
||||
<span class="soft-tag">Workload</span>
|
||||
</div>
|
||||
<el-form :model="form" label-width="118px" size="small">
|
||||
<el-row :gutter="18">
|
||||
@@ -133,9 +130,8 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">家庭与补充因素</h3>
|
||||
<p class="section-caption">作为结果修正项,为预测增加业务语境。</p>
|
||||
<p class="section-caption">家庭负担、行业与婚姻状态</p>
|
||||
</div>
|
||||
<span class="soft-tag">Context</span>
|
||||
</div>
|
||||
<el-form :model="form" label-width="118px" size="small">
|
||||
<el-row :gutter="18">
|
||||
@@ -166,9 +162,8 @@
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h3 class="section-title">预测设置</h3>
|
||||
<p class="section-caption">支持自动选择最优模型或查看模型对比。</p>
|
||||
<p class="section-caption">模型选择与对比</p>
|
||||
</div>
|
||||
<span class="soft-tag">Action</span>
|
||||
</div>
|
||||
<el-form :model="form" label-width="118px" size="small">
|
||||
<el-row :gutter="18">
|
||||
@@ -203,10 +198,9 @@
|
||||
<template #header>
|
||||
<div class="section-heading" style="margin-bottom: 0">
|
||||
<div>
|
||||
<h3 class="section-title">预测结果与风险说明</h3>
|
||||
<p class="section-caption">在同一张卡片内查看预测值、模型信息和风险区间说明。</p>
|
||||
<h3 class="section-title">预测结果与风险等级</h3>
|
||||
<p class="section-caption">缺勤时长、风险等级与模型信息</p>
|
||||
</div>
|
||||
<span class="soft-tag">Result</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="merged-result-grid">
|
||||
@@ -264,19 +258,90 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="result?.jdr_snapshot" class="insight-stack">
|
||||
<section class="insight-block">
|
||||
<div class="insight-heading">
|
||||
<div>
|
||||
<h3 class="section-title">JD-R 快照</h3>
|
||||
<p class="section-caption">工作要求、资源与平衡度</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="snapshot-grid">
|
||||
<div v-for="item in jdrSnapshotCards" :key="item.key" class="snapshot-card">
|
||||
<div class="snapshot-label">{{ item.label }}</div>
|
||||
<div class="snapshot-score">{{ item.score }}</div>
|
||||
<el-tag :type="item.tone" size="small">{{ item.status }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="mechanismSummary" class="insight-block">
|
||||
<div class="insight-heading">
|
||||
<div>
|
||||
<h3 class="section-title">风险机制</h3>
|
||||
<p class="section-caption">当前样本的主要风险路径</p>
|
||||
</div>
|
||||
<el-tag v-if="mechanismSummary.pathway_label" :type="mechanismSummary.pathway_tone || 'info'" effect="light">
|
||||
{{ mechanismSummary.pathway_label }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="summary-text">
|
||||
<p>{{ mechanismSummary.conclusion }}</p>
|
||||
<p>{{ mechanismSummary.mechanism }}</p>
|
||||
<p>{{ mechanismSummary.pathway_detail }}</p>
|
||||
<p v-if="mechanismSummary.buffer_text">{{ mechanismSummary.buffer_text }}</p>
|
||||
<p v-if="mechanismSummary.scenario_hint">{{ mechanismSummary.scenario_hint }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="mechanismSummary.top_drivers?.length" class="driver-group">
|
||||
<div class="driver-label">主要推高因素</div>
|
||||
<div class="driver-chip-row">
|
||||
<span v-for="item in mechanismSummary.top_drivers" :key="`up-${item.name}`" class="driver-chip driver-chip-up">
|
||||
{{ item.name_cn }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="mechanismSummary.protective_factors?.length" class="driver-group">
|
||||
<div class="driver-label">缓冲因素</div>
|
||||
<div class="driver-chip-row">
|
||||
<span v-for="item in mechanismSummary.protective_factors" :key="`down-${item.name}`" class="driver-chip driver-chip-down">
|
||||
{{ item.name_cn }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="interventionGroups.length" class="insight-block">
|
||||
<div class="insight-heading">
|
||||
<div>
|
||||
<h3 class="section-title">干预建议</h3>
|
||||
<p class="section-caption">管理关注方向</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="suggestion-grid">
|
||||
<div v-for="group in interventionGroups" :key="group.category" class="suggestion-card">
|
||||
<div class="suggestion-title">{{ group.category }}</div>
|
||||
<div class="suggestion-list">
|
||||
<p v-for="item in group.items" :key="item">{{ item }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="compareResults.length > 0" class="panel-card compare-card" shadow="never">
|
||||
<el-card v-if="shouldShowCompareCard" v-loading="compareLoading" class="panel-card compare-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="section-heading" style="margin-bottom: 0">
|
||||
<div>
|
||||
<h3 class="section-title">模型对比结果</h3>
|
||||
<p class="section-caption">选择最适合展示的候选模型。</p>
|
||||
<p class="section-caption">不同模型预测结果对比</p>
|
||||
</div>
|
||||
<span class="soft-tag">Compare</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="compareResults" size="small" :row-class-name="getRowClass">
|
||||
<el-table v-if="compareResults.length > 0" :data="compareResults" size="small" :row-class-name="getRowClass">
|
||||
<el-table-column prop="model_name_cn" label="模型" width="100" />
|
||||
<el-table-column prop="predicted_hours" label="预测时长" width="90">
|
||||
<template #default="{ row }">{{ row.predicted_hours }}h</template>
|
||||
@@ -295,16 +360,16 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-else description="暂无模型对比结果" />
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="shapLocalData" class="panel-card shap-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="section-heading" style="margin-bottom: 0">
|
||||
<div>
|
||||
<h3 class="section-title">SHAP 预测解释</h3>
|
||||
<p class="section-caption">每个特征对本次预测的贡献度(红色推高/蓝色拉低)</p>
|
||||
<h3 class="section-title">特征贡献</h3>
|
||||
<p class="section-caption">特征贡献方向与贡献强度</p>
|
||||
</div>
|
||||
<span class="soft-tag">Explain</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="shap-dimension-badges">
|
||||
@@ -322,11 +387,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import * as echarts from 'echarts'
|
||||
import request from '@/api/request'
|
||||
import { getLocalExplanation } from '@/api/shap'
|
||||
|
||||
const industries = ['制造业', '互联网', '零售连锁', '物流运输', '金融服务', '医药健康', '建筑工程']
|
||||
const shiftTypes = ['标准白班', '两班倒', '三班倒', '弹性班']
|
||||
@@ -377,6 +441,23 @@ const riskTagType = computed(() => {
|
||||
return getRiskType(result.value.risk_level)
|
||||
})
|
||||
|
||||
const jdrSnapshotCards = computed(() => {
|
||||
const snapshot = result.value?.jdr_snapshot
|
||||
if (!snapshot) return []
|
||||
return [
|
||||
snapshot.job_demands,
|
||||
snapshot.job_resources,
|
||||
snapshot.personal_resources,
|
||||
snapshot.balance,
|
||||
snapshot.burnout_risk,
|
||||
snapshot.engagement,
|
||||
].filter(Boolean)
|
||||
})
|
||||
|
||||
const mechanismSummary = computed(() => result.value?.mechanism_summary || null)
|
||||
const interventionGroups = computed(() => result.value?.intervention_suggestions || [])
|
||||
const shouldShowCompareCard = computed(() => showCompare.value || compareLoading.value || compareResults.value.length > 0)
|
||||
|
||||
function getRiskType(level) {
|
||||
return level === 'low' ? 'success' : level === 'medium' ? 'warning' : 'danger'
|
||||
}
|
||||
@@ -408,9 +489,13 @@ async function handlePredict() {
|
||||
const params = { ...form.value }
|
||||
if (selectedModel.value) params.model_type = selectedModel.value
|
||||
result.value = await request.post('/predict/single', params)
|
||||
shapLocalData.value = result.value?.shap_local || null
|
||||
if (shapLocalData.value?.features?.length) {
|
||||
requestAnimationFrame(() => {
|
||||
nextTick().then(renderShapForce)
|
||||
})
|
||||
}
|
||||
if (showCompare.value) await handleCompare()
|
||||
// 加载 SHAP 局部解释
|
||||
loadShapLocal(params)
|
||||
} catch (e) {
|
||||
ElMessage.error(`预测失败: ${e.message}`)
|
||||
} finally {
|
||||
@@ -418,19 +503,6 @@ async function handlePredict() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadShapLocal(params) {
|
||||
try {
|
||||
const modelType = params.model_type || ''
|
||||
const data = await getLocalExplanation({ ...params, model_type: modelType })
|
||||
if (data && !data.error) {
|
||||
shapLocalData.value = data
|
||||
requestAnimationFrame(() => {
|
||||
nextTick().then(renderShapForce)
|
||||
})
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function renderShapForce() {
|
||||
const el = shapForceRef.value
|
||||
if (!el || !shapLocalData.value?.features) { console.warn('shapForce: DOM or data missing'); return }
|
||||
@@ -443,7 +515,7 @@ function renderShapForce() {
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
grid: { left: 120, right: 30, top: 10, bottom: 30 },
|
||||
xAxis: { type: 'value', name: 'SHAP值' },
|
||||
xAxis: { type: 'value', name: '贡献值' },
|
||||
yAxis: { type: 'category', data: sorted.map(f => f.name_cn) },
|
||||
series: [{
|
||||
type: 'bar', data: sorted.map(f => ({
|
||||
@@ -457,6 +529,7 @@ function renderShapForce() {
|
||||
async function handleCompare() {
|
||||
compareLoading.value = true
|
||||
try {
|
||||
showCompare.value = true
|
||||
const res = await request.post('/predict/compare', form.value)
|
||||
compareResults.value = res.results || []
|
||||
} catch (e) {
|
||||
@@ -469,6 +542,16 @@ async function handleCompare() {
|
||||
onMounted(() => {
|
||||
loadModels()
|
||||
})
|
||||
|
||||
watch(showCompare, (visible) => {
|
||||
if (!visible) {
|
||||
compareResults.value = []
|
||||
return
|
||||
}
|
||||
if (!compareLoading.value && compareResults.value.length === 0) {
|
||||
handleCompare()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -643,10 +726,133 @@ onMounted(() => {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.insight-stack {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.insight-block {
|
||||
padding: 18px;
|
||||
border: 1px solid var(--line-soft);
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.76);
|
||||
}
|
||||
|
||||
.insight-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.snapshot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.snapshot-card {
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, rgba(58, 122, 254, 0.06), rgba(15, 118, 110, 0.08));
|
||||
border: 1px solid rgba(58, 122, 254, 0.1);
|
||||
}
|
||||
|
||||
.snapshot-label {
|
||||
font-size: 13px;
|
||||
color: var(--text-subtle);
|
||||
}
|
||||
|
||||
.snapshot-score {
|
||||
margin: 8px 0 12px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.summary-text {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.summary-text p,
|
||||
.suggestion-list p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.driver-group + .driver-group {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.driver-group {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.driver-label,
|
||||
.suggestion-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.driver-chip-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.driver-chip {
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.driver-chip-up {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.18);
|
||||
}
|
||||
|
||||
.driver-chip-down {
|
||||
color: #2563eb;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 1px solid rgba(59, 130, 246, 0.18);
|
||||
}
|
||||
|
||||
.suggestion-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.suggestion-card {
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--line-soft);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(244, 248, 255, 0.9));
|
||||
}
|
||||
|
||||
.suggestion-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.prediction-input-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.snapshot-grid,
|
||||
.suggestion-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -654,6 +860,15 @@ onMounted(() => {
|
||||
.result-stack {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.snapshot-grid,
|
||||
.suggestion-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.insight-heading {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.shap-card {
|
||||
|
||||
Reference in New Issue
Block a user