活动介绍
file-type

详细解析三种MFC对话框加载ToolBar的方法

RAR文件

1星 | 下载需积分: 11 | 1.46MB | 更新于2025-02-16 | 86 浏览量 | 6 下载量 举报 收藏
download 立即下载
从给定文件信息中,可以提炼出以下IT知识点: **MFC对话框创建工具栏的3种方式实例解析** MFC(Microsoft Foundation Classes)是一个用于创建Windows应用程序的C++库。它为开发者提供了创建Windows应用程序的框架和一系列预构建的控件和类。本实例将深入探讨如何在MFC对话框中加载和使用ToolBar(工具栏)。 **知识点1:MFC对话框** 在MFC中,对话框(Dialog Box)是一种用于显示信息、接收用户输入或提供选择的窗口。在对话框中加入ToolBar可以使界面更加友好,增加用户的交互体验。 **知识点2:ToolBar(工具栏)** ToolBar是一个常用的用户界面元素,它提供了一系列的按钮供用户快速进行命令操作。在MFC中,ToolBar是CToolBar类的一个实例。开发者可以通过编程方式控制ToolBar的创建、添加按钮以及响应按钮事件。 **知识点3:MFC中ToolBar的三种创建方式** 作者在源代码中展示了三种不同的方式创建ToolBar,并且注释详尽,方便开发者理解每一步的作用。由于具体的源代码细节未在此处提供,我们将基于作者所描述的“三种方式”的概念进行讨论。 1. **第一种方式**可能涉及到了最基本的ToolBar创建方法,可能是直接使用MFC提供的类和方法创建ToolBar,并进行基本的配置和按钮添加。 2. **第二种方式**可能对ToolBar进行了进一步的定制或优化,比如对ToolBar的样式进行调整,或者改变了按钮的外观。 3. **第三种方式**作者特别推荐,因为它具有较好的扩展性。扩展性可能指的是,这种方式允许更容易地为ToolBar添加新功能、支持新的按钮类型或是在将来进行界面更新时可以更容易地修改。 作者在描述中提到当前的缺陷是无法将ToolBar按钮的3D边框设置为透明,使得界面美化成了一个头疼的问题。这表明在MFC中,ToolBar控件具有默认的3D边框样式,且更改它需要特别的技术处理。 **知识点4:ToolBar按钮的3D边框** 在MFC中,ToolBar中的按钮默认具有3D边框,这些边框是灰色的,并且无法直接通过属性更改。作者提到的“希望懂得的网友”可能指了解如何通过编程方式修改ToolBar样式的人,尤其是如何修改按钮边框的样式,使其透明化,以便后期可以更加灵活地进行界面美化。 **知识点5:CToolBar的DrawItem** 为了实现透明按钮边框的效果,可能需要重载CToolBar类的DrawItem函数。DrawItem函数是MFC控件用于自定义绘制项的函数。通过重载此函数,开发者可以自定义按钮的绘制方式,包括3D边框的处理。 **知识点6:文件名称列表说明** - InsertToolBar.sln: 这是一个Visual Studio解决方案文件,包含了项目配置、文件依赖关系以及解决方案的设置。 - InsertToolBar.suo: 这是Visual Studio解决方案用户选项文件,包含了用户自定义的一些设置,比如窗口布局、工具栏设置等。 - InsertToolBar: 这个文件是项目的主要工作目录,其中应包含了项目的所有源代码和资源文件。 - Debug: 这个目录通常包含用于调试的编译后的二进制文件,如.exe和.dll文件,以及其他的调试信息。 通过以上知识点分析,可以了解到MFC对话框创建ToolBar的三种方式的不同特点,以及它们的潜在优缺点。同时,也对MFC中ToolBar控件的样式定制,尤其是按钮边框透明化的需求有所了解。此外,还掌握了与Visual Studio项目相关的一些文件及其作用。

相关推荐

filetype

<template>
坐标系: <el-radio-group v-model="coordinateSystem" size="small"> <el-radio-button label="gcj02">高德/火星坐标</el-radio-button> <el-radio-button label="bd09">百度坐标</el-radio-button> </el-radio-group>
<Search @location-selected="handleLocationSelected" /> <LocationBar v-if="loaded" :update-interval="100" :use-dms-format="useDmsFormat" /> </template> <script setup lang="ts"> // 原有导入 + 新增坐标转换工具类导入 import { computed, onUnmounted, onMounted, reactive, ref } from "vue"; import LocationBar from "./location-bar.vue"; import Search from "./search.vue"; import initMap from "./init"; import { throttle } from "lodash"; import { loadRipplePoints, createMultipleRippleCircles } from "./circle.js"; import { $prototype } from "../../main.ts"; import markerImage from "@/assets/images/building.png"; import { imageDark } from "naive-ui/es/image/styles"; // 新增:导入坐标转换工具类 import { CoordinateTransformer } from "./coordinate-transformer"; // 修复类型定义 type Rectangle = any; // 原有状态 + 新增坐标系选择状态 const miniMapContainer = ref<HTMLElement>(); let viewIndicator: Rectangle; const currentPosition = reactive({ longitude: 113.361538, latitude: 27.339318, }); const ZHUZHOU_EXTENT = { west: 112.5, east: 114.5, south: 26.0, north: 28.0, }; const rippleEntities = ref<any[]>([]); const heightThreshold = 80000; const indicatorStyle = ref({ left: "50%", top: "50%", display: "block", }); const loaded = ref(false); const useDmsFormat = ref(false); const overviewViewer = ref(null); let currentMarker: any = null; // 新增:坐标系选择状态(默认高德GCJ-02) const coordinateSystem = ref("gcj02"); // 'gcj02' | 'bd09' // 原有方法完全不变(波纹可见性更新) const updateRippleVisibility = throttle(() => { if (!$prototype.$map || rippleEntities.value.length === 0) return; let shouldShow = false; const cartographic = $prototype.$map.camera.positionCartographic; if (cartographic) { const cameraHeight = cartographic.height; shouldShow = cameraHeight > heightThreshold; } rippleEntities.value.forEach((entity) => { //entity.show = shouldShow; entity.show = false; }); }, 200); // 原有方法完全不变(指示器位置更新) const updateIndicatorPosition = () => { if (!$prototype.$map) return; const camera = $prototype.$map.camera; const rect = camera.computeViewRectangle(); if (!rect) return; const center = Cesium.Rectangle.center(rect); const lon = Cesium.Math.toDegrees(center.longitude); const lat = Cesium.Math.toDegrees(center.latitude); const constrainedLon = Math.max( ZHUZHOU_EXTENT.west, Math.min(ZHUZHOU_EXTENT.east, lon) ); const constrainedLat = Math.max( ZHUZHOU_EXTENT.south, Math.min(ZHUZHOU_EXTENT.north, lat) ); const lonPercent = ((constrainedLon - ZHUZHOU_EXTENT.west) / (ZHUZHOU_EXTENT.east - ZHUZHOU_EXTENT.west)) * 100; const latPercent = 100 - ((constrainedLat - ZHUZHOU_EXTENT.south) / (ZHUZHOU_EXTENT.north - ZHUZHOU_EXTENT.south)) * 100; indicatorStyle.value = { left: `${lonPercent}%`, top: `${latPercent}%`, display: "block", }; }; // 原有方法完全不变(鹰眼地图更新) const updateOverview = () => { if (!$prototype.$map || !overviewViewer.value) return; const rectangle = $prototype.$map.camera.computeViewRectangle(); if (!rectangle) return; updateIndicatorPosition(); // 添加视口矩形更新逻辑 if (viewIndicator) { viewIndicator.rectangle.coordinates = rectangle; } }; // 原有方法完全不变(初始化鹰眼地图) const initMiniMap = () => { Cesium.Ion.defaultAccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIxMDhlNDdmYy03NzFhLTQ1ZTQtOWQ3NS1lZDAzNDc3YjE4NDYiLCJpZCI6MzAxNzQyLCJpYXQiOjE3NDcwNTMyMDN9.eaez8rQxVbPv2LKEU0sMDclPWyHKhh1tR27Vg-_rQSM"; if (!miniMapContainer.value) return; const worldProvider = new Cesium.UrlTemplateImageryProvider({ url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", minimumLevel: 0, maximumLevel: 19, tilingScheme: new Cesium.WebMercatorTilingScheme(), }); const zhuzhouProvider = new Cesium.UrlTemplateImageryProvider({ url: "http://124.232.190.30:9000/proxy/pk1725866655224/map/zzzsyx_18/{z}/{x}/{y}.png", minimumLevel: 1, maximumLevel: 17, tilingScheme: new Cesium.WebMercatorTilingScheme(), }); overviewViewer.value = new Cesium.Viewer(miniMapContainer.value, { sceneMode: Cesium.SceneMode.SCENE2D, baseLayerPicker: false, homeButton: false, timeline: false, navigationHelpButton: false, animation: false, scene3DOnly: true, selectionIndicator: false, infoBox: false, imageryProvider: worldProvider, terrainProvider: undefined, mapProjection: new Cesium.WebMercatorProjection(), skyBox: false, skyAtmosphere: false, }); // 禁用所有相机交互控制(新增功能代码) const cameraController = overviewViewer.value.scene.screenSpaceCameraController; cameraController.enableTranslate = false; cameraController.enableZoom = false; cameraController.enableRotate = false; cameraController.enableTilt = false; cameraController.enableLook = false; const zhuzhouLayer = overviewViewer.value.imageryLayers.addImageryProvider(zhuzhouProvider); overviewViewer.value.camera.setView({ destination: Cesium.Rectangle.fromDegrees( ZHUZHOU_EXTENT.west, ZHUZHOU_EXTENT.south, ZHUZHOU_EXTENT.east, ZHUZHOU_EXTENT.north ), }); const toolbar = overviewViewer.value.container.getElementsByClassName( "cesium-viewer-toolbar" )[0]; if (toolbar) toolbar.style.display = "none"; overviewViewer.value.cesiumWidget.creditContainer.style.display = "none"; initRectangle(); }; // 原有方法完全不变(初始化视图指示器) function initRectangle() { viewIndicator = overviewViewer.value.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees( ZHUZHOU_EXTENT.west, ZHUZHOU_EXTENT.south, ZHUZHOU_EXTENT.east, ZHUZHOU_EXTENT.north ), material: Cesium.Color.RED.withAlpha(0.3), outline: true, outlineColor: Cesium.Color.RED, outlineWidth: 2, }, }); } // 原有方法完全不变(添加演示图形) function addDemoGraphics() { const chinaBoundary = $prototype.$map.dataSources.add( Cesium.GeoJsonDataSource.load("/shp_zz.geojson", { stroke: Cesium.Color.YELLOW, strokeWidth: 0.8, fill: false, clampToGround: true, describe: null, }) ); chinaBoundary.then((dataSource) => { const entities = dataSource.entities.values; for (let entity of entities) { if (entity.polyline) { entity.polyline.fill = false; } } }); } // 原有方法完全不变(飞行到默认位置) function flyToDes() { return new Promise<void>((resolve) => { const center = Cesium.Cartesian3.fromDegrees(-98.0, 40.0); $prototype.$map.camera.flyTo({ destination: new Cesium.Cartesian3( -2432812.6687511606, 5559483.804371395, 2832009.419525571 ), orientation: { heading: 6.283185307179421, pitch: -1.0472145569408116, roll: 6.2831853071795205, }, complete: function () { resolve(); // 飞行完成后 resolve }, }); }); } // 原有方法完全不变(监听相机变化) const setupCameraListener = () => { $prototype.$map.camera.changed.addEventListener(updateOverview); }; // 原有方法完全不变(鹰眼地图点击处理) const handleMiniMapClick = (event: MouseEvent) => { if (!miniMapContainer.value || !overviewViewer.value) return; const rect = miniMapContainer.value.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; const xPercent = (x / rect.width) * 100; const yPercent = (y / rect.height) * 100; const lon = ZHUZHOU_EXTENT.west + (xPercent / 100) * (ZHUZHOU_EXTENT.east - ZHUZHOU_EXTENT.west); const lat = ZHUZHOU_EXTENT.north - (yPercent / 100) * (ZHUZHOU_EXTENT.north - ZHUZHOU_EXTENT.south); $prototype.$map.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(lon, lat, 1000000), }); }; // 原有生命周期方法完全不变 onMounted(() => { initMap(); loaded.value = true; addDemoGraphics(); // 先执行飞行,在飞行完成后再初始化鹰眼 flyToDes().then(() => { initMiniMap(); setupCameraListener(); updateOverview(); // 此时相机位置已稳定 // 波纹加载逻辑保持不变 (async () => { try { const ripplePoints = await loadRipplePoints(); rippleEntities.value = createMultipleRippleCircles( $prototype.$map, ripplePoints ); $prototype.$map.camera.changed.addEventListener(updateRippleVisibility); updateRippleVisibility(); } catch (error) { console.error("加载波纹圆失败:", error); } })(); }); }); // 原有方法完全不变(创建标记) const createMarker = (wgsLocation) => { if (currentMarker) { $prototype.$map.entities.remove(currentMarker); } console.log("标记图片加载状态:", markerImage); console.log("创建标记位置:", wgsLocation); currentMarker = $prototype.$map.entities.add({ position: Cesium.Cartesian3.fromDegrees( wgsLocation.lng, wgsLocation.lat, 10 ), label: { text: wgsLocation.name + "(标记位置)", font: "18px sans-serif", fillColor: Cesium.Color.YELLOW, outlineColor: Cesium.Color.BLACK, outlineWidth: 2, verticalOrigin: Cesium.VerticalOrigin.TOP, pixelOffset: new Cesium.Cartesian2(0, 20), heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND, show: true, }, // 关键:用 billboard 替换 point billboard: { image: new URL("@/assets/images/building.png", import.meta.url).href, // 本地图标 width: 32, // 图标宽度(像素) height: 32, // 图标高度(像素) verticalOrigin: Cesium.VerticalOrigin.BOTTOM, // 让图标尖点对地 heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND, scale: 1.0, }, }); console.log("标记实体已添加:", currentMarker); $prototype.$map.scene.requestRender(); return currentMarker; }; // 核心修改:使用工具类实现动态坐标系转换 const handleLocationSelected = (location: { lng: number; lat: number; name: string; }) => { if (!$prototype.$map) return; // 根据选择的坐标系进行转换 let wgsLocation; if (coordinateSystem.value === "gcj02") { // 高德/火星坐标转WGS84 wgsLocation = CoordinateTransformer.gcj02ToWgs84( location.lng, location.lat ); } else { // 百度坐标转WGS84 wgsLocation = CoordinateTransformer.bd09ToWgs84(location.lng, location.lat); } console.log( `转换前(${coordinateSystem.value})坐标:`, location.lng, location.lat ); console.log("转换后(WGS84)坐标:", wgsLocation.lng, wgsLocation.lat); // 使用转换后的坐标执行原有逻辑 const destination = Cesium.Cartesian3.fromDegrees( wgsLocation.lng, wgsLocation.lat, 3000 ); $prototype.$map.camera.flyTo({ destination, orientation: { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(-90), roll: 0, }, duration: 2, complete: () => { createMarker({ ...location, ...wgsLocation }); }, }); }; // 原有方法完全不变(组件销毁清理) onUnmounted(() => { if ($prototype.$map) { $prototype.$map.destroy(); $prototype.$map = null; } if ($prototype.$map) { $prototype.$map.camera.changed.removeEventListener(updateRippleVisibility); } console.log("组件销毁"); }); const emit = defineEmits(["onload", "onclick"]); const initMars3d = async (option: any) => { emit("onclick", true); emit("onload", $prototype.$map); }; </script> <style lang="less"> /**cesium 工具按钮栏*/ .cesium-viewer-toolbar { top: auto !important; bottom: 35px !important; left: 12px !important; right: auto !important; } .cesium-toolbar-button img { height: 100%; } .cesium-viewer-toolbar > .cesium-toolbar-button, .cesium-navigationHelpButton-wrapper, .cesium-viewer-geocoderContainer { margin-bottom: 5px; float: left; clear: both; text-align: center; } .cesium-button { background-color: rgba(23, 49, 71, 0.8); color: #e6e6e6; fill: #e6e6e6; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); line-height: 32px; } .cesium-button:hover { background: #3ea6ff; } /**cesium 底图切换面板*/ .cesium-baseLayerPicker-dropDown { bottom: 0; left: 40px; max-height: 700px; margin-bottom: 5px; background-color: rgba(23, 49, 71, 0.8); } /**cesium 帮助面板*/ .cesium-navigation-help { top: auto; bottom: 0; left: 40px; transform-origin: left bottom; background: none; background-color: rgba(23, 49, 71, 0.8); .cesium-navigation-help-instructions { background: none; } .cesium-navigation-button { background: none; } .cesium-navigation-button-selected, .cesium-navigation-button-unselected:hover { background: rgba(0, 138, 255, 0.2); } } /**cesium 二维三维切换*/ .cesium-sceneModePicker-wrapper { width: auto; } .cesium-sceneModePicker-wrapper .cesium-sceneModePicker-dropDown-icon { float: right; margin: 0 3px; } /**cesium POI查询输入框*/ .cesium-viewer-geocoderContainer .search-results { left: 0; right: 40px; width: auto; z-index: 9999; } .cesium-geocoder-searchButton { background-color: rgba(23, 49, 71, 0.8); } .cesium-viewer-geocoderContainer .cesium-geocoder-input { background-color: rgba(63, 72, 84, 0.7); } .cesium-viewer-geocoderContainer .cesium-geocoder-input:focus { background-color: rgba(63, 72, 84, 0.9); } .cesium-viewer-geocoderContainer .search-results { background-color: rgba(23, 49, 71, 0.8); } /**cesium info信息框*/ .cesium-infoBox { top: 50px; background-color: rgba(23, 49, 71, 0.8); } .cesium-infoBox-title { background-color: rgba(23, 49, 71, 0.8); } /**cesium 任务栏的FPS信息*/ .cesium-performanceDisplay-defaultContainer { top: auto; bottom: 35px; right: 50px; } .cesium-performanceDisplay-ms, .cesium-performanceDisplay-fps { color: #fff; } /**cesium tileset调试信息面板*/ .cesium-viewer-cesiumInspectorContainer { top: 10px; left: 10px; right: auto; } .cesium-cesiumInspector { background-color: rgba(23, 49, 71, 0.8); } /**覆盖mars3d内部控件的颜色等样式*/ .mars3d-compass .mars3d-compass-outer { fill: rgba(23, 49, 71, 0.8); } .mars3d-contextmenu-ul, .mars3d-sub-menu { background-color: rgba(23, 49, 71, 0.8); > li > a:hover, > li > a:focus, > li > .active { background-color: #3ea6ff; } > .active > a, > .active > a:hover, > .active > a:focus { background-color: #3ea6ff; } } /* Popup样式*/ .mars3d-popup-color { color: #ffffff; } .mars3d-popup-background { background: rgba(23, 49, 71, 0.8); } .mars3d-popup-content { margin: 15px; } .mars3d-template-content label { padding-right: 6px; } .mars3d-template-titile { border-bottom: 1px solid #3ea6ff; } .mars3d-template-titile a { font-size: 16px; } .mars3d-tooltip { background: rgba(23, 49, 71, 0.8); border: 1px solid rgba(23, 49, 71, 0.8); } .mars3d-popup-btn-custom { padding: 3px 10px; border: 1px solid #209ffd; background: #209ffd1c; } .mars-dialog .mars-dialog__content { height: 100%; width: 100%; overflow: auto; padding: 5px; } .image { border: solid 2px #fff; } .content { height: 90%; padding-top: 10px; overflow-x: auto; overflow-y: auto; } .content-text { padding: 0 10px; text-indent: 30px; font-size: 17px; } .details-video { width: 100%; height: 760px; background-color: #000; } :where(.css-lt97qq9).ant-space { display: inline-flex; } :where(.css-lt97qq9).ant-space-align-center { align-items: center; } :where(.css-lt97qq9).ant-image .ant-image-mask { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #fff; background: rgba(0, 0, 0, 0.5); cursor: pointer; opacity: 0; transition: opacity 0.3s; } :where(.css-lt97qq9).ant-image .ant-image-mask .ant-image-mask-info { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; padding: 0 4px; } :where(.css-1t97qq9)[class^="ant-image"] [class^="ant-image"], :where(.css-1t97qq9)[class*=" ant-image"] [class^="ant-image"], :where(.css-1t97qq9)[class^="ant-image"] [class*=" ant-image"], :where(.css-1t97qq9)[class*=" ant-image"] [class*=" ant-image"] { box-sizing: border-box; } :where(.css-lt97qq9).ant-image .ant-image-img { width: 100%; height: auto; vertical-align: middle; } </style> <style scoped> .mini-map-container { position: relative; width: 100%; height: 100%; } .main-viewer { width: 100%; height: 100%; } .mini-map { position: absolute; right: 3vw; bottom: 6vh; width: 12vw; height: 17vh; border: 2px solid #fff; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); z-index: 999; cursor: pointer; overflow: hidden; } .location-indicator { position: absolute; width: 19px; height: 19px; transform: translate(-50%, -50%); z-index: 100; /* border: 2px solid red; border-radius: 2px; */ } /* 原十字准星伪元素保持不变 */ .location-indicator::before, .location-indicator::after { content: ""; position: absolute; background-color: red; top: 50%; left: 50%; transform: translate(-50%, -50%); } .location-indicator::before { width: 14px; height: 2px; } .location-indicator::after { width: 2px; height: 14px; } .view-rectangle { position: absolute; border: 2px solid #00f2fe; z-index: 99; pointer-events: none; box-shadow: 0 0 20px rgba(0, 242, 254, 0.7); } /* 相机聚焦样式 - 四个角折角 */ .corner { position: absolute; width: 25px; height: 25px; z-index: 100; pointer-events: none; } .corner::before, .corner::after { content: ""; position: absolute; background: #00f2fe; box-shadow: 0 0 10px rgba(0, 242, 254, 0.8); } .corner-top-left { top: -2px; left: -2px; } .corner-top-left::before { top: 0; left: 0; width: 15px; height: 3px; } .corner-top-left::after { top: 0; left: 0; width: 3px; height: 15px; } .corner-top-right { top: -2px; right: -2px; } .corner-top-right::before { top: 0; right: 0; width: 15px; height: 3px; } .corner-top-right::after { top: 0; right: 0; width: 3px; height: 15px; } .corner-bottom-left { bottom: -2px; left: -2px; } .corner-bottom-left::before { bottom: 0; left: 0; width: 15px; height: 3px; } .corner-bottom-left::after { bottom: 0; left: 0; width: 3px; height: 15px; } .corner-bottom-right { bottom: -2px; right: -2px; } .corner-bottom-right::before { bottom: 0; right: 0; width: 15px; height: 3px; } .corner-bottom-right::after { bottom: 0; right: 0; width: 3px; height: 15px; } .camera-icon { position: absolute; top: 10px; right: 10px; color: #00f2fe; font-size: 24px; z-index: 100; text-shadow: 0 0 10px rgba(0, 242, 254, 0.8); } .focus-effect { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 98; border: 2px solid rgba(0, 242, 254, 0.2); border-radius: 5px; box-shadow: inset 0 0 30px rgba(0, 242, 254, 0.1); } .pulse { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 10px; height: 10px; border-radius: 50%; background: rgba(0, 242, 254, 0.7); box-shadow: 0 0 0 0 rgba(0, 242, 254, 0.7); animation: pulse 2s infinite; } ::v-deep.cesium-viewer-toolbar { display: none; } </style> 检查代码,为什么这个鹰眼地图不加载了

filetype

