活动介绍
file-type

await-timeout: Node.js中方便的setTimeout与clearTimeout实现

下载需积分: 29 | 62KB | 更新于2024-12-04 | 111 浏览量 | 0 下载量 举报 收藏
download 立即下载
知识点详细说明: 1. await-timeout概念与用途 "await-timeout"是一个JavaScript库,它为开发者提供了一种基于Promise的异步延时机制。这个库将传统的setTimeout和clearTimeout包装成了Promise的形式,允许开发者使用await来处理异步等待操作,并能够以Promise的方式返回结果或拒绝。它主要是用于在支持Promise的环境中(如Node.js或现代JavaScript浏览器),处理超时或延时任务。 2. setTimeout和clearTimeout的作用 在JavaScript中,setTimeout函数用于设置一个定时器,该定时器在指定的毫秒数后执行一段代码。而clearTimeout用于取消一个之前通过setTimeout设置的定时器。这两个函数是JavaScript异步编程的核心部分,广泛应用于需要延时或超时处理的场景。 3. Promise对象基础 Promise是JavaScript中处理异步操作的一种机制,它代表了一个中间状态(pending),既不是成功,也不是失败。一旦Promise的状态变为fulfilled或rejected,它将被固定下来,并且不会改变。Promise的主要优势在于它允许异步操作以同步的方式进行编码,更加直观易懂。 4. 使用await-timeout的好处 通过await-timeout,开发者可以以更简洁的方式来处理异步等待。它允许在async函数中使用await语句来替代传统的回调函数,从而使得代码更加易于阅读和维护。此外,由于Promise的特性,我们可以更方便地处理异步操作的错误,比如使用try...catch结构来捕获并处理超时错误。 5. 安装与使用 要使用await-timeout,首先需要通过npm进行安装。在项目目录中打开命令行工具,执行命令npm install await-timeout --save,这会将await-timeout添加到你的项目依赖中。安装完成后,你可以通过import语句来导入await-timeout模块,并使用其提供的功能。例如,可以设置一个延时并等待其完成或超时返回错误信息。 6. 示例代码解析 示例代码展示了如何使用await-timeout模块来等待一个延时。首先,通过import语句导入Timeout类。然后创建一个Timeout实例,并调用实例的set方法来实现等待操作。set方法的两个参数分别代表等待时间(以毫秒为单位)和超时后返回的错误信息。如果在等待时间内调用了Timeout实例的cancel方法,Promise将会被拒绝,并返回指定的错误信息。 7. try...finally块的使用 在使用await-timeout时,通常会结合try...finally块一起使用,以确保在异步操作完成(无论是正常还是异常)后执行清理工作。这样可以避免可能的内存泄漏或资源占用问题。 8. 标签相关知识点 这个库的标签包括"nodejs", "javascript", "promise", "timeout", "settimeout", 和 "promisify"。这些标签反映了await-timeout的主要使用场景和功能,它适用于Node.js和JavaScript环境,基于Promise进行延时操作,并且将setTimeout进行Promise化处理。 9. 压缩包子文件的文件名称列表 压缩包子文件的文件名称列表中包含了"await-timeout-master",这通常意味着该库的代码托管在GitHub上,并且列表中的名称代表了代码仓库的名称。开发者可以通过访问对应的GitHub仓库来获取源代码、文档以及可能的示例等资源。 通过以上知识点的解释,我们可以看到await-timeout库为JavaScript开发者提供了一种优雅处理超时等待的方案,使得异步编程更加直观和简洁。同时,它也展示了Promise的强大功能和Node.js中的异步编程模式。

相关推荐

filetype

