Browse Source

更改创建框架

dusenyao 1 year ago
parent
commit
0478a367b5
34 changed files with 1023 additions and 175 deletions
  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>

File diff suppressed because it is too large
+ 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

Some files were not shown because too many files changed in this diff