huangxw 4 luni în urmă
părinte
comite
8a195931f9

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

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

+ 49 - 6
src/components/TelViewTem/TelViewTem.vue

@@ -1,5 +1,5 @@
 <template>
 <template>
-    <div class="tel-view-tem" :style="{ width: `${width}px`, height: `${height}px`, background: bgColor, position: 'relative', userSelect: 'none' }" @mousedown="onMouseDown">
+    <div class="tel-view-tem" :style="containerStyle" @mousedown="onMouseDown">
         <!-- 拖动选区样式 -->
         <!-- 拖动选区样式 -->
         <div v-if="isDragging" class="drag-area" :style="dragAreaStyle"></div>
         <div v-if="isDragging" class="drag-area" :style="dragAreaStyle"></div>
         <slot></slot>
         <slot></slot>
@@ -7,14 +7,15 @@
 </template>
 </template>
 
 
 <script setup lang="ts">
 <script setup lang="ts">
-import { ref, computed } from 'vue';
+import { ref, computed, watch } from 'vue';
 import { propTypes } from '@/utils/propTypes';
 import { propTypes } from '@/utils/propTypes';
 
 
 const props = defineProps({
 const props = defineProps({
   width: propTypes.number.def(750),
   width: propTypes.number.def(750),
-  height: propTypes.number.def(1000),
+  minHeight: propTypes.number.def(1000),
   bgColor: propTypes.string.def('#fff'),
   bgColor: propTypes.string.def('#fff'),
-  enableDraw: propTypes.bool.def(true) // 新增开关属性
+  enableDraw: propTypes.bool.def(true),
+  bgSrc: propTypes.string.def(''),
 });
 });
 
 
 const emit = defineEmits(['selectArea']);
 const emit = defineEmits(['selectArea']);
@@ -23,7 +24,48 @@ const isDragging = ref(false);
 const startPoint = ref({ x: 0, y: 0 });
 const startPoint = ref({ x: 0, y: 0 });
 const endPoint = ref({ x: 0, y: 0 });
 const endPoint = ref({ x: 0, y: 0 });
 
 
