已修改7个文件
已添加2个文件
546 ■■■■■ 文件已修改
easegen-front/package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easegen-front/src/assets/imgs/1.png 补丁 | 查看 | 原始文档 | blame | 历史
easegen-front/src/assets/imgs/2.png 补丁 | 查看 | 原始文档 | blame | 历史
easegen-front/src/locales/zh-CN.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easegen-front/src/views/Login/Login.vue 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easegen-front/src/views/Login/components/LoginForm.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
easegen-front/src/views/Login/components/LoginFormTitle.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easegen-front/src/views/digitalcourse/digitalhumans/AuditForm.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easegen-front/src/views/digitalcourse/template/TemplateForm.vue 500 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
easegen-front/package.json
@@ -57,6 +57,7 @@
    "face-api.js": "^0.22.2",
    "fast-xml-parser": "^4.3.2",
    "highlight.js": "^11.9.0",
    "html2canvas": "^1.4.1",
    "jsencrypt": "^3.3.2",
    "lodash-es": "^4.17.21",
    "markdown-it": "^14.1.0",
easegen-front/src/assets/imgs/1.png
easegen-front/src/assets/imgs/2.png
easegen-front/src/locales/zh-CN.ts
@@ -44,6 +44,7 @@
    confirmTitle: '系统提示',
    exportMessage: '是否确认导出数据项?',
    importMessage: '是否确认导入数据项?',
    NeedAddPpt:"请添加ppt",
    createSuccess: '新增成功',
    updateSuccess: '修改成功',
    delMessage: '是否删除所选中数据?',
@@ -369,7 +370,7 @@
    },
    login: {
      backSignIn: '返回',
      signInFormTitle: '登录',
      signInFormTitle: '登 录',
      ssoFormTitle: '三方授权',
      mobileSignInFormTitle: '手机登录',
      qrSignInFormTitle: '二维码登录',
easegen-front/src/views/Login/Login.vue
@@ -4,7 +4,8 @@
      <!-- 左侧图片 -->
      <div class="Left-Area">
        <div class="TitleText">
          <h1>数字人智能交互平台</h1>
          <h1>数字人</h1>
          <h1>智能交互平台</h1>
        </div>
      </div>
      <!-- 右边的登录界面 -->
@@ -90,7 +91,7 @@
}
.bei .BeiArea .Left-Area {
  width: 500px;
  width: 420px;
  height: 450px;
  display: flex;
  justify-content: center;
@@ -100,6 +101,7 @@
.bei .BeiArea .Left-Area .TitleText {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
@@ -107,13 +109,18 @@
.bei .BeiArea .Left-Area .TitleText h1 {
  color: #fff;
  font-size: 2.6rem;
  text-align: center;
}
.bei .BeiArea .Left-Area .TitleText h1:first-of-type{
  margin-bottom: 20px;
}
.bei .BeiArea .form-box {
  box-sizing: border-box;
  background: #fff;
  /* width: calc(400px + (100vw - 1900px) * 0.5); */
  width: 500px;
  width: 420px;
  height: 450px;
  padding: 20px 40px;
}
@@ -143,13 +150,13 @@
@media ( max-width:1550px ){
  .bei .BeiArea .Left-Area .TitleText h1{
    font-size: 2.4rem !important;
    font-size: 2.6rem !important;
  }
}
@media ( max-width:1050px ){
  .bei .BeiArea .Left-Area .TitleText h1{
    font-size: 2rem !important;
    font-size: 2.6rem !important;
  }
}
easegen-front/src/views/Login/components/LoginForm.vue
@@ -382,7 +382,7 @@
      padding: 20px;
      box-sizing: border-box;
      font-size: 20px;
      margin-top: 10px;
      margin-top: 15px;
    }
  }
}
easegen-front/src/views/Login/components/LoginFormTitle.vue
@@ -3,11 +3,14 @@
.c717a8a{
  color: #717a8a;
  text-align: center;
  font-weight: 400;
  margin-bottom: 5px;
  margin-top: 45px;
}
</style>
<template>
  <h2 class="enter-x mb-3 text-2xl font-bold xl:text-3xl c717a8a"  >
  <h2 class="enter-x mb-3 text-2xl xl:text-3xl c717a8a"  >
    {{ getFormTitle }}
  </h2>
