refactor: 重构设备页面样式和功能、对接设备实时数据和设备在线状态、产品、设备管理页面适配白天/黑暗模式切换
- 更新设备详情页面布局和样式 - 优化实时数据展示和图表功能 - 统一模拟界面样式 - 调整产品详情组件样式 - 优化主题切换支持
This commit is contained in:
parent
f0a666fb1d
commit
a9a4c31c1f
|
@ -0,0 +1,431 @@
|
|||
<style data-app-loading="inject-css">
|
||||
@keyframes bounce05 {
|
||||
85%,
|
||||
92%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
89% {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
95% {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide05 {
|
||||
5% {
|
||||
transform: translateX(14px);
|
||||
}
|
||||
|
||||
15%,
|
||||
30% {
|
||||
transform: translateX(6px);
|
||||
}
|
||||
|
||||
40%,
|
||||
55% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
65%,
|
||||
70% {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
80%,
|
||||
89% {
|
||||
transform: translateX(-12px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(14px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes paper05 {
|
||||
5% {
|
||||
transform: translateY(46px);
|
||||
}
|
||||
|
||||
20%,
|
||||
30% {
|
||||
transform: translateY(34px);
|
||||
}
|
||||
|
||||
40%,
|
||||
55% {
|
||||
transform: translateY(22px);
|
||||
}
|
||||
|
||||
65%,
|
||||
70% {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
80%,
|
||||
85% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
92%,
|
||||
100% {
|
||||
transform: translateY(46px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes keyboard05 {
|
||||
5%,
|
||||
12%,
|
||||
21%,
|
||||
30%,
|
||||
39%,
|
||||
48%,
|
||||
57%,
|
||||
66%,
|
||||
75%,
|
||||
84% {
|
||||
box-shadow:
|
||||
15px 0 0 var(--key),
|
||||
30px 0 0 var(--key),
|
||||
45px 0 0 var(--key),
|
||||
60px 0 0 var(--key),
|
||||
75px 0 0 var(--key),
|
||||
90px 0 0 var(--key),
|
||||
22px 10px 0 var(--key),
|
||||
37px 10px 0 var(--key),
|
||||
52px 10px 0 var(--key),
|
||||
60px 10px 0 var(--key),
|
||||
68px 10px 0 var(--key),
|
||||
83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
9% {
|
||||
box-shadow:
|
||||
15px 2px 0 var(--key),
|
||||
30px 0 0 var(--key),
|
||||
45px 0 0 var(--key),
|
||||
60px 0 0 var(--key),
|
||||
75px 0 0 var(--key),
|
||||
90px 0 0 var(--key),
|
||||
22px 10px 0 var(--key),
|
||||
37px 10px 0 var(--key),
|
||||
52px 10px 0 var(--key),
|
||||
60px 10px 0 var(--key),
|
||||
68px 10px 0 var(--key),
|
||||
83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
18% {
|
||||
box-shadow:
|
||||
15px 0 0 var(--key),
|
||||
30px 0 0 var(--key),
|
||||
45px 0 0 var(--key),
|
||||
60px 2px 0 var(--key),
|
||||
75px 0 0 var(--key),
|
||||
90px 0 0 var(--key),
|
||||
22px 10px 0 var(--key),
|
||||
37px 10px 0 var(--key),
|
||||
52px 10px 0 var(--key),
|
||||
60px 10px 0 var(--key),
|
||||
68px 10px 0 var(--key),
|
||||
83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
27% {
|
||||
box-shadow:
|
||||
15px 0 0 var(--key),
|
||||
30px 0 0 var(--key),
|
||||
45px 0 0 var(--key),
|
||||
60px 0 0 var(--key),
|
||||
75px 0 0 var(--key),
|
||||
90px 0 0 var(--key),
|
||||
22px 12px 0 var(--key),
|
||||
37px 10px 0 var(--key),
|
||||
52px 10px 0 var(--key),
|
||||
60px 10px 0 var(--key),
|
||||
68px 10px 0 var(--key),
|
||||
83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
36% {
|
||||
box-shadow:
|
||||
15px 0 0 var(--key),
|
||||
30px 0 0 var(--key),
|
||||
45px 0 0 var(--key),
|
||||
60px 0 0 var(--key),
|
||||
75px 0 0 var(--key),
|
||||
90px 0 0 var(--key),
|
||||
22px 10px 0 var(--key),
|
||||
37px 10px 0 var(--key),
|
||||
52px 12px 0 var(--key),
|
||||
60px 12px 0 var(--key),
|
||||
68px 12px 0 var(--key),
|
||||
83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
45% {
|
||||
box-shadow:
|
||||
15px 0 0 var(--key),
|
||||
30px 0 0 var(--key),
|
||||
45px 0 0 var(--key),
|
||||
60px 0 0 var(--key),
|
||||
75px 0 0 var(--key),
|
||||
90px 2px 0 var(--key),
|
||||
22px 10px 0 var(--key),
|
||||
37px 10px 0 var(--key),
|
||||
52px 10px 0 var(--key),
|
||||
60px 10px 0 var(--key),
|
||||
68px 10px 0 var(--key),
|
||||
83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
54% {
|
||||
box-shadow:
|
||||
15px 0 0 var(--key),
|
||||
30px 2px 0 var(--key),
|
||||
45px 0 0 var(--key),
|
||||
60px 0 0 var(--key),
|
||||
75px 0 0 var(--key),
|
||||
90px 0 0 var(--key),
|
||||
22px 10px 0 var(--key),
|
||||
37px 10px 0 var(--key),
|
||||
52px 10px 0 var(--key),
|
||||
60px 10px 0 var(--key),
|
||||
68px 10px 0 var(--key),
|
||||
83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
63% {
|
||||
box-shadow:
|
||||
15px 0 0 var(--key),
|
||||
30px 0 0 var(--key),
|
||||
45px 0 0 var(--key),
|
||||
60px 0 0 var(--key),
|
||||
75px 0 0 var(--key),
|
||||
90px 0 0 var(--key),
|
||||
22px 10px 0 var(--key),
|
||||
37px 10px 0 var(--key),
|
||||
52px 10px 0 var(--key),
|
||||
60px 10px 0 var(--key),
|
||||
68px 10px 0 var(--key),
|
||||
83px 12px 0 var(--key);
|
||||
}
|
||||
|
||||
72% {
|
||||
box-shadow:
|
||||
15px 0 0 var(--key),
|
||||
30px 0 0 var(--key),
|
||||
45px 2px 0 var(--key),
|
||||
60px 0 0 var(--key),
|
||||
75px 0 0 var(--key),
|
||||
90px 0 0 var(--key),
|
||||
22px 10px 0 var(--key),
|
||||
37px 10px 0 var(--key),
|
||||
52px 10px 0 var(--key),
|
||||
60px 10px 0 var(--key),
|
||||
68px 10px 0 var(--key),
|
||||
83px 10px 0 var(--key);
|
||||
}
|
||||
|
||||
81% {
|
||||
box-shadow:
|
||||
15px 0 0 var(--key),
|
||||
30px 0 0 var(--key),
|
||||
45px 0 0 var(--key),
|
||||
60px 0 0 var(--key),
|
||||
75px 0 0 var(--key),
|
||||
90px 0 0 var(--key),
|
||||
22px 10px 0 var(--key),
|
||||
37px 12px 0 var(--key),
|
||||
52px 10px 0 var(--key),
|
||||
60px 10px 0 var(--key),
|
||||
68px 10px 0 var(--key),
|
||||
83px 10px 0 var(--key);
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
/* stylelint-disable selector-id-pattern */ /* 禁用规则 */
|
||||
#__app-loading__ {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#__app-loading__.hidden {
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: all 1s ease-out;
|
||||
}
|
||||
|
||||
#__app-loading__ .title {
|
||||
margin-top: 20px;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
color: rgb(0 0 0 / 85%);
|
||||
}
|
||||
|
||||
.dark #__app-loading__ {
|
||||
background: #0d0d10;
|
||||
}
|
||||
|
||||
.dark #__app-loading__ .title {
|
||||
color: #fff;
|
||||
}
|
||||
/* stylelint-enable selector-id-pattern */ /* 重新启用规则 */
|
||||
|
||||
/* ... */
|
||||
.typewriter {
|
||||
--blue: #5c86ff;
|
||||
--blue-dark: #275efe;
|
||||
--key: #fff;
|
||||
--paper: #eef0fd;
|
||||
--text: #d3d4ec;
|
||||
--tool: #fbc56c;
|
||||
--duration: 3s;
|
||||
|
||||
position: relative;
|
||||
animation: bounce05 var(--duration) linear infinite;
|
||||
}
|
||||
|
||||
.typewriter .slide {
|
||||
width: 92px;
|
||||
height: 20px;
|
||||
margin-left: 14px;
|
||||
background: linear-gradient(var(--blue), var(--blue-dark));
|
||||
border-radius: 3px;
|
||||
transform: translateX(14px);
|
||||
animation: slide05 var(--duration) ease infinite;
|
||||
}
|
||||
|
||||
.typewriter .slide::before,
|
||||
.typewriter .slide::after,
|
||||
.typewriter .slide i::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
background: var(--tool);
|
||||
}
|
||||
|
||||
.typewriter .slide::before {
|
||||
top: 6px;
|
||||
left: 100%;
|
||||
width: 2px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.typewriter .slide::after {
|
||||
top: 3px;
|
||||
left: 94px;
|
||||
width: 6px;
|
||||
height: 14px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.typewriter .slide i {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 100%;
|
||||
display: block;
|
||||
width: 6px;
|
||||
height: 4px;
|
||||
background: var(--tool);
|
||||
}
|
||||
|
||||
.typewriter .slide i::before {
|
||||
top: -2px;
|
||||
right: 100%;
|
||||
width: 4px;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.typewriter .paper {
|
||||
position: absolute;
|
||||
top: -26px;
|
||||
left: 24px;
|
||||
width: 40px;
|
||||
height: 46px;
|
||||
background: var(--paper);
|
||||
border-radius: 5px;
|
||||
transform: translateY(46px);
|
||||
animation: paper05 var(--duration) linear infinite;
|
||||
}
|
||||
|
||||
.typewriter .paper::before {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 6px;
|
||||
left: 6px;
|
||||
height: 4px;
|
||||
content: '';
|
||||
background: var(--text);
|
||||
border-radius: 2px;
|
||||
box-shadow:
|
||||
0 12px 0 var(--text),
|
||||
0 24px 0 var(--text),
|
||||
0 36px 0 var(--text);
|
||||
transform: scaleY(0.8);
|
||||
}
|
||||
|
||||
.typewriter .keyboard {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 120px;
|
||||
height: 56px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.typewriter .keyboard::before,
|
||||
.typewriter .keyboard::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.typewriter .keyboard::before {
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, var(--blue), var(--blue-dark));
|
||||
border-radius: 7px;
|
||||
transform: perspective(10px) rotateX(2deg);
|
||||
transform-origin: 50% 100%;
|
||||
}
|
||||
|
||||
.typewriter .keyboard::after {
|
||||
top: 25px;
|
||||
left: 2px;
|
||||
width: 11px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
box-shadow:
|
||||
15px 0 0 var(--key),
|
||||
30px 0 0 var(--key),
|
||||
45px 0 0 var(--key),
|
||||
60px 0 0 var(--key),
|
||||
75px 0 0 var(--key),
|
||||
90px 0 0 var(--key),
|
||||
22px 10px 0 var(--key),
|
||||
37px 10px 0 var(--key),
|
||||
52px 10px 0 var(--key),
|
||||
60px 10px 0 var(--key),
|
||||
68px 10px 0 var(--key),
|
||||
83px 10px 0 var(--key);
|
||||
animation: keyboard05 var(--duration) linear infinite;
|
||||
}
|
||||
</style>
|
||||
<div id="__app-loading__">
|
||||
<!-- ... -->
|
||||
<div class="typewriter">
|
||||
<div class="slide"><i></i></div>
|
||||
<div class="paper"></div>
|
||||
<div class="keyboard"></div>
|
||||
</div>
|
||||
<div class="title"><%= VITE_APP_TITLE %></div>
|
||||
</div>
|
|
@ -52,7 +52,6 @@
|
|||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"pinia": "catalog:",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"rxjs": "^7.8.2",
|
||||
"tinymce": "^7.3.0",
|
||||
"unplugin-vue-components": "^0.27.3",
|
||||
|
|
|
@ -41,3 +41,14 @@ const tokenTheme = computed(() => {
|
|||
</App>
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.ant-drawer .ant-drawer-content {
|
||||
background: hsl(var(--background)) !important;
|
||||
}
|
||||
|
||||
.ant-modal .ant-modal-header,
|
||||
.ant-modal .ant-modal-content {
|
||||
background: hsl(var(--background)) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -93,5 +93,7 @@ defineExpose({
|
|||
<style scoped lang="scss">
|
||||
.editor {
|
||||
height: 100%;
|
||||
border: 1px solid hsl(var(--border)) !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -28,6 +28,10 @@ export const useDeviceStore = defineStore('device', {
|
|||
this.deviceCount = count;
|
||||
},
|
||||
|
||||
updateDeviceStatus(state: string) {
|
||||
this.currentDevice.deviceState = state;
|
||||
},
|
||||
|
||||
async getDetail(id: string) {
|
||||
try {
|
||||
const res = await deviceInfo(id);
|
||||
|
|
|
@ -6,10 +6,16 @@ import { computed, ref } from 'vue';
|
|||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { EditOutlined, EyeOutlined } from '@ant-design/icons-vue';
|
||||
import { Button, Descriptions, DescriptionsItem, Drawer, Space } from 'ant-design-vue';
|
||||
import {
|
||||
Button,
|
||||
Descriptions,
|
||||
DescriptionsItem,
|
||||
Drawer,
|
||||
Space,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { deviceTypeOptions, networkTypeOptions } from '#/constants/dicts';
|
||||
import TSLViewer from '#/views/device/product/detail/components/TSLViewer.vue';
|
||||
import TSLViewer from '#/views/device/product/detail/components/metadata/TSLViewer.vue';
|
||||
|
||||
import deviceDrawer from '../../device-drawer.vue';
|
||||
|
||||
|
@ -53,7 +59,12 @@ const metadataObj = computed(() => {
|
|||
<div class="info-header">
|
||||
<h3>设备信息</h3>
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit" v-access:code="['device:device:edit']">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleEdit"
|
||||
v-access:code="['device:device:edit']"
|
||||
>
|
||||
<template #icon>
|
||||
<EditOutlined />
|
||||
</template>
|
||||
|
@ -79,7 +90,11 @@ const metadataObj = computed(() => {
|
|||
{{ deviceInfo.deviceName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="设备类型">
|
||||
{{ deviceTypeOptions.find((option) => option.value === deviceInfo.deviceType)?.label }}
|
||||
{{
|
||||
deviceTypeOptions.find(
|
||||
(option) => option.value === deviceInfo.deviceType,
|
||||
)?.label
|
||||
}}
|
||||
</DescriptionsItem>
|
||||
<!-- <DescriptionsItem label="产品名称">
|
||||
{{ deviceInfo.productName }}
|
||||
|
@ -88,7 +103,11 @@ const metadataObj = computed(() => {
|
|||
{{ deviceInfo.otaVersion || '-' }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="接入方式">
|
||||
{{ networkTypeOptions.find((option) => option.value === deviceInfo?.productObj?.provider)?.label || '-' }}
|
||||
{{
|
||||
networkTypeOptions.find(
|
||||
(option) => option.value === deviceInfo?.productObj?.provider,
|
||||
)?.label || '-'
|
||||
}}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="消息协议">
|
||||
{{ deviceInfo?.productObj?.protocolName || '-' }}
|
||||
|
@ -112,12 +131,16 @@ const metadataObj = computed(() => {
|
|||
</div>
|
||||
|
||||
<!-- 产品参数展示 -->
|
||||
<div class="product-params-section">
|
||||
<div class="product-params-section" v-if="deviceInfo?.productParamArray">
|
||||
<div class="section-header">
|
||||
<h3>设备参数</h3>
|
||||
</div>
|
||||
<Descriptions bordered :column="3">
|
||||
<DescriptionsItem v-for="param in deviceInfo.productParamArray" :key="param.key" :label="param.label">
|
||||
<DescriptionsItem
|
||||
v-for="param in deviceInfo.productParamArray"
|
||||
:key="param.key"
|
||||
:label="param.label"
|
||||
>
|
||||
{{ param.value }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
|
@ -126,8 +149,14 @@ const metadataObj = computed(() => {
|
|||
<DeviceDrawer @reload="emit('refresh')" />
|
||||
|
||||
<!-- TSL查看抽屉 -->
|
||||
<Drawer v-model:open="tslVisible" title="物模型" width="800px" @close="handleTSLClose">
|
||||
<Drawer
|
||||
v-model:open="tslVisible"
|
||||
title="物模型"
|
||||
width="800px"
|
||||
@close="handleTSLClose"
|
||||
>
|
||||
<TSLViewer
|
||||
v-if="tslVisible"
|
||||
:product-id="deviceInfo?.productId"
|
||||
:product-info="deviceInfo?.productObj"
|
||||
:file-name="deviceInfo?.deviceName"
|
||||
|
@ -149,12 +178,10 @@ const metadataObj = computed(() => {
|
|||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.info-content {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
|
@ -173,13 +200,11 @@ const metadataObj = computed(() => {
|
|||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-params {
|
||||
padding: 20px 0;
|
||||
color: #8c8c8c;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
|
@ -187,36 +212,6 @@ const metadataObj = computed(() => {
|
|||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.params-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
|
||||
.param-card {
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
|
||||
.param-key {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.param-value {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -249,7 +249,6 @@ onMounted(() => {
|
|||
overflow: auto;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
background: #fafafa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
<script lang="ts" setup>
|
||||
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
|
||||
import {
|
||||
DataZoomInsideComponent,
|
||||
DataZoomSliderComponent,
|
||||
} from 'echarts/components';
|
||||
import { use } from 'echarts/core';
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
data: () => [],
|
||||
});
|
||||
|
||||
use([DataZoomSliderComponent, DataZoomInsideComponent]);
|
||||
|
||||
interface Props {
|
||||
data?: Array<{ property: string; timestamp: number; value: number }>;
|
||||
}
|
||||
|
||||
const chartRef = ref<EchartsUIType>();
|
||||
const { renderEcharts } = useEcharts(chartRef);
|
||||
|
||||
const renderChart = () => {
|
||||
if (props.data.length === 0) return;
|
||||
|
||||
// 按时间升序排序
|
||||
const sortedData = [...props.data].sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
// ECharts时间序列数据格式:[时间, 数值]
|
||||
const seriesData = sortedData.map((item) => [
|
||||
item.timestamp,
|
||||
Number.parseFloat(item.value),
|
||||
]);
|
||||
|
||||
renderEcharts({
|
||||
tooltip: {
|
||||
// 提示框组件
|
||||
trigger: 'axis',
|
||||
formatter(params) {
|
||||
const item = params[0];
|
||||
const date = new Date(item.value[0]);
|
||||
const timeStr = `${date.getFullYear()}-${(date.getMonth() + 1)
|
||||
.toString()
|
||||
.padStart(
|
||||
2,
|
||||
'0',
|
||||
)}-${date.getDate().toString().padStart(2, '0')} ${date
|
||||
.getHours()
|
||||
.toString()
|
||||
.padStart(
|
||||
2,
|
||||
'0',
|
||||
)}:${date.getMinutes().toString().padStart(2, '0')}:${date
|
||||
.getSeconds()
|
||||
.toString()
|
||||
.padStart(2, '0')}`;
|
||||
return `时间: ${timeStr}<br/>值: ${item.value[1]}`;
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 90,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
name: '时间',
|
||||
axisLabel: {
|
||||
rotate: 30,
|
||||
formatter(value: number) {
|
||||
const date = new Date(value);
|
||||
return `${date.getFullYear()}-${(date.getMonth() + 1)
|
||||
.toString()
|
||||
.padStart(
|
||||
2,
|
||||
'0',
|
||||
)}-${date.getDate().toString().padStart(2, '0')}\n${date
|
||||
.getHours()
|
||||
.toString()
|
||||
.padStart(
|
||||
2,
|
||||
'0',
|
||||
)}:${date.getMinutes().toString().padStart(2, '0')}:${date
|
||||
.getSeconds()
|
||||
.toString()
|
||||
.padStart(2, '0')}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '值',
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
type: 'slider', // 滑动条型
|
||||
show: true, // 显示组件
|
||||
xAxisIndex: [0], // 控制第一个x轴
|
||||
top: '90%', // 放置在底部
|
||||
start: 0, // 初始起始范围比例为0%
|
||||
end: 100, // 初始结束范围比例为100%
|
||||
left: '17%',
|
||||
width: '65%',
|
||||
},
|
||||
{
|
||||
type: 'inside', // 内置型
|
||||
xAxisIndex: [0], // 控制第一个x轴
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '数据系列',
|
||||
type: 'line',
|
||||
data: seriesData,
|
||||
// 可选:配置线条样式
|
||||
lineStyle: {
|
||||
color: '#5470C6',
|
||||
},
|
||||
// 可选:配置区域样式
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(84, 112, 198, 0.5)', // 起始颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(84, 112, 198, 0.1)', // 结束颜色
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// 可选:配置符号大小
|
||||
symbolSize: 6,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
watch(() => props.data, renderChart, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
renderChart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EchartsUI ref="chartRef" height="400px" />
|
||||
</template>
|
|
@ -3,10 +3,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
|||
|
||||
import { EllipsisText } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
LineChartOutlined,
|
||||
UnorderedListOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { UnorderedListOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
|
@ -27,6 +24,8 @@ import dayjs from 'dayjs';
|
|||
|
||||
import { getWebSocket } from '#/utils/websocket';
|
||||
|
||||
import RealtimeChart from './RealtimeChart.vue';
|
||||
|
||||
interface Props {
|
||||
deviceId: string;
|
||||
metadata: any;
|
||||
|
@ -59,15 +58,16 @@ const subscribeRealtimeData = () => {
|
|||
properties: props.metadata.properties.map((p: any) => p.id),
|
||||
interval: '3s',
|
||||
}).subscribe((data: any) => {
|
||||
if (data.payload?.deviceKey === deviceKey && data.payload?.properties) {
|
||||
console.log('收到设备实时数据', data);
|
||||
if (data.payload?.value && data.requestId === id) {
|
||||
// 更新实时属性数据
|
||||
data.payload.properties.forEach((propData: any) => {
|
||||
Object.entries(data.payload.value).forEach(([propId, propValue]) => {
|
||||
const property = runtimeProperties.value.find(
|
||||
(p: any) => p.id === propData.id,
|
||||
(p: any) => p.id === propId,
|
||||
);
|
||||
if (property) {
|
||||
property.value = propData.value;
|
||||
property.timestamp = dayjs(propData.timestamp).format(
|
||||
property.value = propValue;
|
||||
property.timestamp = dayjs(data.payload.timeString).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
);
|
||||
}
|
||||
|
@ -135,8 +135,14 @@ const logData = ref<any[]>([]);
|
|||
|
||||
const logColumns = [
|
||||
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 200 },
|
||||
{ title: '值', dataIndex: 'value', key: 'value', width: 150 },
|
||||
{ title: '操作', key: 'action', width: 100 },
|
||||
{
|
||||
title: '值',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
},
|
||||
// { title: '操作', key: 'action', width: 100 },
|
||||
];
|
||||
|
||||
const openPropertyLog = (property: any) => {
|
||||
|
@ -154,6 +160,24 @@ const loadPropertyLog = async () => {
|
|||
{ timestamp: '2025-08-19 16:19:40', value: '0.5' },
|
||||
{ timestamp: '2025-08-19 16:18:40', value: '0.4' },
|
||||
{ timestamp: '2025-08-19 16:17:40', value: '0.3' },
|
||||
{ timestamp: '2025-08-20 16:19:40', value: '0.5' },
|
||||
{ timestamp: '2025-08-20 16:18:40', value: '0.4' },
|
||||
{ timestamp: '2025-08-20 16:17:40', value: '0.3' },
|
||||
{ timestamp: '2025-08-21 14:19:40', value: '0.5' },
|
||||
{ timestamp: '2025-08-21 16:18:40', value: '0.4' },
|
||||
{ timestamp: '2025-08-21 18:17:40', value: '0.3' },
|
||||
{ timestamp: '2025-08-22 16:19:40', value: '0.5' },
|
||||
{ timestamp: '2025-08-22 16:18:40', value: '0.4' },
|
||||
{ timestamp: '2025-08-22 16:17:40', value: '0.3' },
|
||||
{ timestamp: '2025-08-23 16:19:40', value: '0.5' },
|
||||
{ timestamp: '2025-08-23 16:18:40', value: '0.4' },
|
||||
{ timestamp: '2025-08-23 16:17:40', value: '0.3' },
|
||||
{ timestamp: '2025-08-24 16:19:40', value: '0.5' },
|
||||
{ timestamp: '2025-08-24 16:18:40', value: '0.4' },
|
||||
{ timestamp: '2025-08-24 16:17:40', value: '0.3' },
|
||||
{ timestamp: '2025-08-25 16:19:40', value: '0.5' },
|
||||
{ timestamp: '2025-08-25 16:18:40', value: '0.4' },
|
||||
{ timestamp: '2025-08-25 16:17:40', value: '0.3' },
|
||||
];
|
||||
} finally {
|
||||
logLoading.value = false;
|
||||
|
@ -203,7 +227,7 @@ const formatValue = (property: any) => {
|
|||
return valueParams.enumConf.find((item: any) => item.value === value)?.text;
|
||||
}
|
||||
|
||||
if (valueParams?.formType === 'time') {
|
||||
if (valueParams?.formType === 'time' && valueParams?.dataType !== 'string') {
|
||||
return dayjs(value).format(valueParams.format);
|
||||
}
|
||||
|
||||
|
@ -230,74 +254,12 @@ const closeLogModal = () => {
|
|||
logData.value = [];
|
||||
};
|
||||
|
||||
let refreshTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
const generateRandomString = (length: number) => {
|
||||
const chars =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const startRefreshTimer = () => {
|
||||
console.log('开始生成随机数据');
|
||||
refreshTimer = setInterval(() => {
|
||||
runtimeProperties.value.forEach((item: any) => {
|
||||
switch (item.valueParams?.dataType) {
|
||||
case 'boolean': {
|
||||
item.value = Math.random() > 0.5 ? 'true' : 'false';
|
||||
break;
|
||||
}
|
||||
case 'date': {
|
||||
item.value = Date.now();
|
||||
break;
|
||||
}
|
||||
case 'double':
|
||||
case 'float':
|
||||
case 'int':
|
||||
case 'long': {
|
||||
item.value = Math.random() * 100;
|
||||
break;
|
||||
}
|
||||
case 'enum': {
|
||||
item.value =
|
||||
item.valueParams.enumConf[
|
||||
Math.floor(Math.random() * item.valueParams.enumConf.length)
|
||||
].value;
|
||||
break;
|
||||
}
|
||||
case 'string': {
|
||||
item.value = generateRandomString(
|
||||
Math.floor(Math.random() * 1_000_000),
|
||||
);
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
item.timestamp = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
});
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const stopRefreshTimer = () => {
|
||||
console.log('停止生成随机数据');
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initRuntime();
|
||||
startRefreshTimer();
|
||||
subscribeRealtimeData();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopRefreshTimer();
|
||||
unsubscribeRealtimeData();
|
||||
});
|
||||
|
||||
|
@ -417,6 +379,7 @@ watch(
|
|||
:pagination="false"
|
||||
size="small"
|
||||
row-key="timestamp"
|
||||
:scroll="{ y: 500 }"
|
||||
>
|
||||
<template #bodyCell="{ column }">
|
||||
<template v-if="column.key === 'action'">
|
||||
|
@ -433,8 +396,22 @@ watch(
|
|||
>
|
||||
<div class="chart-container">
|
||||
<div class="chart-placeholder">
|
||||
<LineChartOutlined style="font-size: 48px; color: #d9d9d9" />
|
||||
<p>图表功能开发中...</p>
|
||||
<RealtimeChart
|
||||
v-if="logData && logData.length > 0"
|
||||
:data="logData"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 300px;
|
||||
color: #999;
|
||||
"
|
||||
>
|
||||
<Empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
|
@ -459,7 +436,6 @@ watch(
|
|||
|
||||
.control-label {
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
@ -483,15 +459,14 @@ watch(
|
|||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.property-log-btn {
|
||||
padding: 4px;
|
||||
color: #8c8c8c;
|
||||
color: hsl(var(--muted-foreground));
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -506,7 +481,7 @@ watch(
|
|||
margin-bottom: 12px;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #1890ff;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.property-unit {
|
||||
|
@ -514,7 +489,7 @@ watch(
|
|||
right: 0;
|
||||
bottom: 0;
|
||||
font-size: 20px;
|
||||
color: #8c8c8c;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -578,32 +578,30 @@ onMounted(() => {
|
|||
|
||||
.event-sidebar {
|
||||
width: 200px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #d9d9d9;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
|
||||
.sidebar-header {
|
||||
padding: 10px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
background: #f0f0f0;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
background: hsl(var(--muted));
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #e6f7ff;
|
||||
background: hsl(var(--muted));
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: white;
|
||||
background: #1890ff;
|
||||
background: hsl(var(--primary));
|
||||
|
||||
.event-id {
|
||||
color: rgb(255 255 255 / 80%);
|
||||
|
@ -634,7 +632,7 @@ onMounted(() => {
|
|||
align-items: center;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -645,14 +643,14 @@ onMounted(() => {
|
|||
.input-section {
|
||||
flex: 5;
|
||||
min-height: 300px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
|
||||
.simple-form {
|
||||
:deep(.ant-table) {
|
||||
.ant-table-thead > tr > th {
|
||||
font-weight: 500;
|
||||
background: #fafafa;
|
||||
background: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
|
@ -687,28 +685,26 @@ onMounted(() => {
|
|||
.parameter-box,
|
||||
.result-box {
|
||||
padding: 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
|
||||
.parameter-header,
|
||||
.result-header {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.parameter-content,
|
||||
.result-content {
|
||||
min-height: calc(100% - 32px);
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 4px;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #262626;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
|
|
@ -839,32 +839,30 @@ onMounted(() => {
|
|||
|
||||
.function-sidebar {
|
||||
width: 200px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #d9d9d9;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
|
||||
.sidebar-header {
|
||||
padding: 10px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
background: #f0f0f0;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
background: hsl(var(--muted));
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #e6f7ff;
|
||||
background: hsl(var(--muted));
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: white;
|
||||
background: #1890ff;
|
||||
background: hsl(var(--primary));
|
||||
|
||||
.function-id {
|
||||
color: rgb(255 255 255 / 80%);
|
||||
|
@ -895,11 +893,11 @@ onMounted(() => {
|
|||
align-items: center;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
color: hsl(var(--muted-foreground));
|
||||
|
||||
.info-icon {
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -911,14 +909,14 @@ onMounted(() => {
|
|||
.input-section {
|
||||
flex: 5;
|
||||
min-height: 300px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
|
||||
.simple-form {
|
||||
:deep(.ant-table) {
|
||||
.ant-table-thead > tr > th {
|
||||
font-weight: 500;
|
||||
background: #fafafa;
|
||||
background: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
|
@ -953,40 +951,31 @@ onMounted(() => {
|
|||
.parameter-box,
|
||||
.result-box {
|
||||
padding: 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
|
||||
.parameter-header,
|
||||
.result-header {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.parameter-content,
|
||||
.result-content {
|
||||
min-height: calc(100% - 32px);
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 4px;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #262626;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-box {
|
||||
.result-content {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #91d5ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -327,7 +327,7 @@ onMounted(() => {
|
|||
align-items: center;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -337,14 +337,14 @@ onMounted(() => {
|
|||
|
||||
.input-section {
|
||||
flex: 5;
|
||||
border: 1px solid #d9d9d9;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
|
||||
.simple-form {
|
||||
:deep(.ant-table) {
|
||||
.ant-table-thead > tr > th {
|
||||
font-weight: 500;
|
||||
background: #fafafa;
|
||||
background: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
|
@ -373,28 +373,26 @@ onMounted(() => {
|
|||
.parameter-box,
|
||||
.result-box {
|
||||
padding: 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
|
||||
.parameter-header,
|
||||
.result-header {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.parameter-content,
|
||||
.result-content {
|
||||
min-height: 120px;
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 4px;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #262626;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
|
|
@ -712,32 +712,30 @@ onMounted(() => {
|
|||
|
||||
.property-sidebar {
|
||||
width: 200px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #d9d9d9;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
|
||||
.sidebar-header {
|
||||
padding: 10px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
background: #f0f0f0;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
background: hsl(var(--muted));
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #e6f7ff;
|
||||
background: hsl(var(--muted));
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: white;
|
||||
background: #1890ff;
|
||||
background: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.function-name {
|
||||
|
@ -763,7 +761,6 @@ onMounted(() => {
|
|||
|
||||
.control-label {
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
@ -778,11 +775,11 @@ onMounted(() => {
|
|||
align-items: center;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
color: hsl(var(--muted-foreground));
|
||||
|
||||
.info-icon {
|
||||
font-weight: bold;
|
||||
color: #1890ff;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -794,14 +791,14 @@ onMounted(() => {
|
|||
.input-section {
|
||||
flex: 5;
|
||||
min-height: 300px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
|
||||
.simple-form {
|
||||
:deep(.ant-table) {
|
||||
.ant-table-thead > tr > th {
|
||||
font-weight: 500;
|
||||
background: #fafafa;
|
||||
background: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
|
@ -821,7 +818,6 @@ onMounted(() => {
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 300px;
|
||||
color: #8c8c8c;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
|
@ -849,26 +845,24 @@ onMounted(() => {
|
|||
.parameter-box {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
|
||||
.parameter-header {
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.parameter-content {
|
||||
min-height: calc(100% - 32px);
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 4px;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #262626;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
||||
import { Image, message, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
import { Image, message, QRCode, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { deviceStateOptions } from '#/constants/dicts';
|
||||
// import { deviceUpdateStatus } from '#/api/device/device';
|
||||
|
@ -33,6 +32,8 @@ let deviceStatusSubscription: any = null;
|
|||
const subscribeDeviceStatus = () => {
|
||||
if (!currentDevice.value) return;
|
||||
|
||||
if (deviceStatusSubscription) return;
|
||||
|
||||
const productKey = currentDevice.value.productObj.productKey;
|
||||
|
||||
const id = `device-status-${currentDevice.value.deviceKey}`;
|
||||
|
@ -41,9 +42,12 @@ const subscribeDeviceStatus = () => {
|
|||
deviceStatusSubscription = getWebSocket(id, topic, {
|
||||
deviceKey: currentDevice.value.deviceKey,
|
||||
}).subscribe((data: any) => {
|
||||
if (data.payload?.deviceKey === currentDevice.value.deviceKey) {
|
||||
if (data.requestId === id) {
|
||||
// 更新设备状态
|
||||
deviceStore.updateDeviceStatus(data.payload);
|
||||
deviceStore.updateDeviceStatus(data.payload.value);
|
||||
message.success(
|
||||
`设备‘${currentDevice.value.deviceName}’${data.payload.value === 'online' ? '上线' : '下线'}了`,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -114,7 +118,7 @@ onUnmounted(() => {
|
|||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="device-detail">
|
||||
<div class="device-detail bg-background">
|
||||
<!-- 页面头部 -->
|
||||
<div class="detail-header">
|
||||
<div class="header-left">
|
||||
|
@ -155,15 +159,12 @@ onUnmounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<QrcodeVue
|
||||
<QRCode
|
||||
:value="currentDevice.deviceKey"
|
||||
:image-settings="{
|
||||
src: currentDevice.imgUrl,
|
||||
height: 20,
|
||||
width: 20,
|
||||
}"
|
||||
:size="80"
|
||||
level="H"
|
||||
:size="100"
|
||||
:icon-size="20"
|
||||
error-level="H"
|
||||
:icon="currentDevice.imgUrl"
|
||||
/>
|
||||
<!-- <a-button
|
||||
type="primary"
|
||||
|
@ -234,7 +235,6 @@ onUnmounted(() => {
|
|||
<style lang="scss" scoped>
|
||||
.device-detail {
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
|
||||
.detail-header {
|
||||
position: relative;
|
||||
|
@ -261,7 +261,6 @@ onUnmounted(() => {
|
|||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.device-status {
|
||||
|
@ -271,7 +270,7 @@ onUnmounted(() => {
|
|||
|
||||
.status-label {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
|
@ -284,21 +283,21 @@ onUnmounted(() => {
|
|||
line-height: 20px;
|
||||
|
||||
&.warning {
|
||||
color: #faad14;
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: #52c41a;
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: #ff4d4f;
|
||||
color: hsl(var(--secondary));
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.status-switch {
|
||||
|
@ -314,8 +313,6 @@ onUnmounted(() => {
|
|||
right: 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
padding: 10px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -332,16 +329,15 @@ onUnmounted(() => {
|
|||
|
||||
.basic-label {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.basic-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
|
||||
&.product-link {
|
||||
color: #1890ff;
|
||||
color: hsl(var(--primary));
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
|
|
|
@ -99,6 +99,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||
label: '产品KEY',
|
||||
fieldName: 'productKey',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '产品名称',
|
||||
|
@ -139,8 +140,9 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||
h(
|
||||
'span',
|
||||
{
|
||||
class: 'text-[14px] text-black/25 truncate',
|
||||
style: 'max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
||||
class: 'text-[14px] text-muted-foreground truncate',
|
||||
style:
|
||||
'max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
||||
},
|
||||
option.tooltip,
|
||||
),
|
||||
|
|
|
@ -41,7 +41,8 @@ const pagination = reactive({
|
|||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number, range: [number, number]) => `第 ${range[0]}-${range[1]}条/总共${total}条`,
|
||||
showTotal: (total: number, range: [number, number]) =>
|
||||
`第 ${range[0]}-${range[1]}条/总共${total}条`,
|
||||
});
|
||||
|
||||
// 网关列表数据
|
||||
|
@ -55,7 +56,8 @@ const providerOptions = [
|
|||
value: 'child-device',
|
||||
channel: 'child-device',
|
||||
transport: 'Gateway',
|
||||
description: '需要通过网关与平台进行数据通信的设备,将作为网关子设备接入到平台。',
|
||||
description:
|
||||
'需要通过网关与平台进行数据通信的设备,将作为网关子设备接入到平台。',
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -230,7 +232,10 @@ onMounted(() => {
|
|||
{{ getProviderText(gateway.provider) }}
|
||||
</Tag>
|
||||
</div>
|
||||
<Tag class="gateway-status" :color="getStatusColor(gateway.enabled)">
|
||||
<Tag
|
||||
class="gateway-status"
|
||||
:color="getStatusColor(gateway.enabled)"
|
||||
>
|
||||
{{ getStatusText(gateway.enabled) }}
|
||||
</Tag>
|
||||
<div class="gateway-footer">
|
||||
|
@ -309,18 +314,17 @@ onMounted(() => {
|
|||
.gateway-card {
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
border: 1px solid #e4e4e7;
|
||||
border: 1px solid hsl(var(--border));
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 4px 12px rgb(24 144 255 / 15%);
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: hsl(var(--accent-hover));
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #f0f8ff;
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 4px 12px rgb(24 144 255 / 20%);
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow: hsl(var(--accent-hover));
|
||||
}
|
||||
|
||||
.gateway-main {
|
||||
|
@ -343,7 +347,7 @@ onMounted(() => {
|
|||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
background: #1890ff;
|
||||
background: hsl(var(--primary));
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
@ -367,7 +371,6 @@ onMounted(() => {
|
|||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -392,12 +395,11 @@ onMounted(() => {
|
|||
|
||||
.gateway-footer-item-label {
|
||||
font-size: 12px;
|
||||
color: #3d3d3d;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.gateway-footer-item-value {
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -416,7 +418,7 @@ onMounted(() => {
|
|||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
background: #1890ff;
|
||||
background: hsl(var(--primary));
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
@ -436,9 +438,7 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
.footer-actions {
|
||||
padding-top: 16px;
|
||||
text-align: right;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { computed } from 'vue';
|
|||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
|
||||
import { EditOutlined } from '@ant-design/icons-vue';
|
||||
import { Button, Descriptions, DescriptionsItem } from 'ant-design-vue';
|
||||
import { Button, Descriptions, DescriptionsItem, Empty } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { deviceTypeOptions } from '#/constants/dicts';
|
||||
|
@ -152,7 +152,10 @@ const productParams = computed(() =>
|
|||
</div>
|
||||
|
||||
<div v-if="productParams.length === 0" class="empty-params">
|
||||
<p>暂无产品参数</p>
|
||||
<Empty
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
description="暂无产品参数"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Descriptions bordered :column="3" v-else>
|
||||
|
@ -187,7 +190,6 @@ const productParams = computed(() =>
|
|||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
|
@ -225,45 +227,15 @@ const productParams = computed(() =>
|
|||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.params-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
|
||||
.param-card {
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
|
||||
.param-key {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.param-value {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-descriptions-item-label) {
|
||||
font-weight: 500;
|
||||
color: #595959;
|
||||
//color: #595959;
|
||||
}
|
||||
|
||||
:deep(.ant-descriptions-item-content) {
|
||||
color: #262626;
|
||||
//color: #262626;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -69,7 +69,8 @@ const accessConfigs = ref([]);
|
|||
// 表格列配置
|
||||
const tableColumns = computed(() => {
|
||||
const isMQTT =
|
||||
accessInfo.value.provider === 'mqtt-server-gateway' || accessInfo.value.provider === 'mqtt-client-gateway';
|
||||
accessInfo.value.provider === 'mqtt-server-gateway' ||
|
||||
accessInfo.value.provider === 'mqtt-client-gateway';
|
||||
|
||||
return isMQTT
|
||||
? [
|
||||
|
@ -265,9 +266,18 @@ onMounted(() => {
|
|||
<h4>连接信息</h4>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div v-if="accessInfo.addresses && accessInfo.addresses.length > 0">
|
||||
<div v-for="item in accessInfo.addresses" :key="item.address" class="address-item">
|
||||
<Badge :color="item.health === -1 ? 'red' : 'green'" :text="item.address" />
|
||||
<div
|
||||
v-if="accessInfo.addresses && accessInfo.addresses.length > 0"
|
||||
>
|
||||
<div
|
||||
v-for="item in accessInfo.addresses"
|
||||
:key="item.address"
|
||||
class="address-item"
|
||||
>
|
||||
<Badge
|
||||
:color="item.health === -1 ? 'red' : 'green'"
|
||||
:text="item.address"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-address">暂无连接信息</div>
|
||||
|
@ -297,7 +307,11 @@ onMounted(() => {
|
|||
</div> -->
|
||||
|
||||
<!-- 其它接入配置 -->
|
||||
<div v-for="(config, index) in accessConfigs" :key="index" class="config-section">
|
||||
<div
|
||||
v-for="(config, index) in accessConfigs"
|
||||
:key="index"
|
||||
class="config-section"
|
||||
>
|
||||
<div class="section-header">
|
||||
<h4>{{ config.name }}</h4>
|
||||
<Tooltip title="此配置来自于产品接入网关所选择的协议">
|
||||
|
@ -329,13 +343,19 @@ onMounted(() => {
|
|||
placeholder="请输入"
|
||||
/>
|
||||
<Select
|
||||
v-if="item.type.type === 'enum' || item.type.type === 'boolean'"
|
||||
v-if="
|
||||
item.type.type === 'enum' || item.type.type === 'boolean'
|
||||
"
|
||||
v-model:value="formData[item.property]"
|
||||
placeholder="请选择"
|
||||
:options="getOptions(item)"
|
||||
/>
|
||||
<InputNumber
|
||||
v-if="['int', 'float', 'double', 'long'].includes(item.type.type)"
|
||||
v-if="
|
||||
['int', 'float', 'double', 'long'].includes(
|
||||
item.type.type,
|
||||
)
|
||||
"
|
||||
v-model:value="formData[item.property]"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
|
@ -348,7 +368,9 @@ onMounted(() => {
|
|||
<div class="config-section">
|
||||
<div class="section-header">
|
||||
<h4>存储策略</h4>
|
||||
<Tooltip title="若修改存储策略,需要手动做数据迁移,平台只能搜索最新存储策略中的数据">
|
||||
<Tooltip
|
||||
title="若修改存储策略,需要手动做数据迁移,平台只能搜索最新存储策略中的数据"
|
||||
>
|
||||
<QuestionCircleOutlined />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
@ -364,18 +386,27 @@ onMounted(() => {
|
|||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="action-buttons">
|
||||
<a-button type="primary" @click="handleSave" :loading="saveLoading" v-access:code="['device:product:edit']">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="handleSave"
|
||||
:loading="saveLoading"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
保存
|
||||
</a-button>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<!-- 右侧信息展示 -->
|
||||
<Col :span="12" v-if="accessInfo.routes && accessInfo.routes.length > 0">
|
||||
<Col
|
||||
:span="12"
|
||||
v-if="accessInfo.routes && accessInfo.routes.length > 0"
|
||||
>
|
||||
<div class="info-panel">
|
||||
<h4>
|
||||
{{
|
||||
accessInfo.provider === 'mqtt-server-gateway' || accessInfo.provider === 'mqtt-client-gateway'
|
||||
accessInfo.provider === 'mqtt-server-gateway' ||
|
||||
accessInfo.provider === 'mqtt-client-gateway'
|
||||
? 'Topic信息'
|
||||
: 'URL信息'
|
||||
}}
|
||||
|
@ -427,7 +458,11 @@ onMounted(() => {
|
|||
@cancel="handleAccessModalClose"
|
||||
:footer="null"
|
||||
>
|
||||
<AccessSelector :product-id="productInfo.id" @select="handleModalSelect" @close="handleAccessModalClose" />
|
||||
<AccessSelector
|
||||
:product-id="productInfo.id"
|
||||
@select="handleModalSelect"
|
||||
@close="handleAccessModalClose"
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -443,7 +478,7 @@ onMounted(() => {
|
|||
.config-section {
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
|
||||
.section-header {
|
||||
|
@ -456,7 +491,6 @@ onMounted(() => {
|
|||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -465,12 +499,10 @@ onMounted(() => {
|
|||
margin: 0 0 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.access-desc {
|
||||
margin: 0;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.address-item {
|
||||
|
|
|
@ -14,9 +14,9 @@ import {
|
|||
|
||||
import { productUpdateById } from '#/api/device/product';
|
||||
|
||||
import ImportForm from './ImportForm.vue';
|
||||
import MetadataTable from './MetadataTable.vue';
|
||||
import TSLViewer from './TSLViewer.vue';
|
||||
import ImportForm from './metadata/ImportForm.vue';
|
||||
import MetadataTable from './metadata/MetadataTable.vue';
|
||||
import TSLViewer from './metadata/TSLViewer.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
productId: string;
|
||||
|
@ -424,6 +424,7 @@ onBeforeRouteLeave((to, from, next) => {
|
|||
<!-- 导入抽屉 -->
|
||||
<Drawer
|
||||
v-model:open="importVisible"
|
||||
v-if="importVisible"
|
||||
title="快速导入"
|
||||
width="600px"
|
||||
@close="handleImportClose"
|
||||
|
@ -442,6 +443,7 @@ onBeforeRouteLeave((to, from, next) => {
|
|||
@close="handleTSLClose"
|
||||
>
|
||||
<TSLViewer
|
||||
v-if="tslVisible"
|
||||
:product-id="productInfo.id"
|
||||
:product-info="productInfo"
|
||||
:file-name="productInfo.productName"
|
||||
|
@ -468,12 +470,11 @@ onBeforeRouteLeave((to, from, next) => {
|
|||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -226,7 +226,6 @@ defineExpose({
|
|||
.param-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,12 +244,11 @@ defineExpose({
|
|||
}
|
||||
|
||||
.param-items {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
|
||||
.param-item {
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-bottom: none;
|
||||
|
||||
.param-row {
|
||||
|
@ -267,7 +265,6 @@ defineExpose({
|
|||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #595959;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,15 +279,15 @@ defineExpose({
|
|||
|
||||
.param-tips {
|
||||
padding: 12px 16px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
background: hsl(var(--success-foreground));
|
||||
border: 1px solid hsl(var(--success));
|
||||
border-radius: 6px;
|
||||
|
||||
.tip-text {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #52c41a;
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,7 +164,6 @@ const handleCancel = () => {
|
|||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
|
||||
.header-item {
|
||||
flex: 1;
|
||||
|
@ -205,7 +204,7 @@ const handleCancel = () => {
|
|||
padding-top: 16px;
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
border-top: 1px dashed #d9d9d9;
|
||||
border-top: 1px dashed hsl(var(--border));
|
||||
|
||||
.add-button {
|
||||
width: 100%;
|
|
@ -1,6 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
|
||||
import { usePreferences } from '@vben/preferences';
|
||||
|
||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Button,
|
||||
|
@ -25,6 +27,8 @@ defineExpose({});
|
|||
const formRef = ref();
|
||||
const importLoading = ref(false);
|
||||
|
||||
const editorTheme = ref('vs');
|
||||
|
||||
const formData = reactive({
|
||||
script: `{
|
||||
"properties": [],
|
||||
|
@ -146,9 +150,6 @@ const parseFileContent = (content: string, fileName: string) => {
|
|||
throw error;
|
||||
}
|
||||
};
|
||||
// const handleJsonChange = (value: string) => {
|
||||
// formData.script = value;
|
||||
// };
|
||||
|
||||
// 导入
|
||||
const handleImport = async () => {
|
||||
|
@ -194,6 +195,9 @@ const handleImport = async () => {
|
|||
const handleCancel = () => {
|
||||
emit('success');
|
||||
};
|
||||
// 监听主题切换x
|
||||
const { isDark } = usePreferences();
|
||||
editorTheme.value = isDark.value ? 'vs-dark' : 'vs';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -223,8 +227,8 @@ const handleCancel = () => {
|
|||
<MonacoEditor
|
||||
v-model="formData.script"
|
||||
lang="json"
|
||||
theme="vs"
|
||||
style="height: 400px; border: 1px solid #d9d9d9; border-radius: 6px"
|
||||
:theme="editorTheme"
|
||||
style="min-height: 100%"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
|
@ -248,66 +252,14 @@ const handleCancel = () => {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.import-form {
|
||||
height: 100%;
|
||||
|
||||
.upload-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.preview-area {
|
||||
overflow: hidden;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.preview-tips {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #666;
|
||||
background: #f0f9ff;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.category-preview {
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
|
||||
:deep(.ant-tabs) {
|
||||
.ant-tabs-tab {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
.ant-table-thead > tr > th {
|
||||
font-weight: 500;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.script-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -316,9 +268,7 @@ const handleCancel = () => {
|
|||
}
|
||||
|
||||
.script-editor {
|
||||
overflow: hidden;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
height: calc(100vh - 300px);
|
||||
}
|
||||
|
||||
.form-actions {
|
|
@ -35,7 +35,8 @@ const tableData = ref<any[]>([]);
|
|||
const formData = ref<any>(null);
|
||||
|
||||
// 生成稳定主键
|
||||
const generatePk = (): string => `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
||||
const generatePk = (): string =>
|
||||
`${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
||||
|
||||
// 为记录补齐主键
|
||||
const withPk = (record: any) => {
|
||||
|
@ -52,7 +53,8 @@ const withPk = (record: any) => {
|
|||
};
|
||||
|
||||
// 通过主键查找索引
|
||||
const findIndexByPk = (pk: string) => tableData.value.findIndex((item) => item._pk === pk);
|
||||
const findIndexByPk = (pk: string) =>
|
||||
tableData.value.findIndex((item) => item._pk === pk);
|
||||
|
||||
// 表格列配置
|
||||
const columns = computed(() => {
|
||||
|
@ -273,7 +275,11 @@ onMounted(() => {
|
|||
<template>
|
||||
<div class="metadata-table">
|
||||
<div class="table-header">
|
||||
<Button type="primary" @click="handleAdd" v-access:code="['device:product:edit']">
|
||||
<Button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
|
@ -281,26 +287,52 @@ onMounted(() => {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<Table :columns="columns" :data-source="tableData" :loading="loading" :pagination="false" row-key="_pk">
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="_pk"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<Space>
|
||||
<Button type="link" size="small" @click="handleEdit(record, index)" v-access:code="['device:product:edit']">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="handleEdit(record, index)"
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm title="确认删除?" @confirm="handleDelete(record)">
|
||||
<Button type="link" size="small" danger v-access:code="['device:product:edit']"> 删除 </Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
v-access:code="['device:product:edit']"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dataType'">
|
||||
<Tag color="processing">
|
||||
{{ dataTypeOptions.find((item) => item.value === record.valueParams?.dataType)?.label }}
|
||||
{{
|
||||
dataTypeOptions.find(
|
||||
(item) => item.value === record.valueParams?.dataType,
|
||||
)?.label
|
||||
}}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'expands.type'">
|
||||
<Tag color="processing">
|
||||
{{ readWriteTypeOptions.find((item) => item.value === record.expands.type)?.label }}
|
||||
{{
|
||||
readWriteTypeOptions.find(
|
||||
(item) => item.value === record.expands.type,
|
||||
)?.label
|
||||
}}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'required'">
|
|
@ -579,25 +579,4 @@ watch(
|
|||
</Drawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.enum-preview {
|
||||
.enum-items-preview {
|
||||
padding: 12px;
|
||||
margin-top: 12px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 4px;
|
||||
|
||||
.preview-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.preview-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style></style>
|
|
@ -140,7 +140,9 @@ const handleDeleteProperty = (propertyId: string) => {
|
|||
// 属性选择确认
|
||||
const handlePropertySelectionConfirm = (selectedProperties: any[]) => {
|
||||
// 过滤掉已经选择的属性
|
||||
const newProperties = selectedProperties.filter((prop) => !formData.value.properties.some((p) => p.id === prop.id));
|
||||
const newProperties = selectedProperties.filter(
|
||||
(prop) => !formData.value.properties.some((p) => p.id === prop.id),
|
||||
);
|
||||
|
||||
if (newProperties.length > 0) {
|
||||
formData.value.properties.push(...newProperties);
|
||||
|
@ -176,7 +178,12 @@ const handleClose = () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer v-model:open="visible" :title="isEdit ? '编辑分组' : '添加'" width="800px" @close="handleClose">
|
||||
<Drawer
|
||||
v-model:open="visible"
|
||||
:title="isEdit ? '编辑分组' : '添加'"
|
||||
width="800px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||
<Row :gutter="16">
|
||||
<Col :span="12">
|
||||
|
@ -202,12 +209,23 @@ const handleClose = () => {
|
|||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<Button type="link" size="small" danger @click="handleDeleteProperty(record.id)"> 删除 </Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
@click="handleDeleteProperty(record.id)"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="add-button-container">
|
||||
<a-button @click="handleAddProperty" type="primary" class="add-button">
|
||||
<a-button
|
||||
@click="handleAddProperty"
|
||||
type="primary"
|
||||
class="add-button"
|
||||
>
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
|
@ -218,12 +236,21 @@ const handleClose = () => {
|
|||
</Col>
|
||||
<Col :span="12">
|
||||
<FormItem label="排序" name="sort">
|
||||
<InputNumber v-model:value="formData.sort" :min="1" style="width: 100%" placeholder="请输入" />
|
||||
<InputNumber
|
||||
v-model:value="formData.sort"
|
||||
:min="1"
|
||||
style="width: 100%"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="24">
|
||||
<FormItem label="描述" name="description">
|
||||
<Textarea v-model:value="formData.description" placeholder="请输入" :rows="3" />
|
||||
<Textarea
|
||||
v-model:value="formData.description"
|
||||
placeholder="请输入"
|
||||
:rows="3"
|
||||
/>
|
||||
</FormItem>
|
||||
</Col>
|
||||
</Form>
|
||||
|
@ -231,7 +258,9 @@ const handleClose = () => {
|
|||
<template #footer>
|
||||
<Space>
|
||||
<Button @click="handleClose">取消</Button>
|
||||
<Button type="primary" :loading="saveLoading" @click="handleSave"> 确认 </Button>
|
||||
<Button type="primary" :loading="saveLoading" @click="handleSave">
|
||||
确认
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
|
|
@ -172,6 +172,7 @@ const getReadWriteTypeLabel = (type: string) => {
|
|||
:pagination="false"
|
||||
row-key="id"
|
||||
size="small"
|
||||
:scroll="{ y: 500 }"
|
||||
:row-selection="{
|
||||
selectedRowKeys: selectedPropertyIds,
|
||||
onChange: (selectedRowKeys: string[]) => {
|
||||
|
@ -218,14 +219,12 @@ const getReadWriteTypeLabel = (type: string) => {
|
|||
.search-section {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
background-color: #fafafa;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { CopyOutlined, DownloadOutlined, ReloadOutlined } from '@ant-design/icons-vue';
|
||||
import { usePreferences } from '@vben/preferences';
|
||||
|
||||
import {
|
||||
CopyOutlined,
|
||||
DownloadOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { message, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
|
@ -14,6 +20,8 @@ const props = defineProps<{
|
|||
productInfo: object;
|
||||
}>();
|
||||
|
||||
const editorTheme = ref('vs');
|
||||
|
||||
const mockData = {
|
||||
properties: [],
|
||||
functions: [],
|
||||
|
@ -117,6 +125,9 @@ watch(
|
|||
onMounted(() => {
|
||||
loadTSLData();
|
||||
});
|
||||
// 监听主题切换x
|
||||
const { isDark } = usePreferences();
|
||||
editorTheme.value = isDark.value ? 'vs-dark' : 'vs';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -151,7 +162,12 @@ onMounted(() => {
|
|||
|
||||
<div class="viewer-content">
|
||||
<div class="viewer-toolbar">
|
||||
<MonacoEditor v-model="tslJson" lang="javascript" style="height: 100%" theme="vs" />
|
||||
<MonacoEditor
|
||||
v-model="tslJson"
|
||||
lang="javascript"
|
||||
style="height: 100%"
|
||||
:theme="editorTheme"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -171,8 +187,8 @@ onMounted(() => {
|
|||
.viewer-content-tips {
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
color: #8c8c8c;
|
||||
background: #f5f5f5;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: hsl(var(--muted));
|
||||
}
|
||||
|
||||
.viewer-content {
|
|
@ -106,7 +106,7 @@ onUnmounted(() => {
|
|||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="product-detail">
|
||||
<div class="product-detail bg-background">
|
||||
<!-- 页面头部 -->
|
||||
<div class="detail-header">
|
||||
<div class="header-left">
|
||||
|
@ -185,7 +185,6 @@ onUnmounted(() => {
|
|||
<style lang="scss" scoped>
|
||||
.product-detail {
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
|
@ -211,7 +210,6 @@ onUnmounted(() => {
|
|||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.product-status {
|
||||
|
|
|
@ -204,7 +204,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
|||
h(
|
||||
'span',
|
||||
{
|
||||
class: 'text-[14px] text-black/25 truncate',
|
||||
class: 'text-[14px] text-muted-foreground truncate',
|
||||
style:
|
||||
'max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue