Ver Fonte

对接接口

dusenyao há 1 ano atrás
pai
commit
c1201b2de1

+ 1 - 1
src/api/app.js

@@ -56,7 +56,7 @@ export function SaveFileByteBase64Text(data) {
 
 /**
  * 上传文件
- * @param {String} SecurityLevel 保密级别
+ * @param {string} SecurityLevel 保密级别
  * @param {object} file 文件对象
  * @param {object} option 上传选项
  * @param {function} option.handleUploadProgress 上传进度回调

+ 122 - 2
src/api/book.js

@@ -1,8 +1,128 @@
-import http from '@/utils/http';
+import { http } from '@/utils/http';
 
 /**
  * 分页查询教材列表
  */
 export function PageQueryBookList(data) {
-  return http.post(`book-book_manager-PageQueryBookList`, data);
+  return http.post(`${process.env.VUE_APP_BookWebSI}?MethodName=book-book_manager-PageQueryBookList`, data);
+}
+
+/**
+ * 获取教材创建人列表
+ */
+export function GetBookCreatorList() {
+  return http.post(`${process.env.VUE_APP_BookWebSI}?MethodName=book-book_manager-GetBookCreatorList`);
+}
+
+/**
+ * 上架、下架教材
+ */
+export function SetPublishStatusForBook(data) {
+  return http.post(`${process.env.VUE_APP_BookWebSI}?MethodName=book-book_manager-SetPublishStatusForBook`, data);
+}
+
+/**
+ * 分页查询机构索引列表
+ */
+export function PageQueryOrgIndexList_OpenQuery(data) {
+  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=org_manager-PageQueryOrgIndexList_OpenQuery`, data);
+}
+
+/**
+ * 添加教材
+ */
+export function AddBook(data) {
+  return http.post(`${process.env.VUE_APP_BookWebSI}?MethodName=book-book_manager-AddBook`, data);
+}
+
+/**
+ * 得到教材
+ */
+export function GetBook(data) {
+  return http.post(`${process.env.VUE_APP_BookWebSI}?MethodName=book-book_manager-GetBook`, data);
+}
+
+/**
+ * 修改教材
+ */
+export function UpdateBook(data) {
+  return http.post(`${process.env.VUE_APP_BookWebSI}?MethodName=book-book_manager-UpdateBook`, data);
+}
+
+/**
+ * 删除教材
+ */
+export function DeleteBook(data) {
+  return http.post(`${process.env.VUE_APP_BookWebSI}?MethodName=book-book_manager-DeleteBook`, data);
+}
+
+/**
+ * 获取教材类型列表
+ */
+export function GetBookTypeList(data) {
+  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=dict_manager-GetBookTypeList`, data);
+}
+
+/**
+ * 得到教材章节结构
+ */
+export function GetBookChapterStruct(data) {
+  return http.post(`${process.env.VUE_APP_BookWebSI}?MethodName=book-book_manager-GetBookChapterStruct`, data);
+}
+
+/**
+ * 添加教材章节
+ */
+export function AddChapterToBook(data) {
+  return http.post(`${process.env.VUE_APP_BookWebSI}?MethodName=book-chapter_manager-AddChapterToBook`, data);
+}
+
+/**
+ * 修改教材章节
+ */
+export function UpdateChapter(data) {
+  return http.post(`${process.env.VUE_APP_BookWebSI}?MethodName=book-chapter_manager-UpdateChapter`, data);
+}
+
+/**
+ * 删除教材章节
+ */
+export function DeleteChapter(data) {
+  return http.post(`${process.env.VUE_APP_BookWebSI}?MethodName=book-chapter_manager-DeleteChapter`, data);
+}
+
+/**
+ * 得到章节下的互动课件列表
+ * @param {object} data
+ * @param {string} data.chapter_id 章节ID
+ */
+export function GetCoursewareList_Chapter(data) {
+  return http.post(
+    `${process.env.VUE_APP_BookWebSI}?MethodName=book-courseware_manager-GetCoursewareList_Chapter`,
+    data,
+  );
+}
+
+/**
+ * 保存互动课件内容
+ */
+export function SaveCoursewareContent(data) {
+  return http.post(`${process.env.VUE_APP_BookWebSI}?MethodName=book-courseware_manager-SaveCoursewareContent`, data);
+}
+
+/**
+ * 得到互动课件内容
+ */
+export function GetCoursewareContent(data) {
+  return http.post(`book-courseware_manager-GetCoursewareContent`, data);
+}
+
+/**
+ * 保存互动课件组件内容
+ */
+export function SaveCoursewareComponentContent(data) {
+  return http.post(
+    `${process.env.VUE_APP_BookWebSI}?MethodName=book-courseware_manager-SaveCoursewareComponentContent`,
+    data,
+  );
 }

+ 6 - 0
src/icons/svg/add-rectangle.svg

@@ -0,0 +1,6 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path d="M7.5 11V8.5H5V7.5H7.5V5H8.5V7.5H11V8.5H8.5V11H7.5Z" fill="currentColor" fill-opacity="0.9" />
+  <path
+    d="M3 14C2.44772 14 2 13.5523 2 13V3C2 2.44771 2.44772 2 3 2H13C13.5523 2 14 2.44772 14 3L14 13C14 13.5523 13.5523 14 13 14L3 14ZM3 13L13 13L13 3L3 3L3 13Z"
+    fill="currentColor" fill-opacity="0.9" />
+</svg>

+ 12 - 0
src/icons/svg/courseware/background-img.svg

@@ -0,0 +1,12 @@
+<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_9648)">
+    <path
+      d="M7.51321 8.14413L9.99967 4L15.333 14H1.33301L5.99967 5.33333L7.51321 8.14413ZM8.25947 9.49187L9.99121 12.6667H13.1108L9.93141 6.70533L8.25947 9.49187ZM3.56529 12.6667H8.43407L5.99967 8.14567L3.56529 12.6667ZM3.66634 5.33333C2.74587 5.33333 1.99967 4.58714 1.99967 3.66667C1.99967 2.74619 2.74587 2 3.66634 2C4.58681 2 5.33301 2.74619 5.33301 3.66667C5.33301 4.58714 4.58681 5.33333 3.66634 5.33333Z"
+      fill="black" />
+  </g>
+  <defs>
+    <clipPath id="clip0_271_9648">
+      <rect width="16" height="16" fill="white" />
+    </clipPath>
+  </defs>
+</svg>

+ 10 - 0
src/icons/svg/courseware/menu.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_9197)">
+<path d="M5.33333 2.66536H14V3.9987H5.33333V2.66536ZM2 2.33203H4V4.33203H2V2.33203ZM2 6.9987H4V8.9987H2V6.9987ZM2 11.6654H4V13.6654H2V11.6654ZM5.33333 7.33203H14V8.66536H5.33333V7.33203ZM5.33333 11.9987H14V13.332H5.33333V11.9987Z" fill="black"/>
+</g>
+<defs>
+<clipPath id="clip0_271_9197">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 10 - 0
src/icons/svg/courseware/template.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_9644)">
+<path d="M14 13.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 2H13.3333C13.7015 2 14 2.29848 14 2.66667V13.3333ZM7.33333 3.33333H3.33333V12.6667H7.33333V3.33333ZM12.6667 8.66667H8.66667V12.6667H12.6667V8.66667ZM12.6667 3.33333H8.66667V7.33333H12.6667V3.33333Z" fill="black"/>
+</g>
+<defs>
+<clipPath id="clip0_271_9644">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 7 - 0
src/icons/svg/delete-2.svg

@@ -0,0 +1,7 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path d="M6 12V6H7V12H6Z" fill="currentColor" fill-opacity="0.9" />
+  <path d="M9 6V12H10V6H9Z" fill="currentColor" fill-opacity="0.9" />
+  <path
+    d="M10.5 3H14V4H13V14C13 14.5523 12.5523 15 12 15H4C3.44772 15 3 14.5523 3 14V4H2V3H5.5L5.5 1.8C5.5 1.35817 5.85817 1 6.3 1H9.7C10.1418 1 10.5 1.35817 10.5 1.8V3ZM6.5 3H9.5L9.5 2L6.5 2V3ZM4 4V14H12V4H4Z"
+    fill="currentColor" fill-opacity="0.9" />
+</svg>

+ 1 - 1
src/router/modules/book.js

@@ -12,7 +12,7 @@ export const createBookRouters = [
         component: () => import('@/views/book/create.vue'),
       },
       {
-        path: 'setting/:id',
+        path: 'setting/:book_id',
         component: () => import('@/views/book/setting.vue'),
       },
     ],

+ 1 - 2
src/router/modules/courseware.js

@@ -6,10 +6,9 @@ import DEFAULT from '@/layouts/default';
 const CoursewareModulePage = {
   path: '/courseware',
   component: DEFAULT,
-  redirect: '/courseware/create',
   children: [
     {
-      path: 'create',
+      path: 'create/:courseware_id',
       component: () => import('@/views/book/courseware/create/index.vue'),
     },
   ],

+ 4 - 1
src/utils/http.js

@@ -1,4 +1,6 @@
 import axios from 'axios';
+import store from '@/store';
+import router from '@/router';
 
 import { getToken } from '@/utils/auth';
 import { Message } from 'element-ui';
@@ -53,7 +55,8 @@ service.interceptors.response.use(
         duration: 3 * 1000,
       });
 
-      return Promise.reject(new Error(`${error}` || 'Error'));
+      store.dispatch('user/signOut');
+      router.push('/login');
     }
 
     return res;

+ 36 - 0
src/utils/index.js

@@ -0,0 +1,36 @@
+/**
+ * @description 生成指定位随机数的函数
+ * @param {number} length 随机数的位数
+ * @returns {string} 随机36进制数
+ */
+export function getRandomNumber(length = 8) {
+  return Math.random()
+    .toString(36)
+    .substring(2, 2 + length);
+}
+
+/**
+ * 下载文件
+ * @param {string} url 文件地址
+ * @param {string} downloadName 文件名称
+ */
+export function downloadFile(url, downloadName) {
+  const a = document.createElement('a');
+  a.href = url;
+  a.download = downloadName;
+  a.click();
+  a.remove();
+}
+
+/**
+ * 柯里化函数(将接受多个参数的函数转换为一系列接受单个参数的函数)
+ * @param {Function} fn 需要柯里化的函数
+ * @returns Function
+ */
+export let curry = (fn) => {
+  if (typeof fn !== 'function') throw new Error('No function provided');
+  return function curriedFn(...args) {
+    if (args.length < fn.length) return (...args2) => curriedFn(...args, ...args2);
+    return fn(...args);
+  };
+};

+ 138 - 11
src/views/book/chapter.vue

@@ -1,9 +1,21 @@
 <template>
   <div class="chapter">
     <div class="chapter-top">
-      <div class="catalogue"></div>
+      <div class="catalogue">
+        <i class="el-icon-arrow-left" @click="goBack"></i>
+        <div class="name">
+          <span>{{ getCatalogueName() }}</span>
+        </div>
+        <el-popover v-model="visibleStatus" placement="bottom" trigger="click">
+          <CatalogueTree :nodes="nodes" @selectNode="selectNode" />
+
+          <span slot="reference" class="pointer"><SvgIcon icon-class="menu" /> 目录</span>
+        </el-popover>
+      </div>
       <div class="operation">
-        <el-button type="primary" class="add"><SvgIcon icon-class="artboard" /> 添加教材内容</el-button>
+        <el-button type="primary" class="add" @click="addCoursewareToBook">
+          <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>
@@ -11,28 +23,135 @@
       </div>
     </div>
 
-    <div class="book-content" @dblclick="enterCourseware">
+    <div
+      v-for="({ courseware_id }, i) in courseware_list"
+      :key="courseware_id"
+      class="book-content"
+      @dblclick="enterCourseware(courseware_id)"
+    >
       <div class="book-content-top">
-        <span class="book-content-title">教材内容</span>
-        <SvgIcon icon-class="delete" />
+        <span class="book-content-title">教材内容 {{ i + 1 }}</span>
+        <SvgIcon icon-class="delete" @click="deleteCourseware(courseware_id)" />
         <SvgIcon icon-class="setup" />
       </div>
-      <div class="tip"><span>双击开始编辑教材内容 1</span></div>
+      <div class="tip">
+        <span>双击开始编辑教材内容 {{ i + 1 }}</span>
+      </div>
     </div>
   </div>
 </template>
 
 <script>