<template>
<Search @location-selected="handleLocationSelected" /> <LocationBar v-if="loaded" :update-interval="100" :use-dms-format="useDmsFormat" /> </template> <style> /* @import "/temp/css/divGraphic.css"; */ </style> <script setup lang="ts"> import { computed, onUnmounted, onMounted, reactive } from "vue"; import LocationBar from "./location-bar.vue"; import Search from "./search.vue"; import initMap from "./init"; import { ref } from "vue"; import { throttle } from 'lodash'; import { loadRipplePoints, createMultipleRippleCircles } from './circle.js'; import { $prototype } from "../../main.ts"; const miniMapContainer = ref<HTMLElement>(); let viewIndicator: Rectangle; // 视图指示器样式 const currentPosition = reactive({ longitude: 113.361538, latitude: 27.339318, }); // 株洲市范围常量 const ZHUZHOU_EXTENT = { west: 112.5, // 株洲市西边界 east: 114.5, // 株洲市东边界 south: 26.0, // 株洲市南边界 north: 28.0 // 株洲市北边界 }; const rippleEntities = ref<any[]>([]); // 存储波纹圆实体 const heightThreshold = 80000; // 高度阈值(米),高于此值隐藏波纹圆 // 修复1:重新定义 indicatorStyle 为 ref const indicatorStyle = ref({ left: '50%', top: '50%', display: "block" }); const updateRippleVisibility = throttle(() => { if (!$prototype.$map || rippleEntities.value.length === 0) return; // 修复1:将 shouldShow 提取到块级作用域外部 let shouldShow = false; // 获取相机高度 const cartographic = $prototype.$map.camera.positionCartographic; // 修复2:添加空值检查 if (cartographic) { const cameraHeight = cartographic.height; shouldShow = cameraHeight > heightThreshold; } // 修复3:确保 shouldShow 已定义 rippleEntities.value.forEach(entity => { entity.show = shouldShow; }); }, 200); // 更新指示器位置 const updateIndicatorPosition = () => { if (!$prototype.$map) return; const camera = $prototype.$map.camera; const rect = camera.computeViewRectangle(); if (!rect) return; // 计算在株洲市范围内的相对位置 const center = Cesium.Rectangle.center(rect); const lon = Cesium.Math.toDegrees(center.longitude); const lat = Cesium.Math.toDegrees(center.latitude); // 确保位置在株洲市范围内 const constrainedLon = Math.max(ZHUZHOU_EXTENT.west, Math.min(ZHUZHOU_EXTENT.east, lon)); const constrainedLat = Math.max(ZHUZHOU_EXTENT.south, Math.min(ZHUZHOU_EXTENT.north, lat)); const lonPercent = ((constrainedLon - ZHUZHOU_EXTENT.west) / (ZHUZHOU_EXTENT.east - ZHUZHOU_EXTENT.west)) * 100; const latPercent = 100 - ((constrainedLat - ZHUZHOU_EXTENT.south) / (ZHUZHOU_EXTENT.north - ZHUZHOU_EXTENT.south)) * 100; // 更新CSS指示器 indicatorStyle.value = { left: `${lonPercent}%`, top: `${latPercent}%`, display: "block" }; }; // 更新鹰眼地图 - 只更新指示器位置 const updateOverview = () => { if (!$prototype.$map || !overviewViewer.value) return; // 获取主地图的当前视图中心 const rectangle = $prototype.$map.camera.computeViewRectangle(); if (!rectangle) return; // 更新指示器位置 updateIndicatorPosition(); }; const overviewViewer = ref(null); // 初始化鹰眼地图 - 使用全球底图+株洲市叠加层 const initMiniMap = () => { Cesium.Ion.defaultAccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIxMDhlNDdmYy03NzFhLTQ1ZTQtOWQ3NS1lZDAzNDc3YjE4NDYiLCJpZCI6MzAxNzQyLCJpYXQiOjE3NDcwNTMyMDN9.eaez8rQxVbPv2LKEU0sMDclPWyHKhh1tR27Vg-_rQSM"; if (!miniMapContainer.value) return; // 创建全球底图提供器 const worldProvider = new Cesium.UrlTemplateImageryProvider({ url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", minimumLevel: 0, maximumLevel: 19, tilingScheme: new Cesium.WebMercatorTilingScheme(), }); // 创建株洲市专用影像提供器 const zhuzhouProvider = new Cesium.UrlTemplateImageryProvider({ url: "http://124.232.190.30:9000/proxy/pk1725866655224/map/zzzsyx_18/{z}/{x}/{y}.png", minimumLevel: 1, maximumLevel: 17, tilingScheme: new Cesium.WebMercatorTilingScheme(), }); // 鹰眼地图初始化 - 使用全球底图 overviewViewer.value = new Cesium.Viewer(miniMapContainer.value, { sceneMode: Cesium.SceneMode.SCENE2D, baseLayerPicker: false, homeButton: false, timeline: false, navigationHelpButton: false, animation: false, scene3DOnly: true, selectionIndicator: false, infoBox: false, imageryProvider: worldProvider, // 使用全球底图 terrainProvider: undefined, mapProjection: new Cesium.WebMercatorProjection(), skyBox: false, skyAtmosphere: false }); // 添加株洲市影像作为叠加层 const zhuzhouLayer = overviewViewer.value.imageryLayers.addImageryProvider(zhuzhouProvider); // 设置固定视野为株洲市范围 overviewViewer.value.camera.setView({ destination: Cesium.Rectangle.fromDegrees( ZHUZHOU_EXTENT.west, ZHUZHOU_EXTENT.south, ZHUZHOU_EXTENT.east, ZHUZHOU_EXTENT.north ) }); // 隐藏控件 const toolbar = overviewViewer.value.container.getElementsByClassName("cesium-viewer-toolbar")[0]; if (toolbar) toolbar.style.display = "none"; // 隐藏版权信息 overviewViewer.value.cesiumWidget.creditContainer.style.display = "none"; // 初始化视图指示器 initRectangle(); }; // 初始化视图指示器 function initRectangle() { // 创建视图指示器(株洲市范围框) viewIndicator = overviewViewer.value.entities.add({ rectangle: { coordinates: Cesium.Rectangle.fromDegrees( ZHUZHOU_EXTENT.west, ZHUZHOU_EXTENT.south, ZHUZHOU_EXTENT.east, ZHUZHOU_EXTENT.north ), material: Cesium.Color.RED.withAlpha(0.3), outline: true, outlineColor: Cesium.Color.RED, outlineWidth: 2, }, }); } const loaded = ref(false); const useDmsFormat = ref(false); function addDemoGraphics() { const chinaBoundary = $prototype.$map.dataSources.add( Cesium.GeoJsonDataSource.load("/shp_zz.geojson", { stroke: Cesium.Color.WHITE, fill: false, clampToGround: true, describe: null, }) ); chinaBoundary.then((dataSource) => { const entities = dataSource.entities.values; for (let entity of entities) { if (entity.polyline) { entity.polyline.fill = false; } } }); } function flyToDes() { const center = Cesium.Cartesian3.fromDegrees(-98.0, 40.0); $prototype.$map.camera.flyTo({ destination: new Cesium.Cartesian3( -2432812.6687511606, 5559483.804371395, 2832009.419525571 ), orientation: { heading: 6.283185307179421, pitch: -1.0472145569408116, roll: 6.2831853071795205, }, complete: function () {}, }); } // 监听主地图相机变化 const setupCameraListener = () => { $prototype.$map.camera.changed.addEventListener(updateOverview); }; // 鹰眼地图点击处理 const handleMiniMapClick = (event: MouseEvent) => { if (!miniMapContainer.value || !overviewViewer.value) return; const rect = miniMapContainer.value.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; // 计算在固定范围内的相对位置 const xPercent = (x / rect.width) * 100; const yPercent = (y / rect.height) * 100; // 转换为株洲市范围内的经纬度 const lon = ZHUZHOU_EXTENT.west + (xPercent / 100) * (ZHUZHOU_EXTENT.east - ZHUZHOU_EXTENT.west); const lat = ZHUZHOU_EXTENT.north - (yPercent / 100) * (ZHUZHOU_EXTENT.north - ZHUZHOU_EXTENT.south); // 主地图飞向点击位置 $prototype.$map.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(lon, lat, 1000000), }); }; function addImage() { var rightImageProvider = new Cesium.UrlTemplateImageryProvider({ name: "影像图", type: "xyz", layer: "vec_d", url: "http://124.232.190.30:9000/proxy/pk1725866655224/map/zzzsyx_18/{z}/{x}/{y}.png", minimumLevel: 1, maximumLevel: 17, crs: "EPSG:3857", }); $prototype.$map.imageryLayers.addImageryProvider(rightImageProvider); rightImageProvider.splitDirection = Cesium.SplitDirection.right; } onMounted(() => { initMap(); addImage(); loaded.value = true; addDemoGraphics(); flyToDes(); // 修复6:确保正确的初始化顺序 setTimeout(() => { initMiniMap(); setupCameraListener(); // 初始更新 updateOverview(); }, 1000); (async () => { try { const ripplePoints = await loadRipplePoints(); rippleEntities.value = createMultipleRippleCircles( $prototype.$map, ripplePoints ); $prototype.$map.camera.changed.addEventListener(updateRippleVisibility); updateRippleVisibility(); } catch (error) { console.error('加载波纹圆失败:', error); } })(); }); let currentMarker: any = null; const createMarker = (location: { lng: number; lat: number; name: string }) => { if (currentMarker) { $prototype.$map.entities.remove(currentMarker); } currentMarker = $prototype.$map.entities.add({ position: Cesium.Cartesian3.fromDegrees( location.lng, location.lat, 50 ), point: { pixelSize: 200, color: Cesium.Color.RED, outlineColor: Cesium.Color.WHITE, outlineWidth: 2, heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND, }, label: { text: location.name, font: "40px sans-serif", fillColor: Cesium.Color.WHITE, outlineColor: Cesium.Color.BLACK, outlineWidth: 1, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, pixelOffset: new Cesium.Cartesian2(0, -20), heightReference: Cesium.HeightReference.RELATIVE_TO_GROUND, distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 10000), }, description: `

${location.name}

经度:${location.lng.toFixed(6)}

纬度:${location.lat.toFixed(6)}

