fix(mp): 接入 vue-tsc 真类型检查并修掉它抓出的两个真实缺陷
小程序端此前**没有任何类型门禁**:tsc 不检查 .vue,而 9 个页面全是 .vue, npm run build:mp-weixin 只验语法不验类型。接入 vue-tsc 后立刻抓到 17 个错误, 其中两个是真 BUG: 1. me/index.vue:写 const user = authStore.user 之后又写 user.value —— Pinia setup store 对外已把 ref 解包,user.value 恒为 undefined ⇒ **头像上传成功后写回 store 的那行是空操作**(要等冷启动才显示新头像); 且直接取值是快照、丢掉响应式。改为 computed(() => authStore.user),模板无需改动。 2. api/index.ts:CompartmentDetail 漏了后端确实会返回的 temp (routes/h5.rs 的 COMPARTMENT_REALTIME_FIELDS 第 6 项,有单测守门) ⇒ compartment-detail 页读 data.temp 报 TS2339。已按后端契约补上。 其余 12 处是宿主全局 wx 未声明(@dcloudio/types 只声明跨端 uni), 在 env.d.ts 里**只声明实际用到的两个 API**(刻意不写 [k:string]:any, 否则等于把这一层检查关掉)。 顺带清理: - 删除死页面 pages/devices/index.vue(不在 pages.json、无任何跳转引用,grep 确认); - 删除仓控板「在线/离线」徽标:cabin_boards.status **全后端没有任何写入方** (生产库实测取值只有 0),该徽标恒定显示"离线",属对用户的谎报; 协议层面也没有"仓控板在线"这个概念(TCP 长连接的是柜控板,按 IMEI 标识)。 按「柜的就显示到柜子」+「不显示推不出来的状态」去掉,并在类型上加禁止说明。 已按仓库铁律自证门禁能失败:植入 export const probe: number = 'x' → exit 2;撤销 → exit 0。
This commit is contained in:
parent
c3a3ea7c3a
commit
aa02cd4e58
@ -3,7 +3,8 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:mp-weixin": "uni build --platform mp-weixin"
|
||||
"build:mp-weixin": "uni build --platform mp-weixin",
|
||||
"typecheck": "vue-tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dcloudio/uni-app": "3.0.0-4060620250520001",
|
||||
@ -21,6 +22,7 @@
|
||||
"@dcloudio/uni-stacktracey": "3.0.0-4060620250520001",
|
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.8"
|
||||
"vite": "^5.2.8",
|
||||
"vue-tsc": "^2.2.12"
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,7 +116,21 @@ export interface CabinetItem {
|
||||
id: number
|
||||
abstract_id: string
|
||||
name?: string
|
||||
/**
|
||||
* **配置态**:`1` = 启用(默认)/ `2` = 禁用(后端据此拦截开合闸、开门、充电)。
|
||||
*
|
||||
* ⛔ **不表示在线** —— 全仓没有任何写入方会把它写成 `0`(离线)。
|
||||
* 展示"在线/离线"一律用 [`CabinetItem.is_online`]。
|
||||
*/
|
||||
status: number
|
||||
/**
|
||||
* **实时在线**(`GET /api/h5/cabinets` 返回,值来自 Redis `device:{imei}.online`)
|
||||
*
|
||||
* `true` 在线 / `false` 明确离线 / `null`、缺失 = 无数据(键或字段不存在)。
|
||||
* 口径与 PC 设备列表 `is_online` **同源**(后端同一纯函数)。
|
||||
* ⚠️ 与 `status`(启用/禁用配置态)严格分离,不得互相推断。
|
||||
*/
|
||||
is_online?: boolean | null
|
||||
project_id?: number
|
||||
rssi?: number
|
||||
imei?: string
|
||||
@ -164,6 +178,16 @@ export interface CompartmentInfo {
|
||||
export interface CabinBoard {
|
||||
id: number
|
||||
board_id: number
|
||||
/**
|
||||
* ⛔ **不要用它推断"仓控板在线"** —— `cabin_boards.status` 在**整个后端没有任何写入方**
|
||||
* (全仓只有建表默认值 0,`INSERT INTO cabin_boards (cabinet_id, board_id)` 没写这一列;
|
||||
* 生产库实测取值分布只有 `0`)。协议里也**没有"仓控板在线"的概念**:
|
||||
* 与平台保持 TCP 长连接的是**柜控板**(按 IMEI 标识),仓控板挂在柜内 485 总线上,
|
||||
* 没有独立上行通路 ⇒ 这个状态**推不出来**。
|
||||
* 该列保留仅为兼容后端响应形状;**界面不得据此显示"在线/离线"**
|
||||
* (此前小程序展示的板级徽标恒定显示"离线",属谎报,已删除)。
|
||||
* 柜级在线状态请看 `CabinetDetail.is_online`(Redis 实时值)。
|
||||
*/
|
||||
status: number
|
||||
compartments: CompartmentInfo[]
|
||||
}
|
||||
@ -174,7 +198,19 @@ export interface CabinetDetail {
|
||||
name?: string
|
||||
imei?: string
|
||||
iccid?: string
|
||||
/**
|
||||
* **配置态**:`1` = 启用 / `2` = 禁用(后端据此拦截指令链路)。
|
||||
*
|
||||
* ⛔ **不表示在线**;展示"在线/离线"请用 [`CabinetDetail.is_online`]。
|
||||
*/
|
||||
status: number
|
||||
/**
|
||||
* **实时在线**(`GET /api/cabinets/{id}` 返回,值来自 Redis `device:{imei}.online`)
|
||||
*
|
||||
* `true` 在线 / `false` 明确离线 / `null`、缺失 = 无数据。
|
||||
* 判据与设备列表 `is_online` **同源**(后端同一纯函数)。
|
||||
*/
|
||||
is_online?: boolean | null
|
||||
rssi?: number
|
||||
/** 直流停电(协议 §5.4);true=停电 / false=正常 / null=无数据 */
|
||||
pow_fail_dc?: boolean | null
|
||||
@ -214,6 +250,15 @@ export interface CompartmentDetail {
|
||||
batt_status?: string | null
|
||||
/** 电源状态(协议 §3.8,2 位 hex);与电池状态相互独立,null/缺失 = 展示 "-" */
|
||||
pow_status?: string | null
|
||||
/**
|
||||
* **仓体温度**(协议附 A,K×100)
|
||||
*
|
||||
* 后端 `GET /api/h5/compartments/{id}` 会返回该字段
|
||||
* (`routes/h5.rs` 的 `COMPARTMENT_REALTIME_FIELDS` 第 6 项 `"temp"`,有单测守门),
|
||||
* 展示走 `utils/deviceStatus.ts::formatCelsius`。
|
||||
* 此前类型里漏了它 ⇒ 页面 `data.temp` 报 TS2339(类型契约与后端不一致,已补)。
|
||||
*/
|
||||
temp?: number | null
|
||||
}
|
||||
|
||||
export interface ChargeRecord {
|
||||
|
||||
28
software/h5-uniapp/src/env.d.ts
vendored
28
software/h5-uniapp/src/env.d.ts
vendored
@ -5,3 +5,31 @@ declare module '*.vue' {
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序宿主全局 `wx`。
|
||||
*
|
||||
* **为什么需要手工声明**:`@dcloudio/types` 只声明跨端的 `uni`,**不声明宿主特有的 `wx`**;
|
||||
* 而自定义导航栏的安全区计算必须用宿主 API(胶囊按钮位置没有等价的跨端取法),
|
||||
* 所以 6 个页面里直接用了 `wx.*`。不声明时 `vue-tsc` 会报 12 处
|
||||
* `TS2304: Cannot find name 'wx'` —— 噪声会把**真正的类型错误淹掉**
|
||||
* (与「打印 ❌ 却 exit 0」是同一类问题:判据不再指向真实缺陷)。
|
||||
*
|
||||
* ⚠️ **刻意只声明实际用到的两个 API**,不写 `[k: string]: any`:
|
||||
* 后者会让 `wx.anything()` 全部通过,等于把这一层检查关掉。需要新宿主 API 时,
|
||||
* 按同样方式在下面补一条(签名照官方文档抄)。
|
||||
*
|
||||
* 运行期说明:`wx` 只存在于 `mp-weixin` 产物中,而本工程只构建该目标(`build:mp-weixin`)。
|
||||
*/
|
||||
declare const wx: {
|
||||
/** 取宿主系统信息(本工程只消费 `statusBarHeight`) */
|
||||
getSystemInfoSync(): {
|
||||
/** 状态栏高度(px) */
|
||||
statusBarHeight?: number
|
||||
}
|
||||
/** 取右上角胶囊按钮布局,用于按"胶囊高度 + 上下间距"反推自定义导航栏高度 */
|
||||
getMenuButtonBoundingClientRect(): {
|
||||
top: number
|
||||
height: number
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,13 @@
|
||||
<view class="info-top">
|
||||
<view><text class="info-id">{{ data.abstract_id }}</text></view>
|
||||
<view class="info-status-row">
|
||||
<view class="status-badge" :class="data.status === 1 ? 'online' : 'offline'"><text>{{ data.status === 1 ? '在线' : '离线' }}</text></view>
|
||||
<!-- 状态徽标:`status === 2` = 禁用(配置态);其余按**接口的实时在线字段** `is_online`
|
||||
(Redis `device:{imei}.online`,与 PC 设备列表 `is_online` 同源)。
|
||||
⛔ 不得用 `data.status` 推断在线。 -->
|
||||
<view
|
||||
class="status-badge"
|
||||
:class="data.status === 2 ? 'disabled' : (data.is_online === true ? 'online' : 'offline')"
|
||||
><text>{{ data.status === 2 ? '禁用' : (data.is_online === true ? '在线' : '离线') }}</text></view>
|
||||
<view class="signal"><view v-for="(color, i) in rssiBarColors(data.rssi)" :key="i" class="signal-bar" :class="'l' + (i + 1)" :style="{ background: color }"></view></view>
|
||||
<text class="rssi-val">{{ data.rssi != null ? data.rssi : '—' }}</text>
|
||||
<view class="pow-tags">
|
||||
@ -71,7 +77,16 @@
|
||||
<view v-for="board in data.cabin_boards" :key="board.id" class="board-section">
|
||||
<view class="board-header">
|
||||
<text class="board-title">{{ data.imei }}-{{ board.board_id }}</text>
|
||||
<view class="board-status" :class="board.status === 1 ? 'online' : 'offline'"><text>{{ board.status === 1 ? '在线' : '离线' }}</text></view>
|
||||
<!-- ⛔ 这里**不再显示"在线/离线"徽标**。
|
||||
原因(实测 2026-09-20):`cabin_boards.status` 在**整个后端没有任何写入方**
|
||||
—— 全仓只有建表默认值,`INSERT INTO cabin_boards (cabinet_id, board_id)` 根本没写它;
|
||||
生产库实测取值分布只有 `0`(2/2 行),接口也只是把该列读出来。
|
||||
所以此前那句 `board.status === 1 ? '在线' : '离线'` 是**恒定显示"离线"**,
|
||||
属对用户的**谎报**(比不显示更糟:运维会据此以为板子坏了)。
|
||||
协议层面也**没有"仓控板在线"这个概念**:与平台保持 TCP 连接的是柜控板(按 IMEI 标识),
|
||||
仓控板挂在柜内 485 总线上,没有独立的上行通路 ⇒ 该项状态**推不出来**。
|
||||
按既定口径「柜的就显示到柜子,仓体的就显示到仓上」+「不显示推不出来的状态」,
|
||||
这里直接去掉;柜级在线状态在同页顶部按 Redis 实时值显示。 -->
|
||||
</view>
|
||||
<view class="comp-grid">
|
||||
<view v-for="comp in board.compartments" :key="comp.id" class="comp" :class="compClass(comp.status)" @click="goCompartment(comp.id)">
|
||||
@ -247,7 +262,9 @@ function openLocation() {
|
||||
.status-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; font-size: 11px; font-weight: 500; border-radius: 999px; }
|
||||
.status-badge.online { background: #f0fdf4; color: #22c55e; }
|
||||
.status-badge.offline { background: #f1f5f9; color: #94a3b8; }
|
||||
.status-badge.online::before, .status-badge.offline::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
|
||||
/* 配置态「禁用」(`cabinets.status === 2`),与"离线"区分 */
|
||||
.status-badge.disabled { background: #fff7ed; color: #f97316; }
|
||||
.status-badge.online::before, .status-badge.offline::before, .status-badge.disabled::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
|
||||
.signal { display: flex; align-items: flex-end; gap: 1.5px; height: 14px; }
|
||||
.signal-bar { width: 2.5px; border-radius: 1px; }
|
||||
.signal-bar.l1 { height: 4px; } .signal-bar.l2 { height: 7px; } .signal-bar.l3 { height: 10px; } .signal-bar.l4 { height: 13px; } .signal-bar.l5 { height: 14px; }
|
||||
@ -287,8 +304,8 @@ function openLocation() {
|
||||
.board-section { margin: 12px 16px; background: #fff; border-radius: 12px; padding: 14px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }
|
||||
.board-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
||||
.board-title { font-size: 14px; font-weight: 600; color: #111827; font-variant-numeric: tabular-nums; }
|
||||
.board-status { font-size: 11px; color: #94a3b8; }
|
||||
.board-status.online { color: #22c55e; }
|
||||
/* `.board-status` 样式已随"板级在线徽标"一并删除:该状态无数据来源(见模板注释),
|
||||
留着只会诱使后来者再加回那个恒定显示"离线"的徽标。 */
|
||||
.comp-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
|
||||
.comp { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 8px; border: 1.5px solid transparent; }
|
||||
.comp:active { transform: scale(0.97); }
|
||||
|
||||
@ -40,7 +40,16 @@
|
||||
<view class="device-header">
|
||||
<view class="device-id-row">
|
||||
<text class="device-id">{{ cab.abstract_id }}</text>
|
||||
<view class="status-badge" :class="cab.status === 1 ? 'online' : 'offline'"><text>{{ cab.status === 1 ? '在线' : '离线' }}</text></view>
|
||||
<!-- 状态徽标:**配置态与实时态分开**
|
||||
- `status === 2` = 禁用(`cabinets.status` 配置态,后端据此拦截指令);
|
||||
- 否则按**接口返回的实时在线字段** `is_online` 显示「在线/离线」
|
||||
(值来自 Redis `device:{imei}.online`,与 PC 设备列表 `is_online` 同源)。
|
||||
⛔ 不得再用 `cab.status` 推断在线 —— 那是启用/禁用配置态,全仓没有写入方会把
|
||||
它写成 0(离线),拿它判在线会在一台设备都没连时依然显示「在线」。 -->
|
||||
<view
|
||||
class="status-badge"
|
||||
:class="cab.status === 2 ? 'disabled' : (cab.is_online === true ? 'online' : 'offline')"
|
||||
><text>{{ cab.status === 2 ? '禁用' : (cab.is_online === true ? '在线' : '离线') }}</text></view>
|
||||
</view>
|
||||
<view class="header-right">
|
||||
<text v-if="cab.temp != null" class="temp-badge">{{ formatCelsius(cab.temp) }}</text>
|
||||
@ -267,7 +276,9 @@ function showScanInput() {
|
||||
.status-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; font-size: 11px; font-weight: 500; border-radius: 999px; }
|
||||
.status-badge.online { background: #f0fdf4; color: #22c55e; }
|
||||
.status-badge.offline { background: #f1f5f9; color: #94a3b8; }
|
||||
.status-badge.online::before, .status-badge.offline::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
|
||||
/* 配置态「禁用」(`cabinets.status === 2`):与"离线"区分开的橙色徽标 */
|
||||
.status-badge.disabled { background: #fff7ed; color: #f97316; }
|
||||
.status-badge.online::before, .status-badge.offline::before, .status-badge.disabled::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
|
||||
.temp-badge { font-size: 12px; color: #475569; font-variant-numeric: tabular-nums; flex-shrink: 0; }
|
||||
.header-right { display: flex; align-items: center; gap: 8px; }
|
||||
.signal-inline { display: flex; align-items: center; gap: 3px; }
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useAuthStore } from '../../store/auth'
|
||||
import AppIcon from '../../components/AppIcon.vue'
|
||||
|
||||
@ -46,7 +46,12 @@ setTimeout(calcNav, 300)
|
||||
setTimeout(calcNav, 600)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const user = authStore.user
|
||||
// ⚠️ 必须用 `computed` 包一层,不能写 `const user = authStore.user`:
|
||||
// Pinia 的 **setup store** 对外已把 `ref` 解包,`authStore.user` 是**值**而不是 Ref,
|
||||
// 于是 ① `user.value` 恒为 `undefined`(头像上传成功后写回 store 的那行成了**空操作**,
|
||||
// 页面要等下次冷启动才显示新头像);② 直接取值是**快照**,store 之后被 `restore()`/
|
||||
// 重新登录改写时,页面仍指向旧对象(响应式丢失)。两处都是 vue-tsc 报出来的真实缺陷。
|
||||
const user = computed(() => authStore.user)
|
||||
function goFeedback() { uni.navigateTo({ url: '/pages/feedback/index' }) }
|
||||
function confirmLogout() {
|
||||
uni.showModal({ title: '退出登录', content: '确定要退出当前账号?', success: (res) => { if (res.confirm) { authStore.logout(); uni.reLaunch({ url: '/pages/login/index' }) } } })
|
||||
|
||||
@ -54,10 +54,16 @@ const STATUS_MAP: Record<number, { label: string; color: string }> = {
|
||||
3: { label: '故障', color: 'red' },
|
||||
};
|
||||
|
||||
/** 柜子状态映射 */
|
||||
/**
|
||||
* 柜子**配置态**映射(`cabinets.status`):只有「启用 / 禁用」两档。
|
||||
*
|
||||
* ⚠️ 这里**不含"在线/离线"**:`cabinets.status` 是启用/禁用配置态,
|
||||
* 全仓没有任何写入方会把它写成 `0`(离线),拿它判在线会让"一台设备都没连"时
|
||||
* 依然显示"在线"。实时在线一律看接口的 `is_online`(Redis,见 `onlineTag`)。
|
||||
* ⛔ 不要再往本表里加"离线"档。
|
||||
*/
|
||||
const CABINET_STATUS_MAP: Record<number, { label: string; color: string }> = {
|
||||
0: { label: '离线', color: 'red' },
|
||||
1: { label: '在线', color: 'green' },
|
||||
1: { label: '启用', color: 'green' },
|
||||
2: { label: '禁用', color: 'orange' },
|
||||
};
|
||||
|
||||
@ -483,7 +489,20 @@ export default function CabinetDetail() {
|
||||
);
|
||||
}
|
||||
|
||||
const cabStatus = CABINET_STATUS_MAP[detail.status] || { label: '未知', color: 'gray' };
|
||||
const cabStatus = CABINET_STATUS_MAP[detail.status] || { label: '启用', color: 'green' };
|
||||
/**
|
||||
* **实时在线**标签:`is_online` 来自接口(Redis `device:{imei}.online`,
|
||||
* 与设备列表 `is_online` 同源)。三态 —— `true` 在线 / `false` 离线 / `null`、缺失 = 无数据。
|
||||
*
|
||||
* ⚠️ 与 `detail.status`(启用/禁用配置态)**无关**:后者永不表示在线,不得再用来推断。
|
||||
* 无数据时显示"在线未知"而不是"离线"("没收到上报" 与 "明确离线" 必须区分)。
|
||||
*/
|
||||
const onlineTag: { label: string; color: string } =
|
||||
detail.is_online === true
|
||||
? { label: '在线', color: 'green' }
|
||||
: detail.is_online === false
|
||||
? { label: '离线', color: 'red' }
|
||||
: { label: '在线未知', color: 'gray' };
|
||||
/** 充电工作模式:后端未返回时为 null(界面上按默认「平台指令模式」展示并标注) */
|
||||
const workMode: '0' | '1' | null = detail.work_mode === '1' ? '1' : detail.work_mode === '0' ? '0' : null;
|
||||
/** 柜级柜门三态(协议 §4.2):`status1` bit4;无数据 → 「未知」(绝不显示成"关闭") */
|
||||
@ -500,6 +519,9 @@ export default function CabinetDetail() {
|
||||
<Button type="text" icon={<IconLeft />} onClick={() => navigate('/devices')} style={{ padding: '0 4px' }} />
|
||||
<IconDesktop style={{ fontSize: 20, color: 'var(--color-text-2)' }} />
|
||||
<Typography.Text style={{ fontSize: 16, fontWeight: 600 }}>柜子 {detail.abstract_id}</Typography.Text>
|
||||
{/* 实时在线(Redis `device:{imei}.online`,接口 `is_online`):三态,无数据 → 「在线未知」 */}
|
||||
<Tag color={onlineTag.color} size="small">{onlineTag.label}</Tag>
|
||||
{/* 配置态(启用/禁用):与上面"是否在线"是两个维度,同时展示 */}
|
||||
<Tag color={cabStatus.color} size="small">{cabStatus.label}</Tag>
|
||||
<div style={{ flex: 1 }} />
|
||||
<span style={{ fontSize: 12, color: 'var(--color-text-2)', background: 'var(--color-fill-1)', borderRadius: 4, padding: '2px 8px' }}>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user