12451 b553c4e519 feat: API 统一 + 设备详情补全 + H5 导航优化
- API 路由统一: 移除 /api/h5/,统一 /api/pms/ 入口
- 二进制改名: api-server -> pms-api(与服务名一致)
- nginx: 合并 /api/ + /pms/api/ 为 /api/pms/
- 新增 docs/API.md 接口文档
- 后端: CabinetDetail 增加 rssi/pow_fail/address/soc 等字段
- Redis 管道批量读取仓体实时数据(SOC/电压/电流/功率)
- DB: 所有 TIMESTAMP 列改为 DATETIME(修复类型不匹配)
- 用户管理: 新增创建时间/修改时间列,状态改用 Switch
- 角色管理/用户管理: 操作按钮改为图标
- PC 设备详情: 基本信息补全 RSSI/ICCID/电源/地址
- PC 卡片: 通道统计改用 Lucide 图标,颜色统一
- PC 二维码: canvas 渲染,含编号文字,支持右键下载
- H5: 按 uni-app 规范修正导航(navigateTo 替代 reLaunch)
- H5: 详情页加返回导航条
- H5: 扫码支持摄像头(jsQR)+ ?id= 参数
- H5: 底部 tabBar 在所有页面常显
- H5: 首页统计改为 6 项(设备/通道/空闲/充电/充满/故障)
- H5: 下拉刷新 / 上拉加载
- 产品首页: 添加业务联系
- 文档: 清理过时文件,补充缺失规范章节
2026-07-04 04:03:02 +08:00

6.5 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Use InjectionKey for Type-Safe Provide/Inject MEDIUM Without InjectionKey, injected values are typed as unknown requiring manual type assertions best-practice
vue3
typescript
provide-inject
injection-key
composition-api

Use InjectionKey for Type-Safe Provide/Inject

Impact: MEDIUM - When using provide/inject with TypeScript, use InjectionKey<T> with Symbol to achieve type-safe dependency injection. Without it, injected values have unknown type or require manual type assertions.

Task Checklist

  • Define injection keys using Symbol() as InjectionKey<T>
  • Store injection keys in a shared file for provider/consumer access
  • Understand that injected values are always T | undefined
  • Provide default values to remove undefined from the type

The Problem

<!-- Provider.vue -->
<script setup lang="ts">
import { provide } from 'vue'

interface User {
  id: string
  name: string
}

const user: User = { id: '1', name: 'John' }

// String key - no type information
provide('user', user)
</script>
<!-- Consumer.vue -->
<script setup lang="ts">
import { inject } from 'vue'

// Type is unknown!
const user = inject('user')  // Type: unknown

// Must manually assert type - error prone
const user = inject('user') as User  // Works but risky
</script>

The Solution: InjectionKey

// keys.ts - Shared injection keys file
import type { InjectionKey, Ref } from 'vue'

export interface User {
  id: string
  name: string
}

export interface AuthContext {
  user: Ref<User | null>
  login: (credentials: Credentials) => Promise<void>
  logout: () => Promise<void>
}

// Type-safe injection keys
export const userKey: InjectionKey<User> = Symbol('user')
export const authKey: InjectionKey<AuthContext> = Symbol('auth')
<!-- Provider.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue'
import { userKey, authKey, type User, type AuthContext } from '@/keys'

const user: User = { id: '1', name: 'John' }

// Type-checked! Must provide correct type
provide(userKey, user)

// Error if type doesn't match
provide(userKey, { wrong: 'data' })  // TypeScript error!

// Complex context
const authContext: AuthContext = {
  user: ref(null),
  login: async (creds) => { /* ... */ },
  logout: async () => { /* ... */ }
}
provide(authKey, authContext)
</script>
<!-- Consumer.vue -->
<script setup lang="ts">
import { inject } from 'vue'
import { userKey, authKey } from '@/keys'

// Automatically typed as User | undefined
const user = inject(userKey)

// Type: AuthContext | undefined
const auth = inject(authKey)

// Access with proper typing
if (user) {
  console.log(user.name)  // TypeScript knows this is string
}
</script>

Handling the Undefined Type

Injected values are always potentially undefined because the provider might not exist:

<script setup lang="ts">
import { inject } from 'vue'
import { userKey, type User } from '@/keys'

// Option 1: Provide a default value (removes undefined)
const user = inject(userKey, { id: '0', name: 'Guest' })
// Type: User (not undefined)

// Option 2: Use factory function for default
const user = inject(userKey, () => ({ id: '0', name: 'Guest' }), true)
// Type: User (factory is called only if no provider)

// Option 3: Assert non-null when certain provider exists
const user = inject(userKey)!
// Type: User (use sparingly!)

// Option 4: Guard with runtime check
const user = inject(userKey)
if (!user) {
  throw new Error('User provider not found. Wrap component in UserProvider.')
}
// Type: User after check
</script>

Creating a useInject Composable

For cleaner consumer code, create typed composables:

// composables/useAuth.ts
import { inject } from 'vue'
import { authKey, type AuthContext } from '@/keys'

export function useAuth(): AuthContext {
  const auth = inject(authKey)

  if (!auth) {
    throw new Error(
      'useAuth() requires an AuthProvider ancestor. ' +
      'Make sure to wrap your component tree with <AuthProvider>.'
    )
  }

  return auth
}
<!-- Consumer.vue -->
<script setup lang="ts">
import { useAuth } from '@/composables/useAuth'

// Clean API, guaranteed non-null, proper error message
const { user, login, logout } = useAuth()
</script>

Organizing Injection Keys

For larger applications, organize keys by domain:

src/
├── injection-keys/
│   ├── index.ts        # Re-exports all keys
│   ├── auth.ts         # Auth-related keys
│   ├── theme.ts        # Theme-related keys
│   └── i18n.ts         # i18n-related keys
// injection-keys/auth.ts
import type { InjectionKey, Ref } from 'vue'

export interface AuthState {
  isAuthenticated: Ref<boolean>
  user: Ref<User | null>
}

export interface AuthActions {
  login: (email: string, password: string) => Promise<void>
  logout: () => Promise<void>
  refresh: () => Promise<void>
}

export interface AuthContext extends AuthState, AuthActions {}

export const authStateKey: InjectionKey<AuthState> = Symbol('authState')
export const authActionsKey: InjectionKey<AuthActions> = Symbol('authActions')
export const authContextKey: InjectionKey<AuthContext> = Symbol('authContext')
// injection-keys/index.ts
export * from './auth'
export * from './theme'
export * from './i18n'

Generic Injection Keys

For reusable patterns with generics:

// keys/list.ts
import type { InjectionKey, Ref } from 'vue'

export interface ListContext<T> {
  items: Ref<T[]>
  selectedItem: Ref<T | null>
  selectItem: (item: T) => void
}

// Factory function for creating typed keys
export function createListKey<T>(name: string): InjectionKey<ListContext<T>> {
  return Symbol(name) as InjectionKey<ListContext<T>>
}

// Usage
interface Product { id: string; name: string }
export const productListKey = createListKey<Product>('productList')

Best Practices Summary

  1. Always use InjectionKey for TypeScript projects
  2. Use Symbols to prevent key collisions
  3. Create composables like useAuth() for clean consumer API
  4. Throw descriptive errors when required providers are missing
  5. Organize keys in a shared location accessible to providers and consumers
  6. Provide default values when the provider is optional

Reference