Compare commits

..

45 Commits

Author SHA1 Message Date
Rice 9d20c79709 Merge branch 'origin/绿色触摸屏' into 触摸屏-直流
2 weeks ago
wangqiyang 849f860ddc 固定实时监控十路显示
2 weeks ago
Rice a1bf5cedf6 Merge branch 'origin/绿色触摸屏' into 触摸屏-直流
2 weeks ago
wangqiyang 4a14221648 优化小屏监控布局
2 weeks ago
Rice 8ea9c5c489 fix: 修复字体文件路径错误
2 weeks ago
Rice 5486127839 Merge branch 'origin/绿色触摸屏' into 触摸屏-直流
2 weeks ago
wangqiyang 4e0405808c 支持故障展开隐藏显示
2 weeks ago
wangqiyang e455bb5a60 精简实时监控故障显示
2 weeks ago
Rice d57c077a1c Merge branch 'origin/绿色触摸屏' into 触摸屏-直流
2 weeks ago
wangqiyang f9e6ddf832 优化实时监控故障布局
2 weeks ago
Rice 3eb1421cac Merge branch 'origin/绿色触摸屏' into 触摸屏-直流
2 weeks ago
wangqiyang 34d28b188b Merge branch '绿色触摸屏' of https://gitee.com/lufaneng/central-control into 绿色触摸屏
2 weeks ago
wangqiyang 26df8428fb 优化触摸屏监控与报警分页
2 weeks ago
Rice 2f052d319e Merge branch 'origin/绿色触摸屏' into 触摸屏-直流
3 weeks ago
Rice 15fbd4f429 fix: 修复字体文件路径错误
3 weeks ago
wangqiyang 74672d31b5 实现扫描数量前端配置
3 weeks ago
wangqiyang ba04609631 实现当前故障前端展示
3 weeks ago
Lufaneng 92a178a3a3 Merge branch '绿色触摸屏' into 触摸屏-直流
3 months ago
Lufaneng c249539205 电表型号修复
3 months ago
Lufaneng c652469c37 Merge branch '绿色触摸屏' into 触摸屏-直流
3 months ago
Lufaneng 3d2a70a829 增加选择电表型号
3 months ago
Lufaneng 3a7fff040d Merge branch '绿色触摸屏' into 触摸屏-直流
3 months ago
Lufaneng 09794cf463 重新加载修改
3 months ago
Lufaneng c2f75881be 更改名称
4 months ago
Lufaneng c703fa3730 增加是否开启密码功能
4 months ago
Lufaneng dc64f9528b 1
4 months ago
Lufaneng b5686ced94 优化
4 months ago
Lufaneng de483cecf8 增加密码
4 months ago
Lufaneng b75fecefce 更改刷新时间
11 months ago
Lufaneng 2a3aec3e82 背景更换
11 months 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
.DS_Store
dist
dist.zip
dist-ssr
coverage
*.local

14
package-lock.json generated

@ -13,6 +13,8 @@
"element-plus": "^2.9.4",
"nprogress": "^0.2.0",
"pinia": "^2.3.1",
"simple-keyboard": "^3.8.69",
"simple-keyboard-layouts": "^3.4.109",
"vue": "^3.5.13",
"vue-echarts": "^7.0.3",
"vue-router": "^4.5.0"
@ -4840,6 +4842,18 @@
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/sirv/-/sirv-3.0.0.tgz",

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

