Skip to content

设备信息工具

概述

RuoYi-Plus-UniApp 移动端提供了完整的设备信息获取和平台检测解决方案,基于 UniApp 的系统 API 进行深度封装,实现了跨平台的设备信息获取、平台判断、环境检测等功能。

核心特性

  • 系统信息获取 - 基于 uni.getSystemInfoSync() 获取完整的设备和系统信息
  • 平台检测 - 提供 isAppisMpisMpWeixin 等平台判断常量
  • 环境检测 - 检测微信环境、支付宝环境、开发者工具等运行环境
  • 屏幕适配 - 提供 rpxToPxpxToRpx 等单位转换工具
  • 安全区域 - 获取状态栏高度、安全区域等适配信息
  • 新旧 API 兼容 - 自动兼容微信小程序新旧 API
  • TypeScript 支持 - 完整的类型定义,提供开发时类型检查

技术栈

依赖版本说明
UniApp3.0.0+跨平台框架
Vue 33.4.21组合式 API
TypeScript5.7.2类型支持

系统信息获取

uni.getSystemInfoSync()

这是 UniApp 提供的核心 API,用于同步获取系统信息:

typescript
// 获取系统信息
const systemInfo = uni.getSystemInfoSync()

console.log('设备品牌:', systemInfo.brand)
console.log('设备型号:', systemInfo.model)
console.log('操作系统:', systemInfo.system)
console.log('平台:', systemInfo.platform)
console.log('屏幕宽度:', systemInfo.screenWidth)
console.log('屏幕高度:', systemInfo.screenHeight)
console.log('窗口宽度:', systemInfo.windowWidth)
console.log('窗口高度:', systemInfo.windowHeight)
console.log('状态栏高度:', systemInfo.statusBarHeight)
console.log('设备像素比:', systemInfo.pixelRatio)
console.log('系统语言:', systemInfo.language)

返回值说明

属性类型说明
brandstring设备品牌,如 "Apple"、"Huawei"
modelstring设备型号,如 "iPhone 14 Pro"
systemstring操作系统及版本,如 "iOS 17.0"
platformstring客户端平台,如 "ios"、"android"、"devtools"
screenWidthnumber屏幕宽度(px)
screenHeightnumber屏幕高度(px)
windowWidthnumber可使用窗口宽度(px)
windowHeightnumber可使用窗口高度(px)
windowTopnumber可使用窗口顶部位置(px)
windowBottomnumber可使用窗口底部位置(px)
statusBarHeightnumber状态栏高度(px)
pixelRationumber设备像素比
languagestring系统语言,如 "zh_CN"
versionstring引擎版本号
SDKVersionstring客户端基础库版本
safeAreaobject安全区域信息
safeAreaInsetsobject安全区域边距

安全区域信息

typescript
interface SafeArea {
  left: number      // 安全区域左上角横坐标
  right: number     // 安全区域右下角横坐标
  top: number       // 安全区域左上角纵坐标
  bottom: number    // 安全区域右下角纵坐标
  width: number     // 安全区域宽度
  height: number    // 安全区域高度
}

interface SafeAreaInsets {
  left: number      // 左边安全边距
  right: number     // 右边安全边距
  top: number       // 上边安全边距
  bottom: number    // 下边安全边距
}

// 使用示例
const { safeArea, safeAreaInsets } = uni.getSystemInfoSync()

// 适配刘海屏
const contentStyle = {
  paddingTop: `${safeAreaInsets.top}px`,
  paddingBottom: `${safeAreaInsets.bottom}px`,
}

平台检测工具

平台常量

项目在 utils/platform.ts 中提供了平台判断常量:

typescript
// utils/platform.ts

/**
 * 当前运行平台
 */
export const platform = uni.getSystemInfoSync().platform

/**
 * 是否为 App 环境
 */
// #ifdef APP-PLUS
export const isApp = true
// #endif
// #ifndef APP-PLUS
export const isApp = false
// #endif

/**
 * 是否为小程序环境
 */
// #ifdef MP
export const isMp = true
// #endif
// #ifndef MP
export const isMp = false
// #endif

/**
 * 是否为微信小程序
 */
// #ifdef MP-WEIXIN
export const isMpWeixin = true
// #endif
// #ifndef MP-WEIXIN
export const isMpWeixin = false
// #endif

/**
 * 是否为支付宝小程序
 */
// #ifdef MP-ALIPAY
export const isMpAlipay = true
// #endif
// #ifndef MP-ALIPAY
export const isMpAlipay = false
// #endif

/**
 * 是否为头条小程序
 */
// #ifdef MP-TOUTIAO
export const isMpToutiao = true
// #endif
// #ifndef MP-TOUTIAO
export const isMpToutiao = false
// #endif

/**
 * 是否为 H5 环境
 */
// #ifdef H5
export const isH5 = true
// #endif
// #ifndef H5
export const isH5 = false
// #endif

使用示例

