yuanxingsheji/施工操作台/图像处理2.html

3881 lines
189 KiB
HTML
Raw Permalink Normal View History

2025-07-08 10:16:29 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>风电叶片图像管理平台</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.27.2/dist/axios.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.15.8/lib/theme-chalk/index.css">
<script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.8/lib/index.js"></script>
<!-- 添加Docx模板生成库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/docx/7.1.0/docx.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
<style>
body {
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f7fa;
color: #333;
}
.app-container {
display: flex;
min-height: 100vh;
flex-direction: column;
}
.header {
background-color: #1a3e72;
color: white;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.header-title {
font-size: 20px;
padding: 15px 0;
font-weight: 500;
}
.menu-bar {
background-color: #2c4b82;
padding: 0 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.menu-item {
display: inline-block;
padding: 12px 20px;
color: #fff;
cursor: pointer;
position: relative;
transition: all 0.3s;
}
.menu-item:hover {
background-color: #3a5d9e;
}
.menu-item.active {
background-color: #3a5d9e;
color: #66b1ff;
}
.submenu {
position: absolute;
top: 100%;
left: 0;
background-color: #fff;
min-width: 160px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
z-index: 100;
border-radius: 4px;
display: none;
}
.menu-item:hover .submenu {
display: block;
}
.submenu-item {
padding: 10px 20px;
color: #606266;
cursor: pointer;
transition: all 0.3s;
}
.submenu-item:hover {
background-color: #ecf5ff;
color: #409EFF;
}
.main-content {
display: flex;
flex: 1;
padding: 20px;
flex-direction: column;
background-color: #f0f2f5;
}
.content-row {
display: flex;
flex: 1;
margin-bottom: 20px;
min-height: 0;
}
.project-tree-container {
width: 280px;
border: 1px solid #e6e6e6;
background-color: white;
padding: 15px;
margin-right: 20px;
overflow-y: auto;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
.image-viewer-container {
flex: 1;
border: 1px solid #e6e6e6;
background-color: white;
padding: 15px;
display: flex;
flex-direction: column;
position: relative;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
.image-list-container {
flex: 1;
border: 1px solid #e6e6e6;
background-color: white;
padding: 15px;
overflow-y: auto;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
}
.tree-title {
font-weight: bold;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 1px solid #eee;
color: #1a3e72;
font-size: 16px;
}
.image-toolbar {
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.image-display {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
border: 1px solid #eee;
background-color: #f9f9f9;
position: relative;
border-radius: 4px;
}
.image-preview {
max-width: 100%;
max-height: 100%;
transition: transform 0.3s;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
}
.empty-state {
color: #909399;
text-align: center;
padding: 20px;
}
.component-node {
display: flex;
align-items: center;
padding: 5px 0;
}
.component-icon {
margin-right: 8px;
color: #409EFF;
font-size: 16px;
}
.batch-actions {
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.import-btn {
margin-bottom: 15px;
}
.highlight-row {
background-color: #f0f7ff !important;
}
.wizard-actions {
margin-top: 20px;
text-align: right;
}
.wizard-step {
margin-bottom: 20px;
}
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 15px;
}
.image-thumbnail {
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 8px;
cursor: pointer;
transition: all .3s;
position: relative;
background-color: white;
}
.image-thumbnail:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px 0 rgba(0,0,0,.1);
border-color: #409EFF;
}
.image-thumbnail.selected-thumbnail {
border-color: #409EFF;
background-color: #f0f7ff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.image-thumbnail img {
width: 100%;
height: 100px;
object-fit: cover;
border-radius: 4px;
}
.image-thumbnail .name {
font-size: 13px;
margin-top: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
.display-toggle {
margin-left: 10px;
}
.temp-range {
display: flex;
align-items: center;
}
.temp-range .separator {
margin: 0 10px;
}
.context-menu {
position: absolute;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
border-radius: 4px;
padding: 5px 0;
z-index: 2000;
min-width: 150px;
}
.context-menu-item {
padding: 8px 20px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.context-menu-item:hover {
background-color: #ecf5ff;
color: #409EFF;
}
.info-tabs {
margin-top: 20px;
}
.info-table {
width: 100%;
border-collapse: collapse;
}
.info-table th, .info-table td {
padding: 12px 15px;
border-bottom: 1px solid #ebeef5;
text-align: left;
}
.info-table th {
width: 120px;
color: #909399;
font-weight: normal;
}
.info-table td {
color: #606266;
}
.map-container {
height: 300px;
background-color: #f5f7fa;
display: flex;
justify-content: center;
align-items: center;
color: #909399;
margin-top: 15px;
border-radius: 4px;
}
/* 新增样式 */
.defect-canvas {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
.defect-polygon {
fill: rgba(255, 0, 0, 0.2);
stroke: #ff0000;
stroke-width: 2;
}
.defect-line {
stroke: #ff0000;
stroke-width: 2;
stroke-dasharray: 5,5;
}
.defect-point {
fill: #ff0000;
stroke: #ffffff;
stroke-width: 1;
r: 4;
}
.defect-label {
font-size: 12px;
fill: #ffffff;
text-anchor: middle;
dominant-baseline: middle;
font-weight: bold;
pointer-events: none;
}
.defect-label-bg {
fill: rgba(0, 0, 0, 0.5);
rx: 4;
ry: 4;
}
.defect-controls {
position: absolute;
top: 10px;
right: 10px;
background: rgba(255, 255, 255, 0.9);
padding: 10px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 100;
}
.defect-panel {
width: 300px;
border-left: 1px solid #e6e6e6;
background-color: white;
padding: 15px;
overflow-y: auto;
border-radius: 0 4px 4px 0;
}
.defect-list-container {
height: 100%;
display: flex;
flex-direction: column;
}
.defect-list {
flex: 1;
overflow-y: auto;
margin-bottom: 15px;
}
.defect-item {
padding: 10px;
border-bottom: 1px solid #ebeef5;
cursor: pointer;
transition: all 0.3s;
}
.defect-item:hover {
background-color: #f5f7fa;
}
.defect-item.selected {
background-color: #f0f7ff;
border-left: 3px solid #409EFF;
}
.defect-item .title {
font-weight: bold;
margin-bottom: 5px;
color: #1a3e72;
}
.defect-item .meta {
font-size: 12px;
color: #909399;
}
.defect-info {
border-top: 1px solid #ebeef5;
padding-top: 15px;
}
.defect-info-title {
font-weight: bold;
margin-bottom: 10px;
color: #1a3e72;
}
.annotation-mode {
background-color: #409EFF;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
margin-left: 10px;
}
.defect-tree {
margin-top: 10px;
}
.defect-tree-node {
padding: 5px 0;
}
.defect-tree-node .label {
display: flex;
align-items: center;
padding: 5px;
border-radius: 4px;
cursor: pointer;
}
.defect-tree-node .label:hover {
background-color: #f5f7fa;
}
.defect-tree-node .label i {
margin-right: 5px;
color: #409EFF;
}
.defect-tree-node .children {
padding-left: 20px;
}
.defect-form {
margin-top: 15px;
}
.form-footer {
margin-top: 15px;
text-align: right;
}
.library-selector {
margin-top: 5px;
}
.defect-position {
display: flex;
margin-bottom: 15px;
}
.defect-position-item {
flex: 1;
margin-right: 10px;
}
.defect-position-item:last-child {
margin-right: 0;
}
/* 报告预览样式 */
.report-preview-container {
width: 100%;
height: 500px;
border: 1px solid #e6e6e6;
background-color: white;
padding: 20px;
overflow-y: auto;
margin-bottom: 20px;
border-radius: 4px;
}
.report-title {
font-size: 24px;
font-weight: bold;
text-align: center;
margin-bottom: 10px;
color: #1a3e72;
}
.report-subtitle {
font-size: 16px;
text-align: center;
margin-bottom: 20px;
color: #606266;
}
.report-section {
margin-bottom: 30px;
}
.report-section-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
padding-bottom: 5px;
border-bottom: 1px solid #ebeef5;
color: #1a3e72;
}
.report-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
}
.report-table th, .report-table td {
border: 1px solid #ebeef5;
padding: 8px 12px;
text-align: left;
}
.report-table th {
background-color: #f5f7fa;
font-weight: normal;
color: #606266;
}
.report-image {
max-width: 100%;
max-height: 200px;
margin: 10px 0;
display: block;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.report-defect-item {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px dashed #ebeef5;
}
.report-defect-title {
font-weight: bold;
margin-bottom: 5px;
color: #1a3e72;
}
.report-defect-meta {
font-size: 12px;
color: #909399;
margin-bottom: 5px;
}
/* 报告选项表单样式 */
.report-form-section {
margin-bottom: 20px;
padding: 15px;
background-color: white;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
.report-form-section-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
padding-bottom: 5px;
border-bottom: 1px solid #ebeef5;
color: #1a3e72;
}
.image-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.image-list-title {
font-weight: bold;
font-size: 16px;
color: #1a3e72;
}
.image-list-actions {
display: flex;
align-items: center;
}
.el-table {
border-radius: 4px;
overflow: hidden;
}
.el-table th {
background-color: #f5f7fa;
color: #606266;
}
.el-table td {
padding: 8px 0;
}
.el-tag {
margin-left: 5px;
}
/* 自动识别进度条 */
.auto-detect-progress {
margin: 20px 0;
}
.auto-detect-result {
margin-top: 20px;
padding: 15px;
background-color: #f5f7fa;
border-radius: 4px;
}
.detected-defect-item {
padding: 10px;
border-bottom: 1px solid #ebeef5;
cursor: pointer;
}
.detected-defect-item:hover {
background-color: #ecf5ff;
}
.detected-defect-item .title {
font-weight: bold;
color: #1a3e72;
}
.detected-defect-item .confidence {
font-size: 12px;
color: #67C23A;
}
.detected-defect-item .type {
font-size: 12px;
color: #909399;
}
/* 文件夹选择器样式 */
.folder-selector {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.folder-path {
flex: 1;
margin-right: 10px;
padding: 8px 15px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #f5f7fa;
}
/* 封面图片上传样式 */
.cover-image-upload {
text-align: center;
margin: 20px 0;
}
.cover-image-preview {
max-width: 200px;
max-height: 150px;
margin: 10px auto;
display: block;
}
/* 封面信息样式 */
.cover-info {
text-align: center;
margin-bottom: 30px;
}
.cover-title {
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
}
.cover-subtitle {
font-size: 20px;
margin-bottom: 15px;
}
.cover-company {
font-size: 18px;
margin: 10px 0;
}
.cover-meta {
font-size: 16px;
margin: 5px 0;
}
.cover-divider {
border-top: 1px solid #ebeef5;
margin: 20px 0;
}
</style>
</head>
<body>
<div id="app" class="app-container">
<div class="header">
<div class="header-title">风电叶片图像管理平台</div>
</div>
<div class="menu-bar">
<div class="menu-item" @mouseenter="activeMenu = 'start'" @mouseleave="activeMenu = ''" @click="resetToDefaultView">
开始
<div class="submenu" v-show="activeMenu === 'start'">
<div class="submenu-item" @click="startImport">
<i class="el-icon-upload"></i> 导入图像
</div>
<div class="submenu-item" @click="showImageInfo" :class="{disabled: !selectedImage}">
<i class="el-icon-picture"></i> 图像信息
</div>
</div>
</div>
<div class="menu-item" @mouseenter="activeMenu = 'industry'" @mouseleave="activeMenu = ''">
工业图像
<div class="submenu" v-show="activeMenu === 'industry'">
<div class="submenu-item" @click="startAutoDetectDefects">
<i class="el-icon-cpu"></i> 自动识别缺陷
</div>
<div class="submenu-item" @click="startDefectAnnotation">
<i class="el-icon-edit"></i> 缺陷标注
</div>
</div>
</div>
<div class="menu-item" @click="showReportDialog">
<i class="el-icon-document"></i> 生成检测报告
</div>
</div>
<div class="main-content">
<div class="content-row">
<!-- 项目管理树 -->
<div class="project-tree-container" v-show="!isDefectAnnotationMode && !isAutoDetectMode">
<div class="tree-title">项目管理</div>
<el-tree
:data="projectTree"
:props="treeProps"
@node-click="handleNodeClick"
:expand-on-click-node="false"
node-key="id"
default-expand-all
:render-content="renderTreeNode">
</el-tree>
</div>
<!-- 缺陷列表树 -->
<div class="project-tree-container" v-show="isDefectAnnotationMode && !isAutoDetectMode">
<div class="tree-title">缺陷列表</div>
<div class="defect-tree">
<div v-for="component in defectComponents" :key="component.id" class="defect-tree-node">
<div class="label" @click="toggleComponent(component)">
<i :class="getComponentIcon(component)"></i>
<span>{{component.name}}</span>
<el-tag v-if="getComponentDefectCount(component.id) > 0" size="mini" type="danger" style="margin-left: 5px;">
{{getComponentDefectCount(component.id)}}
</el-tag>
</div>
<div class="children" v-if="selectedComponent && selectedComponent.id === component.id">
<div v-for="defect in getComponentDefects(component.id)" :key="defect.id"
class="defect-tree-node"
:class="{selected: selectedDefect?.id === defect.id}"
@click="selectDefect(defect)">
<div class="label">
<i class="el-icon-warning-outline"></i>
<span>{{defect.name}} | {{defect.code}} | {{defect.position}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 自动识别面板 -->
<div class="project-tree-container" v-show="isAutoDetectMode">
<div class="tree-title">自动识别设置</div>
<div style="padding: 10px;">
<el-form :model="autoDetectForm" label-width="80px" size="small">
<el-form-item label="识别算法">
<el-select v-model="autoDetectForm.algorithm" placeholder="请选择算法">
<el-option label="YOLOv5" value="yolov5"></el-option>
<el-option label="Faster R-CNN" value="fasterrcnn"></el-option>
<el-option label="Mask R-CNN" value="maskrcnn"></el-option>
</el-select>
</el-form-item>
<el-form-item label="置信度">
<el-slider v-model="autoDetectForm.confidence" :min="50" :max="100"></el-slider>
</el-form-item>
<el-form-item label="缺陷类型">
<el-checkbox-group v-model="autoDetectForm.defectTypes">
<el-checkbox label="裂纹"></el-checkbox>
<el-checkbox label="腐蚀"></el-checkbox>
<el-checkbox label="磨损"></el-checkbox>
<el-checkbox label="分层"></el-checkbox>
<el-checkbox label="变形"></el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
<el-button
type="primary"
size="small"
@click="startDetection"
:disabled="!selectedImage">
开始识别
</el-button>
</div>
</div>
<!-- 工业图像视图 -->
<div class="image-viewer-container">
<div class="image-toolbar">
<div>
<span v-if="currentComponent" style="font-weight: bold; color: #1a3e72;">
当前部件: {{currentComponent.name}} ({{currentComponent.type}})
</span>
<span v-else style="color: #909399;">
请从左侧选择部件查看图像
</span>
<span v-if="isAnnotating" class="annotation-mode">标注模式</span>
<span v-if="isAutoDetectMode" class="annotation-mode">自动识别模式</span>
</div>
<div>
<el-button-group>
<el-button
size="small"
icon="el-icon-zoom-in"
@click="zoomIn"
:disabled="!selectedImage">
</el-button>
<el-button
size="small"
icon="el-icon-zoom-out"
@click="zoomOut"
:disabled="!selectedImage">
</el-button>
<el-button
size="small"
icon="el-icon-refresh-left"
@click="resetZoom"
:disabled="!selectedImage">
</el-button>
</el-button-group>
</div>
</div>
<div class="image-display" ref="imageDisplay">
<img
v-if="selectedImage"
:src="selectedImage.url"
class="image-preview"
:style="{transform: `scale(${zoomLevel})`}"
ref="imagePreview">
<div v-else class="empty-state">
<p>暂无图像预览</p>
<p>请从下方图像列表中选择图像</p>
</div>
<!-- 缺陷标注画布 -->
<svg class="defect-canvas" ref="defectCanvas" v-if="selectedImage"></svg>
<!-- 标注控制按钮 -->
<div class="defect-controls" v-if="isAnnotating && selectedImage">
<el-button-group>
<el-button
size="mini"
icon="el-icon-circle-close"
@click="cancelAnnotation">
取消标注
</el-button>
<el-button
size="mini"
icon="el-icon-delete"
@click="clearCurrentDrawing"
:disabled="drawingState.currentPoints.length === 0">
清除当前
</el-button>
<el-button
size="mini"
icon="el-icon-check"
type="primary"
@click="completeAnnotation"
:disabled="drawingState.currentPoints.length < 3">
完成标注
</el-button>
</el-button-group>
</div>
</div>
</div>
<!-- 缺陷详情面板 -->
<div class="defect-panel" v-if="isDefectAnnotationMode && !isAutoDetectMode">
<div class="defect-list-container">
<div v-if="selectedDefect">
<div class="defect-info-title">缺陷详情</div>
<el-form :model="selectedDefect" label-width="80px" size="small" class="defect-form">
<el-form-item label="编号">
<el-input v-model="selectedDefect.code"></el-input>
</el-form-item>
<el-form-item label="缺陷名称">
<el-input v-model="selectedDefect.name"></el-input>
</el-form-item>
<el-form-item label="部位">
<el-input v-model="selectedDefect.position"></el-input>
</el-form-item>
<el-form-item label="缺陷类型">
<el-select v-model="selectedDefect.type" placeholder="请选择缺陷类型">
<el-option label="裂纹" value="裂纹"></el-option>
<el-option label="腐蚀" value="腐蚀"></el-option>
<el-option label="磨损" value="磨损"></el-option>
<el-option label="分层" value="分层"></el-option>
<el-option label="变形" value="变形"></el-option>
<el-option label="其他" value="其他"></el-option>
</el-select>
</el-form-item>
<el-form-item label="严重程度">
<el-select v-model="selectedDefect.severity" placeholder="请选择严重程度">
<el-option label="轻微" value="轻微"></el-option>
<el-option label="中等" value="中等"></el-option>
<el-option label="严重" value="严重"></el-option>
<el-option label="危险" value="危险"></el-option>
</el-select>
</el-form-item>
<el-form-item label="维修状态">
<el-select v-model="selectedDefect.repairStatus" placeholder="请选择维修状态">
<el-option label="未处理" value="未处理"></el-option>
<el-option label="已计划" value="已计划"></el-option>
<el-option label="维修中" value="维修中"></el-option>
<el-option label="已完成" value="已完成"></el-option>
</el-select>
</el-form-item>
<div class="defect-position">
<div class="defect-position-item">
<el-form-item label="轴向尺寸(mm)">
<el-input-number v-model="selectedDefect.axialLength" :min="0" :step="1"></el-input-number>
</el-form-item>
</div>
<div class="defect-position-item">
<el-form-item label="弦向尺寸(mm)">
<el-input-number v-model="selectedDefect.chordLength" :min="0" :step="1"></el-input-number>
</el-form-item>
</div>
</div>
<el-form-item label="面积(mm²)">
<el-input-number v-model="selectedDefect.area" :min="0" :step="1" disabled></el-input-number>
</el-form-item>
<el-form-item label="描述">
<el-input type="textarea" v-model="selectedDefect.description" :rows="2"></el-input>
<div class="library-selector">
<el-button size="mini" @click="showDescriptionLibrary">从标准描述库选择</el-button>
</div>
</el-form-item>
<el-form-item label="维修建议">
<el-input type="textarea" v-model="selectedDefect.repairAdvice" :rows="2"></el-input>
<div class="library-selector">
<el-button size="mini" @click="showRepairAdviceLibrary">从标准信息库选择</el-button>
</div>
</el-form-item>
<div class="form-footer">
<el-button type="primary" size="small" @click="updateDefect">保存</el-button>
<el-button size="small" @click="deleteDefect">删除</el-button>
</div>
</el-form>
</div>
<div v-else style="color: #909399; text-align: center; padding: 20px;">
{{
isAnnotating ? '请完成缺陷标注' :
(selectedComponent ? '请选择缺陷查看详情' : '请选择部件查看缺陷')
}}
</div>
</div>
</div>
<!-- 自动识别结果面板 -->
<div class="defect-panel" v-if="isAutoDetectMode">
<div class="defect-list-container">
<div v-if="detectionInProgress">
<div class="auto-detect-progress">
<el-progress :percentage="detectionProgress" :status="detectionStatus"></el-progress>
<div style="text-align: center; margin-top: 10px;">
{{detectionMessage}}
</div>
</div>
</div>
<div v-else-if="detectedDefects.length > 0">
<div class="defect-info-title">识别结果</div>
<div class="auto-detect-result">
<div v-for="(defect, index) in detectedDefects" :key="index"
class="detected-defect-item"
@click="highlightDetectedDefect(defect)">
<div class="title">{{defect.type}} (置信度: {{defect.confidence}}%)</div>
<div class="type">位置: {{defect.position.join(', ')}}</div>
<div class="confidence">建议: {{getDefectAdvice(defect)}}</div>
</div>
</div>
<div class="form-footer" style="margin-top: 15px;">
<el-button type="primary" size="small" @click="saveDetectedDefects">保存结果</el-button>
<el-button size="small" @click="cancelDetection">取消</el-button>
</div>
</div>
<div v-else style="color: #909399; text-align: center; padding: 20px;">
{{selectedImage ? '请点击"开始识别"按钮进行缺陷检测' : '请先选择一张图像'}}
</div>
</div>
</div>
</div>
<!-- 工业图像列表 -->
<div class="image-list-container">
<div class="image-list-header">
<div>
<span class="image-list-title">工业图像列表</span>
<span v-if="currentComponent" style="margin-left: 10px; color: #409EFF;">
{{currentComponent.name}} (共 {{filteredImages.length}} 张)
</span>
</div>
<div class="image-list-actions">
<el-input
placeholder="输入关键字过滤"
v-model="imageFilter"
clearable
size="small"
style="width: 200px; margin-right: 10px;">
</el-input>
<el-radio-group v-model="displayMode" size="small" class="display-toggle">
<el-radio-button label="grid"><i class="el-icon-picture"></i></el-radio-button>
<el-radio-button label="list"><i class="el-icon-tickets"></i></el-radio-button>
</el-radio-group>
</div>
</div>
<!-- 图片网格模式 -->
<div v-if="displayMode === 'grid'" class="image-grid">
<div
v-for="img in filteredImages"
:key="img.id"
class="image-thumbnail"
:class="{'selected-thumbnail': selectedImage?.id === img.id}"
@click="selectImage(img)"
@dblclick="showImageDetail(img)"
@contextmenu.prevent="showContextMenu($event, img)">
<img :src="img.url" :alt="img.name">
<div class="name">{{img.name}}</div>
<div v-if="img.defectCount" style="position: absolute; top: 5px; right: 5px; background: #f56c6c; color: white; border-radius: 10px; padding: 0 5px; font-size: 12px;">
{{img.defectCount}}
</div>
</div>
<div v-if="filteredImages.length === 0" style="grid-column: 1 / -1; text-align: center; color: #909399; padding: 40px 0;">
暂无图像数据
</div>
</div>
<!-- 列表模式 -->
<el-table
v-else
:data="filteredImages"
height="100%"
@row-click="selectImage"
@row-dblclick="showImageDetail"
style="width: 100%"
:row-class-name="tableRowClassName"
@cell-contextmenu="showTableContextMenu">
<el-table-column
prop="name"
label="图像名称"
width="180">
<template slot-scope="scope">
<span>{{scope.row.name}}</span>
<el-tag v-if="scope.row.defectCount" size="mini" type="danger" style="margin-left: 5px;">{{scope.row.defectCount}}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="component"
label="部件"
width="150">
</el-table-column>
<el-table-column
prop="resolution"
label="分辨率"
width="120">
</el-table-column>
<el-table-column
prop="shootTime"
label="拍摄时间"
width="150">
</el-table-column>
<el-table-column
prop="focalLength"
label="焦距(mm)"
width="100">
</el-table-column>
<el-table-column
prop="cameraManufacturer"
label="相机制造商"
width="120">
</el-table-column>
<el-table-column
prop="cameraModel"
label="相机型号"
width="120">
</el-table-column>
<el-table-column
prop="weather"
label="天气"
width="100">
</el-table-column>
<el-table-column
prop="humidity"
label="湿度(%)"
width="80">
</el-table-column>
<el-table-column
prop="temperature"
label="温度(°C)"
width="80">
</el-table-column>
<el-table-column
prop="windSpeed"
label="风力"
width="80">
</el-table-column>
<el-table-column
prop="shootMethod"
label="拍摄类型"
width="120">
</el-table-column>
<el-table-column
prop="shootDistance"
label="拍摄距离(m)"
width="100">
</el-table-column>
</el-table>
</div>
</div>
<!-- 导入图像对话框 -->
<el-dialog
title="导入工业图像"
:visible.sync="importDialogVisible"
width="60%"
:close-on-click-modal="false">
<div class="wizard-step" v-if="importStep === 1">
<h3>步骤1: 选择部件</h3>
<el-radio-group v-model="importForm.component" size="small">
<el-radio-button label="unit1-blade1">机组1-叶片1</el-radio-button>
<el-radio-button label="unit1-blade2">机组1-叶片2</el-radio-button>
<el-radio-button label="unit1-blade3">机组1-叶片3</el-radio-button>
<el-radio-button label="unit1-nacelle">机组1-机舱</el-radio-button>
<el-radio-button label="unit1-tower">机组1-塔筒</el-radio-button>
</el-radio-group>
</div>
<div class="wizard-step" v-if="importStep === 2">
<h3>步骤2: 选择图像文件夹</h3>
<div class="folder-selector">
<input type="text" class="folder-path" v-model="importForm.folderPath" readonly placeholder="请选择图像文件夹">
<el-button
size="small"
type="primary"
@click="selectFolder">
选择文件夹
</el-button>
</div>
<el-table
:data="folderImages"
height="250"
style="width: 100%; margin-top: 15px;">
<el-table-column
prop="name"
label="文件名"
width="180">
</el-table-column>
<el-table-column
label="预览"
width="80">
<template slot-scope="scope">
<img :src="scope.row.url" style="width: 50px; height: 30px; object-fit: cover;">
</template>
</el-table-column>
<el-table-column
prop="size"
label="大小"
width="120"
:formatter="formatFileSize">
</el-table-column>
<el-table-column
prop="lastModified"
label="修改时间"
width="150"
:formatter="formatFileDate">
</el-table-column>
</el-table>
</div>
<div class="wizard-step" v-if="importStep === 3">
<h3>步骤3: 设置图像采集信息</h3>
<el-form :model="importForm" label-width="120px">
<el-form-item label="拍摄时间范围">
<el-date-picker
v-model="importForm.shootTimeRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间">
</el-date-picker>
</el-form-item>
<el-form-item label="天气">
<el-select v-model="importForm.weather" placeholder="请选择天气">
<el-option label="晴天" value="晴天"></el-option>
<el-option label="多云" value="多云"></el-option>
<el-option label="阴天" value="阴天"></el-option>
<el-option label="小雨" value="小雨"></el-option>
<el-option label="中雨" value="中雨"></el-option>
<el-option label="大雨" value="大雨"></el-option>
<el-option label="雾" value="雾"></el-option>
<el-option label="雪" value="雪"></el-option>
</el-select>
</el-form-item>
<el-form-item label="环境温度(°C)">
<div class="temp-range">
<el-input-number v-model="importForm.minTemperature" :min="-20" :max="50"></el-input-number>
<span class="separator">~</span>
<el-input-number v-model="importForm.maxTemperature" :min="-20" :max="50"></el-input-number>
</div>
</el-form-item>
<el-form-item label="湿度(%)">
<el-input-number v-model="importForm.humidity" :min="0" :max="100"></el-input-number>
</el-form-item>
<el-form-item label="风力">
<el-select v-model="importForm.windSpeed" placeholder="请选择风力">
<el-option label="0级无风" value="0"></el-option>
<el-option label="1级软风" value="1"></el-option>
<el-option label="2级轻风" value="2"></el-option>
<el-option label="3级微风" value="3"></el-option>
<el-option label="4级和风" value="4"></el-option>
<el-option label="5级清风" value="5"></el-option>
<el-option label="6级强风" value="6"></el-option>
<el-option label="7级疾风" value="7"></el-option>
<el-option label="8级大风" value="8"></el-option>
<el-option label="9级烈风" value="9"></el-option>
<el-option label="10级狂风" value="10"></el-option>
</el-select>
</el-form-item>
<el-form-item label="拍摄方式">
<el-radio-group v-model="importForm.shootMethod">
<el-radio label="无人机航拍"></el-radio>
<el-radio label="人工拍摄"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="拍摄距离(米)" v-if="importForm.shootMethod === '无人机航拍'">
<el-input-number v-model="importForm.shootDistance" :min="1" :max="500"></el-input-number>
</el-form-item>
<el-form-item label="采集员">
<el-input v-model="importForm.collector" style="width: 200px;"></el-input>
</el-form-item>
<el-form-item label="相机型号">
<el-input v-model="importForm.cameraModel" style="width: 200px;" placeholder="如: ILCE-7RM4"></el-input>
</el-form-item>
</el-form>
</div>
<div class="wizard-actions">
<el-button @click="cancelImport" v-if="importStep === 1">取消</el-button>
<el-button @click="prevStep" v-if="importStep > 1">上一步</el-button>
<el-button type="primary" @click="nextStep" v-if="importStep < 3">下一步</el-button>
<el-button type="primary" @click="confirmImport" v-if="importStep === 3">完成导入</el-button>
</div>
</el-dialog>
<!-- 图像信息对话框 -->
<el-dialog
title="图像详细信息"
:visible.sync="imageInfoVisible"
width="60%"
v-if="selectedImage">
<el-tabs type="border-card" class="info-tabs">
<el-tab-pane label="基本信息">
<table class="info-table">
<tr>
<th>图像名称</th>
<td>{{selectedImage.name}}</td>
</tr>
<tr>
<th>文件路径</th>
<td>/images/{{selectedImage.componentId}}/{{selectedImage.name}}</td>
</tr>
<tr>
<th>文件大小</th>
<td>3.2 MB</td>
</tr>
<tr>
<th>修改时间</th>
<td>{{selectedImage.shootTime}}</td>
</tr>
<tr>
<th>分辨率</th>
<td>{{selectedImage.resolution}}</td>
</tr>
<tr>
<th>文件格式</th>
<td>JPEG</td>
</tr>
</table>
</el-tab-pane>
<el-tab-pane label="拍摄信息">
<table class="info-table">
<tr>
<th>拍摄时间</th>
<td>{{selectedImage.shootTime}}</td>
</tr>
<tr>
<th>焦距</th>
<td>{{selectedImage.focalLength}}</td>
</tr>
<tr>
<th>相机制造商</th>
<td>{{selectedImage.cameraManufacturer}}</td>
</tr>
<tr>
<th>相机型号</th>
<td>{{selectedImage.cameraModel}}</td>
</tr>
<tr>
<th>拍摄方式</th>
<td>{{selectedImage.shootMethod}}</td>
</tr>
<tr>
<th>拍摄距离</th>
<td>{{selectedImage.shootDistance}} 米</td>
</tr>
</table>
</el-tab-pane>
<el-tab-pane label="位置地图">
<div class="map-container">
<div>
<i class="el-icon-location-information" style="font-size: 40px;"></i>
<p>地图位置风电场1号机组</p>
<p>坐标39.9042°N, 116.4074°E</p>
</div>
</div>
</el-tab-pane>
</el-tabs>
<span slot="footer" class="dialog-footer">
<el-button @click="imageInfoVisible = false">关闭</el-button>
<el-button type="primary" @click="imageInfoVisible = false">确定</el-button>
</span>
</el-dialog>
<!-- 标准描述库对话框 -->
<el-dialog
title="标准描述库"
:visible.sync="descriptionLibraryVisible"
width="50%">
<el-table :data="descriptionLibrary" height="300" @row-click="selectDescription">
<el-table-column prop="content" label="描述内容"></el-table-column>
<el-table-column prop="type" label="适用类型" width="120"></el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="descriptionLibraryVisible = false">取消</el-button>
<el-button type="primary" @click="applySelectedDescription">确定</el-button>
</span>
</el-dialog>
<!-- 标准维修建议库对话框 -->
<el-dialog
title="标准维修建议库"
:visible.sync="repairAdviceLibraryVisible"
width="50%">
<el-table :data="repairAdviceLibrary" height="300" @row-click="selectRepairAdvice">
<el-table-column prop="content" label="建议内容"></el-table-column>
<el-table-column prop="type" label="适用类型" width="120"></el-table-column>
<el-table-column prop="severity" label="适用严重程度" width="120"></el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="repairAdviceLibraryVisible = false">取消</el-button>
<el-button type="primary" @click="applySelectedRepairAdvice">确定</el-button>
</span>
</el-dialog>
<!-- 报告生成对话框 -->
<el-dialog
title="生成检测报告"
:visible.sync="reportDialogVisible"
width="80%"
:close-on-click-modal="false">
<el-steps :active="reportStep" finish-status="success" style="margin-bottom: 20px;">
<el-step title="报告选项"></el-step>
<el-step title="预览报告"></el-step>
<el-step title="导出报告"></el-step>
</el-steps>
<!-- 步骤1: 报告选项 -->
<div v-if="reportStep === 0">
<div class="report-form-section">
<div class="report-form-section-title">封面信息</div>
<el-form :model="reportForm" label-width="120px">
<el-form-item label="主标题">
<el-input v-model="reportForm.mainTitle" placeholder="例如: 国华新疆(哈密)望洋台风电一场"></el-input>
</el-form-item>
<el-form-item label="副标题">
<el-input v-model="reportForm.subTitle" placeholder="例如: 风机叶片检查报告"></el-input>
</el-form-item>
<el-form-item label="机组编号">
<el-input v-model="reportForm.unitNumber" placeholder="例如: 35#机组"></el-input>
</el-form-item>
<el-form-item label="封面图片">
<el-upload
class="cover-image-upload"
action=""
:auto-upload="false"
:on-change="handleCoverImageChange"
:show-file-list="false">
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">建议尺寸: 800x600像素</div>
</el-upload>
<img v-if="reportForm.coverImage" :src="reportForm.coverImage" class="cover-image-preview">
</el-form-item>
<el-form-item label="乙方公司">
<el-input v-model="reportForm.companyName" placeholder="例如: 武汉迪特聚能有限公司"></el-input>
</el-form-item>
<el-form-item label="编制人员">
<el-input v-model="reportForm.reportAuthor" placeholder="报告编制人员姓名"></el-input>
</el-form-item>
<el-form-item label="审核人员">
<el-input v-model="reportForm.reportReviewer" placeholder="报告审核人员姓名"></el-input>
</el-form-item>
</el-form>
</div>
<div class="report-form-section">
<div class="report-form-section-title">项目概况</div>
<el-form :model="reportForm" label-width="120px">
<el-form-item label="风场名称">
<el-input v-model="reportForm.windFarmName" placeholder="例如: 望洋台风电一场"></el-input>
</el-form-item>
<el-form-item label="风场地址">
<el-input v-model="reportForm.windFarmAddress" placeholder="例如: 新疆哈密市伊州区"></el-input>
</el-form-item>
<el-form-item label="委托单位">
<el-input v-model="reportForm.clientCompany" placeholder="例如: 国华能源投资有限公司"></el-input>
</el-form-item>
<el-form-item label="检查单位">
<el-input v-model="reportForm.inspectionCompany" placeholder="例如: 武汉迪特聚能有限公司"></el-input>
</el-form-item>
<el-form-item label="委托联系人">
<el-input v-model="reportForm.clientContact" placeholder="委托单位联系人姓名"></el-input>
</el-form-item>
<el-form-item label="委托联系电话">
<el-input v-model="reportForm.clientPhone" placeholder="委托单位联系电话"></el-input>
</el-form-item>
<el-form-item label="检查负责人">
<el-input v-model="reportForm.inspectionLeader" placeholder="例如: 吴名洲"></el-input>
</el-form-item>
<el-form-item label="检查联系电话">
<el-input v-model="reportForm.inspectionPhone" placeholder="例如: 18807109269"></el-input>
</el-form-item>
<el-form-item label="机组型号">
<el-input v-model="reportForm.unitModel" placeholder="例如: 金风科技2.5MW"></el-input>
</el-form-item>
<el-form-item label="项目规模">
<el-input v-model="reportForm.projectScale" placeholder="例如: 15台"></el-input>
</el-form-item>
<el-form-item label="总工期">
<el-input v-model="reportForm.projectDuration" placeholder="例如: 40天"></el-input>
</el-form-item>
</el-form>
</div>
<div class="report-form-section">
<div class="report-form-section-title">检查信息</div>
<el-form :model="reportForm" label-width="120px">
<el-form-item label="检查日期">
<el-date-picker
v-model="reportForm.inspectionDate"
type="date"
placeholder="选择日期"
value-format="yyyy-MM-dd">
</el-date-picker>
</el-form-item>
<el-form-item label="检查内容">
<el-input type="textarea" v-model="reportForm.inspectionContent"
placeholder="例如: 叶片外观检查、内部检查、防雷检测等" :rows="3"></el-input>
</el-form-item>
<el-form-item label="检查方式">
<el-input v-model="reportForm.inspectionMethod" placeholder="例如: 无人机巡检、人工检查"></el-input>
</el-form-item>
<el-form-item label="检查人员">
<el-tag
v-for="person in reportForm.inspectionPersons"
:key="person"
closable
@close="removeInspectionPerson(person)">
{{person}}
</el-tag>
<el-input
class="input-new-tag"
v-if="inputPersonVisible"
v-model="inputPersonValue"
ref="saveTagInput"
size="small"
@keyup.enter.native="addInspectionPerson"
@blur="addInspectionPerson">
</el-input>
<el-button v-else class="button-new-tag" size="small" @click="showInputPerson">+ 添加人员</el-button>
</el-form-item>
</el-form>
</div>
<div class="report-form-section">
<div class="report-form-section-title">机组信息</div>
<el-form :model="reportForm" label-width="120px">
<el-form-item label="主机厂商">
<el-input v-model="reportForm.hostManufacturer" placeholder="例如: 金风科技"></el-input>
</el-form-item>
<el-form-item label="部件厂商">
<el-input v-model="reportForm.componentManufacturer" placeholder="例如: 中材科技风电叶片股份有限公司"></el-input>
</el-form-item>
<el-form-item label="风机型号">
<el-input v-model="reportForm.windTurbineModel" placeholder="例如: 2.5MW"></el-input>
</el-form-item>
<el-form-item label="叶片型号">
<el-input v-model="reportForm.bladeModel" placeholder="例如: SI52.2B-2.5MW"></el-input>
</el-form-item>
<el-form-item label="机组编号">
<el-input v-model="reportForm.unitNumber" placeholder="例如: 35#机组"></el-input>
</el-form-item>
<el-form-item label="叶片1编号">
<el-input v-model="reportForm.blade1Number" placeholder="例如: D-15-051"></el-input>
</el-form-item>
<el-form-item label="叶片2编号">
<el-input v-model="reportForm.blade2Number" placeholder="例如: B-15-042"></el-input>
</el-form-item>
<el-form-item label="叶片3编号">
<el-input v-model="reportForm.blade3Number" placeholder="例如: B-15-059"></el-input>
</el-form-item>
<el-form-item label="机组编号图片">
<el-upload
class="cover-image-upload"
action=""
:auto-upload="false"
:on-change="handleUnitImageChange"
:show-file-list="false">
<el-button size="small" type="primary">点击上传</el-button>
</el-upload>
<img v-if="reportForm.unitImage" :src="reportForm.unitImage" class="cover-image-preview">
</el-form-item>
</el-form>
</div>
</div>
<!-- 步骤2: 预览报告 -->
<div v-if="reportStep === 1" class="report-preview-container">
<!-- 封面信息 -->
<div class="cover-info">
<div class="cover-title">{{reportForm.mainTitle || '国华新疆(哈密)望洋台风电一场'}}</div>
<div class="cover-subtitle">{{reportForm.subTitle || '风机叶片检查报告'}}</div>
<div class="cover-subtitle">{{reportForm.unitNumber || '35#机组'}}</div>
<div class="cover-divider"></div>
<img v-if="reportForm.coverImage" :src="reportForm.coverImage" class="cover-image-preview">
<div class="cover-divider"></div>
<div class="cover-company">{{reportForm.companyName || '武汉迪特聚能有限公司'}}</div>
<div class="cover-meta">编制:{{reportForm.reportAuthor || '王五'}}</div>
<div class="cover-meta">审核:{{reportForm.reportReviewer || '赵六'}}</div>
<div class="cover-meta">日期:{{reportForm.reportDate || new Date().toISOString().slice(0, 10)}}</div>
</div>
<!-- 项目概况 -->
<div class="report-section">
<div class="report-section-title">一、项目概况</div>
<div style="margin-bottom: 15px;">
<div><strong>风场名称:</strong>{{reportForm.windFarmName || '望洋台风电一场'}}</div>
<div><strong>风场地址:</strong>{{reportForm.windFarmAddress || '新疆哈密市伊州区'}}</div>
<div><strong>委托单位:</strong>{{reportForm.clientCompany || '国华能源投资有限公司'}}</div>
<div><strong>检查单位:</strong>{{reportForm.inspectionCompany || '武汉迪特聚能有限公司'}}</div>
<div><strong>委托联系人:</strong>{{reportForm.clientContact || '张经理'}} {{reportForm.clientPhone || '13800138000'}}</div>
<div><strong>检查负责人:</strong>{{reportForm.inspectionLeader || '吴名洲'}} {{reportForm.inspectionPhone || '18807109269'}}</div>
<div><strong>机组型号:</strong>{{reportForm.unitModel || '金风科技2.5MW'}}</div>
<div><strong>项目规模:</strong>{{reportForm.projectScale || '15台'}}</div>
<div><strong>总工期:</strong>{{reportForm.projectDuration || '40天'}}</div>
</div>
</div>
<!-- 机组信息 -->
<div class="report-section">
<div class="report-section-title">二、机组信息</div>
<div style="margin-bottom: 15px;">
<div><strong>主机厂商:</strong>{{reportForm.hostManufacturer || '金风科技'}}</div>
<div><strong>部件厂商:</strong>{{reportForm.componentManufacturer || '中材科技风电叶片股份有限公司'}}</div>
<div><strong>风机型号:</strong>{{reportForm.windTurbineModel || '2.5MW'}}</div>
<div><strong>叶片型号:</strong>{{reportForm.bladeModel || 'SI52.2B-2.5MW'}}</div>
<div><strong>机组编号:</strong>{{reportForm.unitNumber || '35#机组'}}</div>
<div><strong>叶片1编号</strong>{{reportForm.blade1Number || 'D-15-051'}}</div>
<div><strong>叶片2编号</strong>{{reportForm.blade2Number || 'B-15-042'}}</div>
<div><strong>叶片3编号</strong>{{reportForm.blade3Number || 'B-15-059'}}</div>
</div>
<img v-if="reportForm.unitImage" :src="reportForm.unitImage" class="report-image">
</div>
<!-- 检查信息 -->
<div class="report-section">
<div class="report-section-title">三、检查信息</div>
<table class="report-table">
<tr>
<th width="150">检查人员</th>
<td>{{reportForm.inspectionPersons.join('、') || '张三、李四'}}</td>
</tr>
<tr>
<th>检查日期</th>
<td>{{reportForm.inspectionDate || new Date().toISOString().slice(0, 10)}}</td>
</tr>
<tr>
<th>检查方式</th>
<td>{{reportForm.inspectionMethod || '无人机巡检、人工检查'}}</td>
</tr>
<tr>
<th>检查内容</th>
<td>{{reportForm.inspectionContent || '叶片外观检查、内部检查、防雷检测等'}}</td>
</tr>
</table>
</div>
<!-- 检查方案 -->
<div class="report-section">
<div class="report-section-title">四、检查方案</div>
<table class="report-table">
<tr>
<th width="200">工作内容</th>
<th width="200">人员配置</th>
<th width="150">设备配置</th>
<th>备注</th>
</tr>
<tr>
<td>无人机叶片外观巡检</td>
<td>1人主检飞手1人</td>
<td>无人机</td>
<td></td>
</tr>
<tr>
<td>叶片内部检查</td>
<td>2人轮毂作业检查2人</td>
<td>检查设备</td>
<td></td>
</tr>
<tr>
<td>无人机叶片防雷导通测</td>
<td>2人主检飞手1人副检抄表1人</td>
<td>防雷检测设备</td>
<td></td>
</tr>
</table>
</div>
<!-- 检查情况汇总 -->
<div class="report-section">
<div class="report-section-title">五、检查情况汇总</div>
<div v-for="defect in defects" :key="defect.id" class="report-defect-item">
<div class="report-defect-title">{{defect.name}} ({{defect.code}})</div>
<div class="report-defect-meta">
<span>位置: {{defect.position}}</span>
<span style="margin-left: 15px;">类型: {{defect.type}}</span>
<span style="margin-left: 15px;">严重程度: {{defect.severity}}</span>
</div>
<div>{{defect.description}}</div>
<div style="margin-top: 5px;">
<strong>维修建议:</strong> {{defect.repairAdvice}}
</div>
</div>
</div>
<!-- 成果递交 -->
<div class="report-section">
<div class="report-section-title">六、成果递交</div>
<table class="report-table">
<tr>
<th width="150">报告编制</th>
<td width="250">{{reportForm.reportAuthor || '王五'}}</td>
<th width="150">编制时间</th>
<td>{{reportForm.reportDate || new Date().toISOString().slice(0, 10)}}</td>
</tr>
<tr>
<th>报告复核</th>
<td>{{reportForm.reportReviewer || '赵六'}}</td>
<th>复核时间</th>
<td>{{reportForm.reportDate || new Date().toISOString().slice(0, 10)}}</td>
</tr>
<tr>
<th>报告审核</th>
<td>{{reportForm.reportApprover || '钱七'}}</td>
<th>审核时间</th>
<td>{{reportForm.reportDate || new Date().toISOString().slice(0, 10)}}</td>
</tr>
</table>
</div>
</div>
<!-- 步骤3: 导出报告 -->
<div v-if="reportStep === 2" style="text-align: center; padding: 40px 0;">
<i class="el-icon-success" style="font-size: 60px; color: #67C23A;"></i>
<p style="font-size: 18px; margin-top: 20px;">报告已准备就绪点击下方按钮导出Word文档</p>
</div>
<div class="wizard-actions">
<el-button @click="cancelReport" v-if="reportStep === 0">取消</el-button>
<el-button @click="prevReportStep" v-if="reportStep > 0">上一步</el-button>
<el-button type="primary" @click="nextReportStep" v-if="reportStep < 2">下一步</el-button>
<el-button type="primary" @click="exportReport" v-if="reportStep === 2">导出Word报告</el-button>
</div>
</el-dialog>
<!-- 右键菜单 -->
<div
class="context-menu"
v-show="contextMenu.visible"
:style="{left: contextMenu.x + 'px', top: contextMenu.y + 'px'}"
@click.stop>
<div class="context-menu-item" @click="showImageDetail(contextMenu.image)">
<i class="el-icon-view"></i> 查看图像
</div>
<div class="context-menu-item" @click="showImageInfo(contextMenu.image)">
<i class="el-icon-picture"></i> 图像信息
</div>
<div class="context-menu-item">
<i class="el-icon-download"></i> 导出图像
</div>
<div class="context-menu-item">
<i class="el-icon-delete"></i> 删除图像
</div>
</div>
<!-- 缺陷右键菜单 -->
<div
class="context-menu"
v-show="defectContextMenu.visible"
:style="{left: defectContextMenu.x + 'px', top: defectContextMenu.y + 'px'}"
@click.stop>
<div class="context-menu-item" @click="viewDefect(defectContextMenu.defect)">
<i class="el-icon-view"></i> 查看缺陷
</div>
<div class="context-menu-item" @click="editDefect(defectContextMenu.defect)">
<i class="el-icon-edit"></i> 编辑缺陷
</div>
<div class="context-menu-item" @click="deleteDefect(defectContextMenu.defect)">
<i class="el-icon-delete"></i> 删除缺陷
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
// 顶部菜单状态
activeMenu: '',
// 项目树数据
projectTree: [
{
id: 'project1',
name: '风电场检测项目1',
type: 'project',
children: [
{
id: 'unit1',
name: '机组1',
type: 'unit',
children: [
{ id: 'unit1-nacelle', name: '机舱', type: 'component' },
{ id: 'unit1-blade1', name: '叶片1', type: 'component' },
{ id: 'unit1-blade2', name: '叶片2', type: 'component' },
{ id: 'unit1-blade3', name: '叶片3', type: 'component' },
{ id: 'unit1-tower', name: '塔筒', type: 'component' }
]
}
]
}
],
treeProps: {
children: 'children',
label: 'name'
},
// 当前选中状态
currentComponent: null,
selectedImage: null,
zoomLevel: 1,
imageFilter: '',
displayMode: 'grid', // grid/list
// 图像数据
images: [
{
id: 1,
name: '叶片1-正面-20230510.jpg',
url: 'https://via.placeholder.com/800x600?text=叶片1-正面',
componentId: 'unit1-blade1',
component: '机组1-叶片1',
shootTime: '2023-05-10 09:30:00',
resolution: '6186×4126',
focalLength: '155.00mm',
cameraManufacturer: '索尼',
cameraModel: 'ILCE-7RM4',
weather: '晴天',
temperature: 22,
humidity: 45,
windSpeed: '3',
shootMethod: '无人机航拍',
shootDistance: 50,
collector: '张三',
defectCount: 2
},
{
id: 2,
name: '叶片1-背面-20230510.jpg',
url: 'https://via.placeholder.com/800x600?text=叶片1-背面',
componentId: 'unit1-blade1',
component: '机组1-叶片1',
shootTime: '2023-05-10 10:15:00',
resolution: '6186×4126',
focalLength: '155.00mm',
cameraManufacturer: '索尼',
cameraModel: 'ILCE-7RM4',
weather: '晴天',
temperature: 24,
humidity: 42,
windSpeed: '3',
shootMethod: '无人机航拍',
shootDistance: 50,
collector: '张三',
defectCount: 1
},
{
id: 3,
name: '塔筒-底部-20230511.jpg',
url: 'https://via.placeholder.com/800x600?text=塔筒-底部',
componentId: 'unit1-tower',
component: '机组1-塔筒',
shootTime: '2023-05-11 08:45:00',
resolution: '6186×4126',
focalLength: '155.00mm',
cameraManufacturer: '索尼',
cameraModel: 'ILCE-7RM4',
weather: '多云',
temperature: 20,
humidity: 50,
windSpeed: '2',
shootMethod: '人工拍摄',
shootDistance: 5,
collector: '李四',
defectCount: 0
}
],
// 缺陷数据
defects: [
{
id: 1,
imageId: 1,
componentId: 'unit1-blade1',
code: 'DF-20230510-001',
name: '前缘裂纹',
position: '叶片前缘',
type: '裂纹',
severity: '中等',
axialLength: 15,
chordLength: 2,
area: 30,
repairStatus: '未处理',
description: '叶片前缘纵向裂纹长度约15mm宽度约2mm',
repairAdvice: '建议进行表面修复处理,防止裂纹扩展',
polygon: [{x: 100, y: 100}, {x: 150, y: 80}, {x: 200, y: 120}, {x: 180, y: 150}],
createdAt: '2023-05-10 10:30:00',
creator: '王五'
},
{
id: 2,
imageId: 1,
componentId: 'unit1-blade1',
code: 'DF-20230510-002',
name: '表面腐蚀',
position: '叶片中部',
type: '腐蚀',
severity: '轻微',
axialLength: 8,
chordLength: 5,
area: 40,
repairStatus: '已计划',
description: '叶片表面轻微腐蚀面积约40mm²',
repairAdvice: '建议进行表面清洁和防腐处理',
polygon: [{x: 300, y: 200}, {x: 350, y: 180}, {x: 380, y: 220}, {x: 360, y: 250}],
createdAt: '2023-05-10 11:15:00',
creator: '王五'
},
{
id: 3,
imageId: 2,
componentId: 'unit1-blade1',
code: 'DF-20230510-003',
name: '后缘磨损',
position: '叶片后缘',
type: '磨损',
severity: '严重',
axialLength: 25,
chordLength: 10,
area: 250,
repairStatus: '维修中',
description: '叶片后缘严重磨损长度约25mm宽度约10mm',
repairAdvice: '建议进行局部修复或更换受损部分',
polygon: [{x: 200, y: 300}, {x: 250, y: 280}, {x: 300, y: 320}, {x: 280, y: 350}],
createdAt: '2023-05-10 14:20:00',
creator: '王五'
}
],
selectedDefect: null,
selectedComponent: null,
// 标注状态
isAnnotating: false,
isDefectAnnotationMode: false,
drawingState: {
isDrawing: false,
lastPoint: null,
currentLine: null,
currentPoints: []
},
// 自动识别缺陷状态
isAutoDetectMode: false,
autoDetectForm: {
algorithm: 'yolov5',
confidence: 80,
defectTypes: ['裂纹', '腐蚀', '磨损', '分层', '变形']
},
detectionInProgress: false,
detectionProgress: 0,
detectionStatus: '',
detectionMessage: '',
detectedDefects: [],
// 标准库数据
descriptionLibrary: [
{ id: 1, type: '裂纹', content: '纵向裂纹,长度约{length}mm宽度约{width}mm' },
{ id: 2, type: '裂纹', content: '横向裂纹,长度约{length}mm深度约{depth}mm' },
{ id: 3, type: '腐蚀', content: '表面腐蚀,面积约{area}mm²' },
{ id: 4, type: '磨损', content: '边缘磨损,长度约{length}mm宽度约{width}mm' },
{ id: 5, type: '分层', content: '材料分层,面积约{area}mm²' }
],
repairAdviceLibrary: [
{ id: 1, type: '裂纹', severity: '轻微', content: '建议进行表面修复处理,防止裂纹扩展' },
{ id: 2, type: '裂纹', severity: '中等', content: '建议进行局部修复并加强监测' },
{ id: 3, type: '裂纹', severity: '严重', content: '建议更换受损部件' },
{ id: 4, type: '腐蚀', severity: '轻微', content: '建议进行表面清洁和防腐处理' },
{ id: 5, type: '腐蚀', severity: '中等', content: '建议进行表面修复和防腐处理' },
{ id: 6, type: '磨损', severity: '轻微', content: '建议进行表面修复处理' },
{ id: 7, type: '磨损', severity: '严重', content: '建议更换受损部件' }
],
selectedDescription: null,
selectedRepairAdvice: null,
descriptionLibraryVisible: false,
repairAdviceLibraryVisible: false,
// 导入图像对话框数据
importDialogVisible: false,
importStep: 1,
importForm: {
component: 'unit1-blade1',
folderPath: '',
shootTimeRange: [],
weather: '晴天',
minTemperature: 15,
maxTemperature: 25,
humidity: 50,
windSpeed: '3',
shootMethod: '无人机航拍',
shootDistance: 50,
collector: '',
cameraModel: 'ILCE-7RM4'
},
folderImages: [],
// 图像信息对话框
imageInfoVisible: false,
// 右键菜单
contextMenu: {
visible: false,
x: 0,
y: 0,
image: null
},
// 缺陷右键菜单
defectContextMenu: {
visible: false,
x: 0,
y: 0,
defect: null
},
// 报告生成对话框数据
reportDialogVisible: false,
reportStep: 0,
reportForm: {
mainTitle: '国华新疆(哈密)望洋台风电一场',
subTitle: '风机叶片检查报告',
companyName: '武汉迪特聚能有限公司',
coverImage: null,
unitImage: null,
reportAuthor: '王五',
reportReviewer: '赵六',
reportApprover: '钱七',
reportDate: new Date().toISOString().slice(0, 10),
// 项目概况
windFarmName: '望洋台风电一场',
windFarmAddress: '新疆哈密市伊州区',
clientCompany: '国华能源投资有限公司',
inspectionCompany: '武汉迪特聚能有限公司',
clientContact: '张经理',
clientPhone: '13800138000',
inspectionLeader: '吴名洲',
inspectionPhone: '18807109269',
unitModel: '金风科技2.5MW',
projectScale: '15台',
projectDuration: '40天',
// 检查信息
inspectionDate: new Date().toISOString().slice(0, 10),
inspectionContent: '叶片外观检查、内部检查、防雷检测等',
inspectionMethod: '无人机巡检、人工检查',
inspectionPersons: ['张三', '李四'],
// 机组信息
hostManufacturer: '金风科技',
componentManufacturer: '中材科技风电叶片股份有限公司',
windTurbineModel: '2.5MW',
bladeModel: 'SI52.2B-2.5MW',
unitNumber: '35#机组',
blade1Number: 'D-15-051',
blade2Number: 'B-15-042',
blade3Number: 'B-15-059'
},
inputPersonVisible: false,
inputPersonValue: ''
}
},
computed: {
filteredImages() {
if (!this.currentComponent) return [];
return this.images.filter(img => {
const matchesComponent = img.componentId === this.currentComponent.id;
const matchesFilter = this.imageFilter ?
(img.name.toLowerCase().includes(this.imageFilter.toLowerCase()) ||
img.shootTime.includes(this.imageFilter)) : true;
return matchesComponent && matchesFilter;
});
},
// 当前图像的缺陷
currentImageDefects() {
if (!this.selectedImage) return [];
return this.defects.filter(defect => defect.imageId === this.selectedImage.id);
},
// 缺陷组件列表
defectComponents() {
if (!this.projectTree.length || !this.projectTree[0].children.length) return [];
return this.projectTree[0].children[0].children;
}
},
watch: {
selectedImage(newVal) {
if (newVal) {
this.renderDefects();
// 自动选择第一个缺陷
if (this.currentImageDefects.length > 0 && !this.selectedDefect) {
this.selectedDefect = this.currentImageDefects[0];
}
}
},
selectedComponent(newVal) {
if (newVal) {
// 自动选择该组件的第一个缺陷
const componentDefects = this.getComponentDefects(newVal.id);
if (componentDefects.length > 0) {
this.selectedDefect = componentDefects[0];
// 自动选择对应的图像
const image = this.images.find(img => img.id === this.selectedDefect.imageId);
if (image) {
this.selectedImage = image;
}
} else {
this.selectedDefect = null;
}
}
}
},
methods: {
// 渲染树节点
renderTreeNode(h, { node, data }) {
let iconName = '';
switch(data.type) {
case 'project': iconName = 'el-icon-office-building'; break;
case 'unit': iconName = 'el-icon-wind-power'; break;
case 'component':
if (data.name.includes('叶片')) iconName = 'el-icon-leaf';
else if (data.name.includes('机舱')) iconName = 'el-icon-cpu';
else iconName = 'el-icon-data-line';
break;
}
return h('span', { class: 'component-node' }, [
h('i', { class: `el-icon ${iconName} component-icon` }),
h('span', node.label)
]);
},
// 获取组件图标
getComponentIcon(component) {
if (component.name.includes('叶片')) return 'el-icon-leaf';
if (component.name.includes('机舱')) return 'el-icon-cpu';
return 'el-icon-data-line';
},
// 处理树节点点击
handleNodeClick(data) {
if (data.type === 'component') {
this.currentComponent = data;
this.selectedImage = null;
this.selectedDefect = null;
}
},
// 切换组件展开状态
toggleComponent(component) {
if (this.selectedComponent && this.selectedComponent.id === component.id) {
this.selectedComponent = null;
} else {
this.selectedComponent = component;
}
},
// 选择图像
selectImage(row) {
this.selectedImage = row;
this.zoomLevel = 1;
this.selectedDefect = null;
this.cancelAnnotation();
},
// 显示图像详情(在主视图显示)
showImageDetail(row) {
this.selectedImage = row || this.selectedImage;
this.zoomLevel = 1;
this.contextMenu.visible = false;
},
// 显示图像信息对话框
showImageInfo(row) {
if (row) {
this.selectedImage = row;
}
if (this.selectedImage) {
this.imageInfoVisible = true;
} else {
this.$message.warning('请先选择一张图像');
}
this.contextMenu.visible = false;
},
// 图像缩放控制
zoomIn() {
this.zoomLevel += 0.1;
this.renderDefects();
},
zoomOut() {
if (this.zoomLevel > 0.2) {
this.zoomLevel -= 0.1;
this.renderDefects();
}
},
resetZoom() {
this.zoomLevel = 1;
this.renderDefects();
},
// 表格行样式
tableRowClassName({row}) {
return row.id === this.selectedImage?.id ? 'highlight-row' : '';
},
// 显示右键菜单(网格模式)
showContextMenu(e, image) {
this.contextMenu = {
visible: true,
x: e.clientX,
y: e.clientY,
image: image
};
// 点击其他地方关闭菜单
document.addEventListener('click', this.closeContextMenu);
},
// 显示右键菜单(表格模式)
showTableContextMenu(row, column, event) {
this.contextMenu = {
visible: true,
x: event.clientX,
y: event.clientY,
image: row
};
// 点击其他地方关闭菜单
document.addEventListener('click', this.closeContextMenu);
},
// 显示缺陷右键菜单
showDefectContextMenu(e, defect) {
this.defectContextMenu = {
visible: true,
x: e.clientX,
y: e.clientY,
defect: defect
};
// 点击其他地方关闭菜单
document.addEventListener('click', this.closeDefectContextMenu);
},
// 关闭右键菜单
closeContextMenu() {
this.contextMenu.visible = false;
document.removeEventListener('click', this.closeContextMenu);
},
// 关闭缺陷右键菜单
closeDefectContextMenu() {
this.defectContextMenu.visible = false;
document.removeEventListener('click', this.closeDefectContextMenu);
},
// 重置到默认视图
resetToDefaultView() {
this.isDefectAnnotationMode = false;
this.isAutoDetectMode = false;
this.isAnnotating = false;
this.selectedDefect = null;
this.selectedComponent = null;
this.clearCurrentDrawing();
},
// 导入图像相关方法
startImport() {
this.importDialogVisible = true;
this.importStep = 1;
this.importForm = {
component: 'unit1-blade1',
folderPath: '',
shootTimeRange: [],
weather: '晴天',
minTemperature: 15,
maxTemperature: 25,
humidity: 50,
windSpeed: '3',
shootMethod: '无人机航拍',
shootDistance: 50,
collector: '',
cameraModel: 'ILCE-7RM4'
};
this.folderImages = [];
},
// 选择文件夹
selectFolder() {
// 模拟文件夹选择
// 实际应用中应该使用Electron或类似技术访问本地文件系统
this.importForm.folderPath = 'C:/风电图像/机组1/叶片1/20230510';
// 模拟扫描文件夹中的图像
this.folderImages = [
{
name: '叶片1-正面-20230510.jpg',
url: 'https://via.placeholder.com/800x600?text=叶片1-正面',
size: 3245678,
lastModified: new Date('2023-05-10 09:30:00')
},
{
name: '叶片1-背面-20230510.jpg',
url: 'https://via.placeholder.com/800x600?text=叶片1-背面',
size: 3123456,
lastModified: new Date('2023-05-10 10:15:00')
},
{
name: '叶片1-细节-20230510.jpg',
url: 'https://via.placeholder.com/800x600?text=叶片1-细节',
size: 2987654,
lastModified: new Date('2023-05-10 11:30:00')
}
];
this.$message.success(`从文件夹 ${this.importForm.folderPath} 扫描到 ${this.folderImages.length} 张图像`);
},
cancelImport() {
this.importDialogVisible = false;
},
prevStep() {
if (this.importStep > 1) {
this.importStep--;
}
},
nextStep() {
if (this.importStep === 1 && !this.importForm.component) {
this.$message.error('请选择部件');
return;
}
if (this.importStep === 2 && !this.importForm.folderPath) {
this.$message.error('请选择图像文件夹');
return;
}
this.importStep++;
},
formatFileSize(row, column, cellValue) {
if (!cellValue) return '0 KB';
const size = parseInt(cellValue);
if (size < 1024) return size + ' B';
if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB';
return (size / (1024 * 1024)).toFixed(1) + ' MB';
},
formatFileDate(row, column, cellValue) {
if (!cellValue) return '';
const date = new Date(cellValue);
return date.toLocaleString();
},
confirmImport() {
if (!this.importForm.weather) {
this.$message.error('请选择天气');
return;
}
if (!this.importForm.shootTimeRange || this.importForm.shootTimeRange.length !== 2) {
this.$message.error('请设置拍摄时间范围');
return;
}
if (!this.importForm.collector) {
this.$message.error('请输入采集员');
return;
}
if (!this.importForm.cameraModel) {
this.$message.error('请输入相机型号');
return;
}
// 模拟导入过程
this.$message.success('开始导入图像...');
setTimeout(() => {
// 计算每张图像的拍摄时间(在时间范围内均匀分布)
const startTime = new Date(this.importForm.shootTimeRange[0]).getTime();
const endTime = new Date(this.importForm.shootTimeRange[1]).getTime();
const timeInterval = (endTime - startTime) / (this.folderImages.length + 1);
// 计算温度范围
const tempInterval = (this.importForm.maxTemperature - this.importForm.minTemperature) / (this.folderImages.length + 1);
// 添加新图像
this.folderImages.forEach((file, index) => {
const shootTime = new Date(startTime + (index + 1) * timeInterval);
const formattedTime = shootTime.getFullYear() + '-' +
(shootTime.getMonth() + 1).toString().padStart(2, '0') + '-' +
shootTime.getDate().toString().padStart(2, '0') + ' ' +
shootTime.getHours().toString().padStart(2, '0') + ':' +
shootTime.getMinutes().toString().padStart(2, '0') + ':' +
shootTime.getSeconds().toString().padStart(2, '0');
const temperature = parseFloat((this.importForm.minTemperature + (index + 1) * tempInterval).toFixed(1));
const componentName = {
'unit1-blade1': '机组1-叶片1',
'unit1-blade2': '机组1-叶片2',
'unit1-blade3': '机组1-叶片3',
'unit1-nacelle': '机组1-机舱',
'unit1-tower': '机组1-塔筒'
}[this.importForm.component];
const newImage = {
id: this.images.length + 1 + index,
name: file.name,
url: file.url,
componentId: this.importForm.component,
component: componentName,
shootTime: formattedTime,
resolution: '6186×4126',
focalLength: '155.00mm',
cameraManufacturer: '索尼',
cameraModel: this.importForm.cameraModel,
weather: this.importForm.weather,
temperature: temperature,
humidity: this.importForm.humidity,
windSpeed: this.importForm.windSpeed,
shootMethod: this.importForm.shootMethod,
shootDistance: this.importForm.shootDistance,
collector: this.importForm.collector,
defectCount: 0
};
this.images.push(newImage);
});
this.$message.success(`成功导入 ${this.folderImages.length} 张图像`);
this.importDialogVisible = false;
// 自动选中导入的部件
this.currentComponent = this.projectTree[0].children[0].children.find(
item => item.id === this.importForm.component
);
}, 1500);
},
// 缺陷标注相关方法
startDefectAnnotation() {
if (!this.selectedImage) {
this.$message.warning('请先选择一张图像');
return;
}
this.isDefectAnnotationMode = true;
this.isAutoDetectMode = false;
this.isAnnotating = true;
this.selectedComponent = this.currentComponent;
this.selectedDefect = null;
this.drawingState = {
isDrawing: false,
lastPoint: null,
currentLine: null,
currentPoints: []
};
// 添加事件监听
this.$refs.imageDisplay.addEventListener('mousedown', this.handleMouseDown);
this.$refs.imageDisplay.addEventListener('mousemove', this.handleMouseMove);
this.$refs.imageDisplay.addEventListener('mouseup', this.handleMouseUp);
this.$refs.imageDisplay.addEventListener('dblclick', this.handleDoubleClick);
},
cancelAnnotation() {
this.isAnnotating = false;
this.clearCurrentDrawing();
// 移除事件监听
this.$refs.imageDisplay.removeEventListener('mousedown', this.handleMouseDown);
this.$refs.imageDisplay.removeEventListener('mousemove', this.handleMouseMove);
this.$refs.imageDisplay.removeEventListener('mouseup', this.handleMouseUp);
this.$refs.imageDisplay.removeEventListener('dblclick', this.handleDoubleClick);
},
handleMouseDown(e) {
if (!this.isAnnotating || !this.selectedImage || e.button !== 0) return;
// 获取相对于图像的位置
const rect = this.$refs.imagePreview.getBoundingClientRect();
const x = (e.clientX - rect.left) / this.zoomLevel;
const y = (e.clientY - rect.top) / this.zoomLevel;
this.drawingState.isDrawing = true;
// 如果是第一个点,创建起点
if (this.drawingState.currentPoints.length === 0) {
this.drawingState.currentPoints.push({x, y});
this.drawPoint(x, y);
}
// 否则开始绘制直线
else {
this.drawingState.lastPoint = {x, y};
this.drawingState.currentLine = this.drawLine(
this.drawingState.currentPoints[this.drawingState.currentPoints.length - 1],
{x, y}
);
}
},
handleMouseMove(e) {
if (!this.isAnnotating || !this.selectedImage || !this.drawingState.isDrawing || !this.drawingState.lastPoint) return;
// 获取相对于图像的位置
const rect = this.$refs.imagePreview.getBoundingClientRect();
const x = (e.clientX - rect.left) / this.zoomLevel;
const y = (e.clientY - rect.top) / this.zoomLevel;
// 更新当前线段的终点
this.updateLine(
this.drawingState.currentLine,
this.drawingState.currentPoints[this.drawingState.currentPoints.length - 1],
{x, y}
);
},
handleMouseUp(e) {
if (!this.isAnnotating || !this.selectedImage || !this.drawingState.isDrawing || e.button !== 0) return;
// 获取相对于图像的位置
const rect = this.$refs.imagePreview.getBoundingClientRect();
const x = (e.clientX - rect.left) / this.zoomLevel;
const y = (e.clientY - rect.top) / this.zoomLevel;
this.drawingState.isDrawing = false;
// 完成当前线段
if (this.drawingState.lastPoint) {
this.drawingState.currentPoints.push({x, y});
this.drawPoint(x, y);
this.drawingState.lastPoint = null;
this.drawingState.currentLine = null;
}
},
handleDoubleClick(e) {
if (!this.isAnnotating || !this.selectedImage || this.drawingState.currentPoints.length < 3) return;
// 完成多边形绘制
this.completeAnnotation();
},
drawPoint(x, y) {
const svgNS = "http://www.w3.org/2000/svg";
const circle = document.createElementNS(svgNS, "circle");
circle.setAttribute('class', 'defect-point');
circle.setAttribute('cx', x * this.zoomLevel);
circle.setAttribute('cy', y * this.zoomLevel);
this.$refs.defectCanvas.appendChild(circle);
},
drawLine(start, end) {
const svgNS = "http://www.w3.org/2000/svg";
const line = document.createElementNS(svgNS, "line");
line.setAttribute('class', 'defect-line');
line.setAttribute('x1', start.x * this.zoomLevel);
line.setAttribute('y1', start.y * this.zoomLevel);
line.setAttribute('x2', end.x * this.zoomLevel);
line.setAttribute('y2', end.y * this.zoomLevel);
this.$refs.defectCanvas.appendChild(line);
return line;
},
updateLine(line, start, end) {
if (!line) return;
line.setAttribute('x2', end.x * this.zoomLevel);
line.setAttribute('y2', end.y * this.zoomLevel);
},
clearCurrentDrawing() {
// 清除画布上的临时绘制元素
this.$refs.defectCanvas.innerHTML = '';
this.drawingState = {
isDrawing: false,
lastPoint: null,
currentLine: null,
currentPoints: []
};
// 重新渲染已有的缺陷
this.renderDefects();
},
completeAnnotation() {
if (this.drawingState.currentPoints.length < 3) {
this.$message.warning('至少需要3个点才能构成多边形');
return;
}
// 计算缺陷面积
const area = Math.round(this.calculatePolygonArea(this.drawingState.currentPoints) / 100);
const axialLength = Math.round(Math.sqrt(area) * 2);
const chordLength = Math.round(Math.sqrt(area));
// 创建新缺陷
const newDefect = {
id: this.defects.length + 1,
imageId: this.selectedImage.id,
componentId: this.currentComponent.id,
code: `DF-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${this.defects.length + 1}`,
name: '新缺陷',
position: '待确定',
type: '裂纹',
severity: '轻微',
axialLength: axialLength,
chordLength: chordLength,
area: area,
repairStatus: '未处理',
description: '',
repairAdvice: '',
polygon: [...this.drawingState.currentPoints],
createdAt: new Date().toLocaleString(),
creator: '当前用户'
};
this.defects.push(newDefect);
this.selectedDefect = newDefect;
// 更新图像的缺陷计数
const image = this.images.find(img => img.id === this.selectedImage.id);
if (image) {
image.defectCount = (image.defectCount || 0) + 1;
}
// 清除当前绘制
this.clearCurrentDrawing();
// 退出标注模式
this.isAnnotating = false;
this.$message.success('缺陷标注完成');
},
// 计算多边形面积(使用鞋带公式)
calculatePolygonArea(points) {
let area = 0;
const n = points.length;
for (let i = 0; i < n; i++) {
const j = (i + 1) % n;
area += points[i].x * points[j].y;
area -= points[j].x * points[i].y;
}
return Math.abs(area / 2);
},
// 渲染所有缺陷
renderDefects() {
// 清除画布
this.$refs.defectCanvas.innerHTML = '';
// 设置画布大小与图像一致
if (this.selectedImage && this.$refs.imagePreview) {
const img = this.$refs.imagePreview;
this.$refs.defectCanvas.setAttribute('width', img.offsetWidth);
this.$refs.defectCanvas.setAttribute('height', img.offsetHeight);
}
// 如果没有选中图像或没有缺陷,直接返回
if (!this.selectedImage || this.currentImageDefects.length === 0) return;
// 渲染每个缺陷
this.currentImageDefects.forEach(defect => {
this.renderDefect(defect);
});
// 渲染检测到的缺陷
if (this.isAutoDetectMode && this.detectedDefects.length > 0) {
this.detectedDefects.forEach(defect => {
this.renderDetectedDefect(defect);
});
}
},
// 渲染单个缺陷
renderDefect(defect) {
if (!defect.polygon || defect.polygon.length < 3) return;
const svgNS = "http://www.w3.org/2000/svg";
const svg = this.$refs.defectCanvas;
// 创建多边形
const polygon = document.createElementNS(svgNS, "polygon");
polygon.setAttribute('class', 'defect-polygon');
polygon.setAttribute('points', defect.polygon.map(p =>
`${p.x * this.zoomLevel},${p.y * this.zoomLevel}`).join(' ')
);
polygon.setAttribute('data-defect-id', defect.id);
// 添加点击事件
polygon.addEventListener('click', () => {
this.selectDefect(defect);
});
// 添加右键菜单
polygon.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.showDefectContextMenu(e, defect);
});
svg.appendChild(polygon);
// 创建顶点
defect.polygon.forEach(point => {
const circle = document.createElementNS(svgNS, "circle");
circle.setAttribute('class', 'defect-point');
circle.setAttribute('cx', point.x * this.zoomLevel);
circle.setAttribute('cy', point.y * this.zoomLevel);
circle.setAttribute('data-defect-id', defect.id);
svg.appendChild(circle);
});
// 创建标签背景
const labelX = defect.polygon.reduce((sum, p) => sum + p.x, 0) / defect.polygon.length * this.zoomLevel;
const labelY = defect.polygon.reduce((sum, p) => sum + p.y, 0) / defect.polygon.length * this.zoomLevel;
const labelBg = document.createElementNS(svgNS, "rect");
labelBg.setAttribute('class', 'defect-label-bg');
labelBg.setAttribute('x', labelX - 30);
labelBg.setAttribute('y', labelY - 15);
labelBg.setAttribute('width', 60);
labelBg.setAttribute('height', 20);
labelBg.setAttribute('data-defect-id', defect.id);
svg.appendChild(labelBg);
// 创建标签文本
const label = document.createElementNS(svgNS, "text");
label.setAttribute('class', 'defect-label');
label.setAttribute('x', labelX);
label.setAttribute('y', labelY);
label.setAttribute('data-defect-id', defect.id);
label.textContent = defect.code.slice(-3); // 显示缺陷代码后三位
svg.appendChild(label);
// 高亮选中的缺陷
if (this.selectedDefect && this.selectedDefect.id === defect.id) {
polygon.setAttribute('stroke-width', '3');
polygon.setAttribute('stroke', '#409EFF');
polygon.setAttribute('fill', 'rgba(64, 158, 255, 0.3)');
}
},
// 渲染检测到的缺陷
renderDetectedDefect(defect) {
const svgNS = "http://www.w3.org/2000/svg";
const svg = this.$refs.defectCanvas;
// 创建矩形框
const rect = document.createElementNS(svgNS, "rect");
rect.setAttribute('class', 'defect-polygon');
rect.setAttribute('x', defect.position[0] * this.zoomLevel);
rect.setAttribute('y', defect.position[1] * this.zoomLevel);
rect.setAttribute('width', (defect.position[2] - defect.position[0]) * this.zoomLevel);
rect.setAttribute('height', (defect.position[3] - defect.position[1]) * this.zoomLevel);
rect.setAttribute('stroke', '#FFA500');
rect.setAttribute('fill', 'rgba(255, 165, 0, 0.2)');
rect.setAttribute('stroke-dasharray', '5,5');
rect.setAttribute('data-detected-id', defect.id);
svg.appendChild(rect);
// 创建标签背景
const labelBg = document.createElementNS(svgNS, "rect");
labelBg.setAttribute('class', 'defect-label-bg');
labelBg.setAttribute('x', defect.position[0] * this.zoomLevel);
labelBg.setAttribute('y', (defect.position[1] - 20) * this.zoomLevel);
labelBg.setAttribute('width', 100);
labelBg.setAttribute('height', 20);
labelBg.setAttribute('fill', 'rgba(255, 165, 0, 0.7)');
labelBg.setAttribute('data-detected-id', defect.id);
svg.appendChild(labelBg);
// 创建标签文本
const label = document.createElementNS(svgNS, "text");
label.setAttribute('class', 'defect-label');
label.setAttribute('x', (defect.position[0] + 50) * this.zoomLevel);
label.setAttribute('y', (defect.position[1] - 10) * this.zoomLevel);
label.setAttribute('data-detected-id', defect.id);
label.textContent = `${defect.type} ${defect.confidence}%`;
svg.appendChild(label);
},
// 选择缺陷
selectDefect(defect) {
this.selectedDefect = defect;
this.renderDefects(); // 重新渲染以更新高亮状态
},
// 查看缺陷
viewDefect(defect) {
this.selectedDefect = defect || this.selectedDefect;
this.defectContextMenu.visible = false;
},
// 编辑缺陷
editDefect(defect) {
this.selectedDefect = defect || this.selectedDefect;
this.defectContextMenu.visible = false;
},
// 删除缺陷
deleteDefect(defect) {
const targetDefect = defect || this.selectedDefect;
if (!targetDefect) return;
this.$confirm('确定要删除此缺陷吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const index = this.defects.findIndex(d => d.id === targetDefect.id);
if (index !== -1) {
this.defects.splice(index, 1);
// 更新图像的缺陷计数
const image = this.images.find(img => img.id === targetDefect.imageId);
if (image && image.defectCount > 0) {
image.defectCount--;
}
// 如果删除的是当前选中的缺陷,清空选中状态
if (this.selectedDefect && this.selectedDefect.id === targetDefect.id) {
this.selectedDefect = null;
}
this.$message.success('缺陷已删除');
this.renderDefects();
}
}).catch(() => {});
this.defectContextMenu.visible = false;
},
// 更新缺陷
updateDefect() {
if (!this.selectedDefect) return;
// 计算面积
this.selectedDefect.area = Math.round(
this.calculatePolygonArea(this.selectedDefect.polygon) / 100
);
this.$message.success('缺陷信息已保存');
},
// 获取组件的缺陷
getComponentDefects(componentId) {
return this.defects.filter(defect => defect.componentId === componentId);
},
// 获取组件的缺陷数量
getComponentDefectCount(componentId) {
return this.getComponentDefects(componentId).length;
},
// 显示标准描述库
showDescriptionLibrary() {
this.descriptionLibraryVisible = true;
this.selectedDescription = null;
},
// 选择标准描述
selectDescription(row) {
this.selectedDescription = row;
},
// 应用选中的标准描述
applySelectedDescription() {
if (this.selectedDescription && this.selectedDefect) {
// 替换模板中的占位符
let content = this.selectedDescription.content;
content = content.replace('{length}', this.selectedDefect.axialLength);
content = content.replace('{width}', this.selectedDefect.chordLength);
content = content.replace('{area}', this.selectedDefect.area);
this.selectedDefect.description = content;
this.descriptionLibraryVisible = false;
this.$message.success('标准描述已应用');
} else {
this.$message.warning('请先选择一条标准描述');
}
},
// 显示标准维修建议库
showRepairAdviceLibrary() {
this.repairAdviceLibraryVisible = true;
this.selectedRepairAdvice = null;
},
// 选择标准维修建议
selectRepairAdvice(row) {
this.selectedRepairAdvice = row;
},
// 应用选中的标准维修建议
applySelectedRepairAdvice() {
if (this.selectedRepairAdvice && this.selectedDefect) {
this.selectedDefect.repairAdvice = this.selectedRepairAdvice.content;
this.repairAdviceLibraryVisible = false;
this.$message.success('标准维修建议已应用');
} else {
this.$message.warning('请先选择一条标准维修建议');
}
},
// 自动识别缺陷相关方法
startAutoDetectDefects() {
if (!this.selectedImage) {
this.$message.warning('请先选择一张图像');
return;
}
this.isAutoDetectMode = true;
this.isDefectAnnotationMode = false;
this.isAnnotating = false;
this.detectedDefects = [];
this.clearCurrentDrawing();
},
startDetection() {
if (!this.selectedImage) {
this.$message.warning('请先选择一张图像');
return;
}
if (this.autoDetectForm.defectTypes.length === 0) {
this.$message.warning('请至少选择一种缺陷类型');
return;
}
this.detectionInProgress = true;
this.detectionProgress = 0;
this.detectionStatus = '';
this.detectionMessage = '正在初始化检测算法...';
// 模拟检测过程
const timer = setInterval(() => {
this.detectionProgress += 10;
if (this.detectionProgress < 30) {
this.detectionMessage = '正在加载模型...';
} else if (this.detectionProgress < 60) {
this.detectionMessage = '正在分析图像...';
} else if (this.detectionProgress < 90) {
this.detectionMessage = '正在识别缺陷...';
} else {
this.detectionMessage = '正在生成结果...';
}
if (this.detectionProgress >= 100) {
clearInterval(timer);
this.detectionInProgress = false;
this.detectionStatus = 'success';
this.detectionMessage = '检测完成';
// 模拟检测结果
this.detectedDefects = [
{
id: 1,
type: '裂纹',
confidence: 85,
position: [100, 100, 200, 150],
severity: '中等'
},
{
id: 2,
type: '腐蚀',
confidence: 78,
position: [300, 200, 380, 250],
severity: '轻微'
},
{
id: 3,
type: '磨损',
confidence: 92,
position: [200, 300, 280, 350],
severity: '严重'
}
];
this.$message.success(`检测到 ${this.detectedDefects.length} 处缺陷`);
this.renderDefects();
}
}, 300);
},
cancelDetection() {
this.isAutoDetectMode = false;
this.detectedDefects = [];
this.renderDefects();
},
// 高亮检测到的缺陷
highlightDetectedDefect(defect) {
// 在实际应用中,这里可以高亮显示选中的检测结果
this.$message.info(`选中缺陷: ${defect.type}, 置信度: ${defect.confidence}%`);
},
// 获取缺陷建议
getDefectAdvice(defect) {
const adviceMap = {
'裂纹': {
'轻微': '建议进行表面修复处理,防止裂纹扩展',
'中等': '建议进行局部修复并加强监测',
'严重': '建议更换受损部件'
},
'腐蚀': {
'轻微': '建议进行表面清洁和防腐处理',
'中等': '建议进行表面修复和防腐处理',
'严重': '建议更换受损部件'
},
'磨损': {
'轻微': '建议进行表面修复处理',
'中等': '建议进行局部修复',
'严重': '建议更换受损部件'
},
'分层': {
'轻微': '建议进行粘接修复',
'中等': '建议进行局部修复',
'严重': '建议更换受损部件'
},
'变形': {
'轻微': '建议进行矫正处理',
'中等': '建议进行局部修复',
'严重': '建议更换受损部件'
}
};
return adviceMap[defect.type]?.[defect.severity] || '请根据实际情况制定维修方案';
},
// 保存检测结果
saveDetectedDefects() {
if (this.detectedDefects.length === 0) {
this.$message.warning('没有检测到可保存的缺陷');
return;
}
this.$confirm('确定要将检测结果保存为正式缺陷记录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 将检测结果转换为正式缺陷记录
this.detectedDefects.forEach(detectedDefect => {
const area = Math.round(
(detectedDefect.position[2] - detectedDefect.position[0]) *
(detectedDefect.position[3] - detectedDefect.position[1]) / 100
);
const axialLength = Math.round(detectedDefect.position[2] - detectedDefect.position[0]);
const chordLength = Math.round(detectedDefect.position[3] - detectedDefect.position[1]);
const polygon = [
{x: detectedDefect.position[0], y: detectedDefect.position[1]},
{x: detectedDefect.position[2], y: detectedDefect.position[1]},
{x: detectedDefect.position[2], y: detectedDefect.position[3]},
{x: detectedDefect.position[0], y: detectedDefect.position[3]}
];
const newDefect = {
id: this.defects.length + 1,
imageId: this.selectedImage.id,
componentId: this.currentComponent.id,
code: `DF-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-${this.defects.length + 1}`,
name: `${detectedDefect.type}(${detectedDefect.confidence}%)`,
position: '自动识别位置',
type: detectedDefect.type,
severity: detectedDefect.severity,
axialLength: axialLength,
chordLength: chordLength,
area: area,
repairStatus: '未处理',
description: `自动识别${detectedDefect.type}, 置信度${detectedDefect.confidence}%`,
repairAdvice: this.getDefectAdvice(detectedDefect),
polygon: polygon,
createdAt: new Date().toLocaleString(),
creator: '自动识别系统'
};
this.defects.push(newDefect);
});
// 更新图像的缺陷计数
const image = this.images.find(img => img.id === this.selectedImage.id);
if (image) {
image.defectCount = (image.defectCount || 0) + this.detectedDefects.length;
}
this.$message.success(`成功保存 ${this.detectedDefects.length} 个缺陷记录`);
this.isAutoDetectMode = false;
this.detectedDefects = [];
this.renderDefects();
}).catch(() => {});
},
// 取消检测
cancelDetection() {
this.isAutoDetectMode = false;
this.detectedDefects = [];
this.renderDefects();
},
// 报告生成相关方法
showReportDialog() {
this.reportDialogVisible = true;
this.reportStep = 0;
},
cancelReport() {
this.reportDialogVisible = false;
},
prevReportStep() {
if (this.reportStep > 0) {
this.reportStep--;
}
},
nextReportStep() {
if (this.reportStep === 0) {
// 验证基本信息
if (!this.reportForm.mainTitle) {
this.$message.error('请输入报告主标题');
return;
}
if (this.reportForm.inspectionPersons.length === 0) {
this.$message.error('请至少添加一名检查人员');
return;
}
}
this.reportStep++;
},
showInputPerson() {
this.inputPersonVisible = true;
this.$nextTick(_ => {
this.$refs.saveTagInput.$refs.input.focus();
});
},
addInspectionPerson() {
if (this.inputPersonValue) {
this.reportForm.inspectionPersons.push(this.inputPersonValue);
this.inputPersonValue = '';
}
this.inputPersonVisible = false;
},
removeInspectionPerson(person) {
this.reportForm.inspectionPersons.splice(this.reportForm.inspectionPersons.indexOf(person), 1);
},
// 处理封面图片上传
handleCoverImageChange(file) {
const reader = new FileReader();
reader.onload = (e) => {
this.reportForm.coverImage = e.target.result;
};
reader.readAsDataURL(file.raw);
},
// 处理机组编号图片上传
handleUnitImageChange(file) {
const reader = new FileReader();
reader.onload = (e) => {
this.reportForm.unitImage = e.target.result;
};
reader.readAsDataURL(file.raw);
},
// 导出Word报告
exportReport() {
const { docx } = window;
// 创建文档
const doc = new docx.Document({
styles: {
paragraphStyles: [
{
id: "Normal",
name: "Normal",
run: {
size: 24, // 半角
font: "Times New Roman"
},
paragraph: {
spacing: {
line: 276, // 1.15倍行距
}
}
},
{
id: "Heading1",
name: "Heading 1",
basedOn: "Normal",
next: "Normal",
run: {
size: 32,
bold: true,
color: "000000"
},
paragraph: {
spacing: {
before: 240, // 1行
after: 240,
},
alignment: docx.AlignmentType.CENTER
}
},
{
id: "Heading2",
name: "Heading 2",
basedOn: "Normal",
next: "Normal",
run: {
size: 28,
bold: true,
color: "000000"
},
paragraph: {
spacing: {
before: 240,
after: 240,
},
alignment: docx.AlignmentType.LEFT,
border: {
bottom: {
color: "AAAAAA",
space: 1,
value: "single",
size: 6
}
}
}
},
{
id: "TableHeader",
name: "Table Header",
basedOn: "Normal",
run: {
bold: true,
color: "000000"
},
paragraph: {
spacing: {
before: 120,
after: 120,
}
}
}
]
}
});
// 添加封面
doc.addSection({
properties: {},
children: [
new docx.Paragraph({
text: this.reportForm.mainTitle || "国华新疆(哈密)望洋台风电一场",
heading: docx.HeadingLevel.HEADING_1,
spacing: {
after: 200
}
}),
new docx.Paragraph({
text: this.reportForm.subTitle || "风机叶片检查报告",
alignment: docx.AlignmentType.CENTER,
spacing: {
after: 100
}
}),
new docx.Paragraph({
text: this.reportForm.unitNumber || "35#机组",
alignment: docx.AlignmentType.CENTER,
spacing: {
after: 400
}
}),
// 封面图片
...(this.reportForm.coverImage ? [
new docx.Paragraph({
children: [
new docx.ImageRun({
data: this.reportForm.coverImage.split(',')[1],
transformation: {
width: 400,
height: 300
}
})
],
alignment: docx.AlignmentType.CENTER,
spacing: {
after: 400
}
})
] : []),
new docx.Paragraph({
text: this.reportForm.companyName || "武汉迪特聚能有限公司",
alignment: docx.AlignmentType.CENTER,
spacing: {
after: 100
}
}),
new docx.Paragraph({
text: `编制:${this.reportForm.reportAuthor || "王五"}`,
alignment: docx.AlignmentType.CENTER,
spacing: {
after: 100
}
}),
new docx.Paragraph({
text: `审核:${this.reportForm.reportReviewer || "赵六"}`,
alignment: docx.AlignmentType.CENTER,
spacing: {
after: 100
}
}),
new docx.Paragraph({
text: `日期:${this.reportForm.reportDate || new Date().toISOString().slice(0, 10)}`,
alignment: docx.AlignmentType.CENTER,
spacing: {
after: 400
}
}),
// 分页
new docx.Paragraph({
children: [new docx.TextRun({ text: "", break: 1 })]
}),
// 一、项目概况
new docx.Paragraph({
text: "一、项目概况",
heading: docx.HeadingLevel.HEADING_2
}),
new docx.Paragraph({
children: [
new docx.TextRun({ text: "风场名称:", bold: true }),
new docx.TextRun({ text: this.reportForm.windFarmName || "望洋台风电一场", break: 1 }),
new docx.TextRun({ text: "风场地址:", bold: true }),
new docx.TextRun({ text: this.reportForm.windFarmAddress || "新疆哈密市伊州区", break: 1 }),
new docx.TextRun({ text: "委托单位:", bold: true }),
new docx.TextRun({ text: this.reportForm.clientCompany || "国华能源投资有限公司", break: 1 }),
new docx.TextRun({ text: "检查单位:", bold: true }),
new docx.TextRun({ text: this.reportForm.inspectionCompany || "武汉迪特聚能有限公司", break: 1 }),
new docx.TextRun({ text: "委托联系人:", bold: true }),
new docx.TextRun({ text: `${this.reportForm.clientContact || "张经理"} ${this.reportForm.clientPhone || "13800138000"}`, break: 1 }),
new docx.TextRun({ text: "检查负责人:", bold: true }),
new docx.TextRun({ text: `${this.reportForm.inspectionLeader || "吴名洲"} ${this.reportForm.inspectionPhone || "18807109269"}`, break: 1 }),
new docx.TextRun({ text: "机组型号:", bold: true }),
new docx.TextRun({ text: this.reportForm.unitModel || "金风科技2.5MW", break: 1 }),
new docx.TextRun({ text: "项目规模:", bold: true }),
new docx.TextRun({ text: this.reportForm.projectScale || "15台", break: 1 }),
new docx.TextRun({ text: "总工期:", bold: true }),
new docx.TextRun({ text: this.reportForm.projectDuration || "40天", break: 2 })
]
}),
// 二、机组信息
new docx.Paragraph({
text: "二、机组信息",
heading: docx.HeadingLevel.HEADING_2
}),
new docx.Paragraph({
children: [
new docx.TextRun({ text: "主机厂商:", bold: true }),
new docx.TextRun({ text: this.reportForm.hostManufacturer || "金风科技", break: 1 }),
new docx.TextRun({ text: "部件厂商:", bold: true }),
new docx.TextRun({ text: this.reportForm.componentManufacturer || "中材科技风电叶片股份有限公司", break: 1 }),
new docx.TextRun({ text: "风机型号:", bold: true }),
new docx.TextRun({ text: this.reportForm.windTurbineModel || "2.5MW", break: 1 }),
new docx.TextRun({ text: "叶片型号:", bold: true }),
new docx.TextRun({ text: this.reportForm.bladeModel || "SI52.2B-2.5MW", break: 1 }),
new docx.TextRun({ text: "机组编号:", bold: true }),
new docx.TextRun({ text: this.reportForm.unitNumber || "35#机组", break: 1 }),
new docx.TextRun({ text: "叶片1编号", bold: true }),
new docx.TextRun({ text: this.reportForm.blade1Number || "D-15-051", break: 1 }),
new docx.TextRun({ text: "叶片2编号", bold: true }),
new docx.TextRun({ text: this.reportForm.blade2Number || "B-15-042", break: 1 }),
new docx.TextRun({ text: "叶片3编号", bold: true }),
new docx.TextRun({ text: this.reportForm.blade3Number || "B-15-059", break: 2 })
]
}),
// 机组编号图片
...(this.reportForm.unitImage ? [
new docx.Paragraph({
children: [
new docx.ImageRun({
data: this.reportForm.unitImage.split(',')[1],
transformation: {
width: 300,
height: 200
}
})
],
alignment: docx.AlignmentType.CENTER,
spacing: {
after: 400
}
})
] : []),
// 三、检查信息
new docx.Paragraph({
text: "三、检查信息",
heading: docx.HeadingLevel.HEADING_2
}),
new docx.Table({
width: {
size: 100,
type: docx.WidthType.PERCENTAGE
},
borders: {
top: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
bottom: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
left: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
right: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
insideHorizontal: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
insideVertical: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" }
},
rows: [
new docx.TableRow({
children: [
new docx.TableCell({
width: {
size: 20,
type: docx.WidthType.PERCENTAGE
},
children: [new docx.Paragraph({ text: "检查人员", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
width: {
size: 80,
type: docx.WidthType.PERCENTAGE
},
children: [new docx.Paragraph({
text: this.reportForm.inspectionPersons.join('、') || "张三、李四"
})],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
})
]
}),
new docx.TableRow({
children: [
new docx.TableCell({
children: [new docx.Paragraph({ text: "检查时间", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({
text: this.reportForm.inspectionDate || new Date().toISOString().slice(0, 10)
})],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
})
]
}),
new docx.TableRow({
children: [
new docx.TableCell({
children: [new docx.Paragraph({ text: "检查方式", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({
text: this.reportForm.inspectionMethod || "无人机巡检、人工检查"
})],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
})
]
}),
new docx.TableRow({
children: [
new docx.TableCell({
children: [new docx.Paragraph({ text: "检查内容", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({
text: this.reportForm.inspectionContent || "叶片外观检查、内部检查、防雷检测等"
})],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
})
]
})
]
}),
// 四、检查方案
new docx.Paragraph({
text: "四、检查方案",
heading: docx.HeadingLevel.HEADING_2
}),
new docx.Table({
width: {
size: 100,
type: docx.WidthType.PERCENTAGE
},
borders: {
top: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
bottom: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
left: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
right: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
insideHorizontal: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
insideVertical: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" }
},
rows: [
new docx.TableRow({
children: [
new docx.TableCell({
width: {
size: 25,
type: docx.WidthType.PERCENTAGE
},
children: [new docx.Paragraph({ text: "工作内容", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
width: {
size: 25,
type: docx.WidthType.PERCENTAGE
},
children: [new docx.Paragraph({ text: "人员配置", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
width: {
size: 20,
type: docx.WidthType.PERCENTAGE
},
children: [new docx.Paragraph({ text: "设备配置", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
width: {
size: 30,
type: docx.WidthType.PERCENTAGE
},
children: [new docx.Paragraph({ text: "备注", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
})
]
}),
new docx.TableRow({
children: [
new docx.TableCell({
children: [new docx.Paragraph({ text: "无人机叶片外观巡检" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: "1人主检飞手1人" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: "无人机" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: "" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
})
]
}),
new docx.TableRow({
children: [
new docx.TableCell({
children: [new docx.Paragraph({ text: "叶片内部检查" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: "2人轮毂作业检查2人" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: "检查设备" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: "" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
})
]
}),
new docx.TableRow({
children: [
new docx.TableCell({
children: [new docx.Paragraph({ text: "无人机叶片防雷导通测" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: "2人主检飞手1人副检抄表1人" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: "防雷检测设备" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: "" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
})
]
})
]
}),
// 五、检查情况汇总
new docx.Paragraph({
text: "五、检查情况汇总",
heading: docx.HeadingLevel.HEADING_2
}),
...this.defects.map(defect => {
return new docx.Paragraph({
children: [
new docx.TextRun({
text: `${defect.name} (${defect.code})`,
bold: true,
break: 1
}),
new docx.TextRun({
text: `位置: ${defect.position} | 类型: ${defect.type} | 严重程度: ${defect.severity}`,
size: 20,
break: 1
}),
new docx.TextRun({
text: defect.description,
break: 1
}),
new docx.TextRun({
text: `维修建议: ${defect.repairAdvice}`,
break: 2
})
]
});
}),
// 六、成果递交
new docx.Paragraph({
text: "六、成果递交",
heading: docx.HeadingLevel.HEADING_2
}),
new docx.Table({
width: {
size: 100,
type: docx.WidthType.PERCENTAGE
},
borders: {
top: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
bottom: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
left: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
right: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
insideHorizontal: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" },
insideVertical: { style: docx.BorderStyle.SINGLE, size: 4, color: "AAAAAA" }
},
rows: [
new docx.TableRow({
children: [
new docx.TableCell({
width: {
size: 20,
type: docx.WidthType.PERCENTAGE
},
children: [new docx.Paragraph({ text: "报告编制", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
width: {
size: 30,
type: docx.WidthType.PERCENTAGE
},
children: [new docx.Paragraph({ text: this.reportForm.reportAuthor || "王五" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
width: {
size: 20,
type: docx.WidthType.PERCENTAGE
},
children: [new docx.Paragraph({ text: "编制时间", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
width: {
size: 30,
type: docx.WidthType.PERCENTAGE
},
children: [new docx.Paragraph({ text: this.reportForm.reportDate || new Date().toISOString().slice(0, 10) })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
})
]
}),
new docx.TableRow({
children: [
new docx.TableCell({
children: [new docx.Paragraph({ text: "报告复核", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: this.reportForm.reportReviewer || "赵六" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: "复核时间", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: this.reportForm.reportDate || new Date().toISOString().slice(0, 10) })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
})
]
}),
new docx.TableRow({
children: [
new docx.TableCell({
children: [new docx.Paragraph({ text: "报告审核", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: this.reportForm.reportApprover || "钱七" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: "审核时间", style: "TableHeader" })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
}),
new docx.TableCell({
children: [new docx.Paragraph({ text: this.reportForm.reportDate || new Date().toISOString().slice(0, 10) })],
margins: {
top: 100,
bottom: 100,
left: 100,
right: 100
}
})
]
})
]
})
]
});
// 生成文档
docx.Packer.toBlob(doc).then(blob => {
saveAs(blob, `${this.reportForm.mainTitle || '风机叶片检查报告'}.docx`);
this.$message.success('报告导出成功');
this.reportDialogVisible = false;
});
}
}
});
</script>
</body>
</html>