Parcourir la source

Merge branch 'feature/book' into develop

cc12458 il y a 1 an
Parent
commit
2ed317bab8

+ 75 - 1
src/api/knowledge.js

@@ -318,4 +318,78 @@ export function getMerD(data) {
         method: 'post',
         data
     })
-};
+};
+
+export function getBookCategories() {
+    return request1({
+        url: '/book/bookClassify/list',
+        method: 'post',
+    }).then(res => res.Data.map(item => ({label: item['classifyName'], value: item['classifyId']})));
+}
+
+export function getBookList(query) {
+    const data = {page: 1, limit: 40};
+    for (const [key, value] of Object.entries(query)) {
+        if (value) { data[key] = value; }
+    }
+    return request1({
+        url: '/book/book/list',
+        method: 'post',
+        data,
+    }).then(res => {
+        if (res.Data == null) res.Data = {current: 1, total: 0, records: []};
+        return {
+            page: res.Data.current,
+            total: res.Data.total,
+            data: res.Data.records,
+        };
+    });
+}
+
+export function getBookListOfMy() {
+    return request1({
+        url: '/book/book/myBookshelf',
+        method: 'post',
+    }).then(res => {
+        return {
+            page: 1,
+            total: res.Data.length,
+            data: res.Data.map(item => Object.assign(item, {id: item.bookId})),
+        };
+    });
+}
+
+export function getBookListOfRecommend(id) {
+    return request1({
+        url: '/book/book/recommend',
+        method: 'post',
+        data: {id},
+    }).then(res => res.Data.filter(item => item.id !== id));
+}
+
+export function getBook(id) {
+    return request1({
+        url: `/book/book/detail`,
+        method: 'post',
+        data: {id},
+    }).then(res => res.Data);
+}
+
+export function getBookContent(data) {
+    return request1({
+        url: `/book/bookContent/detail`,
+        method: 'post',
+        data,
+    }).then(res => res.Data.content || '(空)');
+}
+
+export function setBookStatus(id, status) {
+    return request1({
+        url: `/book/book/addBookshelf`,
+        method: 'post',
+        data: {
+            ...status,
+            bookId: id,
+        },
+    });
+}

BIN
src/assets/book-cover.png


+ 18 - 0
src/router/knowledge.js

@@ -96,4 +96,22 @@ export default [{
             pftitle: '名医名方'
         }
     },
+    {
+        path: 'knowledge/book',
+        name: 'knowledgeBook',
+        component: () => import('@/views/knowledge/Book.vue'),
+        meta: {
+            title: '经典医书',
+            pftitle: '知识学习'
+        }
+    },
+    {
+        path: 'knowledge/book/detail',
+        name: 'knowledgeBookDetail',
+        component: () => import('@/views/knowledge/BookD.vue'),
+        meta: {
+            title: '经典医书/详情',
+            pftitle: '知识学习'
+        }
+    },
 ]

+ 50 - 0
src/utils/format.js

@@ -128,4 +128,54 @@ export function subtractMonths(value, date = new Date()) {
     }
 
     return _date;
+}
+
+export function numberToChinese(num) {
+    if (num === 0) return "零";
+
+    const units = ["", "十", "百", "千"];
+    const bigUnits = ["", "万", "亿", "兆"];
+    const digits = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
+
+    let result = "";
+    let section = 0; // 当前处理的节(每四位为一节)
+
+    while (num > 0) {
+        let part = num % 10000; // 取出当前节的四位数字
+        let partStr = "";
+
+        if (part !== 0) {
+            let unitIndex = 0; // 当前处理的位(个、十、百、千)
+            while (part > 0) {
+                let digit = part % 10; // 当前位的数字
+                if (digit !== 0) {
+                    partStr = digits[digit] + units[unitIndex] + partStr;
+                } else if (!partStr.startsWith(digits[0])) {
+                    partStr = digits[0] + partStr;
+                }
+                part = Math.floor(part / 10);
+                unitIndex++;
+            }
+        }
+
+        if (partStr !== "") {
+            partStr += bigUnits[section];
+            result = partStr + result;
+        } else if (result !== "" && !result.startsWith(digits[0])) {
+            result = digits[0] + result;
+        }
+
+        num = Math.floor(num / 10000);
+        section++;
+    }
+
+    // 处理特殊情况:连续的零
+    result = result.replace(/零{2,}/g, "零");
+
+    // 处理特殊情况:末尾的零
+    if (result.endsWith("零")) {
+        result = result.slice(0, -1);
+    }
+
+    return result;
 }

