浏览代码

feat: 简/繁转换

chenlei 10 月之前
父节点
当前提交
a46c7b218c
共有 64 个文件被更改,包括 4380 次插入735 次删除
  1. 8 0
      .prettierignore
  2. 24 0
      packages/base/package.json
  3. 76 0
      packages/base/src/constants.js
  4. 2 0
      packages/base/src/index.js
  5. 128 0
      packages/base/src/stores/epub.js
  6. 1 0
      packages/base/src/stores/index.js
  7. 26 0
      packages/base/src/theme.scss
  8. 6 4
      packages/pc/index.html
  9. 7 2
      packages/pc/package.json
  10. 1 0
      packages/pc/public/epub.min.js
  11. 二进制
      packages/pc/public/favicon.ico
  12. 15 0
      packages/pc/public/jszip.min.js
  13. 二进制
      packages/pc/public/test.epub
  14. 二进制
      packages/pc/public/test2.epub
  15. 25 0
      packages/pc/src/App.vue
  16. 二进制
      packages/pc/src/assets/images/icon_search2-min.png
  17. 二进制
      packages/pc/src/assets/images/logo-min.png
  18. 二进制
      packages/pc/src/assets/images/logo_03-min.png
  19. 16 0
      packages/pc/src/assets/svgs/icon_fullscreen_yellow.svg
  20. 6 0
      packages/pc/src/assets/svgs/icon_like_yellow.svg
  21. 8 0
      packages/pc/src/assets/svgs/icon_mark_yellow.svg
  22. 8 0
      packages/pc/src/assets/svgs/icon_menu_yellow.svg
  23. 8 0
      packages/pc/src/assets/svgs/icon_search_yellow.svg
  24. 8 0
      packages/pc/src/assets/svgs/icon_setting_yellow.svg
  25. 15 0
      packages/pc/src/components/BookCard/index.vue
  26. 31 0
      packages/pc/src/components/PagePane.vue
  27. 13 0
      packages/pc/src/components/SelectItem/index.scss
  28. 54 0
      packages/pc/src/components/SelectItem/index.vue
  29. 39 0
      packages/pc/src/components/SvgIcon.jsx
  30. 二进制
      packages/pc/src/components/TopNav/images/icon_book-min.png
  31. 二进制
      packages/pc/src/components/TopNav/images/icon_book_dark-min.png
  32. 二进制
      packages/pc/src/components/TopNav/images/icon_font-min.png
  33. 二进制
      packages/pc/src/components/TopNav/images/icon_font_active-min.png
  34. 二进制
      packages/pc/src/components/TopNav/images/icon_img-min.png
  35. 二进制
      packages/pc/src/components/TopNav/images/icon_img_active-min.png
  36. 二进制
      packages/pc/src/components/TopNav/images/icon_text-min.png
  37. 二进制
      packages/pc/src/components/TopNav/images/icon_text_active-min.png
  38. 二进制
      packages/pc/src/components/TopNav/images/icon_video-min.png
  39. 二进制
      packages/pc/src/components/TopNav/images/icon_video_active-min.png
  40. 58 1
      packages/pc/src/components/TopNav/index.scss
  41. 101 3
      packages/pc/src/components/TopNav/index.vue
  42. 8 0
      packages/pc/src/main.js
  43. 14 0
      packages/pc/src/router/index.js
  44. 8 0
      packages/pc/src/stores/detail.js
  45. 2 0
      packages/pc/src/stores/index.js
  46. 1 1
      packages/pc/src/views/Bookshelf/index.scss
  47. 74 0
      packages/pc/src/views/Detail/components/Directory.vue
  48. 96 0
      packages/pc/src/views/Detail/components/Drawer.vue
  49. 30 0
      packages/pc/src/views/Detail/components/Reader/index.scss
  50. 40 0
      packages/pc/src/views/Detail/components/Reader/index.vue
  51. 96 0
      packages/pc/src/views/Detail/components/Setting.vue
  52. 41 0
      packages/pc/src/views/Detail/components/Toolbar/index.scss
  53. 104 0
      packages/pc/src/views/Detail/components/Toolbar/index.vue
  54. 二进制
      packages/pc/src/views/Detail/images/btn_03.png
  55. 0 0
      packages/pc/src/views/Detail/index.scss
  56. 18 0
      packages/pc/src/views/Detail/index.vue
  57. 44 0
      packages/pc/src/views/Stack/components/SearchInput/index.vue
  58. 77 0
      packages/pc/src/views/Stack/components/Sidebar/index.scss
  59. 143 0
      packages/pc/src/views/Stack/components/Sidebar/index.vue
  60. 30 0
      packages/pc/src/views/Stack/index.scss
  61. 36 1
      packages/pc/src/views/Stack/index.vue
  62. 14 1
      packages/pc/vite.config.js
  63. 2820 721
      pnpm-lock.yaml
  64. 0 1
      pnpm-workspace.yaml

+ 8 - 0
.prettierignore

@@ -0,0 +1,8 @@
+build/
+public/
+**/*.png
+**/*.svg
+**/*.jpg
+.DS_Store
+.history
+package.json

+ 24 - 0
packages/base/package.json

@@ -0,0 +1,24 @@
+{
+  "name": "@lsq/base",
+  "version": "1.0.0",
+  "description": "",
+  "sideEffects": false,
+  "module": "src/index.js",
+  "main": "src/index.js",
+  "files": [
+    "src"
+  ],
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "peerDependencies": {
+    "pinia": "2.*",
+    "vue": "3.*"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "chinese-conv": "^2.1.1"
+  }
+}

+ 76 - 0
packages/base/src/constants.js

@@ -0,0 +1,76 @@
+export const THEMES = [
+  {
+    key: "default",
+    color: "#ffffff",
+    textColor: "#464646",
+    borderColor: "#D1BB9E",
+  },
+  {
+    key: "rose",
+    color: "#F9F3E8",
+    textColor: "#464646",
+    borderColor: "#9A9A9A",
+  },
+  {
+    key: "green",
+    color: "#D6E9CB",
+    textColor: "#464646",
+    borderColor: "#9A9A9A",
+  },
+  {
+    key: "dark",
+    color: "#2C2C2C",
+    textColor: "#ECECEC",
+    borderColor: "#9A9A9A",
+  },
+];
+
+export const FONT_FAMILYS = [
+  {
+    label: "黑体",
+    value: "Microsoft Yahei",
+  },
+  {
+    label: "宋体",
+    value: "SimSun",
+  },
+  {
+    label: "楷体",
+    value: "KaiTi",
+  },
+  {
+    label: "圆体",
+    value: "YouYuan",
+  },
+  {
+    label: "方体(Mac only)",
+    value: "PingFang SC",
+  },
+];
+
+export const FONT_SIZES = [
+  {
+    label: "14",
+    value: "14",
+  },
+  {
+    label: "16",
+    value: "16",
+  },
+  {
+    label: "18",
+    value: "18",
+  },
+  {
+    label: "20",
+    value: "20",
+  },
+  {
+    label: "22",
+    value: "22",
+  },
+  {
+    label: "24",
+    value: "24",
+  },
+];

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

@@ -0,0 +1,2 @@
+export * from "./stores";
+export * from "./constants";

+ 128 - 0
packages/base/src/stores/epub.js

@@ -0,0 +1,128 @@
+import { ref } from "vue";
+import { tify, sify } from "chinese-conv";
+import { defineStore } from "pinia";
+import { FONT_FAMILYS, THEMES } from "../constants";
+
+export const EPUB_THEME_KEY = "epub-theme";
+export const EPUB_FONT_KEY = "epub-font";
+export const EPUB_FONTSIZE_KEY = "epub-fontsize";
+export const EPUB_SIMPLIFIED = "epub-simplified";
+
+export const useEpubStore = defineStore("epub", () => {
+  let bodyNode = null;
+  const book = ref(null);
+  /**
+   * @see http://epubjs.org/documentation/0.3/#rendition
+   */
+  const rendition = ref(null);
+  /**
+   * 是否简体
+   */
+  const isSimplified = ref(
+    localStorage.getItem(EPUB_SIMPLIFIED)
+      ? Boolean(Number(localStorage.getItem(EPUB_SIMPLIFIED)))
+      : true
+  );
+
+  const init = (url) => {
+    const _book = ePub(url);
+    const _rendition = _book.renderTo("reader", {
+      width: "100%",
+      height: "100%",
+    });
+    console.log(_rendition);
+
+    _rendition.hooks.render.register((v) => {
+      bodyNode = v.document.body;
+      convertText();
+    });
+
+    _book.loaded.metadata.then((res) => {
+      console.log("metadata: ", res);
+    });
+
+    _book.loaded.navigation.then((res) => {
+      console.log("navigation: ", res);
+    });
+
+    THEMES.forEach((theme) => {
+      _rendition.themes.register(theme.key, {
+        body: {
+          color: theme.textColor,
+          background: theme.color,
+        },
+      });
+    });
+
+    _rendition.themes.font(
+      localStorage.getItem(EPUB_FONT_KEY) ?? FONT_FAMILYS[0].value
+    );
+
+    _rendition.themes.fontSize(
+      (localStorage.getItem(EPUB_FONTSIZE_KEY) ?? EPUB_FONTSIZE_KEY[0].value) +
+        "px"
+    );
+
+    _rendition.display();
+    book.value = _book;
+    rendition.value = _rendition;
+  };
+
+  const convertText = (node = bodyNode) => {
+    if (node.nodeType === 3) {
+      const text = node.nodeValue;
+      const convertedText = isSimplified.value ? sify(text) : tify(text);
+      node.nodeValue = convertedText;
+    } else if (node.nodeType === 1 && node.childNodes) {
+      node.childNodes.forEach((child) => convertText(child));
+    }
+  };
+
+  /**
+   * 简/繁体转换
+   */
+  const toggleText = () => {
+    isSimplified.value = !isSimplified.value;
+    convertText();
+    localStorage.setItem(EPUB_SIMPLIFIED, Number(isSimplified.value));
+  };
+
+  /**
+   * 修改主题色
+   * @param theme {String}
+   */
+  const toggleTheme = (theme) => {
+    rendition.value.themes.select(theme);
+    localStorage.setItem(EPUB_THEME_KEY, theme);
+  };
+
+  /**
+   * 修改字体
+   * @param font {String}
+   */
+  const toggleFont = (font) => {
+    rendition.value.themes.font(font);
+    localStorage.setItem(EPUB_FONT_KEY, font);
+  };
+
+  /**
+   * 修改字体大小
+   * @param size {String | Number}
+   */
+  const toggleFontSize = (size) => {
+    rendition.value.themes.fontSize(size + "px");
+    localStorage.setItem(EPUB_FONTSIZE_KEY, size);
+  };
+
+  return {
+    book,
+    rendition,
+    isSimplified,
+    init,
+    convertText,
+    toggleText,
+    toggleTheme,
+    toggleFont,
+    toggleFontSize,
+  };
+});

+ 1 - 0
packages/base/src/stores/index.js

@@ -0,0 +1 @@
+export * from "./epub";

+ 26 - 0
packages/base/src/theme.scss

@@ -0,0 +1,26 @@
+html {
+  --topnav-bg-color: #f8f6f2;
+  --page-bg-color: #f3f3f3;
+  --pane-bg-color: white;
+  --text-color-secondary: rgba(70, 70, 70, 0.8);
+  --color-primary-opacity-5: rgba(209, 187, 158, 0.5);
+  --icon-color: #d1bb9e;
+}
+
+/**
+==========
+dark
+==========
+ */
+html.dark {
+  --topnav-bg-color: #414141;
+  --page-bg-color: #373737;
+  --pane-bg-color: #2c2c2c;
+  --text-color-secondary: rgba(236, 236, 236, 0.8);
+  --color-primary-opacity-5: rgba(209, 187, 158, 0.5);
+  --icon-color: #2c2c2c;
+
+  body {
+    color: white;
+  }
+}

+ 6 - 4
packages/pc/index.html

@@ -1,13 +1,15 @@
 <!DOCTYPE html>
 <html lang="en">
   <head>
-    <meta charset="UTF-8">
-    <link rel="icon" href="/favicon.ico">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Vite App</title>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>刘少奇同志纪念馆</title>
   </head>
   <body>
     <div id="app"></div>
     <script type="module" src="/src/main.js"></script>
+    <script src="./jszip.min.js"></script>
+    <script src="./epub.min.js"></script>
   </body>
 </html>

+ 7 - 2
packages/pc/package.json

@@ -1,5 +1,5 @@
 {
-  "name": "pc",
+  "name": "@lsq/pc",
   "version": "0.0.0",
   "private": true,
   "type": "module",
@@ -9,6 +9,9 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@element-plus/icons-vue": "^2.3.1",
+    "@lsq/base": "workspace:^",
+    "@vueuse/core": "^11.1.0",
     "element-plus": "^2.8.3",
     "pinia": "^2.1.7",
     "vue": "^3.4.29",
@@ -16,9 +19,11 @@
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "^5.0.5",
+    "@vitejs/plugin-vue-jsx": "^4.0.1",
     "sass": "1.53.0",
     "unplugin-auto-import": "^0.18.3",
     "unplugin-vue-components": "^0.27.4",
-    "vite": "^5.3.1"
+    "vite": "^5.3.1",
+    "vite-plugin-svg-icons": "^2.0.1"
   }
 }

文件差异内容过多而无法显示
+ 1 - 0
packages/pc/public/epub.min.js


二进制
packages/pc/public/favicon.ico


文件差异内容过多而无法显示
+ 15 - 0
packages/pc/public/jszip.min.js


二进制
packages/pc/public/test.epub


二进制
packages/pc/public/test2.epub


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

@@ -10,6 +10,10 @@ import TopNav from "@/components/TopNav/index.vue";
 </template>
 
 <style lang="scss">
+html.dark {
+  --el-color-primary: #b49d7e !important;
+}
+
 #app {
   --topnav-height: 80px;
 
@@ -21,4 +25,25 @@ import TopNav from "@/components/TopNav/index.vue";
   width: 1100px;
   overflow: hidden;
 }
+
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.2s ease;
+}
+.fade-enter-from,
+.fade-leave-to {
+  opacity: 0;
+}
+
+.slide-left-enter-active,
+.slide-left-leave-active {
+  transition: all 0.2s ease-in-out;
+  opacity: 1;
+  transform: translateX(0);
+}
+.slide-left-enter-from,
+.slide-left-leave-to {
+  opacity: 0;
+  transform: translateX(50%);
+}
 </style>

二进制
packages/pc/src/assets/images/icon_search2-min.png


二进制
packages/pc/src/assets/images/logo-min.png


二进制
packages/pc/src/assets/images/logo_03-min.png


文件差异内容过多而无法显示
+ 16 - 0
packages/pc/src/assets/svgs/icon_fullscreen_yellow.svg


文件差异内容过多而无法显示
+ 6 - 0
packages/pc/src/assets/svgs/icon_like_yellow.svg


文件差异内容过多而无法显示
+ 8 - 0
packages/pc/src/assets/svgs/icon_mark_yellow.svg


文件差异内容过多而无法显示
+ 8 - 0
packages/pc/src/assets/svgs/icon_menu_yellow.svg


文件差异内容过多而无法显示
+ 8 - 0
packages/pc/src/assets/svgs/icon_search_yellow.svg


文件差异内容过多而无法显示
+ 8 - 0
packages/pc/src/assets/svgs/icon_setting_yellow.svg


+ 15 - 0
packages/pc/src/components/BookCard/index.vue

@@ -59,6 +59,21 @@ const isRow = computed(() => props.type === "row");
     gap: 18px;
     line-height: 16px;
 
+    &.large {
+      width: 100%;
+      font-size: 18px;
+      line-height: 21px;
+
+      .book-card-img {
+        width: 114px;
+        height: 146px;
+      }
+      .book-card__name {
+        margin-bottom: 20px;
+        font-size: 24px;
+        line-height: 28px;
+      }
+    }
     .book-card-img {
       width: 90px;
       height: 115px;

+ 31 - 0
packages/pc/src/components/PagePane.vue

@@ -0,0 +1,31 @@
+<template>
+  <div class="page-pane">
+    <div class="page-pane-container">
+      <slot />
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.page-pane {
+  padding: 27px 0;
+  min-width: 1434px;
+  min-height: calc(100vh - var(--topnav-height));
+  background: var(--page-bg-color);
+  box-sizing: border-box;
+
+  &-container {
+    position: relative;
+    display: flex;
+    gap: 70px;
+    margin: 0 auto;
+    padding: 26px 40px;
+    width: 1434px;
+    height: calc(100vh - var(--topnav-height) - 54px);
+    border-radius: 10px;
+    background: var(--pane-bg-color);
+    box-shadow: 0px 0px 21px 0px rgba(0, 0, 0, 0.1);
+    box-sizing: border-box;
+  }
+}
+</style>

+ 13 - 0
packages/pc/src/components/SelectItem/index.scss

@@ -0,0 +1,13 @@
+.select-item {
+  &-select {
+    border-bottom: 1px solid #d9d9d9;
+
+    :deep(.el-select__wrapper) {
+      --el-select-input-color: var(--el-color-primary);
+      padding: 4px 6px 4px 0;
+      height: 40px;
+      font-size: 18px;
+      box-shadow: none;
+    }
+  }
+}

+ 54 - 0
packages/pc/src/components/SelectItem/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class="select-item">
+    <p class="select-item__title">{{ title }}</p>
+
+    <el-select
+      v-model="value"
+      v-bind="$attrs"
+      class="select-item-select"
+      placeholder="请选择"
+      :suffix-icon="CaretBottom"
+    >
+      <el-option
+        v-for="item in options"
+        :key="item.value"
+        :label="item.label"
+        :value="item.value"
+      />
+    </el-select>
+  </div>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { CaretBottom } from "@element-plus/icons-vue";
+
+const props = defineProps({
+  title: {
+    type: String,
+    required: true,
+  },
+  modelValue: {
+    type: [String, Number],
+    required: true,
+  },
+  options: {
+    type: Object,
+    required: true,
+  },
+});
+const emits = defineEmits(["update:modelValue"]);
+
+const value = computed({
+  get() {
+    return props.modelValue;
+  },
+  set(v) {
+    emits("update:modelValue", v);
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 39 - 0
packages/pc/src/components/SvgIcon.jsx

@@ -0,0 +1,39 @@
+import { defineComponent } from "vue";
+
+// TOFIX: .vue后缀会被unplugin-vue-components自动注册导致冲突
+export default defineComponent({
+  props: {
+    // xlink:href的属性值前缀
+    prefix: {
+      type: String,
+      default: "#icon-",
+    },
+    // 需要使用的svg的图标的名字
+    name: {
+      type: String,
+      required: true,
+    },
+    // 需要使用的svg的图标的颜色
+    color: {
+      type: String,
+      default: "var(--el-text-color-regular)",
+    },
+    // 需要使用的svg的图标的宽度
+    width: {
+      type: String,
+      default: "16px",
+    },
+    // 需要使用的svg的图标的高度
+    height: {
+      type: String,
+      default: "16px",
+    },
+  },
+  render() {
+    return (
+      <svg style={{ width: this.width, height: this.height }}>
+        <use xlink:href={this.prefix + this.name} color={this.color}></use>
+      </svg>
+    );
+  },
+});

二进制
packages/pc/src/components/TopNav/images/icon_book-min.png


二进制
packages/pc/src/components/TopNav/images/icon_book_dark-min.png


二进制
packages/pc/src/components/TopNav/images/icon_font-min.png


二进制
packages/pc/src/components/TopNav/images/icon_font_active-min.png


二进制
packages/pc/src/components/TopNav/images/icon_img-min.png


二进制
packages/pc/src/components/TopNav/images/icon_img_active-min.png


二进制
packages/pc/src/components/TopNav/images/icon_text-min.png


二进制
packages/pc/src/components/TopNav/images/icon_text_active-min.png


二进制
packages/pc/src/components/TopNav/images/icon_video-min.png


二进制
packages/pc/src/components/TopNav/images/icon_video_active-min.png


+ 58 - 1
packages/pc/src/components/TopNav/index.scss

@@ -7,7 +7,7 @@
     left: 0;
     right: 0;
     height: inherit;
-    background: #f8f6f2;
+    background: var(--topnav-bg-color);
     border-bottom: 1px solid rgba(211, 191, 162, 0.5);
     z-index: 1000;
 
@@ -80,4 +80,61 @@
     font-family: "Source Han Serif CN-Bold";
     cursor: pointer;
   }
+
+  &__logo {
+    display: block;
+    cursor: pointer;
+  }
+
+  &-detail {
+    display: flex;
+    align-items: center;
+    gap: 30px;
+
+    h1 {
+      font-size: 24px;
+      font-family: "Source Han Serif CN-Bold";
+    }
+    p {
+      color: var(--text-color-secondary);
+    }
+  }
+
+  &-detail-nav {
+    display: flex;
+    align-items: center;
+    gap: 30px;
+
+    &__item {
+      position: relative;
+      cursor: pointer;
+
+      &.active {
+        p {
+          color: var(--el-color-primary);
+        }
+      }
+      &:hover {
+        p {
+          color: var(--el-color-primary);
+        }
+      }
+      img {
+        display: block;
+      }
+      p {
+        position: absolute;
+        left: 50%;
+        bottom: -20px;
+        line-height: 20px;
+        white-space: nowrap;
+        color: var(--color-primary-opacity-5);
+        transform: translateX(-50%);
+      }
+    }
+    img {
+      display: block;
+      cursor: pointer;
+    }
+  }
 }

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

@@ -12,17 +12,62 @@
             class="top-nav__logo"
             draggable="false"
             src="@/assets/images/logo_02-min.png"
+            @click="$router.push({ name: 'home' })"
           />
+
+          <div v-if="isDetail" class="top-nav-detail">
+            <img :src="isDark ? LogoWhiteIcon : LogoIcon" draggable="false" />
+            <div class="top-nav-detail__inner">
+              <h1>刘少奇传</h1>
+              <p>金冲及 中共中央文献研究室 编</p>
+            </div>
+          </div>
         </div>
 
         <div class="top-nav-rg">
           <ul class="top-nav-rg-first">
             <li>手机版</li>
-            <li>书库</li>
+            <li v-if="!isDetail" @click="$router.push({ name: 'stack' })">
+              书库
+            </li>
+            <li
+              v-else
+              v-loading="joinLoading"
+              element-loading-custom-class="top-nav__loading"
+            >
+              加入书架
+            </li>
           </ul>
 
           <div class="top-nav-rg__divider" />
 
+          <template v-if="isDetail">
+            <div class="top-nav-detail-nav">
+              <div
+                v-for="(item, index) in DETAIL_NAV"
+                :key="item.text"
+                class="top-nav-detail-nav__item"
+                :class="{ active: index === curNav }"
+              >
+                <img :src="index === curNav ? item.activeIcon : item.icon" />
+                <p>{{ item.text }}</p>
+              </div>
+
+              <!-- 简/繁体 -->
+              <img
+                :src="isSimplified ? jtIcon : ftIcon"
+                @click="epubStore.toggleText"
+              />
+
+              <!-- 书库 -->
+              <router-link :to="{ name: 'stack' }">
+                <img :src="isDark ? skDarkIcon : skIcon" />
+              </router-link>
+            </div>
+
+            <div class="top-nav-rg__divider" />
+          </template>
+
           <el-popover
             v-if="isLogin"
             :width="124"
@@ -54,15 +99,59 @@
 <script setup>
 import { watch, ref } from "vue";
 import { useRoute } from "vue-router";
-import { useBaseStore } from "@/stores";
+import { useDark } from "@vueuse/core";
+import { storeToRefs } from "pinia";
+import { useBaseStore, useDetailStore, useEpubStore } from "@/stores";
+
+import LogoIcon from "@/assets/images/logo_03-min.png";
+import LogoWhiteIcon from "@/assets/images/logo-min.png";
+import TextIcon from "./images/icon_text-min.png";
+import TextActiveIcon from "./images/icon_text_active-min.png";
+import VideoIcon from "./images/icon_video-min.png";
+import VideoActiveIcon from "./images/icon_video_active-min.png";
+import ImgIcon from "./images/icon_img-min.png";
+import ImgActiveIcon from "./images/icon_img_active-min.png";
+import jtIcon from "./images/icon_font-min.png";
+import ftIcon from "./images/icon_font_active-min.png";
+import skIcon from "./images/icon_book-min.png";
+import skDarkIcon from "./images/icon_book_dark-min.png";
+
+const DETAIL_NAV = [
+  {
+    icon: TextIcon,
+    activeIcon: TextActiveIcon,
+    text: "文本",
+  },
+  {
+    icon: VideoIcon,
+    activeIcon: VideoActiveIcon,
+    text: "视频",
+  },
+  {
+    icon: ImgIcon,
+    activeIcon: ImgActiveIcon,
+    text: "影印",
+  },
+];
 
 const route = useRoute();
-const { isLogin } = useBaseStore();
+const isDark = useDark();
+
+const baseStore = useBaseStore();
+const { isLogin } = storeToRefs(baseStore);
+const detailStore = useDetailStore();
+const { curNav } = storeToRefs(detailStore);
+const epubStore = useEpubStore();
+const { isSimplified } = storeToRefs(epubStore);
+
+const isDetail = ref(false);
 const showLogo = ref(false);
 const hideBgColor = ref(false);
 const bgColor = ref("");
+const joinLoading = ref(false);
 
 watch(route, (v) => {
+  isDetail.value = v.name === "detail";
   showLogo.value = v.meta.showLogo ?? false;
   hideBgColor.value =
     (v.meta.hideTopNavBgColor || Boolean(v.meta.topNavBgColor)) ?? true;
@@ -79,4 +168,13 @@ watch(route, (v) => {
   --el-popover-padding: 10px;
   min-width: 124px;
 }
+
+.top-nav__loading {
+  --el-loading-spinner-size: 20px;
+
+  top: -5px;
+  left: -10px;
+  right: -10px;
+  bottom: -5px;
+}
 </style>

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

@@ -1,4 +1,7 @@
 import "./assets/main.css";
+import "@lsq/base/src/theme.scss";
+import "element-plus/theme-chalk/dark/css-vars.css";
+import "virtual:svg-icons-register";
 
 import { createApp } from "vue";
 import { createPinia } from "pinia";
@@ -6,9 +9,14 @@ import { createPinia } from "pinia";
 import App from "./App.vue";
 import router from "./router";
 
+import SvgIcon from "./components/SvgIcon";
+import PagePane from "./components/PagePane.vue";
+
 const app = createApp(App);
 
 app.use(createPinia());
 app.use(router);
+app.component("svg-icon", SvgIcon);
+app.component("page-pane", PagePane);
 
 app.mount("#app");

+ 14 - 0
packages/pc/src/router/index.js

@@ -28,6 +28,20 @@ const router = createRouter({
         showLogo: true,
       },
     },
+    {
+      path: "/stack",
+      name: "stack",
+      component: () => import("@/views/Stack/index.vue"),
+      meta: {
+        showLogo: true,
+      },
+    },
+    {
+      path: "/detail/:id",
+      name: "detail",
+      component: () => import("@/views/Detail/index.vue"),
+      meta: {},
+    },
   ],
 });
 

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

@@ -0,0 +1,8 @@
+import { ref } from "vue";
+import { defineStore } from "pinia";
+
+export const useDetailStore = defineStore("detail", () => {
+  const curNav = ref(0);
+
+  return { curNav };
+});

+ 2 - 0
packages/pc/src/stores/index.js

@@ -1 +1,3 @@
+export * from "@lsq/base/src/stores";
 export * from "./base";
+export * from "./detail";

+ 1 - 1
packages/pc/src/views/Bookshelf/index.scss

@@ -71,7 +71,7 @@
 
   &-inner {
     padding: 43px 67px;
-    border-radius: 8px;
+    border-radius: 10px;
     background: #ffffff;
     box-shadow: 0px -1px 67px 0px rgba(171, 171, 171, 0.15);
 

+ 74 - 0
packages/pc/src/views/Detail/components/Directory.vue

@@ -0,0 +1,74 @@
+<template>
+  <Drawer v-model:visible="show" @close="emits('close')">
+    <div class="directory">
+      <div class="directory-info">
+        <el-image src="" />
+
+        <div class="directory-info-inner">
+          <h1>刘少奇传</h1>
+          <p>金冲及</p>
+          <p>中共中央文献研究室 编</p>
+        </div>
+      </div>
+
+      <ul class="directory-list">
+        <li v-for="item in 20" :key="item">一、农家子弟</li>
+      </ul>
+    </div>
+  </Drawer>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import Drawer from "./Drawer.vue";
+
+const props = defineProps(["visible"]);
+const emits = defineEmits(["update:visible", "close"]);
+
+const show = computed({
+  get() {
+    return props.visible;
+  },
+  set(v) {
+    emits("update:visible", v);
+  },
+});
+</script>
+
+<style lang="scss" scoped>
+.directory {
+  padding: 34px 36px;
+
+  &-info {
+    display: flex;
+    align-items: center;
+    gap: 22px;
+
+    .el-image {
+      width: 87px;
+      height: 117px;
+    }
+    h1 {
+      font-size: 24px;
+      font-family: "Source Han Serif CN-Bold";
+    }
+    p {
+      color: var(--text-color-secondary);
+    }
+  }
+
+  &-list {
+    margin-top: 28px;
+    border-top: 1px solid #d9d9d9;
+
+    li {
+      padding-left: 3px;
+      font-size: 18px;
+      height: 48px;
+      line-height: 48px;
+      cursor: pointer;
+      border-bottom: 1px solid #d9d9d9;
+    }
+  }
+}
+</style>

+ 96 - 0
packages/pc/src/views/Detail/components/Drawer.vue

@@ -0,0 +1,96 @@
+<template>
+  <transition name="slide-left">
+    <div v-if="show" class="detail-drawer">
+      <el-icon
+        class="detail-drawer__close"
+        color="#585757"
+        :size="30"
+        @click="handleClose"
+        ><Close
+      /></el-icon>
+
+      <el-scrollbar style="width: 100%">
+        <p v-if="title" class="detail-drawer__title">{{ title }}</p>
+
+        <slot />
+      </el-scrollbar>
+    </div>
+  </transition>
+
+  <transition name="fade">
+    <div v-if="show" class="detail-drawer-mask" />
+  </transition>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { Close } from "@element-plus/icons-vue";
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    required: true,
+  },
+  title: {
+    type: String,
+    default: "",
+    required: false,
+  },
+});
+const emits = defineEmits(["update:visible", "close"]);
+
+const show = computed({
+  get() {
+    return props.visible;
+  },
+  set(v) {
+    emits("update:visible", v);
+  },
+});
+
+const handleClose = () => {
+  show.value = false;
+  emits("close");
+};
+</script>
+
+<style lang="scss" scoped>
+.detail-drawer {
+  position: fixed;
+  top: calc(var(--topnav-height) + 27px);
+  right: calc(8vw + 44px + 60px);
+  width: 355px;
+  height: calc(100vh - var(--topnav-height) - 54px);
+  border-radius: 10px;
+  background: var(--pane-bg-color);
+  z-index: 999;
+
+  &__close {
+    position: absolute;
+    top: 34px;
+    right: 27px;
+    cursor: pointer;
+    z-index: 1;
+  }
+  &__title {
+    padding: 31px 0 31px 33px;
+    font-size: 24px;
+    font-family: "Source Han Serif CN-Bold";
+  }
+  &-mask {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: rgba(0, 0, 0, 0.5);
+    z-index: 998;
+  }
+}
+
+@media screen and (max-width: 1555px) {
+  .detail-drawer {
+    right: calc(10px + 44px + 60px);
+  }
+}
+</style>

+ 30 - 0
packages/pc/src/views/Detail/components/Reader/index.scss

@@ -0,0 +1,30 @@
+.detail-text {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  height: 100%;
+
+  #reader {
+    flex: 1;
+    width: 100%;
+  }
+  &-pagination {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-top: 28px;
+
+    &__btn {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 110px;
+      height: 40px;
+      color: var(--el-color-primary);
+      font-size: 18px;
+      user-select: none;
+      background: url("../../images/btn_03.png") no-repeat center / contain;
+      cursor: pointer;
+    }
+  }
+}

+ 40 - 0
packages/pc/src/views/Detail/components/Reader/index.vue

@@ -0,0 +1,40 @@
+<template>
+  <div class="detail-text">
+    <div id="reader" />
+
+    <div class="detail-text-pagination">
+      <div class="detail-text-pagination__btn" @click="prePage">
+        <el-icon><ArrowLeft /></el-icon>
+        上一页
+      </div>
+
+      <div class="detail-text-pagination__btn" @click="nextPage">
+        下一页
+        <el-icon><ArrowRight /></el-icon>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted } from "vue";
+import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
+import { useEpubStore } from "@/stores";
+
+const epubStore = useEpubStore();
+
+onMounted(() => {
+  epubStore.init("./test2.epub");
+});
+
+const prePage = () => {
+  epubStore.rendition?.prev();
+};
+const nextPage = () => {
+  epubStore.rendition?.next();
+};
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 96 - 0
packages/pc/src/views/Detail/components/Setting.vue

@@ -0,0 +1,96 @@
+<template>
+  <Drawer v-model:visible="show" title="设置" @close="emits('close')">
+    <div class="setting">
+      <select-item
+        v-model="currentFamily"
+        title="字体"
+        :options="FONT_FAMILYS"
+        @change="epubStore.toggleFont(currentFamily)"
+      />
+
+      <select-item
+        v-model="currentSize"
+        title="文字大小"
+        :options="FONT_SIZES"
+        style="margin: 15px 0"
+        @change="epubStore.toggleFontSize(currentSize)"
+      />
+
+      <p>主题</p>
+      <div class="theme-list">
+        <div
+          v-for="item in THEMES"
+          :key="item.key"
+          class="theme-list__item"
+          :style="{ background: item.color, borderColor: item.borderColor }"
+          @click="handleTheme(item)"
+        />
+      </div>
+    </div>
+  </Drawer>
+</template>
+
+<script setup>
+import { computed, ref } from "vue";
+import { useDark, useToggle } from "@vueuse/core";
+import {
+  THEMES,
+  FONT_FAMILYS,
+  FONT_SIZES,
+  EPUB_FONT_KEY,
+  EPUB_FONTSIZE_KEY,
+} from "@lsq/base";
+import { useEpubStore } from "@/stores";
+import SelectItem from "@/components/SelectItem/index.vue";
+import Drawer from "./Drawer.vue";
+
+const props = defineProps(["visible"]);
+const emits = defineEmits(["update:visible", "close"]);
+
+const isDark = useDark();
+const toggleDark = useToggle(isDark);
+const epubStore = useEpubStore();
+
+const show = computed({
+  get() {
+    return props.visible;
+  },
+  set(v) {
+    emits("update:visible", v);
+  },
+});
+
+const currentFamily = ref(
+  localStorage.getItem(EPUB_FONT_KEY) ?? FONT_FAMILYS[0].value
+);
+const currentSize = ref(
+  localStorage.getItem(EPUB_FONTSIZE_KEY) ?? FONT_SIZES[0].value
+);
+
+const handleTheme = (item) => {
+  epubStore.toggleTheme(item.key);
+  toggleDark(item.key === "dark");
+};
+</script>
+
+<style lang="scss" scoped>
+.setting {
+  padding: 6px 33px;
+
+  .theme-list {
+    margin-top: 7px;
+    display: flex;
+    gap: 15px;
+
+    &__item {
+      width: 40px;
+      height: 40px;
+      border-radius: 50%;
+      overflow: hidden;
+      border: 1px solid #9a9a9a;
+      box-sizing: border-box;
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 41 - 0
packages/pc/src/views/Detail/components/Toolbar/index.scss

@@ -0,0 +1,41 @@
+.detail-toolbar {
+  // position: fixed;
+  // top: 50%;
+  // right: 8vw;
+  // display: flex;
+  // flex-direction: column;
+  // gap: 22px;
+  // transform: translateY(-50%);
+
+  &-item {
+    position: fixed;
+    top: 50%;
+    right: 8vw;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    cursor: pointer;
+    z-index: 997;
+    transform: translateY(-50%);
+
+    &.active {
+      color: var(--el-color-white);
+      pointer-events: none;
+      z-index: 999;
+    }
+    &:hover span {
+      color: var(--el-color-primary);
+    }
+    span {
+      line-height: 20px;
+    }
+  }
+}
+
+@media screen and (max-width: 1555px) {
+  .detail-toolbar {
+    &-item {
+      right: 10px;
+    }
+  }
+}

+ 104 - 0
packages/pc/src/views/Detail/components/Toolbar/index.vue

@@ -0,0 +1,104 @@
+<template>
+  <div
+    v-for="(item, idx) in list"
+    :key="item.key"
+    class="detail-toolbar-item"
+    :class="{ active: active === idx }"
+    :style="{
+      top: item.top + 'px',
+    }"
+    @click="handleToolbar(item.key, idx)"
+  >
+    <svg-icon
+      :name="item.icon"
+      width="40px"
+      height="40px"
+      color="var(--icon-color)"
+    />
+    <span>{{ item.label }}</span>
+  </div>
+
+  <directory v-model:visible="directoryVisible" @close="handleClose" />
+  <setting v-model:visible="settingVisible" @close="handleClose" />
+</template>
+
+<script setup>
+import { ref, computed } from "vue";
+import Directory from "../Directory.vue";
+import Setting from "../Setting.vue";
+
+const active = ref(-1);
+const directoryVisible = ref(false);
+const settingVisible = ref(false);
+const list = computed(() => {
+  const stack = [
+    {
+      top: 0,
+      label: "目录",
+      icon: "icon_menu_yellow",
+      key: "directory",
+    },
+    {
+      top: 0,
+      label: "搜索",
+      icon: "icon_search_yellow",
+      key: "search",
+    },
+    {
+      top: 0,
+      label: "笔记",
+      icon: "icon_mark_yellow",
+      key: "mark",
+    },
+    {
+      top: 0,
+      label: "书签",
+      icon: "icon_like_yellow",
+      key: "like",
+    },
+    {
+      top: 0,
+      label: "设置",
+      icon: "icon_setting_yellow",
+      key: "setting",
+    },
+    {
+      top: 0,
+      label: "全屏",
+      icon: "icon_fullscreen_yellow",
+      key: "fullscreen",
+    },
+  ];
+  const itemHeight = 60;
+  const listHeight = itemHeight * stack.length;
+  let top = (window.innerHeight - listHeight) / 2;
+
+  stack.forEach((item) => {
+    item.top = top;
+    top += itemHeight + 22;
+  });
+
+  return stack;
+});
+
+const handleToolbar = (key, idx) => {
+  switch (key) {
+    case "directory":
+      directoryVisible.value = true;
+      break;
+    case "setting":
+      settingVisible.value = true;
+      break;
+  }
+
+  active.value = idx;
+};
+
+const handleClose = () => {
+  active.value = -1;
+};
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

二进制
packages/pc/src/views/Detail/images/btn_03.png


+ 0 - 0
packages/pc/src/views/Detail/index.scss


+ 18 - 0
packages/pc/src/views/Detail/index.vue

@@ -0,0 +1,18 @@
+<template>
+  <page-pane>
+    <component :is="comp" />
+  </page-pane>
+
+  <toolbar />
+</template>
+
+<script setup>
+import Toolbar from "./components/Toolbar/index.vue";
+import Reader from "./components/Reader/index.vue";
+
+const comp = Reader;
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

+ 44 - 0
packages/pc/src/views/Stack/components/SearchInput/index.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="search-input">
+    <input
+      placeholder="请输入关键词..."
+      :value="modelValue"
+      @input="emit('update:modelValue', $event.target.value)"
+    />
+
+    <img src="@/assets/images/icon_search2-min.png" draggable="false" />
+  </div>
+</template>
+
+<script setup>
+const props = defineProps(["modelValue"]);
+const emit = defineEmits(["update:modelValue"]);
+</script>
+
+<style lang="scss" scoped>
+.search-input {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 8px 13px;
+  width: 300px;
+  height: 40px;
+  overflow: hidden;
+  border-radius: 50px;
+  border: 1px solid #d1bb9e;
+  box-sizing: border-box;
+
+  input {
+    flex: 1;
+    border: 0;
+    height: 100%;
+    outline: none;
+    font-size: 16px;
+    color: #464646;
+    font-family: "Source Han Sans CN-Regular";
+  }
+  img {
+    cursor: pointer;
+  }
+}
+</style>

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

@@ -0,0 +1,77 @@
+.stack-sidebar {
+  $itemHeight: 40px;
+
+  height: 100%;
+
+  :deep(.el-tabs) {
+    height: 100%;
+  }
+  :deep(.el-tab-pane) {
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+  }
+  :deep(.el-tabs__item) {
+    --el-font-size-base: 18px;
+    --el-text-color-primary: rgba(70, 70, 70, 0.5);
+
+    align-items: flex-end;
+    padding-bottom: 8px;
+    font-family: "Source Han Serif CN-Bold";
+    line-height: 1;
+    z-index: 2;
+
+    &.is-active {
+      color: #464646;
+      font-size: 24px;
+    }
+  }
+  :deep(.el-tabs__nav-wrap)::after {
+    display: none;
+  }
+  :deep(.el-tabs__active-bar) {
+    bottom: 5px;
+    height: 7px;
+    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;
+  }
+
+  &-list {
+    padding-right: 20px;
+  }
+  &-item {
+    padding-left: 24px;
+    height: $itemHeight;
+    line-height: $itemHeight;
+    font-size: 18px;
+    font-family: "Source Han Serif CN-Bold";
+    cursor: pointer;
+
+    &.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%
+      );
+    }
+  }
+}

+ 143 - 0
packages/pc/src/views/Stack/components/Sidebar/index.vue

@@ -0,0 +1,143 @@
+<template>
+  <div class="stack-sidebar">
+    <el-tabs v-model="activeTab">
+      <el-tab-pane label="按书籍分类" name="book">
+        <search-input v-model="bookSearchKey" />
+
+        <el-scrollbar 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"
+            />
+          </div>
+        </el-scrollbar>
+      </el-tab-pane>
+
+      <el-tab-pane label="按藏品分类" name="collection">
+        <search-input />
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import SearchInput from "../SearchInput/index.vue";
+
+/**
+ * 当前选项
+ * 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",
+          },
+        ],
+      },
+    ],
+  },
+  {
+    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",
+          },
+        ],
+      },
+    ],
+  },
+]);
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

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

@@ -0,0 +1,30 @@
+.stack {
+  &-left {
+    flex: 0 0 360px;
+    padding-right: 20px;
+    border-right: 1px solid rgba(70, 70, 70, 0.2);
+  }
+  &-main {
+    flex: 1;
+    padding-top: 15px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+  }
+
+  &-breadcrumb {
+    --el-text-color-primary: rgba(70, 70, 70, 1);
+    --el-text-color-regular: rgba(70, 70, 70, 0.5);
+  }
+
+  &-list {
+    display: flex;
+    flex-wrap: wrap;
+    margin: -12.5px 0;
+
+    li {
+      margin: 12.5px;
+      width: calc(50% - 25px);
+    }
+  }
+}

+ 36 - 1
packages/pc/src/views/Stack/index.vue

@@ -1,3 +1,38 @@
 <template>
-  <div class="stack"></div>
+  <page-pane class="stack">
+    <div class="stack-left">
+      <sidebar />
+    </div>
+
+    <div class="stack-main">
+      <el-breadcrumb
+        class="stack-breadcrumb"
+        :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>
+
+      <el-scrollbar style="flex: 1; height: 0; margin: 18px -12.5px">
+        <ul class="stack-list">
+          <li v-for="item in 17" :key="item">
+            <book-card type="row" size="large" />
+          </li>
+        </ul>
+      </el-scrollbar>
+
+      <el-pagination background layout="prev, pager, next" :total="1000" />
+    </div>
+  </page-pane>
 </template>
+
+<script setup>
+import { ArrowRight } from "@element-plus/icons-vue";
+import Sidebar from "./components/Sidebar/index.vue";
+import BookCard from "@/components/BookCard/index.vue";
+</script>
+
+<style lang="scss" scoped>
+@import "./index.scss";
+</style>

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

@@ -1,7 +1,10 @@
+import path from "path";
 import { fileURLToPath, URL } from "node:url";
+import { createSvgIconsPlugin } from "vite-plugin-svg-icons";
 
 import { defineConfig } from "vite";
 import vue from "@vitejs/plugin-vue";
+import vueJsx from "@vitejs/plugin-vue-jsx";
 
 import AutoImport from "unplugin-auto-import/vite";
 import Components from "unplugin-vue-components/vite";
@@ -10,6 +13,7 @@ import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
 // https://vitejs.dev/config/
 export default defineConfig({
   base: "./",
+  publicDir: "public",
   server: {
     proxy: {
       "/api": {
@@ -21,18 +25,27 @@ export default defineConfig({
   css: {
     preprocessorOptions: {
       scss: {
-        additionalData: `@use "@/assets/element.scss" as *;`,
+        additionalData: `
+          @use "@/assets/element.scss" as *;
+        `,
       },
     },
   },
   plugins: [
     vue(),
+    vueJsx(),
     AutoImport({
       resolvers: [ElementPlusResolver()],
     }),
     Components({
       resolvers: [ElementPlusResolver({ importStyle: "sass" })],
     }),
+    createSvgIconsPlugin({
+      // 指定需要缓存的图标文件夹
+      iconDirs: [path.resolve(process.cwd(), "src/assets/svgs")],
+      // 指定symbolId格式
+      symbolId: "icon-[name]",
+    }),
   ],
   resolve: {
     alias: {

文件差异内容过多而无法显示
+ 2820 - 721
pnpm-lock.yaml


+ 0 - 1
pnpm-workspace.yaml

@@ -1,3 +1,2 @@
 packages:
   - "packages/*"
-  - "packages/backend-cli/template"