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

3897 lines
190 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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.reportCompany) {
this.$message.error('请输入报告单位');
return;
}
if (!this.reportForm.reportDate) {
this.$message.error('请选择报告日期');
return;
}
if (!this.reportForm.inspectionDate) {
this.$message.error('请选择检查日期');
return;
}
if (!this.reportForm.inspectionLocation) {
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>