+ 199 - 0
src/views/knowledge/Book.vue

@@ -0,0 +1,199 @@
+<script>
+import {getBookCategories, getBookList, getBookListOfMy} from '@/api/knowledge';
+
+export default {
+  name: 'KnowledgeBook',
+  data() {
+    return {
+      categories: [],
+      books: [],
+      query: {
+        index: '',
+        keyWord: '',
+        page: 1,
+      },
+
+      keyword: '',
+      loading: false,
+      searched: false,
+      containerHeight: 0,
+    };
+  },
+  created() {
+    this.list();
+    this.getCategories();
+  },
+  mounted() {
+    setTimeout(() => {
+      try { this.containerHeight = `${this.$el.parentElement.getBoundingClientRect().height - 10}px`; } catch (e) { }
+    }, 500);
+  },
+  methods: {
+    async getCategories() {
+      const categories = await getBookCategories().catch(() => []);
+      this.categories = [{label: '我的书架', value: -1}, ...categories];
+    },
+    async list() {
+      let loading;
+      if (this.query.page === 1) {
+        this.loading = true;
+        loading = this.$loading({
+          lock: true,
+          text: '拼命加载中',
+          spinner: 'el-icon-loading',
+          background: 'rgba(0, 0, 0, 0.7)',
+        });
+      }
+      try {
+        const {index, ...query} = this.query;
+        const classifyId = (this.categories[index] || {value: ''}).value;
+        const result = classifyId === -1 ? await getBookListOfMy() : await getBookList({
+          ...query,
+          classifyId,
+        });
+        this.query.page = result.page;
+        this.query.total = result.total;
+        const books = this.query.page === 1 ? [] : this.books;
+        this.books = [...books, ...result.data];
+      } catch (error) {}
+      this.loading = false;
+      if (loading) loading.close();
+      if (this.query.page === 1) { setTimeout(() => { this.$refs.scroller.$el.scrollTo({top: 0, behavior: 'smooth'}); }, 200); }
+    },
+    load() {
+      if (this.books.length >= this.query.total) return;
+      this.query.page += 1;
+      this.list();
+    },
+    onSearch() {
+      try {if (this.categories[this.query.index].value === -1) {this.query.index = '';}} catch (e) { }
+      this.query.keyWord = this.keyword;
+      this.query.page = 1;
+      this.list();
+      this.searched = !!this.query.keyWord;
+    },
+    onselect(index) {
+      index = index.toString();
+      this.query.index = index === this.query.index ? '' : index;
+      this.query.page = 1;
+      this.list();
+    },
+    onPreview(book) {
+      this.$router.push({path: `/index/knowledge/book/detail?id=${book.id}`});
+    },
+  },
+};
+</script>
+
+<template>
+  <div class="knowledge-book-list" :style="{height: containerHeight}">
+    <el-row style="height: 100%;overflow: hidden;">
+      <el-col style="height: 100%;overflow: hidden;" :sm="4" :xl="3">
+        <el-menu style="height: 100%;overflow-y: auto" :default-active="query.index">
+          <el-menu-item v-for="(item, index) in categories" :key="item.value" :index="index + ''"
+                        style="font-size: 18px;"
+                        @click="onselect(index)">
+            <span slot="title">{{ item.label }}</span>
+          </el-menu-item>
+        </el-menu>
+      </el-col>
+      <el-col style="height: 100%;overflow: hidden;display: flex;flex-direction: column;" :sm="20" :xl="21">
+        <div class="search-wrapper" style="flex: none;">
+          <el-input clearable placeholder="输入书名、作者、朝代" v-model="keyword"
+                    @clear="searched && onSearch()"
+                    @keyup.enter.native="onSearch()"></el-input>
+          <el-button type="primary" @click="onSearch()">查询</el-button>
+        </div>
+        <el-row ref="scroller" class="books-wrapper" style="flex: auto;" v-infinite-scroll="load">
+          <el-empty v-if="!books.length" description="暂无数据"></el-empty>
+          <el-col v-for="(book, index) in books" :key="book.id" :sm="8" :xl="6">
+            <div class="book-wrapper" @click="onPreview(book)">
+              <div class="cover">
+                <div class="name">{{ book.bookName }}</div>
+              </div>
+              <div class="content" style="font-weight: 700;">{{ book.bookName }}</div>
+              <div class="content">
+                <span v-if="book.dynasty">{{ book.dynasty }}</span>
+                <span>{{ book.author || '佚名' }}</span>
+              </div>
+              <div style="min-height: 32px;">
+                <el-tag v-if="book.readProgress">已读 {{ book.readProgress }}%</el-tag>
+              </div>
+            </div>
+          </el-col>
+        </el-row>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.knowledge-book-list {
+  overflow: hidden;
+}
+
+.search-wrapper {
+  display: flex;
+  justify-content: center;
+  padding: 12px 0;
+
+  .el-input {
+    max-width: 300px;
+
+    &::v-deep .el-input__inner {
+      border-top-right-radius: 0;
+      border-bottom-right-radius: 0;
+    }
+
+    + .el-button {
+      border-top-left-radius: 0;
+      border-bottom-left-radius: 0;
+    }
+  }
+}
+
+.books-wrapper {
+  height: 100%;
+  overflow-y: auto;
+  .book-wrapper {
+    padding: 24px;
+  }
+}
+
+.book-wrapper {
+  $zoom: 0.6;
+  cursor: pointer;
+
+  .cover {
+    position: relative;
+    width: 272px * $zoom;
+    height: 400px * $zoom;
+    background: url("../../assets/book-cover.png") no-repeat center / 100%;
+  }
+
+  .name {
+    position: absolute;
+    top: 30px * $zoom;
+    right: 40px * $zoom;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 48px * $zoom;
+    height: 234px * $zoom;
+    font-size: max(24px * $zoom, 12px);
+    font-weight: 700;
+    letter-spacing: 0.3em;
+    -ms-writing-mode: tb-rl;
+    writing-mode: vertical-rl;
+  }
+
+  .content {
+    margin: 8px 0;
+    font-size: 16px;
+    span + span::before {
+      content: '·';
+      margin: 0 3px;
+    }
+  }
+}
+</style>

