Files
SecMPS/doc/代码审核/代码审核20260604.md

16 KiB
Raw Blame History

warehouse 客户端代码深度审核报告(含完整修复方案)

日期: 2026-06-04 | 项目: warehouse/ | 扫描: 38 源文件, ~12,000 行 | 问题: 70 项


1. api/http.js — 9 项

H1 [🔴] lang_storage_key 未定义

位置: 第 134 行

function setHeaderLang(_header) {
  let langType = localStorage.getItem(lang_storage_key)  // ← 未定义!
}

后果: 运行时报错 lang_storage_key is not defined,语言功能完全失效。 修复 — 在函数上方加常量:

const lang_storage_key = 'lang'

H2 [🔴] replaceToken 未定义

位置: 第 107 行

function checkResponse(res) { if (res.headers.vol_exp == '1') { replaceToken() } }

后果: Token 过期后调用未定义函数,静默失败。 修复 — 在 checkResponse 前追加:

function replaceToken() {
  store.dispatch('clearUserInfo')
  window.location.href = '/login'
}

跨文件影响: 需确认 store/index.jsclearUserInfo mutation已存在

H3 [🔴] toLogin 未定义

位置: 第 77 行

if (error.response.status == '401') { toLogin() }

修复 — 在文件顶部 import router 后追加:

import router from '@/router'
function toLogin() { router.push('/login') }

跨文件影响: 需确认 router/index.ts 导出 router 实例。

H4 [🟠] 降级地址硬编码

位置: 第 25-33 行

axios.defaults.baseURL = 'http://192.168.3.108:9100/'
dataViewUrl = 'http://192.168.3.108:9200/'

修复:

axios.defaults.baseURL = window.location.origin
dataViewUrl = (window as any).apiConfig?.dataViewUrl || window.location.origin

H5 [🟠] get() 参数 param 未使用

位置: 第 176 行

function get(url, param, loading, config) { axios.get(url, config) }

修复:

function get(url, param, loading, config) {
  const cfg = { ...config }
  if (param) cfg.params = param
  // ... 其余不变
  axios.get(url, cfg).then(...)
}

H6 [🟡] closeLoading 冗余

位置: 第 92-101 行

if (loadingInstance) loadingInstance.close()
if (loadingStatus) { loadingStatus = false; if (loadingInstance) loadingInstance.close() }

修复:

loadingStatus = false
loadingInstance?.close()

H7 [🟡] alert() 弹窗

位置: 第 199 行

alert('http.js未配置大屏url地址')

修复:

import { ElMessage } from 'element-plus'
ElMessage.error('未配置大屏URL地址')

H8 [🟡] 无类型安全

修复 — 在文件头部加 JSDoc不改变运行时:

/**
 * @template T
 * @param {string} url
 * @param {object} [params]
 * @param {boolean|string} [loading]
 * @param {object} [config]
 * @returns {Promise<T>}
 */
function post(url, params, loading, config) { ... }

H9 [] 文件臃肿 (409行)

修复 — 拆为 3 文件:

  • api/http-client.ts — Axios 实例 + baseURL + 拦截器 (80行)
  • api/http-auth.ts — getToken/replaceToken/toLogin (40行)
  • api/http-loading.ts — showLoading/closeLoading (20行)

api/http.js 改为:

import { createHttpClient } from './http-client'
export default createHttpClient()

2. api/gateway.ts — 3 项

GW1 [🟠] 网关地址硬编码

位置: 第 5 行 const GW_BASE = 'http://192.168.3.108:5100'

修复:

const GW_BASE = (window as any).apiConfig?.gatewayUrl || 'http://localhost:5100'

GW2 [🟡] fetch() 无超时

修复 — 完整重写 gwGetgwPost 同理):

export async function gwGet(url: string, timeoutMs = 10000): Promise<any> {
  const ctrl = new AbortController()
  const timer = setTimeout(() => ctrl.abort(), timeoutMs)
  try {
    const resp = await fetch(`${GW_BASE}${url}`, { signal: ctrl.signal })
    if (!resp.ok) throw new Error(`网关请求失败: ${resp.status}`)
    return resp.json()
  } finally { clearTimeout(timer) }
}

GW3 [🟡] 模型映射混入API层

修复 — 新建 warehouse/src/services/cameraService.ts:

import { gwGet, type Camera, type StandardDevice } from '@/api/gateway'

