chenlei 10 ay önce
ebeveyn
işleme
3e4ddc12d6
44 değiştirilmiş dosya ile 1334 ekleme ve 308 silme
  1. 4 1
      package.json
  2. 2 1
      packages/base/package.json
  3. 65 0
      packages/base/src/api/index.js
  4. 2 0
      packages/base/src/index.js
  5. 12 0
      packages/base/src/utils/index.js
  6. 59 0
      packages/base/src/utils/usePagination.js
  7. 1 0
      packages/pc/.env.development
  8. 1 0
      packages/pc/.env.production
  9. 3 2
      packages/pc/index.html
  10. 3 1
      packages/pc/package.json
  11. 0 30
      packages/pc/public/three/click.js
  12. 37 39
      packages/pc/public/three/index.html
  13. 1 6
      packages/pc/public/three/index.js
  14. 1 0
      packages/pc/public/wxLogin.js
  15. 0 6
      packages/pc/src/App.vue
  16. 19 0
      packages/pc/src/assets/main.css
  17. 7 0
      packages/pc/src/assets/svgs/icon_upload_yellow.svg
  18. 11 5
      packages/pc/src/components/BookCard/index.vue
  19. 6 4
      packages/pc/src/components/RankPanel/index.vue
  20. 58 7
      packages/pc/src/components/TopNav/components/LoginDialog.vue
  21. 9 3
      packages/pc/src/components/TopNav/index.vue
  22. 106 0
      packages/pc/src/components/TreeMenu.vue
  23. 36 0
      packages/pc/src/configure.js
  24. 1 0
      packages/pc/src/main.js
  25. 9 1
      packages/pc/src/stores/base.js
  26. 8 1
      packages/pc/src/stores/detail.js
  27. 325 0
      packages/pc/src/views/Bookshelf/components/UploadDialog.vue
  28. 12 0
      packages/pc/src/views/Bookshelf/index.scss
  29. 39 6
      packages/pc/src/views/Bookshelf/index.vue
  30. 12 5
      packages/pc/src/views/Detail/components/Directory.vue
  31. 15 7
      packages/pc/src/views/Detail/components/IntroductionDialog.vue
  32. 5 1
      packages/pc/src/views/Detail/components/Reader/index.vue
  33. 25 6
      packages/pc/src/views/Detail/index.vue
  34. 1 0
      packages/pc/src/views/Home/index.scss
  35. 71 12
      packages/pc/src/views/Home/index.vue
  36. 44 11
      packages/pc/src/views/Home2/components/News.vue
  37. 28 17
      packages/pc/src/views/Home2/components/NewsDialog.vue
  38. 51 3
      packages/pc/src/views/Home2/index.vue
  39. 0 14
      packages/pc/src/views/Stack/components/Sidebar/index.scss
  40. 135 110
      packages/pc/src/views/Stack/components/Sidebar/index.vue
  41. 4 0
      packages/pc/src/views/Stack/index.scss
  42. 56 7
      packages/pc/src/views/Stack/index.vue
  43. 3 1
      packages/pc/vite.config.js
  44. 47 1
      pnpm-lock.yaml

+ 4 - 1
package.json

@@ -8,5 +8,8 @@
   },
   "keywords": [],
   "author": "",
-  "license": "ISC"
+  "license": "ISC",
+  "dependencies": {
+    "tslib": "^2.8.0"
+  }
 }

+ 2 - 1
packages/base/package.json

@@ -13,7 +13,8 @@
   },
   "peerDependencies": {
     "pinia": "2.*",
-    "vue": "3.*"
+    "vue": "3.*",
+    "@dage/service": "*"
   },
   "keywords": [],
   "author": "",

+ 65 - 0
packages/base/src/api/index.js

@@ -0,0 +1,65 @@
+import { requestByGet, requestByPost } from "@dage/service";
+
+// 推荐列表
+export const getRecommendListApi = (params) => {
+  return requestByPost("/api/show/rank/getList", params);
+};
+
+// 图书-阅读排行
+export const getReadListApi = () => {
+  return requestByGet("/api/show/book/getVisit");
+};
+
+// 图书-总数
+export const getBookCountApi = () => {
+  return requestByGet("/api/show/book/count");
+};
+
+// 图书-详情
+export const getBookDetailApi = (id) => {
+  return requestByGet(`/api/show/book/detail/${id}`);
+};
+
+// 图书列表-分页
+export const getBookListApi = (params) => {
+  return requestByPost("/api/show/book/pageList", params);
+};
+
+// 字典列表
+export const getDictListApi = () => {
+  return requestByGet("/api/show/dict/getList");
+};
+
+// 公告列表-分页
+export const getNoticeListApi = (params) => {
+  return requestByPost("/api/show/notice/pageList", params);
+};
+
+// 公告-保存访问量
+export const addNoticeVisitApi = (noticeId) => {
+  return requestByGet(`/api/show/notice/addVisit/${noticeId}`);
+};
+
+// 书籍分类
+export const getStorageTreeApi = () => {
+  return requestByGet("/api/show/storage/getTree");
+};
+
+// 藏品分类
+export const getExhibitTypeListApi = (params) => {
+  return requestByPost("/api/show/exhibitType/getList", params);
+};
+
+// 微信-登录
+export const wxLoginApi = (code) => {
+  return requestByGet(`/api/show/wx/login/${code}`);
+};
+
+// 图书-文件-上传
+export const uploadApi = (params) => {
+  return requestByPost("/api/wx/book/upload", params, {
+    headers: {
+      "content-type": "application/x-www-form-urlencoded",
+    },
+  });
+};

+ 2 - 0
packages/base/src/index.js

@@ -1,2 +1,4 @@
 export * from "./stores";
 export * from "./constants";
+export * from "./api";
+export * from "./utils";

+ 12 - 0
packages/base/src/utils/index.js

@@ -0,0 +1,12 @@
+import { getBaseURL } from "@dage/service";
+
+const baseUrl = getBaseURL();
+
+export const isDevelopment = import.meta.env.MODE === "development";
+
+export const getBaseUrl = () => {
+  return baseUrl;
+  // return `${baseUrl}${isDevelopment ? "/api" : ""}`;
+};
+
+export * from "./usePagination";

+ 59 - 0
packages/base/src/utils/usePagination.js

@@ -0,0 +1,59 @@
+import { computed, ref } from "vue";
+
+export const PaginationType = {
+  DEFAULT: 0,
+  CONCAT: 1,
+};
+
+export const DEFAULT_PAGE_SIZE = 10;
+
+export const usePagination = (
+  handler,
+  type = PaginationType.DEFAULT,
+  size = DEFAULT_PAGE_SIZE
+) => {
+  const pageNum = ref(1);
+  const total = ref(0);
+  const loading = ref(false);
+  const list = ref([]);
+  const noData = computed(() => !total.value && !loading.value);
+  const noMore = computed(() => size * pageNum.value > total.value);
+
+  const getList = async () => {
+    try {
+      loading.value = true;
+
+      const data = await handler({
+        pageNum: pageNum.value,
+        pageSize: size,
+      });
+      total.value = data.total;
+
+      if (type === PaginationType.DEFAULT) {
+        list.value = data.records;
+      } else {
+        list.value =
+          pageNum.value > 1 ? list.value.concat(data.records) : data.records;
+      }
+    } finally {
+      loading.value = false;
+    }
+  };
+
+  const resetParams = () => {
+    pageNum.value = 1;
+    total.value = 0;
+    list.value = [];
+  };
+
+  return {
+    pageNum,
+    loading,
+    list,
+    total,
+    noData,
+    noMore,
+    getList,
+    resetParams,
+  };
+};

+ 1 - 0
packages/pc/.env.development

@@ -0,0 +1 @@
+VITE_BASE_URL=https://sit-liushaoqibwg.4dage.com

+ 1 - 0
packages/pc/.env.production

@@ -0,0 +1 @@
+VITE_BASE_URL=https://sit-liushaoqibwg.4dage.com

+ 3 - 2
packages/pc/index.html

@@ -9,7 +9,8 @@
   <body>
     <div id="app"></div>
     <script type="module" src="/src/main.js"></script>
-    <script type="text/javascript" src="./jszip.min.js"></script>
-    <script type="text/javascript" src="./epub.min.js"></script>
+    <script type="text/javascript" src="/jszip.min.js"></script>
+    <script type="text/javascript" src="/epub.min.js"></script>
+    <script type="text/javascript" src="/wxLogin.js"></script>
   </body>
 </html>

+ 3 - 1
packages/pc/package.json

@@ -2,17 +2,19 @@
   "name": "@lsq/pc",
   "version": "0.0.0",
   "private": true,
