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