export function toCamera(d: StandardDevice): Camera { ... }
export async function fetchCameras(adapter: string): Promise<Camera[]> { ... }

跨文件影响: Live.vue, VideoWall.vue, History.vue 的 import 改为 from '@/services/cameraService'


3. api/buttons.js — 1 项

B1 [🟡] Element UI 旧版图标语法

icon: 'el-icon-search'

Element Plus ≥2.0 已废弃字符串图标。

修复 — 改为组件引用:

import { Search, Plus, Edit, DocumentCopy, Delete, Check, Finished, Top, Bottom, Printer } from '@element-plus/icons-vue'
import { shallowRef } from 'vue'

const buttons = [
  { name:'查询', icon: shallowRef(Search), ... },
  { name:'新建', icon: shallowRef(Plus), ... },
  // ... 其余同理
]

跨文件影响: 使用 buttons 的组件需确认其渲染逻辑支持组件引用(通常通过 <component :is="btn.icon" />)。


4. api/permission.js — 2 项

PE1 [🟠] 权限缺失时静默放行

位置: 第 24 行

if (!permission) { permission = { permission: ['Search'] } }

修复:

if (!permission) { return [] }

空数组使所有按钮不可见(安全默认)。

PE2 [🟡] to401 空实现

修复:

import router from '@/router'
function to401() { router.push('/401') }

5. router/index.ts — 5 项

R1 [🟠] 40+ 条路由指向同一组件

所有菜单子项渲染 Index.vue,用户看到重复空白页。

修复 — 3 步方案:

  1. 为已实现的页面保留独立路由VideoWall/AlarmRecord/AccessRecord 等已存在)
  2. 其余指向一个占位组件:
{ path: "/index/goods/list", component: () => import("@/view/Placeholder.vue") }
  1. Placeholder.vue:
<template><el-empty description="功能开发中" /></template>

R2 [🟡] /new-dv 不要求认证

{ path:"/new-dv", meta:{ requiresAuth: false } }

修复: 改为 requiresAuth: true

R3 [🟡] 缺少 beforeEach 守卫

修复 — 在 router/index.ts 末尾追加:

router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth !== false) {
    const token = localStorage.getItem('token')
    if (token) next()
    else next('/login')
  } else { next() }
})

R4 [] @ts-ignore 绕过 store 类型

修复: 创建 warehouse/src/types/store.d.ts:

declare module '@/store' {
  const store: import('vuex').Store<any>
  export default store
}

R5 [] 仓库/货物/出入库路由 20+ 条未使用

修复: 删除。若后续需要可从 git 恢复。


6. main.ts — 1 项

M1 [🟡] 暗色模式写死

app.use(ElementPlus, { dark: true })

修复:

const dark = localStorage.getItem('dark-mode') !== 'false'
app.use(ElementPlus, { dark })

7. view/index.js — 6 项

SI1 [🔴] displayedMessageIds Set 无限增长

位置: 第 8 行 let displayedMessageIds = new Set() 后果: 运行数天后 Set 包含成千上万条 ID → 内存泄漏。

修复 — 改为 LRU 缓存(保留最近 500 条):

