Browse Source

收藏/留言

master
xuhuajiao 1 month ago
parent
commit
11190ea2a1
  1. 51
      api/user.js
  2. 62
      components/book-list-item/book-list-item.vue
  3. 33
      subpkg/pages/collect-list/collect-list.vue
  4. 49
      subpkg/pages/feedback-detail/feedback-detail.vue
  5. 158
      subpkg/pages/feedback-list/feedback-list.vue
  6. 46
      subpkg/pages/feedback/feedback.vue
  7. 184
      utils/bookCover.js

51
api/user.js

@ -102,3 +102,54 @@ export function FetchUnbindReadCard(data) {
data
});
}
// 读者留言列表
// ?libcode=1&openId=1&page=0&phone=1&readCardNo=1&readName=1&size=10&title=1
export function FetchInitReaderMessage(data) {
return request({
url: '/api/weixin/initReaderMessage',
data
});
}
// 根据id查看读者留言详情
// ?id=1
export function FetchReaderMessageDetailsById(data) {
return request({
url: '/api/weixin/getReaderMessageDetailsById',
data
});
}
// 读者留言
// {
// "libcode": "馆代码",
// "openid": "string",
// "phone": "联系方式",
// "readCardNo": "读者证号",
// "readName": "读者名称",
// "suggestion": "建议",
// "title": "标题"
// }
export function FetchReaderMessage(data) {
return request({
url: '/api/weixin/readerMessage',
method: 'POST',
data
});
}
// 回复读者留言
// {
// "id": 0,
// "reply": "回复"
// }
export function FetchReplyReaderMessage(data) {
return request({
url: '/api/weixin/replyReaderMessage',
method: 'POST',
data
});
}

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

