图书馆小程序
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.

474 lines
14 KiB

2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
1 month ago
2 months ago
1 month ago
1 month ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
1 month ago
2 months ago
1 month ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
1 month ago
1 month ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
1 month ago
2 months ago
1 month ago
1 month ago
1 month ago
2 months ago
1 month ago
1 month ago
2 months ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
2 months ago
1 month ago
1 month ago
1 month ago
2 months ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
2 months ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
2 months ago
1 month ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month ago
1 month ago
1 month ago
2 months ago
1 month ago
2 months ago
1 month 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.png'"
  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. export default {
  97. data() {
  98. return {
  99. baseUrl: config.baseUrl,
  100. currentTab: 0,
  101. isCollected: false,
  102. bookrecno: '',
  103. opacUrl: '',
  104. fromRecommend: false,
  105. bookInfo: {},
  106. searchListInfo: {},
  107. holdingsData: [],
  108. dictionaryTree: {},
  109. libraryMap: {},
  110. locationMap: {}
  111. };
  112. },
  113. onLoad(options) {
  114. // 1. 首页/列表页 直接带完整 bookData 过来,优先走这个
  115. if (options.bookData) {
  116. const bookData = JSON.parse(decodeURIComponent(options.bookData));
  117. this.bookInfo = bookData;
  118. this.bookrecno = bookData.bookrecno || "";
  119. this.fromRecommend = options.fromRecommend === 'true';
  120. this.holdingsData = [];
  121. this.checkCollectStatus();
  122. return;
  123. }
  124. console.log('options',options)
  125. // 2. 检索页只传 bookrecno,请求接口拿详情
  126. if(options.searchData){
  127. const bookData = JSON.parse(decodeURIComponent(options.searchData));
  128. this.searchListInfo = bookData;
  129. this.bookrecno = bookData.bookrecno || "";
  130. }
  131. // 3. 如果从收藏列表进入,直接设置为已收藏状态
  132. if (options.isCollected === 'true') {
  133. this.isCollected = true;
  134. }
  135. this.fromRecommend = false;
  136. this.getOpacUrl();
  137. this.getDictionaryTree();
  138. },
  139. computed: {
  140. // 在馆数量
  141. // 馆藏状态,编目=1,在馆=2,借出=3,丢失=4,剔除=5,交换=6,赠送=7,装订=8,锁定=9,预借=10, 清点=12
  142. inLibraryCount() {
  143. return this.holdingsData.filter(item => item.state === 2).length;
  144. },
  145. // 借出数量(新增)
  146. lendCount() {
  147. return this.holdingsData.filter(item => item.state === 3).length;
  148. },
  149. // 第一个馆藏状态
  150. firstStateText() {
  151. if (this.holdingsData.length === 0) return '无馆藏';
  152. return this.getStateText(this.holdingsData[0].state);
  153. }
  154. },
  155. methods: {
  156. onImgError(e) {
  157. e.target.src = "/static/images/default-book.png";
  158. },
  159. // 获取字典项 - 馆代码
  160. async getDictionaryTree() {
  161. try {
  162. const res = await FetchDictionaryTree();
  163. console.log('dictionaryTree',res);
  164. const data = res.data || [];
  165. // 1. 找到分馆字典项
  166. const fgItem = data.find(item => item.dictionaryCode === 'FG' && item.dictionaryName === '分馆');
  167. if (fgItem && fgItem.childDictionarys) {
  168. // 建立分馆映射:dictionaryCode -> dictionaryName
  169. this.libraryMap = {};
  170. fgItem.childDictionarys.forEach(child => {
  171. this.libraryMap[child.dictionaryCode] = child.dictionaryName;
  172. });
  173. }
  174. // 2. 找到馆藏地点字典项
  175. const gcdItem = data.find(item => item.dictionaryCode === 'GCD' && item.dictionaryName === '馆藏地点');
  176. if (gcdItem && gcdItem.childDictionarys) {
  177. // 建立馆藏地点映射:dictionaryCode -> dictionaryName
  178. this.locationMap = {};
  179. gcdItem.childDictionarys.forEach(child => {
  180. this.locationMap[child.dictionaryCode] = child.dictionaryName;
  181. });
  182. }
  183. this.dictionaryTree = data;
  184. } catch (err) {
  185. console.error('获取字典树失败', err);
  186. }
  187. },
  188. // 获取配置
  189. async getOpacUrl() {
  190. try {
  191. const res = await FetchInitScreenSetting({ libcode: config.LIB_CODE });
  192. this.opacUrl = res.data.opac_url?.context || '';
  193. this.getBookDetail();
  194. } catch (err) {}
  195. },
  196. // 获取图书详情
  197. async getBookDetail() {
  198. if (!this.bookrecno || !this.opacUrl) return;
  199. uni.showLoading({ title: '加载中...' });
  200. try {
  201. const params = {
  202. opacUrl: this.opacUrl,
  203. bookrecno: this.bookrecno
  204. };
  205. const res = await FetchFindbookByQuery(params);
  206. // console.log('bookrecno详情',res);
  207. const apiData = res.data || {};
  208. this.bookInfo = apiData.biblios || {};
  209. // console.log('bookrecno-bookInfo详情',this.bookInfo);
  210. this.holdingsData = apiData.holdings || [];
  211. // console.log('bookrecno-holdingsData详情',this.holdingsData);
  212. this.checkCollectStatus();
  213. } catch (err) {
  214. console.error(err);
  215. } finally {
  216. uni.hideLoading();
  217. }
  218. },
  219. // 馆藏状态文字
  220. getStateText(state) {
  221. const map = {
  222. 1: '编目',
  223. 2: '在馆',
  224. 3: '借出',
  225. 4: '丢失',
  226. 5: '剔除',
  227. 6: '交换',
  228. 7: '赠送',
  229. 8: '装订',
  230. 9: '锁定',
  231. 10: '预借',
  232. 12: '清点'
  233. }
  234. return map[state] || '未知';
  235. },
  236. // 获取所在馆名称
  237. getLibraryName(orglib) {
  238. if (!orglib) return '葛店经济技术开发区图书馆';
  239. return this.libraryMap[orglib] || orglib || '葛店经济技术开发区图书馆';
  240. },
  241. // 获取馆藏地名称
  242. getLocationName(orglocal) {
  243. if (!orglocal) return '葛店图书馆';
  244. return this.locationMap[orglocal] || orglocal || '葛店图书馆';
  245. },
  246. // 收藏状态检查(如果已经通过参数传入则跳过)
  247. checkCollectStatus() {
  248. if (this.isCollected) return;
  249. const list = uni.getStorageSync('collectList') || [];
  250. this.isCollected = list.includes(this.bookrecno);
  251. },
  252. async toggleCollect() {
  253. const openId = await getOpenId();
  254. if (!openId) {
  255. uni.showToast({ title: '获取用户信息失败', icon: 'none' });
  256. return;
  257. }
  258. if (this.isCollected) {
  259. try {
  260. console.log(' this.searchListInfo.id',this.searchListInfo.id);
  261. const res = await FetchCancelCollectionBook({id: this.searchListInfo.id});
  262. if (res.code === 200) {
  263. this.isCollected = false;
  264. // 清空收藏记录 id,避免影响后续收藏操作
  265. if (this.searchListInfo) {
  266. this.searchListInfo.id = null;
  267. }
  268. // 设置取消收藏标记,通知收藏列表页面刷新
  269. uni.setStorageSync('needRefreshCollect', true);
  270. uni.showToast({ title: '取消收藏', icon: 'success' });
  271. } else {
  272. uni.showToast({ title: res.message || '取消收藏失败', icon: 'none' });
  273. }
  274. } catch (err) {
  275. console.error('取消收藏图书失败', err);
  276. uni.showToast({ title: '取消收藏失败', icon: 'none' });
  277. }
  278. } else {
  279. try {
  280. const params = {
  281. ...this.searchListInfo,
  282. openid: openId,
  283. libcode: config.LIB_CODE
  284. };
  285. const res = await FetchCollectionBook(params);
  286. if (res.code === 200) {
  287. this.isCollected = true;
  288. // 保存收藏记录的 id,供后续取消收藏使用
  289. if (res.data && res.data.id) {
  290. this.searchListInfo.id = res.data.id;
  291. }
  292. uni.showToast({ title: '收藏成功', icon: 'success' });
  293. } else {
  294. uni.showToast({ title: res.message || '收藏失败', icon: 'none' });
  295. }
  296. } catch (err) {
  297. console.error('收藏图书失败', err);
  298. uni.showToast({ title: '收藏失败', icon: 'none' });
  299. }
  300. }
  301. }
  302. },
  303. onShareAppMessage() {
  304. return {
  305. title: this.bookInfo.title || '图书详情',
  306. path: '/subpkg/pages/book-detail/book-detail?bookrecno=' + this.bookrecno,
  307. imageUrl: this.bookInfo.cover
  308. };
  309. }
  310. };
  311. </script>
  312. <style lang="scss" scoped>
  313. .detail-container {
  314. padding: 15px;
  315. background-color: #f5f5f5;
  316. min-height: 100vh;
  317. padding-bottom: 60px;
  318. }
  319. .article-detail-container {
  320. display: flex;
  321. background-color: #fff;
  322. border-radius: 10px;
  323. padding: 20px;
  324. margin-bottom: 12px;
  325. }
  326. .article-detail-left {
  327. flex: 1;
  328. }
  329. .article-detail-title {
  330. font-size: 20px;
  331. font-weight: bold;
  332. color: #333;
  333. margin-bottom: 10px;
  334. }
  335. .article-detail-info {
  336. font-size: 14px;
  337. color: #666;
  338. line-height: 1.5;
  339. }
  340. .article-detail-right {
  341. width: 110px;
  342. height: 150px;
  343. margin-left: 15px;
  344. }
  345. .article-detail-right image {
  346. width: 100%;
  347. height: 100%;
  348. border-radius: 6px;
  349. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  350. }
  351. .book-store-info {
  352. background-color: #fff;
  353. border-radius: 10px;
  354. padding: 20px;
  355. display: flex;
  356. justify-content: space-around;
  357. margin-bottom: 12px;
  358. }
  359. .book-store-info-item {
  360. display: flex;
  361. flex-direction: column;
  362. align-items: center;
  363. }
  364. .store-txt1 {
  365. font-size: 22px;
  366. font-weight: bold;
  367. color: #2b85e4;
  368. }
  369. .store-txt2 {
  370. font-size: 13px;
  371. color: #999;
  372. margin-top: 4px;
  373. }
  374. .store-info-list {
  375. background-color: #fff;
  376. border-radius: 10px;
  377. padding: 10px;
  378. margin-bottom: 15px;
  379. }
  380. .store-info-item {
  381. display: flex;
  382. flex-direction: column;
  383. font-size: 14px;
  384. color: #666;
  385. padding: 10px;
  386. margin-bottom: 10px;
  387. border: 1px solid #eee;
  388. border-radius: 10px;
  389. &:last-child {
  390. margin-bottom: 0;
  391. }
  392. }
  393. .store-info-item view {
  394. display: flex;
  395. align-items: center;
  396. justify-content: flex-start;
  397. line-height: 28px;
  398. .info-item-title{
  399. width: 80px;
  400. font-weight: bold;
  401. color: #333;
  402. }
  403. }
  404. .content-tab {
  405. display: flex;
  406. background-color: #fff;
  407. border-radius: 10px;
  408. overflow: hidden;
  409. margin-bottom: 12px;
  410. }
  411. .tab-item {
  412. flex: 1;
  413. text-align: center;
  414. padding: 15px 0;
  415. font-size: 15px;
  416. color: #666;
  417. position: relative;
  418. }
  419. .tab-item.active {
  420. color: #2b85e4;
  421. font-weight: bold;
  422. }
  423. .tab-item.active::after {
  424. content: "";
  425. position: absolute;
  426. bottom: 0;
  427. left: 50%;
  428. transform: translateX(-50%);
  429. width: 30px;
  430. height: 3px;
  431. background-color: #2b85e4;
  432. border-radius: 2px;
  433. }
  434. .content-item {
  435. background-color: #fff;
  436. border-radius: 10px;
  437. padding: 20px;
  438. font-size: 15px;
  439. color: #333;
  440. line-height: 1.6;
  441. }
  442. .detail-bottom{
  443. justify-content: space-around;
  444. }
  445. </style>