+import { GetBookChapterStruct, GetCoursewareList_Chapter, AddCoursewareToBook, DeleteCourseware } from '@/api/book';
+
+import CatalogueTree from './components/catalogueTree.vue';
+
 export default {
   name: 'ChapterPage',
+  components: {
+    CatalogueTree,
+  },
+  provide() {
+    return {
+      selectNode: this.selectNode,
+    };
+  },
   data() {
+    const { book_id, chapter_id } = this.$route.query;
     return {
-      chapter_id: this.$route.query.id,
+      chapter_id,
+      book_id,
+      visibleStatus: false,
+      curPosition: [],
+      nodes: [],
+      courseware_list: [],
     };
   },
+  created() {
+    GetBookChapterStruct({ book_id: this.book_id, node_deep_mode: 0 }).then(({ nodes }) => {
+      this.nodes = nodes ?? [];
+      this.setCurPosition(this.chapter_id);
+    });
+    this.getCoursewareList_Chapter(this.chapter_id);
+  },
   methods: {
-    enterCourseware() {
-      this.$router.push('/courseware');
+    goBack() {
+      this.$router.push(`/book/setting/${this.book_id}`);
+    },
+    enterCourseware(courseware_id) {
+      this.$router.push({
+        path: `/courseware/create/${courseware_id}`,
+        query: { book_id: this.book_id, chapter_id: this.chapter_id },
+      });
+    },
+    getNodeName(index) {
+      let node = this.nodes;
+      for (let i = 0; i <= index; i++) {
+        node = i === 0 ? node[this.curPosition[i]] : node.nodes[this.curPosition[i]];
+      }
+      return node?.name;
+    },
+    getCatalogueName() {
+      return this.curPosition.map((item, index) => this.getNodeName(index)).join(' / ');
+    },
+    getCoursewareList_Chapter(chapter_id) {
+      GetCoursewareList_Chapter({ chapter_id }).then(({ courseware_list }) => {
+        this.courseware_list = courseware_list ?? [];
+      });
+    },
+    /**
+     * 根据节点id查找节点在nodes中的位置
+     * @param {array} nodes 节点数组
+     * @param {string} id 节点id
+     * @param {array} position 节点位置
+     */
+    findNodeIndexById(nodes, id, position = []) {
+      for (let i = 0; i < nodes.length; i++) {
+        const node = nodes[i];
+        if (node.id === id) {
+          position.push(i);
+          return position;
+        }
+        if (node.nodes && node.nodes.length > 0) {
+          const childPosition = this.findNodeIndexById(node.nodes, id, [...position, i]);
+          if (childPosition.length > 0) {
+            return childPosition;
+          }
+        }
+      }
+      return [];
+    },
+    setCurPosition(id) {
+      this.curPosition = this.findNodeIndexById(this.nodes, id);
+    },
+    selectNode(id) {
+      this.setCurPosition(id);
+      this.getCoursewareList_Chapter(id);
+      this.visibleStatus = false;
+    },
+    addCoursewareToBook() {
+      AddCoursewareToBook({ book_id: this.book_id, chapter_id: this.chapter_id, name: '教材内容' }).then(() => {
+        this.getCoursewareList_Chapter(this.chapter_id);
+        this.$message.success('添加成功');
+      });
+    },
+    deleteCourseware(id) {
+      this.$confirm('是否删除该教材内容?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          DeleteCourseware({ id })
+            .then(() => {
+              this.getCoursewareList_Chapter(id);
+              this.$message.success('删除成功');
+            })
+            .catch(() => {
+              this.$message.error('删除失败');
+            });
+        })
+        .catch(() => {});
     },
   },
 };