class LruSet {
  #set = new Set()
  #max = 500
  add(v) { if (this.#set.has(v)) return; this.#set.add(v); if (this.#set.size > this.#max) { this.#set.delete(this.#set.values().next().value) } }
  has(v) { return this.#set.has(v) }
}
const displayedMessageIds = new LruSet()

SI2 [🟠] 消息队列 3s 延迟堆积

修复: 删除 messageQueue/processMessageQueue,直接调 receive:

connection.on("ReceiveHomePageMessage", function (data) {
  if (displayedMessageIds.has(data.id)) return
  displayedMessageIds.add(data.id)
  receive(data)
})

SI3 [🟠] connection 启动无重试

connection.start().catch(...) 失败后永远不重试。

修复:

async function startWithRetry(retries = 5) {
  for (let i = 0; i < retries; i++) {
    try { await connection.start(); return }
    catch (e) { console.warn(`SignalR retry ${i+1}/${retries}: ${e.message}`); await new Promise(r => setTimeout(r, 2000)) }
  }
  console.error('SignalR connection failed after retries')
}
startWithRetry()

SI4 [🟡] console.log 残留 6 处

修复: 全部替换为 if (import.meta.env.DEV) console.log(...)

SI5 [🟡] receive 被双重调用

processMessageQueueReceiveHomePageMessage 都调了 receive

修复: SI2 删除了 processMessageQueue 后此问题自动消除。

SI6 [🟡] 用户信息获取失败静默

修复:

http.post("api/user/GetCurrentUserInfo").then(...).catch(error => {
  console.error('获取用户信息失败SignalR未启动:', error)
})

8-28: 视图层文件21 个 Vue 文件)

通用修复模式(适用于所有 Mock 页面)

以下 17 个页面全 Mock —— 统一修复方案:

AccessRecord.vue, AlarmRecord.vue, EmergencyAlarmRecord.vue,
KeyInfo.vue, KeyApply.vue, EnvVarManagement.vue,
PatrolLog.vue, ScheduleManagement.vue, PathManagement.vue,
DroneManagement.vue, dataview.vue, CarApply.vue, CarManager.vue,
DeviceStatus.vue(V), VisitorsManagement.vue, VisitCarManagement.vue

统一修复: 每个页面增加网关 API 调用骨架Mock 数据降级为 fallback:

// 以 AlarmRecord.vue 为例
import { gwGet } from '@/api/gateway'

const fetchData = async () => {
  try {
    const data = await gwGet('/api/gateway/alarms/Owl:main?page=1&size=100')
    alarmData.value = data.items.map((a: StandardAlarm) => ({
      id: a.alarmId, alarmTime: a.occurTime, deviceName: a.title,
      location: a.deviceId, status: a.status, imageUrl: '/images/placeholder.png'
    }))
  } catch {
    alarmData.value = getMockAlarmData() // fallback
  }
}

单个页面专项修复

DataView.vue (1840行):

  • DV1: 告警等级 → 改用 level 字段或网关 StandardAlarm.level
  • DV2: setTimeoutclearTimeout:
    const timers = new Set<number>()
    onBeforeUnmount(() => timers.forEach(t => clearTimeout(t)))
    // 使用时: timers.add(setTimeout(...))
    
  • DV6: originalData: JSON.parse(JSON.stringify(data)) 避免循环引用

DeviceInfo.vue (1300行):

  • DI1: 对接网关 B4 获取真实在线率
  • DI2: randomVideoImagegwGet('/api/gateway/streams/.../live')
  • DI3: handleTurnOngwPost('/api/gateway/realtime/.../control', { deviceId, pointIndex, value })

Live.vue / VideoWall.vue / History.vue:

  • LV2/VH2: setInterval 加清理:
    const timer = setInterval(updateTime, 1000)
    onBeforeUnmount(() => clearInterval(timer))
    

Main.vue:

  • MA1: icon 改为 @element-plus/icons-vue 组件引用
  • MA2: Menu 配置提取:
    const menuItems = [
      { index:'1', icon: VideoCamera, label:'视频监控', children:[
        { index:'/index/video/videowall', label:'视频墙' }
      ]}
    ]
    
  • MA5: 统一 import { useMapStore } from '@/stores/mapStore'

Map.vue:

  • MP1: const m = /#\/(\d+)/.exec(location.hash); const mapId = m?.[1] || 'default'

Index.vue:

  • IN1: const mapId = import.meta.env.VITE_MAP_ID || 'default'

29-31: 组件层3 文件)

Filter.vue [🟡]

console.log 8 处: 同 SI4改为条件输出。

硬件设备图标映射 提取为常量:

const DEVICE_ICONS: Record<string, string> = {
  '摄像头':'/images/dataview/deviceinfo/camera.png',
  '门禁':'/images/dataview/deviceinfo/access.png',
  // ...
}

Fence.vue [🟡]

硬编码仓库名: ['1号库','2号库','12号库'] 修复: 从 store 或配置注入:

const warehouseNames = inject('warehouseNames', ['1号库'])
const inFencePoints = store.polygonDataAll.filter(p => warehouseNames.includes(p.name))

32-34: 状态管理层3 文件)

SM1 [🔴] 两份 useMapStore 同名

文件: stores/mapStore.jsstore/useMapStore.jsdefineStore('map', ...)

修复 — 选择一份保留(推荐 stores/mapStore.js 功能更全),另一份删除:

git rm warehouse/src/store/useMapStore.js