vue
<template>
  <view class="platform-demo">
    <view class="info-item">
      <text class="label">当前平台:</text>
      <text class="value">{{ platformName }}</text>
    </view>
    <view class="info-item">
      <text class="label">运行环境:</text>
      <text class="value">{{ envName }}</text>
    </view>

    <!-- 平台特定内容 -->
    <view v-if="isApp" class="app-only">
      <text>App 专属功能</text>
    </view>

    <view v-if="isMpWeixin" class="weixin-only">
      <text>微信小程序专属功能</text>
    </view>

    <view v-if="isH5" class="h5-only">
      <text>H5 专属功能</text>
    </view>
  </view>
</template>

<script lang="ts" setup>
import { computed } from 'vue'
import { platform, isApp, isMp, isMpWeixin, isH5 } from '@/utils/platform'

const platformName = computed(() => {
  if (isApp) return 'App'
  if (isMpWeixin) return '微信小程序'
  if (isMp) return '小程序'
  if (isH5) return 'H5'
  return platform
})

const envName = computed(() => {
  const info = uni.getSystemInfoSync()
  return `${info.system} - ${info.model}`
})
</script>

环境检测函数

安全获取 User Agent

typescript
/**
 * 安全获取 User Agent
 * 兼容所有平台,避免在小程序等环境下报错
 */
export function safeGetUserAgent(): string {
  try {
    // #ifdef H5
    if (typeof navigator !== 'undefined' && navigator.userAgent) {
      return navigator.userAgent.toLowerCase()
    }
    // #endif
    return ''
  } catch (error) {
    console.warn('[platform] 获取 UserAgent 失败:', error)
    return ''
  }
}

// 使用示例
const userAgent = safeGetUserAgent()
const isIOS = userAgent.includes('iphone') || userAgent.includes('ipad')
const isAndroid = userAgent.includes('android')

安全获取 Location

typescript
/**
 * 安全获取 window.location
 * 兼容所有平台
 */
export function safeGetLocation(): Location | null {
  try {
    // #ifdef H5
    if (typeof window !== 'undefined' && window.location) {
      return window.location
    }
    // #endif
    return null
  } catch (error) {
    console.warn('[platform] 获取 location 失败:', error)
    return null
  }
}

// 使用示例
const location = safeGetLocation()
if (location) {
  console.log('当前URL:', location.href)
  console.log('域名:', location.hostname)
  console.log('路径:', location.pathname)
  console.log('参数:', location.search)
}

检测微信环境

typescript
/**
 * 检测是否在微信环境中
 * 包括微信内置浏览器和微信小程序
 */
export function isWechatEnvironment(): boolean {
  // 微信小程序环境
  // #ifdef MP-WEIXIN
  return true
  // #endif

  // H5 环境检测微信浏览器
  // #ifdef H5
  const ua = safeGetUserAgent()
  return ua.includes('micromessenger')
  // #endif

  return false
}

// 使用示例
if (isWechatEnvironment()) {
  // 微信环境特殊处理
  console.log('当前在微信环境中')
}

检测支付宝环境

typescript
/**
 * 检测是否在支付宝环境中
 * 包括支付宝内置浏览器和支付宝小程序
 */
export function isAlipayEnvironment(): boolean {
  // 支付宝小程序环境
  // #ifdef MP-ALIPAY
  return true
  // #endif

  // H5 环境检测支付宝浏览器
  // #ifdef H5
  const ua = safeGetUserAgent()
  return ua.includes('alipay') || ua.includes('alipayclient')
  // #endif

  return false
}

// 使用示例
if (isAlipayEnvironment()) {
  // 支付宝环境特殊处理
  console.log('当前在支付宝环境中')
}

检测开发者工具

typescript
/**
 * 检测是否在开发者工具中运行
 */
export function isInDevTools(): boolean {
  try {
    const systemInfo = uni.getSystemInfoSync()

    // 微信开发者工具
    if (systemInfo.platform === 'devtools') {
      return true
    }

    // 支付宝开发者工具
    // #ifdef MP-ALIPAY
    if (my.canIUse('getSystemInfoSync.return.platform')) {
      return systemInfo.platform === 'devtools'
    }
    // #endif

    // H5 开发环境
    // #ifdef H5
    if (process.env.NODE_ENV === 'development') {
      return true
    }
    // #endif

    return false
  } catch (error) {
    console.warn('[platform] 检测开发工具失败:', error)
    return false
  }
}

// 使用示例
if (isInDevTools()) {
  console.log('当前在开发者工具中运行')
  // 开发环境下的特殊处理
}

检测微信 JS Bridge

typescript
/**
 * 检测是否存在微信 JS Bridge
 * 用于判断微信 JSSDK 是否可用
 */
export function hasWeixinJSBridge(): boolean {
  // #ifdef H5
  try {
    return typeof WeixinJSBridge !== 'undefined'
  } catch (error) {
    return false
  }
  // #endif

  return false
}

// 使用示例
if (hasWeixinJSBridge()) {
  // 可以使用微信 JSSDK
  WeixinJSBridge.invoke('getBrandWCPayRequest', payParams, (res) => {
    if (res.err_msg === 'get_brand_wcpay_request:ok') {
      console.log('支付成功')
    }
  })
}