@@ -56,13 +175,21 @@ export default {
 
     .catalogue {
       display: flex;
+      column-gap: 8px;
       align-items: center;
-      min-width: 200px;
+      min-width: 240px;
       height: 40px;
-      padding: 4px 12px;
+      padding: 8px 16px;
+      font-size: 14px;
+      font-weight: bold;
       background-color: #fff;
       border-radius: 4px;
       box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 8%);
+
+      .name {
+        flex: 1;
+        margin-right: 6px;
+      }
     }
 
     .operation {

+ 59 - 0
src/views/book/components/catalogueTree.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="node-wrapper">
+    <div v-for="node in nodes" :key="node.id" class="node">
+      <div
+        :class="['node-name', { content: node.is_leaf_chapter === 'true' }]"
+        @click="handleSelectNode(node.is_leaf_chapter, node.id)"
+      >
+        <span>{{ node.name }}</span>
+      </div>
+      <CatalogueTree v-if="node.nodes && node.is_leaf_chapter === 'false'" :nodes="node.nodes" />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'CatalogueTree',
+  inject: ['selectNode'],
+  props: {
+    nodes: {
+      type: Array,
+      required: true,
+    },
+  },
+  data() {
+    return {};
+  },
+  methods: {
+    handleSelectNode(is_leaf_chapter, id) {
+      if (is_leaf_chapter === 'false') return;
+      this.selectNode(id);
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.node {
+  &-name {
+    padding-bottom: 4px;
+
+    &.content {
+      cursor: pointer;
+
+      &:hover {
+        color: #409eff;
+      }
+    }
+
+    &:not(.content) {
+      font-weight: bold;
+    }
+  }
+
+  .node {
+    margin-left: 20px;
+  }
+}
+</style>

+ 96 - 13
src/views/book/courseware/create/index.vue

@@ -21,9 +21,14 @@
     </div>
 
     <div class="create-middle">
-      <div></div>
+      <div class="create-operation">
+        <el-button><SvgIcon icon-class="background-img" />背景图</el-button>
+        <el-button><SvgIcon icon-class="template" />模板</el-button>
+        <el-button><i class="el-icon-close"></i>退出编辑</el-button>
+      </div>
+
       <main ref="canvas" class="canvas">
-        <div class="drag-line" data-row="-1"></div>
+        <span class="drag-line" data-row="-1"></span>
         <div v-for="(row, i) in data.row_list" :key="i" class="row">
           <div
             v-for="(col, j) in row.col_list"
@@ -36,6 +41,7 @@
               gridTemplateRows: col.grid_template_rows,
             }"
           >
+            <span class="drag-vertical-line start" :data-row="i" :data-col="j"></span>
             <component
               :is="componentList[grid.type]"
               v-for="(grid, k) in col.grid_list"
@@ -44,8 +50,9 @@
               :style="{ gridArea: grid.grid_area }"
               @showSetting="showSetting"
             />
+            <span class="drag-vertical-line end" :data-row="i" :data-col="j"></span>
           </div>
-          <div class="drag-line" :data-row="i"></div>
+          <span class="drag-line" :data-row="i"></span>
         </div>
       </main>
     </div>
@@ -59,6 +66,8 @@
 
 <script>
 import { bookTypeOption, componentList, componentSettingList } from '../data/bookType';
+import { getRandomNumber } from '@/utils/index';
+import { SaveCoursewareContent, GetCoursewareContent } from '@/api/book';
 
 export default {
   name: 'CreatePage',
@@ -68,7 +77,12 @@ export default {
     };
   },
   data() {
+    const { book_id, chapter_id } = this.$route.query;
+
     return {
+      courseware_id: this.$route.params.courseware_id,
+      book_id,
+      chapter_id,
       data: {
         id: '',
         background_image_url: '',
@@ -88,6 +102,7 @@ export default {
       componentSettingList,
       bookTypeOption,
       curRow: -2,
+      curCol: -1,
       enterCanvas: false, // 是否进入画布
       // 拖拽状态
       drag: {
@@ -116,9 +131,19 @@ export default {
           item.style.opacity = 0;
         });
         this.curRow = -2;
+        const dragVerticalLineList = document.querySelectorAll('.drag-vertical-line');
+        dragVerticalLineList.forEach((item) => {
+          item.style.opacity = 0;
+        });
+        this.curCol = -1;
       },
     },
   },
+  created() {
+    GetCoursewareContent({ id: this.courseware_id }).then((res) => {
+      this.data = res.data;
+    });
+  },
   mounted() {
     document.addEventListener('mousemove', this.dragMove);
     document.addEventListener('mouseup', this.dragEnd);
@@ -129,7 +154,12 @@ export default {
   },
   methods: {
     back() {
-      this.$router.push('/chapter?id=1');
+      this.$router.push({ path: '/chapter', query: { chapter_id: this.chapter_id, book_id: this.book_id } });
+    },
+    saveCoursewareContent() {
+      SaveCoursewareContent({ id: this.courseware_id, category: 'NEW' }).then(() => {
+        this.$message.success('保存成功');
+      });
     },
     /**
      * 显示设置
@@ -172,15 +202,26 @@ export default {
       this.drag.clientX = clientX;
       this.drag.clientY = clientY;
 
-      let { leftMarginDifference, topMarginDifference, isInsideCanvas } = this.getMarginDifferences();
+      let { isInsideCanvas } = this.getMarginDifferences();
 
       this.enterCanvas = isInsideCanvas;
       if (!isInsideCanvas) return;
 
-      const dragLineList = document.querySelectorAll('.drag-line');
+      this.showRecentLine(clientX, clientY);
+    },
+    /**
+     * 显示最近的线
+     * @param {number} clientX
+     * @param {number} clientY
+     */
+    showRecentLine(clientX, clientY) {
+      const dragLineList = document.querySelectorAll('.drag-line'); // 获取所有的横线
+      const dragVerticalLineList = document.querySelectorAll('.drag-vertical-line'); // 获取所有的竖线
       let minDistance = Infinity;
       let minIndex = -1;
-      dragLineList.forEach((item, index) => {
+      const list = [...dragLineList, ...dragVerticalLineList];
+
+      list.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),
@@ -190,17 +231,20 @@ export default {
           minIndex = index;
         }
       });
-      dragLineList.forEach((item, index) => {
+
+      list.forEach((item, index) => {
         if (index === minIndex) {
-          // 获取 item 中的 data-row
           const row = item.getAttribute('data-row');
           this.curRow = Number(row);
+          const col = item.getAttribute('data-col');
+          if (col) this.curCol = Number(col);
           item.style.opacity = 1;
         } else {
           item.style.opacity = 0;
         }
       });
     },
+
     /**
      * 鼠标松开
      */
@@ -227,20 +271,21 @@ export default {
           num += item.grid_list.length;
         });
       }
+      const id = getRandomNumber(12);
       const letter = String.fromCharCode(65 + num);
 
       return {
         col_list: [
           {
             width: '100%',
-            grid_template_areas: `'${letter}'`,
-            grid_template_columns: 'auto',
+            grid_template_areas: `'start ${letter} end'`,
+            grid_template_columns: '0 auto 0',
             grid_template_rows: 'auto',
             grid_list: [
               {
+                id,
                 grid_area: letter,
                 type: this.curType,
-                id: letter,
               },
             ],
           },
@@ -339,12 +384,31 @@ export default {
     overflow: auto;
     background-color: #ececec;
 
+    .create-operation {
+      display: flex;
+      column-gap: 6px;
+      align-items: center;
+      justify-content: center;
+      margin-bottom: 16px;
+
+      .el-button {
+        height: 40px;
+        font-weight: bold;
+
+        :deep > span {
+          display: flex;
+          column-gap: 8px;
+          align-items: center;
+        }
+      }
+    }
+
     .canvas {
       display: flex;
       flex-direction: column;
       row-gap: 6px;
       width: 100%;
-      min-height: 100%;
+      min-height: calc(100% - 56px);
       padding: 24px;
       background-color: #fff;
       border-radius: 4px;
@@ -367,6 +431,25 @@ export default {
         border-radius: 4px;
         opacity: 0;
       }
+
+      .drag-vertical-line {
+        position: relative;
+        width: 4px;
+        height: 100%;
+        background-color: #379fff;
+        border-radius: 4px;
+        opacity: 0;
+
+        &.start {
+          right: 8px;
+          grid-area: start;
+        }
+
+        &.end {
+          left: 4px;
+          grid-area: end;
+        }
+      }
     }
   }
 

+ 95 - 39
src/views/book/create.vue

@@ -2,10 +2,10 @@
   <div class="create">
     <div class="breadcrumb">
       <ul>
-        <li v-for="(item, i) in breadcrumbList" :key="item">
+        <li v-for="({ name, path }, i) in breadcrumbList" :key="name">
           <span v-if="i !== 0" class="separator">></span>
-          <span class="breadcrumb-name">
-            {{ item }}
+          <span :class="['breadcrumb-name', { pointer: path }]" @click="path ? $router.push(path) : ''">
+            {{ name }}
           </span>
         </li>
       </ul>
@@ -13,7 +13,7 @@
     <div class="basic-info">
       <div class="basic-info-title">基本信息</div>
       <el-form ref="form" :model="form" :rules="formRules" label-width="80px">
-        <el-form-item label="封面" prop="cover_image_url">
+        <el-form-item label="封面" prop="picture_url">
           <el-upload
             class="cover-uploader"
             action="none"
@@ -21,7 +21,7 @@
             :http-request="uploadCover"
             :before-upload="beforeCoverUpload"
           >
-            <img v-if="form.cover_image_url" :src="form.cover_image_url" class="cover" />
+            <img v-if="form.picture_url" :src="form.picture_url" class="cover" />
             <div v-else class="cover-uploader-icon">
               <i class="el-icon-plus"></i>
               <span>点击上传封面</span>
@@ -35,28 +35,34 @@
         <el-form-item label="作者" prop="author">
           <el-input v-model="form.author" placeholder="请输入内容" />
         </el-form-item>
-        <el-form-item label="现价" prop="current_price">
-          <el-input v-model="form.current_price" placeholder="请输入内容">
+        <el-form-item label="现价" prop="price">
+          <el-input v-model="form.price" placeholder="请输入内容">
             <template slot="append">元</template>
           </el-input>
         </el-form-item>
-        <el-form-item label="原价" prop="original_price">
-          <el-input v-model="form.original_price" placeholder="请输入内容">
+        <el-form-item label="原价" prop="price_old">
+          <el-input v-model="form.price_old" placeholder="请输入内容">
             <template slot="append">元</template>
           </el-input>
         </el-form-item>
-        <el-form-item label="标签" prop="tags">
-          <el-input v-model="form.tags" placeholder="请输入内容" />
+        <el-form-item label="标签" prop="label_name_list">
+          <el-select
+            v-model="form.label_name_list"
+            multiple
+            filterable
+            allow-create
+            default-first-option
+            placeholder="请输入内容"
+          />
         </el-form-item>
-        <el-form-item label="分类" prop="classify">
-          <el-select v-model="form.classify" placeholder="选择分类">
-            <el-option label="分类1" value="1" />
-            <el-option label="分类2" value="2" />
+        <el-form-item label="分类" prop="type_id">
+          <el-select v-model="form.type_id" placeholder="选择分类">
+            <el-option v-for="{ id: typeId, name } in type_list" :key="typeId" :label="name" :value="typeId" />
           </el-select>
         </el-form-item>
-        <el-form-item label="简介" prop="brief_introduction">
+        <el-form-item label="简介" prop="description">
           <el-input
-            v-model="form.brief_introduction"
+            v-model="form.description"
             type="textarea"
             :autosize="{ minRows: 12 }"
             :maxlength="500"
@@ -65,7 +71,7 @@
           />
         </el-form-item>
         <el-form-item>
-          <el-button type="primary" @click="confirm">确定</el-button>
+          <el-button type="primary" @click="addBook">确定</el-button>
           <el-button @click="$router.push('/')">取消</el-button>
         </el-form-item>
       </el-form>
@@ -75,49 +81,55 @@
 
 <script>
 import { fileUpload } from '@/api/app';
+import { GetBook, UpdateBook, AddBook, GetBookTypeList } from '@/api/book';
 
 export default {
   name: 'CreateBookPage',
   data() {
     return {
-      id: this.$route.query.id,
+      book_id: this.$route.query.book_id,
       form: {
-        cover_image_url: '',
-        image_id: '',
+        picture_url: '',
+        picture_id: '',
         name: '',
         author: '',
-        current_price: '',
-        original_price: '',
-        tags: '',
-        classify: '',
-        brief_introduction: '',
+        price: '',
+        price_old: '',
+        label_name_list: '',
+        type_id: '',
+        description: '',
       },
       formRules: {
-        cover_image_url: [{ required: true, message: '请上传封面', trigger: 'blur' }],
+        picture_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' }],
+        price: [{ required: true, message: '请输入现价', trigger: 'blur' }],
+        type_id: [{ required: true, message: '请选择分类', trigger: 'blur' }],
+        description: [{ required: true, message: '请输入简介', trigger: 'blur' }],
       },
-      breadcrumbList: ['教材管理', '新建教材', '基本信息'],
+      type_list: [],
+      breadcrumbList: [
+        { name: '教材管理', path: '/home' },
+        { name: '新建教材', path: '' },
+        { name: '基本信息', path: '' },
+      ],
     };
   },
   created() {
-    if (this.id) {
-      // TODO 获取书籍信息
+    if (this.book_id) {
+      this.getBookDetail();
     }
+    GetBookTypeList().then(({ type_list }) => {
+      this.type_list = type_list;
+    });
   },
   methods: {
-    confirm() {
-      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_id } = file_info_list[0];
-          this.form.cover_image_url = file_url;
-          this.form.image_id = file_id;
+          this.form.picture_url = file_url;
+          this.form.picture_id = file_id;
         }
       });
     },
@@ -147,12 +159,51 @@ export default {
             reject();
           }
 
-          if (!isCorrectSize && isJPG && isLt5M) {
+          if (isCorrectSize && isJPG && isLt5M) {
             resolve();
+          } else {
+            reject();
           }
         };
       });
     },
+    addBook() {
+      this.$refs.form.validate((valid) => {
+        if (valid) {
+          if (this.book_id) {
+            this.updateBook();
+          } else {
+            AddBook({ ...this.form, sys_version_type: 1 }).then(({ id }) => {
+              this.$router.push(`/book/setting/${id}`);
+            });
+          }
+        } else {
+          return false;
+        }
+      });
+    },
+    getBookDetail() {
+      GetBook({ id: this.book_id }).then(
+        ({ name, picture_id, picture_url, author, type_id, price, price_old, label_name_list, description }) => {
+          this.form = {
+            name,
+            picture_id,
+            picture_url,
+            author,
+            type_id,
+            price,
+            price_old,
+            label_name_list,
+            description,
+          };
+        },
+      );
+    },
+    updateBook() {
+      UpdateBook({ id: this.book_id, ...this.form }).then(() => {
+        this.$router.push(`/book/setting/${this.book_id}`);
+      });
+    },
   },
 };
 </script>
