Explorar el Código

课件背景设置

dsy hace 1 semana
padre
commit
2225d2df5c

+ 1 - 1
.env

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

+ 1 - 1
.eslintrc.js

@@ -278,7 +278,7 @@ module.exports = {
     'max-len': [1, { code: 120, ignoreUrls: true, ignoreTemplateLiterals: true, ignoreRegExpLiterals: true }],
     'max-nested-callbacks': 1,
     'max-params': [1, 6],
-    'max-statements': [1, 40],
+    'max-statements': [1, 50],
     'new-parens': 2,
     'object-shorthand': 1,
     'operator-assignment': 1,

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "eep_page",
-  "version": "2026.03.12",
+  "version": "2026.03.16",
   "private": true,
   "main": "main.js",
   "description": "智慧梧桐数字教材编辑器",

BIN
src/assets/icon/opacity.png


+ 2 - 0
src/components/CommonPreview.vue

@@ -539,6 +539,7 @@ export default {
           left: 0,
           top: 0,
         },
+        background: {},
       },
       node_list: [],
       data: { row_list: [] },
@@ -745,6 +746,7 @@ export default {
             this.background = {
               background_image_url: _content.background_image_url,
               background_position: _content.background_position,
+              background: _content.background,
             };
           } else {
             this.data = { row_list: [] };

+ 75 - 17
src/views/book/courseware/create/components/CreateCanvas.vue

@@ -1,19 +1,5 @@
 <template>
-  <main
-    ref="canvas"
-    class="canvas"
-    :style="[
-      {
-        backgroundImage: data.background_image_url ? `url(${data.background_image_url})` : '',
-        backgroundSize: data.background_image_url
-          ? `${data.background_position.width}% ${data.background_position.height}%`
-          : '',
-        backgroundPosition: data.background_image_url
-          ? `${data.background_position.left}% ${data.background_position.top}%`
-          : '',
-      },
-    ]"
-  >
+  <main ref="canvas" class="canvas" :style="computedCanvasStyle()">
     <template v-if="isEdit">
       <div v-for="item in lineList" :key="item[0]" class="group-line" :style="computedGroupLine(item)"></div>
       <span class="drag-line" data-row="-1"></span>
@@ -134,6 +120,14 @@
       :settings="data.unified_attrib"
       @fullTextSettings="fullTextSettings"
     />
+
+    <SetBackground
+      :visible.sync="visibleBackground"
+      :url="data.background_image_url"
+      :position="data.background_position"
+      :background-data="data.background"
+      @setBackground="setBackground"
+    />
   </main>
 </template>
 
@@ -151,12 +145,14 @@ import { unified_attrib } from '@/common/data';
 
 import PreviewEdit from './PreviewEdit.vue';
 import FullTextSettings from '../components/FullTextSettings.vue';
