lisy vor 3 Monaten
Ursprung
Commit
4d9f2d9b02

+ 3 - 1
.env.development

@@ -40,4 +40,6 @@ VITE_APP_SSE = true
 VITE_APP_PACKAGE_SHARE_URL = 'http://dm.share.yujin.shuziyunyao.com/package'
 # 测试版微信小程序扫码域名
 VITE_APP_SHARE_QR_CODE_URL = 'http://dm.share.yujin.shuziyunyao.com'
-VITE_H5_URL = 'https://tlm.yujin.shuziyunyao.com/'
+VITE_H5_URL = 'https://tlm.yujin.shuziyunyao.com/'
+# 会议门户地址
+VITE_APP_MEETING_URL = 'https://lm.yujin.shuziyunyao.com/trainpage'

+ 2 - 0
.env.production

@@ -46,3 +46,5 @@ VITE_APP_APPID = '1890328853823459329'
 VITE_APP_SHARE_QR_CODE_URL = 'https://lm.tcmma.com.cn'
 
 VITE_H5_URL = 'https://t.tcmma.com.cn/'
+# 会议门户地址
+VITE_APP_MEETING_URL = 'https://t.tcmma.com.cn/trainpage'

+ 1 - 0
package.json

@@ -22,6 +22,7 @@
     "url": "https://gitee.com/JavaLionLi/plus-ui.git"
   },
   "dependencies": {
+    "@gausszhou/vue3-drag-resize-rotate": "^3.0.2",
     "@element-plus/icons-vue": "2.3.1",
     "@highlightjs/vue-plugin": "2.1.0",
     "@vueup/vue-quill": "1.2.0",

+ 70 - 0
src/api/training/index.ts

@@ -107,3 +107,73 @@ export const offOrNoTemp = (params: any): AxiosPromise => {
         params
     });
 };
+// 修改价格
+export const editPrice = (data: any): AxiosPromise => {
+    return request({
+        url: `/dgtmedicine/trainingSignup/editPrice`,
+        method: 'post',
+        data
+    });
+};
+// 开关会议临时状态
+export const confirmSigPublicPay = (id: any, payType:any): AxiosPromise => {
+    return request({
+        url: `/dgtmedicine/trainingSignup/confirmSigPublicPay/${id}?payType=${payType}`,
+        method: 'get'
+    });
+};
+// 上传发票
+export const uploadInvoice = (data: any): AxiosPromise => {
+    return request({
+        url: `/dgtmedicine/trainingSignup/uploadInvoice`,
+        method: 'post',
+        data
+    });
+};
+// 会议门户开关
+export const switchPage = (params: any): AxiosPromise => {
+    return request({
+        url: `/dgtmedicine/training/switchPage`,
+        method: 'get',
+        params
+    });
+};
+// 导入会议特殊人员收费列表
+export const importFeeList = (data: any): AxiosPromise => {
+    return request({
+        url: `/dgtmedicine/training/importFeeList`,
+        method: 'post',
+        data
+    });
+};
+// 查询培训特殊人员收费列表
+export const trainingfee = (params: any): AxiosPromise => {
+    return request({
+        url: `/dgtmedicine/trainingfee/list`,
+        method: 'get',
+        params
+    });
+};
+// 清空会议特殊人员收费列表
+export const clearFeeList = (id: any): AxiosPromise => {
+    return request({
+        url: `/dgtmedicine/training/clearFeeList/${id}`,
+        method: 'get'
+    });
+};
+//报名信息标注
+export const markTags = (data: any): AxiosPromise => {
+    return request({
+        url: `/dgtmedicine/trainingSignup/markTags`,
+        method: 'post',
+        data
+    });
+};
+//修改指定联系人
+export const signupContact = (data: any): AxiosPromise => {
+    return request({
+        url: `/dgtmedicine/trainingSignup/signupContact`,
+        method: 'post',
+        data
+    });
+};

BIN
src/assets/images/bg_music_icon.png


BIN
src/assets/images/has_page_index_icon.png


BIN
src/assets/images/set_index_icon.png


+ 95 - 0
src/components/DragResizeRotate/DragResizeRotate.vue

