Quellcode durchsuchen

新版预览、编辑

dusenyao vor 9 Monaten
Ursprung
Commit
1666f39d64
47 geänderte Dateien mit 2507 neuen und 259 gelöschten Zeilen
  1. 3 0
      .env
  2. 2 0
      .env.development
  3. 2 0
      .env.production
  4. 2 2
      package.json
  5. 2 9
      src/api/app.js
  6. 117 0
      src/api/book.js
  7. 21 0
      src/api/list.js
  8. 55 0
      src/api/project.js
  9. 3 1
      src/api/user.js
  10. 52 0
      src/components/PaginationPage.vue
  11. 37 18
      src/layouts/default/header/index.vue
  12. 2 2
      src/router/guard/index.js
  13. 1 1
      src/router/modules/basic.js
  14. 10 1
      src/router/modules/index.js
  15. 144 0
      src/router/modules/project.js
  16. 2 1
      src/store/modules/user.js
  17. 14 0
      src/styles/mixin.scss
  18. 87 105
      src/views/book/courseware/create/components/CreateCanvas.vue
  19. 12 8
      src/views/book/courseware/create/components/PreviewEdit.vue
  20. 17 15
      src/views/book/courseware/create/components/common/ModuleMixin.js
  21. 3 3
      src/views/book/courseware/create/components/question/article/ArticleSetting.vue
  22. 34 0
      src/views/book/courseware/create/components/question/input/Input.vue
  23. 94 0
      src/views/book/courseware/create/components/question/input/InputSetting.vue
  24. 2 2
      src/views/book/courseware/create/components/question/math/MathSetting.vue
  25. 10 69
      src/views/book/courseware/create/index.vue
  26. 11 0
      src/views/book/courseware/data/bookType.js
  27. 90 0
      src/views/book/courseware/data/input.js
  28. 2 2
      src/views/book/courseware/data/math.js
  29. 3 3
      src/views/book/courseware/preview/components/character/CharacterPreview.vue
  30. 8 16
      src/views/book/courseware/preview/components/common/PreviewMixin.js
  31. 60 0
      src/views/book/courseware/preview/components/input/InputPreview.vue
  32. 227 0
      src/views/create_project/createProject.vue
  33. 47 0
      src/views/create_project/createSuccess.vue
  34. 52 0
      src/views/create_project/startCreate.vue
  35. 4 1
      src/views/login/index.vue
  36. 21 0
      src/views/personal_center/index.vue
  37. 28 0
      src/views/personal_workbench/check_task/index.vue
  38. 99 0
      src/views/personal_workbench/edit_task/edit/index.vue
  39. 199 0
      src/views/personal_workbench/edit_task/index.vue
  40. 142 0
      src/views/personal_workbench/edit_task/preview/index.vue
  41. 86 0
      src/views/personal_workbench/menu.vue
  42. 77 0
      src/views/personal_workbench/project/AddChapter.vue
  43. 453 0
      src/views/personal_workbench/project/ProductionEditorialManage.vue
  44. 53 0
      src/views/personal_workbench/project/SetProducer.vue
  45. 84 0
      src/views/personal_workbench/project/index.vue
  46. 21 0
      src/views/project_manage/index.vue
  47. 12 0
      vue.config.js

+ 3 - 0
.env

@@ -6,3 +6,6 @@ VUE_APP_LearnWebSI = '/GCLSLearnWebSI/ServiceInterface'
 
 # BookWebSI
 VUE_APP_BookWebSI = '/GCLSBookWebSI/ServiceInterface'
+
+#eepServer
+VUE_APP_EepServer = '/EEPServer/SI'

+ 2 - 0
.env.development

@@ -2,3 +2,5 @@
 VUE_APP_BASE_API = '/api'
 
 VUE_APP_FILE = '/file'
+
+VUE_APP_EEP = '/eep'

+ 2 - 0
.env.production

@@ -2,3 +2,5 @@
 VUE_APP_BASE_API = 'https://gcls.helxsoft.cn/'
 
 VUE_APP_FILE = 'https://file-kf.helxsoft.cn/'
+
+VUE_APP_EEP = 'http://eep.helxsoft.cn/'

+ 2 - 2
package.json

