Industrial-image-management.../src/components/ImageImportWizard/index.vue

1130 lines
29 KiB
Vue
Raw Normal View History

2025-07-14 11:11:33 +08:00
<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>