Bladeren bron

Merge branch 'master-charge' of http://git.yujin.shuziyunyao.com/yujin/digital-medicine-front

huangxw 3 maanden geleden
bovenliggende
commit
b6ac6354b0
38 gewijzigde bestanden met toevoegingen van 2391 en 260 verwijderingen
  1. 5 0
      .env.development
  2. 3 1
      .env.production
  3. 4 1
      package.json
  4. 70 0
      src/api/training/index.ts
  5. BIN
      src/assets/images/bg_music_icon.png
  6. BIN
      src/assets/images/has_page_index_icon.png
  7. BIN
      src/assets/images/set_index_icon.png
  8. 6 1
      src/assets/styles/ruoyi.scss
  9. 54 54
      src/assets/styles/variables.module.scss
  10. 4 1
      src/assets/styles/vxe-table.scss
  11. 95 0
      src/components/DragResizeRotate/DragResizeRotate.vue
  12. 82 0
      src/components/SelectWepArea/SelectWepArea.vue
  13. 147 0
      src/components/TelViewTem/TelViewTem.vue
  14. 49 49
      src/directive/common/copyText.ts
  15. 1 1
      src/layout/components/Navbar.vue
  16. 0 0
      src/store/modules/sideNum.ts
  17. 203 0
      src/utils/httpRequests.ts
  18. 2 2
      src/utils/models.ts
  19. 4 0
      src/views/appointment-record/experience/index.vue
  20. 3 2
      src/views/cdt/menus/form/index.vue
  21. 17 32
      src/views/cdt/menus/index.vue
  22. 27 25
      src/views/cdt/models/transferItems.vue
  23. 1 13
      src/views/components/H5ModelLook.vue
  24. 1 1
      src/views/dgtmedicine/member/index.vue
  25. 150 31
      src/views/training/meeting-add/index.vue
  26. 4 3
      src/views/training/meeting-detail/index.vue
  27. 50 3
      src/views/training/meeting/index.vue
  28. 7 2
      src/views/training/models/index.ts
  29. 432 35
      src/views/training/models/meeting-detail-attend.vue
  30. 28 2
      src/views/training/models/meeting-detail-info.vue
  31. 1 1
      src/views/training/models/meeting-editors.vue
  32. 237 0
      src/views/training/models/meeting-special-list.vue
  33. 140 0
      src/views/training/models/meeting-tpl-events.vue
  34. 108 0
      src/views/training/models/meeting-tpl-h5.vue
  35. 117 0
      src/views/training/models/meeting-tpl-list.vue
  36. 104 0
      src/views/training/models/select-meeting-tpl-page.vue
  37. 232 0
      src/views/training/ptpl/edit/index.vue
  38. 3 0
      src/views/training/ptpl/list/index.vue

+ 5 - 0
.env.development

@@ -42,3 +42,8 @@ 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://t.yujin.shuziyunyao.com/'
+# pagetpl地址
+VITE_APP_PAGETPL_URL = 'https://dm.yujin.shuziyunyao.com/trainpage'
+
+# 会议门户地址
+VITE_APP_MEETING_URL = 'https://dm.yujin.shuziyunyao.com/trainpage'

+ 3 - 1
.env.production

@@ -45,4 +45,6 @@ VITE_APP_APPID = '1890328853823459329'
 
 VITE_APP_SHARE_QR_CODE_URL = 'https://www.shuziyunyao.com/dm'
 
-VITE_H5_URL = 'https://t.zycpzs.cn/'
+VITE_H5_URL = 'https://t.zycpzs.cn/'
+# 会议门户地址
+VITE_APP_MEETING_URL = 'https://www.shuziyunyao.com/trainpage'

+ 4 - 1
package.json