@@ -213,6 +264,11 @@ export default {
       }
 
       .cover-uploader {
+        :deep img {
+          max-width: 228px;
+          max-height: 320px;
+        }
+
         :deep .el-upload {
           position: relative;
           overflow: hidden;

+ 385 - 55
src/views/book/setting.vue

@@ -3,7 +3,7 @@
     <div class="breadcrumb">
       <ul>
         <li>
-          <span>教材管理</span>
+          <span @click="$router.push('/')">教材管理</span>
         </li>
         <li>
           <span class="separator">></span>
@@ -18,16 +18,17 @@
           <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">
+        <el-image :src="data.picture_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 class="brief-introduction">{{ data.description }}</div>
       </div>
 
-      <div class="catalogue-wrapper">
+      <div class="catalogue-wrapper" :style="{ backgroundColor: isEdit ? 'inherit' : '#fff' }">
+        <!-- 显示区域 -->
         <template v-if="!isEdit">
           <div class="catalogue-top">
             <div class="title">目录</div>
@@ -38,88 +39,323 @@
             </div>
           </div>
 
-          <div v-for="{ id, name, children } in catalogueList" :key="id" class="catalogue">
+          <div v-for="{ id, name, nodes: children } in nodes" :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']">
+              <div
+                :key="item.id"
+                :class="['catalogue-item', item.is_leaf_chapter === 'true' ? 'content' : 'subdirectory']"
+              >
                 <span class="name">{{ item.name }}</span>
                 <span class="time">{{ item.time }} {{ item.editor }}</span>
-                <span class="edit" @click="editBookContent(item.id)">编辑</span>
+                <span
+                  v-if="item.is_leaf === 'true' && item.is_leaf_chapter === 'true'"
+                  class="edit"
+                  @click="editBookContent(item.id)"
+                  >编辑</span
+                >
               </div>
-              <div v-for="li in item.children" :key="li.id">
+              <div v-for="li in item.nodes" :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>
+                  <span class="name">{{ li?.name }}</span>
+                  <span class="time">{{ li?.time }} {{ li.editor }}</span>
+                  <span v-if="li.is_leaf_chapter === 'true'" class="edit" @click="editBookContent(li.id)">编辑</span>
                 </div>
               </div>
             </template>
           </div>
         </template>
-        <template v-else></template>
+        <!-- 编辑区域 -->
+        <div v-else class="catalogue-edit">
+          <div class="catalogue-edit-top">
+            <span class="title">编辑目录</span>
+            <div class="operation">
+              <el-button class="cancel" @click="isEdit = false">取消</el-button>
+              <el-button type="primary" @click="isEdit = false">完成</el-button>
+            </div>
+          </div>
+
+          <div class="nodes">
+            <template v-for="item in nodes">
+              <!-- 一级目录 -->
+              <div v-if="item.id === curEditNodeId" :key="item.id" class="nodes-edit">
+                <el-input v-model="item.name" placeholder="请输入标题" />
+                <el-button type="primary" @click="confirmEditNode(item.id, '', 'false')">确定</el-button>
+                <el-button @click="cancelEditNode(item.id)">取消</el-button>
+              </div>
+              <div v-else :key="item.id" class="nodes-item">
+                <span class="title">{{ item.name }}</span>
+                <span class="operation">
+                  <el-dropdown trigger="click">
+                    <span class="el-dropdown-link">
+                      <SvgIcon icon-class="add-rectangle" />
+                    </span>
+                    <el-dropdown-menu slot="dropdown">
+                      <el-dropdown-item @click.native="createChildCatalogue(item.id, 'false')">子目录</el-dropdown-item>
+                      <el-dropdown-item @click.native="createChildCatalogue(item.id, 'true')">内容</el-dropdown-item>
+                    </el-dropdown-menu>
+                  </el-dropdown>
+                  <SvgIcon icon-class="edit" @click="setCurEditNodeId(item.id)" />
+                  <SvgIcon icon-class="delete-2" @click="deleteChapter(item.id)" />
+                </span>
+              </div>
+
+              <!-- 子目录及内容 -->
+              <template v-if="item.is_leaf_chapter === 'false' && item.nodes?.length > 0">
+                <template v-for="li in item.nodes">
+                  <div v-if="li.id === curEditNodeId" :key="li.id" class="nodes-edit">
+                    <el-input v-model="li.name" placeholder="请输入标题" />
+                    <el-button type="primary" @click="confirmEditNode(li.id, item.id, li.is_leaf_chapter)">
+                      确定
+                    </el-button>
+                    <el-button @click="cancelEditNode(li.id)">取消</el-button>
+                  </div>
+                  <div
+                    v-else
+                    :key="li.id"
+                    :class="['nodes-item', 'children', li.is_leaf_chapter === 'true' ? '' : 'subdirectory']"
+                  >
+                    <span class="name">{{ li.name }}</span>
+                    <span class="operation">
+                      <el-dropdown trigger="click">
+                        <span class="el-dropdown-link">
+                          <SvgIcon icon-class="add-rectangle" />
+                        </span>
+                        <el-dropdown-menu slot="dropdown">
+                          <el-dropdown-item
+                            v-if="li.is_leaf_chapter === 'true'"
+                            @click.native="createChildCatalogue(item.id, 'false')"
+                            >子目录</el-dropdown-item
+                          >
+                          <el-dropdown-item
+                            @click.native="
+                              createChildCatalogue(li.is_leaf_chapter === 'true' ? item.id : li.id, 'true')
+                            "
+                            >内容</el-dropdown-item
+                          >
+                        </el-dropdown-menu>
+                      </el-dropdown>
+                      <SvgIcon icon-class="edit" @click="setCurEditNodeId(li.id)" />
+                      <SvgIcon icon-class="delete-2" @click="deleteChapter(li.id)" />
+                    </span>
+                  </div>
+
+                  <template v-if="li.is_leaf_chapter === 'false' && li.nodes?.length > 0">
+                    <template v-for="child in li.nodes">
+                      <div v-if="child.id === curEditNodeId" :key="child.id" class="nodes-edit">
+                        <el-input v-model="child.name" placeholder="请输入标题" />
+                        <el-button type="primary" @click="confirmEditNode(child.id, li.id, child.is_leaf_chapter)">
+                          确定
+                        </el-button>
+                        <el-button @click="cancelEditNode(child.id)">取消</el-button>
+                      </div>
+                      <div v-else :key="child.id" :class="['nodes-item', 'children']" :style="{ paddingLeft: '32px' }">
+                        <span class="name">{{ child.name }}</span>
+                        <span class="operation">
+                          <el-dropdown trigger="click">
+                            <span class="el-dropdown-link">
+                              <SvgIcon icon-class="add-rectangle" />
+                            </span>
+                            <el-dropdown-menu slot="dropdown">
+                              <el-dropdown-item
+                                @click.native="
+                                  createChildCatalogue(child.is_leaf_chapter === 'true' ? li.id : child.id, 'true')
+                                "
+                                >内容</el-dropdown-item
+                              >
+                            </el-dropdown-menu>
+                          </el-dropdown>
+                          <SvgIcon icon-class="edit" @click="setCurEditNodeId(child.id)" />
+                          <SvgIcon icon-class="delete-2" @click="deleteChapter(child.id)" />
+                        </span>
+                      </div>
+                    </template>
+                  </template>
+                </template>
+              </template>
+            </template>
+          </div>
+
+          <el-button type="primary" @click="createCatalogue"><SvgIcon icon-class="add-rectangle" /> 新建目录</el-button>
+        </div>
       </div>
     </main>
   </div>
 </template>
 
 <script>
+import { GetBook, GetBookChapterStruct, AddChapterToBook, UpdateChapter, DeleteChapter } from '@/api/book';
+
 export default {
   name: 'SettingPage',
   data() {
     return {
-      book_id: this.$route.params.id,
+      book_id: this.$route.params.book_id,
       isEdit: false, // 是否编辑状态
+      curEditNodeId: '', // 当前编辑的节点id
       data: {
-        cover_image_url: '',
-        image_id: '',
-        name: '新实用汉语',
+        picture_url: '',
+        picture_id: '',
+        name: '',
         author: '',
         current_price: '',
         original_price: '',
-        tags: '',
-        classify: '',
-        brief_introduction: '123',
+        label_name_list: '',
+        type_id: '',
+        description: '',
       },
-      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: [],
-            },
-          ],
-        },
-      ],
+      nodes: [],
     };
   },
