用手机扫码打开链接
'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>
)
}
用法
import { QRCode } from '@hina-ui/react'
export function ShareCode() {
return <QRCode value="https://hinaui.dev" label="Hina UI 文档站" />
}
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>
)
}
颜色应保持深码浅底和足够对比度,标志尽量小,并用实际扫码设备验收。较高纠错等级不能保证任意面积的遮挡都可恢复。默认 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>
)
}
自定义状态
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>
)
}
导出图片
通过组件 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>
)
}
'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>
</>
)
}
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 均从包根导出。