修正工作流批次③(问题5): - 三个滑块统一改用 TouchSlider 组件(id 选择器定位 + touchstart 缓存 rect),删除原 .in(e.currentTarget) 坏实现与死代码 - 预览弃用 Canvas ctx.filter(开发者工具不支持),改 Image + CSS filter 所见即所得;Canvas 导出链路整体移除 - 保存改为 edits 参数化持久化(src 不变;裁剪/线稿的 src 替换经 pendingSrc 一并落库),不再依赖 canvasToTempFilePath - DIY 工作台画布与预览弹层渲染端消费 edits(同一 CSS filter 逻辑), checkout 预览消费 edits 记入 R3 待办(routes/R2/README.md #6) Co-Authored-By: Claude <noreply@anthropic.com>
556 lines
21 KiB
TypeScript
556 lines
21 KiB
TypeScript
import { View, Text, Image } from '@tarojs/components'
|
||
import Taro from '@tarojs/taro'
|
||
import { useState, useEffect, useRef } from 'react'
|
||
import './index.scss'
|
||
import { getProductById } from '../../utils/productConfig'
|
||
import { getDesignList, setDesignList, updateDesign, type DesignItem, type StickerItem } from '../../utils/store'
|
||
import { createDesign, updateDesign as updateDesignApi, persistDesignMedia } 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'
|
||
|
||
export default function DIYPage() {
|
||
const { theme, resolvedTheme } = useThemeContext()
|
||
const safe = useSafeArea()
|
||
useStatusBar(resolvedTheme)
|
||
const [category, setCategory] = useState<any>(null)
|
||
const [step, setStep] = useState(1)
|
||
const [designId, setDesignId] = useState('')
|
||
const [quantity, setQuantity] = useState(1)
|
||
const [stickers, setStickers] = useState<StickerItem[]>([])
|
||
const [activeStickerId, setActiveStickerId] = useState<string | null>(null)
|
||
const [isDragging, setIsDragging] = useState(false)
|
||
const [startPos, setStartPos] = useState({ x: 0, y: 0 })
|
||
const [previewMode, setPreviewMode] = useState(false)
|
||
const [hasOverlap, setHasOverlap] = useState(false)
|
||
|
||
/** 进入 DIY 即创建清单条目,并推进到「设计中」(问题3 修正:进入工作台即 SUBMITTED),失败降级本地 */
|
||
const createDesignEntry = (product: { id: string; name: string; price: number; icon: string }, count: number) => {
|
||
createDesign({
|
||
productId: product.id,
|
||
productName: product.name,
|
||
unitPrice: product.price,
|
||
count
|
||
})
|
||
.then(item => {
|
||
setDesignList([item, ...getDesignList()]) // 回写缓存,checkout 等页面仍读缓存
|
||
setDesignId(item.id)
|
||
return updateDesignApi(item.id, { status: 'designing' })
|
||
.then(updated => {
|
||
setDesignList(getDesignList().map(d => d.id === item.id ? { ...d, status: updated.status } : d))
|
||
})
|
||
.catch(() => {
|
||
setDesignList(getDesignList().map(d => d.id === item.id ? { ...d, status: 'designing' as const } : d))
|
||
})
|
||
})
|
||
.catch(() => {
|
||
const newDesign: DesignItem = {
|
||
id: 'DSG' + Date.now(),
|
||
productId: product.id,
|
||
productName: product.name,
|
||
productIcon: product.icon,
|
||
unitPrice: product.price,
|
||
count,
|
||
status: 'designing',
|
||
createdAt: new Date().toISOString().slice(0, 10)
|
||
}
|
||
setDesignList([...getDesignList(), newDesign])
|
||
setDesignId(newDesign.id)
|
||
})
|
||
}
|
||
|
||
useEffect(() => {
|
||
const router = Taro.getCurrentInstance().router
|
||
const params = router ? router.params : undefined
|
||
const source = params ? params.source : undefined
|
||
const productId = (params && params.productId) || (params && params.category)
|
||
|
||
if (source === 'product' && productId) {
|
||
const product = getProductById(productId)
|
||
if (product) {
|
||
setCategory(product)
|
||
const count = Number(params ? params.quantity : undefined) || 1
|
||
setQuantity(count)
|
||
createDesignEntry(product, count)
|
||
}
|
||
return
|
||
}
|
||
|
||
if (source === 'designList' && params && params.designId) {
|
||
const dId = params.designId
|
||
const list = getDesignList()
|
||
const design = list.find(d => d.id === dId)
|
||
if (design) {
|
||
setDesignId(dId)
|
||
setQuantity(design.count)
|
||
const product = getProductById(design.productId)
|
||
if (product) setCategory(product)
|
||
const dd = design.designData
|
||
const rawStickers = dd ? dd.stickers : undefined
|
||
if (rawStickers) {
|
||
// 问题1 留意点:旧数据 width/height 为图片原始像素语义,读取端按 mask 等比归一化
|
||
//(契约实现注记,不做存量迁移);新语义数据(≤mask 短边 60%)原样通过
|
||
const mask = (dd && dd.category && dd.category.mask) || (product ? product.mask : undefined)
|
||
const normalized: StickerItem[] = mask
|
||
? rawStickers.map(s => {
|
||
if (s.width > mask.width || s.height > mask.height) {
|
||
const k = (Math.min(mask.width, mask.height) * 0.6) / Math.max(s.width, s.height)
|
||
return { ...s, width: Math.round(s.width * k), height: Math.round(s.height * k) }
|
||
}
|
||
return s
|
||
})
|
||
: rawStickers
|
||
setStickers(normalized)
|
||
} else if (dd ? dd.imageSrc : undefined) {
|
||
// 兼容旧版数据
|
||
const s: StickerItem = {
|
||
id: 'legacy_' + Date.now(),
|
||
src: dd.imageSrc,
|
||
x: dd.imagePos ? dd.imagePos.x || 0 : 0,
|
||
y: dd.imagePos ? dd.imagePos.y || 0 : 0,
|
||
scale: dd.imagePos ? dd.imagePos.scale || 1 : 1,
|
||
width: 200,
|
||
height: 200,
|
||
isOverlapping: false
|
||
}
|
||
setStickers([s])
|
||
}
|
||
|
||
// 问题3 修正:继续设计同样进入「设计中」状态(单向状态机,undesigned → designing)
|
||
if (design.status === 'undesigned') {
|
||
updateDesign(dId, { status: 'designing' })
|
||
updateDesignApi(dId, { status: 'designing' }).catch(() => {})
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
// 默认情况(从首页 old category 参数兼容)
|
||
if (productId) {
|
||
const found = getProductById(productId)
|
||
if (found) {
|
||
setCategory(found)
|
||
createDesignEntry(found, 1)
|
||
}
|
||
}
|
||
}, [])
|
||
|
||
// ---------- 草稿持久化(修正工作流 问题2)----------
|
||
// 贴纸变化防抖 800ms:本地缓存即时同步 + 服务端静默同步;不推状态(SUBMITTED 只由显式动作触发)
|
||
const draftTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||
const latestRef = useRef({ designId: '', quantity: 1, category: null as any, stickers: [] as StickerItem[] })
|
||
latestRef.current = { designId, quantity, category, stickers }
|
||
|
||
const saveDraft = () => {
|
||
const { designId: dId, quantity: qty, category: cat, stickers: st } = latestRef.current
|
||
if (!dId || !cat) return
|
||
// WCD 红线:基于缓存中服务端最新 designData 合并(保留 wordcloud 分组),只覆盖本页编辑字段
|
||
const cached = getDesignList().find(d => d.id === dId)
|
||
const designData = {
|
||
...(cached && cached.designData ? cached.designData : {}),
|
||
version: 1 as const,
|
||
category: { id: cat.id, mask: cat.mask, ...(cat.tone ? { tone: cat.tone } : {}) },
|
||
stickers: st
|
||
}
|
||
updateDesign(dId, { designData }) // 本地缓存即时同步(问题4:贴纸编辑页从这里读取)
|
||
updateDesignApi(dId, {
|
||
item: { productId: cat.id, productName: cat.name, unitPrice: cat.price, count: qty, designData }
|
||
}).catch(() => {}) // 草稿静默降级:失败仅落缓存
|
||
}
|
||
|
||
const flushDraft = () => {
|
||
if (draftTimerRef.current) {
|
||
clearTimeout(draftTimerRef.current)
|
||
draftTimerRef.current = null
|
||
}
|
||
saveDraft()
|
||
}
|
||
|
||
// 隐藏页面(切后台/跳转)时强制落盘,防止防抖窗口内的改动丢失
|
||
Taro.useDidHide(flushDraft)
|
||
// 卸载时同样落盘
|
||
useEffect(() => flushDraft, [])
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
|
||
const skipDraftRef = useRef(true)
|
||
useEffect(() => {
|
||
if (skipDraftRef.current) { skipDraftRef.current = false; return }
|
||
if (draftTimerRef.current) clearTimeout(draftTimerRef.current)
|
||
draftTimerRef.current = setTimeout(saveDraft, 800)
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [stickers])
|
||
|
||
// 矩形碰撞检测
|
||
const checkOverlap = (list: StickerItem[]) => {
|
||
const newList = list.map(s => ({ ...s, isOverlapping: false }))
|
||
for (let i = 0; i < newList.length; i++) {
|
||
for (let j = i + 1; j < newList.length; j++) {
|
||
const a = newList[i]
|
||
const b = newList[j]
|
||
const aw = a.width * a.scale
|
||
const ah = a.height * a.scale
|
||
const bw = b.width * b.scale
|
||
const bh = b.height * b.scale
|
||
if (
|
||
a.x < b.x + bw &&
|
||
a.x + aw > b.x &&
|
||
a.y < b.y + bh &&
|
||
a.y + ah > b.y
|
||
) {
|
||
newList[i].isOverlapping = true
|
||
newList[j].isOverlapping = true
|
||
}
|
||
}
|
||
}
|
||
const overlapAny = newList.some(s => s.isOverlapping)
|
||
setHasOverlap(overlapAny)
|
||
return newList
|
||
}
|
||
|
||
const addSticker = () => {
|
||
Taro.chooseImage({
|
||
count: 1,
|
||
sizeType: ['compressed'],
|
||
sourceType: ['album', 'camera'],
|
||
success: (res) => {
|
||
const src = res.tempFilePaths[0]
|
||
// 获取图片尺寸用于碰撞检测
|
||
Taro.getImageInfo({
|
||
src,
|
||
success: (info) => {
|
||
// 问题1 修正:归一化到画布坐标空间(width/height = 画布显示像素,契约实现注记)
|
||
const maskMin = Math.min(category.mask.width, category.mask.height)
|
||
const k = (maskMin * 0.6) / Math.max(info.width, info.height)
|
||
const newSticker: StickerItem = {
|
||
id: 'stk_' + Date.now(),
|
||
src,
|
||
x: 0,
|
||
y: 0,
|
||
scale: 1,
|
||
width: Math.round(info.width * k),
|
||
height: Math.round(info.height * k),
|
||
isOverlapping: false
|
||
}
|
||
const next = [...stickers, newSticker]
|
||
const checked = checkOverlap(next)
|
||
setStickers(checked)
|
||
setActiveStickerId(newSticker.id)
|
||
},
|
||
fail: () => {
|
||
// fallback 尺寸(同样按画布空间归一化)
|
||
const maskMin = Math.min(category.mask.width, category.mask.height)
|
||
const k = (maskMin * 0.6) / 200
|
||
const newSticker: StickerItem = {
|
||
id: 'stk_' + Date.now(),
|
||
src,
|
||
x: 0,
|
||
y: 0,
|
||
scale: 1,
|
||
width: Math.round(200 * k),
|
||
height: Math.round(200 * k),
|
||
isOverlapping: false
|
||
}
|
||
const next = [...stickers, newSticker]
|
||
const checked = checkOverlap(next)
|
||
setStickers(checked)
|
||
setActiveStickerId(newSticker.id)
|
||
}
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
const deleteSticker = (id: string) => {
|
||
const next = stickers.filter(s => s.id !== id)
|
||
const checked = checkOverlap(next)
|
||
setStickers(checked)
|
||
if (activeStickerId === id) setActiveStickerId(null)
|
||
}
|
||
|
||
const handleStickerTouchStart = (e: any, id: string) => {
|
||
const touch = e.touches[0]
|
||
const s = stickers.find(x => x.id === id)
|
||
if (!s) return
|
||
setActiveStickerId(id)
|
||
setIsDragging(true)
|
||
setStartPos({ x: touch.clientX - s.x, y: touch.clientY - s.y })
|
||
}
|
||
|
||
const handleStickerTouchMove = (e: any) => {
|
||
if (!isDragging || !activeStickerId) return
|
||
const touch = e.touches[0]
|
||
const next = stickers.map(s => {
|
||
if (s.id !== activeStickerId) return s
|
||
// 问题1 修正:坐标钳制在画布内,杜绝负坐标与拖出画布(碰撞盒 = x/y + 显示宽高)
|
||
const maxX = Math.max(0, category.mask.width - s.width * s.scale)
|
||
const maxY = Math.max(0, category.mask.height - s.height * s.scale)
|
||
return {
|
||
...s,
|
||
x: Math.min(Math.max(touch.clientX - startPos.x, 0), maxX),
|
||
y: Math.min(Math.max(touch.clientY - startPos.y, 0), maxY)
|
||
}
|
||
})
|
||
const checked = checkOverlap(next)
|
||
setStickers(checked)
|
||
}
|
||
|
||
const handleTouchEnd = () => setIsDragging(false)
|
||
|
||
const handleEditSticker = () => {
|
||
if (!activeStickerId || !designId) return
|
||
flushDraft() // 问题4:跳编辑页前清掉防抖窗口,缓存里保证有最新贴纸
|
||
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
|
||
const scale = Math.max(0.3, Math.min(3, s.scale + delta))
|
||
// 问题1 修正:缩放后把位置钳回画布内
|
||
const maxX = Math.max(0, category.mask.width - s.width * scale)
|
||
const maxY = Math.max(0, category.mask.height - s.height * scale)
|
||
return {
|
||
...s,
|
||
scale,
|
||
x: Math.min(Math.max(s.x, 0), maxX),
|
||
y: Math.min(Math.max(s.y, 0), maxY)
|
||
}
|
||
})
|
||
const checked = checkOverlap(next)
|
||
setStickers(checked)
|
||
}
|
||
|
||
// 问题5 修正:渲染端消费贴纸 edits(亮度/色相/对比度 → CSS filter),
|
||
// 与贴纸编辑页同一呈现逻辑;checkout 预览消费 edits 记入 R3 待办
|
||
const stickerFilter = (s: StickerItem) => {
|
||
const ed = s.edits
|
||
if (!ed) return undefined
|
||
const b = ed.brightness || 0
|
||
const h = ed.hue || 0
|
||
const c = ed.contrast || 0
|
||
if (b === 0 && h === 0 && c === 0) return undefined
|
||
return `brightness(${100 + b}%) hue-rotate(${h}deg) contrast(${100 + c}%)`
|
||
}
|
||
|
||
const goBack = () => {
|
||
Taro.navigateBack()
|
||
}
|
||
|
||
const getMaskStyle = () => {
|
||
if (!category) return { width: 300, height: 420 }
|
||
const base: any = { width: category.mask.width, height: category.mask.height }
|
||
if (category.mask.shape === 'rect') {
|
||
base.borderRadius = category.mask.borderRadius || 0
|
||
}
|
||
if (category.mask.shape === 'circle') {
|
||
base.borderRadius = '50%'
|
||
}
|
||
return base
|
||
}
|
||
|
||
const handleComplete = async () => {
|
||
if (hasOverlap) {
|
||
Taro.showToast({ title: '贴纸不能重叠', icon: 'none' })
|
||
return
|
||
}
|
||
if (designId && category) {
|
||
// 贴纸/底图持久化(决策#4,R4 负责):本地图 → COS 持久 URL,保证后端 WCD 打包可下载素材
|
||
const data = await persistDesignMedia({ stickers, category, version: 1 })
|
||
// category 裁剪为 {id, mask, tone}(阶段0 决策#2;契约约束#3:mask 必须保留)
|
||
const trimmedCategory = {
|
||
id: category.id,
|
||
mask: category.mask,
|
||
...(category.tone ? { tone: category.tone } : {})
|
||
}
|
||
// WCD 红线:基于服务端已有 designData 合并(保留 R4 写入的 wordcloud 分组等),只覆盖本页编辑的字段
|
||
const cached = getDesignList().find(d => d.id === designId)
|
||
const designData = {
|
||
...(cached && cached.designData ? cached.designData : {}),
|
||
version: 1 as const,
|
||
category: trimmedCategory,
|
||
stickers: data.stickers || stickers
|
||
}
|
||
// 本地缓存同步(checkout 仍读缓存)
|
||
updateDesign(designId, { status: 'designing', designData })
|
||
try {
|
||
await updateDesignApi(designId, {
|
||
status: 'designing',
|
||
item: {
|
||
productId: category.id,
|
||
productName: category.name,
|
||
unitPrice: category.price,
|
||
count: quantity,
|
||
designData
|
||
}
|
||
})
|
||
} catch {
|
||
Taro.showToast({ title: '网络不可用,已暂存到本地', icon: 'none' })
|
||
}
|
||
}
|
||
setPreviewMode(false)
|
||
Taro.navigateTo({ url: `/pages/checkout/index?designId=${designId}` })
|
||
}
|
||
|
||
const activeSticker = activeStickerId ? stickers.find(s => s.id === activeStickerId) : undefined
|
||
const activeScale = activeSticker ? activeSticker.scale : 1
|
||
|
||
if (!category) {
|
||
return (
|
||
<View className={`theme-${resolvedTheme}`}>
|
||
<ThemedPageMeta />
|
||
<ScrollTopMask title="设计工作台" targetSelector=".page-header.surface-card" showBack />
|
||
<View className='diy-page'>
|
||
<Text className='page-title'>加载中...</Text>
|
||
</View>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<View className={`theme-${resolvedTheme}`}>
|
||
<ThemedPageMeta />
|
||
<ScrollTopMask title="设计工作台" targetSelector=".page-header.surface-card" showBack />
|
||
<View className='diy-page'>
|
||
|
||
<View className='page-header surface-card' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||
<View className='flex-between' style={{ width: '100%' }}>
|
||
<Text className='back-btn' onTap={goBack}>←</Text>
|
||
<Text className='page-title'>设计工作台</Text>
|
||
<View className='header-spacer' />
|
||
</View>
|
||
</View>
|
||
|
||
{/* 画布区域 — catchtouchmove 阻止事件冒泡导致页面滚动 */}
|
||
<View className='canvas-wrapper surface-card mt-20' catchMove>
|
||
<View className='canvas-area' style={getMaskStyle()}>
|
||
{stickers.map(s => (
|
||
<Image
|
||
key={s.id}
|
||
className={`canvas-image ${s.isOverlapping ? 'overlap' : ''} ${activeStickerId === s.id ? 'active' : ''}`}
|
||
src={s.src}
|
||
style={{
|
||
transform: `translate(${s.x}px, ${s.y}px) scale(${s.scale})`,
|
||
opacity: isDragging && activeStickerId === s.id ? 0.8 : 1,
|
||
zIndex: activeStickerId === s.id ? 10 : 5,
|
||
filter: stickerFilter(s),
|
||
width: s.width || 200,
|
||
height: s.height || 200
|
||
}}
|
||
onTouchStart={(e) => handleStickerTouchStart(e, s.id)}
|
||
onTouchMove={handleStickerTouchMove}
|
||
onTouchEnd={handleTouchEnd}
|
||
onTap={() => setActiveStickerId(s.id)}
|
||
mode='aspectFit'
|
||
/>
|
||
))}
|
||
<View className='mask-border' style={getMaskStyle()} />
|
||
</View>
|
||
</View>
|
||
|
||
{/* 贴纸列表与控制 */}
|
||
<View className='sticker-panel surface-card mt-20'>
|
||
<Text className='section-title'>贴纸</Text>
|
||
<View className='sticker-list'>
|
||
{stickers.map(s => (
|
||
<View key={s.id} className={`sticker-thumb ${s.isOverlapping ? 'overlap' : ''} ${activeStickerId === s.id ? 'active' : ''}`}>
|
||
<Image src={s.src} className='sticker-thumb-img' mode='aspectFit' onTap={() => setActiveStickerId(s.id)} />
|
||
<View className='sticker-del' onTap={() => deleteSticker(s.id)}>
|
||
<Text className='del-icon'>×</Text>
|
||
</View>
|
||
</View>
|
||
))}
|
||
<View className='sticker-add-btn' onTap={addSticker}>
|
||
<Text className='add-icon'>+</Text>
|
||
<Text className='add-label'>添加</Text>
|
||
</View>
|
||
</View>
|
||
{hasOverlap && <Text className='overlap-hint'>有贴纸重叠,请调整位置</Text>}
|
||
</View>
|
||
|
||
{/* 控制工具栏 */}
|
||
{activeStickerId && (
|
||
<View className='toolbar surface-card mt-20'>
|
||
<View className='tool-row'>
|
||
<Text className='tool-label'>缩放</Text>
|
||
<View className='flex-center' style={{ gap: '20px' }}>
|
||
<View className='tool-btn' onTap={() => handleScale(activeStickerId, -0.1)}>-</View>
|
||
<Text className='tool-value'>{Math.round(activeScale * 100)}%</Text>
|
||
<View className='tool-btn' onTap={() => handleScale(activeStickerId, 0.1)}>+</View>
|
||
</View>
|
||
</View>
|
||
<View className='tool-row'>
|
||
<Text className='tool-label'>提示</Text>
|
||
<Text className='tool-hint'>拖动调整位置,贴纸不可重叠</Text>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{/* 常驻编辑按钮 */}
|
||
<View className='edit-btn-bar mt-20'>
|
||
<View
|
||
className={`edit-btn ${activeStickerId ? 'active' : 'disabled'}`}
|
||
onTap={handleEditSticker}
|
||
>
|
||
<Text>编辑</Text>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 底部操作按钮 */}
|
||
<BottomActionBar>
|
||
<View className='btn-primary' onTap={() => setPreviewMode(true)}>
|
||
<Text>预览效果</Text>
|
||
</View>
|
||
<View className='btn-secondary' onTap={addSticker}>
|
||
<Text>添加贴纸</Text>
|
||
</View>
|
||
</BottomActionBar>
|
||
|
||
{/* 预览弹层 */}
|
||
{previewMode && (
|
||
<View className='preview-overlay'>
|
||
<View className='preview-card surface-card'>
|
||
<Text className='preview-title'>确认效果</Text>
|
||
<View className='preview-canvas' style={getMaskStyle()}>
|
||
{stickers.map(s => (
|
||
<Image
|
||
key={s.id}
|
||
className={`preview-image ${s.isOverlapping ? 'overlap' : ''}`}
|
||
src={s.src}
|
||
style={{
|
||
transform: `translate(${s.x}px, ${s.y}px) scale(${s.scale})`,
|
||
filter: stickerFilter(s),
|
||
width: s.width || 200,
|
||
height: s.height || 200
|
||
}}
|
||
mode='aspectFit'
|
||
/>
|
||
))}
|
||
</View>
|
||
<View className='product-info'>
|
||
<Text className='product-name'>{category.name}</Text>
|
||
<Text className='product-size'>画布尺寸: {category.mask.width} × {category.mask.height} px</Text>
|
||
{hasOverlap && <Text className='overlap-hint'>贴纸有重叠,不可提交</Text>}
|
||
</View>
|
||
<View className='preview-actions'>
|
||
<View className='btn-secondary' onTap={() => setPreviewMode(false)}>
|
||
<Text>返回修改</Text>
|
||
</View>
|
||
<View className={`btn-primary ${hasOverlap ? 'disabled' : ''}`} onTap={handleComplete}>
|
||
<Text>确认完成</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
<View className='safe-bottom-placeholder' />
|
||
</View>
|
||
</View>
|
||
)
|
||
}
|