Преглед изворни кода

Merge branch 'hotfix/20250508-01'

cc12458 пре 1 година
родитељ
комит
77ab146a40

+ 86 - 79
package-lock.json

@@ -1702,63 +1702,6 @@
           "integrity": "sha1-/q7SVZc9LndVW4PbwIhRpsY1IPo=",
           "dev": true
         },
-        "ansi-styles": {
-          "version": "4.3.0",
-          "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
-          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-convert": "^2.0.1"
-          }
-        },
-        "chalk": {
-          "version": "4.1.2",
-          "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
-          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
-        "color-convert": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
-          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-name": "~1.1.4"
-          }
-        },
-        "color-name": {
-          "version": "1.1.4",
-          "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
-          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-          "dev": true,
-          "optional": true
-        },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
-          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-          "dev": true,
-          "optional": true
-        },
-        "loader-utils": {
-          "version": "2.0.4",
-          "resolved": "https://registry.npmmirror.com/loader-utils/-/loader-utils-2.0.4.tgz",
-          "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "big.js": "^5.2.2",
-            "emojis-list": "^3.0.0",
-            "json5": "^2.1.2"
-          }
-        },
         "ssri": {
           "version": "8.0.1",
           "resolved": "https://registry.npmmirror.com/ssri/download/ssri-8.0.1.tgz",
@@ -1767,28 +1710,6 @@
           "requires": {
             "minipass": "^3.1.1"
           }
-        },
-        "supports-color": {
-          "version": "7.2.0",
-          "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
-          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
-        },
-        "vue-loader-v16": {
-          "version": "npm:vue-loader@16.8.3",
-          "resolved": "https://registry.npmmirror.com/vue-loader/-/vue-loader-16.8.3.tgz",
-          "integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "chalk": "^4.1.0",
-            "hash-sum": "^2.0.0",
-            "loader-utils": "^2.0.0"
-          }
         }
       }
     },
@@ -4104,6 +4025,11 @@
         "assert-plus": "^1.0.0"
       }
     },
+    "dayjs": {
+      "version": "1.11.13",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
+      "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
+    },
     "de-indent": {
       "version": "1.0.2",
       "resolved": "https://registry.npmmirror.com/de-indent/download/de-indent-1.0.2.tgz",
@@ -11112,6 +11038,87 @@
         }
       }
     },