</template>
easegen-front/src/views/digitalcourse/digitalhumans/AuditForm.vue
@@ -18,8 +18,8 @@
      </el-form-item>
      <el-form-item v-if="(formData.useModel == 2 || formData.useModel == 3) && !(formData.videoUrl || formData.fixVideoUrl) " :label="t('digitalhumans.video')" prop="videoUrl">
        <UploadFile v-if="!(formData.videoUrl || formData.fixVideoUrl)" v-model="formData.videoUrl" :fileType="['mp4']" :limit="1" @on-success="handleFileSuccess('fixVideoUrl', $event)"/>
        <!-- 播放mov视频 -->
         <!-- 下面的是原本的 -->
@@ -63,11 +63,11 @@
            </el-input>
            <!-- 保留原有的上传功能 -->
            <div class="text-gray-500 text-sm">{{ t('digitalhumans.orUploadVideo') }}</div>
            <UploadFile
              v-model="formData.fixVideoUrl"
              :fileType="['mp4','mov']"
              :limit="1"
              :file-size="1024"
            <UploadFile
              v-model="formData.fixVideoUrl"
              :fileType="['mp4','mov']"
              :limit="1"
              :file-size="1024"
              @on-success="handleFileSuccess('fixVideoUrl', $event)"
            />
          </div>
@@ -181,7 +181,7 @@
    message.warning(t('digitalhumans.pleaseInputVideoUrl'))
    return
  }
  // 验证URL格式
  try {
    new URL(formData.value.fixVideoUrl)
@@ -216,7 +216,7 @@
      message.success(t('common.createSuccess'))
    } else {
      await DigitalHumansApi.updateDigitalHumans(data)
      message.success(t('common.updateSuccess'))
      message.success('开始受理')
    }
    dialogVisible.value = false
    // 发送操作成功的事件