-  "type": "module",
+  "type": "commonjs",
   "scripts": {
     "dev": "vite",
     "build": "vite build",
     "preview": "vite preview"
   },
   "dependencies": {
+    "@dage/service": "^1.0.3",
     "@dage/utils": "^1.0.2",
     "@element-plus/icons-vue": "^2.3.1",
     "@lsq/base": "workspace:^",
     "@vueuse/core": "^11.1.0",
+    "@vueuse/router": "^11.1.0",
     "element-plus": "^2.8.3",
     "lodash": "^4.17.21",
     "pinia": "^2.1.7",

+ 0 - 30
packages/pc/public/three/click.js

@@ -1,34 +1,4 @@
-// 点击
-viewer.addEventListener('clickObject', e => {
-  window.top.clickObject(e.imgName);
-  // 暂停动画
-  viewer.setAutoMove(false)
-})
-
 // 继续动画 - 给 父页面调用
 window.stareMove = (val) => {
   viewer.setAutoMove(val)
 }
-
-let flag = false
-
-// 鼠标移入
-viewer.addEventListener('hoverObject', e => {
-  // console.log('鼠标移入',e);
-  flag = true
-  window.top.hoverObject(e);
-})
-
-// 鼠标移出
-viewer.addEventListener('mouseoutObject', e => {
-  // console.log('鼠标移出',e);
-  flag = false
-  window.top.mouseoutObject(e);
-})
-
-
-document.querySelector('#player').onmousemove = (event) => {
-  if (!flag) return
-  let e = event || window.event;
-  window.top.mouseLoc(e.clientX, e.clientY);
-}

+ 37 - 39
packages/pc/public/three/index.html

@@ -4,7 +4,7 @@
     <meta charset="UTF-8" />
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
     <meta name="viewport" content="width=device-width,initial-scale=1.0" />
-    <title>artsandculture</title>
+    <title>刘少奇同志纪念馆</title>
     <style>
       #player,
       body,
@@ -42,26 +42,9 @@
       <canvas></canvas>
     </div>
 
-    <script type="text/javascript" src="jquery-2.1.1.min.js"></script>
-    <script type="text/javascript" src="three.min.js"></script>
+    <script type="text/javascript" src="./jquery-2.1.1.min.js"></script>
+    <script type="text/javascript" src="./three.min.js"></script>
     <script>
-      const cardNames = [
-        "1.jpg",
-        "2.jpg",
-        "3.jpg",
-        "4.jpg",
-        "5.jpg",
-        "6.jpg",
-        "7.jpg",
-        "8.jpg",
-        "9.jpg",
-        "10.jpg",
-        "11.jpg",
-        "12.jpg",
-        "13.jpg",
-        "14.jpg",
-      ];
-
       let vfov = 60; //垂直视角范围度数
       window.setting = {
         vfov,
@@ -74,30 +57,45 @@
         },
       };
 
-      /* var textarea = document.createElement('textarea');
-    textarea.id = "consoleLog";
+      function initViewer(cardNames) {
+        window.cardNames = cardNames;
+        var startTime = new Date().getTime();
+        window.viewer = new Viewer(0, $("#player")[0])
+
+        // 点击
+        viewer.addEventListener('clickObject', e => {
+          window.top.clickObject(e.imgName);
+          // 暂停动画
+          viewer.setAutoMove(false)
+        })
 
-    document.getElementsByTagName("body")[0].appendChild(textarea);
-    var list = ["log", "error", "warn", "debug", "info", "time", "timeEnd"]
-    var exchange = function (o) {
-    console["old" + o] = console[o];
-    console[o] = function (str) {
-      console["old" + o](str);
-      var t = document.getElementById("consoleLog").innerHTML;
-      document.getElementById("consoleLog").innerHTML = str + "\n\n" + t;
-    }
-    }
+        let flag = false
 
-    for (var i = 0; i < list.length; i++) {
-    exchange(list[i])
-    }
+        // 鼠标移入
+        viewer.addEventListener('hoverObject', e => {
+          // console.log('鼠标移入',e);
+          flag = true
+          window.top.hoverObject(e);
+        })
 
-    */
+        // 鼠标移出
+        viewer.addEventListener('mouseoutObject', e => {
+          // console.log('鼠标移出',e);
+          flag = false
+          window.top.mouseoutObject(e);
+        })
+
+        document.querySelector('#player').onmousemove = (event) => {
+          if (!flag) return
+          let e = event || window.event;
+          window.top.mouseLoc(e.clientX, e.clientY);
+        }
+      }
     </script>
 
-    <script type="text/javascript" src="utils.js"></script>
-    <script type="text/javascript" src="PanoramaControls.js"></script>
-    <script type="text/javascript" src="index.js"></script>
+    <script type="text/javascript" src="./utils.js"></script>
+    <script type="text/javascript" src="./PanoramaControls.js"></script>
+    <script type="text/javascript" src="./index.js"></script>
     <script src="./click.js"></script>
   </body>
 </html>

+ 1 - 6
packages/pc/public/three/index.js