安全获取 URL 参数

typescript
/**
 * 安全获取当前页面 URL 参数
 */
export function safeGetUrlParams(): Record<string, string> {
  const params: Record<string, string> = {}

  try {
    // #ifdef H5
    const location = safeGetLocation()
    if (location && location.search) {
      const searchParams = new URLSearchParams(location.search)
      searchParams.forEach((value, key) => {
        params[key] = value
      })
    }
    // #endif

    // 小程序环境获取页面参数
    // #ifdef MP
    const pages = getCurrentPages()
    if (pages.length > 0) {
      const currentPage = pages[pages.length - 1]
      Object.assign(params, currentPage.options || {})
    }
    // #endif
  } catch (error) {
    console.warn('[platform] 获取URL参数失败:', error)
  }

  return params
}

// 使用示例
const urlParams = safeGetUrlParams()
console.log('页面参数:', urlParams)
// { id: '123', type: 'detail' }

检查支付授权目录

typescript
/**
 * 检查当前页面是否在微信支付授权目录内
 */
export function checkPaymentUrl(): boolean {
  // #ifdef H5
  try {
    const location = safeGetLocation()
    if (!location) return false

    // 获取配置的支付授权目录
    const authorizedDomains = [
      'https://your-domain.com/pay/',
      'https://your-domain.com/order/',
    ]

    const currentUrl = location.href
    return authorizedDomains.some(domain => currentUrl.startsWith(domain))
  } catch (error) {
    console.warn('[platform] 检查支付授权目录失败:', error)
    return false
  }
  // #endif

  // 小程序环境默认支持支付
  return true
}

// 使用示例
if (!checkPaymentUrl()) {
  console.warn('当前页面不在支付授权目录内')
}

单位转换工具

rpx 与 px 互转

typescript
/**
 * rpx 转 px
 * @param rpx - rpx 值
 * @returns px 值
 */
export function rpxToPx(rpx: number): number {
  const systemInfo = uni.getSystemInfoSync()
  const screenWidth = systemInfo.screenWidth || 375
  return (rpx / 750) * screenWidth
}

/**
 * px 转 rpx
 * @param px - px 值
 * @returns rpx 值
 */
export function pxToRpx(px: number): number {
  const systemInfo = uni.getSystemInfoSync()
  const screenWidth = systemInfo.screenWidth || 375
  return (px / screenWidth) * 750
}

// 使用示例
const pxValue = rpxToPx(100)  // 100rpx 转为 px
const rpxValue = pxToRpx(50)  // 50px 转为 rpx

console.log(`100rpx = ${pxValue}px`)
console.log(`50px = ${rpxValue}rpx`)

使用场景

vue
<template>
  <view class="canvas-demo">
    <canvas
      canvas-id="myCanvas"
      :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
    />
  </view>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { rpxToPx } from '@/utils/device'

// Canvas 需要使用 px 单位
const canvasWidth = ref(rpxToPx(600))   // 600rpx 转 px
const canvasHeight = ref(rpxToPx(400))  // 400rpx 转 px

onMounted(() => {
  const ctx = uni.createCanvasContext('myCanvas')

  // 绑制内容时也需要转换单位
  ctx.setFillStyle('#409EFF')
  ctx.fillRect(0, 0, rpxToPx(200), rpxToPx(100))

  ctx.draw()
})
</script>

高清屏适配

typescript
/**
 * 获取 Canvas 绑制所需的实际尺寸
 * 考虑设备像素比,用于高清屏适配
 */
export function getCanvasSize(rpxWidth: number, rpxHeight: number) {
  const systemInfo = uni.getSystemInfoSync()
  const pixelRatio = systemInfo.pixelRatio || 1

  const width = rpxToPx(rpxWidth)
  const height = rpxToPx(rpxHeight)

  return {
    // CSS 尺寸
    cssWidth: width,
    cssHeight: height,
    // Canvas 实际尺寸(乘以像素比)
    canvasWidth: width * pixelRatio,
    canvasHeight: height * pixelRatio,
    // 像素比
    pixelRatio,
  }
}

// 使用示例
const { cssWidth, cssHeight, canvasWidth, canvasHeight, pixelRatio } = getCanvasSize(600, 400)

// Canvas 样式使用 CSS 尺寸
// <canvas :style="{ width: cssWidth + 'px', height: cssHeight + 'px' }">

// Canvas 绑制时使用实际尺寸
const ctx = uni.createCanvasContext('canvas')
ctx.scale(pixelRatio, pixelRatio)  // 缩放以适配高清屏

组件中的设备信息应用

导航栏状态栏适配

vue
<template>
  <view class="navbar" :style="navbarStyle">
    <view class="navbar-content">
      <slot />
    </view>
  </view>
</template>

<script lang="ts" setup>
import { computed } from 'vue'

const navbarStyle = computed(() => {
  const { statusBarHeight } = uni.getSystemInfoSync()
  return {
    paddingTop: `${statusBarHeight}px`,
  }
})
</script>

下拉菜单窗口高度

