QRCode 二维码

将链接或文本生成为可扫描、可导出的二维码。

用手机扫码打开链接

<script setup lang="ts">
  import { ref } from 'vue'
  import { FormField, Input, QRCode, Stack, Text } from '@hina-ui/vue'
  const value = ref('https://hinaui.dev')
</script>
<template>
  <Stack align="center" class="w-full max-w-sm" data-demo-qr-hero>
    <QRCode :value="value" label="Hina UI 文档站" />
    <Text size="sm" tone="muted">用手机扫码打开链接</Text>
    <FormField label="二维码内容" class="w-full">
      <Input v-model="value" placeholder="https://hinaui.dev" />
    </FormField>
  </Stack>
</template>
vue

用法

<script setup lang="ts">
  import { QRCode } from '@hina-ui/vue'
</script>

<template>
  <QRCode value="https://hinaui.dev" label="Hina UI 文档站" />
</template>
vue

value 是要编码的原始字符串,链接、中文和普通文本都可以。组件不访问链接,也不解释查询参数。label 为辅助技术描述二维码用途;可访问的页面链接仍应由调用方另外提供。

直接输出 SVG,SSR 首屏就有完整码图和尺寸,水合不需要测量、Canvas 或重新请求二维码。空字符串显示空态;内容超过二维码容量时显示错误态并触发 error,修正内容后自动恢复。

示例

尺寸、颜色与标志

尺寸包含周围留白,容器较窄时等比缩小。color 和 background 接受 CSS 颜色或 token;默认使用专用语义 token,在深色主题下仍保持深色码、浅色底,避免自动反色。

中心标志使用 logo。未指定 level 时,带标志默认采用 H,普通二维码默认采用 M。logoSize 和 logoMargin 是设计尺寸内的像素值,会随整体缩放;标志边长最多占总边长的四分之一。图片加载失败时移除标志及其底色,恢复完整码图,并触发 logo-error。

192px

<script setup lang="ts">
  import { ref } from 'vue'
  import {
    FormField,
    Inline,
    QRCode,
    SegmentedControl,
    Slider,
    Stack,
    Switch,
    Text,
    type QRCodeLevel,
  } from '@hina-ui/vue'
  const size = ref(192)
  const logo = ref(true)
  const brand = ref(false)
  const level = ref<QRCodeLevel>('H')
  const levels = ['L', 'M', 'Q', 'H'].map(value => ({ value, label: value }))
</script>
<template>
  <Stack align="center" class="w-full max-w-sm" data-demo-qr-appearance>
    <QRCode
      value="https://hinaui.dev"
      :size="size"
      :level="level"
      :logo="logo ? '/favicon.png' : undefined"
      :color="brand ? 'var(--color-brand-800)' : undefined"
    />
    <Stack class="w-full">
      <FormField label="尺寸">
        <Inline :wrap="false" gap="sm">
          <Slider v-model="size" :min="128" :max="256" :step="8" class="flex-1" />
          <Text size="sm" tone="muted" class="w-14 shrink-0 tabular-nums">{{ size }}px</Text>
        </Inline>
      </FormField>
      <FormField label="纠错等级">
        <SegmentedControl v-model="level" :options="levels" block />
      </FormField>
      <Inline justify="between">
        <Switch v-model="logo">中心标志</Switch>
        <Switch v-model="brand">品牌色</Switch>
      </Inline>
    </Stack>
  </Stack>
</template>
vue

颜色应保持深码浅底和足够对比度,标志尽量小,并用实际扫码设备验收。较高纠错等级不能保证任意面积的遮挡都可恢复。默认 :margin="4" 是四个码格的安静区,采用 QR Code 官方规定的留白;它不是像素内边距。

状态与刷新

status 由调用方控制。组件没有倒计时、轮询或请求逻辑。加载、过期和已扫描状态隐藏旧码,保留方形占位;过期态的按钮只触发 refresh,等待业务请求更新 value 和 status。

二维码已过期

刷新在这里模拟一次异步请求,状态由调用方控制。

<script setup lang="ts">
  import { onScopeDispose, ref } from 'vue'
  import { QRCode, Select, Stack, Text, type QRCodeStatus } from '@hina-ui/vue'
  const status = ref<QRCodeStatus>('expired')
  const options = [
    { value: 'active', label: '可扫描' },
    { value: 'loading', label: '加载中' },
    { value: 'expired', label: '已过期' },
    { value: 'scanned', label: '已扫描' },
  ]
  let timer: ReturnType<typeof setTimeout> | undefined
  function refresh() {
    clearTimeout(timer)
    status.value = 'loading'
    timer = setTimeout(() => {
      status.value = 'active'
    }, 800)
  }
  function change(value: unknown) {
    clearTimeout(timer)
    status.value = value as QRCodeStatus
  }
  onScopeDispose(() => clearTimeout(timer))
</script>
<template>
  <Stack align="center" class="w-full max-w-sm" data-demo-qr-status>
    <QRCode value="https://hinaui.dev" :status="status" @refresh="refresh" />
    <Select
      :model-value="status"
      :options="options"
      aria-label="二维码状态"
      class="w-48"
      @update:model-value="change"
    />
    <Text size="sm" tone="muted" class="text-center">
      刷新在这里模拟一次异步请求,状态由调用方控制。
    </Text>
  </Stack>
</template>
vue

自定义状态

