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,19 +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'
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)
// 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']
@ -19,16 +21,19 @@ declare module 'vue' {
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>

@ -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)
}

@ -18,12 +18,12 @@
<!-- 主体区域 -->
<div class="control-content">
<div style="width: 100%;">
<el-button @click="handleDeal" class="handleBtn" type="success">
<!-- <el-button @click="handleDeal" class="handleBtn" type="success">
<el-icon>
<Delete />
</el-icon>
批量处理
</el-button>
</el-button> -->
<el-button @click="handleDel" class="delBtn" type="danger">
<el-icon>
<Delete />
@ -31,7 +31,7 @@
批量删除
</el-button>
</div>
<el-table :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="回路地址" />
@ -47,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>
<!-- 底部信息 -->
@ -96,22 +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 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 handleSizeChange = () => {
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 = (size) => {
pageSize.value = size
currentPage.value = 1
getData()
applyPageData()
}
const handleCurrentChange = () => {
getData()
applyPageData()
}
const tableData = ref([])
const multipleTableRef = ref(null)
const multipleSelection = ref([])
const selectable = (row) => true
@ -164,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
@ -183,8 +194,10 @@ 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
})
@ -229,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>
@ -247,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);
}
@ -291,18 +321,24 @@ 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;
}
}
:deep(.el-pagination.is-background .el-pager li.is-active) {
background-color: #24806B;
}
}
@ -342,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);
}
}
@ -366,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>