vue
<template>
  <view class="dropdown-menu">
    <view
      v-if="showOverlay"
      class="dropdown-overlay"
      :style="{ height: windowHeight + 'px' }"
      @click="closeMenu"
    />
    <view class="dropdown-content">
      <slot />
    </view>
  </view>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue'

const showOverlay = ref(false)
const windowHeight = ref(0)

onMounted(() => {
  // 获取窗口高度用于遮罩层
  windowHeight.value = uni.getSystemInfoSync().windowHeight
})

const closeMenu = () => {
  showOverlay.value = false
}
</script>

图片裁剪器定位

vue
<template>
  <view class="img-cropper">
    <view
      class="cropper-image"
      :style="{
        left: imgLeft + 'rpx',
        top: imgTop + 'rpx',
      }"
    >
      <image :src="imgSrc" mode="aspectFit" />
    </view>
  </view>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { pxToRpx } from '@/utils/device'

const props = defineProps<{
  imgSrc: string
}>()

const imgLeft = ref(0)
const imgTop = ref(0)

const TOP_PERCENT = 0.45  // 图片居中偏上

onMounted(() => {
  const { windowWidth, windowHeight } = uni.getSystemInfoSync()

  // 将 px 转为 rpx 用于定位
  imgLeft.value = pxToRpx(windowWidth / 2)
  imgTop.value = pxToRpx((windowHeight / 2) * TOP_PERCENT)
})
</script>

水印组件高清适配

vue
<template>
  <view class="watermark-container">
    <canvas
      canvas-id="watermarkCanvas"
      :style="{
        width: canvasWidth / pixelRatio + 'px',
        height: canvasHeight / pixelRatio + 'px',
      }"
    />
  </view>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { rpxToPx } from '@/utils/device'

const props = withDefaults(defineProps<{
  width?: number
  height?: number
  gutterX?: number
  gutterY?: number
  content?: string
}>(), {
  width: 200,
  height: 200,
  gutterX: 0,
  gutterY: 0,
  content: '水印',
})

const pixelRatio = ref(1)
const canvasWidth = ref(0)
const canvasHeight = ref(0)

onMounted(() => {
  // 获取设备像素比
  pixelRatio.value = uni.getSystemInfoSync().pixelRatio

  // 计算 Canvas 实际尺寸(考虑像素比)
  canvasWidth.value = rpxToPx(props.width + props.gutterX) * pixelRatio.value
  canvasHeight.value = rpxToPx(props.height + props.gutterY) * pixelRatio.value

  drawWatermark()
})

const drawWatermark = () => {
  const ctx = uni.createCanvasContext('watermarkCanvas')

  // 缩放以适配高清屏
  ctx.scale(pixelRatio.value, pixelRatio.value)

  // 绑制水印文字
  ctx.setFontSize(14)
  ctx.setFillStyle('rgba(0, 0, 0, 0.1)')
  ctx.rotate(-20 * Math.PI / 180)
  ctx.fillText(props.content, 10, 50)

  ctx.draw()
}
</script>

浮动按钮边界计算

vue
<template>
  <view
    class="fab"
    :style="{
      right: fabRight + 'px',
      bottom: fabBottom + 'px',
    }"
    @click="handleClick"
  >
    <slot>
      <text class="fab-icon">+</text>
    </slot>
  </view>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { isH5 } from '@/utils/platform'

const props = withDefaults(defineProps<{
  right?: number
  bottom?: number
}>(), {
  right: 20,
  bottom: 100,
})

const emit = defineEmits<{
  click: []
}>()

const fabRight = ref(props.right)
const fabBottom = ref(props.bottom)

const screen = {
  width: 0,
  height: 0,
}

onMounted(() => {
  getBounding()
})

const getBounding = () => {
  const sysInfo = uni.getSystemInfoSync()

  const statusBarHeight = sysInfo.statusBarHeight || 0
  const navBarHeight = 44

  screen.width = sysInfo.windowWidth
  // H5 需要加上 windowTop
  screen.height = isH5
    ? sysInfo.windowTop + sysInfo.windowHeight
    : sysInfo.windowHeight

  // 确保不超出边界
  fabRight.value = Math.min(props.right, screen.width - 60)
  fabBottom.value = Math.min(props.bottom, screen.height - 60)
}

const handleClick = () => {
  emit('click')
}
</script>

新旧 API 兼容

微信小程序 API 兼容

微信小程序在基础库 2.20.1 版本后拆分了 getSystemInfo API:

typescript
/**
 * 兼容获取窗口信息
 * 优先使用新 API,降级使用旧 API
 */
export function getWindowInfo() {
  // #ifdef MP-WEIXIN
  if (uni.canIUse('getWindowInfo')) {
    return uni.getWindowInfo()
  }
  // #endif

  const systemInfo = uni.getSystemInfoSync()
  return {
    windowWidth: systemInfo.windowWidth,
    windowHeight: systemInfo.windowHeight,
    windowTop: systemInfo.windowTop || 0,
    windowBottom: systemInfo.windowBottom || 0,
    statusBarHeight: systemInfo.statusBarHeight,
    safeArea: systemInfo.safeArea,
  }
}

