Files
file-tool-wails/frontend/src/components/image/ImageRotateTool.vue
2026-06-17 08:05:20 +08:00

125 lines
9.0 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.
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
filePath: String,
filePreview: String,
imageInfo: Object,
originalWidth: Number,
originalHeight: Number,
processing: Boolean,
resultInfo: Object,
outputPath: String,
config: Object,
})
const emit = defineEmits(['update:outputPath', 'process', 'selectFile'])
const angleSlider = ref(0)
const rotationBgColor = ref('white')
const flipH = ref(false)
const flipV = ref(false)
function buildRequest() {
return {
inputPath: props.filePath,
outputPath: props.outputPath,
format: 'rotate',
angle: angleSlider.value,
bgColor: rotationBgColor.value,
flipH: flipH.value,
flipV: flipV.value,
}
}
function onProcess() { emit('process', buildRequest()) }
function getFileName() { return props.filePath ? props.filePath.split(/[/\\]/).pop() : '' }
function onDragOver(e) { e.preventDefault(); e.currentTarget.classList.add('dragover') }
function onDragLeave(e) { e.currentTarget.classList.remove('dragover') }
function onDrop(e) { e.preventDefault(); e.currentTarget.classList.remove('dragover'); if (e.dataTransfer?.files?.length) { const ext = e.dataTransfer.files[0].name.split('.').pop().toLowerCase(); if (!['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'tif', 'webp', 'ico'].includes(ext)) { ElMessage.warning('请选择图片文件'); return } emit('selectFile', e.dataTransfer.files[0].path) } }
</script>
<template>
<div v-if="!filePath" class="file-drop-zone" @click="$emit('selectFile')" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
<el-icon :size="40" class="drop-icon"><UploadFilled /></el-icon>
<div class="drop-text">点击选择图片</div>
<div class="drop-hint">支持拖拽图片到此处</div>
</div>
<div v-else class="image-workspace">
<div class="image-canvas-area">
<div class="canvas-container"><div class="canvas-wrapper"><img v-if="filePreview" :src="filePreview" class="preview-img" /></div></div>
<div class="image-info-bar">
<span class="image-dims">{{ originalWidth }} × {{ originalHeight }} px</span>
<span class="image-filename">{{ getFileName() }}</span>
<el-button type="danger" text size="small" @click="$emit('selectFile')"><el-icon><Delete /></el-icon></el-button>
</div>
</div>
<div class="image-panel">
<div class="panel-section">
<div class="panel-title">旋转设置</div>
<div class="param-group">
<div class="param-label">角度: {{ angleSlider }}°</div>
<el-slider v-model="angleSlider" :min="0" :max="360" :step="1" />
<div class="slider-hint"><span>0°</span><span>180°</span><span>360°</span></div>
</div>
<div class="param-group">
<div class="param-label">背景颜色</div>
<div class="tag-group">
<div class="tag-option" :class="{ active: rotationBgColor === 'white' }" @click="rotationBgColor = 'white'"><span class="bg-mini-swatch" style="background:#fff"></span>白色</div>
<div class="tag-option" :class="{ active: rotationBgColor === 'transparent' }" @click="rotationBgColor = 'transparent'"><span class="bg-mini-swatch bg-checker"></span>透明</div>
<div class="tag-option" :class="{ active: rotationBgColor === 'black' }" @click="rotationBgColor = 'black'"><span class="bg-mini-swatch" style="background:#1a1a1a"></span>黑色</div>
</div>
</div>
<div class="param-group">
<div class="param-label">翻转</div>
<div class="flip-btns">
<button class="flip-btn" :class="{ active: flipH }" @click="flipH = !flipH"><el-icon><DCaret /></el-icon> 水平</button>
<button class="flip-btn" :class="{ active: flipV }" @click="flipV = !flipV"><el-icon style="transform:rotate(90deg)"><DCaret /></el-icon> 垂直</button>
</div>
</div>
</div>
<div class="panel-section"><div class="panel-title">输出路径</div><div class="output-row"><el-input :value="outputPath" :placeholder="config?.defaultOutputDir ? '默认: ' + config.defaultOutputDir : '默认: 与输入文件同目录'" readonly size="small" /></div></div>
<el-button class="process-btn" type="primary" size="large" :loading="processing" :disabled="!filePath" @click="onProcess"><el-icon v-if="!processing"><Check /></el-icon>{{ processing ? '处理中...' : '开始处理' }}</el-button>
</div>
</div>
</template>
<style scoped>
.file-drop-zone { border: 2px dashed var(--glass-border); border-radius: var(--radius-lg); padding: 80px 40px; text-align: center; cursor: pointer; transition: all var(--transition-normal); background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); max-width: 600px; margin: 40px auto; }
.file-drop-zone:hover, .file-drop-zone.dragover { border-color: var(--accent-primary); background: rgba(59, 130, 246, 0.05); box-shadow: 0 0 20px rgba(59, 130, 246, 0.1); }
.drop-icon { color: var(--text-muted); margin-bottom: 8px; }
.drop-text { font-size: 15px; color: var(--text-primary); margin-bottom: 4px; }
.drop-hint { font-size: 12px; color: var(--text-muted); }
.image-workspace { display: grid; grid-template-columns: 1fr 320px; gap: 20px; height: calc(100vh - 160px); max-height: 800px; }
.image-canvas-area { display: flex; flex-direction: column; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); overflow: hidden; }
.canvas-container { flex: 1; display: flex; align-items: center; justify-content: center; background: #1a1a25; overflow: hidden; min-height: 400px; }
.canvas-wrapper { position: relative; display: inline-block; }
.preview-img { max-width: 100%; max-height: 100%; object-fit: contain; }
.image-info-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--glass-bg); border-top: 1px solid var(--glass-border); font-size: 13px; }
.image-dims { color: var(--text-secondary); font-family: monospace; }
.image-filename { color: var(--text-primary); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.image-panel { display: flex; flex-direction: column; gap: 16px; overflow-y: auto; background: var(--glass-bg); backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 20px; }
.panel-section { display: flex; flex-direction: column; gap: 4px; }
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--glass-border); }
.param-group { margin-bottom: 12px; }
.param-label { font-size: 13px; font-weight: 500; color: var(--text-secondary); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
.tag-group { display: flex; gap: 6px; flex-wrap: wrap; }
.tag-option { display: flex; align-items: center; gap: 4px; padding: 7px 14px; border-radius: var(--radius-sm); font-size: 13px; font-weight: 500; cursor: pointer; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--glass-border); transition: all var(--transition-normal); user-select: none; }
.tag-option:hover { color: var(--text-primary); border-color: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.05); }
.tag-option.active { background: var(--accent-primary); color: #ffffff; border-color: var(--accent-primary); box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); }
.slider-hint { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-muted); margin-top: 4px; }
.flip-btns { display: flex; gap: 8px; }
.flip-btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: var(--radius-sm); font-size: 13px; cursor: pointer; background: var(--bg-surface); border: 1px solid var(--glass-border); color: var(--text-secondary); transition: all var(--transition-normal); }
.flip-btn.active { background: var(--accent-primary); color: #fff; border-color: var(--accent-primary); }
.flip-btn:hover:not(.active) { color: var(--text-primary); border-color: rgba(255, 255, 255, 0.15); }
.bg-mini-swatch { width: 14px; height: 14px; border-radius: 4px; border: 1px solid rgba(255, 255, 255, 0.1); display: inline-block; }
.bg-checker { background-color: #fff; background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc), linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc); background-size: 8px 8px; background-position: 0 0, 4px 4px; }
.output-row { display: flex; gap: 8px; }
.output-row .el-input { flex: 1; }
.process-btn { width: 100%; height: 48px; font-size: 15px; font-weight: 600; border-radius: var(--radius-md); background: var(--gradient-primary); border: none; transition: all var(--transition-normal); }
.process-btn:hover:not(:disabled) { box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4); transform: translateY(-1px); }
.process-btn:active:not(:disabled) { transform: translateY(0) scale(0.98); }
</style>