Files
qitongxue-wx/src/pages/tools/image/watermark.vue
2026-09-17 14:39:15 +08:00

241 lines
5.6 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 lang="ts">
import { onReady } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { bindImageCanvas, chooseOneImage, saveResult } from '@/utils/image/core'
import { runTask } from '@/utils/image/task'
import { addWatermark } from '@/utils/image/watermark'
import { navPadTop } from '@/utils/safearea'
const src = ref('')
const result = ref<{ path: string, name: string } | null>(null)
const text = ref('')
const mode = ref<'tile' | 'center'>('tile')
const opacity = ref(35)
const angle = ref(-30)
const navPad = navPadTop()
function goBack() {
uni.navigateBack()
}
function onOpacityChange(e: { detail: { value: number } }) {
opacity.value = e.detail.value
}
function onAngleChange(e: { detail: { value: number } }) {
angle.value = e.detail.value
}
onReady(async () => {
// #ifdef MP-WEIXIN
await bindImageCanvas()
// #endif
})
async function pick() {
try {
const p = await chooseOneImage()
src.value = p.path
result.value = null
}
catch {
/* 取消 */
}
}
async function run() {
if (!src.value) {
uni.showToast({ title: '请先选择图片', icon: 'none' })
return
}
const r = await runTask('生成中', () =>
addWatermark(src.value, {
text: text.value,
mode: mode.value,
opacity: opacity.value / 100,
rotate: angle.value,
}))
if (r)
result.value = r
}
async function save() {
if (!result.value)
return
await saveResult(result.value.path, result.value.name)
}
</script>
<template>
<view class="page">
<view class="nav" :style="{ paddingTop: navPad }">
<view class="nav__back press" hover-class="press-active" @tap="goBack">
<wd-icon name="arrow-left" size="36rpx" color="#3d2a1e" />
</view>
<text class="nav__title">
文字水印
</text>
</view>
<view v-if="!src" class="pick press" hover-class="press-active" @tap="pick">
<wd-icon name="add" size="56rpx" color="#ff6b35" />
<text class="pick__text">
选择图片
</text>
</view>
<view v-else class="preview" @tap="pick">
<image :src="result?.path || src" mode="aspectFit" class="preview__img" />
<text class="preview__tip">
点击重新选择
</text>
</view>
<view class="card">
<wd-input v-model="text" placeholder="水印文字,如 @小工具" clearable />
<view class="modes">
<view class="m press" :class="{ 'm--on': mode === 'tile' }" hover-class="press-active" @tap="mode = 'tile'">
<text>平铺</text>
</view>
<view class="m press" :class="{ 'm--on': mode === 'center' }" hover-class="press-active" @tap="mode = 'center'">
<text>居中</text>
</view>
</view>
<view class="slider-row">
<text class="slider-label">
透明度
</text>
<slider class="slider" :value="opacity" :min="5" :max="100" active-color="#ff6b35" background-color="#ffe3d3" @change="onOpacityChange" />
</view>
<view v-if="mode === 'tile'" class="slider-row">
<text class="slider-label">
角度 {{ angle }}°
</text>
<slider class="slider" :value="angle" :min="-90" :max="90" :step="5" active-color="#ff6b35" background-color="#ffe3d3" @change="onAngleChange" />
</view>
</view>
<wd-button type="primary" block @click="run">
生成水印
</wd-button>
<wd-button v-if="result" type="primary" block @click="save">
保存
</wd-button>
<!-- #ifdef MP-WEIXIN -->
<!-- 图片引擎工作画布:页面内隐藏节点,onReady 时由 bindImageCanvas 绑定 -->
<canvas id="imgCanvas" type="2d" class="hidden-canvas" />
<!-- #endif -->
</view>
</template>
<style scoped>
.page {
min-height: 100vh;
padding: 0 32rpx calc(60rpx + env(safe-area-inset-bottom, 0px));
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 28rpx;
}
.nav {
display: flex;
align-items: center;
padding: 0 0 16rpx;
}
.nav__back {
flex-shrink: 0;
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: #ffffff;
border: 1rpx solid #ffe9dd;
box-shadow: 0 6rpx 16rpx rgba(107, 47, 15, 0.08);
display: flex;
align-items: center;
justify-content: center;
}
.nav__title {
flex: 1;
text-align: center;
font-size: 34rpx;
font-weight: 700;
color: #3d2a1e;
margin-right: 64rpx;
}
.pick {
background: #ffffff;
border: 2rpx dashed #ffd4b8;
border-radius: 32rpx;
padding: 100rpx 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
}
.pick__text {
font-size: 26rpx;
color: #9a7b65;
}
.preview {
position: relative;
background: #ffffff;
border: 2rpx solid #ffe3d3;
border-radius: 32rpx;
padding: 20rpx;
}
.preview__img {
width: 100%;
height: 480rpx;
border-radius: 24rpx;
}
.preview__tip {
position: absolute;
right: 32rpx;
bottom: 32rpx;
font-size: 22rpx;
color: #9a7b65;
background: rgba(240, 253, 250, 0.9);
border-radius: 999rpx;
padding: 8rpx 20rpx;
}
.card {
background: #ffffff;
border: 2rpx solid #ffe3d3;
border-radius: 32rpx;
padding: 28rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.modes {
display: flex;
gap: 16rpx;
}
.m {
flex: 1;
text-align: center;
padding: 20rpx 0;
border-radius: 20rpx;
background: #fff9f2;
border: 2rpx solid #ffe3d3;
font-size: 26rpx;
color: #3d2a1e;
min-height: 44px;
box-sizing: border-box;
}
.m--on {
background: #ff6b35;
border-color: #ff6b35;
color: #ffffff;
}
.slider-row {
display: flex;
align-items: center;
gap: 24rpx;
}
.slider-label {
flex-shrink: 0;
width: 140rpx;
font-size: 24rpx;
color: #9a7b65;
}
</style>