Compare commits

..

45 Commits

Author SHA1 Message Date
Rice 9d20c79709 Merge branch 'origin/绿色触摸屏' into 触摸屏-直流
3 months ago
wangqiyang 849f860ddc 固定实时监控十路显示
3 months ago
Rice a1bf5cedf6 Merge branch 'origin/绿色触摸屏' into 触摸屏-直流
3 months ago
wangqiyang 4a14221648 优化小屏监控布局
3 months ago
Rice 8ea9c5c489 fix: 修复字体文件路径错误
3 months ago
Rice 5486127839 Merge branch 'origin/绿色触摸屏' into 触摸屏-直流
3 months ago
wangqiyang 4e0405808c 支持故障展开隐藏显示
3 months ago
wangqiyang e455bb5a60 精简实时监控故障显示
3 months ago
Rice d57c077a1c Merge branch 'origin/绿色触摸屏' into 触摸屏-直流
4 months ago
wangqiyang f9e6ddf832 优化实时监控故障布局
4 months ago
Rice 3eb1421cac Merge branch 'origin/绿色触摸屏' into 触摸屏-直流
4 months ago
wangqiyang 34d28b188b Merge branch '绿色触摸屏' of https://gitee.com/lufaneng/central-control into 绿色触摸屏
4 months ago
wangqiyang 26df8428fb 优化触摸屏监控与报警分页
4 months ago
Rice 2f052d319e Merge branch 'origin/绿色触摸屏' into 触摸屏-直流
4 months ago
Rice 15fbd4f429 fix: 修复字体文件路径错误
4 months ago
wangqiyang 74672d31b5 实现扫描数量前端配置
4 months ago
wangqiyang ba04609631 实现当前故障前端展示
4 months ago
Lufaneng 92a178a3a3 Merge branch '绿色触摸屏' into 触摸屏-直流
6 months ago
Lufaneng c249539205 电表型号修复
6 months ago
Lufaneng c652469c37 Merge branch '绿色触摸屏' into 触摸屏-直流
6 months ago
Lufaneng 3d2a70a829 增加选择电表型号
6 months ago
Lufaneng 3a7fff040d Merge branch '绿色触摸屏' into 触摸屏-直流
6 months ago
Lufaneng 09794cf463 重新加载修改
6 months ago
Lufaneng c2f75881be 更改名称
7 months ago
Lufaneng c703fa3730 增加是否开启密码功能
7 months ago
Lufaneng dc64f9528b 1
7 months ago
Lufaneng b5686ced94 优化
7 months ago
Lufaneng de483cecf8 增加密码
7 months ago
Lufaneng b75fecefce 更改刷新时间
1 year ago
Lufaneng 2a3aec3e82 背景更换
1 year ago
Lufaneng 5f0db86dc3 字体修改
1 year ago
Lufaneng 5a8b6085e6 1
1 year ago
Lufaneng 870b6a99c1 1
1 year ago
Lufaneng c708b6e337 优化
1 year ago
Lufaneng f0c4e647b2 1
1 year ago
Lufaneng 10938c0502 1
1 year ago
Lufaneng 3081722f4d 1
1 year ago
Lufaneng 3d5d523da7 1
1 year ago
Lufaneng 1986634e17 改sessionStorage
1 year ago
Lufaneng 8378155ac8 1
1 year ago
Lufaneng 06a8d5e425 Merge branch '触摸屏' into 绿色触摸屏
1 year ago
Lufaneng 3533e7da33 根路径修改
1 year ago
Lufaneng 2f3246b444 键盘暂存1
1 year ago
Lufaneng 719c4a786c 键盘暂存
1 year ago
Lufaneng d023bea163 暂存
1 year ago

1
.gitignore vendored

@ -10,6 +10,7 @@ lerna-debug.log*
node_modules node_modules
.DS_Store .DS_Store
dist dist
dist.zip
dist-ssr dist-ssr
coverage coverage
*.local *.local

14
package-lock.json generated

