修改项目树问题 绘制稳态治理分析页面
This commit is contained in:
569
src/views/govern/manage/process/index.vue
Normal file
569
src/views/govern/manage/process/index.vue
Normal file
@@ -0,0 +1,569 @@
|
||||
<template>
|
||||
<div class="default-main manage-process" :style="{ height: pageHeight.height }">
|
||||
<DeviceTree @node-click="nodeClick" @init="nodeClick" @deviceTypeChange=""></DeviceTree>
|
||||
<div class="manage-process-right">
|
||||
<div class="process-flow-section">
|
||||
<el-descriptions title="数据链路">
|
||||
|
||||
</el-descriptions>
|
||||
<div class="process-flow-wrap">
|
||||
<div class="process-flow">
|
||||
<template v-for="(node, index) in flowNodes" :key="node.label">
|
||||
<div class="process-step">
|
||||
<div class="process-node" :class="`process-node--${getNodeState(index)}`">
|
||||
<div class="process-node__icon" :style="{ color: getNodeColor(index) }">
|
||||
<el-icon v-if="node.isEl"><component :is="node.icon" /></el-icon>
|
||||
<component v-else :is="node.icon" :color="getNodeColor(index)" />
|
||||
</div>
|
||||
<div class="process-node__name">{{ node.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="index < flowNodes.length - 1"
|
||||
class="process-bridge"
|
||||
:class="`process-bridge--${getLineState(index)}`"
|
||||
>
|
||||
<svg class="process-bridge__svg" viewBox="0 0 120 16" preserveAspectRatio="none">
|
||||
<line class="process-bridge__bg" x1="0" y1="8" x2="104" y2="8" />
|
||||
<line class="process-bridge__active" x1="0" y1="8" x2="104" y2="8" />
|
||||
<line
|
||||
v-if="getLineState(index) === 'flowing'"
|
||||
class="process-bridge__flow"
|
||||
x1="0"
|
||||
y1="8"
|
||||
x2="104"
|
||||
y2="8"
|
||||
/>
|
||||
<polygon class="process-bridge__head" points="106,4 116,8 106,12" />
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="process-list-section">
|
||||
<el-descriptions title="数据列表" />
|
||||
<div class="process-list-body">
|
||||
<el-table :data="processTableData" border stripe height="100%">
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="name" label="节点" min-width="100" align="center" />
|
||||
<el-table-column prop="link" label="上游链路" min-width="140" align="center" />
|
||||
<el-table-column prop="status" label="连接状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="`process-table-status process-table-status--${row.state}`">
|
||||
{{ row.status }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { h, watch, onUnmounted, ref, computed, defineComponent } from 'vue'
|
||||
import { Monitor, Coin } from '@element-plus/icons-vue'
|
||||
import { mainHeight } from '@/utils/layout'
|
||||
import DeviceTree from '@/components/tree/govern/deviceTree.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'process'
|
||||
})
|
||||
|
||||
const pageHeight = mainHeight(20)
|
||||
|
||||
/** 连接状态:null-初始,0-终端绿/终端↔前置,1-前置绿/前置↔MQTT,2-MQTT绿/MQTT↔数据库,3-数据库绿/数据库↔Web,4-全部完成 */
|
||||
const linkStatus = ref<number | null>(2)
|
||||
|
||||
const COLOR = {
|
||||
pending: '#94a3b8',
|
||||
success: '#009f05',
|
||||
error: '#e05252'
|
||||
}
|
||||
|
||||
const createProcessIcon = (render: (color: string) => ReturnType<typeof h>[]) =>
|
||||
defineComponent({
|
||||
props: {
|
||||
color: { type: String, default: COLOR.pending }
|
||||
},
|
||||
setup(props) {
|
||||
return () =>
|
||||
h(
|
||||
'svg',
|
||||
{
|
||||
viewBox: '0 0 48 48',
|
||||
fill: 'none',
|
||||
xmlns: 'http://www.w3.org/2000/svg',
|
||||
class: 'process-node__svg'
|
||||
},
|
||||
render(props.color)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
/** 终端:采集设备 */
|
||||
const IconTerminal = createProcessIcon(c => [
|
||||
h('rect', { x: 8, y: 10, width: 32, height: 22, rx: 3, stroke: c, 'stroke-width': 2.2 }),
|
||||
h('rect', { x: 14, y: 15, width: 20, height: 12, rx: 1.5, stroke: c, 'stroke-width': 1.5, opacity: 0.45 }),
|
||||
h('path', { d: 'M4 36h40', stroke: c, 'stroke-width': 2.2, 'stroke-linecap': 'round' }),
|
||||
h('path', { d: 'M22 32v4', stroke: c, 'stroke-width': 2.2, 'stroke-linecap': 'round' }),
|
||||
h('circle', { cx: 38, cy: 8, r: 3, fill: c, opacity: 0.85 })
|
||||
])
|
||||
|
||||
/** 前置:双机服务器 */
|
||||
const IconFrontEnd = createProcessIcon(c => [
|
||||
h('rect', { x: 10, y: 8, width: 28, height: 13, rx: 2.5, stroke: c, 'stroke-width': 2.2, fill: 'rgba(148,163,184,0.08)' }),
|
||||
h('circle', { cx: 16, cy: 14.5, r: 2, fill: c }),
|
||||
h('line', { x1: 22, y1: 14.5, x2: 34, y2: 14.5, stroke: c, 'stroke-width': 1.5, opacity: 0.5 }),
|
||||
h('rect', { x: 10, y: 27, width: 28, height: 13, rx: 2.5, stroke: c, 'stroke-width': 2.2, fill: 'rgba(148,163,184,0.08)' }),
|
||||
h('circle', { cx: 16, cy: 33.5, r: 2, fill: c }),
|
||||
h('line', { x1: 22, y1: 33.5, x2: 34, y2: 33.5, stroke: c, 'stroke-width': 1.5, opacity: 0.5 })
|
||||
])
|
||||
|
||||
/** MQTT:云 + 节点 */
|
||||
const IconMqtt = createProcessIcon(c => [
|
||||
h('circle', { cx: 24, cy: 26, r: 2.5, fill: c }),
|
||||
h('circle', { cx: 12, cy: 16, r: 2, fill: c, opacity: 0.75 }),
|
||||
h('circle', { cx: 36, cy: 16, r: 2, fill: c, opacity: 0.75 }),
|
||||
h('circle', { cx: 12, cy: 36, r: 2, fill: c, opacity: 0.75 }),
|
||||
h('circle', { cx: 36, cy: 36, r: 2, fill: c, opacity: 0.75 }),
|
||||
h('line', { x1: 24, y1: 26, x2: 12, y2: 16, stroke: c, 'stroke-width': 1.4, opacity: 0.55 }),
|
||||
h('line', { x1: 24, y1: 26, x2: 36, y2: 16, stroke: c, 'stroke-width': 1.4, opacity: 0.55 }),
|
||||
h('line', { x1: 24, y1: 26, x2: 12, y2: 36, stroke: c, 'stroke-width': 1.4, opacity: 0.55 }),
|
||||
h('line', { x1: 24, y1: 26, x2: 36, y2: 36, stroke: c, 'stroke-width': 1.4, opacity: 0.55 }),
|
||||
h('path', {
|
||||
d: 'M15 24c0-4.5 3.5-7.5 9-7.5s9 3 9 7.5c3.5 0 6 2.5 6 6s-2.5 6-6 6H15c-3.5 0-6-2.5-6-6s2.5-6 6-6z',
|
||||
stroke: c,
|
||||
'stroke-width': 2,
|
||||
fill: '#fff'
|
||||
}),
|
||||
h('text', { x: 24, y: 27.5, 'text-anchor': 'middle', fill: c, 'font-size': 7.5, 'font-weight': 'bold' }, 'MQTT')
|
||||
])
|
||||
|
||||
const flowNodes = [
|
||||
{ label: '终端', icon: IconTerminal },
|
||||
{ label: '前置', icon: IconFrontEnd },
|
||||
{ label: 'MQTT', icon: IconMqtt },
|
||||
{ label: '数据库', icon: Coin, isEl: true },
|
||||
{ label: 'Web', icon: Monitor, isEl: true }
|
||||
]
|
||||
|
||||
/** 全部完成状态值 */
|
||||
const COMPLETE_STATUS = 4
|
||||
/** 连线数量 */
|
||||
const LINE_COUNT = flowNodes.length - 1
|
||||
|
||||
const createErrorArray = <T,>(length: number, value: T) => Array.from({ length }, () => value)
|
||||
|
||||
type NodeState = 'pending' | 'success' | 'error'
|
||||
type LineState = 'idle' | 'flowing' | 'success' | 'error'
|
||||
|
||||
const lineErrors = ref(createErrorArray(LINE_COUNT, false))
|
||||
const nodeErrors = ref(createErrorArray(flowNodes.length, false))
|
||||
let timeoutTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const clearTimeoutTimer = () => {
|
||||
if (timeoutTimer) {
|
||||
clearTimeout(timeoutTimer)
|
||||
timeoutTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const resetErrorsFrom = (fromIndex: number) => {
|
||||
for (let i = fromIndex; i < lineErrors.value.length; i++) {
|
||||
lineErrors.value[i] = false
|
||||
}
|
||||
for (let i = fromIndex + 1; i < nodeErrors.value.length; i++) {
|
||||
nodeErrors.value[i] = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetAllErrors = () => {
|
||||
lineErrors.value = createErrorArray(LINE_COUNT, false)
|
||||
nodeErrors.value = createErrorArray(flowNodes.length, false)
|
||||
}
|
||||
|
||||
const startTimeout = (status: number | null) => {
|
||||
clearTimeoutTimer()
|
||||
if (status === null || status >= COMPLETE_STATUS) return
|
||||
|
||||
const capturedStatus = status
|
||||
timeoutTimer = setTimeout(() => {
|
||||
if (linkStatus.value !== capturedStatus) return
|
||||
lineErrors.value[capturedStatus] = true
|
||||
nodeErrors.value[capturedStatus + 1] = true
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
watch(
|
||||
linkStatus,
|
||||
(val, oldVal) => {
|
||||
const prev = oldVal ?? null
|
||||
if (val !== null && (prev === null || val > prev)) {
|
||||
resetErrorsFrom(val)
|
||||
} else if (val !== null && prev !== null && val < prev) {
|
||||
resetAllErrors()
|
||||
} else if (val === null) {
|
||||
resetAllErrors()
|
||||
}
|
||||
startTimeout(val)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onUnmounted(clearTimeoutTimer)
|
||||
|
||||
const getNodeState = (index: number): NodeState => {
|
||||
if (nodeErrors.value[index]) return 'error'
|
||||
if (linkStatus.value === null) return 'pending'
|
||||
if (linkStatus.value >= COMPLETE_STATUS) return 'success'
|
||||
if (index <= linkStatus.value) return 'success'
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
const getLineState = (index: number): LineState => {
|
||||
if (lineErrors.value[index]) return 'error'
|
||||
if (linkStatus.value === null) return 'idle'
|
||||
if (linkStatus.value >= COMPLETE_STATUS) return 'success'
|
||||
if (index < linkStatus.value) return 'success'
|
||||
if (index === linkStatus.value) return 'flowing'
|
||||
return 'idle'
|
||||
}
|
||||
|
||||
const getNodeColor = (index: number) => COLOR[getNodeState(index)]
|
||||
|
||||
const getNodeStatusText = (index: number) => {
|
||||
const state = getNodeState(index)
|
||||
if (state === 'error') return '连接失败'
|
||||
if (state === 'success') return '连接成功'
|
||||
if (linkStatus.value !== null && linkStatus.value < COMPLETE_STATUS && index === linkStatus.value + 1) {
|
||||
return '连接中'
|
||||
}
|
||||
return '待连接'
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const processTableData = computed(() =>
|
||||
flowNodes.map((node, index) => ({
|
||||
name: node.label,
|
||||
link: index === 0 ? '-' : `${flowNodes[index - 1].label} → ${node.label}`,
|
||||
status: getNodeStatusText(index),
|
||||
state: getNodeState(index)
|
||||
}))
|
||||
)
|
||||
|
||||
const nodeClick = async (e: anyObj) => {
|
||||
console.log('点击设备树节点')
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
$green: #009f05;
|
||||
$green-light: #e8f7e9;
|
||||
$red: #e05252;
|
||||
$red-light: #fdeeee;
|
||||
$gray: #cbd5e1;
|
||||
$gray-text: #94a3b8;
|
||||
$node-min: 110px;
|
||||
$node-max: 120px;
|
||||
$bridge-min: 60px;
|
||||
|
||||
.manage-process {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 10px 10px 10px 0;
|
||||
|
||||
:deep(.el-descriptions__header) {
|
||||
height: 36px;
|
||||
margin-bottom: 7px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.process-flow-section {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.process-list-section {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.process-list-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.process-table-status {
|
||||
&--success {
|
||||
color: $green;
|
||||
}
|
||||
|
||||
&--error {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
&--pending {
|
||||
color: $gray-text;
|
||||
}
|
||||
}
|
||||
|
||||
.process-flow-wrap {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 8px;
|
||||
padding: 0 10%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.process-flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: min(100%, calc(#{$node-min} * 5 + #{$bridge-min} * 4));
|
||||
min-height: $node-min;
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.process-step {
|
||||
flex: 1 1 $node-min;
|
||||
min-width: $node-min;
|
||||
max-width: $node-max;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.process-node {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: $node-min;
|
||||
min-height: $node-min;
|
||||
max-width: $node-max;
|
||||
aspect-ratio: 1;
|
||||
height: auto;
|
||||
padding: clamp(8px, 8%, 12px);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
border-radius: 16px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&__index {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #cbd5e1;
|
||||
font-variant-numeric: tabular-nums;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: clamp(52px, 48%, 60px);
|
||||
height: clamp(52px, 48%, 60px);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
.el-icon {
|
||||
font-size: clamp(34px, 32%, 40px);
|
||||
}
|
||||
|
||||
.process-node__svg {
|
||||
width: clamp(34px, 32%, 40px);
|
||||
height: clamp(34px, 32%, 40px);
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin-top: 8px;
|
||||
font-size: clamp(13px, 12%, 15px);
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
letter-spacing: 1px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&__status {
|
||||
margin-top: 4px;
|
||||
font-size: clamp(12px, 11%, 13px);
|
||||
color: $gray-text;
|
||||
transition: color 0.3s;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: linear-gradient(160deg, #fff 0%, $green-light 100%);
|
||||
border-color: rgba(0, 159, 5, 0.25);
|
||||
box-shadow: 0 4px 16px rgba(0, 159, 5, 0.1);
|
||||
|
||||
.process-node__index {
|
||||
color: rgba(0, 159, 5, 0.45);
|
||||
}
|
||||
|
||||
.process-node__icon {
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 2px rgba(0, 159, 5, 0.15);
|
||||
}
|
||||
|
||||
.process-node__status {
|
||||
color: $green;
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
background: linear-gradient(160deg, #fff 0%, $red-light 100%);
|
||||
border-color: rgba(224, 82, 82, 0.3);
|
||||
box-shadow: 0 4px 16px rgba(224, 82, 82, 0.1);
|
||||
|
||||
.process-node__index {
|
||||
color: rgba(224, 82, 82, 0.45);
|
||||
}
|
||||
|
||||
.process-node__icon {
|
||||
box-shadow: 0 0 0 2px rgba(224, 82, 82, 0.15);
|
||||
}
|
||||
|
||||
.process-node__status {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.process-bridge {
|
||||
flex: 1 1 0;
|
||||
min-width: $bridge-min;
|
||||
height: 25px;
|
||||
padding: 0 2px;
|
||||
align-self: center;
|
||||
|
||||
&__svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__bg {
|
||||
stroke: #e8edf3;
|
||||
stroke-width: 5;
|
||||
stroke-linecap: round;
|
||||
transition: stroke 0.4s;
|
||||
}
|
||||
|
||||
&__active {
|
||||
stroke: transparent;
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
transition: stroke 0.4s;
|
||||
}
|
||||
|
||||
&__flow {
|
||||
stroke: $green;
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 5 9;
|
||||
animation: bridge-dash 0.85s linear infinite;
|
||||
}
|
||||
|
||||
&__head {
|
||||
fill: #cbd5e1;
|
||||
transition: fill 0.4s;
|
||||
}
|
||||
|
||||
&--success {
|
||||
.process-bridge__active {
|
||||
stroke: $green;
|
||||
filter: drop-shadow(0 0 3px rgba(0, 159, 5, 0.35));
|
||||
}
|
||||
|
||||
.process-bridge__head {
|
||||
fill: $green;
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
.process-bridge__active {
|
||||
stroke: $red;
|
||||
filter: drop-shadow(0 0 3px rgba(224, 82, 82, 0.35));
|
||||
}
|
||||
|
||||
.process-bridge__head {
|
||||
fill: $red;
|
||||
}
|
||||
}
|
||||
|
||||
&--flowing {
|
||||
.process-bridge__active {
|
||||
stroke: #dce3ec;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.process-bridge__head {
|
||||
fill: $green;
|
||||
animation: head-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&--idle {
|
||||
.process-bridge__active {
|
||||
stroke: transparent;
|
||||
}
|
||||
|
||||
.process-bridge__head {
|
||||
fill: #cbd5e1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bridge-dash {
|
||||
to {
|
||||
stroke-dashoffset: -14;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes head-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.45;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user