@ -6,25 +6,34 @@
</keep-alive>
<component :is="Component" :key="$route.name" v-if="!route.meta.keepAlive" />
</router-view>
<!-- 全局密码验证弹框 -->
<PasswordDialog ref="passwordDialogRef" />
</el-config-provider>
</template>
<script setup>
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import RPC from '@/utils/rpc'
const locale = zhCn
import PasswordDialog from '@/components/PasswordDialog.vue'
const locale = zhCn
const route = useRoute()
const passwordDialogRef = ref(null)
//
const showPasswordDialog = () => {
passwordDialogRef.value?.open()
}
onMounted(() => {
console.log(import.meta.env.VITE_APP_ENV)
RPC.post('/sys/basic/infoJson').then(res => {
console.log(res);
const arr = res || []
localStorage.setItem('SysBasicTgkTitle', arr?.find(item => item.conf_code === 'SysBasicTgkTitle')?.conf_value || '')
})
// 401 session
window.addEventListener('m9z:session-expired', showPasswordDialog)
})
onUnmounted(() => {
window.removeEventListener('m9z:session-expired', showPasswordDialog)
})
</script>
<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 */
declare module 'vue' {
export interface GlobalComponents {
BaseKeyboardInput: typeof import('./components/BaseKeyboardInput.vue')['default']
BasePasswordInput: typeof import('./components/BasePasswordInput.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
@ -17,16 +19,21 @@ declare module 'vue' {
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSlider: typeof import('element-plus/es')['ElSlider']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
Linechart: typeof import('./components/Linechart.vue')['default']
Pagination: typeof import('./components/Pagination/index.vue')['default']
PasswordDialog: typeof import('./components/PasswordDialog.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
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'
},
// layoutNamenumber
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'
},
// layoutNamenumber
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),
axisLabel: {
color: '#fff'
},
splitLine: {
show: true, // 线
lineStyle: {
color: '#ccc', // 线
type: 'dashed' // 线 'solid' | 'dashed' | 'dotted'
}
}
},
yAxis: {
@ -68,7 +61,7 @@ const option = computed(() => {
bottom: '10%',
},
title: {
text: '24小时有功功率数据可视化-kW',
text: '有功功率w',
textStyle: {
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'),
meta: { title: '报警预览' },
},
{
path: '/system-settings',
name: 'SystemSettings',
component: () => import('@/views/SystemSettings/index.vue'),
meta: { title: '系统设置' },
},
]
const router = createRouter({

@ -7,5 +7,5 @@
}
: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 {
padding: 0;
margin: 0;
font-family: SourceHanSansCN-Bold;
}
*:focus-visible {
outline: none;

@ -17,7 +17,7 @@ const guid = () => {
}
const instance = axios.create({
baseURL: '/',
timeout: 100000,
timeout: 30 * 1000,
responseType: 'json',
})
@ -32,7 +32,7 @@ instance.interceptors.request.use(
jsonrpc: '2.0',
method,
params,
session: localStorage.getItem('token'),
session: localStorage.getItem('m9z_session_id'),
timestampin: Date.now().toString(),
}
return config
@ -48,6 +48,12 @@ instance.interceptors.response.use(
if (response.data.error.code === 200) {
return Promise.resolve(response.data.result)
} 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)
return Promise.reject(response.data.error.message)
}

@ -17,42 +17,21 @@
<!-- 主体区域 -->
<div class="control-content">
<div style="width: 100%;display: flex;justify-content: space-between;align-items: center;">
<div class="tabBox">
<div class="tab" :class="{ select: type === '重大预警' }" @click="changeTab('重大预警')"></div>
<div class="tab" :class="{ select: type === '一般预警' }" @click="changeTab('一般预警')"></div>
</div>
<div v-if="type === '一般预警'">
<el-button @click="handleDeal" class="handleBtn" type="success">
<el-icon>
<Delete />
</el-icon>
批量处理
</el-button>
<el-button @click="handleDel" class="delBtn" type="danger">
<el-icon>
<Delete />
</el-icon>
批量删除
</el-button>
</div>
<div style="width: 100%;">
<!-- <el-button @click="handleDeal" class="handleBtn" type="success">
<el-icon>
<Delete />
</el-icon>
批量处理
</el-button> -->
<el-button @click="handleDel" class="delBtn" type="danger">
<el-icon>
<Delete />
</el-icon>
批量删除
</el-button>
</div>
<el-table v-show="type === '重大预警'" :data="bigTableData" style="width: 100%" height="366" stripe v-loading="loading">
<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 class="alarm-table" :data="showTableData" style="width: 100%" stripe v-loading="loading" @selection-change="handleSelectionChange" row-key="id" ref="multipleTableRef">
<el-table-column type="selection" :selectable="selectable" width="55" />
<el-table-column align="center" prop="id" 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') : '' }}
</template>
</el-table-column>
<el-table-column align="center" prop="voltage" label="处理状态">
<!-- <el-table-column align="center" prop="voltage" label="处理状态">
<template #default="{ row }">
{{ row.operate_status == 0 ? '未处理' : '已处理' }}
</template>
</el-table-column>
</el-table-column> -->
</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>
<!-- 底部信息 -->
@ -82,9 +61,6 @@
<div style="color:#fff">
设备号{{ commit_uid }}
</div>
<div style="color:#fff">
设备名称{{ device_name }}
</div>
<div class="bottom_item">
<!-- <div class="time-item">
<span>日出时间{{ sunriseTime }}</span>
@ -120,40 +96,32 @@ import dayjs from 'dayjs';
const router = useRouter()
const currentTime = ref('')
const systemRuntime = ref('38分钟')
const commit_uid = localStorage.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 = sessionStorage.getItem('central_control_comm_uid')
// const commit_uid = '868656077692382'
const loading = ref(false)
const total = ref(0)
const pageSize = ref(8)
const pageSize = ref(6)
const currentPage = ref(1)
const type = ref('重大预警')
const changeTab = (tab) => {
type.value = tab
if (type.value === '重大预警') {
getBigData()
} else {
getData()
}
const tableData = ref([])
const showTableData = ref([])
const applyPageData = () => {
const start = (currentPage.value - 1) * pageSize.value
showTableData.value = tableData.value.slice(start, start + pageSize.value)
}
const handleSizeChange = () => {
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
if (type.value === '重大预警') {
getBigData()
} else {
getData()
}
applyPageData()
}
const handleCurrentChange = () => {
if (type.value === '重大预警') {
getBigData()
} else {
getData()
}
applyPageData()
}
const tableData = ref([])
const multipleTableRef = ref(null)
const multipleSelection = ref([])
const selectable = (row) => true
@ -206,9 +174,10 @@ const handleDeal = () => {
}
const getData = () => {
currentPage.value = 1
const params = {
"limit": pageSize.value,
"page": currentPage.value,
// "limit": pageSize.value,
// "page": currentPage.value,
"and": {
"comm_uid": [
commit_uid
@ -225,57 +194,19 @@ const getData = () => {
loading.value = true
tableData.value = []
RPC.post('/m9z/fault/list', params).then(res => {
total.value = res.count || 0
tableData.value = res.rows || []
const rows = res?.rows || []
total.value = rows.length
tableData.value = rows
applyPageData()
}).finally(() => {
loading.value = false
})
}
// 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()
getData()
const reflesh = () => {
currentPage.value = 1
if (type.value === '重大预警') {
getBigData()
} else {
getData()
}
getData()
}
@ -311,15 +242,27 @@ const goBack = () => {
let timeInterval = null
// session
const handleSessionRestored = () => {
console.log('Session 已恢复,重新加载数据')
getData()
}
onMounted(() => {
updateTime()
timeInterval = setInterval(updateTime, 1000)
// session
window.addEventListener('m9z:session-restored', handleSessionRestored)
})
onUnmounted(() => {
if (timeInterval) {
clearInterval(timeInterval)
}
//
window.removeEventListener('m9z:session-restored', handleSessionRestored)
})
</script>
@ -329,23 +272,28 @@ onUnmounted(() => {
height: 100vh;
box-sizing: border-box;
position: relative;
overflow: auto;
background: center / cover url('@/assets/images/handle_control_bg.png') no-repeat;
overflow: hidden;
background: center / cover url('@/assets/images/背景小@2x.png') no-repeat;
background-color: #0A3933;
}
.main-content {
position: relative;
z-index: 2;
width: 100%;
height: 100%;
height: calc(100vh - 50px);
padding: 10px 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header-status {
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
margin-bottom: 20px;
color: rgba(255, 255, 255, 0.9);
}
@ -373,36 +321,23 @@ onUnmounted(() => {
.control-content {
width: 100%;
flex: 1;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
overflow: hidden;
:deep(.el-table__header) {
.el-table__cell {
background-color: #0881FE;
background-color: #24806B;
color: #fff;
}
}
.tabBox {
display: flex;
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;
}
}
:deep(.el-pagination.is-background .el-pager li.is-active) {
background-color: #24806B;
}
}
@ -443,9 +378,16 @@ onUnmounted(() => {
}
.back-button {
background-color: #000;
color: #fff;
// background-color: #000;
// color: #fff;
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;
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>

@ -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'
},
// layoutNamenumber
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 class="item" v-for="(ele, idx) in item.loops">
<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>
</div>
@ -93,6 +93,8 @@
</div>
<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="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>
@ -166,7 +168,7 @@
<el-dialog v-model="modeDialogVisible" title="选择模式" width="600px" :modal="true" class="brightness-dialog">
<div class="setting-content">
<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>
<template #footer>
@ -212,16 +214,17 @@
<div class="lightBox">
<div class="item" v-for="(item, index) in currentTask.loops">
<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>
<span>%</span>
</template>
</el-input-number>
</el-input-number> -->
</div>
</div>
<div class="timeBox">
<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 :value="0">定时时间</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 RPC from '@/utils/rpc'
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 selectBoxValue = ref([])
@ -255,10 +260,14 @@ const currentTaskName = ref('开始任务')
const currentTask = ref({})
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) => {
if (!isOnline.value) {
return ElMessage.error('设备不在线')
}
currentTaskName.value = item
if (item === '中间任务') {
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].timeType = currentTask.value.timeType
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 = {
"comm_uid": commit_uid,
"mode": deviceMode.value,
@ -346,12 +352,11 @@ const tasks = computed(() => [startTask.value, stopTask.value, middleTask.value]
const deviceMode = ref(0)
const currentMode = ref(0)
RPC.post('/m9z/getDeviceMode', { comm_uid: commit_uid }).then(res => {
currentMode.value = res
})
const changeMode = () => {
if (!isOnline.value) {
return ElMessage.error('设备不在线')
}
modeActive.value = currentMode.value
modeDialogVisible.value = true
}
@ -373,36 +378,30 @@ const getTask = () => {
startTaskLoading.value = true
stopTaskLoading.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
}).finally(() => {
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
}).finally(() => {
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 || []
}).finally(() => {
middleTaskLoading.value = false
})
}
getTask()
const addTask = () => {
if (!isOnline.value) {
return ElMessage.error('设备不在线')
}
middleTask.value.push({
"execTime": '2025-06-07 19:00:00',
"turnOn": true,
"pwm": 50
})
middleTask.value.forEach(item => {
item.execTime = item.execTime ? dayjs(item.execTime).format('YYYY-MM-DD HH:mm:ss') : ''
})
const middleParams = {
"comm_uid": commit_uid,
"mode": deviceMode.value,
@ -420,9 +419,6 @@ const addTask = () => {
})
}
const delTask = (index) => {
if (!isOnline.value) {
return ElMessage.error('设备不在线')
}
ElMessageBox.confirm(
'确认删除?',
'',
@ -435,9 +431,6 @@ const delTask = (index) => {
)
.then(() => {
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 = {
"comm_uid": commit_uid,
"mode": deviceMode.value,
@ -483,6 +476,11 @@ const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四
const sunriseTime = 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 now = new Date()
@ -554,33 +552,35 @@ const restartSystem = () => {
}
let timeInterval = null
const isOnline = ref(false)
onMounted(() => {
RPC.post('/lightm/rtu/isOnline', { commUid: commit_uid }).then(res => {
console.log('/lightm/rtu/isOnlin', res)
isOnline.value = res
if (isOnline.value) {
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') : ''
})
RPC.post('/m9z/getDeviceMode', { comm_uid: commit_uid }).then(res => {
currentMode.value = res
})
getTask()
} else {
getTask()
// ElMessage.error('线')
}
// session
const handleSessionRestored = () => {
console.log('Session 已恢复,重新加载数据')
getTask()
RPC.post('/m9z/getDeviceMode', { comm_uid: commit_uid }).then(res => {
currentMode.value = res
})
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()
timeInterval = setInterval(updateTime, 1000)
// session
window.addEventListener('m9z:session-restored', handleSessionRestored)
})
onUnmounted(() => {
if (timeInterval) {
clearInterval(timeInterval)
}
//
window.removeEventListener('m9z:session-restored', handleSessionRestored)
})
const delMode = () => {
@ -610,7 +610,8 @@ const delMode = () => {
box-sizing: border-box;
position: relative;
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 {
@ -691,7 +692,7 @@ const delMode = () => {
}
&.selected {
background-color: #0881FE;
background-color: #FFAF11;
}
}
}
@ -706,6 +707,20 @@ const delMode = () => {
gap: 10px;
height: 100%;
// &::-webkit-scrollbar {
// width: 32px;
// /* */
// height: 22px;
// /* */
// }
// &::-webkit-scrollbar-thumb {
// background-color: darkgrey;
// /* */
// border-radius: 10px;
// /* */
// }
.time {
width: 100px;
border-left: 1px solid #ccc;
@ -722,12 +737,12 @@ const delMode = () => {
display: flex;
align-items: center;
justify-content: center;
background-color: #3e4458;
background-color: rgba($color: #188549, $alpha: .3);
.el-icon {
font-size: 30px;
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;
height: 40px;
border-radius: 5px;
@ -761,7 +776,7 @@ const delMode = () => {
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #212329;
background-color: rgba($color: #188549, $alpha: .3);
width: 30px;
height: 100%;
}
@ -943,7 +958,7 @@ const delMode = () => {
}
.time-item {
color: rgba(255, 255, 255, 0.7);
color: #fff;
font-size: 16px;
margin: 0 20px;
}
@ -959,9 +974,11 @@ const delMode = () => {
}
.back-button {
background-color: #000;
color: #fff;
// background-color: #000;
// color: #fff;
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 {
.el-button {
margin: 15px 0;
margin: 10px 0;
&:last-child {
margin-bottom: 0;
@ -1018,12 +1035,17 @@ const delMode = () => {
margin-bottom: 10px;
&.add {
background-color: #397ff6;
background: linear-gradient(180deg, rgba(51, 224, 188, 1) 0%, rgba(5, 149, 117, 1) 100%);
}
&.del {
background-color: #d42d1f;
}
&.dark {
color: #333;
border: 1px solid #333;
}
}
.mode-button {
@ -1033,8 +1055,8 @@ const delMode = () => {
}
.mode-button.active {
background: #ffffff;
color: #1976d2;
background: linear-gradient(180deg, rgba(51, 224, 188, 1) 0%, rgba(5, 149, 117, 1) 100%);
color: #fff;
}
.setting-button {
@ -1064,8 +1086,9 @@ const delMode = () => {
/* 弹窗样式 */
:deep(.brightness-dialog) {
background: rgba(30, 30, 30, 0.95);
backdrop-filter: blur(20px);
// background: rgba(30, 30, 30, 0.95);
// 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) {
@ -1074,14 +1097,14 @@ const delMode = () => {
}
:deep(.brightness-dialog .el-dialog__title) {
color: #ffffff;
color: #6C75A0 100%;
font-size: 18px;
font-weight: 500;
}
:deep(.brightness-dialog .el-dialog__body) {
// background: rgba(255, 255, 255, 0.05);
color: #ffffff;
color: #6C75A0 100%;
}
.brightness-content {
@ -1093,7 +1116,7 @@ const delMode = () => {
font-size: 18px;
font-weight: 500;
margin-bottom: 30px;
color: #ffffff;
color: #6C75A0;
}
.brightness-slider-container {
@ -1169,12 +1192,12 @@ const delMode = () => {
:deep(.el-input-number__increase) {
background-color: transparent;
color: #fff;
color: #6C75A0;
}
:deep(.el-input-number__decrease) {
background-color: transparent;
color: #fff;
color: #6C75A0;
}
}
@ -1182,10 +1205,10 @@ const delMode = () => {
:deep(.el-input__wrapper) {
background-color: transparent;
color: #fff;
color: #6C75A0;
input {
color: #fff;
color: #6C75A0;
}
}

@ -11,7 +11,7 @@
<h1>手动控制</h1>
</div>
<div class="system-status">
<span>设备时间{{ deviceTime ? dayjs(deviceTime).format('YYYY-MM-DD HH:mm') : '' }}</span>
<!-- <span>系统已运行{{ systemRuntime }}</span> -->
</div>
</div>
@ -20,7 +20,7 @@
<!-- 左侧电力参数区域 -->
<div class="power-section">
<!-- 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 class="phase-name"><span>{{ phase.name }}</span></div>
<div class="param-list">
@ -68,7 +68,7 @@
</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 v-for="(circuit, index) in info.loops.slice(0, 5)" :key="circuit.name" class="circuit-item">
@ -99,7 +99,7 @@
</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 v-for="(channel, index) in info.loops.slice(0, 5)" :key="channel.name" class="dimming-item">
@ -123,7 +123,7 @@
</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-title">
@ -175,15 +175,12 @@
设备号{{ commit_uid }}
</div>
<div class="bottom_item">
<el-button class="restart-button" :loading="timeLoading" @click="setDeviceTime">
同步时间
</el-button>
<el-button class="restart-button" @click="refresh">
<!-- <el-button class="restart-button" @click="restartSystem">
<el-icon>
<RefreshRight />
</el-icon>
刷新
</el-button>
远程重启
</el-button> -->
<el-button class="back-button" @click="goBack">
<el-icon>
<HomeFilled />
@ -204,7 +201,7 @@
<template #footer>
<div class="dialog-footer">
<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>
</template>
</el-dialog>
@ -217,42 +214,19 @@ import { useRouter } from 'vue-router'
import RPC from '@/utils/rpc'
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 currentTime = ref('')
const deviceTime = ref('')
const systemRuntime = ref('38分钟')
const brightnessDialogVisible = ref(false)
const currentBrightness = ref(50)
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 weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const phaseLoading = ref(false)
//
@ -386,9 +360,7 @@ const info = ref({
})
const isSync = ref(true)
const infoLoading = ref(false)
const getInfo = () => {
infoLoading.value = true
isSync.value = true
RPC.post('/m9z/getDeviceManualInfo', { comm_uid: commit_uid }).then(res => {
info.value = res
@ -397,39 +369,6 @@ const getInfo = () => {
}, 1000);
}).finally(() => {
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 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 = () => {
@ -463,9 +435,6 @@ const updateTime = () => {
week.value = weekDays[now.getDay()]
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('请切换至手动模式')
const circuit = info.value.loops.find(c => c.name === circuitName)
if (circuit) {
ElMessageBox.confirm(
`确认${circuit.is_open ? '关' : '开'}灯?`,
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
circuit.is_open = !circuit.is_open
})
circuit.is_open = !circuit.is_open
}
}
@ -523,18 +482,8 @@ const openGlobalBrightnessDialog = () => {
//
const openLightSettings = () => {
if (!info.value.disable) return ElMessage.warning('请切换至手动模式')
ElMessageBox.confirm(
`确认全开?`,
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
info.value.loops.forEach(circuit => {
circuit.is_open = true
})
info.value.loops.forEach(circuit => {
circuit.is_open = true
})
// dimmingChannels.value.forEach(channel => {
// channel.brightness = 100
@ -544,18 +493,8 @@ const openLightSettings = () => {
//
const turnOffAll = () => {
if (!info.value.disable) return ElMessage.warning('请切换至手动模式')
ElMessageBox.confirm(
`确认全关?`,
'提示',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
info.value.loops.forEach(circuit => {
circuit.is_open = false
})
info.value.loops.forEach(circuit => {
circuit.is_open = false
})
// dimmingChannels.value.forEach(channel => {
// channel.brightness = 0
@ -587,9 +526,6 @@ const restartSystem = () => {
}
const changeMode = () => {
if (!isOnline.value) {
return ElMessage.error('设备不在线')
}
ElMessageBox.confirm(
`此操作将切换到${info.value.disable ? '自动' : '手动'}模式,是否继续?`,
'提示',
@ -598,63 +534,39 @@ const changeMode = () => {
cancelButtonText: '取消',
type: 'warning',
}
).then(() => {
info.value.disable = !info.value.disable
// ElMessage({
// type: 'success',
// message: `${info.value.disable ? '' : ''}`,
// })
})
)
.then(() => {
info.value.disable = !info.value.disable
// ElMessage({
// type: 'success',
// message: `${info.value.disable ? '' : ''}`,
// })
})
}
let timeInterval = null
// session
const handleSessionRestored = () => {
console.log('Session 已恢复,重新加载数据')
getInfo()
}
onMounted(() => {
updateTime()
timeInterval = setInterval(updateTime, 1000)
RPC.post('/lightm/rtu/isOnline', { commUid: commit_uid }).then(res => {
console.log('/lightm/rtu/isOnlin', res)
isOnline.value = res
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('设备不在线')
}
})
// session
window.addEventListener('m9z:session-restored', handleSessionRestored)
})
onUnmounted(() => {
if (timeInterval) {
clearInterval(timeInterval)
}
//
window.removeEventListener('m9z:session-restored', handleSessionRestored)
})
</script>
@ -665,7 +577,8 @@ onUnmounted(() => {
box-sizing: border-box;
position: relative;
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 {
@ -700,7 +613,7 @@ onUnmounted(() => {
}
.system-status {
min-width: 300px;
min-width: 200px;
font-size: 18px;
font-weight: 300;
width: 270px;
@ -751,7 +664,7 @@ onUnmounted(() => {
justify-content: center;
align-items: center;
margin-bottom: 15px;
background-color: #212329;
background-color: #24806B;
width: 30px;
height: 100%;
}
@ -812,9 +725,9 @@ onUnmounted(() => {
}
.circuit-status.active {
background: rgba(33, 150, 243, 0.2);
border-color: rgba(33, 150, 243, 0.5);
color: #64b5f6;
background: rgba(31, 223, 130, 0.2);
border-color: rgba(35, 219, 62, 0.764);
color: #25b15f;
}
.circuit-icon {
@ -883,9 +796,9 @@ onUnmounted(() => {
}
.brightness-display.active {
background: rgba(33, 150, 243, 0.3);
border-color: rgba(33, 150, 243, 0.6);
color: #64b5f6;
background: rgba(31, 223, 130, 0.2);
border-color: rgba(35, 219, 62, 0.764);
color: #25b15f;
box-shadow: 0 0 10px rgba(33, 150, 243, 0.3);
}
@ -914,7 +827,7 @@ onUnmounted(() => {
}
.time-item {
color: rgba(255, 255, 255, 0.7);
color: #fff;
font-size: 16px;
margin: 0 20px;
}
@ -930,9 +843,11 @@ onUnmounted(() => {
}
.back-button {
background-color: #000;
// background-color: #000;
color: #fff;
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 {
background: #ffffff;
color: #1976d2;
background: linear-gradient(180deg, rgba(51, 224, 188, 1) 0%, rgba(5, 149, 117, 1) 100%);
color: #fff;
}
.setting-button {
@ -1054,11 +969,11 @@ onUnmounted(() => {
}
:deep(.brightness-slider .el-slider__bar) {
background: #1976d2;
background: #25b15f;
}
:deep(.brightness-slider .el-slider__button) {
border-color: #1976d2;
border-color: #25b15f;
}
:deep(.brightness-slider .el-slider__stop) {

@ -3,14 +3,17 @@
<!-- 主要内容 -->
<div class="main-content">
<!-- 时间显示 -->
<div class="time-display">
<!-- <div class="time-display">
{{ currentTime }}
</div> -->
<div class="logo">
<!-- <img src="@/assets/images/绿盟.png" alt=""> -->
</div>
<!-- 主标题 -->
<div class="main-title">
<div class="title-decoration left"></div>
<h1>{{ SysBasicTgkTitle }}</h1>
<h1>直流照明监控平台</h1>
<div class="title-decoration right"></div>
</div>
@ -32,17 +35,10 @@
设备号{{ commit_uid }}
</div>
<div class="status-item">
设备名称{{ device_name }}
版本号{{ version }}
</div>
<div class="status-item">
版本号1.0.0
</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>
系统状态<span class="status-online">开启</span>
</div>
</div>
</div>
@ -56,7 +52,8 @@ import {
Operation,
Setting,
VideoPlay,
Bell
Bell,
Tools
} from '@element-plus/icons-vue'
import RPC from '@/utils/rpc'
const currentTime = ref('')
@ -64,31 +61,24 @@ const router = useRouter()
const route = useRoute()
// console.log(route.query.comm_uid)
if (route.query.comm_uid) {
localStorage.setItem('central_control_comm_uid', route.query.comm_uid)
}
if (route.query.deviceName) {
localStorage.setItem('central_control_device_name', route.query.deviceName)
sessionStorage.setItem('central_control_comm_uid', route.query.comm_uid)
}
if (route.query.temperature) {
localStorage.setItem('temperature', route.query.temperature)
}
const commit_uid = localStorage.getItem('central_control_comm_uid')
const device_name = localStorage.getItem('central_control_device_name')
const status = ref(-1)
onMounted(() => {
RPC.post('/m9z/getRelayStatus', { comm_uid: commit_uid }).then(res => {
status.value = res
})
const version = ref('1.0.0')
const commit_uid = ref('')
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)
})
const functionCards = [
{ key: 'monitor', title: '实时监控', icon: Camera, route: '/monitoring' },
{ key: 'control', title: '手动控制', icon: Operation, route: '/handle-control' },
{ key: 'setting', title: '参数设置', icon: Setting, route: '/params-config' },
{ 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 = () => {
@ -114,29 +104,47 @@ const handleCardClick = (key) => {
}
}
}
const SysBasicTgkTitle = localStorage.getItem('SysBasicTgkTitle') || '智能中控装置'
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(() => {
updateTime()
timeInterval = setInterval(updateTime, 1000)
// session
window.addEventListener('m9z:session-restored', handleSessionRestored)
})
onUnmounted(() => {
if (timeInterval) {
clearInterval(timeInterval)
}
//
window.removeEventListener('m9z:session-restored', handleSessionRestored)
})
</script>
<style scoped>
<style scoped lang="scss">
.smart-control-app {
width: 100vw;
height: 100vh;
position: relative;
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 {
@ -149,6 +157,16 @@ onUnmounted(() => {
padding: 40px;
box-sizing: border-box;
justify-content: center;
.logo {
position: absolute;
top: 20px;
left: 20px;
img {
height: 30px;
}
}
}
.time-display {
@ -180,7 +198,7 @@ onUnmounted(() => {
.title-decoration {
width: 80px;
height: 6px;
background: linear-gradient(90deg, transparent, #64b5f6, #42a5f5, #64b5f6, transparent);
background: linear-gradient(90deg, transparent, #64f6be, #42f5a4, #54e98d, transparent);
position: relative;
animation: titleGlow 2s ease-in-out infinite alternate;
}
@ -193,7 +211,7 @@ onUnmounted(() => {
transform: translateX(-50%);
width: 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%;
box-shadow:
0 0 15px #64b5f6,
@ -330,11 +348,6 @@ onUnmounted(() => {
font-weight: bold;
}
.status-offline {
color: #f12323;
font-weight: bold;
}
/* Element Plus 卡片样式重写 */
:deep(.el-card) {
background: transparent !important;

@ -20,7 +20,7 @@
<!-- 左侧电力参数区域 -->
<div class="power-section">
<!-- 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 class="phase-name"><span>{{ phase.name }}</span></div>
<div class="param-list">
@ -73,89 +73,73 @@
</el-icon>
当前模式
</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 v-loading="infoLoading" element-loading-background="rgba(122, 122, 122, 0.8)">
<div class="circuit-section">
<!-- 上排回路 -->
<div class="circuit-row">
<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 }">
直流回路{{ index + 1 }}{{ circuit.is_open ? '打开' : '关闭' }}
</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 class="fault-section" :class="{ 'fault-section-expanded': faultListExpanded }">
<div class="fault-header" @click="toggleFaultList">
<div>
<div class="fault-title">
当前故障
<span v-if="currentFaults.length" class="fault-count">{{ currentFaults.length }}</span>
<span class="fault-subtitle">当前扫描设备数{{ scanDeviceCount }} · {{ faultStatusText }}</span>
</div>
</div>
<!-- 下排回路 -->
<div class="circuit-row">
<div v-for="(circuit, index) in info.Loops.slice(5, 10)" :key="circuit.name" class="circuit-item">
<div class="circuit-status" :class="{ active: circuit.is_open }">
直流回路{{ index + 6 }}{{ circuit.is_open ? '打开' : '关闭' }}
</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 class="fault-actions">
<el-button v-if="currentFaults.length" class="fault-toggle-button" @click.stop="toggleFaultList">
{{ faultListExpanded ? '隐藏' : '展开' }}
</el-button>
<el-button class="fault-refresh-button" :loading="faultRefreshing" @click.stop="refreshCurrentFaults">
<el-icon>
<RefreshRight />
</el-icon>
刷新
</el-button>
</div>
</div>
<div class="dimming-section">
<!-- 上排调光通道 -->
<div class="dimming-row">
<div v-for="(channel, index) in info.Loops.slice(0, 5)" :key="channel.name" class="dimming-item">
<div class="channel-label">调光通道{{ index + 1 }}</div>
<div class="brightness-display" :class="{ active: channel.pwm > 0 }">
{{ channel.pwm }}%
<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">
<span class="fault-loop">回路{{ fault.loop_idx }}</span>
<span class="fault-message" :title="fault.code ? `${fault.message}(${fault.code})` : fault.message">{{ fault.message }}</span>
</div>
<div v-if="hiddenFaultCount" class="fault-item fault-more">+{{ hiddenFaultCount }}</div>
</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 class="dimming-row">
<div v-for="(channel, index) in info.Loops.slice(5, 10)" :key="channel.name" class="dimming-item">
<div class="channel-label">调光通道{{ index + 6 }}</div>
<div class="brightness-display" :class="{ active: channel.pwm > 0 }">
{{ channel.pwm }}%
</div>
</div>
</div>
<div class="dimming-section">
<div class="dimming-grid">
<div v-for="(channel, index) in monitorLoops" :key="`${channel.name}-${index}`" class="dimming-item">
<div class="channel-label">调光通道{{ loopNo(channel, index) }}</div>
<div class="brightness-display" :class="{ active: channel.pwm > 0 }">
{{ channel.pwm || 0 }}%
</div>
</div>
</div>
</div>
<div class="chartBox">
<!-- <div class="chartBox">
<div class="search">
<el-date-picker value-format="YYYY-MM-DD" v-model="recdDate" type="date" placeholder="请选择时间" />
<el-button type="primary" :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>
<el-button type="primary" class="mode-button active" style="width:70px;height: 32px; margin-bottom: 0;" :loading="lineDataLoading" @click="getLineData"></el-button>
</div>
<Linechart :lineData="lineData"></Linechart>
</div>
</div> -->
</div>
</div>
</div>
@ -172,12 +156,6 @@
<div style="color:#fff">
设备号{{ commit_uid }}
</div>
<div style="color:#fff">
设备名称{{ device_name }}
</div>
<div style="color:#fff" v-if="temperature">
温度{{ temperature }}
</div>
<div class="bottom_item">
<!-- <el-button class="restart-button" @click="restartSystem">
<el-icon>
@ -213,15 +191,14 @@
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { computed, ref, onMounted, onUnmounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import RPC from '@/utils/rpc'
import dayjs from 'dayjs'
import Linechart from '@/components/Linechart.vue';
const commit_uid = localStorage.getItem('central_control_comm_uid')
const device_name = localStorage.getItem('central_control_device_name')
const temperature = localStorage.getItem('temperature')
// const commit_uid = '868656077692382'
const commit_uid = sessionStorage.getItem('central_control_comm_uid')
const router = useRouter()
const currentTime = ref('')
@ -229,6 +206,8 @@ const systemRuntime = ref('38分钟')
const brightnessDialogVisible = ref(false)
const currentBrightness = ref(50)
const selectedChannelName = ref(null)
const faultRefreshing = ref(false)
const faultListExpanded = ref(false)
const week = ref('')
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
@ -249,57 +228,14 @@ function transformHourlyData(data) {
const lineData = ref([])
const recdDate = ref(dayjs().add(-1, 'day').format('YYYY-MM-DD'))
const lineDataLoading = ref(false)
const agvEnergy = ref(0)
const maxEnergy = ref(0)
const getLineData = () => {
lineDataLoading.value = true
// RPC.post('/lightm/rtu/his', { commUid: commit_uid, recdDate: recdDate.value }).then(res => {
// lineData.value = transformHourlyData(res)
// }).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
}
})
}
// const getLineData = () => {
// lineDataLoading.value = true
// RPC.post('/lightm/rtu/his', { commUid: commit_uid, recdDate: recdDate.value }).then(res => {
// lineData.value = transformHourlyData(res)
// }).finally(() => {
// lineDataLoading.value = false
// })
// }
// getLineData()
//
@ -329,7 +265,7 @@ const powerPhases = ref([
const energyParams = ref({
dcVoltage: '0.0V',
activeEnergy: '0kh',
activeEnergy: '0kwh',
activePower: '0kW',
powerFactor: '0'
})
@ -337,6 +273,7 @@ const energyParams = ref({
const info = ref({
"Mode": 0,
"scan_device_count": 10,
"backup_byte1": 0,
"Loops": [
{
@ -433,64 +370,176 @@ const info = ref({
"backup_byte4": 0
})
const infoLoading = ref(false)
const phaseLoading = ref(false)
const scanDeviceCount = computed(() => {
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 = () => {
// RPC.post('/m9z/getDeviceManualInfo', { comm_uid: commit_uid }).then(res => {
// info.value = res
// })
infoLoading.value = true
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()
return loadDeviceStatus().finally(() => loadPowerData())
// 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') : ''
// })
}
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(() => {
clearInterval(timer)
if (timer) {
clearInterval(timer)
}
})
const oldInfo = ref('')
@ -603,25 +652,27 @@ const restartSystem = () => {
let timeInterval = null
const isOnline = ref(false)
// session
const handleSessionRestored = () => {
console.log('Session 已恢复,重新加载数据')
getInfo()
}
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()
timeInterval = setInterval(updateTime, 1000)
// session
window.addEventListener('m9z:session-restored', handleSessionRestored)
})
onUnmounted(() => {
if (timeInterval) {
clearInterval(timeInterval)
}
//
window.removeEventListener('m9z:session-restored', handleSessionRestored)
})
</script>
@ -632,7 +683,8 @@ onUnmounted(() => {
box-sizing: border-box;
position: relative;
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 {
@ -641,15 +693,18 @@ onUnmounted(() => {
width: 100%;
height: 100%;
overflow: hidden;
padding: 10px 20px;
padding: 8px 20px 52px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.header-status {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
flex-shrink: 0;
margin-bottom: 8px;
color: rgba(255, 255, 255, 0.9);
}
@ -660,7 +715,7 @@ onUnmounted(() => {
}
.page-title h1 {
font-size: 30px;
font-size: 26px;
font-weight: bold;
margin: 0;
color: #ffffff;
@ -677,26 +732,29 @@ onUnmounted(() => {
.control-content {
display: flex;
width: 100%;
gap: 30px;
gap: 20px;
box-sizing: border-box;
height: calc(100% - 110px);
flex: 1;
min-height: 0;
}
.power-section {
flex: 1;
display: flex;
flex-direction: column;
// justify-content: space-between;
justify-content: space-between;
/* width: 500px; */
gap: 20px;
gap: 6px;
height: 100%;
overflow-y: auto;
min-height: 0;
overflow: hidden;
}
.power-params {
display: flex;
justify-content: space-between;
gap: 10px;
gap: 8px;
flex-shrink: 0;
}
.phase-card,
@ -721,7 +779,7 @@ onUnmounted(() => {
justify-content: center;
align-items: center;
margin-bottom: 15px;
background-color: #212329;
background-color: #24806B;
width: 30px;
height: 100%;
}
@ -729,16 +787,16 @@ onUnmounted(() => {
.param-list {
display: flex;
flex-direction: column;
padding: 10px;
padding: 4px 8px;
flex: 1;
gap: 8px;
gap: 3px;
justify-content: space-between;
}
.param-item {
display: flex;
justify-content: space-between;
font-size: 12px;
font-size: 11px;
}
.param-label {
@ -750,16 +808,148 @@ onUnmounted(() => {
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;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 10px;
cursor: pointer;
}
.circuit-row {
.fault-title {
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;
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 {
color: #fff;
@ -768,16 +958,19 @@ onUnmounted(() => {
.line {
width: 100%;
display: flex;
margin-top: 10px;
display: flex;
margin-top: 3px;
justify-content: space-between;
.line_item {
width: 45%;
width: 48%;
// flex: 1;
flex-shrink: 0;
display: flex;
justify-content: space-between;
&.line_item_full {
width: 100%;
}
}
}
}
@ -787,15 +980,20 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
align-items: center;
// gap: 10px;
width: 18%;
justify-content: center;
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 {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 8px 12px;
padding: 3px 5px;
color: rgba(255, 255, 255, 0.7);
text-align: center;
width: 100%;
@ -837,21 +1035,23 @@ onUnmounted(() => {
.dimming-section {
display: flex;
flex-direction: column;
gap: 10px;
font-size: 12px;
gap: 5px;
font-size: 11px;
flex-shrink: 0;
}
.dimming-row {
display: flex;
justify-content: space-between;
.dimming-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 5px 8px;
}
.dimming-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
width: 18%;
gap: 4px;
min-width: 0;
}
.channel-label {
@ -863,13 +1063,12 @@ onUnmounted(() => {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 6px;
padding: 4px 6px;
color: rgba(255, 255, 255, 0.7);
font-weight: bold;
text-align: center;
width: 100%;
box-sizing: border-box;
cursor: pointer;
transition: all 0.3s ease;
}
@ -880,19 +1079,14 @@ onUnmounted(() => {
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 {
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #eee;
width: 100vw;
padding: 0 30px;
height: 50px;
padding: 0 24px;
height: 44px;
position: fixed;
bottom: 0;
left: 0;
@ -913,17 +1107,19 @@ onUnmounted(() => {
.back-button,
.restart-button {
width: 100%;
height: 38px;
border-radius: 19px;
font-size: 16px;
height: 34px;
border-radius: 17px;
font-size: 15px;
font-weight: 500;
transition: all 0.3s ease;
}
.back-button {
background-color: #000;
color: #fff;
// background-color: #000;
// color: #fff;
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);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 10px;
padding: 0 10px;
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
justify-content: center;
}
.control-title {
@ -978,8 +1177,8 @@ onUnmounted(() => {
}
.mode-button.active {
background: #ffffff;
color: #1976d2;
background: linear-gradient(180deg, rgba(51, 224, 188, 1) 0%, rgba(5, 149, 117, 1) 100%);
color: #fff;
}
.setting-button {
@ -1077,14 +1276,6 @@ onUnmounted(() => {
border-color: #006eff;
margin-left: 20px;
}
.info_item {
color: #fff;
margin-left: 20px;
border: 0.5px solid #00E0F4;
padding: 5px 10px;
border-radius: 5px;
}
}
}
</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')
// https://vite.dev/config/
export default defineConfig({
base: 'central-control',
// base: 'central-control',
plugins: [
vue(),
// vueDevTools(),
@ -59,9 +59,8 @@ export default defineConfig({
open: true,
proxy: {
'/jsonrpc': {
// target: 'https://lamp.ruixininfo.com',
target: 'https://smart-light.ruixininfo.com',
// target: 'http://171.208.94.46:7070',
target: 'https://lamp-dev.ruixininfo.com',
// target: 'http://10.21.77.100:71',
changeOrigin: true,
secure: false,
},

Loading…
Cancel
Save