@@ -23,7 +23,9 @@
     "url": "https://gitee.com/JavaLionLi/plus-ui.git"
   },
   "dependencies": {
+    "@amap/amap-jsapi-loader": "^1.0.1",
     "@element-plus/icons-vue": "2.3.1",
+    "@gausszhou/vue3-drag-resize-rotate": "^3.0.2",
     "@highlightjs/vue-plugin": "2.1.0",
     "@vueup/vue-quill": "1.2.0",
     "@vueuse/core": "11.3.0",
@@ -55,7 +57,8 @@
     "vue-qr": "^4.0.9",
     "vue-router": "4.4.5",
     "vue-types": "5.1.3",
-    "vxe-table": "4.6.17"
+    "vxe-table": "4.6.17",
+    "esbuild": "0.25.10"
   },
   "devDependencies": {
     "@eslint/js": "9.15.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


+ 6 - 1
src/assets/styles/ruoyi.scss

@@ -354,7 +354,6 @@ $colors: (
   flex: 1;
   display: flex;
   flex-direction: column;
-  box-sizing: border-box;
   padding: 16px;
   overflow: hidden;
   box-sizing: border-box;
@@ -388,3 +387,9 @@ $colors: (
     flex: 1;
     overflow-y: auto;
 }
+.box-sizing-border {
+  box-sizing: border-box;
+}
+.u-s-n {
+  user-select: none;
+}

+ 54 - 54
src/assets/styles/variables.module.scss

@@ -22,60 +22,60 @@
   --tags-view-active-border-color: var(--el-color-primary);
 }
 
-html.dark {
-  --menuBg: #1d1e1f;
-  --menuColor: #bfcbd9;
-  --menuActiveText: #f4f4f5;
-  --menuHover: #171819;
-
-  --subMenuBg: #1d1e1f;
-  --subMenuActiveText: #1d1e1f;
-  --subMenuHover: #171819;
-  --subMenuTitleHover: #171819;
-
-  --fixedHeaderBg: #171819;
-  --tableHeaderBg: var(--el-bg-color);
-  --tableHeaderTextColor: var(--el-text-color);
-
-  // 覆盖ele 高亮当前行的标准暗色
-  .el-tree-node__content {
-    --el-color-primary-light-9: #262727;
-  }
-
-  .el-button--primary {
-    --el-button-bg-color: var(--el-color-primary-dark-6);
-    --el-button-border-color: var(--el-color-primary-light-2);
-  }
-
-  .el-switch {
-    --el-switch-on-color: var(--el-color-primary-dark-6);
-    --el-switch-border-color: var(--el-color-primary-light-2);
-  }
-
-  .el-tag--primary {
-    --el-tag-bg-color: var(--el-color-primary-dark-6);
-    --el-tag-border-color: var(--el-color-primary-light-2);
-  }
-
-  // 在深色模式下使用更深的颜色
-  --tags-view-active-bg: var(--el-color-primary-dark-6);
-  --tags-view-active-border-color: var(--el-color-primary-light-2);
-  // vxe-table 主题
-  --vxe-font-color: #98989e;
-  --vxe-primary-color: #2c9049;
-  --vxe-icon-background-color: #98989e;
-  --vxe-table-font-color: #98989e;
-  --vxe-table-resizable-color: #95969a;
-  --vxe-table-header-background-color: #28282a;
-  --vxe-table-body-background-color: #151518;
-  --vxe-table-background-color: #4a5663;
-  --vxe-table-border-width: 1px;
-  --vxe-table-border-color: #37373a;
-  --vxe-toolbar-background-color: #37373a;
-
-  // ele
-  --brder-color: #37373a;
-}
+// html.dark {
+//   --menuBg: #1d1e1f;
+//   --menuColor: #bfcbd9;
+//   --menuActiveText: #f4f4f5;
+//   --menuHover: #171819;
+
+//   --subMenuBg: #1d1e1f;
+//   --subMenuActiveText: #1d1e1f;
+//   --subMenuHover: #171819;
+//   --subMenuTitleHover: #171819;
+
+//   --fixedHeaderBg: #171819;
+//   --tableHeaderBg: var(--el-bg-color);
+//   --tableHeaderTextColor: var(--el-text-color);
+
+//   // 覆盖ele 高亮当前行的标准暗色
+//   .el-tree-node__content {
+//     --el-color-primary-light-9: #262727;
+//   }
+
+//   .el-button--primary {
+//     --el-button-bg-color: var(--el-color-primary-dark-6);
+//     --el-button-border-color: var(--el-color-primary-light-2);
+//   }
+
+//   .el-switch {
+//     --el-switch-on-color: var(--el-color-primary-dark-6);
+//     --el-switch-border-color: var(--el-color-primary-light-2);
+//   }
+
+//   .el-tag--primary {
+//     --el-tag-bg-color: var(--el-color-primary-dark-6);
+//     --el-tag-border-color: var(--el-color-primary-light-2);
+//   }
+
+//   // 在深色模式下使用更深的颜色
+//   --tags-view-active-bg: var(--el-color-primary-dark-6);
+//   --tags-view-active-border-color: var(--el-color-primary-light-2);
+//   // vxe-table 主题
+//   --vxe-font-color: #98989e;
+//   --vxe-primary-color: #2c9049;
+//   --vxe-icon-background-color: #98989e;
+//   --vxe-table-font-color: #98989e;
+//   --vxe-table-resizable-color: #95969a;
+//   --vxe-table-header-background-color: #28282a;
+//   --vxe-table-body-background-color: #151518;
+//   --vxe-table-background-color: #4a5663;
+//   --vxe-table-border-width: 1px;
+//   --vxe-table-border-color: #37373a;
+//   --vxe-toolbar-background-color: #37373a;
+
+//   // ele
+//   --brder-color: #37373a;
+// }
 
 // base color
 $blue: #324157;

+ 4 - 1
src/assets/styles/vxe-table.scss

@@ -3,13 +3,16 @@ $vxe-primary-color: #2c9049;
 [data-vxe-table-theme="default"] {
     --vxe-modal-header-background-color: #fff;
     --vxe-table-row-hover-background-color:#fafafa;
-    
+    --vxe-table-border-color:#666;
+    --vxe-font-color:#555;
     --vxe-font-family: PingFang, PingFang SC, Microsoft YaHei, Arial, sans-serif, Helvetica Neue, Helvetica, Hiragino Sans GB;
 }
 [data-vxe-ui-theme=light] {
     --vxe-table-row-checkbox-checked-background-color: #e6f7ff;
     --vxe-table-row-hover-checkbox-checked-background-color: #e6f7ff;
+    --vxe-font-color:#555;
     --vxe-font-family: PingFang, PingFang SC, Microsoft YaHei, Arial, sans-serif, Helvetica Neue, Helvetica, Hiragino Sans GB;
+    --vxe-table-border-color:#666
 }
 .table-box-row-highlight {
     --vxe-table-row-radio-checked-background-color: #e6f7ff;

+ 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>

+ 82 - 0
src/components/SelectWepArea/SelectWepArea.vue

@@ -0,0 +1,82 @@
+<template>
+    <el-select filterable remote v-model="keywords" reserve-keyword :placeholder="placeholder" :remote-method="remoteMethod" @change="searchKeywords" :loading="loading" clearable style="width: 440px">
+        <el-option v-for="item in options" :key="item.id" :label="item.district + item.address.toString()" :value="item.id" />
+    </el-select>
+</template>
+<script setup lang="ts">
+import { debounce } from 'lodash';
+import AMapLoader from '@amap/amap-jsapi-loader';
+const props = defineProps<{
+    modelValue: any;
+    placeholder?: string;
+}>();
+const emit = defineEmits<{
+    (e: 'update:modelValue', value: string): void;
+}>();
+import { httpRequests } from '@/utils/httpRequests';
+const keywords = ref<string>('');
+const options = ref<any[]>([]);
+const loading = ref<boolean>(false);
+const remoteMethod = debounce((keywords: string) => {
+    if (!keywords) {
+        return;
+    }
+    loading.value = true;
+    let autoComplete = new mapData.AMap.Autocomplete();
+    autoComplete.search(keywords, function (status: string, result: any) {
+        console.log(result.tips);
+        if (!result.tips?.length) {
+            options.value = [];
+            loading.value = false;
+            return;
+        }
+        const tips = result.tips?.filter((item: any) => item.id);
+        options.value = [...tips];
+        loading.value = false;
+    });
+}, 1000);
+const mapData: any = {
+    AMap: null,
+    map: null,
+    marker: null,
+    circle: null
+};
+const initMap = (positions: any[] = []) => {
+    window._AMapSecurityConfig = {
+        securityJsCode: '059c519d3546bc48566ecca0b38f22ae',
+    };
+    AMapLoader.load({
+        key: '26b919a68880ad60637f5cabd6c94a76', // 申请好的Web端开发者Key,首次调用 load 时必填
+        version: '2.0', // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
+        plugins: [
+            'AMap.PlaceSearch',
+            'AMap.AutoComplete'
+        ] // 需要使用的的插件列表,如比例尺'AMap.Scale'等
+    })
+        .then((AMap) => {
+            mapData.AMap = AMap;
+        })
+        .catch((e) => {
+            console.log(e);
+        });
+};
+const searchKeywords = (val: string) => {
+    console.log(val);
+    const item = options.value.find(i => i.id === val);
+    if (item) {
+        emit('update:modelValue', item);
+    } else {
+        emit('update:modelValue', '');
+    }
+};
+watch(() => props.modelValue, (val) => {
+    if (val && val.name) {
+        keywords.value = (val?.name || '') + (val?.district || '');
+    } else {
+        keywords.value = '';
+    }
+}, { immediate: true });
+onMounted(() => {
+    initMap();
+});
+</script>

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

@@ -0,0 +1,147 @@
+<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 - 1
src/layout/components/Navbar.vue

@@ -6,7 +6,7 @@
 
         <div class="right-menu flex align-center">
             <div class="d-flex a-c">
-                <div class="mr10"><el-avatar :size="36" src="https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images-lm/common/cpy_avatar.png" /></div>
+                <div class="mr10"><el-avatar :size="36" :src="userStore.avatar || 'https://ta.zycpzs.cn/oss-file/smart-trace/szyy/images/common/cpy_avatar.png'" /></div>
                 <div class="d-flex a-c">
                     <span class="f-s-14 c-333 f-w-5">{{ userStore.nickname }}</span>
                     <span class="f-s-12 c-999">({{ userStore.rolesName }})</span>

+ 0 - 0
src/store/modules/sideNum.ts


+ 203 - 0
src/utils/httpRequests.ts

@@ -0,0 +1,203 @@
+import request from '@/utils/request';
+import type { AxiosRequestConfig, AxiosResponse } from 'axios';
+
+// 定义通用的响应接口
+export interface ApiResponse<T = any> {
+    code: number;
+    data: T;
+    msg?: string;
+    message?: string;
+}
+
+// 定义请求配置接口
+export interface RequestConfig extends AxiosRequestConfig {
+    // 是否显示loading
+    loading?: boolean;
+    // 是否显示错误信息
+    showError?: boolean;
+    // 自定义错误处理
+    errorHandler?: (error: any) => void;
+}
+
+/**
+ * GET请求
+ * @param url 请求地址
+ * @param params 请求参数
+ * @param config 请求配置
+ * @returns Promise<T>
+ */
+export const get = <T = any>(url: string, params?: Record<string, any>, config?: RequestConfig): Promise<ApiResponse<T>> => {
+    return request.get(url, {
+        params,
+        ...config
+    });
+};
+
+/**
+ * POST请求
+ * @param url 请求地址
+ * @param data 请求数据
+ * @param config 请求配置
+ * @returns Promise<T>
+ */
+export const post = <T = any>(url: string, data?: any, config?: RequestConfig): Promise<ApiResponse<T>> => {
+    return request.post(url, data, config);
+};
+
+/**
+ * PUT请求
+ * @param url 请求地址
+ * @param data 请求数据
+ * @param config 请求配置
+ * @returns Promise<T>
+ */
+export const put = <T = any>(url: string, data?: any, config?: RequestConfig): Promise<ApiResponse<T>> => {
+    return request.put(url, data, config);
+};
+
+/**
+ * DELETE请求
+ * @param url 请求地址
+ * @param config 请求配置
+ * @returns Promise<T>
+ */
+export const del = <T = any>(url: string, config?: RequestConfig): Promise<ApiResponse<T>> => {
+    return request.delete(url, config);
+};
+
+/**
+ * PATCH请求
+ * @param url 请求地址
+ * @param data 请求数据
+ * @param config 请求配置
+ * @returns Promise<T>
+ */
+export const patch = <T = any>(url: string, data?: any, config?: RequestConfig): Promise<ApiResponse<T>> => {
+    return request.patch(url, data, config);
+};
+
+/**
+ * 上传文件
+ * @param url 请求地址
+ * @param formData 表单数据
+ * @param config 请求配置
+ * @returns Promise<T>
+ */
+export const upload = <T = any>(url: string, formData: FormData, config?: RequestConfig): Promise<ApiResponse<T>> => {
+    return request.post(url, formData, {
+        headers: {
+            'Content-Type': 'multipart/form-data'
+        },
+        ...config
+    });
+};
+
+/**
+ * 下载文件
+ * @param url 请求地址
+ * @param params 请求参数
+ * @param filename 文件名
+ * @param config 请求配置
+ * @returns Promise<Blob>
+ */
+export const download = (url: string, params?: Record<string, any>, filename?: string, config?: RequestConfig): Promise<Blob> => {
+    return request
+        .get(url, {
+            params,
+            responseType: 'blob',
+            ...config
+        })
+        .then((response: any) => {
+            // 创建下载链接
+            const blob = new Blob([response]);
+            const downloadUrl = window.URL.createObjectURL(blob);
+            const link = document.createElement('a');
+            link.href = downloadUrl;
+            link.download = filename || 'download';
+            document.body.appendChild(link);
+            link.click();
+            document.body.removeChild(link);
+            window.URL.revokeObjectURL(downloadUrl);
+            return blob;
+        });
+};
+
+/**
+ * 请求拦截器辅助方法
+ * @param config 请求配置
+ * @returns 处理后的配置
+ */
+export const requestInterceptor = (config: RequestConfig) => {
+    // 可以在这里添加通用的请求处理逻辑
+    // 比如添加loading、token等
+    return config;
+};
+
+/**
+ * 响应拦截器辅助方法
+ * @param response 响应数据
+ * @returns 处理后的响应
+ */
+export const responseInterceptor = <T = any>(response: AxiosResponse<ApiResponse<T>>) => {
+    // 可以在这里添加通用的响应处理逻辑
+    // 比如统一的错误处理、数据格式化等
+    return response.data;
+};
+
+/**
+ * 并发请求
+ * @param requests 请求数组
+ * @returns Promise<T[]>
+ */
+export const concurrent = <T = any>(requests: Array<Promise<any>>): Promise<T[]> => {
+    return Promise.allSettled(requests).then((results) => {
+        return results
+            .map((result) => {
+                if (result.status === 'fulfilled') {
+                    return result.value;
+                } else {
+                    console.error('Request failed:', result.reason);
+                    return null;
+                }
+            })
+            .filter(Boolean);
+    });
+};
+
+/**
+ * 重试请求
+ * @param requestFn 请求函数
+ * @param maxRetries 最大重试次数
+ * @param delay 重试延迟(毫秒)
+ * @returns Promise<T>
+ */
+export const retry = <T = any>(requestFn: () => Promise<T>, maxRetries: number = 3, delay: number = 1000): Promise<T> => {
+    return requestFn().catch((error) => {
+        if (maxRetries > 0) {
+            return new Promise((resolve) => {
+                setTimeout(() => {
+                    resolve(retry(requestFn, maxRetries - 1, delay));
+                }, delay);
+            });
+        } else {
+            throw error;
+        }
+    });
+};
+
+// 导出默认的request实例,以便直接使用
+export { request as default };
+
+// 导出所有方法
+export const httpRequests = {
+    get,
+    post,
+    put,
+    delete: del,
+    patch,
+    upload,
+    download,
+    concurrent,
+    retry,
+    request
+};

+ 2 - 2
src/utils/models.ts

@@ -14,10 +14,10 @@ export const importFileGetUrl = async (types: string[] = ['xlsx', 'xls']) => {
     const { data } = await uploadFile(formData);
     return data;
 };
-export const importFileGetUrls = async (types: string[] = ['png', 'jpg']) => {
+export const importFileGetUrls = async (types: string[] = ['png', 'jpg'], multiple = true) => {
     const { files } = await VXETable.readFile({
         types,
-        multiple: true
+        multiple
     });
     const promises = Array.from(files).map((file: File) => importFileGetUrlByFile(file));
     return Promise.all(promises);

+ 4 - 0
src/views/appointment-record/experience/index.vue

@@ -0,0 +1,4 @@
+<template>
+    <div></div>
+</template>
+<script setup lang="ts"></script>

+ 3 - 2
src/views/cdt/menus/form/index.vue

@@ -173,7 +173,7 @@
             </div>
         </div>
     </div>
-    <TransferItems ref="TransferItemsRef" v-model:show="showSelectItems" @change="changeItems"></TransferItems>
+    <TransferItems v-if="showSelectItems" ref="TransferItemsRef" v-model:show="showSelectItems" @change="changeItems" :items="form.items"></TransferItems>
 </template>
 
 <script setup name="Menus-form" lang="ts">
@@ -305,7 +305,8 @@ const changeItemCpy = (val: any[]) => {
 // 继续添加方法
 const addItems = () => {
     // 保留之前选中
-    TransferItemsRef.value?.setSelectItems(form.value.items);
+    console.log(form.value.items);
+    
     showSelectItems.value = true;
 };
 // 清空重选方法

+ 17 - 32
src/views/cdt/menus/index.vue

@@ -10,12 +10,10 @@
                     <div class="flex1 ov-hd d-flex j-ed">
                         <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="auto">
                             <el-form-item label="套餐名称:" prop="name">
-                                <el-input v-model="queryParams.name" placeholder="请输入套餐名称关键字" clearable style="width: 180px"
-                                    @keyup.enter="handleQuery" />
+                                <el-input v-model="queryParams.name" placeholder="请输入套餐名称关键字" clearable style="width: 180px" @keyup.enter="handleQuery" />
                             </el-form-item>
                             <el-form-item label="套餐状态" prop="status">
-                                <el-select style="width: 160px" v-model="queryParams.status" clearable placeholder="请选择套餐状态"
-                                    @change="handleQuery">
+                                <el-select style="width: 160px" v-model="queryParams.status" clearable placeholder="请选择套餐状态" @change="handleQuery">
                                     <el-option label="未上架" value="0"></el-option>
                                     <el-option label="在售" value="1"></el-option>
                                     <el-option label="已下架" value="2"></el-option>
@@ -23,15 +21,12 @@
                                 </el-select>
                             </el-form-item>
                             <el-form-item label="制定规则" prop="permitType">
-                                <el-select style="width: 160px" v-model="queryParams.permitType" clearable
-                                    placeholder="请选择制定规则" @change="handleQuery">
-                                    <el-option v-for="item in dm_permit_type" :key="item.value" :label="item.label"
-                                        :value="item.value"></el-option>
+                                <el-select style="width: 160px" v-model="queryParams.permitType" clearable placeholder="请选择制定规则" @change="handleQuery">
+                                    <el-option v-for="item in dm_permit_type" :key="item.value" :label="item.label" :value="item.value"></el-option>
                                 </el-select>
                             </el-form-item>
                             <el-form-item label="创建人" prop="createByName">
-                                <el-input v-model="queryParams.createByName" placeholder="请输入创建人关键字" clearable
-                                    style="width: 160px" @keyup.enter="handleQuery" />
+                                <el-input v-model="queryParams.createByName" placeholder="请输入创建人关键字" clearable style="width: 160px" @keyup.enter="handleQuery" />
                             </el-form-item>
                             <el-form-item>
                                 <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -42,16 +37,13 @@
                 </div>
             </div>
             <div class="flex1 ov-hd pd-16 d-flex flex-cln">
-                <searchTabs v-model="queryParams.publicFlag" @change="handleQuery" :list="tabs" key-label="name"
-                    key-value="type" key-count="num"></searchTabs>
+                <searchTabs v-model="queryParams.publicFlag" @change="handleQuery" :list="tabs" key-label="name" key-value="type" key-count="num"></searchTabs>
                 <div class="pd-8"></div>
                 <div class="flex1 ov-hd">
-                    <vxe-table :loading="loading" border :data="list" min-height="0" max-height="100%"
-                        :row-class-name="rowClassName">
+                    <vxe-table :loading="loading" border :data="list" min-height="0" max-height="100%" :row-class-name="rowClassName">
                         <!-- 序号 -->
                         <vxe-column type="seq" fixed="left" width="60" title="序号" align="center" />
-                        <vxe-column title="套餐名称" fixed="left" align="center" field="name" min-width="100"
-                            :formatter="colNoData" />
+                        <vxe-column title="套餐名称" fixed="left" align="center" field="name" min-width="100" :formatter="colNoData" />
                         <vxe-column title="适用对象" field="applyType" min-width="210">
                             <template #default="{ row }">
                                 <view class="d-flex flex-cln" v-if="row?.permitType == '1'">
@@ -64,10 +56,8 @@
                                 </view>
                                 <view class="d-flex flex-cln" v-if="row?.permitType == '2'">
                                     <view>
-                                        {{ row?.permitCpyNames?.join(',') }}-{{ NP.times(row?.priceDetail?.length &&
-                                            row?.priceDetail[0]?.memberDiscount || 0, 10) }}折
-                                        <span class="c-333 f-w-5">({{ row?.priceDetail?.length && row?.priceDetail[0]?.price
-                                        }})</span>
+                                        {{ row?.permitCpyNames?.join(',') }}-{{ NP.times(row?.priceDetail?.length && row?.priceDetail[0]?.memberDiscount || 0, 10)}}折
+                                        <span class="c-333 f-w-5">({{ row?.priceDetail?.length && row?.priceDetail[0]?.price }})</span>
                                     </view>
                                 </view>
                             </template>
@@ -102,9 +92,7 @@
                         </vxe-column>
                         <vxe-column title="剩余时间" field="restDay" width="80">
                             <template #default="{ row }">
-                                <span v-if="new Date(row?.validUntil) > new Date()">{{ Math.floor((new
-                                    Date(row?.validUntil).getTime() - new Date().getTime()) / (3600 * 24 * 1000)) + 1
-                                }}天</span>
+                                <span v-if="new Date(row?.validUntil) > new Date()">{{ Math.floor((new Date(row?.validUntil).getTime() - new Date().getTime()) / (3600 * 24 * 1000)) + 1}}天</span>
                                 <span v-else>已过期</span>
                             </template>
                         </vxe-column>
@@ -122,8 +110,7 @@
                                 <template v-if="+row?.status === 0">
                                     <el-button @click="putaway(row)" text type="primary">上架</el-button>
                                     <span></span>
-                                    <el-button @click="router.push({ path: 'menus-form', query: { id: row?.id } })" text
-                                        type="primary">编辑</el-button>
+                                    <el-button @click="router.push({ path: 'menus-form', query: { id: row?.id } })" text type="primary">编辑</el-button>
                                     <el-button @click="delItem(row)" v-ifv-if text type="primary">删除</el-button>
                                 </template>
                                 <template v-if="+row?.status === 1">
@@ -140,15 +127,13 @@
                                 <el-button @click="copyItem(row)" text type="primary">复制</el-button>
 
                                 <span></span>
-                                <el-button @click="router.push({ path: 'menus-detail', query: { id: row?.id } })" text
-                                    type="primary">详情</el-button>
+                                <el-button @click="router.push({ path: 'menus-detail', query: { id: row?.id } })" text type="primary">详情</el-button>
                             </template>
                         </vxe-column>
                     </vxe-table>
                 </div>
             </div>
-            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
-                v-model:limit="queryParams.pageSize" @pagination="getList" />
+            <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
         </div>
     </div>
     <el-dialog title="套餐分享" v-model="shareDialog" width="350px">
@@ -289,11 +274,11 @@ const downloadQrCode = () => {
     }).then((canvas) => {
         const url = canvas.toDataURL('image/png')
         const a: any = document.createElement('a')
-        // 下载的文件名
+        //下载的文件名
         a.download = `${sharePkgName.value}.png`
-        // url
+        //url
         a.href = url
-        // 触发点击
+        //触发点击
         a.click()
     })
 }

+ 27 - 25
src/views/cdt/models/transferItems.vue

@@ -41,7 +41,7 @@
                     </div>
                     <div class="flex1 ov-hd d-flex flex-cln">
                         <div class="flex1 ov-hd">
-                            <vxe-table ref="tableLeftRef" :loading="loading" border :data="list" height="100%" :column-config="{ resizable: true }" :row-config="{keyField: 'id',isCurrent: true, isHover: true}" :checkbox-config="{ checkRowKeys: checkRowKeys,  highlight: true, range: true, trigger: 'row', reserve: true }">
+                            <vxe-table ref="tableLeftRef" :loading="loading" border :data="list" height="100%" :column-config="{ resizable: true }" :row-config="{keyField: 'id',isCurrent: true, isHover: true}" :checkbox-config="{ highlight: true, range: true, trigger: 'row', reserve: true }">
                                 <vxe-column type="checkbox" width="60"></vxe-column>
                                 <!-- 序号 -->
                                 <vxe-column type="seq" width="60" title="序号" align="center" />
@@ -97,7 +97,8 @@ const props = defineProps({
     show: propTypes.bool.def(false),
     title: propTypes.string.def('选择检测项目'),
     width: propTypes.string.def('80vw'),
-    info: propTypes.any.def(null)
+    info: propTypes.any.def(null),
+    items: propTypes.array.def([]) // 已选检测项目
 });
 const treeItemsRef = ref<any>();
 const treeStandardsRef = ref<any>();
@@ -114,18 +115,19 @@ const total = ref(0);
 const list = ref<any>([]);
 const targetList = ref<any>([]);
 const itemsData = ref<any>([]);
+// 是否打开过刚才id
+const mapIdsOpened = ref<any>({});
 const getList = async () => {
     loading.value = true;
     const res = await itemsList(queryParams.value);
     if (!res || res.code !== 200) return;
-    list.value = res.rows;
-    res.rows.forEach((item: any) => {
-        if (targetList.value.some((target: any) => target.id === item.id)) {
-            tableLeftRef.value?.setCheckboxRow([item], true);
-        }
+    res.rows.forEach(element => {
+        mapIdsOpened.value[element.id] = true;
     });
+    list.value = res.rows
     total.value = res.total;
     loading.value = false;
+    tableLeftRef.value?.setCheckboxRow(targetList.value, true);
 };
 const handleQuery = () => {
     queryParams.value.pageNum = 1;
@@ -190,26 +192,18 @@ const submitForm = async () => {
 const tableLeftRef = ref<any>();
 const tableRightRef = ref<any>();
 const transferRight = () => {
-    targetList.value = tableLeftRef.value?.getCheckboxReserveRecords(true).concat(tableLeftRef.value?.getCheckboxRecords());
+    // 去重
+    const newsList = tableLeftRef.value?.getCheckboxReserveRecords(true).concat(tableLeftRef.value?.getCheckboxRecords());
+    // 过滤没打开过的
+    const noOpenList = targetList.value.filter((item: any) => !mapIdsOpened.value[item.id]);
+    // 合并newsList和noOpenList
+    targetList.value = newsList
 };
 const transferLeft = () => {
     targetList.value = targetList.value.filter((item: any) => !tableRightRef.value?.getCheckboxRecords().includes(item));
     tableLeftRef.value?.setCheckboxRow(tableRightRef.value?.getCheckboxRecords(), false);
     tableRightRef.value.setCheckboxRow(tableRightRef.value?.getCheckboxRecords(), false);
 };
-// 清空全部已选
-const clearAll = () => {
-    tableLeftRef.value?.clearCheckboxRow();
-    tableRightRef.value?.clearCheckboxRow();
-    targetList.value = [];
-};
-const checkRowKeys = ref<any>([]);
-// const c
-// 设置已选
-const setSelectItems = (val: any) => {
-    targetList.value = val;
-    checkRowKeys.value = val.map((item: any) => item.id);
-};
 watch(
     () => props.show,
     (val) => {
@@ -221,10 +215,18 @@ watch(
     },
     { immediate: true }
 );
-defineExpose({
-    clearAll,
-    setSelectItems
-});
+watch(
+    () => props.items,
+    (val) => {
+        console.log(val);
+        if (val && val.length) {
+            targetList.value = val;
+        } else {
+            targetList.value = [];
+        }
+    },
+    { immediate: true }
+);
 </script>
 <style lang="scss" scoped>
 .tree-wrap {

+ 1 - 13
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>
@@ -36,7 +25,6 @@ const close = () => {
 watch(
     () => props.show,
     (val) => {
-        console.log(val);
         dialogVisible.value = val;
     },
     { immediate: true }

+ 1 - 1
src/views/dgtmedicine/member/index.vue

@@ -180,7 +180,7 @@ const resetQuery = () => {
 };
 
 const memberDetail = (row: any) => {
-    router.push({ path: `/szyy/member-detail`, query: { memberId: row.id } });
+    router.push({ path: `member-detail`, query: { memberId: row.id } });
 };
 const deleteItem = async (row: any) => {
     ElMessageBox({

+ 150 - 31
src/views/training/meeting-add/index.vue

@@ -53,7 +53,34 @@
                                     <el-input v-model="form.tel" maxlength="20" placeholder="请输入联系电话" clearable />
                                 </el-form-item>
                             </el-col>
-                            <el-col :span="12">
+                            <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">
@@ -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(dm_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>
@@ -229,7 +278,7 @@
                                         <el-col :span="10">
                                             <el-form-item label="证书名称" :prop="`certificateInfo.${index}.certType`" :rules="[{ required: true, message: '请选择证书名称', trigger: 'change' }]">
                                                 <el-select v-model="item.certType" placeholder="证书名称" clearable>
-                                                    <el-option v-for="item in lm_training_cert" :key="item.value" :label="item.label" :value="item.value" />
+                                                    <el-option v-for="item in dm_training_cert" :key="item.value" :label="item.label" :value="item.value" />
                                                 </el-select>
                                             </el-form-item>
                                         </el-col>
@@ -251,7 +300,6 @@
                                 </template>
                             </template>
                         </div>
-
                         <div class="w-50% d-flex flex-cln j-c a-c pl-20">
                             <el-button type="primary" class="w-100%" plain style="height: 70px; margin-bottom: 20px;" @click="showSignIn = true">点击去编辑报名信息></el-button>
                             <div class="w-400 h-700 border over-auto">
@@ -297,9 +345,10 @@ import { FieldDefinition } from '../models/type';
 import meetingCustomPreview from '../models/meeting-custom-preview.vue';
 import MeetingEditors from '../models/meeting-editors.vue';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { lm_training_join_type, yes_no, lm_training_cert, dm_check_join_type } = toRefs<any>(proxy?.useDict('lm_training_join_type', 'yes_no', 'lm_training_cert', 'dm_check_join_type'));
+const { lm_training_join_type, yes_no, dm_training_cert, dm_check_join_type } = toRefs<any>(proxy?.useDict('lm_training_join_type', 'yes_no', 'dm_training_cert', 'dm_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',
@@ -317,6 +366,13 @@ const fixedField = ref<FieldDefinition[]>([{
     label: '联系方式', type: '1', readonly: '0',
     required: '1'
 }])
+const hasPartialFree = ref([{
+    label: '全部免费',
+    value: '0'
+}, {
+    label: '部分免费',
+    value: '1'
+}])
 const scrollOptions = {
     block: 'center',
     behavior: 'smooth'
@@ -347,24 +403,23 @@ const form = ref<any>({
         levelTypeCheck: levelTypeCheck,
         typeCheck: [
             {
-                vipLevel: "0",
+                vipLevel: "5",
                 check: "0"
             },
             {
-                vipLevel: "1",
+                vipLevel: "4",
                 check: "0"
             },
-
             {
                 vipLevel: "3",
                 check: "0"
             },
             {
-                vipLevel: "4",
+                vipLevel: "1",
                 check: "0"
             },
             {
-                vipLevel: "5",
+                vipLevel: "0",
                 check: "0"
             },
             {
@@ -418,6 +473,51 @@ const form = ref<any>({
             check: "0",
             total: ''
         }]
+    },
+    // 收取参会费用
+    meetingCharge: {
+        hasFee: null,//是否收取参会费用
+        pricing: null,//收费标准
+        hasFlatFee: null,//收费标准类型 0所有人统一收取费用 1按报名人员类型收取
+        typeCharge: [
+        {
+            vipLevel: '5',
+            check: "0",
+            total: '',
+            certType: '1',
+            cost: null
+        },{
+            vipLevel: '4',
+            check: "0",
+            total: '',
+            certType: '1',
+            cost: null
+        }, {
+            vipLevel: '3',
+            check: "0",
+            total: '',
+            certType: '1',
+            cost: null
+        }, {
+            vipLevel: '1',
+            check: "0",
+            total: '',
+            certType: '1',
+            cost: null
+        },{
+            vipLevel: '0',
+            check: "0",
+            total: '',//免费的人数
+            certType: '1',//全部免费还是部分免费 0全部 1部分
+            cost: null //每人收费多少
+        },
+        {
+            vipLevel: "P",
+            check: "0",
+            total: '',
+            certType: '1',
+            cost: null
+        }],
     }
 });
 
@@ -465,6 +565,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' }],
@@ -478,6 +581,18 @@ const rules = reactive({
 const formRef = ref();
 const save = debounce(async () => {
     await formRef.value.validate();
+    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
@@ -511,7 +626,7 @@ const save = debounce(async () => {
     }
 }, 500);
 const goEditor = () => {
-    window.open('https://lm.yujin.shuziyunyao.com/poster#/editor', '_blank');
+    window.open('https://lm.yujin.shuziyunyao.com/poster#/editor?type=2', '_blank');
 }
 
 const addCertInfo = () => {
@@ -535,9 +650,13 @@ 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)

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

@@ -19,6 +19,7 @@
             </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>
@@ -27,16 +28,16 @@
 import { trainingDetailById, trainingMembers } from '@/api/training';
 import router from '@/router';
 import { onMounted, ref } from 'vue';
-import { MeetingDetailInfo } from '../models';
+import { MeetingDetailInfo,meetingSpecialList } from '../models';
 import MeetingDetailAttend from '../models/meeting-detail-attend.vue';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { lm_training_join_type, yes_no, lm_training_cert, lm_training_status, lm_training_signup_status_list } = toRefs<any>(proxy?.useDict('lm_training_join_type', 'yes_no', 'lm_training_cert', 'lm_training_status', 'lm_training_signup_status_list'));
+const { lm_training_join_type, yes_no, dm_training_cert, lm_training_status, lm_training_signup_status_list } = toRefs<any>(proxy?.useDict('lm_training_join_type', 'yes_no', 'dm_training_cert', 'lm_training_status', 'lm_training_signup_status_list'));
 // 获取详情
 const tabs = ref([
     { label: '参会人员信息', value: '1' },
     { label: '会议信息', value: '2' },
-
+    {label: '特殊清单', value: '3'}
 ])
 
 const activeName = ref('1');

+ 50 - 3
src/views/training/meeting/index.vue

@@ -79,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: 'ptpl-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>
@@ -125,17 +132,28 @@
     </div>
     <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>
+    <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: 'ptpl-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 { offOrNoTemp, publishTraining, queryTrainingCount, trainingDelete, trainingList, unpublishTraining } from '@/api/training';
+import { offOrNoTemp, publishTraining, queryTrainingCount, trainingDelete, trainingList, unpublishTraining,switchPage } from '@/api/training';
 import { colNoData } from '@/utils/noData';
 import { searchTabs } from '@/views/models';
 import { SignInCode, TemporaryRegistration } 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_cert, lm_training_status,lm_training_join_status } = toRefs<any>(proxy?.useDict('lm_training_join_type', 'yes_no', 'lm_training_cert', 'lm_training_status',"lm_training_join_status"));
+const { lm_training_join_type, yes_no, dm_training_cert, lm_training_status,lm_training_join_status } = toRefs<any>(proxy?.useDict('lm_training_join_type', 'yes_no', 'dm_training_cert', 'lm_training_status',"lm_training_join_status"));
 const loading = ref(true);
 const showSearch = ref(true);
 const showSignIn = ref(false);
@@ -152,6 +170,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);
@@ -282,6 +302,33 @@ 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();

+ 7 - 2
src/views/training/models/index.ts

@@ -3,5 +3,10 @@ export { default as MeetingDetailInfo } from './meeting-detail-info.vue';
 export { default as MeetingDetailattend } from './meeting-detail-attend.vue';
 export { default as TemporaryRegistration } from './temporary-registration.vue'; // 查看签到码
 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 registrationInfo } from './registration-info.vue';
+export { default as meetingCustomPreview } from './meeting-custom-preview.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';

+ 432 - 35
src/views/training/models/meeting-detail-attend.vue

@@ -1,17 +1,17 @@
 <template>
     <div class="pd-16" style="overflow: auto;">
-        <div class="d-flex mb-16 flex-cln">
-            <div class="info-title">可参会单位类型</div>
-            <div class="bg-#fafafa pd-20">
+        <div class="d-flexflex-cln">
+            <div class="info-title mb-10">可参会单位类型</div>
+            <div class="bg-#fafafa pd-16 mb-20">
                 <template v-for="item, index in form?.conditions?.typeCheck" :key="index">
                     <span class="pr-5" v-if="item.check == '1'">
                         {{ selectDictLabel(dm_check_join_type, item?.vipLevel)}}
                     </span>
                 </template>
             </div>
-            <div class="info-title">报名限制条件</div>
-            <div class="bg-#fafafa pd-20" v-if="form?.conditions?.totalCheck == '0' || !form?.conditions?.totalCheck">无</div>
-            <div class="bg-#fafafa pd-20 d-flex flex-cln" v-else>
+            <div class="info-title mb-10">报名限制条件</div>
+            <div class="bg-#fafafa pd-16 mb-20" v-if="form?.conditions?.totalCheck == '0' || !form?.conditions?.totalCheck">无</div>
+            <div class="bg-#fafafa pd-16 d-flex flex-cln" v-else>
                 <div class="pd-5 pb-15" v-if="form?.conditions?.total">报名总人数 : {{ form?.conditions?.total }}人</div>
                 <div class="d-flex">
                     <div>
@@ -39,28 +39,66 @@
                     </div>
                 </div>
             </div>
+            <div class="info-title mb-10">参会费用</div>
+            <div class="bg-#fafafa pd-16 d-flex flex-cln">
+                <div class="pd-5 pb-15">是否收取费用 : {{ form?.meetingCharge?.hasFee == '1' ? '是' : '否' }}</div>
+                <div class="pd-5 pb-15" v-if="form?.meetingCharge?.hasFee == '1'">收费标准 : {{ form?.meetingCharge?.pricing }}元/每人</div>
+                <div class="pd-5 pb-15" v-if="form?.meetingCharge?.hasFee == '1'&& form?.meetingCharge?.hasFlatFee =='0'">所有人统一收取标准费用</div>
+                <div class="pd-5 pb-15" v-if="form?.meetingCharge?.hasFee == '1' && form?.meetingCharge?.hasFlatFee =='1'">按报名人员类型收取,不同人员收取不同费用</div>
+                <div class="d-flex flex-cln" v-if="form?.meetingCharge?.hasFee == '1' && form?.meetingCharge?.hasFlatFee =='1'">
+                    <template v-for="(item, index) in form?.meetingCharge?.typeCharge" :key="index">
+                        <view v-if="+item?.check" class="pd-5">
+                            <span class="f-w-6">
+                                {{ selectDictLabels(dm_check_join_type, item?.vipLevel, ',') }}
+                            </span>
+                            <span v-if="!+item?.certType">不收取费用</span>
+                            <span v-if="+item?.certType">
+                                <span v-if="item?.vipLevel !== 'P'">每个单位</span>
+                                免除费用{{ item?.total }}人,其余报名人员每人收取费用{{ item?.cost }}元
+                            </span>
+                        </view>
+                        <div class="pd-5" v-if="!+item?.check && +form?.conditions?.typeCheck?.find(items => items?.vipLevel ==item?.vipLevel)?.check">
+                            <span class="f-w-6">
+                                {{ selectDictLabels(dm_check_join_type, item?.vipLevel, ',') }}
+                            </span>
+                            <span v-if="item?.vipLevel !== 'P'">每个单位</span>
+                            <span>每人收取费用{{ form?.meetingCharge?.pricing }}元</span>
+                        </div>
+                    </template>
+                </div>
+            </div>
         </div>
         <div class="d-flex mb-16 ">
             <div class="info-title">
                 <span>参会人员信息</span>
-                <span class="f-s-14 c-666">(提交报名:{{ form?.submitCount || 0 }}人 | 审核通过:{{ form?.joinCount || 0 }}人 | 签到:{{ form?.signCount || 0 }}人 | 领取证书:{{ form?.certCount || 0 }}人)</span>
+                <span class="f-s-14 c-666">(提交报名:{{ form?.submitCount || 0 }}人 | 已缴费:{{ form?.payCount || 0 }} 人 | 待审核:{{ form?.resWaitingCount || 0 }}人 | 审核通过:{{ form?.joinCount || 0 }}人 | 签到:{{ form?.signCount || 0 }}人 | 领取证书:{{ form?.certCount || 0 }}人)</span>
             </div>
         </div>
         <div class="d-flex j-sb">
-            <div>
+            <!-- <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>
+            </div> -->
             <span style="width: 1px;"></span>
             <el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="auto">
-                <el-form-item label="姓名:" prop="name">
-                    <el-input v-model="queryParams.name" placeholder="请输入姓名" clearable style="width: 180px" />
+                <el-form-item label="是否设置一对一联系人" prop="hasContact">
+                    <el-select v-model="queryParams.hasContact" placeholder="请选择" clearable style="width: 180px">
+                        <el-option v-for="item in [{value:'0',label:'否'},{value:'1',label:'是'}]" :key="item.value" :label="item.label" :value="item.value" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="标签" prop="tags">
+                    <el-select v-model="queryParams.tags" placeholder="请选择" clearable style="width: 180px">
+                        <el-option v-for="item in signup_tags_type" :key="item.value" :label="item.label" :value="item.value" />
+                    </el-select>
                 </el-form-item>
                 <el-form-item label="企业名称:" prop="company">
                     <el-input v-model="queryParams.company" 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 label="参会状态:" prop="signupStatus">
                     <el-select v-model="queryParams.signupStatus" placeholder="请选择参会状态" clearable style="width: 180px">
-                        <el-option v-for="item in lm_signup_status_app_show" :key="item.value" :label="item.label" :value="item.value" />
+                        <el-option v-for="item in lm_signup_status_app_query" :key="item.value" :label="item.label" :value="item.value" />
                     </el-select>
                 </el-form-item>
                 <el-form-item label="报名方式:" prop="tempJoin">
@@ -68,6 +106,26 @@
                         <el-option v-for="item in temp_join_type" :key="item.value" :label="item.label" :value="item.value" />
                     </el-select>
                 </el-form-item>
+                <el-form-item label="审核状态:" prop="res">
+                    <el-select v-model="queryParams.res" placeholder="请选择审核状态" clearable style="width: 180px">
+                        <el-option v-for="item in cpy_res_status" :key="item.value" :label="item.label" :value="item.value" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="支付状态:" prop="payStatus">
+                    <el-select v-model="queryParams.payStatus" placeholder="请选择支付状态" clearable style="width: 180px">
+                        <el-option v-for="item in dm_pay_status" :key="item.value" :label="item.label" :value="item.value" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="支付方式:" prop="payType">
+                    <el-select v-model="queryParams.payType" placeholder="请选择支付方式" clearable style="width: 180px">
+                        <el-option v-for="item in [{value:'1',label:'微信支付'},{value:'2',label:'对公转账/现场支付'},{value:'3',label:'对公转账'},{value:'4',label:'现场支付'}]" :key="item.value" :label="item.label" :value="item.value" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item label="是否需要开票:" prop="hasInvoice">
+                    <el-select v-model="queryParams.hasInvoice" placeholder="请选择" clearable style="width: 180px">
+                        <el-option v-for="item in [{value:'0',label:'不需要'},{value:'1',label:'需要'}]" :key="item.value" :label="item.label" :value="item.value" />
+                    </el-select>
+                </el-form-item>
                 <el-form-item label="所在地区" label-width="70" prop="adcode">
                     <AreaCascader v-model="queryParams.adcode" :zlevel="2" checkStrictly @change="handleQuery"></AreaCascader>
                 </el-form-item>
@@ -78,7 +136,7 @@
                 </el-form-item>
             </el-form>
         </div>
-        <vxe-table :loading="loading" border :data="list" min-height="0">
+        <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="company" min-width="100" :formatter="colNoData" />
@@ -90,30 +148,69 @@
                     </div>
                 </template>
             </vxe-column>
-            <vxe-column title="姓名" field="name" min-width="100" :formatter="colNoData" />
-            <vxe-column title="职务" field="position" min-width="100" :formatter="colNoData" />
+            <vxe-column title="姓名" field="name" min-width="100" :formatter="colNoData">
+                <template #default="{ row }">
+                    <div class="f-w-5">{{ row?.name }}</div>
+                    <div v-if="row?.extendInfo?.tags" class="d-flex flex-cln">
+                        <el-tag class="mb-5" type="warning" v-for="(item,index) in row?.extendInfo?.tags.split(',')" :key="index">{{ item }}</el-tag>
+                    </div>
+                </template>
+            </vxe-column>
+            <!-- <vxe-column title="职务" field="position" min-width="100" :formatter="colNoData" /> -->
             <vxe-column title="联系方式" field="contact" min-width="100" :formatter="colNoData" />
             <vxe-column title="备注" field="remark" min-width="100" :formatter="colNoData" />
-            <vxe-column title="报名信息" min-width="100" fixed="right">
+            <vxe-column title="参会费用" field="joinFee" min-width="100" :formatter="colNoData" v-if="form?.meetingCharge?.hasFee =='1'">
                 <template #default="{ row }">
-                    <div class="c-s-p"><u @click="checkRegostrationInfo(row)">查看报名信息</u></div>
+                    <div class="f-w-5 c-red">{{ row?.joinFee }}</div>
                 </template>
             </vxe-column>
-            <vxe-column title="报名时间" align="center" field="createTime" min-width="100" :formatter="colNoData" />
-            <vxe-column title="参会状态" min-width="100" fixed="right">
+            <vxe-column title="支付方式" field="payType" min-width="100" :formatter="colNoData" v-if="form?.meetingCharge?.hasFee =='1'">
                 <template #default="{ row }">
-                    <DictTag v-if="row?.signupStatusForPc" :options="lm_signup_status_app_show" :value="row?.signupStatusForPc"></DictTag>
-                    <div v-else>-</div>
+                    <div v-if="row.payType === '1' && +row?.joinFee" class="f-w-5">微信支付</div>
+                    <div v-if="row.payType === '2' && +row?.joinFee" class="f-w-5">对公转账/现场支付</div>
+                    <div v-if="row.payType === '3' && +row?.joinFee" class="f-w-5">对公转账</div>
+                    <div v-if="row.payType === '4' && +row?.joinFee" class="f-w-5">现场支付</div>
+                    <div v-if="(row?.payType !=='1'&& row?.payType !=='2' && row?.payType !=='3' && row?.payType !=='4') || !+row?.joinFee">-</div>
+                </template>
+            </vxe-column>
+            <vxe-column title="开票信息" field="remark" min-width="100" :formatter="colNoData" v-if="form?.meetingCharge?.hasFee =='1'">
+                <template #default="{ row }">
+                    <div v-if="row.payStatus == '1'&& !+row?.invoiceStatus && +row?.hasInvoice" @click="checkInvoiceData(row?.invoiceInfo)" class="pointer">查看</div>
+                    <div v-if="row.payStatus == '1' && +row?.invoiceStatus && +row?.hasInvoice" @click="openPDF(row?.invoiceUrl?.url)" class="pointer" style="color: red;">已开票,点击查看</div>
+                    <div v-if="row?.payStatus == '0'">{{ '-' }}</div>
+                    <div v-if="row?.payStatus !== '0' &&!+row?.hasInvoice">{{ '不需要' }}</div>
                 </template>
             </vxe-column>
-            <vxe-column title="特殊说明" min-width="100" fixed="right">
+            <vxe-column title="报名时间" align="center" field="createTime" min-width="100" :formatter="colNoData" />
+            <vxe-column title="特殊说明" min-width="100" field="tempJoin">
                 <template #default="{ row }">
                     {{ +row?.tempJoin?'通过临时报名通道报名':'-' }}
                 </template>
             </vxe-column>
+            <vxe-column title="支付状态" field="payStatus" min-width="100" :formatter="colNoData" v-if="form?.meetingCharge?.hasFee =='1'" fixed="right">
+                <template #default="{ row }">
+                    <div class="f-w-5">{{ selectDictLabel(dm_pay_status, row?.payStatus)}}</div>
+                    <el-tooltip :content="`系统单号${row?.outTradeNo}`" placement="top" effect="light">
+                        <el-icon v-if="row.payStatus == '1'"><QuestionFilled /></el-icon>
+                    </el-tooltip>
+                </template>
+            </vxe-column>
+            <vxe-column title="报名信息" min-width="80" fixed="right">
+                <template #default="{ row }">
+                    <div class="c-s-p"><u @click="checkRegostrationInfo(row)">查看报名信息</u></div>
+                </template>
+            </vxe-column>
+            <vxe-column title="参会状态" min-width="100" fixed="right">
+                <template #default="{ row }">
+                    <DictTag v-if="row?.signupStatusForPc" :options="lm_signup_status_app_show" :value="row?.signupStatusForPc"></DictTag>
+                    <div v-else>-</div>
+                </template>
+            </vxe-column>
+
             <vxe-column v-if="form?.conditions?.totalCheck == '1'" title="审核状态" min-width="100" fixed="right">
                 <template #default="{ row }">
-                    <div class="d-flex a-c ">
+                    <div v-if="row?.res == '3'||row?.res == '4'||row?.res == '5'">{{ '-' }}</div>
+                    <div v-else class="d-flex a-c">
                         <DictTag :class="{ 'c-red': row?.res === '0' }" :options="cpy_res_status" :value="row?.res" />
                         <el-tooltip class="box-item" effect="dark" :content="row?.msg" placement="top">
                             <el-icon v-show="row?.res == '2'">
@@ -123,15 +220,37 @@
                     </div>
                 </template>
             </vxe-column>
-            <vxe-column v-if="form?.conditions?.totalCheck == '1'" title="操作" width="250" align="center" fixed="right">
+            <vxe-column v-if="form?.conditions?.totalCheck == '1' || form?.meetingCharge?.hasFee == '1'" title="操作" width="140" align="center" fixed="right">
                 <template #default="{ row }">
-                    <el-button type="danger" size="small" v-if="row?.res !== '0' && row?.signInFlag !=='1'" @click="openDialog(row)" style="color: white">重审</el-button>
-                    <el-button size="small" color="#33aeeb" @click="openDialog(row)" style="color: white" v-if="row?.res == '0'">审核</el-button>
+                    <div class="d-flex flex-cln">
+                        <el-button class="mb-10" type="danger" size="small" v-if="row?.res !== '0' && row?.signInFlag !=='1' && form?.conditions?.totalCheck == '1'&& row?.payStatus == '1' && form?.meetingCharge?.hasFee !== '1'" @click="openDialog(row)" style="color: white">重审</el-button>
+                        <span></span>
+                        <el-button class="mb-10" size="small" color="#33aeeb" @click="openDialog(row)" style="color: white" v-if="row?.res == '0' && form?.conditions?.totalCheck == '1' &&form?.trainingStatus !== '2'">审核</el-button>
+                        <span></span>
+                        <el-button class="mb-10" v-if="row.res=='3' || row.res== '5'" size="small" color="#e99d42" style="color: white;" @click="openModification(row)">修改费用金额</el-button>
+                        <span></span>
+                        <el-button class="mb-10" v-if="row.res== '5' && row?.payType=='2'" size="small" color="#81b337" style="color: white;" @click="openTransfer(row)">确认收到款项</el-button>
+                        <span></span>
+                        <div v-if="row.payStatus == '1' && (row?.res == '0'|| row?.res == '2') && form?.trainingStatus == '2' && +row?.joinFee && row.payStatus !== '10'">用户报名未成功,请至微信商户号进行退款</div>
+                        <div v-if="row.payStatus === '10'">该报名已退款</div>
+                        <el-upload v-if="form?.trainingStatus == '2' && +row.joinFee && +row?.hasInvoice && !+row?.invoiceStatus && row.payStatus == '1' &&form?.meetingCharge?.hasFee =='1' && row?.res !== '0' && row?.res !== '2'" class="upload-demo" :action="uploadFileUrl" multiple :limit="1" :on-success="handleSuccess" :headers="headers" :show-file-list="false" accept=".pdf,.PDF">
+                            <el-button class="mb-10" size="small" color="#33aeeb" style="color: white" @click=" invoiceId = row.id">上传发票</el-button>
+                        </el-upload>
+                        <span></span>
+                        <el-upload v-if="form?.trainingStatus == '2' && +row.joinFee && +row?.hasInvoice && +row?.invoiceStatus && row.payStatus == '1' &&form?.meetingCharge?.hasFee =='1'&& row?.res !== '0' && row?.res !== '2'" class="upload-demo" :action="uploadFileUrl" multiple :limit="1" :on-success="handleSuccess" :headers="headers" :show-file-list="false" accept=".pdf,.PDF">
+                            <el-button size="small" class="mb-10" type="primary" color="#33aeeb" style="color: white" @click=" invoiceId = row.id">重新上传</el-button>
+                        </el-upload>
+                        <el-button size="small" class="mb-10" @click="openPersonnelLabel(row?.id)">设置人员标签</el-button>
+                        <span></span>
+                        <el-button size="small" class="mb-10" @click="opencontactPerson(row?.id,row)" v-if="!+row?.extendInfo?.contactInfo?.specifyConcatTel">设置联系人</el-button>
+                        <el-button type="primary" size="small" class="mb-10" @click="opencontactPerson(row?.id,row)" v-else>查看联系人</el-button>
+                    </div>
                 </template>
             </vxe-column>
         </vxe-table>
-        <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+        <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" :pageSizes="[50, 100, 150, 500, 1000]" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </div>
+
     <el-dialog v-model="dialogVisible" title="审核信息" width="500" center>
         <el-form ref="formRef" :model="fromvalue" :rules="rules" label-width="80px">
             <el-form-item label="审核结果" prop="res">
@@ -151,18 +270,125 @@
             </div>
         </template>
     </el-dialog>
+    <el-dialog v-model="modiFication" title="修改费用金额" width="300" center>
+        <div class="d-flex a-c">
+            <span class="flex1" style="white-space: nowrap;">参会费用:</span>
+            <el-input v-model="(participationFee)" placeholder="请填写费用金额" type="number" />
+        </div>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="modiFication = false">我再看看</el-button>
+                <el-button type="primary" @click="changeEditPrice">确认修改</el-button>
+            </div>
+        </template>
+    </el-dialog>
+    <el-dialog v-model="showTransfer" title="系统提示" width="760" center>
+        <template #header="{ titleId, titleClass }">
+            <div :id="titleId" :class="titleClass" class="titleClass">系统提示</div>
+        </template>
+        <div class="d-flex a-c flex-cln" style="align-items: flex-start;">
+            <div class="f-s-26" style="margin-bottom: 40px;margin-top: 20px;">请选择具体的收款方式:</div>
+            <!-- <el-form-item label="" prop="res">
+                <el-radio-group v-model="collectiontype">
+                    <el-radio size="large" value="3" border :class="{ 'orange-radio': collectiontype === '4' }" style="height: 100px;width: 300px;">对公转账收款</el-radio>
+                    <el-radio size="large" value="4" border style="height: 100px;width: 300px;">现场支付</el-radio>
+                </el-radio-group>
+            </el-form-item> -->
+            <div class="d-flex">
+                <div class="orange-button" :class="{'orange-active': collectiontype === '3'}" @click="collectiontype = '3'">对公转账收款</div>
+                <div class="green-button" :class="{'green-active': collectiontype === '4'}" @click="collectiontype = '4'">现场收款</div>
+            </div>
+            <div class="f-s-24">确认收款后将直接改为已支付状态,并进入报名审核流程,操作无法撤回请慎重操作。</div>
+        </div>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="showTransfer = false" size="large" class="mr-20">我再看看</el-button>
+                <el-button type="primary" @click="confirmTransfer" size="large">确认收款</el-button>
+            </div>
+        </template>
+    </el-dialog>
+    <el-dialog v-model="invoicingInformation" title="开票信息" width="600" center>
+        <div class="d-flex a-c j-sb pt-10 pb-10 f-s-16">
+            <div>名称:</div>
+            <div>{{ invoiceData?.headTitle || '-' }}</div>
+        </div>
+        <div class="d-flex a-c j-sb pt-10 pb-10 f-s-16">
+            <div>税号:</div>
+            <div>{{ invoiceData?.taxSn || '-'}}</div>
+        </div>
+        <div class="d-flex a-c j-sb pt-10 pb-10 f-s-16">
+            <div>单位地址:</div>
+            <div>{{ invoiceData?.address || '-'}}</div>
+        </div>
+        <div class="d-flex a-c j-sb pt-10 pb-10 f-s-16">
+            <div>电话号码:</div>
+            <div>{{ invoiceData?.contactPhone || '-'}}</div>
+        </div>
+        <div class="d-flex a-c j-sb pt-10 pb-10 f-s-16">
+            <div>开户银行:</div>
+            <div>{{ invoiceData?.bankName || '-'}}</div>
+        </div>
+        <div class="d-flex a-c j-sb pt-10 pb-10 f-s-16">
+            <div>银行账户:</div>
+            <div>{{ invoiceData?.bankAccount || '-'}}</div>
+        </div>
+    </el-dialog>
+    <el-dialog v-model="personnelLabel" title="设置人员标签" width="600" center>
+        <el-checkbox-group v-model="radioLabel">
+            <el-checkbox v-for="(item,index) in signup_tags_type" :key="index" :label="item?.label" :value="item.value" />
+        </el-checkbox-group>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="personnelLabel = false">取消</el-button>
+                <el-button type="primary" @click="confirmPersonnelLabel()">确认添加</el-button>
+            </div>
+        </template>
+    </el-dialog>
+    <el-dialog v-model="contactPerson" title="设置1对1联系人" width="600" center>
+        <template #header="{titleId }">
+            <div class="my-header">
+                <div :id="titleId" class="f-s-20">
+                    为
+                    <span class="c-primary">{{contactData?.name}}({{ contactData?.contact}})</span>
+                    设置1对1联系人
+                </div>
+            </div>
+        </template>
+        <el-form :model="contactPersonData">
+            <el-form-item prop="contactName" :rules="[{ required: true, message: '请填写指定联系人名称' }]">
+                <div class="d-flex a-c">
+                    <div class="pr-20">指定联系人名称</div>
+                    <el-input v-model="contactPersonData.contactName" style="width: 300px;"></el-input>
+                </div>
+            </el-form-item>
+            <el-form-item prop="contactTel" :rules="phoneRules">
+                <div class="d-flex pt-20 a-c">
+                    <div class="pr-20">指定联系人电话</div>
+                    <el-input v-model.number="contactPersonData.contactTel" style="width: 300px;"></el-input>
+                </div>
+            </el-form-item>
+        </el-form>
+        <template #footer>
+            <div class="dialog-footer">
+                <el-button @click="contactPerson = false">取消</el-button>
+                <el-button type="primary" @click="confirmContactPerson()" v-if="!+contactData?.extendInfo?.contactInfo?.specifyConcatTel">确认添加</el-button>
+                <el-button type="primary" @click="confirmContactPerson()" v-else>确认修改</el-button>
+            </div>
+        </template>
+    </el-dialog>
     <registrationInfo v-if="showTemporary" v-model:show="showTemporary" :info="temporaryRegistration"></registrationInfo>
 </template>
 <script setup name="MeetingDetailInfo" lang="ts">
-import { exportTrainingMembers, signupApproval, signupCount, trainingMembers } from '@/api/training';
+import { exportTrainingMembers, signupApproval, signupCount, trainingMembers,editPrice,confirmSigPublicPay,uploadInvoice,markTags,signupContact} from '@/api/training';
 import { colNoData } from '@/utils/noData';
 import { searchTabs } from '@/views/models';
 import { debounce } from 'lodash';
-import { onMounted, reactive, ref } from 'vue';
+import { onMounted, reactive, ref ,ComponentPublicInstance} from 'vue';
 import registrationInfo from './registration-info.vue';
 import { AreaCascader } from '@/views/components';
+import { globalHeaders } from '@/utils/request';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { cpy_res_status, lm_signup_status_app_show, dm_check_join_type,temp_join_type } = toRefs<any>(proxy?.useDict('cpy_res_status', 'lm_signup_status_app_show', 'dm_check_join_type','temp_join_type'));
+const { cpy_res_status, lm_signup_status_app_show, dm_check_join_type,temp_join_type,dm_pay_status,lm_signup_status_app_query,signup_tags_type} = 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","signup_tags_type"));
 // 获取详情
 const props = defineProps({
     form: {
@@ -170,7 +396,27 @@ const props = defineProps({
         default: () => ({})
     }
 });
+const phoneRules = [
+  {
+    required: true,
+    message: '请填写指定联系人电话'
+  },
+  {
+    validator: (rule, value, callback) => {
+      // 去除所有点号
+      const cleanValue = value ? value.toString().replace(/\./g, '') : '';
 
+      // 验证是否为11位数字
+      if (cleanValue && /^\d{11}$/.test(cleanValue)) {
+        callback();
+      } else {
+        callback(new Error('请输入11位数字的电话号码'));
+      }
+    }
+  }
+];
+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' }
@@ -185,10 +431,62 @@ const rules = reactive({
 const showTemporary = ref(false);
 const dialogVisible = ref(false);
 const temporaryRegistration = ref();
-const fromvalue = ref({
+const modiFication=ref<any>(false)
+const showTransfer = ref(false)
+const participationFee = ref()
+const editPricedata = ref()
+const invoicingInformation = ref(false)
+const invoiceData = ref()
+const invoiceId = ref()
+const headers = ref(globalHeaders());
+const fromvalue = ref<any>({
     targetId: '',
     msg: ''
 })
+const collectiontype =ref()
+const personnelLabel = ref(false)
+const radioLabel = ref()
+const radioLabelId = ref()
+const contactPerson = ref(false)
+const contactId = ref()
+const contactData = ref()
+const contactPersonData = ref({
+    contactName:'',
+    contactTel:''
+})
+const openPersonnelLabel = (id)=>{
+    radioLabel.value = []
+    personnelLabel.value = true
+    radioLabelId.value = id
+}
+const confirmPersonnelLabel = async()=>{
+    personnelLabel.value = false
+    await markTags({id:radioLabelId.value,tags:radioLabel.value.join(',')})
+    radioLabel.value = []
+    getList()
+}
+const opencontactPerson = (id,data)=>{
+    contactId.value = id
+    contactData.value= data
+    contactPerson.value = true
+    if(data?.extendInfo?.contactInfo?.specifyContact && data?.extendInfo?.contactInfo?.specifyConcatTel){
+        contactPersonData.value.contactName = data.extendInfo.contactInfo.specifyContact
+        contactPersonData.value.contactTel = data.extendInfo.contactInfo.specifyConcatTel
+    }else{
+        contactPersonData.value.contactName = ''
+        contactPersonData.value.contactTel = ''
+    }
+
+}
+const confirmContactPerson = async()=>{
+    await signupContact({id:contactId.value,contactInfo:{specifyConcatTel:contactPersonData.value?.contactTel,specifyContact:contactPersonData.value?.contactName}})
+    contactPerson.value = false
+    getList()
+}
+const checkInvoiceData = (row)=>{
+    invoicingInformation.value = true
+    invoiceData.value = row
+}
 const checkRegostrationInfo = (row) => {
     temporaryRegistration.value = row;
     showTemporary.value = true;
@@ -202,8 +500,11 @@ const query = useRoute().query;
 
 const queryParams = ref<any>({
     pageNum: 1,
-    pageSize: 10,
+    pageSize: 50,
     trainingId: query?.id || '',
+    payStatus:'',
+    payType:'',
+    invoiceStatus:''
 });
 const loading = ref(false);
 const total = ref(0);
@@ -221,8 +522,6 @@ const getList = async () => {
     const res = await trainingMembers(queryParams.value);
     if (!res || res.code !== 200) return;
     list.value = res.rows;
-    console.log(list.value);
-
     total.value = res.total;
     loading.value = false;
 };
@@ -250,6 +549,43 @@ const exportSearch = debounce(() => {
     delete params.pageSize;
     exportTrainingMembers(params);
 }, 500);
+// 打开修改金额的按钮
+const openModification = (row)=>{
+    modiFication.value = true;
+    participationFee.value = row.joinFee
+    editPricedata.value = row
+}
+const openTransfer = (row)=>{
+    showTransfer.value = true
+    editPricedata.value = row
+}
+const confirmTransfer = async()=>{
+    await confirmSigPublicPay(editPricedata.value?.id,collectiontype.value)
+    showTransfer.value = false
+    getList();
+}
+const changeEditPrice = async ()=>{
+    await editPrice({
+        id:editPricedata.value?.id,
+        price:participationFee.value
+    });
+    modiFication.value = false
+    getList();
+}
+const handleSuccess = async(res,uploadFile)=>{
+    await uploadInvoice({
+        id:invoiceId.value,
+        invoiceUrl:{
+            fileName:res.data.fileName,
+            url:res.data.url,
+            fileSize:uploadFile.raw.size
+        }
+    })
+    getList();
+}
+const openPDF = (url)=>{
+    window.open(url)
+}
 onMounted(() => {
     getList();
     getExpertPersonCount();
@@ -280,7 +616,9 @@ onMounted(() => {
 .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;
@@ -293,4 +631,63 @@ onMounted(() => {
 :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>

+ 28 - 2
src/views/training/models/meeting-detail-info.vue

@@ -30,7 +30,33 @@
                 <el-descriptions-item min-width="100px" label="每成功参会(签到成功)1人发放积分数:" v-if="form?.pointsFlag == '1'">{{ form?.points || '-' }}</el-descriptions-item>
                 <el-descriptions-item min-width="100px" label="创建人:">{{ form?.createByName || '-' }}</el-descriptions-item>
                 <el-descriptions-item min-width="100px" label="创建时间:">{{ form?.createTime || '-' }}</el-descriptions-item>
+                <el-descriptions-item min-width="100px" label="收取参会费用标准:">{{ form?.meetingCharge?.pricing || '-' }}元/每人</el-descriptions-item>
+                <el-descriptions-item min-width="100px" label="是否收取参会费用:">{{ form?.meetingCharge?.hasFee == '1' ? '是':'否' }}</el-descriptions-item>
             </el-descriptions>
+            <div class="d-flex f-s-14 c-666 mb-10" v-if="+form?.meetingCharge?.hasFee">
+                <div>参会费用:</div>
+                <div class="flex1 ov-hd" v-if="+form?.meetingCharge?.hasFlatFee">
+                    <template v-for="(item, index) in form?.meetingCharge?.typeCharge" :key="index">
+                        <div v-if="+item?.check">
+                            <span class="f-w-6">
+                                {{ selectDictLabels(dm_check_join_type, item?.vipLevel, ',')}}
+                            </span>
+                            <span v-if="!+item?.certType ">不收取费用</span>
+                            <span v-if="+item?.certType">
+                                <span v-if="item?.vipLevel !== 'P'">每个单位</span>
+                                免除费用{{ item?.total }}人,其余报名人员每人收取费用{{ item?.cost }}元
+                            </span>
+                        </div>
+                        <div v-if="!+item?.check && +form?.conditions?.typeCheck?.find(items => items?.vipLevel ==item?.vipLevel)?.check">
+                            <span class="f-w-6">
+                                {{ selectDictLabels(dm_check_join_type, item?.vipLevel, ',')}}
+                            </span>
+                            <span>每人收取费用{{ form?.meetingCharge?.pricing }}元</span>
+                        </div>
+                    </template>
+                </div>
+                <div class="flex1 ov-hd" v-if="!+form?.meetingCharge?.hasFlatFee">统一收取费用{{form?.meetingCharge?.pricing}}元</div>
+            </div>
             <div class="d-flex f-s-14 c-666 mb-10">
                 <div class="flex1">
                     <div class="c-333 mb-10">
@@ -103,7 +129,7 @@
                     <vxe-column type="seq" width="60" title="序号" align="center" />
                     <vxe-column title="证书名称" min-width="100">
                         <template #default="{ row }">
-                            <DictTag :options="lm_training_cert" :value="row?.certType"></DictTag>
+                            <DictTag :options="dm_training_cert" :value="row?.certType"></DictTag>
                         </template>
                     </vxe-column>
                     <vxe-column title="证书模板">
@@ -147,7 +173,7 @@ import { onMounted, ref } from 'vue';
 import meetingCustomPreview from './meeting-custom-preview.vue';
 import { FieldDefinition } from './type';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { lm_training_join_type, dm_check_join_type, lm_training_cert, lm_training_status, lm_training_signup_status_list } = toRefs<any>(proxy?.useDict('lm_training_join_type', 'dm_check_join_type', 'lm_training_cert', 'lm_training_status', 'lm_training_signup_status_list'));
+const { lm_training_join_type, dm_check_join_type, dm_training_cert, lm_training_status, lm_training_signup_status_list } = toRefs<any>(proxy?.useDict('lm_training_join_type', 'dm_check_join_type', 'dm_training_cert', 'lm_training_status', 'lm_training_signup_status_list'));
 const props = defineProps({
     form: {
         type: Object,

+ 1 - 1
src/views/training/models/meeting-editors.vue

@@ -193,7 +193,7 @@ import { propTypes } from '@/utils/propTypes';
 import MeetingCustom from './meeting-custom.vue';
 import { FieldDefinition } from './type';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const { lm_training_join_type, yes_no, lm_training_cert, vip_level } = toRefs<any>(proxy?.useDict('lm_training_join_type', 'yes_no', 'lm_training_cert', 'vip_level'));
+const { lm_training_join_type, yes_no, dm_training_cert, vip_level } = toRefs<any>(proxy?.useDict('lm_training_join_type', 'yes_no', 'dm_training_cert', 'vip_level'));
 const props = defineProps({
     field: propTypes.any,
     show: propTypes.bool.def(false),

+ 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>

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

@@ -0,0 +1,232 @@
+<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>