@@ -193,7 +193,7 @@ Viewer.prototype.addCard = function (around) {
     
     
     let cardIndex = Math.floor(cardNames.length * Math.random())
-    common.loadTexture("assets/" + cardNames[cardIndex], (map) => {
+    common.loadTexture(cardNames[cardIndex], (map) => {
 
 
         /* let card = new THREE.Mesh(planeGeo, new THREE.MeshBasicMaterial({ 
@@ -487,11 +487,6 @@ Viewer.prototype.setAutoMove = function (state) {//设置相机飞行状态
 
 Object.assign(Viewer.prototype, THREE.EventDispatcher.prototype);
 
-//============
-
-var startTime = new Date().getTime();
-var viewer = new Viewer(0, $("#player")[0])
-
 
 
 

Dosya farkı çok büyük olduğundan ihmal edildi
+ 1 - 0
packages/pc/public/wxLogin.js


+ 0 - 6
packages/pc/src/App.vue

@@ -20,12 +20,6 @@ html.dark {
   --topnav-height: 80px;
 }
 
-.w1100 {
-  margin: 0 auto;
-  width: 1100px;
-  overflow: hidden;
-}
-
 .fade-enter-active,
 .fade-leave-active {
   transition: opacity 0.2s ease;

+ 19 - 0
packages/pc/src/assets/main.css

@@ -129,6 +129,11 @@ a {
   color: inherit;
   text-decoration: none;
 }
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+  -webkit-appearance: none;
+  margin: 0;
+}
 
 .limit-line {
   display: -webkit-box;
@@ -144,6 +149,20 @@ a {
   -webkit-line-clamp: 2;
 }
 
+.w100 {
+  width: 100%;
+}
+
+.w350 {
+  width: 350px !important;
+}
+
+.w1100 {
+  margin: 0 auto;
+  width: 1100px;
+  overflow: hidden;
+}
+
 @font-face {
   font-family: "Source Han Sans CN-Regular";
   src: url("@/assets/fonts/SourceHanSansCN-Regular.otf");

Dosya farkı çok büyük olduğundan ihmal edildi
+ 7 - 0
packages/pc/src/assets/svgs/icon_upload_yellow.svg


+ 11 - 5
packages/pc/src/components/BookCard/index.vue

@@ -1,11 +1,11 @@
 <template>
   <router-link
-    :to="{ name: 'detail', params: { id: 1, type: 'reader' } }"
+    :to="{ name: 'detail', params: { id: item.id, type: 'reader' } }"
     class="book-card"
     :class="{ row: isRow, large: size === 'large' }"
   >
     <div class="book-card-img">
-      <el-image src="" fit="cover" />
+      <el-image :src="`${baseUrl}${item.thumb}`" fit="cover" />
 
       <img
         v-if="showLike"
@@ -16,15 +16,16 @@
     </div>
 
     <div class="book-card-inner">
-      <p class="book-card__name limit-line">刘少奇传</p>
-      <p v-if="isRow" class="limit-line">刘少奇</p>
-      <p class="limit-line">中共中央文献研究室 编</p>
+      <p class="book-card__name limit-line">{{ item.name }}</p>
+      <p v-if="isRow" class="limit-line">{{ item.author }}</p>
+      <p class="limit-line">{{ item.press }}</p>
     </div>
   </router-link>
 </template>
 
 <script setup>
 import { computed } from "vue";
+import { getBaseUrl } from "@lsq/base";
 
 const props = defineProps({
   // 模式 row-横版 column-竖版
@@ -44,8 +45,13 @@ const props = defineProps({
     required: false,
     default: false,
   },
+  item: {
+    type: Object,
+    required: true,
+  },
 });
 
+const baseUrl = getBaseUrl();
 const isRow = computed(() => props.type === "row");
 </script>
 

+ 6 - 4
packages/pc/src/components/RankPanel/index.vue

@@ -7,18 +7,20 @@
       <p class="rank-panel-header__subtitle">{{ subTitle }}</p>
     </div>
 
-    <ul class="rank-panel-list">
-      <li v-for="key in 8" :key="key">
-        <book-card />
+    <ul v-if="list.length" class="rank-panel-list">
+      <li v-for="item in list" :key="item.id">
+        <book-card :item="item" />
       </li>
     </ul>
+
+    <el-empty v-else :image-size="50" description="暂无数据" />
   </div>
 </template>
 
 <script setup>
 import BookCard from "@/components/BookCard/index.vue";
 
-defineProps(["subTitle"]);
+defineProps(["subTitle", "list"]);
 </script>
 
 <style lang="scss" scoped>

+ 58 - 7
packages/pc/src/components/TopNav/components/LoginDialog.vue

@@ -1,12 +1,19 @@
 <template>
   <el-dialog v-model="show" class="login-dialog" width="425px">
-    <el-image class="login-dialog__scan" src="" />
-    <span>手机扫描二维码</span>
+    <div
+      v-loading.fullscreen.lock="loading"
+      element-loading-text="登录中"
+      id="login-dialog__scan"
+    />
+    <!-- <span>手机扫描二维码</span> -->
   </el-dialog>
 </template>
 
 <script setup>
-import { computed } from "vue";
+import { computed, ref, watch, nextTick } from "vue";
+import { useRoute } from "vue-router";
+import { wxLoginApi } from "@lsq/base";
+import { useRouter } from "vue-router";
 
 const props = defineProps({
   visible: {
@@ -15,6 +22,9 @@ const props = defineProps({
   },
 });
 const emits = defineEmits(["update:visible"]);
+const route = useRoute();
+const router = useRouter();
+const loading = ref(false);
 
 const show = computed({
   get() {
@@ -24,6 +34,47 @@ const show = computed({
     emits("update:visible", v);
   },
 });
+
+const login = () => {
+  new WxLogin({
+    self_redirect: false, //true:手机点击确认登录后可以在 iframe 内跳转到 redirect_uri
+    id: "login-dialog__scan", //显示二维码容器设置
+    appid: "wxc09c15943347c4a4", //应用位置标识appid
+    scope: "snsapi_login", //当前微信扫码登录页面已经授权了
+    redirect_uri: encodeURIComponent(location.href), //填写授权回调域路径,就是用户授权成功以后,微信服务器向公司后台推送code地址
+    state: "", //state就是后台服务器重定向的地址携带用户信息
+    style: "black",
+    href: "",
+  });
+};
+
+watch(route, async (v) => {
+  if (v.query.code) {
+    try {
+      loading.value = true;
+      const data = await wxLoginApi(v.query.code);
+      console.log(data);
+    } finally {
+      const query = { ...route.query };
+      delete query.code;
+      delete query.state;
+      router.replace({
+        name: route.name,
+        query,
+      });
+
+      show.value = false;
+      loading.value = false;
+    }
+  }
+});
+
+watch(show, (v) => {
+  v &&
+    nextTick(() => {
+      login();
+    });
+});
 </script>
 
 <style lang="scss">
@@ -44,9 +95,9 @@ const show = computed({
       font-family: "Source Han Serif CN-Bold";
     }
   }
-  &__scan {
-    width: 321px;
-    height: 321px;
-  }
+}
+#login-dialog__scan {
+  width: 300px;
+  height: 400px;
 }
 </style>

+ 9 - 3
packages/pc/src/components/TopNav/index.vue

@@ -44,6 +44,7 @@
               v-else
               v-loading="joinLoading"
               element-loading-custom-class="top-nav__loading"
+              @click="handleLike"
             >
               加入书架
             </li>
@@ -172,9 +173,9 @@ const route = useRoute();
 const isDark = useDark();
 
 const detailStore = useDetailStore();
-const { introductionVisible } = storeToRefs(detailStore);
+const { introductionVisible, detail } = storeToRefs(detailStore);
 const baseStore = useBaseStore();
-const { isLogin } = storeToRefs(baseStore);
+const { isLogin, loginVisible } = storeToRefs(baseStore);
 const epubStore = useEpubStore();
 const { isSimplified } = storeToRefs(epubStore);
 
@@ -183,7 +184,12 @@ const showLogo = ref(false);
 const hideBgColor = ref(false);
 const bgColor = ref("");
 const joinLoading = ref(false);
-const loginVisible = ref(false);
+
+// 加入书架
+const handleLike = () => {
+  if (!baseStore.loginValidator()) {
+  }
+};
 
 watch(route, (v) => {
   isDetail.value = v.name === "detail";

+ 106 - 0
packages/pc/src/components/TreeMenu.vue

@@ -0,0 +1,106 @@
+<script lang="jsx">
+import { defineComponent } from "vue";
+
+const MenuItem = defineComponent({
+  props: {
+    menu: {
+      type: Array,
+      required: true,
+    },
+    selected: {
+      type: Number,
+      default: -1,
+    },
+  },
+  emits: ["select"],
+  setup(props, { emit }) {
+    const renderMenu = (menuItem) => {
+      if (menuItem.children && menuItem.children.length > 0) {
+        return (
+          <ElSubMenu
+            class={{ active: props.selected === menuItem.id }}
+            index={String(menuItem.id)}
+          >
+            {{
+              title: () => (
+                <p
+                  className="w100 limit-line"
+                  onClick={(e) => {
+                    e.stopPropagation();
+                    emit("select", menuItem);
+                  }}
+                >
+                  {menuItem.name}
+                </p>
+              ),
+              default: () =>
+                menuItem.children.map((child) => renderMenu(child)),
+            }}
+          </ElSubMenu>
+        );
+      } else {
+        return (
+          <ElMenuItem
+            class={{ active: props.selected === menuItem.id }}
+            index={String(menuItem.id)}
+            onClick={() => {
+              emit("select", menuItem);
+            }}
+          >
+            <p className="limit-line">{menuItem.name}</p>
+          </ElMenuItem>
+        );
+      }
+    };
+
+    return () => <>{props.menu.map((item) => renderMenu(item))}</>;
+  },
+});
+
+export default defineComponent({
+  components: { MenuItem },
+  props: {
+    defaultOpeneds: {
+      type: Array,
+      required: false,
+    },
+  },
+  setup(props, { attrs }) {
+    return () => (
+      <div className="tree-menu">
+        <ElMenu default-openeds={props.defaultOpeneds}>
+          <MenuItem {...attrs} />
+        </ElMenu>
+      </div>
+    );
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.tree-menu {
+  --el-menu-item-height: 40px;
+  --el-menu-item-font-size: 18px;
+  --el-menu-active-color: var(--text-color-secondary);
+  --el-menu-text-color: var(--text-color-secondary);
+
+  .el-menu {
+    border: none;
+  }
+  :deep(p) {
+    flex: 1;
+    width: 0;
+    white-space: normal;
+  }
+  :deep(.active > .el-sub-menu__title),
+  :deep(.el-menu-item.active) {
+    color: var(--el-color-primary);
+    border-left: 3px solid var(--el-color-primary);
+    background: linear-gradient(
+      90deg,
+      rgba(209, 187, 158, 0.2) 0%,
+      rgba(209, 187, 158, 0) 100%
+    );
+  }
+}
+</style>

+ 36 - 0
packages/pc/src/configure.js

@@ -0,0 +1,36 @@
+import { compose, initial } from "@dage/service";
+import router from "./router";
+
+const showMessage = (msg, type = "error") => {
+  ElMessage({
+    type,
+    message: msg,
+    duration: 4000,
+  });
+};
+
+initial({
+  fetch: window.fetch.bind(window),
+  baseURL: import.meta.env.VITE_BASE_URL,
+  interceptor: compose(async (request, next) => {
+    try {
+      const response = await next();
+      const { showError = true } = request.meta;
+
+      if (response.code !== 0 && showError) {
+        if ([2404, 2001].includes(response.code)) {
+          router.replace({ name: "home" });
+
+          return response;
+        }
+
+        const message = response.__raw__.data.msg ?? "请稍后重试";
+        showMessage(message);
+      }
+
+      return response;
+    } catch (err) {
+      showMessage("请稍后重试");
+    }
+  }),
+});

+ 1 - 0
packages/pc/src/main.js

@@ -3,6 +3,7 @@ import "@lsq/base/src/theme.scss";
 import "element-plus/theme-chalk/dark/css-vars.css";
 import "element-plus/theme-chalk/el-notification.css";
 import "virtual:svg-icons-register";
+import "./configure";
 
 import { createApp } from "vue";
 import { createPinia } from "pinia";

+ 9 - 1
packages/pc/src/stores/base.js

@@ -3,6 +3,14 @@ import { defineStore } from "pinia";
 
 export const useBaseStore = defineStore("base", () => {
   const isLogin = ref(false);
+  const loginVisible = ref(false);
 
-  return { isLogin };
+  const loginValidator = () => {
+    if (isLogin.value) return true;
+
+    loginVisible.value = true;
+    return false;
+  };
+
+  return { isLogin, loginVisible, loginValidator };
 });

+ 8 - 1
packages/pc/src/stores/detail.js

@@ -2,6 +2,7 @@ import { ref } from "vue";
 import { defineStore } from "pinia";
 
 export const useDetailStore = defineStore("detail", () => {
+  const detail = ref(null);
   const searchVisible = ref(false);
   const searchKey = ref("");
 
@@ -15,5 +16,11 @@ export const useDetailStore = defineStore("detail", () => {
     searchVisible.value = true;
   };
 
-  return { searchVisible, searchKey, introductionVisible, openSearchDrawer };
+  return {
+    detail,
+    searchVisible,
+    searchKey,
+    introductionVisible,
+    openSearchDrawer,
+  };
 });

+ 325 - 0
packages/pc/src/views/Bookshelf/components/UploadDialog.vue

@@ -0,0 +1,325 @@
+<template>
+  <el-dialog
+    v-model="show"
+    class="upload-dialog"
+    title="上传图书"
+    width="663px"
+    :close-on-click-modal="false"
+  >
+    <el-form ref="ruleFormRef" :model="form" :rules="rules" label-width="140px">
+      <el-form-item label="书名" prop="name">
+        <el-input
+          v-model="form.name"
+          clearable
+          class="w350"
+          placeholder="请填入内容,不超过20个字"
+          :maxlength="20"
+        />
+      </el-form-item>
+      <el-form-item label="简介" prop="description">
+        <el-input
+          v-model="form.description"
+          type="textarea"
+          class="w350"
+          placeholder="请填入内容,不超过200个字"
+          :maxlength="200"
+          :autosize="{ minRows: 5 }"
+        />
+      </el-form-item>
+      <el-form-item label="作者" prop="author">
+        <el-input
+          v-model="form.author"
+          clearable
+          class="w350"
+          placeholder="请填入内容,不超过20个字"
+          :maxlength="20"
+        />
+      </el-form-item>
+      <el-form-item label="出版社" prop="press">
+        <el-input
+          v-model="form.press"
+          clearable
+          class="w350"
+          placeholder="请填入内容,不超过20个字"
+          :maxlength="20"
+        />
+      </el-form-item>
+      <el-form-item label="出版年份" prop="year">
+        <el-date-picker
+          v-model="form.year"
+          clearable
+          class="w350"
+          type="year"
+          placeholder="请选择年份"
+        />
+      </el-form-item>
+      <el-form-item label="封面" prop="thumb">
+        <div>
+          <el-upload
+            class="upload-dialog__upload"
+            :http-request="handleUpload"
+            :show-file-list="false"
+            accept="image/jpeg,image/png,image/jpg"
+            :before-upload="onBeforeUploadImage"
+          >
+            <img v-if="imageUrl" :src="imageUrl" />
+            <div v-else>
+              <el-icon
+                :size="30"
+                color="var(--el-color-primary)"
+                class="avatar-uploader-icon"
+                ><Plus
+              /></el-icon>
+            </div>
+          </el-upload>
+          <p style="font-size: 12px; color: var(--text-color-placeholder)">
+            格式要求:支持png、jpg图片格式;最大支持5M,最多1张
+          </p>
+        </div>
+      </el-form-item>
+      <el-form-item label="中图法分类" prop="storageId">
+        <el-cascader
+          v-model="form.storageId"
+          class="w350"
+          :loading="storageLoading"
+          :options="storageList"
+          placeholder="请选择"
+          :props="{ value: 'id', label: 'name', checkStrictly: true }"
+        />
+      </el-form-item>
+      <el-form-item label="展示分类" prop="exhibitTypeId">
+        <el-select
+          v-model="form.exhibitTypeId"
+          :loading="exhibitLoading"
+          placeholder="请选择"
+          class="w350"
+        >
+          <el-option
+            v-for="item in exhibitList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="ISBN编号" prop="num">
+        <el-input
+          v-model="form.num"
+          class="w350"
+          clearable
+          placeholder="请填入内容,不超过20个字"
+          :maxlength="20"
+        />
+      </el-form-item>
+      <el-form-item label="验证码" prop="randCode">
+        <div class="w350" style="display: flex; gap: 10px">
+          <el-input
+            v-model="form.randCode"
+            class="code"
+            type="number"
+            placeholder="请输入验证码"
+          />
+          <el-image
+            style="flex: 1; height: 50px; cursor: pointer"
+            :src="`${baseUrl}/api/wx/getRandCode?t=${timestamp}`"
+            @click="timestamp = new Date().getTime()"
+          />
+        </div>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          class="upload-dialog__btn confirm"
+          type="primary"
+          @click="handleSubmit"
+          >提交</el-button
+        >
+        <el-button
+          class="upload-dialog__btn"
+          type="primary"
+          plain
+          @click="handleClose"
+          >取消</el-button
+        >
+      </el-form-item>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed, ref, reactive, watch } from "vue";
+import { Plus } from "@element-plus/icons-vue";
+import {
+  uploadApi,
+  getStorageTreeApi,
+  getExhibitTypeListApi,
+  getBaseUrl,
+} from "@lsq/base";
+
+const DEFAULT_FORM = {
+  name: "",
+  description: "",
+  author: "",
+  author: "",
+  year: "",
+  thumb: "",
+  storageId: "",
+  exhibitTypeId: "",
+  num: "",
+  randCode: "",
+};
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    required: true,
+  },
+});
+const emits = defineEmits(["update:visible"]);
+const ruleFormRef = ref();
+const baseUrl = getBaseUrl();
+const storageLoading = ref(false);
+const storageList = ref([]);
+const exhibitLoading = ref(false);
+const exhibitList = ref([]);
+const timestamp = ref(new Date().getTime());
+const form = reactive({ ...DEFAULT_FORM });
+const rules = reactive({
+  randCode: [{ required: true, message: "请输入验证码", trigger: "blur" }],
+  num: [{ required: true, message: "请输入编号", trigger: "blur" }],
+  name: [{ required: true, message: "请输入书名", trigger: "blur" }],
+  author: [{ required: true, message: "请输入作者", trigger: "blur" }],
+  press: [{ required: true, message: "请输入出版社", trigger: "blur" }],
+  year: [{ required: true, message: "请选择出版年份", trigger: "blur" }],
+  thumb: [{ required: true, message: "请上传封面", trigger: "blur" }],
+  storageId: [{ required: true, message: "请选择", trigger: "blur" }],
+  exhibitTypeId: [{ required: true, message: "请选择", trigger: "blur" }],
+});
+const imageUrl = ref("");
+
+const show = computed({
+  get() {
+    return props.visible;
+  },
+  set(v) {
+    emits("update:visible", v);
+  },
+});
+
+const onBeforeUploadImage = (file) => {
+  const isIMAGE = ["image/jpeg", "image/jpg", "image/png"].includes(file.type);
+  const isLt1M = file.size / 1024 / 1024 < 5;
+  if (!isIMAGE) {
+    ElMessage.error("上传文件只能是图片格式!");
+  }
+  if (!isLt1M) {
+    ElMessage.error("上传文件大小不能超过 5MB!");
+  }
+  return isIMAGE && isLt1M;
+};
+
+const handleUpload = async (params) => {
+  const formData = new FormData();
+  formData.append("file", params.file);
+  formData.append("type", "img");
+  try {
+    const data = await uploadApi(formData);
+    console.log(data);
+    params.onSuccess();
+  } catch (err) {
+    params.onError();
+  }
+};
+
+const getStorageTree = async () => {
+  try {
+    storageLoading.value = true;
+    const data = await getStorageTreeApi();
+    storageList.value = data;
+  } finally {
+    storageLoading.value = false;
+  }
+};
+
+const getExhibitTypeList = async () => {
+  try {
+    exhibitLoading.value = true;
+    const data = await getExhibitTypeListApi();
+    exhibitList.value = data;
+  } finally {
+    exhibitLoading.value = false;
+  }
+};
+
+const handleSubmit = async () => {
+  await ruleFormRef.value.validate((valid) => {
+    if (valid) {
+      console.log("submit!");
+    }
+  });
+};
+
+const handleClose = () => {
+  form.value = { ...DEFAULT_FORM };
+  show.value = false;
+  ruleFormRef.value.resetFields();
+};
+
+watch(show, (v) => {
+  if (v) {
+    !exhibitList.value.length && getExhibitTypeList();
+    !storageList.value.length && getStorageTree();
+  }
+});
+</script>
+
+<style lang="scss">
+.upload-dialog {
+  &__btn {
+    width: 132px;
+    height: 55px;
+    border-radius: 0;
+    font-size: 16px;
+    background: white;
+
+    &.confirm {
+      border: 0;
+      background: url("@/assets/images/btn_02-min.png") no-repeat center right /
+        cover;
+    }
+  }
+  .el-dialog__header {
+    font-size: 24px;
+    font-family: "Source Han Serif CN-Bold";
+  }
+  .el-form {
+    --el-form-label-font-size: 18px;
+  }
+  .el-input {
+    --el-input-height: 50px;
+  }
+  .el-form-item__label {
+    height: 50px;
+    line-height: 50px;
+  }
+  .el-select__wrapper {
+    height: 50px;
+  }
+  .code {
+    width: 167px;
+  }
+  &__upload {
+    width: 85px;
+    height: 85px;
+    border-radius: 5px;
+    border: 1px dotted #d1bb9e;
+
+    div {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 100%;
+      height: 100%;
+    }
+  }
+}
+</style>

+ 12 - 0
packages/pc/src/views/Bookshelf/index.scss

@@ -16,6 +16,15 @@
     padding: 0 50px;
     height: 207px;
 
+    &__upload {
+      width: 144px;
+      height: 55px;
+      border: 0;
+      border-radius: 0;
+      font-size: 20px;
+      font-family: "Source Han Serif CN-Bold";
+      background: #d3bfa2;
+    }
     &__logout {
       width: 132px;
       height: 55px;
@@ -25,6 +34,9 @@
       font-family: "Source Han Serif CN-Bold";
       background: url("@/assets/images/btn_02-min.png") no-repeat center / cover;
     }
+    .el-button + .el-button {
+      margin-left: 20px;
+    }
   }
 
   &-info {

+ 39 - 6
packages/pc/src/views/Bookshelf/index.vue

@@ -19,15 +19,34 @@
           <div class="bookshelf-info-inner">
             <div class="bookshelf-info__name">
               <span>SAMSARA</span>
-              <img draggable="false" src="@/assets/images/icon_edit-min.png" />
+              <img
+                draggable="false"
+                src="@/assets/images/icon_edit-min.png"
+                @click="openEditName"
+              />
             </div>
             <p class="bookshelf-info__id">ID:6855576858</p>
           </div>
         </div>
 
-        <el-button type="primary" class="bookshelf-header__logout"
-          >退出登录</el-button
-        >
+        <div>
+          <el-button
+            type="primary"
+            class="bookshelf-header__upload"
+            @click="uploadVisible = true"
+          >
+            <svg-icon
+              name="icon_upload_yellow"
+              width="24px"
+              height="24px"
+              color="white"
+            />
+            上传图书</el-button
+          >
+          <el-button type="primary" class="bookshelf-header__logout"
+            >退出登录</el-button
+          >
+        </div>
       </div>
 
       <div class="bookshelf-inner">
@@ -35,22 +54,25 @@
 
         <ul>
           <li v-for="key in 10" :key="key">
-            <book-card type="column" size="large" show-like />
+            <!-- <book-card type="column" size="large" show-like /> -->
           </li>
         </ul>
       </div>
     </div>
   </div>
+
+  <upload-dialog v-model:visible="uploadVisible" />
 </template>
 
 <script setup>
 import { ref } from "vue";
-import { ElMessage } from "element-plus";
 import BookCard from "@/components/BookCard/index.vue";
+import UploadDialog from "./components/UploadDialog.vue";
 
 const imageUrl = ref(
   "https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100"
 );
+const uploadVisible = ref(false);
 
 const beforeAvatarUpload = (rawFile) => {
   if (rawFile.type !== "image/jpeg") {
@@ -62,6 +84,17 @@ const beforeAvatarUpload = (rawFile) => {
   }
   return true;
 };
+
+const openEditName = () => {
+  ElMessageBox.prompt("修改昵称", "", {
+    confirmButtonText: "确认",
+    cancelButtonText: "取消",
+    inputPattern: /^[\w\W]{1,20}$/,
+    inputErrorMessage: "昵称长度为1-20个字符",
+  }).then(({ value }) => {
+    console.log(value);
+  });
+};
 </script>
 
 <style lang="scss" scoped>

+ 12 - 5
packages/pc/src/views/Detail/components/Directory.vue

@@ -2,12 +2,12 @@
   <Drawer v-model:visible="show" @close="emits('close')">
     <div class="directory">
       <div class="directory-info">
-        <el-image src="" />
+        <el-image :src="baseUrl + detail?.thumb" fit="cover" />
 
         <div class="directory-info-inner">
-          <h1>刘少奇传</h1>
-          <p>金冲及</p>
-          <p>中共中央文献研究室 编</p>
+          <h1>{{ detail?.name }}</h1>
+          <p>{{ detail?.author }}</p>
+          <p>{{ detail?.press }} 编</p>
         </div>
       </div>
 
@@ -26,13 +26,18 @@
 
 <script setup>
 import { computed } from "vue";
-import { useEpubStore } from "@/stores";
+import { storeToRefs } from "pinia";
+import { getBaseUrl } from "@lsq/base";
+import { useEpubStore, useDetailStore } from "@/stores";
 import Drawer from "./Drawer.vue";
 
 const props = defineProps(["visible"]);
 const emits = defineEmits(["update:visible", "close"]);
 
+const baseUrl = getBaseUrl();
 const epubStore = useEpubStore();
+const detailStore = useDetailStore();
+const { detail } = storeToRefs(detailStore);
 const show = computed({
   get() {
     return props.visible;
@@ -59,10 +64,12 @@ const goToChapter = (href) => {
     gap: 22px;
 
     .el-image {
+      flex-shrink: 0;
       width: 87px;
       height: 117px;
     }
     h1 {
+      padding-right: 20px;
       font-size: 24px;
       font-family: "Source Han Serif CN-Bold";
     }

+ 15 - 7
packages/pc/src/views/Detail/components/IntroductionDialog.vue

@@ -6,11 +6,15 @@
     width="1174px"
   >
     <div class="introduction-dialog-left">
-      <el-image class="introduction-dialog-left__cover" src="" />
+      <el-image
+        fit="cover"
+        class="introduction-dialog-left__cover"
+        :src="baseUrl + detail.thumb"
+      />
       <div class="introduction-dialog-left-info">
-        <h3>刘少奇传</h3>
-        <p class="introduction-dialog-left-info__auther">金冲及</p>
-        <p>中共中央文献研究室 编</p>
+        <h3>{{ detail.name }}</h3>
+        <p class="introduction-dialog-left-info__auther">{{ detail.author }}</p>
+        <p>{{ detail.press }} 编</p>
       </div>
     </div>
     <div class="introduction-dialog-right">
@@ -20,9 +24,7 @@
           style="margin-top: 30px; padding: 0 10px; font-size: 18px"
           :height="610 - 65"
         >
-          蜿蜒曲折的湘江,像一条绿色的玉带,从南到北缓缓穿越湖南全省,注入中国第二大淡水湖洞庭湖。在湘江西侧的宁乡县境内,有一个普普通通的小山村,叫炭子冲。相传在很久以前,这一带有不少人以伐木烧炭为生,是烧炭人居住和落脚的地方,因此得名炭子冲。
-          “冲”​,是湖南老百姓对山间小块平原的称呼。炭子冲,就是一块夹在两座山岭之间的平地。它的北面背靠着连绵不绝的丘陵,东西两边是长满了密密层层各色杂树的山坡,南面是平坦的农田和宁静的池塘。湘江的支流靳江,在它的西南角不远处淙淙流过。顺着冲口的大路往东北方向行进,约莫四十来公里,便到了湘江。湘江对岸,就是湖南省省会长沙。
-          炭子冲在行政建制上属于湖南省宁乡县花明楼乡。这一带有山有水,盛产稻米、林木、烟叶,是湖南中部较为富庶的地区。由于这里离省会和县城都不远,交通便利,外面的信息容易传播进来,文化也比较发达。
+          <div v-html="detail.description" />
         </el-scrollbar>
       </div>
     </div>
@@ -30,6 +32,7 @@
 </template>
 
 <script setup>
+import { getBaseUrl } from "@lsq/base";
 import { computed } from "vue";
 
 const props = defineProps({
@@ -37,9 +40,14 @@ const props = defineProps({
     type: Boolean,
     required: true,
   },
+  detail: {
+    type: [Object, null],
+    required: true,
+  },
 });
 const emits = defineEmits(["update:visible"]);
 
+const baseUrl = getBaseUrl();
 const show = computed({
   get() {
     return props.visible;

+ 5 - 1
packages/pc/src/views/Detail/components/Reader/index.vue

@@ -79,6 +79,9 @@ import useClipboard from "vue-clipboard3";
 import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
 import { useEpubStore, useDetailStore } from "@/stores";
 import { COLOR_MAP } from "./constants";
+import { getBaseUrl } from "@lsq/base";
+
+const props = defineProps(["detail"]);
 
 const epubStore = useEpubStore();
 const detailStore = useDetailStore();
@@ -86,10 +89,11 @@ const { toClipboard } = useClipboard();
 const selectMenuStyle = ref({});
 const selectedCfi = ref("");
 const selectedCfiStack = ref([]);
+const baseUrl = getBaseUrl();
 
 onMounted(() => {
   epubStore.init({
-    url: "./test.epub",
+    url: baseUrl + props.detail.filePath,
   });
 
   epubStore.rendition.hooks.render.register((v) => {

+ 25 - 6
packages/pc/src/views/Detail/index.vue

@@ -1,16 +1,20 @@
 <template>
-  <page-pane :container-color="paneBgColor" :simple="isPhotocopy">
-    <component :is="comp" />
+  <page-pane
+    v-loading.fullscreen.lock="loading"
+    :container-color="paneBgColor"
+    :simple="isPhotocopy"
+  >
+    <component v-if="detail" :is="comp" :detail="detail" />
   </page-pane>
 
   <toolbar v-if="route.params.type === 'reader'" />
-  <introduction-dialog v-model:visible="introductionVisible" />
+  <introduction-dialog v-model:visible="introductionVisible" :detail="detail" />
 </template>
 
 <script setup>
-import { computed } from "vue";
+import { computed, ref, onMounted } from "vue";
 import { useRoute } from "vue-router";
-import { THEMES } from "@lsq/base";
+import { THEMES, getBookDetailApi } from "@lsq/base";
 import { storeToRefs } from "pinia";
 import { useEpubStore, useDetailStore } from "@/stores";
 import Toolbar from "./components/Toolbar/index.vue";
@@ -22,8 +26,9 @@ import IntroductionDialog from "./components/IntroductionDialog.vue";
 const route = useRoute();
 const epubStore = useEpubStore();
 const detailStore = useDetailStore();
-const { introductionVisible } = storeToRefs(detailStore);
+const { introductionVisible, detail } = storeToRefs(detailStore);
 
+const loading = ref(false);
 const paneBgColor = computed(
   () => THEMES.find((theme) => theme.key === epubStore.curTheme).color
 );
@@ -39,6 +44,20 @@ const comp = computed(() => {
       return Reader;
   }
 });
+
+const getBookDetail = async () => {
+  try {
+    loading.value = true;
+    const data = await getBookDetailApi(route.params.id);
+    detail.value = data;
+  } finally {
+    loading.value = false;
+  }
+};
+
+onMounted(() => {
+  getBookDetail();
+});
 </script>
 
 <style lang="scss" scoped>

+ 1 - 0
packages/pc/src/views/Home/index.scss

@@ -72,6 +72,7 @@
     color: var(--el-color-primary);
     background: url("./images/btn_more-min.png") no-repeat center / contain;
     box-sizing: border-box;
+    text-align: center;
     cursor: pointer;
   }
   &-more {

+ 71 - 12
packages/pc/src/views/Home/index.vue

@@ -1,5 +1,11 @@
 <template>
-  <iframe class="home-iframe" src="./three/index.html" frameborder="0"></iframe>
+  <iframe
+    id="iframe"
+    class="home-iframe"
+    src="./three/index.html"
+    frameborder="0"
+  ></iframe>
+
   <div ref="txtDom" class="txt" :style="{ opacity: txt.show ? '0.8' : '0' }">
     <h2>{{ txt.title }}</h2>
     <p>{{ txt.con }}</p>
@@ -14,15 +20,17 @@
 
     <div class="home-search">
       <input
+        v-model="keyword"
         class="home-search__input"
         type="text"
         placeholder="请输入关键词..."
+        @keyup.enter="handleSearch"
       />
-      <button class="home-search__btn">搜索</button>
+      <button class="home-search__btn" @click="handleSearch">搜索</button>
     </div>
 
-    <div class="home-view">
-      <p class="limit-line">共收录132件藏品,查看书库</p>
+    <div class="home-view" @click="$router.push({ name: 'stack' })">
+      <p class="limit-line">共收录{{ total }}件藏品,查看书库</p>
     </div>
   </div>
 
@@ -34,25 +42,36 @@
 
 <script setup>
 import { onMounted, ref } from "vue";
+import { getRecommendListApi, getBookCountApi, getBaseUrl } from "@lsq/base";
+import { useRouter } from "vue-router";
 
+const router = useRouter();
 const txt = ref({ show: false });
 const txtDom = ref(null);
+const baseUrl = getBaseUrl();
+const list = ref([]);
+const total = ref("--");
+const keyword = ref("");
 
 onMounted(() => {
   // 点击图片
   window.clickObject = (val) => {
-    console.log("000", val);
+    const item = list.value.find((i) => i.thumb.indexOf(val) > -1);
+    router.push({
+      name: "detail",
+      params: {
+        id: item.id,
+        type: "reader",
+      },
+    });
   };
   // 鼠标移入
   window.hoverObject = (val) => {
-    let con =
-      "我是一个文物介绍,我是一个文物介绍,我是一个文物我是一个一个文物介绍,我是一个文物介绍我是一个文物介绍,我是一个文物介绍,我是一个文物我是一个一个文物介绍,我是一个文物介绍";
-    if (val.imgName.includes("1")) {
-      con = "xxxxxxxxxx阿三大苏打xxxxxxxxxx阿三大苏打";
-    }
+    const item = list.value[val.target.index];
+
     txt.value = {
-      title: "文物" + val.imgName,
-      con,
+      title: item.name,
+      con: item.author,
       show: true,
     };
   };
@@ -76,7 +95,47 @@ onMounted(() => {
     txtDom.value.style.top = yRes + "px";
     txtDom.value.style.left = xRes + "px";
   };
+
+  getRecommendList();
+  getBookCount();
 });
+
+const getRecommendList = async () => {
+  const data = await getRecommendListApi({
+    pageNum: 1,
+    pageSize: 20,
+    type: "index",
+  });
+
+  const iframe = document.getElementById("iframe");
+  const iframeDoc = iframe?.contentDocument || iframe?.contentWindow.document;
+  if (iframeDoc?.readyState === "complete") {
+    iframe.contentWindow.initViewer(data.map((i) => `${baseUrl}${i.thumb}`));
+  } else {
+    iframe.onload = () => {
+      iframe.contentWindow.initViewer(data.map((i) => `${baseUrl}${i.thumb}`));
+    };
+  }
+  list.value = data;
+};
+
+const getBookCount = async () => {
+  const data = await getBookCountApi();
+  total.value = data;
+};
+
+const handleSearch = () => {
+  if (!keyword.value) {
+    ElMessage.error("请输入要搜索的内容");
+    return;
+  }
+  router.push({
+    name: "stack",
+    query: {
+      keyword: keyword.value,
+    },
+  });
+};
 </script>
 
 <style lang="scss" scoped>

+ 44 - 11
packages/pc/src/views/Home2/components/News.vue

@@ -1,44 +1,77 @@
 <template>
-  <div class="home2-news">
+  <div v-loading="loading" class="home2-news">
     <div class="w1100">
       <p class="home2-news__title">
         <img draggable="false" src="../images/text_news-min.png" />公告
       </p>
 
-      <div class="home2-news-list">
+      <div v-if="list.length" class="home2-news-list">
         <div
-          v-for="item in 19"
-          :key="item"
+          v-for="item in list"
+          :key="item.id"
           class="home2-news-item"
-          @click="dialogVisible = true"
+          @click="handleClick(item)"
         >
-          <span class="home2-news-item__tag">【公告公示】</span>
+          <span class="home2-news-item__tag">【{{ item.dictName }}】</span>
           <p class="limit-line">
-            2023年度 刘少奇同志纪念馆(刘少奇故里管理局)部门决算
+            {{ item.name }}
           </p>
-          <span class="home2-news-item__date">2024-09-07</span>
+          <span class="home2-news-item__date">{{
+            formatDate(item.createTime, "YYYY-MM-DD")
+          }}</span>
         </div>
       </div>
+      <div v-else>
+        <el-empty description="暂无数据">
+          <template #image><div /></template>
+        </el-empty>
+      </div>
 
       <div class="home2-news-pagination">
         <el-pagination
+          hide-on-single-page
           layout="prev, pager, next"
-          :total="50"
+          :total="total"
           prev-text="上一页"
           next-text="下一页"
+          @change="handlePage"
         />
       </div>
     </div>
   </div>
 
-  <news-dialog v-model:visible="dialogVisible" />
+  <news-dialog v-model:visible="dialogVisible" :item="checkedItem" />
 </template>
 
 <script setup>
-import { ref } from "vue";
+import { ref, onMounted } from "vue";
+import { formatDate } from "@dage/utils";
+import { getNoticeListApi, usePagination } from "@lsq/base";
 import NewsDialog from "./NewsDialog.vue";
 
 const dialogVisible = ref(false);
+const checkedItem = ref(null);
+const { pageNum, total, loading, list, noData, getList } = usePagination(
+  async (params) => {
+    return getNoticeListApi({
+      ...params,
+    });
+  }
+);
+
+onMounted(() => {
+  getList();
+});
+
+const handlePage = (page) => {
+  pageNum.value = page;
+  getList();
+};
+
+const handleClick = (item) => {
+  checkedItem.value = item;
+  dialogVisible.value = true;
+};
 </script>
 
 <style lang="scss" scoped>

Dosya farkı çok büyük olduğundan ihmal edildi
+ 28 - 17
packages/pc/src/views/Home2/components/NewsDialog.vue


+ 51 - 3
packages/pc/src/views/Home2/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="home2">
-    <div class="home2-top">
+    <div v-if="isLogin" class="home2-top">
       <div class="w1100">
         <div class="home2-top-lf">
           <div>
@@ -24,13 +24,21 @@
     </div>
 
     <div class="home2-main w1100">
-      <rank-panel sub-title="用户最喜爱的文献">
+      <rank-panel
+        v-loading="recommendLoading"
+        sub-title="用户最喜爱的文献"
+        :list="recommendList"
+      >
         <template #title-prepend>
           <img draggable="false" src="./images/text_recommend-min.png" />
         </template>
       </rank-panel>
 
-      <rank-panel sub-title="阅读量最多的文献">
+      <rank-panel
+        v-loading="readLoading"
+        sub-title="阅读量最多的文献"
+        :list="readList"
+      >
         <template #title-prepend>
           <img draggable="false" src="./images/text_readomg-min.png" />
         </template>
@@ -42,9 +50,49 @@
 </template>
 
 <script setup>
+import { ref, onMounted } from "vue";
+import { storeToRefs } from "pinia";
+import { useBaseStore } from "@/stores";
+import { getRecommendListApi, getReadListApi } from "@lsq/base";
 import BookCard from "@/components/BookCard/index.vue";
 import RankPanel from "@/components/RankPanel/index.vue";
 import News from "./components/News.vue";
+
+const baseStore = useBaseStore();
+const { isLogin } = storeToRefs(baseStore);
+const recommendLoading = ref(false);
+const recommendList = ref([]);
+const readLoading = ref(false);
+const readList = ref([]);
+
+onMounted(() => {
+  getRecommendList();
+  getReadList();
+});
+
+const getReadList = async () => {
+  try {
+    readLoading.value = true;
+    const data = await getReadListApi();
+    readList.value = data;
+  } finally {
+    readLoading.value = false;
+  }
+};
+
+const getRecommendList = async () => {
+  try {
+    recommendLoading.value = true;
+    const data = await getRecommendListApi({
+      pageNum: 1,
+      pageSize: 10,
+      type: "rank",
+    });
+    recommendList.value = data;
+  } finally {
+    recommendLoading.value = false;
+  }
+};
 </script>
 
 <style lang="scss" scoped>

+ 0 - 14
packages/pc/src/views/Stack/components/Sidebar/index.scss

@@ -35,20 +35,6 @@
     background-color: #d1bb9e;
   }
 
-  :deep(.el-tree-node__content) {
-    height: $itemHeight;
-    font-size: 18px;
-    font-family: "Source Han Serif CN-Bold";
-  }
-  :deep(.el-tree-node__label) {
-    padding-left: 5px;
-  }
-  :deep(.el-tree-node__children) {
-    .el-tree-node__content {
-      font-size: 14px;
-    }
-  }
-
   .search-input {
     margin-bottom: 20px;
   }

+ 135 - 110
packages/pc/src/views/Stack/components/Sidebar/index.vue

@@ -1,141 +1,166 @@
 <template>
   <div class="stack-sidebar">
-    <el-tabs v-model="activeTab">
-      <el-tab-pane label="按书籍分类" name="book">
-        <search-input v-model="bookSearchKey" />
+    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+      <search-input v-model="searchKey" />
 
-        <el-scrollbar scroll-y max-height="100%">
+      <el-tab-pane label="按书籍分类" name="book">
+        <el-scrollbar v-loading="bookLoading" scroll-y max-height="100%">
           <div class="stack-sidebar-list">
-            <div class="stack-sidebar-item active">全部</div>
-
-            <el-tree
-              :data="bookList"
-              node-key="id"
-              :expand-on-click-node="false"
+            <tree-menu
+              v-if="loaded"
+              :default-openeds="defaultOpeneds"
+              ref="menuRef"
+              :menu="bookList"
+              :selected="checkedId"
+              @select="handleSelect"
             />
           </div>
         </el-scrollbar>
       </el-tab-pane>
 
       <el-tab-pane label="按藏品分类" name="collection">
-        <search-input />
+        <el-scrollbar v-loading="collectionLoading" scroll-y max-height="100%">
+          <div class="stack-sidebar-list">
+            <tree-menu
+              ref="menuRef"
+              :menu="collectionList"
+              :selected="checkedId"
+              @select="handleSelect"
+            />
+          </div>
+        </el-scrollbar>
       </el-tab-pane>
     </el-tabs>
   </div>
 </template>
 
 <script setup>
-import { ref } from "vue";
+import { ref, onMounted } from "vue";
+import { useRouteQuery } from "@vueuse/router";
+import { getStorageTreeApi, getExhibitTypeListApi } from "@lsq/base";
+import TreeMenu from "@/components/TreeMenu.vue";
 import SearchInput from "../SearchInput/index.vue";
 
+const emits = defineEmits(["select"]);
+
 /**
  * 当前选项
  * book-书籍 collection-藏品
  */
-const activeTab = ref("book");
-const bookSearchKey = ref("");
-const bookList = ref([
-  {
-    id: 1,
-    label: "Level one 1",
-    children: [
-      {
-        id: 4,
-        label: "Level two 1-1",
-        children: [
-          {
-            id: 9,
-            label: "Level three 1-1-1",
-          },
-          {
-            id: 10,
-            label: "Level three 1-1-2",
-          },
-        ],
-      },
-    ],
-  },
-  {
-    id: 1,
-    label: "Level one 1",
-    children: [
-      {
-        id: 4,
-        label: "Level two 1-1",
-        children: [
-          {
-            id: 9,
-            label: "Level three 1-1-1",
-          },
-          {
-            id: 10,
-            label: "Level three 1-1-2",
-          },
-        ],
-      },
-    ],
-  },
-  {
-    id: 1,
-    label: "Level one 1",
-    children: [
-      {
-        id: 4,
-        label: "Level two 1-1",
-        children: [
-          {
-            id: 9,
-            label: "Level three 1-1-1",
-          },
-          {
-            id: 10,
-            label: "Level three 1-1-2",
-          },
-        ],
-      },
-    ],
+const activeTab = useRouteQuery("active", "book");
+const searchKey = useRouteQuery("keyword", "", {
+  transform(t) {
+    return decodeURIComponent(t);
   },
+});
+const checkedId = useRouteQuery("checked", -1, { transform: Number });
+const defaultOpeneds = ref(null);
+const loaded = ref(false);
+
+const bookLoading = ref(false);
+const bookList = ref([
   {
-    id: 1,
-    label: "Level one 1",
-    children: [
-      {
-        id: 4,
-        label: "Level two 1-1",
-        children: [
-          {
-            id: 9,
-            label: "Level three 1-1-1",
-          },
-          {
-            id: 10,
-            label: "Level three 1-1-2",
-          },
-        ],
-      },
-    ],
+    name: "全部",
+    id: -1,
   },
+]);
+
+const collectionLoading = ref(false);
+const collectionList = ref([
   {
-    id: 1,
-    label: "Level one 1",
-    children: [
-      {
-        id: 4,
-        label: "Level two 1-1",
-        children: [
-          {
-            id: 9,
-            label: "Level three 1-1-1",
-          },
-          {
-            id: 10,
-            label: "Level three 1-1-2",
-          },
-        ],
-      },
-    ],
+    name: "全部",
+    id: -1,
   },
 ]);
+
+const getStorageTree = async () => {
+  try {
+    bookLoading.value = true;
+    const data = await getStorageTreeApi();
+    bookList.value = bookList.value.concat(data);
+
+    emits("select", [bookList.value[0]]);
+  } finally {
+    bookLoading.value = false;
+  }
+};
+
+const getExhibitTypeList = async () => {
+  try {
+    collectionLoading.value = true;
+    const data = await getExhibitTypeListApi();
+    collectionList.value = collectionList.value.concat(data);
+  } finally {
+    collectionLoading.value = false;
+  }
+};
+
+const getBreadCrumb = (item) => {
+  if (!item.ancestor) return [item];
+
+  let temp = bookList.value;
+  const stack = [];
+  const parentIds = item.ancestor.split(",").map((i) => Number(i));
+
+  while (parentIds.length && Array.isArray(temp) && temp.length) {
+    const id = parentIds.shift();
+    const node = temp.find((i) => i.id === id);
+    stack.push(node);
+    temp = node.children;
+  }
+  stack.push(item);
+
+  return stack;
+};
+
+const handleSelect = (item) => {
+  checkedId.value = item.id;
+  emits("select", getBreadCrumb(item));
+};
+
+const findItemById = (data, id) => {
+  for (const item of data) {
+    if (item.id === id) return item;
+    if (item.children && item.children.length) {
+      const res = findItemById(item.children, id);
+      if (res) return res;
+    }
+  }
+  return null;
+};
+
+const handleTab = async () => {
+  const isCollection = activeTab.value === "collection";
+  if (isCollection) {
+    collectionList.value.length === 1 && (await getExhibitTypeList());
+  } else {
+    bookList.value.length === 1 && (await getStorageTree());
+  }
+
+  const item = findItemById(
+    isCollection ? collectionList.value : bookList.value,
+    checkedId.value
+  );
+
+  if (item) {
+    handleSelect(item);
+    return item;
+  }
+};
+
+const handleTabChange = () => {
+  checkedId.value = -1;
+  handleTab();
+};
+
+onMounted(async () => {
+  const item = await handleTab();
+  if (item.ancestor) {
+    defaultOpeneds.value = item.ancestor.split(",");
+  }
+
+  loaded.value = true;
+});
 </script>
 
 <style lang="scss" scoped>

+ 4 - 0
packages/pc/src/views/Stack/index.scss

@@ -7,6 +7,7 @@
   &-main {
     flex: 1;
     padding-top: 15px;
+    margin: 0 -12.5px;
     display: flex;
     flex-direction: column;
     align-items: center;
@@ -15,6 +16,9 @@
   &-breadcrumb {
     --el-text-color-primary: rgba(70, 70, 70, 1);
     --el-text-color-regular: rgba(70, 70, 70, 0.5);
+
+    padding: 0 12.5px;
+    box-sizing: border-box;
   }
 
   &-list {

+ 56 - 7
packages/pc/src/views/Stack/index.vue

@@ -1,7 +1,7 @@
 <template>
   <page-pane class="stack">
     <div class="stack-left">
-      <sidebar />
+      <sidebar @select="handleSelect" />
     </div>
 
     <div class="stack-main">
@@ -10,27 +10,76 @@
         :separator-icon="ArrowRight"
         style="width: 100%"
       >
-        <el-breadcrumb-item :to="{ path: '/' }">homepage</el-breadcrumb-item>
-        <el-breadcrumb-item>promotion management</el-breadcrumb-item>
+        <el-breadcrumb-item v-for="item in breadcrumb" :key="item.id">{{
+          item.name
+        }}</el-breadcrumb-item>
       </el-breadcrumb>
 
-      <el-scrollbar style="flex: 1; height: 0; margin: 18px -12.5px">
+      <el-scrollbar
+        v-loading="loading"
+        style="flex: 1; width: 100%; height: 0; margin: 18px 0"
+      >
         <ul class="stack-list">
-          <li v-for="item in 17" :key="item">
-            <book-card type="row" size="large" />
+          <li v-for="item in list" :key="item.id">
+            <book-card type="row" size="large" :item="item" />
           </li>
         </ul>
+
+        <el-empty v-if="noData" description="暂无数据" />
       </el-scrollbar>
 
-      <el-pagination background layout="prev, pager, next" :total="1000" />
+      <el-pagination
+        hide-on-single-page
+        background
+        layout="prev, pager, next"
+        :total="total"
+        @change="handlePage"
+      />
     </div>
   </page-pane>
 </template>
 
 <script setup>
+import { ref, watch } from "vue";
+import { useRouteQuery } from "@vueuse/router";
 import { ArrowRight } from "@element-plus/icons-vue";
+import { getBookListApi, usePagination } from "@lsq/base";
 import Sidebar from "./components/Sidebar/index.vue";
 import BookCard from "@/components/BookCard/index.vue";
+
+const breadcrumb = ref([]);
+const activeTab = useRouteQuery("active");
+const searchKey = useRouteQuery("keyword");
+
+const { pageNum, total, loading, list, noData, getList } = usePagination(
+  async (params) => {
+    const p = {
+      ...params,
+      searchKey: searchKey.value,
+    };
+    const id = breadcrumb.value[breadcrumb.value.length - 1].id;
+    if (activeTab.value === "collection" && id > -1) {
+      p.exhibitTypeId = id;
+    } else if (id > -1) {
+      p.storageId = id;
+    }
+    return getBookListApi(p);
+  }
+);
+
+const handlePage = (page) => {
+  pageNum.value = page;
+  getList();
+};
+
+const handleSelect = (list) => {
+  breadcrumb.value = list;
+};
+
+watch(breadcrumb, (v) => {
+  pageNum.value = 1;
+  v.length && getList();
+});
 </script>
 
 <style lang="scss" scoped>

+ 3 - 1
packages/pc/vite.config.js

@@ -15,9 +15,11 @@ export default defineConfig({
   base: "./",
   publicDir: "public",
   server: {
+    host: "0.0.0.0",
+    port: 80,
     proxy: {
       "/api": {
-        target: "https://sit-shoubov2.4dage.com",
+        target: "https://sit-liushaoqibwg.4dage.com",
         changeOrigin: true,
       },
     },

+ 47 - 1
pnpm-lock.yaml

@@ -6,10 +6,17 @@ settings:
 
 importers:
 
-  .: {}
+  .:
+    dependencies:
+      tslib:
+        specifier: ^2.8.0
+        version: 2.8.0
 
   packages/base:
     dependencies:
+      '@dage/service':
+        specifier: '*'
+        version: 1.0.1(lodash@4.17.21)
       chinese-conv:
         specifier: ^2.1.1
         version: 2.1.1
@@ -80,6 +87,9 @@ importers:
 
   packages/pc:
     dependencies:
+      '@dage/service':
+        specifier: ^1.0.3
+        version: 1.0.3(lodash@4.17.21)
       '@dage/utils':
         specifier: ^1.0.2
         version: 1.0.2(lodash@4.17.21)
@@ -92,6 +102,9 @@ importers:
       '@vueuse/core':
         specifier: ^11.1.0
         version: 11.1.0(vue@3.5.11)
+      '@vueuse/router':
+        specifier: ^11.1.0
+        version: 11.1.0(vue-router@4.4.5)(vue@3.5.11)
       element-plus:
         specifier: ^2.8.3
         version: 2.8.3(vue@3.5.11)
@@ -435,6 +448,22 @@ packages:
     resolution: {integrity: sha512-VHNVJbY5gAMvqur7pOmxZ8W9l4LRnwK/OqMIuAt4VLXLkldUTyyfJmWRkPpCdHDWNbn7bATgAA/+ziV01rozDA==}
     dev: false
 
+  /@dage/service@1.0.1(lodash@4.17.21):
+    resolution: {integrity: sha512-G2KJ3LGrTGFkbuZHopC6ZKynOwo0ilOYXHt6ihivqRIdCLp9NLLEMEMu7vpEpfjla6e5slCBfSKEfNB8Ol/isw==}
+    dependencies:
+      '@dage/utils': 1.0.2(lodash@4.17.21)
+    transitivePeerDependencies:
+      - lodash
+    dev: false
+
+  /@dage/service@1.0.3(lodash@4.17.21):
+    resolution: {integrity: sha512-rZKn9NUQWpZFtzreZyXIl1zK/4+3SsvosuNoq2LWFZl2DakniDdGuLOkkggb1hnRkmscTNQrZQOMpepYrglVWw==}
+    dependencies:
+      '@dage/utils': 1.0.2(lodash@4.17.21)
+    transitivePeerDependencies:
+      - lodash
+    dev: false
+
   /@dage/utils@1.0.2(lodash@4.17.21):
     resolution: {integrity: sha512-txmTlVDYn9wwq1Hfcn0r893c53u5faftqyUjgw95u2hzLHeELI1FM7OxcfOUcYLMOMx4zY1T54M6mpQiCr0VrQ==}
     peerDependencies:
@@ -1194,6 +1223,19 @@ packages:
     resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
     dev: false
 
+  /@vueuse/router@11.1.0(vue-router@4.4.5)(vue@3.5.11):
+    resolution: {integrity: sha512-OjTNIOzX5jD1HDzmDApMke2QsCZ+gWKaydfndKJ3j9ttn41Pr11cQbSY6ZBp+bNjctAR+jhQBV/DGtL3iKpuHg==}
+    peerDependencies:
+      vue-router: '>=4.0.0-rc.1'
+    dependencies:
+      '@vueuse/shared': 11.1.0(vue@3.5.11)
+      vue-demi: 0.14.10(vue@3.5.11)
+      vue-router: 4.4.5(vue@3.5.11)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+    dev: false
+
   /@vueuse/shared@11.1.0(vue@3.5.11):
     resolution: {integrity: sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==}
     dependencies:
@@ -3402,6 +3444,10 @@ packages:
       which-typed-array: 1.1.15
     dev: true
 
+  /tslib@2.8.0:
+    resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==}
+    dev: false
+
   /typed-array-buffer@1.0.2:
     resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==}
     engines: {node: '>= 0.4'}