Files
wechat_wc/src/pages/diy/index.tsx
T
broccoli d0b37d6aa3 状态栏修复更新
1.添加状态栏间距来防止内容遮挡
2.添加了跟随页面自动变色的状态栏标题色改变
2026-08-04 17:41:24 +08:00

412 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { View, Text, Image } from '@tarojs/components'
import Taro from '@tarojs/taro'
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 { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
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)
useEffect(() => {
const params = Taro.getCurrentInstance().router?.params
const source = params?.source
const productId = params?.productId || params?.category
if (source === 'product' && productId) {
const product = getProductById(productId)
if (product) {
setCategory(product)
setQuantity(Number(params?.quantity) || 1)
const newDesign: DesignItem = {
id: 'DSG' + Date.now(),
productId: product.id,
productName: product.name,
productIcon: product.icon,
unitPrice: product.price,
count: Number(params?.quantity) || 1,
status: 'designing',
createdAt: new Date().toISOString().slice(0, 10)
}
const list = getDesignList()
setDesignList([...list, newDesign])
setDesignId(newDesign.id)
}
return
}
if (source === 'designList' && 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)
if (design.designData?.stickers) {
setStickers(design.designData.stickers)
} else if (design.designData?.imageSrc) {
// 兼容旧版数据
const s: StickerItem = {
id: 'legacy_' + Date.now(),
src: design.designData.imageSrc,
x: design.designData.imagePos?.x || 0,
y: design.designData.imagePos?.y || 0,
scale: design.designData.imagePos?.scale || 1,
width: 200,
height: 200,
isOverlapping: false
}
setStickers([s])
}
}
return
}
// 默认情况(从首页 old category 参数兼容)
if (productId) {
const found = getProductById(productId)
if (found) {
setCategory(found)
const newDesign: DesignItem = {
id: 'DSG' + Date.now(),
productId: found.id,
productName: found.name,
productIcon: found.icon,
unitPrice: found.price,
count: 1,
status: 'designing',
createdAt: new Date().toISOString().slice(0, 10)
}
const list = getDesignList()
setDesignList([...list, newDesign])
setDesignId(newDesign.id)
}
}
}, [])
// 矩形碰撞检测
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) => {
const newSticker: StickerItem = {
id: 'stk_' + Date.now(),
src,
x: 0,
y: 0,
scale: 1,
width: info.width,
height: info.height,
isOverlapping: false
}
const next = [...stickers, newSticker]
const checked = checkOverlap(next)
setStickers(checked)
setActiveStickerId(newSticker.id)
},
fail: () => {
// fallback 尺寸
const newSticker: StickerItem = {
id: 'stk_' + Date.now(),
src,
x: 0,
y: 0,
scale: 1,
width: 200,
height: 200,
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
return { ...s, x: touch.clientX - startPos.x, y: touch.clientY - startPos.y }
})
const checked = checkOverlap(next)
setStickers(checked)
}
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
return { ...s, scale: Math.max(0.3, Math.min(3, s.scale + delta)) }
})
const checked = checkOverlap(next)
setStickers(checked)
}
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 = () => {
if (hasOverlap) {
Taro.showToast({ title: '贴纸不能重叠', icon: 'none' })
return
}
if (designId) {
updateDesign(designId, {
designData: {
stickers,
category
}
})
}
setPreviewMode(false)
Taro.navigateTo({ url: `/pages/checkout/index?designId=${designId}` })
}
if (!category) {
return (
<View className={`theme-${resolvedTheme}`}>
<View className='diy-page'>
<Text className='page-title'>加载中...</Text>
</View>
</View>
)
}
return (
<View className={`theme-${resolvedTheme}`}>
<View className='diy-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
<View className='star-badge' />
<View className='flex-between' style={{ width: '100%' }}>
<Text className='back-btn' onTap={goBack}></Text>
<Text className='page-title'>设计工作台</Text>
<View style={{ width: '60px' }} />
</View>
</View>
{/* 画布区域 — catchtouchmove 阻止事件冒泡导致页面滚动 */}
<View className='canvas-wrapper dashed-card mt-20' catchMove>
<View className='star-badge' />
<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,
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 dashed-card mt-20'>
<View className='star-badge' />
<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 dashed-card mt-20'>
<View className='star-badge' />
<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((stickers.find(s => s.id === activeStickerId)?.scale || 1) * 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>
{/* 底部操作按钮 */}
<View className='action-btns mt-20'>
<View className='btn-gradient' onTap={() => setPreviewMode(true)}>
<Text>预览效果</Text>
</View>
<View className='btn-outline' onTap={addSticker}>
<Text>添加贴纸</Text>
</View>
</View>
{/* 预览弹层 */}
{previewMode && (
<View className='preview-overlay'>
<View className='preview-card dashed-card'>
<View className='star-badge' />
<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})`,
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-outline' onTap={() => setPreviewMode(false)}>
<Text>返回修改</Text>
</View>
<View className={`btn-gradient ${hasOverlap ? 'disabled' : ''}`} onTap={handleComplete}>
<Text>确认完成</Text>
</View>
</View>
</View>
</View>
)}
<View style={{ height: '40px' }} />
</View>
</View>
)
}