`, }); return currentMarker; }; const handleLocationSelected = (location: { lng: number; lat: number; name: string; }) => { if (!$prototype.$map) return; const destination = Cesium.Cartesian3.fromDegrees( location.lng, location.lat, 200 ); $prototype.$map.camera.flyTo({ destination, orientation: { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(-45), roll: 0, }, duration: 2, complete: () => { createMarker(location); }, }); }; onUnmounted(() => { if ($prototype.$map) { $prototype.$map.destroy(); $prototype.$map = null; } if ($prototype.$map) { $prototype.$map.camera.changed.removeEventListener(updateRippleVisibility); } console.log("组件销毁"); }); const emit = defineEmits(["onload", "onclick"]); const initMars3d = async (option: any) => { emit("onclick", true); emit("onload", $prototype.$map); }; </script> <style lang="less"> /**cesium 工具按钮栏*/ .cesium-viewer-toolbar { top: auto !important; bottom: 35px !important; left: 12px !important; right: auto !important; } .cesium-toolbar-button img { height: 100%; } .cesium-viewer-toolbar > .cesium-toolbar-button, .cesium-navigationHelpButton-wrapper, .cesium-viewer-geocoderContainer { margin-bottom: 5px; float: left; clear: both; text-align: center; } .cesium-button { background-color: rgba(23, 49, 71, 0.8); color: #e6e6e6; fill: #e6e6e6; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); line-height: 32px; } .cesium-button:hover { background: #3ea6ff; } /**cesium 底图切换面板*/ .cesium-baseLayerPicker-dropDown { bottom: 0; left: 40px; max-height: 700px; margin-bottom: 5px; background-color: rgba(23, 49, 71, 0.8); } /**cesium 帮助面板*/ .cesium-navigation-help { top: auto; bottom: 0; left: 40px; transform-origin: left bottom; background: none; background-color: rgba(23, 49, 71, 0.8); .cesium-navigation-help-instructions { background: none; } .cesium-navigation-button { background: none; } .cesium-navigation-button-selected, .cesium-navigation-button-unselected:hover { background: rgba(0, 138, 255, 0.2); } } /**cesium 二维三维切换*/ .cesium-sceneModePicker-wrapper { width: auto; } .cesium-sceneModePicker-wrapper .cesium-sceneModePicker-dropDown-icon { float: right; margin: 0 3px; } /**cesium POI查询输入框*/ .cesium-viewer-geocoderContainer .search-results { left: 0; right: 40px; width: auto; z-index: 9999; } .cesium-geocoder-searchButton { background-color: rgba(23, 49, 71, 0.8); } .cesium-viewer-geocoderContainer .cesium-geocoder-input { background-color: rgba(63, 72, 84, 0.7); } .cesium-viewer-geocoderContainer .cesium-geocoder-input:focus { background-color: rgba(63, 72, 84, 0.9); } .cesium-viewer-geocoderContainer .search-results { background-color: rgba(23, 49, 71, 0.8); } /**cesium info信息框*/ .cesium-infoBox { top: 50px; background-color: rgba(23, 49, 71, 0.8); } .cesium-infoBox-title { background-color: rgba(23, 49, 71, 0.8); } /**cesium 任务栏的FPS信息*/ .cesium-performanceDisplay-defaultContainer { top: auto; bottom: 35px; right: 50px; } .cesium-performanceDisplay-ms, .cesium-performanceDisplay-fps { color: #fff; } /**cesium tileset调试信息面板*/ .cesium-viewer-cesiumInspectorContainer { top: 10px; left: 10px; right: auto; } .cesium-cesiumInspector { background-color: rgba(23, 49, 71, 0.8); } /**覆盖mars3d内部控件的颜色等样式*/ .mars3d-compass .mars3d-compass-outer { fill: rgba(23, 49, 71, 0.8); } .mars3d-contextmenu-ul, .mars3d-sub-menu { background-color: rgba(23, 49, 71, 0.8); > li > a:hover, > li > a:focus, > li > .active { background-color: #3ea6ff; } > .active > a, > .active > a:hover, > .active > a:focus { background-color: #3ea6ff; } } /* Popup样式*/ .mars3d-popup-color { color: #ffffff; } .mars3d-popup-background { background: rgba(23, 49, 71, 0.8); } .mars3d-popup-content { margin: 15px; } .mars3d-template-content label { padding-right: 6px; } .mars3d-template-titile { border-bottom: 1px solid #3ea6ff; } .mars3d-template-titile a { font-size: 16px; } .mars3d-tooltip { background: rgba(23, 49, 71, 0.8); border: 1px solid rgba(23, 49, 71, 0.8); } .mars3d-popup-btn-custom { padding: 3px 10px; border: 1px solid #209ffd; background: #209ffd1c; } .mars-dialog .mars-dialog__content { height: 100%; width: 100%; overflow: auto; padding: 5px; } .image { border: solid 2px #fff; } .content { height: 90%; padding-top: 10px; overflow-x: auto; overflow-y: auto; } .content-text { padding: 0 10px; text-indent: 30px; font-size: 17px; } .details-video { width: 100%; height: 760px; background-color: #000; } :where(.css-lt97qq9).ant-space { display: inline-flex; } :where(.css-lt97qq9).ant-space-align-center { align-items: center; } :where(.css-lt97qq9).ant-image .ant-image-mask { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #fff; background: rgba(0, 0, 0, 0.5); cursor: pointer; opacity: 0; transition: opacity 0.3s; } :where(.css-lt97qq9).ant-image .ant-image-mask .ant-image-mask-info { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; padding: 0 4px; } :where(.css-1t97qq9)[class^="ant-image"] [class^="ant-image"], :where(.css-1t97qq9)[class*=" ant-image"] [class^="ant-image"], :where(.css-1t97qq9)[class^="ant-image"] [class*=" ant-image"], :where(.css-1t97qq9)[class*=" ant-image"] [class*=" ant-image"] { box-sizing: border-box; } :where(.css-lt97qq9).ant-image .ant-image-img { width: 100%; height: auto; vertical-align: middle; } </style> <style scoped> .mini-map-container { position: relative; width: 100%; height: 100%; } .main-viewer { width: 100%; height: 100%; } .mini-map { position: absolute; right: 3vw; bottom: 6vh; width: 12vw; height: 17vh; border: 2px solid #fff; box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); z-index: 999; cursor: pointer; overflow: hidden; } .location-indicator { position: absolute; width: 14px; height: 14px; background: #ff3e3e; border: 2px solid white; border-radius: 50%; transform: translate(-50%, -50%); box-shadow: 0 0 15px rgba(255, 62, 62, 1); z-index: 100; } .view-rectangle { position: absolute; border: 2px solid #00f2fe; z-index: 99; pointer-events: none; box-shadow: 0 0 20px rgba(0, 242, 254, 0.7); } /* 相机聚焦样式 - 四个角折角 */ .corner { position: absolute; width: 25px; height: 25px; z-index: 100; pointer-events: none; } .corner::before, .corner::after { content: ""; position: absolute; background: #00f2fe; box-shadow: 0 0 10px rgba(0, 242, 254, 0.8); } .corner-top-left { top: -2px; left: -2px; } .corner-top-left::before { top: 0; left: 0; width: 15px; height: 3px; } .corner-top-left::after { top: 0; left: 0; width: 3px; height: 15px; } .corner-top-right { top: -2px; right: -2px; } .corner-top-right::before { top: 0; right: 0; width: 15px; height: 3px; } .corner-top-right::after { top: 0; right: 0; width: 3px; height: 15px; } .corner-bottom-left { bottom: -2px; left: -2px; } .corner-bottom-left::before { bottom: 0; left: 0; width: 15px; height: 3px; } .corner-bottom-left::after { bottom: 0; left: 0; width: 3px; height: 15px; } .corner-bottom-right { bottom: -2px; right: -2px; } .corner-bottom-right::before { bottom: 0; right: 0; width: 15px; height: 3px; } .corner-bottom-right::after { bottom: 0; right: 0; width: 3px; height: 15px; } .camera-icon { position: absolute; top: 10px; right: 10px; color: #00f2fe; font-size: 24px; z-index: 100; text-shadow: 0 0 10px rgba(0, 242, 254, 0.8); } .focus-effect { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 98; border: 2px solid rgba(0, 242, 254, 0.2); border-radius: 5px; box-shadow: inset 0 0 30px rgba(0, 242, 254, 0.1); } .pulse { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 10px; height: 10px; border-radius: 50%; background: rgba(0, 242, 254, 0.7); box-shadow: 0 0 0 0 rgba(0, 242, 254, 0.7); animation: pulse 2s infinite; } ::v-deep.cesium-viewer-toolbar { display: none; } </style> 如上代码,新新的要求,那个株洲市影像图代码你帮我注释掉,先不用了,改而在鹰眼地图上展示就和主地图一样的那个株洲市白色边界即可,基于此修改一下代码,尽可能减少对vue中其他无关代码的修改

filetype

Error: Please import the top-level fullcalendar lib before attempting to import a plugin.下面是我的代码,并且使用的版本一致<template>
<el-card body-style="padding:0 0 0 0; " class="normal-page">
我的日程 <el-button type="primary" size="small" :icon="Plus" @click="addSchedule"> {{ $t('calendar.newschedule') }} </el-button>
<el-row :gutter="14"> <el-col :span="7"> <calendar-bar ref="calendarBar" @handle-date="handleDate" /> </el-col> <el-col :span="17" class="line" v-loading="loading" element-loading-text="数据正在加载中">
<el-radio-group v-model="time" size="small" @change="selectChange"> <el-radio-button v-for="(itemn, indexn) in fromList" :key="indexn" :label="itemn.val" > {{ itemn.text }} </el-radio-button> </el-radio-group>
<FullCalendar ref="calendar" id="calendar" style="height: calc(100vh - 150px)" :options="calendarOptions"> <template v-slot:slotLabelContent="arg">
</template> <template v-slot:dayHeaderContent="arg">
</template> <template v-slot:eventContent="arg">
{{ arg.event.title }}
<template v-if="arg.event.extendedProps.show > -1"> </template>
</template> </FullCalendar>
</el-col> </el-row> </el-card> <add-todo ref="addTodo" @get-list="getList" :left-time="leftTime"></add-todo> <calendar-details ref="calendarDetails" @delete-fn="getList" @edit-fn="editFn" :date-info="dateInfo" ></calendar-details> <contract-dialog ref="contractDialog" :config="configContract" @is-ok="getList"></contract-dialog> <el-dialog title="添加跟进记录" class="record" v-model="dialogVisible" width="40%"> <record-upload :form-info="formInfo" @change="recordChange"></record-upload> </el-dialog> <edit-examine ref="editExamine" :parameter-data="parameterData" :ids="formInfo.data.id" @schedule-record="scheduleRecord" ></edit-examine>
</template> <script> import FullCalendar from '@fullcalendar/vue3' // 核心库必须第一个导入 import dayGridPlugin from '@fullcalendar/daygrid' import timeGridPlugin from '@fullcalendar/timegrid' import interactionPlugin from '@fullcalendar/interaction' import listPlugin from '@fullcalendar/list' import { defineComponent, defineAsyncComponent, ref, reactive, onMounted, computed, toRefs, nextTick } from 'vue'; import { Plus } from '@element-plus/icons-vue'; //import { ElMessage } from 'element-plus'; import moment from 'moment'; // 正确顺序:先核心库再插件 // 样式导入(必须在插件之后) // import '@fullcalendar/common/main.css' // import '@fullcalendar/daygrid/main.css' // import '@fullcalendar/timegrid/main.css' // import '@fullcalendar/list/main.css' console.log(window.FullCalendar); // 检查全局对象 console.log(window.FullCalendarCore); // 如果你手动挂载了核心库 // 根据实际需要取消注释这些API导入 // import { // clientRemindDetailApi, // scheduleListApi, // scheduleStatusApi // } from '@/api/enterprise'; // import { configRuleApproveApi } from '@/api/config'; // import { toGetWeek, getColor } from '@/utils/format'; export default defineComponent({ name: 'WorkDealt', components: { CalendarBar: defineAsyncComponent(() => import('./components/calendarBar.vue')), AddTodo: defineAsyncComponent(() => import('./components/addTodo')), FullCalendar, CalendarDetails: defineAsyncComponent(() => import('./components/calendarDetails')) // ContractDialog: defineAsyncComponent(() => import('@/views/customer/contract/components/contractDialog')), // RecordUpload: defineAsyncComponent(() => import('@/views/customer/list/components/recordUpload')), // EditExamine: defineAsyncComponent(() => import('@/views/user/examine/components/editExamine')) }, setup() { const state = reactive({ loading: false, calendarApi: null, title: '', time: '1', cid: null, dialogVisible: false, configContract: {}, formInfo: { avatar: '', type: 'add', show: 1, data: {}, follow_id: 0 }, fromList: computed(() => [ {text: '日', val: '1'}, {text: '周', val: '2'}, {text: '月', val: '3'} ]), start_time: '', end_time: '', dateInfo: {}, type: [], period: 1, calendarOptions: { height: 'calc(100vh - 232px)', eventColor: '', initialDate: moment().format('YYYY-MM-DD HH:mm:ss'), plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin, listPlugin], handleWindowResize: true, displayEventTime: false, slotDuration: '00:60:00', scrollTime: '05:30:00', slotMinTime: '00:00:00', slotMaxTime: '24:00:00', headerToolbar: { left: '', center: 'prev title next', right: '' }, buttonText: { month: '月', week: '周', day: '天' }, allDaySlot: true, dayMaxEventRows: 6, allDayText: '', moreLinkContent: state => `还有${state.num}个日程`, weekends: true, nowIndicator: true, weekNumbers: false, slotLabelFormat: { hour: '2-digit', minute: '2-digit', meridiem: false, hour12: false }, selectable: true, displayEventEnd: false, initialView: 'timeGridDay', dateClick: (e) => handleDateClick(e), customButtons: { next: { text: 'PREV', click: () => next() }, prev: { text: 'PREV', click: () => prev() } }, events: [], slotEventOverlap: false, eventOverlap: false, locale: 'zh-cn', weekNumberCalculation: 'ISO' }, detailedData: {}, parameterData: { contract_id: '', customer_id: '', invoice_id: '', bill_id: '' }, leftTime: '', buildData: [], item: {}, status: 0 }) const calendarBar = ref(null); const addTodo = ref(null); const calendarDetails = ref(null); const contractDialog = ref(null); const editExamine = ref(null); const userId = ref(null); // 日期更新函数 const updateCalendarDates = () => { if (!state.calendarApi) return; state.title = moment(state.calendarApi.view.currentStart).format('YYYY-MM-DD'); state.start_time = state.title + ' 00:00:00'; state.end_time = state.title + ' 23:59:59'; }; onMounted(() => { const userInfo = JSON.parse(localStorage.getItem('userInfo')); if (userInfo) { userId.value = userInfo.userId; state.formInfo.avatar = userInfo.avatar; } // 使用 nextTick 确保在 DOM 更新后执行 nextTick(() => { if (calendarBar.value && calendarBar.value.getApi) { state.calendarApi = calendarBar.value.getApi(); dayRender(); updateCalendarDates(); } }); }); // 原始方法开始(保持所有方法逻辑不变) // const handleEventClick = e => { calendarDetails.value.open(e.event.extendedProps); } const recordChange = () => { state.dialogVisible = false; getList(); } const getConfigApprove = async () => { // 实际项目中取消注释 // const result = await configRuleApproveApi(0); // state.buildData = result.data; } const putStatus = async (text, status, item) => { const {extendedProps} = item; state.item = extendedProps; const bill_id = extendedProps.bill_id; switch (extendedProps.cid_value) { case 3: case 4: const id = bill_id.bill_id !== 0 ? bill_id.bill_id : bill_id.remind_id; if (status == 3) { // 实际项目中取消注释 // clientRemindDetailApi(id).then((res) => { // res.data.id = bill_id.remind_id; // state.parameterData.customer_id = res.data.eid; // state.parameterData.contract_id = res.data.cid; // // const switchType = res.data.types === 0 ? 'contract_refund_switch' : 'contract_renew_switch'; // const data = {id: state.buildData[switchType]}; // editExamine.value.open(data, res.data.cid, switchType, item, status); // }) } break; case 2: if (extendedProps.finish === 3) return false; state.formInfo.follow_id = bill_id ? bill_id.follow_id : 0; state.formInfo.data.eid = extendedProps.link_id; state.dialogVisible = true; break; default: scheduleRecord(extendedProps, status); } } const scheduleRecord = async (data = state.item, status = state.status) => { const {start_time: start, end_time: end, itemId: id} = data; const info = {status, start, end}; // 实际项目中取消注释 // await scheduleStatusApi(id, info); await getList(); } const handleDateClick = e => { state.detailedData = { startDate: moment(e.date).format('YYYY-MM-DD'), startTime: moment(e.date).format('HH:mm:ss'), endDate: moment(e.date).format('YYYY-MM-DD'), time: moment(e.date).format('YYYY-MM-DD HH:mm:ss') } addTodo.value.open(state.detailedData); } const dayRender = () => { document.documentElement.style.setProperty(`--fc-today-bg-color`, '#fff'); let dayTime = document.querySelectorAll('.fc-timegrid-slot-label-cushion'); dayTime[0]?.classList.add('fcTime'); document.querySelectorAll('.fc-button-primary').forEach(li => li.setAttribute('title', '')); } const day = () => { state.period = 1; state.calendarApi.changeView('timeGridDay'); getList(); removeTitle(); dayRender(); } const month = () => { state.period = 3; state.calendarApi.changeView('dayGridMonth'); getList(); removeTitle(); } const week = () => { state.period = 2; state.calendarApi.changeView('timeGridWeek'); getList(); removeTitle(); dayRender(); } const prev = () => { state.calendarApi.prev(); updateCalendarDates(); getList(); if (state.period === 2) { const date = calendarBar.value.value; calendarBar.value.value = moment(date).subtract(7, 'd').format('YYYY-MM-DD'); } else { calendarBar.value.value = moment(state.calendarApi.view.currentStart).format('YYYY-MM-DD'); } } const next = () => { state.calendarApi.next(); updateCalendarDates(); getList(); if (state.period == 2) { let date = calendarBar.value.value; calendarBar.value.value = moment(date).add(7, 'd').format('YYYY-MM-DD'); } else { calendarBar.value.value = moment(state.calendarApi.view.currentStart).format('YYYY-MM-DD'); } } const getList = () => { // 实际项目中取消注释 // scheduleListApi().then(res => { // let newArr = [] // res.data.forEach(item => { // // ...处理逻辑... // }) // state.calendarOptions.events = newArr // }) calendarBar.value?.getList(); } const findItem = (arr, key, val) => { for (var i = 0; i < arr.length; i++) { if (arr[i].id === val || arr[i].id == userId.value) { return 2; } } return -1; } // const getWeek = date => toGetWeek(date); const editFn = (id, type, date) => { let data = { id, type, edit: true, date } addTodo.value.openBox(data); } const handleDate = (data, val) => { state.cid = data.type; state.leftTime = data.time; if (val) { state.calendarApi.gotoDate(data.time); } setTimeout(() => { state.calendarApi = calendarBar.value.getApi(); state.time = 1; day(); getList(); }, 300); } const selectChange = e => { state.time = e; if (state.time == 1) { day(); } else if (state.time == 2) { week(); } else { month(); } } const addSchedule = () => addTodo.value.open(); // 临时 getColorFn 实现 - 实际项目中应该使用真实实现 const getColorFn = (thisColor, thisOpacity) => { // 如果是透明背景 if (state.period === 3) { // RGBA 格式 const r = parseInt(thisColor.slice(1, 3), 16); const g = parseInt(thisColor.slice(3, 5), 16); const b = parseInt(thisColor.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${thisOpacity})`; } return thisColor; } const removeTitle = () => { setTimeout(() => { document.querySelectorAll('.fc-daygrid-day-bottom a').forEach(li => li.title = ''); }, 300); } return { ...toRefs(state), Plus, calendarBar, addTodo, calendarDetails, contractDialog, editExamine, handleEventClick, recordChange, putStatus, scheduleRecord, handleDateClick, dayRender, day, month, week, prev, next, getList, findItem, // getWeek, editFn, handleDate, selectChange, addSchedule, getColorFn, removeTitle } } }) </script> <style lang="scss" scoped> .divBox { :deep(.fc-header-toolbar) { position: absolute; top: 0; left: 0; } .header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #dcdfe6; .title { font-weight: 500; font-size: 18px; color: #303133; } } .day-header { display: flex; flex-direction: column; justify-content: center; height: 52px; font-family: PingFang SC-Regular, PingFang SC; line-height: 20px; .week { font-size: 13px; color: #909399; } .date { font-size: 18px; color: #606266; font-weight: 800; } } .ml30 { margin-left: -30px; } :deep(.fc-timegrid-slot-label-cushion) { color: #909399; font-size: 12px; } .line { border-left: 1px solid #f0f2f5; } .item { width: 100%; display: flex; font-size: 14px; font-family: PingFang SC-Regular, PingFang SC; justify-content: space-between; align-items: center; padding-right: 4px; border-radius: 13px; .img { flex-shrink: 0; width: 18px; height: 18px; border-radius: 50%; margin-right: 5px; object-fit: cover; } .yuan { width: 12px; height: 12px; border-radius: 50px; border: 1px solid #fff; } } .over-title { width: 98% !important; overflow: hidden !important; white-space: nowrap; text-overflow: ellipsis !important; } } </style>

filetype

对代码进行标准化以及压缩。减少不必要的计算及冗余,压缩内存及代码量,前提是不影响程序稳定以及精度:import os import sys import re import json import gc import time import tempfile import concurrent.futures import difflib import threading import traceback import numpy as np import librosa import torch import psutil import requests import hashlib import shutil from typing import List, Dict, Tuple, Optional, Set from threading import Lock, Semaphore, RLock from datetime import datetime from pydub import AudioSegment from pydub.silence import split_on_silence from modelscope.pipelines import pipeline from modelscope.utils.constant import Tasks from transformers import AutoModelForSequenceClassification, AutoTokenizer from torch.utils.data import TensorDataset, DataLoader from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, QTextEdit, QFileDialog, QProgressBar, QGroupBox, QMessageBox, QListWidget, QSplitter, QTabWidget, QTableWidget, QTableWidgetItem, QHeaderView, QAction, QMenu, QToolBar, QCheckBox, QComboBox, QSpinBox, QDialog, QDialogButtonBox, QStatusBar) from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer, QSize from PyQt5.QtGui import QFont, QTextCursor, QColor, QIcon # ====================== 资源监控器 ====================== class ResourceMonitor: """统一资源监控器(增强版)""" def __init__(self): self.gpu_available = torch.cuda.is_available() def memory_percent(self) -> Dict[str, float]: """获取内存使用百分比,同时返回CPU和GPU信息""" try: result = { "cpu": psutil.virtual_memory().percent } if self.gpu_available: allocated = torch.cuda.memory_allocated() / (1024 ** 3) total = torch.cuda.get_device_properties(0).total_memory / (1024 ** 3) result["gpu"] = (allocated / total) * 100 if total > 0 else 0 return result except Exception as e: print(f"获取内存使用百分比失败: {str(e)}") return {"cpu": 0, "gpu": 0} # ====================== 方言配置中心(优化版) ====================== class DialectConfig: """集中管理方言配置,便于维护和扩展(带缓存)""" # 标准关键词 STANDARD_KEYWORDS = { "opening": ["您好", "很高兴为您服务", "请问有什么可以帮您"], "closing": ["感谢来电", "祝您生活愉快", "再见"], "forbidden": ["不知道", "没办法", "你投诉吧", "随便你"] } # 贵州方言关键词 GUIZHOU_KEYWORDS = { "opening": ["麻烦您喽", "请问搞哪样", "有咋个可以帮您", "多谢喽"], "closing": ["搞归一喽", "麻烦您喽", "再见喽", "慢走喽"], "forbidden": ["搞不成", "没得法", "随便你喽", "你投诉吧喽"] } # 方言到标准表达的映射(扩展更多贵州方言) DIALECT_MAPPING = { "恼火得很": "非常生气", "鬼火戳": "很愤怒", "搞不成": "无法完成", "没得": "没有", "搞哪样嘛": "做什么呢", "归一喽": "完成了", "咋个": "怎么", "克哪点": "去哪里", "麻烦您喽": "麻烦您了", "多谢喽": "多谢了", "憨包": "傻瓜", "归一": "结束", "板扎": "很好", "鬼火冒": "非常生气", "背时": "倒霉", "吃豁皮": "占便宜" } # 类属性缓存 _combined_keywords = None _compiled_opening = None _compiled_closing = None _hotwords = None _dialect_trie = None # 使用Trie树替换正则表达式 class TrieNode: """Trie树节点类""" def __init__(self): self.children = {} self.is_end = False self.value = "" @classmethod def _build_dialect_trie(cls): """构建方言Trie树""" root = cls.TrieNode() # 按长度降序添加关键词 for dialect, standard in sorted(cls.DIALECT_MAPPING.items(), key=lambda x: len(x[0]), reverse=True): node = root for char in dialect: if char not in node.children: node.children[char] = cls.TrieNode() node = node.children[char] node.is_end = True node.value = standard return root @classmethod def get_combined_keywords(cls) -> Dict[str, List[str]]: """获取合并后的关键词集(带缓存)""" if cls._combined_keywords is None: cls._combined_keywords = { "opening": cls.STANDARD_KEYWORDS["opening"] + cls.GUIZHOU_KEYWORDS["opening"], "closing": cls.STANDARD_KEYWORDS["closing"] + cls.GUIZHOU_KEYWORDS["closing"], "forbidden": cls.STANDARD_KEYWORDS["forbidden"] + cls.GUIZHOU_KEYWORDS["forbidden"] } return cls._combined_keywords @classmethod def get_compiled_opening(cls) -> List[re.Pattern]: """获取预编译的开场关键词正则表达式(带缓存)""" if cls._compiled_opening is None: keywords = cls.get_combined_keywords()["opening"] cls._compiled_opening = [re.compile(re.escape(kw)) for kw in keywords] return cls._compiled_opening @classmethod def get_compiled_closing(cls) -> List[re.Pattern]: """获取预编译的结束关键词正则表达式(带缓存)""" if cls._compiled_closing is None: keywords = cls.get_combined_keywords()["closing"] cls._compiled_closing = [re.compile(re.escape(kw)) for kw in keywords] return cls._compiled_closing @classmethod def get_asr_hotwords(cls) -> List[str]: """获取ASR热词列表(带缓存)""" if cls._hotwords is None: combined = cls.get_combined_keywords() cls._hotwords = sorted(set( combined["opening"] + combined["closing"] )) return cls._hotwords @classmethod def preprocess_text(cls, texts: List[str]) -> List[str]: """将方言文本转换为标准表达(使用Trie树优化)""" if cls._dialect_trie is None: cls._dialect_trie = cls._build_dialect_trie() processed_texts = [] for text in texts: # 使用Trie树进行高效替换 processed = [] i = 0 n = len(text) while i < n: node = cls._dialect_trie j = i found = False # 查找最长匹配 while j < n and text[j] in node.children: node = node.children[text[j]] j += 1 if node.is_end: processed.append(node.value) i = j found = True break if not found: processed.append(text[i]) i += 1 processed_texts.append(''.join(processed)) return processed_texts # ====================== 系统配置管理器 ====================== class ConfigManager: """管理应用程序配置""" _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._init_config() return cls._instance def _init_config(self): """初始化默认配置""" self.config = { "model_paths": { "asr": "./models/iic-speech_paraformer-large-vad-punc-spk_asr_nat-zh-cn", "sentiment": "./models/IDEA-CCNL-Erlangshen-Roberta-110M-Sentiment" }, "sample_rate": 16000, "silence_thresh": -40, "min_silence_len": 1000, "max_concurrent": 1, "dialect_config": "guizhou", "max_audio_duration": 3600 # 最大音频时长(秒) } self.load_config() def load_config(self): """从文件加载配置""" try: if os.path.exists("config.json"): with open("config.json", "r") as f: self.config.update(json.load(f)) except: pass def save_config(self): """保存配置到文件""" try: with open("config.json", "w") as f: json.dump(self.config, f, indent=2) except: pass def get(self, key: str, default=None): """获取配置值""" return self.config.get(key, default) def set(self, key: str, value): """设置配置值""" self.config[key] = value self.save_config() # ====================== 音频处理工具(优化版) ====================== class AudioProcessor: """处理音频转换和特征提取(避免重复加载)""" SUPPORTED_FORMATS = ('.mp3', '.wav', '.amr', '.m4a') @staticmethod def convert_to_wav(input_path: str, temp_dir: str) -> Optional[List[str]]: """将音频转换为WAV格式(在静音处分割)""" try: os.makedirs(temp_dir, exist_ok=True) # 检查文件格式 if not any(input_path.lower().endswith(ext) for ext in AudioProcessor.SUPPORTED_FORMATS): raise ValueError(f"不支持的音频格式: {os.path.splitext(input_path)[1]}") if input_path.lower().endswith('.wav'): return [input_path] # 已经是WAV格式 # 检查ffmpeg是否可用 try: AudioSegment.converter = "ffmpeg" # 显式指定ffmpeg audio = AudioSegment.from_file(input_path) except FileNotFoundError: print("错误: 未找到ffmpeg,请安装并添加到环境变量") return None # 检查音频时长是否超过限制 max_duration = ConfigManager().get("max_audio_duration", 3600) * 1000 # 毫秒 if len(audio) > max_duration: return AudioProcessor._split_long_audio(audio, input_path, temp_dir) else: return AudioProcessor._convert_single_audio(audio, input_path, temp_dir) except Exception as e: print(f"格式转换失败: {str(e)}") return None @staticmethod def _split_long_audio(audio: AudioSegment, input_path: str, temp_dir: str) -> List[str]: """分割长音频文件""" wav_paths = [] # 在静音处分割音频 chunks = split_on_silence( audio, min_silence_len=ConfigManager().get("min_silence_len", 1000), silence_thresh=ConfigManager().get("silence_thresh", -40), keep_silence=500 ) # 合并小片段,避免分段过多 merged_chunks = [] current_chunk = AudioSegment.empty() for chunk in chunks: if len(current_chunk) + len(chunk) < 5 * 60 * 1000: # 5分钟 current_chunk += chunk else: if len(current_chunk) > 0: merged_chunks.append(current_chunk) current_chunk = chunk if len(current_chunk) > 0: merged_chunks.append(current_chunk) # 导出分段音频 sample_rate = ConfigManager().get("sample_rate", 16000) for i, chunk in enumerate(merged_chunks): chunk = chunk.set_frame_rate(sample_rate).set_channels(1) chunk_path = os.path.join( temp_dir, f"{os.path.splitext(os.path.basename(input_path))[0]}_part{i + 1}.wav" ) chunk.export(chunk_path, format="wav") wav_paths.append(chunk_path) return wav_paths @staticmethod def _convert_single_audio(audio: AudioSegment, input_path: str, temp_dir: str) -> List[str]: """转换单个短音频文件""" sample_rate = ConfigManager().get("sample_rate", 16000) audio = audio.set_frame_rate(sample_rate).set_channels(1) wav_path = os.path.join(temp_dir, os.path.splitext(os.path.basename(input_path))[0] + ".wav") audio.export(wav_path, format="wav") return [wav_path] @staticmethod def extract_features_from_audio(y: np.ndarray, sr: int) -> Dict[str, float]: """从音频数据中提取特征(流式处理优化)""" try: duration = librosa.get_duration(y=y, sr=sr) segment_length = 60 # 60秒分段 total_segments = max(1, int(np.ceil(duration / segment_length))) syllable_rates = [] volume_stabilities = [] total_samples = len(y) samples_per_segment = int(segment_length * sr) # 流式处理每个分段 for i in range(total_segments): start = i * samples_per_segment end = min((i + 1) * samples_per_segment, total_samples) y_segment = y[start:end] if len(y_segment) == 0: continue # 语速计算(使用VAD检测语音段) intervals = librosa.effects.split(y_segment, top_db=20) speech_samples = sum(end - start for start, end in intervals) speech_duration = speech_samples / sr if speech_duration > 0.1: syllable_rate = len(intervals) / speech_duration else: syllable_rate = 0 syllable_rates.append(syllable_rate) # 音量稳定性(使用RMS能量) rms = librosa.feature.rms(y=y_segment, frame_length=2048, hop_length=512)[0] if len(rms) > 0 and np.mean(rms) > 0: volume_stability = np.std(rms) / np.mean(rms) volume_stabilities.append(volume_stability) # 计算加权平均值(按时长加权) valid_syllable = [r for r in syllable_rates if r > 0] valid_volume = [v for v in volume_stabilities if v > 0] return { "duration": duration, "syllable_rate": round(np.mean(valid_syllable) if valid_syllable else 0, 2), "volume_stability": round(np.mean(valid_volume) if valid_volume else 0, 4) } except Exception as e: print(f"特征提取错误: {str(e)}") return {"duration": 0, "syllable_rate": 0, "volume_stability": 0} # ====================== 模型加载器(优化版) ====================== class ModelLoader: """加载和管理AI模型(使用RLock)""" asr_pipeline = None sentiment_model = None sentiment_tokenizer = None model_lock = RLock() # 使用RLock代替Lock models_loaded = False # 添加模型加载状态标志 @classmethod def load_models(cls): """加载所有模型""" config = ConfigManager() # 加载ASR模型 if not cls.asr_pipeline: with cls.model_lock: if not cls.asr_pipeline: # 双重检查锁定 cls.load_asr_model(config.get("model_paths")["asr"]) # 加载情感分析模型 if not cls.sentiment_model: with cls.model_lock: if not cls.sentiment_model: # 双重检查锁定 cls.load_sentiment_model(config.get("model_paths")["sentiment"]) cls.models_loaded = True @classmethod def reload_models(cls): """重新加载模型(配置变更后)""" with cls.model_lock: cls.asr_pipeline = None cls.sentiment_model = None cls.sentiment_tokenizer = None gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() cls.load_models() @classmethod def load_asr_model(cls, model_path: str): """加载语音识别模型""" try: if not os.path.exists(model_path): raise FileNotFoundError(f"ASR模型路径不存在: {model_path}") asr_kwargs = {} if hasattr(torch, 'quantization'): asr_kwargs['quantize'] = 'int8' print("启用ASR模型量化") cls.asr_pipeline = pipeline( task=Tasks.auto_speech_recognition, model=model_path, device='cuda' if torch.cuda.is_available() else 'cpu', **asr_kwargs ) print("ASR模型加载完成") except Exception as e: print(f"加载ASR模型失败: {str(e)}") raise @classmethod def load_sentiment_model(cls, model_path: str): """加载情感分析模型""" try: if not os.path.exists(model_path): raise FileNotFoundError(f"情感分析模型路径不存在: {model_path}") cls.sentiment_model = AutoModelForSequenceClassification.from_pretrained(model_path) cls.sentiment_tokenizer = AutoTokenizer.from_pretrained(model_path) if torch.cuda.is_available(): cls.sentiment_model = cls.sentiment_model.cuda() print("情感分析模型加载完成") except Exception as e: print(f"加载情感分析模型失败: {str(e)}") raise # ====================== 核心分析线程(优化版) ====================== class AnalysisThread(QThread): progress_updated = pyqtSignal(int, str, str) result_ready = pyqtSignal(dict) finished_all = pyqtSignal() error_occurred = pyqtSignal(str, str) memory_warning = pyqtSignal() resource_cleanup = pyqtSignal() def __init__(self, audio_paths: List[str], temp_dir: str = "temp_wav"): super().__init__() self.audio_paths = audio_paths self.temp_dir = temp_dir self.is_running = True self.current_file = "" self.max_concurrent = min( ConfigManager().get("max_concurrent", 1), self.get_max_concurrent_tasks() ) self.resource_monitor = ResourceMonitor() self.semaphore = Semaphore(self.max_concurrent) os.makedirs(temp_dir, exist_ok=True) def run(self): try: if not ModelLoader.models_loaded: self.error_occurred.emit("模型未加载", "请等待模型加载完成后再开始分析") return self.progress_updated.emit(0, f"最大并行任务数: {self.max_concurrent}", "") # 使用线程池并行处理 with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_concurrent) as executor: # 创建任务 future_to_path = {} for path in self.audio_paths: if not self.is_running: break # 使用信号量控制并发 self.semaphore.acquire() batch_size = self.get_available_batch_size() future = executor.submit(self.analyze_audio, path, batch_size) future_to_path[future] = path future.add_done_callback(lambda f: self.semaphore.release()) # 处理完成的任务 for i, future in enumerate(concurrent.futures.as_completed(future_to_path)): if not self.is_running: break path = future_to_path[future] self.current_file = os.path.basename(path) # 内存检查 if self.check_memory_usage(): self.memory_warning.emit() self.is_running = False break try: result = future.result() if result: self.result_ready.emit(result) # 更新进度 progress = int((i + 1) / len(self.audio_paths) * 100) self.progress_updated.emit( progress, f"完成: {self.current_file} ({i + 1}/{len(self.audio_paths)})", self.current_file ) except Exception as e: result = { "file_name": self.current_file, "status": "error", "error": f"分析失败: {str(e)}" } self.result_ready.emit(result) # 分析完成后 if self.is_running: self.finished_all.emit() except Exception as e: self.error_occurred.emit("系统错误", str(e)) traceback.print_exc() finally: # 确保资源清理 self.resource_cleanup.emit() self.cleanup_resources() def analyze_audio(self, audio_path: str, batch_size: int) -> Dict: """分析单个音频文件(整合所有优化)""" result = { "file_name": os.path.basename(audio_path), "status": "processing" } wav_paths = [] try: # 1. 音频格式转换 wav_paths = AudioProcessor.convert_to_wav(audio_path, self.temp_dir) if not wav_paths: result["error"] = "格式转换失败(请检查ffmpeg是否安装)" result["status"] = "error" return result # 2. 提取音频特征(合并所有分段) audio_features = self._extract_audio_features(wav_paths) result.update(audio_features) result["duration_str"] = self._format_duration(audio_features["duration"]) # 3. 语音识别与处理(使用批处理优化) all_segments, full_text = self._process_asr_segments(wav_paths) # 4. 说话人区分(使用优化后的方法) agent_segments, customer_segments = self.identify_speakers(all_segments) # 5. 生成带说话人标签的文本 labeled_text = self._generate_labeled_text(all_segments, agent_segments, customer_segments) result["asr_text"] = labeled_text.strip() # 6. 文本分析(包含方言预处理) text_analysis = self._analyze_text(agent_segments, customer_segments, batch_size) result.update(text_analysis) # 7. 服务规范检查(使用方言适配的关键词) service_check = self._check_service_rules(agent_segments) result.update(service_check) # 8. 问题解决率(上下文关联) result["issue_resolved"] = self._check_issue_resolution(customer_segments, agent_segments) result["status"] = "success" except Exception as e: result["error"] = f"分析失败: {str(e)}" result["status"] = "error" finally: # 清理临时文件(使用优化后的清理方法) self._cleanup_temp_files(wav_paths) # 显式内存清理 self.cleanup_resources() return result def identify_speakers(self, segments: List[Dict]) -> Tuple[List[Dict], List[Dict]]: """区分客服与客户(增强版)""" if not segments: return [], [] # 1. 基于关键词的识别 agent_id = self._identify_by_keywords(segments) # 2. 基于说话模式的识别(如果关键词识别失败) if agent_id is None and len(segments) >= 4: agent_id = self._identify_by_speech_patterns(segments) # 3. 使用说话频率最高的作为客服(最后手段) if agent_id is None: spk_counts = {} for seg in segments: spk_id = seg["spk_id"] spk_counts[spk_id] = spk_counts.get(spk_id, 0) + 1 agent_id = max(spk_counts, key=spk_counts.get) if spk_counts else None if agent_id is None: return [], [] # 使用集合存储agent的spk_id agent_spk_ids = {agent_id} return ( [seg for seg in segments if seg["spk_id"] in agent_spk_ids], [seg for seg in segments if seg["spk_id"] not in agent_spk_ids] ) def _identify_by_keywords(self, segments: List[Dict]) -> Optional[str]: """基于关键词识别客服""" opening_patterns = DialectConfig.get_compiled_opening() closing_patterns = DialectConfig.get_compiled_closing() # 策略1:在前3段中查找开场白关键词 for seg in segments[:3]: text = seg["text"] for pattern in opening_patterns: if pattern.search(text): return seg["spk_id"] # 策略2:在后3段中查找结束语关键词 for seg in reversed(segments[-3:] if len(segments) >= 3 else segments): text = seg["text"] for pattern in closing_patterns: if pattern.search(text): return seg["spk_id"] return None def _identify_by_speech_patterns(self, segments: List[Dict]) -> Optional[str]: """基于说话模式识别客服""" # 分析说话模式特征 speaker_features = {} for seg in segments: spk_id = seg["spk_id"] if spk_id not in speaker_features: speaker_features[spk_id] = { "total_duration": 0.0, "turn_count": 0, "question_count": 0 } features = speaker_features[spk_id] features["total_duration"] += (seg["end"] - seg["start"]) features["turn_count"] += 1 # 检测问题(包含疑问词) if any(q_word in seg["text"] for q_word in ["吗", "呢", "?", "?", "如何", "怎样"]): features["question_count"] += 1 # 客服通常说话时间更长、提问更多 if speaker_features: # 计算说话时间占比 max_duration = max(f["total_duration"] for f in speaker_features.values()) # 计算提问频率 question_rates = { spk_id: features["question_count"] / features["turn_count"] for spk_id, features in speaker_features.items() } # 综合评分 candidates = [] for spk_id, features in speaker_features.items(): score = ( 0.6 * (features["total_duration"] / max_duration) + 0.4 * question_rates[spk_id] ) candidates.append((spk_id, score)) # 返回得分最高的说话人 return max(candidates, key=lambda x: x[1])[0] return None def _analyze_text(self, agent_segments: List[Dict], customer_segments: List[Dict], batch_size: int) -> Dict: """文本情感分析(优化版:向量化批处理)""" def analyze_speaker(segments: List[Dict], speaker_type: str) -> Dict: if not segments: return { f"{speaker_type}_negative": 0.0, f"{speaker_type}_neutral": 1.0, f"{speaker_type}_positive": 0.0, f"{speaker_type}_emotions": "无" } # 方言预处理 - 使用优化的一次性替换 texts = [seg["text"] for seg in segments] processed_texts = DialectConfig.preprocess_text(texts) # 使用DataLoader进行批处理 with ModelLoader.model_lock: inputs = ModelLoader.sentiment_tokenizer( processed_texts, padding=True, truncation=True, max_length=128, return_tensors="pt" ) # 创建TensorDataset和DataLoader dataset = TensorDataset(inputs['input_ids'], inputs['attention_mask']) dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False) device = "cuda" if torch.cuda.is_available() else "cpu" sentiment_dist = [] emotions = [] # 批量处理 for batch in dataloader: input_ids, attention_mask = batch inputs = { 'input_ids': input_ids.to(device), 'attention_mask': attention_mask.to(device) } with torch.no_grad(): outputs = ModelLoader.sentiment_model(**inputs) batch_probs = torch.nn.functional.softmax(outputs.logits, dim=-1) sentiment_dist.append(batch_probs.cpu()) # 情绪识别(批量) emotion_keywords = ["愤怒", "生气", "鬼火", "不耐烦", "搞哪样嘛", "恼火", "背时"] for text in processed_texts: if any(kw in text for kw in emotion_keywords): if any(kw in text for kw in ["愤怒", "生气", "鬼火", "恼火"]): emotions.append("愤怒") elif any(kw in text for kw in ["不耐烦", "搞哪样嘛"]): emotions.append("不耐烦") elif "背时" in text: emotions.append("沮丧") # 合并结果 if sentiment_dist: all_probs = torch.cat(sentiment_dist, dim=0) avg_sentiment = torch.mean(all_probs, dim=0).tolist() else: avg_sentiment = [0.0, 1.0, 0.0] # 默认值 return { f"{speaker_type}_negative": round(avg_sentiment[0], 4), f"{speaker_type}_neutral": round(avg_sentiment[1], 4), f"{speaker_type}_positive": round(avg_sentiment[2], 4), f"{speaker_type}_emotions": ",".join(set(emotions)) if emotions else "无" } return { **analyze_speaker(agent_segments, "agent"), **analyze_speaker(customer_segments, "customer") } # ====================== 辅助方法 ====================== def get_available_batch_size(self) -> int: """根据GPU内存动态调整batch size(考虑并行)""" if not torch.cuda.is_available(): return 4 # CPU默认批次 total_mem = torch.cuda.get_device_properties(0).total_memory / (1024 ** 3) # GB per_task_mem = total_mem / self.max_concurrent # 修正批次大小逻辑:显存越少,批次越小 if per_task_mem < 2: return 2 elif per_task_mem < 4: return 4 else: return 8 def get_max_concurrent_tasks(self) -> int: """根据系统资源计算最大并行任务数""" if torch.cuda.is_available(): total_mem = torch.cuda.get_device_properties(0).total_memory / (1024 ** 3) if total_mem < 6: return 1 elif total_mem < 12: return 2 else: return 3 else: # CPU模式下根据核心数设置 return max(1, os.cpu_count() // 2) def check_memory_usage(self) -> bool: try: mem_percent = self.resource_monitor.memory_percent() return mem_percent.get("cpu", 0) > 85 or mem_percent.get("gpu", 0) > 85 except: return False def _extract_audio_features(self, wav_paths: List[str]) -> Dict[str, float]: """提取音频特征(合并所有分段)""" combined_y = np.array([], dtype=np.float32) sr = ConfigManager().get("sample_rate", 16000) for path in wav_paths: y, _ = librosa.load(path, sr=sr) combined_y = np.concatenate((combined_y, y)) return AudioProcessor.extract_features_from_audio(combined_y, sr) def _process_asr_segments(self, wav_paths: List[str]) -> Tuple[List[Dict], str]: """处理ASR分段(批处理优化)""" segments = [] full_text = "" # 分批处理(根据GPU内存动态调整批次大小) batch_size = min(4, len(wav_paths), self.get_available_batch_size()) for i in range(0, len(wav_paths), batch_size): if not self.is_running: break batch_paths = wav_paths[i:i + batch_size] try: # 批处理调用ASR模型 results = ModelLoader.asr_pipeline( batch_paths, hotwords=DialectConfig.get_asr_hotwords(), output_dir=None, batch_size=batch_size ) for result in results: for seg in result[0]["sentences"]: segments.append({ "start": seg["start"], "end": seg["end"], "text": seg["text"], "spk_id": seg.get("spk_id", "0") }) full_text += seg["text"] + " " except Exception as e: print(f"ASR批处理错误: {str(e)}") # 失败时回退到单文件处理 for path in batch_paths: try: result = ModelLoader.asr_pipeline( path, hotwords=DialectConfig.get_asr_hotwords(), output_dir=None ) for seg in result[0]["sentences"]: segments.append({ "start": seg["start"], "end": seg["end"], "text": seg["text"], "spk_id": seg.get("spk_id", "0") }) full_text += seg["text"] + " " except: continue return segments, full_text.strip() def _generate_labeled_text(self, all_segments: List[Dict], agent_segments: List[Dict], customer_segments: List[Dict]) -> str: """生成带说话人标签的文本""" agent_spk_id = agent_segments[0]["spk_id"] if agent_segments else None customer_spk_id = customer_segments[0]["spk_id"] if customer_segments else None labeled_text = [] for seg in all_segments: if seg["spk_id"] == agent_spk_id: speaker = "客服" elif seg["spk_id"] == customer_spk_id: speaker = "客户" else: speaker = f"说话人{seg['spk_id']}" labeled_text.append(f"[{speaker}]: {seg['text']}") return "\n".join(labeled_text) def _check_service_rules(self, agent_segments: List[Dict]) -> Dict: """检查服务规范""" forbidden_keywords = DialectConfig.get_combined_keywords()["forbidden"] found_forbidden = [] found_opening = False found_closing = False # 检查开场白(前3段) for seg in agent_segments[:3]: text = seg["text"] if any(kw in text for kw in DialectConfig.get_combined_keywords()["opening"]): found_opening = True break # 检查结束语(后3段) for seg in reversed(agent_segments[-3:] if len(agent_segments) >= 3 else agent_segments): text = seg["text"] if any(kw in text for kw in DialectConfig.get_combined_keywords()["closing"]): found_closing = True break # 检查禁用词 for seg in agent_segments: text = seg["text"] for kw in forbidden_keywords: if kw in text: found_forbidden.append(kw) break return { "opening_found": found_opening, "closing_found": found_closing, "forbidden_words": ", ".join(set(found_forbidden)) if found_forbidden else "无" } def _check_issue_resolution(self, customer_segments: List[Dict], agent_segments: List[Dict]) -> bool: """检查问题是否解决(增强版)""" if not customer_segments or not agent_segments: return False # 提取所有文本 customer_texts = [seg["text"] for seg in customer_segments] agent_texts = [seg["text"] for seg in agent_segments] full_conversation = " ".join(customer_texts + agent_texts) # 问题解决关键词 resolution_keywords = ["解决", "处理", "完成", "已", "好了", "可以了", "没问题"] thank_keywords = ["谢谢", "感谢", "多谢"] negative_keywords = ["没解决", "不行", "不对", "还是", "仍然", "再"] # 检查是否有负面词汇 has_negative = any(kw in full_conversation for kw in negative_keywords) if has_negative: return False # 检查客户最后是否表达感谢 last_customer_text = customer_segments[-1]["text"] if any(kw in last_customer_text for kw in thank_keywords): return True # 检查是否有解决关键词 if any(kw in full_conversation for kw in resolution_keywords): return True # 检查客服是否确认解决 for agent_text in reversed(agent_texts[-3:]): # 检查最后3段 if any(kw in agent_text for kw in resolution_keywords): return True return False def _cleanup_temp_files(self, paths: List[str]): """清理临时文件(增强兼容性)""" def safe_remove(path): """安全删除文件(多平台兼容)""" try: if os.path.exists(path): if sys.platform == 'win32': # Windows系统需要特殊处理 os.chmod(path, 0o777) # 确保有权限 for _ in range(5): # 最多尝试5次 try: os.remove(path) break except PermissionError: time.sleep(0.2) else: os.remove(path) except Exception: pass # 使用线程池并行删除 with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: executor.map(safe_remove, paths) # 额外清理:删除超过1小时的临时文件 now = time.time() for file in os.listdir(self.temp_dir): file_path = os.path.join(self.temp_dir, file) if os.path.isfile(file_path): file_age = now - os.path.getmtime(file_path) if file_age > 3600: # 1小时 safe_remove(file_path) def _format_duration(self, seconds: float) -> str: """将秒转换为时分秒格式""" minutes, seconds = divmod(int(seconds), 60) hours, minutes = divmod(minutes, 60) return f"{hours:02d}:{minutes:02d}:{seconds:02d}" def cleanup_resources(self): """显式清理资源""" gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() def stop(self): """停止分析""" self.is_running = False # ====================== 模型加载线程 ====================== class ModelLoadThread(QThread): progress_updated = pyqtSignal(int, str) finished = pyqtSignal(bool, str) def run(self): try: # 检查模型路径 config = ConfigManager().get("model_paths") if not os.path.exists(config["asr"]): self.finished.emit(False, "ASR模型路径不存在") return if not os.path.exists(config["sentiment"]): self.finished.emit(False, "情感分析模型路径不存在") return self.progress_updated.emit(20, "加载语音识别模型...") ModelLoader.load_asr_model(config["asr"]) self.progress_updated.emit(60, "加载情感分析模型...") ModelLoader.load_sentiment_model(config["sentiment"]) self.progress_updated.emit(100, "模型加载完成") self.finished.emit(True, "模型加载成功。建议:可通过设置界面修改模型路径") except Exception as e: self.finished.emit(False, f"模型加载失败: {str(e)}。建议:检查模型路径是否正确,或重新下载模型文件") # ====================== GUI主界面 ====================== class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("贵州方言客服质检系统") self.setGeometry(100, 100, 1200, 800) self.setup_ui() self.setup_menu() self.analysis_thread = None self.model_load_thread = None self.temp_dir = "temp_wav" os.makedirs(self.temp_dir, exist_ok=True) self.model_loaded = False def setup_ui(self): """设置用户界面""" # 主布局 main_widget = QWidget() main_layout = QVBoxLayout() main_widget.setLayout(main_layout) self.setCentralWidget(main_widget) # 工具栏 toolbar = QToolBar("主工具栏") toolbar.setIconSize(QSize(24, 24)) self.addToolBar(toolbar) # 添加文件按钮 add_file_action = QAction(QIcon("icons/add.png"), "添加文件", self) add_file_action.triggered.connect(self.add_files) toolbar.addAction(add_file_action) # 开始分析按钮 analyze_action = QAction(QIcon("icons/start.png"), "开始分析", self) analyze_action.triggered.connect(self.start_analysis) toolbar.addAction(analyze_action) # 停止按钮 stop_action = QAction(QIcon("icons/stop.png"), "停止分析", self) stop_action.triggered.connect(self.stop_analysis) toolbar.addAction(stop_action) # 设置按钮 settings_action = QAction(QIcon("icons/settings.png"), "设置", self) settings_action.triggered.connect(self.open_settings) toolbar.addAction(settings_action) # 分割布局 splitter = QSplitter(Qt.Horizontal) main_layout.addWidget(splitter) # 左侧文件列表 left_widget = QWidget() left_layout = QVBoxLayout() left_widget.setLayout(left_layout) file_list_label = QLabel("待分析文件列表") file_list_label.setFont(QFont("Arial", 12, QFont.Bold)) left_layout.addWidget(file_list_label) self.file_list = QListWidget() self.file_list.setSelectionMode(QListWidget.ExtendedSelection) left_layout.addWidget(self.file_list) # 右侧结果区域 right_widget = QWidget() right_layout = QVBoxLayout() right_widget.setLayout(right_layout) # 进度条 progress_label = QLabel("分析进度") progress_label.setFont(QFont("Arial", 12, QFont.Bold)) right_layout.addWidget(progress_label) self.progress_bar = QProgressBar() self.progress_bar.setRange(0, 100) self.progress_bar.setTextVisible(True) right_layout.addWidget(self.progress_bar) # 当前文件标签 self.current_file_label = QLabel("当前文件: 无") right_layout.addWidget(self.current_file_label) # 结果标签页 self.tab_widget = QTabWidget() right_layout.addWidget(self.tab_widget, 1) # 文本结果标签页 text_tab = QWidget() text_layout = QVBoxLayout() text_tab.setLayout(text_layout) self.text_result = QTextEdit() self.text_result.setReadOnly(True) text_layout.addWidget(self.text_result) self.tab_widget.addTab(text_tab, "文本结果") # 详细结果标签页 detail_tab = QWidget() detail_layout = QVBoxLayout() detail_tab.setLayout(detail_layout) self.result_table = QTableWidget() self.result_table.setColumnCount(10) self.result_table.setHorizontalHeaderLabels([ "文件名", "时长", "语速", "音量稳定性", "客服情感", "客户情感", "开场白", "结束语", "禁用词", "问题解决" ]) self.result_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) detail_layout.addWidget(self.result_table) self.tab_widget.addTab(detail_tab, "详细结果") # 添加左右部件到分割器 splitter.addWidget(left_widget) splitter.addWidget(right_widget) splitter.setSizes([300, 900]) def setup_menu(self): """设置菜单栏""" menu_bar = self.menuBar() # 文件菜单 file_menu = menu_bar.addMenu("文件") add_file_action = QAction("添加文件", self) add_file_action.triggered.connect(self.add_files) file_menu.addAction(add_file_action) export_action = QAction("导出结果", self) export_action.triggered.connect(self.export_results) file_menu.addAction(export_action) exit_action = QAction("退出", self) exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # 分析菜单 analysis_menu = menu_bar.addMenu("分析") start_action = QAction("开始分析", self) start_action.triggered.connect(self.start_analysis) analysis_menu.addAction(start_action) stop_action = QAction("停止分析", self) stop_action.triggered.connect(self.stop_analysis) analysis_menu.addAction(stop_action) # 设置菜单 settings_menu = menu_bar.addMenu("设置") config_action = QAction("系统配置", self) config_action.triggered.connect(self.open_settings) settings_menu.addAction(config_action) model_action = QAction("加载模型", self) model_action.triggered.connect(self.load_models) settings_menu.addAction(model_action) def add_files(self): """添加文件到分析列表""" files, _ = QFileDialog.getOpenFileNames( self, "选择音频文件", "", "音频文件 (*.mp3 *.wav *.amr *.m4a)" ) if files: for file in files: self.file_list.addItem(file) def start_analysis(self): """开始分析""" if self.file_list.count() == 0: QMessageBox.warning(self, "警告", "请先添加要分析的音频文件") return if not self.model_loaded: QMessageBox.warning(self, "警告", "模型未加载,请先加载模型") return # 获取文件路径 audio_paths = [self.file_list.item(i).text() for i in range(self.file_list.count())] # 清空结果 self.text_result.clear() self.result_table.setRowCount(0) # 创建分析线程 self.analysis_thread = AnalysisThread(audio_paths, self.temp_dir) # 连接信号 self.analysis_thread.progress_updated.connect(self.update_progress) self.analysis_thread.result_ready.connect(self.handle_result) self.analysis_thread.finished_all.connect(self.analysis_finished) self.analysis_thread.error_occurred.connect(self.show_error) self.analysis_thread.memory_warning.connect(self.handle_memory_warning) self.analysis_thread.resource_cleanup.connect(self.cleanup_resources) # 启动线程 self.analysis_thread.start() def stop_analysis(self): """停止分析""" if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.analysis_thread.wait() QMessageBox.information(self, "信息", "分析已停止") def load_models(self): """加载模型""" if self.model_load_thread and self.model_load_thread.isRunning(): return self.model_load_thread = ModelLoadThread() self.model_load_thread.progress_updated.connect( lambda value, msg: self.progress_bar.setValue(value) ) self.model_load_thread.finished.connect(self.handle_model_load_result) self.model_load_thread.start() def update_progress(self, progress: int, message: str, current_file: str): """更新进度""" self.progress_bar.setValue(progress) self.current_file_label.setText(f"当前文件: {current_file}") def handle_result(self, result: Dict): """处理分析结果""" # 添加到文本结果 self.text_result.append(f"文件: {result['file_name']}") self.text_result.append(f"状态: {result['status']}") if result["status"] == "success": self.text_result.append(f"时长: {result['duration_str']}") self.text_result.append(f"语速: {result['syllable_rate']} 音节/秒") self.text_result.append(f"音量稳定性: {result['volume_stability']}") self.text_result.append(f"客服情感: 负面({result['agent_negative']:.2%}) " f"中性({result['agent_neutral']:.2%}) " f"正面({result['agent_positive']:.2%})") self.text_result.append(f"客服情绪: {result['agent_emotions']}") self.text_result.append(f"客户情感: 负面({result['customer_negative']:.2%}) " f"中性({result['customer_neutral']:.2%}) " f"正面({result['customer_positive']:.2%})") self.text_result.append(f"客户情绪: {result['customer_emotions']}") self.text_result.append(f"开场白: {'有' if result['opening_found'] else '无'}") self.text_result.append(f"结束语: {'有' if result['closing_found'] else '无'}") self.text_result.append(f"禁用词: {result['forbidden_words']}") self.text_result.append(f"问题解决: {'是' if result['issue_resolved'] else '否'}") self.text_result.append("\n=== 对话文本 ===\n") self.text_result.append(result["asr_text"]) self.text_result.append("\n" + "=" * 50 + "\n") # 添加到结果表格 row = self.result_table.rowCount() self.result_table.insertRow(row) self.result_table.setItem(row, 0, QTableWidgetItem(result["file_name"])) self.result_table.setItem(row, 1, QTableWidgetItem(result["duration_str"])) self.result_table.setItem(row, 2, QTableWidgetItem(str(result["syllable_rate"]))) self.result_table.setItem(row, 3, QTableWidgetItem(str(result["volume_stability"]))) self.result_table.setItem(row, 4, QTableWidgetItem( f"负:{result['agent_negative']:.2f} 中:{result['agent_neutral']:.2f} 正:{result['agent_positive']:.2f}" )) self.result_table.setItem(row, 5, QTableWidgetItem( f"负:{result['customer_negative']:.2f} 中:{result['customer_neutral']:.2f} 正:{result['customer_positive']:.2f}" )) self.result_table.setItem(row, 6, QTableWidgetItem("是" if result["opening_found"] else "否")) self.result_table.setItem(row, 7, QTableWidgetItem("是" if result["closing_found"] else "否")) self.result_table.setItem(row, 8, QTableWidgetItem(result["forbidden_words"])) self.result_table.setItem(row, 9, QTableWidgetItem("是" if result["issue_resolved"] else "否")) # 根据结果着色 if not result["opening_found"]: self.result_table.item(row, 6).setBackground(QColor(255, 200, 200)) if not result["closing_found"]: self.result_table.item(row, 7).setBackground(QColor(255, 200, 200)) if result["forbidden_words"] != "无": self.result_table.item(row, 8).setBackground(QColor(255, 200, 200)) if not result["issue_resolved"]: self.result_table.item(row, 9).setBackground(QColor(255, 200, 200)) def analysis_finished(self): """分析完成""" QMessageBox.information(self, "完成", "所有音频分析完成") self.progress_bar.setValue(100) def show_error(self, title: str, message: str): """显示错误信息""" QMessageBox.critical(self, title, message) def handle_memory_warning(self): """处理内存警告""" QMessageBox.warning(self, "内存警告", "内存使用过高,分析已停止。请关闭其他应用程序后重试") def cleanup_resources(self): """清理资源""" gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() def handle_model_load_result(self, success: bool, message: str): """处理模型加载结果""" if success: self.model_loaded = True QMessageBox.information(self, "成功", message) else: QMessageBox.critical(self, "错误", message) def open_settings(self): """打开设置对话框""" settings_dialog = QDialog(self) settings_dialog.setWindowTitle("系统设置") settings_dialog.setFixedSize(500, 400) layout = QVBoxLayout() # ASR模型路径 asr_layout = QHBoxLayout() asr_label = QLabel("ASR模型路径:") asr_line = QLineEdit(ConfigManager().get("model_paths")["asr"]) asr_browse = QPushButton("浏览...") def browse_asr(): path = QFileDialog.getExistingDirectory(self, "选择ASR模型目录") if path: asr_line.setText(path) asr_browse.clicked.connect(browse_asr) asr_layout.addWidget(asr_label) asr_layout.addWidget(asr_line) asr_layout.addWidget(asr_browse) layout.addLayout(asr_layout) # 情感分析模型路径 sentiment_layout = QHBoxLayout() sentiment_label = QLabel("情感模型路径:") sentiment_line = QLineEdit(ConfigManager().get("model_paths")["sentiment"]) sentiment_browse = QPushButton("浏览...") def browse_sentiment(): path = QFileDialog.getExistingDirectory(self, "选择情感模型目录") if path: sentiment_line.setText(path) sentiment_browse.clicked.connect(browse_sentiment) sentiment_layout.addWidget(sentiment_label) sentiment_layout.addWidget(sentiment_line) sentiment_layout.addWidget(sentiment_browse) layout.addLayout(sentiment_layout) # 并发设置 concurrent_layout = QHBoxLayout() concurrent_label = QLabel("最大并发任务:") concurrent_spin = QSpinBox() concurrent_spin.setRange(1, 8) concurrent_spin.setValue(ConfigManager().get("max_concurrent", 1)) concurrent_layout.addWidget(concurrent_label) concurrent_layout.addWidget(concurrent_spin) layout.addLayout(concurrent_layout) # 方言设置 dialect_layout = QHBoxLayout() dialect_label = QLabel("方言设置:") dialect_combo = QComboBox() dialect_combo.addItems(["标准普通话", "贵州方言"]) dialect_combo.setCurrentIndex(1 if ConfigManager().get("dialect_config") == "guizhou" else 0) dialect_layout.addWidget(dialect_label) dialect_layout.addWidget(dialect_combo) layout.addLayout(dialect_layout) # 音频时长限制 duration_layout = QHBoxLayout() duration_label = QLabel("最大音频时长(秒):") duration_spin = QSpinBox() duration_spin.setRange(60, 86400) # 1分钟到24小时 duration_spin.setValue(ConfigManager().get("max_audio_duration", 3600)) duration_layout.addWidget(duration_label) duration_layout.addWidget(duration_spin) layout.addLayout(duration_layout) # 按钮 button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(settings_dialog.accept) button_box.rejected.connect(settings_dialog.reject) layout.addWidget(button_box) settings_dialog.setLayout(layout) if settings_dialog.exec_() == QDialog.Accepted: # 保存设置 ConfigManager().set("model_paths", { "asr": asr_line.text(), "sentiment": sentiment_line.text() }) ConfigManager().set("max_concurrent", concurrent_spin.value()) ConfigManager().set("dialect_config", "guizhou" if dialect_combo.currentIndex() == 1 else "standard") ConfigManager().set("max_audio_duration", duration_spin.value()) # 重新加载模型 ModelLoader.reload_models() def export_results(self): """导出结果""" if self.result_table.rowCount() == 0: QMessageBox.warning(self, "警告", "没有可导出的结果") return path, _ = QFileDialog.getSaveFileName( self, "保存结果", "", "CSV文件 (*.csv)" ) if path: try: with open(path, "w", encoding="utf-8") as f: # 写入表头 headers = [] for col in range(self.result_table.columnCount()): headers.append(self.result_table.horizontalHeaderItem(col).text()) f.write(",".join(headers) + "\n") # 写入数据 for row in range(self.result_table.rowCount()): row_data = [] for col in range(self.result_table.columnCount()): item = self.result_table.item(row, col) row_data.append(item.text() if item else "") f.write(",".join(row_data) + "\n") QMessageBox.information(self, "成功", f"结果已导出到: {path}") except Exception as e: QMessageBox.critical(self, "错误", f"导出失败: {str(e)}") def closeEvent(self, event): """关闭事件处理""" if self.analysis_thread and self.analysis_thread.isRunning(): self.analysis_thread.stop() self.analysis_thread.wait() # 清理临时目录(增强兼容性) try: for file in os.listdir(self.temp_dir): file_path = os.path.join(self.temp_dir, file) if os.path.isfile(file_path): # Windows系统可能需要多次尝试 for _ in range(3): try: os.remove(file_path) break except PermissionError: time.sleep(0.1) os.rmdir(self.temp_dir) except: pass event.accept() # ====================== 程序入口 ====================== if __name__ == "__main__": torch.set_num_threads(4) # 限制CPU线程数 app = QApplication(sys.argv) # 设置应用样式 app.setStyle('Fusion') window = MainWindow() window.show() sys.exit(app.exec_())