#status 可完全替换非激活态的内容,提供 { status, error, refresh }。例如分享链接失效后,需要自己的提示文案和操作。error 只表示编码失败;外部请求错误由调用方处理。

分享链接已过期

重新生成后再扫码打开

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, QRCode, Stack, Text } from '@hina-ui/vue'
  const expired = ref(true)
</script>
<template>
  <Stack align="center" data-demo-qr-custom>
    <QRCode
      value="https://hinaui.dev"
      :status="expired ? 'expired' : 'active'"
      :size="224"
      @refresh="expired = false"
    >
      <template #status="{ refresh }">
        <Text weight="medium">分享链接已过期</Text>
        <Text size="xs" tone="muted">重新生成后再扫码打开</Text>
        <Button size="sm" variant="outline" @click="refresh">重新生成</Button>
      </template>
    </QRCode>
    <Button variant="ghost" size="sm" :disabled="expired" @click="expired = true">模拟过期</Button>
  </Stack>
</template>
vue

导出图片

通过组件 ref 的 toBlob() 导出 PNG,或指定 type: 'image/svg+xml' 获取 SVG。组件只生成文件内容,文件名和下载操作由调用方决定。

导出会固定当前颜色并将标志内嵌,文件不依赖页面 CSS 或标志地址。跨域标志需要服务端允许 CORS;读取失败时 Promise 拒绝,不会静默导出一张缺少标志的图片。非激活态不能导出。

<script setup lang="ts">
  import { onScopeDispose, ref } from 'vue'
  import { Download } from '@lucide/vue'
  import { Button, Inline, QRCode, Stack, Text, type QRCodeExpose } from '@hina-ui/vue'
  const code = ref<QRCodeExpose>()
  const busy = ref(false)
  const error = ref(false)
  const urls = new Set<string>()
  async function save(type: 'image/png' | 'image/svg+xml') {
    if (!code.value || busy.value) return
    busy.value = true
    error.value = false
    try {
      const blob = await code.value.toBlob({ type, scale: 3 })
      const url = URL.createObjectURL(blob)
      urls.add(url)
      const link = document.createElement('a')
      link.href = url
      link.download = type === 'image/png' ? 'hina-ui.png' : 'hina-ui.svg'
      link.click()
      setTimeout(() => {
        URL.revokeObjectURL(url)
        urls.delete(url)
      }, 1000)
    } catch {
      error.value = true
    } finally {
      busy.value = false
    }
  }
  onScopeDispose(() => urls.forEach(url => URL.revokeObjectURL(url)))
</script>
<template>
  <Stack align="center" data-demo-qr-export>
    <QRCode ref="code" value="https://hinaui.dev" logo="/favicon.png" label="Hina UI 文档站" />
    <Inline gap="sm">
      <Button variant="outline" size="sm" :disabled="busy" @click="save('image/png')">
        <template #icon><Download /></template>
        PNG
      </Button>
      <Button variant="outline" size="sm" :disabled="busy" @click="save('image/svg+xml')">
        <template #icon><Download /></template>
        SVG
      </Button>
    </Inline>
    <Text v-if="error" tone="danger" size="sm" role="alert">导出失败,请重试。</Text>
  </Stack>
</template>
vue
import { ref } from 'vue'
import type { QRCodeExpose } from '@hina-ui/vue'

const code = ref<QRCodeExpose>()
const png = await code.value?.toBlob({ scale: 3 })
const svg = await code.value?.toBlob({ type: 'image/svg+xml' })
ts

API

Props

属性
类型
默认值
说明
value
string
—
要编码的内容,必填
label
string
本地化“二维码”
SVG 的无障碍名称
size
number
192
含留白的设计边长,px;最大 4096,窄容器内等比缩小
level
'L' | 'M' | 'Q' | 'H'
有 logo 时 H,否则 M
纠错等级
margin
number
4
四周留白,单位为码格,0–64 的整数
color
string
--hn-qr-foreground
码图颜色
background
string
--hn-qr-background
码图和标志底色
bordered
boolean
true
外轮廓
logo
string
—
中心图片 URL,也可使用 data URL
logoSize
number
32
标志边长,px;上限为设计边长的 25%
logoMargin
number
2
标志四周留白,px;上限为设计边长的 1/32
status
'active' | 'loading' | 'expired' | 'scanned'
'active'
外部状态
class
string
—
根节点样式

Slots

插槽
参数
说明
status
QRCodeStatusSlot
非激活态内容:{ status, error, refresh }

实际状态 QRCodeState 还包括 empty 和 error,由编码结果决定。根节点通过 data-state 暴露实际状态;加载时带 aria-busy。

Events

事件
参数
说明
refresh
—
请求刷新,不自动改变状态
error
Error
编码失败
logo-error
Event
中心图片加载失败

Expose

名称
类型
说明
element
HTMLElement | undefined
根节点
svg
SVGSVGElement | undefined
当前激活码图
toBlob(options?)
Promise<Blob>
浏览器中导出当前二维码;失败时拒绝

QRCodeExportOptions 提供 type(默认 'image/png')和 PNG 的 scale(默认 2,范围 1–8)。PNG 按 size × scale 导出,单边最大 8192px;SVG 按 size 导出。QRCodeProps、QRCodeLevel、QRCodeStatus、QRCodeState、QRCodeStatusSlot、QRCodeExportOptions 和 QRCodeExpose 均从包根导出。