-const dragAreaStyle = computed(() => {
+const imgHeight = ref(props.minHeight);
+const imgWidth = ref(props.width);
+
+watch(() => props.bgSrc, (val) => {
+  if (val) {
+    const img = new window.Image();
+    img.src = val;
+    img.onload = () => {
+      imgHeight.value = img.height;
+      imgWidth.value = img.width;
+    };
+  } else {
+    imgHeight.value = props.minHeight;
+    imgWidth.value = props.width;
+  }
+}, { immediate: true });
+
+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 x = Math.min(startPoint.value.x, endPoint.value.x);
   const y = Math.min(startPoint.value.y, endPoint.value.y);
   const y = Math.min(startPoint.value.y, endPoint.value.y);
   const w = Math.abs(endPoint.value.x - startPoint.value.x);
   const w = Math.abs(endPoint.value.x - startPoint.value.x);
@@ -42,7 +84,7 @@ const dragAreaStyle = computed(() => {
 });
 });
 
 
 function onMouseDown(e: MouseEvent) {
 function onMouseDown(e: MouseEvent) {
-  if (!props.enableDraw) return; // 根据开关属性决定是否允许绘制
+  if (!props.enableDraw) return;
   if (e.button !== 0) return;
   if (e.button !== 0) return;
   const rect = (e.target as HTMLElement).getBoundingClientRect();
   const rect = (e.target as HTMLElement).getBoundingClientRect();
   startPoint.value = {
   startPoint.value = {
@@ -91,6 +133,7 @@ function onMouseUp() {
   position: relative;
   position: relative;
 }
 }
 .drag-area {
 .drag-area {
+  z-index: 100;
   transition: none;
   transition: none;
 }
 }
 </style>
 </style>

+ 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);
     const { data } = await uploadFile(formData);
     return data;
     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({
     const { files } = await VXETable.readFile({
         types,
         types,
-        multiple: true
+        multiple
     });
     });
     const promises = Array.from(files).map((file: File) => importFileGetUrlByFile(file));
     const promises = Array.from(files).map((file: File) => importFileGetUrlByFile(file));
     return Promise.all(promises);
     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>

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

@@ -99,6 +99,11 @@
                                 <el-button v-if="row.tempStatus == '1' && row.trainingStatus !== '2'" type="primary" text @click="temporary(row)" :disabled="row.trainingStatus == '2'">查看</el-button>
                                 <el-button v-if="row.tempStatus == '1' && row.trainingStatus !== '2'" type="primary" text @click="temporary(row)" :disabled="row.trainingStatus == '2'">查看</el-button>
                             </template>
                             </template>
                         </vxe-column>
                         </vxe-column>
+                        <vxe-column title="会议门户" align="center" field="createByName" width="120" :formatter="colNoData">
+                            <template #default="{ row }">
+                                <el-button type="primary" text @click="router.push({ path: 'ptpl-edit', query: { meetid: row?.id } })">编辑</el-button>
+                            </template>
+                        </vxe-column>
                         <vxe-column title=" 创建人" align="center" field="createByName" width="70" :formatter="colNoData" />
                         <vxe-column title=" 创建人" align="center" field="createByName" width="70" :formatter="colNoData" />
                         <vxe-column title="创建时间" align="center" field="createTime" width="160" :formatter="colNoData" />
                         <vxe-column title="创建时间" align="center" field="createTime" width="160" :formatter="colNoData" />
                         <vxe-column title="操作" width="240" fixed="right">
                         <vxe-column title="操作" width="240" fixed="right">

+ 3 - 1
src/views/training/models/index.ts

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

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

@@ -0,0 +1,54 @@
+<template>
+    <template v-if="form.img">
+        <div class="flex1" style="overflow: auto;">
+            <TelViewTem @selectArea="selectArea" :enableDraw="enableDraw" bgColor="#f7f7f7" :bgSrc="form.img">
+                <template v-for="(item, index) in form?.events" :key="index">
+                    <DragResizeRotate v-model="form.events[index]" @activated="activated" @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 { importFileGetUrls } from '@/utils/models';
+
+const props = defineProps<{
+    modelValue: any
+}>();
+const form = ref<any>(props.modelValue || { img: '', events: []});
+const enableDraw = ref(true);
+const addInfo = () => {
+    form.value.events.push({ id: new Date().getTime() + '_btn_drap' });
+}
+const selectArea = (area: any) => {
+    if (area.rect.w < 20 || area.rect.h < 20) {
+        return;
+    }
+    form.value.events.push({ id: new Date().getTime() + '_btn_drap', ...area.rect });
+}
+const activated = () => {
+    enableDraw.value = false;
+}
+const deactivated = () => {
+    enableDraw.value = true;
+}
+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;
+        }
+     });
+  }
+}
+</script>
+<style scoped lang="scss"></style>

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

@@ -0,0 +1,77 @@
+<template>
+    <div>
+        <template v-for="(item, index) in list" :key="index">
+            <div class="border-bottom pd-10 c-s-p item-hover" @click="clickItem(item)">
+                <div v-if="+item.homeFlag" class="f-s-14 f-w-6 c-333 mb-10">门户页面</div>
+                <div>
+                    <div class="w-140 h-180 bg-#ccc bg-img-item_view" :style="{ backgroundImage: 'url('+ item?.img +')' }"></div>
+                </div>
+                <div class="c-666 f-s-14 pd2-10-0 u-s-n c-s-p d-flex a-c">
+                    <span class="c-666 f-s-14 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="addMeetingTpl" 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
+}>();
+const props = defineProps<{
+    meetid: string
+}>()
+const queryParams = ref<any>({
+    pageNum: 1,
+    pageSize: 10,
+});
+const loading = ref(false);
+const total = ref(0);
+const list = ref<any>([]);
+const itemsData = ref<any>([]);
+const getList = async () => {
+    loading.value = true;
+    const res: any = await httpRequests.get(`/dgtmedicine/trainpage/listByTrainId/${props.meetid}`,);
+    if (!res || res.code !== 200) return;
+    list.value = res.data;
+    total.value = res.total;
+    loading.value = false;
+    if (list.value.length) {
+        emit('selectItem', list.value[0] || { img: '', events: [] });
+        return;
+    }
+};
+const handleQuery = () => {
+    queryParams.value.pageNum = 1;
+    getList();
+};
+const addMeetingTpl = async () => {
+    const res: any = await httpRequests.post(`/dgtmedicine/trainpage/add`, {
+        trainId: props.meetid,
+        label: '页面名称',
+    });
+    if (!res || res.code !== 200) return;
+}
+const clickItem = (item: any) => {
+    emit('selectItem', item);
+};
+onMounted(() => {
+    handleQuery();
+});
+</script>
+<style scoped lang="scss">
+.item-hover:hover {
+    background-color: #fff;
+}
+.bg-img-item_view {
+    background-size: cover;
+    background-position: center;
+}
+</style>

