Files
pqs-9100_client/frontend/src/views/home/tabs/dashboard.vue
caozehui 2705bedc71 微调
2026-06-01 14:12:47 +08:00

873 lines
31 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.

<!--
首页Dashboard组件 - 主要功能页面
布局左侧计划树 + 右侧功能区域功能选择饼图统计表格数据
-->
<template>
<div class="static" ref="popupBaseView">
<el-row :gutter="10">
<!-- 左侧计划树区域 (20%) -->
<el-col :lg="4" :xl="4" :md="4" :sm="4">
<div class="left_tree">
<tree
ref="treeRef"
:updateSelectedTreeNode="updateData || (() => {})"
:width="viewWidth"
:height="viewHeight"
:planTable="planTable"
/>
</div>
</el-col>
<!-- 右侧主要内容区域 (80%) -->
<el-col :lg="20" :xl="20" :md="20" :sm="20">
<div class="right_container">
<!-- 功能选择 -->
<div class="container_function">
<div
class="function_item"
:class="item.checked ? 'function_item checked_function' : 'function_item'"
v-for="(item, index) in tabsList"
:key="index"
@click="handleCheckFunction(item.value)"
>
<div class="item_img">
<img :src="item.img" />
</div>
<div class="item_text">
<p>{{ item.label }}</p>
</div>
</div>
</div>
<!--检测计划统计 饼图统计-->
<div class="container_pieShow">
<el-collapse model-value="1" accordion @change="handleCollapseChange">
<el-collapse-item name="1">
<template #title>
<div class="container_pieShow_title">
<span>检测计划统计</span>
<span>{{ planName }}</span>
</div>
</template>
<!-- 饼图 -->
<div class="container_charts">
<div class="charts_info" ref="chartsInfoRef">
<pie
:customData="{
title: '设备检测状态',
textAlign: 'left'
}"
:legendData="{
icon: 'circle',
left: 'left',
top: 'bottom'
}"
:chartsData="chartsData1"
ref="pieRef1"
></pie>
</div>
<div class="charts_info">
<pie
:customData="{
title: '设备检测结果',
textAlign: 'left'
}"
:legendData="{
icon: 'circle',
left: 'left',
top: 'bottom'
}"
:chartsData="chartsData2"
ref="pieRef2"
></pie>
</div>
<div class="charts_info">
<pie
:customData="{
title: '设备报告状态',
textAlign: 'left'
}"
:legendData="{
icon: 'circle',
left: 'left',
top: 'bottom'
}"
:chartsData="chartsData3"
ref="pieRef3"
></pie>
</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
<!--下方表格数据-->
<el-tabs
class="tabs-menu"
type="border-card"
v-model="editableTabsValue"
:style="{ height: tabsHeight }"
>
<el-tab-pane :label="tabLabel1" :style="{ height: tabPaneHeight }">
<!-- 设备数据表格 -->
<div class="container_table" :style="{ height: tableHeight }">
<Table
ref="tableRef1"
:id="currentId"
:plan="select_Plan"
:planArray="planList2"
:planTable="planTable"
@batchGenerateClicked="handleBatchGenerate"
:height="tabPaneHeight"
></Table>
</div>
</el-tab-pane>
</el-tabs>
</div>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { type Plan } from '@/api/plan/interface'
import { type CollapseModelValue } from 'element-plus/es/components/collapse/src/collapse.mjs'
import { type Device } from '@/api/device/interface/device'
import { type ResultData } from '@/api/interface'
import pie from '@/components/echarts/pie/default.vue'
import tree from '../components/tree.vue'
import Table from '../components/table.vue'
import { getBoundPqDevList, getPlanList, getPlanListByPattern } from '@/api/plan/plan'
import { nextTick, onBeforeMount, onUnmounted, ref, watch } from 'vue'
import { useModeStore } from '@/stores/modules/mode' // 引入模式 store
import { useDictStore } from '@/stores/modules/dict'
import { useViewSize } from '@/hooks/useViewSize'
const planName = ref('')
const dictStore = useDictStore()
const modeStore = useModeStore()
const chartsInfoRef = ref<HTMLElement | null>(null)
const chartsWidth = ref<number>(0)
const treeRef = ref()
const form: any = ref({
activeTabs: 0, //功能选择,例如报告生成
checkStatus: 0, //检测状态
checkReportStatus: 0, //检测报告状态
checkResult: 0, //检测结果
deviceBindStatus: 0, //绑定状态
deviceType: 0, //设备类型
manufacturer: 0 //制造厂商
})
const router = useRouter()
const tabShow = ref(false)
const tabLabel1 = ref('设备检测')
const editableTabsValue = ref('0')
const checkStateTable = ref<number[]>([0, 1, 2])
const tabsHeight = ref('calc(100vh - 522px)') // 初始高度
const tabPaneHeight = ref('calc(100% - 5px)') // 初始高度
const tableHeight = ref('calc(100% - 50px)') // 初始高度
// ============================ 计划数据状态 ============================
const planList = ref<Plan.ReqPlan[]>([]) // 计划列表(过滤后)
const planList2 = ref<Plan.ReqPlan[]>([]) // 计划列表原始数据(包含子计划)
const select_Plan = ref<Plan.ReqPlan>() // 当前选中的计划
const planTable = ref<any[]>([]) // 比对模式下的计划表格数据
// ============================ 视图状态 ============================
const isLabelLineShow = ref(true) // 饼图是否显示引导线
const { popupBaseView, viewWidth, viewHeight } = useViewSize() // 视口尺寸hook
/**
* 处理折叠面板展开/收起事件
* 根据面板状态动态调整表格高度,优化空间利用
* @param val - 当前展开的面板值
*/
const handleCollapseChange = (val: CollapseModelValue) => {
// 计算新的高度值
let newHeight
if (Array.isArray(val)) {
// 数组情况:有展开项时高度更小,无展开项时高度更大
newHeight = val.length > 0 ? 'calc(100vh - 522px)' : 'calc(100vh - 333px)'
} else {
// 单个值情况:展开时高度更小,收起时高度更大
newHeight = val ? 'calc(100vh - 538px)' : 'calc(100vh - 333px)'
}
// 更新各个容器的高度
tabsHeight.value = newHeight
tabPaneHeight.value = `calc(100% - 5px)`
tableHeight.value = `calc(100% - 5px)`
}
// 设置默认主题颜色
localStorage.setItem('color', '#91cc75')
// ============================ 功能按钮配置 ============================
/**
* 主功能选项卡配置
* 包含4个主要功能设备检测、报告生成、设备归档、数据操作
*/
const tabsList = ref([
{
label: '设备检测', // 设备检测功能
value: 0,
img: new URL('/src/assets/images/plan/static/1.svg', import.meta.url).href,
checked: true // 默认选中
},
{
label: '报告生成', // 检测报告生成功能
value: 3,
img: new URL('/src/assets/images/plan/static/3.svg', import.meta.url).href,
checked: false
},
{
label: '设备归档', // 设备归档管理功能
value: 4,
img: new URL('/src/assets/images/plan/static/4.svg', import.meta.url).href,
checked: false
},
{
label: '数据操作', // 数据查询和操作功能
value: 5,
img: new URL('/src/assets/images/plan/static/5.svg', import.meta.url).href,
checked: false
}
])
// 初始化默认选中第一个功能选项卡
form.value.activeTabs = tabsList.value[0].value
// ============================ 组件引用和状态 ============================
const tableRef1 = ref() // 主表格组件引用
const currentId = ref('') // 当前选中的计划ID
// ============================ 监听器 ============================
let isUpdatingTabs = false // 防止重复调用的标志
/**
* 监听功能切换并通知表格组件更新配置
* 不再传递静态数据让表格组件通过API获取真实数据
*/
watch(
() => form.value.activeTabs,
async newTabs => {
if (isUpdatingTabs) return // 如果正在更新中,跳过
isUpdatingTabs = true
// 只传递功能模式,不传递静态假数据
// 表格组件会根据功能模式通过API获取对应的真实数据
tableRef1.value && tableRef1.value.changeActiveTabs(newTabs)
// 等待一个微任务队列后重置标志
await nextTick()
isUpdatingTabs = false
}
// 去掉 immediate: true避免初始化时重复调用
)
// ============================ 饼图组件引用和数据 ============================
const pieRef1 = ref(), // 设备检测状态饼图引用
pieRef2 = ref(), // 设备检测结果饼图引用
pieRef3 = ref() // 设备报告状态饼图引用
const chartsData1: any = ref([]), // 设备检测状态统计数据
chartsData2: any = ref([]), // 设备检测结果统计数据
chartsData3: any = ref([]) // 设备报告状态统计数据
// ============================ 工具函数 ============================
/**
* 递归查找指定 ID 的计划
* @param plans - 计划数组
* @param id - 计划 ID
* @returns 找到的计划对象或 undefined
*/
const findPlanById = (plans: any, id: string): Plan.ReqPlan | undefined => {
if (!plans) return undefined
for (const plan of plans) {
if (plan?.id === id) {
return plan
}
// 递归搜索子计划
if (plan?.children) {
const foundPlan = findPlanById(plan.children, id)
if (foundPlan) {
return foundPlan
}
}
}
return undefined
}
/**
* 处理树节点选中事件的回调函数
* 根据选中的计划节点更新饼图数据并切换相应的功能模式
* @param id - 选中的计划 ID
*/
const updateData = (id: string) => {
// 刷新饼图数据
getPieData(id)
// 查找当前计划的父节点名称
const parentNodeName = ref('')
if (planList.value?.length) {
for (let i = 0; i < planList.value.length; i++) {
const children = planList.value[i]?.children
if (Array.isArray(children) && children.length > 0) {
for (let j = 0; j < children.length; j++) {
if (children[j]?.id === id) {
parentNodeName.value = planList.value[i]?.name || ''
break
}
}
}
}
}
// 根据父节点名称自动切换功能模式
if (parentNodeName.value === '检测完成') {
handleCheckFunction(5) // 切换到数据操作模式
} else {
handleCheckFunction(0) // 切换到设备检测模式
}
}
/**
* 获取指定计划的设备统计数据并更新饼图
* 分别统计设备的检测状态、检测结果、报告状态分布情况
* @param id - 计划 ID
*/
const getPieData = async (id: string) => {
currentId.value = id // 设置当前选中的计划ID
// 初始化各类统计计数器
const checkStateCount: { [key: number]: number } = { 0: 0, 1: 0, 2: 0, 3: 0 } // 检测状态计数:未检(0)、检测中(1)、检测完成(2)、归档(3)
const checkResultCount: { [key: number]: number } = { 0: 0, 1: 0, 2: 0 } // 检测结果计数:不符合(0)、符合(1)、未检(2)
const reportStateCount: { [key: number]: number } = { 0: 0, 1: 0, 2: 0 } // 报告状态计数:未生成(0)、已生成(1)、未检(2)
if (id) {
const boundPqDevList = ref<Device.ResPqDev[]>([]) //根据检测计划id查询出所有已绑定的设备
const plan = findPlanById(planList.value, id)
planName.value = '所选计划:' + (plan?.name || '')
select_Plan.value = plan
const pqDevList_Result2 = await getBoundPqDevList({ planIdList: [id], checkStateList: [0, 1, 2, 3] })
boundPqDevList.value = pqDevList_Result2.data as Device.ResPqDev[]
// 遍历 boundPqDevList 并更新计数对象
boundPqDevList.value.forEach(t => {
if (t.checkState !== undefined && t.checkState !== null && checkStateCount[t.checkState] !== undefined) {
checkStateCount[t.checkState]++
}
})
boundPqDevList.value.forEach(t => {
if (
t.checkResult !== undefined &&
t.checkResult !== null &&
checkResultCount[t.checkResult] !== undefined
) {
checkResultCount[t.checkResult]++
}
})
boundPqDevList.value.forEach(t => {
if (
t.reportState !== undefined &&
t.reportState !== null &&
reportStateCount[t.reportState] !== undefined
) {
reportStateCount[t.reportState]++
}
})
// 检查 checkStateCount 是否全为 0
if (boundPqDevList.value.length != 0) {
isLabelLineShow.value = true
const allZero = Object.values(checkStateCount).every(count => count === 0)
chartsData1.value = [
{
value: allZero ? 0 : checkStateCount[0] === 0 ? null : checkStateCount[0],
name: '未检',
itemStyle: { color: '#fac858' }
},
{
value: allZero ? 0 : checkStateCount[1] === 0 ? null : checkStateCount[1],
name: '检测中',
itemStyle: { color: '#ee6666' }
},
{
value: allZero ? 0 : checkStateCount[2] === 0 ? null : checkStateCount[2],
name: '检测完成',
itemStyle: { color: '#91cc75' }
},
{
value: allZero ? 0 : checkStateCount[3] === 0 ? null : checkStateCount[3],
name: '归档',
itemStyle: { color: '#5470c6' }
}
]
// 同样处理 chartsData2 和 chartsData3
const allZeroResult = Object.values(checkResultCount).every(count => count === 0)
chartsData2.value = [
{
value: allZeroResult ? 0 : checkResultCount[2] === 0 ? null : checkResultCount[2],
name: '未检',
itemStyle: { color: '#fac858' }
},
{
value: allZeroResult ? 0 : checkResultCount[0] === 0 ? null : checkResultCount[0],
name: '不符合',
itemStyle: { color: '#ee6666' }
},
{
value: allZeroResult ? 0 : checkResultCount[1] === 0 ? null : checkResultCount[1],
name: '符合',
itemStyle: { color: '#91cc75' }
}
]
// 检查 reportStateCount 是否全为 0
const allZeroReport = Object.values(reportStateCount).every(count => count === 0)
chartsData3.value = [
{
value: allZeroReport ? 0 : reportStateCount[2] === 0 ? null : reportStateCount[2],
name: '未检',
itemStyle: { color: '#fac858' }
},
{
value: allZeroReport ? 0 : reportStateCount[0] === 0 ? null : reportStateCount[0],
name: '未生成',
itemStyle: { color: '#ee6666' }
},
{
value: allZeroReport ? 0 : reportStateCount[1] === 0 ? null : reportStateCount[1],
name: '已生成',
itemStyle: { color: '#91cc75' }
}
]
} else {
isLabelLineShow.value = false //不展示引导线
chartsData1.value = [
{ value: null, name: '未检', itemStyle: { color: '#fac858' } },
{ value: null, name: '检测中', itemStyle: { color: '#ee6666' } },
{ value: null, name: '检测完成', itemStyle: { color: '#91cc75' } },
{ value: null, name: '归档', itemStyle: { color: '#5470c6' } },
{ value: 0, itemStyle: { color: '#eeeeee' } }
]
chartsData2.value = [
{ value: null, name: '未检', itemStyle: { color: '#fac858' } },
{ value: null, name: '不符合', itemStyle: { color: '#ee6666' } },
{ value: null, name: '符合', itemStyle: { color: '#91cc75' } },
{ value: 0, itemStyle: { color: '#eeeeee' } }
]
chartsData3.value = [
{ value: null, name: '未检', itemStyle: { color: '#fac858' } },
{ value: null, name: '未生成', itemStyle: { color: '#ee6666' } },
{ value: null, name: '已生成', itemStyle: { color: '#91cc75' } },
{ value: 0, itemStyle: { color: '#eeeeee' } }
]
}
} else {
planName.value = '所选计划:'
}
if (pieRef1.value && pieRef2.value && pieRef3.value) {
pieRef1.value.init()
pieRef2.value.init()
pieRef3.value.init()
}
}
/**
* 初始化树组件数据
* @param data - 计划数据
*/
const getTree = (data?: any) => {
treeRef.value.getTreeData(data)
}
// ============================ 路由跳转函数 ============================
/**
* 跳转到检测页面
*/
const handleDetection = () => {
router.push({
path: '/detection'
})
}
/**
* 跳转到计划详情页面
*/
const planDetail = () => {
router.push({
path: '/plan/planList'
})
}
// ============================ 主要业务逻辑函数 ============================
/**
* 处理功能选项卡切换
* 根据选中的功能更新UI状态和表格显示内容
* @param val - 功能选项卡值 (0:设备检测, 3:报告生成, 4:设备归档, 5:数据操作)
*/
const handleCheckFunction = (val: any) => {
// 重置tab状态
editableTabsValue.value = '0'
// 更新功能按钮的选中状态
tabsList.value.map((item: any, index: any) => {
item.checked = val == item.value
})
tabShow.value = false
// 根据选中的功能设置不同的过滤条件和标签
switch (val) {
case 0: // 设备检测模式
checkStateTable.value = [0, 1, 2] // 显示未检、检测中、检测完成的设备
tabLabel1.value = '设备检测'
break
case 1: // 手动检测模式(预留)
tabLabel1.value = '手动检测'
break
case 2: // 设备复检模式(预留)
tabLabel1.value = '设备复检'
break
case 3: // 报告生成模式
checkStateTable.value = [2, 3] // 显示检测完成和已归档的设备
tabLabel1.value = '报告生成'
break
case 4: // 设备归档模式
checkStateTable.value = [2] // 只显示检测完成的设备
tabLabel1.value = '设备归档'
break
case 5: // 数据查询模式
checkStateTable.value = [2, 3] // 显示检测完成和已归档的设备
tabLabel1.value = '数据查询'
break
}
// 更新当前激活的功能选项卡
form.value.activeTabs = val
// 刷新饼图数据以确保统计信息同步
if (currentId.value) {
getPieData(currentId.value)
}
}
// ============================ 监听器和事件处理 ============================
/**
* 饼图容器尺寸变化监听器
* 当容器大小变化时自动调整饼图尺寸,保持响应式设计
*/
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
// 更新容器宽度
chartsWidth.value = entry.contentRect.width
// 同步调整三个饼图的尺寸宽度为容器的95%高度固定180px
pieRef1.value?.reSize(chartsWidth.value * 0.95, 180, true)
pieRef2.value?.reSize(chartsWidth.value * 0.95, 180, true)
pieRef3.value?.reSize(chartsWidth.value * 0.95, 180, true)
}
})
// ============================ 初始化函数 ============================
/**
* 初始化计划数据
* 根据当前模式获取相应的计划列表数据
*/
const initPlan = async () => {
// 获取当前模式对应的数据字典ID
const patternId = dictStore.getDictData('Pattern').find(item => item.name === modeStore.currentMode)?.id ?? ''
// 构建计划查询请求对象
const reqPlan: Plan.ReqPlan = {
pattern: patternId, // 模式ID
datasourceIds: '',
sourceIds: '',
planId: '',
scriptName: '',
errorSysName: '',
sourceName: '',
devIds: [],
id: '',
name: '',
dataSourceId: '',
scriptId: '',
errorSysId: '',
timeCheck: 0,
testState: 0,
reportState: 0,
result: 0,
code: 0,
state: 0,
standardDevNameStr: '',
associateReport: 0,
reportTemplateName: '',
reportTemplateVersion: '',
dataRule: '',
testItemNameStr: '',
testItems: [],
standardDevIds: [],
standardDevMap: new Map<string, number>()
}
// 获取计划数据
const result = (await getPlanListByPattern(reqPlan)) as ResultData<Plan.ReqPlan[]>
planList2.value = result.data || []
// 创建计划数据的副本用于过滤处理
planList.value = JSON.parse(JSON.stringify(planList2.value))
// 过滤子计划,只保留 pid 为 '0' 的项目
planList.value = planList.value.map((item: any) => {
if (item?.children) {
item.children = item.children.filter((child: any) => child?.pid === '0')
}
return item
})
}
// ============================ 生命周期函数 ============================
/**
* 组件挂载前的初始化操作
* 1. 初始化计划数据
* 2. 设置默认选中的计划
* 3. 初始化图表和树组件
* 4. 根据模式加载额外数据
*/
onBeforeMount(async () => {
// 初始化计划数据
await initPlan()
// 找到第一个有子计划的项目,并设置为默认选中
if (planList.value?.length) {
for (let i = 0; i < planList.value.length; i++) {
const children = planList.value[i]?.children
if (Array.isArray(children) && children.length > 0) {
currentId.value = children[0]?.id // 选中第一个子计划
break // 确保只选中一个
}
}
}
// 初始化图表尺寸监听器
if (chartsInfoRef.value) {
resizeObserver.observe(chartsInfoRef.value)
}
// 初始化树组件和饼图数据
getTree(planList.value || [])
getPieData(currentId.value)
// 如果不是比对模式,直接返回
if (modeStore.currentMode != '比对式') return
// 比对模式下加载额外的计划表格数据
const patternId2 = dictStore.getDictData('Pattern').find(item => item.name === modeStore.currentMode)?.id
if (patternId2 !== undefined) {
planTable.value = await getPlanList({ patternId: patternId2 })
}
})
/**
* 组件卸载时的清理操作
* 移除尺寸监听器,防止内存泄漏
*/
onUnmounted(async () => {
if (chartsInfoRef.value) {
resizeObserver.unobserve(chartsInfoRef.value)
}
})
/**
* 处理批量操作完成后的数据更新
* 更新计划数据、树状态和饼图表格会通过watch自动更新
*/
const handleBatchGenerate = async () => {
// 重新获取计划数据
await initPlan()
// 更新树的选中状态
treeRef.value.clickTableToTree(planList.value || [], currentId.value)
// 重新获取饼图数据deviceData更新后watch会自动触发表格更新
getPieData(currentId.value)
// 批量操作后的表格刷新 - 这个调用与watch监听器无关是通过emit触发的
tableRef1.value && tableRef1.value.changeActiveTabs(form.value.activeTabs)
}
</script>
<style lang="scss" scoped>
.static {
.left_tree {
height: 100%;
background-color: #fff;
}
.right_container {
flex: none;
height: 100%;
display: flex;
flex-direction: column;
.container_function {
width: 100%;
height: auto;
background: #fff;
border-radius: 4px;
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 10px;
padding: 10px 20px 10px 20px;
box-sizing: border-box;
.function_item {
flex: none;
width: 6%;
height: 70px;
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
cursor: pointer;
background-color: #607eab;
border-radius: 8px;
padding: 0px 30px;
margin-right: 50px;
.item_img {
width: 60px;
height: 60px;
border-radius: 50%;
// background-color: #607eab;
display: flex;
align-items: center;
justify-content: center;
img {
width: 40px;
height: auto;
}
}
.item_img:nth-child(3),
.item_img:nth-child(6) {
padding: 10px !important;
img {
width: 20px !important;
height: auto;
}
}
.item_text {
p {
margin: 0;
font-weight: 800;
color: #fff;
font-size: 14px;
font-family: 'Microsoft YaHei', '微软雅黑', 'Arial', sans-serif;
}
}
}
.function_item:hover,
.checked_function {
background-color: var(--el-color-primary);
.item_img {
// background-color: var(--el-color-primary);
}
.item_text {
p {
// color: var(--el-color-primary);
color: #fff;
}
}
}
}
.container_pieShow {
width: 100% !important;
height: auto;
background-color: #eee;
margin-bottom: 10px;
.container_pieShow_title {
display: flex;
justify-content: space-between;
padding: 0 15px;
font-weight: bold;
width: 99%;
font-size: 14px;
}
}
.el-collapse {
width: 100% !important;
height: 100% !important;
background-color: #eee;
}
.el-collapse-item {
width: 100% !important;
height: 100% !important;
background-color: #eee;
}
.container_charts {
width: 100%;
height: 100%;
background-color: #eee;
display: flex;
justify-content: space-between;
.charts_info {
margin-top: 1px;
flex: none;
width: 33.1%;
height: 100% !important;
background-color: #fff;
}
}
.el-tabs {
width: 100% !important;
border-radius: 4px;
}
.tabs-menu {
height: 100%;
border: 0;
}
.container_table {
// width: 100%;
flex: 1 !important;
//height: calc(100vh - 360px - 180px);
height: 100% !important;
border-radius: 4px;
width: 100% !important;
// display: none;
.table_info {
width: 100%;
height: 100%;
}
}
}
}
:deep(.el-collapse-item__header) {
color: var(--el-color-primary);
font-size: 14px;
font-family: 'Microsoft YaHei', '微软雅黑', 'Arial', sans-serif;
}
:deep(.el-collapse-item__conten) {
padding-bottom: 0px !important;
}
:deep(.el-collapse-item__content) {
padding-bottom: 0px !important;
}
:deep(.el-tabs--border-card > .el-tabs__content) {
padding: 0 !important;
}
</style>