filetype

获取播放地址 接口功能: 该接口用于通过设备序列号、通道号获取单台设备的播放地址信息,无法获取永久有效期播放地址。 请求地址 https://open.ys7.com/api/lapp/v2/live/address/get 子账户token请求所需最小权限 "Permission":"Get" "Resource":"dev:序列号" 请求方式 POST 请求参数 参数名 类型 描述 是否必选 accessToken String 授权过程获取的access_token Y deviceSerial String 设备序列号例如427734222,均采用英文符号,限制最多50个字符 Y channelNo Integer 通道号,非必选,默认为1 N protocol Integer 流播放协议,1-ezopen、2-hls、3-rtmp、4-flv,默认为1 N code String ezopen协议地址的设备的视频加密密码 N expireTime Integer 过期时长,单位秒;针对hls/rtmp/flv设置有效期,相对时间;30秒-720天 N type String 地址的类型,1-预览,2-本地录像回放,3-云存储录像回放,非必选,默认为1;回放仅支持rtmp、ezopen、flv协议 N quality Integer 视频清晰度,1-高清(主码流)、2-流畅(子码流) N startTime String 本地录像/云存储录像回放开始时间,云存储开始结束时间必须在同一天,示例:2019-12-01 00:00:00 N stopTime String 本地录像/云存储录像回放结束时间,云存储开始结束时间必须在同一天,示例:2019-12-01 23:59:59 N supportH265 Integer 请判断播放端是否要求播放视频为H265编码格式,1表示需要,0表示不要求 N playbackSpeed String 回放倍速。倍速为 -1( 支持的最大倍速)、0.5、1、2、4、8、16; 仅支持protocol为4-flv 且 type为2-本地录像回放( 部分设备可能不支持16倍速) 或者 3-云存储录像回放 N gbchannel String 国标设备的通道编号,视频通道编号ID N HTTP请求报文 POST /api/lapp/v2/live/address/get HTTP/1.1 Host: open.ys7.com Content-Type: application/x-www-form-urlencoded accessToken=at.dunwhxt2azk02hcn7phqygsybbw0wv6p&deviceSerial=C78957921&channelNo=1 返回数据 { "msg": "Operation succeeded", "code": "200", "data": { "id": "254708522214232064", "url": "https://open.ys7.com/v3/openlive/C78957921_1_1.m3u8?expire=1606999273&id=254708522214232064&t=093e5c6668d981e0f0b8d2593d69bdc98060407d1b2f42eaaa17a62b15ee4f99&ev=100", "expireTime": "2020-12-03 20:41:13" } } 返回字段: 字段名 类型 描述 code String 状态码,参考下方返回码。优先判断该错误码,返回200即表示成功。 msg String 状态描述 id String 状态描述 url String 直播地址 expireTime long 直播地址有效期。expireTime参数为空时该字段无效 注意:该接口请求时先解析code属性,如果返回200即表示成功,可继续解析data属性的内容,每一个地址对象中先解析ret属性,如果返回200表示成功,再根据status属性和exception属性判断是否存在异常。 返回码 返回码 返回消息 备注 200 操作成功,获取指定有效期的直播地址 请求成功 201 Created 401 Unauthorized 403 Forbidden 404 Not Found 403 用户不存在 依据上述获取播放地址api整合进下面代码中并且生成整合后完整的代码。package com.hik.netsdk.SimpleDemo.View; import android.app.AlertDialog; import android.content.Intent; import android.graphics.PixelFormat; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.widget.EditText; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.Toast; import com.hik.netsdk.SimpleDemo.R; import com.hik.netsdk.SimpleDemo.View.DevMgtUI.AddDevActivity; import com.videogo.errorlayer.ErrorInfo; import com.videogo.openapi.EZConstants; import com.videogo.openapi.EZOpenSDK; import com.videogo.openapi.EZPlayer; public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback { // 核心参数 private String mDeviceSerial; private String mVerifyCode; private int mCameraNo = 1; private boolean isEzDevice = false; // 萤石云相关参数 private String mAppKey = "a794d58c13154caeb7d2fbb5c3420c65"; private String mAccessToken = ""; // UI组件 private SurfaceView mPreviewSurface; private SurfaceHolder mSurfaceHolder; private Toolbar m_toolbar; private ProgressBar mProgressBar; private RelativeLayout mControlLayout; private ImageButton mRotateButton; // 萤石云播放器 private EZPlayer mEZPlayer; // 萤石云回调Handler private Handler mEzHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { case EZConstants.EZRealPlayConstants.MSG_REALPLAY_PLAY_SUCCESS: Log.d("EZPlay", "播放成功"); mProgressBar.setVisibility(View.GONE); break; case EZConstants.EZRealPlayConstants.MSG_REALPLAY_PLAY_FAIL: ErrorInfo errorInfo = (ErrorInfo) msg.obj; int errorCode = errorInfo.errorCode; String description = errorInfo.description; Log.e("EZPlay", "播放失败,错误码:" + errorCode + ", 描述:" + description); // 处理需要验证码的情况 if (errorCode == ErrorInfo.ERROR_INNER_VERIFYCODE_NEED || errorCode == ErrorInfo.ERROR_INNER_VERIFYCODE_ERROR) { showVerifyCodeDialog(); } else { handleError("播放失败,错误码:" + errorCode, errorCode); } break; case EZConstants.MSG_VIDEO_SIZE_CHANGED: String resolution = (String) msg.obj; String[] resParts = resolution.split("\\*"); if (resParts.length == 2) { try { int width = Integer.parseInt(resParts[0]); int height = Integer.parseInt(resParts[1]); Log.d("EZResolution", "视频分辨率变化: " + width + "x" + height); // 可以在这里调整UI布局 } catch (NumberFormatException e) { Log.e("EZResolution", "分辨率解析错误: " + resolution); } } break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 初始化UI组件 initUIComponents(); // 获取启动参数 parseIntentParams(); // 初始化SDK initSDK(); // 参数校验 if (mDeviceSerial == null || mDeviceSerial.isEmpty()) { showErrorAndFinish("设备序列号不能为空"); return; } Log.d("MainActivity", "onCreate完成: isEzDevice=" + isEzDevice); } private void initUIComponents() { // 基础UI m_toolbar = findViewById(R.id.toolbar); setSupportActionBar(m_toolbar); // 预览相关UI mPreviewSurface = findViewById(R.id.realplay_sv); mSurfaceHolder = mPreviewSurface.getHolder(); mSurfaceHolder.addCallback(this); mSurfaceHolder.setFormat(PixelFormat.TRANSLUCENT); mProgressBar = findViewById(R.id.liveProgressBar); mControlLayout = findViewById(R.id.rl_control); mRotateButton = findViewById(R.id.ib_rotate2); // 设置旋转按钮点击事件 mRotateButton.setOnClickListener(v -> changeScreen()); // 隐藏管理UI DrawerLayout drawer = findViewById(R.id.drawer_layout); if (drawer != null) { drawer.setVisibility(View.GONE); } if (m_toolbar != null) { m_toolbar.setVisibility(View.GONE); } // 初始化控制按钮 initControlButtons(); Log.d("UI", "UI组件初始化完成"); } private void parseIntentParams() { // 获取Intent参数 Intent intent = getIntent(); isEzDevice = true; // 只支持萤石设备 mAccessToken = intent.getStringExtra("accessToken"); // 优先使用bundle Bundle bundle = intent.getExtras(); if (bundle != null) { mDeviceSerial = bundle.getString("devSn"); mVerifyCode = bundle.getString("verifyCode"); mCameraNo = bundle.getInt("cameraNo", 1); } // 兼容直接Extra方式 if (mDeviceSerial == null) mDeviceSerial = intent.getStringExtra("devSn"); if (mVerifyCode == null) mVerifyCode = intent.getStringExtra("verifyCode"); if (mCameraNo == 0) mCameraNo = intent.getIntExtra("cameraNo", 1); Log.d("Params", "设备序列号: " + mDeviceSerial + ", 通道号: " + mCameraNo); } private void initSDK() { // 初始化萤石云SDK if (!EZOpenSDK.getInstance().isInitialized()) { EZOpenSDK.initLib(getApplication(), mAppKey); Log.d("EZSDK", "萤石SDK初始化完成"); } if (mAccessToken != null && !mAccessToken.isEmpty()) { EZOpenSDK.getInstance().setAccessToken(mAccessToken); Log.d("EZToken", "AccessToken设置成功"); } else { Log.w("EZToken", "AccessToken缺失!"); } } private void initControlButtons() { // 云台控制按钮 findViewById(R.id.ptz_top_btn).setOnClickListener(v -> controlPTZ("UP")); findViewById(R.id.ptz_bottom_btn).setOnClickListener(v -> controlPTZ("DOWN")); findViewById(R.id.ptz_left_btn).setOnClickListener(v -> controlPTZ("LEFT")); findViewById(R.id.ptz_right_btn).setOnClickListener(v -> controlPTZ("RIGHT")); // 变焦控制 findViewById(R.id.focus_add).setOnClickListener(v -> controlZoom("ZOOM_IN")); findViewById(R.id.foucus_reduce).setOnClickListener(v -> controlZoom("ZOOM_OUT")); // 水平布局控制按钮 findViewById(R.id.ptz_top_btn2).setOnClickListener(v -> controlPTZ("UP")); findViewById(R.id.ptz_bottom_btn2).setOnClickListener(v -> controlPTZ("DOWN")); findViewById(R.id.ptz_left_btn2).setOnClickListener(v -> controlPTZ("LEFT")); findViewById(R.id.ptz_right_btn2).setOnClickListener(v -> controlPTZ("RIGHT")); // 添加萤石云特有功能按钮 findViewById(R.id.btn_record).setOnClickListener(v -> startLocalRecord()); findViewById(R.id.btn_stop_record).setOnClickListener(v -> stopLocalRecord()); findViewById(R.id.btn_capture).setOnClickListener(v -> capturePicture()); Log.d("Controls", "控制按钮初始化完成"); } @Override public void surfaceCreated(SurfaceHolder holder) { Log.d("Surface", "Surface created"); startEzCloudPreview(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Log.d("Surface", "Surface changed: " + width + "x" + height); if (mEZPlayer != null) { mEZPlayer.setSurfaceHold(holder); } } @Override public void surfaceDestroyed(SurfaceHolder holder) { Log.d("Surface", "Surface destroyed"); stopPreview(); } // ======================== 萤石云设备预览方法 ======================== private void startEzCloudPreview() { try { mProgressBar.setVisibility(View.VISIBLE); // 1. 创建播放器 mEZPlayer = EZOpenSDK.getInstance().createPlayer(mDeviceSerial, mCameraNo); if (mEZPlayer == null) { handleError("创建播放器失败", -1); return; } // 2. 设置Handler回调 mEZPlayer.setHandler(mEzHandler); // 3. 设置显示区域 mEZPlayer.setSurfaceHold(mSurfaceHolder); // 4. 设置视频加密密码(如果需要) if (mVerifyCode != null && !mVerifyCode.isEmpty()) { mEZPlayer.setPlayVerifyCode(mVerifyCode); Log.d("EZPlay", "设置视频加密密码: " + mVerifyCode); } // 5. 启动直播 mEZPlayer.startRealPlay(); Log.d("EZPlay", "开始萤石云预览"); } catch (Exception e) { handleError("萤石云预览失败: " + e.getMessage(), -1); Log.e("EZPlay", "启动失败", e); } } // 显示验证码输入对话框 private void showVerifyCodeDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("需要验证码"); builder.setMessage("请输入设备的验证码"); final EditText input = new EditText(this); input.setHint("6位设备验证码"); builder.setView(input); builder.setPositiveButton("确定", (dialog, which) -> { String verifyCode = input.getText().toString(); if (verifyCode.length() == 6) { mVerifyCode = verifyCode; if (mEZPlayer != null) { // 设置新验证码 mEZPlayer.setPlayVerifyCode(verifyCode); // 停止播放 mEZPlayer.stopRealPlay(); // 重新开始播放 mEZPlayer.startRealPlay(); // 显示进度条 mProgressBar.setVisibility(View.VISIBLE); } } else { Toast.makeText(this, "验证码必须为6位数字", Toast.LENGTH_SHORT).show(); } }); builder.setNegativeButton("取消", (dialog, which) -> dialog.cancel()); builder.show(); } // ======================== 萤石云控制方法 ======================== private void controlPTZ(String direction) { if (mEZPlayer == null) return; try { EZConstants.EZPTZAction action; switch (direction) { case "UP": action = EZConstants.EZPTZAction.EZPTZActionUp; break; case "DOWN": action = EZConstants.EZPTZAction.EZPTZActionDown; break; case "LEFT": action = EZConstants.EZPTZAction.EZPTZActionLeft; break; case "RIGHT": action = EZConstants.EZPTZAction.EZPTZActionRight; break; default: return; } // 开始控制 mEZPlayer.controlPTZ(action, EZConstants.EZPTZCommand.EZPTZCommandStart, 2); // 300ms后停止控制 new Handler().postDelayed(() -> { if (mEZPlayer != null) { mEZPlayer.controlPTZ(EZConstants.EZPTZAction.EZPTZActionStop, EZConstants.EZPTZCommand.EZPTZCommandStop, 0); } }, 300); } catch (Exception e) { Log.e("EZPTZ", "云台控制异常", e); } } private void controlZoom(String command) { if (mEZPlayer == null) return; try { EZConstants.EZPTZAction action = "ZOOM_IN".equals(command) ? EZConstants.EZPTZAction.EZPTZActionZoomIn : EZConstants.EZPTZAction.EZPTZActionZoomOut; // 开始控制 mEZPlayer.controlPTZ(action, EZConstants.EZPTZCommand.EZPTZCommandStart, 2); // 300ms后停止控制 new Handler().postDelayed(() -> { if (mEZPlayer != null) { mEZPlayer.controlPTZ(EZConstants.EZPTZAction.EZPTZActionStop, EZConstants.EZPTZCommand.EZPTZCommandStop, 0); } }, 300); } catch (Exception e) { Log.e("EZZoom", "变焦控制异常", e); } } // ======================== 萤石云扩展功能 ======================== // 开始本地录像 private void startLocalRecord() { if (mEZPlayer != null) { String recordPath = getExternalFilesDir(null) + "/record_" + System.currentTimeMillis() + ".mp4"; mEZPlayer.startLocalRecordWithFile(recordPath); Toast.makeText(this, "开始录像: " + recordPath, Toast.LENGTH_SHORT).show(); } } // 停止本地录像 private void stopLocalRecord() { if (mEZPlayer != null) { mEZPlayer.stopLocalRecord(); Toast.makeText(this, "录像已停止", Toast.LENGTH_SHORT).show(); } } // 截图 private void capturePicture() { if (mEZPlayer != null) { String imagePath = getExternalFilesDir(null) + "/capture_" + System.currentTimeMillis() + ".jpg"; mEZPlayer.capturePicture(imagePath); Toast.makeText(this, "截图已保存: " + imagePath, Toast.LENGTH_SHORT).show(); } } // 切换视频清晰度 private void changeVideoLevel(int level) { if (mEZPlayer != null) { EZOpenSDK.getInstance().setVideoLevel(mDeviceSerial, mCameraNo, level); // 需要重启播放 mEZPlayer.stopRealPlay(); mEZPlayer.startRealPlay(); } } // ======================== 通用功能方法 ======================== public void changeScreen(View view) { changeScreen(); } private void changeScreen() { if (mControlLayout.getVisibility() == View.VISIBLE) { mControlLayout.setVisibility(View.GONE); } else { mControlLayout.setVisibility(View.VISIBLE); } } private void handleError(String message, int errorCode) { String fullMessage = message + " (错误码: " + errorCode + ")"; // 萤石云特有错误码处理 switch (errorCode) { case 400001: fullMessage = "AccessToken无效"; break; case 400002: fullMessage = "设备不存在"; break; case 400007: fullMessage = "设备不在线"; break; case 400034: fullMessage = "验证码错误"; break; case 400035: fullMessage = "设备已被自己添加"; break; case 400036: fullMessage = "设备已被别人添加"; break; default: fullMessage = "萤石云错误: " + errorCode; } new AlertDialog.Builder(this) .setTitle("预览失败") .setMessage(fullMessage) .setPositiveButton("确定", (d, w) -> finish()) .setCancelable(false) .show(); } private void showErrorAndFinish(String message) { Toast.makeText(this, message, Toast.LENGTH_LONG).show(); new Handler().postDelayed(this::finish, 3000); } private void stopPreview() { if (mEZPlayer != null) { try { mEZPlayer.stopRealPlay(); mEZPlayer.release(); } catch (Exception e) { Log.e("EZStop", "停止预览异常", e); } mEZPlayer = null; } Log.d("Preview", "预览已停止"); } @Override protected void onDestroy() { super.onDestroy(); stopPreview(); // 移除Handler回调 if (mEzHandler != null) { mEzHandler.removeCallbacksAndMessages(null); } } @Override protected void onPause() { super.onPause(); if (mEZPlayer != null) { mEZPlayer.stopRealPlay(); Log.d("EZPlay", "暂停时停止播放"); } } @Override protected void onResume() { super.onResume(); if (mEZPlayer != null && mSurfaceHolder != null) { try { mEZPlayer.setSurfaceHold(mSurfaceHolder); mEZPlayer.startRealPlay(); Log.d("EZPlay", "恢复时重新开始播放"); } catch (Exception e) { Log.e("EZResume", "恢复播放失败", e); } } } @Override public void onBackPressed() { DrawerLayout drawer = findViewById(R.id.drawer_layout); if (drawer != null && drawer.isDrawerOpen(GravityCompat.START)) { drawer.closeDrawer(GravityCompat.START); } else { super.onBackPressed(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main_opt, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.action_1) { startActivity(new Intent(this, AddDevActivity.class)); return true; } return super.onOptionsItemSelected(item); } }

