复制代码
<template>
<div class="image-watermark-container">
<!-- 页面标题 -->
<h1 class="page-title">图片水印工具</h1>
<!-- 主内容区 -->
<el-row :gutter="20" class="main-content">
<!-- 左侧控制面板 -->
<el-col :span="8" class="control-panel">
<el-card class="control-card">
<template #header>
<div class="card-header">
<i class="el-icon-setting"></i>
<span>水印设置</span>
</div>
</template>
<!-- 图片上传区域 -->
<el-upload
class="image-uploader"
action=""
:show-file-list="false"
:before-upload="handleBeforeUpload"
:http-request="handleFileUpload"
>
<div class="upload-area" :class="{ 'has-image': originalImage }">
<i v-if="!originalImage" class="el-icon-upload"></i>
<img v-if="originalImage" :src="originalImage" class="preview-image" alt="预览图">
<div v-if="!originalImage" class="upload-text">
<p>点击或拖拽图片到此处上传</p>
<p class="upload-hint">支持 JPG, PNG, GIF 等格式</p>
</div>
<el-button
v-if="originalImage"
type="text"
class="remove-image"
@click.stop="removeImage"
>
<i class="el-icon-delete"></i> 移除图片
</el-button>
</div>
</el-upload>
<el-divider v-if="originalImage"></el-divider>
<!-- 水印设置项 -->
<div v-if="originalImage" class="watermark-settings">
<el-form label-position="top" size="medium">
<el-form-item label="水印文本">
<el-input
v-model="watermarkText"
placeholder="请输入水印文字"
clearable
></el-input>
</el-form-item>
<el-row :gutter="15">
<el-col :span="12">
<el-form-item label="字体大小 (px)">
<el-input-number
v-model="fontSize"
:min="10"
:max="100"
:step="1"
></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="水印颜色">
<el-color-picker
v-model="watermarkColor"
:predefine="predefinedColors"
show-alpha
></el-color-picker>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="透明度">
<el-slider
v-model="opacity"
:min="0"
:max="1"
:step="0.1"
show-stops
></el-slider>
<div class="slider-value">{{ opacity.toFixed(1) }}</div>
</el-form-item>
<el-row :gutter="15">
<el-col :span="12">
<el-form-item label="旋转角度 (度)">
<el-input-number
v-model="rotation"
:min="-180"
:max="180"
:step="1"
></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="水印间距 (px)">
<el-input-number
v-model="spacing"
:min="10"
:max="100"
:step="1"
></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button
type="primary"
class="generate-btn"
@click="addWatermark"
:loading="isProcessing"
>
<i class="el-icon-magic"></i> 生成水印图片
</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</el-col>
<!-- 右侧预览区域 -->
<el-col :span="16" class="preview-panel">
<el-card class="preview-card">
<template #header>
<div class="card-header">
<i class="el-icon-picture"></i>
<span>图片预览</span>
</div>
</template>
<div class="preview-container">
<!-- 原图预览 -->
<div class="preview-section">
<h3 class="preview-title">
<i class="el-icon-picture-outline"></i> 原图
</h3>
<div class="image-container">
<img
v-if="originalImage"
:src="originalImage"
class="preview-img"
alt="原始图片"
>
<div v-else class="empty-state">
<i class="el-icon-picture-outline"></i>
<p>请上传图片查看预览</p>
</div>
</div>
</div>
<!-- 水印图预览 -->
<div class="preview-section">
<h3 class="preview-title">
<i class="el-icon-picture"></i> 带水印图片
</h3>
<div class="image-container">
<img
v-if="watermarkedImage"
:src="watermarkedImage"
class="preview-img"
alt="带水印的图片"
>
<div v-else class="empty-state">
<i class="el-icon-hourglass"></i>
<p>请点击生成水印图片</p>
</div>
</div>
<el-button
v-if="watermarkedImage"
type="success"
class="download-btn"
@click="downloadImage"
icon="el-icon-download"
>
下载图片
</el-button>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
data() {
return {
// 图片相关
originalImage: null,
watermarkedImage: null,
isProcessing: false,
// 水印设置
watermarkText: '水印文字',
fontSize: 24,
watermarkColor: 'rgba(0, 0, 0, 0.3)',
opacity: 0.3,
rotation: -30,
spacing: 50,
// 预定义颜色
predefinedColors: [
'rgba(0, 0, 0, 0.3)',
'rgba(255, 0, 0, 0.3)',
'rgba(0, 255, 0, 0.3)',
'rgba(0, 0, 255, 0.3)',
'rgba(255, 255, 255, 0.3)',
]
};
},
methods: {
// 处理上传前的验证
handleBeforeUpload(file) {
const isImage = file.type.startsWith('image/');
if (!isImage) {
this.$message.error('请上传图片文件!');
return false;
}
return true;
},
// 处理文件上传
handleFileUpload(params) {
const file = params.file;
const reader = new FileReader();
reader.onload = (event) => {
this.originalImage = event.target.result;
this.watermarkedImage = null; // 重置水印图片
this.$message.success('图片上传成功');
};
reader.readAsDataURL(file);
},
// 移除图片
removeImage() {
this.originalImage = null;
this.watermarkedImage = null;
},
// 添加水印
addWatermark() {
if (!this.originalImage) return;
this.isProcessing = true;
// 使用setTimeout模拟异步处理,提升用户体验
setTimeout(() => {
const img = new Image();
img.src = this.originalImage;
img.onload = () => {
// 创建Canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 设置Canvas尺寸与图片一致
canvas.width = img.width;
canvas.height = img.height;
// 绘制原图
ctx.drawImage(img, 0, 0);
// 设置水印样式
ctx.fillStyle = this.watermarkColor;
ctx.globalAlpha = this.opacity;
ctx.font = `${this.fontSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 计算水印旋转角度(弧度)
const angle = (this.rotation * Math.PI) / 180;
// 获取水印文本宽度
const textWidth = ctx.measureText(this.watermarkText).width;
// 计算水印网格大小
const gridWidth = textWidth + this.spacing;
const gridHeight = this.fontSize + this.spacing;
// 计算需要覆盖的水印数量(考虑旋转后的覆盖范围)
const diagonal = Math.sqrt(canvas.width * canvas.width + canvas.height * canvas.height);
const cols = Math.ceil(diagonal / gridWidth) + 1;
const rows = Math.ceil(diagonal / gridHeight) + 1;
// 绘制水印网格
for (let i = -cols/2; i < cols/2; i++) {
for (let j = -rows/2; j < rows/2; j++) {
// 计算水印位置
const x = canvas.width / 2 + i * gridWidth;
const y = canvas.height / 2 + j * gridHeight;
// 保存当前状态
ctx.save();
// 移动到水印位置并旋转
ctx.translate(x, y);
ctx.rotate(angle);
// 绘制水印文本
ctx.fillText(this.watermarkText, 0, 0);
// 恢复状态
ctx.restore();
}
}
// 将Canvas内容转为图片URL
this.watermarkedImage = canvas.toDataURL('image/png');
this.isProcessing = false;
this.$message.success('水印生成成功');
};
img.onerror = () => {
this.isProcessing = false;
this.$message.error('图片处理失败,请重试');
};
}, 500);
},
// 下载带水印的图片
downloadImage() {
if (!this.watermarkedImage) return;
const link = document.createElement('a');
link.href = this.watermarkedImage;
link.download = 'watermarked-image.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.$message.success('图片下载成功');
}
}
};
</script>
<style lang="scss" scoped>
.image-watermark-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}
.page-title {
color: #1f2329;
font-size: 28px;
font-weight: 600;
text-align: center;
margin-bottom: 30px;
padding: 15px 0;
border-bottom: 1px solid #e5e6eb;
&:before, &:after {
content: "";
display: inline-block;
width: 40px;
height: 2px;
background-color: #409eff;
vertical-align: middle;
margin: 0 15px;
}
}
.main-content {
margin-bottom: 30px;
}
.control-panel, .preview-panel {
transition: all 0.3s ease;
}
.control-card, .preview-card {
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
overflow: hidden;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1);
}
}
.card-header {
display: flex;
align-items: center;
color: #1f2329;
font-size: 16px;
font-weight: 500;
i {
margin-right: 8px;
color: #409eff;
}
}
.image-uploader {
margin-bottom: 20px;
}
.upload-area {
border: 2px dashed #c0ccda;
border-radius: 6px;
padding: 30px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
&:hover {
border-color: #409eff;
background-color: #f0f7ff;
}
i {
font-size: 48px;
color: #8392a5;
margin-bottom: 15px;
transition: color 0.3s ease;
}
&:hover i {
color: #409eff;
}
}
.upload-text {
color: #8392a5;
p {
margin: 0;
line-height: 1.5;
}
.upload-hint {
font-size: 12px;
color: #c0ccda;
}
}
.has-image {
border-style: solid;
padding: 0;
i, .upload-text {
display: none;
}
}
.preview-image {
width: 100%;
height: auto;
border-radius: 4px;
display: block;
}
.remove-image {
position: absolute;
top: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.5);
color: white !important;
width: 32px;
height: 32px;
border-radius: 50%;
padding: 0 !important;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.3s ease;
&:hover {
background-color: rgba(235, 87, 87, 0.8);
}
}
.upload-area:hover .remove-image {
opacity: 1;
}
.watermark-settings {
padding-top: 10px;
}
.el-form-item {
margin-bottom: 18px;
}
.el-form-item__label {
color: #4e5969;
font-weight: 500;
padding-bottom: 6px;
}
.slider-value {
text-align: right;
color: #8392a5;
font-size: 12px;
margin-top: -10px;
}
.generate-btn {
width: 100%;
padding: 12px 0;
font-size: 15px;
}
.preview-container {
padding: 10px 0;
}
.preview-section {
margin-bottom: 30px;
&:last-child {
margin-bottom: 0;
}
}
.preview-title {
color: #4e5969;
font-size: 16px;
font-weight: 500;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 1px solid #e5e6eb;
i {
margin-right: 8px;
color: #409eff;
}
}
.image-container {
width: 100%;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #e5e6eb;
border-radius: 6px;
background-color: #fafafa;
overflow: hidden;
}
.preview-img {
max-width: 100%;
max-height: 500px;
transition: all 0.3s ease;
&:hover {
transform: scale(1.02);
}
}
.empty-state {
text-align: center;
color: #8392a5;
i {
font-size: 48px;
margin-bottom: 15px;
opacity: 0.5;
}
p {
margin: 0;
}
}
.download-btn {
display: block;
margin: 20px auto 0;
padding: 10px 30px;
font-size: 14px;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
}
}
// 响应式调整
@media (max-width: 992px) {
.control-panel, .preview-panel {
width: 100%;
margin-bottom: 20px;
}
.el-col {
&:span-8, &:span-16 {
width: 100%;
}
}
}
@media (max-width: 768px) {
.image-watermark-container {
padding: 10px;
}
.upload-area {
padding: 20px 10px;
}
.page-title {
font-size: 22px;
&:before, &:after {
width: 20px;
}
}
}
</style>