From 565d6e0fcb6b5da2cafb24b2b435b6ab68e07590 Mon Sep 17 00:00:00 2001 From: dsh-agent Date: Sat, 26 Sep 2026 21:38:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(api):=20=E6=96=B0=E5=A2=9E=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E4=BA=8C=E7=BB=B4=E7=A0=81=E6=8E=A5=E5=8F=A3=20/api/q?= =?UTF-8?q?rcode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 小程序没有任何 API 能直接唤起系统浏览器离开小程序打开网址, 唯一能真正离开小程序的官方途径是「长按图片 → 识别图中二维码」。 本接口把 URL 渲染成二维码图片(返回 base64 data URL),供小程序端 + show-menu-by-longpress 使用,让管理员能在微信内置浏览器里 打开完整网页版后台。 安全:需登录;只接受 http/https;长度上限 512。 --- server/api/qrcode.get.ts | 48 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 server/api/qrcode.get.ts diff --git a/server/api/qrcode.get.ts b/server/api/qrcode.get.ts new file mode 100644 index 0000000..e7cadc7 --- /dev/null +++ b/server/api/qrcode.get.ts @@ -0,0 +1,48 @@ +import QRCode from 'qrcode' + +/** + * 通用二维码生成 + * + * 为什么需要这个接口: + * 微信小程序**没有**任何 API 能直接唤起系统浏览器 / 离开小程序打开一个网址。 + * 官方可行的方式只有两种: + * 1. —— 内嵌打开(不离开小程序),且个人主体不可用 + * 2. 复制链接,让用户自己去浏览器粘贴 + * 还有一种能**真正离开小程序**的官方途径:**长按图片 → 识别图中二维码**。 + * 本接口就是把 URL 渲染成二维码图片,供小程序端 wx.previewImage 展示。 + * + * 用法: + * GET /api/qrcode?text=https%3A%2F%2Fbywmusic.bzw-hheb.top%2Fdashboard + * 返回 { success: true, dataUrl: 'data:image/png;base64,...' } + * + * 安全: + * · 需要登录(避免被当成免费二维码生成服务) + * · 只接受 http/https 链接,且长度受限 + */ +export default defineEventHandler(async (event) => { + const user = event.context.user + if (!user) { + throw createError({ statusCode: 401, message: '未授权访问' }) + } + + const query = getQuery(event) + const text = typeof query.text === 'string' ? query.text.trim() : '' + + if (!text) { + throw createError({ statusCode: 400, message: '缺少 text 参数' }) + } + if (text.length > 512) { + throw createError({ statusCode: 400, message: '内容过长(最多 512 字符)' }) + } + if (!/^https?:\/\//i.test(text)) { + throw createError({ statusCode: 400, message: '只支持 http/https 链接' }) + } + + const dataUrl = await QRCode.toDataURL(text, { + errorCorrectionLevel: 'M', + margin: 1, + width: 480 + }) + + return { success: true, dataUrl } +})