/**
 * 兼容获取设备信息
 */
export function getDeviceInfo() {
  // #ifdef MP-WEIXIN
  if (uni.canIUse('getDeviceInfo')) {
    return uni.getDeviceInfo()
  }
  // #endif

  const systemInfo = uni.getSystemInfoSync()
  return {
    brand: systemInfo.brand,
    model: systemInfo.model,
    system: systemInfo.system,
    platform: systemInfo.platform,
    devicePixelRatio: systemInfo.pixelRatio,
  }
}

/**
 * 兼容获取 App 基础信息
 */
export function getAppBaseInfo() {
  // #ifdef MP-WEIXIN
  if (uni.canIUse('getAppBaseInfo')) {
    return uni.getAppBaseInfo()
  }
  // #endif

  const systemInfo = uni.getSystemInfoSync()
  return {
    SDKVersion: systemInfo.SDKVersion,
    enableDebug: systemInfo.enableDebug,
    language: systemInfo.language,
    version: systemInfo.version,
    theme: systemInfo.theme,
  }
}

// 使用示例
const windowInfo = getWindowInfo()
const deviceInfo = getDeviceInfo()
const appInfo = getAppBaseInfo()

console.log('窗口宽度:', windowInfo.windowWidth)
console.log('设备系统:', deviceInfo.system)
console.log('SDK版本:', appInfo.SDKVersion)

完整的系统信息封装

typescript
// utils/device.ts

/**
 * 设备信息接口
 */
export interface DeviceInfo {
  // 设备信息
  brand: string
  model: string
  system: string
  platform: string

  // 屏幕信息
  screenWidth: number
  screenHeight: number
  windowWidth: number
  windowHeight: number
  windowTop: number
  windowBottom: number
  statusBarHeight: number
  pixelRatio: number

  // 安全区域
  safeArea: {
    left: number
    right: number
    top: number
    bottom: number
    width: number
    height: number
  }
  safeAreaInsets: {
    left: number
    right: number
    top: number
    bottom: number
  }

  // 系统信息
  language: string
  version: string
  SDKVersion: string
  theme: 'light' | 'dark'

  // 环境信息
  isApp: boolean
  isMp: boolean
  isMpWeixin: boolean
  isH5: boolean
  isDevTools: boolean
}

/**
 * 获取完整的设备信息
 */
export function getFullDeviceInfo(): DeviceInfo {
  const systemInfo = uni.getSystemInfoSync()

  // 获取安全区域边距
  const safeAreaInsets = {
    left: systemInfo.safeArea?.left || 0,
    right: systemInfo.screenWidth - (systemInfo.safeArea?.right || systemInfo.screenWidth),
    top: systemInfo.safeArea?.top || 0,
    bottom: systemInfo.screenHeight - (systemInfo.safeArea?.bottom || systemInfo.screenHeight),
  }

  return {
    // 设备信息
    brand: systemInfo.brand || '',
    model: systemInfo.model || '',
    system: systemInfo.system || '',
    platform: systemInfo.platform || '',

    // 屏幕信息
    screenWidth: systemInfo.screenWidth,
    screenHeight: systemInfo.screenHeight,
    windowWidth: systemInfo.windowWidth,
    windowHeight: systemInfo.windowHeight,
    windowTop: systemInfo.windowTop || 0,
    windowBottom: systemInfo.windowBottom || 0,
    statusBarHeight: systemInfo.statusBarHeight || 0,
    pixelRatio: systemInfo.pixelRatio || 1,

    // 安全区域
    safeArea: systemInfo.safeArea || {
      left: 0,
      right: systemInfo.screenWidth,
      top: 0,
      bottom: systemInfo.screenHeight,
      width: systemInfo.screenWidth,
      height: systemInfo.screenHeight,
    },
    safeAreaInsets,

    // 系统信息
    language: systemInfo.language || 'zh_CN',
    version: systemInfo.version || '',
    SDKVersion: systemInfo.SDKVersion || '',
    theme: (systemInfo.theme as 'light' | 'dark') || 'light',

    // 环境信息
    isApp,
    isMp,
    isMpWeixin,
    isH5,
    isDevTools: isInDevTools(),
  }
}

useDevice Composable

封装响应式设备信息

typescript
// composables/useDevice.ts
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import type { DeviceInfo } from '@/utils/device'
import { getFullDeviceInfo, rpxToPx, pxToRpx } from '@/utils/device'