+import SetBackground from '../components/SetBackground.vue';
 
 export default {
   name: 'CreateCanvas',
   components: {
     PreviewEdit,
     FullTextSettings,
+    SetBackground,
   },
   inject: ['getCurSettingId'],
   provide() {
@@ -187,11 +183,13 @@ export default {
           top: 0,
           left: 0,
         },
+        background: {},
         // 组件列表
         row_list: [],
         // 样式调整
         unified_attrib,
       },
+      visibleBackground: false, // 背景设置弹窗
       rowCheckList: {}, // 行复选框列表
       content_group_row_list: [], // 行分组id列表
       gridBorderColorList: ['#3fc7cc', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#d863ff', '#724fff'], // 网格边框颜色列表
@@ -538,12 +536,16 @@ export default {
         loading.close();
       }
     },
-    setBackgroundImage(url, position) {
+    setBackground(url, position, background) {
       this.data.background_image_url = url;
       this.data.background_position = position;
+      this.data.background = background;
 
       this.$message.success('设置背景图成功');
     },
+    showSetBackground() {
+      this.visibleBackground = true;
+    },
     /**
      * 显示设置
      * @param {object} setting 组件设置数据
@@ -1656,6 +1658,63 @@ export default {
       this.data.row_list = [];
       this.content_group_row_list = [];
     },
+    computedCanvasStyle() {
+      const { background_image_url, background_position, background } = this.data;
+
+      let canvasStyle = {
+        backgroundSize: background_image_url ? `${background_position.width}% ${background_position.height}%` : '',
+        backgroundPosition: background_image_url ? `${background_position.left}% ${background_position.top}%` : '',
+        backgroundImage: background_image_url ? `url(${background_image_url})` : '',
+        backgroundColor: background_image_url ? `rgba(255, 255, 255, ${1 - background.image_opacity / 100})` : '',
+      };
+
+      if (background) {
+        if (background.mode === 'image') {
+          canvasStyle['backgroundBlendMode'] = 'lighten';
+        } else {
+          canvasStyle['backgroundBlendMode'] = '';
+        }
+
+        if (background.imageMode === 'fill') {
+          canvasStyle['backgroundRepeat'] = 'repeat';
+        } else {
+          canvasStyle['backgroundRepeat'] = 'no-repeat';
+        }
+
+        if (background.imageMode === 'stretch') {
+          canvasStyle['backgroundSize'] = '100% 100%';
+        }
+
+        if (background.imageMode === 'adapt') {
+          canvasStyle['backgroundSize'] = 'contain';
+        }
+
+        if (background.mode === 'color') {
+          canvasStyle['backgroundColor'] = background.color;
+          canvasStyle['backgroundImage'] = '';
+          canvasStyle['backgroundRepeat'] = '';
+          canvasStyle['backgroundPosition'] = '';
+          canvasStyle['backgroundSize'] = '';
+        }
+
+        if (background.enable_border) {
+          canvasStyle['border'] = `${background.border_width}px ${background.border_style} ${background.border_color}`;
+        } else {
+          canvasStyle['border'] = 'none';
+        }
+
+        if (background.enable_radius) {
+          canvasStyle['border-top-left-radius'] = `${background.top_left_radius}px`;
+          canvasStyle['border-top-right-radius'] = `${background.top_right_radius}px`;
+          canvasStyle['border-bottom-left-radius'] = `${background.bottom_left_radius}px`;
+          canvasStyle['border-bottom-right-radius'] = `${background.bottom_right_radius}px`;
+        } else {
+          canvasStyle['border-radius'] = '0';
+        }
+      }
+
+      return canvasStyle;
+    },
   },
 };
 </script>
@@ -1672,7 +1731,6 @@ export default {
   margin: 0 auto;
   background-color: #fff;
   background-repeat: no-repeat;
-  border-radius: 4px;
 
   .group-line {
     position: absolute;

+ 590 - 0
src/views/book/courseware/create/components/SetBackground.vue

@@ -0,0 +1,590 @@
+<template>
+  <el-dialog
+    custom-class="background"
+    width="720px"
+    :close-on-click-modal="false"
+    :visible="visible"
+    :before-close="handleClose"
+  >
+    <div class="background-title">背景设置</div>
+    <div class="set-background">
+      <div class="background-img">
+        <div v-if="file_url" class="img-set" :style="{ top: imgTop, left: imgLeft, position: 'relative' }">
+          <div class="dot top-left" @mousedown="dragStart($event, 'nwse-resize', 'top-left')"></div>
+          <div class="horizontal-line" @mousedown="dragStart($event, 'ns-resize', 'top')"></div>
+          <div class="dot top-right" @mousedown="dragStart($event, 'nesw-resize', 'top-right')"></div>
+          <div class="vertical-line" @mousedown="dragStart($event, 'ew-resize', 'left')"></div>
+          <img
+            ref="img"
+            :src="file_url"
+            draggable="false"
+            :style="{ width: imgWidth, height: imgHeight }"
+            @mousedown="dragStart($event, 'move', 'move')"
+          />
+          <div class="vertical-line" @mousedown="dragStart($event, 'ew-resize', 'right')"></div>
+          <div class="dot bottom-left" @mousedown="dragStart($event, 'nesw-resize', 'bottom-left')"></div>
+          <div class="horizontal-line" @mousedown="dragStart($event, 'ns-resize', 'bottom')"></div>
+          <div class="dot bottom-right" @mousedown="dragStart($event, 'nwse-resize', 'bottom-right')"></div>
+        </div>
+      </div>
+
+      <div class="setup">
+        <div class="setup-item">
+          <div class="setup-top">
+            <el-radio v-model="background.mode" label="image">图片</el-radio>
+          </div>
+          <div class="setup-content">
+            <div class="image-set">
+              <el-upload
+                ref="upload"
+                class="file-uploader"
+                action="no"
+                accept="image/*"
+                :show-file-list="false"
+                :limit="1"
+                :http-request="uploadFile"
+              >
+                <span style="cursor: pointer">选择图片</span>
+              </el-upload>
+              <div>
+                <span class="opacity-icon"></span>
+                <el-input v-model="background.image_opacity" size="mini" :min="0" :max="100" style="width: 55px" />%
+              </div>
+            </div>
+            <div class="mode-list">
+              <span
+                v-for="mode in imageModeList"
+                :key="mode.value"
+                :class="{ active: mode.value === background.imageMode }"
+                @click="background.imageMode = mode.value"
+              >
+                {{ mode.label }}
+              </span>
+            </div>
+          </div>
+        </div>
+        <div class="setup-item">
+          <div class="setup-top">
+            <el-radio v-model="background.mode" label="color">颜色</el-radio>
+          </div>
+          <div class="setup-content">
+            <el-color-picker v-model="background.color" show-alpha />
+          </div>
+        </div>
+        <div class="setup-item">
+          <div class="setup-top">
+            <el-checkbox v-model="background.enable_border" label="边框" />
+          </div>
+          <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">
+                <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">
+                <el-option label="实线" value="solid" />
+                <el-option label="虚线" value="dashed" />
+                <el-option label="点线" value="dotted" />
+              </el-select>
+            </div>
+          </div>
+        </div>
+        <div class="setup-item">
+          <div class="setup-top">
+            <el-checkbox v-model="background.enable_radius" label="圆角" />
+          </div>
+          <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" />
+            </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" />
+            </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" />
+            </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" />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div slot="footer">
+      <el-button @click="handleClose">取 消</el-button>
+      <el-button type="primary" @click="confirm">确 定</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { fileUpload } from '@/api/app';
+
+export default {
+  name: 'SetBackground',
+  props: {
+    visible: {
+      type: Boolean,
+      required: true,
+    },
+    url: {
+      type: String,
+      default: '',
+    },
+    position: {
+      type: Object,
+      default: () => ({ width: 0, height: 0, top: 0, left: 0 }),
+    },
+    backgroundData: {
+      type: Object,
+      default: () => ({}),
+    },
+  },
+  data() {
+    return {
+      maxWidth: 480,
+      maxHeight: 410,
+
+      imgData: {
+        width: 0,
+        height: 0,
+        top: 0,
+        left: 0,
+      },
+      drag: {
+        dragging: false,
+        startX: 0,
+        startY: 0,
+        type: '',
+      },
+      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, // 右下圆角
+      },
+      imageModeList: [
+        { label: '填充', value: 'fill' },
+        { label: '拉伸', value: 'stretch' },
+        { label: '适应', value: 'adapt' },
+        { label: '自定', value: 'auto' },
+      ],
+    };
+  },
+  computed: {
+    imgTop() {
+      return `${this.imgData.top - 9}px`;
+    },
+    imgLeft() {
+      return `${this.imgData.left}px`;
+    },
+    imgWidth() {
+      return `${this.imgData.width}px`;
+    },
+    imgHeight() {
+      return `${this.imgData.height}px`;
+    },
+  },
+  watch: {
+    visible(newVal) {
+      if (newVal) {
+        this.file_url = this.url;
+        this.imgData = {
+          width: (this.position.width * this.maxWidth) / 100,
+          height: (this.position.height * this.maxHeight) / 100,
+          top: (this.position.top * this.maxHeight) / 100,
+          left: (this.position.left * this.maxWidth) / 100,
+        };
+        if (this.backgroundData && Object.keys(this.backgroundData).length) {
+          this.background = { ...this.backgroundData };
+        }
+
+        this.$nextTick(() => {
+          document.querySelector('.background-img').addEventListener('mousemove', this.mouseMove);
+        });
+      } else {
+        document.querySelector('.background-img')?.removeEventListener('mousemove', this.mouseMove);
+      }
+    },
+  },
+  mounted() {
+    document.body.addEventListener('mouseup', this.mouseUp);
+  },
+  beforeDestroy() {
+    document.querySelector('.background-img')?.removeEventListener('mousemove', this.mouseMove);
+    document.body.removeEventListener('mouseup', this.mouseUp);
+  },
+  methods: {
+    handleClose() {
+      this.$emit('update:visible', false);
+    },
+    /**
+     * 拖拽开始
+     * @param {MouseEvent} event
+     * @param {string} cursor
+     * @param {string} type
+     */
+    dragStart(event, cursor, type) {
+      const { clientX, clientY } = event;
+      this.drag = {
+        dragging: true,
+        startX: clientX,
+        startY: clientY,
+        type,
+      };
+
+      document.querySelector('.el-dialog__wrapper').style.cursor = cursor;
+    },
+    /**
+     * 鼠标移动
+     * @param {MouseEvent} event
+     */
+    mouseMove(event) {
+      if (!this.drag.dragging) return;
+      const { clientX, clientY } = event;
+      const { startX, startY, type } = this.drag;
+
+      const widthDiff = clientX - startX;
+      const heightDiff = clientY - startY;
+
+      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));
+        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));
+      } else if (type === 'top-right') {
+        this.imgData.width = Math.min(this.maxWidth, this.imgData.width + widthDiff);
+        this.imgData.height = Math.min(this.maxHeight, Math.max(0, this.imgData.height - heightDiff));
+        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));
+      } else if (type === 'bottom-left') {
+        this.imgData.width = Math.min(this.maxWidth, Math.max(0, this.imgData.width - widthDiff));
+        this.imgData.height = Math.min(this.maxHeight, this.imgData.height + heightDiff);
+        this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top));
+        this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left + widthDiff));
+      } else if (type === 'bottom-right') {
+        this.imgData.width = Math.min(this.maxWidth, this.imgData.width + widthDiff);
+        this.imgData.height = Math.min(this.maxHeight, this.imgData.height + heightDiff);
+        this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top));
+        this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left));
+      }
+
+      if (type === 'top') {
+        this.imgData.height = Math.min(this.maxHeight, Math.max(0, this.imgData.height - heightDiff));
+        this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top + heightDiff));
+      } else if (type === 'bottom') {
+        this.imgData.height = Math.min(this.maxHeight, this.imgData.height + heightDiff);
+        this.imgData.top = Math.min(this.maxHeight - this.imgData.height, Math.max(0, this.imgData.top));
+      } else if (type === 'left') {
+        this.imgData.width = Math.min(this.maxWidth, this.imgData.width - widthDiff);
+        this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left + widthDiff));
+      } else if (type === 'right') {
+        this.imgData.width = Math.min(this.maxWidth, this.imgData.width + widthDiff);
+        this.imgData.left = Math.min(this.maxWidth - this.imgData.width, Math.max(0, this.imgData.left));
+      }
+
+      if (type === 'move') {
+        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));
+      }
+
+      this.drag.startX = clientX;
+      this.drag.startY = clientY;
+    },
+    /**
+     * 鼠标抬起
+     */
+    mouseUp() {
+      this.drag.dragging = false;
+      document.querySelector('.el-dialog__wrapper').style.cursor = 'auto';
+    },
+    /**
+     * 计算圆角样式
+     * @param {string} position 位置,top/bottom
+     * @param {string} direction 方向,left/right
+     * @returns {Object} 圆角样式对象
+     */
+    computedBorderRadius(position, direction) {
+      const radius = this.background[`${position}_${direction}_radius`];
+      let borderWidth = {};
+      if (position === 'top') {
+        borderWidth['border-bottom-width'] = 0;
+      } else {
+        borderWidth['border-top-width'] = 0;
+      }
+      if (direction === 'left') {
+        borderWidth['border-right-width'] = 0;
+      } else {
+        borderWidth['border-left-width'] = 0;
+      }
+      return {
+        [`border-${position}-${direction}-radius`]: `${radius}px`,
+        ...borderWidth,
+      };
+    },
+    uploadFile(file) {
+      fileUpload('Mid', file, { isGlobalprogress: true })
+        .then(({ file_info_list }) => {
+          this.$refs.upload.clearFiles();
+          if (file_info_list.length > 0) {
+            const fileUrl = file_info_list[0].file_url;
+            const img = new Image();
+            img.src = fileUrl;
+            img.onload = () => {
+              const { width, height } = img;
+              this.normalComputed(width, height);
+              this.file_url = fileUrl;
+            };
+          }
+        })
+        .catch(() => {
+          this.$message.error('上传失败');
+        });
+    },
+    /**
+     * 正常填充
+     * @param {number} width 图片宽度
+     * @param {number} height 图片高度
+     */
+    normalComputed(width, height) {
+      if (width > this.maxWidth || height > this.maxHeight) {
+        const wScale = width / this.maxWidth;
+        const hScale = height / this.maxHeight;
+        const scale = wScale > hScale ? this.maxWidth / 2 / width : this.maxHeight / 2 / height;
+
+        this.imgData = {
+          width: width * scale,
+          height: height * scale,
+          top: 0,
+          left: 0,
+        };
+      } else {
+        this.imgData = {
+          width,
+          height,
+          top: 0,
+          left: 0,
+        };
+      }
+    },
+    confirm() {
+      this.$emit(
+        'setBackground',
+        this.file_url,
+        {
+          width: (this.imgData.width / this.maxWidth) * 100,
+          height: (this.imgData.height / this.maxHeight) * 100,
+          top: (this.imgData.top / this.maxHeight) * 100,
+          left: (this.imgData.left / this.maxWidth) * 100,
+        },
+        this.background,
+      );
+      this.handleClose();
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.background-title {
+  margin-bottom: 12px;
+  font-size: 16px;
+  font-weight: 500;
+  color: $font-light-color;
+}
+
+.set-background {
+  display: flex;
+  column-gap: 8px;
+
+  .background-img {
+    flex: 1;
+    height: 410px;
+    border: 1px dashed rgba(0, 0, 0, 8%);
+
+    .img-set {
+      display: inline-grid;
+      grid-template:
+        ' . . . ' 2px
+        ' . img . ' auto
+        ' . . . ' 2px
+        / 2px auto 2px;
+
+      img {
+        object-fit: cover;
+      }
+
+      .horizontal-line,
+      .vertical-line {
+        background-color: $main-color;
+      }
+
+      .horizontal-line {
+        width: 100%;
+        height: 2px;
+        cursor: ns-resize;
+      }
+
+      .vertical-line {
+        width: 2px;
+        height: 100%;
+        cursor: ew-resize;
+      }
+
+      .dot {
+        z-index: 1;
+        width: 6px;
+        height: 6px;
+        background-color: $main-color;
+
+        &.top-left {
+          top: -2px;
+          left: -2px;
+        }
+
+        &.top-right {
+          top: -2px;
+          right: 2px;
+        }
+
+        &.bottom-left {
+          bottom: 2px;
+          left: -2px;
+        }
+
+        &.bottom-right {
+          right: 2px;
+          bottom: 2px;
+        }
+
+        &.top-left,
+        &.bottom-right {
+          position: relative;
+          cursor: nwse-resize;
+        }
+
+        &.top-right,
+        &.bottom-left {
+          position: relative;
+          cursor: nesw-resize;
+        }
+      }
+    }
+  }
+
+  .setup {
+    display: flex;
+    flex-direction: column;
+    row-gap: 12px;
+    width: 200px;
+
+    &-item {
+      display: flex;
+      flex-direction: column;
+      column-gap: 8px;
+
+      .setup-top {
+        margin-bottom: 6px;
+      }
+
+      .setup-content {
+        display: flex;
+        flex-direction: column;
+        row-gap: 8px;
+
+        .image-set {
+          display: flex;
+          align-items: center;
+          justify-content: space-around;
+          height: 32px;
+          color: $font-light-color;
+          background-color: #f2f3f5;
+          border: 1px solid #d9d9d9;
+          border-radius: 4px;
+
+          .opacity-icon {
+            display: inline-block;
+            width: 20px;
+            height: 20px;
+            vertical-align: middle;
+            background: url('@/assets/icon/opacity.png');
+          }
+        }
+
+        .mode-list {
+          display: flex;
+          padding: 2px 3px;
+          background-color: #f2f3f5;
+          border: 1px solid #d9d9d9;
+          border-radius: 4px;
+
+          span {
+            flex: 1;
+            padding: 2px 6px;
+            text-align: center;
+            cursor: pointer;
+
+            &.active {
+              color: $main-color;
+              background-color: #fff;
+            }
+          }
+        }
+
+        &.border-radius {
+          display: grid;
+          grid-template-rows: 1fr 1fr;
+          grid-template-columns: 1fr 1fr;
+          gap: 8px 8px;
+
+          .radius-item {
+            display: flex;
+            flex: 1;
+            column-gap: 4px;
+            align-items: center;
+            height: 32px;
+            padding: 2px 6px;
+            background-color: #f2f3f5;
+
+            .span-radius {
+              display: inline-block;
+              width: 16px;
+              height: 16px;
+              border-color: #000;
+              border-style: solid;
+              border-width: 2px;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+.el-dialog.background {
+  .el-dialog__header {
+    display: none;
+  }
+
+  .el-dialog__body {
+    padding: 8px 16px;
+  }
+}
+</style>

+ 2 - 2
src/views/book/courseware/create/components/base/label/Label.vue

@@ -19,9 +19,9 @@
               :key="'tag-' + i"
               closable
               :style="{ color: tag.color }"
+              class="dynamic-tag"
               @close="handleClose(i)"
               @dblclick.native="handleEdit(i)"
-              class="dynamic-tag"
             >
               {{ tag.name }}
             </el-tag>
@@ -161,7 +161,7 @@ export default {
       this.$set(this.data.dynamicTags[index], 'editName', this.data.dynamicTags[index].name);
 
       this.$nextTick(() => {
-        const input = this.$refs['inputRefs-' + index];
+        const input = this.$refs[`inputRefs-${index}`];
         if (input && input.length > 0) {
           input[0].focus();
         }

+ 1 - 1
src/views/book/courseware/create/components/question/article/CheckArticle.vue

@@ -76,12 +76,12 @@
       <CheckStyle :data="data" @saveStyle="saveStyle" />
     </el-dialog>
     <el-dialog
+      v-if="remarkVisible"
       title="标注"
       :visible.sync="remarkVisible"
       width="50%"
       :close-on-click-modal="false"
       append-to-body
-      v-if="remarkVisible"
     >
       <div v-if="remark" class="remark">
         <div class="adult-book-input-item">

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

@@ -42,7 +42,6 @@
       />
     </div>
 
-    <SelectBackground :visible.sync="visible" @setBackgroundImage="setBackgroundImage" />
     <WarnSave :visible.sync="visibleWarn" @directQuit="back" @saveAndClose="saveCoursewareContent" />
   </main>
 </template>
@@ -82,7 +81,7 @@ export default {
       curSettingId: '',
       componentSettingList,
       bookTypeOption,
-      visible: false,
+      visible: false, // 组件背景设置弹窗
       visibleWarn: false,
       isEdit: true, // 是否编辑状态
       isChange: false, // 是否有改动
@@ -152,7 +151,7 @@ export default {
       this.isChange = false;
     },
     showSetBackground() {
-      this.visible = true;
+      this.$refs.createCanvas.showSetBackground();
     },
     showFullTextSettings() {
       if (this.isEdit) {
@@ -209,9 +208,6 @@ export default {
     addAnswerAndAnalysis(type) {
       this.$refs.createCanvas.addAnswerAndAnalysis(type);
     },
-    setBackgroundImage(...data) {
-      this.$refs.createCanvas.setBackgroundImage(...data);
-    },
     getRichTextContent() {
       return this.$refs.createCanvas.getRichTextContent();
     },

+ 77 - 15
src/views/book/courseware/preview/CoursewarePreview.vue

@@ -512,28 +512,89 @@ export default {
      * @returns {Object} 课件背景样式对象
      */
     computedCourserwareStyle() {
-      const { background_image_url: bcImgUrl = '', background_position: pos = {} } = this.background || {};
-      const hasNoRows = !Array.isArray(this.data?.row_list) || this.data.row_list.length === 0;
+      const {
+        background_image_url: bcImgUrl = '',
+        background_position: pos = {},
+        background: back,
+      } = this.background || {};
+
+      const hasNoRows = !Array.isArray(this.data?.row_list) || this.data.row_list.length === 0; // 判断是否没有行
       const projectCover = this.project?.cover_image_file_url || '';
 
       // 优先在空行时使用背景图或项目封面
-      const backgroundImage = hasNoRows ? bcImgUrl || projectCover : bcImgUrl;
+      if (hasNoRows) {
+        const backgroundImage = hasNoRows ? bcImgUrl || projectCover : bcImgUrl;
 
-      // 保护性读取位置/大小值,避免 undefined 导致字符串 "undefined%"
-      const widthPct = typeof pos.width === 'undefined' ? '' : pos.width;
-      const heightPct = typeof pos.height === 'undefined' ? '' : pos.height;
-      const leftPct = typeof pos.left === 'undefined' ? '' : pos.left;
-      const topPct = typeof pos.top === 'undefined' ? '' : pos.top;
+        // 保护性读取位置/大小值,避免 undefined 导致字符串 "undefined%"
+        const widthPct = typeof pos.width === 'undefined' ? '' : pos.width;
+        const heightPct = typeof pos.height === 'undefined' ? '' : pos.height;
+        const leftPct = typeof pos.left === 'undefined' ? '' : pos.left;
+        const topPct = typeof pos.top === 'undefined' ? '' : pos.top;
 
-      const hasBcImg = Boolean(bcImgUrl);
-      const backgroundSize = hasBcImg ? `${widthPct}% ${heightPct}%` : hasNoRows ? 'contain' : '';
-      const backgroundPosition = hasBcImg ? `${leftPct}% ${topPct}%` : hasNoRows ? 'center' : '';
+        const hasBcImg = Boolean(bcImgUrl);
+        const backgroundSize = hasBcImg ? `${widthPct}% ${heightPct}%` : hasNoRows ? 'contain' : '';
+        const backgroundPosition = hasBcImg ? `${leftPct}% ${topPct}%` : hasNoRows ? 'center' : '';
 
-      return {
-        backgroundImage: backgroundImage ? `url(${backgroundImage})` : '',
-        backgroundSize,
-        backgroundPosition,
+        return {
+          backgroundImage: backgroundImage ? `url(${backgroundImage})` : '',
+          backgroundSize,
+          backgroundPosition,
+        };
+      }
+
+      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})` : '',
       };
+
+      if (back) {
+        if (back.mode === 'image') {
+          canvasStyle['backgroundBlendMode'] = 'lighten';
+        } else {
+          canvasStyle['backgroundBlendMode'] = '';
+        }
+
+        if (back.imageMode === 'fill') {
+          canvasStyle['backgroundRepeat'] = 'repeat';
+        } else {
+          canvasStyle['backgroundRepeat'] = 'no-repeat';
+        }
+
+        if (back.imageMode === 'stretch') {
+          canvasStyle['backgroundSize'] = '100% 100%';
+        }
+
+        if (back.imageMode === 'adapt') {
+          canvasStyle['backgroundSize'] = 'contain';
+        }
+
+        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;
     },
     handleContextMenu(event, id) {
       if (this.canRemark) {
@@ -931,6 +992,7 @@ export default {
   padding-top: $courseware-top-padding;
   padding-bottom: $courseware-bottom-padding;
   margin: 15px 0;
+  overflow: hidden;
   background-color: #fff;
   background-repeat: no-repeat;
   border-bottom-right-radius: 12px;

+ 1 - 1
src/views/personal_workbench/edit_task/edit/index.vue

@@ -46,7 +46,7 @@
             <span class="link" @click="useTemplate()">使用模板</span>
             <span class="link" @click="switchComponent()">交换组件</span>
           </template>
-          <span class="link" @click="showSetBackground">背景</span>
+          <span class="link" @click="showSetBackground">背景设置</span>
           <span class="link" @click="saveCoursewareContent('quit')">退出编辑</span>
           <span class="link" @click="saveCoursewareContent()">保存</span>
           <span v-if="isEdit && !type" class="link" @click="showFullTextSettings">样式调整</span>

+ 3 - 1
src/views/personal_workbench/template_list/preview/CommonPreview.vue

@@ -2,7 +2,7 @@
   <div class="common-preview">
     <div class="common-preview__header" :class="[type && type !== 'personal' ? 'common-preview__header_org' : '']">
       <div class="menu-container">
-        <el-popover placement="bottom" width="330" trigger="click" v-model="popoverShow">
+        <el-popover v-model="popoverShow" placement="bottom" width="330" trigger="click">
           <div class="courseware-tree">
             <div
               v-for="{ id: nodeId, name, deep, is_leaf_chapter } in node_list"
@@ -123,6 +123,7 @@ export default {
           left: 0,
           top: 0,
         },
+        background: {},
       },
       data: { row_list: [] },
       component_list: [],
@@ -238,6 +239,7 @@ export default {
             this.background = {
               background_image_url: _content.background_image_url,
               background_position: _content.background_position,
+              background: _content.background,
             };
           } else {
             this.data = { row_list: [] };