+    "vue-loader-v16": {
+      "version": "npm:vue-loader@16.8.3",
+      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
+      "integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "chalk": "^4.1.0",
+        "hash-sum": "^2.0.0",
+        "loader-utils": "^2.0.0"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
+        "chalk": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true,
+          "optional": true
+        },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true,
+          "optional": true
+        },
+        "loader-utils": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
+          "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "big.js": "^5.2.2",
+            "emojis-list": "^3.0.0",
+            "json5": "^2.1.2"
+          }
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        }
+      }
+    },
     "vue-print-nb": {
       "version": "1.7.4",
       "resolved": "https://registry.npmmirror.com/vue-print-nb/download/vue-print-nb-1.7.4.tgz",

+ 1 - 0
package.json

@@ -10,6 +10,7 @@
   "dependencies": {
     "axios": "^0.21.1",
     "core-js": "^3.6.5",
+    "dayjs": "^1.11.13",
     "echarts": "^4.8.0",
     "element-ui": "^2.15.14",
     "vue": "^2.6.11",

+ 0 - 1
src/api/city.js

@@ -11,7 +11,6 @@ export function getProver() {
 // 获取市 区数据
 
 export function getArea(data) {
-    return
     return request({
         url: `/area/getChildren/${data}`,
         method: 'get'

+ 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,
+        },
+    });
+}

+ 32 - 3
src/api/system.js

@@ -1,4 +1,5 @@
-import request from '@/utils/request.js'
+import request from '@/utils/request.js';
+import dayjs from 'dayjs';
 
 
 // 添加医共体
@@ -271,10 +272,11 @@ export function addUserMsg(data) {
 };
 
 // 获取用户详细信息
-export function getUserDetail(data) {
+export function getUserDetail(data, pid) {
     return request({
         url: `/portal/userMgr/${data}`,
-        method: 'get'
+        method: 'get',
+        params: { pid }
     })
 };
 
@@ -430,4 +432,31 @@ export function editInsurance(data) {
         method: 'post',
         data
     })
+}
+
+export function getLogFile() {
+    return request({
+        url: '/system/version/recordList',
+        method: 'post',
+    }).then(res => res.Data.map(item => {
+        const time = dayjs((item.showTime || item.updateTime || item.createTime || ''));
+        return {
+            id: item.id,
+            content: item.content,
+            title: time.format('YYYY/MM/DD HH:mm:ss'),
+            time: time.valueOf()
+        };
+    }).sort((a, b) => b.time - a.time));
+}
+
+export function setLogFile(data) {
+    return request({
+        url: '/system/version/saveOrUpdate',
+        method: 'post',
+        data: {
+            id: data.id === -1 ? null : data.id,
+            showTime: data.time.replace(' ', 'T'),
+            content: data.content,
+        }
+    })
 }

BIN
src/assets/book-cover.png


+ 20 - 5
src/components/TCMDiagnosis.vue

@@ -9,6 +9,7 @@
       <div class="value">
         <el-popover placement="bottom" width="180" trigger="focus" :close-delay="100">
           <el-input
+            :class="{invalid: name && !zy_dise_id}"
             :size="size"
             slot="reference"
             :placeholder="key1?key1:'中医病名'"
@@ -267,6 +268,7 @@ export default {
       this.key1 = item.disname;
       this.$refs.zybm.blur();
 
+      this.key2 = '';
       this.syndrome = "";
       this.zhengxingid = "";
       this.therapy = "";
@@ -369,17 +371,30 @@ export default {
           zhengxing: ""
         }
       };
-      let res = await addDiagnosisData(params);
-      if (res.ResultCode == 0) {
-        this.addRecipeFrom();
-      } else {
-        this.$message.error(res.ResultInfo);
+      let res = await addDiagnosisData(params).catch(() => ({ResultCode: -1}));
+      if (+res.ResultCode === 0) {
+        if (!this.name) {
+          res.ResultCode = -1;
+          res.ResultInfo = `请选择${this.title}病名`;
+        } else if (!this.zy_dise_id) {
+          res.ResultCode = -1;
+          res.ResultInfo = `当前疾病编码与医保不匹配,请更换中医病名等诊断信息!`;
+        } else {
+          this.addRecipeFrom().catch();
+        }
       }
+      if (+res.ResultCode !== 0 && res.ResultInfo) this.$message.error(res.ResultInfo)
+      return +res.ResultCode === 0;
     }
   }
 };
 </script>
 <style scoped lang="scss">
+.el-input.invalid::v-deep {
+  input {
+    color: #ff0000;
+  }
+}
 div {
   box-sizing: border-box;
 }

+ 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: '知识学习'
+        }
+    },
 ]

+ 3 - 0
src/store/index.js