+  created() {
+    GetBook({ id: this.book_id }).then(({ name, picture_id, picture_url, author, type_id, price, description }) => {
+      this.data = {
+        name,
+        picture_id,
+        picture_url,
+        author,
+        type_id,
+        current_price: price,
+        original_price: '0.00',
+        label_name_list: [],
+        description,
+      };
+    });
+    this.getBookChapterStruct();
+  },
   methods: {
     edit() {
       this.$router.push({ path: '/book/create', query: { id: this.book_id } });
     },
-    editBookContent(id) {
-      this.$router.push({ path: '/chapter', query: { id } });
+    editBookContent(chapter_id) {
+      this.$router.push({ path: '/chapter', query: { chapter_id, book_id: this.book_id } });
+    },
+    getBookChapterStruct() {
+      GetBookChapterStruct({ book_id: this.book_id, node_deep_mode: 0 }).then(({ nodes }) => {
+        this.nodes = nodes ?? [];
+      });
+    },
+    confirmEditNode(id, parent_id, is_leaf_chapter) {
+      const position = this.findNodeIndexById(this.nodes, id);
+      let nodes = this.nodes;
+      for (let i = 0; i < position.length - 1; i++) {
+        nodes = nodes[position[i]].nodes;
+      }
+      const name = nodes[position[position.length - 1]].name;
+      if (name.length <= 0) {
+        this.$message.error('请输入名称');
+        return;
+      }
+      if (nodes[position[position.length - 1]].temporary) {
+        this.addChapterToBook(name, parent_id, is_leaf_chapter);
+      } else {
+        this.updateChapter(id, name);
+      }
+    },
+    addChapterToBook(name, parent_id, is_leaf) {
+      AddChapterToBook({ name, book_id: this.book_id, parent_id, is_leaf }).then(() => {
+        this.getBookChapterStruct();
+      });
+    },
+    updateChapter(id, name) {
+      UpdateChapter({ id, name }).then(() => {
+        this.getBookChapterStruct();
+        this.curEditNodeId = '';
+      });
+    },
+    deleteChapter(id) {
+      this.$confirm('确定删除该目录吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          DeleteChapter({ id, is_force_delete: 'true' }).then(() => {
+            this.$message.success('删除成功');
+            this.getBookChapterStruct();
+          });
+        })
+        .catch(() => {});
+    },
+    createCatalogue() {
+      let id = new Date().getTime();
+      const level_index = this.nodes.length;
+      this.nodes.push({
+        id,
+        name: '',
+        is_leaf_chapter: 'false',
+        level_index,
+        temporary: true, // 临时节点
+        nodes: [],
+      });
+      this.curEditNodeId = id;
+    },
+    createChildCatalogue(parent_id, is_leaf_chapter) {
+      let id = new Date().getTime();
+      let position = this.findNodeIndexById(this.nodes, parent_id);
+
+      let parent = this.nodes;
+      for (let i = 0; i < position.length; i++) {
+        if (i === position.length - 1) {
+          parent = parent[position[i]];
+        } else {
+          parent = parent[position[i]].nodes;
+        }
+      }
+      const level_index = position.join('-');
+      if (!Object.hasOwn(parent, 'nodes')) {
+        this.$set(parent, 'nodes', []);
+      }
+      parent.nodes.push({
+        id,
+        name: '',
+        is_leaf_chapter,
+        level_index,
+        temporary: true, // 临时节点
+        nodes: [],
+      });
+      this.curEditNodeId = id;
+    },
+    setCurEditNodeId(id) {
+      this.curEditNodeId = id;
+    },
+    /**
+     * 根据节点id查找节点在nodes中的位置
+     * @param {array} nodes 节点数组
+     * @param {string} id 节点id
+     * @param {array} position 节点位置
+     */
+    findNodeIndexById(nodes, id, position = []) {
+      for (let i = 0; i < nodes.length; i++) {
+        const node = nodes[i];
+        if (node.id === id) {
+          position.push(i);
+          return position;
+        }
+        if (node.nodes && node.nodes.length > 0) {
+          const childPosition = this.findNodeIndexById(node.nodes, id, [...position, i]);
+          if (childPosition.length > 0) {
+            return childPosition;
+          }
+        }
+      }
+      return [];
+    },
+    /**
+     * 取消编辑节点
+     * @param {string} id 节点id
+     */
+    cancelEditNode(id) {
+      const indexArr = this.findNodeIndexById(this.nodes, id);
+      let nodes = this.nodes;
+      for (let i = 0; i < indexArr.length - 1; i++) {
+        nodes = nodes[indexArr[i]].nodes;
+      }
+      // 删除临时节点
+      if (nodes[indexArr[indexArr.length - 1]].temporary) {
+        nodes.splice(indexArr[indexArr.length - 1], 1);
+      }
+      this.curEditNodeId = '';
     },
   },
 };
