Browse Source

优化

master
xuhuajiao 6 months ago
parent
commit
1ae949b55e
  1. 8
      api/book.js
  2. 28
      components/book-list-item/book-list-item.vue
  3. 216
      pages/home/home.vue
  4. 13
      pages/lendCar/lendCar.vue
  5. 41
      pages/login/login.vue
  6. 17
      pages/search/search.vue
  7. 121
      pages/user/user.vue
  8. 4
      static/iconfont.css
  9. BIN
      static/iconfont.ttf
  10. BIN
      static/images/activity1.png
  11. BIN
      static/images/activity2.png
  12. BIN
      static/images/avatar.png
  13. BIN
      static/images/card-img1.png
  14. BIN
      static/images/card-img2.png
  15. BIN
      static/images/default-avatar.png
  16. BIN
      static/images/default-book.png
  17. BIN
      static/images/logo.jpg
  18. BIN
      static/images/menu-jy.png
  19. BIN
      static/images/menu-sc.png
  20. BIN
      static/images/mingqi-beij@2x.png
  21. BIN
      static/images/mylending-img1.png
  22. BIN
      static/images/mylending-img2.png
  23. BIN
      static/images/mylending-img3.png
  24. BIN
      static/images/ranking-bg.png
  25. 49
      styles/global.scss
  26. 307
      subpkg/pages/activity-detail/activity-detail.vue
  27. 294
      subpkg/pages/activity-list/activity-list.vue
  28. 43
      subpkg/pages/book-detail/book-detail.vue
  29. 75
      subpkg/pages/book-list/book-list.vue
  30. 36
      subpkg/pages/collect-list/collect-list.vue
  31. 2
      subpkg/pages/feedback-list/feedback-list.vue
  32. 68
      subpkg/pages/myLending/myLending.vue
  33. 103
      subpkg/pages/reader-card/reader-card.vue
  34. 122
      subpkg/pages/user-info/user-info.vue
  35. 16
      uni_modules/w-qrcode/changelog.md
  36. 732
      uni_modules/w-qrcode/components/w-qrcode/w-qrcode.vue
  37. 619
      uni_modules/w-qrcode/js_sdk/qrcode.js
  38. 104
      uni_modules/w-qrcode/package.json
  39. 229
      uni_modules/w-qrcode/readme.md

8
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

28
components/book-list-item/book-list-item.vue

@ -2,28 +2,17 @@
<view class="item-container" @click="$emit('click')">
<view class="item-box">
<view class="item-box-left">
<!-- <image
class="img-item"
:src="data.imgCover"
mode="scaleToFill"
/> -->
<image
class="img-item"
:src="data.cover || data.imgCover || (data.imgPath ? baseUrl + '/api/fileRelevant/getImg?imgType=2&imgId=' + data.imgPath : '/static/images/default-book.png')"
mode="scaleToFill"
@error="onImgError"
></image>
class="img-item"
:src="data.base64Cover || data.cover || data.imgCover || '/static/images/default-book.png'"
mode="scaleToFill"
@error="onImgError"
></image>
</view>
<view class="item-box-right">
<view class="item-title line-clamp-2">{{ data.title || data.name || '暂无标题' }}</view>
<text class="item-author">{{ data.author || '佚名' }}</text>
<text class="item-publish">{{ data.publisher || '暂无出版社数据' }}</text>
<!-- <view class="item-desc line-clamp-2">{{ data.desc || '暂无简介' }}</view> -->
<!-- <view class="item-bottom-box">
<view class="hot-box">
<text class="hot-text">{{ hotNumber(data.views) }} 热度</text>
</view>
</view>-->
</view>
</view>
</view>
@ -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;
}
}
};

216
pages/home/home.vue

@ -3,14 +3,14 @@
<view class="top-bar">
<image class="top-bar-bg" src="@/static/images/mingqi-beij@2x.png" mode="aspectFill"></image>
<view class="library-info">
<image class="avatar" src="@/static/images/avatar.png" mode="aspectFill"></image>
<image class="avatar" src="@/static/images/logo.jpg" mode="aspectFill"></image>
<view class="library-name">葛店经济技术开发区图书馆</view>
</view>
<uni-icons class="search-icon" type="search" size="30" @click="onToSearch"></uni-icons>
</view>
<view class="status-cards">
<view class="status-card" @click="toBorrowPage(1)">
<view class="status-card" @click="toBorrowPage(0)">
<view class="status-icon icon-color1" >
<uni-icons custom-prefix="iconfont" type="icon-daiban02" size="22"></uni-icons>
</view>
@ -28,7 +28,7 @@
</view>
<view class="status-label">已过期</view>
</view> -->
<view class="status-card" @click="toBorrowPage(0)">
<view class="status-card" @click="toBorrowPage(1)">
<view class="status-icon icon-color4" >
<uni-icons custom-prefix="iconfont" type="icon-a-zhidu4" size="22"></uni-icons>
</view>
@ -45,7 +45,8 @@
</swiper-item>
</swiper>
<swiper class="menu-swiper" interval="6000" :vertical="false" :indicator-dots="true" :autoplay="false">
<!-- :indicator-dots="true" -->
<swiper class="menu-swiper" interval="6000" :vertical="false" :autoplay="false">
<swiper-item>
<view class="menu-grid">
<view class="menu-item" @click="onToSearch">
@ -113,13 +114,14 @@
:circular="false"
>
<swiper-item v-for="item in readerCardList" :key="item.id">
<view class="card-item">
<view class="card-item" @click.stop="showQrcode(item.bindValue)">
<image class="card-left-img" src="@/static/images/card-img2.png" mode="widthFix" />
<view class="card-right-info">
<text class="info-title">读者证号</text>
<text class="info-num">{{ item.bindValue }}</text>
<text class="info-num">{{ formatCardNo(item.bindValue) }}</text>
</view>
<view class="default-tag" v-if="item.bindDefault">默认证</view>
<uni-icons class="erweima-style" custom-prefix="iconfont" type="icon-erweima" size="28"></uni-icons>
</view>
</swiper-item>
</swiper>
@ -133,11 +135,34 @@
<button class="bind-btn">立即绑定</button>
</view>
<root-portal>
<uni-popup
ref="popup"
type="center"
:mask-click="true"
@maskClick="closePopup"
:custom-style="popupFixedStyle"
>
<view class="qrcode-popup">
<view class="qrcode-title">读者证二维码</view>
<w-qrcode
v-if="qrcodeText"
:value="qrcodeText"
:size="160"
foreground="#333"
background="#ffffff"
/>
<view class="qrcode-num">{{ qrcodeText }}</view>
<view class="close-btn" @click="closePopup">关闭</view>
</view>
</uni-popup>
</root-portal>
<view class="recommendation-section">
<view class="section-title">
<text class="left-txt">图书推荐</text>
<view class="more-link" @click="getMoreBoook">
<view class="more-link" @click="getMoreBoook" v-if="!(recommendedBooks.length === 0 && !isLoading)">
<text style="margin-right: 5px;">查看更多</text>
<uni-icons custom-prefix="iconfont" type="icon-gengduo" size="12"></uni-icons>
</view>
@ -148,14 +173,17 @@
<view v-else class="book-item" v-for="(item,index) in recommendedBooks.slice(0,3)" @click="goToBookDetail(item)" :key="index" >
<image
class="book-cover"
:src="item.imgPath ? baseUrl + '/api/fileRelevant/getImg?imgType=2&imgId=' + item.imgPath : '/static/images/default-book.png'"
:src="item.base64Cover || '/static/images/default-book.png'"
@error="onImgError"
></image>
<view class="book-title">{{item.name}}</view>
</view>
</view>
<view class="empty-box" v-if="recommendedBooks.length === 0 && !isLoading">
<uni-icons custom-prefix="iconfont" type="icon-kongshuju" size="80"></uni-icons>
<text style="margin-top: 20px;">暂无推荐书籍~</text>
</view>
</view>
</view>
</template>
@ -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
});
});
},
}
}
</script>
@ -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;
}
</style>

13
pages/lendCar/lendCar.vue