export function useDevice() {
  const deviceInfo = reactive<DeviceInfo>(getFullDeviceInfo())
  const isReady = ref(false)

  // 更新设备信息
  const updateDeviceInfo = () => {
    Object.assign(deviceInfo, getFullDeviceInfo())
  }

  // 监听屏幕旋转(仅 App 和 H5)
  let resizeHandler: (() => void) | null = null

  onMounted(() => {
    isReady.value = true

    // #ifdef H5
    resizeHandler = () => {
      updateDeviceInfo()
    }
    window.addEventListener('resize', resizeHandler)
    // #endif
  })

  onUnmounted(() => {
    // #ifdef H5
    if (resizeHandler) {
      window.removeEventListener('resize', resizeHandler)
    }
    // #endif
  })

  return {
    // 设备信息(响应式)
    deviceInfo,
    isReady,

    // 方法
    updateDeviceInfo,
    rpxToPx,
    pxToRpx,

    // 便捷属性
    get screenWidth() {
      return deviceInfo.screenWidth
    },
    get screenHeight() {
      return deviceInfo.screenHeight
    },
    get windowWidth() {
      return deviceInfo.windowWidth
    },
    get windowHeight() {
      return deviceInfo.windowHeight
    },
    get statusBarHeight() {
      return deviceInfo.statusBarHeight
    },
    get pixelRatio() {
      return deviceInfo.pixelRatio
    },
    get safeAreaInsets() {
      return deviceInfo.safeAreaInsets
    },
    get isIOS() {
      return deviceInfo.platform === 'ios'
    },
    get isAndroid() {
      return deviceInfo.platform === 'android'
    },
    get isDarkMode() {
      return deviceInfo.theme === 'dark'
    },
  }
}

使用示例

vue
<template>
  <view class="device-demo">
    <view class="info-card">
      <view class="card-title">设备信息</view>
      <view class="info-list">
        <view class="info-row">
          <text class="label">品牌型号:</text>
          <text class="value">{{ deviceInfo.brand }} {{ deviceInfo.model }}</text>
        </view>
        <view class="info-row">
          <text class="label">操作系统:</text>
          <text class="value">{{ deviceInfo.system }}</text>
        </view>
        <view class="info-row">
          <text class="label">屏幕尺寸:</text>
          <text class="value">{{ screenWidth }} x {{ screenHeight }}</text>
        </view>
        <view class="info-row">
          <text class="label">窗口尺寸:</text>
          <text class="value">{{ windowWidth }} x {{ windowHeight }}</text>
        </view>
        <view class="info-row">
          <text class="label">状态栏高度:</text>
          <text class="value">{{ statusBarHeight }}px</text>
        </view>
        <view class="info-row">
          <text class="label">像素比:</text>
          <text class="value">{{ pixelRatio }}</text>
        </view>
        <view class="info-row">
          <text class="label">安全区域:</text>
          <text class="value">
            上{{ safeAreaInsets.top }} 下{{ safeAreaInsets.bottom }}
          </text>
        </view>
      </view>
    </view>

    <view class="info-card">
      <view class="card-title">平台信息</view>
      <view class="info-list">
        <view class="info-row">
          <text class="label">当前平台:</text>
          <text class="value">{{ deviceInfo.platform }}</text>
        </view>
        <view class="info-row">
          <text class="label">是否 iOS:</text>
          <text class="value">{{ isIOS ? '是' : '否' }}</text>
        </view>
        <view class="info-row">
          <text class="label">是否 Android:</text>
          <text class="value">{{ isAndroid ? '是' : '否' }}</text>
        </view>
        <view class="info-row">
          <text class="label">深色模式:</text>
          <text class="value">{{ isDarkMode ? '是' : '否' }}</text>
        </view>
        <view class="info-row">
          <text class="label">开发工具:</text>
          <text class="value">{{ deviceInfo.isDevTools ? '是' : '否' }}</text>
        </view>
      </view>
    </view>

    <view class="info-card">
      <view class="card-title">单位转换测试</view>
      <view class="convert-demo">
        <text>100rpx = {{ rpxToPx(100).toFixed(2) }}px</text>
        <text>50px = {{ pxToRpx(50).toFixed(2) }}rpx</text>
      </view>
    </view>
  </view>
</template>

<script lang="ts" setup>
import { useDevice } from '@/composables/useDevice'

const {
  deviceInfo,
  screenWidth,
  screenHeight,
  windowWidth,
  windowHeight,
  statusBarHeight,
  pixelRatio,
  safeAreaInsets,
  isIOS,
  isAndroid,
  isDarkMode,
  rpxToPx,
  pxToRpx,
} = useDevice()
</script>

系统语言检测

获取系统语言

typescript
// locales/i18n.ts

/**
 * 语言代码枚举
 */
export enum LanguageCode {
  zh_CN = 'zh-CN',
  en_US = 'en-US',
}

/**
 * 从系统获取默认语言
 */
export function getSystemLanguage(): LanguageCode {
  try {
    const systemInfo = uni.getSystemInfoSync()
    const systemLanguage = systemInfo.language

    // 匹配系统语言
    if (systemLanguage.includes('zh') || systemLanguage.includes('cn')) {
      return LanguageCode.zh_CN
    } else if (systemLanguage.includes('en')) {
      return LanguageCode.en_US
    }
  } catch (error) {
    console.warn('[i18n] 获取系统语言失败:', error)
  }

  // 默认返回中文
  return LanguageCode.zh_CN
}

/**
 * 从缓存获取语言设置,无缓存时使用系统语言
 */
export function getLanguageFromCache(): LanguageCode {
  try {
    // 优先从缓存获取
    const cachedLanguage = uni.getStorageSync('language') as LanguageCode
    if (cachedLanguage && Object.values(LanguageCode).includes(cachedLanguage)) {
      return cachedLanguage
    }

    // 缓存无效时使用系统语言
    return getSystemLanguage()
  } catch (error) {
    console.warn('[i18n] 获取语言设置失败:', error)
    return LanguageCode.zh_CN
  }
}

// 使用示例
const currentLanguage = getLanguageFromCache()
console.log('当前语言:', currentLanguage)

主题模式检测

检测系统主题

typescript
/**
 * 获取系统主题模式
 */
export function getSystemTheme(): 'light' | 'dark' {
  try {
    const systemInfo = uni.getSystemInfoSync()
    return systemInfo.theme || 'light'
  } catch (error) {
    console.warn('[theme] 获取系统主题失败:', error)
    return 'light'
  }
}

/**
 * 监听系统主题变化
 */
export function onSystemThemeChange(callback: (theme: 'light' | 'dark') => void) {
  // #ifdef MP-WEIXIN
  uni.onThemeChange((res) => {
    callback(res.theme)
  })
  // #endif

  // #ifdef H5
  if (window.matchMedia) {
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
    mediaQuery.addEventListener('change', (e) => {
      callback(e.matches ? 'dark' : 'light')
    })
  }
  // #endif
}

// 使用示例
const theme = getSystemTheme()
console.log('当前主题:', theme)

onSystemThemeChange((newTheme) => {
  console.log('主题变化:', newTheme)
  // 更新应用主题
})

类型定义

完整类型定义

typescript
/**
 * 平台类型
 */
export type Platform = 'ios' | 'android' | 'devtools' | 'windows' | 'mac' | string

/**
 * 主题类型
 */
export type ThemeMode = 'light' | 'dark'

/**
 * 安全区域
 */
export interface SafeArea {
  left: number
  right: number
  top: number
  bottom: number
  width: number
  height: number
}

/**
 * 安全区域边距
 */
export interface SafeAreaInsets {
  left: number
  right: number
  top: number
  bottom: number
}

/**
 * 设备信息
 */
export interface DeviceInfo {
  /** 设备品牌 */
  brand: string
  /** 设备型号 */
  model: string
  /** 操作系统及版本 */
  system: string
  /** 客户端平台 */
  platform: Platform
  /** 屏幕宽度(px) */
  screenWidth: number
  /** 屏幕高度(px) */
  screenHeight: number
  /** 可使用窗口宽度(px) */
  windowWidth: number
  /** 可使用窗口高度(px) */
  windowHeight: number
  /** 可使用窗口顶部位置(px) */
  windowTop: number
  /** 可使用窗口底部位置(px) */
  windowBottom: number
  /** 状态栏高度(px) */
  statusBarHeight: number
  /** 设备像素比 */
  pixelRatio: number
  /** 安全区域 */
  safeArea: SafeArea
  /** 安全区域边距 */
  safeAreaInsets: SafeAreaInsets
  /** 系统语言 */
  language: string
  /** 引擎版本号 */
  version: string
  /** 客户端基础库版本 */
  SDKVersion: string
  /** 系统主题 */
  theme: ThemeMode
  /** 是否 App 环境 */
  isApp: boolean
  /** 是否小程序环境 */
  isMp: boolean
  /** 是否微信小程序 */
  isMpWeixin: boolean
  /** 是否 H5 环境 */
  isH5: boolean
  /** 是否开发者工具 */
  isDevTools: boolean
}

/**
 * useDevice 返回类型
 */
export interface UseDeviceReturn {
  /** 设备信息(响应式) */
  deviceInfo: DeviceInfo
  /** 是否就绪 */
  isReady: Ref<boolean>
  /** 更新设备信息 */
  updateDeviceInfo: () => void
  /** rpx 转 px */
  rpxToPx: (rpx: number) => number
  /** px 转 rpx */
  pxToRpx: (px: number) => number
  /** 屏幕宽度 */
  screenWidth: number
  /** 屏幕高度 */
  screenHeight: number
  /** 窗口宽度 */
  windowWidth: number
  /** 窗口高度 */
  windowHeight: number
  /** 状态栏高度 */
  statusBarHeight: number
  /** 设备像素比 */
  pixelRatio: number
  /** 安全区域边距 */
  safeAreaInsets: SafeAreaInsets
  /** 是否 iOS */
  isIOS: boolean
  /** 是否 Android */
  isAndroid: boolean
  /** 是否深色模式 */
  isDarkMode: boolean
}

最佳实践

1. 缓存系统信息

typescript
// 避免频繁调用 getSystemInfoSync
let cachedSystemInfo: UniApp.GetSystemInfoResult | null = null

export function getSystemInfo(): UniApp.GetSystemInfoResult {
  if (!cachedSystemInfo) {
    cachedSystemInfo = uni.getSystemInfoSync()
  }
  return cachedSystemInfo
}

// 需要更新时清除缓存
export function clearSystemInfoCache() {
  cachedSystemInfo = null
}