filetype

<template>
<el-row :gutter="20"> <splitpanes :horizontal="appStore.device === 'mobile'" class="default-theme"> <pane size="16"> <el-col>
<el-input v-model="materiaTypelName" placeholder="请输入物料类型名称" clearable prefix-icon="Search" style="margin-bottom: 20px"/>
<el-tree :data="typeOptions" :props="{ label: 'label', children: 'children' }" :expand-on-click-node="false" :filter-node-method="filterNode" ref="treeRef" node-key="id" highlight-current default-expand-all @node-click="handleNodeClick"/>
</el-col> </pane> <pane size="84"> <el-col> <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px"> <el-form-item label="编号" prop="number"> <el-input v-model="queryParams.number" placeholder="请输入编号" clearable @keyup.enter.native="handleQuery" style="width: 240px"/> </el-form-item> <el-form-item label="名称" prop="name"> <el-input v-model="queryParams.name" placeholder="请输入名称" clearable @keyup.enter.native="handleQuery" style="width: 240px"/> </el-form-item> <el-form-item label="状态" prop="status"> <el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 240px"> <el-option v-for="dict in mdm_common_status_list" :key="dict.value" :label="dict.label" :value="dict.value"/> </el-select> </el-form-item> <el-form-item> <el-button type="primary" icon="search" @click="handleQuery">搜索</el-button> <el-button icon="refresh" @click="resetQuery">重置</el-button> </el-form-item> </el-form> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['mdm:typeAttr:add']" >新增 </el-button> </el-col> <el-col :span="1.5"> <el-button type="success" plain icon="edit" :disabled="single" @click="handleUpdate" v-hasPermi="['mdm:typeAttr:edit']" >修改 </el-button> </el-col> <el-col :span="1.5"> <el-button type="danger" plain icon="delete" :disabled="multiple" @click="handleDelete" v-hasPermi="['mdm:typeAttr:remove']" >删除 </el-button> </el-col> <el-col :span="1.5"> <el-button type="warning" plain icon="download" @click="handleExport" v-hasPermi="['mdm:typeAttr:export']" >导出 </el-button> </el-col> <el-col :span="1.5"> <el-button type="info" plain icon="upload" @click="handleImport" v-hasPermi="['mdm:typeAttr:import']" >导入 </el-button> </el-col> <el-col :span="1.5"> <el-button type="warning" plain icon="edit" @click="handleUrlUpdate" v-hasPermi="['mdm:mdmMaterialType:edit']"> 编辑命名规则 </el-button> </el-col> <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar> </el-row> <el-table v-loading="loading" :data="typeAttrList" @selection-change="handleSelectionChange"> <el-table-column type="selection" width="45" align="center" fixed/> <el-table-column label="编号" align="center" prop="number"/> <el-table-column label="名称" align="center" prop="name"/> <el-table-column label="类型名称" align="center" prop="typemgrName" width="140"/> <el-table-column label="类型编号" align="center" prop="typemgrNum" width="140"/> <el-table-column label="字段类型" align="center" prop="dataType"> <template #default="scope"> <dict-tag :options="mdm_common_column_type" :value="scope.row.dataType"/> </template> </el-table-column> <el-table-column label="文本最大长度" align="center" prop="length" width="120"/> <el-table-column label="数值最大值" align="center" prop="maxVal" width="120"/> <el-table-column label="数值最小值" align="center" prop="minVal" width="120"/> <el-table-column label="默认值" align="center" prop="defaultVal"/> <el-table-column label="枚举ID" align="center" prop="enumerateId"/> <el-table-column label="枚举表" align="center" prop="enumerateTable"/> <el-table-column label="单位" align="center" prop="unit"/> <el-table-column label="是否必填" align="center" prop="isNull"> <template #default="scope"> <dict-tag :options="mdm_common_flag_list" :value="scope.row.isNull"/> </template> </el-table-column> <el-table-column label="弹窗编辑器ID" align="center" prop="popupEditId" width="120"/> <el-table-column label="URL地址" align="center" prop="href"/> <el-table-column label="是否可查询" align="center" prop="isQuery" width="120"> <template #default="scope"> <dict-tag :options="mdm_common_flag_list" :value="scope.row.isQuery"/> </template> </el-table-column> <el-table-column label="是否表单显示" align="center" prop="isShowForm" width="120"> <template #default="scope"> <dict-tag :options="mdm_common_flag_list" :value="scope.row.isShowForm"/> </template> </el-table-column> <el-table-column label="是否列表显示" align="center" prop="isShowList" width="120"> <template #default="scope"> <dict-tag :options="mdm_common_flag_list" :value="scope.row.isShowList"/> </template> </el-table-column> <el-table-column label="是否只读" align="center" prop="isReadOnly"> <template #default="scope"> <dict-tag :options="mdm_common_flag_list" :value="scope.row.isReadOnly"/> </template> </el-table-column> <el-table-column label="表单排序" align="center" prop="orderNum"/> <el-table-column label="是否支持排序" align="center" prop="sortFlag" width="120"> <template #default="scope"> <dict-tag :options="mdm_common_flag_list" :value="scope.row.sortFlag"/> </template> </el-table-column> <el-table-column label="数值是否有公差" align="center" prop="isTolerance" width="120"> <template #default="scope"> <dict-tag :options="mdm_common_flag_list" :value="scope.row.isTolerance"/> </template> </el-table-column> <el-table-column label="正则表达式校验" align="center" prop="regularCheck" width="120"/> <el-table-column label="状态" align="center" prop="status"> <template #default="scope"> <dict-tag :options="mdm_common_status_list" :value="scope.row.status"/> </template> </el-table-column> <el-table-column label="合法值" align="center" prop="validStr" width="150"/> <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="240" fixed="right"> <template #default="scope"> <el-button link icon="edit" type="primary" @click="handleUpdate(scope.row)" v-hasPermi="['mdm:typeAttr:edit']" >修改 </el-button> <el-button link type="primary" icon="delete" @click="handleDelete(scope.row)" v-hasPermi="['mdm:typeAttr:remove']" >删除 </el-button> <el-button link type="primary" icon="edit" @click="eidtValid(scope.row)" v-hasPermi="['mdm:typeAttr:remove']" >编辑合法值 </el-button> </template> </el-table-column> </el-table> <pagination v-show="total>0" :total="total" v-model:page="queryParams.pageNum" v-model::limit="queryParams.pageSize" @pagination="getList"/> </el-col> </pane> </splitpanes> </el-row> <el-dialog :title="title" v-model="open" width="850px" append-to-body> <el-form :model="form" :rules="rules" ref="formRef" label-width="120px"> <el-row> <el-col :span="12"> <el-form-item label="编号" prop="number"> <el-input v-model="form.number" placeholder="请输入编号" style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="名称" prop="name"> <el-input v-model="form.name" placeholder="请输入名称" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="类型ID" prop="typemgrId"> <el-tree-select v-model="form.typemgrId" :data="typeOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" :current-node-key="form.typemgrId" placeholder="请选择物料类型" check-strictly style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="字段类型" prop="dataType"> <el-select v-model="form.dataType" placeholder="请选择字段类型" style="width: 240px"> <el-option v-for="dict in mdm_common_column_type" :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="文本最大长度" prop="length"> <el-input v-model="form.length" placeholder="请输入文本最大长度" style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="数值最大值" prop="maxVal"> <el-input v-model="form.maxVal" placeholder="请输入数值最大值" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="数值最小值" prop="minVal"> <el-input v-model="form.minVal" placeholder="请输入数值最小值" style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="默认值" prop="defaultVal"> <el-input v-model="form.defaultVal" placeholder="请输入默认值" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="枚举ID" prop="enumerateId"> <el-input v-model="form.enumerateId" placeholder="请输入枚举ID" style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="正则表达式校验" prop="regularCheck"> <el-input v-model="form.regularCheck" placeholder="请输入正则表达式校验" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="枚举表" prop="enumerateTable"> <el-input v-model="form.enumerateTable" placeholder="请输入枚举表" style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="单位" prop="unit"> <el-input v-model="form.unit" placeholder="请输入单位" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="是否必填" prop="isNull"> <el-select v-model="form.isNull" placeholder="请选择是否必填" style="width: 240px"> <el-option v-for="dict in mdm_common_flag_list" :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="是否可查询" prop="isQuery"> <el-select v-model="form.isQuery" placeholder="请选择是否可查询" style="width: 240px"> <el-option v-for="dict in mdm_common_flag_list" :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="是否表单显示" prop="isShowForm"> <el-select v-model="form.isShowForm" placeholder="请选择是否表单显示" style="width: 240px"> <el-option v-for="dict in mdm_common_flag_list " :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="是否列表显示" prop="isShowList"> <el-select v-model="form.isShowList" placeholder="请选择是否列表显示" style="width: 240px"> <el-option v-for="dict in mdm_common_flag_list" :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="是否只读" prop="isReadOnly"> <el-select v-model="form.isReadOnly" placeholder="请选择是否只读" style="width: 240px"> <el-option v-for="dict in mdm_common_flag_list" :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="是否支持排序" prop="sortFlag"> <el-select v-model="form.sortFlag" placeholder="请选择是否支持排序" style="width: 240px"> <el-option v-for="dict in mdm_common_flag_list" :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="数值是否有公差" prop="isTolerance"> <el-select v-model="form.isTolerance" placeholder="请选择数值是否有公差" style="width: 240px"> <el-option v-for="dict in mdm_common_flag_list" :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="表单排序" prop="orderNum"> <el-input v-model="form.orderNum" placeholder="请输入表单排序" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="弹窗编辑器ID" prop="popupEditId"> <el-input v-model="form.popupEditId" placeholder="请输入弹窗编辑器ID" style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="URL地址" prop="href"> <el-input v-model="form.href" placeholder="请输入URL地址" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="状态" prop="status"> <el-radio-group v-model="form.status"> <el-radio v-for="dict in mdm_common_status_list" :key="dict.value" :value="dict.value" >{{ dict.label }} </el-radio> </el-radio-group> </el-form-item> </el-col> </el-row> </el-form> <template #footer>
<el-button type="primary" @click="submitForm">确 定</el-button> <el-button @click="cancel">取 消</el-button>
</template> </el-dialog> <el-dialog title="修改命名规则" v-model="openUrledit" width="500px" append-to-body> <el-form ref="urlformRef" :model="urlform" :rules="urlEditrules" label-width="80px"> <el-form-item label="命名规则" prop="namingrule"> <el-input v-model="urlform.namingrule" placeholder="请输入命名规则"/> </el-form-item> </el-form> <template #footer>
<el-button type="primary" @click="submitFormNamingrule">确 定</el-button> <el-button @click="cancelSubmitUrlEdit">取 消</el-button>
</template> </el-dialog> <el-dialog :title="validtitle" v-model="validOpen" width="650px" append-to-body> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="primary" plain icon="plus" @click="handleValidAdd" v-hasPermi="['mdm:validlist:add']" >新增 </el-button> </el-col> <el-col :span="1.5"> <el-button type="success" plain icon="edit" :disabled="validsingle" @click="handleValidUpdate" v-hasPermi="['mdm:validlist:edit']" >修改 </el-button> </el-col> <el-col :span="1.5"> <el-button type="danger" plain icon="delete" :disabled="validMultiple" @click="handleValidDeleteBtn" v-hasPermi="['mdm:validlist:remove']" >删除 </el-button> </el-col> </el-row> <el-table v-loading="validTableLoading" :data="validlistList" @selection-change="handleValidSelectionChange"> <el-table-column type="selection" width="55" align="center"/> <el-table-column label="合法值名称" align="center" prop="name"/> <el-table-column label="排序" align="center" prop="orderNum"/> <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <template #default="scope"> <el-button link icon="edit" @click="handleValidUpdate(scope.row)" v-hasPermi="['mdm:validlist:edit']" >修改 </el-button> <el-button link icon="delete" @click="handleValidDelete(scope.row)" v-hasPermi="['mdm:validlist:remove']" >删除 </el-button> </template> </el-table-column> </el-table> <template #footer>
<el-button type="primary" @click="validSubmitForm">关 闭</el-button>
</template> </el-dialog> <el-dialog :title="validAddtitle" v-model="validAddOpen" width="500px" append-to-body> <el-form ref="validformRef" :model="validform" :rules="validRules" label-width="80px"> <el-form-item label="合法值名称" prop="name" label-width="95px"> <el-input v-model="validform.name" placeholder="请输入合法值名称"/> </el-form-item> <el-form-item label="排序" prop="orderNum" label-width="95px"> <el-input v-model="validform.orderNum" placeholder="请输入排序"/> </el-form-item> </el-form> <template #footer>
<el-button type="primary" @click="validAddSubmitForm">确 定</el-button> <el-button @click="validAddCancel">取 消</el-button>
</template> </el-dialog>
</template> <script setup name="typeAtrr"> import {listTypeAttr, getTypeAttr, delTypeAttr, addTypeAttr, updateTypeAttr} from "@/api/mdm/typeAttr"; import {getMdmMaterialType, updateMdmMaterialType} from "@/api/mdm/mdmMaterialType"; import {treeselect} from "@/api/mdm/mdmMaterialType"; import {listValidlist, getValidlist, delValidlist, addValidlist, updateValidlist} from "@/api/mdm/validlist"; import {getToken} from "@/utils/auth"; import "splitpanes/dist/splitpanes.css" import useAppStore from '@/store/modules/app' import {Splitpanes, Pane} from "splitpanes" const {proxy} = getCurrentInstance(); const appStore = useAppStore() // 使用字典 const { mdm_common_flag_list, mdm_common_status_list, mdm_common_column_type } = proxy.useDict("mdm_common_flag_list", "mdm_common_status_list", "mdm_common_column_type"); // 响应式状态 const loading = ref(true); const showSearch = ref(true); const open = ref(false); const validOpen = ref(false); const validAddOpen = ref(false); const openUrledit = ref(false); const title = ref(''); const validtitle = ref(''); const validAddtitle = ref(''); const total = ref(0); const single = ref(true); const multiple = ref(true); const validsingle = ref(false); const validMultiple = ref(true); const validTableLoading = ref(true); const materiaTypelName = ref(''); const typeOptions = ref([]); const typeAttrList = ref([]); const validlistList = ref([]); const queryFormRef = ref(null); const formRef = ref(null); const validformRef = ref(null); const urlformRef = ref(null); const uploadRef = ref(null); // 查询参数 const queryParams = reactive({ pageNum: 1, pageSize: 10, number: null, name: null, typemgrId: null, dataType: null, length: null, maxVal: null, minVal: null, defaultVal: null, validList: null, enumerateId: null, enumerateTable: null, unit: null, isNull: null, popupEditId: null, href: null, isQuery: null, isShowForm: null, isShowList: null, isReadOnly: null, orderNum: null, sortFlag: null, isTolerance: null, regularCheck: null, status: null, }); // 表单数据 const data = reactive({ form: { id: null, number: null, name: null, typemgrId: null, dataType: null, length: null, maxVal: null, minVal: null, defaultVal: null, validList: null, enumerateId: null, enumerateTable: null, unit: null, isNull: null, popupEditId: null, href: null, isQuery: null, isShowForm: null, isShowList: null, isReadOnly: null, orderNum: null, sortFlag: null, isTolerance: null, regularCheck: null, status: null }, //合法值 validform: { id: null, attributeId: null, name: null, orderNum: null, }, //命名规则 urlform: { id: null, namingrule: null, } }); const {form, validform, urlform} = toRefs(data); // 上传配置 const upload = reactive({ open: false, title: "", isUploading: false, updateSupport: 0, headers: {Authorization: "Bearer " + getToken()}, url: import.meta.env.VITE_APP_BASE_API + "/mdm/typeAttr/importData" }); // 选中ID集合 const ids = ref([]); const validIds = ref([]); // 当前选中属性行 const selectedAttrRow = ref(null); // 表单验证规则 const rules = reactive({ number: [{required: true, message: "编号不能为空", trigger: "blur"}], name: [{required: true, message: "名称不能为空", trigger: "blur"}], typemgrId: [{required: true, message: "类型ID不能为空", trigger: "blur"}], dataType: [{required: true, message: "字段类型不能为空", trigger: "change"}], }); const urlEditrules = reactive({ namingrule: [{required: true, message: "命名规则不能为空", trigger: "blur"}], }); const validRules = reactive({ name: [{required: true, message: "名称不能为空", trigger: "blur"}], }); // 监听物料类型名称变化 watch(materiaTypelName, (val) => { proxy.$refs["treeRef"].filter(val); }); // 获取列表 function getList() { console.log("========属性规则getList======"); loading.value = true; try { listTypeAttr(queryParams).then(response => { console.log("========属性规则getList======", response); typeAttrList.value = response.rows; total.value = response.total; }) } catch (error) { console.error('获取列表失败:', error); // ElMessage.error('获取数据失败'); } finally { loading.value = false; } }; function resetFormState() { form.value = { id: null, number: null, name: null, typemgrId: null, dataType: null, length: null, maxVal: null, minVal: null, defaultVal: null, validList: null, enumerateId: null, enumerateTable: null, unit: null, isNull: null, popupEditId: null, href: null, isQuery: null, isShowForm: null, isShowList: null, isReadOnly: null, orderNum: null, sortFlag: null, isTolerance: null, regularCheck: null, status: null }; proxy.resetForm("formRef"); }; function resetValidAddForm() { validform.value = { id: null, attributeId: null, name: null, orderNum: null } proxy.resetForm("validformRef"); }; function resetUrlForm() { urlform.value = { id: null, namingrule: null } proxy.resetForm("urlformRef"); }; function handleQuery() { queryParams.pageNum = 1; getList(); }; function resetQuery() { proxy.resetForm("queryForm"); handleQuery(); }; function handleSelectionChange(selection) { ids.value = selection.map(item => item.id); single.value = selection.length != 1; multiple.value = !selection.length; }; function handleAdd() { resetFormState(); // getTreeselect(); open.value = true; title.value = "添加属性规则"; }; function handleUpdate(row) { resetFormState(); // getTreeselect() const id = row.id || ids.value; getTypeAttr(id).then(response => { open.value = true; title.value = "修改属性规则"; form.value = response.data; }); }; function submitForm() { proxy.$refs["formRef"].validate(valid => { if (valid) { if (form.value.id != null) { updateTypeAttr(form.value).then(response => { proxy.$modal.msgSuccess("修改成功"); open.value = false; getList(); }); } else { // console.log(form.value); addTypeAttr(form.value).then(response => { proxy.$modal.msgSuccess("新增成功"); open.value = false; getList(); }); } } }) }; function cancel() { open.value = false; resetFormState(); }; function handleDelete(row) { // console.log('删除ids', ids.value); const delIds = row.id || ids.value; proxy.$modal.confirm('是否确认删除属性规则编号为"' + delIds + '"的数据项?').then(function () { return delTypeAttr(delIds); }).then(() => { getList(); proxy.$modal.msgSuccess("删除成功"); }).catch(() => { }); }; function handleExport() { proxy.download('mdm/typeAttr/export', { ...queryParams.value }, `typeAttr_${new Date().getTime()}.xlsx`) }; function filterNode(value, data) { if (!value) return true; return data.label.includes(value); }; function handleNodeClick(data) { queryParams.typemgrId = data.id; handleQuery(); }; function getTreeselect() { const response = treeselect().then(response => { console.log('获取树形数据成功:', response); typeOptions.value = response.data; }); }; function eidtValid(row) { selectedAttrRow.value = row; getValidDataList(row); }; function getValidDataList(row) { validOpen.value = true; validTableLoading.value = true; try { const query = { pageNum: 1, pageSize: 1000, attributeId: row.id }; listValidlist(query).then(response => { // console.log('获取合法值列表成功:', response); validlistList.value = response.rows; }); } catch (error) { // console.error('获取合法值列表失败:', error); // ElMessage.error('获取合法值列表失败'); } finally { validTableLoading.value = false; } }; function validSubmitForm() { validOpen.value = false; }; function handleValidAdd() { validAddOpen.value = true; validAddtitle.value = "添加合法值"; resetValidAddForm(); }; function validAddSubmitForm() { console.log('添加合法值', selectedAttrRow.value.id); validform.value.attributeId = selectedAttrRow.value.id proxy.$refs["validformRef"].validate(valid => { if (valid) { if (validform.value.id != null) { updateValidlist(validform.value).then(response => { proxy.$modal.msgSuccess("修改成功"); validAddOpen.value = false; getValidDataList(selectedAttrRow.value); }); } else { addValidlist(validform.value).then(response => { proxy.$modal.msgSuccess("新增成功"); validAddOpen.value = false; getValidDataList(selectedAttrRow.value); }); } } }); }; function validAddCancel() { validAddOpen.value = false; resetValidAddForm(); }; function handleValidSelectionChange(selection) { validIds.value = selection.map(item => item.id); validsingle.value = selection.length !== 1; validMultiple.value = !selection.length; }; function handleValidDeleteBtn() { const ids = validIds.value; proxy.$modal.confirm('是否确认删除合法值列表编号为"' + ids + '"的数据项?').then(function () { return delValidlist(ids); }).then(() => { proxy.$modal.msgSuccess("删除成功"); getValidDataList(selectedAttrRow.value); }).catch(() => { }); }; function handleValidDelete(row) { const ids = row.id; proxy.$modal.confirm('是否确认删除合法值列表编号为"' + ids + '"的数据项?').then(function () { return delValidlist(ids); }).then(() => { getValidDataListByAttrId(row.attributeId); proxy.$modal.msgSuccess("删除成功"); }).catch(() => { }); }; function getValidDataListByAttrId(attrId) { const query = { pageNum: 1, pageSize: 1000, attributeId: attrId }; try { listValidlist(query).then(response => { validlistList.value = response.rows; }); } catch (error) { console.error('获取合法值列表失败:', error); } }; function handleUrlUpdate() { // openUrledit.value = true; resetUrlForm(); // console.log('修改命名规则'); const currentNode = proxy.$refs.treeRef.getCurrentNode(); // const currentNode = treeRef.value.getCurrentNode(); if (!currentNode) { proxy.$modal.msgWarning("请先在左侧树中选择一个物料类型"); return; } console.log('进入到', currentNode.id); getMdmMaterialType(currentNode.id).then(response => { openUrledit.value = true; // console.log('获取数据成功:', response); urlform.value = response.data; }); }; function submitFormNamingrule() { proxy.$refs["urlformRef"].validate(valid => { if (valid) { if (urlform.value.id != null) { updateMdmMaterialType(urlform.value).then(response => { proxy.$modal.msgSuccess("修改成功"); openUrledit.value = false; }); } } }); }; function cancelSubmitUrlEdit() { openUrledit.value = false; resetUrlForm(); }; function handleImport() { upload.title = "导入属性"; upload.open = true; }; function importTemplate() { // 下载模板实现(需根据实际API调整) console.log('下载模板'); }; function handleFileUploadProgress() { upload.isUploading = true; }; function handleFileSuccess(response) { upload.open = false; upload.isUploading = false; if (uploadRef.value) { uploadRef.value.clearFiles(); } ElMessageBox.alert( `
${response.msg}
`, "导入结果", {dangerouslyUseHTMLString: true} ); getList(); }; function submitFileForm() { if (uploadRef.value) { uploadRef.value.submit(); } }; // 编辑合法值 function handleValidUpdate(row) { resetValidAddForm() const id = row.id || validIds.value getValidlist(id).then(response => { validform.value = response.data; validAddOpen.value = true; title.value = "修改合法值列表"; }); } getTreeselect() getList() </script> 只有首次能够进入 ,再次进入会是空白 ,而且其他菜单也显示不出来了

filetype

# -*- coding: utf-8 -*- import sys import os import cv2 import numpy as np from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QMessageBox, QLabel, QFileDialog, QToolBox, QComboBox, QStatusBar, QGroupBox, QSlider, QDockWidget, QProgressDialog) from PyQt5.QtCore import QRect, Qt, QSettings, QThread, pyqtSignal sys.path.append("D:\\海康\\MVS\\Development\\Samples\\Python\\BasicDemo") from MvCameraControl_class import * from MvErrorDefine_const import * from CameraParams_header import * from PyUICBasicDemo import Ui_MainWindow import ctypes from datetime import datetime import logging import platform # 配置日志系统 logging.basicConfig( level=logging.DEBUG, # 设置为DEBUG级别获取更多信息 format='%(asctime)s - %(name)s - %(levelname)s - %message)s', handlers=[ logging.FileHandler("cloth_inspection_debug.log"), logging.StreamHandler() ] ) logging.info("布料印花检测系统启动") # 全局变量 current_sample_path = "" # 当前使用的样本路径 detection_history = [] # 检测历史记录 # ==================== 核心功能类 ==================== class CameraOperation: def __init__(self, cam, device_list, index): self.cam = cam self.device_list = device_list self.index = index self.g_bFrameAvailable = False # 帧数据可用标志 self.current_frame = None # 当前帧数据 self.exposure_time = 0.0 # 曝光时间 self.gain = 0.0 # 增益 self.frame_rate = 0.0 # 帧率 def __getattr__(self, name): """处理未定义属性的访问""" raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") def is_frame_available(self): """检查是否有可用的帧数据""" return self.g_bFrameAvailable def get_current_frame(self): """获取当前帧数据""" return self.current_frame def open_device(self): """打开设备""" # 打开设备代码... # 在成功打开设备后设置初始状态 self.g_bFrameAvailable = False return MV_OK def start_grabbing(self, hwnd): """开始取流""" # 取流代码... # 在成功开始取流后设置标志 self.g_bFrameAvailable = True return MV_OK def Stop_grabbing(self): """停止取流""" # 停止取流代码... # 在停止取流后重置标志 self.g_bFrameAvailable = False return MV_OK def close_device(self): """关闭设备""" # 关闭设备代码... # 在关闭设备后重置标志 self.g_bFrameAvailable = False # 修复:self极_bFrameAvailable -> self.g_bFrameAvailable return MV_OK def get_one_frame(self): """获取一帧数据""" # 获取帧数据代码... # 在成功获取帧后更新当前帧 self.current_frame = frame_data self.g_bFrameAvailable = True return frame_data # 添加触发模式设置方法 def set_trigger_mode(self, is_trigger_mode): """ 设置相机触发模式 :param is_trigger_mode: True-触发模式, False-连续模式 :return: 状态码 """ try: # 设置触发模式 if is_trigger_mode: # 触发模式开启 ret = self.cam.MV_CC_SetEnumValue("TriggerMode", MV_TRIGGER_MODE_ON) if ret != MV_OK: logging.error(f"设置触发模式开启失败: {ToHexStr(ret)}") return ret # 设置触发源为软触发 ret = self.cam.MV_CC_SetEnumValue("TriggerSource", MV_TRIGGER_SOURCE_SOFTWARE) if ret != MV_OK: logging.error(f"设置软触发源失败: {ToHexStr(ret)}") return ret logging.info("已设置为触发模式(软触发)") else: # 触发模式关闭 ret = self.cam.MV_CC_SetEnumValue("TriggerMode", MV_TRIGGER_MODE_OFF) if ret != MV_OK: logging.error(f"设置触发模式关闭失败: {ToHexStr(ret)}") return ret logging.info极("已设置为连续模式") return MV_OK except Exception as e: logging.exception("设置触发模式时发生异常") return MV_E_ABNORMAL_IMAGE def trigger_once(self): """ 执行一次软触发 :return: 状态码 """ try: ret = self.cam.MV_极_CC_SetCommandValue("TriggerSoftware") if ret != MV_OK: logging.error(f"软触发失败: {ToHexStr(ret)}") else: logging.info("软触发成功") return ret except Exception as e: logging.exception("执行软触发时发生异常") return MV_E_ABNORMAL_IMAGE # 添加获取参数方法 def get_parameters(self): """ 获取相机参数(曝光时间、增益、帧率) :return: 状态码 """ try: # 获取曝光时间 st_float_param = MVCC_FLOATVALUE() ret = self.cam.MV_CC_GetFloatValue("ExposureTime", st_float_param) if ret != MV_OK: logging.error(f"获取曝光时间失败: {ToHexStr(ret)}") return ret self.exposure_time = st_float_param.fCurValue # 获取增益 st_float_param = MVCC_FLOATVALUE() ret = self.cam.MV_CC_GetFloatValue("Gain", st_float_param) if ret != MV_OK: logging.error(f"获取增益失败: {ToHexStr(ret)}") return ret self.gain = st_float_param.fCurValue # 获取帧率 st_float_param = MVCC_FLOATVALUE() ret = self.cam.MV_CC_GetFloatValue("AcquisitionFrameRate", st_float_param) if ret != MV_OK: logging.error(f"获取帧率失败: {ToHexStr(ret)}") return ret self.frame_rate = st_float_param.fCurValue logging.info(f"获取参数成功: 曝光={self.exposure_time} 增益={self.gain} 帧率={self.frame_rate}") return MV_OK except Exception as e: logging.exception("获取参数时发生异常") return MV_E_ABNORMAL_IMAGE # 添加设置参数方法 def set_param(self, frame_rate, exposure_time, gain): """ 设置相机参数 :param frame_rate: 帧率 :param exposure_time: 曝光时间 :param gain: 增益 :return: 状态码 """ try: # 设置帧率 ret = self.cam.MV_CC_SetFloatValue("AcquisitionFrameRate", frame_rate) if ret != MV_OK: logging.error(f"设置帧率失败: {ToHexStr(ret)}") return ret # 设置曝光时间 ret = self.cam.MV_CC_SetFloatValue("ExposureTime", exposure_time) if ret != MV_OK: logging.error(f"设置曝光时间失败: {ToHexStr(ret)}") return ret # 设置增益 ret = self.cam.MV_CC_SetFloatValue("Gain", gain) if ret != MV_OK: logging.error(f"设置增益失败: {ToHexStr(ret)}") return ret # 更新对象属性 self.frame_rate = frame_rate self.exposure_time = exposure_time self.gain = gain logging.info(f"参数设置成功: 帧率={frame_rate} 曝光={exposure_time} 增益={gain}") return MV_OK except Exception as e: logging.exception("设置参数时发生异常") return MV_E_ABNORMAL_IMAGE # 添加保存图像方法 def save_image(self, file_path, save_format): """ 保存当前帧到文件 :param file_path: 文件路径 :param save_format: 保存格式 ("bmp", "jpg", "png") :return: 状态码 """ try: if self.current_frame is None: logging.error("无法保存图像: 当前帧为空") return MV_E_NODATA # 根据格式保存图像 if save_format == "bmp": cv2.imwrite(file_path, self.current_frame) elif save_format == "jpg": cv2.imwrite(file_path, self.current_frame, [int(cv2.IMWRITE_JPEG_QUALITY), 95]) elif save_format == "png": cv2.imwrite(file_path, self.current_frame, [int(cv2.IMWRITE_PNG_COMPRESSION), 9]) else: logging.error(f"不支持的保存格式: {save_format}") return MV_E_UNSUPPORTED logging.info(f"图像保存成功: {file_path}") return MV_OK except Exception as e: logging.exception(f"保存图像时发生异常: {file_path}") return MV_E_ABNORMAL_IMAGE # ==================== 线程类 ==================== # 帧监控线程(修复版) class FrameMonitorThread(QThread): frame_status = pyqtSignal(str) def __init__(self, cam_operation): super().__init__() self.cam_operation = cam_operation self.running = True def run(self): while self.running: if self.cam_operation: # 使用正确的方法检查帧状态 frame_available = self.cam_operation.is_frame_available() frame_text = "有帧" if frame_available else "无帧" self.frame_status.emit(f"帧状态: {frame_text}") QThread.msleep(500) def stop(self): self.running = False # ==================== 核心检测功能 ==================== # 布料印花检测函数(修复版) def check_print_quality(sample_image_path, test_image, threshold=0.05): """ 检测布料印花是否合格,直接使用内存中的测试图像 :param sample_image_path: 合格样本图像路径 :param test_image: 内存中的测试图像 (numpy数组) :param threshold: 差异阈值 :return: 是否合格,差异值,标记图像 """ # 读取样本图像 try: sample_img_data = np.fromfile(sample_image_path, dtype=np.uint8) sample_image = cv2.imdecode(sample_img_data, cv2.IMREAD_GRAYSCALE) # 修复:cv2.READ_GRAYSCALE -> cv2.IMREAD_GRAYSCALE if sample_image is None: logging.error(f"无法解码样本图像: {sample_image_path}") return None, None, None except Exception as e: logging.exception(f"样本图像读取异常: {str(e)}") return None, None, None # 确保测试图像是灰度图 if len(test_image.shape) == 3: # 如果是彩色图像 test_image = cv2.cvtColor(test_image, cv2.COLOR_BGR2GRAY) # 确保两个图像大小一致 try: test_image = cv2.resize(test_image, (sample_image.shape[1], sample_image.shape[0])) except Exception as e: logging.error(f"图像调整大小失败: {str(e)}") return None, None, None # 计算差异 diff = cv2.absdiff(sample_image, test_image) # 二值化差异 _, thresholded = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY) # 计算差异比例 diff_pixels = np.count_nonzero(thresholded) total_pixels = sample_image.size diff_ratio = diff_pixels / total_pixels # 判断是否合格 is_qualified = diff_ratio <= threshold # 创建标记图像(红色标记差异区域) marked_image = cv2.cvtColor(test_image, cv2.COLOR_GRAY2BGR) marked_image[thresholded == 255] = [0, 0, 255] # 红色标记 return is_qualified, diff_ratio, marked_image # ==================== UI更新函数 ==================== # 更新检测结果显示 def update_diff_display(diff_ratio, is_qualified): """ 更新差异度显示控件 """ # 更新当前差异度显示 ui.lblCurrentDiff.setText(f"当前差异度: {diff_ratio*100:.2f}%") # 根据合格状态设置颜色 if is_qualified: ui.lblDiffStatus.setText("状态: 合格") ui.lblDiffStatus.setStyleSheet("color: green; font-size: 12px;") else: ui.lblDiffStatus.setText("状态: 不合格") ui.lblDiffStatus.setStyleSheet("color: red; font-size: 12px;") # 更新差异度阈值显示 def update_diff_threshold(value): """ 当滑块值改变时更新阈值显示 """ ui.lblDiffValue.setText(f"{value}%") # 布料印花检测功能(修复版) def check_print(): global isGrabbing, obj_cam_operation, current_sample_path, detection_history logging.info("检测印花质量按钮按下") # 1. 检查相机状态 if not isGrabbing: logging.warning("相机未取流") QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok) return # 2. 检查相机操作对象 if not obj_cam_operation: logging.error("相机操作对象未初始化") QMessageBox.warning(mainWindow, "错误", "相机未正确初始化!", QMessageBox.Ok) return # 3. 检查样本路径 if not current_sample_path or not os.path.exists(current_sample_path): logging.warning(f"无效样本路径: {current_sample_path}") QMessageBox.warning(mainWindow, "错误", "请先设置有效的标准样本图像!", QMessageBox.Ok) return # 使用进度对话框防止UI阻塞 progress = QProgressDialog("正在检测...", "取消", 0, 100, mainWindow) progress.setWindowModality(Qt.WindowModal) progress.setValue(10) try: # 4. 获取当前帧 logging.info("尝试获取当前极") test_image = obj_cam_operation.get_current_frame() progress.setValue(30) if test_image is None: logging.warning("获取当前帧失败") QMessageBox.warning(mainWindow, "错误", "无法获取当前帧图像!", QMessageBox.Ok) return # 5. 获取差异度阈值 diff_threshold = ui.sliderDiffThreshold.value() / 100.0 logging.info(f"使用差异度阈值: {diff_threshold}") progress.setValue(50) # 6. 执行检测 is_qualified, diff_ratio, marked_image = check_print_quality( current_sample_path, test_image, threshold=diff_threshold ) progress.setValue(70) # 检查返回结果是否有效 if is_qualified is None: logging.error("检测函数返回无效结果") QMessageBox.critical(mainWindow, "检测错误", "检测失败,请检查日志", QMessageBox.Ok) return logging.info(f"检测结果: 合格={is_qualified}, 差异={diff_ratio}") progress.setValue(90) # 7. 更新UI update_diff_display(diff_ratio, is_qualified) result_text = f"印花是否合格: {'合格' if is_qualified else '不合格'}\n差异占比: {diff_ratio*100:.2f}%\n阈值: {diff_threshold*100:.2f}%" QMessageBox.information(mainWindow, "检测结果", result_text, QMessageBox.Ok) if marked_image is not None: cv2.imshow("缺陷标记结果", marked_image) cv2.waitKey(0) cv2.destroyAllWindows() else: logging.warning("标记图像为空") # 8. 记录检测结果 detection_result = { 'timestamp': datetime.now(), 'qualified': is_qualified, 'diff_ratio': diff_ratio, 'threshold': diff_threshold } detection_history.append(detection_result) update_history_display() progress.setValue(100) except Exception as e: logging.exception("印花检测失败") QMessageBox.critical(mainWindow, "检测错误", f"检测过程中发生错误: {str(e)}", QMessageBox.Ok) finally: progress.close() # 保存标准样本函数 def save_sample_image(): global isGrabbing, obj_cam_operation, current_sample_path if not isGrabbing: QMessageBox.warning(mainWindow, "错误", "请先开始取流并捕获图像!", QMessageBox.Ok) return # 检查是否有有效图像 if not obj_cam_operation.is_frame_available(): QMessageBox.warning(mainWindow, "无有效图像", "未捕获到有效图像,请检查相机状态!", QMessageBox.Ok) return # 读取上次使用的路径 settings = QSettings("ClothInspection", "CameraApp") last_dir = settings.value("last_save_dir", os.path.join(os.getcwd(), "captures")) # 创建默认文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") default_filename = f"sample_{timestamp}" # 弹出文件保存对话框 file_path, selected_filter = QFileDialog.getSaveFileName( mainWindow, "保存标准样本图像", os.path.join(last_dir, default_filename), "BMP Files (*.bmp);;PNG Files (*.png);;JPEG Files (*.jpg);;所有文件 (*)", options=QFileDialog.DontUseNativeDialog ) if not file_path: logging.info("用户取消了图像保存操作") return # 用户取消保存 # 处理文件扩展名 file_extension = os.path.splitext(file_path)[1].lower() if not file_extension: # 根据选择的过滤器添加扩展名 if "BMP" in selected_filter: file_path += ".bmp" elif "PNG" in selected_filter: file_path += ".png" elif "JPEG" in selected_filter or "JPG" in selected_filter: file_path += ".jpg" else: # 默认使用BMP格式 file_path += ".bmp" file_extension = os.path.splitext(file_path)[1].lower() # 根据扩展名设置保存格式 format_mapping = { ".bmp": "bmp", ".png": "png", ".jpg": "jpg", ".jpeg": "jpg" } save_format = format_mapping.get(file_extension) if not save_format: QMessageBox.warning(mainWindow, "错误", "不支持的文件格式!", QMessageBox.Ok) return # 确保目录存在 directory = os.path.dirname(file_path) if directory and not os.path.exists(directory): try: os.makedirs(directory, exist_ok=True) logging.info(f"创建目录: {directory}") except OSError as e: error_msg = f"无法创建目录 {directory}: {str(e)}" QMessageBox.critical(mainWindow, "目录创建错误", error_msg, QMessageBox.Ok) return # 保存当前帧作为标准样本 try: ret = obj_cam_operation.save_image(file_path, save_format) if ret != MV_OK: strError = f"保存样本图像失败: {hex(ret)}" QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) else: success_msg = f"标准样本已保存至:\n{file_path}" QMessageBox.information(mainWindow, "成功", success_msg, QMessageBox.Ok) # 更新当前样本路径 current_sample_path = file_path update_sample_display() # 保存当前目录 settings.setValue("last_save_dir", os.path.dirname(file_path)) except Exception as e: error_msg = f"保存图像时发生错误: {str(e)}" QMessageBox.critical(mainWindow, "异常错误", error_msg, QMessageBox.Ok) logging.exception("保存样本图像时发生异常") # 预览当前样本 def preview_sample(): global current_sample_path if not current_sample_path or not os.path.exists(current_sample_path): QMessageBox.warning(mainWindow, "错误", "请先设置有效的标准样本图像!", QMessageBox.Ok) return try: # 使用安全方法读取图像 img_data = np.fromfile(current_sample_path, dtype=np.uint8) sample_img = cv2.imdecode(img_data, cv2.IMREAD_COLOR) if sample_img is None: raise Exception("无法加载图像") cv2.imshow("标准样本预览", sample_img) cv2.waitKey(0) cv2.destroyAllWindows() except Exception as e: QMessageBox.warning(mainWindow, "错误", f"预览样本失败: {str(e)}", QMessageBox.Ok) # 更新样本路径显示 def update_sample_display(): global current_sample_path if current_sample_path: ui.lblSamplePath.setText(f"当前样本: {os.path.basename(current_sample_path)}") ui.lblSamplePath.setToolTip(current_sample_path) ui.bnPreviewSample.setEnabled(True) else: ui.lblSamplePath.setText("当前样本: 未设置样本") ui.bnPreviewSample.setEnabled(False) # 更新历史记录显示 def update_history_display(): global detection_history ui.cbHistory.clear() for i, result in enumerate(detection_history[-10:]): # 显示最近10条记录 timestamp = result['timestamp'].strftime("%H:%M:%S") status = "合格" if result['qualified'] else "不合格" ratio = f"{result['diff_ratio']*100:.2f}%" ui.cbHistory.addItem(f"[{timestamp}] {status} - 差异: {ratio}") # 获取选取设备信息的索引,通过[]之间的字符去解析 def TxtWrapBy(start_str, end, all): start = all.find(start_str) if start >= 0: start += len(start_str) end = all.find(end, start) if end >= 0: return all[start:end].strip() # 将返回的错误码转换为十六进制显示 def ToHexStr(num): """将错误码转换为十六进制字符串""" # 处理非整数输入 if not isinstance(num, int): try: # 尝试转换为整数 num = int(num) except: # 无法转换时返回类型信息 return f"<非整数:{type(num)}>" chaDic = {10: 'a', 11: 'b', 12: 'c', 13: 'd', 14: 'e', 15: 'f'} hexStr = "" # 处理负数 if num < 0: num = num + 2 ** 32 # 转换为十六进制 while num >= 16: digit = num % 16 hexStr = chaDic.get(digit, str(digit)) + hexStr num //= 16 hexStr = chaDic.get(num, str(num)) + hexStr return "0x" + hexStr # ch:初始化SDK | en: initialize SDK MvCamera.MV_CC_Initialize() # 全局变量声明 global deviceList global cam global nSelCamIndex global obj_cam_operation global isOpen global isGrabbing global isCalibMode global frame_monitor_thread # 初始化全局变量 deviceList = MV_CC_DEVICE_INFO_LIST() cam = MvCamera() nSelCamIndex = 0 obj_cam_operation = None isOpen = False isGrabbing = False isCalibMode = True # 是否是标定模式(获取原始图像) frame_monitor_thread = None # 绑定下拉列表至设备信息索引 def xFunc(event): global nSelCamIndex nSelCamIndex = TxtWrapBy("[", "]", ui.ComboDevices.get()) # Decoding Characters def decoding_char(c_ubyte_value): c_char_p_value = ctypes.cast(c_ubyte_value, ctypes.c_char_p) try: decode_str = c_char_p_value.value.decode('gbk') # Chinese characters except UnicodeDecodeError: decode_str = str(c_char_p_value.value) return decode_str # ch:枚举相机 | en:enum devices def enum_devices(): global deviceList global obj_cam_operation deviceList = MV_CC_DEVICE_INFO_LIST() n_layer_type = (MV_GIGE_DEVICE | MV_USB_DEVICE | MV_GENTL_CAMERALINK_DEVICE | MV_GENTL_CXP_DEVICE | MV_GENTL_XOF_DEVICE) ret = MvCamera.MV_CC_EnumDevices(n_layer_type, deviceList) if ret != 0: strError = "Enum devices fail! ret = :" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) return ret if deviceList.nDeviceNum == 0: QMessageBox.warning(mainWindow, "Info", "Find no device", QMessageBox.Ok) return ret print("Find %d devices!" % deviceList.nDeviceNum) devList = [] for i in range(0, deviceList.nDeviceNum): # 修复:device.nDeviceNum -> deviceList.nDeviceNum mvcc_dev_info = cast(deviceList.pDeviceInfo[i], POINTER(MV_CC_DEVICE_INFO)).contents if mvcc_dev_info.nTLayerType == MV_GIGE_DEVICE or mvcc_dev_info.nTLayerType == MV_GENTL_GIGE_DEVICE: print("\ngige device: [%d]" % i) user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stGigEInfo.chUserDefinedName) model_name = decoding_char(mvcc_dev_info.SpecialInfo.stGigEInfo.chModelName) print("device user define name: " + user_defined_name) print("device model name: " + model_name) nip1 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0xff000000) >> 24) nip2 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x00ff0000) >> 16) nip3 = ((mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x0000ff00) >> 8) nip4 = (mvcc_dev_info.SpecialInfo.stGigEInfo.nCurrentIp & 0x000000ff) print("current ip: %d.%d.%d.%d " % (nip1, nip2, nip3, nip4)) devList.append( "[" + str(i) + "]GigE: " + user_defined_name + " " + model_name + "(" + str(nip1) + "." + str( nip2) + "." + str(nip3) + "." + str(nip4) + ")") elif mvcc_dev_info.nTLayerType == MV_USB_DEVICE: print("\nu3v device: [%d]" % i) user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stUsb3VInfo.chUserDefinedName) model_name = decoding_char(mvcc_dev_info.SpecialInfo.stUsb3VInfo.chModelName) print("device user define name: " + user_defined_name) print("device model name: " + model_name) strSerialNumber = "" for per in mvcc_dev_info.SpecialInfo.stUsb3VInfo.chSerialNumber: if per == 0: break strSerialNumber = strSerialNumber + chr(per) print("user serial number: " + strSerialNumber) devList.append("[" + str(i) + "]USB: " + user_defined_name + " " + model_name + "(" + str(strSerialNumber) + ")") elif mvcc_dev_info.nTLayerType == MV_GENTL_CAMERALINK_DEVICE: print("\nCML device: [%d]" % i) user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stCMLInfo.chUserDefinedName) model_name = decoding_char(mvcc_dev_info.SpecialInfo.stCMLInfo.chModelName) print("device user define name: " + user_defined_name) print("device model name: " + model_name) strSerialNumber = "" for per in mvcc_dev_info.SpecialInfo.stCMLInfo.chSerialNumber: if per == 0: break strSerialNumber = strSerialNumber + chr(per) print("user serial number: " + strSerialNumber) devList.append("[" + str(i) + "]CML: " + user_defined_name + " "+ model_name + "(" + str(strSerialNumber) + ")") elif mvcc_dev_info.nTLayerType == MV_GENTL_CXP_DEVICE: print("\nCXP device: [%d]" % i) user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stCXPInfo.chUserDefinedName) model_name = decoding_char(mvcc_dev_info.SpecialInfo.stCXPInfo.chModelName) print("device user define name: " + user_defined_name) print("device model name: " + model_name) strSerialNumber = "" for per in mvcc_dev_info.SpecialInfo.stCXPInfo.chSerialNumber: if per == 0: break strSerialNumber = strSerialNumber + chr(per) print("user serial number: "+strSerialNumber) devList.append("[" + str(i) + "]CXP: " + user_defined_name + " " + model_name + "(" + str(strSerialNumber) + ")") elif mvcc_dev_info.nTLayerType == MV_GENTL_XOF_DEVICE: print("\nXoF device: [%d]" % i) user_defined_name = decoding_char(mvcc_dev_info.SpecialInfo.stXoFInfo.chUserDefinedName) model_name = decoding_char(mvcc_dev_info.SpecialInfo.stXoFInfo.chModelName) print("device user define name: " + user_defined_name) print("device model name: " + model_name) strSerialNumber = "" for per in mvcc_dev_info.SpecialInfo.stXoFInfo.chSerialNumber: if per == 0: break strSerialNumber = strSerialNumber + chr(per) print("user serial number: " + strSerialNumber) devList.append("[" + str(i) + "]XoF: " + user_defined_name + " " + model_name + "(" + str(strSerialNumber) + ")") ui.ComboDevices.clear() ui.ComboDevices.addItems(devList) ui.ComboDevices.setCurrentIndex(0) # ch:打开相机 | en:open device def open_device(): global deviceList global nSelCamIndex global obj_cam_operation global isOpen global frame_monitor_thread if isOpen: QMessageBox.warning(mainWindow, "Error", 'Camera is Running!', QMessageBox.Ok) return MV_E_CALLORDER nSelCamIndex = ui.ComboDevices.currentIndex() if nSelCamIndex < 0: QMessageBox.warning(mainWindow, "Error", 'Please select a camera!', QMessageBox.Ok) return MV_E_CALLORDER obj_cam_operation = CameraOperation(cam, deviceList, nSelCamIndex) ret = obj_cam_operation.open_device() if 0 != ret: strError = "Open device failed ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) isOpen = False else: set_continue_mode() get_param() isOpen = True enable_controls() # 启动帧监控线程(修复版) frame_monitor_thread = FrameMonitorThread(obj_cam_operation) frame_monitor_thread.frame_status.connect(ui.statusBar.showMessage) frame_monitor_thread.start() # ch:开始取流 | en:Start grab image def start_grabbing(): global obj_cam_operation global isGrabbing ret = obj_cam_operation.start_grabbing(ui.widgetDisplay.winId()) if ret != 0: strError = "Start grabbing failed ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: isGrabbing = True enable_controls() # ch:停止取流 | en:Stop grab image def stop_grabbing(): global obj_cam_operation global isGrabbing ret = obj_cam_operation.Stop_grabbing() if ret != 0: strError = "Stop grabbing failed ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: isGrabbing = False enable_controls() # ch:关闭设备 | Close device def close_device(): global isOpen global isGrabbing global obj_cam_operation global frame_monitor_thread # 停止帧监控线程 if frame_monitor_thread and frame_monitor_thread.isRunning(): frame_monitor_thread.stop() frame_monitor_thread.wait(2000) if isOpen: obj_cam_operation.close_device() isOpen = False isGrabbing = False enable_controls() # ch:设置触发模式 | en:set trigger mode def set_continue_mode(): ret = obj_cam_operation.set_trigger_mode(False) if ret != 0: strError = "Set continue mode failed ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: ui.radioContinueMode.setChecked(True) ui.radioTriggerMode.setChecked(False) ui.bnSoftwareTrigger.setEnabled(False) # ch:设置软触发模式 | en:set software trigger mode def set_software_trigger_mode(): ret = obj_cam_operation.set_trigger_mode(True) if ret != 0: strError = "Set trigger mode failed ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) else: ui.radioContinueMode.setChecked(False) ui.radioTriggerMode.setChecked(True) ui.bnSoftwareTrigger.setEnabled(isGrabbing) # ch:设置触发命令 | en:set trigger software def trigger_once(): ret = obj_cam_operation.trigger_once() if ret != 0: strError = "TriggerSoftware failed ret:" + ToHexStr(ret) QMessageBox.warning(mainWindow, "Error", strError, QMessageBox.Ok) # 保存图像对话框 def save_image_dialog(): """ 打开保存图像对话框并保存当前帧 """ global isGrabbing, obj_cam_operation # 检查相机状态 if not isGrabbing: QMessageBox.warning(mainWindow, "相机未就绪", "请先开始取流并捕获图像!", QMessageBox.Ok) return # 检查是否有有效图像 if not obj_cam_operation.is_frame_available(): QMessageBox.warning(mainWindow, "无有效图像", "未捕获到有效图像,请检查相机状态!", QMessageBox.Ok) return # 读取上次使用的路径 settings = QSettings("ClothInspection", "CameraApp") last_dir = settings.value("last_save_dir", os.path.join(os.getcwd(), "captures")) # 创建默认文件名 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # 修复:%H%M%极 -> %H%M%S default_filename = f"capture_{timestamp}" # 弹出文件保存对话框 file_path, selected_filter = QFileDialog.getSaveFileName( mainWindow, "保存图像", os.path.join(last_dir, default_filename), # 初始路径 "BMP 图像 (*.bmp);;JPEG 图像 (*.jpg);;PNG 图像 (*.png);;TIFF 图像 (*.tiff);;所有文件 (*)", options=QFileDialog.DontUseNativeDialog ) # 用户取消操作 if not file_path: logging.info("用户取消了图像保存操作") return # 处理文件扩展名 file_extension = os.path.splitext(file_path)[1].lower() if not file_extension: # 根据选择的过滤器添加扩展名 if "BMP" in selected_filter: file_path += ".bmp" elif "JPEG" in selected_filter or "JPG" in selected_filter: file_path += ".jpg" elif "PNG" in selected_filter: file_path += ".png" elif "TIFF" in selected_filter: file_path += ".tiff" else: # 默认使用BMP格式 file_path += ".bmp" # 确定保存格式 format_mapping = { ".bmp": "bmp", ".jpg": "jpg", ".jpeg": "jpg", ".png": "png", ".tiff": "tiff", ".tif": "tiff" } file_extension = os.path.splitext(file_path)[1].lower() save_format = format_mapping.get(file_extension, "bmp") # 确保目录存在 directory = os.path.dirname(file_path) if directory and not os.path.exists(directory): try: os.makedirs(directory, exist_ok=True) except OSError as e: QMessageBox.critical(mainWindow, "目录错误", f"无法创建目录:\n{str(e)}", QMessageBox.Ok) return # 保存图像 try: ret = obj_cam_operation.save_image(file_path, save_format) if ret == MV_OK: QMessageBox.information(mainWindow, "保存成功", f"图像已保存至:\n{file_path}", QMessageBox.Ok) logging.info(f"图像保存成功: {file_path}") # 保存当前目录 settings.setValue("last_save_dir", os.path.dirname(file_path)) else: error_msg = f"保存失败! 错误代码: {hex(ret)}" QMessageBox.warning(mainWindow, "保存失败", error_msg, QMessageBox.Ok) logging.error(f"图像保存失败: {file_path}, 错误代码: {hex(ret)}") except Exception as e: QMessageBox.critical(mainWindow, "保存错误", f"保存图像时发生错误:\n{str(e)}", QMessageBox.Ok) logging.exception(f"保存图像时发生异常: {file_path}") def is_float(str): try: float(str) return True except ValueError: return False # ch: 获取参数 | en:get param def get_param(): try: # 调用方法获取参数 ret = obj_cam_operation.get_parameters() # 记录调用结果(调试用) logging.debug(f"get_param() 返回: {ret} (类型: {type(ret)})") # 处理错误码 if ret != MV_OK: strError = "获取参数失败,错误码: " + ToHexStr(ret) QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) else: # 成功获取参数后更新UI ui.edtExposureTime.setText("{0:.2f}".format(obj_cam_operation.exposure_time)) ui.edtGain.setText("{0:.2f}".format(obj_cam_operation.gain)) ui.edtFrameRate.setText("{0:.2f}".format(obj_cam_operation.frame_rate)) # 记录成功信息 logging.info("成功获取相机参数") except Exception as e: # 处理所有异常 error_msg = f"获取参数时发生错误: {str(e)}" logging.error(error_msg) QMessageBox.critical(mainWindow, "严重错误", error_msg, QMessageBox.Ok) # ch: 设置参数 | en:set param def set_param(): frame_rate = ui.edtFrameRate.text() exposure = ui.edtExposureTime.text() gain = ui.edtGain.text() if not (is_float(frame_rate) and is_float(exposure) and is_float(gain)): strError = "设置参数失败: 参数必须是有效的浮点数" QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) return MV_E_PARAMETER try: # 使用正确的参数顺序和关键字 ret = obj_cam_operation.set_param( frame_rate=float(frame_rate), exposure_time=float(exposure), gain=float(gain) ) if ret != MV_OK: strError = "设置参数失败,错误码: " + ToHexStr(ret) QMessageBox.warning(mainWindow, "错误", strError, QMessageBox.Ok) else: logging.info("参数设置成功") return MV_OK except Exception as e: error_msg = f"设置参数时发生错误: {str(e)}" logging.error(error_msg) QMessageBox.critical(mainWindow, "严重错误", error_msg, QMessageBox.Ok) return MV_E_STATE # ch: 设置控件状态 | en:set enable status def enable_controls(): global isGrabbing global isOpen # 先设置group的状态,再单独设置各控件状态 ui.groupGrab.setEnabled(isOpen) ui.groupParam.setEnabled(isOpen) ui.bnOpen.setEnabled(not isOpen) ui.bnClose.setEnabled(isOpen) ui.bnStart.setEnabled(isOpen and (not isGrabbing)) ui.bnStop.setEnabled(isOpen and isGrabbing) ui.bnSoftwareTrigger.setEnabled(isGrabbing and ui.radioTriggerMode.isChecked()) ui.bnSaveImage.setEnabled(isOpen and isGrabbing) # 添加检测按钮控制 ui.bnCheckPrint.setEnabled(isOpen and isGrabbing) ui.bnSaveSample.setEnabled(isOpen and isGrabbing) ui.bnPreviewSample.setEnabled(bool(current_sample_path)) if __name__ == "__main__": # ch:初始化SDK | en: initialize SDK MvCamera.MV_CC_Initialize() # 初始化全局变量 deviceList = MV_CC_DEVICE_INFO_LIST() cam = MvCamera() nSelCamIndex = 0 obj_cam_operation = None isOpen = False isGrabbing = False isCalibMode = True # 是否是标定模式(获取原始图像) frame_monitor_thread = None # 初始化UI app = QApplication(sys.argv) mainWindow = QMainWindow() ui = Ui_MainWindow() ui.setupUi(mainWindow) # 扩大主窗口尺寸 mainWindow.resize(1200, 800) # 宽度1200,高度800 # 创建工具栏 toolbar = mainWindow.addToolBar("检测工具") # 添加检测按钮 ui.bnCheckPrint = QPushButton("检测印花质量") toolbar.addWidget(ui.bnCheckPrint) # 添加保存样本按钮 ui.bnSaveSample = QPushButton("保存标准样本") toolbar.addWidget(ui.bnSaveSample) # 添加预览样本按钮 ui.bnPreviewSample = QPushButton("预览样本") toolbar.addWidget(ui.bnPreviewSample) # 添加历史记录下拉框 ui.cbHistory = QComboBox() ui.cbHistory.setMinimumWidth(300) toolbar.addWidget(QLabel("历史记录:")) toolbar.addWidget(ui.cbHistory) # 添加当前样本显示标签 ui.lblSamplePath = QLabel("当前样本: 未设置样本") status_bar = mainWindow.statusBar() status_bar.addPermanentWidget(ui.lblSamplePath) # === 新增差异度调整控件 === # 创建右侧面板容器 right_panel = QWidget() right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(10, 10, 10, 10) # 创建差异度调整组 diff_group = QGroupBox("差异度调整") diff_layout = QVBoxLayout(diff_group) # 差异度阈值控制 ui.lblDiffThreshold = QLabel("差异度阈值 (0-100%):") ui.sliderDiffThreshold = QSlider(Qt.Horizontal) ui.sliderDiffThreshold.setRange(0, 100) # 0-100% ui.sliderDiffThreshold.setValue(5) # 默认5% ui.lblDiffValue = QLabel("5%") # 当前差异度显示 ui.lblCurrentDiff = QLabel("当前差异度: -") ui.lblCurrentDiff.setStyleSheet("font-size: 14px; font-weight: bold;") # 差异度状态指示器 ui.lblDiffStatus = QLabel("状态: 未检测") ui.lblDiffStatus.setStyleSheet("font-size: 12px;") # 布局控件 diff_layout.addWidget(ui.lblDiffThreshold) diff_layout.addWidget(ui.sliderDiffThreshold) diff_layout.addWidget(ui.lblDiffValue) diff_layout.addWidget(ui.lblCurrentDiff) diff_layout.addWidget(ui.lblDiffStatus) # 添加差异度组到右侧布局 right_layout.addWidget(diff_group) # 添加拉伸项使控件靠上 right_layout.addStretch(1) # 创建停靠窗口 dock = QDockWidget("检测控制面板", mainWindow) dock.setWidget(right_panel) dock.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable) mainWindow.addDockWidget(Qt.RightDockWidgetArea, dock) # === 差异度调整功能实现 === # 更新差异度阈值显示 def update_diff_threshold(value): ui.lblDiffValue.setText(f"{value}%") # 修复:ui.lbl极DiffValue -> ui.lblDiffValue # 连接滑块信号 ui.sliderDiffThreshold.valueChanged.connect(update_diff_threshold) # 更新检测结果显示 def update_diff_display(diff_ratio, is_qualified): # 更新当前差异度显示 ui.lblCurrentDiff.setText(f"当前差异度: {diff_ratio*100:.2f}%") # 根据合格状态设置颜色 if is_qualified: ui.lblDiffStatus.setText("状态: 合格") ui.lblDiffStatus.setStyleSheet("color: green; font-size: 12px;") else: ui.lblDiffStatus.setText("状态: 不合格") ui.lblDiffStatus.setStyleSheet("color: red; font-size: 12px;") # 绑定按钮事件 ui.bnCheckPrint.clicked.connect(check_print) ui.bnSaveSample.clicked.connect(save_sample_image) ui.bnPreviewSample.clicked.connect(preview_sample) # 绑定其他按钮事件 ui.bnEnum.clicked.connect(enum_devices) ui.bnOpen.clicked.connect(open_device) ui.bnClose.clicked.connect(close_device) ui.bnStart.clicked.connect(start_grabbing) ui.bnStop.clicked.connect(stop_grabbing) ui.bnSoftwareTrigger.clicked.connect(trigger_once) ui.radioTriggerMode.clicked.connect(set_software_trigger_mode) ui.radioContinueMode.clicked.connect(set_continue_mode) ui.bnGetParam.clicked.connect(get_param) ui.bnSetParam.clicked.connect(set_param) # 修改保存图像按钮连接 ui.bnSaveImage.clicked.connect(save_image_dialog) # 显示主窗口 mainWindow.show() # 执行应用 app.exec_() # 关闭设备 close_device() # ch:反初始化SDK | en: finalize SDK MvCamera.MV_CC_Finalize() sys.exit() 把你刚刚的解决方案用来改进这个代码并完整展示

filetype

<template>
<el-row :gutter="20"> <el-col :span="4" :xs="24">
<el-input v-model="materiaTypelName" placeholder="请输入物料类型名称" clearable size="small" prefix-icon="el-icon-search" style="margin-bottom: 20px" />
<el-tree :data="typeOptions" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode" ref="tree" default-expand-all highlight-current @node-click="handleNodeClick" />
</el-col> <el-col :span="20" :xs="24"> <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px"> <el-form-item label="编号" prop="number"> <el-input v-model="queryParams.number" placeholder="请输入编号" clearable @keyup.enter.native="handleQuery" /> </el-form-item> <el-form-item label="名称" prop="name"> <el-input v-model="queryParams.name" placeholder="请输入名称" clearable @keyup.enter.native="handleQuery" /> </el-form-item> <el-form-item label="状态" prop="status"> <el-select v-model="queryParams.status" placeholder="请选择状态" clearable> <el-option v-for="dict in dict.type.mdm_common_status_list" :key="dict.value" :label="dict.label" :value="dict.value" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> </el-form-item> </el-form> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['mdm:typeAttr:add']" >新增 </el-button> </el-col> <el-col :span="1.5"> <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['mdm:typeAttr:edit']" >修改 </el-button> </el-col> <el-col :span="1.5"> <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['mdm:typeAttr:remove']" >删除 </el-button> </el-col> <el-col :span="1.5"> <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['mdm:typeAttr:export']" >导出 </el-button> </el-col> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> </el-row> <el-table v-loading="loading" :data="typeAttrList" @selection-change="handleSelectionChange"> <el-table-column type="selection" width="45" align="center" fixed/> <el-table-column label="编号" align="center" prop="number"/> <el-table-column label="名称" align="center" prop="name"/> <el-table-column label="类型名称" align="center" prop="typemgrName" width="140"/> <el-table-column label="类型编号" align="center" prop="typemgrNum" width="140"/> <el-table-column label="字段类型" align="center" prop="dataType"> <template slot-scope="scope"> <dict-tag :options="dict.type.mdm_common_column_type" :value="scope.row.dataType"/> </template> </el-table-column> <el-table-column label="文本最大长度" align="center" prop="length" width="120"/> <el-table-column label="数值最大值" align="center" prop="maxVal" width="120"/> <el-table-column label="数值最小值" align="center" prop="minVal" width="120"/> <el-table-column label="默认值" align="center" prop="defaultVal"/> <el-table-column label="合法值列表" align="center" prop="validList" width="120"/> <el-table-column label="枚举ID" align="center" prop="enumerateId"/> <el-table-column label="枚举表" align="center" prop="enumerateTable"/> <el-table-column label="单位" align="center" prop="unit"/> <el-table-column label="是否必填" align="center" prop="isNull"> <template slot-scope="scope"> <dict-tag :options="dict.type.mdm_common_flag_list" :value="scope.row.isNull"/> </template> </el-table-column> <el-table-column label="弹窗编辑器ID" align="center" prop="popupEditId" width="120"/> <el-table-column label="URL地址" align="center" prop="href"/> <el-table-column label="是否可查询" align="center" prop="isQuery" width="120"> <template slot-scope="scope"> <dict-tag :options="dict.type.mdm_common_flag_list" :value="scope.row.isQuery"/> </template> </el-table-column> <el-table-column label="是否表单显示" align="center" prop="isShowForm" width="120"> <template slot-scope="scope"> <dict-tag :options="dict.type.mdm_common_flag_list" :value="scope.row.isShowForm"/> </template> </el-table-column> <el-table-column label="是否列表显示" align="center" prop="isShowList" width="120"> <template slot-scope="scope"> <dict-tag :options="dict.type.mdm_common_flag_list" :value="scope.row.isShowList"/> </template> </el-table-column> <el-table-column label="是否只读" align="center" prop="isReadOnly"> <template slot-scope="scope"> <dict-tag :options="dict.type.mdm_common_flag_list" :value="scope.row.isReadOnly"/> </template> </el-table-column> <el-table-column label="表单排序" align="center" prop="orderNum"/> <el-table-column label="是否支持排序" align="center" prop="sortFlag" width="120"> <template slot-scope="scope"> <dict-tag :options="dict.type.mdm_common_flag_list" :value="scope.row.sortFlag"/> </template> </el-table-column> <el-table-column label="数值是否有公差" align="center" prop="isTolerance" width="120"> <template slot-scope="scope"> <dict-tag :options="dict.type.mdm_common_flag_list" :value="scope.row.isTolerance"/> </template> </el-table-column> <el-table-column label="正则表达式校验" align="center" prop="regularCheck" width="120"/> <el-table-column label="状态" align="center" prop="status"> <template slot-scope="scope"> <dict-tag :options="dict.type.mdm_common_status_list" :value="scope.row.status"/> </template> </el-table-column> <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="220" fixed="right"> <template slot-scope="scope"> <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['mdm:typeAttr:edit']" >修改 </el-button> <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['mdm:typeAttr:remove']" >删除 </el-button> <el-button size="mini" type="text" icon="el-icon-edit" @click="eidtValid(scope.row)" v-hasPermi="['mdm:typeAttr:remove']" >编辑合法值 </el-button> </template> </el-table-column> </el-table> <pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" /> <el-dialog :title="title" :visible.sync="open" width="850px" append-to-body destroy-on-close> <el-form ref="form" :model="form" :rules="rules" label-width="120px" label-position="right"> <el-row> <el-col :span="12"> <el-form-item label="编号" prop="number"> <el-input v-model="form.number" placeholder="请输入编号" style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="名称" prop="name"> <el-input v-model="form.name" placeholder="请输入名称" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="类型ID" prop="typemgrId"> <treeselect v-model="form.typemgrId" :options="typeOptions" :show-count="true" placeholder="请选择物料类型" style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="字段类型" prop="dataType"> <el-select v-model="form.dataType" placeholder="请选择字段类型" style="width: 240px"> <el-option v-for="dict in dict.type.mdm_common_column_type" :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="文本最大长度" prop="length"> <el-input v-model="form.length" placeholder="请输入文本最大长度" style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="数值最大值" prop="maxVal"> <el-input v-model="form.maxVal" placeholder="请输入数值最大值" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="数值最小值" prop="minVal"> <el-input v-model="form.minVal" placeholder="请输入数值最小值" style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="默认值" prop="defaultVal"> <el-input v-model="form.defaultVal" placeholder="请输入默认值" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="枚举ID" prop="enumerateId"> <el-input v-model="form.enumerateId" placeholder="请输入枚举ID" style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="正则表达式校验" prop="regularCheck"> <el-input v-model="form.regularCheck" placeholder="请输入正则表达式校验" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="枚举表" prop="enumerateTable"> <el-input v-model="form.enumerateTable" placeholder="请输入枚举表" style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="单位" prop="unit"> <el-input v-model="form.unit" placeholder="请输入单位" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="是否必填" prop="isNull"> <el-select v-model="form.isNull" placeholder="请选择是否必填" style="width: 240px"> <el-option v-for="dict in dict.type.mdm_common_flag_list " :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="是否可查询" prop="isQuery"> <el-select v-model="form.isQuery" placeholder="请选择是否可查询" style="width: 240px"> <el-option v-for="dict in dict.type.mdm_common_flag_list " :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="是否表单显示" prop="isShowForm"> <el-select v-model="form.isShowForm" placeholder="请选择是否表单显示" style="width: 240px"> <el-option v-for="dict in dict.type.mdm_common_flag_list " :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="是否列表显示" prop="isShowList"> <el-select v-model="form.isShowList" placeholder="请选择是否列表显示" style="width: 240px"> <el-option v-for="dict in dict.type.mdm_common_flag_list" :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="是否只读" prop="isReadOnly"> <el-select v-model="form.isReadOnly" placeholder="请选择是否只读" style="width: 240px"> <el-option v-for="dict in dict.type.mdm_common_flag_list" :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="是否支持排序" prop="sortFlag"> <el-select v-model="form.sortFlag" placeholder="请选择是否支持排序" style="width: 240px"> <el-option v-for="dict in dict.type.mdm_common_flag_list" :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="数值是否有公差" prop="isTolerance"> <el-select v-model="form.isTolerance" placeholder="请选择数值是否有公差" style="width: 240px"> <el-option v-for="dict in dict.type.mdm_common_flag_list" :key="dict.value" :label="dict.label" :value="dict.value" ></el-option> </el-select> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="表单排序" prop="orderNum"> <el-input v-model="form.orderNum" placeholder="请输入表单排序" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="弹窗编辑器ID" prop="popupEditId"> <el-input v-model="form.popupEditId" placeholder="请输入弹窗编辑器ID" style="width: 240px"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="URL地址" prop="href"> <el-input v-model="form.href" placeholder="请输入URL地址" style="width: 240px"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="12"> <el-form-item label="状态" prop="status"> <el-radio-group v-model="form.status"> <el-radio v-for="dict in dict.type.mdm_common_status_list" :key="dict.value" :label="dict.value" >{{ dict.label }} </el-radio> </el-radio-group> </el-form-item> </el-col> </el-row> </el-form>
<el-button type="primary" @click="submitForm">确 定</el-button> <el-button @click="cancel">取 消</el-button>
</el-dialog> </el-col> </el-row> <el-dialog :title="validtitle" :visible.sync="validOpen" width="650px" append-to-body> <el-row :gutter="10" class="mb8"> <el-col :span="1.5"> <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleValidAdd" v-hasPermi="['mdm:validlist:add']" >新增 </el-button> </el-col> <el-col :span="1.5"> <el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="validsingle" @click="handleValidUpdate" v-hasPermi="['mdm:validlist:edit']" >修改 </el-button> </el-col> <el-col :span="1.5"> <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="validMultiple" @click="handleValidDeleteBtn" v-hasPermi="['mdm:validlist:remove']" >删除 </el-button> </el-col> </el-row> <el-table v-loading="validTableLoading" :data="validlistList" @selection-change="handleValidSelectionChange"> <el-table-column type="selection" width="55" align="center"/> <el-table-column label="合法值名称" align="center" prop="name"/> <el-table-column label="排序" align="center" prop="orderNum"/> <el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <template slot-scope="scope"> <el-button size="mini" type="text" icon="el-icon-edit" @click="handleValidUpdate(scope.row)" v-hasPermi="['mdm:validlist:edit']" >修改 </el-button> <el-button size="mini" type="text" icon="el-icon-delete" @click="handleValidDelete(scope.row)" v-hasPermi="['mdm:validlist:remove']" >删除 </el-button> </template> </el-table-column> </el-table>
<el-button type="primary" @click="validSubmitForm">关 闭</el-button>
</el-dialog> <el-dialog :title="validAddtitle" :visible.sync="validAddOpen" width="500px" append-to-body> <el-form ref="validform" :model="validform" :rules="validRules" label-width="80px"> <el-form-item label="合法值名称" prop="name" label-width="95px"> <el-input v-model="validform.name" placeholder="请输入合法值名称"/> </el-form-item> <el-form-item label="排序" prop="orderNum" label-width="95px"> <el-input v-model="validform.orderNum" placeholder="请输入排序"/> </el-form-item> </el-form>
<el-button type="primary" @click="validAddSubmitForm">确 定</el-button> <el-button @click="validAddCancel">取 消</el-button>
</el-dialog>
</template> <script> import {listTypeAttr, getTypeAttr, delTypeAttr, addTypeAttr, updateTypeAttr} from "@/api/mdm/typeAttr"; import {treeselect} from "@/api/mdm/mdmMaterialType"; import Treeselect from "@riophae/vue-treeselect"; import "@riophae/vue-treeselect/dist/vue-treeselect.css"; import {listData, getData, delData, addData, updateData} from "@/api/system/dict/data"; import {listValidlist, getValidlist, delValidlist, addValidlist, updateValidlist} from "@/api/mdm/validlist"; export default { name: "TypeAttr", dicts: ['mdm_common_flag_list', 'mdm_common_status_list', 'mdm_common_column_type'], components: {Treeselect}, data() { return { // 遮罩层 loading: true, // 选中数组 ids: [], // 非单个禁用 single: true, // 非多个禁用 multiple: true, // 显示搜索条件 showSearch: true, // 总条数 total: 0, // 属性规则表格数据 typeAttrList: [], // 弹出层标题 title: "", // 是否显示弹出层 open: false, // 查询参数 queryParams: { pageNum: 1, pageSize: 10, number: null, name: null, typemgrId: null, dataType: null, length: null, maxVal: null, minVal: null, defaultVal: null, validList: null, enumerateId: null, enumerateTable: null, unit: null, isNull: null, popupEditId: null, href: null, isQuery: null, isShowForm: null, isShowList: null, isReadOnly: null, orderNum: null, sortFlag: null, isTolerance: null, regularCheck: null, status: null, }, // 表单参数 form: {}, // 表单校验 rules: { number: [ {required: true, message: "编号不能为空", trigger: "blur"} ], name: [ {required: true, message: "名称不能为空", trigger: "blur"} ], typemgrId: [ {required: true, message: "类型ID不能为空", trigger: "blur"} ], dataType: [ {required: true, message: "字段类型不能为空", trigger: "change"} ], }, validRules: { name: [ {required: true, message: "名称不能为空", trigger: "blur"} ], }, defaultProps: { children: "children", label: "label" }, typeOptions: undefined, materiaTypelName: undefined, //合法值列表字典值 validDataList: undefined, validDataListTotal: undefined, validLoading: true, // treeselectOptions: [], selectedValues: [],// 存储当前选中的 dictValue 数组 isInitSelection: false, validtitle: "", validOpen: false, validform: {}, // 合法值列表表格数据 validlistList: [], validTableLoading: true, validAddOpen: false, validAddtitle: "", //选中的属性ID selectedAttrId: null, //选中的属性行 selectedAttrRow: null, validIds: [], validMultiple: true, validsingle: false, }; }, watch: { // 根据名称筛选部门树 materiaTypelName(val) { this.$refs.tree.filter(val); } }, created() { this.getList(); this.getTreeselect() }, methods: { /** 查询属性规则列表 */ getList() { this.loading = true; listTypeAttr(this.queryParams).then(response => { this.typeAttrList = response.rows; this.total = response.total; this.loading = false; }); }, // 取消按钮 cancel() { this.open = false; this.reset(); }, // 表单重置 reset() { this.form = { id: null, number: null, name: null, typemgrId: null, dataType: null, length: null, maxVal: null, minVal: null, defaultVal: null, validList: null, enumerateId: null, enumerateTable: null, unit: null, isNull: null, popupEditId: null, href: null, isQuery: null, isShowForm: null, isShowList: null, isReadOnly: null, orderNum: null, sortFlag: null, isTolerance: null, regularCheck: null, status: null, createBy: null, createTime: null, updateBy: null, updateTime: null }; this.resetForm("form"); }, /** 搜索按钮操作 */ handleQuery() { this.queryParams.pageNum = 1; this.getList(); }, /** 重置按钮操作 */ resetQuery() { this.resetForm("queryForm"); this.handleQuery(); }, // 多选框选中数据 handleSelectionChange(selection) { this.ids = selection.map(item => item.id) this.single = selection.length !== 1 this.multiple = !selection.length }, /** 新增按钮操作 */ handleAdd() { this.reset(); this.getTreeselect(); this.open = true; this.title = "添加属性规则"; // this.getValidList() }, /** 修改按钮操作 */ handleUpdate(row) { this.reset(); const id = row.id || this.ids getTypeAttr(id).then(response => { this.form = response.data; // this.form = {}; // this.$set(this, 'form', response.data); // 强制设置响应式 console.log("点击修改按钮后:") console.log(this.form.validList) this.open = true; this.title = "修改属性规则"; // this.getValidList(); // this.$nextTick().then(() => { // this.initDefaultSelection() // }) }); }, /** 提交按钮 */ submitForm() { this.$refs["form"].validate(valid => { if (valid) { if (this.form.id != null) { updateTypeAttr(this.form).then(response => { this.$modal.msgSuccess("修改成功"); this.open = false; this.getList(); }); } else { addTypeAttr(this.form).then(response => { this.$modal.msgSuccess("新增成功"); this.open = false; this.getList(); }); } } }); }, /** 删除按钮操作 */ handleDelete(row) { const ids = row.id || this.ids; this.$modal.confirm('是否确认删除属性规则编号为"' + ids + '"的数据项?').then(function () { return delTypeAttr(ids); }).then(() => { this.getList(); this.$modal.msgSuccess("删除成功"); }).catch(() => { }); }, /** 导出按钮操作 */ handleExport() { this.download('mdm/typeAttr/export', { ...this.queryParams }, `typeAttr_${new Date().getTime()}.xlsx`) }, // 筛选节点 filterNode(value, data) { if (!value) return true; return data.label.indexOf(value) !== -1; }, // 节点单击事件 handleNodeClick(data) { this.queryParams.typemgrId = data.id; this.handleQuery(); }, /** 查询物料类型树结构 */ getTreeselect() { treeselect().then(response => { const originalData = response.data; this.typeOptions = response.data; }); }, /** 查询字典数据列表 */ getValidList() { console.log("进入字典初始化") this.validLoading = true; const query = { pageNum: 1, pageSize: 1000, dictType: "mdm_validlist_dict", } listData(query).then(response => { this.validDataList = response.rows; this.validDataListTotal = response.total; // 数据加载完成后使用 nextTick 确保 DOM 已渲染表格 this.$nextTick(() => { this.initDefaultSelection(); // 安全地初始化选中 }); this.validLoading = false; }); }, initDefaultSelection() { this.isInitSelection = true; if (this.form.validList == null) { return } const table = this.$refs.validTable; const selectedArray = this.form.validList.split(','); console.log("初始化字典数据:"); console.log(selectedArray); // 根据唯一标识(如 dictCode)查找要选中的行 // 遍历数据,匹配需要默认选中的行 this.selectedValues = []; this.validDataList.forEach(row => { if (selectedArray.includes(row.dictValue)) { table.toggleRowSelection(row, true); this.selectedValues.push(row.dictValue); } }); // 初始化完成后恢复标志位 this.$nextTick(() => { this.isInitSelection = false; }); }, // handleValidSelectionChange(selection) { // console.log("进入选中方法"); // if (this.isInitSelection) { // return; // } // console.log("进入选中方法"); // // 更新选中项的 dictValue 到 selectedValues 并同步到 form.validList // this.selectedValues = selection.map(item => item.dictValue); // this.form.validList = this.selectedValues.join(','); // console.log("选中后:"); // console.log(this.form.validList); // }, // 编辑合法值 handleValidUpdate(row) { this.resetValidAddForm() const id = row.id || this.validIds getValidlist(id).then(response => { this.validform = response.data; this.validAddOpen = true; this.title = "修改合法值列表"; }); }, //编辑合法值按钮 eidtValid(row) { this.selectedAttrRow = row this.getValidDataList(row); }, //获取合法值列表 getValidDataList(row) { this.validOpen = true; this.title = "编辑合法值"; this.validTableLoading = true; const query = { pageNum: 1, pageSize: 1000, attributeId: row.id } listValidlist(query).then(response => { this.validlistList = response.rows; this.total = response.total; this.validTableLoading = false; }); }, validSubmitForm() { this.validOpen = false; }, //新增合法值按钮操作 handleValidAdd() { this.validAddOpen = true; this.validAddtitle = "添加合法值"; this.resetValidAddForm(); }, //确定添加合法值按钮操作 validAddSubmitForm() { this.validform.attributeId = this.selectedAttrRow.id this.$refs["validform"].validate(valid => { if (valid) { if (this.validform.id != null) { updateValidlist(this.validform).then(response => { this.$modal.msgSuccess("修改成功"); this.validAddOpen = false; this.getValidDataList(this.selectedAttrRow); }); } else { addValidlist(this.validform).then(response => { this.$modal.msgSuccess("新增成功"); this.validAddOpen = false; this.getValidDataList(this.selectedAttrRow); }); } } }); // this.resetValidAddForm() }, validAddCancel() { this.validAddOpen = false; this.resetValidAddForm(); }, // 表单重置 resetValidAddForm() { this.validform = { id: null, attributeId: null, name: null, orderNum: null, createBy: null, createTime: null, updateBy: null, updateTime: null }; this.resetForm("validform"); }, /** 删除合法值按钮操作 */ handleValidDeleteBtn() { const ids = this.validIds; this.$modal.confirm('是否确认删除合法值列表编号为"' + ids + '"的数据项?').then(function () { return delValidlist(ids); }).then(() => { this.getValidDataList(this.selectedAttrRow); this.$modal.msgSuccess("删除成功"); }).catch(() => { }); }, /** 删除单个合法值按钮操作 */ handleValidDelete(row) { const ids = row.id; this.$modal.confirm('是否确认删除合法值列表编号为"' + ids + '"的数据项?').then(function () { return delValidlist(ids); }).then(() => { this.getValidDataListByAttrId(row.attributeId); this.$modal.msgSuccess("删除成功"); }).catch(() => { }); }, //获取合法值列表 getValidDataListByAttrId(attrId) { const query = { pageNum: 1, pageSize: 1000, attributeId: attrId } listValidlist(query).then(response => { this.validlistList = response.rows; this.total = response.total; // this.validTableLoading = false; }); }, // 多选框选中数据 handleValidSelectionChange(selection) { this.validIds = selection.map(item => item.id) this.validsingle = selection.length !== 1 this.validMultiple = !selection.length }, } }; </script> 转为vue3 语法

lzwml
  • 粉丝: 20
上传资源 快速赚钱