@@ -17,6 +17,9 @@ export default new Vuex.Store({
       // console.log(state.user, '啊啊啊')
       return state.user.userInfo
     },
+    getPermissions(state) {
+      return state.user.permissions
+    },
     getPatiensInfo(state) {
       return state.user.patiensInfo
     },

+ 19 - 0
src/store/modules/user.js

@@ -1,9 +1,12 @@
+import {getPermissionsMenu} from '@/api/system';
+
 export default {
     namespaced: true,
     state: {
         patiensInfo: sessionStorage.getItem('patiensInfo') ? JSON.parse(sessionStorage.getItem('patiensInfo')) : null, // 病人信息
         // userInfo: sessionStorage.getItem('userinfo') ? JSON.parse(sessionStorage.getItem('userinfo')) : {}, // 当前登录用户信息
         userInfo: sessionStorage.getItem('userinfo') ? JSON.parse(sessionStorage.getItem('userinfo')) : {}, // 当前登录用户信息
+        permissions: sessionStorage.getItem('permissions') ? JSON.parse(sessionStorage.getItem('permissions')) : [], // 权限标识
         isSeeDoctor: sessionStorage.getItem('isSeeDoctor') ? sessionStorage.getItem('isSeeDoctor') : true, // 当前状态是就诊还是查看 
         preNo: sessionStorage.getItem('edit_preNo') ? sessionStorage.getItem('edit_preNo') : '', // 当前(进入)状态是修改处方还是创建处方
         outpatientDiagnosis: sessionStorage.getItem('outpatientDiagnosis') ? sessionStorage.getItem('outpatientDiagnosis') : null, // 转病例到中医诊断
@@ -19,6 +22,10 @@ export default {
             sessionStorage.setItem('userinfo', JSON.stringify(data))
             state.userInfo = data
         },
+        setPermissions(state, data) {
+            sessionStorage.setItem('permissions', JSON.stringify(data));
+            state.permissions = data;
+        },
         setIsSee(state, data) {
             sessionStorage.setItem('isSeeDoctor', data)
             state.isSeeDoctor = data
@@ -42,5 +49,17 @@ export default {
         user(state) {
             return state
         }
+    },
+    actions: {
+        async setUserinfoAndPermissions(state, data) {
+            state.commit('setUserinfo', data);
+            let permissions = [];
+            try {
+                const roles = data.roles.map(item => getPermissionsMenu({RoleID: item.roleid}).then(res => res.Data.RoleRight.split(',')).catch(() => []));
+                const results = await Promise.all(roles);
+                permissions = [...new Set(results.flat(1))];
+            } catch (e) {}
+            state.commit('setPermissions', permissions);
+        },
     }
 };

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

+ 4 - 4
src/views/Index.vue

@@ -25,12 +25,12 @@ export default {
     async getUserInfo() {
       let res = await getUserInfo();
       if (res.ResultCode == 0) {
-        this.setInfo(res.Data);
+        await this.setInfo(res.Data);
       }
     },
-    ...mapMutations({
-      setInfo: "user/setUserinfo"
-    })
+    ...mapActions({
+      setInfo: "user/setUserinfoAndPermissions"
+    }),
   }
 };
 </script>

+ 18 - 35
src/views/diagnosis/Prescribing.vue

@@ -2601,6 +2601,8 @@ export default {
       let zhongPrescriptionVo = this.dealRecipe1();
       if (!zhongPrescriptionVo.length) return;
 
+      if (!await this.$refs.TCM.saveDiagnosisData()) return;
+
       this.saving = true;
       try {
         const {options, force} = await this.getRationalSafeUse();
@@ -2668,7 +2670,6 @@ export default {
         )
           return;*/
 
-        this.$refs.TCM.saveDiagnosisData();
         this._getRecipePriview(
           zhongPrescriptionVo,
           chengPrescriptionVo,
@@ -3558,7 +3559,6 @@ export default {
       }, {title: `中药处方${index + 1}`});
     },
     async getRationalMed(id) {
-      let ids = [];
       // this.rationalMed = []
       let children = this.$children.filter(item => {
         return (
@@ -3568,21 +3568,9 @@ export default {
         );
       });
       let child = children[0];
-
-      child.recipe_tabs[child.recipe_tabs_c].totalTableD.forEach(item => {
-        if (item.name) {
-          ids.push(item.medid);
-        }
-      });
-
-      let idsIndex = 0;
-      ids.forEach((item, index) => {
-        if (item == id) {
-          idsIndex = index;
-        }
-      });
-
-      ids.splice(idsIndex, 1);
+      const medicines = child.recipe_tabs[child.recipe_tabs_c].totalTableD || [];
+      const medicine = medicines.find(medicine => medicine.medid === id);
+      const ids = medicines.filter(medicine => medicine.name && medicine.medid !== id).map(medicine => medicine.medid);
       // this.rationalMed = []
       let res = await getRationalMed({
         matID: id,
@@ -3592,7 +3580,7 @@ export default {
       if (res.code == 0 && res.message) {
         res.data.reqID = id;
 
-        this.rationalMed.push(res.data);
+        this.rationalMed.push(Object.assign(res.data, {__medicine__: medicine}));
         // 去重
         const removeRepeat = (arr, key) => {
           let obj = {};
@@ -3678,6 +3666,10 @@ export default {
         }
 
         if (item.matmaxdosage && item.matmindosage) {
+          try {
+            const dose = +item.__medicine__.dose;
+            if (dose && (dose < item.matmindosage || dose > item.matmaxdosage)) item.showDose = true;
+          } catch (e) {}
           this.rationalMed10.push(item);
         }
       });
@@ -3695,7 +3687,6 @@ export default {
     },
     // 获取合理用药信息 平台
     async getRationalMedForPlat(id) {
-      let ids = [];
       this.rationalMed = [];
       let children = this.$children.filter(item => {
         return (
@@ -3705,21 +3696,9 @@ export default {
         );
       });
       let child = children[0];
-
-      child.recipe_tabs[child.recipe_tabs_c].totalTableD.forEach(item => {
-        if (item.name) {
-          ids.push(item.medid);
-        }
-      });
-
-      let idsIndex = 0;
-      ids.forEach((item, index) => {
-        if (item == id) {
-          idsIndex = index;
-        }
-      });
-
-      ids.splice(idsIndex, 1);
+      const medicines = child.recipe_tabs[child.recipe_tabs_c].totalTableD || [];
+      const medicine = medicines.find(medicine => medicine.medid === id);
+      const ids = medicines.filter(medicine => medicine.name && medicine.medid !== id).map(medicine => medicine.medid);
 
       let res = await getRationalMedForPlat({
         matID: id,
@@ -3729,7 +3708,7 @@ export default {
       if (res.code == 0 && res.message) {
         res.data.reqID = id;
 
-        this.rationalMed.push(res.data);
+        this.rationalMed.push(Object.assign(res.data, {__medicine__: medicine}));
 
         // 去重
         const removeRepeat = (arr, key) => {
@@ -3791,6 +3770,10 @@ export default {
           }
 
           if (item.matmaxdosage && item.matmindosage) {
+            try {
+              const dose = +item.__medicine__.dose;
+              if (dose < item.matmindosage || dose > item.matmaxdosage) item.showDose = true;
+            } catch (e) {}
             this.rationalMed10.push(item);
           }
         });

+ 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>

+ 90 - 4
src/views/system/Help.vue

@@ -33,12 +33,54 @@
       <el-button size="mini" type="danger" @click="logOut">退出登录</el-button>
       <!-- <div class="sure flex-center bg-red" @click="logOut()">退出登录</div> -->
     </div>
+    <div style="margin: 12px 0;">
+      <div>
+        <span>版本更新记录:</span>
+        <el-button v-if="logEditable" type="primary" size="small" icon="el-icon-plus" circle
+                   @click="start();"
+        ></el-button>
+      </div>
+      <div style="margin-top: 16px;max-width: 800px;">
+        <el-timeline v-loading="logLoading">
+          <template v-for="item in log">
+            <el-timeline-item v-if="item.id === logEdit" :key="item.id" icon="el-icon-edit">
+              <el-date-picker v-model="logModel.time" placeholder="版本更新时间" size="small"
+                              type="datetime" value-format="yyyy-MM-dd HH:mm:ss" :clearable="false"
+                              :disabled="logSaving"
+              ></el-date-picker>
+              <el-card style="margin-top: 12px;">
+                <el-input v-model="logModel.content" placeholder="版本更新内容"
+                          type="textarea" :autosize="{ minRows: 2}"
+                          :disabled="logSaving"
+                ></el-input>
+                <div style="margin-top: 12px; text-align: right;">
+                  <el-button size="small" :disabled="logSaving" @click="logEdit = false;logModel.content=''">取消
+                  </el-button>
+                  <el-button type="primary" size="small" :disabled="logSaving"
+                             @click="setLogFile();logModel.content=''">保存
+                  </el-button>
+                </div>
+              </el-card>
+            </el-timeline-item>
+            <el-timeline-item v-else type="info" placement="top" :timestamp="item.title">
+              <el-card style="position: relative">
+                <div style="white-space: pre;line-height: 1.75">{{ item.content }}</div>
+                <el-button v-if="logEditable" style="position: absolute; right: 6px;bottom: 6px;"
+                           type="primary" icon="el-icon-edit" circle size="mini"
+                           @click="start(item)"></el-button>
+              </el-card>
+            </el-timeline-item>
+          </template>
+        </el-timeline>
+      </div>
+    </div>
   </div>
 </template>
 <script>
-import { changePas } from "@/api/system.js";
-import { load } from "mime";
-import { mapState, mapGetters, mapActions, mapMutations } from "vuex";
+import dayjs from 'dayjs';
+import {changePas, getLogFile, setLogFile} from '@/api/system.js';
+import {mapGetters, mapMutations} from 'vuex';
+
 export default {
   data() {
     return {
@@ -46,10 +88,54 @@ export default {
       pas: "",
       newPas: "",
       newPas1: "",
-      downloadUrl: process.env.VUE_APP_DOWNLOAD
+      downloadUrl: process.env.VUE_APP_DOWNLOAD,
+
+      log: [],
+      logModel: {
+        time: '',
+        content: '',
+      },
+      logEdit: false,
+      logEditable: false,
+      logLoading: false,
+      logSaving: false,
     };
   },
+  computed: {
+    ...mapGetters(['getPermissions']),
+  },
+  created() {
+    this.getLogFile();
+    setTimeout(() => {
+      this.logEditable = this.getPermissions.includes('5-9-edit');
+    }, 500);
+  },
   methods: {
+    async setLogFile() {
+      this.logSaving = true;
+      try {
+        await setLogFile(this.logModel);
+        this.$message.success(`保存成功`);
+        if (this.log[0].id === -1) { this.log.shift();}
+        this.logEdit = '';
+        await this.getLogFile();
+      } catch (error) { }
+      this.logSaving = false;
+    },
+    async getLogFile() {
+      try {
+        this.logLoading = true;
+        this.log = await getLogFile();
+      } catch (error) {}
+      this.logLoading = false;
+    },
+    start(data) {
+      if (this.log.length && this.log[0].id === -1) {this.log.shift();}
+      const {id = -1, content = '', time = Date.now()} = data || {};
+      this.logModel = {id, content, time: dayjs(time).format('YYYY-MM-DD HH:mm:ss')};
+      this.logEdit = this.logModel.id;
+      if (id === -1) this.log.unshift(this.logModel);
+    },
     // 退出登录
     logOut() {
       this.setActive("0");

+ 5 - 4
src/views/system/UserSytem.vue

@@ -648,7 +648,7 @@ export default {
         type: "warning"
       })
         .then(() => {
-          this.getUserDetail(scope.row.userid);
+          this.getUserDetail(scope.row.userid, scope.row);
           setTimeout(() => {
             this.editUserMsg(pid, state == 0 ? "1" : "0");
           }, 500);
@@ -658,7 +658,7 @@ export default {
     // 编辑数据
     editData(scope) {
       this.type = "edit";
-      this.getUserDetail(scope.row.userid);
+      this.getUserDetail(scope.row.userid, scope.row);
       this.diaform.pid = scope.row.pid;
       this.showDialog = true;
     },
@@ -1113,8 +1113,9 @@ export default {
       }
     },
     // 获取用户详细信息
-    async getUserDetail(id) {
-      let res = await getUserDetail(id);
+    async getUserDetail(id, row) {
+      if (!row) row = {}
+      let res = await getUserDetail(id, row.pid);
       if (res.ResultCode == 0) {
         this.diaform = {
           pid: res.Data.pid,