@@ -233,7 +469,6 @@ export default {
       row-gap: 16px;
       width: 916px;
       padding: 24px;
-      background-color: #fff;
       border-radius: 4px;
 
       .catalogue-top {
@@ -271,10 +506,16 @@ export default {
           font-size: 14px;
           border-bottom: 1px solid #ebebeb;
 
-          &.subdirectory {
+          &:hover {
             background-color: #f3f3f3;
           }
 
+          &.subdirectory {
+            .name {
+              font-weight: bold;
+            }
+          }
+
           &.children {
             padding-left: 32px;
           }
@@ -295,6 +536,95 @@ export default {
           }
         }
       }
+
+      .catalogue-edit {
+        display: flex;
+        flex-direction: column;
+        row-gap: 16px;
+
+        &-top {
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+
+          .title {
+            font-weight: bold;
+            color: #000;
+          }
+
+          .operation {
+            .cancel {
+              background-color: #e7e7e7;
+            }
+
+            .el-button + .el-button {
+              margin-left: 16px;
+            }
+          }
+        }
+
+        .nodes {
+          display: flex;
+          flex-direction: column;
+          row-gap: 8px;
+
+          &-edit {
+            display: flex;
+            column-gap: 8px;
+
+            .el-button + .el-button {
+              margin-left: 0;
+            }
+
+            .el-input {
+              :deep .el-input__inner {
+                background-color: #fff;
+              }
+            }
+          }
+
+          &-item {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            padding-right: 16px;
+
+            .title {
+              flex: 1;
+              font-weight: bold;
+              color: #000;
+            }
+
+            &.children {
+              padding: 8px 16px;
+              background-color: #fff;
+
+              &:hover {
+                background-color: #f3f3f3;
+              }
+
+              .name {
+                font-size: 14px;
+              }
+
+              &.subdirectory {
+                .name {
+                  font-weight: bold;
+                }
+              }
+            }
+
+            .operation {
+              display: flex;
+              column-gap: 8px;
+
+              .svg-icon {
+                cursor: pointer;
+              }
+            }
+          }
+        }
+      }
     }
   }
 }

+ 49 - 9
src/views/home/components/ListingDialog.vue

@@ -3,16 +3,24 @@
     <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 v-model="publish_scope">
+          <el-radio v-for="{ label, value } in publishList" :key="value" :label="value">{{ label }}</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
+          v-model="org_id_list"
+          multiple
+          placeholder="选择机构"
+          :disabled="publish_scope === publishList[0].value"
+        >
+          <el-option
+            v-for="{ id: org_id, name: org_name } in org_list"
+            :key="org_id"
+            :label="org_name"
+            :value="org_id"
+          />
         </el-select>
       </div>
     </div>
@@ -25,6 +33,8 @@
 </template>
 
 <script>