2. 条件编译优化

typescript
// 使用条件编译避免无用代码
export function getPlatformFeatures() {
  const features: string[] = []

  // #ifdef APP-PLUS
  features.push('push', 'fingerprint', 'camera')
  // #endif

  // #ifdef MP-WEIXIN
  features.push('share', 'subscribe', 'miniprogram')
  // #endif

  // #ifdef H5
  features.push('clipboard', 'notification', 'webgl')
  // #endif

  return features
}

3. 安全区域适配

vue
<template>
  <view class="safe-container" :style="safeStyle">
    <slot />
  </view>
</template>

<script lang="ts" setup>
import { computed } from 'vue'
import { useDevice } from '@/composables/useDevice'

const { safeAreaInsets } = useDevice()

const safeStyle = computed(() => ({
  paddingTop: `${safeAreaInsets.top}px`,
  paddingBottom: `${safeAreaInsets.bottom}px`,
  paddingLeft: `${safeAreaInsets.left}px`,
  paddingRight: `${safeAreaInsets.right}px`,
}))
</script>

4. 响应式布局

vue
<script lang="ts" setup>
import { computed } from 'vue'
import { useDevice } from '@/composables/useDevice'

const { windowWidth } = useDevice()

// 根据屏幕宽度计算列数
const columns = computed(() => {
  if (windowWidth < 375) return 2
  if (windowWidth < 750) return 3
  return 4
})
</script>

常见问题

1. getSystemInfoSync 返回值不完整

问题原因:

  • 不同平台返回的字段不同
  • 某些字段在特定平台下为 undefined

解决方案:

typescript
// 使用默认值处理
const systemInfo = uni.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 20
const pixelRatio = systemInfo.pixelRatio || 1
const safeArea = systemInfo.safeArea || {
  left: 0,
  right: systemInfo.screenWidth,
  top: 0,
  bottom: systemInfo.screenHeight,
  width: systemInfo.screenWidth,
  height: systemInfo.screenHeight,
}

2. 屏幕旋转后信息不更新

问题原因:

  • getSystemInfoSync 获取的是调用时的信息
  • 屏幕旋转后需要重新获取

解决方案:

typescript
// 监听屏幕旋转
// #ifdef APP-PLUS
plus.screen.onOrientationChange(() => {
  updateDeviceInfo()
})
// #endif

// #ifdef H5
window.addEventListener('resize', () => {
  updateDeviceInfo()
})
// #endif

3. 小程序和 H5 行为不一致

问题原因:

  • 不同平台 API 实现差异
  • 某些功能仅特定平台支持

解决方案:

typescript
// 使用条件编译处理差异
export function getNavigatorInfo() {
  // #ifdef H5
  return {
    userAgent: navigator.userAgent,
    language: navigator.language,
    online: navigator.onLine,
  }
  // #endif

  // #ifndef H5
  const systemInfo = uni.getSystemInfoSync()
  return {
    userAgent: '',
    language: systemInfo.language,
    online: true,  // 小程序默认在线
  }
  // #endif
}

4. Canvas 绘制模糊

问题原因:

  • 未考虑设备像素比
  • Canvas 尺寸与显示尺寸不匹配

解决方案:

typescript
const { pixelRatio, windowWidth } = uni.getSystemInfoSync()

// Canvas 实际尺寸 = 显示尺寸 * 像素比
const canvasWidth = 300 * pixelRatio
const canvasHeight = 200 * pixelRatio

// 绘制时缩放
const ctx = uni.createCanvasContext('canvas')
ctx.scale(pixelRatio, pixelRatio)

// 正常尺寸绑制(会自动放大)
ctx.fillRect(0, 0, 100, 100)
ctx.draw()

5. 状态栏高度获取为 0

问题原因:

  • H5 环境没有状态栏概念
  • 某些 Android 设备返回异常

解决方案:

typescript
export function getStatusBarHeight(): number {
  const systemInfo = uni.getSystemInfoSync()

  // H5 环境返回 0
  // #ifdef H5
  return 0
  // #endif

  // 小程序和 App
  const height = systemInfo.statusBarHeight

  // 异常值处理
  if (!height || height < 0) {
    // iOS 默认 20px(非刘海屏)或 44px(刘海屏)
    if (systemInfo.platform === 'ios') {
      return systemInfo.safeArea?.top || 20
    }
    // Android 默认 24px
    return 24
  }

  return height
}

6. 安全区域计算错误

问题原因:

  • safeArea 字段可能为 undefined
  • 不同平台计算方式不同

解决方案:

typescript
export function getSafeAreaInsets(): SafeAreaInsets {
  const systemInfo = uni.getSystemInfoSync()
  const { screenWidth, screenHeight, safeArea } = systemInfo

  if (!safeArea) {
    return { left: 0, right: 0, top: 0, bottom: 0 }
  }

  return {
    left: safeArea.left || 0,
    right: screenWidth - (safeArea.right || screenWidth),
    top: safeArea.top || 0,
    bottom: screenHeight - (safeArea.bottom || screenHeight),
  }
}