showDevicesOnMap(devices) { // 清除旧图层(确保移除所有相关图层) const layersToRemove = [ "devices-layer", "device-labels", "label-backgrounds", "selection-layer", ]; layersToRemove.forEach((layerId) => { if (this.mapObj.getLayer(layerId)) this.mapObj.removeLayer(layerId); }); if (this.mapObj.getSource("devices-source")) { this.mapObj.removeSource("devices-source"); } // 创建GeoJSON数据源(确保每个特征有唯一ID) const geojson = { type: "FeatureCollection", features: devices.map((device, index) => ({ type: "Feature", id: device.id, // 确保id唯一 geometry: { type: "Point", coordinates: [device.longitude, device.latitude], }, properties: { ...device, index: index + 1, isSelected: this.selectedDeviceId === device.id, }, })), }; // 添加数据源 this.mapObj.addSource("devices-source", { type: "geojson", data: geojson, generateId: true, // 如果特征本身没有id,则生成一个 }); // 预加载图标 const loadPromises = [ this.loadImageSafe("jk_online", "/images/jk_online.png"), this.loadImageSafe("jk_unline", "/images/jk_unline.png"), ]; // 加载图标并添加图层 Promise.all(loadPromises) .then(() => { // 检查图标路径是否正确(使用绝对路径) const iconBaseUrl = window.location.origin + "/images/"; console.log("图标访问路径:", iconBaseUrl + "jk_online.png"); // 添加设备图标层 if (!this.mapObj.getLayer("devices-layer")) { this.mapObj.addLayer({ id: "devices-layer", type: "symbol", source: "devices-source", layout: { "icon-image": [ "case", ["==", ["get", "online"], true], "jk_online", "jk_unline", ], "icon-size": 0.8, "icon-anchor": "bottom", "icon-allow-overlap": true, // 防止图标被隐藏 }, }); } // 添加序号背景层(圆形) if (!this.mapObj.getLayer("label-backgrounds")) { this.mapObj.addLayer({ id: "label-backgrounds", type: "circle", source: "devices-source", paint: { "circle-radius": 10, "circle-color": [ "case", ["==", ["feature-state", "isSelected"], true], "#FF6666", "#45CCDA", ], "circle-stroke-width": 1, "circle-stroke-color": "#fff", "circle-translate": [0, -22], // 向上平移,与图标位置配合 }, }); } // 添加序号文本层 if (!this.mapObj.getLayer("device-labels")) { this.mapObj.addLayer({ id: "device-labels", type: "symbol", source: "devices-source", layout: { "text-field": ["get", "index"], "text-font": ["Noto Sans Bold"], "text-size": 12, "text-offset": [0, -1.8], // 调整位置到背景圆中心 "text-anchor": "top", "text-allow-overlap": true, }, paint: { "text-color": "#FFFFFF", "text-halo-color": "rgba(0, 0, 0, 0.5)", "text-halo-width": 1, }, }); } // 设置初始特征状态 devices.forEach((device) => { this.mapObj.setFeatureState( { source: "devices-source", id: device.id }, { isSelected: device.id === this.selectedDeviceId } ); }); }) .catch((error) => { console.error("图层添加失败:", error); }); // 点击事件处理 this.mapObj.off("click"); // 移除旧的监听器,避免重复绑定 this.mapObj.on( "click", ["devices-layer", "device-labels", "label-backgrounds"], (e) => { if (e.features && e.features.length > 0) { const deviceId = e.features[0].properties.id; this.toggleDeviceSelection(deviceId); } } ); }, loadImageSafe(name, url) { return new Promise((resolve) => { if (this.mapObj.hasImage(name)) { resolve(); } else { this.mapObj.loadImage(url, (error, image) => { if (error) { console.error(`图片加载失败: ${url}`, error); resolve(); // 即使失败也继续执行 } else { this.mapObj.addImage(name, image); resolve(); } }); } }); },图标访问路径: http://localhost:8080/images/jk_online.png浏览器可以打开这个图片,但是仍然没有在地图上显示,而且:打开开发者工具(F12)的“网络(Network)”选项卡,没有这个图标的请求

filetype
filetype

<template>
{{ section.name }}
<a-empty description="暂无分类" />
<a-form :model="searchForm" label-align="left" auto-label-width layout="inline" :size="'large'" class="search-header" ref="searchHeaderRef"> <a-form-item style="width: 320px;" field="base_title" label="能力名称:" size="large"> <a-input allow-clear v-model="searchForm.base_title" placeholder="搜索能力..." @press-enter="fetchTableData" @clear="resetSearch"> <template #prefix> <icon-search /> </template> </a-input> </a-form-item> <a-form-item> <a-space size='large'> <a-button type="primary" size="medium" @click="fetchTableData"> <template #icon><icon-search /></template>查询 </a-button> <a-button type="outline" size="medium" @click="resetSearch"> <template #icon><icon-refresh /></template>重置 </a-button> </a-space> </a-form-item> </a-form> <a-spin :loading="loading" style="width: 100%">
<icon-apps /> {{ section.name }}
<template v-for="(subSection, subIndex) in section.children" :key="subIndex">
{{ subSection.name }}
能力背景
能力图标
<a-image width="100" height="100" src="some-error.png" />
{{ item.title }}
{{ item.description }}
</template>
<a-empty description="暂无能力数据" />
</a-spin>
</template> <script setup lang="ts"> import { ref, onMounted, nextTick, reactive } from 'vue'; import { IconSearch, IconApps, IconRefresh } from '@arco-design/web-vue/es/icon'; import { Message } from '@arco-design/web-vue'; import { useAbilityMallStore } from '@/store/modules/ability-mall'; import { storeToRefs } from 'pinia'; import { useRouter } from 'vue-router'; import { getAbilityMallList, getabilityMallDetails, createAbilityMall, } from '@/api/abilityMall'; const abilityMallStore = useAbilityMallStore(); const { abilityMallList, abilityMallDetails } = storeToRefs(abilityMallStore); const { getabilityMallDetailsStore } = abilityMallStore; const router = useRouter(); // 定义三级分类数据结构 interface CategoryItem { id: string; title: string; description: string; image: string; } interface SubCategory { id: string; name: string; children: CategoryItem[]; } interface MainCategory { id: string; name: string; children: SubCategory[]; } // 状态管理 const sections = ref<MainCategory[]>([]); const sectionContainerRef = ref<HTMLElement | null>(null); const searchHeaderRef = ref<HTMLElement | null>(null); // 添加搜索表单引用 const sectionRefs = ref<HTMLElement[]>([]); const activeIndex = ref(0); const loading = ref(false); const headerHeight = ref(0); const searchForm = reactive({ base_title: '', }); // 更高效的数据转换函数 const transformData = (apiData: any[]): MainCategory[] => { const categoryMap = new Map<string, MainCategory>(); const subcategoryMap = new Map<string, SubCategory>(); apiData.forEach((item) => { const categoryName = item.base_category; const categoryId = categoryName.replace(/\s+/g, '-').toLowerCase(); // 处理一级分类 if (!categoryMap.has(categoryId)) { categoryMap.set(categoryId, { id: categoryId, name: categoryName, children: [], }); } const category = categoryMap.get(categoryId)!; // 处理二级分类 const subcategoryName = item.base_subcategory; const subcategoryId = `${categoryId}-${subcategoryName .replace(/\s+/g, '-') .toLowerCase()}`; if (!subcategoryMap.has(subcategoryId)) { const subcategory: SubCategory = { id: subcategoryId, name: subcategoryName, children: [], }; subcategoryMap.set(subcategoryId, subcategory); category.children.push(subcategory); } const subcategory = subcategoryMap.get(subcategoryId)!; // 添加三级分类项 subcategory.children.push({ id: item.id, title: item.base_title, description: item.base_content, image: item.base_image, // 添加图片字段 }); }); return Array.from(categoryMap.values()); }; // 设置章节引用 const setSectionRef = (el: any) => { if (el) { sectionRefs.value.push(el); } }; // 获取搜索表单高度 const getSearchHeaderHeight = (): number => { return searchHeaderRef.value?.offsetHeight || 80; }; // 滚动到指定章节 - 修复滚动位置计算 const scrollToSection = (index: number) => { if (!sectionContainerRef.value || index < 0 || index >= sectionRefs.value.length) return; activeIndex.value = index; const targetSection = sectionRefs.value[index]; const headerHeight = getSearchHeaderHeight(); // 精确计算滚动位置:目标位置 - 顶部间距 + 容器滚动位置 const targetOffset = targetSection.offsetTop - headerHeight; sectionContainerRef.value.scrollTo({ top: targetOffset, behavior: 'smooth', }); }; // 初始化滚动监听 const initScrollListener = () => { if (!sectionContainerRef.value) return; // 初始计算一次搜索表单高度 headerHeight.value = getSearchHeaderHeight(); }; // 处理滚动事件 - 修复滚动位置判断逻辑 const handleScroll = () => { if (!sectionContainerRef.value || sectionRefs.value.length === 0) return; const scrollTop = sectionContainerRef.value.scrollTop; const containerHeight = sectionContainerRef.value.clientHeight; const headerHeight = getSearchHeaderHeight(); // 计算有效滚动位置(考虑搜索表单高度) const effectiveScrollTop = scrollTop + headerHeight; // 1. 处理滚动到底部的情况 if (scrollTop + containerHeight >= sectionContainerRef.value.scrollHeight - 10) { activeIndex.value = sectionRefs.value.length - 1; return; } // 2. 精确计算当前激活的section let currentIndex = 0; let closestDistance = Number.MAX_VALUE; // 找到距离顶部最近的section for (let i = 0; i < sectionRefs.value.length; i++) { const section = sectionRefs.value[i]; const distance = Math.abs(section.offsetTop - effectiveScrollTop); if (distance < closestDistance) { closestDistance = distance; currentIndex = i; } } // 3. 处理最后两个section的特殊情况 if (sectionRefs.value.length > 1) { const lastSection = sectionRefs.value[sectionRefs.value.length - 1]; const secondLastSection = sectionRefs.value[sectionRefs.value.length - 2]; // 当滚动位置接近最后两个section时 if (effectiveScrollTop >= secondLastSection.offsetTop - 50) { // 如果已经滚动到最后一个section if (effectiveScrollTop >= lastSection.offsetTop - headerHeight) { currentIndex = sectionRefs.value.length - 1; } else { currentIndex = sectionRefs.value.length - 2; } } } // 只有当索引变化时才更新 if (activeIndex.value !== currentIndex) { activeIndex.value = currentIndex; } }; // 重置搜索 const resetSearch = () => { searchForm.base_title = ''; fetchTableData(); }; // 获取数据 async function fetchTableData() { loading.value = true; try { const { data } = await getAbilityMallList(searchForm); const fakeData = data.filter((item) => item.deleted === 0); sections.value = transformData(fakeData || []); // 重置引用 nextTick(() => { sectionRefs.value = []; // 初始化滚动监听 setTimeout(() => { initScrollListener(); handleScroll(); // 初始计算一次激活状态 // 添加初始滚动位置修正 if (sections.value.length > 0) { scrollToSection(0); } }, 100); }); } catch (error) { console.error('获取能力模型失败:', error); Message.error('获取能力模型失败'); } finally { loading.value = false; } } // 跳转详情 const sceneDetail = async (item: CategoryItem) => { loading.value = true; await getabilityMallDetailsStore(item.id); router.push({ name: 'AbilityDetails', query: { id: item.id }, }); loading.value = false; }; // 生命周期钩子 onMounted(() => { fetchTableData(); }); </script> <style scoped> .container { display: flex; height: 100vh; position: relative; padding: 0; background: #fff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } /* 左侧导航栏样式 */ .tabs { width: 200px; padding: 20px 0; flex-shrink: 0; position: sticky; top: 0; max-height: 100vh; overflow-y: auto; z-index: 10; background: #f9fafb; border-right: 1px solid #e5e6eb; .tabs-wrapper { padding: 0 10px; } .tab { height: 44px; line-height: 44px; text-align: center; margin: 8px 0; font-size: 15px; color: #1d2129; cursor: pointer; position: relative; transition: all 0.3s ease; border-radius: 6px; overflow: hidden; font-weight: 500; &:hover { background: #f2f3f5; color: #3261CE; transform: translateX(3px); } } .tab.active { color: #3261CE; background: #e8f3ff; font-weight: 600; box-shadow: 0 1px 4px rgba(22, 93, 255, 0.15); &::before { content: ''; position: absolute; left: 0; top: 0; height: 100%; width: 3px; background: #3261CE; } } .empty-container { padding: 40px 20px; } } /* 右侧内容区域 */ .content { flex: 1; height: 100%; position: relative; padding: 0; overflow: hidden; display: flex; flex-direction: column; } /* 搜索头部样式 */ .search-header { width: 100%; display: flex; gap: 16px; padding: 15px 24px; background: #fff; border-bottom: 1px solid #e5e6eb; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.03); position: sticky; top: 0; z-index: 20; .header-title { font-size: 20px; font-weight: 600; margin-right: 20px; color: #1d2129; } } /* 分类区域样式 - 关键修改 */ .section-container { flex: 1; overflow-y: auto; scroll-behavior: smooth; padding: 0 24px; position: relative; height: calc(100vh - 80px); /* 减去搜索栏高度 */ } .section { padding: 32px 0; border-bottom: 1px solid #e5e6eb; &:first-child { padding-top: 24px; } &:last-child { border-bottom: none; } } .section-title { font-size: 22px; color: #1d2129; padding-bottom: 16px; margin-bottom: 16px; border-bottom: 1px solid #e5e6eb; font-weight: 600; display: flex; align-items: center; .arco-icon { margin-right: 10px; color: #3261CE; font-size: 20px; } } .title { font-size: 18px; color: #3261CE; margin: 24px 0 16px; padding-left: 12px; border-left: 4px solid #3261CE; font-weight: 500; } /* 内容项网格布局 */ .sub-content { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 20px; margin-top: 8px; } /* 内容项样式 */ .content-item { position: relative; height: 95px; cursor: pointer; border-radius: 8px; overflow: hidden; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.03); transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); background: #fff; border: 1px solid #e5e6eb; /* 背景图片容器 */ .img-wrapper { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; opacity: 0; transform: scale(1.05); transition: all 0.5s cubic-bezier(0.22, 0.61, 0.36, 1); img { width: 100%; height: 100%; object-fit: cover; filter: blur(0); transition: filter 0.5s ease; } } /* 悬停效果 */ &:hover { transform: translateY(-5px); box-shadow: 0 12px 25px rgba(22, 93, 255, 0.2); border-color: rgba(22, 93, 255, 0.3); .img-wrapper { opacity: 1; transform: scale(1); img { filter: blur(4px) brightness(0.9); } } .item { background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(4px); } .item-image img { transform: scale(1.8); } } } /* 内容卡片样式 */ .item { display: flex; position: relative; overflow: hidden; border-radius: 8px; width: 100%; height: 100%; z-index: 2; background: rgba(255, 255, 255, 0.95); transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } /* 图片样式 */ .item-image { width: 100px; height: 100%; flex-shrink: 0; overflow: hidden; display: flex; align-items: center; justify-content: center; background: #f7f8fa; transition: background 0.3s ease; img { width: 60px; height: 60px; object-fit: contain; display: block; transition: transform 0.5s cubic-bezier(0.22, 0.61, 0.36, 1); } .image-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: #c9cdd4; .arco-icon { font-size: 32px; } } } /* 文本区域样式 */ .item-text { flex: 1; padding: 16px 12px; display: flex; flex-direction: column; justify-content: center; overflow: hidden; } .item-title { font-size: 16px; color: #1d2129; font-weight: 500; margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color 0.3s ease; } .item-desc { font-size: 13px; color: #86909c; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; line-height: 1.5; transition: color 0.3s ease; } /* 响应式调整 */ @media (max-width: 992px) { .container { flex-direction: column; } .tabs { width: 100%; position: sticky; top: 0; z-index: 30; display: flex; overflow-x: auto; padding: 10px 0; height: auto; border-right: none; border-bottom: 1px solid #e5e6eb; .tabs-wrapper { display: flex; padding: 0 16px; } .tab { flex-shrink: 0; margin: 0 8px; padding: 0 16px; height: 36px; line-height: 36px; } } .content { margin-left: 0; } .search-header { padding: 12px 16px; flex-wrap: wrap; } .section-container { padding: 0 16px; height: calc(100vh - 140px); /* 调整移动端高度 */ } .section { padding: 24px 0; } } @media (max-width: 576px) { .sub-content { grid-template-columns: 1fr; } .header-title { display: none; } .search-header { :deep(.arco-form-item) { width: 100%; margin-bottom: 12px; } } .section-container { height: calc(100vh - 180px); /* 调整小屏幕高度 */ } .content-item { height: 110px; } .item-image { width: 80px; } } </style>在这个代码基础上修改

filetype

"url": "https://shoushu.sxycykj.net/api/app/clientAPI/getLevelList", "type": "post", "timeout": 1000, "params": { }, "status": "success", "status_code": 200, -"result": { "code": 1000, "message": "success", -"data": [ -{ "id": 1, "createTime": "2025-06-25 11:44:34", "updateTime": "2025-06-25 14:57:37", "level": 1, "img": "https://mjxsmj.oss-cn-zhangjiakou.aliyuncs.com/app%2Fbase%2F0a9b2594dfad478e8d05d4e5d82fb2ef_119c5989-81dc-49ca-8755-60990aa08151.png", "question": "从右侧图片中选出“发红光的指环”,为小丸子带上", "img1": "https://mjxsmj.oss-cn-zhangjiakou.aliyuncs.com/app%2Fbase%2F032095c35fe14c7e811f171a6e3c9c43_%E5%9B%BE%E5%B1%82%2011%20%E6%8B%B7%E8%B4%9D.png", "img2": "https://mjxsmj.oss-cn-zhangjiakou.aliyuncs.com/app%2Fbase%2Fb43dcdb5c6944cf282a2643f6e1116d2_%E5%9B%BE%E5%B1%82%2012.png", "img3": "https://mjxsmj.oss-cn-zhangjiakou.aliyuncs.com/app%2Fbase%2Fadba2d38d23f4b2f8734fe2d325f7f31_%E5%9B%BE%E5%B1%82%2013.png", "answer": "2", "video": "https://mjxsmj.oss-cn-zhangjiakou.aliyuncs.com/app%2Fbase%2Fc3faf3daaa42422488d7a3a05c78c370_video(42).mp4" }, -{ "id": 2, "createTime": "2025-06-26 18:07:16", "updateTime": "2025-06-26 18:07:16", "level": 2, "img": "https://mjxsmj.oss-cn-zhangjiakou.aliyuncs.com/app%2Fbase%2Fe167f8db0cfc4ce083d776d814992114_%E5%9B%BE%E5%B1%82%2014.png", "question": "从以下图片中选出氧气面罩,为小女孩吸氧", "img1": "https://mjxsmj.oss-cn-zhangjiakou.aliyuncs.com/app%2Fbase%2Fb0575ef3dce341fca38e82b2de0bccd0_%E5%9B%BE%E5%B1%82%2015.png", "img2": "https://mjxsmj.oss-cn-zhangjiakou.aliyuncs.com/app%2Fbase%2F617043b91fa345689fcb3b449a379ad9_%E5%9B%BE%E5%B1%82%2016.png", "img3": "https://mjxsmj.oss-cn-zhangjiakou.aliyuncs.com/app%2Fbase%2F1e0faa02ff8241de8d7e0d7f669383aa_%E5%9B%BE%E5%B1%82%2013.png", "answer": "3", "video": "https://mjxsmj.oss-cn-zhangjiakou.aliyuncs.com/app%2Fbase%2Fbe2cd588224141e48a51c8ec0d9d0808_video(43).mp4" } ] } }

