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",
|
"lodash-es": "^4.17.21",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"pinia": "catalog:",
|
"pinia": "catalog:",
|
||||||
"qrcode.vue": "^3.6.0",
|
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"tinymce": "^7.3.0",
|
"tinymce": "^7.3.0",
|
||||||
"unplugin-vue-components": "^0.27.3",
|
"unplugin-vue-components": "^0.27.3",
|
||||||
|
|
|
@ -41,3 +41,14 @@ const tokenTheme = computed(() => {
|
||||||
</App>
|
</App>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</template>
|
</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">
|
<style scoped lang="scss">
|
||||||
.editor {
|
.editor {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
border: 1px solid hsl(var(--border)) !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -28,6 +28,10 @@ export const useDeviceStore = defineStore('device', {
|
||||||
this.deviceCount = count;
|
this.deviceCount = count;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateDeviceStatus(state: string) {
|
||||||
|
this.currentDevice.deviceState = state;
|
||||||
|
},
|
||||||
|
|
||||||
async getDetail(id: string) {
|
async getDetail(id: string) {
|
||||||
try {
|
try {
|
||||||
const res = await deviceInfo(id);
|
const res = await deviceInfo(id);
|
||||||
|
|
|
@ -6,10 +6,16 @@ import { computed, ref } from 'vue';
|
||||||
import { useVbenDrawer } from '@vben/common-ui';
|
import { useVbenDrawer } from '@vben/common-ui';
|
||||||
|
|
||||||
import { EditOutlined, EyeOutlined } from '@ant-design/icons-vue';
|
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 { 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';
|
import deviceDrawer from '../../device-drawer.vue';
|
||||||
|
|
||||||
|
@ -53,7 +59,12 @@ const metadataObj = computed(() => {
|
||||||
<div class="info-header">
|
<div class="info-header">
|
||||||
<h3>设备信息</h3>
|
<h3>设备信息</h3>
|
||||||
<Space>
|
<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>
|
<template #icon>
|
||||||
<EditOutlined />
|
<EditOutlined />
|
||||||
</template>
|
</template>
|
||||||
|
@ -79,7 +90,11 @@ const metadataObj = computed(() => {
|
||||||
{{ deviceInfo.deviceName }}
|
{{ deviceInfo.deviceName }}
|
||||||
</DescriptionsItem>
|
</DescriptionsItem>
|
||||||
<DescriptionsItem label="设备类型">
|
<DescriptionsItem label="设备类型">
|
||||||
{{ deviceTypeOptions.find((option) => option.value === deviceInfo.deviceType)?.label }}
|
{{
|
||||||
|
deviceTypeOptions.find(
|
||||||
|
(option) => option.value === deviceInfo.deviceType,
|
||||||
|
)?.label
|
||||||
|
}}
|
||||||
</DescriptionsItem>
|
</DescriptionsItem>
|
||||||
<!-- <DescriptionsItem label="产品名称">
|
<!-- <DescriptionsItem label="产品名称">
|
||||||
{{ deviceInfo.productName }}
|
{{ deviceInfo.productName }}
|
||||||
|
@ -88,7 +103,11 @@ const metadataObj = computed(() => {
|
||||||
{{ deviceInfo.otaVersion || '-' }}
|
{{ deviceInfo.otaVersion || '-' }}
|
||||||
</DescriptionsItem>
|
</DescriptionsItem>
|
||||||
<DescriptionsItem label="接入方式">
|
<DescriptionsItem label="接入方式">
|
||||||
{{ networkTypeOptions.find((option) => option.value === deviceInfo?.productObj?.provider)?.label || '-' }}
|
{{
|
||||||
|
networkTypeOptions.find(
|
||||||
|
(option) => option.value === deviceInfo?.productObj?.provider,
|
||||||
|
)?.label || '-'
|
||||||
|
}}
|
||||||
</DescriptionsItem>
|
</DescriptionsItem>
|
||||||
<DescriptionsItem label="消息协议">
|
<DescriptionsItem label="消息协议">
|
||||||
{{ deviceInfo?.productObj?.protocolName || '-' }}
|
{{ deviceInfo?.productObj?.protocolName || '-' }}
|
||||||
|
@ -112,12 +131,16 @@ const metadataObj = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 产品参数展示 -->
|
<!-- 产品参数展示 -->
|
||||||
<div class="product-params-section">
|
<div class="product-params-section" v-if="deviceInfo?.productParamArray">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3>设备参数</h3>
|
<h3>设备参数</h3>
|
||||||
</div>
|
</div>
|
||||||
<Descriptions bordered :column="3">
|
<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 }}
|
{{ param.value }}
|
||||||
</DescriptionsItem>
|
</DescriptionsItem>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
|
@ -126,8 +149,14 @@ const metadataObj = computed(() => {
|
||||||
<DeviceDrawer @reload="emit('refresh')" />
|
<DeviceDrawer @reload="emit('refresh')" />
|
||||||
|
|
||||||
<!-- TSL查看抽屉 -->
|
<!-- TSL查看抽屉 -->
|
||||||
<Drawer v-model:open="tslVisible" title="物模型" width="800px" @close="handleTSLClose">
|
<Drawer
|
||||||
|
v-model:open="tslVisible"
|
||||||
|
title="物模型"
|
||||||
|
width="800px"
|
||||||
|
@close="handleTSLClose"
|
||||||
|
>
|
||||||
<TSLViewer
|
<TSLViewer
|
||||||
|
v-if="tslVisible"
|
||||||
:product-id="deviceInfo?.productId"
|
:product-id="deviceInfo?.productId"
|
||||||
:product-info="deviceInfo?.productObj"
|
:product-info="deviceInfo?.productObj"
|
||||||
:file-name="deviceInfo?.deviceName"
|
:file-name="deviceInfo?.deviceName"
|
||||||
|
@ -149,12 +178,10 @@ const metadataObj = computed(() => {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-content {
|
.info-content {
|
||||||
background: #fff;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,13 +200,11 @@ const metadataObj = computed(() => {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-params {
|
.empty-params {
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
color: #8c8c8c;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
@ -187,36 +212,6 @@ const metadataObj = computed(() => {
|
||||||
font-size: 14px;
|
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>
|
</style>
|
||||||
|
|
|
@ -249,7 +249,6 @@ onMounted(() => {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
background: #fafafa;
|
|
||||||
border-radius: 4px;
|
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 { EllipsisText } from '@vben/common-ui';
|
||||||
|
|
||||||
import {
|
import { UnorderedListOutlined } from '@ant-design/icons-vue';
|
||||||
LineChartOutlined,
|
|
||||||
UnorderedListOutlined,
|
|
||||||
} from '@ant-design/icons-vue';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
@ -27,6 +24,8 @@ import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { getWebSocket } from '#/utils/websocket';
|
import { getWebSocket } from '#/utils/websocket';
|
||||||
|
|
||||||
|
import RealtimeChart from './RealtimeChart.vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
metadata: any;
|
metadata: any;
|
||||||
|
@ -59,15 +58,16 @@ const subscribeRealtimeData = () => {
|
||||||
properties: props.metadata.properties.map((p: any) => p.id),
|
properties: props.metadata.properties.map((p: any) => p.id),
|
||||||
interval: '3s',
|
interval: '3s',
|
||||||
}).subscribe((data: any) => {
|
}).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(
|
const property = runtimeProperties.value.find(
|
||||||
(p: any) => p.id === propData.id,
|
(p: any) => p.id === propId,
|
||||||
);
|
);
|
||||||
if (property) {
|
if (property) {
|
||||||
property.value = propData.value;
|
property.value = propValue;
|
||||||
property.timestamp = dayjs(propData.timestamp).format(
|
property.timestamp = dayjs(data.payload.timeString).format(
|
||||||
'YYYY-MM-DD HH:mm:ss',
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -135,8 +135,14 @@ const logData = ref<any[]>([]);
|
||||||
|
|
||||||
const logColumns = [
|
const logColumns = [
|
||||||
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 200 },
|
{ 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) => {
|
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:19:40', value: '0.5' },
|
||||||
{ timestamp: '2025-08-19 16:18:40', value: '0.4' },
|
{ timestamp: '2025-08-19 16:18:40', value: '0.4' },
|
||||||
{ timestamp: '2025-08-19 16:17:40', value: '0.3' },
|
{ 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 {
|
} finally {
|
||||||
logLoading.value = false;
|
logLoading.value = false;
|
||||||
|
@ -203,7 +227,7 @@ const formatValue = (property: any) => {
|
||||||
return valueParams.enumConf.find((item: any) => item.value === value)?.text;
|
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);
|
return dayjs(value).format(valueParams.format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,74 +254,12 @@ const closeLogModal = () => {
|
||||||
logData.value = [];
|
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(() => {
|
onMounted(() => {
|
||||||
initRuntime();
|
initRuntime();
|
||||||
startRefreshTimer();
|
|
||||||
subscribeRealtimeData();
|
subscribeRealtimeData();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopRefreshTimer();
|
|
||||||
unsubscribeRealtimeData();
|
unsubscribeRealtimeData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -417,6 +379,7 @@ watch(
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
size="small"
|
size="small"
|
||||||
row-key="timestamp"
|
row-key="timestamp"
|
||||||
|
:scroll="{ y: 500 }"
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column }">
|
<template #bodyCell="{ column }">
|
||||||
<template v-if="column.key === 'action'">
|
<template v-if="column.key === 'action'">
|
||||||
|
@ -433,8 +396,22 @@ watch(
|
||||||
>
|
>
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<div class="chart-placeholder">
|
<div class="chart-placeholder">
|
||||||
<LineChartOutlined style="font-size: 48px; color: #d9d9d9" />
|
<RealtimeChart
|
||||||
<p>图表功能开发中...</p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
@ -459,7 +436,6 @@ watch(
|
||||||
|
|
||||||
.control-label {
|
.control-label {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #262626;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -483,15 +459,14 @@ watch(
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-log-btn {
|
.property-log-btn {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
color: #8c8c8c;
|
color: hsl(var(--muted-foreground));
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #1890ff;
|
color: hsl(var(--primary));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -506,7 +481,7 @@ watch(
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1890ff;
|
color: hsl(var(--primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.property-unit {
|
.property-unit {
|
||||||
|
@ -514,7 +489,7 @@ watch(
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
color: #8c8c8c;
|
color: hsl(var(--muted-foreground));
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -578,32 +578,30 @@ onMounted(() => {
|
||||||
|
|
||||||
.event-sidebar {
|
.event-sidebar {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
background: #fafafa;
|
border: 1px solid hsl(var(--border));
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #262626;
|
background: hsl(var(--muted));
|
||||||
background: #f0f0f0;
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
border-bottom: 1px solid #d9d9d9;
|
|
||||||
border-radius: 6px 6px 0 0;
|
border-radius: 6px 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item {
|
.sidebar-item {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #e6f7ff;
|
background: hsl(var(--muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: white;
|
color: white;
|
||||||
background: #1890ff;
|
background: hsl(var(--primary));
|
||||||
|
|
||||||
.event-id {
|
.event-id {
|
||||||
color: rgb(255 255 255 / 80%);
|
color: rgb(255 255 255 / 80%);
|
||||||
|
@ -634,7 +632,7 @@ onMounted(() => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #8c8c8c;
|
color: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -645,14 +643,14 @@ onMounted(() => {
|
||||||
.input-section {
|
.input-section {
|
||||||
flex: 5;
|
flex: 5;
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
.simple-form {
|
.simple-form {
|
||||||
:deep(.ant-table) {
|
:deep(.ant-table) {
|
||||||
.ant-table-thead > tr > th {
|
.ant-table-thead > tr > th {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background: #fafafa;
|
background: hsl(var(--muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-tbody > tr > td {
|
.ant-table-tbody > tr > td {
|
||||||
|
@ -687,28 +685,26 @@ onMounted(() => {
|
||||||
.parameter-box,
|
.parameter-box,
|
||||||
.result-box {
|
.result-box {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
.parameter-header,
|
.parameter-header,
|
||||||
.result-header {
|
.result-header {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameter-content,
|
.parameter-content,
|
||||||
.result-content {
|
.result-content {
|
||||||
min-height: calc(100% - 32px);
|
min-height: calc(100% - 32px);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: #f5f5f5;
|
background: hsl(var(--muted));
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #262626;
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -839,32 +839,30 @@ onMounted(() => {
|
||||||
|
|
||||||
.function-sidebar {
|
.function-sidebar {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
background: #fafafa;
|
border: 1px solid hsl(var(--border));
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #262626;
|
background: hsl(var(--muted));
|
||||||
background: #f0f0f0;
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
border-bottom: 1px solid #d9d9d9;
|
|
||||||
border-radius: 6px 6px 0 0;
|
border-radius: 6px 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item {
|
.sidebar-item {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #e6f7ff;
|
background: hsl(var(--muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: white;
|
color: white;
|
||||||
background: #1890ff;
|
background: hsl(var(--primary));
|
||||||
|
|
||||||
.function-id {
|
.function-id {
|
||||||
color: rgb(255 255 255 / 80%);
|
color: rgb(255 255 255 / 80%);
|
||||||
|
@ -895,11 +893,11 @@ onMounted(() => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #8c8c8c;
|
color: hsl(var(--muted-foreground));
|
||||||
|
|
||||||
.info-icon {
|
.info-icon {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #1890ff;
|
color: hsl(var(--primary));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -911,14 +909,14 @@ onMounted(() => {
|
||||||
.input-section {
|
.input-section {
|
||||||
flex: 5;
|
flex: 5;
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
.simple-form {
|
.simple-form {
|
||||||
:deep(.ant-table) {
|
:deep(.ant-table) {
|
||||||
.ant-table-thead > tr > th {
|
.ant-table-thead > tr > th {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background: #fafafa;
|
background: hsl(var(--muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-tbody > tr > td {
|
.ant-table-tbody > tr > td {
|
||||||
|
@ -953,40 +951,31 @@ onMounted(() => {
|
||||||
.parameter-box,
|
.parameter-box,
|
||||||
.result-box {
|
.result-box {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
.parameter-header,
|
.parameter-header,
|
||||||
.result-header {
|
.result-header {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameter-content,
|
.parameter-content,
|
||||||
.result-content {
|
.result-content {
|
||||||
min-height: calc(100% - 32px);
|
min-height: calc(100% - 32px);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: #f5f5f5;
|
background: hsl(var(--muted));
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #262626;
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-box {
|
|
||||||
.result-content {
|
|
||||||
background: #f0f9ff;
|
|
||||||
border: 1px solid #91d5ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -327,7 +327,7 @@ onMounted(() => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #8c8c8c;
|
color: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,14 +337,14 @@ onMounted(() => {
|
||||||
|
|
||||||
.input-section {
|
.input-section {
|
||||||
flex: 5;
|
flex: 5;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
.simple-form {
|
.simple-form {
|
||||||
:deep(.ant-table) {
|
:deep(.ant-table) {
|
||||||
.ant-table-thead > tr > th {
|
.ant-table-thead > tr > th {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background: #fafafa;
|
background: hsl(var(--muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-tbody > tr > td {
|
.ant-table-tbody > tr > td {
|
||||||
|
@ -373,28 +373,26 @@ onMounted(() => {
|
||||||
.parameter-box,
|
.parameter-box,
|
||||||
.result-box {
|
.result-box {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
.parameter-header,
|
.parameter-header,
|
||||||
.result-header {
|
.result-header {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameter-content,
|
.parameter-content,
|
||||||
.result-content {
|
.result-content {
|
||||||
min-height: 120px;
|
min-height: 120px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: #f5f5f5;
|
background: hsl(var(--muted));
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #262626;
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -712,32 +712,30 @@ onMounted(() => {
|
||||||
|
|
||||||
.property-sidebar {
|
.property-sidebar {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
background: #fafafa;
|
border: 1px solid hsl(var(--border));
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #262626;
|
background: hsl(var(--muted));
|
||||||
background: #f0f0f0;
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
border-bottom: 1px solid #d9d9d9;
|
|
||||||
border-radius: 6px 6px 0 0;
|
border-radius: 6px 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item {
|
.sidebar-item {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #e6f7ff;
|
background: hsl(var(--muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: white;
|
color: white;
|
||||||
background: #1890ff;
|
background: hsl(var(--primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
.function-name {
|
.function-name {
|
||||||
|
@ -763,7 +761,6 @@ onMounted(() => {
|
||||||
|
|
||||||
.control-label {
|
.control-label {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #262626;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -778,11 +775,11 @@ onMounted(() => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #8c8c8c;
|
color: hsl(var(--muted-foreground));
|
||||||
|
|
||||||
.info-icon {
|
.info-icon {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #1890ff;
|
color: hsl(var(--primary));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -794,14 +791,14 @@ onMounted(() => {
|
||||||
.input-section {
|
.input-section {
|
||||||
flex: 5;
|
flex: 5;
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
.simple-form {
|
.simple-form {
|
||||||
:deep(.ant-table) {
|
:deep(.ant-table) {
|
||||||
.ant-table-thead > tr > th {
|
.ant-table-thead > tr > th {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background: #fafafa;
|
background: hsl(var(--muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-tbody > tr > td {
|
.ant-table-tbody > tr > td {
|
||||||
|
@ -821,7 +818,6 @@ onMounted(() => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
color: #8c8c8c;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
@ -849,26 +845,24 @@ onMounted(() => {
|
||||||
.parameter-box {
|
.parameter-box {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
.parameter-header {
|
.parameter-header {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameter-content {
|
.parameter-content {
|
||||||
min-height: calc(100% - 32px);
|
min-height: calc(100% - 32px);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: #f5f5f5;
|
background: hsl(var(--muted));
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #262626;
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, onMounted, onUnmounted } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
import { ArrowLeftOutlined } from '@ant-design/icons-vue';
|
||||||
import { Image, message, Switch, TabPane, Tabs } from 'ant-design-vue';
|
import { Image, message, QRCode, Switch, TabPane, Tabs } from 'ant-design-vue';
|
||||||
import QrcodeVue from 'qrcode.vue';
|
|
||||||
|
|
||||||
import { deviceStateOptions } from '#/constants/dicts';
|
import { deviceStateOptions } from '#/constants/dicts';
|
||||||
// import { deviceUpdateStatus } from '#/api/device/device';
|
// import { deviceUpdateStatus } from '#/api/device/device';
|
||||||
|
@ -33,6 +32,8 @@ let deviceStatusSubscription: any = null;
|
||||||
const subscribeDeviceStatus = () => {
|
const subscribeDeviceStatus = () => {
|
||||||
if (!currentDevice.value) return;
|
if (!currentDevice.value) return;
|
||||||
|
|
||||||
|
if (deviceStatusSubscription) return;
|
||||||
|
|
||||||
const productKey = currentDevice.value.productObj.productKey;
|
const productKey = currentDevice.value.productObj.productKey;
|
||||||
|
|
||||||
const id = `device-status-${currentDevice.value.deviceKey}`;
|
const id = `device-status-${currentDevice.value.deviceKey}`;
|
||||||
|
@ -41,9 +42,12 @@ const subscribeDeviceStatus = () => {
|
||||||
deviceStatusSubscription = getWebSocket(id, topic, {
|
deviceStatusSubscription = getWebSocket(id, topic, {
|
||||||
deviceKey: currentDevice.value.deviceKey,
|
deviceKey: currentDevice.value.deviceKey,
|
||||||
}).subscribe((data: any) => {
|
}).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>
|
<template>
|
||||||
<Page :auto-content-height="true">
|
<Page :auto-content-height="true">
|
||||||
<div class="device-detail">
|
<div class="device-detail bg-background">
|
||||||
<!-- 页面头部 -->
|
<!-- 页面头部 -->
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
|
@ -155,15 +159,12 @@ onUnmounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<QrcodeVue
|
<QRCode
|
||||||
:value="currentDevice.deviceKey"
|
:value="currentDevice.deviceKey"
|
||||||
:image-settings="{
|
:size="100"
|
||||||
src: currentDevice.imgUrl,
|
:icon-size="20"
|
||||||
height: 20,
|
error-level="H"
|
||||||
width: 20,
|
:icon="currentDevice.imgUrl"
|
||||||
}"
|
|
||||||
:size="80"
|
|
||||||
level="H"
|
|
||||||
/>
|
/>
|
||||||
<!-- <a-button
|
<!-- <a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
@ -234,7 +235,6 @@ onUnmounted(() => {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.device-detail {
|
.device-detail {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
.detail-header {
|
.detail-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -261,7 +261,6 @@ onUnmounted(() => {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-status {
|
.device-status {
|
||||||
|
@ -271,7 +270,7 @@ onUnmounted(() => {
|
||||||
|
|
||||||
.status-label {
|
.status-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #8c8c8c;
|
color: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
|
@ -284,21 +283,21 @@ onUnmounted(() => {
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
color: #faad14;
|
color: hsl(var(--warning));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.success {
|
&.success {
|
||||||
color: #52c41a;
|
color: hsl(var(--success));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
color: #ff4d4f;
|
color: hsl(var(--secondary));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-text {
|
.status-text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #262626;
|
color: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-switch {
|
.status-switch {
|
||||||
|
@ -314,8 +313,6 @@ onUnmounted(() => {
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -332,16 +329,15 @@ onUnmounted(() => {
|
||||||
|
|
||||||
.basic-label {
|
.basic-label {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #8c8c8c;
|
color: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
.basic-value {
|
.basic-value {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #262626;
|
|
||||||
|
|
||||||
&.product-link {
|
&.product-link {
|
||||||
color: #1890ff;
|
color: hsl(var(--primary));
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
@ -99,6 +99,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
||||||
label: '产品KEY',
|
label: '产品KEY',
|
||||||
fieldName: 'productKey',
|
fieldName: 'productKey',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
|
rules: 'required',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '产品名称',
|
label: '产品名称',
|
||||||
|
@ -139,8 +140,9 @@ export const drawerSchema: FormSchemaGetter = () => [
|
||||||
h(
|
h(
|
||||||
'span',
|
'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;',
|
style:
|
||||||
|
'max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
||||||
},
|
},
|
||||||
option.tooltip,
|
option.tooltip,
|
||||||
),
|
),
|
||||||
|
|
|
@ -41,7 +41,8 @@ const pagination = reactive({
|
||||||
total: 0,
|
total: 0,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
showQuickJumper: 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',
|
value: 'child-device',
|
||||||
channel: 'child-device',
|
channel: 'child-device',
|
||||||
transport: 'Gateway',
|
transport: 'Gateway',
|
||||||
description: '需要通过网关与平台进行数据通信的设备,将作为网关子设备接入到平台。',
|
description:
|
||||||
|
'需要通过网关与平台进行数据通信的设备,将作为网关子设备接入到平台。',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -230,7 +232,10 @@ onMounted(() => {
|
||||||
{{ getProviderText(gateway.provider) }}
|
{{ getProviderText(gateway.provider) }}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<Tag class="gateway-status" :color="getStatusColor(gateway.enabled)">
|
<Tag
|
||||||
|
class="gateway-status"
|
||||||
|
:color="getStatusColor(gateway.enabled)"
|
||||||
|
>
|
||||||
{{ getStatusText(gateway.enabled) }}
|
{{ getStatusText(gateway.enabled) }}
|
||||||
</Tag>
|
</Tag>
|
||||||
<div class="gateway-footer">
|
<div class="gateway-footer">
|
||||||
|
@ -309,18 +314,17 @@ onMounted(() => {
|
||||||
.gateway-card {
|
.gateway-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid #e4e4e7;
|
border: 1px solid hsl(var(--border));
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: #1890ff;
|
border-color: hsl(var(--primary));
|
||||||
box-shadow: 0 4px 12px rgb(24 144 255 / 15%);
|
box-shadow: hsl(var(--accent-hover));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background: #f0f8ff;
|
border-color: hsl(var(--primary));
|
||||||
border-color: #1890ff;
|
box-shadow: hsl(var(--accent-hover));
|
||||||
box-shadow: 0 4px 12px rgb(24 144 255 / 20%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gateway-main {
|
.gateway-main {
|
||||||
|
@ -343,7 +347,7 @@ onMounted(() => {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: white;
|
||||||
background: #1890ff;
|
background: hsl(var(--primary));
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -367,7 +371,6 @@ onMounted(() => {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -392,12 +395,11 @@ onMounted(() => {
|
||||||
|
|
||||||
.gateway-footer-item-label {
|
.gateway-footer-item-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #3d3d3d;
|
color: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
.gateway-footer-item-value {
|
.gateway-footer-item-value {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #000;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -416,7 +418,7 @@ onMounted(() => {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: white;
|
||||||
background: #1890ff;
|
background: hsl(var(--primary));
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -436,9 +438,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-actions {
|
.footer-actions {
|
||||||
padding-top: 16px;
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { computed } from 'vue';
|
||||||
import { useVbenDrawer } from '@vben/common-ui';
|
import { useVbenDrawer } from '@vben/common-ui';
|
||||||
|
|
||||||
import { EditOutlined } from '@ant-design/icons-vue';
|
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 dayjs from 'dayjs';
|
||||||
|
|
||||||
import { deviceTypeOptions } from '#/constants/dicts';
|
import { deviceTypeOptions } from '#/constants/dicts';
|
||||||
|
@ -152,7 +152,10 @@ const productParams = computed(() =>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="productParams.length === 0" class="empty-params">
|
<div v-if="productParams.length === 0" class="empty-params">
|
||||||
<p>暂无产品参数</p>
|
<Empty
|
||||||
|
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||||
|
description="暂无产品参数"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Descriptions bordered :column="3" v-else>
|
<Descriptions bordered :column="3" v-else>
|
||||||
|
@ -187,7 +190,6 @@ const productParams = computed(() =>
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
|
@ -225,45 +227,15 @@ const productParams = computed(() =>
|
||||||
font-size: 14px;
|
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) {
|
:deep(.ant-descriptions-item-label) {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #595959;
|
//color: #595959;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ant-descriptions-item-content) {
|
:deep(.ant-descriptions-item-content) {
|
||||||
color: #262626;
|
//color: #262626;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -69,7 +69,8 @@ const accessConfigs = ref([]);
|
||||||
// 表格列配置
|
// 表格列配置
|
||||||
const tableColumns = computed(() => {
|
const tableColumns = computed(() => {
|
||||||
const isMQTT =
|
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
|
return isMQTT
|
||||||
? [
|
? [
|
||||||
|
@ -265,9 +266,18 @@ onMounted(() => {
|
||||||
<h4>连接信息</h4>
|
<h4>连接信息</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-content">
|
<div class="section-content">
|
||||||
<div v-if="accessInfo.addresses && accessInfo.addresses.length > 0">
|
<div
|
||||||
<div v-for="item in accessInfo.addresses" :key="item.address" class="address-item">
|
v-if="accessInfo.addresses && accessInfo.addresses.length > 0"
|
||||||
<Badge :color="item.health === -1 ? 'red' : 'green'" :text="item.address" />
|
>
|
||||||
|
<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>
|
</div>
|
||||||
<div v-else class="no-address">暂无连接信息</div>
|
<div v-else class="no-address">暂无连接信息</div>
|
||||||
|
@ -297,7 +307,11 @@ onMounted(() => {
|
||||||
</div> -->
|
</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">
|
<div class="section-header">
|
||||||
<h4>{{ config.name }}</h4>
|
<h4>{{ config.name }}</h4>
|
||||||
<Tooltip title="此配置来自于产品接入网关所选择的协议">
|
<Tooltip title="此配置来自于产品接入网关所选择的协议">
|
||||||
|
@ -329,13 +343,19 @@ onMounted(() => {
|
||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
/>
|
/>
|
||||||
<Select
|
<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]"
|
v-model:value="formData[item.property]"
|
||||||
placeholder="请选择"
|
placeholder="请选择"
|
||||||
:options="getOptions(item)"
|
:options="getOptions(item)"
|
||||||
/>
|
/>
|
||||||
<InputNumber
|
<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]"
|
v-model:value="formData[item.property]"
|
||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
/>
|
/>
|
||||||
|
@ -348,7 +368,9 @@ onMounted(() => {
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h4>存储策略</h4>
|
<h4>存储策略</h4>
|
||||||
<Tooltip title="若修改存储策略,需要手动做数据迁移,平台只能搜索最新存储策略中的数据">
|
<Tooltip
|
||||||
|
title="若修改存储策略,需要手动做数据迁移,平台只能搜索最新存储策略中的数据"
|
||||||
|
>
|
||||||
<QuestionCircleOutlined />
|
<QuestionCircleOutlined />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -364,18 +386,27 @@ onMounted(() => {
|
||||||
|
|
||||||
<!-- 保存按钮 -->
|
<!-- 保存按钮 -->
|
||||||
<div class="action-buttons">
|
<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>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</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">
|
<div class="info-panel">
|
||||||
<h4>
|
<h4>
|
||||||
{{
|
{{
|
||||||
accessInfo.provider === 'mqtt-server-gateway' || accessInfo.provider === 'mqtt-client-gateway'
|
accessInfo.provider === 'mqtt-server-gateway' ||
|
||||||
|
accessInfo.provider === 'mqtt-client-gateway'
|
||||||
? 'Topic信息'
|
? 'Topic信息'
|
||||||
: 'URL信息'
|
: 'URL信息'
|
||||||
}}
|
}}
|
||||||
|
@ -427,7 +458,11 @@ onMounted(() => {
|
||||||
@cancel="handleAccessModalClose"
|
@cancel="handleAccessModalClose"
|
||||||
:footer="null"
|
:footer="null"
|
||||||
>
|
>
|
||||||
<AccessSelector :product-id="productInfo.id" @select="handleModalSelect" @close="handleAccessModalClose" />
|
<AccessSelector
|
||||||
|
:product-id="productInfo.id"
|
||||||
|
@select="handleModalSelect"
|
||||||
|
@close="handleAccessModalClose"
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -443,7 +478,7 @@ onMounted(() => {
|
||||||
.config-section {
|
.config-section {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
|
@ -456,7 +491,6 @@ onMounted(() => {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -465,12 +499,10 @@ onMounted(() => {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.access-desc {
|
.access-desc {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #8c8c8c;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-item {
|
.address-item {
|
||||||
|
|
|
@ -14,9 +14,9 @@ import {
|
||||||
|
|
||||||
import { productUpdateById } from '#/api/device/product';
|
import { productUpdateById } from '#/api/device/product';
|
||||||
|
|
||||||
import ImportForm from './ImportForm.vue';
|
import ImportForm from './metadata/ImportForm.vue';
|
||||||
import MetadataTable from './MetadataTable.vue';
|
import MetadataTable from './metadata/MetadataTable.vue';
|
||||||
import TSLViewer from './TSLViewer.vue';
|
import TSLViewer from './metadata/TSLViewer.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
productId: string;
|
productId: string;
|
||||||
|
@ -424,6 +424,7 @@ onBeforeRouteLeave((to, from, next) => {
|
||||||
<!-- 导入抽屉 -->
|
<!-- 导入抽屉 -->
|
||||||
<Drawer
|
<Drawer
|
||||||
v-model:open="importVisible"
|
v-model:open="importVisible"
|
||||||
|
v-if="importVisible"
|
||||||
title="快速导入"
|
title="快速导入"
|
||||||
width="600px"
|
width="600px"
|
||||||
@close="handleImportClose"
|
@close="handleImportClose"
|
||||||
|
@ -442,6 +443,7 @@ onBeforeRouteLeave((to, from, next) => {
|
||||||
@close="handleTSLClose"
|
@close="handleTSLClose"
|
||||||
>
|
>
|
||||||
<TSLViewer
|
<TSLViewer
|
||||||
|
v-if="tslVisible"
|
||||||
:product-id="productInfo.id"
|
:product-id="productInfo.id"
|
||||||
:product-info="productInfo"
|
:product-info="productInfo"
|
||||||
:file-name="productInfo.productName"
|
:file-name="productInfo.productName"
|
||||||
|
@ -468,12 +470,11 @@ onBeforeRouteLeave((to, from, next) => {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.desc {
|
.desc {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #8c8c8c;
|
color: hsl(var(--muted-foreground));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -226,7 +226,6 @@ defineExpose({
|
||||||
.param-title {
|
.param-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,12 +244,11 @@ defineExpose({
|
||||||
}
|
}
|
||||||
|
|
||||||
.param-items {
|
.param-items {
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid hsl(var(--border));
|
||||||
|
|
||||||
.param-item {
|
.param-item {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: #fafafa;
|
border: 1px solid hsl(var(--border));
|
||||||
border: 1px solid #f0f0f0;
|
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
|
||||||
.param-row {
|
.param-row {
|
||||||
|
@ -267,7 +265,6 @@ defineExpose({
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #595959;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,15 +279,15 @@ defineExpose({
|
||||||
|
|
||||||
.param-tips {
|
.param-tips {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: #f6ffed;
|
background: hsl(var(--success-foreground));
|
||||||
border: 1px solid #b7eb8f;
|
border: 1px solid hsl(var(--success));
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
.tip-text {
|
.tip-text {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #52c41a;
|
color: hsl(var(--success));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,7 +164,6 @@ const handleCancel = () => {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #262626;
|
|
||||||
|
|
||||||
.header-item {
|
.header-item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -205,7 +204,7 @@ const handleCancel = () => {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-top: 1px dashed #d9d9d9;
|
border-top: 1px dashed hsl(var(--border));
|
||||||
|
|
||||||
.add-button {
|
.add-button {
|
||||||
width: 100%;
|
width: 100%;
|
|
@ -1,6 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref } from 'vue';
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { usePreferences } from '@vben/preferences';
|
||||||
|
|
||||||
import { UploadOutlined } from '@ant-design/icons-vue';
|
import { UploadOutlined } from '@ant-design/icons-vue';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
@ -25,6 +27,8 @@ defineExpose({});
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
const importLoading = ref(false);
|
const importLoading = ref(false);
|
||||||
|
|
||||||
|
const editorTheme = ref('vs');
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
script: `{
|
script: `{
|
||||||
"properties": [],
|
"properties": [],
|
||||||
|
@ -146,9 +150,6 @@ const parseFileContent = (content: string, fileName: string) => {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// const handleJsonChange = (value: string) => {
|
|
||||||
// formData.script = value;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// 导入
|
// 导入
|
||||||
const handleImport = async () => {
|
const handleImport = async () => {
|
||||||
|
@ -194,6 +195,9 @@ const handleImport = async () => {
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
emit('success');
|
emit('success');
|
||||||
};
|
};
|
||||||
|
// 监听主题切换x
|
||||||
|
const { isDark } = usePreferences();
|
||||||
|
editorTheme.value = isDark.value ? 'vs-dark' : 'vs';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -223,8 +227,8 @@ const handleCancel = () => {
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
v-model="formData.script"
|
v-model="formData.script"
|
||||||
lang="json"
|
lang="json"
|
||||||
theme="vs"
|
:theme="editorTheme"
|
||||||
style="height: 400px; border: 1px solid #d9d9d9; border-radius: 6px"
|
style="min-height: 100%"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -248,66 +252,14 @@ const handleCancel = () => {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.import-form {
|
.import-form {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
.upload-tip {
|
.upload-tip {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #8c8c8c;
|
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 {
|
.script-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -316,9 +268,7 @@ const handleCancel = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.script-editor {
|
.script-editor {
|
||||||
overflow: hidden;
|
height: calc(100vh - 300px);
|
||||||
border: 1px solid #d9d9d9;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-actions {
|
.form-actions {
|
|
@ -35,7 +35,8 @@ const tableData = ref<any[]>([]);
|
||||||
const formData = ref<any>(null);
|
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) => {
|
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(() => {
|
const columns = computed(() => {
|
||||||
|
@ -273,7 +275,11 @@ onMounted(() => {
|
||||||
<template>
|
<template>
|
||||||
<div class="metadata-table">
|
<div class="metadata-table">
|
||||||
<div class="table-header">
|
<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>
|
<template #icon>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</template>
|
</template>
|
||||||
|
@ -281,26 +287,52 @@ onMounted(() => {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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 #bodyCell="{ column, record, index }">
|
||||||
<template v-if="column.key === 'action'">
|
<template v-if="column.key === 'action'">
|
||||||
<Space>
|
<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>
|
</Button>
|
||||||
<Popconfirm title="确认删除?" @confirm="handleDelete(record)">
|
<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>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'dataType'">
|
<template v-else-if="column.key === 'dataType'">
|
||||||
<Tag color="processing">
|
<Tag color="processing">
|
||||||
{{ dataTypeOptions.find((item) => item.value === record.valueParams?.dataType)?.label }}
|
{{
|
||||||
|
dataTypeOptions.find(
|
||||||
|
(item) => item.value === record.valueParams?.dataType,
|
||||||
|
)?.label
|
||||||
|
}}
|
||||||
</Tag>
|
</Tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'expands.type'">
|
<template v-else-if="column.key === 'expands.type'">
|
||||||
<Tag color="processing">
|
<Tag color="processing">
|
||||||
{{ readWriteTypeOptions.find((item) => item.value === record.expands.type)?.label }}
|
{{
|
||||||
|
readWriteTypeOptions.find(
|
||||||
|
(item) => item.value === record.expands.type,
|
||||||
|
)?.label
|
||||||
|
}}
|
||||||
</Tag>
|
</Tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'required'">
|
<template v-else-if="column.key === 'required'">
|
|
@ -579,25 +579,4 @@ watch(
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style></style>
|
||||||
.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>
|
|
|
@ -140,7 +140,9 @@ const handleDeleteProperty = (propertyId: string) => {
|
||||||
// 属性选择确认
|
// 属性选择确认
|
||||||
const handlePropertySelectionConfirm = (selectedProperties: any[]) => {
|
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) {
|
if (newProperties.length > 0) {
|
||||||
formData.value.properties.push(...newProperties);
|
formData.value.properties.push(...newProperties);
|
||||||
|
@ -176,7 +178,12 @@ const handleClose = () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical">
|
||||||
<Row :gutter="16">
|
<Row :gutter="16">
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
|
@ -202,12 +209,23 @@ const handleClose = () => {
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'action'">
|
<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>
|
||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
<div class="add-button-container">
|
<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>
|
<template #icon>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
</template>
|
</template>
|
||||||
|
@ -218,12 +236,21 @@ const handleClose = () => {
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
<FormItem label="排序" name="sort">
|
<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>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="24">
|
<Col :span="24">
|
||||||
<FormItem label="描述" name="description">
|
<FormItem label="描述" name="description">
|
||||||
<Textarea v-model:value="formData.description" placeholder="请输入" :rows="3" />
|
<Textarea
|
||||||
|
v-model:value="formData.description"
|
||||||
|
placeholder="请输入"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -231,7 +258,9 @@ const handleClose = () => {
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Space>
|
<Space>
|
||||||
<Button @click="handleClose">取消</Button>
|
<Button @click="handleClose">取消</Button>
|
||||||
<Button type="primary" :loading="saveLoading" @click="handleSave"> 确认 </Button>
|
<Button type="primary" :loading="saveLoading" @click="handleSave">
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -172,6 +172,7 @@ const getReadWriteTypeLabel = (type: string) => {
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
size="small"
|
size="small"
|
||||||
|
:scroll="{ y: 500 }"
|
||||||
:row-selection="{
|
:row-selection="{
|
||||||
selectedRowKeys: selectedPropertyIds,
|
selectedRowKeys: selectedPropertyIds,
|
||||||
onChange: (selectedRowKeys: string[]) => {
|
onChange: (selectedRowKeys: string[]) => {
|
||||||
|
@ -218,14 +219,12 @@ const getReadWriteTypeLabel = (type: string) => {
|
||||||
.search-section {
|
.search-section {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
background-color: #fafafa;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-info {
|
.pagination-info {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #666;
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,7 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, watch } from 'vue';
|
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 { message, Space } from 'ant-design-vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
@ -14,6 +20,8 @@ const props = defineProps<{
|
||||||
productInfo: object;
|
productInfo: object;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const editorTheme = ref('vs');
|
||||||
|
|
||||||
const mockData = {
|
const mockData = {
|
||||||
properties: [],
|
properties: [],
|
||||||
functions: [],
|
functions: [],
|
||||||
|
@ -117,6 +125,9 @@ watch(
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadTSLData();
|
loadTSLData();
|
||||||
});
|
});
|
||||||
|
// 监听主题切换x
|
||||||
|
const { isDark } = usePreferences();
|
||||||
|
editorTheme.value = isDark.value ? 'vs-dark' : 'vs';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -151,7 +162,12 @@ onMounted(() => {
|
||||||
|
|
||||||
<div class="viewer-content">
|
<div class="viewer-content">
|
||||||
<div class="viewer-toolbar">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -171,8 +187,8 @@ onMounted(() => {
|
||||||
.viewer-content-tips {
|
.viewer-content-tips {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
color: #8c8c8c;
|
color: hsl(var(--muted-foreground));
|
||||||
background: #f5f5f5;
|
background: hsl(var(--muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
.viewer-content {
|
.viewer-content {
|
|
@ -106,7 +106,7 @@ onUnmounted(() => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page :auto-content-height="true">
|
<Page :auto-content-height="true">
|
||||||
<div class="product-detail">
|
<div class="product-detail bg-background">
|
||||||
<!-- 页面头部 -->
|
<!-- 页面头部 -->
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
|
@ -185,7 +185,6 @@ onUnmounted(() => {
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.product-detail {
|
.product-detail {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: #fff;
|
|
||||||
|
|
||||||
.detail-header {
|
.detail-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -211,7 +210,6 @@ onUnmounted(() => {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #262626;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-status {
|
.product-status {
|
||||||
|
|
|
@ -204,7 +204,7 @@ export const drawerSchema: FormSchemaGetter = () => [
|
||||||
h(
|
h(
|
||||||
'span',
|
'span',
|
||||||
{
|
{
|
||||||
class: 'text-[14px] text-black/25 truncate',
|
class: 'text-[14px] text-muted-foreground truncate',
|
||||||
style:
|
style:
|
||||||
'max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
'max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue