Quellcode durchsuchen

更改创建框架

dusenyao vor 1 Jahr
Ursprung
Commit
0478a367b5
34 geänderte Dateien mit 1023 neuen und 175 gelöschten Zeilen
  1. 26 4
      package-lock.json
  2. 1 1
      package.json
  3. 8 0
      src/api/book.js
  4. BIN
      src/assets/mask_group.png
  5. 3 2
      src/common/SvgIcon/index.vue
  6. 3 0
      src/icons/svg/artboard.svg
  7. 4 0
      src/icons/svg/browse.svg
  8. 3 1
      src/icons/svg/delete.svg
  9. 4 0
      src/icons/svg/edit.svg
  10. 3 0
      src/icons/svg/palette.svg
  11. 10 0
      src/icons/svg/save.svg
  12. 0 0
      src/icons/svg/setup.svg
  13. 23 11
      src/router/modules/book.js
  14. 2 2
      src/router/modules/index.js
  15. 1 2
      src/styles/element-variables.scss
  16. 2 2
      src/utils/http.js
  17. 123 4
      src/views/book/chapter.vue
  18. 7 5
      src/views/book/courseware/create/components/base/audio/Audio.vue
  19. 4 24
      src/views/book/courseware/create/components/base/audio/AudioSetting.vue
  20. 3 4
      src/views/book/courseware/create/components/base/divider/Divider.vue
  21. 4 21
      src/views/book/courseware/create/components/base/divider/DividerSetting.vue
  22. 3 3
      src/views/book/courseware/create/components/base/spacing/Spacing.vue
  23. 4 21
      src/views/book/courseware/create/components/base/spacing/SpacingSetting.vue
  24. 13 2
      src/views/book/courseware/create/components/common/ModuleBase.vue
  25. 15 9
      src/views/book/courseware/create/components/common/ModuleMixin.js
  26. 20 0
      src/views/book/courseware/create/components/common/SettingMixin.js
  27. 258 10
      src/views/book/courseware/create/index.vue
  28. 10 8
      src/views/book/courseware/data/audio.js
  29. 10 8
      src/views/book/courseware/data/divider.js
  30. 9 7
      src/views/book/courseware/data/spacing.js
  31. 31 18
      src/views/book/create.vue
  32. 290 4
      src/views/book/setting.vue
  33. 112 0
      src/views/home/components/ListingDialog.vue
  34. 14 2
      src/views/home/index.vue

+ 26 - 4
package-lock.json

@@ -40,7 +40,7 @@
         "electron": "^29.1.5",
         "eslint": "^8.57.0",
         "eslint-plugin-prettier": "^5.1.3",
-        "eslint-plugin-vue": "^9.23.0",
+        "eslint-plugin-vue": "^9.24.0",
         "nodemon": "^3.1.0",
         "patch-package": "^8.0.0",
         "postcss-html": "^1.6.0",
@@ -8931,12 +8931,13 @@
       }
     },
     "node_modules/eslint-plugin-vue": {
-      "version": "9.23.0",
-      "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.23.0.tgz",
-      "integrity": "sha512-Bqd/b7hGYGrlV+wP/g77tjyFmp81lh5TMw0be9093X02SyelxRRfCI6/IsGq/J7Um0YwB9s0Ry0wlFyjPdmtUw==",
+      "version": "9.24.0",
+      "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.24.0.tgz",
+      "integrity": "sha512-9SkJMvF8NGMT9aQCwFc5rj8Wo1XWSMSHk36i7ZwdI614BU7sIOR28ZjuFPKp8YGymZN12BSEbiSwa7qikp+PBw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
+        "globals": "^13.24.0",
         "natural-compare": "^1.4.0",
         "nth-check": "^2.1.1",
         "postcss-selector-parser": "^6.0.15",
@@ -8951,6 +8952,18 @@
         "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0"
       }
     },
+    "node_modules/eslint-plugin-vue/node_modules/globals": {
+      "version": "13.24.0",
+      "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz",
+      "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+      "dev": true,
+      "dependencies": {
+        "type-fest": "^0.20.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/eslint-plugin-vue/node_modules/lru-cache": {
       "version": "6.0.0",
       "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -8978,6 +8991,15 @@
         "node": ">=10"
       }
     },
+    "node_modules/eslint-plugin-vue/node_modules/type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "dev": true,
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/eslint-plugin-vue/node_modules/yallist": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz",

+ 1 - 1
package.json

@@ -47,7 +47,7 @@
     "electron": "^29.1.5",
     "eslint": "^8.57.0",
     "eslint-plugin-prettier": "^5.1.3",
-    "eslint-plugin-vue": "^9.23.0",
+    "eslint-plugin-vue": "^9.24.0",
     "prettier": "^3.2.5",
     "nodemon": "^3.1.0",
     "sass": "^1.72.0",

+ 8 - 0
src/api/book.js

@@ -0,0 +1,8 @@
+import http from '@/utils/http';
+
+/**
+ * 分页查询教材列表
+ */
+export function PageQueryBookList(data) {
+  return http.post(`book-book_manager-PageQueryBookList`, data);
+}

BIN
src/assets/mask_group.png


+ 3 - 2
src/common/SvgIcon/index.vue

@@ -47,10 +47,11 @@ export default {
       return `#icon-${this.iconClass}`;
     },
     svgClass() {
+      const classes = ['svg-icon', this.iconClass];
       if (this.className) {
-        return `svg-icon ${this.className}`;
+        classes.push(this.className);
       }
-      return 'svg-icon';
+      return classes.join(' ');
     },
     styleExternalIcon() {
       return {

+ 3 - 0
src/icons/svg/artboard.svg

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.33301 5.33203V10.6654H10.6663V5.33203H5.33301ZM3.99967 3.9987H11.9997V11.9987H3.99967V3.9987ZM3.99967 1.33203H5.33301V3.33203H3.99967V1.33203ZM3.99967 12.6654H5.33301V14.6654H3.99967V12.6654ZM1.33301 3.9987H3.33301V5.33203H1.33301V3.9987ZM1.33301 10.6654H3.33301V11.9987H1.33301V10.6654ZM12.6663 3.9987H14.6663V5.33203H12.6663V3.9987ZM12.6663 10.6654H14.6663V11.9987H12.6663V10.6654ZM10.6663 1.33203H11.9997V3.33203H10.6663V1.33203ZM10.6663 12.6654H11.9997V14.6654H10.6663V12.6654Z" fill="white"/>
+</svg>

+ 4 - 0
src/icons/svg/browse.svg

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.8796 8C10.8796 9.59052 9.59033 10.8799 7.99976 10.8799C6.40918 10.8799 5.11987 9.59052 5.11987 8C5.11987 6.40955 6.40918 5.12018 7.99976 5.12018C9.59033 5.12018 10.8796 6.40955 10.8796 8ZM9.87964 8C9.87964 6.96179 9.03784 6.12018 7.99976 6.12018C6.96143 6.12018 6.11987 6.96179 6.11987 8C6.11987 9.03821 6.96143 9.87988 7.99976 9.87988C9.03784 9.87988 9.87964 9.03821 9.87964 8Z" fill="black" fill-opacity="0.9"/>
+<path d="M1.11694 8.22998C2.46143 10.87 5.10156 12.5 8.00488 12.5C10.8984 12.5 13.5386 10.87 14.8831 8.22998L15 8L14.8831 7.77002C13.5386 5.14001 10.8984 3.5 8.00488 3.5C5.10156 3.5 2.46143 5.14001 1.11694 7.77002L1 8L1.11694 8.22998ZM8.00488 11.5C5.5498 11.5 3.3186 10.17 2.1106 8C3.3186 5.83002 5.5498 4.5 8.00488 4.5C10.4502 4.5 12.6814 5.83002 13.8992 8C12.6909 10.17 10.4502 11.5 8.00488 11.5Z" fill="black" fill-opacity="0.9"/>
+</svg>

+ 3 - 1
src/icons/svg/delete.svg

@@ -1,3 +1,5 @@
 <svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M7.5 2H10V3H9V9.5C9 9.77615 8.77615 10 8.5 10H1.5C1.22386 10 1 9.77615 1 9.5V3H0V2H2.5V0.5C2.5 0.22386 2.72386 0 3 0H7C7.27615 0 7.5 0.22386 7.5 0.5V2ZM8 3H2V9H8V3ZM5.7071 5.99985L6.591 6.88375L5.8839 7.59085L5 6.70695L4.1161 7.59085L3.40901 6.88375L4.2929 5.99985L3.40901 5.116L4.1161 4.4089L5 5.29275L5.8839 4.4089L6.591 5.116L5.7071 5.99985ZM3.5 1V2H6.5V1H3.5Z" fill="#ED4646"/>
+  <path
+    d="M7.5 2H10V3H9V9.5C9 9.77615 8.77615 10 8.5 10H1.5C1.22386 10 1 9.77615 1 9.5V3H0V2H2.5V0.5C2.5 0.22386 2.72386 0 3 0H7C7.27615 0 7.5 0.22386 7.5 0.5V2ZM8 3H2V9H8V3ZM5.7071 5.99985L6.591 6.88375L5.8839 7.59085L5 6.70695L4.1161 7.59085L3.40901 6.88375L4.2929 5.99985L3.40901 5.116L4.1161 4.4089L5 5.29275L5.8839 4.4089L6.591 5.116L5.7071 5.99985ZM3.5 1V2H6.5V1H3.5Z"
+    fill="currentColor" />
 </svg>

+ 4 - 0
src/icons/svg/edit.svg

@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.8819 1.73836L14.1253 4.98174L14.8324 4.27463L11.589 1.03125L10.8819 1.73836Z" fill="black" fill-opacity="0.9"/>
+<path d="M2.35217 13.8646L5.96513 13.142L13.2627 5.84445L10.0194 2.60107L2.72175 9.89867L1.99916 13.5116C1.95717 13.7215 2.14225 13.9066 2.35217 13.8646ZM10.0194 4.01528L11.8485 5.84445L5.47212 12.2208L3.18566 12.6781L3.64295 10.3917L10.0194 4.01528Z" fill="black" fill-opacity="0.9"/>
+</svg>

+ 3 - 0
src/icons/svg/palette.svg

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.99967 1.33203C11.6811 1.33203 14.6663 3.98388 14.6663 7.25796C14.6663 9.30243 13.0071 10.9616 10.9626 10.9616H9.65154C9.03674 10.9616 8.54041 11.458 8.54041 12.0728C8.54041 12.3542 8.65154 12.6135 8.82187 12.8061C8.99967 13.0061 9.11081 13.2654 9.11081 13.5542C9.11081 14.1691 8.59967 14.6654 7.99967 14.6654C4.31819 14.6654 1.33301 11.6802 1.33301 7.9987C1.33301 4.31722 4.31819 1.33203 7.99967 1.33203ZM7.20707 12.0728C7.20707 10.7216 8.30034 9.6283 9.65154 9.6283H10.9626C12.2707 9.6283 13.333 8.56603 13.333 7.25796C13.333 4.75816 10.9781 2.66536 7.99967 2.66536C5.05457 2.66536 2.66634 5.0536 2.66634 7.9987C2.66634 10.792 4.81474 13.0844 7.54901 13.3132C7.32954 12.9432 7.20707 12.5161 7.20707 12.0728ZM4.99967 7.9987C4.44739 7.9987 3.99967 7.55096 3.99967 6.9987C3.99967 6.44641 4.44739 5.9987 4.99967 5.9987C5.55196 5.9987 5.99967 6.44641 5.99967 6.9987C5.99967 7.55096 5.55196 7.9987 4.99967 7.9987ZM10.9997 7.9987C10.4474 7.9987 9.99967 7.55096 9.99967 6.9987C9.99967 6.44641 10.4474 5.9987 10.9997 5.9987C11.5519 5.9987 11.9997 6.44641 11.9997 6.9987C11.9997 7.55096 11.5519 7.9987 10.9997 7.9987ZM7.99967 5.9987C7.44741 5.9987 6.99967 5.55098 6.99967 4.9987C6.99967 4.44641 7.44741 3.9987 7.99967 3.9987C8.55194 3.9987 8.99967 4.44641 8.99967 4.9987C8.99967 5.55098 8.55194 5.9987 7.99967 5.9987Z" fill="black"/>
+</svg>

+ 10 - 0
src/icons/svg/save.svg

@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_271_9204)">
+<path d="M4.66667 12.6667V8.66667H11.3333V12.6667H12.6667V5.21895L10.7811 3.33333H3.33333V12.6667H4.66667ZM2.66667 2H11.3333L14 4.66667V13.3333C14 13.7015 13.7015 14 13.3333 14H2.66667C2.29848 14 2 13.7015 2 13.3333V2.66667C2 2.29848 2.29848 2 2.66667 2ZM6 10V12.6667H10V10H6Z" fill="white"/>
+</g>
+<defs>
+<clipPath id="clip0_271_9204">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
src/icons/svg/setup.svg


+ 23 - 11
src/router/modules/book.js

@@ -1,13 +1,25 @@
 import DEFAULT from '@/layouts/default';
 
-export const createBookPage = {
-  path: '/book',
-  component: DEFAULT,
-  redirect: '/book/create',
-  children: [
-    {
-      path: 'create',
-      component: () => import('@/views/book/create.vue'),
-    },
-  ],
-};
+export const createBookRouters = [
+  // 教材
+  {
+    path: '/book',
+    component: DEFAULT,
+    redirect: '/book/create',
+    children: [
+      {
+        path: 'create',
+        component: () => import('@/views/book/create.vue'),
+      },
+      {
+        path: 'setting/:id',
+        component: () => import('@/views/book/setting.vue'),
+      },
+    ],
+  },
+  // 章节
+  {
+    path: '/chapter',
+    component: () => import('@/views/book/chapter.vue'),
+  },
+];

+ 2 - 2
src/router/modules/index.js

@@ -1,5 +1,5 @@
 import { homePage, loginPage, NotFoundPage } from './basic';
-import { createBookPage } from './book';
+import { createBookRouters } from './book';
 import CoursewareRouters from './courseware';
 
-export const routes = [homePage, loginPage, createBookPage, ...CoursewareRouters, NotFoundPage];
+export const routes = [homePage, loginPage, ...createBookRouters, ...CoursewareRouters, NotFoundPage];

+ 1 - 2
src/styles/element-variables.scss

@@ -16,8 +16,7 @@ $--input-small-font-size: 14px;
 // button
 $--button-small-font-size: 14px;
 $--button-primary-background-color: $main-color;
-$--button-default-background-color: #f3f3f3;
-$--button-default-border-color: #f3f3f3;
+$--button-default-border-color: #dcdcdc;
 $--button-small-padding-vertical: 8px;
 
 /* 改变 icon 字体路径变量,必需 */

+ 2 - 2
src/utils/http.js

@@ -1,6 +1,6 @@
 import axios from 'axios';
-import store from '@/store';
 
+import { getToken } from '@/utils/auth';
 import { Message } from 'element-ui';
 
 const service = axios.create({
@@ -78,7 +78,7 @@ service.interceptors.response.use(
  * @returns {object} 返回必需的请求参数
  * */
 function getRequestParams() {
-  const token = store.state.user;
+  const token = getToken();
 
   return {
     AccessToken: token?.access_token ?? '',

+ 123 - 4
src/views/book/chapter.vue

@@ -1,15 +1,134 @@
 <template>
-  <div></div>
+  <div class="chapter">
+    <div class="chapter-top">
+      <div class="catalogue"></div>
+      <div class="operation">
+        <el-button type="primary" class="add"><SvgIcon icon-class="artboard" /> 添加教材内容</el-button>
+        <el-button class="preview"
+          ><SvgIcon icon-class="browse" /><span>预览</span>
+          <el-button type="primary"><SvgIcon icon-class="save" />保存</el-button>
+        </el-button>
+      </div>
+    </div>
+
+    <div class="book-content" @dblclick="enterCourseware">
+      <div class="book-content-top">
+        <span class="book-content-title">教材内容</span>
+        <SvgIcon icon-class="delete" />
+        <SvgIcon icon-class="setup" />
+      </div>
+      <div class="tip"><span>双击开始编辑教材内容 1</span></div>
+    </div>
+  </div>
 </template>
 
 <script>
 export default {
   name: 'ChapterPage',
   data() {
-    return {};
+    return {
+      chapter_id: this.$route.query.id,
+    };
+  },
+  methods: {
+    enterCourseware() {
+      this.$router.push('/courseware');
+    },
   },
-  methods: {},
 };
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.chapter {
+  display: flex;
+  flex-direction: column;
+  row-gap: 16px;
+  width: 100%;
+  height: 100%;
+  padding: 24px;
+  background: url('~@/assets/mask_group.png') repeat 0 0;
+
+  &-top {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 8px;
+
+    .catalogue {
+      display: flex;
+      align-items: center;
+      min-width: 200px;
+      height: 40px;
+      padding: 4px 12px;
+      background-color: #fff;
+      border-radius: 4px;
+      box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 8%);
+    }
+
+    .operation {
+      display: flex;
+
+      .el-button {
+        height: 40px;
+        font-size: 16px;
+        font-weight: bold;
+
+        :deep > span {
+          display: flex;
+          column-gap: 4px;
+          align-items: center;
+        }
+      }
+
+      .preview {
+        padding: 3px;
+
+        :deep > span {
+          padding-left: 12px;
+
+          > .el-button {
+            height: 32px;
+            margin-left: 12px;
+          }
+        }
+      }
+    }
+  }
+
+  .book-content {
+    height: 550px;
+    padding: 8px 12px;
+    cursor: pointer;
+    background: rgba(241, 246, 255, 44%);
+    border: 1px solid #005aff;
+    border-radius: 4px;
+
+    &-top {
+      display: flex;
+      column-gap: 8px;
+      align-items: center;
+
+      .book-content-title {
+        font-size: 14px;
+        font-weight: bold;
+        color: #1c6cff;
+      }
+
+      .svg-icon {
+        color: #babbbe;
+      }
+    }
+
+    .tip {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 100%;
+      height: calc(100% - 42px);
+      font-size: 14px;
+      font-weight: bold;
+      color: #000;
+    }
+  }
+}
+</style>

+ 7 - 5
src/views/book/courseware/create/components/base/audio/Audio.vue

@@ -2,28 +2,30 @@
   <ModuleBase :type="data.type">
     <template #content>
       <span>音频</span>
-      <el-upload>
+      <el-upload action="none">
         <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
         <el-button size="small" type="primary">上传</el-button>
         <el-divider />
-        <div slot="tip" class="el-upload__tip">支持上传mp3、acc、wma,等格式音频文件,单个文件最大100MB,总文件体积不超1G。</div>
+        <div slot="tip" class="el-upload__tip">
+          支持上传mp3、acc、wma,等格式音频文件,单个文件最大100MB,总文件体积不超1G。
+        </div>
       </el-upload>
     </template>
   </ModuleBase>
 </template>
 
 <script>
-import { AudioData } from '@/views/book/courseware/data/audio';
+import { getAudioData } from '@/views/book/courseware/data/audio';
 
 import ModuleMixin from '../../common/ModuleMixin';
 
 export default {
-  name: 'Divider',
+  name: 'AudioPage',
   components: {},
   mixins: [ModuleMixin],
   data() {
     return {
-      data: AudioData,
+      data: getAudioData(),
     };
   },
   computed: {

+ 4 - 24
src/views/book/courseware/create/components/base/audio/AudioSetting.vue

@@ -20,41 +20,21 @@
 </template>
 
 <script>
-import ModuleMixin from '../../common/ModuleMixin';
+import SettingMixin from '@/views/book/courseware/create/components/common/SettingMixin';
 
 export default {
-  name: 'DividerSetting',
-  mixins: [ModuleMixin],
+  name: 'AudioSetting',
+  mixins: [SettingMixin],
   data() {
     return {
       labelPosition: 'left',
-      isSet: false, // 父组件是否已设置
       setting: {
         height: 100,
         type: 'solid',
       },
     };
   },
-  watch: {
-    setting: {
-      handler(val) {
-        if (this.isSet) {
-          this.$emit('updateSetting', val);
-        }
-      },
-      deep: true,
-    },
-  },
-  methods: {
-    /**
-     * @description 设置属性
-     * @param {Object} setting 属性
-     */
-    setSetting(setting) {
-      this.isSet = true;
-      this.setting = setting;
-    },
-  },
+  methods: {},
 };
 </script>
 

+ 3 - 4
src/views/book/courseware/create/components/base/divider/Divider.vue

@@ -7,17 +7,16 @@
 </template>
 
 <script>
-import { DividerData } from '@/views/book/courseware/data/divider';
+import { getDividerData } from '@/views/book/courseware/data/divider';
 
 import ModuleMixin from '../../common/ModuleMixin';
 
 export default {
-  name: 'Divider',
-  components: {},
+  name: 'DividerPage',
   mixins: [ModuleMixin],
   data() {
     return {
-      data: DividerData,
+      data: getDividerData(),
     };
   },
   computed: {

+ 4 - 21
src/views/book/courseware/create/components/base/divider/DividerSetting.vue

@@ -14,38 +14,21 @@
 </template>
 
 <script>
+import SettingMixin from '@/views/book/courseware/create/components/common/SettingMixin';
+
 export default {
   name: 'DividerSetting',
+  mixins: [SettingMixin],
   data() {
     return {
       labelPosition: 'left',
-      isSet: false, // 父组件是否已设置
       setting: {
         height: 100,
         type: 'solid',
       },
     };
   },
-  watch: {
-    setting: {
-      handler(val) {
-        if (this.isSet) {
-          this.$emit('updateSetting', val);
-        }
-      },
-      deep: true,
-    },
-  },
-  methods: {
-    /**
-     * @description 设置属性
-     * @param {Object} setting 属性
-     */
-    setSetting(setting) {
-      this.isSet = true;
-      this.setting = setting;
-    },
-  },
+  methods: {},
 };
 </script>
 

+ 3 - 3
src/views/book/courseware/create/components/base/spacing/Spacing.vue

@@ -7,17 +7,17 @@
 </template>
 
 <script>
-import { SpacingData } from '@/views/book/courseware/data/spacing';
+import { getSpacingData } from '@/views/book/courseware/data/spacing';
 
 import ModuleMixin from '../../common/ModuleMixin';
 
 export default {
-  name: 'Spacing',
+  name: 'SpacingPage',
   components: {},
   mixins: [ModuleMixin],
   data() {
     return {
-      data: SpacingData,
+      data: getSpacingData(),
     };
   },
   computed: {

+ 4 - 21
src/views/book/courseware/create/components/base/spacing/SpacingSetting.vue

@@ -9,37 +9,20 @@
 </template>
 
 <script>
+import SettingMixin from '@/views/book/courseware/create/components/common/SettingMixin';
+
 export default {
   name: 'SpacingSetting',
+  mixins: [SettingMixin],
   data() {
     return {
       labelPosition: 'left',
-      isSet: false, // 父组件是否已设置
       setting: {
         height: 40,
       },
     };
   },
-  watch: {
-    setting: {
-      handler(val) {
-        if (this.isSet) {
-          this.$emit('updateSetting', val);
-        }
-      },
-      deep: true,
-    },
-  },
-  methods: {
-    /**
-     * @description 设置属性
-     * @param {Object} setting 属性
-     */
-    setSetting(setting) {
-      this.isSet = true;
-      this.setting = setting;
-    },
-  },
+  methods: {},
 };
 </script>
 

+ 13 - 2
src/views/book/courseware/create/components/common/ModuleBase.vue

@@ -4,7 +4,9 @@
       <span class="title">{{ componentNameList[type] }}</span>
       <div class="module-icon">
         <span><SvgIcon icon-class="copy" size="10" /></span>
-        <span class="active"><SvgIcon icon-class="setup" size="10" /></span>
+        <span :class="[{ active: getCurSettingId() === id }]" @click="showSetting">
+          <SvgIcon icon-class="setup" size="10" />
+        </span>
         <span><SvgIcon icon-class="delete" size="10" /></span>
       </div>
     </div>
@@ -19,6 +21,7 @@ import { componentNameList } from '@/views/book/courseware/data/bookType.js';
 
 export default {
   name: 'ModuleBase',
+  inject: ['id', 'showSetting', 'getCurSettingId'],
   props: {
     type: {
       type: String,
@@ -37,7 +40,7 @@ export default {
 <style lang="scss" scoped>
 .module {
   padding: 8px;
-  border: 1px solid #5a5a5a;
+  border: 1px solid #ebebeb;
 
   &-top {
     display: flex;
@@ -67,6 +70,14 @@ export default {
       &.active {
         background-color: #c9c9c9;
       }
+
+      .svg-icon.setup {
+        color: #000;
+      }
+
+      .svg-icon.delete {
+        color: #ed4646;
+      }
     }
   }
 

+ 15 - 9
src/views/book/courseware/create/components/common/ModuleMixin.js

@@ -1,5 +1,6 @@
 // 组件混入
 import ModuleBase from './ModuleBase.vue';
+
 import { questionNumberTypeList } from '@/views/book/courseware/data/common';
 
 const mixin = {
@@ -9,27 +10,32 @@ const mixin = {
     };
   },
   props: {
-    questionId: {
+    id: {
       type: String,
-      default: '',
+      required: true,
     },
   },
   components: {
     ModuleBase,
   },
-  mounted() {
-    // TODO: 为了先显示设置页面,以后需要修改
-    this.$emit('showSetting', this.data.setting);
+  provide() {
+    return {
+      showSetting: this.showSetting,
+      id: this.id,
+    };
   },
-  watch: {},
+  created() {},
   methods: {
-    // 显示设置
+    /**
+     * @description 显示设置
+     */
     showSetting() {
-      this.$emit('showSetting', this.data.setting);
+      this.$emit('showSetting', this.data.setting, this.data.type, this.id);
     },
     /**
      * @description 更新属性
-     * @param {Object} setting 属性
+     * @param {object} setting 属性
+     * @param {string} type 属性类型
      */
     updateSetting(setting) {
       this.data.setting = setting;

+ 20 - 0
src/views/book/courseware/create/components/common/SettingMixin.js

@@ -0,0 +1,20 @@
+import { questionNumberTypeList } from '../../../data/common';
+
+const mixin = {
+  data() {
+    return {
+      questionNumberTypeList,
+    };
+  },
+  methods: {
+    /**
+     * @description 设置属性
+     * @param {object} setting 属性
+     */
+    setSetting(setting) {
+      this.setting = setting;
+    },
+  },
+};
+
+export default mixin;

+ 258 - 10
src/views/book/courseware/create/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="create">
+  <div ref="create" class="create">
     <div class="create-left">
       <div class="back-container">
         <el-button class="back" @click="back"><i class="el-icon-arrow-left"></i> 返回</el-button>
@@ -10,8 +10,9 @@
         <div
           v-for="{ value: childValue, icon, label: childLabel } in children"
           :key="childValue"
+          ref="componentsItem"
           class="components-item"
-          @click="curType = childValue"
+          @mousedown="dragStart($event, childValue)"
         >
           <SvgIcon v-if="icon" :icon-class="icon" />
           <span>{{ childLabel }}</span>
@@ -22,13 +23,36 @@
     <div class="create-middle">
       <div></div>
       <main ref="canvas" class="canvas">
-        <component :is="componentList[curType]" ref="components" @showSetting="showSetting" />
+        <div class="drag-line" data-row="-1"></div>
+        <div v-for="(row, i) in data.row_list" :key="i" class="row">
+          <div
+            v-for="(col, j) in row.col_list"
+            :key="j"
+            class="col"
+            :style="{
+              width: col.width,
+              gridTemplateAreas: col.grid_template_areas,
+              gridTemplateColumns: col.grid_template_columns,
+              gridTemplateRows: col.grid_template_rows,
+            }"
+          >
+            <component
+              :is="componentList[grid.type]"
+              v-for="(grid, k) in col.grid_list"
+              :id="grid.id"
+              :key="k"
+              :style="{ gridArea: grid.grid_area }"
+              @showSetting="showSetting"
+            />
+          </div>
+          <div class="drag-line" :data-row="i"></div>
+        </div>
       </main>
     </div>
 
     <div class="create-right">
       <div class="setting-tittle">设置</div>
-      <component :is="componentSettingList[curType]" ref="setting" @updateSetting="updateSetting" />
+      <component :is="componentSettingList[curSettingType]" ref="setting" />
     </div>
   </div>
 </template>
@@ -38,23 +62,212 @@ import { bookTypeOption, componentList, componentSettingList } from '../data/boo
 
 export default {
   name: 'CreatePage',
+  provide() {
+    return {
+      getCurSettingId: () => this.curSettingId,
+    };
+  },
   data() {
     return {
+      data: {
+        id: '',
+        background_image_url: '',
+        background_position: {
+          x: 0,
+          y: 0,
+          width: 100,
+          height: 100,
+        },
+        // 组件列表
+        row_list: [],
+      },
+      curSettingType: '',
+      curSettingId: '',
       curType: 'divider',
       componentList,
       componentSettingList,
       bookTypeOption,
+      curRow: -2,
+      enterCanvas: false, // 是否进入画布
+      // 拖拽状态
+      drag: {
+        clientX: 0,
+        clientY: 0,
+        dragging: false,
+      },
     };
   },
+  watch: {
+    drag: {
+      handler(val) {
+        if (val.dragging) {
+          const dragging = document.querySelector('.dragging');
+          dragging.style.left = `${val.clientX}px`;
+          dragging.style.top = `${val.clientY}px`;
+        }
+      },
+      deep: true,
+    },
+    enterCanvas: {
+      handler(val) {
+        if (val) return;
+        const dragLineList = document.querySelectorAll('.drag-line');
+        dragLineList.forEach((item) => {
+          item.style.opacity = 0;
+        });
+        this.curRow = -2;
+      },
+    },
+  },
+  mounted() {
+    document.addEventListener('mousemove', this.dragMove);
+    document.addEventListener('mouseup', this.dragEnd);
+  },
+  beforeDestroy() {
+    document.removeEventListener('mousemove', this.dragMove);
+    document.removeEventListener('mouseup', this.dragEnd);
+  },
   methods: {
     back() {
-      this.$router.push('/');
+      this.$router.push('/chapter?id=1');
+    },
+    /**
+     * 显示设置
+     * @param {object} setting
+     * @param {string} type
+     */
+    showSetting(setting, type, id) {
+      this.curSettingType = type;
+      this.curSettingId = id;
+      this.$nextTick(() => {
+        this.$refs.setting.setSetting(setting);
+      });
+    },
+    /**
+     * 拖拽开始
+     * 用点击模拟拖拽
+     * @param {MouseEvent} event
+     * @param {string} type
+     */
+    dragStart(event, type) {
+      // 获取鼠标位置
+      const { clientX, clientY } = event;
+      document.body.style.userSelect = 'none'; // 禁止选中文本
+      this.drag.dragging = true;
+      this.curType = type;
+      // 在鼠标位置创建一个拖拽元素
+      const dragging = document.createElement('div');
+      dragging.className = 'dragging';
+      this.drag.clientX = clientX;
+      this.drag.clientY = clientY;
+      document.body.appendChild(dragging);
+    },
+    /**
+     * 鼠标移动
+     */
+    dragMove(event) {
+      if (!this.drag.dragging) return;
+
+      const { clientX, clientY } = event;
+      this.drag.clientX = clientX;
+      this.drag.clientY = clientY;
+
+      let { leftMarginDifference, topMarginDifference, isInsideCanvas } = this.getMarginDifferences();
+
+      this.enterCanvas = isInsideCanvas;
+      if (!isInsideCanvas) return;
+
+      const dragLineList = document.querySelectorAll('.drag-line');
+      let minDistance = Infinity;
+      let minIndex = -1;
+      dragLineList.forEach((item, index) => {
+        const rect = item.getBoundingClientRect();
+        const distance = Math.sqrt(
+          Math.pow(clientX - rect.left - rect.width / 2, 2) + Math.pow(clientY - rect.top - rect.height / 2, 2),
+        );
+        if (distance < minDistance) {
+          minDistance = distance;
+          minIndex = index;
+        }
+      });
+      dragLineList.forEach((item, index) => {
+        if (index === minIndex) {
+          // 获取 item 中的 data-row
+          const row = item.getAttribute('data-row');
+          this.curRow = Number(row);
+          item.style.opacity = 1;
+        } else {
+          item.style.opacity = 0;
+        }
+      });
+    },
+    /**
+     * 鼠标松开
+     */
+    dragEnd() {
+      document.body.style.userSelect = 'auto';
+      const dragging = document.querySelector('.dragging');
+      if (dragging) {
+        document.body.removeChild(dragging);
+        this.drag.dragging = false;
+      }
+
+      if (this.enterCanvas && this.curRow >= -1) {
+        this.data.row_list.splice(this.curRow + 1, 0, this.calculateInsertedObject());
+      }
+      this.enterCanvas = false;
     },
-    showSetting(setting) {
-      this.$refs.setting.setSetting(setting);
+    /**
+     * 计算插入的对象
+     */
+    calculateInsertedObject() {
+      let num = 0; // 计算当前行之前的所有 grid_list 的数量
+      for (let i = 0; i <= this.curRow; i++) {
+        this.data.row_list[i]?.col_list.forEach((item) => {
+          num += item.grid_list.length;
+        });
+      }
+      const letter = String.fromCharCode(65 + num);
+
+      return {
+        col_list: [
+          {
+            width: '100%',
+            grid_template_areas: `'${letter}'`,
+            grid_template_columns: 'auto',
+            grid_template_rows: 'auto',
+            grid_list: [
+              {
+                grid_area: letter,
+                type: this.curType,
+                id: letter,
+              },
+            ],
+          },
+        ],
+      };
     },
-    updateSetting(setting) {
-      this.$refs.components.updateSetting(setting);
+    /**
+     * 获取拖拽元素和画布的边距差值
+     * @returns {object} { leftMarginDifference, topMarginDifference, isInsideCanvas }
+     * leftMarginDifference: 拖拽元素和画布左边距差值
+     * topMarginDifference: 拖拽元素和画布上边距差值
+     * isInsideCanvas: 是否在画布内
+     */
+    getMarginDifferences() {
+      const rect1 = document.querySelector('.dragging').getBoundingClientRect();
+      const rect2 = this.$refs.canvas.getBoundingClientRect();
+
+      const leftMarginDifference = rect1.left - rect2.left + 128;
+      const topMarginDifference = rect1.top - rect2.top + 72;
+
+      let isInsideCanvas =
+        leftMarginDifference > 0 &&
+        leftMarginDifference < rect2.width &&
+        topMarginDifference > 0 &&
+        topMarginDifference < rect2.height;
+
+      return { leftMarginDifference, topMarginDifference, isInsideCanvas };
     },
   },
 };
@@ -127,11 +340,33 @@ export default {
     background-color: #ececec;
 
     .canvas {
+      display: flex;
+      flex-direction: column;
+      row-gap: 6px;
       width: 100%;
       min-height: 100%;
       padding: 24px;
-      background-color: #f6f6f6;
+      background-color: #fff;
       border-radius: 4px;
+
+      .row {
+        display: flex;
+        flex-direction: column;
+        row-gap: 6px;
+
+        .col {
+          display: grid;
+        }
+      }
+
+      .drag-line {
+        width: calc(100% - 16px);
+        height: 4px;
+        margin: 0 8px;
+        background-color: #379fff;
+        border-radius: 4px;
+        opacity: 0;
+      }
     }
   }
 
@@ -148,3 +383,16 @@ export default {
   }
 }
 </style>
+
+<style lang="scss">
+.dragging {
+  position: fixed;
+  z-index: 999;
+  width: 320px;
+  height: 180px;
+  background-color: #eaf5ff;
+  border: 1px solid #b5dbff;
+  border-radius: 4px;
+  transform: translate(-40%, -40%);
+}
+</style>

+ 10 - 8
src/views/book/courseware/data/audio.js

@@ -1,8 +1,10 @@
-export let AudioData = {
-  type: 'audio',
-  title: '音频',
-  setting: {
-    serialNumber: 1,
-    height: 40,
-  },
-};
+export function getAudioData() {
+  return {
+    type: 'audio',
+    title: '音频',
+    setting: {
+      serialNumber: 1,
+      height: 40,
+    },
+  };
+}

+ 10 - 8
src/views/book/courseware/data/divider.js

@@ -1,8 +1,10 @@
-export let DividerData = {
-  type: 'divider',
-  title: '分割线',
-  setting: {
-    height: 40,
-    type: 'solid', // dotted 虚线
-  },
-};
+export function getDividerData() {
+  return {
+    type: 'divider',
+    title: '分割线',
+    setting: {
+      height: 40,
+      type: 'solid', // dotted 虚线
+    },
+  };
+}

+ 9 - 7
src/views/book/courseware/data/spacing.js

@@ -1,7 +1,9 @@
-export let SpacingData = {
-  type: 'spacing',
-  title: '间距',
-  setting: {
-    height: 40,
-  },
-};
+export function getSpacingData() {
+  return {
+    type: 'spacing',
+    title: '间距',
+    setting: {
+      height: 40,
+    },
+  };
+}

+ 31 - 18
src/views/book/create.vue

@@ -12,17 +12,17 @@
     </div>
     <div class="basic-info">
       <div class="basic-info-title">基本信息</div>
-      <el-form ref="form" :model="form" label-width="80px">
-        <el-form-item label="封面">
+      <el-form ref="form" :model="form" :rules="formRules" label-width="80px">
+        <el-form-item label="封面" prop="cover_image_url">
           <el-upload
-            class="avatar-uploader"
+            class="cover-uploader"
             action="none"
             :show-file-list="false"
             :http-request="uploadCover"
             :before-upload="beforeCoverUpload"
           >
-            <img v-if="form.image_url" :src="form.image_url" class="avatar" />
-            <div v-else class="avatar-uploader-icon">
+            <img v-if="form.cover_image_url" :src="form.cover_image_url" class="cover" />
+            <div v-else class="cover-uploader-icon">
               <i class="el-icon-plus"></i>
               <span>点击上传封面</span>
             </div>
@@ -80,8 +80,10 @@ export default {
   name: 'CreateBookPage',
   data() {
     return {
+      id: this.$route.query.id,
       form: {
-        image_url: '',
+        cover_image_url: '',
+        image_id: '',
         name: '',
         author: '',
         current_price: '',
@@ -90,18 +92,32 @@ export default {
         classify: '',
         brief_introduction: '',
       },
+      formRules: {
+        cover_image_url: [{ required: true, message: '请上传封面', trigger: 'blur' }],
+        name: [{ required: true, message: '请输入书籍名称', trigger: 'blur' }],
+        author: [{ required: true, message: '请输入作者', trigger: 'blur' }],
+        current_price: [{ required: true, message: '请输入现价', trigger: 'blur' }],
+        classify: [{ required: true, message: '请选择分类', trigger: 'blur' }],
+        brief_introduction: [{ required: true, message: '请输入简介', trigger: 'blur' }],
+      },
       breadcrumbList: ['教材管理', '新建教材', '基本信息'],
     };
   },
+  created() {
+    if (this.id) {
+      // TODO 获取书籍信息
+    }
+  },
   methods: {
     confirm() {
-      this.$router.push('/courseware');
+      this.$router.push('/book/setting/123');
     },
     uploadCover(file) {
       fileUpload('Mid', file, { isGlobalprogress: true }).then(({ file_info_list }) => {
         if (file_info_list.length > 0) {
-          const { file_url } = file_info_list[0];
-          this.form.image_url = file_url;
+          const { file_url, file_id } = file_info_list[0];
+          this.form.cover_image_url = file_url;
+          this.form.image_id = file_id;
         }
       });
     },
@@ -153,6 +169,7 @@ export default {
       display: flex;
 
       li {
+        font-size: 14px;
         font-weight: 400;
         color: $font-light-color;
 
@@ -161,14 +178,6 @@ export default {
           color: #c9cdd4;
         }
 
-        .breadcrumb-name {
-          font-size: 14px;
-        }
-
-        &:not(:last-child) {
-          cursor: pointer;
-        }
-
         &:nth-last-child(1) {
           font-weight: bold;
           color: $font-color;
@@ -203,7 +212,7 @@ export default {
         }
       }
 
-      .avatar-uploader {
+      .cover-uploader {
         :deep .el-upload {
           position: relative;
           overflow: hidden;
@@ -247,6 +256,10 @@ export default {
         font-size: 12px;
         color: #8c8c8c;
       }
+
+      &-item {
+        margin-bottom: 24px;
+      }
     }
   }
 }

+ 290 - 4
src/views/book/setting.vue

@@ -1,15 +1,301 @@
 <template>
-  <div></div>
+  <div class="setting">
+    <div class="breadcrumb">
+      <ul>
+        <li>
+          <span>教材管理</span>
+        </li>
+        <li>
+          <span class="separator">></span>
+          <span class="breadcrumb-name">{{ data.name }}</span>
+        </li>
+      </ul>
+    </div>
+
+    <main class="setting-wrapper">
+      <div class="basic-info">
+        <div class="editor">
+          <div class="title">基本信息</div>
+          <el-button @click="edit"><SvgIcon icon-class="edit" /> 编辑</el-button>
+        </div>
+        <el-image :src="data.cover_image_url" class="cover-image">
+          <div slot="error" class="image-slot">
+            <i class="el-icon-picture-outline"></i>
+          </div>
+        </el-image>
+        <div class="name">{{ data.name }}</div>
+        <div class="brief-introduction">{{ data.brief_introduction }}</div>
+      </div>
+
+      <div class="catalogue-wrapper">
+        <template v-if="!isEdit">
+          <div class="catalogue-top">
+            <div class="title">目录</div>
+            <div class="operation">
+              <el-button><SvgIcon icon-class="palette" /> 主色设置</el-button>
+              <el-button @click="isEdit = true"><SvgIcon icon-class="edit" /> 编辑目录</el-button>
+              <el-button><SvgIcon icon-class="browse" /> 预览</el-button>
+            </div>
+          </div>
+
+          <div v-for="{ id, name, children } in catalogueList" :key="id" class="catalogue">
+            <div class="catalogue-title">{{ name }}</div>
+            <template v-for="item in children">
+              <div :key="item.id" :class="['catalogue-item', item.type === 'content' ? 'content' : 'subdirectory']">
+                <span class="name">{{ item.name }}</span>
+                <span class="time">{{ item.time }} {{ item.editor }}</span>
+                <span class="edit" @click="editBookContent(item.id)">编辑</span>
+              </div>
+              <div v-for="li in item.children" :key="li.id">
+                <div :class="['catalogue-item', 'children']">
+                  <span class="name">{{ li.name }}</span>
+                  <span class="time">{{ li.time }} {{ li.editor }}</span>
+                  <span class="edit" @click="editBookContent(li.id)">编辑</span>
+                </div>
+              </div>
+            </template>
+          </div>
+        </template>
+        <template v-else></template>
+      </div>
+    </main>
+  </div>
 </template>
 
 <script>
 export default {
   name: 'SettingPage',
   data() {
-    return {};
+    return {
+      book_id: this.$route.params.id,
+      isEdit: false, // 是否编辑状态
+      data: {
+        cover_image_url: '',
+        image_id: '',
+        name: '新实用汉语',
+        author: '',
+        current_price: '',
+        original_price: '',
+        tags: '',
+        classify: '',
+        brief_introduction: '123',
+      },
+      catalogueList: [
+        {
+          id: 1,
+          name: '第一章',
+          children: [
+            {
+              id: 2,
+              type: 'subdirectory',
+              name: '学习拼音',
+              time: '2022/12/27 11:25',
+              editor: '张三',
+              children: [
+                {
+                  id: 4,
+                  type: 'content',
+                  name: '第一节',
+                  time: '2022/12/27 11:25',
+                  editor: '张三',
+                },
+              ],
+            },
+            {
+              id: 3,
+              type: 'content',
+              name: '认识笔画',
+              time: '2022/12/27 11:25',
+              editor: '张三',
+              children: [],
+            },
+          ],
+        },
+      ],
+    };
+  },
+  methods: {
+    edit() {
+      this.$router.push({ path: '/book/create', query: { id: this.book_id } });
+    },
+    editBookContent(id) {
+      this.$router.push({ path: '/chapter', query: { id } });
+    },
   },
-  methods: {},
 };
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+%button {
+  padding: 7px 15px;
+
+  :deep > span {
+    display: flex;
+    column-gap: 8px;
+    align-items: center;
+  }
+}
+
+.setting {
+  height: 100%;
+  padding: 8px 24px;
+
+  .breadcrumb {
+    display: flex;
+    align-items: center;
+    font-size: 14px;
+
+    > ul {
+      display: flex;
+
+      li {
+        font-weight: 400;
+        color: #949494;
+
+        .separator {
+          margin: 0 8px;
+          color: #c9cdd4;
+        }
+
+        &:nth-last-child(1) {
+          font-weight: bold;
+          color: $font-color;
+        }
+      }
+    }
+  }
+
+  .setting-wrapper {
+    display: flex;
+    column-gap: 24px;
+    justify-content: center;
+    height: calc(100% - 40px);
+    margin-top: 16px;
+
+    .basic-info {
+      display: flex;
+      flex-direction: column;
+      row-gap: 16px;
+      width: 276px;
+      padding: 24px;
+      background-color: #fff;
+      border-radius: 4px;
+
+      .editor {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+
+        .title {
+          font-weight: bold;
+          color: #000;
+        }
+
+        .el-button {
+          @extend %button;
+        }
+      }
+
+      .cover-image {
+        width: 228px;
+        height: 320px;
+        background-color: #f3f3f3;
+        border: 1px solid #e0e0e0;
+
+        :deep .image-slot {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 100%;
+          height: 100%;
+
+          i {
+            font-size: 40px;
+            color: #c0c4cc;
+          }
+        }
+      }
+
+      .name {
+        font-weight: bold;
+        color: #000;
+      }
+
+      .brief-introduction {
+        font-size: 14px;
+        color: #666;
+      }
+    }
+
+    .catalogue-wrapper {
+      display: flex;
+      flex-direction: column;
+      row-gap: 16px;
+      width: 916px;
+      padding: 24px;
+      background-color: #fff;
+      border-radius: 4px;
+
+      .catalogue-top {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+
+        .title {
+          font-weight: bold;
+          color: #000;
+        }
+
+        .operation {
+          .el-button {
+            @extend %button;
+          }
+        }
+      }
+
+      .catalogue {
+        display: flex;
+        flex-direction: column;
+        row-gap: 8px;
+
+        &-title {
+          padding: 8px 0;
+          font-weight: bold;
+          color: #000;
+        }
+
+        &-item {
+          display: flex;
+          column-gap: 8px;
+          padding: 8px 52px 8px 16px;
+          font-size: 14px;
+          border-bottom: 1px solid #ebebeb;
+
+          &.subdirectory {
+            background-color: #f3f3f3;
+          }
+
+          &.children {
+            padding-left: 32px;
+          }
+
+          .name {
+            flex: 1;
+            color: #000;
+          }
+
+          .time,
+          .edit {
+            color: #929292;
+          }
+
+          .edit {
+            margin-left: 24px;
+            cursor: pointer;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 112 - 0
src/views/home/components/ListingDialog.vue

@@ -0,0 +1,112 @@
+<template>
+  <el-dialog :visible="visible" title="上架" width="450px" @close="dialogClose">
+    <div class="listing">
+      <div class="visible-range">
+        <div class="visible-range-title">可见范围</div>
+        <el-radio-group v-model="visibleRange">
+          <el-radio label="all">全部机构可见</el-radio>
+          <el-radio label="only">仅授权机构可见</el-radio>
+        </el-radio-group>
+      </div>
+      <div class="authorized-agency">
+        <div class="authorized-agency-title">选择授权机构(可多选)</div>
+        <el-select v-model="authorizedAgency" multiple placeholder="选择机构">
+          <el-option label="机构1" value="1" />
+          <el-option label="机构2" value="2" />
+        </el-select>
+      </div>
+    </div>
+
+    <div slot="footer" class="footer">
+      <el-button round @click="dialogClose">取消</el-button>
+      <el-button type="primary" round @click="listing">上架</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: 'ListingDialog',
+  props: {
+    id: {
+      type: String,
+      required: true,
+    },
+    visible: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      visibleRange: 'all', // 可见范围
+      authorizedAgency: [], // 授权机构
+    };
+  },
+  methods: {
+    dialogClose() {
+      this.$emit('update:visible', false);
+    },
+    // 上架
+    listing() {},
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.el-dialog__wrapper {
+  :deep .el-dialog {
+    .listing {
+      padding: 0 16px;
+
+      .visible-range {
+        display: flex;
+        flex-direction: column;
+        row-gap: 12px;
+
+        &-title {
+          font-size: 14px;
+          color: $font-light-color;
+        }
+      }
+
+      .authorized-agency {
+        display: flex;
+        flex-direction: column;
+        row-gap: 8px;
+        margin-top: 20px;
+      }
+    }
+
+    &__body {
+      padding: 16px 20px;
+    }
+
+    &__header {
+      display: flex;
+      align-items: center;
+      padding: 16px 20px;
+      border-bottom: 1px solid #ebebeb;
+    }
+
+    &__title {
+      font-size: 14px;
+      font-weight: bold;
+      color: $font-color;
+    }
+
+    &__headerbtn .el-dialog__close {
+      color: $font-color;
+    }
+
+    .footer {
+      display: flex;
+      padding: 0 16px;
+
+      .el-button {
+        flex: 1;
+      }
+    }
+  }
+}
+</style>

+ 14 - 2
src/views/home/index.vue

@@ -37,7 +37,7 @@
       <el-table-column prop="operation" label="操作" width="245" fixed="right">
         <template slot-scope="{ row }">
           <span class="link" @click="editBook(row.id)">编辑</span>
-          <span class="link" @click="groundingBook(row.id)">上架</span>
+          <span class="link" @click="listingBook(row.id)">上架</span>
           <span class="link danger" @click="deleteBook(row.id)">删除</span>
         </template>
       </el-table-column>
@@ -54,12 +54,19 @@
       @current-change="changePage"
       @size-change="changePageSize"
     />
+
+    <ListingDialog :id="curId" :visible.sync="dialogVisible" />
   </div>
 </template>
 
 <script>
+import ListingDialog from './components/ListingDialog.vue';
+
 export default {
   name: 'HomePage',
+  components: {
+    ListingDialog,
+  },
   data() {
     return {
       data: undefined,
@@ -79,6 +86,8 @@ export default {
           value: 'draft',
         },
       ],
+      curId: '',
+      dialogVisible: false,
     };
   },
   methods: {
@@ -100,7 +109,10 @@ export default {
      * 上架教材
      * @param {string} id 教材id
      */
-    groundingBook(id) {},
+    listingBook(id) {
+      this.dialogVisible = true;
+      this.curId = id;
+    },
     /**
      * 删除教材
      * @param {string} id 教材id

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.