增加密码

触摸屏-直流
Lufaneng 4 months ago
parent b75fecefce
commit de483cecf8

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

@ -31,6 +31,7 @@ declare module 'vue' {
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,125 @@
<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>
<BaseKeyboardInput
v-model="password"
layout-name="number"
type="password"
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 BaseKeyboardInput from '@/components/BaseKeyboardInput.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)
} 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({

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

@ -52,7 +52,8 @@ import {
Operation,
Setting,
VideoPlay,
Bell
Bell,
Tools
} from '@element-plus/icons-vue'
import RPC from '@/utils/rpc'
const currentTime = ref('')
@ -76,7 +77,8 @@ const functionCards = [
{ 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 = () => {

@ -0,0 +1,434 @@
<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>
<el-input
v-model="pwdForm.oldPassword"
type="password"
placeholder="请输入原密码"
show-password
style="flex:1"
/>
</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"
/>
</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"
/>
</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'
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 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 goBack = () => {
router.push('/')
}
let timeInterval = null
onMounted(() => {
updateTime()
timeInterval = setInterval(updateTime, 1000)
getSystemTime()
})
onUnmounted(() => {
if (timeInterval) clearInterval(timeInterval)
})
</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; }
}
}
}
}
}
.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;
}
.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;
}
```
Loading…
Cancel
Save