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 && ( 处理中… )} ) }