easegen-front/src/views/digitalcourse/template/TemplateForm.vue
@@ -1,121 +1,38 @@
<template>
  <Dialog style="width: 60%;" :title="dialogTitle" v-model="dialogVisible">
  <Dialog style="width: 60%;" :title="dialogTitle" v-model="dialogVisible" @close="cancale" >
    <el-form
      ref="formRef"
      :model="formData"
      :rules="formRules"
      label-width="200px"
      v-loading="formLoading"
    >
      v-loading="formLoading">
      <el-row>
        <el-col :span="12">
          <el-form-item :label="t('template.name')" prop="templateName">
            <el-input v-model="formData.templateName" maxlength="50" :placeholder="t('common.inputText') + t('template.name')" />
          <el-form-item label="模板名称" prop="templateName">
            <el-input v-model="formData.templateName" maxlength="50" placeholder="请输入模板名称" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item :label="t('template.isShowBackground')" prop="showBackground">
            <el-select v-model="formData.showBackground" :placeholder="t('common.selectText') + t('template.isShowBackground')">
              <el-option
                v-for="dict in getIntDictOptions(DICT_TYPE.IS_OR_NOT)"
                :key="dict.value"
                :label="dict.label"
                :value="dict.value"
              />
            </el-select>
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item :label="t('template.isShowDigitalPeople')" prop="showDigitalHuman">
            <el-select v-model="formData.showDigitalHuman" :placeholder="t('common.selectText') + t('template.isShowDigitalPeople')">
              <el-option
                v-for="dict in getIntDictOptions(DICT_TYPE.IS_OR_NOT)"
                :key="dict.value"
                :label="dict.label"
                :value="dict.value"
              />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item :label="t('template.isShowPPt')" prop="showPpt">
            <el-select v-model="formData.showPpt" :placeholder="t('common.selectText') + t('template.isShowPPt')">
              <el-option
                v-for="dict in getIntDictOptions(DICT_TYPE.IS_OR_NOT)"
                :key="dict.value"
                :label="dict.label"
                :value="dict.value"
              />
            </el-select>
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="24">
          <el-form-item :label="t('template.templateSize')" prop="templateSize">
            <el-select
              style="width: 100%"
              v-model="formData.templateSize"
              clearable
              :placeholder="t('common.selectText') + t('template.templateSize')"
              class="!w-240px"
              @change="changeTemplateSize"
          <el-form-item label="背景图片" prop="bgImage">
            <el-upload
              v-model:file-list="fileList"
              class="upload-demo"
              :action="getUploadUrl"
              :auto-upload="false"
              :limit="2"
              :on-exceed="handleExceed"
              :before-upload="beforeUpload"
              :on-change="handleChange"
              accept="image/*"
              :show-file-list="false"
            >
              <el-option
                v-for="dict in getStrDictOptions(DICT_TYPE.TEMPLATE_SIZE)"
                :key="dict.value"
                :label="dict.label"
                :value="dict.value"
              />
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item :label="t('template.pptWidth')" prop="pptW">
            <el-input type="number" v-model="formData.pptW" :placeholder="t('common.inputText') + t('template.pptWidth')" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item :label="t('template.pptHeight')" prop="pptH">
            <el-input type="number" v-model="formData.pptH" :placeholder="t('common.inputText') + t('template.pptHeight')" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item :label="t('template.leftPositionPPT')" prop="pptX">
            <el-input type="number" v-model="formData.pptX" :placeholder="t('common.inputText') + t('template.topPositionPPT')" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item  :label="t('template.topPositionPPT')"  prop="pptY">
            <el-input type="number" v-model="formData.pptY" :placeholder="t('common.inputText') + t('template.leftPositionPPT')" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item :label="t('template.digitalPeopleWidth')" prop="humanW">
            <el-input type="number" disabled v-model="formData.humanW" :placeholder="t('common.inputText') + t('template.digitalPeopleWidth')" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item :label="t('template.digitalPeopleHeight')" prop="humanH">
            <el-input type="number" disabled v-model="formData.humanH" :placeholder="t('common.inputText') + t('template.digitalPeopleHeight')" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item  :label="t('template.leftPositionDigitalPeople')" prop="humanX">
            <el-input type="number" v-model="formData.humanX" :placeholder="t('common.inputText') + t('template.topPositionDigitalPeople')" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item :label="t('template.topPositionDigitalPeople')" prop="humanY">
            <el-input type="number" v-model="formData.humanY" :placeholder="t('common.inputText') + t('template.leftPositionDigitalPeople')" />
              <el-button type="primary">上传图片</el-button>
              <template #tip>
                <div class="el-upload__tip">
                  只能上传jpg/png文件
                </div>
              </template>
            </el-upload>
          </el-form-item>
        </el-col>
      </el-row>