@ -4,7 +4,7 @@
<view class="item-box-left">
<image
class="img-item"
:src="data.base64Cover || data.cover || data.imgCover || '/static/images/default-book.png'"
:src="actualCover"
mode="scaleToFill"
@error="onImgError"
></image>
@ -19,8 +19,6 @@
</template>
<script>
import config from '@/utils/config';
export default {
name: "book-list-item",
props: {
@ -29,12 +27,61 @@ export default {
required: true
}
},
data() {
return {
baseUrl: config.baseUrl
computed: {
actualCover() {
// base64Cover > cover > imgCover
// base64Cover base64 使
if (this.data.base64Cover) {
return this.data.base64Cover;
}
// cover imgCover
const cover = this.data.cover || this.data.imgCover;
//
if (cover && this.isValidCoverLink(cover)) {
return cover;
}
//
return '/static/images/default-book.png';
}
},
methods: {
/**
* 检查封面链接是否有效
* @param {string} coverlink - 封面链接
* @returns {boolean} - 是否有效
*/
isValidCoverLink(coverlink) {
if (!coverlink || typeof coverlink !== 'string') {
return false;
}
// http
if (coverlink.indexOf('http') === -1) {
return false;
}
// 访
const blockedDomains = [
'doubanio.com',
'douban.com'
];
//
for (const domain of blockedDomains) {
if (coverlink.includes(domain)) {
return false;
}
}
return true;
},
/**
* 图片加载失败处理
*/
onImgError(e) {
e.target.src = "/static/images/default-book.png";
}
@ -49,11 +96,9 @@ export default {
display: flex;
justify-content: flex-start;
align-items: flex-start;
// margin: 0 10px;
padding: 10px;
background-color: #fff;
border-radius: 6px;
// box-shadow: 0 1px 4px rgba(0,0,0,0.08);
border-bottom: 1px solid #f4f4f4;
.item-box-left {
margin-right: 8px;
@ -96,7 +141,6 @@ export default {
display: flex;
justify-content: space-between;
align-items: center;
}
}
}

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

@ -28,6 +28,7 @@ import BookListItem from "@/components/book-list-item/book-list-item.vue";
import { FetchFindAllBookCollectionByOpenId } from '@/api/book';
import { getOpenId } from '@/utils/storage';
import config from '@/utils/config';
import { loadBookCovers } from '@/utils/bookCover';
export default {
components: {
@ -67,8 +68,20 @@ export default {
if (this.noMore || this.isLoading) return;
this.loadMore();
},
methods: {
// getTest(){
// // 34352,36153
// wx.request({
// url: `http://218.200.95.251:8081/opac/getExistsBookrecnoList/41534`,
// method: 'POST',
// success: (res) => {
// console.log(res.data || []);
// },
// fail: () => {
// resolve([]);
// }
// });
// },
async loadData() {
this.isLoading = true;
try {
@ -90,6 +103,9 @@ export default {
this.bookCollectList = res.data.content || [];
this.noMore = (res.data.content || []).length < this.pageSize;
this.hasLoadedAll = this.noMore;
//
this.loadCoversForList();
} else {
uni.showToast({ title: res.message || '获取收藏列表失败', icon: 'none' });
}
@ -142,6 +158,11 @@ export default {
const newData = res.data.content || [];
this.bookCollectList = [...this.bookCollectList, ...newData];
this.noMore = newData.length < this.pageSize;
//
if (newData.length > 0) {
await this.loadCoversForList();
}
} else {
this.pageNum--;
uni.showToast({ title: res.message || '加载更多失败', icon: 'none' });
@ -154,6 +175,16 @@ export default {
this.isLoading = false;
}
},
async loadCoversForList() {
await loadBookCovers(this.bookCollectList, (index, coverUrl) => {
console.log('index', index);
console.log('coverUrl', coverUrl);
if (this.bookCollectList[index]) {
// 使 Vue.set
this.$set(this.bookCollectList[index], 'cover', coverUrl);
}
});
},
onItemClick(item) {
console.log('item',item)
uni.navigateTo({

49
subpkg/pages/feedback-detail/feedback-detail.vue

@ -1,9 +1,9 @@
<template>
<view class="feedback-detail-page">
<view class="section">
<text class="subject">{{ detail.subject }}</text>
<view class="section-content">{{ detail.content }}</view>
<text class="time">{{ detail.createTime }}</text>
<text class="subject">{{ detail.title }}</text>
<view class="section-content">{{ detail.suggestion }}</view>
<text class="time">{{ formatTimestamp(detail.suggestionTime) }}</text>
</view>
<view class="section reply-section">
@ -15,6 +15,8 @@
</template>
<script>
import { FetchReaderMessageDetailsById } from '@/api/user';
export default {
data() {
return {
@ -24,6 +26,47 @@ export default {
onLoad(options) {
if (options.item) {
this.detail = JSON.parse(decodeURIComponent(options.item));
this.getReaderMessageDetailsById();
}
},
methods: {
async getReaderMessageDetailsById(){
try {
// id
if (!this.detail.id) {
console.warn('缺少留言 ID');
return;
}
const res = await FetchReaderMessageDetailsById({
id: this.detail.id
});
console.log(res);
if (res && res.code === 200) {
this.detail = res.data || {};
} else {
uni.showToast({ title: res?.msg || '获取留言详情失败', icon: 'none' });
}
} catch (error) {
console.error('获取留言详情异常:', error);
uni.showToast({ title: '获取留言详情失败', icon: 'none' });
}
},
formatTimestamp(timestamp) {
if (!timestamp || typeof timestamp !== 'number') {
return '暂无时间';
}
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
}
};

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

@ -1,6 +1,11 @@
<template>
<view class="feedback-page">
<view class="feedback-list">
<scroll-view
class="feedback-list"
scroll-y
@scrolltolower="loadMore"
:scroll-with-animation="true"
>
<view class="empty" v-if="feedbackList.length === 0">
<uni-icons style="margin-left: 20px;" custom-prefix="iconfont" type="icon-kongshuju" size="80" color="#ccc"></uni-icons>
<text style="margin-top: 20px;">暂无留言</text>
@ -12,11 +17,17 @@
@click="goDetail(item)"
>
<view class="feedback-content">
<text class="subject">{{ item.subject }}</text>
<text class="create-time">{{ item.createTime }}</text>
<text class="subject">{{ item.title }}</text>
<text class="create-time">{{ formatTimestamp(item.suggestionTime) }}</text>
</view>
</view>
<view class="loading-more" v-if="loading">
<text>加载中...</text>
</view>
<view class="no-more" v-if="noMore">
<text>没有更多留言了~</text>
</view>
</scroll-view>
<view class="write-btn-box">
<button class="write-btn" @click="goWriteComment">去留言</button>
</view>
@ -24,44 +35,131 @@
</template>
<script>
import { FetchInitReaderMessage } from '@/api/user';
import config from '@/utils/config';
import { getOpenId } from '@/utils/storage';
export default {
data() {
return {
feedbackList: [
{
id: 1,
subject: '建议增加更多儿童绘本',
content: '图书馆的儿童区绘本种类不够丰富,希望能增加更多适合3-6岁儿童的绘本读物。',
createTime: '2026-05-10 14:30',
reply: '感谢您的建议!我们已将增加儿童绘本纳入采购计划,预计下季度会新增500余册绘本。'
feedbackList: [],
pageNum: 0,
pageSize: 10,
loading: false,
noMore: false,
};
},
{
id: 2,
subject: '自习室空调温度过低',
content: '最近自习室空调开得太冷了,希望能适当调高温度,或者提供毯子。',
createTime: '2026-05-08 09:15',
reply: ''
onLoad() {
this.getInitReaderMessage();
},
{
id: 3,
subject: '希望延长周末开放时间',
content: '周末很多人想来看书,但是下午5点就关门了,希望能够延长到晚上8点。',
createTime: '2026-05-05 16:45',
reply: '您的建议已收悉,我们正在研究调整开放时间的可行性,感谢支持。'
onShow() {
//
const needRefresh = uni.getStorageSync('needRefreshFeedback');
uni.removeStorageSync('needRefreshFeedback');
if (needRefresh) {
this.refreshList();
}
]
};
},
methods: {
async getInitReaderMessage(){
const openId = await getOpenId();
if (!openId) {
uni.showToast({ title: '获取用户信息失败', icon: 'none' });
return;
}
// const currentReaderCard = await getCurrentReaderCard();
const res = await FetchInitReaderMessage({
libcode: config.LIB_CODE,
openId: openId,
// readCardNo: currentReaderCard.readCardNo,
// readName: '',
page: this.pageNum,
size: this.pageSize,
});
if (res.code === 200) {
this.feedbackList = res.data.content || [];
} else {
uni.showToast({ title: res.message || '获取留言列表失败', icon: 'none' });
}
},
goWriteComment() {
uni.navigateTo({
url: '/subpkg/pages/feedback/feedback'
});
},
goDetail(item) {
console.log(item);
uni.navigateTo({
url: '/subpkg/pages/feedback-detail/feedback-detail?item=' + encodeURIComponent(JSON.stringify(item))
});
},
formatTimestamp(timestamp) {
if (!timestamp || typeof timestamp !== 'number') {
return '暂无时间';
}
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
},
async loadMore() {
if (this.loading || this.noMore) return;
this.loading = true;
this.pageNum++;
try {
const openId = await getOpenId();
if (!openId) {
uni.showToast({ title: '获取用户信息失败', icon: 'none' });
return;
}
const res = await FetchInitReaderMessage({
libcode: config.LIB_CODE,
openId: openId,
page: this.pageNum,
size: this.pageSize,
});
if (res.code === 200) {
const newData = res.data.content || [];
if (newData.length > 0) {
this.feedbackList = [...this.feedbackList, ...newData];
} else {
this.noMore = true;
}
} else {
uni.showToast({ title: res.message || '获取留言列表失败', icon: 'none' });
this.pageNum--;
}
} catch (error) {
console.error('加载更多失败:', error);
this.pageNum--;
uni.showToast({ title: '加载更多失败', icon: 'none' });
} finally {
this.loading = false;
}
},
async refreshList() {
//
this.pageNum = 0;
this.noMore = false;
this.loading = false;
//
await this.getInitReaderMessage();
uni.pageScrollTo({
scrollTop: 0,
duration: 300
});
uni.stopPullDownRefresh();
}
}
};
@ -75,8 +173,6 @@ export default {
.feedback-list {
height: calc(100vh - 80px);
overflow-y: scroll;
padding: 10px;
}
.empty {
@ -87,7 +183,7 @@ export default {
background-color: #fff;
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
margin: 0 10px 10px 10px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.06);
}
@ -125,4 +221,12 @@ export default {
font-size: 14px;
padding: 4px 0;
}
.loading-more,
.no-more {
text-align: center;
padding: 15px 0;
font-size: 12px;
color: #999;
}
</style>

46
subpkg/pages/feedback/feedback.vue

@ -53,7 +53,9 @@
</template>
<script>
import { getCurrentReaderCard } from '@/utils/storage';
import { FetchReaderMessage } from '@/api/user';
import config from '@/utils/config';
import { getCurrentReaderCard,getOpenId } from '@/utils/storage';
const USER_KEY = 'user-info';
@ -99,7 +101,7 @@ export default {
return phoneReg.test(phone);
},
onBtnClick() {
async onBtnClick() {
const { name, readerCard, subject, phone, content } = this.formData;
if (!name || name.trim().length === 0) {
@ -153,19 +155,45 @@ export default {
uni.showLoading({
title: '提交中'
});
try {
const openId = await getOpenId();
if (!openId) {
uni.showToast({ title: '获取用户信息失败', icon: 'none' });
this.isLoading = false;
return;
}
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '提交成功',
icon: 'success',
mask: true
const res = await FetchReaderMessage({
libcode: config.LIB_CODE,
openid: openId,
phone,
readCardNo: readerCard,
readName: name,
suggestion: content,
title: subject
});
if (res.code !== 200) {
uni.showToast({ title: res.message || '留言提交失败', icon: 'none' });
this.isLoading = false;
return;
}else{
//
uni.setStorageSync('needRefreshFeedback', true);
uni.showToast({ title: '留言提交成功', icon: 'success' });
setTimeout(() => {
uni.navigateBack();
}, 1500);
}, 1000);
}
} catch (err) {
console.error('留言提交失败', err);
uni.showToast({ title: err.message || '留言提交失败', icon: 'none' });
} finally {
this.isLoading = false;
}
}
}
};

184
utils/bookCover.js

@ -0,0 +1,184 @@
/**
* 获取已加载的 bookrecno 缓存
* @returns {Set} - 已加载的 bookrecno 集合
*/
function getLoadedBookrecnos() {
const loadedList = wx.getStorageSync('loadedBookrecnos') || [];
return new Set(loadedList);
}
/**
* 标记 bookrecno 已加载
* @param {Array} bookrecnoList - 已加载的 bookrecno 列表
*/
function markBookrecnoLoaded(bookrecnoList) {
const loadedSet = getLoadedBookrecnos();
bookrecnoList.forEach(bookrecno => loadedSet.add(bookrecno));
wx.setStorageSync('loadedBookrecnos', Array.from(loadedSet));
}
/**
* 通过远程接口获取封面基于 ISBN
* @param {Array} isbnList - ISBN 列表
* @returns {Promise<Object>} - 封面数据映射 { isbn: coverUrl }
*/
export async function fetchCoversByISBN(isbnList) {
if (!isbnList || isbnList.length === 0) {
return {};
}
// 移除 ISBN 中的连字符
const cleanIsbns = isbnList.map(isbn => isbn.replace(/-/g, ''));
const isbns = cleanIsbns.join(',');
const url = `https://book-resource.dataesb.com/websearch/metares?glc=EGD0755036&cmdACT=getImages&type=0&isbns=${isbns}`;
return new Promise((resolve) => {
wx.request({
url: url,
method: 'GET',
success: (res) => {
const result = {};
let list = [];
try {
// 获取响应数据
let responseData = res.data;
// 如果是字符串,尝试解析为 JSON
if (typeof responseData === 'string') {
// 移除可能的 JSONP 包装括号 ({"result":...})
const trimmed = responseData.trim();
if (trimmed.startsWith('(') && trimmed.endsWith(')')) {
responseData = trimmed.slice(1, -1);
}
responseData = JSON.parse(responseData);
}
// 获取 result 数组
if (responseData && responseData.result && Array.isArray(responseData.result)) {
list = responseData.result;
}
} catch (error) {
console.error('解析封面数据失败:', error);
list = [];
}
list.forEach(item => {
if (item.isbn && item.coverlink && item.coverlink.indexOf('http') !== -1) {
// 存储时移除连字符,便于匹配
result[item.isbn.replace(/-/g, '')] = item.coverlink;
}
});
resolve(result);
},
fail: () => {
resolve({});
}
});
});
}
/**
* 获取服务器存在的 bookrecno 列表
* @param {Array} bookrecnoList - bookrecno 列表
* @returns {Promise<Object>} - 存在的 bookrecno 映射 { bookrecno: coverUrl }
*/
export async function fetchCoversByBookrecno(bookrecnoList) {
if (!bookrecnoList || bookrecnoList.length === 0) {
return {};
}
const bookrecnos = bookrecnoList.join(',');
return new Promise((resolve) => {
wx.request({
url: `http://218.200.95.251:8081/opac/getExistsBookrecnoList/${bookrecnos}`,
method: 'POST',
success: (res) => {
const result = {};
console.log('bookrecnoList',res)
const existsList = res.data || [];
console.log('existsList',existsList)
existsList.forEach(bookrecno => {
const timestamp = new Date().getTime();
result[bookrecno] = `http://218.200.95.251:8081/opac/bscover/${bookrecno}?timestamp=${timestamp}`;
});
// 标记已加载
markBookrecnoLoaded(existsList);
resolve(result);
},
fail: () => {
resolve({});
}
});
});
}
/**
* 加载图书封面核心方法
* @param {Array} bookList - 图书数据列表
* @param {Function} callback - 回调函数接收 (index, coverUrl) 参数
* @returns {Promise<void>}
*/
export async function loadBookCovers(bookList, callback) {
if (!bookList || bookList.length === 0 || typeof callback !== 'function') {
return;
}
try {
// 分离需要查询的 ISBN 和 bookrecno
const loadedSet = getLoadedBookrecnos();
const isbnMap = new Map(); // index -> isbn
const bookrecnoMap = new Map(); // index -> bookrecno(未加载过的)
bookList.forEach((book, index) => {
if (book.isbn) {
isbnMap.set(index, book.isbn.replace(/-/g, ''));
}
if (book.bookrecno && !loadedSet.has(book.bookrecno)) {
bookrecnoMap.set(index, book.bookrecno);
}
});
// 并行请求 ISBN 和 bookrecno 的封面
const [isbnCovers, bookrecnoCovers] = await Promise.all([
fetchCoversByISBN(Array.from(isbnMap.values())),
fetchCoversByBookrecno(Array.from(bookrecnoMap.values()))
]);
// 更新封面
bookList.forEach((book, index) => {
let coverUrl = null;
// 优先使用 ISBN 获取的封面
const isbn = book.isbn ? book.isbn.replace(/-/g, '') : null;
if (isbn && isbnCovers[isbn]) {
coverUrl = isbnCovers[isbn];
}
// 如果 ISBN 没获取到,尝试 bookrecno
else if (book.bookrecno && bookrecnoCovers[book.bookrecno]) {
coverUrl = bookrecnoCovers[book.bookrecno];
}
if (coverUrl) {
callback(index, coverUrl);
}
});
} catch (error) {
console.error('加载图书封面失败:', error);
}
}
/**
* 清除封面加载缓存
*/
export function clearCoverCache() {
wx.removeStorageSync('loadedBookrecnos');
}
export default {
loadBookCovers,
clearCoverCache,
fetchCoversByISBN,
fetchCoversByBookrecno
};
Loading…
Cancel
Save