1130 lines
29 KiB
Vue
1130 lines
29 KiB
Vue
<template>
|
||
<div v-if="modelValue" class="dialog-overlay">
|
||
<div class="dialog-box">
|
||
<div class="dialog-header">
|
||
<div class="dialog-title">{{ getDialogTitle() }} - I3M工业图像智能管理平台</div>
|
||
<div class="dialog-header-btns">
|
||
<button class="dialog-help-btn">?</button>
|
||
<button class="dialog-close-btn" @click="closeDialog">×</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="dialog-body">
|
||
<!-- 左侧步骤指示器 -->
|
||
<div class="steps-sidebar">
|
||
<div class="step" :class="{ active: currentStep === 1 }">
|
||
<span v-if="currentStep === 1" class="step-arrow">➔</span>
|
||
<span class="step-number">1.</span>
|
||
<span class="step-text">选择部件</span>
|
||
</div>
|
||
<div class="step" :class="{ active: currentStep === 2 }">
|
||
<span v-if="currentStep === 2" class="step-arrow">➔</span>
|
||
<span class="step-number">2.</span>
|
||
<span class="step-text">导入图像</span>
|
||
</div>
|
||
<div class="step" :class="{ active: currentStep === 3 }">
|
||
<span v-if="currentStep === 3" class="step-arrow">➔</span>
|
||
<span class="step-number">3.</span>
|
||
<span class="step-text">设置信息</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧内容区域 -->
|
||
<div class="dialog-content-container">
|
||
<!-- 步骤1: 选择部件 -->
|
||
<div v-if="currentStep === 1" class="dialog-content">
|
||
<div class="select-part-content">
|
||
<div class="section-title">选择要导入图像的部件:</div>
|
||
|
||
<div class="parts-container">
|
||
<div
|
||
v-for="part in availableParts"
|
||
:key="getPartId(part)"
|
||
class="part-item"
|
||
:class="{ selected: String(selectedPartId) === String(getPartId(part)) }"
|
||
@click="selectPart(part)"
|
||
:title="`部件ID: ${getPartId(part)}, 选中: ${String(selectedPartId) === String(getPartId(part))}`"
|
||
>
|
||
<div class="part-icon">
|
||
<svg v-if="part.partType === 'engine'" width="40" height="40" viewBox="0 0 70 70" xmlns="http://www.w3.org/2000/svg">
|
||
<rect x="10" y="20" width="50" height="30" fill="#a6c1ed" stroke="#5773a8" stroke-width="1" />
|
||
<rect x="20" y="15" width="10" height="5" fill="#a6c1ed" stroke="#5773a8" stroke-width="1" />
|
||
<rect x="40" y="15" width="10" height="5" fill="#a6c1ed" stroke="#5773a8" stroke-width="1" />
|
||
<rect x="15" y="50" width="10" height="5" fill="#a6c1ed" stroke="#5773a8" stroke-width="1" />
|
||
<rect x="45" y="50" width="10" height="5" fill="#a6c1ed" stroke="#5773a8" stroke-width="1" />
|
||
</svg>
|
||
<svg v-if="part.partType === 'blade'" width="40" height="40" viewBox="0 0 70 70" xmlns="http://www.w3.org/2000/svg">
|
||
<ellipse cx="35" cy="35" rx="20" ry="20" fill="#a6c1ed" stroke="#5773a8" stroke-width="1" />
|
||
<path d="M35,15 L15,35 L35,55 L55,35 z" fill="#a6c1ed" stroke="#5773a8" stroke-width="1" />
|
||
</svg>
|
||
<svg v-if="part.partType === 'tower'" width="40" height="40" viewBox="0 0 70 70" xmlns="http://www.w3.org/2000/svg">
|
||
<rect x="30" y="10" width="10" height="50" fill="#a6c1ed" stroke="#5773a8" stroke-width="1" />
|
||
<rect x="25" y="60" width="20" height="5" fill="#a6c1ed" stroke="#5773a8" stroke-width="1" />
|
||
<rect x="20" y="65" width="30" height="3" fill="#a6c1ed" stroke="#5773a8" stroke-width="1" />
|
||
</svg>
|
||
</div>
|
||
<div class="part-name">{{ getPartName(part) }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="part-info" v-if="selectedPart">
|
||
<div class="info-line">
|
||
<div class="info-label">部件:</div>
|
||
<div class="info-value">{{ getPartName(selectedPart) }}</div>
|
||
</div>
|
||
<div class="info-line">
|
||
<div class="info-label">类型:</div>
|
||
<div class="info-value">{{ getPartTypeText(selectedPart.partType) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 步骤2: 导入图像 -->
|
||
<div v-if="currentStep === 2" class="dialog-content">
|
||
<div class="import-image-content">
|
||
<div class="section-title">导入图像到部件"{{ selectedPart ? getPartName(selectedPart) : '' }}"</div>
|
||
|
||
<div class="image-actions">
|
||
<button class="action-button" @click="handleAddImages">
|
||
<span class="button-icon">+</span>
|
||
添加图像
|
||
</button>
|
||
<button class="action-button" @click="handleRemoveImages" :disabled="!hasSelectedImages">
|
||
<span class="button-icon">-</span>
|
||
移除图像
|
||
</button>
|
||
<!-- 隐藏的文件输入框 -->
|
||
<input
|
||
type="file"
|
||
ref="fileInput"
|
||
accept="image/*"
|
||
style="display: none;"
|
||
multiple
|
||
@change="handleFileSelected"
|
||
/>
|
||
</div>
|
||
|
||
<div class="image-list-container">
|
||
<table class="image-list">
|
||
<thead>
|
||
<tr>
|
||
<th class="checkbox-column">
|
||
<input type="checkbox" @change="toggleSelectAll" :checked="allImagesSelected">
|
||
</th>
|
||
<th class="preview-column">预览</th>
|
||
<th>图像名称</th>
|
||
<th>路径</th>
|
||
<th>尺寸</th>
|
||
<th>拍摄时间</th>
|
||
<th>像素(mm)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(image, index) in importImages" :key="index" @click="toggleImageSelection(image)">
|
||
<td>
|
||
<input type="checkbox" v-model="image.selected" @click.stop>
|
||
</td>
|
||
<td class="preview-cell">
|
||
<img v-if="image.previewUrl" :src="image.previewUrl" class="preview-thumbnail" alt="预览">
|
||
<div v-else class="no-preview">无预览</div>
|
||
</td>
|
||
<td>{{ image.name }}</td>
|
||
<td>{{ image.path }}</td>
|
||
<td>{{ image.width }} x {{ image.height }}</td>
|
||
<td>{{ formatDate(image.timestamp) }}</td>
|
||
<td>{{ image.pixelSize || '155.00' }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 步骤3: 设置图像采集信息 -->
|
||
<div v-if="currentStep === 3" class="dialog-content">
|
||
<div class="image-info-content">
|
||
<div class="section-title">步骤3: 设置图像采集信息</div>
|
||
|
||
<div class="form-container">
|
||
<div class="form-row">
|
||
<div class="form-label">拍摄时间范围</div>
|
||
<div class="form-input datetime-range">
|
||
<input type="text" v-model="imageInfo.startTime" placeholder="开始时间">
|
||
<span class="range-separator">至</span>
|
||
<input type="text" v-model="imageInfo.endTime" placeholder="结束时间">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-label">天气</div>
|
||
<div class="form-input">
|
||
<select v-model="imageInfo.weather">
|
||
<option value="晴天">晴天</option>
|
||
<option value="阴天">阴天</option>
|
||
<option value="多云">多云</option>
|
||
<option value="雨天">雨天</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-label">环境温度(°C)</div>
|
||
<div class="form-input temperature-range">
|
||
<div class="range-input-group">
|
||
<button class="range-btn" @click="imageInfo.minTemperature = Math.max(0, imageInfo.minTemperature - 1)">-</button>
|
||
<input type="number" v-model="imageInfo.minTemperature" step="0.1" min="0" max="50">
|
||
<button class="range-btn" @click="imageInfo.minTemperature = Math.min(50, imageInfo.minTemperature + 1)">+</button>
|
||
</div>
|
||
<span class="range-separator">~</span>
|
||
<div class="range-input-group">
|
||
<button class="range-btn" @click="imageInfo.maxTemperature = Math.max(0, imageInfo.maxTemperature - 1)">-</button>
|
||
<input type="number" v-model="imageInfo.maxTemperature" step="0.1" min="0" max="50">
|
||
<button class="range-btn" @click="imageInfo.maxTemperature = Math.min(50, imageInfo.maxTemperature + 1)">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-label">湿度(%)</div>
|
||
<div class="form-input">
|
||
<div class="range-input-group">
|
||
<button class="range-btn" @click="imageInfo.humidity = Math.max(0, imageInfo.humidity - 1)">-</button>
|
||
<input type="number" v-model="imageInfo.humidity" min="0" max="100">
|
||
<button class="range-btn" @click="imageInfo.humidity = Math.min(100, imageInfo.humidity + 1)">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-label">风力</div>
|
||
<div class="form-input">
|
||
<select v-model="imageInfo.windLevel">
|
||
<option value="0级(无风)">0级(无风)</option>
|
||
<option value="1级(软风)">1级(软风)</option>
|
||
<option value="2级(轻风)">2级(轻风)</option>
|
||
<option value="3级(微风)">3级(微风)</option>
|
||
<option value="4级(和风)">4级(和风)</option>
|
||
<option value="5级(清风)">5级(清风)</option>
|
||
<option value="6级(强风)">6级(强风)</option>
|
||
<option value="7级(疾风)">7级(疾风)</option>
|
||
<option value="8级(大风)">8级(大风)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-label">拍摄方式</div>
|
||
<div class="form-input capture-method">
|
||
<label class="radio-option">
|
||
<input type="radio" v-model="imageInfo.captureMethod" value="无人机航拍">
|
||
<span class="radio-label">无人机航拍</span>
|
||
</label>
|
||
<label class="radio-option">
|
||
<input type="radio" v-model="imageInfo.captureMethod" value="人工拍摄">
|
||
<span class="radio-label">人工拍摄</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-label">拍摄距离(米)</div>
|
||
<div class="form-input">
|
||
<div class="range-input-group">
|
||
<button class="range-btn" @click="imageInfo.captureDistance = Math.max(0, imageInfo.captureDistance - 1)">-</button>
|
||
<input type="number" v-model="imageInfo.captureDistance" min="0">
|
||
<button class="range-btn" @click="imageInfo.captureDistance = imageInfo.captureDistance + 1">+</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-label">采集员</div>
|
||
<div class="form-input">
|
||
<input type="text" v-model="imageInfo.operator">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-label">相机型号</div>
|
||
<div class="form-input">
|
||
<input type="text" v-model="imageInfo.cameraModel">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="dialog-footer">
|
||
<button
|
||
v-if="currentStep > 1"
|
||
class="dialog-button"
|
||
@click="currentStep--"
|
||
>上一步</button>
|
||
<button
|
||
v-if="currentStep < 3"
|
||
class="dialog-button"
|
||
@click="nextStep"
|
||
:disabled="!canGoNext"
|
||
>下一步</button>
|
||
<button
|
||
v-if="currentStep === 3"
|
||
class="dialog-button primary"
|
||
@click="finishImport"
|
||
>完成导入</button>
|
||
<button class="dialog-button" @click="closeDialog">取消</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, reactive, onBeforeUnmount } from 'vue'
|
||
import { Message } from '@arco-design/web-vue'
|
||
|
||
// 定义部件接口
|
||
interface Part {
|
||
id: string
|
||
name: string
|
||
partType: string
|
||
description?: string
|
||
manufacturer?: string
|
||
model?: string
|
||
}
|
||
|
||
// 定义图像接口
|
||
interface ImportImage {
|
||
file?: File
|
||
name: string
|
||
path: string
|
||
width: number
|
||
height: number
|
||
timestamp: string
|
||
pixelSize: string
|
||
selected: boolean
|
||
previewUrl?: string
|
||
}
|
||
|
||
// 定义图像信息接口
|
||
interface ImageInfo {
|
||
startTime: string
|
||
endTime: string
|
||
weather: string
|
||
humidity: number
|
||
minTemperature: number
|
||
maxTemperature: number
|
||
windPower: number
|
||
windLevel: string
|
||
captureMethod: string
|
||
captureDistance: number
|
||
operator: string
|
||
cameraModel: string
|
||
}
|
||
|
||
// 组件props
|
||
const props = defineProps<{
|
||
modelValue: boolean
|
||
selectedTurbineId?: string
|
||
availableParts?: Part[]
|
||
}>()
|
||
|
||
// 发出的事件
|
||
const emit = defineEmits<{
|
||
'update:modelValue': [value: boolean]
|
||
'import-success': [data: {
|
||
part: Part
|
||
images: File[]
|
||
imageInfo: ImageInfo
|
||
}]
|
||
}>()
|
||
|
||
// 当前步骤
|
||
const currentStep = ref(1)
|
||
|
||
// 文件选择器引用
|
||
const fileInput = ref<HTMLInputElement | null>(null)
|
||
|
||
// 选中的部件ID
|
||
const selectedPartId = ref('')
|
||
|
||
// 计算选中的部件对象
|
||
const selectedPart = computed(() => {
|
||
if (!selectedPartId.value) return null
|
||
return props.availableParts?.find(part => String(getPartId(part)) === String(selectedPartId.value))
|
||
})
|
||
|
||
// 待导入的图像列表
|
||
const importImages = ref<ImportImage[]>([])
|
||
|
||
// 图像采集信息
|
||
const imageInfo = reactive<ImageInfo>({
|
||
startTime: formatCurrentDate() + ' 00:00',
|
||
endTime: formatCurrentDate() + ' 23:59',
|
||
weather: '晴天',
|
||
humidity: 50,
|
||
minTemperature: 15,
|
||
maxTemperature: 25,
|
||
windPower: 0,
|
||
windLevel: '3级(微风)',
|
||
captureMethod: '无人机航拍',
|
||
captureDistance: 50,
|
||
operator: '',
|
||
cameraModel: 'ILCE-7RM4'
|
||
})
|
||
|
||
// 格式化当前日期
|
||
function formatCurrentDate() {
|
||
const now = new Date()
|
||
const year = now.getFullYear()
|
||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||
const day = String(now.getDate()).padStart(2, '0')
|
||
return `${year}/${month}/${day}`
|
||
}
|
||
|
||
// 获取对话框标题
|
||
function getDialogTitle() {
|
||
switch (currentStep.value) {
|
||
case 1: return '选择部件'
|
||
case 2: return `导入图像到部件"${selectedPart.value ? getPartName(selectedPart.value) : ''}"`
|
||
case 3: return '设置图像采集信息'
|
||
default: return '导入工业图像'
|
||
}
|
||
}
|
||
|
||
// 获取部件类型文本
|
||
function getPartTypeText(type: string) {
|
||
switch (type) {
|
||
case 'engine': return '机舱'
|
||
case 'blade': return '叶片'
|
||
case 'tower': return '塔筒'
|
||
default: return type
|
||
}
|
||
}
|
||
|
||
// 获取部件ID(兼容不同的字段名)
|
||
function getPartId(part: any): string {
|
||
return part.partId || part.id || ''
|
||
}
|
||
|
||
// 获取部件名称(兼容不同的字段名)
|
||
function getPartName(part: any): string {
|
||
return part.partName || part.name || ''
|
||
}
|
||
|
||
// 选择部件
|
||
function selectPart(part: any) {
|
||
const partId = getPartId(part)
|
||
|
||
if (!part || !partId) {
|
||
console.error('部件数据无效:', part)
|
||
return
|
||
}
|
||
|
||
selectedPartId.value = String(partId)
|
||
}
|
||
|
||
// 触发添加图像
|
||
function handleAddImages() {
|
||
fileInput.value?.click()
|
||
}
|
||
|
||
// 处理文件选择
|
||
function handleFileSelected(event: Event) {
|
||
const target = event.target as HTMLInputElement
|
||
if (target.files && target.files.length > 0) {
|
||
const newImages: ImportImage[] = Array.from(target.files).map(file => {
|
||
// 生成文件的本地URL预览
|
||
const previewUrl = URL.createObjectURL(file)
|
||
|
||
// 创建一个新的图像对象
|
||
return {
|
||
file,
|
||
name: file.name,
|
||
path: `E:/I3M/data/Data3/2B/${file.name}`,
|
||
width: 6186,
|
||
height: 4126,
|
||
timestamp: new Date().toISOString(),
|
||
pixelSize: '155.00',
|
||
selected: false,
|
||
previewUrl
|
||
}
|
||
})
|
||
|
||
// 添加到图像列表
|
||
importImages.value = [...importImages.value, ...newImages]
|
||
}
|
||
|
||
// 重置文件输入,允许再次选择相同文件
|
||
if (fileInput.value) fileInput.value.value = ''
|
||
}
|
||
|
||
// 移除选中的图像
|
||
function handleRemoveImages() {
|
||
// 清理被移除图像的URL对象
|
||
importImages.value.filter(image => image.selected).forEach(image => {
|
||
if (image.previewUrl) {
|
||
URL.revokeObjectURL(image.previewUrl)
|
||
}
|
||
})
|
||
|
||
importImages.value = importImages.value.filter(image => !image.selected)
|
||
}
|
||
|
||
// 切换图像选择状态
|
||
function toggleImageSelection(image: ImportImage) {
|
||
image.selected = !image.selected
|
||
}
|
||
|
||
// 全选/取消全选图像
|
||
function toggleSelectAll(event: Event) {
|
||
const checked = (event.target as HTMLInputElement).checked
|
||
importImages.value.forEach(image => image.selected = checked)
|
||
}
|
||
|
||
// 判断是否有选中的图像
|
||
const hasSelectedImages = computed(() => {
|
||
return importImages.value.some(image => image.selected)
|
||
})
|
||
|
||
// 判断是否所有图像都被选中
|
||
const allImagesSelected = computed(() => {
|
||
return importImages.value.length > 0 && importImages.value.every(image => image.selected)
|
||
})
|
||
|
||
// 格式化日期
|
||
function formatDate(dateString: string) {
|
||
try {
|
||
const date = new Date(dateString)
|
||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
|
||
} catch (e) {
|
||
return dateString
|
||
}
|
||
}
|
||
|
||
// 判断是否可以进入下一步
|
||
const canGoNext = computed(() => {
|
||
if (currentStep.value === 1) {
|
||
return !!selectedPartId.value
|
||
} else if (currentStep.value === 2) {
|
||
return importImages.value.length > 0
|
||
}
|
||
return true
|
||
})
|
||
|
||
// 进入下一步
|
||
function nextStep() {
|
||
if (!canGoNext.value) return
|
||
currentStep.value++
|
||
}
|
||
|
||
// 完成导入
|
||
function finishImport() {
|
||
if (!selectedPart.value) {
|
||
Message.error('请选择部件')
|
||
return
|
||
}
|
||
|
||
if (importImages.value.length === 0) {
|
||
Message.error('请添加图像')
|
||
return
|
||
}
|
||
|
||
// 准备导入的数据
|
||
const files = importImages.value.map(image => image.file!).filter(Boolean)
|
||
|
||
// 构建统一的部件数据格式
|
||
const partData = {
|
||
partId: getPartId(selectedPart.value),
|
||
id: getPartId(selectedPart.value), // 兼容字段
|
||
name: getPartName(selectedPart.value),
|
||
partName: getPartName(selectedPart.value), // 兼容字段
|
||
partType: selectedPart.value.partType
|
||
}
|
||
|
||
// 发送导入事件
|
||
emit('import-success', {
|
||
part: partData,
|
||
images: files,
|
||
imageInfo: { ...imageInfo }
|
||
})
|
||
|
||
Message.success('图像导入成功')
|
||
|
||
// 关闭对话框
|
||
closeDialog()
|
||
}
|
||
|
||
// 关闭对话框
|
||
function closeDialog() {
|
||
emit('update:modelValue', false)
|
||
// 重置状态
|
||
resetState()
|
||
}
|
||
|
||
// 重置状态
|
||
function resetState() {
|
||
currentStep.value = 1
|
||
selectedPartId.value = ''
|
||
|
||
// 清理图像URL对象
|
||
importImages.value.forEach(image => {
|
||
if (image.previewUrl) {
|
||
URL.revokeObjectURL(image.previewUrl)
|
||
}
|
||
})
|
||
importImages.value = []
|
||
|
||
// 重置图像信息
|
||
Object.assign(imageInfo, {
|
||
startTime: formatCurrentDate() + ' 00:00',
|
||
endTime: formatCurrentDate() + ' 23:59',
|
||
weather: '晴天',
|
||
humidity: 70,
|
||
minTemperature: 20,
|
||
maxTemperature: 35,
|
||
windPower: 0,
|
||
captureMethod: '无人机拍摄',
|
||
captureDistance: 15,
|
||
operator: ''
|
||
})
|
||
}
|
||
|
||
// 在组件卸载时清理URL对象
|
||
onBeforeUnmount(() => {
|
||
// 清理所有创建的对象URL以防止内存泄漏
|
||
importImages.value.forEach(image => {
|
||
if (image.previewUrl) {
|
||
URL.revokeObjectURL(image.previewUrl)
|
||
}
|
||
})
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.dialog-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.dialog-box {
|
||
width: 800px;
|
||
max-height: 90vh;
|
||
background-color: #f0f0f0;
|
||
border: 1px solid #999;
|
||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.dialog-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
background: linear-gradient(to right, #4a6eaf, #7a9cd3);
|
||
color: white;
|
||
padding: 3px 5px;
|
||
height: 22px;
|
||
}
|
||
|
||
.dialog-title {
|
||
font-size: 12px;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.dialog-header-btns {
|
||
display: flex;
|
||
}
|
||
|
||
.dialog-help-btn,
|
||
.dialog-close-btn {
|
||
width: 16px;
|
||
height: 16px;
|
||
background-color: #f0f0f0;
|
||
border: 1px solid #999;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-left: 2px;
|
||
font-size: 10px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
}
|
||
|
||
.dialog-close-btn {
|
||
font-weight: bold;
|
||
}
|
||
|
||
.dialog-body {
|
||
display: flex;
|
||
flex-direction: row;
|
||
flex: 1;
|
||
min-height: 500px;
|
||
max-height: 70vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 左侧步骤指示器 */
|
||
.steps-sidebar {
|
||
width: 100px;
|
||
background-color: #f0f0f0;
|
||
border-right: 1px solid #ddd;
|
||
padding: 15px 10px 15px 15px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.step {
|
||
display: flex;
|
||
align-items: center;
|
||
color: #666;
|
||
font-size: 12px;
|
||
padding: 6px 0;
|
||
position: relative;
|
||
}
|
||
|
||
.step.active {
|
||
color: #0050ab;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.step-arrow {
|
||
position: absolute;
|
||
left: -10px;
|
||
color: #0050ab;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.step-number {
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.step-text {
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 右侧内容区域 */
|
||
.dialog-content-container {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
background-color: #fff;
|
||
}
|
||
|
||
.dialog-content {
|
||
flex: 1;
|
||
padding: 10px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
margin-bottom: 15px;
|
||
color: #374151;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
}
|
||
|
||
/* 选择部件样式 */
|
||
.select-part-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
.parts-container {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
padding: 10px;
|
||
border: 1px solid #ddd;
|
||
background-color: #fff;
|
||
margin-bottom: 10px;
|
||
overflow-y: auto;
|
||
max-height: 320px;
|
||
}
|
||
|
||
.part-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
width: 100px;
|
||
height: 100px;
|
||
border: 2px solid transparent;
|
||
padding: 5px;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.part-item:hover {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.part-item.selected {
|
||
border-color: #0050ab;
|
||
background-color: #e6f0ff;
|
||
}
|
||
|
||
.part-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 60px;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.part-name {
|
||
font-size: 12px;
|
||
text-align: center;
|
||
}
|
||
|
||
.part-info {
|
||
padding: 10px;
|
||
background-color: #f5f5f5;
|
||
border: 1px solid #ddd;
|
||
margin-top: auto;
|
||
}
|
||
|
||
.info-line {
|
||
display: flex;
|
||
font-size: 12px;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.info-label {
|
||
width: 60px;
|
||
text-align: right;
|
||
margin-right: 10px;
|
||
color: #666;
|
||
}
|
||
|
||
.info-value {
|
||
color: #333;
|
||
}
|
||
|
||
/* 导入图像样式 */
|
||
.import-image-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
.image-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.action-button {
|
||
display: flex;
|
||
align-items: center;
|
||
background-color: #f0f0f0;
|
||
border: 1px solid #ccc;
|
||
border-radius: 3px;
|
||
padding: 4px 8px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.action-button:hover {
|
||
background-color: #e0e0e0;
|
||
}
|
||
|
||
.action-button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.button-icon {
|
||
margin-right: 5px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.image-list-container {
|
||
flex: 1;
|
||
border: 1px solid #ddd;
|
||
overflow: auto;
|
||
}
|
||
|
||
.image-list {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.image-list th {
|
||
background-color: #f0f0f0;
|
||
border-bottom: 1px solid #ddd;
|
||
padding: 4px 8px;
|
||
text-align: left;
|
||
font-weight: normal;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.image-list td {
|
||
border-bottom: 1px solid #eee;
|
||
padding: 4px 8px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.image-list tr:hover {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.checkbox-column {
|
||
width: 30px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 预览列样式 */
|
||
.preview-column {
|
||
width: 80px;
|
||
}
|
||
|
||
.preview-cell {
|
||
text-align: center;
|
||
}
|
||
|
||
.preview-thumbnail {
|
||
width: 60px;
|
||
height: 40px;
|
||
object-fit: cover;
|
||
border: 1px solid #ddd;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.no-preview {
|
||
color: #999;
|
||
font-size: 11px;
|
||
}
|
||
|
||
/* 设置信息样式 */
|
||
.image-info-content {
|
||
padding: 10px;
|
||
}
|
||
|
||
.form-container {
|
||
background-color: #fff;
|
||
padding: 20px;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 6px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.form-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
padding: 5px 0;
|
||
}
|
||
|
||
.form-label {
|
||
width: 120px;
|
||
text-align: left;
|
||
margin-right: 10px;
|
||
font-size: 12px;
|
||
color: #333;
|
||
}
|
||
|
||
.form-label.secondary {
|
||
width: auto;
|
||
margin-left: 15px;
|
||
color: #666;
|
||
}
|
||
|
||
.form-input {
|
||
flex: 1;
|
||
position: relative;
|
||
max-width: 300px;
|
||
}
|
||
|
||
.form-input input,
|
||
.form-input select {
|
||
width: 100%;
|
||
padding: 6px 10px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
transition: border-color 0.2s ease;
|
||
}
|
||
|
||
.form-input input:focus,
|
||
.form-input select:focus {
|
||
outline: none;
|
||
border-color: #3b82f6;
|
||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
||
}
|
||
|
||
.form-input select {
|
||
height: 30px;
|
||
}
|
||
|
||
.datetime-range {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.temperature-range {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.datetime-range input {
|
||
width: 150px;
|
||
height: 30px;
|
||
}
|
||
|
||
.temperature-range .range-input-group {
|
||
width: 90px;
|
||
}
|
||
|
||
.range-separator {
|
||
margin: 0 5px;
|
||
color: #666;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.number-input {
|
||
width: 100px;
|
||
flex: none;
|
||
}
|
||
|
||
.unit-label {
|
||
position: absolute;
|
||
right: 5px;
|
||
top: 3px;
|
||
font-size: 12px;
|
||
color: #666;
|
||
}
|
||
|
||
/* 新增的表单元素样式 */
|
||
.range-input-group {
|
||
display: flex;
|
||
align-items: center;
|
||
width: 120px;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
height: 30px;
|
||
}
|
||
|
||
.range-btn {
|
||
width: 30px;
|
||
height: 100%;
|
||
background-color: #f8f9fa;
|
||
border: none;
|
||
border-right: 1px solid #d1d5db;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: #6b7280;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.range-btn:hover {
|
||
background-color: #e5e7eb;
|
||
}
|
||
|
||
.range-btn:last-child {
|
||
border-right: none;
|
||
border-left: 1px solid #d1d5db;
|
||
}
|
||
|
||
.range-input-group input {
|
||
flex: 1;
|
||
text-align: center;
|
||
border: none;
|
||
outline: none;
|
||
padding: 4px;
|
||
font-size: 12px;
|
||
min-width: 30px;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.capture-method {
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
.radio-option {
|
||
display: flex;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
padding: 4px 8px;
|
||
border-radius: 3px;
|
||
transition: background-color 0.2s ease;
|
||
}
|
||
|
||
.radio-option:hover {
|
||
background-color: #f8f9fa;
|
||
}
|
||
|
||
.radio-option input[type="radio"] {
|
||
width: auto;
|
||
margin-right: 8px;
|
||
accent-color: #3b82f6;
|
||
}
|
||
|
||
.radio-label {
|
||
color: #374151;
|
||
}
|
||
|
||
/* 对话框底部 */
|
||
.dialog-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 10px;
|
||
background-color: #f0f0f0;
|
||
border-top: 1px solid #ccc;
|
||
}
|
||
|
||
.dialog-button {
|
||
padding: 8px 16px;
|
||
margin-left: 8px;
|
||
background-color: #f8f9fa;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.dialog-button:hover {
|
||
background-color: #e5e7eb;
|
||
border-color: #9ca3af;
|
||
}
|
||
|
||
.dialog-button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.dialog-button.primary {
|
||
background-color: #3b82f6;
|
||
color: white;
|
||
border-color: #3b82f6;
|
||
}
|
||
|
||
.dialog-button.primary:hover {
|
||
background-color: #2563eb;
|
||
}
|
||
</style> |