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

1130 lines
29 KiB
Vue
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.

<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>