1.2.0版本
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
border-radius: 16px;
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mask-border {
|
||||
@@ -225,6 +227,8 @@
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
@@ -272,6 +276,35 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 常驻编辑按钮 */
|
||||
.edit-btn-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
padding: 16px 48px;
|
||||
border-radius: 50px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
background: var(--bg-input);
|
||||
color: var(--text-secondary);
|
||||
border: 2px dashed var(--text-muted);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.edit-btn.active {
|
||||
background: linear-gradient(135deg, rgba(255, 154, 158, 0.12) 0%, rgba(160, 196, 255, 0.12) 100%);
|
||||
color: var(--accent-pink);
|
||||
border-color: var(--accent-pink);
|
||||
}
|
||||
|
||||
.edit-btn.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.action-btns {
|
||||
display: flex;
|
||||
|
||||
+22
-7
@@ -4,11 +4,10 @@ import { useState, useEffect } from 'react'
|
||||
import './index.scss'
|
||||
import { getProductById } from '../../utils/productConfig'
|
||||
import { getDesignList, setDesignList, updateDesign, type DesignItem, type StickerItem } from '../../utils/store'
|
||||
import ThemeToggle from '../../components/ThemeToggle'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
|
||||
export default function DIYPage() {
|
||||
const { theme } = useThemeContext()
|
||||
const { theme, resolvedTheme } = useThemeContext()
|
||||
const [category, setCategory] = useState<any>(null)
|
||||
const [step, setStep] = useState(1)
|
||||
const [designId, setDesignId] = useState('')
|
||||
@@ -202,6 +201,13 @@ export default function DIYPage() {
|
||||
|
||||
const handleTouchEnd = () => setIsDragging(false)
|
||||
|
||||
const handleEditSticker = () => {
|
||||
if (!activeStickerId || !designId) return
|
||||
Taro.navigateTo({
|
||||
url: `/pages/diy/stickerEdit/index?designId=${designId}&stickerId=${activeStickerId}`
|
||||
})
|
||||
}
|
||||
|
||||
const handleScale = (id: string, delta: number) => {
|
||||
const next = stickers.map(s => {
|
||||
if (s.id !== id) return s
|
||||
@@ -246,7 +252,7 @@ export default function DIYPage() {
|
||||
|
||||
if (!category) {
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<View className='diy-page'>
|
||||
<Text className='page-title'>加载中...</Text>
|
||||
</View>
|
||||
@@ -255,9 +261,8 @@ export default function DIYPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={`theme-${theme}`}>
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<View className='diy-page'>
|
||||
<ThemeToggle />
|
||||
|
||||
<View className='page-header dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
@@ -268,8 +273,8 @@ export default function DIYPage() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 画布区域 */}
|
||||
<View className='canvas-wrapper dashed-card mt-20'>
|
||||
{/* 画布区域 — catchtouchmove 阻止事件冒泡导致页面滚动 */}
|
||||
<View className='canvas-wrapper dashed-card mt-20' catchMove>
|
||||
<View className='star-badge' />
|
||||
<View className='canvas-area' style={getMaskStyle()}>
|
||||
{stickers.map(s => (
|
||||
@@ -337,6 +342,16 @@ export default function DIYPage() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 常驻编辑按钮 */}
|
||||
<View className='edit-btn-bar mt-20'>
|
||||
<View
|
||||
className={`edit-btn ${activeStickerId ? 'active' : 'disabled'}`}
|
||||
onTap={handleEditSticker}
|
||||
>
|
||||
<Text>编辑</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 底部操作按钮 */}
|
||||
<View className='action-btns mt-20'>
|
||||
<View className='btn-gradient' onTap={() => setPreviewMode(true)}>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '编辑贴纸'
|
||||
})
|
||||
@@ -0,0 +1,184 @@
|
||||
.sticker-edit-page {
|
||||
padding: 0 24px 24px;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edit-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: calc(env(safe-area-inset-top, 0px) + 24px) 0 24px;
|
||||
}
|
||||
|
||||
.edit-back {
|
||||
font-size: 36px;
|
||||
color: var(--text-primary);
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.edit-title {
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.edit-save {
|
||||
font-size: 28px;
|
||||
color: var(--accent-pink);
|
||||
font-weight: 600;
|
||||
width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 画布区域 */
|
||||
.edit-canvas-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.edit-canvas {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background: var(--bg-input);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
/* 工具面板 */
|
||||
.edit-tools {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.edit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.edit-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.edit-label {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.slider-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.slider-track {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-input);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.slider-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(90deg, #ff9a9e, #fecfef);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
border: 4px solid var(--accent-pink);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 快速操作按钮 */
|
||||
.edit-quick-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
padding: 16px 28px;
|
||||
border-radius: 40px;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
border: 2px dashed var(--line-star);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.quick-btn:active {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.quick-btn.active {
|
||||
background: rgba(255, 154, 158, 0.12);
|
||||
border-color: var(--accent-pink);
|
||||
color: var(--accent-pink);
|
||||
}
|
||||
|
||||
/* 底部操作 */
|
||||
.edit-footer {
|
||||
padding: 20px 0 calc(20px + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.edit-footer .btn-gradient,
|
||||
.edit-footer .btn-outline {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
/* 加载遮罩 */
|
||||
.edit-loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.4);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edit-loading-text {
|
||||
font-size: 28px;
|
||||
color: #fff;
|
||||
margin-top: 20px;
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
import { View, Text, Canvas, Image } from '@tarojs/components'
|
||||
import Taro, { createCanvasContext, canvasToTempFilePath } from '@tarojs/taro'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import './index.scss'
|
||||
import { getProductById } from '../../../utils/productConfig'
|
||||
import { getDesignList, setDesignList, updateDesign, type DesignItem, type StickerItem } from '../../../utils/store'
|
||||
import { useThemeContext } from '../../../context/ThemeContext'
|
||||
|
||||
/* ============================================================
|
||||
使用 Canvas 实现贴纸编辑(亮度/色相/线稿)
|
||||
============================================================ */
|
||||
|
||||
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max)
|
||||
|
||||
/** 拖动手势滑块组件 */
|
||||
interface SliderProps {
|
||||
value: number
|
||||
min: number
|
||||
max: number
|
||||
step?: number
|
||||
onChange: (val: number) => void
|
||||
showValue?: boolean
|
||||
format?: (v: number) => string
|
||||
}
|
||||
|
||||
function TouchSlider({ value, min, max, step = 1, onChange, format }: SliderProps) {
|
||||
const trackRef = useRef<any>(null)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
|
||||
const computedFromClientX = (clientX: number) => {
|
||||
if (!trackRef.current) return value
|
||||
const query = Taro.createSelectorQuery().in(trackRef.current)
|
||||
// 注意:这里需要取 track 元素的位置信息
|
||||
const rect = trackRef.current.getBoundingClientRect ? trackRef.current.getBoundingClientRect() : { left: 0, width: 300 }
|
||||
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
|
||||
let raw = min + (max - min) * ratio
|
||||
if (step > 0) {
|
||||
raw = Math.round(raw / step) * step
|
||||
}
|
||||
return clamp(raw, min, max)
|
||||
}
|
||||
|
||||
const handleTouchStart = (e: any) => {
|
||||
setDragging(true)
|
||||
const x = e.touches?.[0]?.clientX ?? 0
|
||||
onChange(computedFromClientX(x))
|
||||
}
|
||||
|
||||
const handleTouchMove = (e: any) => {
|
||||
if (!dragging) return
|
||||
const x = e.touches?.[0]?.clientX ?? 0
|
||||
onChange(computedFromClientX(x))
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
setDragging(false)
|
||||
}
|
||||
|
||||
const pct = `${((value - min) / (max - min)) * 100}%`
|
||||
|
||||
return (
|
||||
<View className='slider-wrap'>
|
||||
<View
|
||||
className='slider-track'
|
||||
ref={trackRef}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<View className='slider-fill' style={{ width: pct }} />
|
||||
<View className={`slider-thumb ${dragging ? 'dragging' : ''}`} style={{ left: pct }} />
|
||||
</View>
|
||||
<Text className='slider-value'>{format ? format(value) : value}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default function StickerEditPage() {
|
||||
const { resolvedTheme } = useThemeContext()
|
||||
const [designId, setDesignId] = useState('')
|
||||
const [stickerId, setStickerId] = useState('')
|
||||
const [sticker, setSticker] = useState<StickerItem | null>(null)
|
||||
|
||||
// 编辑参数
|
||||
const [brightness, setBrightness] = useState(0)
|
||||
const [hue, setHue] = useState(0)
|
||||
const [contrast, setContrast] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [canvasReady, setCanvasReady] = useState(false)
|
||||
|
||||
const canvasRef = useRef<any>(null)
|
||||
const ctxRef = useRef<any>(null)
|
||||
const canvasNodeRef = useRef<any>(null)
|
||||
const canvasSizeRef = useRef<{width:number, height:number}>({width:0,height:0})
|
||||
|
||||
// 使用 nextTick + 重试初始化 Canvas(小程序 2D)
|
||||
const initCanvas = useCallback((st: StickerItem) => {
|
||||
if (canvasReady) return
|
||||
Taro.nextTick(() => {
|
||||
const query = Taro.createSelectorQuery()
|
||||
query.select('#editCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res: any) => {
|
||||
const canvas = res?.[0]?.node
|
||||
if (!canvas) {
|
||||
setTimeout(() => initCanvas(st), 300)
|
||||
return
|
||||
}
|
||||
const ctx = canvas.getContext('2d')
|
||||
let cw = res[0]?.width || 300
|
||||
let ch = res[0]?.height || 400
|
||||
if (cw <= 0) cw = 300
|
||||
if (ch <= 0) ch = 400
|
||||
const dpr = Taro.getSystemInfoSync().pixelRatio || 1
|
||||
canvas.width = Math.round(cw * dpr)
|
||||
canvas.height = Math.round(ch * dpr)
|
||||
ctx.scale(dpr, dpr)
|
||||
canvasNodeRef.current = canvas
|
||||
ctxRef.current = ctx
|
||||
canvasSizeRef.current = { width: cw, height: ch }
|
||||
setCanvasReady(true)
|
||||
drawSticker(ctx, cw, ch, st)
|
||||
})
|
||||
})
|
||||
}, [canvasReady])
|
||||
|
||||
useEffect(() => {
|
||||
const params = Taro.getCurrentInstance().router?.params
|
||||
const dId = params?.designId as string
|
||||
const sId = params?.stickerId as string
|
||||
setDesignId(dId)
|
||||
setStickerId(sId)
|
||||
|
||||
const list = getDesignList()
|
||||
const design = list.find(d => d.id === dId)
|
||||
const st = design?.designData?.stickers?.find(s => s.id === sId)
|
||||
if (st) {
|
||||
setSticker(st)
|
||||
setBrightness(st.edits?.brightness || 0)
|
||||
setHue(st.edits?.hue || 0)
|
||||
setContrast(st.edits?.contrast || 0)
|
||||
initCanvas(st)
|
||||
}
|
||||
}, [initCanvas])
|
||||
|
||||
/** canvas 上绘制带滤镜的贴纸 */
|
||||
const drawSticker = (ctx: any, cw: number, ch: number, st: StickerItem, opts?: { brightness: number; hue: number; contrast: number }) => {
|
||||
if (!ctx || !canvasNodeRef.current) return
|
||||
const canvas = canvasNodeRef.current
|
||||
|
||||
// 防护:cw/ch 为 0 时容易绘制异常
|
||||
if (!cw || !ch) return
|
||||
const imgW = st.width || 200
|
||||
const imgH = st.height || 200
|
||||
|
||||
const img = canvas.createImage()
|
||||
img.onload = () => {
|
||||
ctx.clearRect(0, 0, cw, ch)
|
||||
ctx.save()
|
||||
|
||||
// 居中缩放显示(限制最大为画板 90%)
|
||||
const scale = Math.min(cw / imgW, ch / imgH, 4) * 0.9
|
||||
const w = imgW * scale
|
||||
const h = imgH * scale
|
||||
const x = (cw - w) / 2
|
||||
const y = (ch - h) / 2
|
||||
|
||||
// 应用滤镜(小程序 Canvas 2D 支持 filter 属性 2.16.0+)
|
||||
const b = opts?.brightness ?? brightness
|
||||
const hVal = opts?.hue ?? hue
|
||||
const c = opts?.contrast ?? contrast
|
||||
if (b !== 0 || hVal !== 0 || c !== 0) {
|
||||
ctx.filter = `brightness(${100 + b}%) hue-rotate(${hVal}deg) contrast(${100 + c}%)`
|
||||
} else {
|
||||
ctx.filter = 'none'
|
||||
}
|
||||
|
||||
ctx.drawImage(img, x, y, w, h)
|
||||
ctx.restore()
|
||||
}
|
||||
img.onerror = (err: any) => {
|
||||
console.error('Canvas load image error:', err, st.src)
|
||||
Taro.showToast({ title: '图片加载失败', icon: 'none' })
|
||||
}
|
||||
if (st.src) {
|
||||
img.src = st.src
|
||||
}
|
||||
}
|
||||
|
||||
/** 重新绘制(参数变化时) */
|
||||
const redraw = () => {
|
||||
if (!sticker || !ctxRef.current || !canvasNodeRef.current) return
|
||||
const { width, height } = canvasSizeRef.current
|
||||
drawSticker(ctxRef.current, width, height, sticker, { brightness, hue, contrast })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
redraw()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [brightness, hue, contrast])
|
||||
|
||||
const handleSave = () => {
|
||||
if (!sticker || !designId) return
|
||||
setLoading(true)
|
||||
// 使用 Canvas 2D 导出
|
||||
// @ts-ignore
|
||||
Taro.canvasToTempFilePath({
|
||||
canvas: canvasNodeRef.current,
|
||||
quality: 0.92,
|
||||
success: (res: any) => {
|
||||
const newSrc = res.tempFilePath
|
||||
const dList = getDesignList()
|
||||
const dIdx = dList.findIndex(d => d.id === designId)
|
||||
if (dIdx === -1) { setLoading(false); return }
|
||||
const stickers = dList[dIdx].designData?.stickers || []
|
||||
const sIdx = stickers.findIndex(s => s.id === stickerId)
|
||||
if (sIdx === -1) { setLoading(false); return }
|
||||
stickers[sIdx] = {
|
||||
...stickers[sIdx],
|
||||
src: newSrc,
|
||||
edits: { brightness, hue, contrast }
|
||||
}
|
||||
updateDesign(designId, { designData: { ...dList[dIdx].designData, stickers } })
|
||||
Taro.showToast({ title: '保存成功', icon: 'success' })
|
||||
Taro.navigateBack()
|
||||
},
|
||||
fail: () => {
|
||||
setLoading(false)
|
||||
Taro.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 裁剪:调用微信cropImage */
|
||||
const handleCrop = () => {
|
||||
if (!sticker) return
|
||||
// @ts-ignore
|
||||
if (!Taro.cropImage) {
|
||||
Taro.showToast({ title: '当前微信版本不支持裁剪', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// @ts-ignore
|
||||
Taro.cropImage({
|
||||
src: sticker.src,
|
||||
cropScale: '1:1',
|
||||
success: (res) => {
|
||||
const newSticker = { ...sticker, src: res.tempFilePath }
|
||||
setSticker(newSticker)
|
||||
// 重绘
|
||||
setTimeout(() => redraw(), 200)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 线稿:预留后端AI接口 */
|
||||
const API_BASE = 'https://your-api-domain.com' // ← 填入你的服务器地址
|
||||
|
||||
const handleSketch = () => {
|
||||
if (!sticker) return
|
||||
setLoading(true)
|
||||
Taro.uploadFile({
|
||||
url: `${API_BASE}/api/sketch`,
|
||||
filePath: sticker.src,
|
||||
name: 'image',
|
||||
success: (res) => {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.url) {
|
||||
const newSticker = { ...sticker, src: data.url, edits: { ...sticker.edits, sketchSrc: data.url } }
|
||||
setSticker(newSticker)
|
||||
setTimeout(() => redraw(), 200)
|
||||
Taro.showToast({ title: '线稿生成成功', icon: 'success' })
|
||||
} else {
|
||||
throw new Error('no url')
|
||||
}
|
||||
} catch {
|
||||
// 如果接口不可用,降级为前端灰度+边缘检测
|
||||
applyFrontendSketch()
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
applyFrontendSketch()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 前端线稿降级方案:灰度+反相高对比 */
|
||||
const applyFrontendSketch = () => {
|
||||
if (!sticker || !ctxRef.current) { setLoading(false); return }
|
||||
Taro.showToast({ title: '使用本地线稿', icon: 'none' })
|
||||
setBrightness(20)
|
||||
setContrast(80)
|
||||
setHue(0)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const bPct = `${((brightness + 100) / 200) * 100}%`
|
||||
const hPct = `${((hue + 180) / 360) * 100}%`
|
||||
const cPct = `${((contrast + 100) / 200) * 100}%`
|
||||
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<View className='sticker-edit-page'>
|
||||
{/* Header */}
|
||||
<View className='edit-header'>
|
||||
<Text className='edit-back' onTap={() => Taro.navigateBack()}>←</Text>
|
||||
<Text className='edit-title'>编辑贴纸</Text>
|
||||
<Text className='edit-save' onTap={handleSave}>保存</Text>
|
||||
</View>
|
||||
|
||||
{/* Canvas 预览 */}
|
||||
<View className='edit-canvas-wrap'>
|
||||
<Canvas
|
||||
id='editCanvas'
|
||||
type='2d'
|
||||
className='edit-canvas'
|
||||
style={{ width: '100%', height: '400px' }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 快速操作 */}
|
||||
<View className='edit-tools dashed-card mt-20'>
|
||||
<View className='edit-row'>
|
||||
<Text className='edit-label'>快速操作</Text>
|
||||
<View className='edit-quick-actions'>
|
||||
<View className='quick-btn' onTap={handleCrop}>
|
||||
<Text>裁剪</Text>
|
||||
</View>
|
||||
<View className='quick-btn' onTap={handleSketch}>
|
||||
<Text>转线稿</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 亮度 */}
|
||||
<View className='edit-row slider-row'
|
||||
onTouchMove={(e) => {
|
||||
const { clientX } = e.changedTouches[0]
|
||||
// 取得滑轨位置
|
||||
const query = Taro.createSelectorQuery().in(e.currentTarget as any)
|
||||
query.select('.slider-track').boundingClientRect((rect: any) => {
|
||||
if (!rect) return
|
||||
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
|
||||
const v = Math.round((-100 + 200 * ratio) / 1) * 1
|
||||
setBrightness(clamp(v, -100, 100))
|
||||
}).exec()
|
||||
}}
|
||||
>
|
||||
<Text className='edit-label'>亮度</Text>
|
||||
<View className='slider-wrap'>
|
||||
<View className='slider-track'>
|
||||
<View className='slider-fill' style={{ width: bPct }} />
|
||||
<View className='slider-thumb' style={{ left: bPct }} />
|
||||
</View>
|
||||
<Text className='slider-value'>{brightness > 0 ? `+${brightness}` : brightness}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 色相 */}
|
||||
<View className='edit-row slider-row'
|
||||
onTouchMove={(e) => {
|
||||
const { clientX } = e.changedTouches[0]
|
||||
const query = Taro.createSelectorQuery().in(e.currentTarget as any)
|
||||
query.select('.slider-track').boundingClientRect((rect: any) => {
|
||||
if (!rect) return
|
||||
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
|
||||
const v = Math.round((-180 + 360 * ratio) / 1) * 1
|
||||
setHue(clamp(v, -180, 180))
|
||||
}).exec()
|
||||
}}
|
||||
>
|
||||
<Text className='edit-label'>色相</Text>
|
||||
<View className='slider-wrap'>
|
||||
<View className='slider-track'>
|
||||
<View className='slider-fill' style={{ width: hPct }} />
|
||||
<View className='slider-thumb' style={{ left: hPct }} />
|
||||
</View>
|
||||
<Text className='slider-value'>{hue > 0 ? `+${hue}` : hue}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 对比度 */}
|
||||
<View className='edit-row slider-row'
|
||||
onTouchMove={(e) => {
|
||||
const { clientX } = e.changedTouches[0]
|
||||
const query = Taro.createSelectorQuery().in(e.currentTarget as any)
|
||||
query.select('.slider-track').boundingClientRect((rect: any) => {
|
||||
if (!rect) return
|
||||
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
|
||||
const v = Math.round((-100 + 200 * ratio) / 1) * 1
|
||||
setContrast(clamp(v, -100, 100))
|
||||
}).exec()
|
||||
}}
|
||||
>
|
||||
<Text className='edit-label'>对比度</Text>
|
||||
<View className='slider-wrap'>
|
||||
<View className='slider-track'>
|
||||
<View className='slider-fill' style={{ width: cPct }} />
|
||||
<View className='slider-thumb' style={{ left: cPct }} />
|
||||
</View>
|
||||
<Text className='slider-value'>{contrast > 0 ? `+${contrast}` : contrast}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ flex: 1 }} />
|
||||
|
||||
{loading && (
|
||||
<View className='edit-loading'>
|
||||
<Text className='edit-loading-text'>处理中…</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user