@@ -130,22 +47,70 @@
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item :label="t('template.backgroundImage')" prop="bgImage">
            <UploadImg v-model="formData.bgImage" />
          </el-form-item>
        <el-col :span="18">
          <div ref="captureElement" style="width: 802px; height: 452px;border: 1px solid #C0C0C0;box-sizing: border-box;position: relative">
            <img :src="lastUploadedFileUrl" style="width: 100%; height: 100%" v-if="lastUploadedFileUrl!=''" />
            <Vue3DraggableResizable
              v-if="isChecked1"
              v-model:w="formData.humanW"
              v-model:h="formData.humanH"
              v-model:x="formData.humanX"
              v-model:y="formData.humanY"
              :initW="formData.humanW"
              :initH="formData.humanH"
              :lock-aspect-ratio="true"
              :minW="350"
            >
              <img
                src="@/assets/imgs/1.png"
                style="width: 100%; height: 100%; object-fit: contain;"
              />
            </Vue3DraggableResizable>
            <Vue3DraggableResizable
              v-if="isChecked"
              :initW="formData.pptW"
              :initH="formData.pptH"
              v-model:w="formData.pptW"
              v-model:h="formData.pptH"
              v-model:x="formData.pptX"
              v-model:y="formData.pptY"
              :lock-aspect-ratio="false"
              :parent="true"
              :minW="350"
            >
              <img
                src="@/assets/imgs/2.png"
                style="width: 100%; height: 100%; object-fit: cover;"
              />
            </Vue3DraggableResizable>
          </div>
        </el-col>
        <el-col :span="12">
          <el-form-item :label="t('template.reviewImage')" prop="previewImage">
            <UploadImg v-model="formData.previewImage" />
          </el-form-item>
        <el-col :span="6">
          <div style="width: 100%;height: 452px;border: 1px solid #C0C0C0;box-sizing: border-box">
            <div class="image-checkbox-wrapper" @click="toggleCheck">
              <img src="@/assets/imgs/2.png" alt="ppt示例图片" class="checkbox-image" />
              <input
                type="checkbox"
                v-model="isChecked"
                class="checkbox-input"
              />
            </div>
            <div class="image-checkbox-wrapper" @click="toggleCheck1">
              <img src="@/assets/imgs/1.png" alt="数字人示例图片" class="checkbox-image" />
              <input
                type="checkbox"
                v-model="isChecked1"
                class="checkbox-input"
              />
            </div>
          </div>
        </el-col>
      </el-row>
    </el-form>
    <template #footer>
      <el-button @click="submitForm" type="primary" :disabled="formLoading">{{ t('common.ok') }}</el-button>
      <el-button @click="dialogVisible = false">{{ t('common.cancel') }}</el-button>
      <el-button v-loading="IsUploadBack" @click="submitForm" type="primary" :disabled="formLoading">{{ t('common.ok') }}</el-button>
      <el-button @click="cancale">{{ t('common.cancel') }}</el-button>
    </template>
  </Dialog>