+ 51 - 34
src/views/training/ptpl/edit/index.vue

@@ -1,39 +1,56 @@
 <template>
 <template>
-    <div>
-        <TelViewTem @selectArea="selectArea" :enableDraw="enableDraw">
-            <template v-for="(item, index) in form?.btns" :key="index">
-                <DragResizeRotate v-model="form.btns[index]" @activated="activated" @deactivated="deactivated"></DragResizeRotate>
-            </template>
-        </TelViewTem>
-        <el-button @click="addInfo">新增</el-button>
-        <el-button @click="clickInfo">信息</el-button>
+    <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 @click="router.go(-1)">取消</el-button>
+                    <el-button @click="previewTpl" type="primary">预览</el-button>
+                    <el-button type="success">发布</el-button>
+                </div>
+            </div>
+            <div class="flex1 ov-hd d-flex">
+                <div class="bg-#f8f8f8 w-200 box-sizing-border over-auto">
+                    <MeetingTplList v-if="meetid" :meetid="meetid" @selectItem="selectItem"></MeetingTplList>
+                </div>
+                <div class="flex1 ov-hd d-flex flex-cln a-c">
+                    <div v-if="form.id" class="w-750 pd2-10-0 f-w-5">
+                        页面名称
+                        <el-icon><EditPen /></el-icon>
+                    </div>
+                    <MeetingTplH5 v-if="form.id" v-model="form"></MeetingTplH5>
+                </div>
+                <div class="bg-#f8f8f8 w-240"></div>
+            </div>
+        </div>
     </div>
     </div>
 </template>
 </template>
 <script setup lang="ts" name="ptpl-edit-index">
 <script setup lang="ts" name="ptpl-edit-index">
-const form = ref<any>({
-    btns: [
-        { id: '2231321321' }
-    ]
-})
-const enableDraw = ref(true);
-const addInfo = () => {
-    form.value.btns.push({ id: new Date().getTime() + '_btn_drap' });
-}
-const selectArea = (area: any) => {
-    if (area.rect.w < 20 || area.rect.h < 20) {
-        return;
-    }
-    form.value.btns.push({ id: new Date().getTime() + '_btn_drap', ...area.rect });
-}
-const activated = () => {
-    console.log('activated');
-    enableDraw.value = false;
-}
-const deactivated = () => {
-    console.log('deactivated');
-    enableDraw.value = true;
-}
-const clickInfo = () => {
-    console.log(form.value);
-}
+import router from '@/router';
+import { MeetingTplH5, MeetingTplList } from '../../models';
+import { useRoute } from 'vue-router';
+import { httpRequests } from '@/utils/httpRequests';
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+// 获取地址栏参数
+const meetid = ref<string>('');
+const form = ref<any>({});
+onMounted(() => {
+    const route = useRoute();
+    meetid.value = (route.query.meetid as string) || '';
+});
+const selectItem = (item: any) => {
+    form.value = item;
+};
+// 预览
+const previewTpl = async () => {
+    const res: any = await httpRequests.post(`/dgtmedicine/trainpage/edit`, form.value);
+    if (!res || res.code !== 200) return;
+    console.log(res);
+};
 </script>
 </script>

+ 1 - 1
src/views/training/ptpl/list/index.vue

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