QRCode 二维码

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

用手机扫码打开链接

'use client'

import { useState } from 'react'
import { FormField, Input, QRCode, Stack, Text } from '@hina-ui/react'

export default function Demo() {
  const [value, setValue] = useState('https://hinaui.dev')

  return (
    <Stack align="center" className="w-full max-w-sm" data-demo-qr-hero="">
      <QRCode value={value} label="Hina UI 文档站" />
      <Text size="sm" tone="muted">
        用手机扫码打开链接
      </Text>
      <FormField label="二维码内容" className="w-full">
        <Input value={value} onValueChange={setValue} placeholder="https://hinaui.dev" />
      </FormField>
    </Stack>
  )
}
tsx

用法

import { QRCode } from '@hina-ui/react'

export function ShareCode() {
  return <QRCode value="https://hinaui.dev" label="Hina UI 文档站" />
}
tsx

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

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

示例

尺寸、颜色与标志

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

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

192px

'use client'

import { useState } from 'react'
import {
  FormField,
  Inline,
  QRCode,
  SegmentedControl,
  Slider,
  Stack,
  Switch,
  Text,
  type QRCodeLevel,
} from '@hina-ui/react'

const levels = ['L', 'M', 'Q', 'H'].map(value => ({ value, label: value }))

export default function Demo() {
  const [size, setSize] = useState<number | undefined>(192)
  const [logo, setLogo] = useState(true)
  const [brand, setBrand] = useState(false)
  const [level, setLevel] = useState<QRCodeLevel>('H')

  return (
    <Stack align="center" className="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 className="w-full">
        <FormField label="尺寸">
          <Inline wrap={false} gap="sm">
            <Slider
              value={size}
              onValueChange={setSize}
              min={128}
              max={256}
              step={8}
              className="flex-1"
            />
            <Text size="sm" tone="muted" className="w-14 shrink-0 tabular-nums">
              {size}px
            </Text>
          </Inline>
        </FormField>
        <FormField label="纠错等级">
          <SegmentedControl
            value={level}
            onValueChange={value => setLevel(value as QRCodeLevel)}
            options={levels}
            block
          />
        </FormField>
        <Inline justify="between">
          <Switch checked={logo} onCheckedChange={setLogo}>
            中心标志
          </Switch>
          <Switch checked={brand} onCheckedChange={setBrand}>
            品牌色
          </Switch>
        </Inline>
      </Stack>
    </Stack>
  )
}
tsx

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

状态与刷新

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

二维码已过期

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

'use client'

import { useEffect, useRef, useState } from 'react'
import { QRCode, Select, Stack, Text, type QRCodeStatus } from '@hina-ui/react'

const options = [
  { value: 'active', label: '可扫描' },
  { value: 'loading', label: '加载中' },
  { value: 'expired', label: '已过期' },
  { value: 'scanned', label: '已扫描' },
]

export default function Demo() {
  const [status, setStatus] = useState<QRCodeStatus>('expired')
  const timer = useRef<ReturnType<typeof setTimeout>>(undefined)

  function refresh() {
    clearTimeout(timer.current)
    setStatus('loading')
    timer.current = setTimeout(() => {
      setStatus('active')
    }, 800)
  }

  function change(value: unknown) {
    clearTimeout(timer.current)
    setStatus(value as QRCodeStatus)
  }

  useEffect(() => () => clearTimeout(timer.current), [])

  return (
    <Stack align="center" className="w-full max-w-sm" data-demo-qr-status="">
      <QRCode value="https://hinaui.dev" status={status} onRefresh={refresh} />
      <Select
        value={status}
        options={options}
        aria-label="二维码状态"
        className="w-48"
        onValueChange={change}
      />
      <Text size="sm" tone="muted" className="text-center">
        刷新在这里模拟一次异步请求,状态由调用方控制。
      </Text>
    </Stack>
  )
}
tsx

自定义状态

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

分享链接已过期

重新生成后再扫码打开

'use client'

import { useState } from 'react'
import { Button, QRCode, Stack, Text } from '@hina-ui/react'

export default function Demo() {
  const [expired, setExpired] = useState(true)

  return (
    <Stack align="center" data-demo-qr-custom="">
      <QRCode
        value="https://hinaui.dev"
        status={expired ? 'expired' : 'active'}
        size={224}
        onRefresh={() => setExpired(false)}
        renderStatus={({ refresh }) => (
          <>
            <Text weight="medium">分享链接已过期</Text>
            <Text size="xs" tone="muted">
              重新生成后再扫码打开
            </Text>
            <Button size="sm" variant="outline" onClick={refresh}>
              重新生成
            </Button>
          </>
        )}
      />
      <Button variant="ghost" size="sm" disabled={expired} onClick={() => setExpired(true)}>
        模拟过期
      </Button>
    </Stack>
  )
}
tsx

导出图片

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

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

'use client'

import { useEffect, useRef, useState } from 'react'
import { Download } from 'lucide-react'
import { Button, Inline, QRCode, Stack, Text, type QRCodeExpose } from '@hina-ui/react'

export default function Demo() {
  const code = useRef<QRCodeExpose>(null)
  const [busy, setBusy] = useState(false)
  const [error, setError] = useState(false)
  const urls = useRef(new Set<string>())

  async function save(type: 'image/png' | 'image/svg+xml') {
    if (!code.current || busy) return
    setBusy(true)
    setError(false)
    try {
      const blob = await code.current.toBlob({ type, scale: 3 })
      const url = URL.createObjectURL(blob)
      urls.current.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.current.delete(url)
      }, 1000)
    } catch {
      setError(true)
    } finally {
      setBusy(false)
    }
  }

  useEffect(() => {
    const pending = urls.current
    return () => pending.forEach(url => URL.revokeObjectURL(url))
  }, [])

  return (
    <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}
          onClick={() => save('image/png')}
          icon={<Download />}
        >
          PNG
        </Button>
        <Button
          variant="outline"
          size="sm"
          disabled={busy}
          onClick={() => save('image/svg+xml')}
          icon={<Download />}
        >
          SVG
        </Button>
      </Inline>
      {error && (
        <Text tone="danger" size="sm" role="alert">
          导出失败,请重试。
        </Text>
      )}
    </Stack>
  )
}
tsx
'use client'

import { useRef } from 'react'
import { Button, QRCode, type QRCodeExpose } from '@hina-ui/react'

export function ShareCode() {
  const code = useRef<QRCodeExpose>(null)

  async function exportCode() {
    const png = await code.current?.toBlob({ scale: 3 })
    const svg = await code.current?.toBlob({ type: 'image/svg+xml' })
  }

  return (
    <>
      <QRCode ref={code} value="https://hinaui.dev" />
      <Button onClick={exportCode}>导出</Button>
    </>
  )
}
tsx

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'
外部状态
className
string
—
根节点样式

内容属性

属性
参数
说明
renderStatus
QRCodeStatusSlot
非激活态内容:{ status, error, refresh }

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

回调

回调
参数
说明
onRefresh
—
请求刷新,不自动改变状态
onError
Error
编码失败
onLogoError
Event
中心图片加载失败

Ref

名称
类型
说明
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 均从包根导出。