diff --git a/api/book.js b/api/book.js index 5dd8a87..7cefddb 100644 --- a/api/book.js +++ b/api/book.js @@ -69,9 +69,6 @@ export function FetchCoverByISBN(data) { // 文献排行榜 -// /api/screenSetting/sync36 - -// 读者借阅排行榜 export function FetchBookRanking(data) { return request({ url: '/api/screenSetting/sync36', @@ -79,8 +76,13 @@ export function FetchBookRanking(data) { }) } +// 读者借阅排行榜 +// /api/screenSetting/sync37 + + // 馆藏量 // /api/screenSetting/sync35 + // 累计借出 // /api/screenSetting/sync82 \ No newline at end of file diff --git a/components/book-list-item/book-list-item.vue b/components/book-list-item/book-list-item.vue index cbaf45e..97a5e05 100644 --- a/components/book-list-item/book-list-item.vue +++ b/components/book-list-item/book-list-item.vue @@ -2,28 +2,17 @@ - + class="img-item" + :src="data.base64Cover || data.cover || data.imgCover || '/static/images/default-book.png'" + mode="scaleToFill" + @error="onImgError" + > {{ data.title || data.name || '暂无标题' }} {{ data.author || '佚名' }} {{ data.publisher || '暂无出版社数据' }} - - @@ -48,13 +37,6 @@ export default { methods: { onImgError(e) { e.target.src = "/static/images/default-book.png"; - }, - hotNumber(num) { - if (!num) return "0"; - if (num >= 10000) { - return (num / 10000).toFixed(1) + "w"; - } - return num; } } }; diff --git a/pages/home/home.vue b/pages/home/home.vue index 14a6e82..fa81ab6 100644 --- a/pages/home/home.vue +++ b/pages/home/home.vue @@ -3,14 +3,14 @@ - + 葛店经济技术开发区图书馆 - + @@ -28,7 +28,7 @@ 已过期 --> - + @@ -45,7 +45,8 @@ - + + @@ -113,13 +114,14 @@ :circular="false" > - + 读者证号 - {{ item.bindValue }} + {{ formatCardNo(item.bindValue) }} 默认证 + @@ -133,11 +135,34 @@ + + + + 读者证二维码 + + {{ qrcodeText }} + 关闭 + + + + 图书推荐 - + 查看更多 @@ -148,14 +173,17 @@ {{item.name}} + + + 暂无推荐书籍~ + - @@ -176,6 +204,8 @@ export default { isBindLibraryCard: false, readerCardList: [], // 多个借阅证 baseUrl: config.baseUrl, + qrcodeText: '', // 二维码内容 + popupFixedStyle: 'position:fixed;top:0;left:0;right:0;bottom:0;' }; }, onLoad() { @@ -185,6 +215,23 @@ export default { this.initUserAndCheckBind(); }, methods:{ + // 格式化读者证号 + formatCardNo(cardNo) { + if (!cardNo) return ''; + const str = String(cardNo); + const len = str.length; + + if (len > 8) { + // 前2位 + 中间全部* + 后2位 + const front = str.substring(0, 2); + const end = str.substring(len - 2); + const star = '*'.repeat(len - 4); // 中间全部隐藏 + return front + star + end; + } else { + // 小于等于8:*** + 后2位 + return '***' + str.substring(len - 2); + } + }, onImgError(e) { e.target.src = "/static/images/default-book.png"; }, @@ -237,6 +284,7 @@ export default { this.readerCardList = defaultCard ? [defaultCard, ...otherCards] : res.data; uni.setStorageSync(READLIST, this.readerCardList); + uni.setStorageSync('currentReaderCard', defaultCard); } else { this.isBindLibraryCard = false; this.readerCardList = []; @@ -254,15 +302,39 @@ export default { this.isBindLibraryCard = list.length > 0; } }, + // 获取图书推荐 + 封面转 base64 async getRecommendBooks() { try { const res = await FetchInitScreenBookRecommend({ libcode: '1201' }); - this.recommendedBooks = res.data || []; - console.log('图书推荐', this.recommendedBooks); - this.isLoading = false + let books = res.data || []; + + // 只遍历前3条,做Base64转换 + const limit = 3; + for (let i = 0; i < books.length && i < limit; i++) { + let item = books[i]; + if (item.imgPath) { + const imageUrl = this.baseUrl + '/api/fileRelevant/getImg?imgType=2&imgId=' + item.imgPath; + try { + const base64 = await this.urlToBase64(imageUrl); + item.base64Cover = base64; + } catch (err) { + item.base64Cover = ''; + } + } else { + item.base64Cover = ''; + } + } + + // 第3条之后直接赋空,用默认图 + for (let i = limit; i < books.length; i++) { + books[i].base64Cover = ''; + } + + this.recommendedBooks = books; + this.isLoading = false; } catch (err) { console.error('获取图书推荐失败:', err); - this.isLoading = false + this.isLoading = false; } }, goToBookDetail(item) { @@ -339,34 +411,39 @@ export default { url: "/pages/user/user" }); }, - onToScan() { - uni.scanCode({ - onlyFromCamera: true, - scanType: ['barCode'], - success: (res) => { - uni.showToast({ - title: '扫码结果:'+ res.result, - icon: 'none' - }) - let isbn = res.result || '' - - if (!isbn || isbn.length < 10) { - isbn = '9787020167319' - } - - uni.navigateTo({ - url: '/subpkg/pages/book-detail/book-detail?isbn=' + isbn - }) - }, - fail: (err) => { - console.log('扫码失败:', err) - uni.showToast({ - title: '扫码取消或失败', - icon: 'none' - }) - } - }) - } + showQrcode(bindValue) { + this.qrcodeText = bindValue; + this.$refs.popup.open(); + }, + closePopup() { + console.log('关闭弹窗'); + this.qrcodeText = ''; + this.$refs.popup.close(); + }, + urlToBase64(url) { + return new Promise((resolve, reject) => { + uni.request({ + url, + method: 'GET', + responseType: 'arraybuffer', + success: (res) => { + try { + const buffer = new Uint8Array(res.data); + let binary = ''; + for (let i = 0; i < buffer.length; i++) { + binary += String.fromCharCode(buffer[i]); + } + const base64 = uni.arrayBufferToBase64(res.data); + const dataUri = 'data:image/jpeg;base64,' + base64; + resolve(dataUri); + } catch (e) { + reject(e); + } + }, + fail: reject + }); + }); + }, } } @@ -555,44 +632,10 @@ swiper.menu-swiper{ .card-list { width: 100%; } - - // .card-item { - // position: relative; - // background: #fff; - // border-radius: 10px; - // padding: 15px; - // margin-bottom: 10px; - // box-shadow: 0 1px 6px rgba(0,0,0,0.06); - - // .item-row { - // display: flex; - // margin-bottom: 8px; - // font-size: 13px; - // .label { - // color: #666; - // width: 85px; - // } - // .value { - // flex: 1; - // color: #333; - // } - // } - - // .default-tag { - // position: absolute; - // right: 12px; - // top: 12px; - // font-size: 11px; - // color: #01a4fe; - // border: 1px solid #01a4fe; - // border-radius: 4px; - // padding: 2px 6px; - // } - // } } .card-swiper { - height: 106px; /* 固定高度,保证滑动流畅 */ + height: 106px; .card-item{ display: flex; justify-content: space-between; @@ -644,4 +687,25 @@ swiper.menu-swiper{ } } } + +.erweima-style{ + position: absolute; + right: 12px; + bottom: 22px; + ::v-deep .icon-erweima{ + // font-weight: bold; + color: #999 !important; + } +} + +/* 空状态 */ +.empty-box { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #999; + font-size: 15px; + padding: 20px; +} \ No newline at end of file diff --git a/pages/lendCar/lendCar.vue b/pages/lendCar/lendCar.vue index c9f47df..5234649 100644 --- a/pages/lendCar/lendCar.vue +++ b/pages/lendCar/lendCar.vue @@ -1,6 +1,6 @@ @@ -58,12 +81,30 @@ export default { isEditMode: false, selectedValue: '', cardList: [], + qrcodeText: '' // 二维码内容 } }, onShow() { this.getBindReaderCardList(); }, methods: { + // 格式化读者证号 + formatCardNo(cardNo) { + if (!cardNo) return ''; + const str = String(cardNo); + const len = str.length; + + if (len > 8) { + // 前2位 + 中间全部* + 后2位 + const front = str.substring(0, 2); + const end = str.substring(len - 2); + const star = '*'.repeat(len - 4); // 中间全部隐藏 + return front + star + end; + } else { + // 小于等于8:*** + 后2位 + return '***' + str.substring(len - 2); + } + }, // 切换编辑模式 + 清空选中 toggleEditMode() { this.isEditMode = !this.isEditMode; @@ -94,52 +135,55 @@ export default { } }, - // 点击卡片 + // 点击卡片(仅编辑模式使用) handleSelectItem(value) { - // 编辑模式:只做选择 if (this.isEditMode) { this.selectedValue = this.selectedValue === value ? '' : value; - return; } + }, - // 正常模式:切换默认 + async setDefaultCard(value) { const currentDefault = this.cardList.find(item => item.bindDefault === true); if (currentDefault && currentDefault.bindValue === value) { - uni.showToast({ - title: '当前已是默认证,请勿重复操作', - icon: 'none' - }); + uni.showToast({ title: '当前已是默认证', icon: 'none' }); return; } const openId = uni.getStorageSync('wx_login_code'); uni.showModal({ title: '提示', - content: '确定切换默认读者证吗?', + content: '确定设为默认读者证吗?', success: async (res) => { if (!res.confirm) return; - try { - const result = await FetchSetDefaultReadCard({ + await FetchSetDefaultReadCard({ bindType: 'rdid', bindValue: value, libcode: '1201', openid: openId }); - - if (result.code === 200) { - uni.setStorageSync('currentReaderCard', value); - await this.getBindReaderCardList(); - uni.showToast({ title: '默认证切换成功', icon: 'success' }); - } + uni.setStorageSync('currentReaderCard', value); + await this.getBindReaderCardList(); + uni.showToast({ title: '设置成功', icon: 'success' }); } catch (err) { - uni.showToast({ title: '切换失败', icon: 'none' }); + uni.showToast({ title: '设置失败', icon: 'none' }); } } }); }, - // 解绑(核心逻辑) + // ========== 新功能:显示二维码 ========== + showQrcode(bindValue) { + this.qrcodeText = bindValue; + this.$refs.popup.open(); + }, + closePopup() { + console.log('关闭弹窗'); + this.qrcodeText = ''; + this.$refs.popup.close(); + }, + + // 解绑(不变) async handleUnbind() { if (!this.selectedValue) { uni.showToast({ title: '请选择要解绑的读者证', icon: 'none' }); @@ -166,12 +210,9 @@ export default { if (result.code === 200) { uni.showToast({ title: '解绑成功', icon: 'success' }); - - // 清空选中(不解绑模式自动选中第一个) this.selectedValue = ''; await this.getBindReaderCardList(); - // 解绑的是默认 → 自动设置第一个为新默认(接口+缓存) if (isUnbindDefault && this.cardList.length > 0) { const newValue = this.cardList[0].bindValue; await FetchSetDefaultReadCard({ @@ -184,7 +225,6 @@ export default { await this.getBindReaderCardList(); } - // 全部解绑 → 返回 if (this.cardList.length === 0) { setTimeout(() => uni.navigateBack(), 1500); } @@ -323,4 +363,19 @@ export default { } } } + +.card-setting{ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + button{ + padding: 0 8px !important; + margin-bottom: 6px; + font-size: 12px; + } + ::v-deep .icon-erweima{ + color: #999 !important; + } +} \ No newline at end of file diff --git a/subpkg/pages/user-info/user-info.vue b/subpkg/pages/user-info/user-info.vue index 3f954c7..ad601c3 100644 --- a/subpkg/pages/user-info/user-info.vue +++ b/subpkg/pages/user-info/user-info.vue @@ -5,7 +5,7 @@ @@ -16,7 +16,7 @@ @@ -33,35 +33,51 @@ import config from '@/utils/config' import { FetchBindRead } from '@/api/user'; -// 缓存 key 与个人中心保持一致 const USER_KEY = 'user-info'; export default { data() { return { - nickName: '', - avatarUrl: '' + nickname: '', + avatarUrl: '', // 仅存放 base64,用于显示 + imgId: '', // 仅用于提交后端 + saving: false // 防重复提交 }; }, onLoad() { - // 页面加载时获取本地缓存的用户信息 this.loadUserInfo(); }, - methods: { - // 加载缓存中的用户信息 - loadUserInfo() { + methods:{ + // 加载用户信息 + async loadUserInfo() { const userInfo = uni.getStorageSync(USER_KEY) || {}; - this.nickName = userInfo.nickName || ''; - this.avatarUrl = userInfo.avatarUrl || ''; + this.nickname = userInfo.nickname || ''; + const avatarId = userInfo.avatarId || ''; + this.imgId = avatarId; + + if (avatarId) { + try { + uni.showLoading({ title: '加载头像中...' }); + const url = `${config.baseUrl}/api/fileRelevant/getImg?imgType=5&imgId=${avatarId}`; + this.avatarUrl = await this.urlToBase64(url); + } catch (err) { + console.error('头像加载失败', err); + this.avatarUrl = ''; + } finally { + uni.hideLoading(); + } + } else { + this.avatarUrl = ''; + } }, - // 选择微信头像 + // 选择头像 + 上传 + 转base64预览 async onChooseAvatar(e) { const tempFilePath = e.detail.avatarUrl; uni.showLoading({ title: '上传中...' }); try { - // 1. 上传头像到服务器 + // 上传头像 const uploadRes = await new Promise((resolve, reject) => { uni.uploadFile({ url: config.baseUrl + '/api/fileRelevant/uploadWxAvatarImg', @@ -69,78 +85,85 @@ export default { name: 'file', success: resolve, fail: reject - }) + }); }); const resData = JSON.parse(uploadRes.data); const imgId = resData.data; + this.imgId = imgId; - if (!imgId) { - uni.showToast({ title: '头像上传失败', icon: 'none' }); - return; - } - - // 拼接头像地址 - const avatarUrl = config.baseUrl + '/api/fileRelevant/getImg?imgType=5&imgId=' + imgId; - this.avatarUrl = avatarUrl; - - uni.showToast({ title: '头像选择成功,点击保存生效', icon: 'success' }); + // 转base64显示 + const url = `${config.baseUrl}/api/fileRelevant/getImg?imgType=5&imgId=${imgId}`; + this.avatarUrl = await this.urlToBase64(url); + uni.showToast({ title: '头像上传成功', icon: 'success' }); } catch (err) { - console.error('头像上传失败:', err); - uni.showToast({ title: '头像更新失败', icon: 'none' }); + console.error(err); + uni.showToast({ title: '头像上传失败', icon: 'none' }); } finally { uni.hideLoading(); } }, - // 保存用户信息(头像 + 昵称) + // 最简稳定版:图片链接转base64 + urlToBase64(url) { + return new Promise((resolve, reject) => { + uni.request({ + url, + method: 'GET', + responseType: 'arraybuffer', + success: (res) => { + try { + const base64 = uni.arrayBufferToBase64(res.data); + resolve(`data:image/jpeg;base64,${base64}`); + } catch (e) { + reject(e); + } + }, + fail: reject + }); + }); + }, + + // 保存(防重复提交 + 同步缓存) async saveUserInfo() { - // 校验昵称 - if (!this.nickName.trim()) { + if (this.saving) return; + if (!this.nickname.trim()) { uni.showToast({ title: '请输入昵称', icon: 'none' }); return; } + this.saving = true; uni.showLoading({ title: '保存中...' }); try { const openId = uni.getStorageSync('wx_login_code') || ''; const params = { - avatar: this.avatarUrl, + avatar: this.imgId, libcode: "1201", - nickname: this.nickName, + nickname: this.nickname, openid: openId }; - // 调用保存接口 await FetchBindRead(params); - // 更新缓存 - const userInfo = { - nickName: this.nickName, - avatarUrl: this.avatarUrl - }; - uni.setStorageSync(USER_KEY, userInfo); - - uni.showToast({ - title: '保存成功', - icon: 'success' + // 保存到缓存 + uni.setStorageSync(USER_KEY, { + nickname: this.nickname, + avatarId: this.imgId }); - // 返回上一页 + uni.showToast({ title: '保存成功', icon: 'success' }); setTimeout(() => { uni.navigateBack(); }, 1500); } catch (error) { - console.error('保存失败:', error); - uni.showToast({ - title: '保存失败,请重试', - icon: 'none' - }); + console.error(error); + uni.showToast({ title: '保存失败', icon: 'none' }); } finally { uni.hideLoading(); + this.saving = false; } } } @@ -155,7 +178,6 @@ export default { box-sizing: border-box; } -/* 头像区域 */ .user-info-section { background: #fff; padding: 40rpx 0; @@ -190,7 +212,6 @@ export default { color: #999; } -/* 表单项 */ .form-item { background-color: #fff; display: flex; @@ -215,7 +236,6 @@ export default { text-align: right; } -/* 保存按钮 */ .save-box { margin-top: 60rpx; padding: 0 20rpx; diff --git a/uni_modules/w-qrcode/changelog.md b/uni_modules/w-qrcode/changelog.md new file mode 100644 index 0000000..4037294 --- /dev/null +++ b/uni_modules/w-qrcode/changelog.md @@ -0,0 +1,16 @@ +## 1.0.1(2025-12-30) +## 1.0.1(2025-12-30) +- 优化二维码生成逻辑 +## 1.0.0(2025-12-29) +- 初始版本发布 +- 纯 JavaScript 实现二维码生成算法 +- 支持 Vue2 和 Vue3 +- 支持自定义前景色、背景色 +- 支持添加 Logo 图片 +- 支持多种纠错级别(L/M/Q/H) +- 支持保存到相册 +- 支持导出临时文件和 Base64 +- 兼容全平台:H5、App、各类小程序 + +## 1.0.1(2025-12-30) +- 优化二维码生成逻辑 \ No newline at end of file diff --git a/uni_modules/w-qrcode/components/w-qrcode/w-qrcode.vue b/uni_modules/w-qrcode/components/w-qrcode/w-qrcode.vue new file mode 100644 index 0000000..c73bd33 --- /dev/null +++ b/uni_modules/w-qrcode/components/w-qrcode/w-qrcode.vue @@ -0,0 +1,732 @@ + + + + + diff --git a/uni_modules/w-qrcode/js_sdk/qrcode.js b/uni_modules/w-qrcode/js_sdk/qrcode.js new file mode 100644 index 0000000..9dcc9ae --- /dev/null +++ b/uni_modules/w-qrcode/js_sdk/qrcode.js @@ -0,0 +1,619 @@ +/** + * QRCode Generator - 纯JavaScript实现 + * 基于 ISO/IEC 18004 标准 + * @version 2.0.0 + */ + +// 纠错级别 - 使用标准索引 +const ECL = { L: 0, M: 1, Q: 2, H: 3 }; + +// 模式指示符 +const MODE = { + Numeric: 0b0001, + Alphanumeric: 0b0010, + Byte: 0b0100, + Kanji: 0b1000 +}; + +// 字母数字字符映射 +const ALPHANUM = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'; + +// 各版本各纠错级别的数据容量(字节模式) +const CAPACITIES = [ + [17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44], + [134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119], + [321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220], + [586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382], + [929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535], + [1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742], + [1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983], + [2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273] +]; + +// RS块信息表 [总码字, [各纠错级别的纠错码字数], [各纠错级别的块数(组1,组2)]] +const RS_BLOCKS = [ + [[26,[7,10,13,17],[[1,0],[1,0],[1,0],[1,0]]]], + [[44,[10,16,22,28],[[1,0],[1,0],[1,0],[1,0]]]], + [[70,[15,26,18,22],[[1,0],[1,0],[2,0],[2,0]]]], + [[100,[20,18,26,16],[[1,0],[2,0],[2,0],[4,0]]]], + [[134,[26,24,18,22],[[1,0],[2,0],[4,0],[4,0]]]], + [[172,[18,16,24,28],[[2,0],[4,0],[4,0],[4,0]]]], + [[196,[20,18,18,26],[[2,0],[4,0],[6,0],[5,0]]]], + [[242,[24,22,22,26],[[2,0],[4,0],[6,0],[6,0]]]], + [[292,[30,22,20,24],[[2,0],[5,0],[8,0],[8,0]]]], + [[346,[18,26,24,28],[[4,0],[5,0],[8,0],[8,0]]]], + [[404,[20,30,28,24],[[4,0],[5,0],[8,0],[11,0]]]], + [[466,[24,22,26,28],[[4,0],[8,0],[10,0],[11,0]]]], + [[532,[26,22,24,22],[[4,0],[9,0],[12,0],[16,0]]]], + [[581,[30,24,20,24],[[4,0],[9,0],[16,0],[16,0]]]], + [[655,[22,24,30,24],[[6,0],[10,0],[12,0],[18,0]]]], + [[733,[24,28,24,30],[[6,0],[10,0],[17,0],[16,0]]]], + [[815,[28,28,28,28],[[6,0],[11,0],[16,0],[19,0]]]], + [[901,[30,26,28,28],[[6,0],[13,0],[18,0],[21,0]]]], + [[991,[28,26,26,26],[[7,0],[14,0],[21,0],[25,0]]]], + [[1085,[28,26,30,28],[[8,0],[16,0],[20,0],[25,0]]]], + [[1156,[28,26,28,30],[[8,0],[17,0],[23,0],[25,0]]]], + [[1258,[28,28,30,24],[[9,0],[17,0],[23,0],[34,0]]]], + [[1364,[30,28,30,30],[[9,0],[18,0],[25,0],[30,0]]]], + [[1474,[30,28,30,30],[[10,0],[20,0],[27,0],[32,0]]]], + [[1588,[26,28,30,30],[[12,0],[21,0],[29,0],[35,0]]]], + [[1706,[28,28,28,30],[[12,0],[23,0],[34,0],[37,0]]]], + [[1828,[30,28,30,30],[[12,0],[25,0],[34,0],[40,0]]]], + [[1921,[30,28,30,30],[[13,0],[26,0],[35,0],[42,0]]]], + [[2051,[30,28,30,30],[[14,0],[28,0],[38,0],[45,0]]]], + [[2185,[30,28,30,30],[[15,0],[29,0],[40,0],[48,0]]]], + [[2323,[30,28,30,30],[[16,0],[31,0],[43,0],[51,0]]]], + [[2465,[30,28,30,30],[[17,0],[33,0],[45,0],[54,0]]]], + [[2611,[30,28,30,30],[[18,0],[35,0],[48,0],[57,0]]]], + [[2761,[30,28,30,30],[[19,0],[37,0],[51,0],[60,0]]]], + [[2876,[30,28,30,30],[[19,0],[38,0],[53,0],[63,0]]]], + [[3034,[30,28,30,30],[[20,0],[40,0],[56,0],[66,0]]]], + [[3196,[30,28,30,30],[[21,0],[43,0],[59,0],[70,0]]]], + [[3362,[30,28,30,30],[[22,0],[45,0],[62,0],[74,0]]]], + [[3532,[30,28,30,30],[[24,0],[47,0],[65,0],[77,0]]]], + [[3706,[30,28,30,30],[[25,0],[49,0],[68,0],[81,0]]]] +]; + +// 更完整的RS块信息 +const EC_PARAMS = [ + // [纠错码字每块, 数据码字每块组1, 块数组1, 数据码字每块组2, 块数组2] + // L, M, Q, H for each version + [[7,19,1,0,0],[10,16,1,0,0],[13,13,1,0,0],[17,9,1,0,0]], // v1 + [[10,34,1,0,0],[16,28,1,0,0],[22,22,1,0,0],[28,16,1,0,0]], // v2 + [[15,55,1,0,0],[26,44,1,0,0],[18,17,2,0,0],[22,13,2,0,0]], // v3 + [[20,80,1,0,0],[18,32,2,0,0],[26,24,2,0,0],[16,9,4,0,0]], // v4 + [[26,108,1,0,0],[24,43,2,0,0],[18,15,2,16,2],[22,11,2,12,2]], // v5 + [[18,68,2,0,0],[16,27,4,0,0],[24,19,4,0,0],[28,15,4,0,0]], // v6 + [[20,78,2,0,0],[18,31,4,0,0],[18,14,2,15,4],[26,13,4,14,1]], // v7 + [[24,97,2,0,0],[22,38,2,39,2],[22,18,4,19,2],[26,14,4,15,2]], // v8 + [[30,116,2,0,0],[22,36,3,37,2],[20,16,4,17,4],[24,12,4,13,4]], // v9 + [[18,68,2,69,2],[26,43,4,44,1],[24,19,6,20,2],[28,15,6,16,2]], // v10 + [[20,81,4,0,0],[30,50,1,51,4],[28,22,4,23,4],[24,12,3,13,8]], // v11 + [[24,92,2,93,2],[22,36,6,37,2],[26,20,4,21,6],[28,14,7,15,4]], // v12 + [[26,107,4,0,0],[22,37,8,38,1],[24,20,8,21,4],[22,11,12,12,4]], // v13 + [[30,115,3,116,1],[24,40,4,41,5],[20,16,11,17,5],[24,12,11,13,5]], // v14 + [[22,87,5,88,1],[24,41,5,42,5],[30,24,5,25,7],[24,12,11,13,7]], // v15 + [[24,98,5,99,1],[28,45,7,46,3],[24,19,15,20,2],[30,15,3,16,13]], // v16 + [[28,107,1,108,5],[28,46,10,47,1],[28,22,1,23,15],[28,14,2,15,17]], // v17 + [[30,120,5,121,1],[26,43,9,44,4],[28,22,17,23,1],[28,14,2,15,19]], // v18 + [[28,113,3,114,4],[26,44,3,45,11],[26,21,17,22,4],[26,13,9,14,16]], // v19 + [[28,107,3,108,5],[26,41,3,42,13],[30,24,15,25,5],[28,15,15,16,10]], // v20 + [[28,116,4,117,4],[26,42,17,0,0],[28,22,17,23,6],[30,16,19,17,6]], // v21 + [[28,111,2,112,7],[28,46,17,0,0],[30,24,7,25,16],[24,13,34,0,0]], // v22 + [[30,121,4,122,5],[28,47,4,48,14],[30,24,11,25,14],[30,15,16,16,14]], // v23 + [[30,117,6,118,4],[28,45,6,46,14],[30,24,11,25,16],[30,16,30,17,2]], // v24 + [[26,106,8,107,4],[28,47,8,48,13],[30,24,7,25,22],[30,15,22,16,13]], // v25 + [[28,114,10,115,2],[28,46,19,47,4],[28,22,28,23,6],[30,16,33,17,4]], // v26 + [[30,122,8,123,4],[28,45,22,46,3],[30,23,8,24,26],[30,15,12,16,28]], // v27 + [[30,117,3,118,10],[28,45,3,46,23],[30,24,4,25,31],[30,15,11,16,31]], // v28 + [[30,116,7,117,7],[28,45,21,46,7],[30,23,1,24,37],[30,15,19,16,26]], // v29 + [[30,115,5,116,10],[28,47,19,48,10],[30,24,15,25,25],[30,15,23,16,25]], // v30 + [[30,115,13,116,3],[28,46,2,47,29],[30,24,42,25,1],[30,15,23,16,28]], // v31 + [[30,115,17,0,0],[28,46,10,47,23],[30,24,10,25,35],[30,15,19,16,35]], // v32 + [[30,115,17,116,1],[28,46,14,47,21],[30,24,29,25,19],[30,15,11,16,46]], // v33 + [[30,115,13,116,6],[28,46,14,47,23],[30,24,44,25,7],[30,16,59,17,1]], // v34 + [[30,121,12,122,7],[28,47,12,48,26],[30,24,39,25,14],[30,15,22,16,41]], // v35 + [[30,121,6,122,14],[28,47,6,48,34],[30,24,46,25,10],[30,15,2,16,64]], // v36 + [[30,122,17,123,4],[28,46,29,47,14],[30,24,49,25,10],[30,15,24,16,46]], // v37 + [[30,122,4,123,18],[28,46,13,47,32],[30,24,48,25,14],[30,15,42,16,32]], // v38 + [[30,117,20,118,4],[28,47,40,48,7],[30,24,43,25,22],[30,15,10,16,67]], // v39 + [[30,118,19,119,6],[28,47,18,48,31],[30,24,34,25,34],[30,15,20,16,61]] // v40 +]; + +// 对齐图案位置 +const ALIGN_POS = [ + [], [6,18], [6,22], [6,26], [6,30], [6,34], + [6,22,38], [6,24,42], [6,26,46], [6,28,50], [6,30,54], [6,32,58], [6,34,62], + [6,26,46,66], [6,26,48,70], [6,26,50,74], [6,30,54,78], [6,30,56,82], [6,30,58,86], [6,34,62,90], + [6,28,50,72,94], [6,26,50,74,98], [6,30,54,78,102], [6,28,54,80,106], [6,32,58,84,110], + [6,30,58,86,114], [6,34,62,90,118], [6,26,50,74,98,122], [6,30,54,78,102,126], + [6,26,52,78,104,130], [6,30,56,82,108,134], [6,34,60,86,112,138], [6,30,58,86,114,142], + [6,34,62,90,118,146], [6,30,54,78,102,126,150], [6,24,50,76,102,128,154], + [6,28,54,80,106,132,158], [6,32,58,84,110,136,162], [6,26,54,82,110,138,166], [6,30,58,86,114,142,170] +]; + +// 格式信息预计算 (ecl*8+mask) -> formatBits +const FORMAT_BITS = [ + 0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976, + 0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0, + 0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed, + 0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b +]; + +// 版本信息 (版本7+) +const VERSION_BITS = [ + 0x07c94, 0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847, 0x0e60d, + 0x0f928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6, 0x15683, 0x168c9, + 0x177ec, 0x18ec4, 0x191e1, 0x1afab, 0x1b08e, 0x1cc1a, 0x1d33f, 0x1ed75, + 0x1f250, 0x209d5, 0x216f0, 0x228ba, 0x2379f, 0x24b0b, 0x2542e, 0x26a64, + 0x27541, 0x28c69 +]; + +// GF(2^8) 运算表 +const EXP = new Uint8Array(512); +const LOG = new Uint8Array(256); +(() => { + let x = 1; + for (let i = 0; i < 255; i++) { + EXP[i] = x; + LOG[x] = i; + x = (x << 1) ^ (x >= 128 ? 0x11d : 0); + } + for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255]; +})(); + +// RS编码 +function rsEncode(data, ecLen) { + const gen = new Uint8Array(ecLen + 1); + gen[0] = 1; + for (let i = 0; i < ecLen; i++) { + for (let j = i + 1; j >= 1; j--) { + gen[j] = gen[j] ? EXP[LOG[gen[j]] + i] ^ gen[j-1] : gen[j-1]; + } + gen[0] = EXP[LOG[gen[0]] + i]; + } + + const result = new Uint8Array(ecLen); + for (let i = 0; i < data.length; i++) { + const coef = data[i] ^ result[0]; + result.copyWithin(0, 1); + result[ecLen - 1] = 0; + if (coef) { + for (let j = 0; j < ecLen; j++) { + result[j] ^= EXP[LOG[gen[ecLen - 1 - j]] + LOG[coef]]; + } + } + } + return result; +} + +// 获取数据模式 +function getMode(text) { + if (/^\d+$/.test(text)) return MODE.Numeric; + if (/^[0-9A-Z $%*+\-./:]+$/.test(text)) return MODE.Alphanumeric; + return MODE.Byte; +} + +// 获取字符计数位数 +function getCharCountBits(ver, mode) { + const idx = ver < 10 ? 0 : ver < 27 ? 1 : 2; + return [[10,9,8,8],[12,11,16,10],[14,13,16,12]][idx][[MODE.Numeric,MODE.Alphanumeric,MODE.Byte,MODE.Kanji].indexOf(mode)]; +} + +// UTF-8编码 +function toUtf8(str) { + const bytes = []; + for (let i = 0; i < str.length; i++) { + let c = str.charCodeAt(i); + if (c < 0x80) { + bytes.push(c); + } else if (c < 0x800) { + bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); + } else if (c >= 0xd800 && c < 0xdc00 && i + 1 < str.length) { + const c2 = str.charCodeAt(++i); + c = 0x10000 + ((c & 0x3ff) << 10) + (c2 & 0x3ff); + bytes.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); + } else { + bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); + } + } + return bytes; +} + +// 获取最小版本 +function getMinVersion(text, ecl) { + const mode = getMode(text); + const len = mode === MODE.Byte ? toUtf8(text).length : text.length; + for (let v = 1; v <= 40; v++) { + if (len <= CAPACITIES[v-1][ecl]) return v; + } + return -1; +} + +// 编码数据 +function encodeData(text, ver, ecl) { + const mode = getMode(text); + const bits = []; + + // 写入位 + const write = (val, len) => { + for (let i = len - 1; i >= 0; i--) bits.push((val >> i) & 1); + }; + + // 模式指示符 + write(mode, 4); + + // 字符计数 + const utf8 = mode === MODE.Byte ? toUtf8(text) : null; + const charCount = utf8 ? utf8.length : text.length; + write(charCount, getCharCountBits(ver, mode)); + + // 数据编码 + if (mode === MODE.Numeric) { + for (let i = 0; i < text.length; i += 3) { + const chunk = text.substr(i, 3); + write(parseInt(chunk, 10), chunk.length * 3 + 1); + } + } else if (mode === MODE.Alphanumeric) { + for (let i = 0; i < text.length; i += 2) { + if (i + 1 < text.length) { + write(ALPHANUM.indexOf(text[i]) * 45 + ALPHANUM.indexOf(text[i+1]), 11); + } else { + write(ALPHANUM.indexOf(text[i]), 6); + } + } + } else { + for (const b of utf8) write(b, 8); + } + + // 获取数据容量 + const params = EC_PARAMS[ver - 1][ecl]; + const [ecPerBlock, dc1, bc1, dc2, bc2] = params; + const totalDC = dc1 * bc1 + dc2 * bc2; + const capacity = totalDC * 8; + + // 终止符 + const termLen = Math.min(4, capacity - bits.length); + for (let i = 0; i < termLen; i++) bits.push(0); + + // 对齐到字节 + while (bits.length % 8) bits.push(0); + + // 填充 + const pads = [0xec, 0x11]; + let padIdx = 0; + while (bits.length < capacity) { + write(pads[padIdx++ % 2], 8); + } + + // 转换为字节 + const bytes = []; + for (let i = 0; i < bits.length; i += 8) { + let b = 0; + for (let j = 0; j < 8; j++) b = (b << 1) | bits[i + j]; + bytes.push(b); + } + + // 分块并添加纠错码 + const blocks = []; + const ecBlocks = []; + let offset = 0; + + for (let i = 0; i < bc1; i++) { + const block = bytes.slice(offset, offset + dc1); + blocks.push(block); + ecBlocks.push(rsEncode(new Uint8Array(block), ecPerBlock)); + offset += dc1; + } + for (let i = 0; i < bc2; i++) { + const block = bytes.slice(offset, offset + dc2); + blocks.push(block); + ecBlocks.push(rsEncode(new Uint8Array(block), ecPerBlock)); + offset += dc2; + } + + // 交织 + const result = []; + const maxDC = Math.max(dc1, dc2); + for (let i = 0; i < maxDC; i++) { + for (const block of blocks) { + if (i < block.length) result.push(block[i]); + } + } + for (let i = 0; i < ecPerBlock; i++) { + for (const ec of ecBlocks) { + result.push(ec[i]); + } + } + + return result; +} + +// 创建二维码矩阵 +function createMatrix(ver) { + const size = ver * 4 + 17; + const matrix = []; + const reserved = []; + for (let i = 0; i < size; i++) { + matrix.push(new Array(size).fill(0)); + reserved.push(new Array(size).fill(false)); + } + + // 标记保留区域 + const mark = (r, c) => { + if (r >= 0 && r < size && c >= 0 && c < size) reserved[r][c] = true; + }; + + // 查找图案 + const placeFinder = (r, c) => { + for (let dr = -1; dr <= 7; dr++) { + for (let dc = -1; dc <= 7; dc++) { + const nr = r + dr, nc = c + dc; + if (nr < 0 || nr >= size || nc < 0 || nc >= size) continue; + mark(nr, nc); + if (dr >= 0 && dr <= 6 && dc >= 0 && dc <= 6) { + const isBlack = dr === 0 || dr === 6 || dc === 0 || dc === 6 || + (dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4); + matrix[nr][nc] = isBlack ? 1 : 0; + } else { + matrix[nr][nc] = 0; + } + } + } + }; + + placeFinder(0, 0); + placeFinder(0, size - 7); + placeFinder(size - 7, 0); + + // 对齐图案 + if (ver >= 2) { + const positions = ALIGN_POS[ver - 1]; + for (const r of positions) { + for (const c of positions) { + if (reserved[r][c]) continue; + for (let dr = -2; dr <= 2; dr++) { + for (let dc = -2; dc <= 2; dc++) { + mark(r + dr, c + dc); + const isBlack = Math.abs(dr) === 2 || Math.abs(dc) === 2 || (dr === 0 && dc === 0); + matrix[r + dr][c + dc] = isBlack ? 1 : 0; + } + } + } + } + } + + // 时序图案 + for (let i = 8; i < size - 8; i++) { + const v = i % 2 === 0 ? 1 : 0; + if (!reserved[6][i]) { matrix[6][i] = v; mark(6, i); } + if (!reserved[i][6]) { matrix[i][6] = v; mark(i, 6); } + } + + // 暗模块 + matrix[size - 8][8] = 1; + mark(size - 8, 8); + + // 格式信息区域 + for (let i = 0; i < 9; i++) { mark(8, i); mark(i, 8); } + for (let i = 0; i < 8; i++) { mark(8, size - 1 - i); mark(size - 1 - i, 8); } + + // 版本信息区域 + if (ver >= 7) { + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 3; j++) { + mark(i, size - 11 + j); + mark(size - 11 + j, i); + } + } + } + + return { matrix, reserved, size }; +} + +// 放置数据 +function placeData(matrix, reserved, data) { + const size = matrix.length; + let bitIdx = 0; + let upward = true; + + for (let col = size - 1; col >= 1; col -= 2) { + if (col === 6) col = 5; + + for (let i = 0; i < size; i++) { + const row = upward ? size - 1 - i : i; + + for (let dc = 0; dc < 2; dc++) { + const c = col - dc; + if (!reserved[row][c]) { + const bit = bitIdx < data.length * 8 + ? (data[Math.floor(bitIdx / 8)] >> (7 - bitIdx % 8)) & 1 + : 0; + matrix[row][c] = bit; + bitIdx++; + } + } + } + upward = !upward; + } +} + +// 应用掩码 +function applyMask(matrix, reserved, mask) { + const size = matrix.length; + const result = matrix.map(row => [...row]); + + const masks = [ + (r, c) => (r + c) % 2 === 0, + (r, c) => r % 2 === 0, + (r, c) => c % 3 === 0, + (r, c) => (r + c) % 3 === 0, + (r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0, + (r, c) => (r * c) % 2 + (r * c) % 3 === 0, + (r, c) => ((r * c) % 2 + (r * c) % 3) % 2 === 0, + (r, c) => ((r + c) % 2 + (r * c) % 3) % 2 === 0 + ]; + + for (let r = 0; r < size; r++) { + for (let c = 0; c < size; c++) { + if (!reserved[r][c] && masks[mask](r, c)) { + result[r][c] ^= 1; + } + } + } + + return result; +} + +// 放置格式信息 +function placeFormatInfo(matrix, ecl, mask) { + const size = matrix.length; + const bits = FORMAT_BITS[ecl * 8 + mask]; + + // 左上水平 + for (let i = 0; i <= 5; i++) matrix[8][i] = (bits >> (14 - i)) & 1; + matrix[8][7] = (bits >> 8) & 1; + matrix[8][8] = (bits >> 7) & 1; + matrix[7][8] = (bits >> 6) & 1; + for (let i = 0; i <= 5; i++) matrix[i][8] = (bits >> i) & 1; + + // 右上和左下 + for (let i = 0; i <= 7; i++) matrix[8][size - 1 - i] = (bits >> i) & 1; + for (let i = 0; i <= 6; i++) matrix[size - 1 - i][8] = (bits >> (14 - i)) & 1; +} + +// 放置版本信息 +function placeVersionInfo(matrix, ver) { + if (ver < 7) return; + const size = matrix.length; + const bits = VERSION_BITS[ver - 7]; + + for (let i = 0; i < 6; i++) { + for (let j = 0; j < 3; j++) { + const bit = (bits >> (i * 3 + j)) & 1; + matrix[i][size - 11 + j] = bit; + matrix[size - 11 + j][i] = bit; + } + } +} + +// 计算惩罚分数 +function calcPenalty(matrix) { + const size = matrix.length; + let penalty = 0; + + // 规则1: 连续同色 + for (let r = 0; r < size; r++) { + let cnt = 1; + for (let c = 1; c < size; c++) { + if (matrix[r][c] === matrix[r][c-1]) cnt++; + else { if (cnt >= 5) penalty += cnt - 2; cnt = 1; } + } + if (cnt >= 5) penalty += cnt - 2; + } + for (let c = 0; c < size; c++) { + let cnt = 1; + for (let r = 1; r < size; r++) { + if (matrix[r][c] === matrix[r-1][c]) cnt++; + else { if (cnt >= 5) penalty += cnt - 2; cnt = 1; } + } + if (cnt >= 5) penalty += cnt - 2; + } + + // 规则2: 2x2块 + for (let r = 0; r < size - 1; r++) { + for (let c = 0; c < size - 1; c++) { + const v = matrix[r][c]; + if (v === matrix[r][c+1] && v === matrix[r+1][c] && v === matrix[r+1][c+1]) { + penalty += 3; + } + } + } + + // 规则3: 特定图案 + const p1 = [1,0,1,1,1,0,1,0,0,0,0]; + const p2 = [0,0,0,0,1,0,1,1,1,0,1]; + for (let r = 0; r < size; r++) { + for (let c = 0; c <= size - 11; c++) { + let m1 = true, m2 = true; + for (let i = 0; i < 11; i++) { + if (matrix[r][c+i] !== p1[i]) m1 = false; + if (matrix[r][c+i] !== p2[i]) m2 = false; + } + if (m1 || m2) penalty += 40; + } + } + for (let c = 0; c < size; c++) { + for (let r = 0; r <= size - 11; r++) { + let m1 = true, m2 = true; + for (let i = 0; i < 11; i++) { + if (matrix[r+i][c] !== p1[i]) m1 = false; + if (matrix[r+i][c] !== p2[i]) m2 = false; + } + if (m1 || m2) penalty += 40; + } + } + + // 规则4: 黑白比例 + let dark = 0; + for (let r = 0; r < size; r++) { + for (let c = 0; c < size; c++) { + if (matrix[r][c]) dark++; + } + } + const ratio = dark / (size * size); + penalty += Math.floor(Math.abs(ratio - 0.5) / 0.05) * 10; + + return penalty; +} + +// 选择最佳掩码 +function selectMask(matrix, reserved, ecl, ver) { + let bestMask = 0; + let bestPenalty = Infinity; + + for (let mask = 0; mask < 8; mask++) { + const masked = applyMask(matrix, reserved, mask); + placeFormatInfo(masked, ecl, mask); + placeVersionInfo(masked, ver); + const p = calcPenalty(masked); + if (p < bestPenalty) { + bestPenalty = p; + bestMask = mask; + } + } + + return bestMask; +} + +/** + * 生成二维码 + */ +function generate(text, options = {}) { + const eclName = (options.errorCorrectionLevel || 'M').toUpperCase(); + const ecl = ECL[eclName] !== undefined ? ECL[eclName] : ECL.M; + + let ver = options.version || getMinVersion(text, ecl); + if (ver < 1) throw new Error('数据过长'); + if (ver > 40) ver = 40; + + const data = encodeData(text, ver, ecl); + const { matrix, reserved, size } = createMatrix(ver); + + placeData(matrix, reserved, data); + + const mask = selectMask(matrix, reserved, ecl, ver); + const final = applyMask(matrix, reserved, mask); + placeFormatInfo(final, ecl, mask); + placeVersionInfo(final, ver); + + return { + version: ver, + size, + modules: final, + errorCorrectionLevel: eclName + }; +} + +export default { generate, ECL, MODE }; +export { generate, ECL, MODE }; diff --git a/uni_modules/w-qrcode/package.json b/uni_modules/w-qrcode/package.json new file mode 100644 index 0000000..69bc576 --- /dev/null +++ b/uni_modules/w-qrcode/package.json @@ -0,0 +1,104 @@ +{ + "id": "w-qrcode", + "displayName": "w-qrcode 二维码生成器", + "version": "1.0.1", + "description": "高性能二维码生成组件,纯JavaScript实现,支持自定义样式、Logo、多种纠错级别,兼容Vue2/Vue3,支持全平台", + "keywords": [ + "qrcode", + "二维码", + "qr-code", + "vue2", + "vue3" +], + "repository": "", + "engines": { + "HBuilderX": "^3.1.0", + "uni-app": "^3.6.15", + "uni-app-x": "" + }, + "dcloudext": { + "type": "component-vue", + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "", + "darkmode": "x", + "i18n": "x", + "widescreen": "x" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "√", + "aliyun": "√", + "alipay": "x" + }, + "client": { + "uni-app": { + "vue": { + "vue2": { + }, + "vue3": { + } + }, + "web": { + "safari": "√", + "chrome": "√" + }, + "app": { + "vue": "√", + "nvue": "√", + "android": "-", + "ios": "-", + "harmony": "-" + }, + "mp": { + "weixin": "√", + "alipay": "-", + "toutiao": "-", + "baidu": "-", + "kuaishou": "-", + "jd": "-", + "harmony": "-", + "qq": "-", + "lark": "-" + }, + "quickapp": { + "huawei": "-", + "union": "-" + } + }, + "uni-app-x": { + "web": { + "safari": "-", + "chrome": "-" + }, + "app": { + "android": "-", + "ios": "-", + "harmony": "-" + }, + "mp": { + "weixin": "-" + } + } + } + } + }, + "main": "js_sdk/qrcode.js" +} \ No newline at end of file diff --git a/uni_modules/w-qrcode/readme.md b/uni_modules/w-qrcode/readme.md new file mode 100644 index 0000000..49650c2 --- /dev/null +++ b/uni_modules/w-qrcode/readme.md @@ -0,0 +1,229 @@ +# w-qrcode 二维码生成器 + +高性能二维码生成组件,纯 JavaScript 实现,无外部依赖。 + +## 特性 + +- 纯 JavaScript 实现,无需任何外部依赖 +- 支持 Vue2 和 Vue3 +- 支持全平台:H5、App(Vue/Nvue)、微信/支付宝/百度/字节跳动/QQ 等小程序 +- 支持自定义前景色、背景色 +- 支持添加 Logo 图片 +- 支持多种纠错级别(L/M/Q/H) +- 支持保存到相册 +- 支持导出为图片 + +## 安装 + +将 `w-qrcode` 目录复制到项目的 `uni_modules` 目录下即可。 + +## 基本使用 + +```vue + +``` + +## 完整示例 + +```vue + + + +``` + +## Vue3 组合式 API 示例 + +```vue + + + +``` + +## Props 属性 + +| 属性名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| value | String | '' | 要编码的文本内容 | +| size | Number/String | 200 | 二维码尺寸(单位:px) | +| foreground | String | '#000000' | 前景色(二维码颜色) | +| background | String | '#FFFFFF' | 背景色 | +| errorCorrectionLevel | String | 'M' | 纠错级别,可选值:L/M/Q/H | +| margin | Number/String | 2 | 二维码边距(单位:模块) | +| logo | String | '' | Logo 图片路径 | +| logoSize | Number/String | 50 | Logo 尺寸(单位:px) | +| logoMargin | Number/String | 5 | Logo 外边距(单位:px) | +| logoRadius | Number/String | 8 | Logo 圆角(单位:px) | +| logoBackground | String | '#FFFFFF' | Logo 背景色 | +| canvas2d | Boolean | true | 是否使用 Canvas 2D(仅小程序有效) | + +## 纠错级别说明 + +| 级别 | 纠错能力 | 说明 | +|------|----------|------| +| L | 约 7% | 数据恢复能力最低 | +| M | 约 15% | 默认级别,平衡 | +| Q | 约 25% | 较高的数据恢复能力 | +| H | 约 30% | 最高级别,适合添加 Logo | + +> 注意:如果需要在二维码中添加 Logo,建议使用 `H` 级别,以保证扫描成功率。 + +## Events 事件 + +| 事件名 | 说明 | 回调参数 | +|--------|------|----------| +| generated | 二维码生成成功时触发 | { version, size, errorCorrectionLevel } | +| error | 生成失败时触发 | Error 对象 | +| longpress | 长按二维码时触发 | Event 对象 | + +## Methods 方法 + +通过 ref 获取组件实例后调用: + +| 方法名 | 说明 | 参数 | 返回值 | +|--------|------|------|--------| +| saveToAlbum | 保存到相册 | - | Promise | +| toTempFilePath | 获取临时文件路径 | options | Promise<{tempFilePath}> | +| toDataURL | 获取 Base64(仅 H5) | type, quality | Promise | +| generate | 重新生成二维码 | - | - | + +### toTempFilePath 参数 + +```javascript +this.$refs.qrcodeRef.toTempFilePath({ + fileType: 'png', // 'jpg' | 'png' + quality: 1, // 0-1,jpg 时有效 + destWidth: 300, // 输出图片宽度 + destHeight: 300 // 输出图片高度 +}); +``` + +## 直接使用 JS SDK + +如果只需要生成二维码数据,可以直接使用 JS SDK: + +```javascript +import QRCode from '@/uni_modules/w-qrcode/js_sdk/qrcode.js'; + +// 生成二维码数据 +const result = QRCode.generate('https://example.com', { + errorCorrectionLevel: 'M' // L/M/Q/H +}); + +console.log(result); +// { +// version: 2, // 版本号 1-40 +// size: 25, // 矩阵尺寸 +// modules: [[...], ...], // 二维码矩阵,1表示黑色,0表示白色 +// errorCorrectionLevel: 'M' +// } +``` + +## 平台差异 + +| 平台 | 渲染方式 | 说明 | +|------|----------|------| +| H5 | Canvas | 完全支持 | +| App-Vue | Canvas | 完全支持 | +| App-Nvue | View 布局 | 使用 Fallback 渲染 | +| 微信小程序 | Canvas 2D | 推荐使用 Canvas 2D | +| 其他小程序 | Canvas | 使用旧版 Canvas API | + +## 常见问题 + +### 1. 二维码扫描不出来? + +- 检查内容是否正确 +- 增加纠错级别(使用 'H') +- 减小 Logo 尺寸(不超过二维码的 30%) +- 增加二维码尺寸 + +### 2. 保存到相册失败? + +- 小程序需要用户授权 +- App 需要配置相册权限 +- 检查图片路径是否正确 + +### 3. Logo 不显示? + +- 确保图片路径正确 +- 本地图片使用绝对路径 +- 网络图片需要在小程序后台配置域名白名单 + +## 更新日志 + +### 1.0.0 + +- 初始版本发布 +- 支持基础二维码生成 +- 支持自定义样式 +- 支持 Logo 图片 +- 支持多平台