+import { SetPublishStatusForBook, PageQueryOrgIndexList_OpenQuery } from '@/api/book';
+
 export default {
   name: 'ListingDialog',
   props: {
@@ -39,16 +49,46 @@ export default {
   },
   data() {
     return {
-      visibleRange: 'all', // 可见范围
-      authorizedAgency: [], // 授权机构
+      publish_scope: 1, // 可见范围
+      publishList: [
+        { label: '全部机构可见', value: 1 },
+        { label: '仅授权机构可见', value: 0 },
+      ],
+      cur_page: 1,
+      org_list: [], // 机构列表
+      org_id_list: [], // 授权机构
     };
   },
+  created() {
+    this.pageQueryOrgIndexList_OpenQuery();
+  },
   methods: {
     dialogClose() {
       this.$emit('update:visible', false);
     },
     // 上架
-    listing() {},
+    listing() {
+      SetPublishStatusForBook({
+        book_id: this.id,
+        publish_status: 1,
+        publish_scope: this.publish_scope,
+        org_id_list: this.org_id_list,
+      }).then(() => {
+        this.dialogClose();
+        this.$emit('PublishSuccess');
+      });
+    },
+    pageQueryOrgIndexList_OpenQuery() {
+      PageQueryOrgIndexList_OpenQuery({ name: '', page_capacity: 50, cur_page: this.cur_page }).then(
+        ({ org_list, total_count, cur_page_end_index }) => {
+          this.org_list.push(...org_list);
+          if (cur_page_end_index < total_count) {
+            this.cur_page += 1;
+            this.pageQueryOrgIndexList_OpenQuery();
+          }
+        },
+      );
+    },
   },
 };
 </script>

+ 139 - 26
src/views/home/index.vue

@@ -7,10 +7,10 @@
 
     <div class="home-tabs">
       <span
-        v-for="{ label, value } in tabList"
+        v-for="{ label, value } in publishStatusList"
         :key="value"
-        :class="['tab-item', value === tab ? 'active' : '']"
-        @click="tab = value"
+        :class="['tab-item', value === publish_status ? 'active' : '']"
+        @click="publish_status = value"
       >
         {{ label }}
       </span>
@@ -18,26 +18,47 @@
 
     <div class="search">
       <span class="search-name">搜索</span>
-      <el-input v-model="search" placeholder="全部" suffix-icon="el-icon-search" />
+      <el-input
+        v-model="name"
+        placeholder="全部"
+        suffix-icon="el-icon-search"
+        @keyup.enter.native="pageQueryBookList"
+      />
       <span class="search-name">创建者</span>
-      <el-select v-model="creator" placeholder="全部">
-        <el-option label="张三" value="1" />
+      <el-select v-model="creator_id" placeholder="全部">
+        <el-option
+          v-for="{ user_id, user_real_name } in user_list"
+          :key="user_id"
+          :label="user_real_name"
+          :value="user_id"
+        />
       </el-select>
     </div>
 
     <el-table :data="data" height="100%">
-      <el-table-column prop="number" label="序号" width="64" />
+      <el-table-column label="序号" width="64">
+        <template slot-scope="{ $index }">
+          <span>{{ $index + 1 }}</span>
+        </template>
+      </el-table-column>
       <el-table-column prop="name" label="教材名称" width="248" />
-      <el-table-column prop="tags" label="标签" width="124" />
-      <el-table-column prop="creator" label="创建者" width="160" />
-      <el-table-column prop="create_time" label="创建时间" width="120" />
-      <el-table-column prop="recently_edited" label="最近编辑" width="100" />
-      <el-table-column prop="recently_edited_time" label="最近编辑时间" width="120" />
-      <el-table-column prop="memo" label="简介" />
+      <el-table-column prop="label_name_list_desc" label="标签" width="124" />
+      <el-table-column prop="creator_name" label="创建者" width="160" />
+      <el-table-column prop="create_time" label="创建时间" width="180" />
+      <el-table-column prop="last_modifier_name" label="最近编辑" width="180" />
+      <el-table-column prop="last_modify_time" label="最近编辑时间" width="180" />
+      <el-table-column prop="description" label="简介" />
       <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="listingBook(row.id)">上架</span>
+          <span
+            :class="['link', { danger: row.publish_status === publishStatusList[0].value }]"
+            @click="
+              row.publish_status === publishStatusList[0].value ? setUnpublishForBook(row.id) : listingBook(row.id)
+            "
+          >
+            {{ row.publish_status === publishStatusList[0].value ? '下架' : '上架' }}
+          </span>
           <span class="link danger" @click="deleteBook(row.id)">删除</span>
         </template>
       </el-table-column>
@@ -48,20 +69,22 @@
       :page-sizes="[10, 20, 30, 40, 50]"
       :page-size="page_capacity"
       layout="total, prev, pager, next, sizes, jumper"
-      :total="total"
+      :total="total_count"
       @prev-click="changePage"
       @next-click="changePage"
       @current-change="changePage"
       @size-change="changePageSize"
     />
 
-    <ListingDialog :id="curId" :visible.sync="dialogVisible" />
+    <ListingDialog :id="curId" :visible.sync="dialogVisible" @PublishSuccess="publishSuccess" />
   </div>
 </template>
 
 <script>
 import ListingDialog from './components/ListingDialog.vue';
 
+import { PageQueryBookList, GetBookCreatorList, SetPublishStatusForBook, DeleteBook } from '@/api/book';
+
 export default {
   name: 'HomePage',
   components: {
@@ -70,26 +93,40 @@ export default {
   data() {
     return {
       data: undefined,
-      search: '',
-      creator: '',
+      name: '',
+      creator_id: '',
       cur_page: 1,
       page_capacity: 10,
-      total: 0,
-      tab: 'published',
-      tabList: [
+      total_count: 0,
+      publish_status: 1,
+      user_list: [],
+      publishStatusList: [
         {
           label: '已上架',
-          value: 'published',
+          value: 1,
         },
         {
           label: '草稿',
-          value: 'draft',
+          value: 0,
         },
       ],
       curId: '',
       dialogVisible: false,
     };
   },
+  watch: {
+    publish_status: {
+      handler() {
+        this.pageQueryBookList();
+      },
+    },
+  },
+  created() {
+    GetBookCreatorList().then(({ user_list }) => {
+      this.user_list = user_list;
+    });
+    this.pageQueryBookList();
+  },
   methods: {
     createBook() {
       this.$router.push('/book/create');
@@ -100,11 +137,27 @@ export default {
     changePageSize(size) {
       this.page_capacity = size;
     },
+    // 分页查询教材列表
+    pageQueryBookList() {
+      PageQueryBookList({
+        name: this.name,
+        creator_id: this.creator_id,
+        cur_page: this.cur_page,
+        page_capacity: this.page_capacity,
+        sys_version_type: 1,
+        publish_status: this.publish_status,
+      }).then(({ book_list, total_count }) => {
+        this.data = book_list;
+        this.total_count = total_count;
+      });
+    },
     /**
      * 编辑教材
-     * @param {string} id 教材id
+     * @param {string} book_id 教材id
      */
-    editBook(id) {},
+    editBook(book_id) {
+      this.$router.push(`/book/create?book_id=${book_id}`);
+    },
     /**
      * 上架教材
      * @param {string} id 教材id
@@ -113,11 +166,71 @@ export default {
       this.dialogVisible = true;
       this.curId = id;
     },
+    // 下架教材
+    setUnpublishForBook(book_id) {
+      SetPublishStatusForBook({ book_id, publish_status: 0, publish_scope: 0 }).then(() => {
+        this.pageQueryBookList();
+      });
+    },
+    /**
+     * 上架成功事件
+     */
+    publishSuccess() {
+      this.pageQueryBookList();
+
+      const div = document.createElement('div');
+      div.style.cssText = `
+        width: 227px;
+        height: 212px;
+        border-radius: 12px;
+        background-color: #4ABB38;
+        position: fixed;
+        top: 20%;
+        left: calc(50% - 113px);
+        display: flex;
+        flex-direction: column;
+        row-gap: 32px;
+        justify-content: center;
+        align-items: center;
+        color: #fff;
+      `;
+
+      const check = document.createElement('div');
+      check.style.cssText = `
+        width: 50px;
+        height: 25px;
+        border-bottom: 4px solid #fff;
+        border-left: 4px solid #fff;
+        transform: rotate(-45deg);
+      `;
+      div.appendChild(check);
+
+      const span = document.createElement('span');
+      span.innerHTML = '上架成功';
+      div.appendChild(span);
+      document.body.appendChild(div);
+
+      setTimeout(() => {
+        document.body.removeChild(div);
+      }, 1000);
+    },
     /**
      * 删除教材
      * @param {string} id 教材id
      */
-    deleteBook(id) {},
+    deleteBook(id) {
+      this.$confirm('是否删除该教材?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      })
+        .then(() => {
+          DeleteBook({ book_id: id, is_force_delete: 'true' }).then(() => {
+            this.pageQueryBookList();
+          });
+        })
+        .catch(() => {});
+    },
   },
 };
 </script>