diff --git a/src/App.vue b/src/App.vue index ff9f8ca..e872eeb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,19 +6,34 @@ + + \ No newline at end of file diff --git a/src/components.d.ts b/src/components.d.ts index 388b3c9..fa37de8 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -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'] } diff --git a/src/components/PasswordDialog.vue b/src/components/PasswordDialog.vue new file mode 100644 index 0000000..1d0b59d --- /dev/null +++ b/src/components/PasswordDialog.vue @@ -0,0 +1,125 @@ + + + + + + + diff --git a/src/router/index.js b/src/router/index.js index bf324bd..d8c8c60 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -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({ diff --git a/src/utils/rpc.js b/src/utils/rpc.js index 294fc8a..c2efce9 100644 --- a/src/utils/rpc.js +++ b/src/utils/rpc.js @@ -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) } diff --git a/src/views/Menu/index.vue b/src/views/Menu/index.vue index e83ca50..e8d7181 100644 --- a/src/views/Menu/index.vue +++ b/src/views/Menu/index.vue @@ -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 = () => { diff --git a/src/views/SystemSettings/index.vue b/src/views/SystemSettings/index.vue new file mode 100644 index 0000000..4148541 --- /dev/null +++ b/src/views/SystemSettings/index.vue @@ -0,0 +1,434 @@ + + + + + diff --git a/tty_password_api.md b/tty_password_api.md new file mode 100644 index 0000000..1b12a8a --- /dev/null +++ b/tty_password_api.md @@ -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; +} +```