添加登录和后端校验
完成后端设计(未在本仓库体现),通过安全的手段完成了登录鉴权
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: 'DIY工作台'
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
.diy-page {
|
||||
padding: 0 24px 24px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 画布区域 */
|
||||
.canvas-wrapper {
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.canvas-area {
|
||||
position: relative;
|
||||
background: linear-gradient(135deg, rgba(255, 154, 158, 0.08) 0%, rgba(160, 196, 255, 0.08) 100%);
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mask-border {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 3px dashed var(--accent-blue);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.canvas-image {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
z-index: 5;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.canvas-image.active {
|
||||
border-color: var(--accent-pink);
|
||||
}
|
||||
|
||||
.canvas-image.overlap {
|
||||
border-color: #ff4757;
|
||||
box-shadow: 0 0 12px rgba(255, 71, 87, 0.5);
|
||||
}
|
||||
|
||||
/* 贴纸面板 */
|
||||
.sticker-panel {
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.sticker-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.sticker-thumb {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 2px dashed var(--line-star);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-input);
|
||||
}
|
||||
|
||||
.sticker-thumb.active {
|
||||
border-color: var(--accent-pink);
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.sticker-thumb.overlap {
|
||||
border-color: #ff4757;
|
||||
}
|
||||
|
||||
.sticker-thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sticker-del {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(255, 71, 87, 0.85);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.del-icon {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sticker-add-btn {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 2px dashed var(--line-star);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-input);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 48px;
|
||||
color: var(--accent-pink);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.add-label {
|
||||
font-size: 22px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.overlap-hint {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
font-size: 26px;
|
||||
color: #ff4757;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 工具栏 */
|
||||
.toolbar {
|
||||
padding: 28px 32px;
|
||||
}
|
||||
|
||||
.tool-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tool-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tool-label {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: linear-gradient(135deg, rgba(255, 154, 158, 0.08) 0%, rgba(160, 196, 255, 0.08) 100%);
|
||||
border: 2px dashed var(--accent-blue);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tool-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--accent-pink);
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tool-hint {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 预览弹窗 */
|
||||
.preview-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
background: var(--bg-card);
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-canvas {
|
||||
background: linear-gradient(135deg, rgba(255, 154, 158, 0.08) 0%, rgba(160, 196, 255, 0.08) 100%);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
margin: 0 auto 24px;
|
||||
border: 3px dashed var(--accent-blue);
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
min-height: 200px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.preview-image.overlap {
|
||||
border: 2px solid #ff4757;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.product-size {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.preview-actions .btn-outline,
|
||||
.preview-actions .btn-gradient {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-actions .btn-gradient.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 常驻编辑按钮 */
|
||||
.edit-btn-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
padding: 16px 48px;
|
||||
border-radius: 50px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
background: var(--bg-input);
|
||||
color: var(--text-secondary);
|
||||
border: 2px dashed var(--text-muted);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.edit-btn.active {
|
||||
background: linear-gradient(135deg, rgba(255, 154, 158, 0.12) 0%, rgba(160, 196, 255, 0.12) 100%);
|
||||
color: var(--accent-pink);
|
||||
border-color: var(--accent-pink);
|
||||
}
|
||||
|
||||
.edit-btn.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.action-btns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '编辑贴纸'
|
||||
})
|
||||
@@ -0,0 +1,184 @@
|
||||
.sticker-edit-page {
|
||||
padding: 0 24px 24px;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edit-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: calc(env(safe-area-inset-top, 0px) + 24px) 0 24px;
|
||||
}
|
||||
|
||||
.edit-back {
|
||||
font-size: 36px;
|
||||
color: var(--text-primary);
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.edit-title {
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.edit-save {
|
||||
font-size: 28px;
|
||||
color: var(--accent-pink);
|
||||
font-weight: 600;
|
||||
width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 画布区域 */
|
||||
.edit-canvas-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.edit-canvas {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
background: var(--bg-input);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
/* 工具面板 */
|
||||
.edit-tools {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.edit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.edit-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.edit-label {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.slider-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.slider-track {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-input);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.slider-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(90deg, #ff9a9e, #fecfef);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.slider-thumb {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
border: 4px solid var(--accent-pink);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
font-size: 24px;
|
||||
color: var(--text-secondary);
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 快速操作按钮 */
|
||||
.edit-quick-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
padding: 16px 28px;
|
||||
border-radius: 40px;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
border: 2px dashed var(--line-star);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.quick-btn:active {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.quick-btn.active {
|
||||
background: rgba(255, 154, 158, 0.12);
|
||||
border-color: var(--accent-pink);
|
||||
color: var(--accent-pink);
|
||||
}
|
||||
|
||||
/* 底部操作 */
|
||||
.edit-footer {
|
||||
padding: 20px 0 calc(20px + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.edit-footer .btn-gradient,
|
||||
.edit-footer .btn-outline {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
/* 加载遮罩 */
|
||||
.edit-loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.4);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edit-loading-text {
|
||||
font-size: 28px;
|
||||
color: #fff;
|
||||
margin-top: 20px;
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
import { View, Text, Canvas, Image } from '@tarojs/components'
|
||||
import Taro, { createCanvasContext, canvasToTempFilePath } from '@tarojs/taro'
|
||||
import { useState, useEffect, useRef, useCallback } 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'
|
||||
|
||||
/* ============================================================
|
||||
使用 Canvas 实现贴纸编辑(亮度/色相/线稿)
|
||||
============================================================ */
|
||||
|
||||
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max)
|
||||
|
||||
/** 拖动手势滑块组件 */
|
||||
interface SliderProps {
|
||||
value: number
|
||||
min: number
|
||||
max: number
|
||||
step?: number
|
||||
onChange: (val: number) => void
|
||||
showValue?: boolean
|
||||
format?: (v: number) => string
|
||||
}
|
||||
|
||||
function TouchSlider({ value, min, max, step = 1, onChange, format }: SliderProps) {
|
||||
const trackRef = useRef<any>(null)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
|
||||
const computedFromClientX = (clientX: number) => {
|
||||
if (!trackRef.current) return value
|
||||
const query = Taro.createSelectorQuery().in(trackRef.current)
|
||||
// 注意:这里需要取 track 元素的位置信息
|
||||
const rect = trackRef.current.getBoundingClientRect ? trackRef.current.getBoundingClientRect() : { left: 0, width: 300 }
|
||||
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
|
||||
let raw = min + (max - min) * ratio
|
||||
if (step > 0) {
|
||||
raw = Math.round(raw / step) * step
|
||||
}
|
||||
return clamp(raw, min, max)
|
||||
}
|
||||
|
||||
const handleTouchStart = (e: any) => {
|
||||
setDragging(true)
|
||||
const x = e.touches?.[0]?.clientX ?? 0
|
||||
onChange(computedFromClientX(x))
|
||||
}
|
||||
|
||||
const handleTouchMove = (e: any) => {
|
||||
if (!dragging) return
|
||||
const x = e.touches?.[0]?.clientX ?? 0
|
||||
onChange(computedFromClientX(x))
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
setDragging(false)
|
||||
}
|
||||
|
||||
const pct = `${((value - min) / (max - min)) * 100}%`
|
||||
|
||||
return (
|
||||
<View className='slider-wrap'>
|
||||
<View
|
||||
className='slider-track'
|
||||
ref={trackRef}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
<View className='slider-fill' style={{ width: pct }} />
|
||||
<View className={`slider-thumb ${dragging ? 'dragging' : ''}`} style={{ left: pct }} />
|
||||
</View>
|
||||
<Text className='slider-value'>{format ? format(value) : value}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default function StickerEditPage() {
|
||||
const { resolvedTheme } = useThemeContext()
|
||||
const safe = useSafeArea()
|
||||
useStatusBar(resolvedTheme)
|
||||
const [designId, setDesignId] = useState('')
|
||||
const [stickerId, setStickerId] = useState('')
|
||||
const [sticker, setSticker] = useState<StickerItem | null>(null)
|
||||
|
||||
// 编辑参数
|
||||
const [brightness, setBrightness] = useState(0)
|
||||
const [hue, setHue] = useState(0)
|
||||
const [contrast, setContrast] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [canvasReady, setCanvasReady] = useState(false)
|
||||
|
||||
const canvasRef = useRef<any>(null)
|
||||
const ctxRef = useRef<any>(null)
|
||||
const canvasNodeRef = useRef<any>(null)
|
||||
const canvasSizeRef = useRef<{width:number, height:number}>({width:0,height:0})
|
||||
|
||||
// 使用 nextTick + 重试初始化 Canvas(小程序 2D)
|
||||
const initCanvas = useCallback((st: StickerItem) => {
|
||||
if (canvasReady) return
|
||||
Taro.nextTick(() => {
|
||||
const query = Taro.createSelectorQuery()
|
||||
query.select('#editCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res: any) => {
|
||||
const canvas = res?.[0]?.node
|
||||
if (!canvas) {
|
||||
setTimeout(() => initCanvas(st), 300)
|
||||
return
|
||||
}
|
||||
const ctx = canvas.getContext('2d')
|
||||
let cw = res[0]?.width || 300
|
||||
let ch = res[0]?.height || 400
|
||||
if (cw <= 0) cw = 300
|
||||
if (ch <= 0) ch = 400
|
||||
const dpr = Taro.getSystemInfoSync().pixelRatio || 1
|
||||
canvas.width = Math.round(cw * dpr)
|
||||
canvas.height = Math.round(ch * dpr)
|
||||
ctx.scale(dpr, dpr)
|
||||
canvasNodeRef.current = canvas
|
||||
ctxRef.current = ctx
|
||||
canvasSizeRef.current = { width: cw, height: ch }
|
||||
setCanvasReady(true)
|
||||
drawSticker(ctx, cw, ch, st)
|
||||
})
|
||||
})
|
||||
}, [canvasReady])
|
||||
|
||||
useEffect(() => {
|
||||
const params = Taro.getCurrentInstance().router?.params
|
||||
const dId = params?.designId as string
|
||||
const sId = params?.stickerId as string
|
||||
setDesignId(dId)
|
||||
setStickerId(sId)
|
||||
|
||||
const list = getDesignList()
|
||||
const design = list.find(d => d.id === dId)
|
||||
const st = design?.designData?.stickers?.find(s => s.id === sId)
|
||||
if (st) {
|
||||
setSticker(st)
|
||||
setBrightness(st.edits?.brightness || 0)
|
||||
setHue(st.edits?.hue || 0)
|
||||
setContrast(st.edits?.contrast || 0)
|
||||
initCanvas(st)
|
||||
}
|
||||
}, [initCanvas])
|
||||
|
||||
/** canvas 上绘制带滤镜的贴纸 */
|
||||
const drawSticker = (ctx: any, cw: number, ch: number, st: StickerItem, opts?: { brightness: number; hue: number; contrast: number }) => {
|
||||
if (!ctx || !canvasNodeRef.current) return
|
||||
const canvas = canvasNodeRef.current
|
||||
|
||||
// 防护:cw/ch 为 0 时容易绘制异常
|
||||
if (!cw || !ch) return
|
||||
const imgW = st.width || 200
|
||||
const imgH = st.height || 200
|
||||
|
||||
const img = canvas.createImage()
|
||||
img.onload = () => {
|
||||
ctx.clearRect(0, 0, cw, ch)
|
||||
ctx.save()
|
||||
|
||||
// 居中缩放显示(限制最大为画板 90%)
|
||||
const scale = Math.min(cw / imgW, ch / imgH, 4) * 0.9
|
||||
const w = imgW * scale
|
||||
const h = imgH * scale
|
||||
const x = (cw - w) / 2
|
||||
const y = (ch - h) / 2
|
||||
|
||||
// 应用滤镜(小程序 Canvas 2D 支持 filter 属性 2.16.0+)
|
||||
const b = opts?.brightness ?? brightness
|
||||
const hVal = opts?.hue ?? hue
|
||||
const c = opts?.contrast ?? contrast
|
||||
if (b !== 0 || hVal !== 0 || c !== 0) {
|
||||
ctx.filter = `brightness(${100 + b}%) hue-rotate(${hVal}deg) contrast(${100 + c}%)`
|
||||
} else {
|
||||
ctx.filter = 'none'
|
||||
}
|
||||
|
||||
ctx.drawImage(img, x, y, w, h)
|
||||
ctx.restore()
|
||||
}
|
||||
img.onerror = (err: any) => {
|
||||
console.error('Canvas load image error:', err, st.src)
|
||||
Taro.showToast({ title: '图片加载失败', icon: 'none' })
|
||||
}
|
||||
if (st.src) {
|
||||
img.src = st.src
|
||||
}
|
||||
}
|
||||
|
||||
/** 重新绘制(参数变化时) */
|
||||
const redraw = () => {
|
||||
if (!sticker || !ctxRef.current || !canvasNodeRef.current) return
|
||||
const { width, height } = canvasSizeRef.current
|
||||
drawSticker(ctxRef.current, width, height, sticker, { brightness, hue, contrast })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
redraw()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [brightness, hue, contrast])
|
||||
|
||||
const handleSave = () => {
|
||||
if (!sticker || !designId) return
|
||||
setLoading(true)
|
||||
// 使用 Canvas 2D 导出
|
||||
// @ts-ignore
|
||||
Taro.canvasToTempFilePath({
|
||||
canvas: canvasNodeRef.current,
|
||||
quality: 0.92,
|
||||
success: (res: any) => {
|
||||
const newSrc = res.tempFilePath
|
||||
const dList = getDesignList()
|
||||
const dIdx = dList.findIndex(d => d.id === designId)
|
||||
if (dIdx === -1) { setLoading(false); return }
|
||||
const stickers = dList[dIdx].designData?.stickers || []
|
||||
const sIdx = stickers.findIndex(s => s.id === stickerId)
|
||||
if (sIdx === -1) { setLoading(false); return }
|
||||
stickers[sIdx] = {
|
||||
...stickers[sIdx],
|
||||
src: newSrc,
|
||||
edits: { brightness, hue, contrast }
|
||||
}
|
||||
updateDesign(designId, { designData: { ...dList[dIdx].designData, stickers } })
|
||||
Taro.showToast({ title: '保存成功', icon: 'success' })
|
||||
Taro.navigateBack()
|
||||
},
|
||||
fail: () => {
|
||||
setLoading(false)
|
||||
Taro.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 裁剪:调用微信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)
|
||||
// 重绘
|
||||
setTimeout(() => redraw(), 200)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 线稿:预留后端AI接口 */
|
||||
const API_BASE = 'https://your-api-domain.com' // ← 填入你的服务器地址
|
||||
|
||||
const handleSketch = () => {
|
||||
if (!sticker) return
|
||||
setLoading(true)
|
||||
Taro.uploadFile({
|
||||
url: `${API_BASE}/api/sketch`,
|
||||
filePath: sticker.src,
|
||||
name: 'image',
|
||||
success: (res) => {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.url) {
|
||||
const newSticker = { ...sticker, src: data.url, edits: { ...sticker.edits, sketchSrc: data.url } }
|
||||
setSticker(newSticker)
|
||||
setTimeout(() => redraw(), 200)
|
||||
Taro.showToast({ title: '线稿生成成功', icon: 'success' })
|
||||
} else {
|
||||
throw new Error('no url')
|
||||
}
|
||||
} catch {
|
||||
// 如果接口不可用,降级为前端灰度+边缘检测
|
||||
applyFrontendSketch()
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
applyFrontendSketch()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 前端线稿降级方案:灰度+反相高对比 */
|
||||
const applyFrontendSketch = () => {
|
||||
if (!sticker || !ctxRef.current) { setLoading(false); return }
|
||||
Taro.showToast({ title: '使用本地线稿', icon: 'none' })
|
||||
setBrightness(20)
|
||||
setContrast(80)
|
||||
setHue(0)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const bPct = `${((brightness + 100) / 200) * 100}%`
|
||||
const hPct = `${((hue + 180) / 360) * 100}%`
|
||||
const cPct = `${((contrast + 100) / 200) * 100}%`
|
||||
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<View className='sticker-edit-page'>
|
||||
{/* Header */}
|
||||
<View className='edit-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<Text className='edit-back' onTap={() => Taro.navigateBack()}>←</Text>
|
||||
<Text className='edit-title'>编辑贴纸</Text>
|
||||
<Text className='edit-save' onTap={handleSave}>保存</Text>
|
||||
</View>
|
||||
|
||||
{/* Canvas 预览 */}
|
||||
<View className='edit-canvas-wrap'>
|
||||
<Canvas
|
||||
id='editCanvas'
|
||||
type='2d'
|
||||
className='edit-canvas'
|
||||
style={{ width: '100%', height: '400px' }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 快速操作 */}
|
||||
<View className='edit-tools dashed-card mt-20'>
|
||||
<View className='edit-row'>
|
||||
<Text className='edit-label'>快速操作</Text>
|
||||
<View className='edit-quick-actions'>
|
||||
<View className='quick-btn' onTap={handleCrop}>
|
||||
<Text>裁剪</Text>
|
||||
</View>
|
||||
<View className='quick-btn' onTap={handleSketch}>
|
||||
<Text>转线稿</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 亮度 */}
|
||||
<View className='edit-row slider-row'
|
||||
onTouchMove={(e) => {
|
||||
const { clientX } = e.changedTouches[0]
|
||||
// 取得滑轨位置
|
||||
const query = Taro.createSelectorQuery().in(e.currentTarget as any)
|
||||
query.select('.slider-track').boundingClientRect((rect: any) => {
|
||||
if (!rect) return
|
||||
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
|
||||
const v = Math.round((-100 + 200 * ratio) / 1) * 1
|
||||
setBrightness(clamp(v, -100, 100))
|
||||
}).exec()
|
||||
}}
|
||||
>
|
||||
<Text className='edit-label'>亮度</Text>
|
||||
<View className='slider-wrap'>
|
||||
<View className='slider-track'>
|
||||
<View className='slider-fill' style={{ width: bPct }} />
|
||||
<View className='slider-thumb' style={{ left: bPct }} />
|
||||
</View>
|
||||
<Text className='slider-value'>{brightness > 0 ? `+${brightness}` : brightness}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 色相 */}
|
||||
<View className='edit-row slider-row'
|
||||
onTouchMove={(e) => {
|
||||
const { clientX } = e.changedTouches[0]
|
||||
const query = Taro.createSelectorQuery().in(e.currentTarget as any)
|
||||
query.select('.slider-track').boundingClientRect((rect: any) => {
|
||||
if (!rect) return
|
||||
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
|
||||
const v = Math.round((-180 + 360 * ratio) / 1) * 1
|
||||
setHue(clamp(v, -180, 180))
|
||||
}).exec()
|
||||
}}
|
||||
>
|
||||
<Text className='edit-label'>色相</Text>
|
||||
<View className='slider-wrap'>
|
||||
<View className='slider-track'>
|
||||
<View className='slider-fill' style={{ width: hPct }} />
|
||||
<View className='slider-thumb' style={{ left: hPct }} />
|
||||
</View>
|
||||
<Text className='slider-value'>{hue > 0 ? `+${hue}` : hue}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 对比度 */}
|
||||
<View className='edit-row slider-row'
|
||||
onTouchMove={(e) => {
|
||||
const { clientX } = e.changedTouches[0]
|
||||
const query = Taro.createSelectorQuery().in(e.currentTarget as any)
|
||||
query.select('.slider-track').boundingClientRect((rect: any) => {
|
||||
if (!rect) return
|
||||
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
|
||||
const v = Math.round((-100 + 200 * ratio) / 1) * 1
|
||||
setContrast(clamp(v, -100, 100))
|
||||
}).exec()
|
||||
}}
|
||||
>
|
||||
<Text className='edit-label'>对比度</Text>
|
||||
<View className='slider-wrap'>
|
||||
<View className='slider-track'>
|
||||
<View className='slider-fill' style={{ width: cPct }} />
|
||||
<View className='slider-thumb' style={{ left: cPct }} />
|
||||
</View>
|
||||
<Text className='slider-value'>{contrast > 0 ? `+${contrast}` : contrast}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={{ flex: 1 }} />
|
||||
|
||||
{loading && (
|
||||
<View className='edit-loading'>
|
||||
<Text className='edit-loading-text'>处理中…</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user