Browse Source

1. 组件背景设置 2. 创建项目时选中项目成员

dsy 2 tháng trước cách đây
mục cha
commit
c975ebdc77

+ 1 - 1
.env

@@ -11,4 +11,4 @@ VUE_APP_BookWebSI = '/GCLSBookWebSI/ServiceInterface'
 VUE_APP_EepServer = '/EEPServer/SI'
 
 #version
-VUE_APP_VERSION = '2026.07.30'
+VUE_APP_VERSION = '2026.07.31'

+ 22 - 11
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -1344,13 +1344,10 @@ export default {
       });
     },
     /**
-     * 根据旧组件 id 将 row_list 中对应 grid 的 id 替换为新 id,并返回新 id
-     * @param {String} oldID 旧组件 id
-     * @returns {String} 新组件 id
+     * 生成唯一的 grid id(检查 row_list 中已有的所有 id,确保不重复)
+     * @returns {String} 唯一 grid id
      */
-    replaceGridIdByOldId(oldID) {
-      if (!oldID) return oldID;
-
+    generateUniqueGridId() {
       const allIds = this.data.row_list.flatMap((row) =>
         row.col_list.flatMap((col) => col.grid_list.map((grid) => grid.id)),
       );
@@ -1359,6 +1356,17 @@ export default {
       while (allIds.includes(newID)) {
         newID = `ID-${getRandomNumber(12, true)}`;
       }
+      return newID;
+    },
+    /**
+     * 根据旧组件 id 将 row_list 中对应 grid 的 id 替换为新 id,并返回新 id
+     * @param {String} oldID 旧组件 id
+     * @returns {String} 新组件 id
+     */
+    replaceGridIdByOldId(oldID) {
+      if (!oldID) return oldID;
+
+      const newID = this.generateUniqueGridId();
 
       for (const row of this.data.row_list) {
         for (const col of row.col_list) {
@@ -1406,7 +1414,7 @@ export default {
      * 计算网格插入的对象
      */
     calculateGridObject() {
-      const id = this.curParams?.id || `ID-${getRandomNumber(12, true)}`;
+      const id = this.curParams?.id || this.generateUniqueGridId();
       const letter = `L${getRandomNumber(6, true)}`;
       const oldId = this.curParams?.oldId;
 
@@ -1466,7 +1474,7 @@ export default {
      * 计算列插入的对象
      */
     calculateColObject() {
-      const id = this.curParams?.id || `ID-${getRandomNumber(12, true)}`;
+      const id = this.curParams?.id || this.generateUniqueGridId();
       const letter = `L${getRandomNumber(6, true)}`;
       const col_id = `C${getRandomNumber(8, true)}`;
 
@@ -1505,7 +1513,7 @@ export default {
      * 计算行插入的对象
      */
     calculateRowInsertedObject() {
-      const id = this.curParams?.id || `ID-${getRandomNumber(12, true)}`;
+      const id = this.curParams?.id || this.generateUniqueGridId();
       const letter = `L${getRandomNumber(6, true)}`;
       const row_id = `R${getRandomNumber(6, true)}`;
       const col_id = `C${getRandomNumber(8, true)}`;
@@ -1712,7 +1720,7 @@ export default {
 
           col.grid_list = col.grid_list.map((grid) => {
             grid.oldId = grid.id;
-            grid.id = `ID-${getRandomNumber(12, true)}`;
+            grid.id = this.generateUniqueGridId();
             grid.grid_area = `L${getRandomNumber(6, true)}`;
             return grid;
           });
@@ -1787,7 +1795,7 @@ export default {
       let col = row.col_list[j];
       let grid = col.grid_list;
 
-      const id = `ID-${getRandomNumber(12, true)}`;
+      const id = this.generateUniqueGridId();
       const letter = `L${getRandomNumber(6, true)}`;
 
       if (['top', 'bottom'].includes(position)) {
@@ -1904,6 +1912,9 @@ export default {
       this.data.row_list = [];
       this.content_group_row_list = [];
     },
+    showSetComponentBackground() {
+      this.$refs.previewEdit.showSetComponentBackground();
+    },
   },
 };
 </script>

+ 110 - 1
src/views/book/courseware/create/components/PreviewEdit.vue

@@ -32,12 +32,14 @@
                 :key="`preview-${grid.id}`"
                 :courseware-id="coursewareId"
                 type="edit"
-                :class="[grid.id]"
+                :class="[grid.id, { active: curSelectId === grid.id }]"
+                :data-id="grid.id"
                 :style="{
                   gridArea: 'preview',
                   height: grid.height,
                   overflow: 'auto',
                 }"
+                @click.native="selectedComponent(grid.id)"
                 @handleHeightChange="handleHeightChange"
               />
             </div>
@@ -45,14 +47,28 @@
         </template>
       </div>
     </template>
+
+    <SetComponentBackground
+      :visible.sync="visibleComponentBackground"
+      :url="componentBackground.background_image_url"
+      :position="componentBackground.background_position"
+      :background-data="componentBackground.background"
+      :component-data="curComponentData"
+      @setComponentBackground="saveCoursewareComponentBackground"
+    />
   </div>
 </template>
 
 <script>
 import { previewComponentList } from '@/views/book/courseware/data/bookType';
+import SetComponentBackground from '@/views/book/courseware/create/components/SetComponentBackground.vue';
+import { GetCoursewareComponentBackground, SaveCoursewareComponentBackground } from '@/api/book';
 
 export default {
   name: 'PreviewEdit',
+  components: {
+    SetComponentBackground,
+  },
   provide() {
     return {
       getDragStatus: () => this.drag.dragging,
@@ -120,6 +136,18 @@ export default {
         can_correct: false, // 可批改
         can_check_correct: false, // 可查看批改
       },
+      // 组件背景设置
+      visibleComponentBackground: false,
+      curComponentData: {
+        courseware_id: '',
+        component_id: '',
+      },
+      componentBackground: {
+        background_image_url: '',
+        background_position: {},
+        background: {},
+      },
+      curSelectId: '', // 当前选中组件id
     };
   },
   created() {
@@ -353,6 +381,83 @@ export default {
     findChildComponentByKey(key) {
       return this.$children.find((child) => child.$vnode.key === key);
     },
+    /**
+     * 处理选中组件事件
+     * @param {string} component_id 组件在课件内部的 ID
+     */
+    selectedComponent(component_id) {
+      this.curSelectId = component_id;
+      this.curComponentData = {
+        courseware_id: this.coursewareId,
+        component_id,
+      };
+      this.$emit('selectedComponent', { courseware_id: this.coursewareId, component_id });
+    },
+    /**
+     * 显示组件背景设置弹窗
+     */
+    async showSetComponentBackground() {
+      if (this.curComponentData.component_id === '') {
+        this.$message.warning('请先选择一个组件');
+        return;
+      }
+      const loading = this.$loading('加载组件数据中...');
+      await GetCoursewareComponentBackground(this.curComponentData).then(({ background }) => {
+        if (background) {
+          this.componentBackground = JSON.parse(background);
+        } else {
+          this.revertComponentBackground();
+        }
+      });
+      loading.close();
+      this.visibleComponentBackground = true;
+    },
+    /**
+     * 设置组件背景
+     * @param {string} url 背景图片地址
+     * @param {string} position 背景图片位置
+     * @param {string} background
+     */
+    saveCoursewareComponentBackground(url, position, background) {
+      this.componentBackground = {
+        background_image_url: url,
+        background_position: position,
+        background,
+      };
+      const backgroundStr = JSON.stringify(this.componentBackground);
+      SaveCoursewareComponentBackground({
+        ...this.curComponentData,
+        background: backgroundStr,
+      })
+        .then(() => {
+          this.$message.success('组件背景设置成功');
+          this.updateComponentBackground(this.curComponentData, backgroundStr);
+        })
+        .finally(() => {
+          this.revertComponentBackground();
+        });
+    },
+    /**
+     * 还原组件背景数据
+     */
+    revertComponentBackground() {
+      this.componentBackground = {
+        background_image_url: '',
+        background_position: {},
+        background: {},
+      };
+    },
+    /**
+     * 更新子组件的背景
+     * @param {object} componentData 组件数据
+     * @param {string} backgroundStr 背景JSON字符串
+     */
+    updateComponentBackground(componentData, backgroundStr) {
+      const child = this.findChildComponentByKey(`preview-${componentData.component_id}`);
+      if (child && typeof child.updateBackground === 'function') {
+        child.updateBackground(JSON.parse(backgroundStr));
+      }
+    },
   },
 };
 </script>
@@ -391,6 +496,10 @@ export default {
       gap: $component-spacing;
       align-items: flex-start;
 
+      .active {
+        box-shadow: 0 0 6px 1px $main-hover-color;
+      }
+
       .grid {
         display: grid;
         grid-template:

+ 86 - 58
src/views/book/courseware/create/components/SetComponentBackground.vue

@@ -58,7 +58,13 @@
               </el-upload>
               <div>
                 <span class="opacity-icon"></span>
-                <el-input v-model="background.image_opacity" size="mini" :min="0" :max="100" style="width: 55px" />%
+                <el-input
+                  v-model.number="background.image_opacity"
+                  size="mini"
+                  :min="0"
+                  :max="100"
+                  style="width: 55px"
+                />%
               </div>
             </div>
             <div class="mode-list">
@@ -97,7 +103,7 @@
           <div class="setup-content">
             <el-color-picker v-model="background.border_color" show-alpha />
             <div>
-              <el-input v-model="background.border_width" style="width: 70px; margin-right: 10px">
+              <el-input v-model.number="background.border_width" style="width: 70px; margin-right: 10px">
                 <i slot="prefix" class="el-icon-s-fold" style="line-height: 32px"></i>
               </el-input>
               <el-select v-model="background.border_style" placeholder="边框样式" style="width: 120px">
@@ -115,19 +121,19 @@
           <div class="setup-content border-radius">
             <div class="radius-item">
               <span class="span-radius" :style="computedBorderRadius('top', 'left')"></span>
-              <el-input v-model="background.top_left_radius" :min="0" type="number" />
+              <el-input v-model.number="background.top_left_radius" :min="0" type="number" />
             </div>
             <div class="radius-item">
               <span class="span-radius" :style="computedBorderRadius('top', 'right')"></span>
-              <el-input v-model="background.top_right_radius" :min="0" type="number" />
+              <el-input v-model.number="background.top_right_radius" :min="0" type="number" />
             </div>
             <div class="radius-item">
               <span class="span-radius" :style="computedBorderRadius('bottom', 'left')"></span>
-              <el-input v-model="background.bottom_left_radius" :min="0" type="number" />
+              <el-input v-model.number="background.bottom_left_radius" :min="0" type="number" />
             </div>
             <div class="radius-item">
               <span class="span-radius" :style="computedBorderRadius('bottom', 'right')"></span>
-              <el-input v-model="background.bottom_right_radius" :min="0" type="number" />
+              <el-input v-model.number="background.bottom_right_radius" :min="0" type="number" />
             </div>
           </div>
         </div>
@@ -146,6 +152,22 @@ import ColorPicker from '@/components/ColorPicker.vue';
 
 import { fileUpload } from '@/api/app';
 
+const DEFAULT_BACKGROUND = {
+  mode: 'image',
+  imageMode: 'auto',
+  image_opacity: 100,
+  color: 'rgba(255, 255, 255, 1)',
+  enable_border: false,
+  border_color: 'rgba(0, 0, 0, 1)',
+  border_width: 1,
+  border_style: 'solid',
+  enable_radius: false,
+  top_left_radius: 4,
+  top_right_radius: 4,
+  bottom_left_radius: 4,
+  bottom_right_radius: 4,
+};
+
 export default {
   name: 'SetComponentBackground',
   components: {
@@ -208,21 +230,7 @@ export default {
         height: 0,
       },
       file_url: '', // 背景图片地址
-      background: {
-        mode: 'image', // 背景模式
-        imageMode: 'auto', // 图片背景模式
-        image_opacity: 100, // 背景图片透明度
-        color: 'rgba(255, 255, 255, 1)', // 背景颜色
-        enable_border: false, // 是否启用边框
-        border_color: 'rgba(0, 0, 0, 1)', // 边框颜色
-        border_width: 1, // 边框宽度
-        border_style: 'solid', // 边框样式
-        enable_radius: false, // 是否启用圆角
-        top_left_radius: 4, // 左上圆角
-        top_right_radius: 4, // 右上圆角
-        bottom_left_radius: 4, // 左下圆角
-        bottom_right_radius: 4, // 右下圆角
-      },
+      background: { ...DEFAULT_BACKGROUND },
       imageModeList: [
         { label: '适应', value: 'adapt' },
         { label: '拉伸', value: 'stretch' },
@@ -230,9 +238,15 @@ export default {
         { label: '自定义', value: 'auto' },
       ],
       lockScale: false, // 是否锁定比例(仅自定义模式)
+      dialogWrapper: null, // 当前弹窗 wrapper 引用,避免全局 querySelector 冲突
     };
   },
   computed: {
+    /**
+     * 图片容器 top 偏移(px)。
+     * -9 补偿了网格布局的顶部行高(2px)与拖拽手柄 dot 的 relative 偏移量之和,
+     * 使图片定位与用户拖拽感知一致。
+     */
     imgTop() {
       return `${this.imgData.top - 9}px`;
     },
@@ -263,6 +277,7 @@ export default {
   watch: {
     visible(newVal) {
       if (!newVal) {
+        this.dialogWrapper = null;
         this.revertData();
         return;
       }
@@ -287,22 +302,26 @@ export default {
         left: (this.position.left * this.maxWidth) / 100,
       };
       if (this.backgroundData && Object.keys(this.backgroundData).length) {
-        this.background = { ...this.backgroundData };
+        this.background = { ...DEFAULT_BACKGROUND, ...this.backgroundData };
       }
       this.cropMode = false;
       this.resetCropRect();
 
       this.$nextTick(() => {
-        document.querySelector('.background-img').addEventListener('mousemove', this.mouseMove);
+        const bg = this.$refs.backgroundImg;
+        if (bg) {
+          this.dialogWrapper = bg.closest('.el-dialog__wrapper');
+        }
         this.syncImageDisplayRect();
       });
     },
   },
   mounted() {
+    document.addEventListener('mousemove', this.mouseMove);
     document.body.addEventListener('mouseup', this.mouseUp);
   },
   beforeDestroy() {
-    document.querySelector('.background-img')?.removeEventListener('mousemove', this.mouseMove);
+    document.removeEventListener('mousemove', this.mouseMove);
     document.body.removeEventListener('mouseup', this.mouseUp);
   },
   methods: {
@@ -339,21 +358,7 @@ export default {
         height: 0,
       };
       this.file_url = ''; // 背景图片地址
-      this.background = {
-        mode: 'image', // 背景模式
-        imageMode: 'auto', // 图片背景模式
-        image_opacity: 100, // 背景图片透明度
-        color: 'rgba(255, 255, 255, 1)', // 背景颜色
-        enable_border: false, // 是否启用边框
-        border_color: 'rgba(0, 0, 0, 1)', // 边框颜色
-        border_width: 1, // 边框宽度
-        border_style: 'solid', // 边框样式
-        enable_radius: false, // 是否启用圆角
-        top_left_radius: 4, // 左上圆角
-        top_right_radius: 4, // 右上圆角
-        bottom_left_radius: 4, // 左下圆角
-        bottom_right_radius: 4, // 右下圆角
-      };
+      this.background = { ...DEFAULT_BACKGROUND };
     },
     /**
      * 拖拽开始
@@ -373,7 +378,9 @@ export default {
         aspectRatio,
       };
 
-      document.querySelector('.el-dialog__wrapper').style.cursor = cursor;
+      if (this.dialogWrapper) {
+        this.dialogWrapper.style.cursor = cursor;
+      }
     },
     /**
      * 鼠标移动
@@ -402,11 +409,30 @@ export default {
       if (!this.drag.dragging) return;
       const { clientX, clientY } = event;
       const { startX, startY, type } = this.drag;
-      const prevImgData = { ...this.imgData };
 
-      const widthDiff = clientX - startX;
-      const heightDiff = clientY - startY;
+      // 锁定比例时跳过常规 resize 计算,直接用 applyLockedScaleResize 处理
+      if (this.shouldLockScale(type)) {
+        const prevImgData = { ...this.imgData };
+        this.applyLockedScaleResize(type, prevImgData);
+        this.drag.startX = clientX;
+        this.drag.startY = clientY;
+        this.syncImageDisplayRect();
+        return;
+      }
+
+      this.applyDragResize(type, clientX - startX, clientY - startY);
 
+      this.drag.startX = clientX;
+      this.drag.startY = clientY;
+      this.syncImageDisplayRect();
+    },
+    /**
+     * 非锁定比例下的拖拽 resize / move,直接修改 imgData。
+     * @param {string} type 拖拽类型
+     * @param {number} widthDiff 水平位移量
+     * @param {number} heightDiff 垂直位移量
+     */
+    applyDragResize(type, widthDiff, heightDiff) {
       if (type === 'top-left') {
         this.imgData.width = Math.min(this.maxWidth, Math.max(0, this.imgData.width - widthDiff));
         this.imgData.height = Math.min(this.maxHeight, Math.max(0, this.imgData.height - heightDiff));
@@ -447,14 +473,6 @@ export default {
         this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top + heightDiff));
         this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left + widthDiff));
       }
-
-      if (this.shouldLockScale(type)) {
-        this.applyLockedScaleResize(type, prevImgData);
-      }
-
-      this.drag.startX = clientX;
-      this.drag.startY = clientY;
-      this.syncImageDisplayRect();
     },
     /**
      * 判断在当前拖动类型下是否应该锁定比例调整尺寸
@@ -502,9 +520,12 @@ export default {
      * 鼠标抬起
      */
     mouseUp() {
+      if (!this.drag.dragging && !this.crop.drawing) return;
       this.crop.drawing = false;
       this.drag.dragging = false;
-      document.querySelector('.el-dialog__wrapper').style.cursor = 'auto';
+      if (this.dialogWrapper) {
+        this.dialogWrapper.style.cursor = 'auto';
+      }
     },
     onBackgroundMouseDown(event) {
       if (!this.cropMode || !this.file_url) return;
@@ -662,6 +683,7 @@ export default {
 
         this.file_url = file_info_list[0].file_url;
         this.normalComputed(safeWidth, safeHeight);
+        this.syncImageDisplayRect();
         this.cancelRectCrop();
         this.$message.success('裁切成功');
       } catch (error) {
@@ -750,12 +772,12 @@ export default {
           if (file_info_list.length > 0) {
             const fileUrl = file_info_list[0].file_url_open;
             const img = new Image();
-            img.src = fileUrl;
             img.onload = () => {
               const { width, height } = img;
               this.normalComputed(width, height);
               this.file_url = fileUrl;
             };
+            img.src = fileUrl;
           }
         })
         .catch(() => {
@@ -763,9 +785,11 @@ export default {
         });
     },
     /**
-     * 正常填充
-     * @param {number} width 图片宽度
-     * @param {number} height 图片高度
+     * 正常填充(等比例缩放使图片适配容器)
+     * 大图初始缩放为容器的一半大小,方便用户后续拖拽到合适位置。
+     * 小图(未超出容器)则保持原始尺寸。
+     * @param {number} width 图片原始宽度
+     * @param {number} height 图片原始高度
      */
     normalComputed(width, height) {
       if (width > this.maxWidth || height > this.maxHeight) {
@@ -789,6 +813,10 @@ export default {
       }
     },
     confirm() {
+      // 防止除零:图片尺寸恰好等于容器大小时,剩余空间为 0
+      const remainingWidth = Math.max(1, this.maxWidth - this.imgData.width);
+      const remainingHeight = Math.max(1, this.maxHeight - this.imgData.height);
+
       this.$emit(
         'setComponentBackground',
         this.file_url,
@@ -797,8 +825,8 @@ export default {
           height: (this.imgData.height / this.maxHeight) * 100,
           top: (this.imgData.top / this.maxHeight) * 100,
           left: (this.imgData.left / this.maxWidth) * 100,
-          imgX: (this.imgData.left / (this.maxWidth - this.imgData.width)) * 100,
-          imgY: (this.imgData.top / (this.maxHeight - this.imgData.height)) * 100,
+          imgX: (this.imgData.left / remainingWidth) * 100,
+          imgY: (this.imgData.top / remainingHeight) * 100,
         },
         this.background,
       );

+ 3 - 0
src/views/book/courseware/create/index.vue

@@ -282,6 +282,9 @@ export default {
     clearContent() {
       this.$refs.createCanvas.clearContent();
     },
+    showSetComponentBackground() {
+      this.$refs.createCanvas.showSetComponentBackground();
+    },
   },
 };
 </script>

+ 89 - 68
src/views/book/courseware/preview/components/common/PreviewMixin.js

@@ -24,6 +24,7 @@ const mixin = {
       answerAnalysisState: null, // 答案解析弹窗前的状态快照
       visibleAnswerCorrect: false, // 是否显示批改弹窗
       isCheckCorrect: false, // 是否查看批改信息
+      componentBackground: {}, // 组件背景
     };
   },
   provide() {
@@ -65,6 +66,78 @@ const mixin = {
     permissionControl() {
       return this.getPermissionControl();
     },
+    /**
+     * 计算组件背景样式
+     * @returns {Object} 组件背景样式对象
+     */
+    componentStyle() {
+      const {
+        background_image_url: bcImgUrl = '',
+        background_position: pos = {},
+        background: back,
+      } = this.componentBackground || {};
+
+      let canvasStyle = {
+        backgroundSize: bcImgUrl ? `${pos.width}% ${pos.height}%` : '',
+        backgroundPosition: bcImgUrl ? `${pos.left}% ${pos.top}%` : '',
+        backgroundImage: bcImgUrl ? `url(${bcImgUrl})` : '',
+        backgroundColor: bcImgUrl ? `rgba(255, 255, 255, ${1 - back?.image_opacity / 100})` : '',
+        overflowY: 'auto',
+      };
+
+      if (back) {
+        if (back.mode === 'image') {
+          canvasStyle['backgroundBlendMode'] = 'lighten';
+        } else {
+          canvasStyle['backgroundBlendMode'] = '';
+        }
+
+        if (back.imageMode === 'fill') {
+          canvasStyle['backgroundRepeat'] = 'repeat';
+          canvasStyle['backgroundSize'] = '';
+          canvasStyle['backgroundPosition'] = '';
+        } else {
+          canvasStyle['backgroundRepeat'] = 'no-repeat';
+        }
+
+        if (back.imageMode === 'stretch') {
+          canvasStyle['backgroundSize'] = '100% 100%';
+        }
+
+        if (back.imageMode === 'adapt') {
+          canvasStyle['backgroundSize'] = 'contain';
+        }
+
+        if (back.imageMode === 'auto') {
+          canvasStyle['backgroundPosition'] = `${pos.imgX}% ${pos.imgY}%`;
+        }
+
+        if (back.mode === 'color') {
+          canvasStyle['backgroundColor'] = back.color;
+          canvasStyle['backgroundImage'] = '';
+          canvasStyle['backgroundRepeat'] = '';
+          canvasStyle['backgroundPosition'] = '';
+          canvasStyle['backgroundSize'] = '';
+        }
+
+        if (back.enable_border) {
+          canvasStyle['border'] = `${back.border_width}px ${back.border_style} ${back.border_color}`;
+        } else {
+          canvasStyle['border'] = 'none';
+        }
+
+        if (back.enable_radius) {
+          canvasStyle['border-top-left-radius'] = `${back.top_left_radius}px`;
+          canvasStyle['border-top-right-radius'] = `${back.top_right_radius}px`;
+          canvasStyle['border-bottom-left-radius'] = `${back.bottom_left_radius}px`;
+          canvasStyle['border-bottom-right-radius'] = `${back.bottom_right_radius}px`;
+        } else {
+          canvasStyle['border-radius'] = '0';
+        }
+      }
+
+      return canvasStyle;
+    },
   },
   watch: {
     content: {
@@ -81,6 +154,13 @@ const mixin = {
       },
       immediate: true,
     },
+    background: {
+      handler(newVal) {
+        if (!newVal) return;
+        this.componentBackground = newVal;
+      },
+      immediate: true,
+    },
   },
   components: {
     SerialNumberPosition,
@@ -100,7 +180,7 @@ const mixin = {
   methods: {
     getCoursewareComponentContent() {
       ContentGetCoursewareComponentContent({ courseware_id: this.coursewareId, component_id: this.id })
-        .then(({ content }) => {
+        .then(({ content, background }) => {
           if (content) {
             let oldData = JSON.parse(content);
             if (oldData.type === 'audio') {
@@ -118,6 +198,9 @@ const mixin = {
             }
             this.data = oldData;
           }
+          if (background) {
+            this.componentBackground = JSON.parse(background);
+          }
         })
         .finally(() => {
           this.loader = true;
@@ -193,76 +276,14 @@ const mixin = {
       };
     },
     /**
-     * 计算组件背景样式
+     * 计算组件背景样式(委托给 computed 以保证响应式更新)
      * @returns {Object} 组件背景样式对象
      */
     getComponentStyle() {
-      const {
-        background_image_url: bcImgUrl = '',
-        background_position: pos = {},
-        background: back,
-      } = this.background || {};
-
-      let canvasStyle = {
-        backgroundSize: bcImgUrl ? `${pos.width}% ${pos.height}%` : '',
-        backgroundPosition: bcImgUrl ? `${pos.left}% ${pos.top}%` : '',
-        backgroundImage: bcImgUrl ? `url(${bcImgUrl})` : '',
-        backgroundColor: bcImgUrl ? `rgba(255, 255, 255, ${1 - back?.image_opacity / 100})` : '',
-        overflowY: 'auto',
-      };
-
-      if (back) {
-        if (back.mode === 'image') {
-          canvasStyle['backgroundBlendMode'] = 'lighten';
-        } else {
-          canvasStyle['backgroundBlendMode'] = '';
-        }
-
-        if (back.imageMode === 'fill') {
-          canvasStyle['backgroundRepeat'] = 'repeat';
-          canvasStyle['backgroundSize'] = '';
-          canvasStyle['backgroundPosition'] = '';
-        } else {
-          canvasStyle['backgroundRepeat'] = 'no-repeat';
-        }
-
-        if (back.imageMode === 'stretch') {
-          canvasStyle['backgroundSize'] = '100% 100%';
-        }
-
-        if (back.imageMode === 'adapt') {
-          canvasStyle['backgroundSize'] = 'contain';
-        }
-
-        if (back.imageMode === 'auto') {
-          canvasStyle['backgroundPosition'] = `${pos.imgX}% ${pos.imgY}%`;
-        }
-
-        if (back.mode === 'color') {
-          canvasStyle['backgroundColor'] = back.color;
-          canvasStyle['backgroundImage'] = '';
-          canvasStyle['backgroundRepeat'] = '';
-          canvasStyle['backgroundPosition'] = '';
-          canvasStyle['backgroundSize'] = '';
-        }
-
-        if (back.enable_border) {
-          canvasStyle['border'] = `${back.border_width}px ${back.border_style} ${back.border_color}`;
-        } else {
-          canvasStyle['border'] = 'none';
-        }
-
-        if (back.enable_radius) {
-          canvasStyle['border-top-left-radius'] = `${back.top_left_radius}px`;
-          canvasStyle['border-top-right-radius'] = `${back.top_right_radius}px`;
-          canvasStyle['border-bottom-left-radius'] = `${back.bottom_left_radius}px`;
-          canvasStyle['border-bottom-right-radius'] = `${back.bottom_right_radius}px`;
-        } else {
-          canvasStyle['border-radius'] = '0';
-        }
-      }
-
-      return canvasStyle;
+      return this.componentStyle;
+    },
+    updateBackground(background) {
+      this.componentBackground = background;
     },
     // 显示答案与解析页面
     showAnswerAnalysis() {

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

@@ -146,6 +146,7 @@
       :visible.sync="visibleMembers"
       :title="selectMembersTitle"
       :type="type"
+      :selected-id-list="type === 'leader' ? project.leader_id_list : project.member_id_list"
       @confirm="handleSelectedMembers"
     />
   </div>

+ 22 - 2
src/views/create_project/selectProjectMembers.vue

@@ -65,7 +65,7 @@
 
 <script>
 import { queryUserList } from '@/api/list';
-import { orgIndexList } from '@/api/user';
+import { orgIndexList, GetUserList_ID } from '@/api/user';
 import { getToken } from '@/utils/auth';
 
 import PaginationPage from '@/components/PaginationPage.vue';
@@ -92,6 +92,11 @@ export default {
       type: Array,
       default: () => [],
     },
+    // 已选用户的 ID 列表,用于回显
+    selectedIdList: {
+      type: Array,
+      default: () => [],
+    },
   },
   data() {
     const token = getToken();
@@ -115,7 +120,12 @@ export default {
     visible: {
       handler(newVal) {
         if (newVal) {
-          this.selectedUsers = [...this.selectedList];
+          // 优先通过 selectedIdList 获取已选用户
+          if (this.selectedIdList.length > 0) {
+            this.fetchSelectedUsers();
+          } else {
+            this.selectedUsers = [...this.selectedList];
+          }
         }
       },
       immediate: true,
@@ -146,6 +156,16 @@ export default {
         this.org_list = org_list;
       });
     },
+    // 通过 ID 列表获取已选用户并回显
+    fetchSelectedUsers() {
+      GetUserList_ID({ id_list: this.selectedIdList })
+        .then(({ user_list }) => {
+          this.selectedUsers = user_list || [];
+        })
+        .catch(() => {
+          this.selectedUsers = [];
+        });
+    },
     // 添加选中的用户
     addSelectedUsers() {
       // 获取选中的用户

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

@@ -47,6 +47,7 @@
           <span class="link" @click="saveCoursewareContent()">保存</span>
           <span v-if="isEdit && !type" class="link" @click="showFullTextSettings">样式调整</span>
           <span v-if="isEdit" class="link" @click="clearContent">清空</span>
+          <span v-if="!isEdit" class="link" @click="showSetComponentBackground">组件背景设置</span>
           <span v-if="type" class="link" @click="goBackTemplateList">返回模板列表</span>
           <span v-else class="link" @click="goBackBookList">返回教材列表</span>
         </div>
@@ -246,6 +247,9 @@ export default {
       this.data.unified_attrib = { ...this.$refs.create.$refs.createCanvas.data.unified_attrib };
       this.visibleFullTextSettings = true;
     },
+    showSetComponentBackground() {
+      this.$refs.create.showSetComponentBackground();
+    },
     fullTextSettings(data) {
       this.$refs.create.$refs.createCanvas.fullTextSettings(data);
     },