@@ -0,0 +1,95 @@
+<template>
+    <VueDragResizeRotate :x="+drapJson.x" :y="+drapJson.y" class-name-handle="my-handle-class-dd handle" class-name-active="my-active-class-dd" class-name="my-class-dd" :w="+drapJson.w" :h="+drapJson.h" :r="+drapJson.r" :parent="true" @resizing="onResize" @resizestop="onResizeStop" @activated="onActivated" @deactivated="onDeactivated" @dragging="onDrag" @dragstop="onDragStop">
+        <slot>
+            <div class="w-100% h-100% d-flex a-c j-c p-rtv" style="background-color: rgba(116, 251, 229, .3)">
+                <span></span>
+                <div class="f-s-18 c-danger delete-icon_box" @click.stop="deleteItem">
+                    <el-icon>
+                        <CircleCloseFilled />
+                    </el-icon>
+                </div>
+            </div>
+        </slot>
+    </VueDragResizeRotate>
+</template>
+<script setup lang="ts" name="ptpl-edit-index">
+import { propTypes } from '@/utils/propTypes';
+import VueDragResizeRotate from '@gausszhou/vue3-drag-resize-rotate';
+import '@gausszhou/vue3-drag-resize-rotate/lib/bundle.esm.css';
+const prop = defineProps({
+    modelValue: propTypes.any.def({
+        id: ''
+    })
+});
+const emit = defineEmits([
+    'update:modelValue',
+    'activated',
+    'deactivated',
+    'dragging',
+    'dragstop',
+    'resizing',
+    'resizestop',
+    'delete'
+]);
+const drapJson = ref({
+     x: 0, y: 0, w: 100, h: 100,
+     r:0,
+    ...prop.modelValue
+});
+const onActivated = () => {
+    emit('activated');
+};
+const onDeactivated = () => {
+    emit('deactivated');
+};
+const onDrag = (left: number, top: number) => {
+    emit('dragging', left, top);
+};
+const onDragStop = (x: number, y: number) => {
+    emit('update:modelValue', { ...drapJson.value, x, y });
+    emit('dragstop', x, y);
+};
+const onResize = (x: number, y: number, width: number, height: number) => {
+    emit('resizing', x, y, width, height);
+};
+const onResizeStop = (x: number, y: number, width: number, height: number) => {
+    emit('update:modelValue', { ...drapJson.value, x, y, w: width, h: height  });
+    emit('resizestop', x, y, width, height);
+};
+const deleteItem = () => {
+    emit('deactivated');
+    emit('delete')
+};
+watch(() => prop.modelValue, (newVal) => {
+    drapJson.value = { ...drapJson.value, ...newVal };
+});
+</script>
+<style lang="scss" scoped>
+.delete-icon_box {
+    position: absolute;
+    top: 0px;
+    right: 0px;
+    width: 24px;
+    height: 24px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.my-active-class-dd {
+    border-color: rgb(116, 251, 229);
+}
+
+.my-class-dd {
+    touch-action: none;
+    position: absolute;
+    box-sizing: border-box;
+    border: 2px dashed;
+    border-color: #74FBE5;
+}
+
+.my-handle-class-dd {
+    border: 2px solid #74FBE5;
+}
+</style>

+ 140 - 0
src/components/TelViewTem/TelViewTem.vue

@@ -0,0 +1,140 @@
+<template>
+    <div class="tel-view-tem" @mousedown="onMouseDown">
+        <img class="bg-src" :src="bgSrc" />
+        <!-- 拖动选区样式 -->
+        <div v-if="isDragging" class="drag-area" :style="dragAreaStyle"></div>
+        <div class="pro-content">
+            <slot></slot>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue';
+import { propTypes } from '@/utils/propTypes';
+const props = defineProps({
+  width: propTypes.number.def(750),
+  minHeight: propTypes.number.def(1000),
+  bgColor: propTypes.string.def('#fff'),
+  enableDraw: propTypes.bool.def(true),
+  bgSrc: propTypes.string.def(''),
+});
+const emit = defineEmits(['selectArea']);
+const isDragging = ref(false);
+const startPoint = ref({ x: 0, y: 0 });
+const endPoint = ref({ x: 0, y: 0 });
+const imgHeight = ref(props.minHeight);
+const imgWidth = ref(props.width);
+const containerStyle: any = computed(() => {
+  if (props.bgSrc) {
+    return {
+      width: props.width ? `${props.width}px` : '100%',
+      height: `${imgHeight.value}px`,
+      backgroundImage: `url(${props.bgSrc})`,
+      backgroundSize: 'contain',
+      backgroundRepeat: 'no-repeat',
+      backgroundPosition: 'center',
+      position: 'relative',
+      userSelect: 'none',
+      minHeight: `${imgHeight.value}px`,
+      backgroundColor: props.bgColor,
+    };
+  }
+  return {
+    width: `${props.width}px`,
+    minHeight: `${props.minHeight}px`,
+    background: props.bgColor,
+    position: 'relative',
+    userSelect: 'none',
+  };
+});
+
+const dragAreaStyle: any = computed(() => {
+  const x = Math.min(startPoint.value.x, endPoint.value.x);
+  const y = Math.min(startPoint.value.y, endPoint.value.y);
+  const w = Math.abs(endPoint.value.x - startPoint.value.x);
+  const h = Math.abs(endPoint.value.y - startPoint.value.y);
+  return {
+    position: 'absolute',
+    left: `${x}px`,
+    top: `${y}px`,
+    width: `${w}px`,
+    height: `${h}px`,
+    border: '2px dashed #409eff',
+    background: 'rgba(116, 251, 229,0.3)',
+    pointerEvents: 'none',
+    zIndex: 10,
+  };
+});
+
+function onMouseDown(e: MouseEvent) {
+  if (!props.enableDraw) return;
+  if (e.button !== 0) return;
+  const rect = (e.target as HTMLElement).getBoundingClientRect();
+  startPoint.value = {
+    x: e.clientX - rect.left,
+    y: e.clientY - rect.top,
+  };
+  endPoint.value = { ...startPoint.value };
+  isDragging.value = true;
+
+  window.addEventListener('mousemove', onMouseMove);
+  window.addEventListener('mouseup', onMouseUp);
+}
+
+function onMouseMove(e: MouseEvent) {
+  if (!isDragging.value) return;
+  const rect = (e.target as HTMLElement).closest('.tel-view-tem')?.getBoundingClientRect();
+  if (!rect) return;
+  endPoint.value = {
+    x: e.clientX - rect.left,
+    y: e.clientY - rect.top,
+  };
+}
+
+function onMouseUp() {
+  if (!isDragging.value) return;
+  isDragging.value = false;
+  emit('selectArea', {
+    start: { ...startPoint.value },
+    end: { ...endPoint.value },
+    rect: {
+      x: Math.min(startPoint.value.x, endPoint.value.x),
+      y: Math.min(startPoint.value.y, endPoint.value.y),
+      w: Math.abs(endPoint.value.x - startPoint.value.x),
+      h: Math.abs(endPoint.value.y - startPoint.value.y),
+    },
+  });
+  window.removeEventListener('mousemove', onMouseMove);
+  window.removeEventListener('mouseup', onMouseUp);
+}
+</script>
+
+<style scoped lang="scss">
+.tel-view-tem {
+  box-sizing: border-box;
+  overflow: hidden;
+  position: relative;
+}
+.drag-area {
+  position: relative;
+  z-index: 100;
+  transition: none;
+}
+.bg-src {
+  width: 750px;
+  display: block;
+  user-select: none;
+  pointer-events: none;
+  height: auto;
+  object-fit: contain;
+  z-index: -1;
+}
+.pro-content {
+  position: absolute;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+}
+</style>

+ 49 - 49
src/directive/common/copyText.ts

@@ -5,63 +5,63 @@
 import { DirectiveBinding } from 'vue';
 
 export default {
-  beforeMount(el: any, { value, arg }: DirectiveBinding) {
-    if (arg === 'callback') {
-      el.$copyCallback = value;
-    } else {
-      el.$copyValue = value;
-      const handler = () => {
-        copyTextToClipboard(el.$copyValue);
-        if (el.$copyCallback) {
-          el.$copyCallback(el.$copyValue);
+    beforeMount(el: any, { value, arg }: DirectiveBinding) {
+        if (arg === 'callback') {
+            el.$copyCallback = value;
+        } else {
+            el.$copyValue = value;
+            const handler = () => {
+                copyTextToClipboard(el.$copyValue);
+                if (el.$copyCallback) {
+                    el.$copyCallback(el.$copyValue);
+                }
+            };
+            el.addEventListener('click', handler);
+            el.$destroyCopy = () => el.removeEventListener('click', handler);
         }
-      };
-      el.addEventListener('click', handler);
-      el.$destroyCopy = () => el.removeEventListener('click', handler);
     }
-  }
 };
 
-function copyTextToClipboard(input: string, { target = document.body } = {}) {
-  const element = document.createElement('textarea');
-  const previouslyFocusedElement = document.activeElement as HTMLInputElement;
-  element.value = input;
-  // Prevent keyboard from showing on mobile
-  element.setAttribute('readonly', '');
+export function copyTextToClipboard(input: string, { target = document.body } = {}) {
+    const element = document.createElement('textarea');
+    const previouslyFocusedElement = document.activeElement as HTMLInputElement;
+    element.value = input;
+    // Prevent keyboard from showing on mobile
+    element.setAttribute('readonly', '');
 
-  element.style.contain = 'strict';
-  element.style.position = 'absolute';
-  element.style.left = '-9999px';
-  element.style.fontSize = '12pt'; // Prevent zooming on iOS
+    element.style.contain = 'strict';
+    element.style.position = 'absolute';
+    element.style.left = '-9999px';
+    element.style.fontSize = '12pt'; // Prevent zooming on iOS
 
-  const selection = document.getSelection();
-  let originalRange;
-  if (selection) {
-    originalRange = selection?.rangeCount > 0 && selection.getRangeAt(0);
-  }
-  target.append(element);
-  element.select();
+    const selection = document.getSelection();
+    let originalRange;
+    if (selection) {
+        originalRange = selection?.rangeCount > 0 && selection.getRangeAt(0);
+    }
+    target.append(element);
+    element.select();
 
-  // Explicit selection workaround for iOS
-  element.selectionStart = 0;
-  element.selectionEnd = input.length;
+    // Explicit selection workaround for iOS
+    element.selectionStart = 0;
+    element.selectionEnd = input.length;
 
-  let isSuccess = false;
-  try {
-    isSuccess = document.execCommand('copy');
-  } catch (err) {
-    console.error(err);
-  }
-  element.remove();
+    let isSuccess = false;
+    try {
+        isSuccess = document.execCommand('copy');
+    } catch (err) {
+        console.error(err);
+    }
+    element.remove();
 
-  if (originalRange) {
-    selection?.removeAllRanges();
-    selection?.addRange(originalRange);
-  }
+    if (originalRange) {
+        selection?.removeAllRanges();
+        selection?.addRange(originalRange);
+    }
 
-  // Get the focus back on the previously focused element, if any
-  if (previouslyFocusedElement) {
-    previouslyFocusedElement.focus();
-  }
-  return isSuccess;
+    // Get the focus back on the previously focused element, if any
+    if (previouslyFocusedElement) {
+        previouslyFocusedElement.focus();
+    }
+    return isSuccess;
 }

+ 1 - 12
src/views/components/H5ModelLook.vue

@@ -1,16 +1,5 @@
 <template>
-    <vxe-modal
-        v-model="dialogVisible"
-        :title="title"
-        resize
-        :show-footer="false"
-        destroy-on-close
-        transfer
-        height="80vh"
-        mask-closable
-        @hide="close"
-        :width="width"
-    >
+    <vxe-modal v-model="dialogVisible" :title="title" resize :show-footer="false" destroy-on-close transfer height="80vh" mask-closable @hide="close" :width="width">
         <template #default>
             <iframe :src="src" class="iframe-wrapper"></iframe>
         </template>

+ 7 - 7
src/views/training/materials/index.vue

@@ -38,17 +38,17 @@
                 <div class="flex1 ov-hd">
                     <vxe-table :loading="loading" border :data="dataList" min-height="0" max-height="100%">
                         <vxe-column title="序号" align="center" type="seq" width="60" />
-                        <vxe-column title="标题" align="center" field="newsTitle" :formatter="colNoData" width="400" />
-                        <vxe-column title="发布时间" align="center" field="publishTime" :formatter="colNoData" />
-                        <vxe-column title="来源" align="center" field="newsOrigin" :formatter="colNoData" />
-                        <vxe-column title="浏览量" align="center" field="viewCount" />
-                        <vxe-column title="类型" align="center" field="newsType" class="materials_downloads_type">
+                        <vxe-column title="标题" align="center" field="newsTitle" :formatter="colNoData" min-width="200" />
+                        <vxe-column title="发布时间" align="center" field="publishTime" :formatter="colNoData" min-width="100" />
+                        <vxe-column title="来源" align="center" field="newsOrigin" :formatter="colNoData" min-width="100" />
+                        <vxe-column title="浏览量" align="center" field="viewCount" min-width="70" />
+                        <vxe-column title="类型" align="center" field="newsType" class="materials_downloads_type" min-width="70">
                             <template #default="{ row }">
                                 <dict-tag :options="materials_downloads_type" :value="row.newsType" />
                             </template>
                         </vxe-column>
-                        <vxe-column title="操作人" align="center" field="createByName" :formatter="colNoData" />
-                        <vxe-column title="操作时间" align="center" field="createTime" :formatter="colNoData" />
+                        <vxe-column title="操作人" align="center" field="createByName" :formatter="colNoData" min-width="100" />
+                        <vxe-column title="操作时间" align="center" field="createTime" :formatter="colNoData" min-width="100" />
                         <vxe-column title="状态" align="center" field="newsStatus">
                             <template #default="{ row }">
                                 <dict-tag :options="news_status" :value="row.newsStatus" />

+ 179 - 37
src/views/training/meeting-add/index.vue

@@ -53,8 +53,35 @@
                                     <el-input v-model="form.tel" maxlength="20" placeholder="请输入联系电话" clearable />
                                 </el-form-item>
                             </el-col>
-                            <el-col :span="12">
-                                <el-form-item label="可报名单位类型" prop="conditions.typeCheck">
+                            <el-col :span="6">
+                                <div class="d-flex flex-cln j-st" style="">
+                                    <el-form-item label="发放积分" prop="pointsFlag" class="">
+                                        <el-radio-group v-model="form.pointsFlag" style="flex-wrap: nowrap">
+                                            <el-radio label="1">是</el-radio>
+                                            <el-radio label="0">否</el-radio>
+                                        </el-radio-group>
+                                    </el-form-item>
+                                    <el-form-item label="" prop="points" v-if="form.pointsFlag == '1'" class="flex1">
+                                        <div class="d-flex f-s-14" style="white-space: nowrap;">
+                                            <div>每成功参会(签到成功)1人发放</div>
+                                            <el-input v-model="form.points" style="width: 45px" />
+                                            <div>个单位积分。</div>
+                                        </div>
+                                    </el-form-item>
+                                </div>
+                            </el-col>
+                            <el-col :span="6">
+                                <el-form-item label="是否电子手签" prop="eleSignature">
+                                    <div class="d-flex a-c">
+                                        <el-radio-group v-model="form.eleSignature" style="flex-wrap: nowrap">
+                                            <el-radio label="1">是</el-radio>
+                                            <el-radio label="0">否</el-radio>
+                                        </el-radio-group>
+                                    </div>
+                                </el-form-item>
+                            </el-col>
+                            <el-col :span="24">
+                                <el-form-item label="可报名人员类型" prop="conditions.typeCheck">
                                     <el-checkbox-group v-model="checkedVipLevels" @change="handleCheckedChange">
                                         <el-checkbox v-for="city in form?.conditions?.typeCheck" :key="city" :label="city" :value="city">
                                             {{ selectDictLabels(lm_check_join_type, city.vipLevel, ',') }}
@@ -76,7 +103,7 @@
                                     </div>
                                 </el-form-item>
                             </el-col>
-                            <el-col :span="12" v-if="form?.conditions?.totalCheck == '1'">
+                            <el-col :span="18" v-if="form?.conditions?.totalCheck == '1'">
                                 <el-form-item prop="restrictiveConditions">
                                     <template #label>
                                         <span>限制条件</span>
@@ -136,31 +163,53 @@
                                     </div>
                                 </el-form-item>
                             </el-col>
-                            <el-col :span="6">
-                                <el-form-item label="是否电子手签" prop="eleSignature">
+                            <el-col :span="24">
+                                <el-form-item label="是否收取参会费用" prop="meetingCharge.hasFee">
                                     <div class="d-flex a-c">
-                                        <el-radio-group v-model="form.eleSignature" style="flex-wrap: nowrap">
-                                            <el-radio label="1">是</el-radio>
+                                        <el-radio-group v-model="form.meetingCharge.hasFee" style="flex-wrap: nowrap">
                                             <el-radio label="0">否</el-radio>
+                                            <el-radio label="1">是</el-radio>
                                         </el-radio-group>
                                     </div>
                                 </el-form-item>
-                            </el-col>
-                            <el-col :span="12">
-                                <div class="d-flex" style="align-items: flex-end;">
-                                    <el-form-item label="发放积分" prop="pointsFlag" class="">
-                                        <el-radio-group v-model="form.pointsFlag" style="flex-wrap: nowrap">
-                                            <el-radio label="1">是</el-radio>
-                                            <el-radio label="0">否</el-radio>
+                                <el-form-item prop="meetingCharge.pricing" v-if="form.meetingCharge.hasFee == '1'">
+                                    <div class="d-flex">
+                                        <div class="c-#606266 f-w-6" style="">收费标准:</div>
+                                        <el-input class="flex1 pl-5" v-model="form.meetingCharge.pricing" maxlength="20" placeholder="请输入收费标准" clearable style="max-width: 200px;" />
+                                        <div class="pl-10">元/人</div>
+                                    </div>
+                                </el-form-item>
+                                <el-form-item v-if="form.meetingCharge.hasFee == '1'" prop="meetingCharge.hasFlatFee">
+                                    <div>
+                                        <el-radio-group v-model="form.meetingCharge.hasFlatFee" style="display: flex;flex-direction: column;align-items: flex-start;">
+                                            <el-radio label="0">所有人统一收取标准费用</el-radio>
+                                            <el-radio label="1">
+                                                按报名人员类型收取,不同人员收取不同费用
+                                                <span class="c-999">(不作设置默认统一收取标准费用。)</span>
+                                            </el-radio>
                                         </el-radio-group>
-                                    </el-form-item>
-                                    <el-form-item label="" prop="points" v-if="form.pointsFlag == '1'" class="flex1 pl-10">
-                                        <div class="d-flex f-s-14">
-                                            <div>每成功参会(签到成功)1人发放</div>
-                                            <el-input v-model="form.points" style="width: 60px" />
-                                            <div>个单位积分。</div>
-                                        </div>
-                                    </el-form-item>
+                                    </div>
+                                </el-form-item>
+                                <div ref="hasFee" class="d-flex flex-cln" v-if="form?.meetingCharge?.hasFee == '1' && form?.meetingCharge?.hasFlatFee == '1'">
+                                    <div class="pl-10 pr-10 pt-5 pb-5 border">
+                                        <template v-for="(item, index) in form.meetingCharge.typeCharge" :key="index">
+                                            <div class="d-flex a-c" v-if="checkedVipLevels.some(items => items.vipLevel === item.vipLevel)">
+                                                <el-checkbox v-model="item.check" true-value="1" false-value="0" :label="selectDictLabels(lm_check_join_type, item.vipLevel, ',') + '每个单位参会人员'" size="large" />
+                                                <el-select v-model="item.certType" placeholder="" clearable style="width: 100px" :disabled="!+item.check">
+                                                    <el-option v-for="items in hasPartialFree" :key="items.value" :label="items.label" :value="items.value" />
+                                                </el-select>
+                                                <div class="d-flex a-c">
+                                                    <div v-if="item.vipLevel != 'P'" class="pl-10 f-s-14" style="white-space: nowrap;">每个单位免费</div>
+                                                    <div v-else class="pl-10 f-s-14" style="white-space: nowrap;">免费</div>
+                                                    <el-input class="pl-10" v-model="item.total" maxlength="20" placeholder="请输入免费人数" style="width: 130px" :disabled="!+item.check || item?.certType !== '1'" />
+                                                    <div class="f-s-14" style="white-space: nowrap;">人,其余每人收费</div>
+                                                    <el-input class="pl-10" v-model="item.cost" maxlength="20" placeholder="请输入费用" style="width: 130px" :disabled="!+item.check || item?.certType !== '1'" />
+                                                    <div f-s-14>元</div>
+                                                </div>
+                                            </div>
+                                        </template>
+                                        <el-empty :image-size="20" description="请先选择报名人员类型" v-if="checkedVipLevels.length == 0" />
+                                    </div>
                                 </div>
                             </el-col>
                         </el-row>
@@ -300,6 +349,7 @@ const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { lm_training_join_type, yes_no, lm_training_cert, lm_check_join_type } = toRefs<any>(proxy?.useDict('lm_training_join_type', 'yes_no', 'lm_training_cert', 'lm_check_join_type'));
 const fields = ref<FieldDefinition[]>([])
 const showSignIn = ref(false);
+const hasFee = ref<any>()
 const fixedField = ref<FieldDefinition[]>([{
     name: `ent-${generateSecureRandomString()}`,
     label: '企业名称', type: '1',
@@ -315,7 +365,14 @@ const fixedField = ref<FieldDefinition[]>([{
 }, {
     name: `Con-${generateSecureRandomString()}`,
     label: '联系方式', type: '1', readonly: '0',
-    required: '1', pattern: '/^\d{11}$/'
+    required: '1'
+}])
+const hasPartialFree = ref([{
+    label: '全部免费',
+    value: '0'
+}, {
+    label: '部分免费',
+    value: '1'
 }])
 const scrollOptions = {
     block: 'center',
@@ -453,7 +510,74 @@ const form = ref<any>({
             vipLevel: '10',
             check: "0",
             total: ''
-        }]
+        }],
+    },
+    // 收取参会费用
+    meetingCharge: {
+        hasFee: null,//是否收取参会费用
+        pricing: null,//收费标准
+        hasFlatFee: null,//收费标准类型 0所有人统一收取费用 1按报名人员类型收取
+        typeCharge: [
+        {
+            vipLevel: '0',
+            check: "0",
+            total: '',
+            certType: '1',
+            cost: null
+        },{
+            vipLevel: '1',
+            check: "0",
+            total: '',
+            certType: '1',
+            cost: null
+        }, {
+            vipLevel: '3',
+            check: "0",
+            total: '',
+            certType: '1',
+            cost: null
+        }, {
+            vipLevel: '5',
+            check: "0",
+            total: '',
+            certType: '1',
+            cost: null
+        },
+        {
+            vipLevel: '6',
+            check: "0",
+            total: '',//免费的人数
+            certType: '1',//全部免费还是部分免费 0全部 1部分
+            cost: null //每人收费多少
+        },
+        {
+            vipLevel: '7',
+            check: "0",
+            total: '',//免费的人数
+            certType: '1',//全部免费还是部分免费 0全部 1部分
+            cost: null //每人收费多少
+        },
+        {
+            vipLevel: '9',
+            check: "0",
+            total: '',//免费的人数
+            certType: '1',//全部免费还是部分免费 0全部 1部分
+            cost: null //每人收费多少
+        },
+        {
+            vipLevel: '10',
+            check: "0",
+            total: '',//免费的人数
+            certType: '1',//全部免费还是部分免费 0全部 1部分
+            cost: null //每人收费多少
+        },
+        {
+            vipLevel: "P",
+            check: "0",
+            total: '',
+            certType: '1',
+            cost: null
+        }],
     }
 });
 
@@ -484,12 +608,12 @@ const rules = reactive({
     trainingTime: [{ required: true, message: '请选择会议时间', trigger: 'blur' }],
     joinType: [{ required: true, message: '请选择会议方式', trigger: 'change' }],
     trainingLocation: [{ required: true, message: '请输入会议地点', trigger: 'blur' }],
-    'conditions.totalCheck': [{ message: '请选择是否限制报名人数', trigger: 'change' }],
+    'conditions.totalCheck': [{ required: true, message: '请选择是否限制报名人数', trigger: 'change' }],
     'conditions.typeCheck': [
         {
             validator: (rule, value, callback) => {
                 setTimeout(() => {
-                    const isChecked = value.some(item => item.check == "1");
+                    const isChecked = value?.some(item => item.check == "1");
                     if (!isChecked) {
                         callback(new Error('请至少选择一种可报名单位类型'));
                     } else {
@@ -501,6 +625,9 @@ const rules = reactive({
             trigger: 'change' // 触发校验的时机
         }
     ],
+    'meetingCharge.hasFee': [{ required: true, message: '请选择是否收取参会费用', trigger: 'change' }],
+    'meetingCharge.pricing': [{ required: true, message: '请输入收费标准', trigger: 'blur' }],
+    'meetingCharge.hasFlatFee': [{ required: true, message: '请选择收费标准', trigger: 'change' }],
     certFlag: [{ required: true, message: '请选择是否颁发证书', trigger: 'change' }],
     certificateInfo: [{ required: true, message: '请选择证书名称', trigger: 'change' }],
     description: [{ required: true, message: '请输入会议详情', trigger: 'blur' }],
@@ -514,16 +641,28 @@ const rules = reactive({
 const formRef = ref();
 const save = debounce(async () => {
     await formRef.value.validate();
-    form.value.conditions.typeCheck.forEach(typeItem => {
+    if (+form.value?.meetingCharge?.hasFee && +form.value?.meetingCharge?.hasFlatFee) {
+        form.value.meetingCharge.typeCharge.forEach((i) => {
+            if (+i.check && +i.certType) {
+                if (!+i.cost || !+i.total) {
+                    hasFee.value.scrollIntoView({
+                        behavior: 'smooth',
+                        block: 'nearest'
+                    })
+                }
+            }
+        })
+    }
+    form?.value?.conditions?.typeCheck?.forEach(typeItem => {
         if (typeItem.check === "0") {
             // Update cpyCheck
-            const cpyItem = form.value.conditions.cpyCheck.find(item => item.vipLevel === typeItem.vipLevel);
+            const cpyItem = form.value?.conditions?.cpyCheck?.find(item => item.vipLevel === typeItem.vipLevel);
             if (cpyItem) {
                 cpyItem.check = "0";
             }
 
             // Update levelCheck
-            const levelItem = form.value.conditions.levelCheck.find(item => item.vipLevel === typeItem.vipLevel);
+            const levelItem = form.value?.conditions?.levelCheck?.find(item => item.vipLevel === typeItem.vipLevel);
             if (levelItem) {
                 levelItem.check = "0";
             }
@@ -531,11 +670,11 @@ const save = debounce(async () => {
     });
     const params = {
         ...form.value,
-        trainingStart: form.value.trainingTime ? form.value.trainingTime[0] : undefined,
-        trainingEnd: form.value.trainingTime ? form.value.trainingTime[1] : undefined,
-        signupStart: form.value.signupsTime ? form.value.signupsTime[0] : undefined,
-        signupEnd: form.value.signupsTime ? form.value.signupsTime[1] : undefined,
-        certificateInfo: +form.value.certFlag ? form.value.certificateInfo : undefined,
+        trainingStart: form.value?.trainingTime ? form.value.trainingTime[0] : undefined,
+        trainingEnd: form.value?.trainingTime ? form.value.trainingTime[1] : undefined,
+        signupStart: form.value?.signupsTime ? form.value.signupsTime[0] : undefined,
+        signupEnd: form.value?.signupsTime ? form.value.signupsTime[1] : undefined,
+        certificateInfo: +form.value?.certFlag ? form.value.certificateInfo : undefined,
         questions: fields.value.map((item, index) => ({
             ...item,
             sort: index + 1  // 从 1 开始
@@ -547,7 +686,7 @@ const save = debounce(async () => {
     }
 }, 500);
 const goEditor = () => {
-    window.open('https://lm.yujin.shuziyunyao.com/poster#/editortype=1', '_blank');
+    window.open('https://lm.yujin.shuziyunyao.com/poster#/editor?type=1', '_blank');
 }
 
 const addCertInfo = () => {
@@ -571,9 +710,12 @@ const getMeetingDetail = async () => {
             ...res.data,
             trainingTime: res.data?.trainingStart && res.data?.trainingEnd ? [res.data.trainingStart, res.data.trainingEnd] : undefined,
             signupsTime: res.data?.signupStart && res.data?.signupEnd ? [res.data.signupStart, res.data.signupEnd] : undefined,
-            conditions: (res.data?.conditions?.typeCheck == null) ? form.value.conditions : (res.data?.conditions || form.value.conditions)
-        };
+            conditions: (res.data?.conditions?.typeCheck == null) ? form.value.conditions : (res.data?.conditions || form.value.conditions),
+            meetingCharge: res.data?.meetingCharge || form.value.meetingCharge};
         fields.value = res.data.questions
+        if (form.value?.meetingCharge.pricing && typeof form.value?.meetingCharge.pricing === 'string') {
+            form.value.meetingCharge.pricing = Number(form.value?.meetingCharge.pricing)
+        }
         form?.value?.conditions?.typeCheck?.forEach((i) => {
             if (i.check == '1') {
                 checkedVipLevels.value.push(i)

+ 3 - 2
src/views/training/meeting-detail/index.vue

@@ -19,13 +19,14 @@
             </div>
             <MeetingDetailAttend v-if="activeName === '1'" :form="form" />
             <MeetingDetailInfo v-if="activeName === '2'" :form="form" />
+            <meetingSpecialList v-if="activeName === '3'" :form="form" />
         </div>
     </div>
 </template>
 
 <script setup name="meeting-detail" lang="ts">
 import { ref, reactive, onMounted } from 'vue';
-import { MeetingDetailInfo } from '../models';
+import { MeetingDetailInfo,meetingSpecialList } from '../models';
 import router from '@/router';
 import { trainingDetailById, trainingMembers } from '@/api/training';
 import MeetingDetailAttend from '../models/meeting-detail-attend.vue'
@@ -36,7 +37,7 @@ const { dm_training_join_type, yes_no, lm_training_cert, lm_training_status, dm_
 const tabs = ref([
     { label: '参会人员信息', value: '1' },
     { label: '会议信息', value: '2' },
-
+    {label: '特殊清单', value: '3'}
 ])
 
 const activeName = ref('1');

+ 48 - 5
src/views/training/meeting/index.vue

@@ -56,15 +56,12 @@
                                 </div>
                             </template>
                         </vxe-column>
-                        <vxe-column field="signupsTime" min-width="350" title="报名时间" class-name="f-w-600 p-rtv">
+                        <vxe-column field="signupsTime" min-width="350" title="报名时间" class-name="f-w-600">
                             <template #default="{ row }">
                                 <div class="d-flex a-c">
                                     {{ row.signupStart.slice(0, -3) }}~{{ row.signupEnd.slice(0, -3) }}
                                     <DictTag :options="lm_training_join_status" :value="row?.joinStatus"></DictTag>
                                 </div>
-                                <div v-if="row.joinStatus == '1'" class="pos-tag">
-                                    <el-button @click="openEnrollRegistration(row)" type="primary" size="small" text>报名二维码{{ '>' }}</el-button>
-                                </div>
                             </template>
                         </vxe-column>
                         <vxe-column field="joinType" title="会议方式" width="80">
@@ -82,6 +79,13 @@
                         <vxe-column field="signCount" title="签到人数" width="60" class-name="f-w-600" />
                         <vxe-column field="waitCount" title="待审核人数" width="60" class-name="f-w-600" />
                         <vxe-column field="certCount" title="领取证书人数" width="80" />
+                        <vxe-column field="pageEnable" title="会议门户" width="80">
+                            <template #default="{ row }">
+                                <el-switch v-model="row.pageEnable" active-value="1" inactive-value="0" @change="changePageEnable(row.id,row?.pageEnable)"></el-switch>
+                                <div v-if="+row.pageId" @click="ckeckpageEnable(row?.trainingName,row?.pageId,row?.id)" class="c-s-p">查看</div>
+                                <div v-else @click="router.push({ path: 'lmmeetingptpl-edit', query: { meetid: row?.id } })" class="c-s-p">去设置</div>
+                            </template>
+                        </vxe-column>
                         <vxe-column field="certFlag" title="签到二维码" width="90" align="center">
                             <template #default="{ row }">
                                 <el-button @click="trainingSignIn(row)" :style="{ color: !['1', '0'].includes(row?.trainingStatus) ? '#999' : '#0079fe' }" text :disabled="!['1', '0'].includes(row?.trainingStatus)">查看</el-button>
@@ -129,13 +133,25 @@
     <SignInCode v-if="showSignIn" v-model:show="showSignIn" :info="rowInfo" :dict="{ lm_training_join_type }"></SignInCode>
     <TemporaryRegistration v-if="showTemporary" v-model:show="showTemporary" :info="temporaryRegistration" :dict="{ lm_training_join_type }"></TemporaryRegistration>
     <EnrollRegistration v-if="showEnroll" v-model:show="showEnroll" :info="rowInfo" :dict="{ lm_training_join_type }"></EnrollRegistration>
+    <el-dialog v-model="dialogVisible" title="会议门户" width="500">
+        <div class="mb-20">会议名称:{{ training?.trainingName }}</div>
+        <div>访问地址:{{ `${VITE_APP_MEETING_URL}/?id=${training?.pageId}` }}</div>
+        <template #footer>
+            <div style="display: flex;justify-content: space-around;">
+                <el-button type="primary" @click="router.push({ path: 'lmmeetingptpl-edit', query: { meetid: training?.id } })">编辑</el-button>
+                <el-button @click="copyToClipboard(`会议名称:${training?.trainingName } 访问地址:${`${VITE_APP_MEETING_URL}/?id=${training?.pageId}`}`)">复制</el-button>
+            </div>
+        </template>
+    </el-dialog>
 </template>
 
 <script setup name="meeting" lang="ts">
+import { trainingList, queryTrainingCount, trainingDelete, publishTraining, unpublishTraining, offOrNoTemp,switchPage } from '@/api/training';
 import { colNoData } from '@/utils/noData';
 import { searchTabs } from '@/views/models';
-import { trainingList, queryTrainingCount, trainingDelete, publishTraining, unpublishTraining, offOrNoTemp } from '@/api/training';
 import { SignInCode, TemporaryRegistration, EnrollRegistration } from '../models';
+import { copyTextToClipboard } from '@/directive/common/copyText';
+const VITE_APP_MEETING_URL = ref(import.meta.env.VITE_APP_MEETING_URL);
 const router = useRouter();
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { lm_training_join_type, yes_no, lm_training_join_status, lm_training_status } = toRefs<any>(proxy?.useDict('lm_training_join_type', 'yes_no', 'lm_training_join_status', 'lm_training_status'));
@@ -156,6 +172,8 @@ const data = reactive<any>({
     },
     rules: {}
 });
+const dialogVisible = ref(false)
+const training = ref<any>({})
 const temporaryRegistration = ref()
 const showTemporary = ref(false)
 const { queryParams, form } = toRefs(data);
@@ -290,6 +308,31 @@ const temporary = (row: any) => {
 
     showTemporary.value = true;
 };
+// 开关会议门户
+const changePageEnable = async(id,pageEnable)=>{
+   const res = await switchPage({id,pageEnable})
+}
+const ckeckpageEnable = (trainingName,pageId,id)=>{
+    training.value.trainingName = trainingName
+    training.value.pageId = pageId
+    training.value.id = id
+    dialogVisible.value = true
+}
+
+// 复制文本到剪贴板
+const copyToClipboard = async (text)=>{
+  try {
+    const sc = copyTextToClipboard(text)
+    if (sc) {
+        ElMessage.success('复制成功');
+    } else {
+        ElMessage.error('复制失败,请手动复制');
+    }
+  } catch (err) {
+    console.error('无法复制文本: ', err);
+    return false;
+  }
+}
 onMounted(() => {
     getMeetingCount();
     getList();

+ 5 - 0
src/views/training/models/index.ts

@@ -6,3 +6,8 @@ export { default as MeetingCustom } from './meeting-custom.vue'; // 查看签到
 export { default as registrationInfo } from './registration-info.vue'; 
 export { default as meetingCustomPreview } from './meeting-custom-preview.vue'; 
 export { default as EnrollRegistration } from './EnrollRegistration.vue'; 
+export { default as MeetingTplH5 } from './meeting-tpl-h5.vue';
+export { default as MeetingTplList } from './meeting-tpl-list.vue';
+export { default as MeetingTplEvents } from './meeting-tpl-events.vue';
+export { default as SelectMeetingTplPage } from './select-meeting-tpl-page.vue';
+export { default as meetingSpecialList } from './meeting-special-list.vue';

+ 237 - 0
src/views/training/models/meeting-special-list.vue

@@ -0,0 +1,237 @@
+<template>
+    <div class="pd-16" style="overflow: auto;">
+        <div class="d-flex f-s-26">配置后按配置金额收费</div>
+        <div class="d-flex j-st pt-20">
+            <!-- <div>
+                <searchTabs v-if="form?.conditions?.totalCheck == '1'" v-model="queryParams.res" @change="handleQuery" :list="tabs" key-label="name" key-count="num" key-value="type"></searchTabs>
+            </div> -->
+            <span style="width: 1px;"></span>
+            <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="auto">
+                <el-form-item label="手机号:" prop="phone">
+                    <el-input v-model="queryParams.phone" placeholder="请输入手机号" clearable style="width: 180px" />
+                </el-form-item>
+                <el-form-item label="姓名:" prop="name">
+                    <el-input v-model="queryParams.name" placeholder="请输入姓名" clearable style="width: 180px" />
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+                    <el-button icon="Refresh" @click="resetQuery" class="mr-10">重置</el-button>
+                    <el-upload class="upload-demo" :action="uploadFileUrl" multiple :limit="1" :on-success="handleSuccess" :headers="headers" :show-file-list="false" accept=".xls,.xlsx">
+                        <el-button class="pl-10 mr-10">导入特殊清单</el-button>
+                    </el-upload>
+                    <el-button @click="dialogVisible = true">清空特殊清单</el-button>
+                </el-form-item>
+            </el-form>
+        </div>
+        <vxe-table v-if="form" :loading="loading" border :data="list" min-height="0">
+            <!-- 序号 -->
+            <vxe-column type="seq" width="60" title="序号" align="center" />
+            <vxe-column title="姓名" field="name" min-width="100" :formatter="colNoData" />
+            <!-- <vxe-column title="职务" field="position" min-width="100" :formatter="colNoData" /> -->
+            <vxe-column title="手机号" field="phone" min-width="100" :formatter="colNoData" />
+            <vxe-column title="参会费用" field="fee" min-width="100" :formatter="colNoData" v-if="form?.meetingCharge?.hasFee =='1'">
+                <template #default="{ row }">
+                    <div class="f-w-5">{{ row?.fee }}</div>
+                </template>
+            </vxe-column>
+            <vxe-column title="备注" field="remark" min-width="100" :formatter="colNoData" />
+        </vxe-table>
+        <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+        <el-dialog v-model="dialogVisible" title="清空所有数据" width="500">
+            <span>确定清空所有数据吗?</span>
+            <template #footer>
+                <div class="dialog-footer">
+                    <el-button @click="dialogVisible = false">取消</el-button>
+                    <el-button type="primary" @click="clearData()">确认</el-button>
+                </div>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+<script setup name="MeetingDetailInfo" lang="ts">
+import {trainingfee,importFeeList,clearFeeList} from '@/api/training';
+import { colNoData } from '@/utils/noData';
+import { searchTabs } from '@/views/models';
+import { debounce } from 'lodash';
+import { onMounted, reactive, ref ,ComponentPublicInstance} from 'vue';
+import registrationInfo from './registration-info.vue';
+import { AreaCascader } from '@/views/components';
+import { globalHeaders } from '@/utils/request';
+import { isWindow } from 'element-plus/es/utils';
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { cpy_res_status, lm_signup_status_app_show, dm_check_join_type,temp_join_type,dm_pay_status,lm_signup_status_app_query} = toRefs<any>(proxy?.useDict('cpy_res_status', 'lm_signup_status_app_show', 'dm_check_join_type','temp_join_type','dm_pay_status',"lm_signup_status_app_query"));
+// 获取详情
+const props = defineProps({
+    form: {
+        type: Object,
+        default: () => ({})
+    }
+});
+const baseUrl = import.meta.env.VITE_APP_BASE_API;
+const uploadFileUrl = ref(baseUrl + '/resource/oss/upload'); // 上传文件服务器地址
+const rules = reactive({
+    res: [
+        { required: true, message: '请选择审核结果', trigger: 'blur' }
+    ],
+    msgRequired: [ // For "不通过" (value="2")
+        { required: true, message: '请填写不通过的理由', trigger: 'blur' }
+    ],
+    msgOptional: [ // For "通过" (value="1")
+        { required: false, trigger: 'blur' }
+    ]
+})
+const headers = ref(globalHeaders());
+const fromvalue = ref<any>({
+    targetId: '',
+    msg: ''
+})
+
+const query = useRoute().query;
+
+const queryParams = ref<any>({
+    pageNum: 1,
+    pageSize: 10,
+    trainId: query?.id || '',
+    phone:'',
+});
+const loading = ref(false);
+const total = ref(0);
+const list = ref<any>([]);
+const tabs = ref([]);
+const formRef = ref()
+const dialogVisible = ref(false)
+const getList = async () => {
+    loading.value = true;
+    const res = await trainingfee(queryParams.value);
+    if (!res || res.code !== 200) return;
+    list.value = res.rows;
+    total.value = res.total;
+    loading.value = false;
+};
+
+const clearData = async()=>{
+    await clearFeeList(props.form.id)
+    dialogVisible.value = false
+    getList();
+}
+const handleQuery = () => {
+    queryParams.value.pageNum = 1;
+    getList();
+};
+const queryFormRef = ref<ElFormInstance>();
+const resetQuery = () => {
+    queryFormRef.value?.resetFields();
+    handleQuery();
+};
+
+const handleSuccess = async(res,uploadFile)=>{
+    await importFeeList({
+        trainingId:props.form.id,
+        xlsUrl:res.data.url
+    })
+    getList();
+}
+onMounted(() => {
+    getList();
+});
+</script>
+<style scoped lang="scss">
+.tabs-item {
+    margin-right: 20px;
+    padding: 8px 20px;
+    font-size: 14px;
+    border-color: #d7d7d7;
+    border-style: solid;
+    border-width: 1px 1px 0 1px;
+    cursor: pointer;
+    user-select: none;
+
+    &.checked {
+        color: #fff;
+        border-color: var(--el-color-primary);
+        background-color: var(--el-color-primary);
+    }
+}
+
+.border-botttom {
+    border-bottom: 1px solid #d7d7d7;
+}
+
+.reject-radio :deep(.el-radio__label) {
+    color: #F56C6C;
+}
+.orange-radio :deep(.el-radio__label) {
+    color: orange;
+}
+.reject-radio :deep(.el-radio__inner) {
+    border-color: #F56C6C;
+    background: #F56C6C;
+}
+
+.reject-radio :deep(.el-radio__border) {
+    border-color: #F56C6C;
+}
+
+:deep(.reject-radio.el-radio.is-bordered.is-checked) {
+    border-color: #F56C6C !important;
+}
+.single{
+    position: absolute;
+    top: -20px;
+    right: 30px;
+    width: 200px;
+    left: 70px;
+}
+.orange-button{
+    border:1px solid #d7d7d7;
+    height: 100px;
+    width: 300px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    font-size: 36px;
+    margin-right: 40px;
+    margin-bottom: 40px;
+    font-weight: 600;
+}
+.orange-button:hover{
+    color: white;
+    background-color: orange;
+    border: 1px solid orange;
+    opacity: 0.5;
+}
+.green-button:hover{
+    color: white;
+    background-color: green;
+    border: 1px solid green;
+    opacity: 0.5;
+}
+.green-button{
+    border:1px solid #d7d7d7;
+    height: 100px;
+    width: 300px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    font-size: 36px;
+    margin-right: 40px;
+    margin-bottom: 40px;
+    font-weight: 600;
+}
+
+.orange-active{
+    color: white;
+    background-color: orange;
+    border: 1px solid orange;
+}
+.green-active{
+    color: white;
+    background-color: green;
+    border: 1px solid green;
+}
+.titleClass{
+    font-size: 30px !important;
+}
+</style>

+ 140 - 0
src/views/training/models/meeting-tpl-events.vue

@@ -0,0 +1,140 @@
+<template>
+    <div class="pd-16">
+        <div class="f-s-16 c-333 f-w-6 mb-16">设置事件</div>
+        <el-form ref="enentFormRef" :model="form" label-position="left" :rules="rules" label-width="auto">
+            <el-form-item label="触发事件" prop="eventName">
+                <el-select v-model="form.eventName" @change="changeEventName" placeholder="请选择事件类型">
+                    <el-option v-for="item in page_event" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                </el-select>
+            </el-form-item>
+            <template v-if="form.eventName === 'callEvent'">
+                <el-form-item label="电话号码" prop="params.phone">
+                    <el-input v-model="form.params.phone" placeholder="请输入电话号码"></el-input>
+                </el-form-item>
+            </template>
+            <template v-if="form.eventName === 'msgEvent'">
+                <el-form-item label="提示内容" prop="params.msg">
+                    <el-input v-model="form.params.msg" placeholder="请输入提示内容"></el-input>
+                </el-form-item>
+            </template>
+            <template v-if="form.eventName === 'viewEvent'">
+                <el-form-item label="文件上传" prop="params.file" label-position="top">
+                    <FileUpload v-model="form.params.file" format="object" :fileSize="100"></FileUpload>
+                </el-form-item>
+            </template>
+            <template v-if="form.eventName === 'gotoEvent'">
+                <el-form-item label="页面" prop="params.pageId" label-position="top">
+                    <el-button v-if="!form.params.pageId" type="primary" @click="showSelectMeeting = true">选择页面</el-button>
+                    <div v-if="form.params.pageId" class="pt-10">
+                        <div class="w-160 p-rtv">
+                            <div class="delete-icon-tpl c-s-p c-danger f-s-20" @click="form.params.pageId = null; selectPageInfo = null;">
+                                <el-icon><CircleCloseFilled /></el-icon>
+                            </div>
+                            <div class="h-180 bg-#ccc bg-img-item_view" :style="{ backgroundImage: 'url('+ selectPageInfo?.img +')' }"></div>
+                            <div class="f-s-14 pd2-10-0 btm-text">{{ selectPageInfo?.label }}</div>
+                        </div>
+                    </div>
+                </el-form-item>
+            </template>
+            <template v-if="form.eventName === 'navigateEvent'">
+                <el-form-item label="地址导航" prop="params.wepArea">
+                    <SelectWepArea v-model="form.params.wepArea" placeholder="选择地址"></SelectWepArea>
+                </el-form-item>
+            </template>
+            <template v-if="form.eventName === 'linkEvent'">
+                <el-form-item label="跳转链接" prop="params.linkType">
+                    <el-radio-group v-model="form.params.linkType">
+                        <el-radio label="内链">内部链接</el-radio>
+                        <el-radio label="外链">外链链接</el-radio>
+                    </el-radio-group>
+                </el-form-item>
+                <el-form-item label="链接地址" prop="params.url">
+                    <el-input v-model="form.params.url" placeholder="请输入链接地址"></el-input>
+                </el-form-item>
+            </template>
+            <el-form-item>
+                <div class="flex1 d-flex j-ed pt-40">
+                    <el-button @click="clearForm">清除</el-button>
+                    <el-button @click="save" type="primary">保存</el-button>
+                </div>
+            </el-form-item>
+        </el-form>
+    </div>
+    <SelectMeetingTplPage v-model:show="showSelectMeeting" :list="list" :pageId="form.params.pageId" @success="selectSuccess"></SelectMeetingTplPage>
+</template>
+<script setup lang="ts">
+import { SelectMeetingTplPage } from '.';
+const props = defineProps<{
+    modelValue: any;
+    dict: any;
+    list: any[];
+}>();
+const { page_event } = toRefs<any>(props.dict);
+const enentFormRef = ref<any>(null);
+const showSelectMeeting = ref(false);
+const emit = defineEmits<{
+    (e: 'update:modelValue', value: any): void;
+    (e: 'save', value: any): void;
+}>();
+const rules = ref<any>({
+    eventName: [{ required: true, message: '请选择触发事件', trigger: 'change' }],
+    'params.phone': [
+        { required: true, message: '请输入电话号码', trigger: 'blur' },
+        { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的电话号码', trigger: 'blur' }
+    ],
+    'params.file': [{ required: true, message: '请上传文件', trigger: 'change' }],
+    'params.msg': [{ required: true, message: '请输入提示内容', trigger: 'change' }],
+    'params.pageId': [{ required: true, message: '请选择页面', trigger: 'change' }],
+    'params.wepArea': [{ required: true, message: '请选择地址', trigger: 'change' }],
+    'params.linkType': [{ required: true, message: '请选择链接类型', trigger: 'change' }],
+    'params.url': [
+        { required: true, message: '请输入跳转链接', trigger: 'blur' },
+        { type: 'url', message: '请输入正确的链接地址', trigger: 'blur' }
+    ]
+});
+const form = ref<any>(props.modelValue || {});
+const save = async () => {
+    await enentFormRef.value.validate();
+    const value = {  ...form.value, id: form.value?.id || new Date().getTime() + '_btn_drap', isSave: '1' };
+    emit('update:modelValue', value);
+    emit('save', value);
+};
+const selectPageInfo = computed<any>(() => {
+    if (!form.value.params || !form.value.params.pageId) return null;
+    return props.list.find((item) => item.id === form.value.params.pageId) || null;
+});
+const selectSuccess = (pageId: string) => {
+    form.value.params.pageId = pageId;
+};
+
+const changeEventName = (val: string) => {
+    form.value.params = {};
+};
+const clearForm = () => {
+    enentFormRef.value.resetFields();
+};
+watch(
+    () => props.modelValue,
+    (val) => {
+        form.value = val || {};
+    },
+    { deep: true }
+);
+</script>
+<style lang="scss" scoped>
+.bg-img-item_view {
+    background-size: cover;
+    background-position: center top;
+    border: 1px solid transparent;
+}
+.delete-icon-tpl {
+    position: absolute;
+    top: -10px;
+    right: -10px;
+    width: 24px;
+    height: 24px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+</style>

+ 108 - 0
src/views/training/models/meeting-tpl-h5.vue

@@ -0,0 +1,108 @@
+<template>
+    <template v-if="form.img">
+        <div class="flex1" style="overflow: auto;">
+            <TelViewTem @selectArea="selectArea" :enableDraw="enableDraw" bgColor="#fff" :bgSrc="form.img">
+                <DragResizeRotate v-if="form.bgm" v-model="form.bgmRact" @activated="activatedBgm($event)" @deactivated="deactivatedBgm">
+                    <div class="w-100% h-100% d-flex j-c a-c">
+                        <img class="w-100% h-100%" src="@/assets/images/bg_music_icon.png" />
+                    </div>
+                </DragResizeRotate>
+                <template v-for="(item, index) in form?.events" :key="index">
+                    <DragResizeRotate v-model="form.events[index]" @delete="deleteItemEvents($event, index, item)" @activated="activated($event, index)" @deactivated="deactivated"></DragResizeRotate>
+                </template>
+            </TelViewTem>
+        </div>
+    </template>
+    <template v-else>
+        <div @click="addImgBg" class="bg-#f7f7f7 flex1 w-750 mb-10 c-s-p d-flex a-c j-c">
+            <div class="c-666 f-s-18 u-s-n">点击此处上传图片</div>
+        </div>
+    </template>
+</template>
+<script setup lang="ts">
+import { httpRequests } from '@/utils/httpRequests';
+import { importFileGetUrls } from '@/utils/models';
+const emit = defineEmits<{
+    (e: 'update:modelValue', value: any): void,
+    (e: 'activated', value: any): void
+    (e: 'deactivated'): void
+    (e: 'deleteItemEvents', value: any): void,
+    (e: 'muisc', value: any): void
+}>();
+const props = defineProps<{
+    modelValue: any
+}>();
+const eventDefault = {
+    id: '',
+    eventName: '',
+    params: {
+        phone: '',
+        url: '',
+    },
+    x: 0,
+    y: 0,
+    w: 100,
+    h: 100,
+};
+const form = ref<any>(props.modelValue || { img: '', events: [] });
+const enableDraw = ref(true);
+
+const selectArea = (area: any) => {
+    if (area.rect.w < 20 || area.rect.h < 20) {
+        return;
+    }
+    form.value.events.push({ ...eventDefault, id: new Date().getTime() + '_btn_drap', ...area.rect });
+}
+const activated = (event, index) => {
+    enableDraw.value = false;
+    emit('activated', form.value.events[index] || { id: '' });
+}
+const activatedBgm = (event) => {
+    enableDraw.value = false;
+}
+const deactivated = () => {
+    enableDraw.value = true;
+    emit('deactivated');
+}
+const deactivatedBgm = async () => {
+    enableDraw.value = true;
+        const musicParams = {
+        id: form.value.id,
+        bgmRact: form.value.bgmRact || {
+            x: 100,
+            y: 650,
+            w: 50,
+            h: 50
+        },
+        bgm: form.value.bgm || null
+    };
+    const saveRes = await httpRequests.post('/dgtmedicine/trainpage/setBgm', musicParams);
+    if (saveRes && saveRes.code === 200) {
+        // 更新背景音乐事件
+        emit('muisc', form.value);
+    }
+    emit('deactivated');
+}
+const deleteItemEvents = (event: any, index: number, item: any) => {
+    form.value.events.splice(index, 1);
+    emit('deleteItemEvents', item);
+    emit('update:modelValue', form.value);
+}
+const clickInfo = () => {
+    console.log(form.value);
+}
+const addImgBg = async () => {
+    const res: any[] = await importFileGetUrls(['png', 'jpg', 'jpeg'], false);
+    if (res.length) {
+        res.forEach(element => {
+            if (element.data?.url) {
+                form.value.img = element.data.url;
+            }
+        });
+    }
+}
+watch(() => props.modelValue, (val) => {
+    form.value = val || { img: '', events: [] };
+}, { deep: true });
+</script>
+<style scoped lang="scss"></style>

+ 117 - 0
src/views/training/models/meeting-tpl-list.vue

@@ -0,0 +1,117 @@
+<template>
+    <div>
+        <template v-for="(item, index) in list" :key="index">
+            <div class="border-bottom pd-10 c-s-p item-hover" :class="{ 'checked': selectTplId === item.id }" @click="clickItem(item)">
+                <div v-if="+item.homeFlag" class="f-s-14 f-w-6 c-333 mb-10">门户页面</div>
+                <div class="d-flex">
+                    <div class="w-140 h-180 bg-#ccc bg-img-item_view p-rtv" :style="{ backgroundImage: 'url('+ item?.img +')' }">
+                        <img v-if="+item?.homeFlag" class="has_page_index_icon" src="@/assets/images/has_page_index_icon.png" />
+                        <img @click.stop="clickSetIndex(item)" v-else class="set_index_icon" src="@/assets/images/set_index_icon.png" />
+                    </div>
+                    <div class="flex1 ov-hd d-flex flex-cln j-sb">
+                        <div class="pd-10">
+                            <el-icon v-if="item?.events?.some(item => item.eventName)"><Sunny /></el-icon>
+                        </div>
+                        <div class="f-s-18 c-danger u-s-n c-s-p pd2-0-10" @click.stop="$emit('delete', item)">
+                            <el-icon><DeleteFilled /></el-icon>
+                        </div>
+                    </div>
+                </div>
+                <div @click.stop="changeLabel(item)" class="c-666 f-s-14 pd2-10-0 u-s-n c-s-p d-flex a-c c-666 f-s-14 btm-text">
+                    <span class="mr-6">{{ item?.label }}</span>
+                    <el-icon><EditPen /></el-icon>
+                </div>
+            </div>
+        </template>
+        <div class="pd-16 d-flex a-c j-c">
+            <el-button @click.stop="addMeetingTpls" plain type="primary">
+                <el-icon><Plus /></el-icon>
+                添加页面
+            </el-button>
+        </div>
+    </div>
+</template>
+<script setup lang="ts">
+import { httpRequests } from '@/utils/httpRequests';
+const emit = defineEmits<{
+    (e: 'selectItem', value: any): void,
+    (e: 'addMeetingTpls'): void,
+    (e: 'delete', value: any): void,
+    (e: 'changeLabel', value: any): void,
+    // 设置首页
+    (e: 'setIndex', value: any): void,
+}>();
+const props = defineProps<{
+    meetid: string,
+    list: any[]
+}>()
+const queryParams = ref<any>({
+    pageNum: 1,
+    pageSize: 10,
+});
+const addMeetingTpls = async () => {
+   emit('addMeetingTpls');
+};
+const selectTplId = ref<string>('');
+const clickItem = async (item: any) => {
+    selectTplId.value = item.id;
+    const res = await httpRequests.get(`/dgtmedicine/trainpage/getInfo/${item.id}`);
+    if (res?.code === 200) {
+        emit('selectItem', res.data);
+    }
+};
+// 更换页面名称
+const changeLabel = async (item: any) => {
+   emit('changeLabel', item);
+};
+const clickSetIndex = async (item: any) => {
+    if (+item.homeFlag) {
+        return;
+    }
+    emit('setIndex', item)
+};
+</script>
+<style scoped lang="scss">
+.item-hover:hover {
+    background-color: #fff;
+}
+.bg-img-item_view {
+    background-size: cover;
+    background-position: center top;
+    border: 1px solid transparent;
+}
+.item-hover {
+    cursor: pointer;
+    &:hover {
+        background-color: rgba(#2A6D52, .3);
+        .bg-img-item_view {
+            border-color: var(--el-color-primary);
+        }
+        .btm-text {
+            color: var(--el-color-primary);
+        }
+    }
+    &.checked {
+         background-color: rgba(#2A6D52, .3);
+        .bg-img-item_view {
+            border-color: var(--el-color-primary);
+        }
+        .btm-text {
+            font-weight: 600;
+            color: var(--el-color-primary);
+        }
+    }
+}
+.has_page_index_icon {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 40px;
+    height: 40px;
+}
+.set_index_icon {
+    position: absolute;
+    bottom: 10px;
+    right: 10px;
+}
+</style>

+ 104 - 0
src/views/training/models/select-meeting-tpl-page.vue

@@ -0,0 +1,104 @@
+<template>
+    <vxe-modal v-model="dialogVisible" :title="title" show-zoom resize show-footer destroy-on-close transfer @hide="close" :width="width" :z-index="1002">
+        <div class="ov-hd">
+            <el-row :gutter="30">
+                <template v-for="(item, index) in list" :key="index">
+                    <el-col :span="4">
+                        <div class="pd-10 w-160 c-s-p item-hover mb-30" @click="clickItem(item)" :class="{ checked: checkedid === item.id }">
+                            <div class="h-180 bg-#ccc bg-img-item_view" :style="{ backgroundImage: 'url('+ item?.img +')' }"></div>
+                            <div class="f-s-14 pd2-10-0 btm-text">{{ item?.label }}</div>
+                        </div>
+                    </el-col>
+                </template>
+            </el-row>
+        </div>
+        <template #footer>
+            <div class="d-flex a-c j-ed">
+                <el-button @click="cancel">取消</el-button>
+                <el-button @click="save" type="primary">保存</el-button>
+            </div>
+        </template>
+    </vxe-modal>
+</template>
+
+<script setup name="lmmeeting-meeting-add" lang="ts">
+import { cloneDeep } from 'lodash';
+import { onMounted, reactive, ref, watch } from 'vue';
+import { useRouter } from 'vue-router';
+// 需要添加以下导入
+import { propTypes } from '@/utils/propTypes';
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const props = defineProps({
+    show: propTypes.bool.def(false),
+    info: propTypes.array.def([]),
+    dict: propTypes.object.def({}),
+    width: propTypes.number.def(1100),
+    title: propTypes.string.def('选择跳转页面'),
+    list: propTypes.any.def([]),
+    pageId: propTypes.string.def('')
+});
+const dialogVisible = ref(false);
+const emit = defineEmits(['update:show', 'close', 'success', 'update:info']);
+const close = () => {
+    emit('update:show', false);
+    emit('close', false);
+};
+const checkedid = ref(props.pageId || '');
+const formRef = ref();
+const cancel = () => {
+    emit('update:show', false);
+    emit('close', false);
+};
+const save = () => {
+    console.log('----');
+    emit('success', checkedid.value);
+    emit('update:show', false);
+};
+const clickItem = (item: any) => {
+    checkedid.value = item.id || '';
+};
+onMounted(() => {});
+watch(
+    () => props.show,
+    (val) => {
+        dialogVisible.value = val;
+    },
+    { immediate: true }
+);
+watch(
+    () => props.pageId,
+    (val) => {
+        checkedid.value = val || '';
+    },
+    { immediate: true }
+);
+</script>
+<style scoped lang="scss">
+.bg-img-item_view {
+    background-size: cover;
+    background-position: center top;
+    border: 1px solid transparent;
+}
+.item-hover {
+    cursor: pointer;
+    &:hover {
+        background-color: rgba(#2A6D52, .3);
+        .bg-img-item_view {
+            border-color: var(--el-color-primary);
+        }
+        .btm-text {
+            color: var(--el-color-primary);
+        }
+    }
+    &.checked {
+        background-color: rgba(#2A6D52, .3);
+        .bg-img-item_view {
+            border-color: var(--el-color-primary);
+        }
+        .btm-text {
+            font-weight: 600;
+            color: var(--el-color-primary);
+        }
+    }
+}
+</style>

+ 1 - 2
src/views/training/models/sign-in-code.vue

@@ -1,6 +1,5 @@
 <template>
-    <vxe-modal v-model="dialogVisible" :title="title" show-zoom resize show-footer destroy-on-close transfer
-        @hide="close" :width="width" :z-index="1002">
+    <vxe-modal v-model="dialogVisible" :title="title" show-zoom resize show-footer destroy-on-close transfer @hide="close" :width="width" :z-index="1002">
         <template #default>
             <div>
                 <div v-if="info" ref="codeImgRef" style="width: 400px; margin: 0 auto;padding: 10px;">

+ 228 - 0
src/views/training/ptpl/edit/index.vue

@@ -0,0 +1,228 @@
+<template>
+    <div class="p-3 d-flex flex-cln">
+        <div class="bg-fff flex1 ov-hd d-flex flex-cln">
+            <div class="pd-16 d-flex a-c border-bottom">
+                <div class="flex1 ov-hd">
+                    <span class="f-s-18 c-333 f-w-6">编辑会议宣传门户</span>
+                    <el-button @click="router.go(-1)" text type="primary">
+                        <el-icon>
+                            <Back />
+                        </el-icon>
+                        返回上一级
+                    </el-button>
+                </div>
+                <div class="d-flex a-c">
+                    <el-button v-if="form.id" @click="setBgMusic">设置背景音乐</el-button>
+                    <el-button @click="router.go(-1)">取消</el-button>
+                    <el-button @click="previewTpl" type="primary">预览</el-button>
+                </div>
+            </div>
+            <div class="flex1 ov-hd d-flex">
+                <div class="bg-#fff w-200 box-sizing-border over-auto">
+                    <MeetingTplList v-if="meetid" :meetid="meetid" @selectItem="selectItem" :list="list" @setIndex="setIndexTpl" @addMeetingTpls="addMeetingTpls" @delete="deleteListTpl" @changeLabel="changeLabel"></MeetingTplList>
+                </div>
+                <div class="flex1 ov-hd d-flex flex-cln a-c bg-#f7f7f7">
+                    <div v-if="form.id" @click="changeLabel(form)" class="w-750 pd2-10-0 f-w-5 f-s-18">
+                        {{ form?.label }}
+                        <el-icon>
+                            <EditPen />
+                        </el-icon>
+                    </div>
+                    <MeetingTplH5 v-if="form.id" v-model="form" @activated="activated" @deleteItemEvents="deleteItemEvents" @muisc="setMuisc"></MeetingTplH5>
+                </div>
+                <div class="bg-#fff w-300">
+                    <MeetingTplEvents v-if="curEvent" v-model="curEvent" :dict="dict" @save="saveEevent" :list="list"></MeetingTplEvents>
+                </div>
+            </div>
+        </div>
+    </div>
+    <H5ModelLook v-if="showPreviewTpl" v-model:show="showPreviewTpl" :src="previewTplStr" title="预览"></H5ModelLook>
+</template>
+<script setup lang="ts" name="ptpl-edit-index">
+import router from '@/router';
+import { MeetingTplH5, MeetingTplList, MeetingTplEvents } from '../../models';
+import { useRoute } from 'vue-router';
+import { httpRequests } from '@/utils/httpRequests';
+import { importFileGetUrls } from '@/utils/models';
+import { H5ModelLook } from '@/views/components';
+
+const VITE_APP_MEETING_URL = ref(import.meta.env.VITE_APP_MEETING_URL || '');
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const dict = proxy?.useDict('page_event')
+const { page_event } = toRefs<any>(dict);
+const showPreviewTpl = ref(false);
+// 获取地址栏参数
+const meetid = ref<string>('');
+const form = ref<any>({});
+onMounted(() => {
+    const route = useRoute();
+    meetid.value = (route.query.meetid as string) || '';
+});
+const curEvent = ref<any>(null);
+const activated = (item: any) => {
+    curEvent.value = item;
+};
+const selectItem = (item: any) => {
+    form.value.id = null;
+    setTimeout(() => {
+        form.value = { ...item };
+        curEvent.value = null;
+    }, 100);
+
+};
+const saveEevent = async (item: any) => {
+    if (!form.value?.events || !form.value?.events.length) return;
+    const index = form.value.events.findIndex((it: any) => it.id === item.id);
+    if (index === -1) return;
+    const oldItem = form.value.events[index];
+    form.value.events[index] = { ...item, x: oldItem.x, y: oldItem.y, w: oldItem.w, h: oldItem.h };
+    // 保存成功
+    const res: any = await httpRequests.post(`/dgtmedicine/trainpage/edit`, form.value);
+    if (!res || res.code !== 200) return;
+    proxy?.$modal.msgSuccess('保存成功');
+};
+const deleteItemEvents = (item: any) => {
+    if (curEvent.value?.id === item.id) {
+        curEvent.value = null;
+    }
+};
+// 预览
+const previewTplStr = ref<string>('');
+const previewTpl = async () => {
+    if (!list.value?.length) {
+        return proxy?.$modal.msgWarning('请先添加页面');
+    }
+    previewTplStr.value = `${VITE_APP_MEETING_URL.value}?id=${list.value[0].id}`;
+    showPreviewTpl.value = true;
+};
+const list = ref<any>([]);
+const itemsData = ref<any>([]);
+const getList = async () => {
+    const res: any = await httpRequests.get(`/dgtmedicine/trainpage/listByTrainId/${meetid.value}`,);
+    if (!res || res.code !== 200) return;
+    list.value = res.data;
+};
+const addMeetingTpls = async () => {
+    const res: any[] = await importFileGetUrls(['png', 'jpg', 'jpeg'], true);
+    // 过滤掉code不为200的
+    if (!res || !res.length) return;
+    const datas = res.filter(item => item.code === 200).map(item => item.data);
+    const promises = datas.map(item => {
+        return httpRequests.post('/dgtmedicine/trainpage/add', {
+            trainId: meetid.value,
+            label: '页面名称',
+            img: item.url,
+            homeFlag: 0,
+            events: []
+        });
+    });
+    const results = await Promise.all(promises);
+    const successResults = results.filter(item => item && item.code === 200).map(item => item.data);
+    if (!successResults.length) return;
+    getList();
+}
+const deleteListTpl = async (item: any) => {
+    ElMessageBox({
+        title: '删除提示',
+        cancelButtonText: '我再看看',
+        confirmButtonText: '确认删除',
+        showCancelButton: true,
+        confirmButtonClass: 'el-button--danger',
+        message: h('p', null, [h('div', null, ``), h('div', null, [h('span', null, '删除后页面和触发事件将同步删除,无法撤回,请谨慎操作!')])]),
+        callback: async (action: string) => {
+            if (action === 'confirm') {
+                const res = await httpRequests.get(`/dgtmedicine/trainpage/remove/${item.id}`);
+                if (res) {
+                    ElMessage.success('删除成功');
+                    if (item.id === form.value.id) {
+                        form.value = {};
+                        curEvent.value = null;
+                    }
+                    getList();
+                }
+            }
+        }
+    });
+};
+const setIndexTpl = async (item) => {
+    ElMessageBox({
+        title: '提示',
+        cancelButtonText: '我再看看',
+        confirmButtonText: '确认设置',
+        showCancelButton: true,
+        message: h('p', null, [h('div', null, ``), h('div', null, [h('span', null, '是否设置该页面为会议宣传门户首页?')])]),
+        callback: async (action: string) => {
+            if (action === 'confirm') {
+                const res = await httpRequests.get(`/dgtmedicine/trainpage/setHomePage/${item.id}`);
+                if (res) {
+                    ElMessage.success('设置成功');
+                    getList();
+                }
+            }
+        }
+    });
+}
+const setMuisc = (item: any) => {
+    if (form.value.id === item.id) {
+        form.value = { ...form.value, ...item }
+    }
+};
+const changeLabel = async (item: any) => {
+    const { value: label } = await ElMessageBox.prompt('请输入页面名称', '修改页面名称', {
+        confirmButtonText: '保存',
+        cancelButtonText: '取消',
+        inputValue: item.label,
+        inputPattern: /^(?!\s*$).+/,
+        inputErrorMessage: '页面名称不能为空'
+    });
+    if (label !== undefined) {
+        const res = await httpRequests.post('/dgtmedicine/trainpage/edit', { ...item, label });
+        if (res && res.code === 200) {
+            ElMessage.success('修改成功');
+            getList();
+            if (item.id === form.value.id) {
+                form.value.label = label;
+            }
+        }
+    }
+};
+// 设置背景音乐
+const setBgMusic = async () => {
+    const res: any[] = await importFileGetUrls(['mp3'], false);
+    if (!res || !res.length) return;
+    const music = res.find(item => item.code === 200);
+    if (!music || !music.data || !music.data.url) return;
+    form.value.bgMusic = music.data.url;
+    const musicParams = {
+        id: form.value.id,
+        bgmRact: form.value.bgmRact || {
+            x: 100,
+            y: 100,
+            w: 50,
+            h: 50
+        },
+        bgm: music.data.url
+    };
+    const saveRes = await httpRequests.post('/dgtmedicine/trainpage/setBgm', musicParams);
+    if (saveRes && saveRes.code === 200) {
+        ElMessage.success('设置背景音乐成功');
+        getList();
+        if (form.value.id) {
+            form.value = { ...form.value, ...musicParams }
+        }
+    }
+};
+// 发布页面
+const publishTpl = async () => {
+    if (!form.value?.id) {
+        ElMessage.warning('请先选择一个页面进行发布');
+        return;
+    };
+    const res: any = await httpRequests.post(`/dgtmedicine/trainpage/edit`, form.value);
+    if (!res || res.code !== 200) return;
+    ElMessage.success('发布成功');
+};
+onMounted(() => {
+    getList();
+});
+</script>

+ 3 - 0
src/views/training/ptpl/list/index.vue

@@ -0,0 +1,3 @@
+<template>
+    <div></div>
+</template>