跨文件影响 — 修改以下文件的 import:

  • Map.vue line 9: '../store/useMapStore''../stores/mapStore'
  • Index.vue line 10: '../stores/mapStore' (已对)
  • Fence.vue line 4: '../store/useMapStore''../stores/mapStore'
  • Filter.vue line 3: '../store/useMapStore''../stores/mapStore'
  • DataView.vue line 10: '../stores/mapStore' (已对)

ST1 [🟠] getServiceList getter 忽略参数

位置: store/index.js

getServiceList: (state) => (path) => { return state.serviceList || [] }

修复: 如果不需要按 path 过滤则简化为:

getServiceList: (state) => state.serviceList || []

ST3 [🟡] test mutation 返回 113344

位置: store/index.js line 49

test(state) { return 113344 }  // 调试代码

修复: 删除此 mutation。

ST4 [] setPermission 数组 push 会叠加

if (data instanceof Array) { state.permission.push(...data) }

每次调用追加而非替换。修复: 始终替换 state.permission = data


35: 项目结构 — 4 项

PS1/PS2 [🟡] 过期副本文件

git rm warehouse/src/view/DataView\ copy.vue
git rm warehouse/src/view/Map.vue.bak

PS3 [🟡] 文档放在源码目录

mkdir -p warehouse/doc
mv warehouse/src/view/intercom/TODO_*.md warehouse/doc/

PS4 [🟠] Vuex + Pinia 共存

修复 — 迁移 Vuex → Pinia:

  1. 新建 stores/authStore.js(替代 Vuex 的 userInfo/token/permission
  2. 迁移 store/index.js 中的 setUserInfo/getToken/getPermission 到 Pinia
  3. npm uninstall vuex
  4. 修改 http.js/Login.vue/permission.js@/stores/authStore 导入

36: 全局问题 — 7 项

G1 [🟠] Mock 覆盖率 86%

22 页面中仅 3 个对接网关。分 4 阶段对接:

阶段 页面 网关接口 预计
1 告警页 (AlarmRecord/EmergencyAlarm) B8 (GET /alarms) 2h
2 环境变量 (EnvVarManagement) B4 (GET /realtime) 2h
3 门禁/钥匙 (AccessRecord/KeyInfo) B2 (GET /devices) + B11 (GET /logs) 3h
4 巡更/无人机/车辆/访客 B2 设备列表 1h

G2 [🟡] 硬编码IP散布6+文件

修复 — 创建 .env.development:

VITE_GATEWAY_URL=http://localhost:5100
VITE_VOLPRO_URL=http://localhost:9100

各文件改为:

const GW_BASE = import.meta.env.VITE_GATEWAY_URL || 'http://localhost:5100'

G3 [🟡] JS/TS 混用

修复: 7 个 .js.ts(逐文件迁移,不改运行时逻辑):

api/http.js → api/http.ts
api/buttons.js → api/buttons.ts
api/permission.js → api/permission.ts
view/index.js → view/index.ts
stores/mapStore.js → stores/mapStore.ts
router/viewGird.js → router/viewGird.ts

G4 [🟡] window.* 全局变量

修复: window.$map → Store 管理(已存在 mapStore.setMap

// Map.vue: 删除 window.$map = map保留 store.setMap(map)
// 其他文件: 改 window.$map → const { map } = useMapStore()

G5 [🟡] console.log 残留 30+ 处

修复 — 一行全局替换:

# 将所有 console.log 改为条件输出
find warehouse/src -name "*.vue" -o -name "*.ts" -o -name "*.js" | xargs sed -i 's/console\.log(/if(import\.meta\.env\.DEV)console.log(/g'

G6 [] 无全局错误边界

修复main.ts:

app.config.errorHandler = (err) => {
  console.error('Global error:', err)
  ElMessage.error('系统异常,请刷新页面')
}

G7 [] counter.ts 模板残留

修复: 删除 warehouse/src/stores/counter.ts


执行优先级

批次 文件 预计 影响
🔴P0 H1+H2+H3+SI1+SM1 5 2h 修复运行时错误
🟠P1 SI3+PE1+R1+R3+G1(告警) 6 4h 功能可用性
🟡P2 GW1+GW2+H5+H6+DV1+LV2 6 3h 代码质量
P3 PS+console+G6+CT1 5 1h 整洁性

总计: 70 项 / 预估 10h。P0 批 5 项必须在联调前完成。