@ -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,6 +260,13 @@ 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) => {
currentTaskName.value = item
if (item === '中间任务') {
@ -541,15 +553,34 @@ const restartSystem = () => {
let timeInterval = null
// 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 = () => {
@ -579,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 {
@ -660,7 +692,7 @@ const delMode = () => {
}
&.selected {
background-color: #0881FE;
background-color: #FFAF11;
}
}
}
@ -675,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;
@ -691,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;
@ -730,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%;
}
@ -912,7 +958,7 @@ const delMode = () => {
}
.time-item {
color: rgba(255, 255, 255, 0.7);
color: #fff;
font-size: 16px;
margin: 0 20px;
}
@ -928,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;
}
}
@ -943,7 +991,7 @@ const delMode = () => {
.buttonBox {
.el-button {
margin: 15px 0;
margin: 10px 0;
&:last-child {
margin-bottom: 0;
@ -987,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 {
@ -1002,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 {
@ -1033,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) {
@ -1043,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 {
@ -1062,7 +1116,7 @@ const delMode = () => {
font-size: 18px;
font-weight: 500;
margin-bottom: 30px;
color: #ffffff;
color: #6C75A0;
}
.brightness-slider-container {
@ -1138,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;
}
}
@ -1151,10 +1205,10 @@ const delMode = () => {
:deep(.el-input__wrapper) {
background-color: transparent;
color: #fff;
color: #6C75A0;
input {
color: #fff;
color: #6C75A0;
}
}

@ -201,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>
@ -214,7 +214,8 @@ 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('')
@ -226,25 +227,7 @@ const selectedChannelName = ref(null)
const week = ref('')
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
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 + 'A'
energyParams.value.powerFactor = res.powerFactor
})
//
const powerPhases = ref([
@ -273,7 +256,7 @@ const powerPhases = ref([
const energyParams = ref({
dcVoltage: '0.0V',
activeEnergy: '0kwh',
activeEnergy: '0kh',
activePower: '0kW',
powerFactor: '0'
})
@ -388,7 +371,6 @@ const getInfo = () => {
isInitData.value = false
})
}
getInfo()
const oldInfo = ref('')
watch(info, () => {
@ -409,6 +391,35 @@ 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
})
})
})
//
@ -535,15 +546,27 @@ const changeMode = () => {
let timeInterval = null
// session
const handleSessionRestored = () => {
console.log('Session 已恢复,重新加载数据')
getInfo()
}
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>
@ -554,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 {
@ -640,7 +664,7 @@ onUnmounted(() => {
justify-content: center;
align-items: center;
margin-bottom: 15px;
background-color: #212329;
background-color: #24806B;
width: 30px;
height: 100%;
}
@ -701,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 {
@ -772,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);
}
@ -803,7 +827,7 @@ onUnmounted(() => {
}
.time-item {
color: rgba(255, 255, 255, 0.7);
color: #fff;
font-size: 16px;
margin: 0 20px;
}
@ -819,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;
}
}
@ -875,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 {
@ -943,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>智能中控装置</h1>
<h1>直流照明监控平台</h1>
<div class="title-decoration right"></div>
</div>
@ -32,7 +35,7 @@
设备号{{ commit_uid }}
</div>
<div class="status-item">
版本号1.0.0
版本号{{ version }}
</div>
<div class="status-item">
系统状态<span class="status-online">开启</span>
@ -49,24 +52,33 @@ import {
Operation,
Setting,
VideoPlay,
Bell
Bell,
Tools
} from '@element-plus/icons-vue'
import RPC from '@/utils/rpc'
const currentTime = ref('')
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)
sessionStorage.setItem('central_control_comm_uid', route.query.comm_uid)
}
const commit_uid = localStorage.getItem('central_control_comm_uid')
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 = () => {
@ -95,25 +107,44 @@ const handleCardClick = (key) => {
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 {
@ -126,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 {
@ -157,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;
}
@ -170,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,

@ -73,80 +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 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 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 class="circuit-icon" :class="{ active: circuit.is_open }">
</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>
<Sunny />
<RefreshRight />
</el-icon>
</div> -->
<div class="circuitStatus">
<div class="line">直流电流: <span>{{ circuit.A }}A</span></div>
<div class="line">
<div class="line_item">电源: <span>{{ circuit.V }}V</span></div>
<div class="line_item">功率: <span>{{ circuit.Kw }}kw</span></div>
</div>
</div>
刷新
</el-button>
</div>
</div>
<!-- 下排回路 -->
<div class="circuit-row">
<div v-for="(circuit, index) in info.Loops.slice(5, 10)" :key="circuit.name" class="circuit-item">
<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 }">
回路{{ index + 6 }}{{ circuit.is_open ? '打开' : '关闭' }}
直流回路{{ loopNo(circuit, index) }}{{ 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">直流电流: <span>{{ circuit.A }}A</span></div>
<div class="line">
<div class="line_item">电源: <span>{{ circuit.V }}V</span></div>
<div class="line_item">功率: <span>{{ circuit.Kw }}kw</span></div>
<div class="line_item">电压: <span>{{ circuit.V ? circuit.V?.toFixed(2) : 0 }}V</span></div>
<div class="line_item">电流: <span>{{ circuit.A ? circuit.A?.toFixed(2) : 0 }}A</span></div>
</div>
<div class="line">
<div class="line_item line_item_full">功率: <span>{{ circuit.Kw ? circuit.Kw?.toFixed(3) : 0 }}kW</span></div>
</div>
</div>
</div>
</div>
</div>
<div 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>
</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="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 }}%
{{ 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>
<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>
@ -198,13 +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 commit_uid = '868656077692382'
const commit_uid = sessionStorage.getItem('central_control_comm_uid')
const router = useRouter()
const currentTime = ref('')
@ -212,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 = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
@ -232,15 +228,15 @@ function transformHourlyData(data) {
const lineData = ref([])
const recdDate = ref(dayjs().add(-1, 'day').format('YYYY-MM-DD'))
const lineDataLoading = ref(false)
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()
// 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()
//
const powerPhases = ref([
@ -277,6 +273,7 @@ const energyParams = ref({
const info = ref({
"Mode": 0,
"scan_device_count": 10,
"backup_byte1": 0,
"Loops": [
{
@ -373,11 +370,113 @@ const info = ref({
"backup_byte4": 0
})
const getInfo = () => {
// RPC.post('/m9z/getDeviceManualInfo', { comm_uid: commit_uid }).then(res => {
// info.value = res
// })
RPC.post('/adl400/readPhaseData', { comm_uid: commit_uid }).then(res => {
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'
@ -393,13 +492,17 @@ const getInfo = () => {
powerPhases.value[2].power = res.cPower + 'kW'
powerPhases.value[2].factor = res.cPowerFactor
energyParams.value.activeEnergy = res.activeEnergy + 'A'
energyParams.value.activeEnergy = res.activeEnergy + 'kW·h'
energyParams.value.activePower = res.activePower + 'kW'
energyParams.value.powerFactor = res.powerFactor
})
RPC.post('/m9z/getDeviceStatus2', { comm_uid: commit_uid }).then((res) => {
//
info.value = res
})
}
const getInfo = () => {
// RPC.post('/m9z/getDeviceManualInfo', { comm_uid: commit_uid }).then(res => {
// info.value = res
// })
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') : ''
@ -408,12 +511,35 @@ const getInfo = () => {
}
getInfo()
const timer = setInterval(() => {
getInfo()
}, 30000)
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)
})
onUnmounted(() => {
clearInterval(timer)
if (timer) {
clearInterval(timer)
}
})
const oldInfo = ref('')
@ -526,15 +652,27 @@ const restartSystem = () => {
let timeInterval = null
// session
const handleSessionRestored = () => {
console.log('Session 已恢复,重新加载数据')
getInfo()
}
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>
@ -545,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 {
@ -554,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);
}
@ -573,7 +715,7 @@ onUnmounted(() => {
}
.page-title h1 {
font-size: 30px;
font-size: 26px;
font-weight: bold;
margin: 0;
color: #ffffff;
@ -590,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,
@ -634,7 +779,7 @@ onUnmounted(() => {
justify-content: center;
align-items: center;
margin-bottom: 15px;
background-color: #212329;
background-color: #24806B;
width: 30px;
height: 100%;
}
@ -642,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 {
@ -663,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;
@ -681,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%;
}
}
}
}
@ -700,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%;
@ -750,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 {
@ -776,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;
}
@ -793,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;
@ -826,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;
}
}
@ -853,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 {
@ -891,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 {

@ -25,7 +25,7 @@ function closed() {
<el-table-column align="center" prop="DeviceAddress" label="设备地址" />
<el-table-column align="center" prop="CurrentPWMDuty" label="电流PWM">
<template #default="{ row }">
{{ row.CurrentPWMDuty && row.CurrentPWMDuty + '%' }}
{{ row.CurrentPWMDuty && row.CurrentPWMDuty?.toFixed(2) + '%' }}
</template>
</el-table-column>
<el-table-column align="center" prop="VoltagePWMDuty" label="电压PWM">

@ -32,19 +32,21 @@
<div class="item">
<div class="key">大地经度</div>
<!-- <el-input v-model="formField.lat" style="width: 100px" placeholder="请输入" :formatter="(value) => `${value}˚`" :parser="(value) => value.replace(/˚/g, '')" /> -->
<el-input-number :min="0" :max="180" v-model="formField.lat" style="width: 100px" placeholder="请输入" :controls="false" />
<!-- <el-input-number :min="0" :max="180" v-model="formField.lat" style="width: 100px" placeholder="请输入" :controls="false" /> -->
<BaseKeyboardInput v-model="formField.lgn" layout-name="number"></BaseKeyboardInput>
</div>
<div class="item">
<div class="key">大地纬度</div>
<!-- <el-input v-model="formField.lgn" style="width: 100px" placeholder="请输入" :formatter="(value) => `${value}˚`" :parser="(value) => value.replace(/˚/g, '')" /> -->
<el-input-number :min="0" :max="90" v-model="formField.lgn" style="width: 100px" placeholder="请输入" :controls="false" />
<!-- <el-input-number :min="0" :max="90" v-model="formField.lgn" style="width: 100px" placeholder="请输入" :controls="false" /> -->
<BaseKeyboardInput v-model="formField.lat" layout-name="number"></BaseKeyboardInput>
</div>
</div>
<div class="line">
<div class="item">
<div class="key">经纬开灯偏差</div>
<!-- <el-input readonly v-model="formField.rise" style="width: 100px" placeholder="请输入" :formatter="(value) => `${value}分`" :parser="(value) => value.replace(/分/g, '')" /> -->
<el-select v-model="formField.rise" placeholder="请选择">
<el-select @click="chooseRiseSet('rise')" disabled v-model="formField.rise" placeholder="请选择">
<el-option v-for="item in RiseSetOption" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
@ -53,7 +55,7 @@
<div class="item">
<div class="key">经纬关灯偏差</div>
<!-- <el-input readonly v-model="formField.set" style="width: 100px" placeholder="请输入" :formatter="(value) => `${value}分`" :parser="(value) => value.replace(/分/g, '')" /> -->
<el-select v-model="formField.set" placeholder="请选择">
<el-select @click="chooseRiseSet('set')" disabled v-model="formField.set" placeholder="请选择">
<el-option v-for="item in RiseSetOption" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
@ -117,7 +119,7 @@
<div class="line">
<div class="item">
<div class="key">电能表型号</div>
<div class="value">ADL400</div>
<div class="value">{{ currentMeterFirm || '--' }}</div>
<!-- <el-input v-model="formField.g" style="width: 100px" placeholder="请输入" /> -->
</div>
</div>
@ -128,6 +130,22 @@
<!-- <el-input v-model="formField.f" style="width: 100px" placeholder="请输入" /> -->
</div>
</div>
<!-- <div class="line">
<div class="item">
<div class="key">测试值</div>
<div class="value">
<BaseKeyboardInput v-model="test.value"></BaseKeyboardInput>
</div>
</div>
</div>
<div class="line">
<div class="item">
<div class="key">测试数字</div>
<div class="value">
<BaseKeyboardInput v-model="test.number" layout-name="number"></BaseKeyboardInput>
</div>
</div>
</div> -->
</div>
</div>
</div>
@ -161,6 +179,22 @@
</div>
</div>
<TableDialog v-if="isShowDialog" @closed="isShowDialog = false" :tableData="currentTableData" :title="currentTitle"></TableDialog>
<el-dialog v-model="showRiseSetDialog" title="选择偏差" width="560">
<div>
<el-radio-group v-model="currentValue">
<el-radio v-for="item in RiseSetOption" :key="item.value" :value="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="showRiseSetDialog = false">取消</el-button>
<el-button type="primary" @click="confirmRiseSet">
确认
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@ -169,6 +203,26 @@ import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import RPC from '@/utils/rpc';
import TableDialog from './TableDialog.vue';
import BaseKeyboardInput from '@/components/BaseKeyboardInput.vue';
const test = ref({
value: '123',
number: 90
})
const showRiseSetDialog = ref(false)
const currentType = ref('')
const currentValue = ref('')
const chooseRiseSet = type => {
currentType.value = type
currentValue.value = formField.value[type]
showRiseSetDialog.value = true
}
const confirmRiseSet = () => {
formField.value[currentType.value] = currentValue.value
showRiseSetDialog.value = false
}
const router = useRouter()
const currentTime = ref('')
@ -176,10 +230,10 @@ const systemRuntime = ref('38分钟')
const formField = ref({})
const deviceTime = ref('')
const subLoopParameter = ref([])
const commit_uid = localStorage.getItem('central_control_comm_uid')
const commit_uid = sessionStorage.getItem('central_control_comm_uid')
// const commit_uid = '868656077692382'
const RiseSetOption = [
{ label: '0', value: 0 },
{ label: '提前60分钟', value: 60 },
{ label: '提前50分钟', value: 50 },
{ label: '提前40分钟', value: 40 },
@ -192,11 +246,12 @@ const RiseSetOption = [
{ label: '延后40分钟', value: -40 },
{ label: '延后50分钟', value: -50 },
{ label: '延后60分钟', value: -60 },
{ label: '0', value: 0 },
]
RPC.post('/m9z/getLgnLatData', { comm_uid: commit_uid }).then(res => {
formField.value.lat = res.lat
formField.value.lgn = res.lgn
formField.value.lat = String(res.lat)
formField.value.lgn = String(res.lgn)
formField.value.loc = res.loc
})
RPC.post('/m9z/getSunRiseSet', { comm_uid: commit_uid }).then(res => {
@ -501,10 +556,10 @@ RPC.post('/m9z/getAllSubLoopParameter', { comm_uid: commit_uid }).then(res => {
// }
})
const address = ref('')
RPC.post('/adl400/getMsgAddress', { comm_uid: commit_uid }).then(res => {
address.value = res
})
const address = ref(sessionStorage.getItem('central_control_meterAddr') || '')
// RPC.post('/adl400/getMsgAddress', { comm_uid: commit_uid }).then(res => {
// address.value = res
// })
const week = ref('')
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
@ -578,15 +633,55 @@ const goBack = () => {
let timeInterval = null
const currentMeterFirm = ref('')
const getMeterFirm = async () => {
try {
const res = await RPC.post('/m9z/meter/getMeterFirm', {})
currentMeterFirm.value = res?.meter_firm || ''
} catch (e) {
// rpc.js
}
}
getMeterFirm()
// session
const handleSessionRestored = () => {
console.log('Session 已恢复,重新加载数据')
//
RPC.post('/m9z/getLgnLatData', { comm_uid: commit_uid }).then(res => {
formField.value.lat = String(res.lat)
formField.value.lgn = String(res.lgn)
formField.value.loc = res.loc
})
RPC.post('/m9z/getSunRiseSet', { comm_uid: commit_uid }).then(res => {
formField.value.rise = res.rise
formField.value.set = res.set
})
RPC.post('/m9z/getDeviceTime', { comm_uid: commit_uid }).then(res => {
deviceTime.value = res.deviceTime
})
RPC.post('/m9z/getAllSubLoopParameter', { comm_uid: commit_uid }).then(res => {
subLoopParameter.value = res
})
getMeterFirm()
}
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>
@ -597,7 +692,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 {
@ -646,7 +742,7 @@ onUnmounted(() => {
box-sizing: border-box;
.main_item {
border: .5px solid #ddd;
border: .5px solid #0e9228;
display: flex;
flex-direction: column;
height: 450px;
@ -657,15 +753,17 @@ onUnmounted(() => {
align-items: center;
height: 50px;
font-size: 16px;
background-color: #21232A;
color: #8088AA;
border-bottom: .5px solid #ddd;
// background-color: #21232A;
background-color: rgba($color: #188549, $alpha: .3);
color: #fff;
border-bottom: .5px solid #0e9228;
}
.main_content {
padding: 20px;
font-size: 14px;
background-color: #282d36;
// background-color: #282d36;
background-color: rgba($color: #000000, $alpha: .3);
flex: 1;
&.energy_meter {
@ -686,8 +784,8 @@ onUnmounted(() => {
.value {
text-align: center;
background-color: #21232A;
color: #8088AA;
background-color: rgba($color: #188549, $alpha: .3);
color: #fff;
padding: 5px 10px;
min-height: 32px;
box-sizing: border-box;
@ -697,7 +795,7 @@ onUnmounted(() => {
.subLoopParameter_item {
border-radius: 10px;
border: 1px solid #fff;
border: 1px solid #0e9228;
padding: 15px;
margin-bottom: 20px;
@ -795,9 +893,11 @@ 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;
}
}
@ -807,22 +907,22 @@ onUnmounted(() => {
}
:deep(.el-input__wrapper) {
background-color: #212329 !important;
background-color: rgba($color: #188549, $alpha: .3) !important;
box-shadow: none;
border: 1px soild #3B3D45;
border: 1px soild #0e9228;
input {
color: #8088AA;
color: #fff;
}
}
:deep(.el-select__wrapper) {
background-color: #212329 !important;
background-color: rgba($color: #188549, $alpha: .3) !important;
box-shadow: none;
border: 1px soild #3B3D45;
border: 1px soild #0e9228;
.el-select__placeholder {
color: #8088AA;
color: #fff;
}
}
</style>

@ -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(),
@ -60,6 +60,7 @@ export default defineConfig({
proxy: {
'/jsonrpc': {
target: 'https://lamp-dev.ruixininfo.com',
// target: 'http://10.21.77.100:71',
changeOrigin: true,
secure: false,
},

Loading…
Cancel
Save