图书馆小程序
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

579 lines
16 KiB

5 months ago
5 months ago
4 months ago
5 months ago
4 months ago
4 months ago
5 months ago
4 months ago
4 weeks ago
4 months ago
5 months ago
4 months ago
5 months ago
4 months ago
5 months ago
4 months ago
5 months ago
4 months ago
5 months ago
4 months ago
4 months ago
5 months ago
4 months ago
4 months ago
5 months ago
4 months ago
5 months ago
4 months ago
4 months ago
4 months ago
4 months ago
5 months ago
5 months ago
5 months ago
4 months ago
5 months ago
4 months ago
5 months ago
5 months ago
4 months ago
4 months ago
5 months ago
4 months ago
4 months ago
4 months ago
4 months ago
5 months ago
4 months ago
4 months ago
5 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
5 months ago
4 months ago
4 weeks ago
4 months ago
4 months ago
4 months ago
4 months ago
5 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
5 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
4 months ago
5 months ago
4 months ago
5 months ago
4 months ago
5 months ago
5 months ago
4 months ago
4 months ago
4 months ago
4 months ago
5 months ago
5 months ago
5 months ago
4 weeks ago
5 months ago
4 months ago
5 months ago
4 months ago
5 months ago
4 months ago
4 months ago
4 months ago
5 months ago
4 months ago
5 months ago
4 months ago
3 weeks ago
4 months ago
4 weeks ago
4 months ago
  1. <template>
  2. <view class="detail-container">
  3. <!-- 书籍头部信息 -->
  4. <view class="article-detail-container">
  5. <view class="article-detail-left">
  6. <view class="article-detail-title">{{ bookInfo.title || bookInfo.name || '暂无书名' }}</view>
  7. <view class="article-detail-info">
  8. <view class="article-detail-author">作者{{ bookInfo.author || '暂无' }}</view>
  9. <view class="article-detail-time">出版社{{ bookInfo.publisher || '暂无' }}</view>
  10. <view class="article-detail-time">ISBN{{ bookInfo.isbn || '暂无' }}</view>
  11. <view class="article-detail-time">出版时间{{ bookInfo.pubdate || '暂无' }}</view>
  12. </view>
  13. </view>
  14. <view class="article-detail-right">
  15. <image
  16. class="img-item"
  17. :src="bookInfo.cover || bookInfo.base64Cover || '/static/images/default-book.jpg'"
  18. mode="widthFix"
  19. @error="onImgError"
  20. ></image>
  21. </view>
  22. </view>
  23. <!-- 馆藏信息 -->
  24. <view v-if="holdingsData.length!==0" class="book-store-info">
  25. <view class="book-store-info-item">
  26. <text class="store-txt1">{{ holdingsData.length || 0 }}</text>
  27. <text class="store-txt2">馆藏总数</text>
  28. </view>
  29. <view class="book-store-info-item">
  30. <text class="store-txt1">{{ inLibraryCount }}</text>
  31. <text class="store-txt2">在馆</text>
  32. </view>
  33. <view class="book-store-info-item">
  34. <text class="store-txt1">{{ lendCount }}</text>
  35. <text class="store-txt2">借出</text>
  36. </view>
  37. </view>
  38. <!-- 索书号/条码号 -->
  39. <view v-if="holdingsData.length!==0" class="store-info-list">
  40. <view class="store-info-item" v-for="(item, index) in holdingsData" :key="index">
  41. <view>
  42. <text class="info-item-title">条码号</text>
  43. <text>{{ item.barcode || '暂无' }}</text>
  44. </view>
  45. <view >
  46. <text class="info-item-title">索书号</text>
  47. <text>{{ item.callno || '暂无' }}</text>
  48. </view>
  49. <view>
  50. <text class="info-item-title">馆藏状态</text>
  51. <text>{{ getStateText(item.state) }}</text>
  52. </view>
  53. <view>
  54. <text class="info-item-title">所在馆</text>
  55. <text>{{ getLibraryName(item.orglib) }}</text>
  56. </view>
  57. <view>
  58. <text class="info-item-title">当前馆藏地</text>
  59. <text>{{ getLocationName(item.orglocal) }}</text>
  60. </view>
  61. </view>
  62. </view>
  63. <!-- TAB 选项卡 -->
  64. <view class="content-tab">
  65. <view class="tab-item" :class="{active: currentTab === 0}" @click="currentTab = 0">
  66. 内容简介
  67. </view>
  68. <!-- <view class="tab-item" :class="{active: currentTab === 1}" @click="currentTab = 1">
  69. 作者简介
  70. </view> -->
  71. </view>
  72. <!-- TAB 内容 -->
  73. <view class="content-item" v-if="currentTab === 0">
  74. {{ bookInfo.summary || bookInfo.explain || '暂无内容简介' }}
  75. </view>
  76. <view class="content-item" v-if="currentTab === 1">
  77. {{ bookInfo.authorbio || '暂无作者简介' }}
  78. </view>
  79. <view class="detail-bottom">
  80. <button open-type="share" class="handle-btn">
  81. <uni-icons custom-prefix="iconfont" type="icon-fenxiang01" size="20"></uni-icons>
  82. <text class="share-text">分享</text>
  83. </button>
  84. <button v-if="!fromRecommend" class="handle-btn" @click="toggleCollect">
  85. <uni-icons :type="isCollected ? 'heart-filled' : 'heart'" size="20" color="#ff4444"></uni-icons>
  86. <text class="share-text">{{ isCollected ? '已收藏' : '收藏' }}</text>
  87. </button>
  88. </view>
  89. </view>
  90. </template>
  91. <script>
  92. import { FetchInitScreenSetting } from '@/api/user';
  93. import { FetchFindbookByQuery, FetchDictionaryTree,FetchCollectionBook,FetchCancelCollectionBook } from '@/api/book';
  94. import { getOpenId } from '@/utils/storage';
  95. import config from '@/utils/config';
  96. import { fetchBookCoverBase64 } from '@/utils/bookCover';
  97. export default {
  98. data() {
  99. return {
  100. baseUrl: config.baseUrl,
  101. currentTab: 0,
  102. isCollected: false,
  103. bookrecno: '',
  104. opacUrl: '',
  105. fromRecommend: false,
  106. bookInfo: {},
  107. searchListInfo: {},
  108. holdingsData: [],
  109. dictionaryTree: {},
  110. libraryMap: {},
  111. locationMap: {}
  112. };
  113. },
  114. onLoad(options) {
  115. console.log('options',options)
  116. const fromShare = options.fromShare === 'true'
  117. // 1. 首页/推荐页 bookData
  118. if (options.bookData) {
  119. const bookData = JSON.parse(decodeURIComponent(options.bookData));
  120. this.bookInfo = bookData;
  121. this.bookrecno = bookData.bookrecno || "";
  122. this.fromRecommend = options.fromRecommend === 'true';
  123. this.holdingsData = [];
  124. this.checkCollectStatus();
  125. // 分享过来,有bookrecno强制拉完整馆藏
  126. if(fromShare && this.bookrecno){
  127. this.getOpacUrl();
  128. }
  129. return;
  130. }
  131. // 2. 分享传入 bookrecno 优先走这里
  132. if(options.bookrecno){
  133. this.bookrecno = options.bookrecno
  134. this.fromRecommend = false
  135. this.bookInfo = {}
  136. this.getOpacUrl();
  137. return;
  138. }
  139. // 3. 搜索列表 searchData
  140. if(options.searchData){
  141. const bookData = JSON.parse(decodeURIComponent(options.searchData));
  142. this.searchListInfo = bookData;
  143. this.bookrecno = bookData.bookrecno || "";
  144. this.bookInfo = bookData;
  145. }
  146. // 4. 收藏列表标记
  147. if (options.isCollected === 'true') {
  148. this.isCollected = true;
  149. }
  150. this.fromRecommend = false;
  151. // 有bookrecno就请求详情,修复原逻辑漏调用getOpacUrl
  152. if(this.bookrecno){
  153. this.getOpacUrl();
  154. }
  155. },
  156. computed: {
  157. // 在馆数量
  158. // 馆藏状态,编目=1,在馆=2,借出=3,丢失=4,剔除=5,交换=6,赠送=7,装订=8,锁定=9,预借=10, 清点=12
  159. inLibraryCount() {
  160. return this.holdingsData.filter(item => item.state === 2).length;
  161. },
  162. // 借出数量(新增)
  163. lendCount() {
  164. return this.holdingsData.filter(item => item.state === 3).length;
  165. },
  166. // 第一个馆藏状态
  167. firstStateText() {
  168. if (this.holdingsData.length === 0) return '无馆藏';
  169. return this.getStateText(this.holdingsData[0].state);
  170. }
  171. },
  172. methods: {
  173. onImgError(e) {
  174. e.target.src = "/static/images/default-book.jpg";
  175. },
  176. // 加载图书封面
  177. async loadBookCover() {
  178. console.log('this.bookInfo',this.bookInfo)
  179. if (!this.bookInfo.isbn) return;
  180. try {
  181. const coverUrl = await fetchBookCoverBase64(this.bookInfo.isbn);
  182. if (coverUrl) {
  183. this.$set(this.bookInfo, 'cover', coverUrl);
  184. }
  185. } catch (error) {
  186. console.error('获取封面失败:', error);
  187. }
  188. },
  189. // 获取字典项 - 馆代码
  190. async getDictionaryTree() {
  191. try {
  192. const res = await FetchDictionaryTree();
  193. console.log('dictionaryTree',res);
  194. const data = res.data || [];
  195. // 1. 找到分馆字典项
  196. const fgItem = data.find(item => item.dictionaryCode === 'FG' && item.dictionaryName === '分馆');
  197. if (fgItem && fgItem.childDictionarys) {
  198. // 建立分馆映射:dictionaryCode -> dictionaryName
  199. this.libraryMap = {};
  200. fgItem.childDictionarys.forEach(child => {
  201. this.libraryMap[child.dictionaryCode] = child.dictionaryName;
  202. });
  203. }
  204. // 2. 找到馆藏地点字典项
  205. const gcdItem = data.find(item => item.dictionaryCode === 'GCD' && item.dictionaryName === '馆藏地点');
  206. if (gcdItem && gcdItem.childDictionarys) {
  207. // 建立馆藏地点映射:dictionaryCode -> dictionaryName
  208. this.locationMap = {};
  209. gcdItem.childDictionarys.forEach(child => {
  210. this.locationMap[child.dictionaryCode] = child.dictionaryName;
  211. });
  212. }
  213. this.dictionaryTree = data;
  214. } catch (err) {
  215. console.error('获取字典树失败', err);
  216. }
  217. },
  218. // 获取配置
  219. async getOpacUrl() {
  220. try {
  221. const res = await FetchInitScreenSetting({ libcode: config.LIB_CODE });
  222. this.opacUrl = res.data.opac_url?.context || '';
  223. this.getDictionaryTree();
  224. this.getBookDetail();
  225. } catch (err) {}
  226. },
  227. // 获取图书详情
  228. async getBookDetail() {
  229. if (!this.bookrecno || !this.opacUrl) return;
  230. uni.showLoading({ title: '加载中...' });
  231. try {
  232. const params = {
  233. opacUrl: this.opacUrl,
  234. bookrecno: this.bookrecno
  235. };
  236. const res = await FetchFindbookByQuery(params);
  237. const apiData = res.data || {};
  238. this.bookInfo = apiData.biblios || {};
  239. this.holdingsData = apiData.holdings || [];
  240. // ✅重点:分享进入场景补齐searchListInfo,防止收藏点击报错
  241. if(!this.searchListInfo || !this.searchListInfo.bookrecno){
  242. this.searchListInfo = {
  243. bookrecno: this.bookInfo.bookrecno,
  244. title: this.bookInfo.title,
  245. author: this.bookInfo.author,
  246. isbn: this.bookInfo.isbn
  247. }
  248. }
  249. // 加载封面
  250. await this.loadBookCover();
  251. // 检查收藏状态
  252. this.checkCollectStatus();
  253. } catch (err) {
  254. console.error(err);
  255. } finally {
  256. uni.hideLoading();
  257. }
  258. },
  259. // 馆藏状态文字
  260. getStateText(state) {
  261. const map = {
  262. 1: '编目',
  263. 2: '在馆',
  264. 3: '借出',
  265. 4: '丢失',
  266. 5: '剔除',
  267. 6: '交换',
  268. 7: '赠送',
  269. 8: '装订',
  270. 9: '锁定',
  271. 10: '预借',
  272. 12: '清点'
  273. }
  274. return map[state] || '未知';
  275. },
  276. // 获取所在馆名称
  277. getLibraryName(orglib) {
  278. if (!orglib) return '葛店经济技术开发区图书馆';
  279. return this.libraryMap[orglib] || orglib || '葛店经济技术开发区图书馆';
  280. },
  281. // 获取馆藏地名称
  282. getLocationName(orglocal) {
  283. if (!orglocal) return '葛店图书馆';
  284. return this.locationMap[orglocal] || orglocal || '葛店图书馆';
  285. },
  286. // 收藏状态检查(如果已经通过参数传入则跳过)
  287. checkCollectStatus() {
  288. if (this.isCollected) return;
  289. const list = uni.getStorageSync('collectList') || [];
  290. this.isCollected = list.includes(this.bookrecno);
  291. },
  292. async toggleCollect() {
  293. const openId = await getOpenId();
  294. if (!openId) {
  295. uni.showToast({ title: '获取用户信息失败', icon: 'none' });
  296. return;
  297. }
  298. // 防御,分享进来searchListInfo为空保护
  299. if(!this.searchListInfo || !this.searchListInfo.bookrecno){
  300. uni.showToast({title:'数据未加载完成,请稍候',icon:'none'})
  301. return
  302. }
  303. if (this.isCollected) {
  304. try {
  305. const res = await FetchCancelCollectionBook({id: this.searchListInfo.id});
  306. if (res.code === 200) {
  307. this.isCollected = false;
  308. // 清空收藏记录 id,避免影响后续收藏操作
  309. if (this.searchListInfo) {
  310. this.searchListInfo.id = null;
  311. }
  312. // 设置取消收藏标记,通知收藏列表页面刷新
  313. uni.setStorageSync('needRefreshCollect', true);
  314. uni.showToast({ title: '取消收藏', icon: 'success' });
  315. } else {
  316. uni.showToast({ title: res.message || '取消收藏失败', icon: 'none' });
  317. }
  318. } catch (err) {
  319. console.error('取消收藏图书失败', err);
  320. uni.showToast({ title: '取消收藏失败', icon: 'none' });
  321. }
  322. } else {
  323. try {
  324. const params = {
  325. ...this.searchListInfo,
  326. openid: openId,
  327. libcode: config.LIB_CODE
  328. };
  329. const res = await FetchCollectionBook(params);
  330. if (res.code === 200) {
  331. this.isCollected = true;
  332. // 保存收藏记录的 id,供后续取消收藏使用
  333. if (res.data && res.data.id) {
  334. this.searchListInfo.id = res.data.id;
  335. }
  336. uni.showToast({ title: '收藏成功', icon: 'success' });
  337. } else {
  338. uni.showToast({ title: res.message || '收藏失败', icon: 'none' });
  339. }
  340. } catch (err) {
  341. console.error('收藏图书失败', err);
  342. uni.showToast({ title: '收藏失败', icon: 'none' });
  343. }
  344. }
  345. }
  346. },
  347. onShareAppMessage() {
  348. const title = this.bookInfo.title || this.bookInfo.name || '图书详情';
  349. let path = '';
  350. let imageUrl = '';
  351. const cover = this.bookInfo.cover || this.bookInfo.base64Cover;
  352. // 微信分享不支持base64图片,过滤
  353. if(cover && !cover.startsWith('data:image')){
  354. imageUrl = cover;
  355. }
  356. if(this.bookrecno){
  357. //优先bookrecno,参数短,不会url超长
  358. path = `/subpkg/pages/book-detail/book-detail?bookrecno=${encodeURIComponent(this.bookrecno)}&fromShare=true`
  359. }else{
  360. path = "/subpkg/pages/book-detail/book-detail?bookData=" + encodeURIComponent(JSON.stringify(this.bookInfo)) + "&fromShare=true"
  361. }
  362. return {
  363. title,
  364. path,
  365. imageUrl
  366. };
  367. },
  368. //分享到朋友圈
  369. onShareTimeline(){
  370. const title = this.bookInfo.title || this.bookInfo.name || '图书详情';
  371. let query = ''
  372. if(this.bookrecno){
  373. query = `bookrecno=${encodeURIComponent(this.bookrecno)}&fromShare=true`
  374. }else{
  375. query = `bookData=${encodeURIComponent(JSON.stringify(this.bookInfo))}&fromShare=true`
  376. }
  377. let imageUrl = ''
  378. const cover = this.bookInfo.cover
  379. if(cover && !cover.startsWith('data:image')){
  380. imageUrl = cover
  381. }
  382. return {
  383. title,
  384. query,
  385. imageUrl
  386. }
  387. }
  388. };
  389. </script>
  390. <style lang="scss" scoped>
  391. .detail-container {
  392. padding: 15px;
  393. background-color: #f5f5f5;
  394. min-height: 100vh;
  395. padding-bottom: 70px;
  396. }
  397. .article-detail-container {
  398. display: flex;
  399. background-color: #fff;
  400. border-radius: 10px;
  401. padding: 20px;
  402. margin-bottom: 12px;
  403. }
  404. .article-detail-left {
  405. flex: 1;
  406. }
  407. .article-detail-title {
  408. font-size: 20px;
  409. font-weight: bold;
  410. color: #333;
  411. margin-bottom: 10px;
  412. }
  413. .article-detail-info {
  414. font-size: 14px;
  415. color: #666;
  416. line-height: 1.5;
  417. }
  418. .article-detail-right {
  419. width: 110px;
  420. height: 150px;
  421. margin-left: 15px;
  422. }
  423. .article-detail-right image {
  424. width: 100%;
  425. height: 100%;
  426. border-radius: 6px;
  427. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  428. }
  429. .book-store-info {
  430. background-color: #fff;
  431. border-radius: 10px;
  432. padding: 20px;
  433. display: flex;
  434. justify-content: space-around;
  435. margin-bottom: 12px;
  436. }
  437. .book-store-info-item {
  438. display: flex;
  439. flex-direction: column;
  440. align-items: center;
  441. }
  442. .store-txt1 {
  443. font-size: 22px;
  444. font-weight: bold;
  445. color: #2b85e4;
  446. }
  447. .store-txt2 {
  448. font-size: 13px;
  449. color: #999;
  450. margin-top: 4px;
  451. }
  452. .store-info-list {
  453. background-color: #fff;
  454. border-radius: 10px;
  455. padding: 10px;
  456. margin-bottom: 15px;
  457. }
  458. .store-info-item {
  459. display: flex;
  460. flex-direction: column;
  461. font-size: 14px;
  462. color: #666;
  463. padding: 10px;
  464. margin-bottom: 10px;
  465. border: 1px solid #eee;
  466. border-radius: 10px;
  467. &:last-child {
  468. margin-bottom: 0;
  469. }
  470. }
  471. .store-info-item view {
  472. display: flex;
  473. align-items: center;
  474. justify-content: flex-start;
  475. line-height: 28px;
  476. .info-item-title{
  477. width: 80px;
  478. font-weight: bold;
  479. color: #333;
  480. }
  481. }
  482. .content-tab {
  483. display: flex;
  484. background-color: #fff;
  485. border-radius: 10px;
  486. overflow: hidden;
  487. margin-bottom: 12px;
  488. }
  489. .tab-item {
  490. flex: 1;
  491. text-align: center;
  492. padding: 15px 0;
  493. font-size: 15px;
  494. color: #666;
  495. position: relative;
  496. }
  497. .tab-item.active {
  498. color: #2b85e4;
  499. font-weight: bold;
  500. }
  501. .tab-item.active::after {
  502. content: "";
  503. position: absolute;
  504. bottom: 0;
  505. left: 50%;
  506. transform: translateX(-50%);
  507. width: 30px;
  508. height: 3px;
  509. background-color: #2b85e4;
  510. border-radius: 2px;
  511. }
  512. .content-item {
  513. background-color: #fff;
  514. border-radius: 10px;
  515. padding: 20px;
  516. font-size: 15px;
  517. color: #333;
  518. line-height: 1.6;
  519. }
  520. .detail-bottom{
  521. position: fixed;
  522. bottom:0;
  523. left:0;
  524. right:0;
  525. height: 50px;
  526. padding: 0 15px;
  527. background:#fff;
  528. display:flex;
  529. justify-content: space-around;
  530. align-items:center;
  531. box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.05);
  532. }
  533. .handle-btn{
  534. display:flex;
  535. align-items:center;
  536. background:#fff;
  537. border:none;
  538. }
  539. .share-text{
  540. margin-left:4px;
  541. font-size:14px;
  542. }
  543. </style>