+ 256 - 0
src/views/knowledge/BookD.vue

@@ -0,0 +1,256 @@
+<script>
+import {getBook, getBookContent, getBookListOfRecommend, setBookStatus} from '@/api/knowledge.js';
+import {numberToChinese} from '@/utils/format';
+
+export default {
+  name: 'KnowledgeBookDetail',
+  data() {
+    return {
+      book: {id: '', catalogueList: []},
+      section: [],
+      recommend: [],
+
+      containerHeight: 0,
+    };
+  },
+  computed: {
+    collection() {
+      return +this.book['isBookshelf'] === 1;
+    },
+  },
+  created() {
+    this.book.id = this.$route.query.id;
+    this.onPreview();
+  },
+  mounted() {
+    setTimeout(() => {
+      try { this.containerHeight = `${this.$el.parentElement.getBoundingClientRect().height - 10}px`; } catch (e) { }
+    }, 200);
+  },
+  methods: {
+    async getBook() {
+      this.book = await getBook(this.book.id);
+      let index = 1;
+      const section = [];
+      for (const {catalogueName: name, catalogueId: id} of this.book.catalogueList) {
+        section.push({
+          id,
+          name: name === '序' ? name : `第${numberToChinese(index)}章 ${name}`,
+          content: '',
+          dirty: false,
+        });
+        if (name !== '序') { index += 1;}
+      }
+      this.section = section;
+    },
+    async getRecommend() {
+      try {
+        this.recommend = await getBookListOfRecommend(this.book.id);
+      } catch (e) {}
+    },
+    async getContent(data, index, section) {
+      try {
+        let content = await getBookContent({bookId: this.book.id, catalogueId: data.id});
+        content = `&emsp;&emsp;${(content || '').replace(/\n|\r/g, '<br>&emsp;&emsp;')}`;
+        this.$set(this.section, index, Object.assign(data, {dirty: true, error: '', content}));
+      } catch (e) {
+        this.$set(this.section, index, Object.assign(data, {dirty: true, error: `出错了(${e})`}));
+      }
+    },
+    async collected() {
+      try {
+        const isBookshelf = this.collection ? '0' : '1';
+        await setBookStatus(this.book.id, {isBookshelf});
+        this.$set(this.book, 'isBookshelf', isBookshelf);
+        this.$message.success('操作成功');
+      } catch (e) {
+
+      }
+    },
+    load(ids) {
+      if (ids && !Array.isArray(ids)) {ids = [ids];}
+      for (const id of ids) {
+        const index = this.section.findIndex(item => item.id === id);
+        const data = this.section[index];
+        if (data.dirty || data.content) continue;
+        this.$set(this.section, index, Object.assign(data, {dirty: true, error: ''}));
+        this.getContent(data, index, this.section);
+      }
+    },
+    async onPreview(book) {
+      if (book) {
+        await this.$router.push({path: `/index/knowledge/book/detail?id=${book.id}`});
+        this.book = book;
+      }
+      try {
+        await this.getBook();
+        await this.getRecommend();
+      } catch (e) {
+
+      }
+    },
+  },
+};
+</script>
+
+<template>
+  <div class="knowledge-book-detail" :style="{height: containerHeight}">
+    <div class="detail-wrapper">
+      <div class="header">
+        <div class="book-wrapper">
+          <div class="cover">
+            <div class="name">{{ book.bookName }}</div>
+          </div>
+        </div>
+        <div class="description-wrapper">
+          <div>
+            <div style="margin-bottom: 6px;font-size: 18px;font-weight: 700;">{{ book.bookName }}</div>
+            <div class="content">
+              <span v-if="book.dynasty">{{ book.dynasty }} · </span>
+              <span>{{ book.author || '佚名' }}</span>
+            </div>
+          </div>
+          <div style="text-indent: 2em;">{{ book.briefIntroduction }}</div>
+          <el-button type="primary" plain round size="small" :icon="collection ? 'el-icon-remove-outline' : 'el-icon-circle-plus-outline'" @click="collected()">
+            {{ collection ? '移出' : '加入' }}书架
+          </el-button>
+        </div>
+      </div>
+      <el-divider content-position="left">阅读<span v-if="book.readProgress">:{{ book.readProgress }}%</span>
+      </el-divider>
+      <el-collapse style="margin: 0 12px;" @change="load">
+        <el-collapse-item v-for="item in section" :key="item.id" :title="item.name" :name="item.id">
+          <div class="book-content-container" v-loading="!item.content">
+            <div v-html="item.content"></div>
+          </div>
+        </el-collapse-item>
+      </el-collapse>
+      <el-backtop style="right: 300px;" target=".detail-wrapper"></el-backtop>
+    </div>
+    <el-card header="相关医书">
+      <div class="book-wrapper" style="margin-bottom: 24px;" v-for="book in recommend" :key="book.id"
+           @click="onPreview(book)">
+        <div class="cover">
+          <div class="name">{{ book.bookName }}</div>
+        </div>
+        <div class="content" style="font-weight: 700;">{{ book.bookName }}</div>
+        <div class="content">
+          <span v-if="book.dynasty">{{ book.dynasty }}</span>
+          <span>{{ book.author || '佚名' }}</span>
+        </div>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.knowledge-book-detail {
+  display: flex;
+  overflow: hidden;
+
+  .detail-wrapper {
+    flex: auto;
+    overflow-y: auto;
+    padding: 0 24px;
+
+    .header {
+      display: flex;
+    }
+
+    .description-wrapper {
+      display: flex;
+      flex-direction: column;
+      justify-content: space-evenly;
+      margin-left: 24px;
+
+      .el-button {
+        max-width: 120px;
+      }
+    }
+  }
+
+  .el-card {
+    flex: none;
+    display: flex;
+    flex-direction: column;
+    width: 262px;
+    overflow: hidden;
+
+    &::v-deep {
+      .el-card__header {
+        flex: none;
+      }
+
+      .el-card__body {
+        flex: auto;
+        overflow-y: auto;
+      }
+    }
+  }
+
+  .el-divider {
+    &::v-deep {
+      .el-divider__text {
+        background-color: #f5f7f9;
+      }
+    }
+  }
+}
+
+.knowledge-book-detail {
+  &::v-deep {
+    .el-collapse-item__header {
+      padding-left: 12px;
+    }
+
+    .el-collapse-item__content {
+      padding-bottom: 6px;
+    }
+
+    .el-collapse-item__wrap {
+      padding: 6px 12px;
+    }
+  }
+}
+
+.book-content-container {
+  min-height: 80px;
+}
+
+.book-wrapper {
+  $zoom: 0.6;
+  cursor: pointer;
+
+  .cover {
+    position: relative;
+    width: 272px * $zoom;
+    height: 400px * $zoom;
+    background: url("../../assets/book-cover.png") no-repeat center / 100%;
+  }
+
+  .name {
+    position: absolute;
+    top: 30px * $zoom;
+    right: 40px * $zoom;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 48px * $zoom;
+    height: 234px * $zoom;
+    font-size: max(24px * $zoom, 12px);
+    font-weight: 700;
+    letter-spacing: 0.3em;
+    -ms-writing-mode: tb-rl;
+    writing-mode: vertical-rl;
+  }
+
+  .content {
+    margin: 8px 0;
+    font-size: 16px;
+    span + span::before {
+      content: '·';
+      margin: 0 3px;
+    }
+  }
+}
+</style>