2
0

2 Commits a6ab4c58c9 ... 15b6d721b8

Autor SHA1 Nachricht Datum
  dusenyao 15b6d721b8 Merge branch 'master' of http://gcls-git.helxsoft.cn/GCLS/eep_page vor 1 Monat
  dusenyao f53b368911 修改预览、编辑,增加调整位置 vor 1 Monat

+ 2 - 2
package.json

@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "private": true,
   "main": "main.js",
-  "description": "An Electron & Vue.js quick start boilerplate",
+  "description": "国际中文智慧教育引擎互动教材编辑器",
   "author": "合新软件",
   "scripts": {
     "electron": "nodemon --watch main.js --exec \"electron .\"",
@@ -73,7 +73,7 @@
   },
   "build": {
     "appId": "com.gcls.page.textbook",
-    "productName": "智慧梧桐桌面端互动教材编辑器",
+    "productName": "国际中文智慧教育引擎互动教材编辑器",
     "copyright": "Copyright © 2025 ${author}",
     "directories": {
       "output": "out"

+ 0 - 2
src/api/app.js

@@ -12,7 +12,6 @@ export function GetLogo(data) {
 
 /**
  * 得到文件 ID 与 URL 的映射
- * TODO: 这个接口需要移动到 eep 服务中
  */
 export function GetFileURLMap(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=file_store_manager-GetFileURLMap`, data);
@@ -20,7 +19,6 @@ export function GetFileURLMap(data) {
 
 /**
  * 得到文件存储信息
- * TODO: 这个接口需要移动到 eep 服务中
  */
 export function GetFileStoreInfo(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=file_store_manager-GetFileStoreInfo`, data);

+ 1 - 2
src/api/book.js

@@ -2,10 +2,9 @@ import { http } from '@/utils/http';
 
 /**
  * 为文本生成拼音解析文本
- * TODO: 这个接口需要移动到 eep 服务中
  */
 export function CrateParsedTextInfo_Pinyin(data) {
-  return http.post(`/TeachingServer/TextAnalyser/CrateParsedTextInfo_Pinyin`, data);
+  return http.post(`/OtherSysTool/TextAnalyser/CrateParsedTextInfo_Pinyin`, data);
 }
 
 /**

+ 2 - 2
src/layouts/default/breadcrumb/index.vue

@@ -35,8 +35,8 @@ export default {
   },
   methods: {
     getBreadcrumb() {
-      this.breadcrumb = this.$route.matched.filter((item) => item?.meta && item.meta.title);
-      this.$emit('update:isShowBreadcrumb', this.isShowBreadcrumb);
+      // this.breadcrumb = this.$route.matched.filter((item) => item?.meta && item.meta.title);
+      // this.$emit('update:isShowBreadcrumb', this.isShowBreadcrumb);
     },
   },
 };

+ 50 - 0
src/styles/mixin.scss

@@ -87,7 +87,57 @@
   min-height: 100%;
   padding: 5px;
   margin: 0 5px;
+  text-align: center;
   background-color: #fff;
   border-radius: 4px;
   box-shadow: 0 2px 4px rgba(0, 0, 0, 10%);
 }
+
+@mixin page-content($isMenu: false) {
+  display: flex;
+  flex-direction: column;
+  min-width: 1100px;
+  height: calc(100% - 51px);
+
+  > .menu {
+    position: sticky;
+    top: 5px;
+    left: 0;
+    margin: 5px 10px;
+  }
+
+  &__header {
+    position: sticky;
+    top: if($isMenu, 56px, 5px);
+    left: 0;
+    z-index: 9;
+    display: flex;
+    align-items: center;
+    height: 46px;
+    padding: 6px 4px;
+    margin-bottom: 5px;
+    background-color: #fff;
+    border-top: $border;
+    border-bottom: $border;
+
+    .button {
+      padding: 0 8px;
+      color: $main-color;
+      cursor: pointer;
+    }
+  }
+
+  > main {
+    display: flex;
+    flex-direction: column;
+    row-gap: 5px;
+    min-width: 1100px;
+    min-height: if($isMenu, calc(100% - 57px), 100%);
+    padding: 5px;
+    margin: 0 5px;
+    text-align: center;
+    background-color: #fff;
+    border-radius: 4px;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 10%);
+  }
+}

+ 102 - 84
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -14,90 +14,94 @@
       },
     ]"
   >
-    <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 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>
     </template>
-    <span class="drag-line" :data-row="data.row_list.length - 1"></span>
+
+    <PreviewEdit v-else :courseware-id="courseware_id" :row-list="data.row_list" @computedMoveData="computedMoveData" />
   </main>
 </template>
 
@@ -114,6 +118,12 @@ export default {
     PreviewEdit,
   },
   inject: ['getCurSettingId'],
+  props: {
+    isEdit: {
+      type: Boolean,
+      required: true,
+    },
+  },
   data() {
     const { book_id, chapter_id } = this.$route.query;
 
@@ -161,6 +171,7 @@ 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;
@@ -246,9 +257,12 @@ export default {
       }).then(() => {
         this.$message.success('保存成功');
         loading.close();
-        if (type === 'quit' || type === 'edit') {
+        if (type === 'quit') {
           this.$emit('back');
         }
+        if (type === 'edit') {
+          this.$emit('changeEditStatus');
+        }
       });
     },
     setBackgroundImage(url, position) {
@@ -683,6 +697,8 @@ export default {
       this.drag.clientX = clientX;
       this.drag.clientY = clientY;
 
+      if (!this.isEdit) return; // 非编辑状态不允许显隐线
+
       let { isInsideCanvas } = this.getMarginDifferences();
 
       this.enterCanvas = isInsideCanvas;
@@ -737,6 +753,8 @@ export default {
         this.drag.dragging = false;
       }
 
+      if (!this.isEdit) return;
+
       if (this.enterCanvas) {
         if (this.curRow >= -1 && this.curCol <= -1) {
           this.calculateRowInsertedObject();

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

@@ -26,9 +26,11 @@
               </template>
               <component
                 :is="previewComponentList[grid.type]"
+                :id="grid.id"
                 ref="preview"
                 :key="`preview-${grid.id}`"
-                :content="computedColContent(grid.id)"
+                :courseware-id="coursewareId"
+                type="edit"
                 :class="[grid.id]"
                 :style="{
                   gridArea: 'preview',
@@ -60,8 +62,8 @@ export default {
       type: Array,
       required: true,
     },
-    componentList: {
-      type: Array,
+    coursewareId: {
+      type: String,
       required: true,
     },
   },
@@ -116,15 +118,6 @@ export default {
     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;

+ 23 - 1
src/views/book/courseware/create/components/question/input/InputSetting.vue

@@ -32,11 +32,31 @@
         </el-select>
       </el-form-item>
 
-      <el-form-item label="输入框文字样式">
+      <el-form-item label="输入框字体" label-width="90px">
         <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-item label="输入框字号" label-width="90px">
+        <el-select v-model="property.font_size">
+          <el-option v-for="size in fontSizeList" :key="size.value" :label="size" :value="size" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="输入框对齐方式" label-width="120px">
+        <el-select v-model="property.text_align">
+          <el-option v-for="{ value, label } in textAlignList" :key="value" :label="label" :value="value" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="输入框文本颜色" label-width="120px">
+        <el-color-picker v-model="property.text_color" :show-alpha="true" />
+      </el-form-item>
+
+      <el-form-item label="输入框背景颜色" label-width="120px">
+        <el-color-picker v-model="property.background_color" :show-alpha="true" />
+      </el-form-item>
     </el-form>
   </div>
 </template>
@@ -50,6 +70,7 @@ import {
   inputStyleList,
   fontList,
   fontSizeList,
+  textAlignList,
 } from '@/views/book/courseware/data/input';
 import { positionList } from '@/views/book/courseware/data/article';
 
@@ -64,6 +85,7 @@ export default {
       inputStyleList,
       fontList,
       fontSizeList,
+      textAlignList,
       labelPosition: 'left',
     };
   },

+ 18 - 2
src/views/book/courseware/create/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div ref="create" class="create">
+  <main ref="create" class="create">
     <div class="create-left">
       <div v-for="{ value, label, children } in bookTypeOption" :key="value" class="components">
         <div class="components-title">{{ label }}</div>
@@ -24,10 +24,12 @@
 
       <CreateCanvas
         ref="createCanvas"
+        :is-edit="isEdit"
         @showSetting="showSetting"
         @showSettingEmpty="showSettingEmpty"
         @changeData="changeData"
         @back="back"
+        @changeEditStatus="changeEditStatus"
       />
     </div>
 
@@ -38,7 +40,7 @@
 
     <SelectBackground :visible.sync="visible" @setBackgroundImage="setBackgroundImage" />
     <WarnSave :visible.sync="visibleWarn" @directQuit="back" @saveAndClose="saveCoursewareContent" />
-  </div>
+  </main>
 </template>
 
 <script>
@@ -75,6 +77,7 @@ export default {
       bookTypeOption,
       visible: false,
       visibleWarn: false,
+      isEdit: true, // 是否编辑状态
       isChange: false, // 是否有改动
     };
   },
@@ -96,6 +99,12 @@ export default {
       this.isChange = true;
     },
     /**
+     * 切换编辑状态
+     */
+    changeEditStatus() {
+      this.isEdit = !this.isEdit;
+    },
+    /**
      * 保存课件内容
      * @param {string} type 是否退出
      */
@@ -104,6 +113,11 @@ export default {
         this.curSettingType = '';
         this.curSettingId = '';
       }
+      if (type === 'edit' && !this.isChange) {
+        this.changeEditStatus();
+        return;
+      }
+
       this.$refs.createCanvas.saveCoursewareContent(type);
       this.isChange = false;
     },
@@ -220,7 +234,9 @@ export default {
 
   &-right {
     width: 280px;
+    height: 100%;
     padding: 16px 8px;
+    overflow-y: auto;
     background-color: #fff;
 
     .setting-tittle {

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

@@ -61,6 +61,21 @@ export const fontList = [
 
 export const fontSizeList = [12, 14, 16, 18, 20, 22, 24, 26, 28, 30];
 
+export const textAlignList = [
+  {
+    value: 'left',
+    label: '左对齐',
+  },
+  {
+    value: 'center',
+    label: '居中对齐',
+  },
+  {
+    value: 'right',
+    label: '右对齐',
+  },
+];
+
 export function getInputProperty() {
   return {
     ...commonSetProperty,
@@ -74,6 +89,9 @@ export function getInputProperty() {
     input_style: inputStyleList[0].value, // 输入框样式
     font: fontList[1].value, // 字体
     font_size: fontSizeList[3], // 字体大小
+    text_align: textAlignList[0].value, // 文本对齐方式
+    text_color: '#1d2129', // 文本颜色
+    background_color: '#ffffff', // 背景色
   };
 }
 

+ 231 - 0
src/views/book/courseware/preview/CoursewarePreview.vue

@@ -0,0 +1,231 @@
+<template>
+  <div
+    class="courserware"
+    :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}%`
+          : '',
+      },
+    ]"
+  >
+    <template v-for="(row, i) in data.row_list">
+      <div :key="i" class="row" :style="getMultipleColStyle(i)">
+        <!-- 列 -->
+        <template v-for="(col, j) in row.col_list">
+          <div :key="j" :class="['col', `col-${i}-${j}`]" :style="computedColStyle(col)">
+            <!-- 网格 -->
+            <template v-for="(grid, k) in col.grid_list">
+              <component
+                :is="previewComponentList[grid.type]"
+                ref="preview"
+                :key="k"
+                :content="computedColContent(grid.id)"
+                :class="[grid.id]"
+                :style="{
+                  gridArea: grid.grid_area,
+                  height: grid.height,
+                }"
+              />
+            </template>
+          </div>
+        </template>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+import { previewComponentList } from '@/views/book/courseware/data/bookType';
+
+export default {
+  name: 'CoursewarePreview',
+  provide() {
+    return {
+      getDragStatus: () => false,
+      bookInfo: this.bookInfo,
+    };
+  },
+  props: {
+    data: {
+      type: Object,
+      default: () => ({}),
+    },
+    background: {
+      type: Object,
+      default: () => ({}),
+    },
+    componentList: {
+      type: Array,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      previewComponentList,
+      bookInfo: {
+        theme_color: '',
+      },
+    };
+  },
+  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.data.row_list[i];
+      let col = row.col_list;
+      if (col.length <= 1) {
+        return {
+          gridTemplateColumns: '100fr',
+        };
+      }
+      let gridTemplateColumns = row.width_list.join(' ');
+
+      return {
+        gridAutoFlow: 'column',
+        gridTemplateColumns,
+        gridTemplateRows: 'auto',
+      };
+    },
+    /**
+     * 分割整数为多个 1的倍数
+     * @param {number} num
+     * @param {number} parts
+     */
+    splitInteger(num, parts) {
+      let base = Math.floor(num / parts);
+      let arr = Array(parts).fill(base);
+      let remainder = num - base * parts;
+      for (let i = 0; remainder > 0; i = (i + 1) % parts) {
+        arr[i] += 1;
+        remainder -= 1;
+      }
+      return arr;
+    },
+    computedColStyle(col) {
+      const grid = col.grid_list;
+
+      let maxCol = 0; // 最大列数
+      let rowList = new Map();
+      grid.forEach(({ row }) => {
+        rowList.set(row, (rowList.get(row) || 0) + 1);
+      });
+      let curMaxRow = 0; // 当前数量最大 row 的值
+      rowList.forEach((value, key) => {
+        if (value > maxCol) {
+          maxCol = value;
+          curMaxRow = key;
+        }
+      });
+      // 计算 grid_template_areas
+      let gridTemplateAreas = '';
+      let gridArr = [];
+      grid.forEach(({ grid_area, row }) => {
+        if (!gridArr[row - 1]) {
+          gridArr[row - 1] = [];
+        }
+        if (curMaxRow === row) {
+          gridArr[row - 1].push(`${grid_area}`);
+        } else {
+          let filter = grid.filter((item) => item.row === row);
+          let find = filter.findIndex((item) => item.grid_area === grid_area);
+          let needNum = maxCol - filter.length; // 需要的数量
+
+          let str = '';
+          if (filter.length === 1) {
+            str = ` ${grid_area} `.repeat(needNum + 1);
+          } else {
+            let arr = this.splitInteger(needNum, filter.length);
+            str = arr[find] === 0 ? ` ${grid_area} ` : ` ${grid_area} `.repeat(arr[find] + 1);
+          }
+          gridArr[row - 1].push(`${str}`);
+        }
+      });
+      gridArr.forEach((item) => {
+        gridTemplateAreas += `'${item.join(' ')}' `;
+      });
+
+      // 计算 grid_template_columns
+      let gridTemplateColumns = '';
+      let max = { row: 0, num: 0 };
+      grid.forEach(({ row }) => {
+        // 计算出 row 的哪个值最多
+        let len = grid.filter((item) => item.row === row).length;
+        if (max.num < len) {
+          max.num = len;
+          max.row = row;
+        }
+      });
+      grid.forEach((item) => {
+        if (item.row === max.row) {
+          gridTemplateColumns += `${item.width} `;
+        }
+      });
+
+      // 计算 grid_template_rows
+      let gridTemplateRows = '';
+      // 将 grid 按照 row 分组
+      let gridMap = new Map();
+      grid.forEach((item) => {
+        if (!gridMap.has(item.row)) {
+          gridMap.set(item.row, []);
+        }
+        gridMap.get(item.row).push(item.height);
+      });
+      gridMap.forEach((value) => {
+        if (value.length === 1) {
+          gridTemplateRows += `${value[0]} `;
+        } else {
+          let isAllAuto = value.every((item) => item === 'auto'); // 是否全是 auto
+          gridTemplateRows += isAllAuto ? 'auto ' : `max(${value.join(', ')}) `;
+        }
+      });
+
+      return {
+        width: col.width,
+        gridTemplateAreas,
+        gridTemplateColumns,
+        gridTemplateRows,
+      };
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.courserware {
+  display: flex;
+  flex-direction: column;
+  row-gap: 6px;
+  width: 100%;
+  height: 100%;
+  min-height: 500px;
+  padding: 24px;
+  background-color: #fff;
+  background-repeat: no-repeat;
+  border-bottom-right-radius: 12px;
+  border-bottom-left-radius: 12px;
+
+  .row {
+    display: grid;
+    gap: 16px;
+
+    .col {
+      display: grid;
+      gap: 16px;
+      overflow: hidden;
+    }
+  }
+}
+</style>

+ 32 - 1
src/views/book/courseware/preview/components/common/PreviewMixin.js

@@ -2,6 +2,7 @@ import SerialNumberPosition from './SerialNumberPosition.vue';
 import DOMPurify from 'dompurify';
 
 import { isEnable } from '@/views/book/courseware/data/common';
+import { ContentGetCoursewareComponentContent } from '@/api/book';
 
 const mixin = {
   data() {
@@ -15,14 +16,27 @@ const mixin = {
     };
   },
   props: {
+    id: {
+      type: String,
+      default: '',
+    },
     content: {
       type: String,
-      required: true,
+      default: '',
+    },
+    coursewareId: {
+      type: String,
+      default: '',
+    },
+    type: {
+      type: String,
+      default: '',
     },
   },
   watch: {
     content: {
       handler(newVal) {
+        if (this.type === 'edit') return;
         this.data = JSON.parse(newVal);
       },
       immediate: true,
@@ -31,7 +45,24 @@ const mixin = {
   components: {
     SerialNumberPosition,
   },
+  created() {
+    // 这里分为 预览 和 编辑调整位置 两种情况
+    // 预览时,content 直接传入
+    // 编辑调整位置时,content 需要通过接口获取
+    if (this.type === 'edit') {
+      this.getCoursewareComponentContent();
+    }
+  },
   methods: {
+    getCoursewareComponentContent() {
+      ContentGetCoursewareComponentContent({ courseware_id: this.coursewareId, component_id: this.id }).then(
+        ({ content }) => {
+          if (content) this.data = JSON.parse(content);
+          this.loader = true;
+        },
+      );
+    },
+
     /**
      * 获取答案
      * @returns {array} 答案

+ 65 - 2
src/views/book/courseware/preview/components/input/InputPreview.vue

@@ -6,9 +6,20 @@
     <div class="main">
       <span class="rich-text" v-html="sanitizeHTML(data.content)"></span>
       <el-input
+        ref="input"
         v-model="data.answer.text"
         type="text"
-        :class="['input', { 'input-horizontal': data.property.input_style === inputStyleList[0].value }]"
+        :style="{
+          font: data.property.font,
+          fontSize: data.property.font_size + 'px',
+          color: data.property.text_color,
+        }"
+        :class="[
+          'input',
+          { 'input-horizontal': data.property.input_style === inputStyleList[0].value },
+          `input-${data.property.text_align}`,
+        ]"
+        @change="onInputChange"
       />
     </div>
   </div>
@@ -16,6 +27,7 @@
 
 <script>
 import { getInputData, inputStyleList } from '@/views/book/courseware/data/input';
+import { addTone, handleToneValue } from '@/utils/common';
 
 import PreviewMixin from '../common/PreviewMixin';
 
@@ -28,7 +40,42 @@ export default {
       inputStyleList,
     };
   },
-  methods: {},
+  mounted() {
+    this.$nextTick(() => {
+      this.$refs.input.$el.querySelector('.el-input__inner').style.backgroundColor =
+        this.data.property.background_color;
+    });
+  },
+  methods: {
+    // 输入后输入框根据property属性,更新内容
+    onInputChange() {
+      if (!this.data.property.is_enable_pinyin) {
+        return;
+      }
+
+      let answer = this.data.answer.text;
+      // 句首大写
+      if (this.data.property.is_enable_sentence_case) {
+        answer = answer.charAt(0).toUpperCase() + answer.slice(1);
+      }
+
+      // 自动修正为拼音
+      if (this.data.property.is_enable_auto_correct) {
+        answer = answer
+          .trim()
+          .split(/\s+/)
+          .map((item) => {
+            return handleToneValue(item);
+          })
+          .map((item) =>
+            item.map(({ number, con }) => (number && con ? addTone(Number(number), con) : number || con || '')),
+          )
+          .filter((item) => item.length > 0)
+          .join(' ');
+      }
+      this.data.answer.text = answer;
+    },
+  },
 };
 </script>
 
@@ -55,6 +102,22 @@ export default {
       border-left-width: 0;
       border-radius: 0;
     }
+
+    .input.input-left :deep .el-input__inner {
+      text-align: left;
+    }
+
+    .input.input-center :deep .el-input__inner {
+      text-align: center;
+    }
+
+    .input.input-right :deep .el-input__inner {
+      text-align: right;
+    }
+
+    .input :deep .el-input__inner {
+      color: currentColor;
+    }
   }
 }
 </style>

+ 5 - 4
src/views/create_project/createProject.vue

@@ -117,6 +117,7 @@ export default {
       labelInput: '', // 用于输入标签
       leaderNames: '', // 组长姓名
       memberNames: '', // 组员姓名
+      visibleMembers: false, // 控制选择成员弹窗的显示与隐藏
       selectMembersTitle: '', // 选择组长弹窗标题
       type: 'leader',
       project: {
@@ -139,7 +140,6 @@ export default {
       formRules: {
         name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
       },
-      visibleMembers: false, // 控制选择成员弹窗的显示与隐藏
     };
   },
   methods: {
@@ -154,9 +154,9 @@ export default {
       this.$refs.projectForm.validate((valid) => {
         if (valid) {
           CreateProject(this.project)
-            .then(() => {
+            .then(({ id }) => {
               this.$message.success('项目创建成功!');
-              this.$router.push('/create_project/success'); // 跳转到创建成功页面
+              this.$router.push({ path: '/create_project/success', query: { id } }); // 跳转到创建成功页面
             })
             .catch(() => {
               this.$message.error('项目创建失败!');
@@ -201,7 +201,8 @@ export default {
   @include page-base;
 
   max-width: 1148px;
-  margin: 0 auto;
+  min-height: calc(100% - 10px);
+  margin: 5px auto;
 
   .project-form {
     .el-input,

+ 16 - 2
src/views/create_project/createSuccess.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="create-success">
-    <p class="tips">教材编辑项目《轻松学中文》创建成功,请切换到“个人工作台” > “我的项目”进行管理。</p>
+    <p class="tips">教材编辑项目《{{ project_name }}》创建成功,请切换到“个人工作台” > “我的项目”进行管理。</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')">
@@ -11,10 +11,24 @@
 </template>
 
 <script>
+import { GetProjectBaseInfo } from '@/api/project';
+
 export default {
   name: 'CreateSuccess',
   data() {
-    return {};
+    return {
+      id: this.$route.query.id,
+      project_name: '',
+    };
+  },
+  created() {
+    this.getProjectBaseInfo();
+  },
+  methods: {
+    async getProjectBaseInfo() {
+      const { project_info } = await GetProjectBaseInfo({ id: this.id });
+      this.project_name = project_info.name;
+    },
   },
 };
 </script>

+ 6 - 1
src/views/create_project/selectProjectMembers.vue

@@ -12,7 +12,7 @@
       </span>
     </div>
 
-    <el-table ref="user" :data="user_list">
+    <el-table ref="user" :data="user_list" height="240">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column prop="real_name" label="真实姓名" width="140" />
       <el-table-column prop="user_name" label="用户名" width="140" />
@@ -100,6 +100,7 @@ export default {
         org_id_list: this.org_id_list,
         page_capacity: this.page_capacity,
         cur_page: this.cur_page,
+        org_manager_status: -1,
       };
       queryUserList({ ...params, ...data }).then(({ user_list, total_count, cur_page }) => {
         this.user_list = user_list;
@@ -142,6 +143,10 @@ export default {
       this.$refs.user.clearSelection();
     },
     confirm() {
+      if (this.selectedUsers.length === 0) {
+        this.$message.warning('请至少选择一名成员');
+        return;
+      }
       this.$emit('confirm', this.selectedUsers);
       this.dialogClose();
     },

+ 36 - 7
src/views/personal_workbench/edit_task/edit/index.vue

@@ -1,5 +1,7 @@
 <template>
   <div class="edit-task">
+    <MenuPage cur-key="edit_task" />
+
     <div class="edit-task__header">
       <div class="menu">
         <span></span>
@@ -8,34 +10,52 @@
       <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('edit')">{{ isEdit ? '调整位置' : '内容编辑' }}</span>
+          <span class="link" @click="saveCoursewareContent('quit')">退出编辑</span>
           <span class="link" @click="saveCoursewareContent">保存</span>
         </div>
       </div>
     </div>
 
-    <CreatePage ref="create" class="edit-task_content" @goBackPreview="goBackPreview" />
+    <CreatePage ref="create" class="edit-task__content" @goBackPreview="goBackPreview" />
   </div>
 </template>
 
 <script>
-import CreatePage from '@/views/book/courseware/create/index.vue';
+import { GetBookCoursewareInfo, GetMyBookCoursewareTaskList } from '@/api/project';
 
-import { GetBookCoursewareInfo } from '@/api/project';
+import CreatePage from '@/views/book/courseware/create/index.vue';
+import MenuPage from '@/views/personal_workbench/menu.vue';
 
 export default {
   name: 'EditTaskPage',
   components: {
     CreatePage,
+    MenuPage,
   },
   data() {
     return {
       id: this.$route.params.courseware_id,
+      project_id: this.$route.query.project_id,
       courseware_info: {},
+      courseware_list: [],
+      isEdit: true, // 是否编辑状态
     };
   },
   created() {
     this.getBookCoursewareInfo();
+    this.getMyBookCoursewareTaskList();
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.$watch(
+        () => this.$refs.create.isEdit,
+        (newVal) => {
+          this.isEdit = newVal;
+        },
+        { immediate: true },
+      );
+    });
   },
   methods: {
     /**
@@ -55,15 +75,23 @@ export default {
     goBackPreview() {
       this.$router.push({ path: `/personal_workbench/edit_task/preview/${this.id}` });
     },
+    /**
+     * 得到我的教材课件任务列表
+     */
+    getMyBookCoursewareTaskList() {
+      GetMyBookCoursewareTaskList({ project_id: this.project_id }).then(({ courseware_list }) => {
+        this.courseware_list = courseware_list;
+      });
+    },
   },
 };
 </script>
 
 <style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
 .edit-task {
-  display: flex;
-  flex-direction: column;
-  height: 100%;
+  @include page-content(true);
 
   &__header {
     display: flex;
@@ -94,6 +122,7 @@ export default {
 
   &__content {
     flex: 1;
+    flex-direction: row !important;
   }
 }
 </style>

+ 10 - 7
src/views/personal_workbench/edit_task/index.vue

@@ -11,13 +11,13 @@
         </div>
         <ul class="textbook-list">
           <li
-            v-for="{ id, name } in project_list"
+            v-for="{ id, sn, 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">{{ sn }}</span>
             <span class="cell">{{ name }}</span>
           </li>
         </ul>
@@ -79,7 +79,10 @@ export default {
      * @param {string} id 课件ID
      */
     navigateToChapter(id) {
-      this.$router.push({ path: `/personal_workbench/edit_task/preview/${id}` });
+      this.$router.push({
+        path: `/personal_workbench/edit_task/preview/${id}`,
+        query: { project_id: this.cur_project_id },
+      });
     },
   },
 };
@@ -128,11 +131,11 @@ export default {
           text-align: center;
 
           &:first-child {
-            width: 300px;
+            width: 120px;
           }
 
           &:last-child {
-            width: 150px;
+            flex: 1;
           }
         }
       }
@@ -164,11 +167,11 @@ export default {
             text-align: center;
 
             &:first-child {
-              width: 300px;
+              width: 120px;
             }
 
             &:last-child {
-              width: 150px;
+              flex: 1;
             }
           }
         }

+ 50 - 37
src/views/personal_workbench/edit_task/preview/index.vue

@@ -1,5 +1,7 @@
 <template>
   <div class="task-preview">
+    <MenuPage cur-key="edit_task" />
+
     <div class="task-preview__header">
       <div class="menu">
         <span></span>
@@ -13,39 +15,35 @@
         </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>
+
+    <main class="preview-main">
+      <span class="title">
+        <SvgIcon icon-class="menu-2" size="24" />
+        <span>{{ courseware_info.name_path }}</span>
+      </span>
+
+      <CoursewarePreview :data="data" :component-list="component_list" :background="background" />
+    </main>
   </div>
 </template>
 
 <script>
-import { GetBookCoursewareInfo } from '@/api/project';
+import { GetBookCoursewareInfo, GetMyBookCoursewareTaskList } from '@/api/project';
 import { ContentGetCoursewareContent_View } from '@/api/book';
 
-import PreviewEdit from '@/views/book/courseware/create/components/PreviewEdit.vue';
+import MenuPage from '@/views/personal_workbench/menu.vue';
+import CoursewarePreview from '@/views/book/courseware/preview/CoursewarePreview.vue';
 
 export default {
   name: 'TaskPreviewPage',
   components: {
-    PreviewEdit,
+    MenuPage,
+    CoursewarePreview,
   },
   data() {
     return {
       id: this.$route.params.id,
+      project_id: this.$route.query.project_id,
       courseware_info: {},
       background: {
         background_image_url: '',
@@ -60,18 +58,23 @@ export default {
         row_list: [],
       },
       component_list: [],
+      courseware_list: [],
     };
   },
   created() {
     this.getBookCoursewareInfo();
     this.getCoursewareComponentContent_View();
+    this.getMyBookCoursewareTaskList();
   },
   methods: {
     goBackBookList() {
       this.$router.push({ path: '/personal_workbench/edit_task' });
     },
     editTask() {
-      this.$router.push({ path: `/personal_workbench/edit_task/edit/${this.id}` });
+      this.$router.push({
+        path: `/personal_workbench/edit_task/edit/${this.id}`,
+        query: { project_id: this.project_id },
+      });
     },
     getCoursewareComponentContent_View() {
       ContentGetCoursewareContent_View({ id: this.id }).then(({ content, component_list }) => {
@@ -87,6 +90,14 @@ export default {
         this.courseware_info = courseware_info;
       });
     },
+    /**
+     * 得到我的教材课件任务列表
+     */
+    getMyBookCoursewareTaskList() {
+      GetMyBookCoursewareTaskList({ project_id: this.project_id }).then(({ courseware_list }) => {
+        this.courseware_list = courseware_list;
+      });
+    },
   },
 };
 </script>
@@ -95,16 +106,9 @@ export default {
 @use '@/styles/mixin.scss' as *;
 
 .task-preview {
-  @include page-base;
-
-  background-color: $main-background-color;
+  @include page-content(true);
 
   &__header {
-    display: flex;
-    align-items: center;
-    border-top: $border;
-    border-bottom: $border;
-
     .menu {
       display: flex;
       justify-content: space-between;
@@ -126,17 +130,26 @@ export default {
     }
   }
 
-  &__content {
-    display: flex;
-    flex: 1;
-    flex-direction: column;
-    row-gap: 6px;
-    width: $courseware-width;
-    padding: 24px;
+  main.preview-main {
+    width: 1100px;
     margin: 0 auto;
     background-color: #fff;
-    background-repeat: no-repeat;
-    border-radius: 4px;
+    border: 3px solid #f44444;
+    border-radius: 16px;
+
+    .title {
+      display: inline-flex;
+      column-gap: 24px;
+      align-items: center;
+      min-width: 280px;
+      height: 64px;
+      padding: 18px 24px;
+      font-size: 20px;
+      color: #fff;
+      background-color: #f44444;
+      border-top-left-radius: 12px;
+      border-bottom-right-radius: 16px;
+    }
   }
 }
 </style>

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

@@ -55,7 +55,9 @@ export default {
 
 <style lang="scss" scoped>
 .menu {
+  z-index: 9;
   display: flex;
+  background-color: $main-background-color;
 
   &-list {
     display: flex;

+ 72 - 71
src/views/personal_workbench/project/ProductionEditorialManage.vue

@@ -1,6 +1,8 @@
 <template>
   <div class="production-editorial">
-    <div class="top-operate">
+    <MenuPage cur-key="project" />
+
+    <div class="production-editorial__header">
       <span class="name">{{ project_info.name }}</span>
       <span class="button" @click="$router.push({ path: `/personal_workbench/project` })">返回项目列表</span>
       <div class="audit-step">
@@ -16,43 +18,22 @@
         <span class="button" @click="addCoursewareDialog">添加教材内容节点</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, id, 'false')">
-          <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>
+
+    <main class="production-editorial__main">
+      <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>
-        <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')"
-          >
-            <div class="name">{{ item.name }}</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, id, 'false')">
+            <div class="chapter-title">{{ name }}</div>
             <div class="producer">
-              <span>{{ item.producer_list.map((producer) => producer.name).join(';') }}</span>
+              <span>{{ producer_list.map((producer) => producer.name).join(';') }}</span>
             </div>
             <div class="audit"></div>
             <div class="status"></div>
@@ -62,29 +43,52 @@
               <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>
+          <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')"
+            >
+              <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(item.id)">设置制作人</span>
+                <span class="link danger" @click="deleteChapter(item.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>
-        </template>
+        </div>
       </div>
-    </div>
-
+    </main>
     <AddChapter
       :id="book_id"
       :visible="visible"
@@ -110,6 +114,7 @@
 <script>
 import AddChapter from './AddChapter.vue';
 import SetProducer from './SetProducer.vue';
+import MenuPage from '@/views/personal_workbench/menu.vue';
 
 import { GetProjectBaseInfo } from '@/api/project';
 import {
@@ -125,6 +130,7 @@ export default {
   components: {
     AddChapter,
     SetProducer,
+    MenuPage,
   },
   data() {
     return {
@@ -181,7 +187,8 @@ export default {
     },
     /**
      * 选择当前章节
-     * @param id - 父章节ID
+     * @param id - 当前选中章节ID
+     * @param parentId - 父节点ID
      * @param is_leaf - 是否是叶子节点
      */
     selectActiveChapter(id, parentId = '', is_leaf = 'false') {
@@ -208,6 +215,11 @@ export default {
       }
     },
     addChapterDialog() {
+      let isSecDir = !this.nodes.some((item) => item.id === this.curSelectId); // 判断是否是二级目录
+      if (isSecDir) {
+        this.$message.warning('二级目录不能添加章节');
+        return;
+      }
       this.visible = true;
     },
     addCoursewareDialog() {
@@ -269,26 +281,15 @@ export default {
 @use '@/styles/mixin.scss' as *;
 
 .production-editorial {
-  @include page-base;
-
-  .top-operate {
-    display: flex;
-    padding: 6px 4px;
-    border-top: $border;
-    border-bottom: $border;
+  @include page-content(true);
 
+  &__header {
     .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;
@@ -309,8 +310,8 @@ export default {
     }
   }
 
-  .chapters-container {
-    flex: 1;
+  main {
+    text-align: left;
 
     .list-title {
       display: flex;

+ 213 - 162
src/views/personal_workbench/project/ProjectInfoManage.vue

@@ -1,116 +1,124 @@
 <template>
-  <div class="project-info-manage">
-    <el-form
-      ref="projectForm"
-      :inline="true"
-      :model="project"
-      :rules="formRules"
-      label-width="120px"
-      class="project-form"
-    >
-      <el-form-item label="项目名称" prop="name" class="link-item">
-        <el-input v-model="project.name" placeholder="请输入项目名称" maxlength="20" />
-        <span class="link">更改</span>
-      </el-form-item>
-      <el-form-item label="项目分类" prop="category" class="link-item">
-        <el-input v-model="project.category" maxlength="30" />
-        <span class="link">更改</span>
-      </el-form-item>
-      <el-form-item label="作品标签" prop="label_list" class="label-input link-item">
-        <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 class="project-info">
+    <MenuPage cur-key="project" />
+
+    <div class="project-info__header">
+      <span class="name">{{ project.name }}</span>
+      <span class="button" @click="$router.push({ path: `/personal_workbench/project` })">返回项目列表</span>
+    </div>
+
+    <main class="project-info-manage">
+      <el-form
+        ref="projectForm"
+        :inline="true"
+        :model="project"
+        :rules="formRules"
+        label-width="120px"
+        class="project-form"
+      >
+        <el-form-item label="项目名称" prop="name" class="link-item">
+          <el-input v-model="project.name" placeholder="请输入项目名称" maxlength="20" />
+          <span class="link">更改</span>
+        </el-form-item>
+        <el-form-item label="项目分类" prop="category" class="link-item">
+          <el-input v-model="project.category" maxlength="30" />
+          <span class="link">更改</span>
+        </el-form-item>
+        <el-form-item label="作品标签" prop="label_list" class="label-input link-item">
+          <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" />
+            <span class="link">更改</span>
           </div>
-          <el-input v-model="labelInput" placeholder="请输入标签" @keyup.enter.native="labelChange" />
+        </el-form-item>
+        <el-form-item label="语种" prop="language" class="link-item">
+          <el-input v-model="project.language" type="text" placeholder="请输入语种" maxlength="20" />
+          <span class="link">更改</span>
+        </el-form-item>
+        <el-form-item label="所属课题" prop="topic" class="link-item">
+          <el-input v-model="project.topic" type="text" placeholder="请输入所属课题" />
+          <span class="link">更改</span>
+        </el-form-item>
+        <el-form-item label="出版单位" prop="publisher" class="link-item">
+          <el-input v-model="project.publisher" type="text" placeholder="请输入出版单位" maxlength="20" />
+          <span class="link">更改</span>
+        </el-form-item>
+        <el-form-item label="内容简介" prop="content_intro" class="link-item">
+          <el-input
+            v-model="project.content_intro"
+            type="textarea"
+            :autosize="{ minRows: 4 }"
+            maxlength="1500"
+            show-word-limit
+            placeholder="请输入内容简介"
+          />
+          <span class="link">更改</span>
+        </el-form-item>
+        <el-form-item label="选题背景" prop="background" class="link-item">
+          <el-input
+            v-model="project.background"
+            type="textarea"
+            :autosize="{ minRows: 4 }"
+            maxlength="1500"
+            show-word-limit
+            placeholder="请输入选题背景"
+          />
+          <span class="link">更改</span>
+        </el-form-item>
+        <el-form-item label="作者简介" prop="author_intro" class="link-item">
+          <el-input
+            v-model="project.author_intro"
+            type="textarea"
+            :autosize="{ minRows: 4 }"
+            maxlength="1500"
+            show-word-limit
+            placeholder="请输入作者简介"
+          />
+          <span class="link">更改</span>
+        </el-form-item>
+        <el-form-item prop="content_count_YG" class="link-item label-tworow">
+          <span slot="label">预计容量<br />(课数)</span>
+          <el-input v-model="project.content_count_YG" type="number" />
+          <span class="link">更改</span>
+        </el-form-item>
+        <el-form-item label="预计字数" prop="word_count_YG" class="link-item">
+          <el-input v-model="project.word_count_YG" type="number" />
+          <span class="link">更改</span>
+        </el-form-item>
+        <el-form-item label="计划出版时间" prop="plan_publish_date" class="link-item">
+          <el-date-picker v-model="project.plan_publish_date" type="date" placeholder="选择日期" />
+          <span class="link">更改</span>
+        </el-form-item>
+        <el-form-item label="读者对象" prop="reader" class="link-item">
+          <el-input v-model="project.reader" type="text" placeholder="请输入读者对象" maxlength="20" />
           <span class="link">更改</span>
-        </div>
-      </el-form-item>
-      <el-form-item label="语种" prop="language" class="link-item">
-        <el-input v-model="project.language" type="text" placeholder="请输入语种" maxlength="20" />
-        <span class="link">更改</span>
-      </el-form-item>
-      <el-form-item label="所属课题" prop="topic" class="link-item">
-        <el-input v-model="project.topic" type="text" placeholder="请输入所属课题" />
-        <span class="link">更改</span>
-      </el-form-item>
-      <el-form-item label="出版单位" prop="publisher" class="link-item">
-        <el-input v-model="project.publisher" type="text" placeholder="请输入出版单位" maxlength="20" />
-        <span class="link">更改</span>
-      </el-form-item>
-      <el-form-item label="内容简介" prop="content_intro" class="link-item">
-        <el-input
-          v-model="project.content_intro"
-          type="textarea"
-          :autosize="{ minRows: 4 }"
-          maxlength="1500"
-          show-word-limit
-          placeholder="请输入内容简介"
-        />
-        <span class="link">更改</span>
-      </el-form-item>
-      <el-form-item label="选题背景" prop="background" class="link-item">
-        <el-input
-          v-model="project.background"
-          type="textarea"
-          :autosize="{ minRows: 4 }"
-          maxlength="1500"
-          show-word-limit
-          placeholder="请输入选题背景"
-        />
-        <span class="link">更改</span>
-      </el-form-item>
-      <el-form-item label="作者简介" prop="author_intro" class="link-item">
-        <el-input
-          v-model="project.author_intro"
-          type="textarea"
-          :autosize="{ minRows: 4 }"
-          maxlength="1500"
-          show-word-limit
-          placeholder="请输入作者简介"
-        />
-        <span class="link">更改</span>
-      </el-form-item>
-      <el-form-item prop="content_count_YG" class="content-count link-item">
-        <span slot="label">预计容量<br />(课数)</span>
-        <el-input v-model="project.content_count_YG" type="number" />
-        <span class="link">更改</span>
-      </el-form-item>
-      <el-form-item label="预计字数" prop="word_count_YG" class="link-item">
-        <el-input v-model="project.word_count_YG" type="number" />
-        <span class="link">更改</span>
-      </el-form-item>
-      <el-form-item label="计划出版时间" prop="plan_publish_date" class="link-item">
-        <el-date-picker v-model="project.plan_publish_date" type="date" placeholder="选择日期" />
-        <span class="link">更改</span>
-      </el-form-item>
-      <el-form-item label="读者对象" prop="reader" class="link-item">
-        <el-input v-model="project.reader" type="text" placeholder="请输入读者对象" maxlength="20" />
-        <span class="link">更改</span>
-      </el-form-item>
-      <el-form-item label="组长列表" prop="leader_id_list" class="link-item">
-        <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>
-        <span class="link">更改</span>
-      </el-form-item>
-      <el-form-item label="组员列表" prop="member_id_list" class="link-item">
-        <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>
-        <span class="link">更改</span>
-      </el-form-item>
-    </el-form>
+        </el-form-item>
+        <el-form-item prop="leader_id_list" class="link-item label-tworow">
+          <span slot="label">邀请其他<br />项目组长</span>
+          <el-input v-model="leaderNames" type="text">
+            <el-button slot="append" @click="selectLeader">选择</el-button>
+          </el-input>
+          <span class="link">更改</span>
+        </el-form-item>
+        <el-form-item label="组员列表" prop="member_id_list" class="link-item">
+          <el-input v-model="memberNames" type="text">
+            <el-button slot="append" @click="selectMembers">选择</el-button>
+          </el-input>
+          <span class="link">更改</span>
+        </el-form-item>
+      </el-form>
+
+      <selectMembers :visible.sync="visibleMembers" :title="selectMembersTitle" @confirm="handleSelectedMembers" />
+    </main>
   </div>
 </template>
 
@@ -118,13 +126,23 @@
 import { GetProjectInfo } from '@/api/project';
 import { GetUserList_ID } from '@/api/user';
 
+import selectMembers from '@/views/create_project/selectProjectMembers.vue';
+import MenuPage from '@/views/personal_workbench/menu.vue';
+
 export default {
   name: 'ProjectInfoManage',
+  components: {
+    selectMembers,
+    MenuPage,
+  },
   data() {
     return {
       id: this.$route.params.id,
       isManage: this.$route.query.isManage,
       labelInput: '',
+      selectMembersTitle: '',
+      visibleMembers: false,
+      type: '',
       leaderNames: '', // 组长姓名
       memberNames: '', // 组员姓名
       project: {
@@ -167,6 +185,29 @@ export default {
         this.labelInput = ''; // 清空输入框
       }
     },
+    selectLeader() {
+      this.selectMembersTitle = '选择组长';
+      this.type = 'leader';
+      this.visibleMembers = true;
+    },
+    selectMembers() {
+      this.selectMembersTitle = '选择项目成员';
+      this.type = 'member';
+      this.visibleMembers = true;
+    },
+    /**
+     * 处理选择的成员
+     * @param {Array} selectedUsers - 选中的用户列表
+     */
+    handleSelectedMembers(selectedUsers) {
+      if (this.type === 'leader') {
+        this.leaderNames = selectedUsers.map((user) => user.real_name).join(';');
+        this.project.leader_id_list = selectedUsers.map((user) => user.id);
+      } else if (this.type === 'member') {
+        this.memberNames = selectedUsers.map((user) => user.real_name).join(';');
+        this.project.member_id_list = selectedUsers.map((user) => user.id);
+      }
+    },
     /**
      * 得到用户列表(指定ID)
      * @param {Array} id_list - 用户ID列表
@@ -188,75 +229,85 @@ export default {
 <style lang="scss" scoped>
 @use '@/styles/mixin.scss' as *;
 
-.project-info-manage {
-  @include page-base;
+.project-info {
+  @include page-content(true);
+
+  &__header {
+    .name {
+      width: 240px;
+      font-size: 16px;
+      font-weight: bold;
+    }
+  }
+
+  &-manage {
+    max-width: 1148px;
+    margin: 0 auto !important;
 
-  max-width: 1148px;
-  margin: 0 auto;
+    .project-form {
+      .link-item {
+        .el-input,
+        .el-select {
+          width: 380px;
+        }
 
-  .project-form {
-    .link-item {
-      .el-input,
-      .el-select {
-        width: 380px;
-      }
+        .el-textarea {
+          width: 930px;
+        }
 
-      .el-textarea {
-        width: 930px;
+        .link {
+          margin-left: 12px;
+        }
       }
 
-      .link {
-        margin-left: 12px;
+      .label-tworow {
+        :deep > label {
+          display: inline-flex;
+          justify-content: flex-end;
+          width: 100%;
+          line-height: 16px;
+        }
       }
-    }
 
-    .el-input,
-    .el-select {
-      width: 420px;
+      .el-input,
+      .el-select {
+        width: 420px;
 
-      :deep .el-input__inner {
-        background-color: #fff;
-        border-color: #dcdcdc;
+        :deep .el-input__inner {
+          background-color: #fff;
+          border-color: #dcdcdc;
+        }
       }
-    }
 
-    .el-textarea {
-      width: 970px;
+      .el-textarea {
+        width: 970px;
 
-      :deep .el-textarea__inner {
-        background-color: #fff;
-        border-color: #dcdcdc;
+        :deep .el-textarea__inner {
+          background-color: #fff;
+          border-color: #dcdcdc;
+        }
       }
-    }
 
-    .label-input {
-      max-width: 540px;
+      .label-input {
+        max-width: 540px;
 
-      :deep .label-input-content {
-        display: flex;
-        align-items: center;
-        width: 420px;
-
-        .label-list {
+        :deep .label-input-content {
           display: flex;
-          flex-wrap: wrap;
-          gap: 4px;
-        }
+          align-items: center;
+          width: 420px;
 
-        .el-input {
-          flex: 1;
-          min-width: 90px;
-          margin-left: 6px;
-        }
-      }
-    }
+          .label-list {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 4px;
+          }
 
-    .content-count {
-      :deep > label {
-        display: inline-flex;
-        justify-content: flex-end;
-        width: 100%;
-        line-height: 16px;
+          .el-input {
+            flex: 1;
+            min-width: 90px;
+            margin-left: 6px;
+          }
+        }
       }
     }
   }

+ 1 - 4
vue.config.js

@@ -24,10 +24,7 @@ const eepApiUrlList = {
 
 const proxy = {};
 
-let name = '智慧梧桐桌面端互动教材编辑器';
-if (NODE_ENV === 'production') {
-  name += '【开发系统】';
-}
+let name = '国际中文智慧教育引擎互动教材编辑器';
 
 if (NODE_ENV === 'development') {
   proxy[process.env.VUE_APP_BASE_API] = {