1
0
forked from zguiy/utils

工具完成

This commit is contained in:
2025-06-28 22:38:49 +08:00
parent 2c668fedd0
commit 8400dbfab9
60 changed files with 23197 additions and 144 deletions

View File

@ -0,0 +1,773 @@
<template>
<div class="space-y-6">
<!-- 工具栏 -->
<div class="card p-4">
<div class="flex flex-wrap gap-2">
<button
@click="triggerFileInput"
class="btn-primary"
>
<FontAwesomeIcon :icon="['fas', 'upload']" class="mr-2" />
上传图片
</button>
<button
@click="addTextWatermark"
:disabled="!originalImage"
class="btn-secondary"
>
<FontAwesomeIcon :icon="['fas', 'font']" class="mr-2" />
文字水印
</button>
<button
@click="triggerWatermarkInput"
:disabled="!originalImage"
class="btn-secondary"
>
<FontAwesomeIcon :icon="['fas', 'image']" class="mr-2" />
图片水印
</button>
<button
@click="downloadImage"
:disabled="!processedImage"
class="btn-secondary"
>
<FontAwesomeIcon :icon="['fas', 'download']" class="mr-2" />
下载图片
</button>
<button
@click="resetImage"
:disabled="!originalImage"
class="btn-secondary"
>
<FontAwesomeIcon :icon="['fas', 'undo']" class="mr-2" />
重置
</button>
</div>
<!-- 隐藏的文件输入 -->
<input
ref="fileInput"
type="file"
accept="image/*"
class="hidden"
@change="handleImageUpload"
>
<input
ref="watermarkInput"
type="file"
accept="image/*"
class="hidden"
@change="handleWatermarkUpload"
>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 设置区域 -->
<div class="space-y-4">
<!-- 文字水印设置 -->
<div v-if="watermarkType === 'text'" class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">文字水印设置</h3>
<div class="space-y-4">
<!-- 水印文字 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">水印文字</label>
<input
v-model="textWatermark.text"
type="text"
placeholder="请输入水印文字"
class="input-field"
@input="updateWatermark"
>
</div>
<!-- 字体大小 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">
字体大小: {{ textWatermark.fontSize }}px
</label>
<input
v-model.number="textWatermark.fontSize"
type="range"
min="12"
max="120"
step="2"
class="w-full"
@input="updateWatermark"
>
</div>
<!-- 字体颜色 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">字体颜色</label>
<div class="flex space-x-2">
<input
v-model="textWatermark.color"
type="color"
class="w-12 h-8 rounded border border-primary border-opacity-20"
@input="updateWatermark"
>
<input
v-model="textWatermark.color"
type="text"
class="input-field flex-1"
@input="updateWatermark"
>
</div>
</div>
<!-- 透明度 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">
透明度: {{ Math.round(textWatermark.opacity * 100) }}%
</label>
<input
v-model.number="textWatermark.opacity"
type="range"
min="0.1"
max="1"
step="0.1"
class="w-full"
@input="updateWatermark"
>
</div>
<!-- 字体样式 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">字体样式</label>
<div class="flex space-x-2">
<button
@click="toggleFontStyle('bold')"
:class="['btn-sm', textWatermark.fontWeight === 'bold' ? 'btn-primary' : 'btn-secondary']"
>
<FontAwesomeIcon :icon="['fas', 'bold']" />
</button>
<button
@click="toggleFontStyle('italic')"
:class="['btn-sm', textWatermark.fontStyle === 'italic' ? 'btn-primary' : 'btn-secondary']"
>
<FontAwesomeIcon :icon="['fas', 'italic']" />
</button>
</div>
</div>
<!-- 旋转角度 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">
旋转角度: {{ textWatermark.rotation }}°
</label>
<input
v-model.number="textWatermark.rotation"
type="range"
min="-45"
max="45"
step="5"
class="w-full"
@input="updateWatermark"
>
</div>
</div>
</div>
<!-- 图片水印设置 -->
<div v-if="watermarkType === 'image'" class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">图片水印设置</h3>
<div class="space-y-4">
<!-- 水印图片预览 -->
<div v-if="watermarkImage">
<label class="block text-sm font-medium text-secondary mb-2">水印图片</label>
<div class="bg-block rounded-lg p-4">
<img :src="watermarkImage" alt="水印图片" class="max-w-full h-20 object-contain mx-auto">
</div>
</div>
<!-- 缩放比例 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">
缩放比例: {{ Math.round(imageWatermark.scale * 100) }}%
</label>
<input
v-model.number="imageWatermark.scale"
type="range"
min="0.1"
max="2"
step="0.1"
class="w-full"
@input="updateWatermark"
>
</div>
<!-- 透明度 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">
透明度: {{ Math.round(imageWatermark.opacity * 100) }}%
</label>
<input
v-model.number="imageWatermark.opacity"
type="range"
min="0.1"
max="1"
step="0.1"
class="w-full"
@input="updateWatermark"
>
</div>
<!-- 旋转角度 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">
旋转角度: {{ imageWatermark.rotation }}°
</label>
<input
v-model.number="imageWatermark.rotation"
type="range"
min="-180"
max="180"
step="15"
class="w-full"
@input="updateWatermark"
>
</div>
</div>
</div>
<!-- 位置设置 -->
<div v-if="originalImage" class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">位置设置</h3>
<div class="space-y-4">
<!-- 预设位置 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">快速定位</label>
<div class="grid grid-cols-3 gap-2">
<button
v-for="(pos, key) in positions"
:key="key"
@click="setPosition(key)"
:class="['btn-sm text-xs', currentPosition.key === key ? 'btn-primary' : 'btn-secondary']"
>
{{ pos.name }}
</button>
</div>
</div>
<!-- 自定义位置 -->
<div>
<label class="block text-sm font-medium text-secondary mb-2">
X坐标: {{ currentPosition.x }}px
</label>
<input
v-model.number="currentPosition.x"
type="range"
:min="0"
:max="canvasWidth"
class="w-full"
@input="updateWatermark"
>
</div>
<div>
<label class="block text-sm font-medium text-secondary mb-2">
Y坐标: {{ currentPosition.y }}px
</label>
<input
v-model.number="currentPosition.y"
type="range"
:min="0"
:max="canvasHeight"
class="w-full"
@input="updateWatermark"
>
</div>
</div>
</div>
<!-- 批量水印设置 -->
<div v-if="originalImage" class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">批量水印</h3>
<div class="space-y-3">
<label class="flex items-center space-x-2">
<input
v-model="batchSettings.enabled"
type="checkbox"
class="form-checkbox"
@change="updateWatermark"
>
<span class="text-secondary">启用平铺水印</span>
</label>
<div v-if="batchSettings.enabled" class="space-y-3">
<div>
<label class="block text-sm font-medium text-secondary mb-2">
水平间距: {{ batchSettings.spacingX }}px
</label>
<input
v-model.number="batchSettings.spacingX"
type="range"
min="50"
max="300"
step="10"
class="w-full"
@input="updateWatermark"
>
</div>
<div>
<label class="block text-sm font-medium text-secondary mb-2">
垂直间距: {{ batchSettings.spacingY }}px
</label>
<input
v-model.number="batchSettings.spacingY"
type="range"
min="50"
max="300"
step="10"
class="w-full"
@input="updateWatermark"
>
</div>
</div>
</div>
</div>
</div>
<!-- 预览区域 -->
<div class="lg:col-span-2 space-y-4">
<!-- 原图预览 -->
<div class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">图片预览</h3>
<div v-if="!originalImage" class="bg-block rounded-lg p-8 text-center">
<FontAwesomeIcon :icon="['fas', 'cloud-upload-alt']" class="text-4xl text-tertiary mb-4" />
<p class="text-secondary mb-4">请上传图片或拖拽图片到此处</p>
<button @click="triggerFileInput" class="btn-primary">
选择图片
</button>
</div>
<div v-else class="relative">
<canvas
ref="previewCanvas"
class="max-w-full border border-primary border-opacity-20 rounded-lg cursor-crosshair"
@click="handleCanvasClick"
/>
<!-- 图片信息 -->
<div class="mt-4 flex justify-between items-center text-sm text-secondary">
<span>{{ imageInfo.width }} × {{ imageInfo.height }}</span>
<span>{{ imageInfo.size }}</span>
<span>{{ imageInfo.format }}</span>
</div>
</div>
</div>
<!-- 处理结果 -->
<div v-if="processedImage" class="card p-4">
<h3 class="text-lg font-semibold text-primary mb-3">处理结果</h3>
<div class="bg-block rounded-lg p-4">
<img :src="processedImage" alt="处理后的图片" class="max-w-full mx-auto">
</div>
<div class="mt-4 flex justify-between items-center text-sm text-secondary">
<span>质量: {{ outputQuality }}%</span>
<span>大小: {{ outputSize }}</span>
</div>
</div>
</div>
</div>
<!-- 状态消息 -->
<div v-if="statusMessage" class="card p-4">
<div :class="[
'flex items-center space-x-2',
statusType === 'success' ? 'text-success' : 'text-error'
]">
<FontAwesomeIcon
:icon="statusType === 'success' ? ['fas', 'check'] : ['fas', 'times']"
/>
<span>{{ statusMessage }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick } from 'vue'
import { useLanguage } from '@/composables/useLanguage'
const { t } = useLanguage()
// DOM引用
const fileInput = ref<HTMLInputElement>()
const watermarkInput = ref<HTMLInputElement>()
const previewCanvas = ref<HTMLCanvasElement>()
// 响应式状态
const originalImage = ref('')
const processedImage = ref('')
const watermarkImage = ref('')
const watermarkType = ref<'text' | 'image' | null>(null)
const statusMessage = ref('')
const statusType = ref<'success' | 'error'>('success')
// 图片信息
const imageInfo = reactive({
width: 0,
height: 0,
size: '',
format: ''
})
// Canvas尺寸
const canvasWidth = ref(0)
const canvasHeight = ref(0)
// 文字水印设置
const textWatermark = reactive({
text: 'Sample Watermark',
fontSize: 36,
color: '#ffffff',
opacity: 0.7,
fontWeight: 'normal',
fontStyle: 'normal',
rotation: 0
})
// 图片水印设置
const imageWatermark = reactive({
scale: 0.3,
opacity: 0.7,
rotation: 0
})
// 位置设置
const currentPosition = reactive({
x: 0,
y: 0,
key: 'center'
})
// 批量水印设置
const batchSettings = reactive({
enabled: false,
spacingX: 150,
spacingY: 150
})
// 预设位置
const positions = {
'top-left': { name: '左上', x: 0.1, y: 0.1 },
'top-center': { name: '居上', x: 0.5, y: 0.1 },
'top-right': { name: '右上', x: 0.9, y: 0.1 },
'center-left': { name: '居左', x: 0.1, y: 0.5 },
'center': { name: '居中', x: 0.5, y: 0.5 },
'center-right': { name: '居右', x: 0.9, y: 0.5 },
'bottom-left': { name: '左下', x: 0.1, y: 0.9 },
'bottom-center': { name: '居下', x: 0.5, y: 0.9 },
'bottom-right': { name: '右下', x: 0.9, y: 0.9 }
}
// 输出设置
const outputQuality = computed(() => 90)
const outputSize = computed(() => {
if (!processedImage.value) return ''
return '估算大小'
})
// 触发文件选择
const triggerFileInput = () => {
fileInput.value?.click()
}
const triggerWatermarkInput = () => {
watermarkInput.value?.click()
}
// 处理图片上传
const handleImageUpload = (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (!file) return
if (!file.type.startsWith('image/')) {
showStatus('请选择有效的图片文件', 'error')
return
}
const reader = new FileReader()
reader.onload = (e) => {
originalImage.value = e.target?.result as string
loadImageInfo(file)
nextTick(() => {
setupCanvas()
})
}
reader.readAsDataURL(file)
}
// 处理水印图片上传
const handleWatermarkUpload = (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (!file) return
if (!file.type.startsWith('image/')) {
showStatus('请选择有效的图片文件', 'error')
return
}
const reader = new FileReader()
reader.onload = (e) => {
watermarkImage.value = e.target?.result as string
watermarkType.value = 'image'
updateWatermark()
}
reader.readAsDataURL(file)
}
// 加载图片信息
const loadImageInfo = (file: File) => {
const img = new Image()
img.onload = () => {
imageInfo.width = img.width
imageInfo.height = img.height
imageInfo.size = formatFileSize(file.size)
imageInfo.format = file.type.split('/')[1].toUpperCase()
}
img.src = originalImage.value
}
// 设置Canvas
const setupCanvas = () => {
if (!previewCanvas.value || !originalImage.value) return
const img = new Image()
img.onload = () => {
const canvas = previewCanvas.value!
const ctx = canvas.getContext('2d')!
// 设置画布尺寸
const maxWidth = 600
const maxHeight = 400
let { width, height } = img
if (width > maxWidth) {
height = (height * maxWidth) / width
width = maxWidth
}
if (height > maxHeight) {
width = (width * maxHeight) / height
height = maxHeight
}
canvas.width = width
canvas.height = height
canvasWidth.value = width
canvasHeight.value = height
// 绘制原图
ctx.drawImage(img, 0, 0, width, height)
// 设置默认水印位置
setPosition('bottom-right')
}
img.src = originalImage.value
}
// 添加文字水印
const addTextWatermark = () => {
watermarkType.value = 'text'
updateWatermark()
}
// 更新水印
const updateWatermark = () => {
if (!previewCanvas.value || !originalImage.value || !watermarkType.value) return
const canvas = previewCanvas.value
const ctx = canvas.getContext('2d')!
// 重新绘制原图
const img = new Image()
img.onload = () => {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
// 绘制水印
if (watermarkType.value === 'text') {
drawTextWatermark(ctx)
} else if (watermarkType.value === 'image' && watermarkImage.value) {
drawImageWatermark(ctx)
}
// 生成处理后的图片
processedImage.value = canvas.toDataURL('image/jpeg', outputQuality.value / 100)
}
img.src = originalImage.value
}
// 绘制文字水印
const drawTextWatermark = (ctx: CanvasRenderingContext2D) => {
ctx.save()
// 设置字体
const fontStyle = textWatermark.fontStyle === 'italic' ? 'italic ' : ''
const fontWeight = textWatermark.fontWeight === 'bold' ? 'bold ' : ''
ctx.font = `${fontStyle}${fontWeight}${textWatermark.fontSize}px Arial`
// 设置颜色和透明度
ctx.fillStyle = textWatermark.color
ctx.globalAlpha = textWatermark.opacity
if (batchSettings.enabled) {
// 平铺水印
for (let x = 0; x < canvasWidth.value; x += batchSettings.spacingX) {
for (let y = 0; y < canvasHeight.value; y += batchSettings.spacingY) {
drawSingleTextWatermark(ctx, x + 50, y + 50)
}
}
} else {
// 单个水印
drawSingleTextWatermark(ctx, currentPosition.x, currentPosition.y)
}
ctx.restore()
}
// 绘制单个文字水印
const drawSingleTextWatermark = (ctx: CanvasRenderingContext2D, x: number, y: number) => {
ctx.save()
ctx.translate(x, y)
ctx.rotate((textWatermark.rotation * Math.PI) / 180)
// 添加阴影效果
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)'
ctx.shadowBlur = 3
ctx.shadowOffsetX = 1
ctx.shadowOffsetY = 1
ctx.fillText(textWatermark.text, 0, 0)
ctx.restore()
}
// 绘制图片水印
const drawImageWatermark = (ctx: CanvasRenderingContext2D) => {
const watermarkImg = new Image()
watermarkImg.onload = () => {
ctx.save()
ctx.globalAlpha = imageWatermark.opacity
const scaledWidth = watermarkImg.width * imageWatermark.scale
const scaledHeight = watermarkImg.height * imageWatermark.scale
if (batchSettings.enabled) {
// 平铺水印
for (let x = 0; x < canvasWidth.value; x += batchSettings.spacingX) {
for (let y = 0; y < canvasHeight.value; y += batchSettings.spacingY) {
drawSingleImageWatermark(ctx, watermarkImg, x, y, scaledWidth, scaledHeight)
}
}
} else {
// 单个水印
drawSingleImageWatermark(ctx, watermarkImg, currentPosition.x, currentPosition.y, scaledWidth, scaledHeight)
}
ctx.restore()
}
watermarkImg.src = watermarkImage.value
}
// 绘制单个图片水印
const drawSingleImageWatermark = (ctx: CanvasRenderingContext2D, img: HTMLImageElement, x: number, y: number, width: number, height: number) => {
ctx.save()
ctx.translate(x, y)
ctx.rotate((imageWatermark.rotation * Math.PI) / 180)
ctx.drawImage(img, -width / 2, -height / 2, width, height)
ctx.restore()
}
// 设置位置
const setPosition = (key: string) => {
const pos = positions[key as keyof typeof positions]
if (pos) {
currentPosition.x = canvasWidth.value * pos.x
currentPosition.y = canvasHeight.value * pos.y
currentPosition.key = key
updateWatermark()
}
}
// 处理Canvas点击
const handleCanvasClick = (event: MouseEvent) => {
if (!previewCanvas.value) return
const rect = previewCanvas.value.getBoundingClientRect()
const scaleX = canvasWidth.value / rect.width
const scaleY = canvasHeight.value / rect.height
currentPosition.x = (event.clientX - rect.left) * scaleX
currentPosition.y = (event.clientY - rect.top) * scaleY
currentPosition.key = 'custom'
updateWatermark()
}
// 切换字体样式
const toggleFontStyle = (style: 'bold' | 'italic') => {
if (style === 'bold') {
textWatermark.fontWeight = textWatermark.fontWeight === 'bold' ? 'normal' : 'bold'
} else {
textWatermark.fontStyle = textWatermark.fontStyle === 'italic' ? 'normal' : 'italic'
}
updateWatermark()
}
// 下载图片
const downloadImage = () => {
if (!processedImage.value) return
const link = document.createElement('a')
link.download = `watermarked-image-${Date.now()}.jpg`
link.href = processedImage.value
link.click()
showStatus('图片下载完成', 'success')
}
// 重置图片
const resetImage = () => {
originalImage.value = ''
processedImage.value = ''
watermarkImage.value = ''
watermarkType.value = null
statusMessage.value = ''
}
// 格式化文件大小
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// 显示状态消息
const showStatus = (message: string, type: 'success' | 'error') => {
statusMessage.value = message
statusType.value = type
setTimeout(() => {
statusMessage.value = ''
}, 3000)
}
</script>