412 lines
14 KiB
TypeScript
412 lines
14 KiB
TypeScript
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>
|
||
)
|
||
}
|