@ -1,6 +1,6 @@
<template>
<view class="item-container">
<view class="count-text">
<view v-if="bookList.length !== 0" class="count-text">
<text>图书数量 ({{ bookList.length }})</text>
</view>
@ -21,6 +21,11 @@
</view>
</view>
<view class="empty" v-if="bookList.length === 0">
<uni-icons style="margin-left: 20px;" custom-prefix="iconfont" type="icon-kongshuju" size="80"></uni-icons>
<text style="margin-top: 20px;">暂无相关数据</text>
</view>
<view class="bottom-placeholder"></view>
<view class="car-bottom">
@ -172,6 +177,7 @@ export default {
padding: 10px;
background: #f5f6f7;
min-height: 100vh;
overflow: hidden;
}
.count-text {
margin-bottom: 10px;
@ -181,6 +187,7 @@ export default {
.car-list {
display: flex;
align-items: flex-start;
overflow-y: auto;
margin-bottom: 10px;
checkbox {
margin-top: 15px;
@ -224,4 +231,8 @@ export default {
&::after{ border: none !important; }
&[disabled] { background: #ccc !important; }
}
.empty {
padding: 0 !important;
height: calc(100vh - 80px);
}
</style>

41
pages/login/login.vue

@ -3,39 +3,12 @@
<view class="top-bar">
<image class="top-bar-bg" src="@/static/images/mingqi-beij@2x.png" mode="aspectFill"></image>
<view class="library-info">
<image class="avatar" src="@/static/images/avatar.png" mode="aspectFill"></image>
<image class="avatar" src="@/static/images/logo.jpg" mode="aspectFill"></image>
<view class="library-name">葛店经济技术开发区图书馆</view>
</view>
</view>
<!-- <view class="user-info-section">
<button open-type="chooseAvatar" @chooseavatar="onChooseAvatar" class="avatar-btn">
<image v-if="avatarUrl" :src="avatarUrl" class="avatar-img"></image>
<image v-else src="@/static/images/avatar.png" class="avatar-img"></image>
<text class="tip-text">点击选择头像</text>
</button>
</view> -->
<view class="form-box">
<!-- <view class="item">
<uni-icons class="form-icon" type="person" size="24"></uni-icons>
<view class="uni-input-wrapper">
<input
class="input"
type="nickname"
v-model="nickName"
placeholder="请输入昵称"
@input="handleInput('nickName')"
/>
<uni-icons
class="clear-icon"
v-if="clearIconStatus.nickName"
@click="clearInput('nickName')"
type="close"
size="20"
></uni-icons>
</view>
</view> -->
<view class="item">
<uni-icons class="form-icon" custom-prefix="iconfont" type="icon-duzhezheng" size="24"></uni-icons>
<view class="uni-input-wrapper">
@ -77,7 +50,7 @@
import { FetchInitScreenSetting, FetchReaderList, FetchBindReadCard,FetchFindAllReaderBindByOpenId } from '@/api/user';
import config from '@/utils/config';
const READLIST = 'reader-card-list';
// const USER_KEY = 'user-info';
export default {
data() {
return {
@ -240,16 +213,6 @@ export default {
title: bindRes.data || '读者证绑定成功',
icon: 'success'
});
// const loginRes = {
// token: 'reader-token-' + Date.now(),
// user: {
// nickName: this.nickName || '小图',
// avatarUrl: this.avatarUrl
// }
// };
// uni.setStorageSync(USER_KEY, loginRes.user);
const data = {
libcode: '1201',
openId: openId

17
pages/search/search.vue

@ -88,7 +88,7 @@ export default {
selectIndex: 0,
value: '',
range: [
// { value: '', text: "任意词" },
{ value: '', text: "任意词" },
{ value: 'title', text: "题名" },
{ value: 'title200a', text: "正题名" },
{ value: 'author', text: "著者" },
@ -116,7 +116,22 @@ export default {
this.selectValueText = this.range[0].text;
this.getOpacUrl();
},
// 每次进入页面都会执行
onShow() {
// this.resetSearchData();
},
methods: {
// 重置搜索数据(每次进入页面清空)
resetSearchData() {
this.value = ''; // 清空搜索框
this.listData = []; // 清空列表
this.page = 1; // 重置页码
this.total = 0; // 清空总数
this.totalPage = 0; // 清空总页数
this.isSearched = false; // 重置搜索状态
// 不重置选择器,保留用户习惯
},
// 获取opacUrl
async getOpacUrl() {
try {

121
pages/user/user.vue

@ -4,12 +4,12 @@
<image class="top-bar-bg" src="@/static/images/mingqi-beij@2x.png" mode="aspectFill"></image>
<view class="user-info">
<view class="avatar-btn" @click="toUserInfoPage">
<image v-if="userInfo.avatarUrl" :src="userInfo.avatarUrl" class="avatar-img"></image>
<image v-else src="@/static/images/avatar.png" class="avatar-img"></image>
<image v-if="userInfo.avatarId" :src="base64Img" class="avatar-img"></image>
<image v-else src="@/static/images/logo.jpg" class="avatar-img"></image>
</view>
<view class="user-info-text">
<text class="user-name" @click="toUserInfoPage">{{ userInfo.nickName || '未登录' }}</text>
<text class="user-name" @click="toUserInfoPage">{{ userInfo.nickname || '未设置' }}</text>
<text class="user-card" v-if="cardNo">读者证:{{ cardNo }}</text>
</view>
</view>
@ -34,18 +34,10 @@
<uni-icons custom-prefix="iconfont" type="icon-shezhi" size="20"></uni-icons>
<text class="left-txt">个人资料</text>
</view>
<!-- <view class="submenu-item" @click="toCheckLogin('修改密码')">
<uni-icons custom-prefix="iconfont" type="icon-xiugai" size="20"></uni-icons>
<text class="left-txt">修改密码</text>
</view> -->
<view class="submenu-item" @click="toCheckLogin('解绑读者证')">
<uni-icons custom-prefix="iconfont" type="icon-UIsheji_menjinxitong-28" size="20"></uni-icons>
<text class="left-txt">解绑读者证</text>
</view>
<!-- <view v-if="isBindLibraryCard" class="submenu-item" @click="toLogOut()">
<uni-icons custom-prefix="iconfont" type="icon-tuichu" size="20"></uni-icons>
<text class="left-txt">退出账号</text>
</view> -->
</view>
<view>
@ -73,10 +65,11 @@ export default {
userInfo: {},
cardNo: "",
isBindLibraryCard: false,
base64Img: "",
};
},
onLoad() {
this.loadUserInfo();
// this.loadUserInfo();
},
onShow() {
this.loadUserInfo();
@ -88,24 +81,6 @@ export default {
});
},
// 调用接口:头像+昵称 → 成功后统一保存缓存
async bindUserInfo(avatar, nickname) {
const openId = uni.getStorageSync('wx_login_code') || '';
const params = {
avatar,
libcode: "1201",
nickname,
openid: openId
};
const res = await FetchBindRead(params);
const userInfo = {
nickName: nickname,
avatarUrl: avatar
};
uni.setStorageSync(USER_KEY, userInfo);
this.userInfo = userInfo;
},
// 加载用户信息(从接口获取)
async loadUserInfo() {
try {
@ -122,9 +97,13 @@ export default {
if (res.code === 200 && res.data) {
// 只赋值用户信息,不处理读者证
this.userInfo = {
nickName: res.data.nickName || res.data.nickname || '',
avatarUrl: res.data.avatarUrl || res.data.avatar || ''
nickname: res.data.nickname || '',
avatarId: res.data.avatar || ''
};
const imageUrl = config.baseUrl + '/api/fileRelevant/getImg?imgType=5&imgId=' + res.data.avatar;
const base64 = await this.urlToBase64(imageUrl);
this.base64Img = base64;
uni.setStorageSync(USER_KEY, this.userInfo);
} else {
this.userInfo = uni.getStorageSync(USER_KEY) || {};
@ -151,28 +130,35 @@ export default {
// 校验登录:只根据 有没有读者证 判断
toCheckLogin(pageName) {
const readerList = uni.getStorageSync(READLIST) || [];
const readerList = uni.getStorageSync(READLIST) || [];
// 没有绑定读者证 → 弹窗
if (readerList.length === 0) {
this.dialogToggle();
return;
}
// 没有绑定读者证 → 弹窗
if (readerList.length === 0) {
this.dialogToggle();
return;
}
// 已绑定 → 正常跳转
const routeMap = {
'借阅': '/subpkg/pages/myLending/myLending',
'我的留言': '/subpkg/pages/feedback-list/feedback-list',
// '修改密码': '/subpkg/pages/change-password/change-password',
'收藏': '/subpkg/pages/collect-list/collect-list',
// '个人资料': '/subpkg/pages/user-info/user-info',
'解绑读者证': '/subpkg/pages/reader-card/reader-card'
}
const url = routeMap[pageName]
if (url) {
uni.navigateTo({ url })
}
},
// 已绑定 → 正常跳转
const routeMap = {
'收藏': '/subpkg/pages/collect-list/collect-list',
// 借阅:自动存 switch_tab_index = 0
'借阅': () => {
uni.setStorageSync('switch_tab_index', 0);
uni.navigateTo({
url: '/subpkg/pages/myLending/myLending'
});
},
'我的留言': '/subpkg/pages/feedback-list/feedback-list',
'解绑读者证': '/subpkg/pages/reader-card/reader-card'
}
const target = routeMap[pageName]
if (typeof target === 'function') {
target() // 执行借阅的逻辑(存值+跳转)
} else if (target) {
uni.navigateTo({ url: target })
}
},
dialogToggle() {
this.$refs.alertDialog.open();
@ -180,6 +166,31 @@ export default {
dialogConfirm() {
uni.navigateTo({ url: "/pages/login/login" });
},
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
});
});
},
dialogClose() {},
// 退出登录
@ -241,7 +252,7 @@ export default {
width: 62px;
height: 62px;
border-radius: 50%;
object-fit: cover;
object-fit: cover;
}
}
.user-info-text{
@ -258,8 +269,8 @@ export default {
}
.user-card{
margin-top: 5px;
font-size: 12px;
font-weight: 40;
font-size: 13px;
// font-weight: bold;
color: #fff;
line-height: 17px;
}

4
static/iconfont.css

@ -11,6 +11,10 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-erweima:before {
content: "\e600";
}
.icon-kongshuju:before {
content: "\e61c";
}

BIN
static/iconfont.ttf

BIN
static/images/activity1.png

After

Width: 675  |  Height: 849  |  Size: 107 KiB

BIN
static/images/activity2.png

After

Width: 673  |  Height: 1102  |  Size: 188 KiB

BIN
static/images/avatar.png

Before

Width: 192  |  Height: 192  |  Size: 4.3 KiB

BIN
static/images/card-img1.png

Before

Width: 610  |  Height: 245  |  Size: 100 KiB

After

Width: 610  |  Height: 245  |  Size: 26 KiB

BIN
static/images/card-img2.png

Before

Width: 80  |  Height: 80  |  Size: 8.8 KiB

After

Width: 80  |  Height: 80  |  Size: 3.2 KiB

BIN
static/images/default-avatar.png

Before

Width: 124  |  Height: 124  |  Size: 5.3 KiB

After

Width: 124  |  Height: 124  |  Size: 1.4 KiB

BIN
static/images/default-book.png

Before

Width: 756  |  Height: 1063  |  Size: 8.1 KiB

After

Width: 574  |  Height: 800  |  Size: 157 KiB

BIN
static/images/logo.jpg

After

Width: 640  |  Height: 640  |  Size: 18 KiB

BIN
static/images/menu-jy.png

Before

Width: 64  |  Height: 64  |  Size: 1.7 KiB

After

Width: 64  |  Height: 64  |  Size: 663 B

BIN
static/images/menu-sc.png

Before

Width: 64  |  Height: 64  |  Size: 1.9 KiB

After

Width: 64  |  Height: 64  |  Size: 684 B

BIN
static/images/mingqi-beij@2x.png

Before

Width: 750  |  Height: 396  |  Size: 181 KiB

After

Width: 750  |  Height: 396  |  Size: 36 KiB

BIN
static/images/mylending-img1.png

Before

Width: 122  |  Height: 107  |  Size: 12 KiB

After

Width: 122  |  Height: 107  |  Size: 3.1 KiB

BIN
static/images/mylending-img2.png

Before

Width: 122  |  Height: 107  |  Size: 7.4 KiB

After

Width: 122  |  Height: 107  |  Size: 2.4 KiB

BIN
static/images/mylending-img3.png

Before

Width: 122  |  Height: 107  |  Size: 11 KiB

After

Width: 122  |  Height: 107  |  Size: 2.9 KiB

BIN
static/images/ranking-bg.png

Before

Width: 750  |  Height: 270  |  Size: 125 KiB

After

Width: 750  |  Height: 270  |  Size: 46 KiB

49
styles/global.scss

@ -235,3 +235,52 @@
background-color: #e5e5e5 !important;
}
}
.empty {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
color: #999;
}
// 覆盖 uni-popup 层级
.uni-popup {
z-index: 9999 !important;
}
.uni-popup__mask {
z-index: 9998 !important;
}
.uni-popup__wrapper {
z-index: 9999 !important;
overflow: hidden;
}
.qrcode-popup {
width: 300px;
padding: 20px;
background: #fff;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
}
.qrcode-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
}
.qrcode-num {
margin: 10px 0;
font-size: 14px;
color: #333;
}
.close-btn {
margin-top: 10px;
background: #01a4fe;
color: #fff;
border-radius: 20px;
font-size: 14px;
padding: 5px 20px;
}

307
subpkg/pages/activity-detail/activity-detail.vue

@ -1,42 +1,39 @@
<template>
<view class="activity-detail">
<image class="activity-img" :src="detail.imgUrl"></image>
<view class="activity-base">
<text class="title">{{ detail.title }}</text>
<view class="time">
<uni-icons class="detail-icon" custom-prefix="iconfont" type="icon-shijian" size="15"></uni-icons>
<text>{{ detail.time }}</text>
</view>
<view class="location">
<uni-icons class="detail-icon" type="location" size="20"></uni-icons>
<text>{{ detail.location }}</text>
</view>
</view>
<view class="activity-content">
<text class="content-title">活动介绍</text>
<text class="content-desc">{{ detail.content }}</text>
</view>
<view class="detail-bottom">
<view class="detail-left">
<!-- #ifdef MP-WEIXIN -->
<button open-type="share" class="handle-btn">
<uni-icons custom-prefix="iconfont" type="icon-fenxiang01" size="20"></uni-icons>
<text class="share-text">分享</text>
</button>
<!-- #endif -->
<button class="handle-btn" @click="toggleCollect">
<!-- 已收藏 / 未收藏 自动切换图标 -->
<uni-icons :type="isCollected ? 'heart-filled' : 'heart'" size="20" color="#ff4444"></uni-icons>
<text class="share-text">{{ isCollected ? '已收藏' : '收藏' }}</text>
</button>
</view>
<button class="join-btn" :class="detail.status === 0 ? 'disabled-btn' : ''">
<text>{{ detail.status === 1 ? '我要参加' : '活动结束' }}</text>
</button>
</view>
<image class="activity-img" :src="detail.imgUrl" mode="aspectFill"></image>
<view class="activity-base">
<text class="title">{{ detail.title }}</text>
<view class="time">
<uni-icons class="detail-icon" custom-prefix="iconfont" type="icon-shijian" size="15"></uni-icons>
<text>{{ detail.time }}</text>
</view>
<view class="location">
<uni-icons class="detail-icon" type="location" size="20"></uni-icons>
<text>{{ detail.location }}</text>
</view>
</view>
<view class="rich-content">
<text class="content-title">活动介绍/回顾</text>
<rich-text :nodes="contentNodes"></rich-text>
</view>
<view class="detail-bottom">
<view class="detail-left">
<button open-type="share" class="handle-btn">
<uni-icons custom-prefix="iconfont" type="icon-fenxiang01" size="20"></uni-icons>
<text>分享</text>
</button>
<button class="handle-btn" @click="toggleCollect">
<uni-icons :type="isCollected ? 'heart-filled' : 'heart'" size="20" color="#ff4444"></uni-icons>
<text>{{ isCollected ? '已收藏' : '收藏' }}</text>
</button>
</view>
<button class="join-btn" :class="detail.status === 0 ? 'disabled-btn' : ''">
{{ detail.status === 1 ? '我要参加' : '活动结束' }}
</button>
</view>
</view>
</template>
@ -44,98 +41,76 @@
export default {
data() {
return {
detail: {
imgUrl: "",
title: "",
time: "",
location: "",
content: "",
status: 1 // 1=可参加 0=已结束
},
isCollected: false, // 收藏状态
activityId: "" // 活动唯一标识(用标题作为唯一ID)
detail: {},
isCollected: false,
activityId: "",
contentNodes: ""
};
},
onLoad(options) {
const title = options.title || "";
this.activityId = title; // 用活动标题作为唯一标识
this.getActivityDetail(title);
const item = JSON.parse(decodeURIComponent(options.item));
this.detail = item;
this.activityId = item.title;
this.formatContent();
this.checkCollectStatus();
},
methods: {
/**
* 模拟 获取活动详情接口
* @param {String} title 活动标题
*/
getActivityDetail(title) {
uni.showLoading({ title: "加载中..." });
setTimeout(() => {
const res = {
imgUrl: "https://qiniu.aiyxlib.com/1605060269830",
title: title,
time: "2025-11-03 00:00 ~ 2025-11-09 00:00",
location: "图书馆一楼",
content: "“酷爱书荟书香满园”作为酷车小镇的品牌活动,此次活动以“世界读书日”为契机,旨在推动“全民阅读”的文化风尚,激发公众的阅读兴趣,营造浓厚的阅读氛围。",
// 模拟状态:标题包含数字3/6/9则结束
status: title.includes("3") || title.includes("6") || title.includes("9") ? 0 : 1
};
this.detail = res;
// 加载完活动数据后,检查收藏状态
this.checkCollectStatus();
uni.hideLoading();
}, 1000);
},
formatContent() {
let content = this.detail.content || "";
// ======================================
// 1. 把 content 里的 <image src="@/static/..."> 转成 <img> 标签
// ======================================
const localImgReg = /<image\s+src="@\/static\/([^"]+)"[^>]*>/gi;
content = content.replace(localImgReg, (match, path) => {
return `
<div style="text-align:center; margin:12px 0;">
<img src="/static/${path}" style="width:90%; border-radius:8px;">
</div>
`;
});
// ======================================
// 2. 把 https://mmbiz.qpic.cn 图片链接转成图片
// ======================================
const httpImgReg = /https:\/\/mmbiz\.qpic\.cn[^ \n\r]+/g;
content = content.replace(httpImgReg, url => {
return `
<div style="text-align:center; margin:12px 0;">
<img src="${url}" style="width:90%; border-radius:8px;">
</div>
`;
});
this.contentNodes = content;
},
// 检查当前活动是否已收藏
checkCollectStatus() {
const collectList = uni.getStorageSync('activityCollectList') || [];
this.isCollected = collectList.includes(this.activityId);
const list = uni.getStorageSync('activityCollectList') || [];
this.isCollected = list.includes(this.activityId);
},
// 切换收藏 / 取消收藏
toggleCollect() {
let collectList = uni.getStorageSync('activityCollectList') || [];
let list = uni.getStorageSync('activityCollectList') || [];
if (this.isCollected) {
// 取消收藏
collectList = collectList.filter(id => id !== this.activityId);
uni.showToast({ title: '取消收藏成功', icon: 'success' });
list = list.filter(i => i !== this.activityId);
uni.showToast({ title: "取消收藏" });
} else {
// 添加收藏
collectList.push(this.activityId);
uni.showToast({ title: '收藏成功', icon: 'success' });
list.push(this.activityId);
uni.showToast({ title: "收藏成功", icon: "success" });
}
// 保存到本地缓存
uni.setStorageSync('activityCollectList', collectList);
// 更新状态
uni.setStorageSync('activityCollectList', list);
this.isCollected = !this.isCollected;
}
// 真实接口示例(备用)
// getActivityDetail(title) {
// uni.showLoading();
// uni.request({
// url: "https://xxx.com/api/activity/detail",
// method: "GET",
// data: { title: title },
// success: (res) => {
// this.detail = res.data;
// this.checkCollectStatus();
// },
// complete: () => {
// uni.hideLoading();
// }
// });
// }
},
// 微信分享
onShareAppMessage() {
return {
title: this.detail.title,
path: "/subpkg/pages/activity-detail/activity-detail?title=" + this.detail.title,
path: "/subpkg/pages/activity-detail/activity-detail?item=" + encodeURIComponent(JSON.stringify(this.detail)),
imageUrl: this.detail.imgUrl
};
}
@ -143,50 +118,43 @@ export default {
</script>
<style lang="scss" scoped>
.activity-img{
width: 100%;
height: 150px;
.activity-detail {
padding-bottom: 60px;
background: #f5f5f5;
}
.activity-base{
display: flex;
flex-direction: column;
justify-content: flex-start;
background-color: #fff;
padding: 15px;
.title{
font-size: 16px;
font-weight: bold;
color: #333;
padding-bottom: 10px;
}
.time,
.location {
display: flex;
justify-content: flex-start;
align-items: center;
font-size: 12px;
color: #999;
.detail-icon{
width: 20px;
}
}
.time {
padding-bottom: 10px;
.detail-icon{
margin-top: 2px;
margin-left: 2px;
}
.activity-img {
width: 100%;
height: 180px;
}
.activity-base {
background: #fff;
padding: 15px;
margin-bottom: 10px;
.title {
font-size: 16px;
font-weight: bold;
padding-bottom: 10px;
}
.time,
.location {
display: flex;
align-items: center;
font-size: 12px;
color: #999;
padding-top: 10px;
.detail-icon {
padding-right: 4px;
}
}
.time{
padding-left: 3px;
}
}
.activity-content{
display: flex;
flex-direction: column;
justify-content: flex-start;
background-color: #fff;
margin-top: 20px;
padding: 15px 0;
.content-title{
.rich-content {
background: #fff;
padding: 10px 0;
.content-title{
position: relative;
font-size: 16px;
font-weight: bold;
@ -203,10 +171,43 @@ export default {
background-color: #01a4fe;
}
}
.content-desc{
padding: 15px;
font-size: 14px;
line-height: 24px;
}
}
.detail-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 15px;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.05);
}
.detail-left {
display: flex;
gap: 10px;
}
.handle-btn {
display: flex;
align-items: center;
gap: 4px;
background: #f5f5f5;
border-radius: 30px;
padding: 0 12px;
height: 36px;
font-size: 14px;
}
.join-btn {
background: #01a4fe;
color: #fff;
border-radius: 30px;
padding: 0 20px;
height: 36px;
}
.disabled-btn {
background: #ccc !important;
}
</style>

294
subpkg/pages/activity-list/activity-list.vue

@ -1,18 +1,15 @@
<template>
<view class="activity-list">
<!-- 下拉刷新区域 -->
<scroll-view
scroll-y
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
lower-threshold="100"
>
<!-- 活动列表 -->
<view class="activity-item" v-for="(item, index) in activityList" @click="toActivityDetail(item)" :key="index">
<image class="activity-img" :src="item.imgUrl"></image>
<image class="activity-img" :src="item.imgUrl" mode="aspectFill"></image>
<view class="activity-info">
<view class="activity-info-left">
<text class="title">{{ item.title }}</text>
@ -21,30 +18,22 @@
<text>{{ item.time }}</text>
</view>
</view>
<button
<!-- <button
class="activity-btn"
:class="item.status === 0 ? 'disabled-btn' : ''"
type="primary"
:disabled="item.status === 0"
>
{{ item.status === 1 ? '立即参加' : '活动结束' }}
</button>
</button> -->
</view>
</view>
<!-- 空状态:没有活动时显示 -->
<view class="empty-box" v-if="activityList.length === 0 && !loading">
<!-- 空状态 -->
<view class="empty-box" v-if="activityList.length === 0">
<uni-icons custom-prefix="iconfont" type="icon-kongshuju" size="80"></uni-icons>
<text style="margin-top: 20px;">暂无活动,敬请期待~</text>
</view>
<!-- 加载提示 -->
<view class="load-tip" v-if="loading">
<text>加载中...</text>
</view>
<view class="load-tip" v-if="noMore && activityList.length > 0">
<text>没有更多数据了</text>
</view>
</scroll-view>
</view>
</template>
@ -53,83 +42,239 @@
export default {
data() {
return {
activityList: [], // 活动列表数据
refreshing: false, // 下拉刷新状态
loading: false, // 加载中
noMore: false, // 是否没有更多数据
page: 1, // 当前页码
pageSize: 5, // 每页条数
total: 20, // 模拟总数据量
activityList: [],
refreshing: false
};
},
onLoad() {
this.getActivityList();
},
methods: {
// 固定2条假数据
getActivityList() {
this.loading = true;
this.refreshing = true;
setTimeout(() => {
// 模拟接口没数据
// const list = this.mockData(this.page, this.pageSize);
const list = []; // 测试空状态
if (this.refreshing) {
this.activityList = list;
this.refreshing = false;
} else {
this.activityList = [...this.activityList, ...list];
}
this.activityList = [
{
imgUrl: "https://mmbiz.qpic.cn/mmbiz_jpg/tuyGibvSXrso7c068ibUK1dXCGM2oiaBHAe3c82icrPjhLr3aFMLzhQ8n4C9X6qY5n0aDkRibvTNxoFVchrQxnicSJ64LX5KJxKj80TutmMicRKoY0/640?wx_fmt=jpeg&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=3",
title: "以爱之名赴健康之约,致敬每一位伟大的母亲",
time: "2026-05-10 09:00 ~ 2026-05-10 11:30",
content: `
<image src="@/static/images/activity1.png" mode="aspectFill"></image>
if (this.activityList.length >= this.total) {
this.noMore = true;
} else {
this.noMore = false;
}
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">2026年5月10日,“以爱之名,守护健康”葛图讲坛35期母亲节健康关爱活动在葛店经开区图书馆温情启幕,汇聚了近40组亲子家庭,将健康科普化作深情告白,把亲子陪伴化作节日惊喜,为每一位母亲送上最真挚的节日祝福与贴心关怀。</p>
this.loading = false;
}, 500);
},
https://mmbiz.qpic.cn/mmbiz_jpg/tuyGibvSXrsp5h4G9Ip4Jl10attsaXjlUyRLOcia8zXKfV0yc9cwOIgmvnSqusKkB4iaia8bMFDnBm2ApntEWA06QEmDt7hbwa5t5k3hofWZm3s/640?wx_fmt=jpeg&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=6
mockData(page, pageSize) {
let arr = [];
for (let i = 0; i < pageSize; i++) {
const num = (page - 1) * pageSize + i + 1;
arr.push({
imgUrl: "https://qiniu.aiyxlib.com/1605060269830",
title: `活动标题${num}`,
time: "2025-11-03 00:00 ~2025-11-09 00:00",
status: num % 3 === 0 ? 0 : 1,
});
}
return arr;
},
<p style="text-align: center;color: rgb(240, 159, 161); padding-bottom: 40px;">HAPPY MOTHER'S DAY</p>
onRefresh() {
this.page = 1;
this.noMore = false;
this.refreshing = true;
this.getActivityList();
<p style="text-align: center; line-height:36px;"> 致敬母亲<br/>
不止于告白<br/>
更在于守护她的岁岁安康</p>
https://mmbiz.qpic.cn/sz_mmbiz_jpg/tuyGibvSXrspDOvN5sicEDS4tqjTXZzurKrqmQibiazDZldSP5j1bWvKKiahCMvJ2OOpvUcvDPSyodGso32IEEC6ajPznwKm2PL5vyMjh2biahh2g/640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=7
<p style="text-align: center; line-height:36px;">
活动特邀湖北省中医院四位专家<br/>
聚焦母亲群体高频健康痛点<br/>
把专业的医学知识<br/>
化作守护母亲的暖心指南<br/>
</p>
<p style="text-align: center; line-height:36px;">
杨露医师重点科普中老年母亲<br/>
高发的睡眠呼吸暂停综合征<br/>
教大家科学识别与干预<br/>
</p>
<p style="text-align: center; line-height:36px;">
王诗蕾护师详解顺时养生与睡眠的关联<br/>
为常见失眠问题提供实用方案<br/>
帮助大家拥抱深度睡眠<br/>
</p>
https://mmbiz.qpic.cn/mmbiz_jpg/tuyGibvSXrsqHcibsoKTrNmhFAOlmA4EibNBoEJibQRd0zoU5IOYPORqIKWAC6xH6qCMYKbKGshU5mkSicMOdhCIP24LpEPkgtyVXN73XXU83vsk/640?wx_fmt=jpeg&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=12
https://mmbiz.qpic.cn/sz_mmbiz_jpg/tuyGibvSXrsoMcRPkKzwAq229WY37uOaqlzvV1lrPrib1WibQyH8iakKGweuJ2eyYZpRXj79Qmm7zC4IMaibDgn5qXfCuxmF8eibFONhjZOpMZtvQ/640?wx_fmt=jpeg&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=13
<p style="text-align: center; line-height:36px;">
认真聆听的模样<br/>
是母亲对自己的温柔善待<br/>
专注记录的瞬间<br/>
是我们对母亲最实在的守护<br/>
</p>
https://mmbiz.qpic.cn/sz_mmbiz_jpg/tuyGibvSXrsphvUJMRz60CzqkEd6xuSxdzoSCjdOoQr8CG3aaib2xl6vpcUr35JAqbXfojqib2lQZWh24cNOgf2ewfVmJrRrtrfJadib8aKkkibY/640?wx_fmt=jpeg&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=17
<p style="text-align: center;color: rgb(240, 159, 161); padding-top: 40px; padding-bottom: 40px;">HAPPY MOTHER'S DAY</p>
<p style="text-align: center; line-height:36px;">
以爱之名,守护健康<br/>
以书为媒,传递温情<br/>
</p>
<p style="text-align: center; line-height:36px; padding-bottom: 40px;">
这场由葛店经开区图书馆<br/>
湖北省中医院葛店院区<br/>
湖畔社区居委会联合举办的<br/>
“葛图讲坛”母亲节特别企划<br/>
是我馆人文关怀与文化传播的一次温柔落地<br/>
更是对每一位母亲的深情致敬<br/>
</p>
<p style="text-align: center; line-height:36px;">
愿更多关注投向母亲健康<br/>
愿每一份爱意及时抵达<br/>
让每一位母亲被时光温柔以待<br/>
被健康悉心守护<br/>
</p>
`,
status: 0,
location: "葛店经开区图书馆",
},
{
imgUrl: "https://mmbiz.qpic.cn/sz_mmbiz_jpg/tuyGibvSXrsqmHJvND8QQFHPDaFEuGGqaNgVvjEO7tqBziaqlcz5bURSyNd4DoKxcD41LWIDComEbUm25pBA86ibeN45zvoPAzDJzaoxayyicMc/640?wx_fmt=jpeg&from=appmsg#imgIndex=22",
title: "别让肥胖偷走健康,馆藏好书为健康支招",
time: "2026-05-11 09:00 ~ 2026-05-11 17:30",
content: `
<image src="@/static/images/activity2.png" mode="aspectFill"></image>
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; font-size: 13px;">5月11日,是专为对抗肥胖问题设立的世界防治肥胖日。当“5·11”谐音成“我要瘦”,这简单的三个字背后,藏着的是全球超20亿超重或肥胖人群的健康焦虑。肥胖早已不是“体态丰满”的小事,而是被世界卫生组织列为十大慢性疾病之一的公共卫生危机。今天,就让我们一起科学认识肥胖,把健康主动权握在自己手中。</p>
<div style="display:flex; justify-content: center; margin:40px auto 20px auto;">
<p style="background-color: rgb(93, 164, 124); padding: 4px 15px; color:#fff; border-radius: 4px; ">科学减重指南书籍推荐</p>
</div>
<p style="text-indent: 2em; line-height:28px; display: inline-block; margin:0 12px; border: 2px solid rgb(93, 164, 124); padding: 10px; background-color: rgb(199, 227, 178); font-size: 13px; text-align: justify; border-radius: 4px;">为什么你拼命运动还是瘦不下来?为什么你严格节食却依旧反弹?为什么别人轻松减肥,你却举步维艰?其实,减肥不是一场靠意志力硬撑的战争,而是一场需要科学指导的持久战。想要打破减肥僵局,不妨从阅读专业的减肥书籍开始。今天,葛店经开区图书馆就为你推荐几本靠谱的减肥好书,帮你重塑身材,拥抱健康生活。</p>
https://mmbiz.qpic.cn/sz_mmbiz_jpg/tuyGibvSXrsoXfmjrjPs3sDmOmIkx7LSHA7bMdkNGvbVDdyN7AywSXk8hfL2Z0S0bv9S6NUSxuYngZnyRGXff2gmvuUn89iaxHfPxVQyNLLZg/640?wx_fmt=jpeg&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=16
<div style="display:flex; justify-content: center; margin:20px auto; flex-direction: column; padding:0 20px; font-size: 13px;">
<p>ISBN:</p>
<p style="text-align: center; line-height:36px;">978-7-5337-9035-6 </p>
<p>馆藏地点:</p>
<p style="text-align: center; line-height:36px;">二楼综合阅览一<br/>27架B面03列03层</p>
<p>内容简介:</p>
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">本书分为五个篇章, 包括认知篇、饮食营养篇、心理篇、运动篇, 以及特殊人群篇。这五个篇章涵盖了超重和肥胖人群的体重管理, 以及消瘦人群的体重管理, 旨在为读者提供全面、科学的体重管理指导。</p>
</div>
https://mmbiz.qpic.cn/mmbiz_jpg/tuyGibvSXrso5oZu6lfdRoV4n8qqp55JYqOh44UrIiaicPHbnicIk8C7QVoCGiaNZXZXByXUWo7WqgiaBhGXztdZtYnC0ps8SYHjiaV9PKY8X6iaPqQ/640?wx_fmt=jpeg&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=17
<div style="display:flex; justify-content: center; margin:20px auto; flex-direction: column; padding:0 20px; font-size: 13px;">
<p>ISBN:</p>
<p style="text-align: center; line-height:36px;">978-7-5001-7513-1 </p>
<p>馆藏地点:</p>
<p style="text-align: center; line-height:36px;">二楼综合阅览一<br/>27架B面03列03层</p>
<p>内容简介:</p>
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">本书主要内容: 身体重置计划不需要节食、不需要有禁食窗口期, 无关饮食热量限制或其他时髦的花招。它包含六大简单的操作技巧, 是为生活在现实世界中的真实成年人设计, 帮助成年人通过饮食实现健康生活, 甚至逆龄生态。书中内容非常易于操作和可持续, 不需要很多额外的餐饮动作, 是一套详尽、有效、丰富的餐饮为主, 配合适当运动的综合性身体调节计划。</p>
</div>
https://mmbiz.qpic.cn/sz_mmbiz_jpg/tuyGibvSXrspsqSQqHXGWtMUuLeKD7y1hJ0gDBqpBNJlRCMyPoZwjB8HN1chbl13zcQEIiaVBbrsRdnznThGze0EDQtfQyKen282oFUunVzB0/640?wx_fmt=jpeg&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=18
<div style="display:flex; justify-content: center; margin:20px auto; flex-direction: column; padding:0 20px; font-size: 13px;">
<p>ISBN:</p>
<p style="text-align: center; line-height:36px;">978-7-5532-1045-2</p>
<p>馆藏地点:</p>
<p style="text-align: center; line-height:36px;">二楼综合阅览一<br/>27架B面02列02层</p>
<p>内容简介:</p>
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">作者谢佳妤在整理其奶奶李瑞芬遗稿的基础上,用年轻化的语言讲述了饮食与健康的关系,说明了各种常见的营养素的作用和正确进行食物搭配的方法,有助于读者学会科学选择食物、理性对待食物,养成均衡饮食、合理膳食的习惯。</p>
</div>
<div style="display:flex; justify-content: center; margin:40px auto 20px auto;">
<p style="background-color: rgb(93, 164, 124); padding: 4px 15px; color:#fff; border-radius: 4px; ">肥胖是疾病的“导火索”</p>
</div>
<div>
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">很多人觉得“胖点没关系,看着富态”,但医学研究早已证实,肥胖是多种疾病的“共同土壤”。它就像一颗埋在身体里的定时炸弹,随时可能引爆健康危机:</p>
https://mmbiz.qpic.cn/sz_mmbiz_png/tuyGibvSXrsoGIHpwHdfEr7sM6Ldqd4YjPwaYBBeFc2CRickibceCwIEXIopOCO6HWMIagYQkEwApf42cYxPUbdUqT3d2rFudibJcM53n3wTG0Q/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=20
<p style="text-align: center; line-height:36px;">1.代谢系统紊乱:</p>
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">约80%的2型糖尿病患者伴有超重或肥胖,内脏脂肪释放的炎症因子会干扰胰岛素信号,让身体对胰岛素“失灵”,血糖随之失控。此外,肥胖还会诱发痛风、高脂血症,让血液变成浓稠的“油脂汤”。</p>
<p style="text-align: center; line-height:36px;">2.心血管不堪重负:</p>
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">肥胖者患高血压的风险比正常人高60%,冠心病风险更是高出2-3倍。腹部堆积的脂肪会压迫血管,加速动脉粥样硬化,心梗、中风等急性心血管事件的发生率也随之飙升。</p>
https://mmbiz.qpic.cn/sz_mmbiz_jpg/tuyGibvSXrsqmHJvND8QQFHPDaFEuGGqaNgVvjEO7tqBziaqlcz5bURSyNd4DoKxcD41LWIDComEbUm25pBA86ibeN45zvoPAzDJzaoxayyicMc/640?wx_fmt=jpeg&from=appmsg#imgIndex=22
<p style="text-align: center; line-height:36px;">3.癌症风险飙升:</p>
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">国际癌症研究机构确认,肥胖与至少13种癌症相关。其中,子宫内膜癌风险增加6倍,食管腺癌、肝癌、肾癌等发病概率也显著上升。脂肪组织分泌的异常激素,会持续刺激细胞变异,为癌症埋下隐患。</p>
<p style="text-align: center; line-height:36px;">4.生活质量直线下降:</p>
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">体重每增加5公斤,膝关节骨关节炎风险上升36%;重度肥胖者中,阻塞性睡眠呼吸暂停综合征患病率超70%,每晚都在“缺氧”中挣扎。更不用提肥胖带来的自卑、抑郁等心理问题,让很多人在社交中抬不起头。 </p>
</div>
<div style="display:flex; justify-content: center; margin:40px auto 20px auto;">
<p style="background-color: rgb(93, 164, 124); padding: 4px 15px; color:#fff; border-radius: 4px; ">科学减重:避开误区找对方法</p>
</div>
<div>
https://mmbiz.qpic.cn/mmbiz_png/tuyGibvSXrsroDeiawZ9ApdibzQc2ZXAp8u1JSsViciaibwfIupY4VTpIets2AoEY87dfbKIS8cuDeR7icLqBGNSUkV8nCA3fmyLDw2hH7dtdWK5FE/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=29
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">提到减肥,很多人第一反应就是“节食”“吃减肥药”,但这些极端方法往往伤身又反弹。真正的科学减重,是建立健康的生活方式,让身体慢慢回归平衡。</p>
<p style="text-align: center; line-height:36px;">1.不是“饿肚子”,是“会吃饭”</p>
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">控制总能量,合理分配:先根据“身高(cm)-105”算出理想体重,再按每天每公斤理想体重15-35kcal计算总能量(轻体力劳动者选20-25kcal)。将一日三餐能量按3:4:3分配,早餐吃好、午餐吃饱、晚餐吃少。</p>
https://mmbiz.qpic.cn/mmbiz_png/tuyGibvSXrsobpIdpicy3GeZ9HxuWwQCKqAr85ibIHfZKGFAsgewVSrh6fwI04vlJGAOBgiaRB5eyp4HHo4NJ3wWiberXHvke4LGs6hnKupzqPqk/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=31
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">拒绝“高热量陷阱”:少吃油炸食品、含糖糕点、肥肉等高能量食物,这些食物往往一口就占了半天的能量配额。多吃全谷物、蔬菜、优质蛋白,比如燕麦、西兰花、鸡胸肉,它们能带来更强的饱腹感,还能保证营养均衡。</p>
https://mmbiz.qpic.cn/sz_mmbiz_png/tuyGibvSXrsq1ypaFcwARIlrCXTqibKZcjwozFc9JlKTap25kzbxY7a8Fk7l5ibLxibKN7bTIak0logVmTGR48N9xHz3NF1nL7gxXZ6MncFvGt8/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=32
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">养成健康进食习惯:细嚼慢咽,每口饭嚼20次以上;定时定量进餐,不吃夜宵;用小盘子盛饭,避免“眼大肚小”吃撑自己。</p>
<p style="text-align: center; line-height:36px;">2. 动起来,让脂肪“动”起来</p>
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">有氧运动为主,抗阻运动为辅:每周进行150-300分钟中等强度有氧运动,比如快走、慢跑、游泳,每次30分钟以上,能有效燃烧脂肪。每周再安排2-3次抗阻运动,举哑铃、做平板支撑都可以,增加肌肉量能提高基础代谢,让你躺着也能消耗更多能量。</p>
https://mmbiz.qpic.cn/mmbiz_png/tuyGibvSXrsp64a3In2BKHfQQHEIkDIUSjCklsbETfrcRkHyd35fW3OUYZeibpJHRE6zicbicSf94DIics1vP9b27o1GPH7X2rQkk0wh8bNNaicoQ/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=34
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">打破“久坐魔咒”:每坐1小时就起身活动3-5分钟,伸伸懒腰、倒杯水、走两步,每天的零碎时间加起来,也是一笔不小的“运动财富”。</p>
<p style="text-align: center; line-height:36px;">3.小习惯,大改变</p>
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">睡够7小时:睡眠不足会导致瘦素分泌减少、饥饿素分泌增加,让你忍不住想吃高糖高脂食物。尽量在晚上11点前入睡,给身体足够的修复时间。 </p>
https://mmbiz.qpic.cn/mmbiz_png/tuyGibvSXrspLUJPiaZuP1eAZXWjyNyGeKLNgnXibwich4xnqgQJjXcyxCIickVWr8ibsmnEmTUx2JuwARYWxDR3yW2ibIJX2O3RzSic3xT3RNlkwaA/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=35
https://mmbiz.qpic.cn/sz_mmbiz_png/tuyGibvSXrsqKnXiak3cs5BPILA59JRapTCkFBycZ0SYdzxBQ1LMnqkXicWZiaNiaYPsTyZ9S1wBOyuv0j24v6pbyjWIO2p8Ue42LWckMYWuaSrs/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=36
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">学会情绪管理:压力大时很多人会“情绪化进食”,不妨试试冥想、听音乐、和朋友聊天,用健康的方式释放压力,别让食物成为“情绪垃圾桶”。</p>
https://mmbiz.qpic.cn/mmbiz_png/tuyGibvSXrsoGPLDZ7l26cKDDac6BewgQuZiaic4M5Rk052cibY1OYBiaE9G46QAIEeCdSZN58usFVWMIto6dcz2lxg04ksYRaECazibulFu9pjf4/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1#imgIndex=37
<p style="padding:20px 12px; text-indent: 2em; line-height:28px; ">定期监测体重:每周固定时间称一次体重,比如每周一早上空腹称重,及时调整饮食和运动计划,避免体重“悄悄失控”。</p>
</div>
<p style="text-align: center; line-height:36px; padding-top: 40px;">
每个人<br/>
从自己做起<br/>
主动学习健康知识<br/>
管住嘴<br/>
迈开腿<br/>
用实际行动对抗肥胖<br/>
</p>
`,
status: 0,
location: "葛店经开区图书馆",
}
];
this.refreshing = false;
}, 300);
},
onLoadMore() {
if (this.loading || this.noMore) return;
this.page++;
// 下拉刷新
onRefresh() {
this.getActivityList();
},
toActivityDetail(item){
uni.navigateTo({
url: "/subpkg/pages/activity-detail/activity-detail?title=" + item.title
});
}
// 跳转详情
toActivityDetail(item) {
if (item.link) {
uni.navigateTo({
url: '/subpkg/pages/webview/webview?url=' + encodeURIComponent(item.link)
});
} else {
// 无外链 → 跳原详情页
uni.navigateTo({
url: "/subpkg/pages/activity-detail/activity-detail?item=" + encodeURIComponent(JSON.stringify(item))
});
}
}
},
};
</script>
<style lang="scss" scoped>
.activity-list {
padding: 20px;
padding: 20px 10px;
height: 100vh;
box-sizing: border-box;
}
@ -138,7 +283,6 @@ scroll-view {
height: 100%;
}
/* 空状态 */
.empty-box {
display: flex;
flex-direction: column;
@ -149,13 +293,6 @@ scroll-view {
font-size: 15px;
}
.load-tip {
text-align: center;
padding: 20px;
color: #999;
font-size: 14px;
}
.activity-item {
background: #fff;
border-radius: 12px;
@ -166,6 +303,7 @@ scroll-view {
.activity-img {
width: 100%;
height: 180rpx;
object-fit: cover;
}
.activity-info {

43
subpkg/pages/book-detail/book-detail.vue

@ -14,7 +14,7 @@
<view class="article-detail-right">
<image
class="img-item"
:src="bookInfo.cover || '/static/images/default-book.png'"
:src="bookInfo.cover || bookInfo.base64Cover || '/static/images/default-book.png'"
mode="widthFix"
@error="onImgError"
></image>
@ -22,7 +22,7 @@
</view>
<!-- 馆藏信息 -->
<view class="book-store-info">
<view v-if="holdingsData.length!==0" class="book-store-info">
<view class="book-store-info-item">
<text class="store-txt1">{{ holdingsData.length || 0 }}</text>
<text class="store-txt2">馆藏总数</text>
@ -38,19 +38,27 @@
</view>
<!-- 索书号/条码号 -->
<view class="store-info-list">
<view v-if="holdingsData.length!==0" class="store-info-list">
<view class="store-info-item" v-for="(item, index) in holdingsData" :key="index">
<view>
<text>索书号</text>
<text class="info-item-title">条码号</text>
<text>{{ item.barcode || '暂无' }}</text>
</view>
<view >
<text class="info-item-title">索书号</text>
<text>{{ item.callno || '暂无' }}</text>
</view>
<view>
<text>条码号</text>
<text>{{ item.barcode || '暂无' }}</text>
<text class="info-item-title">馆藏状态</text>
<text>{{ getStateText(item.state) }}</text>
</view>
<view>
<text>馆藏状态</text>
<text>{{ getStateText(item.state) }}</text>
<text class="info-item-title">所在馆</text>
<text>{{ item.orglib || '葛店图书馆'}}</text>
</view>
<view>
<text class="info-item-title">当前馆藏地</text>
<text>{{ item.orglocal || '葛店图书馆' }}</text>
</view>
</view>
</view>
@ -121,6 +129,7 @@ export default {
},
computed: {
// 在馆数量
// 馆藏状态,编目=1,在馆=2,借出=3,丢失=4,剔除=5,交换=6,赠送=7,装订=8,锁定=9,预借=10, 清点=12
inLibraryCount() {
return this.holdingsData.filter(item => item.state === 2).length;
},
@ -158,10 +167,13 @@ export default {
bookrecno: this.bookrecno
};
const res = await FetchFindbookByQuery(params);
console.log('bookrecno详情',res);
const apiData = res.data || {};
this.bookInfo = apiData.biblios || {};
console.log('bookrecno-bookInfo详情',this.bookInfo);
this.holdingsData = apiData.holdings || [];
console.log('bookrecno-holdingsData详情',this.holdingsData);
this.checkCollectStatus();
} catch (err) {
@ -286,20 +298,29 @@ export default {
.store-info-list {
background-color: #fff;
border-radius: 10px;
padding: 20px;
padding: 10px;
margin-bottom: 15px;
}
.store-info-item {
display: flex;
justify-content: space-between;
flex-direction: column;
font-size: 14px;
color: #666;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #eee;
}
.store-info-item view {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
line-height: 28px;
.info-item-title{
width: 80px;
font-weight: bold;
color: #333;
}
}
.content-tab {

75
subpkg/pages/book-list/book-list.vue

@ -20,6 +20,7 @@
<script>
import bookListItem from "@/components/book-list-item/book-list-item.vue";
import { FetchInitScreenBookRecommend } from '@/api/book';
import config from '@/utils/config';
export default {
components: {
@ -27,19 +28,44 @@ export default {
},
data() {
return {
bookList: [], // 图书列表
loading: false, // 加载状态
noMore: false // 是否到底
bookList: [],
loading: false,
noMore: false,
baseUrl: config.baseUrl
};
},
onLoad() {
this.getBookRecommendList();
},
// 下拉刷新
onPullDownRefresh() {
this.refresh();
},
methods: {
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
});
});
},
// 刷新
refresh() {
this.bookList = [];
@ -47,23 +73,52 @@ export default {
this.getBookRecommendList();
},
// 获取【真实】图书推荐列表(和首页同源)
// 获取列表 + 挨个依次加载图片
async getBookRecommendList() {
this.loading = true;
try {
const res = await FetchInitScreenBookRecommend({ libcode: '1201' });
this.bookList = res.data || [];
this.noMore = true; // 推荐数据一次性返回,没有分页
let books = res.data || [];
// 1. 先显示列表(图片为空,显示默认图)
books = books.map(item => {
item.base64Cover = '';
return item;
});
this.bookList = books;
this.noMore = true;
this.loading = false;
uni.stopPullDownRefresh();
// 2. 延迟一点点,等页面渲染完
setTimeout(() => {
this.loadImagesOneByOne(); // 挨个加载图片
}, 200);
} catch (err) {
console.error('获取推荐图书列表失败:', err);
} finally {
console.error(err);
this.loading = false;
uni.stopPullDownRefresh();
}
},
async loadImagesOneByOne() {
for (let i = 0; i < this.bookList.length; i++) {
let item = this.bookList[i];
if (!item.imgPath) continue;
// 加载当前这张
const url = this.baseUrl + '/api/fileRelevant/getImg?imgType=2&imgId=' + item.imgPath;
const base64 = await this.urlToBase64(url);
if (base64) {
item.base64Cover = base64;
this.$set(this.bookList, i, item); // 强制更新视图
}
}
},
goToDetail(item) {
// 把整个图书对象 转成字符串 带过去
uni.navigateTo({
url: "/subpkg/pages/book-detail/book-detail?bookData=" + encodeURIComponent(JSON.stringify(item))
})

36
subpkg/pages/collect-list/collect-list.vue

@ -81,27 +81,27 @@ export default {
pageSize: 5,
bookCollectList: [
{ isbn: '9787544741110', title: '人工智能基础——数学知识', cover: 'https://qiniu.aiyxlib.com/1606124577077' },
{ isbn: '9787539938032', title: 'Vim 8文本处理实战', cover: 'https://qiniu.aiyxlib.com/1606178450151' },
{ isbn: '9787536692930', title: 'Oracle从入门到精通', cover: 'https://qiniu.aiyxlib.com/1606123986028' }
// { isbn: '9787544741110', title: '人工智能基础——数学知识', cover: 'https://qiniu.aiyxlib.com/1606124577077' },
// { isbn: '9787539938032', title: 'Vim 8文本处理实战', cover: 'https://qiniu.aiyxlib.com/1606178450151' },
// { isbn: '9787536692930', title: 'Oracle从入门到精通', cover: 'https://qiniu.aiyxlib.com/1606123986028' }
],
allActivityList: [
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '读书分享会', time: "2025-11-03 ~2025-11-09", status: 1 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '作者见面会', time: "2025-11-03 ~2025-11-09", status: 0 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '新书签售会', time: "2025-11-05 ~2025-11-10", status: 1 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '文学讲座', time: "2025-11-06 ~2025-11-12", status: 1 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '亲子阅读日', time: "2025-11-07 ~2025-11-13", status: 1 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '书法展览', time: "2025-11-08 ~2025-11-14", status: 1 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '诗词大会', time: "2025-11-09 ~2025-11-15", status: 0 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '绘本故事', time: "2025-11-10 ~2025-11-16", status: 1 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '读书打卡', time: "2025-11-11 ~2025-11-17", status: 1 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '作家访谈', time: "2025-11-12 ~2025-11-18", status: 1 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '文化沙龙', time: "2025-11-13 ~2025-11-19", status: 1 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '手工创作', time: "2025-11-14 ~2025-11-20", status: 0 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '经典诵读', time: "2025-11-15 ~2025-11-21", status: 1 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '图书交换', time: "2025-11-16 ~2025-11-22", status: 1 },
{ imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '阅读竞赛', time: "2025-11-17 ~2025-11-23", status: 1 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '读书分享会', time: "2025-11-03 ~2025-11-09", status: 1 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '作者见面会', time: "2025-11-03 ~2025-11-09", status: 0 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '新书签售会', time: "2025-11-05 ~2025-11-10", status: 1 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '文学讲座', time: "2025-11-06 ~2025-11-12", status: 1 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '亲子阅读日', time: "2025-11-07 ~2025-11-13", status: 1 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '书法展览', time: "2025-11-08 ~2025-11-14", status: 1 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '诗词大会', time: "2025-11-09 ~2025-11-15", status: 0 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '绘本故事', time: "2025-11-10 ~2025-11-16", status: 1 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '读书打卡', time: "2025-11-11 ~2025-11-17", status: 1 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '作家访谈', time: "2025-11-12 ~2025-11-18", status: 1 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '文化沙龙', time: "2025-11-13 ~2025-11-19", status: 1 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '手工创作', time: "2025-11-14 ~2025-11-20", status: 0 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '经典诵读', time: "2025-11-15 ~2025-11-21", status: 1 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '图书交换', time: "2025-11-16 ~2025-11-22", status: 1 },
// { imgUrl: "https://qiniu.aiyxlib.com/1605060269830", title: '阅读竞赛', time: "2025-11-17 ~2025-11-23", status: 1 },
],
activityCollectList: [],
}

2
subpkg/pages/feedback-list/feedback-list.vue

@ -20,7 +20,7 @@
<view class="user-info">
<image
class="avatar"
:src="item.avatar || '/static/images/avatar.png'"
:src="item.avatar || '/static/images/logo.jpg'"
mode="aspectFill"
></image>
<text class="username">{{ item.nickname }}</text>

68
subpkg/pages/myLending/myLending.vue

@ -17,45 +17,47 @@
@animationfinish="onSwiperEnd"
@change="onSwiperChange"
>
<!-- 全部(历史借阅) -->
<!-- 在借中 -->
<swiper-item>
<view class="list-wrapper">
<uni-load-more status="loading" v-if="loadingAll" />
<uni-load-more status="loading" v-if="loadingLending" />
<view class="empty" v-else-if="allList.length === 0">
暂无全部记录
<view class="empty" v-else-if="lendingList.length === 0">
<uni-icons style="margin-left: 20px;" custom-prefix="iconfont" type="icon-kongshuju" size="80"></uni-icons>
<text style="margin-top: 20px;">暂无在借记录</text>
</view>
<lending-list-item
class="list-item-all"
v-for="(item, index) in allList"
class="list-item-lending"
v-for="(item, index) in lendingList"
:key="index"
:data="item"
:is-lending="false"
/>
<uni-load-more
v-if="allList.length > 0 && hasMoreAll"
:status="loadMoreStatusAll"
:is-lending="true"
/>
</view>
</swiper-item>
<!-- 在借中 -->
<!-- 全部(历史借阅) -->
<swiper-item>
<view class="list-wrapper">
<uni-load-more status="loading" v-if="loadingLending" />
<uni-load-more status="loading" v-if="loadingAll" />
<view class="empty" v-else-if="lendingList.length === 0">
暂无在借记录
<view class="empty" v-else-if="allList.length === 0">
<uni-icons style="margin-left: 20px;" custom-prefix="iconfont" type="icon-kongshuju" size="80"></uni-icons>
<text style="margin-top: 20px;">暂无历史借阅记录</text>
</view>
<lending-list-item
class="list-item-lending"
v-for="(item, index) in lendingList"
class="list-item-all"
v-for="(item, index) in allList"
:key="index"
:data="item"
:is-lending="true"
:is-lending="false"
/>
<uni-load-more
v-if="allList.length > 0 && hasMoreAll"
:status="loadMoreStatusAll"
/>
</view>
</swiper-item>
@ -75,8 +77,8 @@ export default {
return {
screenConfig: {},
tabData: [
{ label: '历史借阅', status: 'all' },
{ label: '在借中', status: 'lending' },
{ label: '历史借阅', status: 'all' },
],
currentIndex: 0,
@ -110,15 +112,15 @@ export default {
onPullDownRefresh() {
if (this.currentIndex === 0) {
this.getAllList(true);
} else {
this.getLendingList();
} else {
this.getAllList(true);
}
},
onReachBottom() {
if (this.currentIndex === 0 && this.hasMoreAll) {
this.getAllList();
this.getLendingList();
}
},
@ -154,7 +156,7 @@ export default {
};
// 默认加载第一个 tab
this.getAllList(true);
this.getLendingList(true);
} catch (err) {}
},
@ -248,17 +250,17 @@ export default {
tabClick(index) {
this.currentIndex = index;
if (index === 0) {
if (this.allList.length === 0) {
this.getAllList(true);
} else {
this.currentSwiperHeight = this.swiperHeightData.all || 600;
}
} else {
if (this.lendingList.length === 0) {
this.getLendingList();
} else {
this.currentSwiperHeight = this.swiperHeightData.lending || 600;
}
} else {
if (this.allList.length === 0) {
this.getAllList(true);
} else {
this.currentSwiperHeight = this.swiperHeightData.all || 600;
}
}
},
@ -292,9 +294,5 @@ export default {
padding: 10px;
box-sizing: border-box;
}
.empty {
text-align: center;
padding: 100px 0;
color: #999;
}
</style>

103
subpkg/pages/reader-card/reader-card.vue

@ -17,11 +17,18 @@
<image class="card-left-img" src="@/static/images/card-img2.png" mode="widthFix" />
<view class="card-right-info">
<text class="info-title">读者证号</text>
<text class="info-num">{{ item.bindValue }}</text>
<text class="info-num">{{ formatCardNo(item.bindValue) }}</text>
<text class="default-tag" v-if="item.bindDefault">默认证</text>
</view>
<!-- 编辑模式显示单选框 -->
<radio v-if="isEditMode" :value="item.bindValue" :checked="selectedValue === item.bindValue"/>
<!-- 非编辑模式才显示:设置默认 + 二维码 -->
<view class="card-setting" v-else>
<button v-if="!item.bindDefault" type="default" size="mini" @click.stop="setDefaultCard(item.bindValue)">设置默认</button>
<uni-icons class="erweima-style" custom-prefix="iconfont" type="icon-erweima" size="28" @click.stop="showQrcode(item.bindValue)"></uni-icons>
</view>
</view>
<view class="add-card-btn" @click="toAddReaderCard" v-if="!isEditMode">
@ -40,6 +47,22 @@
>
确认解绑
</button>
<!-- 二维码弹窗 -->
<uni-popup ref="popup" type="center" @maskClick="closePopup">
<view class="qrcode-popup">
<view class="qrcode-title">读者证二维码</view>
<w-qrcode
v-if="qrcodeText"
:value="qrcodeText"
:size="160"
foreground="#333"
background="#f5f5f5"
/>
<view class="qrcode-num">{{ qrcodeText }}</view>
<view class="close-btn" @click="closePopup">关闭</view>
</view>
</uni-popup>
</view>
</template>
@ -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;
}
}
</style>

122
subpkg/pages/user-info/user-info.vue

@ -5,7 +5,7 @@
<!-- 点击获取头像 -->
<button open-type="chooseAvatar" @chooseavatar="onChooseAvatar" class="avatar-btn">
<image v-if="avatarUrl" :src="avatarUrl" class="avatar-img"></image>
<image v-else src="@/static/images/avatar.png" class="avatar-img"></image>
<image v-else src="@/static/images/logo.jpg" class="avatar-img"></image>
<text class="tip-text">点击更换头像</text>
</button>
</view>
@ -16,7 +16,7 @@
<input
class="input"
type="nickname"
v-model="nickName"
v-model="nickname"
placeholder="请输入昵称"
/>
<uni-icons type="right" size="18" color="#ccc"></uni-icons>
@ -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;

16
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)
- 优化二维码生成逻辑

732
uni_modules/w-qrcode/components/w-qrcode/w-qrcode.vue

@ -0,0 +1,732 @@
<template>
<view class="w-qrcode" :style="rootStyle">
<!-- #ifdef MP-WEIXIN -->
<canvas
v-if="canvas2d"
:id="canvasId"
:canvas-id="canvasId"
type="2d"
:style="canvasStyle"
@longpress="onLongpress"
></canvas>
<canvas
v-else
:id="canvasId"
:canvas-id="canvasId"
:style="canvasStyle"
@longpress="onLongpress"
></canvas>
<!-- #endif -->
<!-- #ifdef H5 -->
<canvas
:id="canvasId"
:canvas-id="canvasId"
:style="canvasStyle"
:width="canvasWidth"
:height="canvasHeight"
@longpress="onLongpress"
></canvas>
<!-- #endif -->
<!-- #ifdef APP-PLUS -->
<canvas
:id="canvasId"
:canvas-id="canvasId"
:style="canvasStyle"
@longpress="onLongpress"
></canvas>
<!-- #endif -->
<!-- #ifdef MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ || MP-KUAISHOU || MP-LARK || MP-JD -->
<canvas
:id="canvasId"
:canvas-id="canvasId"
:style="canvasStyle"
@longpress="onLongpress"
></canvas>
<!-- #endif -->
<!-- 用于非Canvas渲染场景的备用方案 -->
<view v-if="showFallback" class="w-qrcode__fallback" :style="fallbackStyle">
<view
v-for="(row, rowIndex) in modules"
:key="rowIndex"
class="w-qrcode__row"
:style="rowStyle"
>
<view
v-for="(cell, colIndex) in row"
:key="colIndex"
class="w-qrcode__cell"
:style="cell ? darkCellStyle : lightCellStyle"
></view>
</view>
</view>
</view>
</template>
<script>
import QRCode from '../../js_sdk/qrcode.js';
// 生成唯一ID
let uid = 0;
const generateId = () => `w-qrcode-${++uid}-${Date.now()}`;
// 获取系统信息用于 rpx 转换
let systemInfo = null;
const getSystemInfo = () => {
if (!systemInfo) {
systemInfo = uni.getSystemInfoSync();
}
return systemInfo;
};
// 单位转换:支持 px、rpx、数字
const parseSize = (size) => {
if (typeof size === 'number') {
return size;
}
const str = String(size).trim();
if (str.endsWith('rpx')) {
const rpxValue = parseFloat(str);
const { windowWidth } = getSystemInfo();
return Math.round(rpxValue * windowWidth / 750);
}
if (str.endsWith('px')) {
return parseFloat(str);
}
return parseFloat(str) || 0;
};
export default {
name: 'w-qrcode',
emits: ['generated', 'error', 'longpress'],
props: {
// 要编码的文本
value: {
type: String,
default: ''
},
// 二维码尺寸(支持 px、rpx)
size: {
type: [Number, String],
default: 200
},
// 前景色
foreground: {
type: String,
default: '#000000'
},
// 背景色
background: {
type: String,
default: '#FFFFFF'
},
// 纠错级别 L/M/Q/H
errorCorrectionLevel: {
type: String,
default: 'M',
validator: (val) => ['L', 'M', 'Q', 'H'].includes(val)
},
// Logo 图片路径
logo: {
type: String,
default: ''
},
// Logo 尺寸(支持 px、rpx)
logoSize: {
type: [Number, String],
default: 50
},
// Logo 边距
logoMargin: {
type: [Number, String],
default: 5
},
// Logo 圆角
logoRadius: {
type: [Number, String],
default: 4
},
// Logo 背景色
logoBackground: {
type: String,
default: '#FFFFFF'
},
// 是否使用 Canvas 2D(仅微信小程序)
canvas2d: {
type: Boolean,
default: true
}
},
data() {
return {
canvasId: generateId(),
modules: [],
showFallback: false,
isReady: false,
ctx: null,
canvasNode: null,
pixelRatio: 1
};
},
computed: {
// 转换后的像素尺寸
sizePx() {
return Math.floor(parseSize(this.size));
},
logoSizePx() {
return Math.floor(parseSize(this.logoSize));
},
logoMarginPx() {
return Math.floor(parseSize(this.logoMargin));
},
logoRadiusPx() {
return Math.floor(parseSize(this.logoRadius));
},
// H5 canvas 属性
canvasWidth() {
return this.sizePx;
},
canvasHeight() {
return this.sizePx;
},
rootStyle() {
const size = this.sizePx;
return `width: ${size}px; height: ${size}px; display: inline-block;`;
},
canvasStyle() {
const size = this.sizePx;
return `width: ${size}px; height: ${size}px; display: block;`;
},
fallbackStyle() {
const size = this.sizePx;
return {
width: `${size}px`,
height: `${size}px`,
backgroundColor: this.background
};
},
// 计算每个模块的像素大小
cellSizePx() {
if (!this.modules.length) return 0;
const moduleCount = this.modules.length;
return Math.floor(this.sizePx / moduleCount);
},
// 偏移量(居中)
offset() {
if (!this.modules.length) return 0;
const actualSize = this.cellSizePx * this.modules.length;
return Math.floor((this.sizePx - actualSize) / 2);
},
rowStyle() {
return {
height: `${this.cellSizePx}px`
};
},
darkCellStyle() {
return {
width: `${this.cellSizePx}px`,
height: `${this.cellSizePx}px`,
backgroundColor: this.foreground
};
},
lightCellStyle() {
return {
width: `${this.cellSizePx}px`,
height: `${this.cellSizePx}px`,
backgroundColor: this.background
};
}
},
watch: {
value: {
handler(val) {
if (val) {
this.$nextTick(() => {
this.generate();
});
}
},
immediate: true
},
foreground() {
this.$nextTick(() => this.draw());
},
background() {
this.$nextTick(() => this.draw());
},
size() {
this.$nextTick(() => this.draw());
},
logo() {
this.$nextTick(() => this.draw());
},
logoSize() {
this.$nextTick(() => this.draw());
},
logoRadius() {
this.$nextTick(() => this.draw());
},
logoMargin() {
this.$nextTick(() => this.draw());
},
logoBackground() {
this.$nextTick(() => this.draw());
},
errorCorrectionLevel() {
this.$nextTick(() => this.generate());
}
},
mounted() {
this.pixelRatio = getSystemInfo().pixelRatio || 1;
this.isReady = true;
if (this.value) {
// 延迟执行确保 canvas 已渲染
setTimeout(() => {
this.generate();
}, 50);
}
},
methods: {
// 生成二维码数据
generate() {
if (!this.value) {
this.modules = [];
return;
}
try {
const result = QRCode.generate(this.value, {
errorCorrectionLevel: this.errorCorrectionLevel
});
this.modules = result.modules;
this.$emit('generated', {
version: result.version,
size: result.size,
errorCorrectionLevel: result.errorCorrectionLevel
});
this.$nextTick(() => {
setTimeout(() => {
this.draw();
}, 20);
});
} catch (error) {
console.error('[w-qrcode] Generate error:', error);
this.$emit('error', error);
}
},
// 绘制二维码
draw() {
if (!this.modules.length || !this.isReady) return;
// #ifdef MP-WEIXIN
if (this.canvas2d) {
this.drawCanvas2D();
return;
}
this.drawCanvasLegacy();
return;
// #endif
// #ifdef APP-NVUE
this.showFallback = true;
return;
// #endif
// #ifdef H5
this.drawCanvasH5();
return;
// #endif
// #ifdef APP-PLUS
this.drawCanvasLegacy();
return;
// #endif
// #ifndef MP-WEIXIN || APP-NVUE || H5 || APP-PLUS
this.drawCanvasLegacy();
// #endif
},
// H5 Canvas 绘制
drawCanvasH5() {
const query = uni.createSelectorQuery().in(this);
query.select(`#${this.canvasId}`)
.fields({ node: true, size: true })
.exec((res) => {
if (res && res[0] && res[0].node) {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
// 设置 canvas 实际像素大小
const dpr = this.pixelRatio;
canvas.width = this.sizePx * dpr;
canvas.height = this.sizePx * dpr;
ctx.scale(dpr, dpr);
this.canvasNode = canvas;
this.ctx = ctx;
this.renderQRCode(ctx, canvas, true);
} else {
// 降级处理
this.drawCanvasLegacy();
}
});
},
// Canvas 2D 绘制(微信小程序)
drawCanvas2D() {
const query = uni.createSelectorQuery().in(this);
query.select(`#${this.canvasId}`)
.fields({ node: true, size: true })
.exec((res) => {
if (!res || !res[0] || !res[0].node) {
this.drawCanvasLegacy();
return;
}
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
const dpr = this.pixelRatio;
canvas.width = this.sizePx * dpr;
canvas.height = this.sizePx * dpr;
ctx.scale(dpr, dpr);
this.canvasNode = canvas;
this.ctx = ctx;
this.renderQRCode(ctx, canvas, true);
});
},
// 旧版 Canvas 绘制
drawCanvasLegacy() {
const ctx = uni.createCanvasContext(this.canvasId, this);
this.ctx = ctx;
this.renderQRCodeLegacy(ctx);
},
// 统一的二维码渲染方法(Canvas 2D / H5)
renderQRCode(ctx, canvas, isCanvas2D) {
const size = this.sizePx;
const modules = this.modules;
const moduleCount = modules.length;
const cellSize = this.cellSizePx;
const offset = this.offset;
// 清空并绘制背景
ctx.clearRect(0, 0, size, size);
ctx.fillStyle = this.background;
ctx.fillRect(0, 0, size, size);
// 绘制二维码模块
ctx.fillStyle = this.foreground;
for (let row = 0; row < moduleCount; row++) {
for (let col = 0; col < moduleCount; col++) {
if (modules[row][col]) {
const x = offset + col * cellSize;
const y = offset + row * cellSize;
ctx.fillRect(x, y, cellSize, cellSize);
}
}
}
// 绘制 Logo
if (this.logo && canvas) {
this.renderLogo(ctx, canvas);
}
},
// 旧版 Canvas 渲染
renderQRCodeLegacy(ctx) {
const size = this.sizePx;
const modules = this.modules;
const moduleCount = modules.length;
const cellSize = this.cellSizePx;
const offset = this.offset;
// 绘制背景
ctx.setFillStyle(this.background);
ctx.fillRect(0, 0, size, size);
// 绘制二维码模块
ctx.setFillStyle(this.foreground);
for (let row = 0; row < moduleCount; row++) {
for (let col = 0; col < moduleCount; col++) {
if (modules[row][col]) {
const x = offset + col * cellSize;
const y = offset + row * cellSize;
ctx.fillRect(x, y, cellSize, cellSize);
}
}
}
// 绘制 Logo
if (this.logo) {
this.renderLogoLegacy(ctx);
} else {
ctx.draw();
}
},
// 绘制 Logo(Canvas 2D / H5)
renderLogo(ctx, canvas) {
const size = this.sizePx;
const logoSize = this.logoSizePx;
const logoMargin = this.logoMarginPx;
const logoRadius = this.logoRadiusPx;
const x = Math.floor((size - logoSize) / 2);
const y = Math.floor((size - logoSize) / 2);
const bgSize = logoSize + logoMargin * 2;
const bgX = Math.floor((size - bgSize) / 2);
const bgY = Math.floor((size - bgSize) / 2);
// 绘制 Logo 背景
ctx.fillStyle = this.logoBackground;
this.drawRoundRect(ctx, bgX, bgY, bgSize, bgSize, logoRadius + logoMargin / 2);
ctx.fill();
// 绘制 Logo 图片
const img = canvas.createImage ? canvas.createImage() : new Image();
img.onload = () => {
ctx.save();
this.drawRoundRect(ctx, x, y, logoSize, logoSize, logoRadius);
ctx.clip();
ctx.drawImage(img, x, y, logoSize, logoSize);
ctx.restore();
};
img.onerror = (err) => {
console.error('[w-qrcode] Logo load error:', err);
};
img.src = this.logo;
},
// 绘制 Logo(旧版 Canvas)
renderLogoLegacy(ctx) {
const size = this.sizePx;
const logoSize = this.logoSizePx;
const logoMargin = this.logoMarginPx;
const logoRadius = this.logoRadiusPx;
const x = Math.floor((size - logoSize) / 2);
const y = Math.floor((size - logoSize) / 2);
const bgSize = logoSize + logoMargin * 2;
const bgX = Math.floor((size - bgSize) / 2);
const bgY = Math.floor((size - bgSize) / 2);
// 绘制 Logo 背景
ctx.setFillStyle(this.logoBackground);
this.drawRoundRect(ctx, bgX, bgY, bgSize, bgSize, logoRadius + logoMargin / 2);
ctx.fill();
// 绘制 Logo 图片
ctx.save();
this.drawRoundRect(ctx, x, y, logoSize, logoSize, logoRadius);
ctx.clip();
ctx.drawImage(this.logo, x, y, logoSize, logoSize);
ctx.restore();
ctx.draw();
},
// 绘制圆角矩形
drawRoundRect(ctx, x, y, width, height, radius) {
if (radius <= 0) {
ctx.beginPath();
ctx.rect(x, y, width, height);
ctx.closePath();
return;
}
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.arcTo(x + width, y, x + width, y + radius, radius);
ctx.lineTo(x + width, y + height - radius);
ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
ctx.lineTo(x + radius, y + height);
ctx.arcTo(x, y + height, x, y + height - radius, radius);
ctx.lineTo(x, y + radius);
ctx.arcTo(x, y, x + radius, y, radius);
ctx.closePath();
},
// 长按事件
onLongpress(e) {
this.$emit('longpress', e);
},
// 保存到相册
saveToAlbum() {
return new Promise((resolve, reject) => {
// #ifdef MP-WEIXIN
if (this.canvas2d && this.canvasNode) {
uni.canvasToTempFilePath({
canvas: this.canvasNode,
success: (res) => {
this.saveImage(res.tempFilePath).then(resolve).catch(reject);
},
fail: reject
});
return;
}
// #endif
// #ifdef H5
if (this.canvasNode) {
try {
const dataURL = this.canvasNode.toDataURL('image/png');
// H5 下载图片
const link = document.createElement('a');
link.download = 'qrcode.png';
link.href = dataURL;
link.click();
resolve({ tempFilePath: dataURL });
return;
} catch (e) {
reject(e);
return;
}
}
// #endif
uni.canvasToTempFilePath({
canvasId: this.canvasId,
success: (res) => {
this.saveImage(res.tempFilePath).then(resolve).catch(reject);
},
fail: reject
}, this);
});
},
// 保存图片
saveImage(tempFilePath) {
return new Promise((resolve, reject) => {
// #ifdef H5
resolve({ tempFilePath });
return;
// #endif
// #ifndef H5
uni.saveImageToPhotosAlbum({
filePath: tempFilePath,
success: () => {
uni.showToast({
title: '保存成功',
icon: 'success'
});
resolve({ tempFilePath });
},
fail: (err) => {
if (err.errMsg && err.errMsg.includes('auth deny')) {
uni.showModal({
title: '提示',
content: '需要您授权保存相册权限',
showCancel: false
});
}
reject(err);
}
});
// #endif
});
},
// 获取临时文件路径
toTempFilePath(options = {}) {
return new Promise((resolve, reject) => {
const defaultOptions = {
fileType: 'png',
quality: 1,
...options
};
// #ifdef MP-WEIXIN
if (this.canvas2d && this.canvasNode) {
uni.canvasToTempFilePath({
...defaultOptions,
canvas: this.canvasNode,
success: resolve,
fail: reject
});
return;
}
// #endif
// #ifdef H5
if (this.canvasNode) {
try {
const dataURL = this.canvasNode.toDataURL('image/png');
resolve({ tempFilePath: dataURL });
return;
} catch (e) {
reject(e);
return;
}
}
// #endif
uni.canvasToTempFilePath({
...defaultOptions,
canvasId: this.canvasId,
success: resolve,
fail: reject
}, this);
});
},
// 获取 Base64
toDataURL(type = 'image/png', quality = 1) {
return new Promise((resolve, reject) => {
// #ifdef H5
if (this.canvasNode) {
try {
resolve(this.canvasNode.toDataURL(type, quality));
return;
} catch (e) {
reject(e);
return;
}
}
// #endif
reject(new Error('toDataURL is only supported on H5'));
});
}
}
};
</script>
<style scoped>
.w-qrcode {
position: relative;
overflow: hidden;
}
.w-qrcode__fallback {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.w-qrcode__row {
display: flex;
flex-direction: row;
}
.w-qrcode__cell {
flex-shrink: 0;
}
</style>

619
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 };

104
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"
}

229
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
<template>
<w-qrcode value="https://example.com" />
</template>
```
## 完整示例
```vue
<template>
<view class="container">
<!-- 基础用法 -->
<w-qrcode
value="https://example.com"
:size="200"
/>
<!-- 自定义颜色 -->
<w-qrcode
value="https://example.com"
:size="200"
foreground="#1989fa"
background="#f5f5f5"
/>
<!-- 带 Logo -->
<w-qrcode
ref="qrcodeRef"
value="https://example.com"
:size="200"
logo="/static/logo.png"
:logo-size="50"
:logo-radius="8"
error-correction-level="H"
@generated="onGenerated"
@longpress="onLongpress"
/>
<button @click="saveToAlbum">保存到相册</button>
</view>
</template>
<script>
export default {
methods: {
onGenerated(info) {
console.log('二维码生成成功:', info);
// { version: 2, size: 25, errorCorrectionLevel: 'H' }
},
onLongpress(e) {
console.log('长按事件');
},
saveToAlbum() {
this.$refs.qrcodeRef.saveToAlbum()
.then(res => {
console.log('保存成功', res);
})
.catch(err => {
console.error('保存失败', err);
});
}
}
};
</script>
```
## Vue3 组合式 API 示例
```vue
<template>
<w-qrcode
ref="qrcodeRef"
:value="qrValue"
:size="200"
/>
</template>
<script setup>
import { ref } from 'vue';
const qrcodeRef = ref(null);
const qrValue = ref('https://example.com');
const saveToAlbum = () => {
qrcodeRef.value.saveToAlbum();
};
</script>
```
## 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<string> |
| 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 图片
- 支持多平台
Loading…
Cancel
Save