import { View, Text, Image } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useState, useEffect, useRef } from 'react'
import './index.scss'
import { getDesignList, updateDesign, type StickerItem } from '../../../utils/store'
import { sketchImage, updateDesign as updateDesignApi } from '../../../utils/api'
import { useThemeContext } from '../../../context/ThemeContext'
import { useSafeArea } from '../../../hooks/useSafeArea'
import { useStatusBar } from '../../../hooks/useStatusBar'
import ThemedPageMeta from '../../../components/ThemedPageMeta'
import ScrollTopMask from '../../../components/ScrollTopMask'
import BottomActionBar from '../../../components/BottomActionBar'
/* ============================================================
贴纸编辑(亮度/色相/对比度/裁剪/线稿)
问题5 修正:edits 参数化保存(src 不变),预览用 Image + CSS filter
所见即所得,不再依赖 Canvas 2D ctx.filter(开发者工具不支持);
DIY 渲染端按同一 edits 套 CSS filter 呈现(契约 §4:edits 不进 WCD
document.json,仅 manifest.meta,src 保持原样对 R4 无影响)。
============================================================ */
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max)
/** 拖动手势滑块组件(问题5 修正:选择器查询按 id 定位,touchstart 时缓存 rect) */
interface SliderProps {
id: string
value: number
min: number
max: number
onChange: (val: number) => void
format?: (v: number) => string
}
function TouchSlider({ id, value, min, max, onChange, format }: SliderProps) {
const rectRef = useRef<{ left: number; width: number } | null>(null)
const measure = (cb?: () => void) => {
Taro.createSelectorQuery()
.select(`#${id}`)
.boundingClientRect((rect: any) => {
if (rect && rect.width > 0) rectRef.current = { left: rect.left, width: rect.width }
if (cb) cb()
})
.exec()
}
const valueFromX = (clientX: number) => {
const rect = rectRef.current
if (!rect || !rect.width) return value
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
return clamp(min + (max - min) * ratio, min, max)
}
const handleTouchStart = (e: any) => {
const touch = e.touches && e.touches[0]
if (!touch) return
measure(() => onChange(valueFromX(touch.clientX)))
}
const handleTouchMove = (e: any) => {
const touch = e.touches && e.touches[0]
if (!touch) return
if (!rectRef.current) {
measure(() => onChange(valueFromX(touch.clientX)))
return
}
onChange(valueFromX(touch.clientX))
}
const pct = `${((value - min) / (max - min)) * 100}%`
return (
{ /* rect 已缓存,无需处理 */ }}
>
{format ? format(value) : value}
)
}
export default function StickerEditPage() {
const { resolvedTheme } = useThemeContext()
const safe = useSafeArea()
useStatusBar(resolvedTheme)
const [designId, setDesignId] = useState('')
const [stickerId, setStickerId] = useState('')
const [sticker, setSticker] = useState(null)
const [notFound, setNotFound] = useState(false)
// 裁剪/线稿产生的 src 替换(保存时一并落库;取消则丢弃)
const [pendingSrc, setPendingSrc] = useState(null)
// 编辑参数
const [brightness, setBrightness] = useState(0)
const [hue, setHue] = useState(0)
const [contrast, setContrast] = useState(0)
const [loading, setLoading] = useState(false)
useEffect(() => {
const router = Taro.getCurrentInstance().router
const params = router ? router.params : undefined
const dId = params ? params.designId : ''
const sId = params ? params.stickerId : ''
setDesignId(dId)
setStickerId(sId)
const list = getDesignList()
const design = list.find(d => d.id === dId)
const st = design && design.designData && design.designData.stickers ? design.designData.stickers.find(s => s.id === sId) : undefined
if (st) {
setSticker(st)
setBrightness((st.edits && st.edits.brightness) || 0)
setHue((st.edits && st.edits.hue) || 0)
setContrast((st.edits && st.edits.contrast) || 0)
} else {
// 问题4 留意点:找不到贴纸时给出空态,而不是停在空白 Canvas
setNotFound(true)
}
}, [])
/** 保存:edits 参数化持久化(src 不变),裁剪/线稿的 src 替换一并落库 */
const handleSave = () => {
if (!sticker || !designId) return
setLoading(true)
const dList = getDesignList()
const dIdx = dList.findIndex(d => d.id === designId)
if (dIdx === -1) { setLoading(false); setNotFound(true); return }
const d = dList[dIdx]
const stickers = (d.designData && d.designData.stickers) || []
const sIdx = stickers.findIndex(s => s.id === stickerId)
if (sIdx === -1) { setLoading(false); setNotFound(true); return }
stickers[sIdx] = {
...stickers[sIdx],
...(pendingSrc ? { src: pendingSrc } : {}),
edits: { brightness, hue, contrast }
}
// WCD 红线:基于服务端已有 designData 合并(保留 wordcloud 分组),只覆盖 stickers
const designData = { ...d.designData, stickers }
updateDesign(designId, { designData }) // 本地缓存同步
// 服务端保存(items 全量替换,必须带全 4 个基本字段);失败暂存本地
updateDesignApi(designId, {
item: {
productId: d.productId,
productName: d.productName,
unitPrice: d.unitPrice,
count: d.count,
designData
}
}).catch(() => Taro.showToast({ title: '网络不可用,已暂存到本地', icon: 'none' }))
setLoading(false)
Taro.showToast({ title: '保存成功', icon: 'success' })
Taro.navigateBack()
}
/** 裁剪:调用微信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)
setPendingSrc(res.tempFilePath)
}
})
}
/** 线稿:调真实后端接口(POST /api/sketch,带 token);失败降级前端灰度参数 */
const handleSketch = async () => {
if (!sticker) return
setLoading(true)
try {
const res = await sketchImage(sticker.src)
if (res?.imageUrl) {
setSticker({ ...sticker, src: res.imageUrl })
setPendingSrc(res.imageUrl)
setLoading(false)
Taro.showToast({ title: '线稿生成成功', icon: 'success' })
} else {
throw new Error('no imageUrl')
}
} catch {
// 如果接口不可用/失败,降级为前端灰度滤镜参数(保存时随 edits 持久化)
Taro.showToast({ title: '使用本地线稿', icon: 'none' })
setBrightness(20)
setContrast(80)
setHue(0)
setLoading(false)
}
}
const previewFilter = brightness === 0 && hue === 0 && contrast === 0
? 'none'
: `brightness(${100 + brightness}%) hue-rotate(${hue}deg) contrast(${100 + contrast}%)`
if (notFound) {
return (
Taro.navigateBack()}>←
编辑贴纸
未找到该贴纸
贴纸可能已被删除,请返回工作台重新添加
Taro.navigateBack()}>
返回工作台
)
}
return (
{/* Header */}
Taro.navigateBack()}>←
编辑贴纸
保存
{/* 预览:Image + CSS filter 所见即所得(不依赖 Canvas ctx.filter) */}
{/* 快速操作 */}
快速操作
裁剪
转线稿
{/* 亮度 */}
亮度
(v > 0 ? `+${v}` : `${v}`)}
/>
{/* 色相 */}
色相
(v > 0 ? `+${v}` : `${v}`)}
/>
{/* 对比度 */}
对比度
(v > 0 ? `+${v}` : `${v}`)}
/>
Taro.navigateBack()}>
取消
保存
{loading && (
处理中…
)}
)
}