@@ -73,13 +73,13 @@
   "build": {
     "appId": "com.gcls.page.textbook",
     "productName": "智慧梧桐桌面端互动教材编辑器",
-    "copyright": "Copyright © 2024 ${author}",
+    "copyright": "Copyright © 2025 ${author}",
     "directories": {
       "output": "out"
     },
     "asar": true,
     "win": {
-      "target": "portable",
+      "target": "nsis",
       "icon": "./public/icon.png",
       "artifactName": "${productName}-Setup.${ext}"
     },

+ 2 - 9
src/api/app.js

@@ -32,13 +32,6 @@ export function GetFileStoreInfo(data) {
 }
 
 /**
- * 得到用户能进入的子系统列表(电脑端)
- */
-export function GetChildSysList_CanEnter_PC(data) {
-  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=login_control-GetChildSysList_CanEnter_PC`, data);
-}
-
-/**
  * 解开访问令牌
  * @param {object} data 请求数据
  * @param {string} data.access_token 访问令牌
@@ -109,6 +102,6 @@ export function GetStaticResources(MethodName, data) {
   return http.post(`/GCLSFileServer/ServiceInterface?MethodName=${MethodName}`, data);
 }
 
-export function GetBookWebSIContent (MethodName, data) {
+export function GetBookWebSIContent(MethodName, data) {
   return http.post(`/GCLSBookWebSI/ServiceInterface?MethodName=${MethodName}`, data);
-}
+}

+ 117 - 0
src/api/book.js

@@ -174,3 +174,120 @@ export function DeleteCourseware(data) {
 export function CrateParsedTextInfo_Pinyin(data) {
   return http.post(`/TeachingServer/TextAnalyser/CrateParsedTextInfo_Pinyin`, data);
 }
+
+/**
+ * @description 得到教材章节结构
+ * @param {object} data
+ */
+export function ChapterGetBookChapterStruct(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_chapter_manager-GetBookChapterStruct`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}
+
+/**
+ * @description 添加章节
+ * @param {object} data
+ */
+export function ChapterAddChapterToBook(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_chapter_manager-AddChapterToBook`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}
+
+/**
+ * @description 添加课件
+ * @param {object} data
+ */
+export function ChapterAddCoursewareToBook(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_chapter_manager-AddCoursewareToBook`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}
+/**
+ * @description 修改章节
+ * @param {object} data
+ */
+export function ChapterUpdateChapter(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_chapter_manager-UpdateChapter`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}
+
+/**
+ * @description 删除章节
+ * @param {object} data
+ */
+export function ChapterDeleteChapter(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_chapter_manager-DeleteChapter`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}
+
+/**
+ * @description 设置制作人
+ * @param {object} data
+ */
+export function ChapterSetProducer(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_chapter_manager-SetProducer`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}
+
+/**
+ * @description 保存课件内容
+ * @param {object} data
+ */
+export function ContentSaveCoursewareContent(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_content_manager-SaveCoursewareContent`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}
+
+/**
+ * @description 保存课件组件内容
+ * @param {object} data
+ */
+export function ContentSaveCoursewareComponentContent(data) {
+  return http.post(
+    `${process.env.VUE_APP_EepServer}?MethodName=book_content_manager-SaveCoursewareComponentContent`,
+    data,
+    {
+      baseURL: process.env.VUE_APP_EEP,
+    },
+  );
+}
+
+/**
+ * @description 得到课件内容
+ * @param {object} data
+ */
+export function ContentGetCoursewareContent(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_content_manager-GetCoursewareContent`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}
+
+/**
+ * @description 得到课件组件内容
+ * @param {object} data
+ */
+export function ContentGetCoursewareComponentContent(data) {
+  return http.post(
+    `${process.env.VUE_APP_EepServer}?MethodName=book_content_manager-GetCoursewareComponentContent`,
+    data,
+    {
+      baseURL: process.env.VUE_APP_EEP,
+    },
+  );
+}
+
+/**
+ * @description 得到课件内容(展示内容)
+ * @param {object} data
+ */
+export function ContentGetCoursewareContent_View(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=book_content_manager-GetCoursewareContent_View`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}

+ 21 - 0
src/api/list.js

@@ -0,0 +1,21 @@
+import { http } from '@/utils/http';
+
+/**
+ * @description 分页查询我的项目列表
+ * @param {object} data
+ */
+export function PageQueryMyProjectList_Leader(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=page_query-PageQueryMyProjectList_Leader`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}
+
+/**
+ * @description 分页查询我的项目列表(制作人)
+ * @param {object} data
+ */
+export function PageQueryMyProjectList_Producer(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=page_query-PageQueryMyProjectList_Producer`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}

+ 55 - 0
src/api/project.js

@@ -0,0 +1,55 @@
+import { http } from '@/utils/http';
+
+/**
+ * @description 创建项目
+ * @param {object} data
+ */
+export function CreateProject(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=project_manager-CreateProject`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}
+
+/**
+ * @description 得到项目基本信息
+ * @param {object} data
+ */
+export function GetProjectBaseInfo(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=project_manager-GetProjectBaseInfo`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}
+
+/**
+ * @description 得到项目信息
+ * @param {object} data
+ */
+export function GetProjectInfo(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=project_manager-GetProjectInfo`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}
+
+/**
+ * @description 得到我的教材课件任务列表
+ * @param {object} data
+ */
+export function GetMyBookCoursewareTaskList(data) {
+  return http.post(
+    `${process.env.VUE_APP_EepServer}?MethodName=project_task_manager-GetMyBookCoursewareTaskList`,
+    data,
+    {
+      baseURL: process.env.VUE_APP_EEP,
+    },
+  );
+}
+
+/**
+ * @description 得到教材课件信息
+ * @param {object} data
+ */
+export function GetBookCoursewareInfo(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=project_task_manager-GetBookCoursewareInfo`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}

+ 3 - 1
src/api/user.js

@@ -5,7 +5,9 @@ import { http } from '@/utils/http';
  * @param {object} data
  */
 export function login(data) {
-  return http.post(`${process.env.VUE_APP_FileServer}?MethodName=login_control-Login`, data);
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=login_control-Login`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
 }
 
 /**

+ 52 - 0
src/components/PaginationPage.vue

@@ -0,0 +1,52 @@
+<template>
+  <el-pagination
+    background
+    :current-page="cur_page"
+    :page-sizes="[10, 20, 30, 40, 50]"
+    :page-size="page_capacity"
+    layout="total, prev, pager, next, sizes, jumper"
+    :total="total"
+    @prev-click="changePage"
+    @next-click="changePage"
+    @current-change="changePage"
+    @size-change="changePageSize"
+  />
+</template>
+
+<script>
+export default {
+  name: 'PaginationPage',
+  props: {
+    pageSize: {
+      type: Number,
+      default: 10,
+    },
+    total: {
+      type: Number,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      cur_page: 1,
+      page_capacity: this.pageSize,
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    changePage(number) {
+      this.cur_page = number;
+      this.getList();
+    },
+    changePageSize(size) {
+      this.page_capacity = size;
+      this.getList();
+    },
+    getList() {
+      this.$emit('getList', { cur_page: this.cur_page, page_capacity: this.page_capacity });
+    },
+  },
+};
+</script>

+ 37 - 18
src/layouts/default/header/index.vue

@@ -1,6 +1,19 @@
 <template>
   <header class="header">
-    <el-image class="logo" :src="$store.state.app.config.logo_image_url" />
+    <div class="logo">
+      <el-image class="logo-image" :src="$store.state.app.config.logo_image_url" />
+    </div>
+
+    <ul class="header-list">
+      <li
+        v-for="(item, index) in projectList"
+        :key="index"
+        :class="{ active: LoginNavIndex === index }"
+        @click="changeProject(item.key)"
+      >
+        {{ item.name }}
+      </li>
+    </ul>
 
     <div v-if="!token" class="selectLoginOrRegistration">
       <span>登录</span>
@@ -25,7 +38,6 @@
 </template>
 
 <script>
-import { GetChildSysList_CanEnter_PC } from '@/api/app';
 import { getToken } from '@/utils/auth';
 
 export default {
@@ -35,28 +47,31 @@ export default {
 
     return {
       token: this.$store.state.user || token,
-      loginNav: 'GCLS-Exercise',
-      LoginNavIndex: 0,
-      projectList: [],
+      activePro: '', // 当前选中的项目
+      LoginNavIndex: 0, // 当前选中的项目索引
+      // 项目列表
+      projectList: [
+        { key: '', name: '个人工作台' },
+        { key: 'project_manage', name: '项目管理' },
+        { key: 'create_project', name: '创建项目' },
+        { key: 'personal_center', name: '个人中心' },
+      ],
     };
   },
-  created() {
-    GetChildSysList_CanEnter_PC().then(({ child_sys_list }) => {
-      if (child_sys_list && child_sys_list.length > 0) {
-        this.projectList = child_sys_list;
-        this.projectList.forEach((item, index) => {
-          if (item.key === this.loginNav) {
-            this.LoginNavIndex = index;
-          }
-        });
-      }
-    });
-  },
   methods: {
     logout() {
       this.$store.dispatch('user/signOut');
       this.$router.push('/login');
     },
+    /**
+     * @description 切换项目
+     * @param {string} key - 项目key
+     */
+    changeProject(key) {
+      this.LoginNavIndex = this.projectList.findIndex((item) => item.key === key);
+      this.activePro = key;
+      this.$router.push({ path: `/${key}` });
+    },
   },
 };
 </script>
@@ -77,13 +92,17 @@ export default {
   border-bottom: 1px solid #ebebeb;
 
   .logo {
+    flex: 1;
     height: 48px;
     margin-right: 36px;
+
+    &-image {
+      height: 100%;
+    }
   }
 
   &-list {
     display: flex;
-    flex: 1;
 
     li {
       padding: 0 12px;

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

@@ -1,4 +1,4 @@
-import { getSessionID, getConfig } from '@/utils/auth';
+import { getConfig } from '@/utils/auth';
 
 import NProgress from 'nprogress';
 import 'nprogress/nprogress.css';
@@ -11,7 +11,7 @@ export function setupRouterGuard(router) {
   router.beforeEach(async (to, from, next) => {
     NProgress.start();
 
-    if (getSessionID() && getConfig()) {
+    if (getConfig()) {
       if (to.path === '/login') {
         next({ path: '/' });
         NProgress.done();

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

@@ -13,7 +13,7 @@ export const ImageChangePage = {
 };
 
 export const homePage = {
-  path: '/',
+  path: '/home',
   component: DEFAULT,
   redirect: '/home',
   meta: {

+ 10 - 1
src/router/modules/index.js

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

+ 144 - 0
src/router/modules/project.js

@@ -0,0 +1,144 @@
+import DEFAULT from '@/layouts/default';
+
+// 个人工作台
+const personalWorkPage = {
+  path: '/',
+  name: 'PersonalWorkbench',
+  component: DEFAULT,
+  redirect: '/personal_workbench/edit_task',
+  meta: {
+    title: '个人工作台',
+    icon: 'practice',
+  },
+  children: [
+    // 我的编辑任务
+    {
+      path: '/personal_workbench/edit_task',
+      name: 'PersonalWorkbenchEdit',
+      meta: {
+        title: '我的编辑任务',
+      },
+      component: () => import('@/views/personal_workbench/edit_task/index.vue'),
+    },
+    // 我的编辑任务 -> 预览
+    {
+      path: '/personal_workbench/edit_task/preview/:id',
+      name: 'PersonalWorkbenchEditPreview',
+      meta: {
+        title: '预览',
+      },
+      component: () => import('@/views/personal_workbench/edit_task/preview/index.vue'),
+    },
+    // 我的编辑任务 -> 编辑
+    {
+      path: '/personal_workbench/edit_task/edit/:courseware_id',
+      name: 'PersonalWorkbenchEditTask',
+      meta: {
+        title: '编辑任务',
+      },
+      component: () => import('@/views/personal_workbench/edit_task/edit/index.vue'),
+    },
+    // 我的审校任务
+    {
+      path: '/personal_workbench/check_task',
+      name: 'PersonalWorkbenchCheck',
+      meta: {
+        title: '我的审校任务',
+      },
+      component: () => import('@/views/personal_workbench/check_task/index.vue'),
+    },
+    // 我的项目
+    {
+      path: '/personal_workbench/project',
+      name: 'PersonalWorkbenchProject',
+      meta: {
+        title: '我的项目',
+      },
+      component: () => import('@/views/personal_workbench/project/index.vue'),
+    },
+    // 制作与审校管理
+    {
+      path: `/personal_workbench/production_editorial/:id`,
+      name: 'ProductionEditorial',
+      meta: {
+        title: '制作与审校管理',
+      },
+      component: () => import('@/views/personal_workbench/project/ProductionEditorialManage.vue'),
+    },
+  ],
+};
+
+// 项目管理
+const projectPage = {
+  path: '/project_manage',
+  name: 'ProjectManage',
+  component: DEFAULT,
+  redirect: '/project_manage',
+  meta: {
+    title: '项目管理',
+    icon: 'practice',
+  },
+  children: [
+    {
+      path: '/project_manage',
+      name: 'ProjectManageIndex',
+      component: () => import('@/views/project_manage/index.vue'),
+    },
+  ],
+};
+
+// 创建项目
+const createProjectPage = {
+  path: '/create_project',
+  name: 'CreateProject',
+  component: DEFAULT,
+  redirect: '/create_project/start',
+  meta: {
+    title: '创建项目',
+    icon: 'practice',
+  },
+  children: [
+    {
+      path: 'start',
+      name: 'CreateProjectStart',
+      component: () => import('@/views/create_project/startCreate.vue'),
+    },
+    {
+      path: 'create',
+      name: 'CreateProjectCreate',
+      meta: {
+        title: '填写项目信息',
+      },
+      component: () => import('@/views/create_project/createProject.vue'),
+    },
+    {
+      path: 'success',
+      name: 'CreateProjectSuccess',
+      meta: {
+        title: '项目创建成功',
+      },
+      component: () => import('@/views/create_project/createSuccess.vue'),
+    },
+  ],
+};
+
+// 个人中心
+const personalCenterPage = {
+  path: '/personal_center',
+  name: 'PersonalCenter',
+  component: DEFAULT,
+  redirect: '/personal_center',
+  meta: {
+    title: '个人中心',
+    icon: 'practice',
+  },
+  children: [
+    {
+      path: '/personal_center',
+      name: 'PersonalCenterIndex',
+      component: () => import('@/views/personal_center/index.vue'),
+    },
+  ],
+};
+
+export default [personalWorkPage, projectPage, createProjectPage, personalCenterPage];

+ 2 - 1
src/store/modules/user.js

@@ -1,6 +1,6 @@
 import { user } from '../mutation-types';
 import { login } from '@/api/user';
-import { setToken, getToken, removeToken } from '@/utils/auth';
+import { setToken, getToken, removeToken, removeConfig } from '@/utils/auth';
 import { resetRouter } from '@/router';
 
 const getDefaultSate = () => {
@@ -61,6 +61,7 @@ const actions = {
   signOut({ commit }) {
     return new Promise((resolve) => {
       removeToken();
+      removeConfig();
       resetRouter();
       commit(user.RESET_STATE);
       resolve();

+ 14 - 0
src/styles/mixin.scss

@@ -77,3 +77,17 @@
   background-color: $fill-color;
   border-radius: 2px;
 }
+
+// 页面基础样式
+@mixin page-base {
+  display: flex;
+  flex-direction: column;
+  row-gap: 16px;
+  min-width: 1100px;
+  min-height: calc(100% - 16px);
+  padding: 24px;
+  margin: 0 24px 16px;
+  background-color: #fff;
+  border-radius: 4px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 10%);
+}

+ 87 - 105
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -14,101 +14,97 @@
       },
     ]"
   >
-    <template v-if="isEdit">
-      <span class="drag-line" data-row="-1"></span>
-      <!-- 行 -->
-      <template v-for="(row, i) in data.row_list">
-        <div :key="i" class="row" :style="computedRowStyle(i)">
-          <!-- 列 -->
-          <template v-for="(col, j) in row.col_list">
-            <span
-              v-if="j === 0"
-              :key="`start-${i}-${j}`"
-              class="drag-vertical-line col-start"
-              :data-row="i"
-              :data-col="j"
-            ></span>
-            <div :key="j" :class="['col', `col-${i}-${j}`]" :style="computedColStyle(col)">
-              <!-- 网格 -->
-              <template v-for="(grid, k) in col.grid_list">
-                <span
-                  v-if="k === 0"
-                  :key="`start-${i}-${j}-${k}`"
-                  class="drag-line grid-line drag-row"
-                  :style="{ gridArea: 'grid-top' }"
-                  :data-row="i"
-                  :data-col="j"
-                  :data-grid="k"
-                  data-type="row"
-                ></span>
-                <span
-                  v-if="grid.row > 1 && grid.row !== col.grid_list[k - 1].row"
-                  :key="`middle-${i}-${j}-${k}`"
-                  :style="{ gridArea: `middle-${grid.grid_area}` }"
-                  :data-row="i"
-                  :data-col="j"
-                  :data-grid="k"
-                  data-type="col-middle"
-                  class="drag-line grid-line"
-                ></span>
-                <span
-                  :key="`left-${i}-${j}-${k}`"
-                  :style="{ gridArea: `left-${grid.grid_area}` }"
-                  :data-row="i"
-                  :data-col="j"
-                  :data-grid="k"
-                  data-type="col-left"
-                  class="drag-vertical-line grid-line grid-line-left"
-                ></span>
-                <component
-                  :is="componentList[grid.type]"
-                  :id="grid.id"
-                  ref="component"
-                  :key="`grid-${grid.id}`"
-                  :class="[grid.id]"
-                  :style="{ gridArea: grid.grid_area, height: grid.height, marginTop: grid.row !== 1 ? '16px' : '0' }"
-                  :delete-component="deleteComponent(i, j, k)"
-                  :component-move="componentMove(i, j, k)"
-                  @showSetting="showSetting"
-                  @changeData="changeData"
-                />
-                <span
-                  :key="`right-${i}-${j}-${k}`"
-                  :style="{ gridArea: `right-${grid.grid_area}` }"
-                  :data-row="i"
-                  :data-col="j"
-                  :data-grid="k + 1"
-                  data-type="col-right"
-                  class="drag-vertical-line grid-line grid-line-right"
-                ></span>
-                <span
-                  v-if="k === col.grid_list.length - 1"
-                  :key="`end-${i}-${j}-${k}`"
-                  class="drag-line grid-line drag-row"
-                  :style="{ gridArea: `grid-bottom` }"
-                  :data-row="i"
-                  :data-col="j"
-                  :data-grid="k + 1"
-                  data-type="row"
-                ></span>
-              </template>
-            </div>
-            <span :key="`end-${i}-${j}`" class="drag-vertical-line col-end" :data-row="i" :data-col="j + 1"></span>
-          </template>
-        </div>
-        <span v-if="i < data.row_list.length - 1" :key="`row-${i}`" class="drag-line" :data-row="i"></span>
-      </template>
-      <span class="drag-line" :data-row="data.row_list.length - 1"></span>
+    <span class="drag-line" data-row="-1"></span>
+    <!-- 行 -->
+    <template v-for="(row, i) in data.row_list">
+      <div :key="i" class="row" :style="computedRowStyle(i)">
+        <!-- 列 -->
+        <template v-for="(col, j) in row.col_list">
+          <span
+            v-if="j === 0"
+            :key="`start-${i}-${j}`"
+            class="drag-vertical-line col-start"
+            :data-row="i"
+            :data-col="j"
+          ></span>
+          <div :key="j" :class="['col', `col-${i}-${j}`]" :style="computedColStyle(col)">
+            <!-- 网格 -->
+            <template v-for="(grid, k) in col.grid_list">
+              <span
+                v-if="k === 0"
+                :key="`start-${i}-${j}-${k}`"
+                class="drag-line grid-line drag-row"
+                :style="{ gridArea: 'grid-top' }"
+                :data-row="i"
+                :data-col="j"
+                :data-grid="k"
+                data-type="row"
+              ></span>
+              <span
+                v-if="grid.row > 1 && grid.row !== col.grid_list[k - 1].row"
+                :key="`middle-${i}-${j}-${k}`"
+                :style="{ gridArea: `middle-${grid.grid_area}` }"
+                :data-row="i"
+                :data-col="j"
+                :data-grid="k"
+                data-type="col-middle"
+                class="drag-line grid-line"
+              ></span>
+              <span
+                :key="`left-${i}-${j}-${k}`"
+                :style="{ gridArea: `left-${grid.grid_area}` }"
+                :data-row="i"
+                :data-col="j"
+                :data-grid="k"
+                data-type="col-left"
+                class="drag-vertical-line grid-line grid-line-left"
+              ></span>
+              <component
+                :is="componentList[grid.type]"
+                :id="grid.id"
+                ref="component"
+                :key="`grid-${grid.id}`"
+                :class="[grid.id]"
+                :style="{ gridArea: grid.grid_area, height: grid.height, marginTop: grid.row !== 1 ? '16px' : '0' }"
+                :delete-component="deleteComponent(i, j, k)"
+                :component-move="componentMove(i, j, k)"
+                @showSetting="showSetting"
+                @changeData="changeData"
+              />
+              <span
+                :key="`right-${i}-${j}-${k}`"
+                :style="{ gridArea: `right-${grid.grid_area}` }"
+                :data-row="i"
+                :data-col="j"
+                :data-grid="k + 1"
+                data-type="col-right"
+                class="drag-vertical-line grid-line grid-line-right"
+              ></span>
+              <span
+                v-if="k === col.grid_list.length - 1"
+                :key="`end-${i}-${j}-${k}`"
+                class="drag-line grid-line drag-row"
+                :style="{ gridArea: `grid-bottom` }"
+                :data-row="i"
+                :data-col="j"
+                :data-grid="k + 1"
+                data-type="row"
+              ></span>
+            </template>
+          </div>
+          <span :key="`end-${i}-${j}`" class="drag-vertical-line col-end" :data-row="i" :data-col="j + 1"></span>
+        </template>
+      </div>
+      <span v-if="i < data.row_list.length - 1" :key="`row-${i}`" class="drag-line" :data-row="i"></span>
     </template>
-
-    <PreviewEdit v-else :courseware-id="courseware_id" :row-list="data.row_list" @computedMoveData="computedMoveData" />
+    <span class="drag-line" :data-row="data.row_list.length - 1"></span>
   </main>
 </template>
 
 <script>
 import { getRandomNumber } from '@/utils/index';
 import { componentList } from '../../data/bookType';
-import { SaveCoursewareContent, GetCoursewareContent } from '@/api/book';
+import { ContentSaveCoursewareContent, ContentGetCoursewareContent } from '@/api/book';
 
 import PreviewEdit from './PreviewEdit.vue';
 
@@ -118,12 +114,6 @@ export default {
     PreviewEdit,
   },
   inject: ['getCurSettingId'],
-  props: {
-    isEdit: {
-      type: Boolean,
-      required: true,
-    },
-  },
   data() {
     const { book_id, chapter_id } = this.$route.query;
 
@@ -171,7 +161,6 @@ export default {
     enterCanvas: {
       handler(val) {
         if (val) return;
-        if (!this.isEdit) return;
         const dragLineList = document.querySelectorAll('.drag-line');
         dragLineList.forEach((item) => {
           item.style.opacity = 0;
@@ -203,7 +192,7 @@ export default {
     },
   },
   created() {
-    GetCoursewareContent({ id: this.courseware_id }).then(({ content }) => {
+    ContentGetCoursewareContent({ id: this.courseware_id }).then(({ content }) => {
       if (content) {
         this.data = JSON.parse(content);
       }
@@ -249,7 +238,7 @@ export default {
         spinner: 'el-icon-loading',
         background: 'rgba(0, 0, 0, 0.7)',
       });
-      SaveCoursewareContent({
+      ContentSaveCoursewareContent({
         id: this.courseware_id,
         category: 'NEW',
         content: JSON.stringify(this.data),
@@ -257,12 +246,9 @@ export default {
       }).then(() => {
         this.$message.success('保存成功');
         loading.close();
-        if (type === 'quit') {
+        if (type === 'quit' || type === 'edit') {
           this.$emit('back');
         }
-        if (type === 'edit') {
-          this.$emit('changeEditStatus');
-        }
       });
     },
     setBackgroundImage(url, position) {
@@ -697,8 +683,6 @@ export default {
       this.drag.clientX = clientX;
       this.drag.clientY = clientY;
 
-      if (!this.isEdit) return; // 非编辑状态不允许显隐线
-
       let { isInsideCanvas } = this.getMarginDifferences();
 
       this.enterCanvas = isInsideCanvas;
@@ -753,8 +737,6 @@ export default {
         this.drag.dragging = false;
       }
 
-      if (!this.isEdit) return;
-
       if (this.enterCanvas) {
         if (this.curRow >= -1 && this.curCol <= -1) {
           this.calculateRowInsertedObject();

+ 12 - 8
src/views/book/courseware/create/components/PreviewEdit.vue

@@ -26,10 +26,9 @@
               </template>
               <component
                 :is="previewComponentList[grid.type]"
-                :id="grid.id"
                 ref="preview"
                 :key="`preview-${grid.id}`"
-                :courseware-id="coursewareId"
+                :content="computedColContent(grid.id)"
                 :class="[grid.id]"
                 :style="{
                   gridArea: 'preview',
@@ -47,7 +46,6 @@
 
 <script>
 import { previewComponentList } from '@/views/book/courseware/data/bookType';
-import { GetBook } from '@/api/book';
 
 export default {
   name: 'PreviewEdit',
@@ -62,8 +60,8 @@ export default {
       type: Array,
       required: true,
     },
-    coursewareId: {
-      type: String,
+    componentList: {
+      type: Array,
       required: true,
     },
   },
@@ -112,15 +110,21 @@ export default {
   created() {
     document.addEventListener('mousemove', this.dragMove);
     document.addEventListener('mouseup', this.dragEnd);
-    GetBook({ id: this.$route.query.book_id }).then(({ theme_color }) => {
-      this.bookInfo.theme_color = theme_color;
-    });
   },
   beforeDestroy() {
     document.removeEventListener('mousemove', this.dragMove);
     document.removeEventListener('mouseup', this.dragEnd);
   },
   methods: {
+    /**
+     * 计算组件内容
+     * @param {string} id 组件id
+     * @returns {string} 组件内容
+     */
+    computedColContent(id) {
+      if (!id) return '';
+      return this.componentList.find((item) => item.component_id === id)?.content || '';
+    },
     getMultipleColStyle(i) {
       let row = this.rowList[i];
       let col = row.col_list;

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

@@ -3,7 +3,7 @@ import ModuleBase from './ModuleBase.vue';
 import RichText from '@/components/RichText.vue';
 
 import { displayList, viewMethodList, isEnable } from '@/views/book/courseware/data/common';
-import { SaveCoursewareComponentContent, GetCoursewareComponentContent } from '@/api/book';
+import { ContentSaveCoursewareComponentContent, ContentGetCoursewareComponentContent } from '@/api/book';
 
 const mixin = {
   data() {
@@ -45,20 +45,22 @@ const mixin = {
   },
   inject: ['courseware_id'],
   created() {
-    GetCoursewareComponentContent({ courseware_id: this.courseware_id, component_id: this.id }).then(({ content }) => {
-      if (content) {
-        this.data = JSON.parse(content);
-        this.property.isGetContent = true;
-      }
+    ContentGetCoursewareComponentContent({ courseware_id: this.courseware_id, component_id: this.id }).then(
+      ({ content }) => {
+        if (content) {
+          this.data = JSON.parse(content);
+          this.property.isGetContent = true;
+        }
 
-      this.$watch(
-        'data',
-        () => {
-          this.$emit('changeData');
-        },
-        { deep: true },
-      );
-    });
+        this.$watch(
+          'data',
+          () => {
+            this.$emit('changeData');
+          },
+          { deep: true },
+        );
+      },
+    );
   },
   methods: {
     /**
@@ -79,7 +81,7 @@ const mixin = {
       this.componentMove({ ...data, id: this.id });
     },
     saveCoursewareComponentContent() {
-      SaveCoursewareComponentContent({
+      ContentSaveCoursewareComponentContent({
         courseware_id: this.courseware_id,
         component_id: this.id,
         component_type: this.data.type,

+ 3 - 3
src/views/book/courseware/create/components/question/article/ArticleSetting.vue

@@ -34,16 +34,16 @@
       </el-form-item>
       <el-divider />
       <el-form-item label="拼音">
-        <el-switch v-model="property.is_enable_pinyin" active-text="" inactive-text=""> </el-switch>
+        <el-switch v-model="property.is_enable_pinyin" active-text="" inactive-text="" />
       </el-form-item>
-      <el-form-item label="拼音位置" v-if="property.is_enable_pinyin">
+      <el-form-item v-if="property.is_enable_pinyin" label="拼音位置">
         <el-radio-group v-model="property.pinyin_position">
           <el-radio v-for="{ value, label } in positionList" :key="value" :label="value" :value="value">
             {{ label }}
           </el-radio>
         </el-radio-group>
       </el-form-item>
-      <el-form-item label="" v-if="property.is_enable_pinyin">
+      <el-form-item v-if="property.is_enable_pinyin" label="">
         <el-checkbox v-model="property.is_enable_sentence_case">句首大写</el-checkbox>
       </el-form-item>
     </el-form>

+ 34 - 0
src/views/book/courseware/create/components/question/input/Input.vue

@@ -0,0 +1,34 @@
+<template>
+  <ModuleBase :type="data.type">
+    <template #content>
+      <RichText
+        ref="rich"
+        v-model="data.content"
+        toolbar="fontselect fontsizeselect alignleft aligncenter alignright forecolor backcolor"
+        :font-size="18"
+        placeholder="请输入内容"
+      />
+    </template>
+  </ModuleBase>
+</template>
+
+<script>
+import ModuleMixin from '../../common/ModuleMixin';
+
+import { getInputData } from '@/views/book/courseware/data/input';
+
+export default {
+  name: 'InputPage',
+  mixins: [ModuleMixin],
+  data() {
+    return {
+      data: getInputData(),
+    };
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+</style>

+ 94 - 0
src/views/book/courseware/create/components/question/input/InputSetting.vue

@@ -0,0 +1,94 @@
+<template>
+  <div>
+    <el-form :model="property" :label-position="labelPosition" label-width="72px">
+      <SerailNumber :property="property" />
+
+      <el-form-item label="拼音">
+        <el-switch v-model="property.is_enable_pinyin" active-text="" inactive-text="" />
+      </el-form-item>
+      <el-form-item v-if="property.is_enable_pinyin" label="拼音位置">
+        <el-radio-group v-model="property.pinyin_position">
+          <el-radio v-for="{ value, label } in positionList" :key="value" :label="value" :value="value">
+            {{ label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item v-if="property.is_enable_pinyin" label="附加功能">
+        <el-checkbox v-model="property.is_enable_sentence_case">句首大写</el-checkbox>
+        <el-checkbox v-model="property.is_enable_auto_correct">自动修正拼音</el-checkbox>
+      </el-form-item>
+
+      <el-form-item label="模式">
+        <el-radio-group v-model="property.model" class="model">
+          <el-radio v-for="{ value, label } in modelList" :key="value" :label="value" :value="value">
+            {{ label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+
+      <el-form-item label="样式">
+        <el-select v-model="property.input_style">
+          <el-option v-for="{ value, label } in inputStyleList" :key="value" :label="label" :value="value" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="输入框文字样式">
+        <el-select v-model="property.font">
+          <el-option v-for="{ value, label } in filterFontList" :key="value" :label="label" :value="value" />
+        </el-select>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import SettingMixin from '@/views/book/courseware/create/components/common/SettingMixin';
+
+import {
+  getInputProperty,
+  modelList,
+  inputStyleList,
+  fontList,
+  fontSizeList,
+} from '@/views/book/courseware/data/input';
+import { positionList } from '@/views/book/courseware/data/article';
+
+export default {
+  name: 'InputSetting',
+  mixins: [SettingMixin],
+  data() {
+    return {
+      property: getInputProperty(),
+      positionList,
+      modelList,
+      inputStyleList,
+      fontList,
+      fontSizeList,
+      labelPosition: 'left',
+    };
+  },
+  computed: {
+    filterFontList() {
+      if (this.property.font === fontList[0].value) {
+        return this.fontList.filter(({ isPinyin }) => isPinyin);
+      }
+      return this.fontList;
+    },
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.el-form {
+  @include setting-base;
+
+  .el-radio-group.model {
+    label.el-radio {
+      margin-right: 8px;
+    }
+  }
+}
+</style>

+ 2 - 2
src/views/book/courseware/create/components/question/math/MathSetting.vue

@@ -9,14 +9,14 @@
 <script>
 import SettingMixin from '@/views/book/courseware/create/components/common/SettingMixin';
 
-import { getFillProperty } from '@/views/book/courseware/data/math';
+import { getMathProperty } from '@/views/book/courseware/data/math';
 
 export default {
   name: 'MatchingSetting',
   mixins: [SettingMixin],
   data() {
     return {
-      property: getFillProperty(),
+      property: getMathProperty(),
     };
   },
   methods: {},

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

@@ -1,10 +1,6 @@
 <template>
   <div ref="create" class="create">
     <div class="create-left">
-      <div class="back-container">
-        <el-button class="back" @click="judgeIsHasChange"><i class="el-icon-arrow-left"></i> 返回</el-button>
-        <span class="title">Frame {{ Number(index) + 1 }}</span>
-      </div>
       <div v-for="{ value, label, children } in bookTypeOption" :key="value" class="components">
         <div class="components-title">{{ label }}</div>
         <div
@@ -23,30 +19,15 @@
     <div class="create-middle">
       <div class="create-operation">
         <el-button @click="showSetBackground"><SvgIcon icon-class="background-img" />背景图</el-button>
-        <el-button><SvgIcon icon-class="template" />模板</el-button>
-        <el-button class="exit-edit">
-          <span @click="saveCoursewareContent('edit')">
-            <template v-if="isEdit"> <i class="el-icon-close"></i><span> 退出编辑</span> </template>
-            <template v-else> <i class="el-icon-edit"></i><span> 编辑</span> </template>
-          </span>
-          <el-button class="save" type="primary">
-            <span @click="saveCoursewareContent"><SvgIcon icon-class="save" /> <span>保存</span></span>
-            <el-popover placement="bottom" popper-class="save-popover" trigger="click">
-              <div class="save-template">保存为模板</div>
-              <i slot="reference" class="el-icon-arrow-down"></i>
-            </el-popover>
-          </el-button>
-        </el-button>
+        <!-- <el-button><SvgIcon icon-class="template" />模板</el-button> -->
       </div>
 
       <CreateCanvas
         ref="createCanvas"
-        :is-edit="isEdit"
         @showSetting="showSetting"
         @showSettingEmpty="showSettingEmpty"
         @changeData="changeData"
         @back="back"
-        @changeEditStatus="changeEditStatus"
       />
     </div>
 
@@ -94,11 +75,9 @@ export default {
       bookTypeOption,
       visible: false,
       visibleWarn: false,
-      isEdit: true, // 是否编辑状态
       isChange: false, // 是否有改动
     };
   },
-
   methods: {
     judgeIsHasChange() {
       if (this.isChange) {
@@ -108,13 +87,7 @@ export default {
       this.back();
     },
     back() {
-      this.$router.push({ path: '/chapter', query: { chapter_id: this.chapter_id, book_id: this.book_id } });
-    },
-    /**
-     * 切换编辑状态
-     */
-    changeEditStatus() {
-      this.isEdit = !this.isEdit;
+      this.$emit('goBackPreview');
     },
     dragStart(event, type) {
       this.$refs.createCanvas.dragStart(event, type);
@@ -131,10 +104,6 @@ export default {
         this.curSettingType = '';
         this.curSettingId = '';
       }
-      if (type === 'edit' && !this.isChange) {
-        this.changeEditStatus();
-        return;
-      }
       this.$refs.createCanvas.saveCoursewareContent(type);
       this.isChange = false;
     },
@@ -182,31 +151,20 @@ export default {
     overflow: auto;
     background-color: #fff;
 
-    .back-container {
+    .components {
       display: flex;
       flex-direction: column;
-      row-gap: 8px;
-      padding: 4px;
+      padding: 24px 8px 4px;
+      margin-top: 12px;
 
-      .back {
-        color: #000;
-        text-align: left;
-        background-color: #f4f4f4;
+      &:first-child {
+        padding-top: 12px;
+        margin-top: 0;
       }
 
-      .title {
-        padding: 0 16px;
-        font-size: 14px;
-        font-weight: bold;
+      &:not(:first-child) {
+        border-top: 1px solid #e5e6eb;
       }
-    }
-
-    .components {
-      display: flex;
-      flex-direction: column;
-      padding: 24px 8px 4px;
-      margin-top: 12px;
-      border-top: 1px solid #e5e6eb;
 
       .components-title {
         padding-left: 8px;
@@ -246,10 +204,6 @@ export default {
       justify-content: center;
       margin-bottom: 16px;
 
-      .exit-edit {
-        padding: 4px 4px 4px 16px;
-      }
-
       > .el-button {
         height: 40px;
         font-size: 16px;
@@ -260,19 +214,6 @@ export default {
           column-gap: 8px;
           align-items: center;
         }
-
-        .save.el-button {
-          height: 32px;
-          margin-left: 8px;
-          font-size: 16px;
-
-          :deep > span {
-            display: flex;
-            column-gap: 4px;
-            align-items: center;
-            margin-top: -1px;
-          }
-        }
       }
     }
   }

+ 11 - 0
src/views/book/courseware/data/bookType.js

@@ -49,6 +49,8 @@ import Article from '../create/components/question/article/Article.vue';
 import ArticleSetting from '../create/components/question/article/ArticleSetting.vue';
 import Math from '../create/components/question/math/Math.vue';
 import MathSetting from '../create/components/question/math/MathSetting.vue';
+import Input from '../create/components/question/input/Input.vue';
+import InputSetting from '../create/components/question/input/InputSetting.vue';
 
 // 预览组件页面列表
 import AudioPreview from '@/views/book/courseware/preview/components/audio/AudioPreview.vue';
@@ -76,6 +78,7 @@ import NotesPreview from '../preview/components/notes/NotesPreview.vue';
 import OtherWordPreview from '../preview/components/other_word/OtherWordPreview.vue';
 import ArticlePreview from '../preview/components/article/index.vue';
 import MathPreview from '../preview/components/math/MathPreview.vue';
+import InputPreview from '../preview/components/input/InputPreview.vue';
 
 export const bookTypeOption = [
   {
@@ -289,6 +292,14 @@ export const bookTypeOption = [
         set: MathSetting,
         preview: MathPreview,
       },
+      {
+        value: 'input',
+        label: '输入框组件',
+        icon: '',
+        component: Input,
+        set: InputSetting,
+        preview: InputPreview,
+      },
     ],
   },
 ];

+ 90 - 0
src/views/book/courseware/data/input.js

@@ -0,0 +1,90 @@
+// 输入框组件
+
+import { positionList } from './article';
+import { commonSetProperty, serialNumberStyleList } from './common';
+
+// 模式列表
+export const modelList = [
+  {
+    value: 'pinyin',
+    label: '拼音',
+  },
+  {
+    value: 'chinese',
+    label: '汉字',
+  },
+  {
+    value: 'english',
+    label: '英文',
+  },
+];
+
+// 输入框样式
+export const inputStyleList = [
+  {
+    value: 'horizontal_line',
+    label: '横线',
+  },
+  {
+    value: 'input',
+    label: '输入框',
+  },
+];
+
+export const fontList = [
+  {
+    value: 'arial, helvetica, sans-serif',
+    label: 'Arial',
+    isPinyin: false,
+  },
+  {
+    value: 'League',
+    label: '拼音=League',
+    isPinyin: true,
+  },
+  {
+    value: '楷体,微软雅黑',
+    label: '楷体=楷体,微软雅黑',
+    isPinyin: false,
+  },
+  {
+    value: '黑体,微软雅黑',
+    label: '黑体=黑体,微软雅黑;',
+    isPinyin: false,
+  },
+  {
+    value: '宋体,微软雅黑',
+    label: '宋体=宋体,微软雅黑;',
+    isPinyin: false,
+  },
+];
+
+export const fontSizeList = [12, 14, 16, 18, 20, 22, 24, 26, 28, 30];
+
+export function getInputProperty() {
+  return {
+    ...commonSetProperty,
+    sn_style: serialNumberStyleList[0].value,
+    sn_background_color: '#ea3232', // 序号背景色
+    is_enable_pinyin: true, // 是否启用拼音
+    pinyin_position: positionList[0].value, // 拼音位置
+    is_enable_sentence_case: true, // 句首大写
+    is_enable_auto_correct: true, // 自动修正
+    model: modelList[0].value, // 模式
+    input_style: inputStyleList[0].value, // 输入框样式
+    font: fontList[1].value, // 字体
+    font_size: fontSizeList[3], // 字体大小
+  };
+}
+
+export function getInputData() {
+  return {
+    type: 'input',
+    title: '输入框',
+    property: getInputProperty(),
+    answer: {
+      text: '', // 答案文本
+    },
+    content: '', // 文本内容
+  };
+}

+ 2 - 2
src/views/book/courseware/data/math.js

@@ -3,7 +3,7 @@
 
 import { commonSetProperty } from './common';
 
-export function getFillProperty() {
+export function getMathProperty() {
   return {
     ...commonSetProperty,
   };
@@ -13,7 +13,7 @@ export function getMathData() {
   return {
     type: 'math',
     title: '公式',
-    property: getFillProperty(),
+    property: getMathProperty(),
     math: '', // 公式内容
   };
 }

+ 3 - 3
src/views/book/courseware/preview/components/character/CharacterPreview.vue

@@ -4,10 +4,10 @@
     <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
 
     <div class="main">
-      <div class="content-box" v-for="(item, index) in data.content_list" :key="index">
+      <div v-for="(item, index) in data.content_list" :key="index" class="content-box">
         <div class="main-top" :style="{ width: item.hz_strokes_list.length * 64 + 'px' }">
           <AudioPlay v-if="isEnable(data.property.is_enable_voice)" :file-id="item.audio_file_id" theme-color="gray" />
-          <span class="pinyin" v-if="isEnable(data.property.is_enable_pinyin)">{{ item.pinyin }}</span>
+          <span v-if="isEnable(data.property.is_enable_pinyin)" class="pinyin">{{ item.pinyin }}</span>
         </div>
         <div class="strock-chinese-box">
           <Strockplayredline
@@ -46,8 +46,8 @@ export default {
       data: getCharacterData(),
     };
   },
-  watch: {},
   computed: {},
+  watch: {},
   created() {},
   methods: {},
 };

+ 8 - 16
src/views/book/courseware/preview/components/common/PreviewMixin.js

@@ -1,7 +1,6 @@
 import SerialNumberPosition from './SerialNumberPosition.vue';
 import DOMPurify from 'dompurify';
 
-import { GetCoursewareComponentContent_View } from '@/api/book';
 import { isEnable } from '@/views/book/courseware/data/common';
 
 const mixin = {
@@ -16,30 +15,23 @@ const mixin = {
     };
   },
   props: {
-    id: {
+    content: {
       type: String,
       required: true,
     },
-    coursewareId: {
-      type: String,
-      required: true,
+  },
+  watch: {
+    content: {
+      handler(newVal) {
+        this.data = JSON.parse(newVal);
+      },
+      immediate: true,
     },
   },
   components: {
     SerialNumberPosition,
   },
-  created() {
-    this.getCoursewareComponentContent_View();
-  },
   methods: {
-    getCoursewareComponentContent_View() {
-      GetCoursewareComponentContent_View({ courseware_id: this.coursewareId, component_id: this.id }).then(
-        ({ content }) => {
-          if (content) this.data = JSON.parse(content);
-          this.loader = true;
-        },
-      );
-    },
     /**
      * 获取答案
      * @returns {array} 答案

+ 60 - 0
src/views/book/courseware/preview/components/input/InputPreview.vue

@@ -0,0 +1,60 @@
+<!-- eslint-disable vue/no-v-html -->
+<template>
+  <div class="input-preview" :style="getAreaStyle()">
+    <SerialNumberPosition v-if="isEnable(data.property.sn_display_mode)" :property="data.property" />
+
+    <div class="main">
+      <span class="rich-text" v-html="sanitizeHTML(data.content)"></span>
+      <el-input
+        v-model="data.answer.text"
+        type="text"
+        :class="['input', { 'input-horizontal': data.property.input_style === inputStyleList[0].value }]"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import { getInputData, inputStyleList } from '@/views/book/courseware/data/input';
+
+import PreviewMixin from '../common/PreviewMixin';
+
+export default {
+  name: 'InputPreview',
+  mixins: [PreviewMixin],
+  data() {
+    return {
+      data: getInputData(),
+      inputStyleList,
+    };
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.input-preview {
+  @include preview-base;
+
+  .main {
+    display: flex;
+    column-gap: 4px;
+    align-items: center;
+
+    .rich-text :deep p {
+      word-break: keep-all;
+    }
+
+    .input.input-horizontal :deep .el-input__inner {
+      background-color: #fff;
+      border-top-width: 0;
+      border-right-width: 0;
+      border-bottom: 1px solid #333;
+      border-left-width: 0;
+      border-radius: 0;
+    }
+  }
+}
+</style>

+ 227 - 0
src/views/create_project/createProject.vue

@@ -0,0 +1,227 @@
+<template>
+  <div class="create-project">
+    <el-form
+      ref="projectForm"
+      :inline="true"
+      :model="project"
+      :rules="formRules"
+      label-width="120px"
+      class="project-form"
+    >
+      <el-form-item label="项目名称" prop="name">
+        <el-input v-model="project.name" placeholder="请输入项目名称" maxlength="20" />
+      </el-form-item>
+      <el-form-item label="项目分类" prop="category">
+        <el-input v-model="project.category" maxlength="30" />
+      </el-form-item>
+      <el-form-item label="作品标签" prop="label_list" class="label-input">
+        <div class="label-input-content">
+          <div class="label-list">
+            <el-tag
+              v-for="(tag, index) in project.label_list"
+              :key="index"
+              closable
+              @close="project.label_list.splice(index, 1)"
+            >
+              {{ tag }}
+            </el-tag>
+          </div>
+          <el-input v-model="labelInput" placeholder="请输入标签" @keyup.enter.native="labelChange" />
+        </div>
+      </el-form-item>
+      <el-form-item label="语种" prop="language">
+        <el-input v-model="project.language" type="text" placeholder="请输入语种" maxlength="20" />
+      </el-form-item>
+      <el-form-item label="所属课题" prop="topic">
+        <el-input v-model="project.topic" type="text" placeholder="请输入所属课题" />
+      </el-form-item>
+      <el-form-item label="出版单位" prop="publisher">
+        <el-input v-model="project.publisher" type="text" placeholder="请输入出版单位" maxlength="20" />
+      </el-form-item>
+      <el-form-item label="内容简介" prop="content_intro">
+        <el-input
+          v-model="project.content_intro"
+          type="textarea"
+          :autosize="{ minRows: 4 }"
+          maxlength="1500"
+          show-word-limit
+          placeholder="请输入内容简介"
+        />
+      </el-form-item>
+      <el-form-item label="选题背景" prop="background">
+        <el-input
+          v-model="project.background"
+          type="textarea"
+          :autosize="{ minRows: 4 }"
+          maxlength="1500"
+          show-word-limit
+          placeholder="请输入选题背景"
+        />
+      </el-form-item>
+      <el-form-item label="作者简介" prop="author_intro">
+        <el-input
+          v-model="project.author_intro"
+          type="textarea"
+          :autosize="{ minRows: 4 }"
+          maxlength="1500"
+          show-word-limit
+          placeholder="请输入作者简介"
+        />
+      </el-form-item>
+      <el-form-item prop="content_count_YG">
+        <span slot="label" style="line-height: 16px">预计容量 <br />(课数)</span>
+        <el-input v-model="project.content_count_YG" type="number" />
+      </el-form-item>
+      <el-form-item label="预计字数" prop="word_count_YG">
+        <el-input v-model="project.word_count_YG" type="number" />
+      </el-form-item>
+      <el-form-item label="计划出版时间" prop="plan_publish_date">
+        <el-date-picker v-model="project.plan_publish_date" type="date" placeholder="选择日期" />
+      </el-form-item>
+      <el-form-item label="读者对象" prop="reader">
+        <el-input v-model="project.reader" type="text" placeholder="请输入读者对象" maxlength="20" />
+      </el-form-item>
+      <el-form-item label="组长列表" prop="leader_id_list">
+        <el-select v-model="project.leader_id_list" multiple placeholder="请选择组长列表">
+          <el-option label="组长1" value="leader1" />
+          <el-option label="组长2" value="leader2" />
+          <el-option label="组长3" value="leader3" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="组员列表" prop="member_id_list">
+        <el-select v-model="project.member_id_list" multiple placeholder="请选择组员列表">
+          <el-option label="组员1" value="member1" />
+          <el-option label="组员2" value="member2" />
+          <el-option label="组员3" value="member3" />
+        </el-select>
+      </el-form-item>
+      <el-form-item class="submit-button">
+        <el-button @click="$router.push('/create_project/start')">取消</el-button>
+        <el-button type="primary" @click="createProject">确定</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { CreateProject } from '@/api/project';
+
+export default {
+  name: 'CreateProject',
+  data() {
+    return {
+      labelInput: '', // 用于输入标签
+      project: {
+        name: '',
+        category: '',
+        label_list: [],
+        language: '',
+        topic: '',
+        publisher: '',
+        content_intro: '',
+        background: '',
+        author_intro: '',
+        content_count_YG: 100,
+        word_count_YG: 100000,
+        plan_publish_date: '', // 计划出版时间
+        reader: '', // 读者对象
+        leader_id_list: [], // 组长列表
+        member_id_list: [], // 组员列表
+      },
+      formRules: {
+        name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
+      },
+    };
+  },
+  methods: {
+    // 处理标签输入
+    labelChange() {
+      if (this.labelInput.trim() !== '') {
+        this.project.label_list.push(this.labelInput.trim());
+        this.labelInput = ''; // 清空输入框
+      }
+    },
+    createProject() {
+      this.$refs.projectForm.validate((valid) => {
+        if (valid) {
+          CreateProject(this.project)
+            .then(() => {
+              this.$message.success('项目创建成功!');
+              this.$router.push('/create_project/success'); // 跳转到创建成功页面
+            })
+            .catch(() => {
+              this.$message.error('项目创建失败!');
+            });
+        } else {
+          this.$message.error('请填写完整的项目信息!');
+          return false;
+        }
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.create-project {
+  @include page-base;
+
+  max-width: 1148px;
+  margin: 0 auto;
+
+  .project-form {
+    .el-input,
+    .el-select {
+      width: 420px;
+
+      :deep .el-input__inner {
+        background-color: #fff;
+        border-color: #dcdcdc;
+      }
+    }
+
+    .el-textarea {
+      width: 970px;
+
+      :deep .el-textarea__inner {
+        background-color: #fff;
+        border-color: #dcdcdc;
+      }
+    }
+
+    .label-input {
+      max-width: 540px;
+
+      :deep .label-input-content {
+        display: flex;
+        align-items: center;
+        width: 420px;
+
+        .label-list {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 4px;
+        }
+
+        .el-input {
+          flex: 1;
+          min-width: 120px;
+          margin-left: 6px;
+        }
+      }
+    }
+
+    .submit-button {
+      width: 100%;
+      padding-right: 10px;
+      text-align: right;
+
+      .el-button {
+        padding: 12px 24px;
+      }
+    }
+  }
+}
+</style>

+ 47 - 0
src/views/create_project/createSuccess.vue

@@ -0,0 +1,47 @@
+<template>
+  <div class="create-success">
+    <p class="tips">教材编辑项目《轻松学中文》创建成功,请切换到“个人工作台” > “我的项目”进行管理。</p>
+    <div class="btns">
+      <el-button type="primary" @click="$router.push('/create_project/create')">创建新的项目</el-button>
+      <el-button type="primary" @click="$router.push('/personal_workbench/project')">
+        跳转到“个人工作台” > “我的项目”
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'CreateSuccess',
+  data() {
+    return {};
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.create-success {
+  @include page-base;
+
+  justify-content: space-between;
+  padding-top: 18%;
+
+  .tips {
+    font-size: 16px;
+    color: #333;
+    text-align: center;
+  }
+
+  .btns {
+    display: flex;
+    align-items: center;
+    justify-content: end;
+
+    .el-button {
+      padding: 12px 24px;
+    }
+  }
+}
+</style>

+ 52 - 0
src/views/create_project/startCreate.vue

@@ -0,0 +1,52 @@
+<template>
+  <div class="start-create">
+    <div class="start-create__content">
+      <div class="create" @click="createProject">创建项目</div>
+      <div class="tips">点击按钮,开始创建项目。</div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'StartCreate',
+  data() {
+    return {};
+  },
+  methods: {
+    createProject() {
+      this.$router.push('/create_project/create');
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.start-create {
+  @include page-base;
+
+  &__content {
+    display: flex;
+    flex: 1;
+    flex-direction: column;
+    align-items: center;
+    padding-top: 20%;
+
+    .create {
+      padding: 20px 40px;
+      margin-bottom: 40px;
+      font-size: 32px;
+      font-weight: bold;
+      color: $main-color;
+      cursor: pointer;
+    }
+
+    .tips {
+      font-size: 16px;
+      color: #666;
+    }
+  }
+}
+</style>

+ 4 - 1
src/views/login/index.vue

@@ -3,6 +3,9 @@
     <main class="login-container">
       <div class="title">登录</div>
       <el-form ref="loginForm" :model="form" :rules="rules" label-position="right">
+        <el-form-item prop="server_address" label="服务器地址">
+          <el-input v-model="form.server_address" placeholder="请输入服务器地址" />
+        </el-form-item>
         <el-form-item prop="user_name" label="用户名">
           <el-input v-model="form.user_name" />
         </el-form-item>
@@ -44,7 +47,7 @@ export default {
     return {
       isAgree: true, // 是否同意用户协议
       form: {
-        user_type: 'TEACHER',
+        user_type: 'USER',
         user_name: '',
         password: '',
         is_password_md5: 'true',

+ 21 - 0
src/views/personal_center/index.vue

@@ -0,0 +1,21 @@
+<template>
+  <div class="personal-center"></div>
+</template>
+
+<script>
+export default {
+  name: 'PersonalCenter',
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.personal-center {
+  @include page-base;
+}
+</style>

+ 28 - 0
src/views/personal_workbench/check_task/index.vue

@@ -0,0 +1,28 @@
+<template>
+  <div class="check-task">
+    <MenuPage cur-key="check_task" />
+  </div>
+</template>
+
+<script>
+import MenuPage from '../menu.vue';
+
+export default {
+  name: 'CheckTaskPage',
+  components: {
+    MenuPage,
+  },
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.check-task {
+  @include page-base;
+}
+</style>

+ 99 - 0
src/views/personal_workbench/edit_task/edit/index.vue

@@ -0,0 +1,99 @@
+<template>
+  <div class="edit-task">
+    <div class="edit-task__header">
+      <div class="menu">
+        <span></span>
+        <span class="link" @click="goBackBookList">返回教材列表</span>
+      </div>
+      <div class="courseware">
+        <span class="name-path">{{ courseware_info.name_path }}</span>
+        <div class="operator">
+          <span class="link" @click="saveCoursewareContent('edit')">退出编辑</span>
+          <span class="link" @click="saveCoursewareContent">保存</span>
+        </div>
+      </div>
+    </div>
+
+    <CreatePage ref="create" class="edit-task_content" @goBackPreview="goBackPreview" />
+  </div>
+</template>
+
+<script>
+import CreatePage from '@/views/book/courseware/create/index.vue';
+
+import { GetBookCoursewareInfo } from '@/api/project';
+
+export default {
+  name: 'EditTaskPage',
+  components: {
+    CreatePage,
+  },
+  data() {
+    return {
+      id: this.$route.params.courseware_id,
+      courseware_info: {},
+    };
+  },
+  created() {
+    this.getBookCoursewareInfo();
+  },
+  methods: {
+    /**
+     * 得到教材课件信息
+     */
+    getBookCoursewareInfo() {
+      GetBookCoursewareInfo({ id: this.id }).then(({ courseware_info }) => {
+        this.courseware_info = courseware_info;
+      });
+    },
+    goBackBookList() {
+      this.$router.push({ path: '/personal_workbench/edit_task', query: { book_id: this.courseware_info.book_id } });
+    },
+    saveCoursewareContent(type = '') {
+      this.$refs.create.saveCoursewareContent(type);
+    },
+    goBackPreview() {
+      this.$router.push({ path: `/personal_workbench/edit_task/preview/${this.id}` });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.edit-task {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+
+  &__header {
+    display: flex;
+    align-items: center;
+    border-top: $border;
+    border-bottom: $border;
+
+    .menu {
+      display: flex;
+      justify-content: space-between;
+      width: 360px;
+      padding: 4px 8px;
+      border-right: $border;
+    }
+
+    .courseware {
+      display: flex;
+      flex-grow: 1;
+      align-items: center;
+      justify-content: space-between;
+      padding: 4px 8px;
+
+      .name-path {
+        font-size: 14px;
+      }
+    }
+  }
+
+  &__content {
+    flex: 1;
+  }
+}
+</style>

+ 199 - 0
src/views/personal_workbench/edit_task/index.vue

@@ -0,0 +1,199 @@
+<template>
+  <div class="edit-task">
+    <MenuPage />
+
+    <div class="edit-main">
+      <div class="textbook-container">
+        <div class="title">待编辑的教材</div>
+        <div class="list-header">
+          <span class="cell">编号</span>
+          <span class="cell">名称</span>
+        </div>
+        <ul class="textbook-list">
+          <li
+            v-for="{ id, name } in project_list"
+            :key="id"
+            class="textbook-item"
+            :class="{ active: cur_project_id === id }"
+            @click="selectProject(id)"
+          >
+            <span class="cell">{{ id }}</span>
+            <span class="cell">{{ name }}</span>
+          </li>
+        </ul>
+      </div>
+      <div class="textbook-chapter">
+        <div v-for="chapter in courseware_list" :key="chapter.id" class="chapter-item">
+          <span class="path" @click="navigateToChapter(chapter.id)">{{ chapter.name_path }}</span>
+          <span class="status">{{ chapter.status_name }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import MenuPage from '../menu.vue';
+
+import { PageQueryMyProjectList_Producer } from '@/api/list';
+import { GetMyBookCoursewareTaskList } from '@/api/project';
+
+export default {
+  name: 'EditTask',
+  components: {
+    MenuPage,
+  },
+  data() {
+    return {
+      project_list: [], // 项目列表
+      cur_project_id: '', // 当前选中的项目ID
+      courseware_list: [], // 课程列表
+    };
+  },
+  created() {
+    this.queryMyProjectList_Producer();
+  },
+  methods: {
+    /**
+     * 查询我的项目列表
+     */
+    queryMyProjectList_Producer() {
+      PageQueryMyProjectList_Producer({ page_capacity: 50, cur_page: 1 }).then(({ project_list }) => {
+        this.project_list = project_list;
+      });
+    },
+    selectProject(id) {
+      this.cur_project_id = id;
+      this.getMyBookCoursewareTaskList();
+    },
+    /**
+     * 得到我的教材课件任务列表
+     */
+    getMyBookCoursewareTaskList() {
+      GetMyBookCoursewareTaskList({ project_id: this.cur_project_id }).then(({ courseware_list }) => {
+        this.courseware_list = courseware_list;
+      });
+    },
+    /**
+     * 跳转到章节预览页面
+     * @param {string} id 课件ID
+     */
+    navigateToChapter(id) {
+      this.$router.push({ path: `/personal_workbench/edit_task/preview/${id}` });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.edit-task {
+  @include page-base;
+
+  .edit-main {
+    display: flex;
+    flex: 1;
+    width: 100%;
+    height: 100%;
+    border-top: $border;
+
+    .textbook-container {
+      display: flex;
+      flex-direction: column;
+      width: 450px;
+      border-right: $border;
+
+      .title {
+        padding-left: 12px;
+        font-size: 14px;
+        font-weight: bold;
+        color: $font-light-color;
+      }
+
+      .list-header {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        height: 40px;
+        padding: 0 12px;
+        font-size: 14px;
+        color: $font-light-color;
+        background-color: #eee;
+        border-radius: 4px;
+
+        .cell {
+          font-size: 14px;
+          font-weight: bold;
+          text-align: center;
+
+          &:first-child {
+            width: 300px;
+          }
+
+          &:last-child {
+            width: 150px;
+          }
+        }
+      }
+
+      .textbook-list {
+        display: flex;
+        flex-direction: column;
+
+        .textbook-item {
+          display: flex;
+          align-items: center;
+          height: 40px;
+          padding: 0 12px;
+          font-size: 14px;
+          color: $font-light-color;
+          cursor: pointer;
+          background-color: #fff;
+          border-radius: 4px;
+
+          &:hover {
+            background-color: #f5f5f5;
+          }
+
+          &.active {
+            background-color: $main-active-color;
+          }
+
+          .cell {
+            text-align: center;
+
+            &:first-child {
+              width: 300px;
+            }
+
+            &:last-child {
+              width: 150px;
+            }
+          }
+        }
+      }
+    }
+
+    .textbook-chapter {
+      display: flex;
+      flex: 1;
+      flex-direction: column;
+
+      .chapter-item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 8px 12px;
+        font-size: 14px;
+        border-bottom: $border;
+
+        .path {
+          color: $main-color;
+          cursor: pointer;
+        }
+      }
+    }
+  }
+}
+</style>

+ 142 - 0
src/views/personal_workbench/edit_task/preview/index.vue

@@ -0,0 +1,142 @@
+<template>
+  <div class="task-preview">
+    <div class="task-preview__header">
+      <div class="menu">
+        <span></span>
+        <span class="link" @click="goBackBookList">返回教材列表</span>
+      </div>
+      <div class="courseware">
+        <span class="name-path">{{ courseware_info.name_path }}</span>
+        <div class="operator">
+          <span class="link" @click="editTask">开始编辑</span>
+          <span class="link">提交审核</span>
+        </div>
+      </div>
+    </div>
+    <div
+      class="task-preview__content"
+      :style="[
+        {
+          backgroundImage: background.background_image_url ? `url(${background.background_image_url})` : '',
+          backgroundSize: background.background_image_url
+            ? `${background.background_position.width}% ${background.background_position.height}%`
+            : '',
+          backgroundPosition: background.background_image_url
+            ? `${background.background_position.left}% ${background.background_position.top}%`
+            : '',
+        },
+      ]"
+    >
+      <PreviewEdit :row-list="data.row_list" :component-list="component_list" />
+    </div>
+  </div>
+</template>
+
+<script>
+import { GetBookCoursewareInfo } from '@/api/project';
+import { ContentGetCoursewareContent_View } from '@/api/book';
+
+import PreviewEdit from '@/views/book/courseware/create/components/PreviewEdit.vue';
+
+export default {
+  name: 'TaskPreviewPage',
+  components: {
+    PreviewEdit,
+  },
+  data() {
+    return {
+      id: this.$route.params.id,
+      courseware_info: {},
+      background: {
+        background_image_url: '',
+        background_position: {
+          left: 0,
+          top: 0,
+          width: 100,
+          height: 100,
+        },
+      },
+      data: {
+        row_list: [],
+      },
+      component_list: [],
+    };
+  },
+  created() {
+    this.getBookCoursewareInfo();
+    this.getCoursewareComponentContent_View();
+  },
+  methods: {
+    goBackBookList() {
+      this.$router.push({ path: '/personal_workbench/edit_task' });
+    },
+    editTask() {
+      this.$router.push({ path: `/personal_workbench/edit_task/edit/${this.id}` });
+    },
+    getCoursewareComponentContent_View() {
+      ContentGetCoursewareContent_View({ id: this.id }).then(({ content, component_list }) => {
+        if (content) this.data = JSON.parse(content);
+        if (component_list) this.component_list = component_list;
+      });
+    },
+    /**
+     * 得到教材课件信息
+     */
+    getBookCoursewareInfo() {
+      GetBookCoursewareInfo({ id: this.id }).then(({ courseware_info }) => {
+        this.courseware_info = courseware_info;
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.task-preview {
+  @include page-base;
+
+  background-color: $main-background-color;
+
+  &__header {
+    display: flex;
+    align-items: center;
+    border-top: $border;
+    border-bottom: $border;
+
+    .menu {
+      display: flex;
+      justify-content: space-between;
+      width: 360px;
+      padding: 4px 8px;
+      border-right: $border;
+    }
+
+    .courseware {
+      display: flex;
+      flex-grow: 1;
+      align-items: center;
+      justify-content: space-between;
+      padding: 4px 8px;
+
+      .name-path {
+        font-size: 14px;
+      }
+    }
+  }
+
+  &__content {
+    display: flex;
+    flex: 1;
+    flex-direction: column;
+    row-gap: 6px;
+    width: $courseware-width;
+    padding: 24px;
+    margin: 0 auto;
+    background-color: #fff;
+    background-repeat: no-repeat;
+    border-radius: 4px;
+  }
+}
+</style>

+ 86 - 0
src/views/personal_workbench/menu.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="menu">
+    <ul class="menu-list">
+      <li
+        v-for="({ key, name }, i) in subMenuList"
+        :key="key"
+        :class="['menu-item', { active: currentIndex === i }]"
+        @click="changeSubMenu(i)"
+      >
+        {{ name }}
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'MenuPage',
+  props: {
+    curKey: {
+      type: String,
+      default: 'edit_task',
+    },
+  },
+  data() {
+    return {
+      currentIndex: 0, // 当前选中的菜单索引
+      // 子菜单列表
+      subMenuList: [
+        { key: 'edit_task', name: '我的编辑任务' },
+        {
+          key: 'check_task',
+          name: '我的审校任务',
+        },
+        { key: 'project', name: '我的项目' },
+      ],
+    };
+  },
+  created() {
+    // 根据当前路由的路径设置当前选中的菜单索引
+    this.currentIndex = this.subMenuList.findIndex((item) => item.key === this.curKey);
+  },
+  methods: {
+    /**
+     * 切换子菜单
+     * @param {number} index - 子菜单索引
+     */
+    changeSubMenu(index) {
+      this.currentIndex = index;
+      this.$router.push({ path: `/personal_workbench/${this.subMenuList[index].key}` });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.menu {
+  display: flex;
+
+  &-list {
+    display: flex;
+    justify-content: space-around;
+    background-color: #f5f5f5;
+    border-radius: 8px;
+
+    .menu-item {
+      padding: 10px 20px;
+      cursor: pointer;
+      background-color: #ebebeb;
+      border: $border;
+      border-radius: 4px;
+      transition: background-color 0.3s ease;
+
+      &:hover {
+        background-color: #e0e0e0;
+      }
+
+      &.active {
+        font-weight: bold;
+        background-color: #fff;
+        box-shadow: 0 2px 4px #e6e6e6;
+      }
+    }
+  }
+}
+</style>

+ 77 - 0
src/views/personal_workbench/project/AddChapter.vue

@@ -0,0 +1,77 @@
+<template>
+  <el-dialog title="添加章节" :visible="visible" width="460px" :show-close="false" @close="dialogClose">
+    <el-form :model="data" :rules="rules" label-width="80px">
+      <el-form-item label="章节名称" prop="name">
+        <el-input v-model="data.name" placeholder="请输入章节名称" />
+      </el-form-item>
+    </el-form>
+    <div slot="footer">
+      <el-button @click="dialogClose">取消</el-button>
+      <el-button type="primary" @click="addChapterNode">确定</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: 'AddChapter',
+  props: {
+    visible: {
+      type: Boolean,
+      required: true,
+    },
+    id: {
+      type: String,
+      default: '',
+    },
+    parentId: {
+      type: String,
+      default: '',
+    },
+    curSelectId: {
+      type: String,
+      default: '',
+    },
+    isLeaf: {
+      type: String,
+      default: 'false',
+    },
+    addType: {
+      type: String,
+      default: 'chapter',
+    },
+  },
+  data() {
+    return {
+      data: {
+        name: '',
+        book_id: this.id,
+      },
+      rules: {
+        name: [{ required: true, message: '请输入章节名称', trigger: 'blur' }],
+      },
+    };
+  },
+  methods: {
+    dialogClose() {
+      this.$emit('close');
+      this.data.name = '';
+    },
+    addChapterNode() {
+      if (this.addType === 'chapter') {
+        this.$emit('addChapterNode', {
+          ...this.data,
+          parent_id: this.parentId,
+          is_leaf: this.isLeaf,
+        });
+      } else {
+        this.$emit('addCourseware', {
+          ...this.data,
+          chapter_id: this.curSelectId,
+        });
+      }
+      this.data.name = '';
+    },
+  },
+};
+</script>

+ 453 - 0
src/views/personal_workbench/project/ProductionEditorialManage.vue

@@ -0,0 +1,453 @@
+<template>
+  <div class="production-editorial">
+    <div class="top-operate">
+      <span class="name">{{ project_info.name }}</span>
+      <span class="button" @click="$router.push({ path: `/personal_workbench/project` })">返回项目列表</span>
+      <div class="audit-step">
+        <span>审核步骤:</span>
+        <span v-for="(node, index) in audit_node_list" :key="index" class="audit-node">
+          {{ node }}
+          <span v-if="index !== audit_node_list.length - 1">→</span>
+        </span>
+      </div>
+      <span class="button">设置审校步骤</span>
+      <div class="operator">
+        <span class="button" @click="addChapterDialog">添加章节节点</span>
+      </div>
+    </div>
+    <div class="chapters-container">
+      <div class="list-title">
+        <span class="title-cell">教材章节结构</span>
+        <span class="title-cell">制作人</span>
+        <span class="title-cell">审校人</span>
+        <span class="title-cell">状态</span>
+        <span class="title-cell">操作</span>
+      </div>
+      <div v-for="{ id, name, nodes: children, producer_list } in nodes" :key="id" class="catalogue">
+        <!-- 一级目录 -->
+        <div
+          :class="['first-level', { active: curSelectId === id }]"
+          @click="selectActiveChapter(id, '', 'false', 'chapter')"
+        >
+          <div class="chapter-title">{{ name }}</div>
+          <div class="producer">
+            <span>{{ producer_list.map((producer) => producer.name).join(';') }}</span>
+          </div>
+          <div class="audit"></div>
+          <div class="status"></div>
+          <div class="operator">
+            <span class="link">修改</span>
+            <span class="link" @click="openSetProducer(id)">设置制作人</span>
+            <span class="link danger" @click="deleteChapter(id)">删除</span>
+          </div>
+        </div>
+        <template v-for="item in children">
+          <!-- 二级目录或内容 -->
+          <div
+            :key="item.id"
+            :class="[
+              'catalogue-item',
+              { active: curSelectId === item.id },
+              item.is_leaf_chapter === 'true' ? 'content' : 'subdirectory',
+            ]"
+            @click="selectActiveChapter(item.id, id, 'false', 'courseware')"
+          >
+            <div class="name">{{ item.name }}</div>
+            <div class="producer">
+              <span>{{ item.producer_list.map((producer) => producer.name).join(';') }}</span>
+            </div>
+            <div class="audit"></div>
+            <div class="status"></div>
+            <div class="operator">
+              <span class="link">修改</span>
+              <span class="link" @click="openSetProducer(id)">设置制作人</span>
+              <span class="link danger" @click="deleteChapter(id)">删除</span>
+            </div>
+          </div>
+          <!-- 二级目录的内容 -->
+          <template v-if="item.is_leaf_chapter === 'false' && item.nodes?.length > 0">
+            <div v-for="li in item.nodes" :key="li.id">
+              <div :class="['catalogue-item', 'children']">
+                <div class="name">{{ li.name }}</div>
+                <div class="producer">
+                  <span>{{ li.producer_list.map((producer) => producer.name).join(';') }}</span>
+                </div>
+                <div class="audit"></div>
+                <div class="status"></div>
+                <div class="operator">
+                  <span class="link">修改</span>
+                  <span class="link" @click="openSetProducer(li.id)">设置制作人</span>
+                  <span class="link">设置审校人</span>
+                  <span class="link danger" @click="deleteChapter(li.id)">删除</span>
+                </div>
+              </div>
+            </div>
+          </template>
+        </template>
+      </div>
+    </div>
+
+    <AddChapter
+      :id="book_id"
+      :visible="visible"
+      :parent-id="parent_id"
+      :cur-select-id="curSelectId"
+      :is-leaf="is_leaf"
+      :add-type="addType"
+      @close="closeAddChapter"
+      @addChapterNode="addChapterNode"
+      @addCourseware="chapterAddCoursewareToBook"
+    />
+
+    <SetProducer
+      :id="producer.id"
+      :visible="producer.visible"
+      :member-list="member_list"
+      @close="producer.visible = false"
+      @chapterSetProducer="chapterSetProducer"
+    />
+  </div>
+</template>
+
+<script>
+import AddChapter from './AddChapter.vue';
+import SetProducer from './SetProducer.vue';
+
+import { GetProjectBaseInfo } from '@/api/project';
+import {
+  ChapterGetBookChapterStruct,
+  ChapterAddChapterToBook,
+  ChapterAddCoursewareToBook,
+  ChapterDeleteChapter,
+  ChapterSetProducer,
+} from '@/api/book';
+
+export default {
+  name: 'ProductionEditorialManage',
+  components: {
+    AddChapter,
+    SetProducer,
+  },
+  data() {
+    return {
+      book_id: this.$route.params.id,
+      visible: false,
+      curSelectId: '', // 当前选中的章节ID
+      parent_id: '', // 父节点ID
+      is_leaf: false, // 是否是叶子节点
+      addType: 'chapter', // 添加类型
+      project_info: {}, // 项目基本信息
+      member_list: [], // 项目成员列表
+      book_info_PBE: {}, // 与项目绑定的的教材实体(project_bind_entity)信息
+      audit_node_list: [], // 审校流程节点列表
+      nodes: [], // 教材章节结构
+      producer: {
+        visible: false, // 设置制作人弹窗
+        id: '', // 章节ID
+      },
+    };
+  },
+  created() {
+    this.getProjectBaseInfo();
+    this.getBookChapterStruct();
+  },
+  methods: {
+    /**
+     * 获取项目基本信息
+     * @param {string} id - 项目ID
+     */
+    getProjectBaseInfo() {
+      GetProjectBaseInfo({ id: this.book_id }).then(({ project_info, book_info_PBE, audit_node_list, member_list }) => {
+        this.project_info = project_info;
+        this.member_list = member_list;
+        this.book_info_PBE = book_info_PBE;
+        this.audit_node_list = audit_node_list;
+      });
+    },
+    /**
+     * 获取教材章节结构
+     * @param {string} book_id - 教材ID
+     * @param {number} node_deep_mode - 节点深度模式
+     * @param {boolean} is_contain_producer - 是否包含制作人
+     */
+    getBookChapterStruct() {
+      ChapterGetBookChapterStruct({ book_id: this.book_id, node_deep_mode: 0, is_contain_producer: 'true' }).then(
+        ({ nodes }) => {
+          this.nodes = nodes;
+        },
+      );
+    },
+    /**
+     * 选择当前章节
+     * @param id - 父章节ID
+     * @param is_leaf - 是否是叶子节点
+     */
+    selectActiveChapter(id, parentId = '', is_leaf = 'false', type = 'chapter') {
+      this.curSelectId = id;
+      this.parent_id = parentId;
+      this.is_leaf = is_leaf;
+      this.addType = type;
+    },
+    chapterAddCoursewareToBook(data) {
+      ChapterAddCoursewareToBook(data).then(() => {
+        this.getBookChapterStruct();
+        this.visible = false;
+      });
+    },
+    addChapterDialog() {
+      this.visible = true;
+    },
+    closeAddChapter() {
+      this.visible = false;
+    },
+    /**
+     * 添加章节
+     */
+    addChapterNode(data) {
+      ChapterAddChapterToBook(data).then(() => {
+        this.getBookChapterStruct();
+        this.visible = false;
+      });
+    },
+    /**
+     * 删除章节
+     * @param {string} id - 章节ID
+     */
+    deleteChapter(id) {
+      ChapterDeleteChapter({ id, is_force_delete: 'true' }).then(() => {
+        this.getBookChapterStruct();
+        this.$message.success('删除成功');
+      });
+    },
+    openSetProducer(id) {
+      this.producer.visible = true;
+      this.producer.id = id;
+    },
+    /**
+     * 设置制作人
+     * @param {Object} data - 章节制作人数据
+     * @param {string} data.node_id - 章节ID
+     * @param {string} data.producer_id_list - 制作人ID列表
+     */
+    chapterSetProducer(data) {
+      ChapterSetProducer({ book_id: this.book_id, ...data })
+        .then(() => {
+          this.getBookChapterStruct();
+          this.producer.visible = false;
+          this.$message.success('制作人设置成功');
+        })
+        .catch(({ error }) => {
+          this.$message.error(`设置制作人失败: ${error}`);
+        });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.production-editorial {
+  @include page-base;
+
+  .top-operate {
+    display: flex;
+    padding: 6px 4px;
+    border-top: $border;
+    border-bottom: $border;
+
+    .name {
+      width: 240px;
+      font-size: 16px;
+      font-weight: bold;
+    }
+
+    .button {
+      padding: 0 8px;
+      color: $main-color;
+      cursor: pointer;
+    }
+
+    .audit-step {
+      padding: 0 12px;
+      border-left: $border;
+
+      .audit-node {
+        &:last-child {
+          margin-right: 18px;
+          color: $danger-color;
+        }
+      }
+    }
+
+    .operator {
+      display: flex;
+      flex: 1;
+      justify-content: flex-end;
+      border-left: $border;
+    }
+  }
+
+  .chapters-container {
+    flex: 1;
+
+    .list-title {
+      display: flex;
+      align-items: center;
+
+      .title-cell {
+        padding: 8px 12px;
+        font-size: 14px;
+        font-weight: bold;
+        color: $main-color;
+        text-align: center;
+        background-color: $main-background-color;
+        border: $border;
+
+        &:not(:last-child) {
+          border-right: none;
+        }
+
+        &:first-child {
+          width: 550px;
+        }
+
+        &:nth-child(2) {
+          width: 200px;
+        }
+
+        &:nth-child(3) {
+          width: 200px;
+        }
+
+        &:nth-child(4) {
+          width: 140px;
+        }
+
+        &:last-child {
+          flex: 1;
+          min-width: 200px;
+        }
+      }
+    }
+
+    .catalogue {
+      display: flex;
+      flex-direction: column;
+      border: $border;
+      border-top: none;
+
+      .active {
+        background-color: $main-active-color;
+      }
+
+      .first-level {
+        display: flex;
+        align-items: center;
+
+        > div {
+          height: 40px;
+          padding: 8px;
+        }
+
+        .chapter-title {
+          font-weight: bold;
+          color: #000;
+        }
+
+        .operator {
+          text-align: center;
+        }
+
+        > :first-child {
+          width: 550px;
+        }
+
+        > :nth-child(2) {
+          width: 200px;
+        }
+
+        > :nth-child(3) {
+          width: 200px;
+        }
+
+        > :nth-child(4) {
+          width: 140px;
+        }
+
+        > :last-child {
+          flex: 1;
+          min-width: 200px;
+        }
+
+        > :not(:last-child) {
+          border-right: $border;
+        }
+      }
+
+      &-item {
+        display: flex;
+        font-size: 14px;
+        border-top: $border;
+
+        &:hover {
+          background-color: #f3f3f3;
+        }
+
+        &.subdirectory {
+          .name {
+            font-weight: bold;
+          }
+        }
+
+        &.children {
+          padding-left: 32px;
+
+          > :first-child {
+            width: 518px;
+          }
+        }
+
+        .name {
+          padding-left: 24px;
+          color: #000;
+        }
+
+        > div {
+          height: 40px;
+          padding: 8px;
+        }
+
+        .operator {
+          text-align: center;
+        }
+
+        > :first-child {
+          width: 550px;
+        }
+
+        > :nth-child(2) {
+          width: 200px;
+        }
+
+        > :nth-child(3) {
+          width: 200px;
+        }
+
+        > :nth-child(4) {
+          width: 140px;
+        }
+
+        > :last-child {
+          flex: 1;
+          min-width: 200px;
+        }
+
+        > :not(:last-child) {
+          border-right: $border;
+        }
+      }
+    }
+  }
+}
+</style>

+ 53 - 0
src/views/personal_workbench/project/SetProducer.vue

@@ -0,0 +1,53 @@
+<template>
+  <el-dialog title="设置制作人" :visible="visible" width="320px" :show-close="false" @close="dialogClose">
+    <el-select v-model="producer" placeholder="请选择制作人" multiple>
+      <el-option v-for="item in memberList" :key="item.id" :label="item.name" :value="item.id" />
+    </el-select>
+
+    <div slot="footer">
+      <el-button @click="dialogClose">取消</el-button>
+      <el-button type="primary" @click="addChapterNode">确定</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+export default {
+  name: 'SetProducer',
+  props: {
+    visible: {
+      type: Boolean,
+      required: true,
+    },
+    id: {
+      type: String,
+      default: '',
+    },
+    memberList: {
+      type: Array,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      producer: [],
+    };
+  },
+  methods: {
+    dialogClose() {
+      this.$emit('close');
+      this.producer = [];
+    },
+    addChapterNode() {
+      this.$emit('chapterSetProducer', { node_id: this.id, producer_id_list: this.producer });
+      this.producer = [];
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.el-select {
+  width: 100%;
+}
+</style>

+ 84 - 0
src/views/personal_workbench/project/index.vue

@@ -0,0 +1,84 @@
+<template>
+  <div class="project">
+    <MenuPage cur-key="project" />
+
+    <div class="project-list">
+      <el-table :data="list">
+        <el-table-column prop="sn" label="编号" width="140" />
+        <el-table-column prop="name" label="名称" width="240" />
+        <el-table-column prop="create_time" label="创建时间" width="180" />
+        <el-table-column prop="leader_name_desc" label="项目组长" width="260" />
+        <el-table-column prop="member_name_desc" label="项目成员" />
+
+        <el-table-column prop="operation" label="操作" fixed="right" width="300">
+          <template slot-scope="{ row }">
+            <span class="link" @click="updateProject(row.id)">修改信息</span>
+            <span class="link" @click="productionEditorialManage(row.id)">制作与审校管理</span>
+            <span class="link danger">删除</span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <PaginationPage ref="pagination" :total="total" @getList="queryMyProjectList_Leader" />
+    </div>
+  </div>
+</template>
+
+<script>
+import MenuPage from '../menu.vue';
+import PaginationPage from '@/components/PaginationPage.vue';
+
+import { PageQueryMyProjectList_Leader } from '@/api/list.js';
+
+export default {
+  name: 'ProjectPage',
+  components: {
+    MenuPage,
+    PaginationPage,
+  },
+  data() {
+    return {
+      list: [],
+      total: 0,
+    };
+  },
+  methods: {
+    queryMyProjectList_Leader(data) {
+      PageQueryMyProjectList_Leader(data).then(({ total_count, project_list }) => {
+        this.total = total_count;
+        this.list = project_list;
+      });
+    },
+    /**
+     * 修改项目
+     * @param {string} id - 项目ID
+     */
+    updateProject(id) {},
+    /**
+     * 制作与审校管理
+     * @param {string} id - 项目ID
+     */
+    productionEditorialManage(id) {
+      this.$router.push({ path: `/personal_workbench/production_editorial/${id}` });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.project {
+  @include page-base;
+
+  &-list {
+    display: flex;
+    flex: 1;
+    flex-direction: column;
+
+    .el-table {
+      flex: 1;
+    }
+  }
+}
+</style>

+ 21 - 0
src/views/project_manage/index.vue

@@ -0,0 +1,21 @@
+<template>
+  <div class="project-manage"></div>
+</template>
+
+<script>
+export default {
+  name: 'ProjectManage',
+  data() {
+    return {};
+  },
+  methods: {},
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.project-manage {
+  @include page-base;
+}
+</style>

+ 12 - 0
vue.config.js

@@ -22,6 +22,11 @@ const fileApiUrlList = {
   '/file': 'https://file-kf.helxsoft.cn/',
 };
 
+const eepApiUrlList = {
+  '': '',
+  '/eep': 'http://eep.helxsoft.cn/',
+};
+
 const proxy = {};
 
 let name = '智慧梧桐桌面端互动教材编辑器';
@@ -37,6 +42,13 @@ if (NODE_ENV === 'development') {
       [`^${process.env.VUE_APP_BASE_API}`]: '',
     },
   };
+  proxy[process.env.VUE_APP_EEP] = {
+    target: eepApiUrlList[process.env.VUE_APP_EEP],
+    changeOrigin: true,
+    pathRewrite: {
+      [`^${process.env.VUE_APP_EEP}`]: '',
+    },
+  };
   proxy[process.env.VUE_APP_FILE] = {
     target: fileApiUrlList[process.env.VUE_APP_FILE],
     changeOrigin: true,