| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702 |
- <template>
- <div class="dashboard">
- <div class="header">
- <div class="title-section">
- <h1><i class="fas fa-shield-alt" style="color:#2c5282; margin-right: 8px;"></i>综合预警工作台</h1>
- <p>员工综合评估(<75分红色预警 | ≥90分优秀)</p>
- </div>
- <div class="badge-group">
- <div class="alert-badge"><i class="fas fa-exclamation-triangle"></i> 实时预警</div>
- <div class="alert-badge" style="border-left-color:#f97316;"><i class="fas fa-chart-line"></i> 动态月度数据
- </div>
- </div>
- </div>
- <div class="filter-bar">
- <div class="time-range">
- <button class="time-btn" :class="{ active: activeRange === 'week' }"
- @click="setActiveRange('week')">近一周</button>
- <button class="time-btn" :class="{ active: activeRange === 'month' }"
- @click="setActiveRange('month')">近一月</button>
- <button class="time-btn" :class="{ active: activeRange === 'quarter' }"
- @click="setActiveRange('quarter')">近三月</button>
- <button class="time-btn" :class="{ active: activeRange === 'year' }"
- @click="setActiveRange('year')">近一年</button>
- <div class="custom-date">
- <el-date-picker v-model="startDate" type="date" placeholder="开始日期" style="width: 150px;" />
- <span style="margin: 0 8px;">至</span>
- <el-date-picker v-model="endDate" type="date" placeholder="结束日期" style="width: 150px;" />
- </div>
- <el-tree-select v-model="selectedOrg" :data="cascadeOptions"
- :props="{ label: 'label', value: 'value', children: 'children' }" node-key="value"
- placeholder="组织架构/员工" clearable filterable check-strictly style="width: 480px;" />
- </div>
- <div class="filter-group">
- <el-select v-model="selectedAlertLevel" placeholder="预警等级" clearable style="width: 250px;">
- <el-option label="全部" value=""></el-option>
- <el-option v-for="item in alert_level" :key="item.value" :label="item.label" :value="item.value" />
- </el-select>
- <el-button type="primary" @click="handleSearch">搜索</el-button>
- <el-button @click="handleReset">重置</el-button>
- </div>
- </div>
- <div class="employee-section">
- <div class="section-title">
- <i class="fas fa-users" style="color:#dc2626;"></i> 员工综合预警
- <span
- style="font-size:0.7rem; background:#eef2ff; padding:4px 12px; border-radius:30px; margin-left:12px;">
- 评分依据:员工配分表
- </span>
- </div>
- <div class="employee-card">
- <div style="overflow-x: auto;">
- <el-table :data="filteredEmployees" :row-class-name="getRowClass" style="width:100%;" border stripe>
- <el-table-column prop="userId" label="员工ID" />
- <el-table-column prop="nickName" label="姓名">
- <template #default="{ row }">
- <strong>{{ row.nickName }}</strong>
- </template>
- </el-table-column>
- <el-table-column prop="deptName" label="所属部门" />
- <el-table-column prop="eventTime" label="事件时间" sortable width="180" />
- <el-table-column prop="dimensionName" label="维度名称" />
- <el-table-column prop="level2Name" label="二级指标名称" />
- <el-table-column prop="totalScore" label="扣分分值合计" sortable />
- <el-table-column prop="occurrenceCount" label="发生次数" sortable />
- <el-table-column prop="warningLevel" label="预警等级">
- <template #default="{ row }">
- <span v-if="row.warningLevel == '1'" style="color:#b91c1c;" class="status-badge">{{
- getAlertLabel(row.warningLevel)
- }}</span>
- <span v-else class="status-excellent"><i class="fas fa-star"></i>
- {{ getAlertLabel(row.warningLevel) }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="coreRisks" label="核心风险" />
- <el-table-column prop="statusLabel" label="状态标签">
- <template #default="{ row }">
- <span v-if="row.statusLabel == '1'" style="color:#b91c1c;"><i class="fas fa-bell"></i>
- {{ getStatusLabel(row.statusLabel) }}</span>
- <span v-else><i class="fas fa-crown"></i> {{ getStatusLabel(row.statusLabel) }}</span>
- </template>
- </el-table-column>
- </el-table>
- <el-pagination v-show="total > 0" :total="total" v-model:current-page="queryParams.pageNum"
- v-model:page-size="queryParams.pageSize" :page-sizes="[10, 20, 50, 100]"
- layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
- @current-change="handlePageChange" style="margin-top: 16px; justify-content: flex-end;" />
- </div>
- <div class="warning-summary">
- <div><i class="fas fa-circle" style="color:#ef4444;"></i> <strong>红色预警</strong> (综合评估 < 75分) 共计
- {{ redAlertCount }} 人 → 立即约谈/培训</div>
- <div><i class="fas fa-circle" style="color:#22c55e;"></i> <strong>优秀标杆</strong> (综合评估 ≥ 90分) 共计 {{
- excellentCount }} 人 → 表彰激励</div>
- <div><i class="fas fa-chart-simple"></i> 全员平均综合得分: <strong>{{ avgScore }}</strong> 分</div>
- </div>
- </div>
- </div>
- <footer>
- <i class="far fa-bell"></i> 员工部门:旅检一/二/三部 | 阈值:<75红色预警 | ≥90优秀
- </footer>
- </div>
- </template>
- <script setup>
- import { ref, computed, reactive, onMounted, watch } from 'vue'
- import { getDeptUserTree } from '@/api/item/items'
- import { getRedLineWarningPageData } from '@/api/warningManage/redLineWarning'
- import { useDict } from '@/utils/dict'
- import { useRoute } from 'vue-router'
- const route = useRoute()
- const { alert_level, status_label } = useDict('alert_level', 'status_label')
- const dateRangeInput = ref(null)
- const activeRange = ref('month')
- const startDate = ref(null)
- const endDate = ref(null)
- const selectedAlertLevel = ref('')
- const selectedOrg = ref('')
- const cascadeOptions = ref([])
- const newNames = [
- "刘孟", "王苡衡", "周海涵", "何欣怡", "王崎",
- "黄政", "刘韵儿", "汪安霖", "张诗敏", "张维", "陈凯琳"
- ]
- const deptList = ["旅检一部", "旅检二部", "旅检三部"]
- const scores = [68, 92, 55, 96, 72, 88, 48, 94, 81, 63, 91]
- const coreRisksOrOutstandingAchievementsList = [
- "违规操作2次+投诉1起,考试成绩62分",
- "优秀服务案例,安保测试满分,无违规",
- "安保测试未通过,不安全事件责任人",
- "典型服务案例主导者,考试成绩98",
- "质控拦截违规2次,考试成绩74分",
- "考试成绩89分,服务巡查良好",
- "严重不规范操作,安保测试未过",
- "质控拦截贡献突出,考试成绩96",
- "表现良好,无安全事故,考试86分",
- "服务巡查扣分,投诉关联2件",
- "临近预警线,服务巡查扣分1次"
- ]
- const indicatorNames = ["安全违规次数", "旅客投诉率", "安检差错率", "培训合格率", "操作规范度"]
- const deductionScores = [2, 5, 3, 1, 4]
- const occurrenceCounts = [1, 2, 3, 1, 2]
- const employeesData = ref([])
- const queryParams = reactive({
- pageNum: 1,
- pageSize: 10
- })
- // 将组织架构数据转换为树形数据
- const transformCascadeData = (nodes) => {
- if (!nodes) return []
- return nodes.map(node => {
- const deptType = node.deptType || node.nodeType
- const label = deptType === 'user'
- ? (node.nickName || node.label)
- : (node.deptName || node.name || node.label)
- let value;
- if (deptType === 'STATION') value = `station_${node.id}`;
- else if (deptType === 'BRIGADE') value = `dept_${node.id}`
- else if (deptType === 'MANAGER') value = `team_${node.id}`
- else if (deptType === 'TEAMS') value = `group_${node.id}`
- else value = `user_${node.id}`
- const children = node.children && node.children.length > 0 ? transformCascadeData(node.children) : undefined
- return {
- label,
- value,
- deptType,
- children
- }
- })
- }
- // 获取选中的部门或用户信息
- const getSelectedInfo = (selectedValue) => {
- if (!selectedValue) return null
- const findNodeByValue = (nodes, value) => {
- for (const node of nodes) {
- if (node.value === value) {
- return node
- }
- if (node.children) {
- const found = findNodeByValue(node.children, value)
- if (found) return found
- }
- }
- return null
- }
- return findNodeByValue(cascadeOptions.value, selectedValue)
- }
- const allFilteredEmployees = computed(() => {
- let result = employeesData.value || []
- // 预警等级筛选
- if (selectedAlertLevel.value) {
- result = result.filter(emp => emp.warningLevel == selectedAlertLevel.value)
- }
- return result
- })
- const filteredEmployees = computed(() => {
- const start = (queryParams.pageNum - 1) * queryParams.pageSize
- const end = start + queryParams.pageSize
- return allFilteredEmployees.value.slice(start, end)
- })
- const total = computed(() => allFilteredEmployees.value.length)
- function handlePageChange(newPage) {
- queryParams.pageNum = newPage
- }
- function handleSizeChange(newSize) {
- queryParams.pageSize = newSize
- queryParams.pageNum = 1
- }
- const redAlertCount = computed(() => {
- return employeesData.value?.redAlertNum || 0
- })
- const excellentCount = computed(() => {
- return employeesData.value?.excellentBenchmarkNum || 0
- })
- const avgScore = computed(() => {
- return employeesData.value?.averageComprehensiveScore || 0
- })
- const getRowClass = ({ row }) => {
- if (row.overallScore < 75) return "employee-warning-row"
- if (row.overallScore >= 90) return "employee-excellent-row"
- return ""
- }
- const getAlertLabel = (value) => {
- if (!value) return ''
- const item = alert_level.value.find(d => d.value === value)
- return item ? item.label : value
- }
- const getStatusLabel = (value) => {
- if (!value) return ''
- const item = status_label.value.find(d => d.value === value)
- return item ? item.label : value
- }
- const setActiveRange = (range) => {
- activeRange.value = range
- // 清除自定义日期
- if (range !== 'custom') {
- startDate.value = null
- endDate.value = null
- }
- }
- const handleSearch = () => {
- fetchWarningData()
- }
- const handleReset = () => {
- startDate.value = null
- endDate.value = null
- activeRange.value = 'month'
- selectedAlertLevel.value = ''
- selectedOrg.value = ''
- queryParams.pageNum = 1
- fetchWarningData()
- }
- const formatDate = (d) => {
- if (!d) return ''
- const date = new Date(d)
- const y = date.getFullYear()
- const m = String(date.getMonth() + 1).padStart(2, '0')
- const day = String(date.getDate()).padStart(2, '0')
- return `${y}-${m}-${day}`
- }
- const getDateRangeFromActive = () => {
- const now = new Date()
- let start = new Date(now)
- if (activeRange.value === 'week') {
- start.setDate(now.getDate() - 7)
- } else if (activeRange.value === 'month') {
- start.setMonth(now.getMonth() - 1)
- } else if (activeRange.value === 'quarter') {
- start.setMonth(now.getMonth() - 3)
- } else {
- start.setFullYear(now.getFullYear() - 1)
- }
- return { startDate: formatDate(start), endDate: formatDate(now) }
- }
- const fetchWarningData = async () => {
- queryParams.pageNum = 1
- let params = {}
- if (startDate.value && endDate.value) {
- params.startDate = formatDate(startDate.value)
- params.endDate = formatDate(endDate.value)
- } else {
- const range = getDateRangeFromActive()
- params.startDate = range.startDate
- params.endDate = range.endDate
- }
- // cascadeOptions 未加载完成时跳过组织筛选,onMounted 加载完后会再调一次
- if (cascadeOptions.value.length > 0 && selectedOrg.value) {
- const selectedInfo = getSelectedInfo(selectedOrg.value)
- if (selectedInfo) {
- const rawId = Number(selectedInfo.value.split('_')[1])
- if (selectedInfo.deptType === 'BRIGADE') params.deptId = rawId
- else if (selectedInfo.deptType === 'MANAGER') params.teamId = rawId
- else if (selectedInfo.deptType === 'TEAMS') params.groupId = rawId
- else if (selectedInfo.deptType === 'user') params.userId = rawId
- }
- }
- try {
- const r1 = await getRedLineWarningPageData(params)
- if (r1.data) {
- employeesData.value = r1.data
- }
- } catch (error) {
- console.error('获取预警数据失败:', error)
- }
- }
- // 从路由参数回显查询条件
- const applyRouteQuery = (query) => {
- const { id, startDate: sd, endDate: ed, activeRange: ar, alertLevel, org } = query
- if (id) {
- selectedOrg.value = `user_${id}`
- } else {
- selectedOrg.value = ''
- }
- if (sd && ed) {
- startDate.value = sd
- endDate.value = ed
- activeRange.value = 'custom'
- } else {
- startDate.value = null
- endDate.value = null
- activeRange.value = ar || 'month'
- }
- selectedAlertLevel.value = alertLevel || ''
- queryParams.pageNum = 1
- }
- onMounted(async () => {
- // 先回显路由参数
- applyRouteQuery(route.query)
- // 再加载组织架构树
- try {
- const res = await getDeptUserTree()
- if (res.data) {
- cascadeOptions.value = transformCascadeData(res.data)
- }
- } catch (error) {
- console.error('获取组织架构数据失败:', error)
- }
- // 最后查询(此时 cascadeOptions 已就绪,不会重复请求)
- fetchWarningData()
- })
- // 监听后续路由参数变化
- watch(() => route.query, (query) => {
- applyRouteQuery(query)
- fetchWarningData()
- })
- </script>
- <style scoped>
- @keyframes subtlePulse {
- 0% {
- opacity: 0.7;
- }
- 50% {
- opacity: 1;
- background: #ffb3b3;
- }
- 100% {
- opacity: 0.7;
- }
- }
- .dashboard {
- max-width: 100%;
- padding: 10px;
- }
- .header {
- margin-bottom: 28px;
- display: flex;
- justify-content: space-between;
- align-items: flex-end;
- flex-wrap: wrap;
- gap: 16px;
- }
- .title-section h1 {
- font-size: 1.7rem;
- font-weight: 700;
- background: linear-gradient(135deg, #1e3c72, #2a5298);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
- letter-spacing: -0.3px;
- }
- .title-section p {
- color: #475569;
- margin-top: 6px;
- font-size: 0.85rem;
- }
- .badge-group {
- display: flex;
- gap: 12px;
- }
- .alert-badge {
- background: #fff1f0;
- border-left: 5px solid #ef4444;
- padding: 6px 16px;
- border-radius: 40px;
- font-weight: 600;
- font-size: 0.85rem;
- }
- .alert-badge i {
- color: #ef4444;
- margin-right: 6px;
- }
- .filter-bar {
- background: white;
- border-radius: 60px;
- padding: 8px 20px;
- margin-bottom: 10px;
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02), 0 1px 2px rgba(0, 0, 0, 0.03);
- border: 1px solid #eef2f6;
- }
- .time-range {
- display: flex;
- gap: 8px;
- align-items: center;
- flex-wrap: wrap;
- }
- .time-btn {
- background: #f8fafc;
- border: 1px solid #e2e8f0;
- padding: 6px 18px;
- border-radius: 40px;
- font-size: 0.8rem;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.2s;
- color: #1e293b;
- }
- .time-btn.active {
- background: #2563eb;
- border-color: #2563eb;
- color: white;
- }
- .time-btn:hover {
- background: #e2e8f0;
- }
- .custom-date {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .clear-btn {
- background: transparent;
- border: 1px solid #e2e8f0;
- padding: 4px 8px;
- border-radius: 40px;
- cursor: pointer;
- color: #64748b;
- }
- .clear-btn:hover {
- background: #f1f5f9;
- }
- .custom-date :deep(.el-date-picker) {
- background: transparent;
- }
- .custom-date :deep(.el-input__wrapper) {
- background: transparent;
- }
- .filter-group {
- display: flex;
- gap: 12px;
- align-items: center;
- }
- .search-wrapper {
- display: flex;
- align-items: center;
- gap: 8px;
- background: #f8fafc;
- padding: 4px 12px;
- border-radius: 40px;
- border: 1px solid #e2e8f0;
- }
- .search-btn {
- background: #2563eb;
- border: none;
- color: white;
- padding: 4px 14px;
- border-radius: 30px;
- font-size: 0.75rem;
- font-weight: 500;
- cursor: pointer;
- transition: 0.2s;
- }
- .search-btn:hover {
- background: #1d4ed8;
- }
- .employee-section {
- margin-top: 20px;
- width: 100%;
- }
- .section-title {
- font-size: 1.2rem;
- font-weight: 700;
- margin-bottom: 1rem;
- display: flex;
- align-items: center;
- gap: 8px;
- border-left: 5px solid #ef4444;
- padding-left: 14px;
- }
- .employee-card {
- background: #ffffff;
- border-radius: 24px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03), 0 1px 2px rgba(0, 0, 0, 0.05);
- border: 1px solid #eef2f6;
- padding: 1rem 1.5rem;
- width: 100%;
- }
- .data-table {
- width: 100%;
- border-collapse: collapse;
- font-size: 0.8rem;
- }
- .data-table th {
- text-align: left;
- padding: 10px 6px 8px 0;
- font-weight: 600;
- color: #334155;
- border-bottom: 2px solid #e2e8f0;
- }
- .data-table td {
- padding: 8px 6px 8px 0;
- border-bottom: 1px solid #f1f5f9;
- vertical-align: middle;
- }
- .employee-warning-row {
- background-color: #fff5f5;
- border-left: 4px solid #ef4444;
- }
- .employee-excellent-row {
- background-color: #f0fdf4;
- border-left: 4px solid #22c55e;
- }
- .score-danger {
- font-weight: 800;
- color: #dc2626;
- background: #fee2e2;
- padding: 2px 8px;
- border-radius: 30px;
- display: inline-block;
- font-size: 0.8rem;
- }
- .score-excellent {
- font-weight: 800;
- color: #15803d;
- background: #dcfce7;
- padding: 2px 8px;
- border-radius: 30px;
- display: inline-block;
- font-size: 0.8rem;
- }
- .warning-summary {
- background: #fff9f0;
- border-radius: 18px;
- padding: 12px 20px;
- margin-top: 18px;
- display: flex;
- gap: 28px;
- flex-wrap: wrap;
- font-weight: 500;
- font-size: 0.85rem;
- }
- footer {
- text-align: center;
- margin-top: 32px;
- font-size: 0.7rem;
- color: #7e8b9c;
- border-top: 1px solid #e2edf7;
- padding-top: 18px;
- }
- @media (max-width: 1200px) {
- .cards-grid {
- grid-template-columns: repeat(4, 1fr);
- gap: 14px;
- }
- }
- @media (max-width: 800px) {
- .cards-grid {
- grid-template-columns: repeat(2, 1fr);
- }
- body {
- padding: 20px;
- }
- .filter-bar {
- border-radius: 24px;
- flex-direction: column;
- align-items: stretch;
- }
- .org-search {
- justify-content: space-between;
- }
- }
- @media (max-width: 550px) {
- .cards-grid {
- grid-template-columns: 1fr;
- }
- }
- </style>
|