filetype

以下代碼載入醫案后,當轉到其他頁面后再點擊醫案編輯器時,能保持原先載入醫案的狀態: <template>
<WangEditor v-model="content" @response="(msg) => content = msg" />
<mreditor1 ref="mreditor1" />
<button @click="toggleDisplayFormat">{{ displayButtonText }}</button> <button @click="resetAll" class="reset-btn">輸入新醫案</button> <input v-model="fetchId" placeholder="輸入醫案ID" type="number" min="1" /> <button @click="fetchById">載入醫案</button> <button @click="updateContent" class="update-btn">更新醫案</button> <button @click="deleteContent" class="delete-btn">刪除醫案</button> <button @click="submitContent" class="submit-btn">提交醫案</button> 醫案 ID: {{ submittedId }} 醫案編輯器
</template> <script> import WangEditor from './WangEditor.vue'; import mreditor1 from './mreditor1.vue'; export default { components: { WangEditor, mreditor1 }, data() { return { content: '', // 移除 isVisible submittedId: null, fetchId: null, dnTags: [], // 存储从API获取的标签数据 // 新增:显示格式状态 showRawHtml: false // false显示渲染格式,true显示原始HTML标签 }; }, computed: { // 计算高亮后的内容 highlightedContent() { if (!this.dnTags.length || !this.content) return this.content; const tempEl = document.createElement('div'); tempEl.innerHTML = this.content; const walker = document.createTreeWalker( tempEl, NodeFilter.SHOW_TEXT ); const nodes = []; while (walker.nextNode()) { nodes.push(walker.currentNode); } nodes.forEach(node => { let text = node.nodeValue; let newHtml = text; this.dnTags .slice() .sort((a, b) => b.length - a.length) .forEach(tag => { const regex = new RegExp( escapeRegExp(tag), 'g' ); newHtml = newHtml.replace( regex, `${tag}` ); }); if (newHtml !== text) { const span = document.createElement('span'); span.innerHTML = newHtml; node.parentNode.replaceChild(span, node); } }); return tempEl.innerHTML; }, // 新增:根据状态返回显示内容 displayContent() { if (this.showRawHtml) { // 显示原始HTML标签 return this.escapeHtml(this.content); } // 显示渲染格式(带高亮) return this.highlightedContent; }, // 新增:动态按钮文字 displayButtonText() { return this.showRawHtml ? '顯示渲染格式' : '顯示原始標籤'; } }, async mounted() { await this.fetchDNTags(); }, methods: { // HTML转义方法 escapeHtml(unsafe) { if (!unsafe) return ''; return unsafe .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }, // 修改:切换显示格式 toggleDisplayFormat() { this.showRawHtml = !this.showRawHtml; }, // 其余方法保持不变 async fetchDNTags() { try { const response = await fetch('DNTag/?format=json'); const data = await response.json(); this.dnTags = data .map(item => item.dnname) .filter(name => name && name.trim().length > 0); } catch (error) { console.error('获取标签失败:', error); alert('标签数据加载失败,高亮功能不可用'); } }, async fetchById() { if (!this.fetchId) { alert('請輸入有效的醫案ID'); return; } try { const response = await fetch(`MRInfo/${this.fetchId}/?format=json`); if (response.ok) { const data = await response.json(); this.$refs.mreditor1.formData.mrname = data.mrname || ''; this.$refs.mreditor1.formData.mrposter = data.mrposter || ''; this.content = data.mrcase || ''; this.submittedId = data.id; this.fetchId = null; await this.fetchDNTags(); alert('醫案數據加載成功!'); } else if (response.status === 404) { alert('未找到該ID的醫案'); } else { throw new Error('獲取醫案失敗'); } } catch (error) { console.error('Error:', error); alert(`獲取醫案失敗: ${error.message}`); } }, async submitContent() { const formData = this.$refs.mreditor1.getFormData(); const postData = { mrcase: this.content, ...formData }; try { const response = await fetch('MRInfo/?format=json', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(postData), }); if(response.ok) { const data = await response.json(); this.submittedId = data.id; alert('醫案提交成功!'); } else { throw new Error('提交失败'); } } catch (error) { console.error('Error:', error); alert(`提交失败: ${error.message}`); } }, async updateContent() { if (!this.submittedId) return; const formData = this.$refs.mreditor1.getFormData(); const postData = { mrcase: this.content, ...formData }; try { const response = await fetch(`MRInfo/${this.submittedId}/?format=json`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(postData), }); if (response.ok) { alert('醫案更新成功!'); } else { throw new Error('更新失败'); } } catch (error) { console.error('Error:', error); alert(`更新失败: ${error.message}`); } }, async deleteContent() { if (!this.submittedId) return; if (!confirm('確定要刪除這個醫案嗎?')) return; try { const response = await fetch(`MRInfo/${this.submittedId}/?format=json`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', } }); if (response.ok) { this.resetAll(); alert('醫案刪除成功!'); } else { throw new Error('刪除失败'); } } catch (error) { console.error('Error:', error); alert(`刪除失败: ${error.message}`); } }, resetAll() { this.content = ''; this.submittedId = null; this.fetchId = null; this.showRawHtml = false; // 重置显示格式 this.$refs.mreditor1.resetForm(); } } }; function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } </script> <style scoped> /* 样式保持不变 */ .wangeditor { flex: 1; padding: 10px; overflow-y: auto; } .right-panel { position: fixed; top: 56px; bottom: 45px; right: 0; width: 30%; background: white; padding: 10px; z-index: 100; overflow-y: auto; } .content-display { position: fixed; top: 490px; left: 0; width: 70%; bottom: 45px; z-index: 999; background-color: white; overflow-y: auto; padding: 10px; border: 1px solid #eee; white-space: pre-wrap; /* 保留原始格式 */ } .sticky-footer { display: flex; justify-content: flex-end; align-items: center; position: fixed; bottom: 0; left: 0; width: 100%; background-color: #ffd800ff; z-index: 999; padding: 10px 20px; box-sizing: border-box; flex-wrap: wrap; } .sticky-footer > span { margin-left: 5px; display: flex; align-items: center; } .submitted-id { padding: 2px; background-color: #e2f0fd; color: #004085; border-radius: 4px; } .reset-btn { margin-left: 10px; padding: 2px; background-color: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; } .reset-btn:hover { background-color: #c82333; } .id-input { display: flex; align-items: center; } .id-input input { width: 100px; padding: 2px 5px; margin-right: 5px; border: 1px solid #ccc; border-radius: 4px; } .submit-btn { background-color: #28a745; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer; } .update-btn { background-color: #007bff; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer; } .delete-btn { background-color: #dc3545; color: white; border: none; padding: 2px 8px; border-radius: 4px; cursor: pointer; } .submit-btn:hover { background-color: #218838; } .update-btn:hover { background-color: #0069d9; } .delete-btn:hover { background-color: #c82333; } </style>

filetype

// app.js - 优化版 App({ onLaunch() { // 性能监控开始 const launchStart = Date.now(); // 分阶段初始化 this.initPhase1(); // 延迟执行非关键任务 setTimeout(() => { this.initPhase2(); console.log(`启动总耗时: ${Date.now() - launchStart}ms`); }, 300); }, // 第一阶段:关键路径初始化 initPhase1() { // 加载必要配置 this.globalData.appConfig = this.loadEssentialConfig(); // 初始化用户系统 this.initUserSystem(); // 预加载关键分包 this.preloadCriticalSubpackages(); }, // 第二阶段:非关键初始化 initPhase2() { // 获取系统信息(使用新API) this.getSystemInfo(); // 加载非关键资源 this.loadNonCriticalResources(); // 初始化分析工具 this.initAnalytics(); }, // 使用新API获取系统信息 async getSystemInfo() { try { const [systemSetting, appAuthorize, deviceInfo, windowInfo, appBaseInfo] = await Promise.all([ this.promiseWrap(wx.getSystemSetting), this.promiseWrap(wx.getAppAuthorizeSetting), this.promiseWrap(wx.getDeviceInfo), this.promiseWrap(wx.getWindowInfo), this.promiseWrap(wx.getAppBaseInfo) ]); this.globalData.systemInfo = { systemSetting, appAuthorize, deviceInfo, windowInfo, appBaseInfo }; } catch (e) { console.error('获取系统信息失败', e); } }, // Promise封装工具 promiseWrap(api) { return new Promise((resolve, reject) => { api({ success: resolve, fail: reject }); }); }, // 预加载关键分包 preloadCriticalSubpackages() { const { preloadRule } = this.globalData.appConfig; if (!preloadRule) return; // 根据当前页面预加载 const currentPage = getCurrentPages()[0]; if (currentPage && preloadRule[currentPage.route]) { const packages = preloadRule[currentPage.route].packages || []; packages.forEach(pkg => { wx.loadSubpackage({ name: pkg, success: () => console.log(`分包 ${pkg} 预加载成功`), fail: (err) => console.error(`分包 ${pkg} 预加载失败`, err) }); }); } }, globalData: { appConfig: null, systemInfo: null } });与// app.js const loadConfig = require('./utils/config-loader'); App({ globalData: { userInfo: null, // 用户信息 token: null, // 用户认证token systemInfo: null, // 原始系统信息 deviceInfo: null, // 增强的设备信息 isLogin: false, // 登录状态 appName: '亮叶企服', // 应用名称 appid: 'wxf49b1aeb1d62f227', // 小程序appid tabBar: null, // TabBar实例引用 safeAreaInsets: { top: 0, bottom: 0 }, // 安全区域数据 tabBarConfig: null // TabBar配置信息 }, // 小程序初始化 onLaunch(options) { // 安全加载配置 this.globalData.appConfig = loadConfig(); // 1. 获取增强版设备信息 this.getEnhancedSystemInfo(); // 2. 初始化安全区域数据 this.calcSafeAreaInsets(); // 3. 存储TabBar配置 this.globalData.tabBarConfig = require('./app.json').tabBar; // 4. 检查登录状态 this.checkLogin(); // 5. 更新管理器 this.checkUpdate(); }, // 获取增强的系统信息(含安全区域计算)[6,7](@ref) getEnhancedSystemInfo() { try { const sys = wx.getSystemInfoSync(); this.globalData.systemInfo = sys; // 安全区域计算(兼容iOS 11+) const safeAreaBottom = sys.screenHeight - (sys.safeArea?.bottom || sys.screenHeight); // 创建增强版设备信息 this.globalData.deviceInfo = { model: sys.model, screenWidth: sys.screenWidth, screenHeight: sys.screenHeight, pixelRatio: sys.pixelRatio, statusBarHeight: sys.statusBarHeight, safeAreaBottom, sdkVersion: sys.SDKVersion, language: sys.language, isIPhoneX: /iPhone (X|1[1-5])/.test(sys.model), // 兼容iPhone X-15系列 isAndroid: /Android/.test(sys.system), isDevTools: sys.platform === 'devtools', safeArea: { top: sys.safeArea?.top || 0, bottom: safeAreaBottom, left: sys.safeArea?.left || 0, right: sys.safeArea?.right || 0, width: sys.safeArea?.width || sys.screenWidth, height: sys.safeArea?.height || sys.screenHeight } }; // 设置全局CSS变量[6](@ref) this.setGlobalCSSVariables(); } catch (e) { console.error('获取系统信息失败', e); } }, // 设置全局CSS变量[7](@ref) setGlobalCSSVariables() { const deviceInfo = this.globalData.deviceInfo; if (!deviceInfo) return; wx.setStorageSync('cssVariables', { '--status-bar-height': `${deviceInfo.statusBarHeight}px`, '--safe-area-top': `${deviceInfo.safeArea.top}px`, '--safe-area-bottom': `${deviceInfo.safeArea.bottom}px`, '--screen-width': `${deviceInfo.screenWidth}px`, '--screen-height': `${deviceInfo.screenHeight}px` }); }, // 计算安全区域(兼容全面屏设备)[6](@ref) calcSafeAreaInsets() { const sys = this.globalData.systemInfo; if (!sys || !sys.safeArea) return; this.globalData.safeAreaInsets = { top: sys.safeArea.top - sys.statusBarHeight, bottom: sys.screenHeight - sys.safeArea.bottom }; }, // 检查登录状态 checkLogin() { const token = wx.getStorageSync('token'); if (token) { this.globalData.token = token; this.globalData.isLogin = true; this.getUserInfo(); } }, // 获取用户信息 getUserInfo() { const that = this; wx.getSetting({ success(res) { if (res.authSetting['scope.userInfo']) { wx.getUserInfo({ success(res) { that.globalData.userInfo = res.userInfo; } }); } } }); }, // 检查更新 checkUpdate() { const updateManager = wx.getUpdateManager(); updateManager.onCheckForUpdate(res => { console.log('检查更新结果:', res.hasUpdate); }); updateManager.onUpdateReady(() => { wx.showModal({ title: '更新提示', content: '新版本已经准备好,是否重启应用?', success(res) { if (res.confirm) { updateManager.applyUpdate(); } } }); }); updateManager.onUpdateFailed(() => { wx.showToast({ title: '更新失败', icon: 'none' }); }); }, // ================ TabBar管理方法 ================ [3,10](@ref) updateActiveTab(pagePath) { if (!pagePath) { const pages = getCurrentPages(); pagePath = pages.length > 0 ? pages[pages.length - 1].route : ''; } if (this.globalData.tabBar) { this.globalData.tabBar.updateActiveTab(pagePath); } else { // 双保险机制:立即执行+延时重试 setTimeout(() => { if (this.globalData.tabBar) { this.globalData.tabBar.updateActiveTab(pagePath); } else { console.warn('TabBar实例未初始化'); } }, 300); } }, showTabBarBadge(index, count = 1) { if (this.globalData.tabBar) { this.globalData.tabBar.showBadge(index, count); } else { setTimeout(() => { if (this.globalData.tabBar) { this.globalData.tabBar.showBadge(index, count); } }, 300); } }, hideTabBarBadge(index) { if (this.globalData.tabBar) { this.globalData.tabBar.hideBadge(index); } else { setTimeout(() => { if (this.globalData.tabBar) { this.globalData.tabBar.hideBadge(index); } }, 300); } }, // ================ 全局功能方法 ================ // 全局登录方法 login(callback) { const that = this; wx.login({ success(res) { if (res.code) { wx.request({ url: 'https://api.yourdomain.com/auth/login', method: 'POST', data: { code: res.code }, success(res) { if (res.data.code === 0) { const token = res.data.token; that.globalData.token = token; that.globalData.isLogin = true; wx.setStorageSync('token', token); // 更新TabBar选中状态[10](@ref) that.updateActiveTab('/pages/mine/index'); callback && callback(true); } else { callback && callback(false, res.data.msg); } }, fail(err) { callback && callback(false, '登录请求失败'); } }); } else { callback && callback(false, '获取登录凭证失败'); } }, fail(err) { callback && callback(false, '登录失败'); } }); }, // 全局请求方法 request(options) { const token = this.globalData.token; const header = token ? { 'Authorization': `Bearer ${token}` } : {}; return new Promise((resolve, reject) => { wx.request({ ...options, header: { ...header, ...(options.header || {}) }, success: (res) => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data); } else { reject(res); } }, fail: (err) => { reject(err); } }); }); }, // 全局错误处理 showErrorToast(msg) { wx.showToast({ title: msg || '操作失败', icon: 'none', duration: 2000 }); } });融合后给我完整代码

filetype

app.js? [sm]:374 性能数据上报失败: {errMsg: "request:fail timeout"}(env: Windows,mp,1.06.2504010; lib: 3.8.10) fail @ app.js? [sm]:374 listOnTimeout @ node:internal/timers:557 processTimers @ node:internal/timers:500 app.js? [sm]:369 GET https://api.bedpage.com/ net::ERR_CONNECTION_TIMED_OUT(env: Windows,mp,1.06.2504010; lib: 3.8.10) reportPerformance @ app.js? [sm]:369 logPerformance @ app.js? [sm]:364 pageLoadHandler @ app.js? [sm]:341 listOnTimeout @ node:internal/timers:557 processTimers @ node:internal/timers:500 app.js? [sm]:374 性能数据上报失败: {errMsg: "request:fail "}(env: Windows,mp,1.06.2504010; lib: 3.8.10) fail @ app.js? [sm]:374 listOnTimeout @ node:internal/timers:557 processTimers @ node:internal/timers:500 app.js? [sm]:369 GET https://api.bedpage.com/ net::ERR_CONNECTION_TIMED_OUT(env: Windows,mp,1.06.2504010; lib: 3.8.10) reportPerformance @ app.js? [sm]:369 logPerformance @ app.js? [sm]:364 pageLoadHandler @ app.js? [sm]:341 listOnTimeout @ node:internal/timers:557 processTimers @ node:internal/timers:500 app.js? [sm]:374 性能数据上报失败: {errMsg: "request:fail "}(env: Windows,mp,1.06.2504010; lib: 3.8.10) fail @ app.js? [sm]:374 listOnTimeout @ node:internal/timers:557 processTimers @ node:internal/timers:500 products.js? [sm]:254 尝试修复路径: /subpackages/auto/pages/newCar/newCar => /subpackages/auto/pages/newCar/newCar _callee$ @ products.js? [sm]:254 s @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1 asyncGeneratorStep @ asyncToGenerator.js?forceSync=true:1 c @ asyncToGenerator.js?forceSync=true:1 Promise.then (async) asyncGeneratorStep @ asyncToGenerator.js?forceSync=true:1 c @ asyncToGenerator.js?forceSync=true:1 (anonymous) @ asyncToGenerator.js?forceSync=true:1 (anonymous) @ asyncToGenerator.js?forceSync=true:1 navigateToDetail @ products.js? [sm]:276 products.js? [sm]:254 尝试修复路径: /subpackages/auto/pages/usedCar/usedCar => /subpackages/auto/pages/usedCar/usedCar _callee$ @ products.js? [sm]:254 s @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1 asyncGeneratorStep @ asyncToGenerator.js?forceSync=true:1 c @ asyncToGenerator.js?forceSync=true:1 Promise.then (async) asyncGeneratorStep @ asyncToGenerator.js?forceSync=true:1 c @ asyncToGenerator.js?forceSync=true:1 (anonymous) @ asyncToGenerator.js?forceSync=true:1 (anonymous) @ asyncToGenerator.js?forceSync=true:1 navigateToDetail @ products.js? [sm]:276 products.js? [sm]:254 尝试修复路径: /subpackages/auto/pages/newCar/newCar => /subpackages/auto/pages/newCar/newCar _callee$ @ products.js? [sm]:254 s @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1 asyncGeneratorStep @ asyncToGenerator.js?forceSync=true:1 c @ asyncToGenerator.js?forceSync=true:1 Promise.then (async) asyncGeneratorStep @ asyncToGenerator.js?forceSync=true:1 c @ asyncToGenerator.js?forceSync=true:1 (anonymous) @ asyncToGenerator.js?forceSync=true:1 (anonymous) @ asyncToGenerator.js?forceSync=true:1 navigateToDetail @ products.js? [sm]:276 products.js? [sm]:421 OA跳转失败: {errMsg: "navigateTo:fail page "pages/oa/apply?category=auto&product=commercialVehicle" is not found"}(env: Windows,mp,1.06.2504010; lib: 3.8.10) fail @ products.js? [sm]:421 applyNow @ products.js? [sm]:418 products.js? [sm]:254 尝试修复路径: /subpackages/auth/pages/npa/otherNPA/otherNPA => /subpackages/auth/pages/Npa/otherNPA/otherNPA _callee$ @ products.js? [sm]:254 s @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1 asyncGeneratorStep @ asyncToGenerator.js?forceSync=true:1 c @ asyncToGenerator.js?forceSync=true:1 Promise.then (async) asyncGeneratorStep @ asyncToGenerator.js?forceSync=true:1 c @ asyncToGenerator.js?forceSync=true:1 (anonymous) @ asyncToGenerator.js?forceSync=true:1 (anonymous) @ asyncToGenerator.js?forceSync=true:1 navigateToDetail @ products.js? [sm]:276 [worker] reportRealtimeAction:fail not support U @ WAWorker.js:1 invoke @ WAWorker.js:1 invoke @ WAWorker.js:1 G @ WAWorker.js:1 (anonymous) @ WAWorker.js:1 qe @ WAWorker.js:1 Z @ WAWorker.js:1 p @ WAWorker.js:1 (anonymous) @ WAWorker.js:1 (anonymous) @ WAWorker.js:1 setTimeout (async) globalThis.setTimeout @ WAWorker.js:1 Y @ WAWorker.js:1 Le @ WAWorker.js:1 (anonymous) @ WAWorker.js:1 r @ WAWorker.js:1 s @ WAWorker.js:1 callAndRemove @ WAWorker.js:1 invokeCallbackHandler @ WAWorker.js:1 eval @ VM10:1 products.js? [sm]:254 尝试修复路径: /subpackages/auth/pages/other/customService/customService => /subpackages/auth/pages/Other/Customservice/Customservice _callee$ @ products.js? [sm]:254 s @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1 (anonymous) @ regeneratorRuntime.js?forceSync=true:1 asyncGeneratorStep @ asyncToGenerator.js?forceSync=true:1 c @ asyncToGenerator.js?forceSync=true:1 Promise.then (async) asyncGeneratorStep @ asyncToGenerator.js?forceSync=true:1 c @ asyncToGenerator.js?forceSync=true:1 (anonymous) @ asyncToGenerator.js?forceSync=true:1 (anonymous) @ asyncToGenerator.js?forceSync=true:1 navigateToDetail @ products.js? [sm]:276 products.js? [sm]:254 尝试修复路径: /subpackages/auto/pages/newCar/newCar => /subpackages/auto/pages/newCar/newCar

filetype

你是一个在C#开发领域有着丰富经验的开发工程师,现在请围绕vue,net,controller,service,采用以下结构导出excel或者csv,将以下代码进行优化,能导出excel,并且能支持高性能,还能异步处理: 前端代码: const exportExcel = async () => { const params = { StartTime: datas.dateRange[0], EndTime: datas.dateRange[1], ReportType: datas.reportType, ReportStatus: datas.reportStatus, Keyword: datas.keyword, Format: "xlsx" ,// 支持 csv 或 xlsx }; // 显示加载动画 if (proxy && proxy.$loading) proxy.$loading.show(); datas.loading = true; try { // 1. 添加请求日志 console.log("导出请求参数:", JSON.stringify(params, null, 2)); const response = await proxy.$api.request({ url: "/https/wenku.csdn.net/vehicleReport/ExportExcel", method: "POST", data: params, responseType: 'blob', headers: { 'Content-Type': 'application/json; charset=UTF-8', 'Authorization': `Bearer ${localStorage.getItem('token')}` // 确保权限 }, timeout: 30000 // 增加超时时间 }); // 2. 检查HTTP状态码 if (response.status !== 200) { // 尝试解析错误信息 let errorMsg = `服务器错误 (${response.status})`; try { const text = await new Response(response.data).text(); if (text) { try { const json = JSON.parse(text); errorMsg = json.message || json.error || text; } catch { errorMsg = text; } } } catch {} throw new Error(errorMsg); } // 3. 文件处理 const contentType = response.headers['content-type'] || (params.Format === 'csv' ? 'text/csv' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); const blob = new Blob([response.data], { type: contentType }); // 4. 验证Blob if (blob.size === 0) { throw new Error("接收到的文件内容为空"); } // 5. 创建下载链接 const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `车辆报告_${new Date().toISOString().slice(0, 10)}.${params.Format}`; // 6. 添加超时处理 const downloadTimeout = setTimeout(() => { proxy.$message.warning("下载处理时间较长,请稍候..."); }, 5000); link.onclick = () => clearTimeout(downloadTimeout); document.body.appendChild(link); link.click(); document.body.removeChild(link); // 7. 释放资源 setTimeout(() => { window.URL.revokeObjectURL(url); }, 1000); } catch (error) { console.error("导出失败详情:", error); // 8. 显示更友好的错误信息 let userMessage = "导出失败"; if (error.message.includes("401")) { userMessage = "登录已过期,请重新登录"; } else if (error.message.includes("500")) { userMessage = "服务器处理失败"; } else if (error.message.includes("空")) { userMessage = "未获取到有效数据"; } else if (error.message.includes("超时")) { userMessage = "请求超时,请稍后再试"; } proxy.$message.error(`${userMessage}: ${error.message}`); } finally { // 统一隐藏 loading if (proxy && proxy.$loading) proxy.$loading.hide(); datas.loading = false; } }; 后端代码: #region =========导出报告============ [HttpPost("/vehicleReport/ExportExcel")] [Authorize] public async Task<IActionResult> ExportExcel([FromBody] VehicleReportExportToExcelParameter searchParam) { try { // 参数验证 if (!searchParam.StartTime.HasValue || !searchParam.EndTime.HasValue) { return BadRequest(ResponseMessage.Error("时间范围不能为空")); } // 构建查询条件 Expression<Func<VehicleReports, bool>> filter = x => x.AddTime >= searchParam.StartTime.Value && x.AddTime <= searchParam.EndTime.Value; if (!string.IsNullOrEmpty(searchParam.Keyword)) { filter = filter.And(x => x.Vin.Contains(searchParam.Keyword) || x.UserName.Contains(searchParam.Keyword)); } // 验证排序字段合法性 var allowedOrderFields = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "AddTime", "Id" }; string orderBy = searchParam.OrderBy ?? "AddTime,Id"; if (!allowedOrderFields.Contains(orderBy.Split(',')[0].Trim())) { return BadRequest(ResponseMessage.Error("不允许的排序字段")); } // 调用服务层方法获取数据 var list = await _vehicleReportService.ExportExcelAsync( 0, // 获取所有数据 filter, orderBy); if (list == null || !list.Any()) { return NotFound(ResponseMessage.Error("暂无数据")); } // 生成 Excel 文件流 using var stream = new MemoryStream(); string format = searchParam.Format?.ToLower() ?? "xlsx"; if (format == "csv") { await GenerateCsvAsync(stream, list); } else { await GenerateXlsxAsync(stream, list); } // 重置流位置 stream.Position = 0; string fileName = $"车辆报告_{DateTime.Now:yyyyMMddHHmmss}_{Guid.NewGuid():N}.{format}"; // 返回文件 return File( stream.ToArray(), format == "csv" ? "text/csv; charset=utf-8" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", fileName ); } catch (Exception ex) when (ex is ArgumentException or InvalidOperationException) { logger.Trace(ex, "参数或操作异常"); return BadRequest(ResponseMessage.Error($"请求参数错误:{ex.Message}")); } catch (Exception ex) { logger.Trace(ex, "导出Excel失败"); return StatusCode(500, ResponseMessage.Error($"服务器内部错误:{ex.Message}")); } } private async Task GenerateCsvAsync(MemoryStream stream, IEnumerable<VehicleReports> list) { // 设置 leaveOpen: true 确保流不被关闭 using (var writer = new StreamWriter(stream, Encoding.UTF8, leaveOpen: true)) { // 写入表头 await writer.WriteLineAsync("报告编号,VIN码,用户名,报告类型,查询状态,创建日期,生效日期"); foreach (var item in list) { string EscapeCsv(string value) { if (string.IsNullOrEmpty(value)) return string.Empty; if (value.StartsWith("=") || value.StartsWith("+") || value.StartsWith("-") || value.StartsWith("@")) return " " + value; // 加空格防止公式注入 return value.Replace("\"", "\"\""); // 转义双引号 } await writer.WriteLineAsync( $"\"{EscapeCsv(item.ReportNo)}\"," + $"\"{EscapeCsv(item.Vin)}\"," + $"\"{EscapeCsv(item.UserName)}\"," + $"\"{EscapeCsv(item.ReportType.ToString())}\"," + $"\"{item.ReportStatus}\"," + $"\"{item.AddTime:yyyy-MM-dd HH:mm:ss}\"," + $"\"{item.ValidTime:yyyy-MM-dd HH:mm:ss}\""); } } } private void GenerateXlsx(XLWorkbook workbook, IEnumerable<VehicleReports> list) { var worksheet = workbook.Worksheets.Add("Reports"); // 添加表头 worksheet.Cell(1, 1).Value = "报告编号"; worksheet.Cell(1, 2).Value = "VIN码"; worksheet.Cell(1, 3).Value = "用户名"; worksheet.Cell(1, 4).Value = "报告类型"; worksheet.Cell(1, 5).Value = "查询状态"; worksheet.Cell(1, 6).Value = "创建日期"; worksheet.Cell(1, 7).Value = "生效日期"; int row = 2; foreach (var item in list) { worksheet.Cell(row, 1).Value = item.ReportNo ?? string.Empty; worksheet.Cell(row, 2).Value = item.Vin ?? string.Empty; worksheet.Cell(row, 3).Value = item.UserName ?? string.Empty; worksheet.Cell(row, 4).Value = item.ReportType.ToString() ?? string.Empty; worksheet.Cell(row, 5).Value = item.ReportStatus; worksheet.Cell(row, 6).Value = item.AddTime; worksheet.Cell(row, 7).Value = item.ValidTime; row++; } } private async Task GenerateXlsxAsync(MemoryStream stream, IEnumerable<VehicleReports> list) { using (var workbook = new XLWorkbook()) { GenerateXlsx(workbook, list); workbook.SaveAs(stream); } } #endregion 服务层代码: #region ======== 导出报告Excel ================ /// /// 查询指定数量列表 /// public async Task<IEnumerable<VehicleReports>> ExportExcelAsync(int top, Expression<Func<VehicleReports, bool>> funcWhere, string orderBy, WriteRoRead writeAndRead = WriteRoRead.Read) { // 参数校验 if (funcWhere == null) throw new ArgumentNullException(nameof(funcWhere)); if (string.IsNullOrWhiteSpace(orderBy)) throw new ArgumentException("排序字段不能为空", nameof(orderBy)); // 防止非法排序字段(简单白名单示例) var allowedOrderFields = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { nameof(VehicleReports.Id), nameof(VehicleReports.AddTime), // 添加其他允许排序的字段 }; var orderFields = orderBy.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(f => f.Trim().Split(' ')[0].Trim()); foreach (var field in orderFields) { if (!allowedOrderFields.Contains(field)) { throw new ArgumentException($"不允许按字段 [{field}] 排序"); } } using var context = _contextFactory.CreateContext(writeAndRead); var result = context.Set<VehicleReports>() .Where(funcWhere) .OrderByBatch(orderBy); // 确保OrderByBatch支持EF Core翻译 if (top > 0) { result = result.Take(top); } return await result.ToListAsync(); } #endregion

filetype

<template>
<el-tabs v-model="tableDescIndex" editable :class="`${prefixCls}__tabs`" @tab-change="handleTabChange" @edit="handleTabsEdit" > <el-tab-pane v-for="(item, index) in routeDescTabs" :key="index" :label="item.routeAbbr" :name="index" > <template #label> <el-tooltip class="box-item" effect="dark" :content="item.lineDesc" placement="top" v-if="item.lineDesc" > {{ item.routeAbbr }} </el-tooltip> {{ item.routeAbbr }} </template> <el-table :data="item.planStopList" :class="`${prefixCls}__table`" :header-cell-style="headerCellStyle" :cell-style="cellStyle" > <el-table-column prop="sort" :label="t('lineEdit.sort')" width="90" align="center"> <template #default="scope">
{{ scope.$index + 1 }} <el-icon @click="addColumnAfterRow(scope.$index)" class="add-icon"> <Plus /> </el-icon> <el-icon @click="removeRow(scope.$index)" class="add-icon"> <Delete /> </el-icon>
</template> </el-table-column> <el-table-column prop="stopId" :label="t('lineMapEdit.stationName')" align="center"> <template #default="scope"> <el-select v-model="scope.row.stopDesc" filterable :filter-method="handleSearch" virtual-scroll :virtual-scroll-item-size="40" :virtual-scroll-visible-items="15" v-select-loadmore="loadMoreData" placeholder="请选择或搜索" > <el-option v-for="item in visibleOptions" :key="item.stopId" :label="item.stopDesc" :value="item.stopId" /> </el-select> </template> </el-table-column> </el-table> </el-tab-pane> </el-tabs>
</template> <script lang="ts" setup> import { headerCellStyle, cellStyle } from '@/components/PlanningComps/common' import { Plus, Delete } from '@element-plus/icons-vue' import { ref } from 'vue' import { debounce } from 'lodash-es' import { getAllStopList } from '@/api/planning/stop/index' import { RouteTab, lineRouteInfoItem } from '@/api/planning/line/type' import { ElMessageBox } from 'element-plus' defineOptions({ name: 'RouteDesc' }) const props = defineProps<{ lineRouteInfoList: lineRouteInfoItem[] }>() const emit = defineEmits(['update:tabIndex', 'update-line-detail', 'stop-added', 'stop-deleted']) const { getPrefixCls } = useDesign() const prefixCls = getPrefixCls('route-desc') const message = useMessage() const { t } = useI18n() const tableDescIndex = ref(0) // 当前选中的选项 const routeDescTabs = ref<lineRouteInfoItem[]>([]) // 新增线路点 const wayPoint = ref([]) // 初始化响应式引用 const allOptions = ref([]) // 所有选项 const filteredOptions = ref([]) // 过滤后的选项 const currentPage = ref(1) // 当前页码 const pageSize = 50 // 每页大小 const hasMore = ref(true) // 是否还有更多数据 const isLoading = ref(false) // 加载状态 const selectKey = ref(0) const searchQuery = ref('') const loadedCount = ref(100) // 初始加载100条 // 新增对话框删除tab const handleTabsEdit = (targetName: TabPaneName | undefined, action: 'remove' | 'add') => { if (action === 'add') { ElMessageBox.prompt('Please enter the line name', 'Prompt', { confirmButtonText: 'Confirm', cancelButtonText: 'Cancel', inputPattern: /.+/, // 正则验证 inputErrorMessage: 'The input cannot be empty' }) .then(({ value }) => { const title = value.trim() routeDescTabs.value.push({ lineId: '', lineDesc: '', routeAbbr: title, planStopList: [{ stopId: '', stopDesc: '', sort: 1, lng: '', lat: '' }], routeGeometry: {} }) tableDescIndex.value = routeDescTabs.value.length - 1 handleTabChange() // lineRouteInfoItem 添加线路描述 emit('update-line-detail', routeDescTabs.value) }) .catch(() => { console.log('User cancellation') }) } else if (action === 'remove') { const tabs = routeDescTabs.value let activeName = tableDescIndex.value if (activeName === targetName) { tabs.forEach((tab, index) => { if (tab.name === targetName) { const nextTab = tabs[index + 1] || tabs[index - 1] if (nextTab) { activeName = nextTab.name } } }) } tableDescIndex.value = activeName routeDescTabs.value = tabs.filter((tab) => tab.name !== targetName) } } // 删除 const removeTab = (targetName: string) => { const tabs = routeDescTabs.value let activeName = tableDescIndex.value if (activeName === targetName) { tabs.forEach((tab, index) => { if (tab.name === targetName) { const nextTab = tabs[index + 1] || tabs[index - 1] if (nextTab) { activeName = nextTab.name } } }) } tableDescIndex.value = activeName routeDescTabs.value = tabs.filter((tab) => tab.name !== targetName) } // 判定索引位置 const determinePositionType = (index: number) => { const stops = routeDescTabs.value[tableDescIndex.value].planStopList if (index === 0) return 'start' if (index === stops.length - 1) return 'end' return 'middle' } // 新增行index 索引(新增包含lineID站点-保存新增索引-判断位置-传递事件-构建几何请求-重绘线路) const addColumnAfterRow = (index: number) => { const newIndex = index + 1 routeDescTabs.value[tableDescIndex.value].planStopList.splice(newIndex, 0, { sort: `${newIndex + 1}`, stopId: '', stopDesc: '', lng: '', lat: '' }) // 更新所有行的 No 值 routeDescTabs.value[tableDescIndex.value].planStopList.forEach((row, i) => { row.sort = `${i + 1}` }) } // 监听删除操作后重排序 const removeRow = (index) => { if (routeDescTabs.value[tableDescIndex.value].planStopList.length <= 1) return message.warning('Only one value cannot be deleted') // 触发线路更新事件,携带被删除站点的类型信息 if (routeDescTabs.value[tableDescIndex.value].lineId != '') { emit('stop-deleted', { delectIndex: index, // 删除站点索引 positionType: determinePositionType(index) // 删除站点位置类型 }) } routeDescTabs.value[tableDescIndex.value].planStopList.splice(index, 1) // 重置编号 routeDescTabs.value[tableDescIndex.value].planStopList.forEach((row, i) => { row.sort = `${i + 1}` }) message.success('Delete successfully') } // 获取站点列表 const getAllStopsList = async () => { try { const data = (await getAllStopList()) as StopListItem[] allOptions.value = data } catch (error) { console.error('获取站点列表失败:', error) allOptions.value = [] filteredOptions.value = [] } } // 防抖远程搜索 const handleSearch = debounce( (query) => { debugger if (!query) { // filteredOptions.value = [] return } try { searchQuery.value = query.toLowerCase() } catch (error) { console.error('搜索站点失败:', error) filteredOptions.value = [] } }, 300, { leading: true, trailing: true } ) // 局部注册自定义滚动指令翻页加载 const vSelectLoadmore = { mounted(el, binding) { // 使用nextTick确保DOM渲染完成 setTimeout(() => { const SELECTWRAP_DOM = el.querySelector('.el-select-dropdown .el-select-dropdown__wrap') if (SELECTWRAP_DOM) { SELECTWRAP_DOM.addEventListener('scroll', () => { // 精确计算是否滚动到底部 const isBottom = Math.ceil(SELECTWRAP_DOM.scrollTop + SELECTWRAP_DOM.clientHeight) >= SELECTWRAP_DOM.scrollHeight - 1 if (isBottom) { binding.value() // 触发绑定的加载函数 } }) } }, 100) } } // 分页加载函数 const loadMoreData = () => { if (loadedCount.value < allOptions.value.length) { loadedCount.value += 50 // 每次加载50条 } } // 动态计算可见选项(结合搜索和分页) const visibleOptions = computed(() => { debugger let result = allOptions.value if (searchQuery.value) { result = result.filter((item) => item.stopDesc.toLowerCase().includes(searchQuery.value.toLowerCase()) ) } return result.slice(0, loadedCount.value) // 只返回当前加载的数据 }) // 双击修改方案名 const handleTabDblClick = (index: number) => { const currentTab = routeDescTabs.value[index] if (!currentTab) return ElMessageBox.prompt('Please enter the new line description', 'Modify the line name', { confirmButtonText: 'Confirm', cancelButtonText: 'Cancel', inputPattern: /.+/, inputErrorMessage: 'Please enter a valid line name', inputValue: currentTab.routeAbbr // 初始值为当前名称 }) .then(({ value }) => { // 更新 tab 的 lineDesc 字段 routeDescTabs.value[index].routeAbbr = value.trim() }) .catch(() => { console.log('User cancels modification') }) } const handleChangeSelectStops = (value, sort) => { console.log(value, sort, 'dddd') // debugger filteredOptions.value.some((item, index) => { if (item.stopId == value) { const tabIndex = tableDescIndex.value const stops = routeDescTabs.value[tabIndex].planStopList // // 获取新增索引 const addedIndex = sort - 1 // 获取原始 stopId // const oldStop = stops[addedIndex]?.stopId // 设置当前站点信息 routeDescTabs.value[tabIndex].planStopList[addedIndex] = { ...stops[addedIndex], stopId: item.stopId, stopDesc: item.stopDesc, lng: item.lng, lat: item.lat } // 新增及替换点的更改 if (routeDescTabs.value[tableDescIndex.value].lineId != '') { emit('stop-added', { stopIndex: addedIndex, positionType: determinePositionType(addedIndex), behindStation: addedIndex > 0 ? stops[addedIndex] : null }) } } }) } const handleTabChange = () => { emit('update:tabIndex', tableDescIndex.value) } // 计算当前选项卡的有效 stopId 数量 const validStopCount = computed(() => { const currentTab = routeDescTabs.value[tableDescIndex.value] if (!currentTab?.planStopList) return 0 return currentTab.planStopList.filter((stop) => stop.stopId).length }) // 监听站点数量变化并触发更新 watch( validStopCount, (newCount, oldCount) => { if (newCount !== oldCount) { // console.log('站点数量变化', newCount, oldCount, tableDescIndex.value, routeDescTabs.value) const currentTab = routeDescTabs.value[tableDescIndex.value] // 新增 if (currentTab && currentTab.lineId == '') { // debugger // console.log(routeDescTabs.value, 'routeDescTabs.value') emit('update-line-detail', routeDescTabs.value) } } }, { immediate: true } ) watchEffect(() => { if (props.lineRouteInfoList && props.lineRouteInfoList.length > 0) { routeDescTabs.value = [...props.lineRouteInfoList] console.log(routeDescTabs.value, 'routeDescTabs.value') } }) onBeforeMount(() => { getAllStopsList() }) onMounted(() => {}) </script> <style lang="scss" scoped> $prefix-cls: #{$namespace}-route-desc; .#{$prefix-cls} { .el-select-dropdown { max-height: 300px; } width: 458px; background: rgba(255, 255, 255, 0.7); border-radius: 4px; padding: 0px 16px; overflow-y: auto; &__tabs { height: calc(100vh - 110px); .tab-title { display: flex; align-items: center; gap: 6px; padding: 0 8px; &:hover .el-icon { opacity: 1; } .el-icon { opacity: 0; transition: opacity 0.2s; &:hover { color: #409eff; } } } } &__table { height: calc(100vh - 175px); // 表头圆角 :deep(.el-table__header) { border-radius: 6px; overflow: hidden; } // 表内容行间距 :deep(.el-table__body) { border-collapse: separate !important; border-spacing: 0 7px !important; /* 第二个值控制行间距 */ overflow: hidden !important; } // 表内容每一行的首尾单元格具有圆角效果 :deep(.el-table__body-wrapper table) { border-collapse: separate; overflow: hidden; } :deep(.el-table__body tr td:first-child), :deep(.el-table__body tr td:last-child) { position: relative; } :deep(.el-table__body tr td:first-child) { border-top-left-radius: 6px; border-bottom-left-radius: 6px; } :deep(.el-table__body tr td:last-child) { border-top-right-radius: 6px; border-bottom-right-radius: 6px; } .add-icon { opacity: 0; transition: opacity 0.2s ease; } tr:hover .add-icon { opacity: 1; cursor: pointer; margin-left: 6px; } } } </style> <style lang="scss"> $prefix-cls: #{$namespace}-route-desc; .#{$prefix-cls} { } </style> 当前页vSelectLoadmore 在搜索数据出来后无法获取到滚动事件的原因

小马甲不小
  • 粉丝: 37
上传资源 快速赚钱