</template>
@@ -153,8 +118,90 @@
import { TemplateApi, TemplateVO } from '@/api/digitalcourse/template'
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
import {getUserProfile} from "@/api/system/user/profile";
import html2canvas from 'html2canvas';
/** 模板 表单 */
defineOptions({ name: 'TemplateForm' })
import { ElMessage } from 'element-plus';
import {updateFile} from "@/api/infra/file";
import Vue3DraggableResizable from 'vue3-draggable-resizable'
import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css'
import { ca } from 'element-plus/es/locale';
import { rule } from 'postcss';
import { truncate } from 'lodash-es';
const getUploadUrl = import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload'
const fileList = ref([]);
const lastUploadedFileUrl = ref('');
const handleExceed = () => {
  ElMessage.warning('只能上传一个文件');
};
// ppt
const isChecked = ref(false);
// 数字人
const isChecked1 = ref(false);
const toggleCheck = () => {
  isChecked.value = !isChecked.value;
  console.log(isChecked.value)
  if (isChecked.value==true) {
    formData.value.showPpt=1
  }else if (isChecked.value==false) {
    formData.value.showPpt=0
  }
  console.log(formData.value.showPpt)
};
const toggleCheck1 = () => {
  isChecked1.value =!isChecked1.value;
  if (isChecked1.value==true) {
    formData.value.showDigitalHuman=1
  }else if (isChecked1.value==false) {
    formData.value.showDigitalHuman=0
  }
}
// 当前是否在上传背景图
const IsUploadBack = ref(false)
const beforeUpload = (file) => {
  console.log("beforeUpload")
  const isImage = file.type.startsWith('image/');
  const isLt2M = file.size / 1024 / 1024 < 2;
  if (!isImage) {
    ElMessage.error('只能上传图片格式的文件!');
    return false;
  }
  if (!isLt2M) {
    ElMessage.error('图片大小不能超过2MB!');
    return false;
  }
  return true;
};
async function updataImage(formData1) {
  IsUploadBack.value = truncate
  const response= await updateFile(formData1)
  console.log(response.data)
  if (response) {
    formData.value.bgImage=response.data
    IsUploadBack.value = false
    ElMessage.success('上传成功');
  }
}
const handleChange = (file, files) => {
  // 当文件变化时,只保留最后一个文件
  if (files.length > 1) {
    fileList.value = [files[files.length - 1]];
  }
  console.log(file)
  const raw=file.raw
  console.log(raw)
  lastUploadedFileUrl.value = URL.createObjectURL(raw);
  console.log(lastUploadedFileUrl.value)
  const formData1 = new FormData();
  formData1.append('file', raw);
  updataImage(formData1)
};
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
@@ -165,20 +212,21 @@
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
  id: undefined,
  showBackground: undefined,
  showBackground: 1,
  templateName: undefined,
  showDigitalHuman: undefined,
  showPpt: undefined,
  pptW: undefined,
  pptH: undefined,
  pptX: '40',
  pptY: '77',
  humanW: undefined,
  humanH: undefined,
  humanX: '349',
  humanY: '92',
  showDigitalHuman: 0,
  showPpt: 0,
  pptW: 505,
  pptH: 290,
  pptX: 40,
  pptY: 50,
  humanW: 640,
  humanH: 360,
  humanX: 349,
  humanY: 92,
  bgImage: undefined,
})
const formRules = reactive({
  templateName: [{ required: true, message: t('template.name') + t('common.notEmpty'), trigger: 'blur' }],
  showBackground: [{ required: true, message: t('template.isShowBackground') + t('common.notEmpty'), trigger: 'blur' }],
@@ -194,6 +242,7 @@
  humanX: [{ required: true, message: t('template.topPositionDigitalPeople') + t('common.notEmpty'), trigger: 'blur' }],
  humanY: [{ required: true, message: t('template.leftPositionDigitalPeople') + t('common.notEmpty'), trigger: 'blur' }],
  zg: [{ required: true, message: '模板类型', trigger: 'blur' }],
  bgImage: [{ required: true, message: '请上传背景图片', trigger: 'blur' }],
})
const formRef = ref() // 表单 Ref
let ishasAdminRole = ref(false)
@@ -214,75 +263,188 @@
    ishasAdminRole=false
    formData.value.zg = '2'
  }
  fileList.value = []
  // 修改时,设置数据
  if (id) {
    formLoading.value = true
    try {
      formData.value = await TemplateApi.getTemplate(id)
      if( formData.value.showDigitalHuman === 1 ){
        isChecked1.value = true
      }
      isChecked.value = true
      lastUploadedFileUrl.value = formData.value.bgImage as unknown as string
    } finally {
      formLoading.value = false
    }
  }
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
const changeTemplateSize = () => {
  console.log(formData.value.templateSize)
  const screenWidth = window.screen.width;
  const screenHeight = window.screen.height;
  if(formData.value.templateSize=='16:9'){
    formData.value.humanW = screenWidth / 3;
    formData.value.humanH = screenHeight / 3;
    formData.value.pptW = '505';
    formData.value.pptH = '290';
  }else if(formData.value.templateSize=='9:16'){
    formData.value.humanH = screenWidth / 3;
    formData.value.humanW = screenHeight / 3;
    formData.value.pptH = '505';
    formData.value.pptW = '290';
  }
}
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
  // 校验表单
  await formRef.value.validate()
  // 提交请求
  formLoading.value = true
  try {
    const data = formData.value as unknown as TemplateVO
    if (formType.value === 'create') {
      await TemplateApi.createTemplate(data)
      message.success(t('common.createSuccess'))
    } else {
      await TemplateApi.updateTemplate(data)
      message.success(t('common.updateSuccess'))
  console.log(formData.value.showPpt)
  if( formData.value.showPpt === 0 ){
     message.error(t('common.NeedAddPpt'))
     return
  }
  const imageFile = await saveAsImage();
  if (imageFile) {
    const formData1 = new FormData();
    formData1.append('file', imageFile);
    const response = await updateFile(formData1);
    formData.value.previewImage = response.data;
    // 提交请求
    formLoading.value = true
    try {
      formData.value.humanX=formData.value.humanX.toString()
      formData.value.humanY=formData.value.humanY.toString()
      formData.value.pptX=formData.value.pptX.toString()
      formData.value.pptY=formData.value.pptY.toString()
      formData.value.pptW=formData.value.pptW.toString()
      formData.value.pptH=formData.value.pptH.toString()
      const data = formData.value as unknown as TemplateVO
      console.log(formData.value)
      if (formType.value === 'create') {
        await TemplateApi.createTemplate(data)
        message.success(t('common.createSuccess'))
      } else {
        await TemplateApi.updateTemplate(data)
        message.success(t('common.updateSuccess'))
      }
      dialogVisible.value = false
      // 发送操作成功的事件
      emit('success')
    } finally {
      formLoading.value = false
    }
    dialogVisible.value = false
    // 发送操作成功的事件
    emit('success')
  } finally {
  }else {
    ElMessage.error('保存截图失败');
    formLoading.value = false
  }
}
/** 重置表单 */
const resetForm = () => {
  formData.value = {
    id: undefined,
    showBackground: undefined,
    showDigitalHuman: undefined,
    showPpt: undefined,
    pptW: undefined,
    pptH: undefined,
    showBackground: 1,
    showDigitalHuman: 0,
    showPpt: 0,
    pptW: 505,
    pptH: 290,
    pptX: 40,
    pptY: 50,
    humanW: 640,
    humanH: 360,
    humanX: 349,
    humanY: 92,
    bgImage: undefined,
    pptX: '40',
    pptY: '77',
    humanW: undefined,
    humanH: undefined,
    humanX: '349',
    humanY: '92',
    zg:1,
    templateSize: '16:9',
  }
  formRef.value?.resetFields()
}
// 取消按钮
const cancale = ()=>{
  resetForm()
  // 页面中不显示弹窗,背景图,数字人,ppt
  lastUploadedFileUrl.value = ""
  isChecked.value = false
  isChecked1.value = false
  dialogVisible.value = false
}
const captureElement = ref<HTMLElement | null>(null);
const saveAsImage = async () => {
  if (!captureElement.value) {
    console.error('DOM 元素未找到!');
    return;
  }
  try {
    const canvas = await html2canvas(captureElement.value, {
      backgroundColor: null,
      scale: 1,
      useCORS: true,
    });
    return new Promise((resolve) => {
      canvas.toBlob((blob) => {
        const file = new File([blob], 'screenshot.png', {
          type: 'image/png',
        });
        resolve(file);
      }, 'image/png');
    });
  }
  catch (error) {
    console.error('生成图片失败:', error);
    return null;
  }
}
onMounted(() => {
  console.log('DOM 已渲染:', captureElement.value);
});
</script>
<style scoped lang="scss">
.image-checkbox-container {
  display: inline-block;
  position: relative;
}
.image-checkbox-wrapper {
  position: relative;
  cursor: pointer;
}
.checkbox-image {
  width: 230px;
  height: 150px;
  object-fit: cover;
  border-radius: 4px;
  border: 1px solid #ddd;
  transition: all 0.3s;
  margin-left: 20px;
  margin-top: 20px;
}
.checkbox-input {
  position: absolute;
  top: 30px;
  left: 30px;
  width: 20px;
  height: 20px;
  cursor: pointer;
  z-index: 2;
  appearance: none; /* 隐藏默认样式 */
  -webkit-appearance: none;
  background-color: #fff;
  border: 1px solid #ddd;
  border-radius: 4px;
}
/* 选中状态样式 */
.checkbox-input:checked {
  background-color: #409eff; /* 蓝色背景 */
  border-color: #409eff; /* 蓝色边框 */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 14px;
}
.image-checkbox-wrapper:hover .checkbox-image {
  border-color: #ddd;
}
.checkbox-input:checked ~ .checkbox-image {
  border: 1px solid #ddd; /* 选中时保持灰色1px边框 */
}
</style>