241 lines
5.6 KiB
Vue
241 lines
5.6 KiB
Vue
<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>
|