feat: 初始化员工缺勤分析系统项目
搭建完整的前后端分离架构,实现数据概览、预测分析、聚类分析等核心功能模块 详细版: feat: 初始化员工缺勤分析系统项目 - 后端:基于 Flask 搭建 RESTful API,包含数据概览、特征分析、预测模型、聚类分析四大模块 - 前端:基于 Vue.js 构建单页应用,实现 Dashboard、预测、聚类、因子分析等页面 - 模型:集成随机森林、XGBoost、LightGBM、Stacking 等多种机器学习模型 - 文档:完成需求规格说明、系统架构设计、接口设计、数据设计、UI原型设计等文档
This commit is contained in:
398
frontend/src/views/Prediction.vue
Normal file
398
frontend/src/views/Prediction.vue
Normal file
@@ -0,0 +1,398 @@
|
||||
<template>
|
||||
<div class="prediction">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="14">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||||
<span>参数输入</span>
|
||||
<el-button size="small" @click="resetForm">重置</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="form" label-width="120px" size="small">
|
||||
<el-divider content-position="left">时间信息</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="缺勤月份">
|
||||
<el-select v-model="form.month_of_absence" style="width: 100%">
|
||||
<el-option v-for="m in 12" :key="m" :label="m + '月'" :value="m" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="星期几">
|
||||
<el-select v-model="form.day_of_week" style="width: 100%">
|
||||
<el-option label="周一" :value="2" />
|
||||
<el-option label="周二" :value="3" />
|
||||
<el-option label="周三" :value="4" />
|
||||
<el-option label="周四" :value="5" />
|
||||
<el-option label="周五" :value="6" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="季节">
|
||||
<el-select v-model="form.seasons" style="width: 100%">
|
||||
<el-option label="夏季" :value="1" />
|
||||
<el-option label="秋季" :value="2" />
|
||||
<el-option label="冬季" :value="3" />
|
||||
<el-option label="春季" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="缺勤原因">
|
||||
<el-select v-model="form.reason_for_absence" style="width: 100%">
|
||||
<el-option label="医疗咨询" :value="23" />
|
||||
<el-option label="牙科咨询" :value="28" />
|
||||
<el-option label="理疗" :value="27" />
|
||||
<el-option label="医疗随访" :value="22" />
|
||||
<el-option label="实验室检查" :value="25" />
|
||||
<el-option label="无故缺勤" :value="26" />
|
||||
<el-option label="献血" :value="24" />
|
||||
<el-option label="传染病" :value="1" />
|
||||
<el-option label="呼吸系统疾病" :value="10" />
|
||||
<el-option label="消化系统疾病" :value="11" />
|
||||
<el-option label="肌肉骨骼疾病" :value="13" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">个人信息</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="年龄">
|
||||
<el-input-number v-model="form.age" :min="18" :max="60" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="工龄">
|
||||
<el-input-number v-model="form.service_time" :min="1" :max="30" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="学历">
|
||||
<el-select v-model="form.education" style="width: 100%">
|
||||
<el-option label="高中" :value="1" />
|
||||
<el-option label="本科" :value="2" />
|
||||
<el-option label="研究生" :value="3" />
|
||||
<el-option label="博士" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="BMI指数">
|
||||
<el-input-number v-model="form.bmi" :min="18" :max="40" :precision="1" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">工作信息</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="交通费用">
|
||||
<el-input-number v-model="form.transportation_expense" :min="100" :max="400" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="通勤距离">
|
||||
<el-input-number v-model="form.distance" :min="1" :max="60" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="工作负荷">
|
||||
<el-input-number v-model="form.work_load" :min="200" :max="350" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="达标率">
|
||||
<el-input-number v-model="form.hit_target" :min="80" :max="100" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="违纪记录">
|
||||
<el-radio-group v-model="form.disciplinary_failure">
|
||||
<el-radio :value="0">无</el-radio>
|
||||
<el-radio :value="1">有</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">生活习惯</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="饮酒习惯">
|
||||
<el-radio-group v-model="form.social_drinker">
|
||||
<el-radio :value="0">否</el-radio>
|
||||
<el-radio :value="1">是</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="吸烟习惯">
|
||||
<el-radio-group v-model="form.social_smoker">
|
||||
<el-radio :value="0">否</el-radio>
|
||||
<el-radio :value="1">是</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="子女数量">
|
||||
<el-input-number v-model="form.son" :min="0" :max="5" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="宠物数量">
|
||||
<el-input-number v-model="form.pet" :min="0" :max="10" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">预测设置</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="选择模型">
|
||||
<el-select v-model="selectedModel" style="width: 100%" :loading="modelsLoading">
|
||||
<el-option label="自动选择最优" value="" />
|
||||
<el-option
|
||||
v-for="model in availableModels"
|
||||
:key="model.name"
|
||||
:label="model.name_cn"
|
||||
:value="model.name"
|
||||
>
|
||||
<span>{{ model.name_cn }}</span>
|
||||
<span style="float: right; color: #909399; font-size: 12px">
|
||||
R²: {{ model.metrics?.r2?.toFixed(2) || '-' }}
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="模型对比">
|
||||
<el-switch v-model="showCompare" active-text="显示" inactive-text="隐藏" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item style="margin-top: 20px">
|
||||
<el-button type="primary" @click="handlePredict" :loading="loading" size="default">
|
||||
开始预测
|
||||
</el-button>
|
||||
<el-button @click="handleCompare" :loading="compareLoading" size="default">
|
||||
模型对比
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>预测结果</span>
|
||||
</template>
|
||||
<div v-if="result" class="result-container">
|
||||
<div class="result-value">{{ result.predicted_hours }}</div>
|
||||
<div class="result-unit">小时</div>
|
||||
<el-tag :type="riskTagType" size="large" style="margin-top: 20px">
|
||||
{{ result.risk_label }}
|
||||
</el-tag>
|
||||
<div style="margin-top: 20px; color: #909399">
|
||||
模型: {{ result.model_name_cn }}
|
||||
</div>
|
||||
<div style="margin-top: 8px; color: #909399; font-size: 12px">
|
||||
置信度: {{ (result.confidence * 100).toFixed(0) }}%
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="请输入参数后点击预测" />
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="compareResults.length > 0" style="margin-top: 20px">
|
||||
<template #header>
|
||||
<span>模型对比结果</span>
|
||||
</template>
|
||||
<el-table :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="80">
|
||||
<template #default="{ row }">{{ row.predicted_hours }}h</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="risk_label" label="风险等级" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getRiskType(row.risk_level)" size="small">{{ row.risk_label }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="r2" label="R²" width="70">
|
||||
<template #default="{ row }">{{ row.r2?.toFixed(3) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="推荐" width="60">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.recommended" type="success" size="small">推荐</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card style="margin-top: 20px">
|
||||
<template #header>
|
||||
<span>风险等级说明</span>
|
||||
</template>
|
||||
<div class="risk-legend">
|
||||
<div class="risk-item">
|
||||
<el-tag type="success" size="small">低风险</el-tag>
|
||||
<span>缺勤时长 < 4小时</span>
|
||||
</div>
|
||||
<div class="risk-item">
|
||||
<el-tag type="warning" size="small">中风险</el-tag>
|
||||
<span>缺勤时长 4-8小时</span>
|
||||
</div>
|
||||
<div class="risk-item">
|
||||
<el-tag type="danger" size="small">高风险</el-tag>
|
||||
<span>缺勤时长 > 8小时</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import request from '@/api/request'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const defaultForm = {
|
||||
reason_for_absence: 23,
|
||||
month_of_absence: 7,
|
||||
day_of_week: 3,
|
||||
seasons: 1,
|
||||
transportation_expense: 200,
|
||||
distance: 20,
|
||||
service_time: 5,
|
||||
age: 30,
|
||||
work_load: 250,
|
||||
hit_target: 95,
|
||||
disciplinary_failure: 0,
|
||||
education: 1,
|
||||
son: 0,
|
||||
pet: 0,
|
||||
bmi: 25,
|
||||
social_drinker: 0,
|
||||
social_smoker: 0
|
||||
}
|
||||
|
||||
const form = ref({ ...defaultForm })
|
||||
const result = ref(null)
|
||||
const loading = ref(false)
|
||||
const compareLoading = ref(false)
|
||||
const compareResults = ref([])
|
||||
const showCompare = ref(false)
|
||||
const selectedModel = ref('')
|
||||
const availableModels = ref([])
|
||||
const modelsLoading = ref(false)
|
||||
|
||||
const riskTagType = computed(() => {
|
||||
if (!result.value) return 'info'
|
||||
return getRiskType(result.value.risk_level)
|
||||
})
|
||||
|
||||
function getRiskType(level) {
|
||||
return level === 'low' ? 'success' : level === 'medium' ? 'warning' : 'danger'
|
||||
}
|
||||
|
||||
function getRowClass({ row }) {
|
||||
return row.recommended ? 'recommended-row' : ''
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.value = { ...defaultForm }
|
||||
result.value = null
|
||||
compareResults.value = []
|
||||
}
|
||||
|
||||
async function loadModels() {
|
||||
modelsLoading.value = true
|
||||
try {
|
||||
const res = await request.get('/predict/models')
|
||||
availableModels.value = res.models || []
|
||||
} catch (e) {
|
||||
console.error('Failed to load models:', e)
|
||||
} finally {
|
||||
modelsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePredict() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { ...form.value }
|
||||
if (selectedModel.value) {
|
||||
params.model_type = selectedModel.value
|
||||
}
|
||||
result.value = await request.post('/predict/single', params)
|
||||
|
||||
if (showCompare.value) {
|
||||
await handleCompare()
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error('预测失败: ' + e.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCompare() {
|
||||
compareLoading.value = true
|
||||
try {
|
||||
const res = await request.post('/predict/compare', form.value)
|
||||
compareResults.value = res.results || []
|
||||
} catch (e) {
|
||||
ElMessage.error('对比失败: ' + e.message)
|
||||
} finally {
|
||||
compareLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadModels()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.result-container {
|
||||
text-align: center;
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.result-unit {
|
||||
font-size: 16px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.risk-legend {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.risk-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.el-divider {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
:deep(.recommended-row) {
|
||||
background-color: #f0f9eb;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user