@ -13,6 +13,8 @@
"element-plus": "^2.9.4", "element-plus": "^2.9.4",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.3.1", "pinia": "^2.3.1",
"simple-keyboard": "^3.8.69",
"simple-keyboard-layouts": "^3.4.109",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-echarts": "^7.0.3", "vue-echarts": "^7.0.3",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
@ -4840,6 +4842,18 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/simple-keyboard": {
"version": "3.8.69",
"resolved": "https://registry.npmmirror.com/simple-keyboard/-/simple-keyboard-3.8.69.tgz",
"integrity": "sha512-H/aCx57Tu8xwOTkMrELdJ0mV0s3p1i/AjzKAb+M1TnHCoFyfLumn2SC9pekIOeoB51QENOQycjF/R8SFranwLw==",
"license": "MIT"
},
"node_modules/simple-keyboard-layouts": {
"version": "3.4.109",
"resolved": "https://registry.npmmirror.com/simple-keyboard-layouts/-/simple-keyboard-layouts-3.4.109.tgz",
"integrity": "sha512-eKiKbWmW7P6WO52WrXhAK2uaGPDTibIrOzFCybS8mL6t4JTU49brHbatizo7coHRsbwCyR4W01dzbDRgvtmZWg==",
"license": "MIT"
},
"node_modules/sirv": { "node_modules/sirv": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmmirror.com/sirv/-/sirv-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/sirv/-/sirv-3.0.0.tgz",

@ -16,6 +16,8 @@
"element-plus": "^2.9.4", "element-plus": "^2.9.4",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pinia": "^2.3.1", "pinia": "^2.3.1",
"simple-keyboard": "^3.8.69",
"simple-keyboard-layouts": "^3.4.109",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-echarts": "^7.0.3", "vue-echarts": "^7.0.3",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"

@ -6,25 +6,34 @@
</keep-alive> </keep-alive>
<component :is="Component" :key="$route.name" v-if="!route.meta.keepAlive" /> <component :is="Component" :key="$route.name" v-if="!route.meta.keepAlive" />
</router-view> </router-view>
<!-- 全局密码验证弹框 -->
<PasswordDialog ref="passwordDialogRef" />
</el-config-provider> </el-config-provider>
</template> </template>
<script setup> <script setup>
import zhCn from 'element-plus/es/locale/lang/zh-cn' import zhCn from 'element-plus/es/locale/lang/zh-cn'
import RPC from '@/utils/rpc' import PasswordDialog from '@/components/PasswordDialog.vue'
const locale = zhCn
const locale = zhCn
const route = useRoute() const route = useRoute()
const passwordDialogRef = ref(null)
// 显示密码弹框
const showPasswordDialog = () => {
passwordDialogRef.value?.open()
}
onMounted(() => { onMounted(() => {
console.log(import.meta.env.VITE_APP_ENV) console.log(import.meta.env.VITE_APP_ENV)
RPC.post('/sys/basic/infoJson').then(res => {
console.log(res); // 仅监听 401 触发的 session 过期事件,才弹出密码框
const arr = res || [] window.addEventListener('m9z:session-expired', showPasswordDialog)
localStorage.setItem('SysBasicTgkTitle', arr?.find(item => item.conf_code === 'SysBasicTgkTitle')?.conf_value || '')
})
}) })
onUnmounted(() => {
window.removeEventListener('m9z:session-expired', showPasswordDialog)
})
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

@ -7,6 +7,8 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
BaseKeyboardInput: typeof import('./components/BaseKeyboardInput.vue')['default']
BasePasswordInput: typeof import('./components/BasePasswordInput.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
@ -17,16 +19,21 @@ declare module 'vue' {
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination'] ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSlider: typeof import('element-plus/es')['ElSlider'] ElSlider: typeof import('element-plus/es')['ElSlider']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker'] ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
Linechart: typeof import('./components/Linechart.vue')['default'] Linechart: typeof import('./components/Linechart.vue')['default']
Pagination: typeof import('./components/Pagination/index.vue')['default'] Pagination: typeof import('./components/Pagination/index.vue')['default']
PasswordDialog: typeof import('./components/PasswordDialog.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
} }

@ -0,0 +1,251 @@
<template>
<div ref="keyboardWrapRef" class="keyboard-wrap" @click="focusInput">
<el-input ref="inputRef" v-model="model" @keyup.enter="handleEnter" :disabled="disabled" v-bind="$attrs">
<template v-for="(item, index) in $slots" :key="index" #[index]>
<slot :name="index"></slot>
</template>
</el-input>
</div>
<el-popover v-if="visible" :visible="true" :virtual-ref="inputRef" virtual-triggering placement="bottom" :width="width" :show-arrow="false" :hide-after="0" popper-style="padding: 0px;color:#000" popper-class="keyboard-popper" @after-enter="keyboardInit">
<div class="simple-keyboard"></div>
</el-popover>
</template>
<script setup lang="ts">
import Keyboard from 'simple-keyboard'
import 'simple-keyboard/build/css/index.css'
import layout from 'simple-keyboard-layouts/build/layouts/chinese.js'
defineOptions({
inheritAttrs: false
})
const model = defineModel<string>()
const emits = defineEmits(['onChange', 'enter', 'close', 'focus'])
const props = defineProps({
layoutName: {
type: String,
default: 'default'
},
// 保留几位小数 layoutName为number时生效
precision: {
type: Number,
default: 10
},
// 获取焦点打开键盘
isOpen: {
type: Boolean,
default: true
},
disabled: Boolean
})
const keyboard = ref<any>(null)
const keyboardWrapRef = ref(null)
const visible = ref(false)
const inputRef = ref()
const width = ref(1000)
if (props.layoutName == 'number') width.value = 300
const displayDefault = ref({
'{bksp}': 'backspace',
'{lock}': 'caps',
'{enter}': 'enter',
'{tab}': 'tab',
'{shift}': 'shift',
'{change}': 'en',
'{space}': 'space',
'{clear}': 'clear',
'{close}': 'close',
'{arrowleft}': '←',
'{arrowright}': '→'
})
const open = () => {
if (visible.value) return
inputRef.value.focus()
emits('focus')
visible.value = true
}
const focusInput = () => {
if (props.disabled) return
if (visible.value) return
emits('focus')
if (props.isOpen) visible.value = true
}
const lang: any = {
'zh-cn': {
// 默认布局
default: [
'` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
'{tab} q w e r t y u i o p [ ] \\',
"{lock} a s d f g h j k l ; ' {enter}",
'{change} z x c v b n m , . / {clear}',
'{arrowleft} {arrowright} {space} {close}'
],
// 大小写
shift: [
'~ ! @ # $ % ^ & * ( ) _ + {bksp}',
'{tab} Q W E R T Y U I O P { } |',
'{lock} A S D F G H J K L : " {enter}',
'{change} Z X C V B N M < > ? {clear}',
'{arrowleft} {arrowright} {space} {close}'
]
},
th: {
default: [
'\u005F \u0E45 \u002F \u002D \u0E20 \u0E16 \u0E38 \u0E36 \u0E04 \u0E15 \u0E08 \u0E02 \u0E0A {bksp}',
'{tab} \u0E46 \u0E44 \u0E33 \u0E1E \u0E30 \u0E31 \u0E35 \u0E23 \u0E19 \u0E22 \u0E1A \u0E25 \u0E03',
'{lock} \u0E1F \u0E2B \u0E01 \u0E14 \u0E40 \u0E49 \u0E48 \u0E32 \u0E2A \u0E27 \u0E07 {enter}',
'{shift} \u0E1C \u0E1B \u0E41 \u0E2D \u0E34 \u0E37 \u0E17 \u0E21 \u0E43 \u0E1D {clear}',
'{arrowleft} {arrowright} {space} {close}'
],
shift: [
'% + \u0E51 \u0E52 \u0E53 \u0E54 \u0E39 \u0E3F \u0E55 \u0E56 \u0E57 \u0E58 \u0E59 {bksp}',
'{tab} \u0E50 \u0022 \u0E0E \u0E11 \u0E18 \u0E4D \u0E4A \u0E13 \u0E2F \u0E0D \u0E10 \u002C \u0E05',
'{lock} \u0E24 \u0E06 \u0E0F \u0E42 \u0E0C \u0E47 \u0E4B \u0E29 \u0E28 \u0E0B \u002E {enter}',
'{shift} ( ) \u0E09 \u0E2E \u0E3A \u0E4C \u003F \u0E12 \u0E2C \u0E26 {clear}',
'{arrowleft} {arrowright} {space} {close}'
]
}
}
const keyboardInit = () => {
let keyboardLayout = {
// 数字布局
number: ['7 8 9', '4 5 6', '1 2 3', '. 0 {bksp}', '{arrowleft} {arrowright} {clear} {close}'],
...lang['zh-cn']
}
// if (lang[useSession('lang').value]) {
// keyboardLayout = { ...lang[useSession('lang').value], ...keyboardLayout }
// }
keyboard.value = new Keyboard('simple-keyboard', {
onChange: onChange,
onKeyPress: onKeyPress,
onInit: onInit,
layout: keyboardLayout,
layoutName: props.layoutName,
display: displayDefault.value
// theme: 'hg-theme-default init-keyboard' // 添加自定义class处理清空逻辑
})
}
const onInit = (keyboard: any) => {
keyboard.setInput(String(model.value || ''))
keyboard.setCaretPosition(inputRef.value?.ref.selectionEnd)
document.addEventListener('click', handlePopClose)
}
const onChange = (input: any) => {
model.value = input
emits('onChange', input)
}
const onKeyPress = (button: any) => {
if (button === '{lock}') return handleLock()
if (button === '{change}') return handleChange()
if (button === '{clear}') return handleClear()
if (button === '{enter}') return handleEnter()
if (button === '{close}') return close()
if (button === '{arrowleft}') return handleArrow(0)
if (button === '{arrowright}') return handleArrow(1)
}
const handleLock = () => {
let currentLayout = keyboard.value.options.layoutName
let shiftToggle = currentLayout === 'default' ? 'shift' : 'default'
keyboard.value.setOptions({
layoutName: shiftToggle
})
}
const handleChange = () => {
let layoutCandidates = keyboard.value.options.layoutCandidates
// 切换中英文输入法
if (layoutCandidates != null && layoutCandidates != undefined) {
displayDefault.value['{change}'] = 'en'
keyboard.value.setOptions({
layoutName: 'default',
layoutCandidates: null,
display: displayDefault.value
})
} else {
displayDefault.value['{change}'] = 'cn'
keyboard.value.setOptions({
layoutName: 'default',
layoutCandidates: (layout as any)?.layoutCandidates,
display: displayDefault.value
})
}
}
const handleClear = () => {
keyboard.value.clearInput()
model.value = ''
}
const handleEnter = () => {
emits('enter')
}
const close = () => {
if (props.layoutName == 'number') {
// 处理精度
model.value = model.value?.replace(new RegExp(`(\\d+)\\.(\\d{${props.precision}}).*$`), '$1.$2').replace(/\.$/, '')
}
document.removeEventListener('click', handlePopClose)
visible.value = false
emits('close')
}
const handleArrow = (num: number) => {
// 处理左右箭头下标位置
const index = keyboard.value.getCaretPositionEnd()
if (num == 0 && index - 1 >= 0) {
keyboard.value.setCaretPosition(index - 1)
} else if (num == 1 && index + 1 <= (model.value?.length || 0)) {
keyboard.value.setCaretPosition(index + 1)
}
}
const handlePopClose = (e: any) => {
// 虚拟键盘区域 输入框区域 中文选项区域
if (
(e.target as Element).closest('.keyboard-popper') ||
(e.target as Element).closest('.keyboard-wrap') == keyboardWrapRef.value ||
/hg-candidate-box/.test(e.target.className)
) {
const index = keyboard.value.getCaretPositionEnd()
inputRef.value.ref.selectionStart = inputRef.value.ref.selectionEnd = index
inputRef.value.focus()
} else {
close()
}
}
onUnmounted(() => {
// 某些情况下未触发动画关闭时销毁事件。此处销毁做后备处理
document.removeEventListener('click', handlePopClose)
})
defineExpose({ inputRef, visible, open, close, keyboardInit })
</script>
<style scoped>
.keyboard-wrap {
width: 100%;
}
</style>
<style>
.hg-theme-default .hg-button.hg-button-arrowleft,
.hg-theme-default .hg-button.hg-button-arrowright {
max-width: 70px;
}
.hg-theme-default .hg-button.hg-button-close {
max-width: 100px;
}
.hg-layout-number .hg-button.hg-button-close {
max-width: none;
}
.hg-layout-number .hg-button.hg-button-bksp {
max-width: 92px;
}
</style>

@ -0,0 +1,256 @@
<template>
<div ref="keyboardWrapRef" class="keyboard-wrap" @click="focusInput">
<el-input ref="inputRef" v-model="model" @keyup.enter="handleEnter" :disabled="disabled" v-bind="$attrs" type="text">
<template v-for="(item, index) in $slots" :key="index" #[index]>
<slot :name="index"></slot>
</template>
</el-input>
</div>
<el-popover v-if="visible" :visible="true" :virtual-ref="inputRef" virtual-triggering placement="bottom" :width="width" :show-arrow="false" :hide-after="0" popper-style="padding: 0px;color:#000" popper-class="keyboard-popper" @after-enter="keyboardInit">
<div class="simple-keyboard"></div>
</el-popover>
</template>
<script setup lang="ts">
import Keyboard from 'simple-keyboard'
import 'simple-keyboard/build/css/index.css'
import layout from 'simple-keyboard-layouts/build/layouts/chinese.js'
defineOptions({
inheritAttrs: false
})
const model = defineModel<string>()
const emits = defineEmits(['onChange', 'enter', 'close', 'focus'])
const props = defineProps({
layoutName: {
type: String,
default: 'default'
},
// 保留几位小数 layoutName为number时生效
precision: {
type: Number,
default: 10
},
// 获取焦点打开键盘
isOpen: {
type: Boolean,
default: true
},
disabled: Boolean
})
const keyboard = ref<any>(null)
const keyboardWrapRef = ref(null)
const visible = ref(false)
const inputRef = ref()
const width = ref(1000)
const inputType = ref('text')
if (props.layoutName == 'number') width.value = 300
const displayDefault = ref({
'{bksp}': 'backspace',
'{lock}': 'caps',
'{enter}': 'enter',
'{tab}': 'tab',
'{shift}': 'shift',
'{change}': 'en',
'{space}': 'space',
'{clear}': 'clear',
'{close}': 'close',
'{arrowleft}': '←',
'{arrowright}': '→'
})
const open = () => {
if (visible.value) return
inputRef.value.focus()
emits('focus')
visible.value = true
}
const focusInput = () => {
if (props.disabled) return
if (visible.value) return
emits('focus')
if (props.isOpen) visible.value = true
}
const lang: any = {
'zh-cn': {
// 默认布局
default: [
'` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
'{tab} q w e r t y u i o p [ ] \\',
"{lock} a s d f g h j k l ; ' {enter}",
'{change} z x c v b n m , . / {clear}',
'{arrowleft} {arrowright} {space} {close}'
],
// 大小写
shift: [
'~ ! @ # $ % ^ & * ( ) _ + {bksp}',
'{tab} Q W E R T Y U I O P { } |',
'{lock} A S D F G H J K L : " {enter}',
'{change} Z X C V B N M < > ? {clear}',
'{arrowleft} {arrowright} {space} {close}'
]
},
th: {
default: [
'\u005F \u0E45 \u002F \u002D \u0E20 \u0E16 \u0E38 \u0E36 \u0E04 \u0E15 \u0E08 \u0E02 \u0E0A {bksp}',
'{tab} \u0E46 \u0E44 \u0E33 \u0E1E \u0E30 \u0E31 \u0E35 \u0E23 \u0E19 \u0E22 \u0E1A \u0E25 \u0E03',
'{lock} \u0E1F \u0E2B \u0E01 \u0E14 \u0E40 \u0E49 \u0E48 \u0E32 \u0E2A \u0E27 \u0E07 {enter}',
'{shift} \u0E1C \u0E1B \u0E41 \u0E2D \u0E34 \u0E37 \u0E17 \u0E21 \u0E43 \u0E1D {clear}',
'{arrowleft} {arrowright} {space} {close}'
],
shift: [
'% + \u0E51 \u0E52 \u0E53 \u0E54 \u0E39 \u0E3F \u0E55 \u0E56 \u0E57 \u0E58 \u0E59 {bksp}',
'{tab} \u0E50 \u0022 \u0E0E \u0E11 \u0E18 \u0E4D \u0E4A \u0E13 \u0E2F \u0E0D \u0E10 \u002C \u0E05',
'{lock} \u0E24 \u0E06 \u0E0F \u0E42 \u0E0C \u0E47 \u0E4B \u0E29 \u0E28 \u0E0B \u002E {enter}',
'{shift} ( ) \u0E09 \u0E2E \u0E3A \u0E4C \u003F \u0E12 \u0E2C \u0E26 {clear}',
'{arrowleft} {arrowright} {space} {close}'
]
}
}
const keyboardInit = () => {
let keyboardLayout = {
// 数字布局
number: ['7 8 9', '4 5 6', '1 2 3', '. 0 {bksp}', '{arrowleft} {arrowright} {clear} {close}'],
...lang['zh-cn']
}
// if (lang[useSession('lang').value]) {
// keyboardLayout = { ...lang[useSession('lang').value], ...keyboardLayout }
// }
keyboard.value = new Keyboard('simple-keyboard', {
onChange: onChange,
onKeyPress: onKeyPress,
onInit: onInit,
layout: keyboardLayout,
layoutName: props.layoutName,
display: displayDefault.value
// theme: 'hg-theme-default init-keyboard' // 添加自定义class处理清空逻辑
})
}
const onInit = (keyboard: any) => {
keyboard.setInput(String(model.value || ''))
keyboard.setCaretPosition(inputRef.value?.ref.selectionEnd)
document.addEventListener('click', handlePopClose)
}
const onChange = (input: any) => {
model.value = input
emits('onChange', input)
}
const onKeyPress = (button: any) => {
if (button === '{lock}') return handleLock()
if (button === '{change}') return handleChange()
if (button === '{clear}') return handleClear()
if (button === '{enter}') return handleEnter()
if (button === '{close}') return close()
if (button === '{arrowleft}') return handleArrow(0)
if (button === '{arrowright}') return handleArrow(1)
}
const handleLock = () => {
let currentLayout = keyboard.value.options.layoutName
let shiftToggle = currentLayout === 'default' ? 'shift' : 'default'
keyboard.value.setOptions({
layoutName: shiftToggle
})
}
const handleChange = () => {
let layoutCandidates = keyboard.value.options.layoutCandidates
// 切换中英文输入法
if (layoutCandidates != null && layoutCandidates != undefined) {
displayDefault.value['{change}'] = 'en'
keyboard.value.setOptions({
layoutName: 'default',
layoutCandidates: null,
display: displayDefault.value
})
} else {
displayDefault.value['{change}'] = 'cn'
keyboard.value.setOptions({
layoutName: 'default',
layoutCandidates: (layout as any)?.layoutCandidates,
display: displayDefault.value
})
}
}
const handleClear = () => {
keyboard.value.clearInput()
model.value = ''
}
const handleEnter = () => {
emits('enter')
}
const close = () => {
if (props.layoutName == 'number') {
// 处理精度
model.value = model.value?.replace(new RegExp(`(\\d+)\\.(\\d{${props.precision}}).*$`), '$1.$2').replace(/\.$/, '')
}
document.removeEventListener('click', handlePopClose)
visible.value = false
emits('close')
}
const handleArrow = (num: number) => {
// 处理左右箭头下标位置
const index = keyboard.value.getCaretPositionEnd()
if (num == 0 && index - 1 >= 0) {
keyboard.value.setCaretPosition(index - 1)
} else if (num == 1 && index + 1 <= (model.value?.length || 0)) {
keyboard.value.setCaretPosition(index + 1)
}
}
const handlePopClose = (e: any) => {
// 虚拟键盘区域 输入框区域 中文选项区域
if (
(e.target as Element).closest('.keyboard-popper') ||
(e.target as Element).closest('.keyboard-wrap') == keyboardWrapRef.value ||
/hg-candidate-box/.test(e.target.className)
) {
const index = keyboard.value.getCaretPositionEnd()
inputRef.value.ref.selectionStart = inputRef.value.ref.selectionEnd = index
inputRef.value.focus()
} else {
close()
}
}
onUnmounted(() => {
// 某些情况下未触发动画关闭时销毁事件。此处销毁做后备处理
document.removeEventListener('click', handlePopClose)
})
defineExpose({ inputRef, visible, open, close, keyboardInit })
</script>
<style scoped>
.keyboard-wrap {
width: 100%;
}
:deep(.el-input input) {
-webkit-text-security: disc;
}
</style>
<style>
.hg-theme-default .hg-button.hg-button-arrowleft,
.hg-theme-default .hg-button.hg-button-arrowright {
max-width: 70px;
}
.hg-theme-default .hg-button.hg-button-close {
max-width: 100px;
}
.hg-layout-number .hg-button.hg-button-close {
max-width: none;
}
.hg-layout-number .hg-button.hg-button-bksp {
max-width: 92px;
}
</style>

@ -43,13 +43,6 @@ const option = computed(() => {
data: props.lineData.map(item => item.name), data: props.lineData.map(item => item.name),
axisLabel: { axisLabel: {
color: '#fff' color: '#fff'
},
splitLine: {
show: true, // 开启网格线
lineStyle: {
color: '#ccc', // 网格线颜色
type: 'dashed' // 虚线,可选 'solid' | 'dashed' | 'dotted'
}
} }
}, },
yAxis: { yAxis: {
@ -68,7 +61,7 @@ const option = computed(() => {
bottom: '10%', bottom: '10%',
}, },
title: { title: {
text: '24小时有功功率数据可视化-kW', text: '有功功率w',
textStyle: { textStyle: {
color: '#fff' color: '#fff'
} }

@ -0,0 +1,106 @@
<template>
<el-dialog v-model="visible" title="请输入操作密码" width="400px" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="closeable" append-to-body class="password-dialog">
<div class="password-dialog-body">
<p class="password-hint">{{ hint }}</p>
<BasePasswordInput v-model="password" layout-name="number" placeholder="请输入密码" clearable @enter="handleVerify" />
</div>
<template #footer>
<div style="display:flex; gap:12px;">
<el-button style="flex:1" @click="handleCancel">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleVerify" style="flex:1">
确认
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import BasePasswordInput from '@/components/BasePasswordInput.vue'
import RPC from '@/utils/rpc'
import { useRouter } from 'vue-router'
const router = useRouter()
const props = defineProps({
// 是否允许关闭(初始无 session 时不允许关闭)
closeable: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['verified'])
const visible = ref(false)
const password = ref('')
const loading = ref(false)
const hint = ref('请输入操作密码以继续')
const open = () => {
password.value = ''
hint.value = '请输入操作密码以继续'
visible.value = true
}
const close = () => {
visible.value = false
}
const handleCancel = () => {
visible.value = false
router.push('/menu')
}
const handleVerify = async () => {
if (!password.value) {
ElMessage.warning('请输入密码')
return
}
loading.value = true
try {
// 直接调用密码验证接口(该接口在白名单内,无需 session)
const res = await RPC.post('/m9z/password/verify', { password: password.value })
if (res?.session_id) {
localStorage.setItem('m9z_session_id', res.session_id)
ElMessage.success('验证成功')
visible.value = false
emits('verified', res.session_id)
// 触发自定义事件,通知当前页面重新加载数据
window.dispatchEvent(new CustomEvent('m9z:session-restored'))
} else {
hint.value = '密码错误,请重试'
password.value = ''
}
} catch (err) {
// 只有 401(密码错误)才在此处理,其他错误由 rpc.js 统一处理
hint.value = '密码错误,请重试'
password.value = ''
} finally {
loading.value = false
}
}
defineExpose({ open, close })
</script>
<style scoped>
.password-dialog-body {
display: flex;
flex-direction: column;
gap: 16px;
}
.password-hint {
margin: 0;
color: #606266;
font-size: 14px;
text-align: center;
}
</style>
<style>
.password-dialog .el-dialog__header {
text-align: center;
}
</style>

@ -59,6 +59,12 @@ export const routes = [
component: () => import('@/views/AlarmInformation/index.vue'), component: () => import('@/views/AlarmInformation/index.vue'),
meta: { title: '报警预览' }, meta: { title: '报警预览' },
}, },
{
path: '/system-settings',
name: 'SystemSettings',
component: () => import('@/views/SystemSettings/index.vue'),
meta: { title: '系统设置' },
},
] ]
const router = createRouter({ const router = createRouter({

@ -7,5 +7,5 @@
} }
:root:root { :root:root {
--el-color-primary: #006eff; /* 改变主色调 */ --el-color-primary: #25b15f; /* 改变主色调 */
} }

@ -1,6 +1,12 @@
@font-face {
font-family: SourceHanSansCN-Bold;
src: url(/fonts/SourceHanSansCN-Bold.otf);
}
body { body {
padding: 0; padding: 0;
margin: 0; margin: 0;
font-family: SourceHanSansCN-Bold;
} }
*:focus-visible { *:focus-visible {
outline: none; outline: none;

@ -17,7 +17,7 @@ const guid = () => {
} }
const instance = axios.create({ const instance = axios.create({
baseURL: '/', baseURL: '/',
timeout: 100000, timeout: 30 * 1000,
responseType: 'json', responseType: 'json',
}) })
@ -32,7 +32,7 @@ instance.interceptors.request.use(
jsonrpc: '2.0', jsonrpc: '2.0',
method, method,
params, params,
session: localStorage.getItem('token'), session: localStorage.getItem('m9z_session_id'),
timestampin: Date.now().toString(), timestampin: Date.now().toString(),
} }
return config return config
@ -48,6 +48,12 @@ instance.interceptors.response.use(
if (response.data.error.code === 200) { if (response.data.error.code === 200) {
return Promise.resolve(response.data.result) return Promise.resolve(response.data.result)
} else { } else {
// 只有 401 时才清除 session 并触发密码弹框,其他错误走通用提示
if (response.data.error.code === 401) {
localStorage.removeItem('m9z_session_id')
window.dispatchEvent(new CustomEvent('m9z:session-expired'))
return Promise.reject(new Error('session expired'))
}
noticeError(response.data.error.message) noticeError(response.data.error.message)
return Promise.reject(response.data.error.message) return Promise.reject(response.data.error.message)
} }

@ -17,42 +17,21 @@
<!-- 主体区域 --> <!-- 主体区域 -->
<div class="control-content"> <div class="control-content">
<div style="width: 100%;display: flex;justify-content: space-between;align-items: center;"> <div style="width: 100%;">
<div class="tabBox"> <!-- <el-button @click="handleDeal" class="handleBtn" type="success">
<div class="tab" :class="{ select: type === '重大预警' }" @click="changeTab('重大预警')">重大预警</div> <el-icon>
<div class="tab" :class="{ select: type === '一般预警' }" @click="changeTab('一般预警')">一般预警</div> <Delete />
</div> </el-icon>
<div v-if="type === '一般预警'"> 批量处理
<el-button @click="handleDeal" class="handleBtn" type="success"> </el-button> -->
<el-icon> <el-button @click="handleDel" class="delBtn" type="danger">
<Delete /> <el-icon>
</el-icon> <Delete />
批量处理 </el-icon>
</el-button> 批量删除
<el-button @click="handleDel" class="delBtn" type="danger"> </el-button>
<el-icon>
<Delete />
</el-icon>
批量删除
</el-button>
</div>
</div> </div>
<el-table v-show="type === '重大预警'" :data="bigTableData" style="width: 100%" height="366" stripe v-loading="loading"> <el-table class="alarm-table" :data="showTableData" style="width: 100%" stripe v-loading="loading" @selection-change="handleSelectionChange" row-key="id" ref="multipleTableRef">
<el-table-column align="center" prop="id" label="事件ID" width="70" />
<el-table-column align="center" prop="event_type_id" label="事件类型" width="120" />
<el-table-column align="center" prop="device_id" label="设备ID" />
<el-table-column align="center" prop="content" label="事件内容" />
<el-table-column align="center" prop="created" label="创建时间" width="170">
<template #default="{ row }">
{{ row.created ? dayjs(row.created * 1000).format('YYYY-MM-DD HH:mm:ss') : '' }}
</template>
</el-table-column>
<el-table-column align="center" prop="latitude" label="纬度" width="100">
</el-table-column>
<el-table-column align="center" prop="longitude" label="经度" width="100">
</el-table-column>
</el-table>
<el-table v-show="type === '一般预警'" :data="tableData" style="width: 100%" height="366" stripe v-loading="loading" @selection-change="handleSelectionChange" row-key="id" ref="multipleTableRef">
<el-table-column type="selection" :selectable="selectable" width="55" /> <el-table-column type="selection" :selectable="selectable" width="55" />
<el-table-column align="center" prop="id" label="序号" /> <el-table-column align="center" prop="id" label="序号" />
<el-table-column align="center" prop="loop_idx" label="回路地址" /> <el-table-column align="center" prop="loop_idx" label="回路地址" />
@ -68,13 +47,13 @@
{{ row.fault_time ? dayjs(row.fault_time).format('YYYY-MM-DD HH:mm:ss') : '' }} {{ row.fault_time ? dayjs(row.fault_time).format('YYYY-MM-DD HH:mm:ss') : '' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" prop="voltage" label="处理状态"> <!-- <el-table-column align="center" prop="voltage" label="处理状态">
<template #default="{ row }"> <template #default="{ row }">
{{ row.operate_status == 0 ? '未处理' : '已处理' }} {{ row.operate_status == 0 ? '未处理' : '已处理' }}
</template> </template>
</el-table-column> </el-table-column> -->
</el-table> </el-table>
<el-pagination v-if="total > 0" @size-change="handleSizeChange" @current-change="handleCurrentChange" :page-sizes="[10, 30, 50, 100]" :total="total" :pager-count="5" v-model:page-size="pageSize" v-model:current-page="currentPage" background layout="prev, pager, next,jumper" /> <el-pagination v-if="total > 0" @size-change="handleSizeChange" @current-change="handleCurrentChange" :page-sizes="[6, 8, 10, 30, 50]" :total="total" :pager-count="5" v-model:page-size="pageSize" v-model:current-page="currentPage" background layout="total, sizes, prev, pager, next" />
</div> </div>
</div> </div>
<!-- 底部信息 --> <!-- 底部信息 -->
@ -82,9 +61,6 @@
<div style="color:#fff"> <div style="color:#fff">
设备号:{{ commit_uid }} 设备号:{{ commit_uid }}
</div> </div>
<div style="color:#fff">
设备名称:{{ device_name }}
</div>
<div class="bottom_item"> <div class="bottom_item">
<!-- <div class="time-item"> <!-- <div class="time-item">
<span>日出时间:{{ sunriseTime }}</span> <span>日出时间:{{ sunriseTime }}</span>
@ -120,40 +96,32 @@ import dayjs from 'dayjs';
const router = useRouter() const router = useRouter()
const currentTime = ref('') const currentTime = ref('')
const systemRuntime = ref('38分钟') const systemRuntime = ref('38分钟')
const commit_uid = localStorage.getItem('central_control_comm_uid') // const commit_uid = sessionStorage.getItem('central_control_comm_uid')
const device_name = localStorage.getItem('central_control_device_name') const commit_uid = sessionStorage.getItem('central_control_comm_uid')
// const commit_uid = '868656077692382'
const loading = ref(false) const loading = ref(false)
const total = ref(0) const total = ref(0)
const pageSize = ref(8) const pageSize = ref(6)
const currentPage = ref(1) const currentPage = ref(1)
const type = ref('重大预警') const tableData = ref([])
const changeTab = (tab) => { const showTableData = ref([])
type.value = tab
if (type.value === '重大预警') { const applyPageData = () => {
getBigData() const start = (currentPage.value - 1) * pageSize.value
} else { showTableData.value = tableData.value.slice(start, start + pageSize.value)
getData()
}
} }
const handleSizeChange = () => {
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1 currentPage.value = 1
if (type.value === '重大预警') { applyPageData()
getBigData()
} else {
getData()
}
} }
const handleCurrentChange = () => { const handleCurrentChange = () => {
if (type.value === '重大预警') { applyPageData()
getBigData()
} else {
getData()
}
} }
const tableData = ref([])
const multipleTableRef = ref(null) const multipleTableRef = ref(null)
const multipleSelection = ref([]) const multipleSelection = ref([])
const selectable = (row) => true const selectable = (row) => true
@ -206,9 +174,10 @@ const handleDeal = () => {
} }
const getData = () => { const getData = () => {
currentPage.value = 1
const params = { const params = {
"limit": pageSize.value, // "limit": pageSize.value,
"page": currentPage.value, // "page": currentPage.value,
"and": { "and": {
"comm_uid": [ "comm_uid": [
commit_uid commit_uid
@ -225,57 +194,19 @@ const getData = () => {
loading.value = true loading.value = true
tableData.value = [] tableData.value = []
RPC.post('/m9z/fault/list', params).then(res => { RPC.post('/m9z/fault/list', params).then(res => {
total.value = res.count || 0 const rows = res?.rows || []
tableData.value = res.rows || [] total.value = rows.length
tableData.value = rows
applyPageData()
}).finally(() => { }).finally(() => {
loading.value = false loading.value = false
}) })
} }
// getData() getData()
const bigTableData = ref([])
const getBigData = () => {
loading.value = true
RPC.post('/eventcenter/type/list', { page: 1, limit: -1, and: { is_fault: [true] } }).then(res => {
const faultList = res.rows || []
if (faultList.length > 0) {
const params = {
"limit": pageSize.value,
"page": currentPage.value,
"and": {
"device_id": [
commit_uid
],
event_type_id: faultList.map(item => item.id)
},
"like": {},
}
RPC.post('/eventcenter/list', params).then(res => {
total.value = res.count || 0
bigTableData.value = res.rows || []
}).finally(() => {
loading.value = false
})
} else {
loading.value = false
total.value = 0
bigTableData.value = []
faults.value = []
}
}).catch(() => {
loading.value = false
})
}
getBigData()
const reflesh = () => { const reflesh = () => {
currentPage.value = 1 currentPage.value = 1
if (type.value === '重大预警') { getData()
getBigData()
} else {
getData()
}
} }
@ -311,15 +242,27 @@ const goBack = () => {
let timeInterval = null let timeInterval = null
// 处理 session 恢复后的重新加载
const handleSessionRestored = () => {
console.log('Session 已恢复,重新加载数据')
getData()
}
onMounted(() => { onMounted(() => {
updateTime() updateTime()
timeInterval = setInterval(updateTime, 1000) timeInterval = setInterval(updateTime, 1000)
// 监听 session 恢复事件
window.addEventListener('m9z:session-restored', handleSessionRestored)
}) })
onUnmounted(() => { onUnmounted(() => {
if (timeInterval) { if (timeInterval) {
clearInterval(timeInterval) clearInterval(timeInterval)
} }
// 移除事件监听
window.removeEventListener('m9z:session-restored', handleSessionRestored)
}) })
</script> </script>
@ -329,23 +272,28 @@ onUnmounted(() => {
height: 100vh; height: 100vh;
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
overflow: auto; overflow: hidden;
background: center / cover url('@/assets/images/handle_control_bg.png') no-repeat; background: center / cover url('@/assets/images/背景小@2x.png') no-repeat;
background-color: #0A3933;
} }
.main-content { .main-content {
position: relative; position: relative;
z-index: 2; z-index: 2;
width: 100%; width: 100%;
height: 100%; height: calc(100vh - 50px);
padding: 10px 20px; padding: 10px 20px;
box-sizing: border-box; box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
} }
.header-status { .header-status {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
flex-shrink: 0;
margin-bottom: 20px; margin-bottom: 20px;
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
} }
@ -373,36 +321,23 @@ onUnmounted(() => {
.control-content { .control-content {
width: 100%; width: 100%;
flex: 1;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
overflow: hidden;
:deep(.el-table__header) { :deep(.el-table__header) {
.el-table__cell { .el-table__cell {
background-color: #0881FE; background-color: #24806B;
color: #fff; color: #fff;
} }
} }
.tabBox { :deep(.el-pagination.is-background .el-pager li.is-active) {
display: flex; background-color: #24806B;
align-items: center;
gap: 10px;
.tab {
color: #fff;
padding: 4px 10px;
border-radius: 4px;
border: 1px solid #fff;
cursor: pointer;
&.select {
color: #0881FE;
border: 1px solid #0881FE;
}
}
} }
} }
@ -443,9 +378,16 @@ onUnmounted(() => {
} }
.back-button { .back-button {
background-color: #000; // background-color: #000;
color: #fff; // color: #fff;
border: #000; border: #000;
background: linear-gradient(180deg, rgba(223, 196, 137, 1) 0%, rgba(190, 151, 99, 1) 100%);
color: #fff;
}
.restart-button {
background: linear-gradient(180deg, rgba(248, 250, 249, 1) 0%, rgba(138, 195, 182, 1) 100%);
color: rgba(41, 112, 93, 1);
} }
} }
@ -467,4 +409,24 @@ onUnmounted(() => {
background-color: #f12323; background-color: #f12323;
border-color: #f12323; border-color: #f12323;
} }
.el-table {
:deep(.el-table__cell) {
padding: 7px 0;
}
}
:deep(.el-checkbox) {
--el-checkbox-input-height: 24px;
--el-checkbox-input-width: 24px;
font-size: 24px;
.el-checkbox__inner:after {
top: 4px;
left: 8px;
width: 4px;
height: 10px;
}
}
</style> </style>

@ -0,0 +1,255 @@
<template>
<div ref="keyboardWrapRef" class="keyboard-wrap" @click="focusInput">
<el-input ref="inputRef" v-model="model" @keyup.enter="handleEnter" :disabled="disabled" v-bind="$attrs">
<!-- <template v-for="(item, index) in $slots" :key="index" #[index]>
<slot :name="index"></slot>
</template> -->
<template #suffix>
<span>%</span>
</template>
</el-input>
</div>
<el-popover v-if="visible" :visible="true" :virtual-ref="inputRef" virtual-triggering placement="top" :width="width" :show-arrow="false" :hide-after="0" popper-style="padding: 0px;color:#000" popper-class="keyboard-popper" @after-enter="keyboardInit">
<div class="simple-keyboard"></div>
</el-popover>
</template>
<script setup lang="ts">
import Keyboard from 'simple-keyboard'
import 'simple-keyboard/build/css/index.css'
import layout from 'simple-keyboard-layouts/build/layouts/chinese.js'
defineOptions({
inheritAttrs: false
})
const model = defineModel<any>()
const emits = defineEmits(['onChange', 'enter', 'close', 'focus'])
const props = defineProps({
layoutName: {
type: String,
default: 'default'
},
// 保留几位小数 layoutName为number时生效
precision: {
type: Number,
default: 10
},
// 获取焦点打开键盘
isOpen: {
type: Boolean,
default: true
},
disabled: Boolean
})
const keyboard = ref<any>(null)
const keyboardWrapRef = ref(null)
const visible = ref(false)
const inputRef = ref()
const width = ref(1000)
if (props.layoutName == 'number') width.value = 300
const displayDefault = ref({
'{bksp}': 'backspace',
'{lock}': 'caps',
'{enter}': 'enter',
'{tab}': 'tab',
'{shift}': 'shift',
'{change}': 'en',
'{space}': 'space',
'{clear}': 'clear',
'{close}': 'close',
'{arrowleft}': '←',
'{arrowright}': '→'
})
const open = () => {
if (visible.value) return
inputRef.value.focus()
emits('focus')
visible.value = true
}
const focusInput = () => {
if (props.disabled) return
if (visible.value) return
emits('focus')
if (props.isOpen) visible.value = true
}
const lang: any = {
'zh-cn': {
// 默认布局
default: [
'` 1 2 3 4 5 6 7 8 9 0 - = {bksp}',
'{tab} q w e r t y u i o p [ ] \\',
"{lock} a s d f g h j k l ; ' {enter}",
'{change} z x c v b n m , . / {clear}',
'{arrowleft} {arrowright} {space} {close}'
],
// 大小写
shift: [
'~ ! @ # $ % ^ & * ( ) _ + {bksp}',
'{tab} Q W E R T Y U I O P { } |',
'{lock} A S D F G H J K L : " {enter}',
'{change} Z X C V B N M < > ? {clear}',
'{arrowleft} {arrowright} {space} {close}'
]
},
th: {
default: [
'\u005F \u0E45 \u002F \u002D \u0E20 \u0E16 \u0E38 \u0E36 \u0E04 \u0E15 \u0E08 \u0E02 \u0E0A {bksp}',
'{tab} \u0E46 \u0E44 \u0E33 \u0E1E \u0E30 \u0E31 \u0E35 \u0E23 \u0E19 \u0E22 \u0E1A \u0E25 \u0E03',
'{lock} \u0E1F \u0E2B \u0E01 \u0E14 \u0E40 \u0E49 \u0E48 \u0E32 \u0E2A \u0E27 \u0E07 {enter}',
'{shift} \u0E1C \u0E1B \u0E41 \u0E2D \u0E34 \u0E37 \u0E17 \u0E21 \u0E43 \u0E1D {clear}',
'{arrowleft} {arrowright} {space} {close}'
],
shift: [
'% + \u0E51 \u0E52 \u0E53 \u0E54 \u0E39 \u0E3F \u0E55 \u0E56 \u0E57 \u0E58 \u0E59 {bksp}',
'{tab} \u0E50 \u0022 \u0E0E \u0E11 \u0E18 \u0E4D \u0E4A \u0E13 \u0E2F \u0E0D \u0E10 \u002C \u0E05',
'{lock} \u0E24 \u0E06 \u0E0F \u0E42 \u0E0C \u0E47 \u0E4B \u0E29 \u0E28 \u0E0B \u002E {enter}',
'{shift} ( ) \u0E09 \u0E2E \u0E3A \u0E4C \u003F \u0E12 \u0E2C \u0E26 {clear}',
'{arrowleft} {arrowright} {space} {close}'
]
}
}
const keyboardInit = () => {
let keyboardLayout = {
// 数字布局
number: ['7 8 9', '4 5 6', '1 2 3', '. 0 {bksp}', '{arrowleft} {arrowright} {clear} {close}'],
...lang['zh-cn']
}
// if (lang[useSession('lang').value]) {
// keyboardLayout = { ...lang[useSession('lang').value], ...keyboardLayout }
// }
keyboard.value = new Keyboard('simple-keyboard', {
onChange: onChange,
onKeyPress: onKeyPress,
onInit: onInit,
layout: keyboardLayout,
layoutName: props.layoutName,
display: displayDefault.value
// theme: 'hg-theme-default init-keyboard' // 添加自定义class处理清空逻辑
})
}
const onInit = (keyboard: any) => {
keyboard.setInput(String(model.value || ''))
keyboard.setCaretPosition(inputRef.value?.ref.selectionEnd)
document.addEventListener('click', handlePopClose)
}
const onChange = (input: any) => {
model.value = input
emits('onChange', input)
}
const onKeyPress = (button: any) => {
if (button === '{lock}') return handleLock()
if (button === '{change}') return handleChange()
if (button === '{clear}') return handleClear()
if (button === '{enter}') return handleEnter()
if (button === '{close}') return close()
if (button === '{arrowleft}') return handleArrow(0)
if (button === '{arrowright}') return handleArrow(1)
}
const handleLock = () => {
let currentLayout = keyboard.value.options.layoutName
let shiftToggle = currentLayout === 'default' ? 'shift' : 'default'
keyboard.value.setOptions({
layoutName: shiftToggle
})
}
const handleChange = () => {
let layoutCandidates = keyboard.value.options.layoutCandidates
// 切换中英文输入法
if (layoutCandidates != null && layoutCandidates != undefined) {
displayDefault.value['{change}'] = 'en'
keyboard.value.setOptions({
layoutName: 'default',
layoutCandidates: null,
display: displayDefault.value
})
} else {
displayDefault.value['{change}'] = 'cn'
keyboard.value.setOptions({
layoutName: 'default',
layoutCandidates: (layout as any)?.layoutCandidates,
display: displayDefault.value
})
}
}
const handleClear = () => {
keyboard.value.clearInput()
model.value = ''
}
const handleEnter = () => {
emits('enter')
}
const close = () => {
if (props.layoutName == 'number') {
// 处理精度
// model.value = model.value?.replace(new RegExp(`(\\d+)\\.(\\d{${String(props.precision)}}).*$`), '$1.$2').replace(/\.$/, '')
model.value = Number(model.value)
}
document.removeEventListener('click', handlePopClose)
visible.value = false
emits('close')
}
const handleArrow = (num: number) => {
// 处理左右箭头下标位置
const index = keyboard.value.getCaretPositionEnd()
if (num == 0 && index - 1 >= 0) {
keyboard.value.setCaretPosition(index - 1)
} else if (num == 1 && index + 1 <= (model.value?.length || 0)) {
keyboard.value.setCaretPosition(index + 1)
}
}
const handlePopClose = (e: any) => {
// 虚拟键盘区域 输入框区域 中文选项区域
if (
(e.target as Element).closest('.keyboard-popper') ||
(e.target as Element).closest('.keyboard-wrap') == keyboardWrapRef.value ||
/hg-candidate-box/.test(e.target.className)
) {
const index = keyboard.value.getCaretPositionEnd()
inputRef.value.ref.selectionStart = inputRef.value.ref.selectionEnd = index
inputRef.value.focus()
} else {
close()
}
}
onUnmounted(() => {
// 某些情况下未触发动画关闭时销毁事件。此处销毁做后备处理
document.removeEventListener('click', handlePopClose)
})
defineExpose({ inputRef, visible, open, close, keyboardInit })
</script>
<style scoped>
.keyboard-wrap {
width: 100%;
}
</style>
<style>
.hg-theme-default .hg-button.hg-button-arrowleft,
.hg-theme-default .hg-button.hg-button-arrowright {
max-width: 70px;
}
.hg-theme-default .hg-button.hg-button-close {
max-width: 100px;
}
.hg-layout-number .hg-button.hg-button-close {
max-width: none;
}
.hg-layout-number .hg-button.hg-button-bksp {
max-width: 92px;
}
</style>

@ -58,7 +58,7 @@
</div> </div>
<div class="item" v-for="(ele, idx) in item.loops"> <div class="item" v-for="(ele, idx) in item.loops">
<div>{{ idx + 1 }}</div> <div>{{ idx + 1 }}</div>
<div :class="{ red: !ele.turnOn, green: ele.turnOn }">{{ ele.turnOn ? '通' : '断' }}</div> <div :class="{ red: !ele.turnOn }">{{ ele.turnOn ? '通' : '断' }}</div>
<div>{{ ele.pwm }}</div> <div>{{ ele.pwm }}</div>
</div> </div>
</div> </div>
@ -93,6 +93,8 @@
</div> </div>
<div class="buttonBox"> <div class="buttonBox">
<el-button icon="ArrowUp" style="margin-left: 0;" class="brightness-button" @click="prev">上滑</el-button>
<el-button icon="ArrowDown" style="margin-left: 0;" class="brightness-button" @click="next">下滑</el-button>
<el-button icon="RefreshRight" style="margin-left: 0;" class="mode-button" @click="restartSystem">远程重启</el-button> <el-button icon="RefreshRight" style="margin-left: 0;" class="mode-button" @click="restartSystem">远程重启</el-button>
<el-button icon="Refresh" style="margin-left: 0;" class="brightness-button" @click="getTask">刷新</el-button> <el-button icon="Refresh" style="margin-left: 0;" class="brightness-button" @click="getTask">刷新</el-button>
<el-button icon="Plus" style="margin-left: 0;" class="brightness-button add" @click="addTask">新增</el-button> <el-button icon="Plus" style="margin-left: 0;" class="brightness-button add" @click="addTask">新增</el-button>
@ -166,7 +168,7 @@
<el-dialog v-model="modeDialogVisible" title="选择模式" width="600px" :modal="true" class="brightness-dialog"> <el-dialog v-model="modeDialogVisible" title="选择模式" width="600px" :modal="true" class="brightness-dialog">
<div class="setting-content"> <div class="setting-content">
<div class="modeBox"> <div class="modeBox">
<el-button v-for="(item, index) in 3" :class="index === modeActive ? 'mode-button active' : 'brightness-button'" @click="modeActive = index">模式{{ item }}</el-button> <el-button v-for="(item, index) in 3" :class="index === modeActive ? 'mode-button active' : 'brightness-button dark'" @click="modeActive = index">模式{{ item }}</el-button>
</div> </div>
</div> </div>
<template #footer> <template #footer>
@ -212,16 +214,17 @@
<div class="lightBox"> <div class="lightBox">
<div class="item" v-for="(item, index) in currentTask.loops"> <div class="item" v-for="(item, index) in currentTask.loops">
<div class="title">调光通道{{ index + 1 }}</div> <div class="title">调光通道{{ index + 1 }}</div>
<el-input-number style="width: 95%;" v-model="item.pwm" :min="0" :max="100"> <BaseKeyboardInput style="width: 95%;" v-model="item.pwm" layout-name="number"></BaseKeyboardInput>
<!-- <el-input-number style="width: 95%;" v-model="item.pwm" :min="0" :max="100">
<template #suffix> <template #suffix>
<span>%</span> <span>%</span>
</template> </template>
</el-input-number> </el-input-number> -->
</div> </div>
</div> </div>
<div class="timeBox"> <div class="timeBox">
<span>时间</span> <span>时间</span>
<el-time-picker v-model="currentTask.execTime" placeholder="请选择" /> <el-time-picker arrow-control v-model="currentTask.execTime" placeholder="请选择" />
<el-radio-group style="margin-left: 20px;" v-model="currentTask.timeType"> <el-radio-group style="margin-left: 20px;" v-model="currentTask.timeType">
<el-radio :value="0">定时时间</el-radio> <el-radio :value="0">定时时间</el-radio>
<el-radio :value="1">经纬度时间</el-radio> <el-radio :value="1">经纬度时间</el-radio>
@ -243,7 +246,9 @@ import { ref, onMounted, onUnmounted, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import RPC from '@/utils/rpc' import RPC from '@/utils/rpc'
import dayjs from 'dayjs' import dayjs from 'dayjs'
const commit_uid = localStorage.getItem('central_control_comm_uid') import BaseKeyboardInput from './BaseKeyboardInput.vue';
const commit_uid = sessionStorage.getItem('central_control_comm_uid')
// const commit_uid = '868656077692382'
const switchValue = ref(true) const switchValue = ref(true)
const selectBoxValue = ref([]) const selectBoxValue = ref([])
@ -255,10 +260,14 @@ const currentTaskName = ref('开始任务')
const currentTask = ref({}) const currentTask = ref({})
const currentMiddleIndex = ref(null) const currentMiddleIndex = ref(null)
const next = () => {
document.querySelector('.power-params').scrollTop = document.querySelector('.power-params').scrollTop + 120
}
const prev = () => {
document.querySelector('.power-params').scrollTop = document.querySelector('.power-params').scrollTop - 120
}
const openSettingDialog = (item, index) => { const openSettingDialog = (item, index) => {
if (!isOnline.value) {
return ElMessage.error('设备不在线')
}
currentTaskName.value = item currentTaskName.value = item
if (item === '中间任务') { if (item === '中间任务') {
currentTask.value = middleTask.value[index] currentTask.value = middleTask.value[index]
@ -318,9 +327,6 @@ const confirmSetting = () => {
middleTask.value[currentMiddleIndex.value].execTime = dayjs(currentTask.value.execTime).format('YYYY-MM-DD HH:mm:ss') middleTask.value[currentMiddleIndex.value].execTime = dayjs(currentTask.value.execTime).format('YYYY-MM-DD HH:mm:ss')
middleTask.value[currentMiddleIndex.value].timeType = currentTask.value.timeType middleTask.value[currentMiddleIndex.value].timeType = currentTask.value.timeType
middleTask.value[currentMiddleIndex.value].loops = currentTask.value.loops middleTask.value[currentMiddleIndex.value].loops = currentTask.value.loops
middleTask.value.forEach(item => {
item.execTime = item.execTime ? dayjs(item.execTime).format('YYYY-MM-DD HH:mm:ss') : ''
})
const middleParams = { const middleParams = {
"comm_uid": commit_uid, "comm_uid": commit_uid,
"mode": deviceMode.value, "mode": deviceMode.value,
@ -346,12 +352,11 @@ const tasks = computed(() => [startTask.value, stopTask.value, middleTask.value]
const deviceMode = ref(0) const deviceMode = ref(0)
const currentMode = ref(0) const currentMode = ref(0)
RPC.post('/m9z/getDeviceMode', { comm_uid: commit_uid }).then(res => {
currentMode.value = res
})
const changeMode = () => { const changeMode = () => {
if (!isOnline.value) {
return ElMessage.error('设备不在线')
}
modeActive.value = currentMode.value modeActive.value = currentMode.value
modeDialogVisible.value = true modeDialogVisible.value = true
} }
@ -373,36 +378,30 @@ const getTask = () => {
startTaskLoading.value = true startTaskLoading.value = true
stopTaskLoading.value = true stopTaskLoading.value = true
middleTaskLoading.value = true middleTaskLoading.value = true
RPC.post('/m9z/getStartTask2', { comm_uid: commit_uid, mode: deviceMode.value, cache: !isOnline.value }).then(res => { RPC.post('/m9z/getStartTask2', { comm_uid: commit_uid, mode: deviceMode.value }).then(res => {
startTask.value = res startTask.value = res
}).finally(() => { }).finally(() => {
startTaskLoading.value = false startTaskLoading.value = false
}) })
RPC.post('/m9z/getStopTask2', { comm_uid: commit_uid, mode: deviceMode.value, cache: !isOnline.value }).then(res => { RPC.post('/m9z/getStopTask2', { comm_uid: commit_uid, mode: deviceMode.value }).then(res => {
stopTask.value = res stopTask.value = res
}).finally(() => { }).finally(() => {
stopTaskLoading.value = false stopTaskLoading.value = false
}) })
RPC.post('/m9z/getMiddleTask2', { comm_uid: commit_uid, mode: deviceMode.value, cache: !isOnline.value }).then(res => { RPC.post('/m9z/getMiddleTask2', { comm_uid: commit_uid, mode: deviceMode.value }).then(res => {
middleTask.value = res.controlTaskList || [] middleTask.value = res.controlTaskList || []
}).finally(() => { }).finally(() => {
middleTaskLoading.value = false middleTaskLoading.value = false
}) })
} }
getTask()
const addTask = () => { const addTask = () => {
if (!isOnline.value) {
return ElMessage.error('设备不在线')
}
middleTask.value.push({ middleTask.value.push({
"execTime": '2025-06-07 19:00:00', "execTime": '2025-06-07 19:00:00',
"turnOn": true, "turnOn": true,
"pwm": 50 "pwm": 50
}) })
middleTask.value.forEach(item => {
item.execTime = item.execTime ? dayjs(item.execTime).format('YYYY-MM-DD HH:mm:ss') : ''
})
const middleParams = { const middleParams = {
"comm_uid": commit_uid, "comm_uid": commit_uid,
"mode": deviceMode.value, "mode": deviceMode.value,
@ -420,9 +419,6 @@ const addTask = () => {
}) })
} }
const delTask = (index) => { const delTask = (index) => {
if (!isOnline.value) {
return ElMessage.error('设备不在线')
}
ElMessageBox.confirm( ElMessageBox.confirm(
'确认删除?', '确认删除?',
'', '',
@ -435,9 +431,6 @@ const delTask = (index) => {
) )
.then(() => { .then(() => {
middleTask.value.splice(index, 1) middleTask.value.splice(index, 1)
middleTask.value.forEach(item => {
item.execTime = item.execTime ? dayjs(item.execTime).format('YYYY-MM-DD HH:mm:ss') : ''
})
const middleParams = { const middleParams = {
"comm_uid": commit_uid, "comm_uid": commit_uid,
"mode": deviceMode.value, "mode": deviceMode.value,
@ -483,6 +476,11 @@ const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四
const sunriseTime = ref('') const sunriseTime = ref('')
const sunsetTime = ref('') const sunsetTime = ref('')
RPC.post('/m9z/getDeviceSunRiseTime', { comm_uid: commit_uid }).then(res => {
sunriseTime.value = res.Sunrise ? dayjs(res.Sunrise).format('HH:mm:ss') : ''
sunsetTime.value = res.Sunset ? dayjs(res.Sunset).format('HH:mm:ss') : ''
})
// 更新时间 // 更新时间
const updateTime = () => { const updateTime = () => {
const now = new Date() const now = new Date()
@ -554,33 +552,35 @@ const restartSystem = () => {
} }
let timeInterval = null let timeInterval = null
const isOnline = ref(false)
onMounted(() => { // 处理 session 恢复后的重新加载
RPC.post('/lightm/rtu/isOnline', { commUid: commit_uid }).then(res => { const handleSessionRestored = () => {
console.log('/lightm/rtu/isOnlin', res) console.log('Session 已恢复,重新加载数据')
isOnline.value = res getTask()
if (isOnline.value) { RPC.post('/m9z/getDeviceMode', { comm_uid: commit_uid }).then(res => {
RPC.post('/m9z/getDeviceSunRiseTime', { comm_uid: commit_uid }).then(res => { currentMode.value = res
sunriseTime.value = res.Sunrise ? dayjs(res.Sunrise).format('HH:mm:ss') : ''
sunsetTime.value = res.Sunset ? dayjs(res.Sunset).format('HH:mm:ss') : ''
})
RPC.post('/m9z/getDeviceMode', { comm_uid: commit_uid }).then(res => {
currentMode.value = res
})
getTask()
} else {
getTask()
// ElMessage.error('设备不在线')
}
}) })
RPC.post('/m9z/getDeviceSunRiseTime', { comm_uid: commit_uid }).then(res => {
sunriseTime.value = res.Sunrise ? dayjs(res.Sunrise).format('HH:mm:ss') : ''
sunsetTime.value = res.Sunset ? dayjs(res.Sunset).format('HH:mm:ss') : ''
})
}
onMounted(() => {
updateTime() updateTime()
timeInterval = setInterval(updateTime, 1000) timeInterval = setInterval(updateTime, 1000)
// 监听 session 恢复事件
window.addEventListener('m9z:session-restored', handleSessionRestored)
}) })
onUnmounted(() => { onUnmounted(() => {
if (timeInterval) { if (timeInterval) {
clearInterval(timeInterval) clearInterval(timeInterval)
} }
// 移除事件监听
window.removeEventListener('m9z:session-restored', handleSessionRestored)
}) })
const delMode = () => { const delMode = () => {
@ -610,7 +610,8 @@ const delMode = () => {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
overflow: auto; overflow: auto;
background: center / cover url('@/assets/images/handle_control_bg.png') no-repeat; background: center / cover url('@/assets/images/背景小@2x.png') no-repeat;
background-color: #0A3933;
} }
.main-content { .main-content {
@ -691,7 +692,7 @@ const delMode = () => {
} }
&.selected { &.selected {
background-color: #0881FE; background-color: #FFAF11;
} }
} }
} }
@ -706,6 +707,20 @@ const delMode = () => {
gap: 10px; gap: 10px;
height: 100%; height: 100%;
// &::-webkit-scrollbar {
// width: 32px;
// /* 滚动条宽度 */
// height: 22px;
// /* 滚动条高度 */
// }
// &::-webkit-scrollbar-thumb {
// background-color: darkgrey;
// /* 滚动条滑块颜色 */
// border-radius: 10px;
// /* 滑块圆角 */
// }
.time { .time {
width: 100px; width: 100px;
border-left: 1px solid #ccc; border-left: 1px solid #ccc;
@ -722,12 +737,12 @@ const delMode = () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: #3e4458; background-color: rgba($color: #188549, $alpha: .3);
.el-icon { .el-icon {
font-size: 30px; font-size: 30px;
background-color: #fff; background-color: #fff;
background: linear-gradient(180deg, rgba(248, 251, 255, 1) 0%, rgba(248, 251, 255, 1) 0%, rgb(124, 144, 181) 96%, rgba(165, 197, 255, 1) 94.08%); background: linear-gradient(180deg, rgba(244, 248, 247, 1) 0%, rgba(142, 198, 185, 1) 96%);
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 5px; border-radius: 5px;
@ -761,7 +776,7 @@ const delMode = () => {
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: #212329; background-color: rgba($color: #188549, $alpha: .3);
width: 30px; width: 30px;
height: 100%; height: 100%;
} }
@ -943,7 +958,7 @@ const delMode = () => {
} }
.time-item { .time-item {
color: rgba(255, 255, 255, 0.7); color: #fff;
font-size: 16px; font-size: 16px;
margin: 0 20px; margin: 0 20px;
} }
@ -959,9 +974,11 @@ const delMode = () => {
} }
.back-button { .back-button {
background-color: #000; // background-color: #000;
color: #fff; // color: #fff;
border: #000; border: #000;
background: linear-gradient(180deg, rgba(223, 196, 137, 1) 0%, rgba(190, 151, 99, 1) 100%);
color: #fff;
} }
} }
@ -974,7 +991,7 @@ const delMode = () => {
.buttonBox { .buttonBox {
.el-button { .el-button {
margin: 15px 0; margin: 10px 0;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
@ -1018,12 +1035,17 @@ const delMode = () => {
margin-bottom: 10px; margin-bottom: 10px;
&.add { &.add {
background-color: #397ff6; background: linear-gradient(180deg, rgba(51, 224, 188, 1) 0%, rgba(5, 149, 117, 1) 100%);
} }
&.del { &.del {
background-color: #d42d1f; background-color: #d42d1f;
} }
&.dark {
color: #333;
border: 1px solid #333;
}
} }
.mode-button { .mode-button {
@ -1033,8 +1055,8 @@ const delMode = () => {
} }
.mode-button.active { .mode-button.active {
background: #ffffff; background: linear-gradient(180deg, rgba(51, 224, 188, 1) 0%, rgba(5, 149, 117, 1) 100%);
color: #1976d2; color: #fff;
} }
.setting-button { .setting-button {
@ -1064,8 +1086,9 @@ const delMode = () => {
/* 弹窗样式 */ /* 弹窗样式 */
:deep(.brightness-dialog) { :deep(.brightness-dialog) {
background: rgba(30, 30, 30, 0.95); // background: rgba(30, 30, 30, 0.95);
backdrop-filter: blur(20px); // backdrop-filter: blur(20px);
background: linear-gradient(180.44deg, rgba(226, 232, 251, 1) 0.64%, rgba(255, 255, 255, 1) 25.32%);
} }
:deep(.brightness-dialog .el-dialog__header) { :deep(.brightness-dialog .el-dialog__header) {
@ -1074,14 +1097,14 @@ const delMode = () => {
} }
:deep(.brightness-dialog .el-dialog__title) { :deep(.brightness-dialog .el-dialog__title) {
color: #ffffff; color: #6C75A0 100%;
font-size: 18px; font-size: 18px;
font-weight: 500; font-weight: 500;
} }
:deep(.brightness-dialog .el-dialog__body) { :deep(.brightness-dialog .el-dialog__body) {
// background: rgba(255, 255, 255, 0.05); // background: rgba(255, 255, 255, 0.05);
color: #ffffff; color: #6C75A0 100%;
} }
.brightness-content { .brightness-content {
@ -1093,7 +1116,7 @@ const delMode = () => {
font-size: 18px; font-size: 18px;
font-weight: 500; font-weight: 500;
margin-bottom: 30px; margin-bottom: 30px;
color: #ffffff; color: #6C75A0;
} }
.brightness-slider-container { .brightness-slider-container {
@ -1169,12 +1192,12 @@ const delMode = () => {
:deep(.el-input-number__increase) { :deep(.el-input-number__increase) {
background-color: transparent; background-color: transparent;
color: #fff; color: #6C75A0;
} }
:deep(.el-input-number__decrease) { :deep(.el-input-number__decrease) {
background-color: transparent; background-color: transparent;
color: #fff; color: #6C75A0;
} }
} }
@ -1182,10 +1205,10 @@ const delMode = () => {
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
background-color: transparent; background-color: transparent;
color: #fff; color: #6C75A0;
input { input {
color: #fff; color: #6C75A0;
} }
} }

@ -11,7 +11,7 @@
<h1>手动控制</h1> <h1>手动控制</h1>
</div> </div>
<div class="system-status"> <div class="system-status">
<span>设备时间:{{ deviceTime ? dayjs(deviceTime).format('YYYY-MM-DD HH:mm') : '' }}</span> <!-- <span>系统已运行:{{ systemRuntime }}</span> -->
</div> </div>
</div> </div>
@ -20,7 +20,7 @@
<!-- 左侧电力参数区域 --> <!-- 左侧电力参数区域 -->
<div class="power-section"> <div class="power-section">
<!-- ABC三相电力参数 --> <!-- ABC三相电力参数 -->
<div class="power-params" v-loading="phaseLoading" element-loading-background="rgba(122, 122, 122, 0.8)"> <div class="power-params">
<div v-for="phase in powerPhases" :key="phase.name" class="phase-card"> <div v-for="phase in powerPhases" :key="phase.name" class="phase-card">
<div class="phase-name"><span>{{ phase.name }}</span>相</div> <div class="phase-name"><span>{{ phase.name }}</span>相</div>
<div class="param-list"> <div class="param-list">
@ -68,7 +68,7 @@
</div> </div>
<!-- 回路控制区域 --> <!-- 回路控制区域 -->
<div class="circuit-section" v-loading="infoLoading" element-loading-background="rgba(122, 122, 122, 0.8)"> <div class="circuit-section">
<!-- 上排回路 --> <!-- 上排回路 -->
<div class="circuit-row"> <div class="circuit-row">
<div v-for="(circuit, index) in info.loops.slice(0, 5)" :key="circuit.name" class="circuit-item"> <div v-for="(circuit, index) in info.loops.slice(0, 5)" :key="circuit.name" class="circuit-item">
@ -99,7 +99,7 @@
</div> </div>
<!-- 调光通道区域 --> <!-- 调光通道区域 -->
<div class="dimming-section" v-loading="infoLoading" element-loading-background="rgba(122, 122, 122, 0.8)"> <div class="dimming-section">
<!-- 上排调光通道 --> <!-- 上排调光通道 -->
<div class="dimming-row"> <div class="dimming-row">
<div v-for="(channel, index) in info.loops.slice(0, 5)" :key="channel.name" class="dimming-item"> <div v-for="(channel, index) in info.loops.slice(0, 5)" :key="channel.name" class="dimming-item">
@ -123,7 +123,7 @@
</div> </div>
<!-- 右侧控制面板 --> <!-- 右侧控制面板 -->
<div class="control-panel" v-loading="infoLoading" element-loading-background="rgba(122, 122, 122, 0.8)"> <div class="control-panel">
<!-- 切换模式 --> <!-- 切换模式 -->
<div class="control-group"> <div class="control-group">
<div class="control-title"> <div class="control-title">
@ -175,15 +175,12 @@
设备号:{{ commit_uid }} 设备号:{{ commit_uid }}
</div> </div>
<div class="bottom_item"> <div class="bottom_item">
<el-button class="restart-button" :loading="timeLoading" @click="setDeviceTime"> <!-- <el-button class="restart-button" @click="restartSystem">
同步时间
</el-button>
<el-button class="restart-button" @click="refresh">
<el-icon> <el-icon>
<RefreshRight /> <RefreshRight />
</el-icon> </el-icon>
刷新 远程重启
</el-button> </el-button> -->
<el-button class="back-button" @click="goBack"> <el-button class="back-button" @click="goBack">
<el-icon> <el-icon>
<HomeFilled /> <HomeFilled />
@ -204,7 +201,7 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="brightnessDialogVisible = false">取消</el-button> <el-button @click="brightnessDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmBrightness">确认</el-button> <el-button type="primary" style="background-color: #25b15f; border-color: #25b15f;" @click="confirmBrightness">确认</el-button>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@ -217,42 +214,19 @@ import { useRouter } from 'vue-router'
import RPC from '@/utils/rpc' import RPC from '@/utils/rpc'
import dayjs from 'dayjs' import dayjs from 'dayjs'
const commit_uid = localStorage.getItem('central_control_comm_uid') const commit_uid = sessionStorage.getItem('central_control_comm_uid')
// const commit_uid = '868656077692382'
const router = useRouter() const router = useRouter()
const currentTime = ref('') const currentTime = ref('')
const deviceTime = ref('')
const systemRuntime = ref('38分钟') const systemRuntime = ref('38分钟')
const brightnessDialogVisible = ref(false) const brightnessDialogVisible = ref(false)
const currentBrightness = ref(50) const currentBrightness = ref(50)
const selectedChannelName = ref(null) const selectedChannelName = ref(null)
const timeLoading = ref(false)
const isUpDateTimeing = ref(false)
const getDeviceTime = () => {
isUpDateTimeing.value = true
RPC.post('/m9z/getDeviceTime', { comm_uid: commit_uid }).then(res => {
deviceTime.value = res.deviceTime
}).finally(() => {
isUpDateTimeing.value = false
})
}
const isOnline = ref(false)
const setDeviceTime = () => {
timeLoading.value = true
RPC.post('/m9z/setDeviceTime', { comm_uid: commit_uid, device_time: dayjs().format('YYYY-MM-DD HH:mm:ss') }).then(res => {
ElMessage.success('同步时间成功')
getDeviceTime()
}).finally(() => {
timeLoading.value = false
})
}
const week = ref('') const week = ref('')
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']; const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const phaseLoading = ref(false)
// 电力参数数据 // 电力参数数据
@ -386,9 +360,7 @@ const info = ref({
}) })
const isSync = ref(true) const isSync = ref(true)
const infoLoading = ref(false)
const getInfo = () => { const getInfo = () => {
infoLoading.value = true
isSync.value = true isSync.value = true
RPC.post('/m9z/getDeviceManualInfo', { comm_uid: commit_uid }).then(res => { RPC.post('/m9z/getDeviceManualInfo', { comm_uid: commit_uid }).then(res => {
info.value = res info.value = res
@ -397,39 +369,6 @@ const getInfo = () => {
}, 1000); }, 1000);
}).finally(() => { }).finally(() => {
isInitData.value = false isInitData.value = false
infoLoading.value = false
})
}
const refresh = () => {
getInfo()
phaseLoading.value = true
RPC.post('/adl400/readPhaseData', { comm_uid: commit_uid }).then(res => {
powerPhases.value[0].voltage = res.aVoltage ? res.aVoltage + 'V' : '0V'
powerPhases.value[0].current = res.aElectricCurrent ? res.aElectricCurrent + 'A' : '0A'
powerPhases.value[0].power = res.aPower ? res.aPower + 'kW' : '0kW'
powerPhases.value[0].factor = res.aPowerFactor ? res.aPowerFactor : 0
powerPhases.value[1].voltage = res.bVoltage ? res.bVoltage + 'V' : '0V'
powerPhases.value[1].current = res.bElectricCurrent ? res.bElectricCurrent + 'A' : '0A'
powerPhases.value[1].power = res.bPower ? res.bPower + 'kW' : '0kW'
powerPhases.value[1].factor = res.bPowerFactor ? res.bPowerFactor : 0
powerPhases.value[2].voltage = res.cVoltage ? res.cVoltage + 'V' : '0V'
powerPhases.value[2].current = res.cElectricCurrent ? res.cElectricCurrent + 'A' : '0A'
powerPhases.value[2].power = res.cPower ? res.cPower + 'kW' : '0kW'
powerPhases.value[2].factor = res.cPowerFactor ? res.cPowerFactor : 0
energyParams.value.activeEnergy = res.activeEnergy ? res.activeEnergy + 'kW·h' : '0kW·h'
energyParams.value.activePower = res.activePower ? res.activePower + 'kW' : '0kW'
energyParams.value.powerFactor = res.powerFactor ? res.powerFactor : 0
}).finally(() => {
phaseLoading.value = false
})
RPC.post('/m9z/getDeviceSunRiseTime', { comm_uid: commit_uid }).then(res => {
sunriseTime.value = res.Sunrise ? dayjs(res.Sunrise).format('HH:mm:ss') : ''
sunsetTime.value = res.Sunset ? dayjs(res.Sunset).format('HH:mm:ss') : ''
}) })
} }
@ -449,6 +388,39 @@ watch(info, () => {
const sunriseTime = ref('') const sunriseTime = ref('')
const sunsetTime = ref('') const sunsetTime = ref('')
RPC.post('/m9z/getDeviceSunRiseTime', { comm_uid: commit_uid }).then(res => {
sunriseTime.value = res.Sunrise ? dayjs(res.Sunrise).format('HH:mm:ss') : ''
sunsetTime.value = res.Sunset ? dayjs(res.Sunset).format('HH:mm:ss') : ''
}).finally(() => {
RPC.post('/m9z/getDeviceManualInfo', { comm_uid: commit_uid }).then(res => {
info.value = res
setTimeout(() => {
isSync.value = false
}, 1000);
}).finally(() => {
isInitData.value = false
RPC.post('/adl400/readPhaseData', { comm_uid: commit_uid }).then(res => {
powerPhases.value[0].voltage = res.aVoltage + 'V'
powerPhases.value[0].current = res.aElectricCurrent + 'A'
powerPhases.value[0].power = res.aPower + 'kW'
powerPhases.value[0].factor = res.aPowerFactor
powerPhases.value[1].voltage = res.bVoltage + 'V'
powerPhases.value[1].current = res.bElectricCurrent + 'A'
powerPhases.value[1].power = res.bPower + 'kW'
powerPhases.value[1].factor = res.bPowerFactor
powerPhases.value[2].voltage = res.cVoltage + 'V'
powerPhases.value[2].current = res.cElectricCurrent + 'A'
powerPhases.value[2].power = res.cPower + 'kW'
powerPhases.value[2].factor = res.cPowerFactor
energyParams.value.activeEnergy = res.activeEnergy + 'kW·h'
energyParams.value.activePower = res.activePower + "kW"
energyParams.value.powerFactor = res.powerFactor
})
})
})
// 更新时间 // 更新时间
const updateTime = () => { const updateTime = () => {
@ -463,9 +435,6 @@ const updateTime = () => {
week.value = weekDays[now.getDay()] week.value = weekDays[now.getDay()]
currentTime.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` currentTime.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
if (deviceTime.value && !isUpDateTimeing.value) {
deviceTime.value = dayjs(deviceTime.value).valueOf() + 1000
}
} }
// 切换回路状态 // 切换回路状态
@ -473,17 +442,7 @@ const toggleCircuit = (circuitName) => {
if (!info.value.disable) return ElMessage.warning('请切换至手动模式') if (!info.value.disable) return ElMessage.warning('请切换至手动模式')
const circuit = info.value.loops.find(c => c.name === circuitName) const circuit = info.value.loops.find(c => c.name === circuitName)
if (circuit) { if (circuit) {
ElMessageBox.confirm( circuit.is_open = !circuit.is_open
`确认${circuit.is_open ? '关' : '开'}灯?`,
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
circuit.is_open = !circuit.is_open
})
} }
} }
@ -523,18 +482,8 @@ const openGlobalBrightnessDialog = () => {
// 全开灯光 // 全开灯光
const openLightSettings = () => { const openLightSettings = () => {
if (!info.value.disable) return ElMessage.warning('请切换至手动模式') if (!info.value.disable) return ElMessage.warning('请切换至手动模式')
ElMessageBox.confirm( info.value.loops.forEach(circuit => {
`确认全开?`, circuit.is_open = true
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
info.value.loops.forEach(circuit => {
circuit.is_open = true
})
}) })
// dimmingChannels.value.forEach(channel => { // dimmingChannels.value.forEach(channel => {
// channel.brightness = 100 // channel.brightness = 100
@ -544,18 +493,8 @@ const openLightSettings = () => {
// 全关灯光 // 全关灯光
const turnOffAll = () => { const turnOffAll = () => {
if (!info.value.disable) return ElMessage.warning('请切换至手动模式') if (!info.value.disable) return ElMessage.warning('请切换至手动模式')
ElMessageBox.confirm( info.value.loops.forEach(circuit => {
`确认全关?`, circuit.is_open = false
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
info.value.loops.forEach(circuit => {
circuit.is_open = false
})
}) })
// dimmingChannels.value.forEach(channel => { // dimmingChannels.value.forEach(channel => {
// channel.brightness = 0 // channel.brightness = 0
@ -587,9 +526,6 @@ const restartSystem = () => {
} }
const changeMode = () => { const changeMode = () => {
if (!isOnline.value) {
return ElMessage.error('设备不在线')
}
ElMessageBox.confirm( ElMessageBox.confirm(
`此操作将切换到${info.value.disable ? '自动' : '手动'}模式,是否继续?`, `此操作将切换到${info.value.disable ? '自动' : '手动'}模式,是否继续?`,
'提示', '提示',
@ -598,63 +534,39 @@ const changeMode = () => {
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
} }
).then(() => { )
info.value.disable = !info.value.disable .then(() => {
// ElMessage({ info.value.disable = !info.value.disable
// type: 'success', // ElMessage({
// message: `切换${info.value.disable ? '手动' : '自动'}成功`, // type: 'success',
// }) // message: `切换${info.value.disable ? '手动' : '自动'}成功`,
}) // })
})
} }
let timeInterval = null let timeInterval = null
// 处理 session 恢复后的重新加载
const handleSessionRestored = () => {
console.log('Session 已恢复,重新加载数据')
getInfo()
}
onMounted(() => { onMounted(() => {
updateTime() updateTime()
timeInterval = setInterval(updateTime, 1000) timeInterval = setInterval(updateTime, 1000)
RPC.post('/lightm/rtu/isOnline', { commUid: commit_uid }).then(res => {
console.log('/lightm/rtu/isOnlin', res) // 监听 session 恢复事件
isOnline.value = res window.addEventListener('m9z:session-restored', handleSessionRestored)
if (isOnline.value) {
getInfo()
RPC.post('/m9z/getDeviceSunRiseTime', { comm_uid: commit_uid }).then(res => {
sunriseTime.value = res.Sunrise ? dayjs(res.Sunrise).format('HH:mm:ss') : ''
sunsetTime.value = res.Sunset ? dayjs(res.Sunset).format('HH:mm:ss') : ''
})
phaseLoading.value = true
RPC.post('/adl400/readPhaseData', { comm_uid: commit_uid }).then(res => {
powerPhases.value[0].voltage = res.aVoltage ? res.aVoltage + 'V' : '0V'
powerPhases.value[0].current = res.aElectricCurrent ? res.aElectricCurrent + 'A' : '0A'
powerPhases.value[0].power = res.aPower ? res.aPower + 'kW' : '0kW'
powerPhases.value[0].factor = res.aPowerFactor ? res.aPowerFactor : 0
powerPhases.value[1].voltage = res.bVoltage ? res.bVoltage + 'V' : '0V'
powerPhases.value[1].current = res.bElectricCurrent ? res.bElectricCurrent + 'A' : '0A'
powerPhases.value[1].power = res.bPower ? res.bPower + 'kW' : '0kW'
powerPhases.value[1].factor = res.bPowerFactor ? res.bPowerFactor : 0
powerPhases.value[2].voltage = res.cVoltage ? res.cVoltage + 'V' : '0V'
powerPhases.value[2].current = res.cElectricCurrent ? res.cElectricCurrent + 'A' : '0A'
powerPhases.value[2].power = res.cPower ? res.cPower + 'kW' : '0kW'
powerPhases.value[2].factor = res.cPowerFactor ? res.cPowerFactor : 0
energyParams.value.activeEnergy = res.activeEnergy ? res.activeEnergy + 'kW·h' : '0kW·h'
energyParams.value.activePower = res.activePower ? res.activePower + 'kW' : '0kW'
energyParams.value.powerFactor = res.powerFactor ? res.powerFactor : 0
}).finally(() => {
phaseLoading.value = false
})
getDeviceTime()
} else {
ElMessage.error('设备不在线')
}
})
}) })
onUnmounted(() => { onUnmounted(() => {
if (timeInterval) { if (timeInterval) {
clearInterval(timeInterval) clearInterval(timeInterval)
} }
// 移除事件监听
window.removeEventListener('m9z:session-restored', handleSessionRestored)
}) })
</script> </script>
@ -665,7 +577,8 @@ onUnmounted(() => {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
overflow: auto; overflow: auto;
background: center / cover url('@/assets/images/handle_control_bg.png') no-repeat; background: center / cover url('@/assets/images/背景小@2x.png') no-repeat;
background-color: #0A3933;
} }
.main-content { .main-content {
@ -700,7 +613,7 @@ onUnmounted(() => {
} }
.system-status { .system-status {
min-width: 300px; min-width: 200px;
font-size: 18px; font-size: 18px;
font-weight: 300; font-weight: 300;
width: 270px; width: 270px;
@ -751,7 +664,7 @@ onUnmounted(() => {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-bottom: 15px; margin-bottom: 15px;
background-color: #212329; background-color: #24806B;
width: 30px; width: 30px;
height: 100%; height: 100%;
} }
@ -812,9 +725,9 @@ onUnmounted(() => {
} }
.circuit-status.active { .circuit-status.active {
background: rgba(33, 150, 243, 0.2); background: rgba(31, 223, 130, 0.2);
border-color: rgba(33, 150, 243, 0.5); border-color: rgba(35, 219, 62, 0.764);
color: #64b5f6; color: #25b15f;
} }
.circuit-icon { .circuit-icon {
@ -883,9 +796,9 @@ onUnmounted(() => {
} }
.brightness-display.active { .brightness-display.active {
background: rgba(33, 150, 243, 0.3); background: rgba(31, 223, 130, 0.2);
border-color: rgba(33, 150, 243, 0.6); border-color: rgba(35, 219, 62, 0.764);
color: #64b5f6; color: #25b15f;
box-shadow: 0 0 10px rgba(33, 150, 243, 0.3); box-shadow: 0 0 10px rgba(33, 150, 243, 0.3);
} }
@ -914,7 +827,7 @@ onUnmounted(() => {
} }
.time-item { .time-item {
color: rgba(255, 255, 255, 0.7); color: #fff;
font-size: 16px; font-size: 16px;
margin: 0 20px; margin: 0 20px;
} }
@ -930,9 +843,11 @@ onUnmounted(() => {
} }
.back-button { .back-button {
background-color: #000; // background-color: #000;
color: #fff; color: #fff;
border: #000; border: #000;
background: linear-gradient(180deg, rgba(223, 196, 137, 1) 0%, rgba(190, 151, 99, 1) 100%);
color: #fff;
} }
} }
@ -986,8 +901,8 @@ onUnmounted(() => {
} }
.mode-button.active { .mode-button.active {
background: #ffffff; background: linear-gradient(180deg, rgba(51, 224, 188, 1) 0%, rgba(5, 149, 117, 1) 100%);
color: #1976d2; color: #fff;
} }
.setting-button { .setting-button {
@ -1054,11 +969,11 @@ onUnmounted(() => {
} }
:deep(.brightness-slider .el-slider__bar) { :deep(.brightness-slider .el-slider__bar) {
background: #1976d2; background: #25b15f;
} }
:deep(.brightness-slider .el-slider__button) { :deep(.brightness-slider .el-slider__button) {
border-color: #1976d2; border-color: #25b15f;
} }
:deep(.brightness-slider .el-slider__stop) { :deep(.brightness-slider .el-slider__stop) {

@ -3,14 +3,17 @@
<!-- 主要内容 --> <!-- 主要内容 -->
<div class="main-content"> <div class="main-content">
<!-- 时间显示 --> <!-- 时间显示 -->
<div class="time-display"> <!-- <div class="time-display">
{{ currentTime }} {{ currentTime }}
</div> -->
<div class="logo">
<!-- <img src="@/assets/images/绿盟.png" alt=""> -->
</div> </div>
<!-- 主标题 --> <!-- 主标题 -->
<div class="main-title"> <div class="main-title">
<div class="title-decoration left"></div> <div class="title-decoration left"></div>
<h1>{{ SysBasicTgkTitle }}</h1> <h1>直流照明监控平台</h1>
<div class="title-decoration right"></div> <div class="title-decoration right"></div>
</div> </div>
@ -32,17 +35,10 @@
设备号:{{ commit_uid }} 设备号:{{ commit_uid }}
</div> </div>
<div class="status-item"> <div class="status-item">
设备名称:{{ device_name }} 版本号:{{ version }}
</div> </div>
<div class="status-item"> <div class="status-item">
版本号:1.0.0 系统状态:<span class="status-online">开启</span>
</div>
<div class="status-item" v-if="status !== -1">
开关量:
<span v-if="status === 1" class="status-online">开启</span>
<span v-else-if="status === 0" class="status-offline">关闭</span>
<span v-else class="status-offline">{{ status }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -56,7 +52,8 @@ import {
Operation, Operation,
Setting, Setting,
VideoPlay, VideoPlay,
Bell Bell,
Tools
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import RPC from '@/utils/rpc' import RPC from '@/utils/rpc'
const currentTime = ref('') const currentTime = ref('')
@ -64,31 +61,24 @@ const router = useRouter()
const route = useRoute() const route = useRoute()
// console.log(route.query.comm_uid) // console.log(route.query.comm_uid)
if (route.query.comm_uid) { if (route.query.comm_uid) {
localStorage.setItem('central_control_comm_uid', route.query.comm_uid) sessionStorage.setItem('central_control_comm_uid', route.query.comm_uid)
}
if (route.query.deviceName) {
localStorage.setItem('central_control_device_name', route.query.deviceName)
} }
if (route.query.temperature) { const version = ref('1.0.0')
localStorage.setItem('temperature', route.query.temperature) const commit_uid = ref('')
} RPC.post('/m9z/touchdisplay/getDeviceID').then(res => {
commit_uid.value = res.deviceId
version.value = res.version
const commit_uid = localStorage.getItem('central_control_comm_uid') sessionStorage.setItem('central_control_comm_uid', res.deviceId)
const device_name = localStorage.getItem('central_control_device_name') sessionStorage.setItem('central_control_meterAddr', res.meterAddr)
const status = ref(-1)
onMounted(() => {
RPC.post('/m9z/getRelayStatus', { comm_uid: commit_uid }).then(res => {
status.value = res
})
}) })
const functionCards = [ const functionCards = [
{ key: 'monitor', title: '实时监控', icon: Camera, route: '/monitoring' }, { key: 'monitor', title: '实时监控', icon: Camera, route: '/monitoring' },
{ key: 'control', title: '手动控制', icon: Operation, route: '/handle-control' }, { key: 'control', title: '手动控制', icon: Operation, route: '/handle-control' },
{ key: 'setting', title: '参数设置', icon: Setting, route: '/params-config' }, { key: 'setting', title: '参数设置', icon: Setting, route: '/params-config' },
{ key: 'auto', title: '自动模式', icon: VideoPlay, route: '/automatic-mode' }, { key: 'auto', title: '自动模式', icon: VideoPlay, route: '/automatic-mode' },
{ key: 'alarm', title: '报警浏览', icon: Bell, route: '/alarm-information' } { key: 'alarm', title: '报警浏览', icon: Bell, route: '/alarm-information' },
{ key: 'system', title: '系统设置', icon: Tools, route: '/system-settings' },
] ]
const updateTime = () => { const updateTime = () => {
@ -114,29 +104,47 @@ const handleCardClick = (key) => {
} }
} }
} }
const SysBasicTgkTitle = localStorage.getItem('SysBasicTgkTitle') || '智能中控装置'
let timeInterval = null let timeInterval = null
// 处理 session 恢复后的重新加载
const handleSessionRestored = () => {
console.log('Session 已恢复,重新加载数据')
// Menu 页面重新获取设备信息
RPC.post('/m9z/touchdisplay/getDeviceID').then(res => {
commit_uid.value = res.deviceId
version.value = res.version
sessionStorage.setItem('central_control_comm_uid', res.deviceId)
sessionStorage.setItem('central_control_meterAddr', res.meterAddr)
})
}
onMounted(() => { onMounted(() => {
updateTime() updateTime()
timeInterval = setInterval(updateTime, 1000) timeInterval = setInterval(updateTime, 1000)
// 监听 session 恢复事件
window.addEventListener('m9z:session-restored', handleSessionRestored)
}) })
onUnmounted(() => { onUnmounted(() => {
if (timeInterval) { if (timeInterval) {
clearInterval(timeInterval) clearInterval(timeInterval)
} }
// 移除事件监听
window.removeEventListener('m9z:session-restored', handleSessionRestored)
}) })
</script> </script>
<style scoped> <style scoped lang="scss">
.smart-control-app { .smart-control-app {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
background: center / cover url('@/assets/images/backgroundImg.png') no-repeat; // background: center / cover url('@/assets/images/首页背景@2x.png') no-repeat;
background-color: #008000;
} }
.main-content { .main-content {
@ -149,6 +157,16 @@ onUnmounted(() => {
padding: 40px; padding: 40px;
box-sizing: border-box; box-sizing: border-box;
justify-content: center; justify-content: center;
.logo {
position: absolute;
top: 20px;
left: 20px;
img {
height: 30px;
}
}
} }
.time-display { .time-display {
@ -180,7 +198,7 @@ onUnmounted(() => {
.title-decoration { .title-decoration {
width: 80px; width: 80px;
height: 6px; height: 6px;
background: linear-gradient(90deg, transparent, #64b5f6, #42a5f5, #64b5f6, transparent); background: linear-gradient(90deg, transparent, #64f6be, #42f5a4, #54e98d, transparent);
position: relative; position: relative;
animation: titleGlow 2s ease-in-out infinite alternate; animation: titleGlow 2s ease-in-out infinite alternate;
} }
@ -193,7 +211,7 @@ onUnmounted(() => {
transform: translateX(-50%); transform: translateX(-50%);
width: 12px; width: 12px;
height: 12px; height: 12px;
background: radial-gradient(circle, #64b5f6 0%, #42a5f5 70%, transparent 100%); background: linear-gradient(90deg, transparent, #64f6be, #42f5a4, #54e98d, transparent);
border-radius: 50%; border-radius: 50%;
box-shadow: box-shadow:
0 0 15px #64b5f6, 0 0 15px #64b5f6,
@ -330,11 +348,6 @@ onUnmounted(() => {
font-weight: bold; font-weight: bold;
} }
.status-offline {
color: #f12323;
font-weight: bold;
}
/* Element Plus 卡片样式重写 */ /* Element Plus 卡片样式重写 */
:deep(.el-card) { :deep(.el-card) {
background: transparent !important; background: transparent !important;

@ -20,7 +20,7 @@
<!-- 左侧电力参数区域 --> <!-- 左侧电力参数区域 -->
<div class="power-section"> <div class="power-section">
<!-- ABC三相电力参数 --> <!-- ABC三相电力参数 -->
<div class="power-params" v-loading="phaseLoading" element-loading-background="rgba(122, 122, 122, 0.8)"> <div class="power-params">
<div v-for="phase in powerPhases" :key="phase.name" class="phase-card"> <div v-for="phase in powerPhases" :key="phase.name" class="phase-card">
<div class="phase-name"><span>{{ phase.name }}</span>相</div> <div class="phase-name"><span>{{ phase.name }}</span>相</div>
<div class="param-list"> <div class="param-list">
@ -73,89 +73,73 @@
</el-icon> </el-icon>
当前模式 当前模式
</div> </div>
<el-button class="mode-button active">{{ info.Mode ? '手动' : '自动' }}</el-button> <el-button style="margin-bottom: 0;" class="mode-button active">{{ info.Mode ? '手动' : '自动' }}</el-button>
</div> </div>
</div> </div>
<div v-loading="infoLoading" element-loading-background="rgba(122, 122, 122, 0.8)"> <div class="fault-section" :class="{ 'fault-section-expanded': faultListExpanded }">
<div class="circuit-section"> <div class="fault-header" @click="toggleFaultList">
<!-- 上排回路 --> <div>
<div class="circuit-row"> <div class="fault-title">
<div v-for="(circuit, index) in info.Loops.slice(0, 5)" :key="circuit.name" class="circuit-item"> 当前故障
<div class="circuit-status" :class="{ active: circuit.is_open }"> <span v-if="currentFaults.length" class="fault-count">{{ currentFaults.length }}个</span>
直流回路{{ index + 1 }}:{{ circuit.is_open ? '打开' : '关闭' }} <span class="fault-subtitle">当前扫描设备数:{{ scanDeviceCount }} · {{ faultStatusText }}</span>
</div>
<!-- <div class="circuit-icon" :class="{ active: circuit.is_open }">
<el-icon>
<Sunny />
</el-icon>
</div> -->
<div class="circuitStatus">
<div class="line">
<div class="line_item">电压: <span>{{ circuit.V ? circuit.V?.toFixed(2) : 0 }}V</span></div>
<div class="line_item">电流: <span>{{ circuit.A ? circuit.A?.toFixed(2) : 0 }}A</span></div>
</div>
<div class="line">
<div class="line_item">功率: <span>{{ circuit.Kw ? circuit.Kw?.toFixed(3) : 0 }}kW</span></div>
</div>
</div>
</div> </div>
</div> </div>
<div class="fault-actions">
<!-- 下排回路 --> <el-button v-if="currentFaults.length" class="fault-toggle-button" @click.stop="toggleFaultList">
<div class="circuit-row"> {{ faultListExpanded ? '隐藏' : '展开' }}
<div v-for="(circuit, index) in info.Loops.slice(5, 10)" :key="circuit.name" class="circuit-item"> </el-button>
<div class="circuit-status" :class="{ active: circuit.is_open }"> <el-button class="fault-refresh-button" :loading="faultRefreshing" @click.stop="refreshCurrentFaults">
直流回路{{ index + 6 }}:{{ circuit.is_open ? '打开' : '关闭' }} <el-icon>
</div> <RefreshRight />
<!-- <div class="circuit-icon" :class="{ active: circuit.is_open }"> </el-icon>
<el-icon> 刷新
<Sunny /> </el-button>
</el-icon>
</div> -->
<div class="circuitStatus">
<div class="line">
<div class="line_item">电压: <span>{{ circuit.V ? circuit.V?.toFixed(2) : 0 }}V</span></div>
<div class="line_item">电流: <span>{{ circuit.A ? circuit.A?.toFixed(2) : 0 }}A</span></div>
</div>
<div class="line">
<div class="line_item">功率: <span>{{ circuit.Kw ? circuit.Kw?.toFixed(3) : 0 }}kW</span></div>
</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="dimming-section"> <div v-if="currentFaults.length && faultListExpanded" class="fault-list" @click.stop>
<!-- 上排调光通道 --> <div v-for="(fault, index) in visibleCurrentFaults" :key="`${fault.loop_idx}-${fault.code}-${fault.message}-${index}`" class="fault-item">
<div class="dimming-row"> <span class="fault-loop">回路{{ fault.loop_idx }}</span>
<div v-for="(channel, index) in info.Loops.slice(0, 5)" :key="channel.name" class="dimming-item"> <span class="fault-message" :title="fault.code ? `${fault.message}(${fault.code})` : fault.message">{{ fault.message }}</span>
<div class="channel-label">调光通道{{ index + 1 }}</div> </div>
<div class="brightness-display" :class="{ active: channel.pwm > 0 }"> <div v-if="hiddenFaultCount" class="fault-item fault-more">+{{ hiddenFaultCount }}</div>
{{ channel.pwm }}% </div>
</div>
<div class="circuit-section">
<div class="circuit-grid">
<div v-for="(circuit, index) in monitorLoops" :key="`${circuit.name}-${index}`" class="circuit-item">
<div class="circuit-status" :class="{ active: circuit.is_open }">
直流回路{{ loopNo(circuit, index) }}:{{ circuit.is_open ? '打开' : '关闭' }}
</div>
<div class="circuitStatus">
<div class="line">
<div class="line_item">电压: <span>{{ circuit.V ? circuit.V?.toFixed(2) : 0 }}V</span></div>
<div class="line_item">电流: <span>{{ circuit.A ? circuit.A?.toFixed(2) : 0 }}A</span></div>
</div>
<div class="line">
<div class="line_item line_item_full">功率: <span>{{ circuit.Kw ? circuit.Kw?.toFixed(3) : 0 }}kW</span></div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- 下排调光通道 --> </div>
<div class="dimming-row"> <div class="dimming-section">
<div v-for="(channel, index) in info.Loops.slice(5, 10)" :key="channel.name" class="dimming-item"> <div class="dimming-grid">
<div class="channel-label">调光通道{{ index + 6 }}</div> <div v-for="(channel, index) in monitorLoops" :key="`${channel.name}-${index}`" class="dimming-item">
<div class="brightness-display" :class="{ active: channel.pwm > 0 }"> <div class="channel-label">调光通道{{ loopNo(channel, index) }}</div>
{{ channel.pwm }}% <div class="brightness-display" :class="{ active: channel.pwm > 0 }">
</div> {{ channel.pwm || 0 }}%
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="chartBox"> <!-- <div class="chartBox">
<div class="search"> <div class="search">
<el-date-picker value-format="YYYY-MM-DD" v-model="recdDate" type="date" placeholder="请选择时间" /> <el-date-picker value-format="YYYY-MM-DD" v-model="recdDate" type="date" placeholder="请选择时间" />
<el-button type="primary" :loading="lineDataLoading" @click="getLineData">查询</el-button> <el-button type="primary" class="mode-button active" style="width:70px;height: 32px; margin-bottom: 0;" :loading="lineDataLoading" @click="getLineData">查询</el-button>
<div class="info_item">当前功率:{{ energyParams.activePower }}</div>
<div class="info_item">今日峰值:{{ maxEnergy }}kW</div>
<div class="info_item">平均功率:{{ agvEnergy }}kW</div>
</div> </div>
<Linechart :lineData="lineData"></Linechart> <Linechart :lineData="lineData"></Linechart>
</div> </div> -->
</div> </div>
</div> </div>
</div> </div>
@ -172,12 +156,6 @@
<div style="color:#fff"> <div style="color:#fff">
设备号:{{ commit_uid }} 设备号:{{ commit_uid }}
</div> </div>
<div style="color:#fff">
设备名称:{{ device_name }}
</div>
<div style="color:#fff" v-if="temperature">
温度:{{ temperature }} ℃
</div>
<div class="bottom_item"> <div class="bottom_item">
<!-- <el-button class="restart-button" @click="restartSystem"> <!-- <el-button class="restart-button" @click="restartSystem">
<el-icon> <el-icon>
@ -213,15 +191,14 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue' import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import RPC from '@/utils/rpc' import RPC from '@/utils/rpc'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import Linechart from '@/components/Linechart.vue'; import Linechart from '@/components/Linechart.vue';
const commit_uid = localStorage.getItem('central_control_comm_uid') // const commit_uid = '868656077692382'
const device_name = localStorage.getItem('central_control_device_name') const commit_uid = sessionStorage.getItem('central_control_comm_uid')
const temperature = localStorage.getItem('temperature')
const router = useRouter() const router = useRouter()
const currentTime = ref('') const currentTime = ref('')
@ -229,6 +206,8 @@ const systemRuntime = ref('38分钟')
const brightnessDialogVisible = ref(false) const brightnessDialogVisible = ref(false)
const currentBrightness = ref(50) const currentBrightness = ref(50)
const selectedChannelName = ref(null) const selectedChannelName = ref(null)
const faultRefreshing = ref(false)
const faultListExpanded = ref(false)
const week = ref('') const week = ref('')
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']; const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
@ -249,57 +228,14 @@ function transformHourlyData(data) {
const lineData = ref([]) const lineData = ref([])
const recdDate = ref(dayjs().add(-1, 'day').format('YYYY-MM-DD')) const recdDate = ref(dayjs().add(-1, 'day').format('YYYY-MM-DD'))
const lineDataLoading = ref(false) const lineDataLoading = ref(false)
const agvEnergy = ref(0) // const getLineData = () => {
const maxEnergy = ref(0) // lineDataLoading.value = true
const getLineData = () => { // RPC.post('/lightm/rtu/his', { commUid: commit_uid, recdDate: recdDate.value }).then(res => {
lineDataLoading.value = true // lineData.value = transformHourlyData(res)
// RPC.post('/lightm/rtu/his', { commUid: commit_uid, recdDate: recdDate.value }).then(res => { // }).finally(() => {
// lineData.value = transformHourlyData(res) // lineDataLoading.value = false
// }).finally(() => { // })
// lineDataLoading.value = false // }
// })
// RPC.post('/lightm/rtu/minHis', { CommUid: commit_uid, recdDate: recdDate.value }).then((res) => {
// // 直流电流、电源、功率
// // console.log(res)
// agvEnergy.value = res.agvEnergy?.toFixed(3)
// maxEnergy.value = res.maxEnergy?.toFixed(3)
// // lineData.value = transformminHisData(res.minutesEnergy)
// }).finally(() => {
// lineDataLoading.value = false
// })
RPC.post('/hisMinEnergy/getMinEnergyListByDay', { CommUid: commit_uid, day: recdDate.value }).then((res) => {
// 直流电流、电源、功率
console.log(res)
agvEnergy.value = res.avg?.toFixed(3) || 0
maxEnergy.value = res.max?.toFixed(3) || 0
const arr = []
for (let k in res?.energy) {
arr.push(k)
}
lineData.value = arr.map(item => {
return {
name: item,
value: res?.energy[item]
}
})
// agvEnergy.value = res.agvEnergy?.toFixed(3)
// maxEnergy.value = res.maxEnergy?.toFixed(3)
// lineData.value = transformminHisData(res.minutesEnergy)
}).finally(() => {
lineDataLoading.value = false
})
}
const transformminHisData = arr => {
if (!arr) return
return arr.map(item => {
return {
name: dayjs(item.record_time).format('HH:mm'),
value: item.energy
}
})
}
// getLineData() // getLineData()
// 电力参数数据 // 电力参数数据
@ -329,7 +265,7 @@ const powerPhases = ref([
const energyParams = ref({ const energyParams = ref({
dcVoltage: '0.0V', dcVoltage: '0.0V',
activeEnergy: '0kW·h', activeEnergy: '0kwh',
activePower: '0kW', activePower: '0kW',
powerFactor: '0' powerFactor: '0'
}) })
@ -337,6 +273,7 @@ const energyParams = ref({
const info = ref({ const info = ref({
"Mode": 0, "Mode": 0,
"scan_device_count": 10,
"backup_byte1": 0, "backup_byte1": 0,
"Loops": [ "Loops": [
{ {
@ -433,64 +370,176 @@ const info = ref({
"backup_byte4": 0 "backup_byte4": 0
}) })
const infoLoading = ref(false) const scanDeviceCount = computed(() => {
const phaseLoading = ref(false) return info.value?.scan_device_count ?? info.value?.ScanDeviceCount ?? info.value?.Loops?.length ?? 0
})
const monitorDisplayLoopCount = 10
const createEmptyMonitorLoop = (index) => ({
index,
name: `#${index + 1}`,
is_open: false,
pwm: 0,
A: 0,
V: 0,
Kw: 0,
})
const normalizeMonitorLoop = (loop, index) => {
const normalized = createEmptyMonitorLoop(index)
if (!loop) return normalized
return {
...normalized,
...loop,
index: loop.index ?? loop.Index ?? index,
name: loop.name ?? loop.Name ?? normalized.name,
is_open: loop.is_open ?? loop.IsOpen ?? normalized.is_open,
pwm: loop.pwm ?? loop.Pwm ?? normalized.pwm,
A: loop.A ?? normalized.A,
V: loop.V ?? normalized.V,
Kw: loop.Kw ?? normalized.Kw,
}
}
const monitorLoops = computed(() => {
const loops = info.value?.Loops || []
return Array.from({ length: monitorDisplayLoopCount }, (_, index) => normalizeMonitorLoop(loops[index], index))
})
const faultPollingLoopCount = computed(() => {
const count = Number(scanDeviceCount.value)
if (!Number.isFinite(count) || count <= 0) return monitorDisplayLoopCount
return Math.min(count, monitorDisplayLoopCount)
})
const loopNo = (loop, index) => {
const loopIndex = loop?.index ?? loop?.Index
return Number.isInteger(loopIndex) ? loopIndex + 1 : index + 1
}
const maxVisibleFaultCount = 10
const currentFaults = computed(() => {
const loops = monitorLoops.value.slice(0, faultPollingLoopCount.value)
return loops.flatMap((loop, index) => {
const loopIdx = loop.loop_idx || loop.LoopIdx || loop.index + 1 || index + 1
const faults = loop.faults || loop.Faults || []
if (Array.isArray(faults) && faults.length > 0) {
return faults.map((fault) => ({
loop_idx: fault.loop_idx || loopIdx,
message: fault.message || fault.fault_msg || fault.Message || '',
code: fault.code || fault.fault_code || fault.Code || '',
})).filter((fault) => fault.message)
}
const message = loop.fault_msg || loop.FaultMsg
if ((loop.has_fault || loop.HasFault) && message) {
return [{
loop_idx: loopIdx,
message,
code: loop.fault_code || loop.FaultCode || '',
}]
}
return []
})
})
const visibleCurrentFaults = computed(() => {
if (currentFaults.value.length > maxVisibleFaultCount) {
return currentFaults.value.slice(0, maxVisibleFaultCount - 1)
}
return currentFaults.value
})
const hiddenFaultCount = computed(() => currentFaults.value.length - visibleCurrentFaults.value.length)
const faultStatusText = computed(() => {
if (!currentFaults.value.length) return '当前无故障'
return faultListExpanded.value ? '已展开' : '已隐藏'
})
const toggleFaultList = () => {
if (!currentFaults.value.length) return
faultListExpanded.value = !faultListExpanded.value
}
watch(currentFaults, (faults) => {
if (!faults.length) {
faultListExpanded.value = false
}
})
const loadDeviceStatus = () => {
return RPC.post('/m9z/getDeviceStatus2', { comm_uid: commit_uid }).then((res) => {
info.value = res
})
}
const loadPowerData = () => {
return RPC.post('/adl400/readPhaseData', { comm_uid: commit_uid }).then(res => {
powerPhases.value[0].voltage = res.aVoltage + 'V'
powerPhases.value[0].current = res.aElectricCurrent + 'A'
powerPhases.value[0].power = res.aPower + 'kW'
powerPhases.value[0].factor = res.aPowerFactor
powerPhases.value[1].voltage = res.bVoltage + 'V'
powerPhases.value[1].current = res.bElectricCurrent + 'A'
powerPhases.value[1].power = res.bPower + 'kW'
powerPhases.value[1].factor = res.bPowerFactor
powerPhases.value[2].voltage = res.cVoltage + 'V'
powerPhases.value[2].current = res.cElectricCurrent + 'A'
powerPhases.value[2].power = res.cPower + 'kW'
powerPhases.value[2].factor = res.cPowerFactor
energyParams.value.activeEnergy = res.activeEnergy + 'kW·h'
energyParams.value.activePower = res.activePower + 'kW'
energyParams.value.powerFactor = res.powerFactor
})
}
const getInfo = () => { const getInfo = () => {
// RPC.post('/m9z/getDeviceManualInfo', { comm_uid: commit_uid }).then(res => { // RPC.post('/m9z/getDeviceManualInfo', { comm_uid: commit_uid }).then(res => {
// info.value = res // info.value = res
// }) // })
infoLoading.value = true return loadDeviceStatus().finally(() => loadPowerData())
phaseLoading.value = true
RPC.post('/adl400/readPhaseData', { comm_uid: commit_uid }).then(res => {
powerPhases.value[0].voltage = res.aVoltage ? res.aVoltage + 'V' : '0V'
powerPhases.value[0].current = res.aElectricCurrent ? res.aElectricCurrent + 'A' : '0A'
powerPhases.value[0].power = res.aPower ? res.aPower + 'kW' : '0kW'
powerPhases.value[0].factor = res.aPowerFactor ? res.aPowerFactor : 0
powerPhases.value[1].voltage = res.bVoltage ? res.bVoltage + 'V' : '0V'
powerPhases.value[1].current = res.bElectricCurrent ? res.bElectricCurrent + 'A' : '0A'
powerPhases.value[1].power = res.bPower ? res.bPower + 'kW' : '0kW'
powerPhases.value[1].factor = res.bPowerFactor ? res.bPowerFactor : 0
powerPhases.value[2].voltage = res.cVoltage ? res.cVoltage + 'V' : '0V'
powerPhases.value[2].current = res.cElectricCurrent ? res.cElectricCurrent + 'A' : '0A'
powerPhases.value[2].power = res.cPower ? res.cPower + 'kW' : '0kW'
powerPhases.value[2].factor = res.cPowerFactor ? res.cPowerFactor : 0
energyParams.value.activeEnergy = res.activeEnergy ? res.activeEnergy + 'kW·h' : '0kW·h'
energyParams.value.activePower = res.activePower ? res.activePower + 'kW' : '0kW'
energyParams.value.powerFactor = res.powerFactor ? res.powerFactor : 0
}).finally(() => {
phaseLoading.value = false
})
RPC.post('/m9z/getDeviceStatus2', { comm_uid: commit_uid }).then((res) => {
// 直流电流、电源、功率
if (res.Loops?.length === 10) {
info.value = res
}
}).finally(() => {
infoLoading.value = false
})
getLineData()
// RPC.post('/m9z/getDeviceSunRiseTime', { comm_uid: commit_uid }).then(res => { // RPC.post('/m9z/getDeviceSunRiseTime', { comm_uid: commit_uid }).then(res => {
// sunriseTime.value = res.Sunrise ? dayjs(res.Sunrise).format('HH:mm:ss') : '' // sunriseTime.value = res.Sunrise ? dayjs(res.Sunrise).format('HH:mm:ss') : ''
// sunsetTime.value = res.Sunset ? dayjs(res.Sunset).format('HH:mm:ss') : '' // sunsetTime.value = res.Sunset ? dayjs(res.Sunset).format('HH:mm:ss') : ''
// }) // })
} }
getInfo()
const refreshCurrentFaults = () => {
faultRefreshing.value = true
loadDeviceStatus()
.then(() => {
ElMessage.success('当前故障已刷新')
})
.finally(() => {
faultRefreshing.value = false
})
}
let timer = null
RPC.post('/getMessageInterval').then(res => {
console.log(`获取消息间隔成功,消息间隔为${res * 1000}毫秒`)
timer = setInterval(() => {
getInfo()
}, res * 1000)
}).catch(() => {
timer = setInterval(() => {
console.log(`获取消息间隔失败,消息间隔为30秒`)
getInfo()
}, 30000)
})
const timer = setInterval(() => {
RPC.post('/lightm/rtu/isOnline', { commUid: commit_uid }).then(res => {
console.log('/lightm/rtu/isOnlin', res)
isOnline.value = res
if (isOnline.value) {
getInfo()
}
})
}, 30000)
onUnmounted(() => { onUnmounted(() => {
clearInterval(timer) if (timer) {
clearInterval(timer)
}
}) })
const oldInfo = ref('') const oldInfo = ref('')
@ -603,25 +652,27 @@ const restartSystem = () => {
let timeInterval = null let timeInterval = null
const isOnline = ref(false) // 处理 session 恢复后的重新加载
const handleSessionRestored = () => {
console.log('Session 已恢复,重新加载数据')
getInfo()
}
onMounted(() => { onMounted(() => {
RPC.post('/lightm/rtu/isOnline', { commUid: commit_uid }).then(res => {
console.log('/lightm/rtu/isOnlin', res)
isOnline.value = res
if (isOnline.value) {
getInfo()
} else {
ElMessage.error('设备不在线')
}
})
updateTime() updateTime()
timeInterval = setInterval(updateTime, 1000) timeInterval = setInterval(updateTime, 1000)
// 监听 session 恢复事件
window.addEventListener('m9z:session-restored', handleSessionRestored)
}) })
onUnmounted(() => { onUnmounted(() => {
if (timeInterval) { if (timeInterval) {
clearInterval(timeInterval) clearInterval(timeInterval)
} }
// 移除事件监听
window.removeEventListener('m9z:session-restored', handleSessionRestored)
}) })
</script> </script>
@ -632,7 +683,8 @@ onUnmounted(() => {
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
background: center / cover url('@/assets/images/handle_control_bg.png') no-repeat; background: center / cover url('@/assets/images/背景小@2x.png') no-repeat;
background-color: #0A3933;
} }
.main-content { .main-content {
@ -641,15 +693,18 @@ onUnmounted(() => {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
padding: 10px 20px; padding: 8px 20px 52px;
box-sizing: border-box; box-sizing: border-box;
display: flex;
flex-direction: column;
} }
.header-status { .header-status {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 15px; flex-shrink: 0;
margin-bottom: 8px;
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
} }
@ -660,7 +715,7 @@ onUnmounted(() => {
} }
.page-title h1 { .page-title h1 {
font-size: 30px; font-size: 26px;
font-weight: bold; font-weight: bold;
margin: 0; margin: 0;
color: #ffffff; color: #ffffff;
@ -677,26 +732,29 @@ onUnmounted(() => {
.control-content { .control-content {
display: flex; display: flex;
width: 100%; width: 100%;
gap: 30px; gap: 20px;
box-sizing: border-box; box-sizing: border-box;
height: calc(100% - 110px); flex: 1;
min-height: 0;
} }
.power-section { .power-section {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
// justify-content: space-between; justify-content: space-between;
/* width: 500px; */ /* width: 500px; */
gap: 20px; gap: 6px;
height: 100%; height: 100%;
overflow-y: auto; min-height: 0;
overflow: hidden;
} }
.power-params { .power-params {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 8px;
flex-shrink: 0;
} }
.phase-card, .phase-card,
@ -721,7 +779,7 @@ onUnmounted(() => {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-bottom: 15px; margin-bottom: 15px;
background-color: #212329; background-color: #24806B;
width: 30px; width: 30px;
height: 100%; height: 100%;
} }
@ -729,16 +787,16 @@ onUnmounted(() => {
.param-list { .param-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px; padding: 4px 8px;
flex: 1; flex: 1;
gap: 8px; gap: 3px;
justify-content: space-between; justify-content: space-between;
} }
.param-item { .param-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
font-size: 12px; font-size: 11px;
} }
.param-label { .param-label {
@ -750,16 +808,148 @@ onUnmounted(() => {
font-weight: 500; font-weight: 500;
} }
.circuit-section { .fault-section {
position: relative;
z-index: 5;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 5px 10px;
color: #fff;
flex-shrink: 0;
}
.fault-section-expanded {
z-index: 20;
}
.fault-header {
display: flex; display: flex;
flex-direction: column; justify-content: space-between;
align-items: center;
gap: 10px; gap: 10px;
cursor: pointer;
} }
.circuit-row { .fault-title {
display: flex; display: flex;
justify-content: space-between; align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
}
.fault-count {
padding: 1px 5px;
border-radius: 10px;
font-size: 11px;
color: #ffd4d4;
background: rgba(220, 53, 69, 0.18);
}
.fault-subtitle {
font-size: 12px; font-size: 12px;
color: rgba(255, 255, 255, 0.68);
}
.fault-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.fault-toggle-button {
flex-shrink: 0;
height: 26px;
padding: 0 8px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
background: rgba(255, 255, 255, 0.1);
}
.fault-refresh-button {
flex-shrink: 0;
height: 26px;
padding: 0 8px;
border-radius: 6px;
border: none;
color: #fff;
background: linear-gradient(180deg, rgba(51, 224, 188, 1) 0%, rgba(5, 149, 117, 1) 100%);
}
.fault-list {
position: absolute;
top: auto;
bottom: calc(100% + 5px);
left: 0;
right: 0;
z-index: 30;
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-template-rows: repeat(2, 21px);
gap: 4px;
padding: 5px 10px;
overflow: hidden;
box-sizing: border-box;
border-radius: 8px;
background: rgba(42, 69, 66, 0.96);
border: 1px solid rgba(255, 120, 120, 0.35);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);
}
.fault-item {
display: flex;
align-items: center;
gap: 4px;
min-height: 21px;
min-width: 0;
padding: 2px 5px;
border-radius: 6px;
background: rgba(220, 53, 69, 0.16);
border: 1px solid rgba(255, 120, 120, 0.35);
}
.fault-loop {
flex-shrink: 0;
font-size: 10px;
color: #ffd4d4;
}
.fault-message {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
font-weight: 600;
color: #fff;
}
.fault-more {
justify-content: center;
font-size: 11px;
font-weight: 600;
color: #ffd4d4;
}
.circuit-section {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1 1 auto;
min-height: 0;
}
.circuit-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
grid-template-rows: repeat(2, minmax(0, 1fr));
gap: 6px;
height: 100%;
min-height: 0;
font-size: 10px;
.circuitStatus { .circuitStatus {
color: #fff; color: #fff;
@ -768,16 +958,19 @@ onUnmounted(() => {
.line { .line {
width: 100%; width: 100%;
display: flex; display: flex;
margin-top: 10px; margin-top: 3px;
display: flex;
justify-content: space-between; justify-content: space-between;
.line_item { .line_item {
width: 45%; width: 48%;
// flex: 1; // flex: 1;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
&.line_item_full {
width: 100%;
}
} }
} }
} }
@ -787,15 +980,20 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
// gap: 10px; justify-content: center;
width: 18%; min-width: 0;
min-height: 0;
padding: 4px 5px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.12);
} }
.circuit-status { .circuit-status {
background: rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px; border-radius: 8px;
padding: 8px 12px; padding: 3px 5px;
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
text-align: center; text-align: center;
width: 100%; width: 100%;
@ -837,21 +1035,23 @@ onUnmounted(() => {
.dimming-section { .dimming-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 5px;
font-size: 12px; font-size: 11px;
flex-shrink: 0;
} }
.dimming-row { .dimming-grid {
display: flex; display: grid;
justify-content: space-between; grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 5px 8px;
} }
.dimming-item { .dimming-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 10px; gap: 4px;
width: 18%; min-width: 0;
} }
.channel-label { .channel-label {
@ -863,13 +1063,12 @@ onUnmounted(() => {
background: rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px; border-radius: 8px;
padding: 6px; padding: 4px 6px;
color: rgba(255, 255, 255, 0.7); color: rgba(255, 255, 255, 0.7);
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
@ -880,19 +1079,14 @@ onUnmounted(() => {
box-shadow: 0 0 10px rgba(33, 150, 243, 0.3); box-shadow: 0 0 10px rgba(33, 150, 243, 0.3);
} }
.brightness-display:hover {
background: rgba(255, 255, 255, 0.12);
transform: translateY(-2px);
}
.bottom-info { .bottom-info {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
border-top: 1px solid #eee; border-top: 1px solid #eee;
width: 100vw; width: 100vw;
padding: 0 30px; padding: 0 24px;
height: 50px; height: 44px;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
@ -913,17 +1107,19 @@ onUnmounted(() => {
.back-button, .back-button,
.restart-button { .restart-button {
width: 100%; width: 100%;
height: 38px; height: 34px;
border-radius: 19px; border-radius: 17px;
font-size: 16px; font-size: 15px;
font-weight: 500; font-weight: 500;
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.back-button { .back-button {
background-color: #000; // background-color: #000;
color: #fff; // color: #fff;
border: #000; border: #000;
background: linear-gradient(180deg, rgba(223, 196, 137, 1) 0%, rgba(190, 151, 99, 1) 100%);
color: #fff;
} }
} }
@ -940,8 +1136,11 @@ onUnmounted(() => {
background: rgba(255, 255, 255, 0.08); background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px; border-radius: 12px;
padding: 10px; padding: 0 10px;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
justify-content: center;
} }
.control-title { .control-title {
@ -978,8 +1177,8 @@ onUnmounted(() => {
} }
.mode-button.active { .mode-button.active {
background: #ffffff; background: linear-gradient(180deg, rgba(51, 224, 188, 1) 0%, rgba(5, 149, 117, 1) 100%);
color: #1976d2; color: #fff;
} }
.setting-button { .setting-button {
@ -1077,14 +1276,6 @@ onUnmounted(() => {
border-color: #006eff; border-color: #006eff;
margin-left: 20px; margin-left: 20px;
} }
.info_item {
color: #fff;
margin-left: 20px;
border: 0.5px solid #00E0F4;
padding: 5px 10px;
border-radius: 5px;
}
} }
} }
</style> </style>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,654 @@
<template>
<div class="control-view">
<div class="main-content">
<!-- 顶部状态栏 -->
<div class="header-status">
<div class="time-info">
<span>{{ currentTime }} {{ week }}</span>
</div>
<div class="page-title">
<h1>系统设置</h1>
</div>
<div class="system-status"></div>
</div>
<!-- 主体区域 -->
<div class="control-content">
<!-- 系统时间卡片 -->
<div class="main_item">
<div class="main_title">系统时间</div>
<div class="main_content">
<!-- 当前设备时间 -->
<div class="line">
<div class="item">
<div class="key">当前设备时间</div>
<div class="value">{{ deviceDatetime || '—' }}</div>
</div>
</div>
<!-- 设置新时间 -->
<div class="line">
<div class="item">
<div class="key">设置时间</div>
<el-date-picker v-model="targetDatetime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择目标时间" style="flex: 1;" />
</div>
</div>
<div class="buttonBox">
<div class="button refresh" @click="getSystemTime">
<el-icon>
<Refresh />
</el-icon>
刷新时间
</div>
<div class="button save" @click="setSystemTime">
<el-icon>
<Check />
</el-icon>
设置时间
</div>
</div>
<!-- 上次设置结果 -->
<div v-if="lastSetResult" class="result-box">
<div class="result-item">
<span class="result-key">修改前:</span>
<span class="result-val">{{ lastSetResult.old_time }}</span>
</div>
<div class="result-item">
<span class="result-key">修改后:</span>
<span class="result-val">{{ lastSetResult.new_time }}</span>
</div>
</div>
</div>
</div>
<!-- 电表型号卡片 -->
<div class="main_item">
<div class="main_title">电表型号</div>
<div class="main_content">
<div class="line">
<div class="item full-width">
<div class="key">当前型号</div>
<div class="value">{{ currentMeterFirm || '—' }}</div>
</div>
</div>
<div class="line">
<div class="item full-width">
<div class="key">选择型号</div>
<el-select v-model="selectedMeterFirm" placeholder="请选择电表型号" style="flex: 1;">
<el-option label="ADL400" value="ADL400" />
<el-option label="ADL200" value="ADL200" />
</el-select>
</div>
</div>
<div class="buttonBox">
<div class="button save" @click="setMeterFirm">
<el-icon>
<Check />
</el-icon>
确认修改
</div>
</div>
<div class="line config-line">
<div class="item full-width">
<div class="key">扫描数量</div>
<el-input-number v-model="scanLoopCount" :min="minScanLoopCount" :max="maxScanLoopCount" :step="1" controls-position="right" style="flex: 1;" />
</div>
</div>
<div class="line">
<div class="item full-width">
<div class="key">当前数量</div>
<div class="value">{{ currentScanLoopCount }}</div>
</div>
</div>
<div class="buttonBox">
<div class="button save" @click="setScanConfig">
<el-icon>
<Check />
</el-icon>
保存扫描数量
</div>
</div>
</div>
</div>
<!-- 修改密码卡片 -->
<div class="main_item">
<div class="main_title">修改操作密码</div>
<div class="main_content">
<div class="line">
<div class="item full-width">
<div class="key">启用密码保护</div>
<el-switch v-model="passwordEnabled" @change="handlePasswordEnableChange" />
</div>
</div>
<div class="line">
<div class="item full-width">
<div class="key">原密码</div>
<!-- <el-input v-model="pwdForm.oldPassword" type="password" placeholder="请输入原密码" show-password style="flex:1" /> -->
<BasePasswordInput v-model="pwdForm.oldPassword" layout-name="number" placeholder="请输入原密码" clearable />
</div>
</div>
<div class="line">
<div class="item full-width">
<div class="key">新密码</div>
<!-- <el-input v-model="pwdForm.newPassword" type="password" placeholder="至少 6 位" show-password style="flex:1" /> -->
<BasePasswordInput v-model="pwdForm.newPassword" layout-name="number" placeholder="请输入新密码" clearable />
</div>
</div>
<div class="line">
<div class="item full-width">
<div class="key">确认新密码</div>
<!-- <el-input v-model="pwdForm.confirmPassword" type="password" placeholder="再次输入新密码" show-password style="flex:1" /> -->
<BasePasswordInput v-model="pwdForm.confirmPassword" layout-name="number" placeholder="再次输入新密码" clearable />
</div>
</div>
<div class="buttonBox">
<div class="button save" @click="changePassword">
<el-icon>
<Check />
</el-icon>
确认修改
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 底部信息 -->
<div class="bottom-info">
<div style="color:#fff">
设备号:{{ commit_uid }}
</div>
<div class="bottom_item"></div>
<div class="bottom_item">
<el-button class="back-button" @click="goBack">
<el-icon>
<HomeFilled />
</el-icon>
返回主界面
</el-button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import RPC from '@/utils/rpc'
import BasePasswordInput from '@/components/BasePasswordInput.vue'
const router = useRouter()
const commit_uid = sessionStorage.getItem('central_control_comm_uid')
// ─── 时间相关 ─────────────────────────────────────
const currentTime = ref('')
const week = ref('')
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
const deviceDatetime = ref('') // 设备当前时间(来自后端)
const targetDatetime = ref('') // 用户选择的目标时间
const lastSetResult = ref(null) // 上次设置结果
const updateTime = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
week.value = weekDays[now.getDay()]
currentTime.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// 获取设备系统时间
const getSystemTime = async () => {
try {
const res = await RPC.post('/m9z/system/getTime', {})
deviceDatetime.value = res?.datetime || ''
} catch (e) {
// 仅在 401 时 rpc.js 会触发密码弹框,其他错误已有通用提示
}
}
// 设置系统时间
const setSystemTime = async () => {
if (!targetDatetime.value) {
ElMessage.warning('请先选择目标时间')
return
}
try {
const res = await RPC.post('/m9z/system/setTime', { datetime: targetDatetime.value })
if (res?.success) {
lastSetResult.value = res
deviceDatetime.value = res.new_time
targetDatetime.value = ''
ElMessage.success('系统时间设置成功')
}
} catch (e) {
// 401 由 rpc.js 统一处理
}
}
// ─── 电表型号 ─────────────────────────────────────
const currentMeterFirm = ref('')
const selectedMeterFirm = ref('')
const minScanLoopCount = ref(1)
const maxScanLoopCount = ref(10)
const currentScanLoopCount = ref(10)
const scanLoopCount = ref(10)
const getMeterFirm = async () => {
try {
const res = await RPC.post('/m9z/meter/getMeterFirm', {})
currentMeterFirm.value = res?.meter_firm || ''
selectedMeterFirm.value = currentMeterFirm.value
} catch (e) {
// 错误由 rpc.js 处理
}
}
const setMeterFirm = async () => {
if (!selectedMeterFirm.value) {
ElMessage.warning('请选择电表型号')
return
}
if (selectedMeterFirm.value === currentMeterFirm.value) {
ElMessage.info('电表型号未变更')
return
}
try {
await ElMessageBox.confirm(
`确认要将电表型号修改为 ${selectedMeterFirm.value} 吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
const res = await RPC.post('/m9z/meter/setMeterFirm', {
meter_firm: selectedMeterFirm.value,
})
if (res?.success) {
currentMeterFirm.value = res.meter_firm
ElMessage.success(`电表型号已修改为 ${res.meter_firm}`)
}
} catch (e) {
if (e === 'cancel' || e === 'close') return
// 其他错误由 rpc.js 处理
}
}
const getScanConfig = async () => {
try {
const res = await RPC.post('/m9z/scan/getConfig', {})
const loopCount = Number(res?.loop_count || 10)
minScanLoopCount.value = Number(res?.min_loop_count || 1)
maxScanLoopCount.value = Number(res?.max_loop_count || 10)
currentScanLoopCount.value = loopCount
scanLoopCount.value = loopCount
} catch (e) {
// 错误由 rpc.js 处理
}
}
const setScanConfig = async () => {
const nextLoopCount = Number(scanLoopCount.value)
if (!Number.isInteger(nextLoopCount)) {
ElMessage.warning('请输入整数扫描数量')
return
}
if (nextLoopCount < minScanLoopCount.value || nextLoopCount > maxScanLoopCount.value) {
ElMessage.warning(`扫描数量范围为 ${minScanLoopCount.value}-${maxScanLoopCount.value}`)
return
}
if (nextLoopCount === currentScanLoopCount.value) {
ElMessage.info('扫描数量未变更')
return
}
try {
await ElMessageBox.confirm(
`确认要将扫描数量修改为 ${nextLoopCount} 吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
const res = await RPC.post('/m9z/scan/setConfig', {
loop_count: nextLoopCount,
})
if (res?.success) {
const loopCount = Number(res.loop_count || nextLoopCount)
currentScanLoopCount.value = loopCount
scanLoopCount.value = loopCount
ElMessage.success(`扫描数量已修改为 ${loopCount}`)
}
} catch (e) {
if (e === 'cancel' || e === 'close') return
// 其他错误由 rpc.js 处理
}
}
// ─── 修改密码 ─────────────────────────────────────
const pwdForm = ref({ oldPassword: '', newPassword: '', confirmPassword: '' })
const changePassword = async () => {
if (!pwdForm.value.oldPassword || !pwdForm.value.newPassword) {
ElMessage.warning('请输入原密码和新密码')
return
}
if (pwdForm.value.newPassword.length < 6) {
ElMessage.warning('新密码至少 6 位')
return
}
if (pwdForm.value.newPassword !== pwdForm.value.confirmPassword) {
ElMessage.warning('两次输入的新密码不一致')
return
}
try {
await RPC.post('/m9z/password/change', {
old_password: pwdForm.value.oldPassword,
new_password: pwdForm.value.newPassword,
})
ElMessage.success('密码修改成功,请重新登录')
// 清除 session,下次操作时重新验证
localStorage.removeItem('m9z_session_id')
pwdForm.value = { oldPassword: '', newPassword: '', confirmPassword: '' }
} catch (e) {
// 401 或其他错误由 rpc.js 统一处理
}
}
// ─── 密码保护开关 ─────────────────────────────────
const passwordEnabled = ref(false)
const getPasswordStatus = async () => {
try {
const res = await RPC.post('/m9z/password/status', {})
passwordEnabled.value = res?.enabled || false
} catch (e) {
// 错误由 rpc.js 处理
}
}
const handlePasswordEnableChange = async (val) => {
const actionText = val ? '开启' : '关闭'
try {
await ElMessageBox.confirm(
`确认要${actionText}密码保护功能吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
const method = val ? '/m9z/password/enable' : '/m9z/password/disable'
try {
await RPC.post(method, {})
ElMessage.success(`${actionText}密码保护成功`)
} catch (e) {
// 如果接口失败,回滚状态
passwordEnabled.value = !val
}
} catch (e) {
// 用户取消,回滚状态
passwordEnabled.value = !val
}
}
const goBack = () => {
router.push('/')
}
let timeInterval = null
// 处理 session 恢复后的重新加载
const handleSessionRestored = () => {
console.log('Session 已恢复,重新加载数据')
getSystemTime()
getPasswordStatus()
getMeterFirm()
getScanConfig()
}
onMounted(() => {
updateTime()
timeInterval = setInterval(updateTime, 1000)
getSystemTime()
getPasswordStatus()
getMeterFirm()
getScanConfig()
// 监听 session 恢复事件
window.addEventListener('m9z:session-restored', handleSessionRestored)
})
onUnmounted(() => {
if (timeInterval) clearInterval(timeInterval)
// 移除事件监听
window.removeEventListener('m9z:session-restored', handleSessionRestored)
})
</script>
<style scoped lang="scss">
.control-view {
width: 100vw;
height: 100vh;
box-sizing: border-box;
position: relative;
overflow: auto;
background: center / cover url('@/assets/images/背景小@2x.png') no-repeat;
background-color: #0A3933;
}
.main-content {
position: relative;
z-index: 2;
width: 100%;
height: 100%;
padding: 10px 20px;
box-sizing: border-box;
}
.header-status {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
color: rgba(255, 255, 255, 0.9);
}
.time-info {
font-size: 18px;
font-weight: 300;
width: 270px;
}
.page-title h1 {
font-size: 30px;
font-weight: bold;
margin: 0;
color: #ffffff;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
}
.system-status {
min-width: 200px;
font-size: 18px;
font-weight: 300;
width: 270px;
}
.control-content {
display: flex;
width: 100%;
gap: 20px;
overflow: hidden;
box-sizing: border-box;
.main_item {
border: .5px solid #0e9228;
display: flex;
flex-direction: column;
min-height: 400px;
width: 420px;
.main_title {
display: flex;
justify-content: center;
align-items: center;
height: 50px;
font-size: 16px;
background-color: rgba(#188549, .3);
color: #fff;
border-bottom: .5px solid #0e9228;
}
.main_content {
padding: 24px 20px;
font-size: 14px;
background-color: rgba(#000000, .3);
flex: 1;
.line {
display: flex;
align-items: center;
margin-bottom: 20px;
.item {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
&.full-width {
width: 100%;
}
}
}
.key {
color: #fff;
flex-shrink: 0;
width: 90px;
}
.value {
flex: 1;
text-align: center;
background-color: rgba(#188549, .3);
color: #fff;
padding: 5px 10px;
min-height: 32px;
box-sizing: border-box;
border-radius: 5px;
}
.buttonBox {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 24px;
}
.button {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 20px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
color: #fff;
user-select: none;
&.save {
background-color: #188549;
&:hover {
background-color: #1fa85d;
}
}
&.refresh {
background-color: #1565a8;
&:hover {
background-color: #1a7fd4;
}
}
}
.config-line {
margin-top: 28px;
padding-top: 18px;
border-top: 1px solid rgba(#0e9228, .35);
}
}
}
}
.result-box {
margin-top: 20px;
padding: 12px 16px;
background-color: rgba(#188549, .15);
border: 1px solid rgba(#0e9228, .5);
border-radius: 8px;
.result-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
color: rgba(255, 255, 255, 0.85);
font-size: 13px;
&:last-child {
margin-bottom: 0;
}
}
.result-key {
color: rgba(255, 255, 255, 0.5);
}
}
.bottom-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
padding: 10px 30px;
box-sizing: border-box;
color: rgba(255, 255, 255, 0.7);
font-size: 16px;
background-color: rgba(0, 0, 0, 0.3);
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
z-index: 999
}
.back-button {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.4);
color: rgba(255, 255, 255, 0.8);
&:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.6);
color: #fff;
}
}
</style>

@ -0,0 +1,403 @@
# 密码验证与系统设置接口文档
## 概述
本模块提供密码验证、会话管理以及系统时间设置功能。设备运行在离线环境下,通过密码保护敏感操作,会话有效期为 30 分钟。
---
## 一、密码验证
### 1.1 验证密码
验证操作密码,成功后返回 session_id,后续请求携带该 ID 即可免密访问。
#### Method
- `/m9z/password/verify`
#### 请求参数示例
```json
{
"jsonrpc": "2.0",
"method": "/m9z/password/verify",
"params": {
"password": "000000"
},
"timestampin": "1743523200000"
}
```
#### 参数说明
| 参数名 | 类型 | 说明 | 必填 | 备注 |
|---------|------|--------|----|--------------|
| password | string | 操作密码 | 是 | 首次默认值为 000000 |
#### 成功响应示例
```json
{
"jsonrpc": "2.0",
"result": {
"success": true,
"session_id": "a1b2c3d4e5f6...",
"message": "verified"
},
"error": {
"message": "ok",
"code": 200
}
}
```
#### 错误响应示例(密码错误)
```json
{
"jsonrpc": "2.0",
"result": null,
"error": {
"message": "invalid password",
"code": 401
}
}
```
---
### 1.2 修改密码
修改操作密码,需要提供原密码。
#### Method
- `/m9z/password/change`
#### 请求参数示例
```json
{
"jsonrpc": "2.0",
"method": "/m9z/password/change",
"params": {
"old_password": "000000",
"new_password": "123456"
},
"timestampin": "1743523200000"
}
```
#### 参数说明
| 参数名 | 类型 | 说明 | 必填 | 备注 |
|-------------|------|------|----|------------|
| old_password | string | 原密码 | 是 | |
| new_password | string | 新密码 | 是 | 至少 6 位字符 |
#### 成功响应示例
```json
{
"jsonrpc": "2.0",
"result": {
"success": true,
"message": "password changed"
},
"error": {
"message": "ok",
"code": 200
}
}
```
#### 错误响应示例(原密码错误)
```json
{
"jsonrpc": "2.0",
"result": null,
"error": {
"message": "old password incorrect",
"code": 401
}
}
```
#### 错误响应示例(新密码太短)
```json
{
"jsonrpc": "2.0",
"result": null,
"error": {
"message": "new password must be at least 6 characters",
"code": 400
}
}
```
---
## 二、会话认证机制
### 2.1 工作流程
1. 前端首次访问时,弹出密码输入框
2. 调用 `/m9z/password/verify` 获取 `session_id`
3. 将 `session_id` 存储在本地(localStorage)
4. 后续所有需要认证的请求,在请求中携带 `session_id`
### 2.2 请求携带 session_id
```json
{
"jsonrpc": "2.0",
"method": "/m9z/device/setMode",
"params": {
"mode": 1
},
"session": "a1b2c3d4e5f6...",
"timestampin": "1743523200000"
}
```
### 2.3 认证失败响应
当 session_id 过期或未提供时,接口返回 401 错误,前端应引导用户重新输入密码。
```json
{
"jsonrpc": "2.0",
"result": null,
"error": {
"message": "session expired or invalid",
"code": 401
}
}
```
### 2.4 会话有效期
- 默认有效期:30 分钟
- 每次验证成功后刷新有效期
- 过期后需重新输入密码
---
## 三、白名单接口
以下接口无需密码验证即可访问:
| Method 前缀 | 说明 |
|-----------------------|-----------------|
| `/m9z/password/*` | 密码相关接口 |
| `/m9z/get*` | 所有读取接口 |
| `/m9z/fault*` | 故障相关接口 |
| `/m9z/system/getTime` | 获取系统时间 |
---
## 四、系统时间设置
### 4.1 获取系统时间
获取当前设备系统时间,用于前端校准显示。
#### Method
- `/m9z/system/getTime`
#### 请求参数示例
```json
{
"jsonrpc": "2.0",
"method": "/m9z/system/getTime",
"params": {},
"session": "a1b2c3d4e5f6...",
"timestampin": "1743523200000"
}
```
#### 成功响应示例
```json
{
"jsonrpc": "2.0",
"result": {
"timestamp": 1743523200,
"datetime": "2026-04-01 12:00:00",
"timezone": "Local"
},
"error": {
"message": "ok",
"code": 200
}
}
```
#### 响应参数说明
| 参数名 | 类型 | 说明 | 备注 |
|---------|--------|-----------|--------------|
| timestamp | int64 | Unix 时间戳 | 秒级 |
| datetime | string | 日期时间字符串 | 格式:YYYY-MM-DD HH:mm:ss |
| timezone | string | 时区信息 | Local 表示本地时区 |
---
### 4.2 设置系统时间
修改设备系统时间。设备运行在离线环境时,用于手动校准时间。
#### Method
- `/m9z/system/setTime`
#### 请求参数示例
```json
{
"jsonrpc": "2.0",
"method": "/m9z/system/setTime",
"params": {
"datetime": "2026-04-01 14:30:00"
},
"session": "a1b2c3d4e5f6...",
"timestampin": "1743523200000"
}
```
#### 参数说明
| 参数名 | 类型 | 说明 | 必填 | 备注 |
|---------|------|------|----|-----------------------------------|
| datetime | string | 目标时间 | 是 | 支持多种格式,见下方说明 |
#### 支持的时间格式
| 格式 | 示例 |
|---------------------------|-------------------|
| YYYY-MM-DD HH:mm:ss | 2026-04-01 14:30:00 |
| YYYY-MM-DDTHH:mm:ssZ | 2026-04-01T14:30:00Z |
| YYYY-MM-DD HH:mm | 2026-04-01 14:30 |
| YYYY-MM-DD | 2026-04-01 |
#### 成功响应示例
```json
{
"jsonrpc": "2.0",
"result": {
"success": true,
"message": "system time updated",
"old_time": "2026-04-01 14:25:30",
"new_time": "2026-04-01 14:30:00",
"unix_time": 1743531000
},
"error": {
"message": "ok",
"code": 200
}
}
```
#### 响应参数说明
| 参数名 | 类型 | 说明 | 备注 |
|---------"|--------|--------|------------|
| success | bool | 是否成功 | |
| message | string | 状态信息 | |
| old_time | string | 修改前时间 | 格式同上 |
| new_time | string | 修改后时间 | 格式同上 |
| unix_time | int64 | 新时间戳 | 秒级 |
#### 错误响应示例(格式错误)
```json
{
"jsonrpc": "2.0",
"result": null,
"error": {
"message": "invalid datetime format, supported: 2006-01-02 15:04:05, 2006-01-02T15:04:05Z, 2006-01-02 15:04, 2006-01-02",
"code": 400
}
}
```
#### 错误响应示例(未提供时间)
```json
{
"jsonrpc": "2.0",
"result": null,
"error": {
"message": "datetime is required",
"code": 400
}
}
```
---
## 五、注意事项
### 5.1 权限要求
- 设置系统时间需要 root 权限,应用需以 root 方式运行
### 5.2 NTP 设置
- 设置时间时会自动关闭 NTP 同步,防止时间被覆盖
- 设置完成后会自动同步到硬件时钟(RTC)
### 5.3 存储位置
- 密码数据存储在:`程序目录/data/m9zTtyPwd.json`
- 密码使用 SHA256 + 自定义盐值加密存储
### 5.4 建议的前端实现
```javascript
// 1. 初始化时检查 session
let sessionId = localStorage.getItem('m9z_session_id');
// 2. 封装请求方法,自动携带 session
async function callRpc(method, params = {}) {
const response = await fetch('/jsonrpc', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method,
params,
session: sessionId,
timestampin: Date.now().toString()
})
});
const data = await response.json();
// 3. 处理认证失败
if (data.error?.code === 401) {
// 弹出密码输入框
sessionId = null;
localStorage.removeItem('m9z_session_id');
showPasswordDialog();
throw new Error('session expired');
}
return data;
}
// 4. 密码验证成功后保存 session
async function verifyPassword(password) {
const res = await callRpc('/m9z/password/verify', { password }, true);
if (res.result?.success) {
sessionId = res.result.session_id;
localStorage.setItem('m9z_session_id', sessionId);
}
return res;
}
```

@ -13,7 +13,7 @@ import Inspect from 'vite-plugin-inspect'
const pathSrc = path.resolve(__dirname, 'src') const pathSrc = path.resolve(__dirname, 'src')
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
base: 'central-control', // base: 'central-control',
plugins: [ plugins: [
vue(), vue(),
// vueDevTools(), // vueDevTools(),
@ -59,9 +59,8 @@ export default defineConfig({
open: true, open: true,
proxy: { proxy: {
'/jsonrpc': { '/jsonrpc': {
// target: 'https://lamp.ruixininfo.com', target: 'https://lamp-dev.ruixininfo.com',
target: 'https://smart-light.ruixininfo.com', // target: 'http://10.21.77.100:71',
// target: 'http://171.208.94.46:7070',
changeOrigin: true, changeOrigin: true,
secure: false, secure: false,
}, },

Loading…
Cancel
Save