feat: refresh product and customization flows

This commit is contained in:
2026-08-07 16:52:13 +08:00
parent 3af43f6e82
commit 6dd9e15ea5
10 changed files with 180 additions and 190 deletions
+14 -27
View File
@@ -19,7 +19,7 @@
position: relative; position: relative;
overflow: hidden; overflow: hidden;
background: rgba(0,0,0,0.03); background: rgba(0,0,0,0.03);
border: 2px dashed var(--line-star); border: 1rpx solid var(--border-strong);
max-width: 100%; max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -42,7 +42,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 20px 0; padding: 20px 0;
border-bottom: 1px dashed rgba(0,0,0,0.06); border-bottom: 1rpx solid var(--border);
} }
.info-row:last-child { .info-row:last-child {
@@ -68,7 +68,7 @@
.info-total { .info-total {
font-size: 40px; font-size: 40px;
font-weight: 800; font-weight: 800;
color: var(--accent-pink); color: var(--accent-primary);
} }
/* 地址卡片 */ /* 地址卡片 */
@@ -86,12 +86,12 @@
.address-change { .address-change {
padding: 6px 16px; padding: 6px 16px;
border-radius: 8px; border-radius: 8px;
background: rgba(91, 140, 255, 0.08); background: var(--accent-secondary-soft);
} }
.change-text { .change-text {
font-size: 24px; font-size: 24px;
color: var(--accent-blue); color: var(--accent-secondary);
} }
.checkout-address-body { .checkout-address-body {
@@ -160,11 +160,11 @@
.addr-picker-item { .addr-picker-item {
padding: 20px 16px; padding: 20px 16px;
border-bottom: 1px dashed rgba(0, 0, 0, 0.05); border-bottom: 1rpx solid var(--border);
} }
.addr-picker-item.active { .addr-picker-item.active {
background: rgba(91, 140, 255, 0.06); background: var(--accent-secondary-soft);
border-radius: 12px; border-radius: 12px;
} }
@@ -188,8 +188,8 @@
.addr-picker-default { .addr-picker-default {
font-size: 22px; font-size: 22px;
color: var(--accent-pink); color: var(--accent-primary);
border: 1px solid var(--accent-pink); border: 1px solid var(--accent-primary);
padding: 2px 8px; padding: 2px 8px;
border-radius: 6px; border-radius: 6px;
} }
@@ -210,31 +210,18 @@
margin-top: 16px; margin-top: 16px;
padding: 20px; padding: 20px;
text-align: center; text-align: center;
border: 2px dashed var(--accent-blue); border: 1rpx solid var(--border-strong);
border-radius: 12px; border-radius: 12px;
} }
.addr-picker-add-text { .addr-picker-add-text {
font-size: 28px; font-size: 28px;
color: var(--accent-blue); color: var(--accent-secondary);
} }
/* 底部操作 */ /* 底部操作 — 毛玻璃规则由全局 .action-bar 提供 */
.checkout-actions { .action-bar .btn-secondary,
position: fixed; .action-bar .btn-primary {
bottom: 0;
left: 0;
right: 0;
background: var(--bg-card);
border-top: var(--line-card);
padding: 20px 32px calc(20px + env(safe-area-inset-bottom));
display: flex;
gap: 24px;
z-index: 500;
}
.checkout-actions .btn-outline,
.checkout-actions .btn-gradient {
flex: 1; flex: 1;
text-align: center; text-align: center;
} }
+21 -24
View File
@@ -18,8 +18,9 @@ export default function CheckoutPage() {
const [addrList, setAddrList] = useState<AddressItem[]>([]) const [addrList, setAddrList] = useState<AddressItem[]>([])
useEffect(() => { useEffect(() => {
const params = Taro.getCurrentInstance().router?.params const router = Taro.getCurrentInstance().router
const dId = params?.designId const params = router ? router.params : undefined
const dId = params ? params.designId : undefined
if (!dId) return if (!dId) return
const list = getDesignList() const list = getDesignList()
const d = list.find(x => x.id === dId) const d = list.find(x => x.id === dId)
@@ -35,7 +36,7 @@ export default function CheckoutPage() {
} }
Taro.showModal({ Taro.showModal({
title: '确认下单', title: '确认下单',
content: `确认后将从设计清单生成订单,并寄送至:\n${address.region?.join(' ')} ${address.detail}`, content: `确认后将从设计清单生成订单,并寄送至:\n${address.region ? address.region.join(' ') : ''} ${address.detail}`,
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
designToOrder(design.id) designToOrder(design.id)
@@ -54,15 +55,15 @@ export default function CheckoutPage() {
Taro.showToast({ title: '地址已更改', icon: 'success' }) Taro.showToast({ title: '地址已更改', icon: 'success' })
} }
const hasStickers = design?.designData?.stickers && design.designData.stickers.length > 0 const hasStickers = design && design.designData && design.designData.stickers && design.designData.stickers.length > 0
const hasLegacyImage = design?.designData?.imageSrc const hasLegacyImage = design && design.designData ? design.designData.imageSrc : undefined
if (!design) { if (!design) {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<View className='checkout-page'> <View className='checkout-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}> <View className='page-header surface-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
<Text className='page-title'></Text> <Text className='page-title'></Text>
</View> </View>
</View> </View>
@@ -71,7 +72,7 @@ export default function CheckoutPage() {
} }
const maskStyle: any = { width: 300, height: 420 } const maskStyle: any = { width: 300, height: 420 }
if (design.designData?.category?.mask) { if (design.designData && design.designData.category && design.designData.category.mask) {
const m = design.designData.category.mask const m = design.designData.category.mask
maskStyle.width = m.width maskStyle.width = m.width
maskStyle.height = m.height maskStyle.height = m.height
@@ -84,8 +85,7 @@ export default function CheckoutPage() {
<ThemedPageMeta /> <ThemedPageMeta />
<View className='checkout-page'> <View className='checkout-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}> <View className='page-header surface-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
<View className='star-badge' />
<View className='flex-between' style={{ width: '100%' }}> <View className='flex-between' style={{ width: '100%' }}>
<Text className='back-btn' onTap={() => Taro.navigateBack()}></Text> <Text className='back-btn' onTap={() => Taro.navigateBack()}></Text>
<Text className='page-title'></Text> <Text className='page-title'></Text>
@@ -94,8 +94,7 @@ export default function CheckoutPage() {
</View> </View>
{/* 效果展示区 */} {/* 效果展示区 */}
<View className='checkout-canvas dashed-card mt-20'> <View className='checkout-canvas surface-card mt-20'>
<View className='star-badge' />
<View className='canvas-area' style={maskStyle}> <View className='canvas-area' style={maskStyle}>
{hasStickers ? ( {hasStickers ? (
design.designData.stickers.map((s: any) => ( design.designData.stickers.map((s: any) => (
@@ -116,7 +115,7 @@ export default function CheckoutPage() {
className='checkout-image' className='checkout-image'
src={design.designData.imageSrc} src={design.designData.imageSrc}
style={{ style={{
transform: `translate(${design.designData.imagePos?.x || 0}px, ${design.designData.imagePos?.y || 0}px) scale(${design.designData.imagePos?.scale || 1})` transform: `translate(${design.designData.imagePos ? design.designData.imagePos.x || 0 : 0}px, ${design.designData.imagePos ? design.designData.imagePos.y || 0 : 0}px) scale(${design.designData.imagePos ? design.designData.imagePos.scale || 1 : 1})`
}} }}
mode='aspectFit' mode='aspectFit'
/> />
@@ -125,8 +124,7 @@ export default function CheckoutPage() {
</View> </View>
{/* 商品信息 */} {/* 商品信息 */}
<View className='checkout-info dashed-card mt-20'> <View className='checkout-info surface-card mt-20'>
<View className='star-badge' />
<View className='info-row'> <View className='info-row'>
<Text className='info-label'></Text> <Text className='info-label'></Text>
<Text className='info-value'>{design.productName}</Text> <Text className='info-value'>{design.productName}</Text>
@@ -146,8 +144,7 @@ export default function CheckoutPage() {
</View> </View>
{/* 收货地址 */} {/* 收货地址 */}
<View className='checkout-address-card dashed-card mt-20'> <View className='checkout-address-card surface-card mt-20'>
<View className='star-badge' />
<View className='checkout-address-header'> <View className='checkout-address-header'>
<Text className='info-label'></Text> <Text className='info-label'></Text>
<View className='address-change' onTap={() => setShowAddrPicker(true)}> <View className='address-change' onTap={() => setShowAddrPicker(true)}>
@@ -161,7 +158,7 @@ export default function CheckoutPage() {
<Text className='addr-phone'>{address.phone}</Text> <Text className='addr-phone'>{address.phone}</Text>
</View> </View>
<Text className='addr-detail'> <Text className='addr-detail'>
{address.region?.join(' ')} {address.detail} {address.region ? address.region.join(' ') : ''} {address.detail}
</Text> </Text>
</View> </View>
) : ( ) : (
@@ -172,21 +169,21 @@ export default function CheckoutPage() {
</View> </View>
{/* 底部操作 */} {/* 底部操作 */}
<View className='checkout-actions'> <View className='action-bar'>
<View className='btn-outline' onTap={() => Taro.navigateBack()}> <View className='btn-secondary' onTap={() => Taro.navigateBack()}>
<Text></Text> <Text></Text>
</View> </View>
<View className='btn-gradient' onTap={handleConfirm}> <View className='btn-primary' onTap={handleConfirm}>
<Text></Text> <Text></Text>
</View> </View>
</View> </View>
<View style={{ height: '40px' }} /> <View className='safe-bottom-placeholder' />
{/* 地址选择底部弹窗 */} {/* 地址选择底部弹窗 */}
{showAddrPicker && ( {showAddrPicker && (
<View className='modal-overlay' style={{ alignItems: 'flex-end', justifyContent: 'flex-end' }}> <View className='modal-overlay' style={{ alignItems: 'flex-end', justifyContent: 'flex-end' }}>
<View className='addr-picker-sheet dashed-card'> <View className='addr-picker-sheet surface-card'>
<View className='addr-picker-header'> <View className='addr-picker-header'>
<Text className='addr-picker-title'></Text> <Text className='addr-picker-title'></Text>
<Text className='addr-picker-close' onTap={() => setShowAddrPicker(false)}></Text> <Text className='addr-picker-close' onTap={() => setShowAddrPicker(false)}></Text>
@@ -195,7 +192,7 @@ export default function CheckoutPage() {
{addrList.map((addr) => ( {addrList.map((addr) => (
<View <View
key={addr.id} key={addr.id}
className={`addr-picker-item ${address?.id === addr.id ? 'active' : ''}`} className={`addr-picker-item ${address && address.id === addr.id ? 'active' : ''}`}
onTap={() => handlePickAddress(addr)} onTap={() => handlePickAddress(addr)}
> >
<View className='addr-picker-row'> <View className='addr-picker-row'>
@@ -204,7 +201,7 @@ export default function CheckoutPage() {
{addr.isDefault && <Text className='addr-picker-default'></Text>} {addr.isDefault && <Text className='addr-picker-default'></Text>}
</View> </View>
<Text className='addr-picker-detail'> <Text className='addr-picker-detail'>
{addr.region?.join(' ')} {addr.detail} {addr.region ? addr.region.join(' ') : ''} {addr.detail}
</Text> </Text>
</View> </View>
))} ))}
+33 -31
View File
@@ -12,7 +12,7 @@
.canvas-area { .canvas-area {
position: relative; position: relative;
background: linear-gradient(135deg, rgba(255, 154, 158, 0.08) 0%, rgba(160, 196, 255, 0.08) 100%); background: var(--bg-input);
overflow: hidden; overflow: hidden;
border-radius: 16px; border-radius: 16px;
min-width: 200px; min-width: 200px;
@@ -27,7 +27,7 @@
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
border: 3px dashed var(--accent-blue); border: 1rpx solid var(--border-strong);
pointer-events: none; pointer-events: none;
z-index: 2; z-index: 2;
} }
@@ -41,12 +41,12 @@
} }
.canvas-image.active { .canvas-image.active {
border-color: var(--accent-pink); border-color: var(--accent-primary);
} }
.canvas-image.overlap { .canvas-image.overlap {
border-color: #ff4757; border-color: var(--danger);
box-shadow: 0 0 12px rgba(255, 71, 87, 0.5); box-shadow: 0 0 12px var(--danger-soft);
} }
/* 贴纸面板 */ /* 贴纸面板 */
@@ -64,19 +64,19 @@
position: relative; position: relative;
width: 120px; width: 120px;
height: 120px; height: 120px;
border: 2px dashed var(--line-star); border: 1rpx solid var(--border);
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
background: var(--bg-input); background: var(--bg-input);
} }
.sticker-thumb.active { .sticker-thumb.active {
border-color: var(--accent-pink); border-color: var(--accent-primary);
border-width: 3px; border-width: 2rpx;
} }
.sticker-thumb.overlap { .sticker-thumb.overlap {
border-color: #ff4757; border-color: var(--danger);
} }
.sticker-thumb-img { .sticker-thumb-img {
@@ -90,7 +90,7 @@
right: 4px; right: 4px;
width: 36px; width: 36px;
height: 36px; height: 36px;
background: rgba(255, 71, 87, 0.85); background: var(--danger);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -107,7 +107,7 @@
.sticker-add-btn { .sticker-add-btn {
width: 120px; width: 120px;
height: 120px; height: 120px;
border: 2px dashed var(--line-star); border: 1rpx solid var(--border);
border-radius: 12px; border-radius: 12px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -118,7 +118,7 @@
.add-icon { .add-icon {
font-size: 48px; font-size: 48px;
color: var(--accent-pink); color: var(--accent-primary);
line-height: 1; line-height: 1;
} }
@@ -132,7 +132,7 @@
display: block; display: block;
margin-top: 16px; margin-top: 16px;
font-size: 26px; font-size: 26px;
color: #ff4757; color: var(--danger);
font-weight: 600; font-weight: 600;
text-align: center; text-align: center;
} }
@@ -162,8 +162,8 @@
.tool-btn { .tool-btn {
width: 56px; width: 56px;
height: 56px; height: 56px;
background: linear-gradient(135deg, rgba(255, 154, 158, 0.08) 0%, rgba(160, 196, 255, 0.08) 100%); background: var(--bg-input);
border: 2px dashed var(--accent-blue); border: 1rpx solid var(--border-strong);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -176,7 +176,7 @@
.tool-value { .tool-value {
font-size: 28px; font-size: 28px;
font-weight: 600; font-weight: 600;
color: var(--accent-pink); color: var(--accent-primary);
min-width: 80px; min-width: 80px;
text-align: center; text-align: center;
} }
@@ -219,11 +219,11 @@
} }
.preview-canvas { .preview-canvas {
background: linear-gradient(135deg, rgba(255, 154, 158, 0.08) 0%, rgba(160, 196, 255, 0.08) 100%); background: var(--bg-input);
border-radius: 16px; border-radius: 16px;
overflow: hidden; overflow: hidden;
margin: 0 auto 24px; margin: 0 auto 24px;
border: 3px dashed var(--accent-blue); border: 1rpx solid var(--border-strong);
position: relative; position: relative;
min-width: 200px; min-width: 200px;
min-height: 200px; min-height: 200px;
@@ -239,7 +239,7 @@
} }
.preview-image.overlap { .preview-image.overlap {
border: 2px solid #ff4757; border: 2px solid var(--danger);
} }
.product-info { .product-info {
@@ -265,13 +265,13 @@
gap: 20px; gap: 20px;
} }
.preview-actions .btn-outline, .preview-actions .btn-secondary,
.preview-actions .btn-gradient { .preview-actions .btn-primary {
flex: 1; flex: 1;
text-align: center; text-align: center;
} }
.preview-actions .btn-gradient.disabled { .preview-actions .btn-primary.disabled {
opacity: 0.5; opacity: 0.5;
pointer-events: none; pointer-events: none;
} }
@@ -290,14 +290,14 @@
text-align: center; text-align: center;
background: var(--bg-input); background: var(--bg-input);
color: var(--text-secondary); color: var(--text-secondary);
border: 2px dashed var(--text-muted); border: 1rpx solid var(--border-strong);
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.edit-btn.active { .edit-btn.active {
background: linear-gradient(135deg, rgba(255, 154, 158, 0.12) 0%, rgba(160, 196, 255, 0.12) 100%); background: var(--accent-primary-soft);
color: var(--accent-pink); color: var(--accent-primary);
border-color: var(--accent-pink); border-color: var(--accent-primary);
} }
.edit-btn.disabled { .edit-btn.disabled {
@@ -305,11 +305,13 @@
pointer-events: none; pointer-events: none;
} }
/* 按钮 */ /* 底部操作栏 — 毛玻璃规则由全局 .action-bar 提供 */
.action-btns { .action-bar .btn-primary,
display: flex; .action-bar .btn-secondary {
flex-direction: column; flex: 1;
gap: 20px; text-align: center;
padding: 24px 0;
font-size: 28px;
} }
.mt-20 { .mt-20 {
+27 -30
View File
@@ -25,22 +25,23 @@ export default function DIYPage() {
const [hasOverlap, setHasOverlap] = useState(false) const [hasOverlap, setHasOverlap] = useState(false)
useEffect(() => { useEffect(() => {
const params = Taro.getCurrentInstance().router?.params const router = Taro.getCurrentInstance().router
const source = params?.source const params = router ? router.params : undefined
const productId = params?.productId || params?.category const source = params ? params.source : undefined
const productId = (params && params.productId) || (params && params.category)
if (source === 'product' && productId) { if (source === 'product' && productId) {
const product = getProductById(productId) const product = getProductById(productId)
if (product) { if (product) {
setCategory(product) setCategory(product)
setQuantity(Number(params?.quantity) || 1) setQuantity(Number(params ? params.quantity : undefined) || 1)
const newDesign: DesignItem = { const newDesign: DesignItem = {
id: 'DSG' + Date.now(), id: 'DSG' + Date.now(),
productId: product.id, productId: product.id,
productName: product.name, productName: product.name,
productIcon: product.icon, productIcon: product.icon,
unitPrice: product.price, unitPrice: product.price,
count: Number(params?.quantity) || 1, count: Number(params ? params.quantity : undefined) || 1,
status: 'designing', status: 'designing',
createdAt: new Date().toISOString().slice(0, 10) createdAt: new Date().toISOString().slice(0, 10)
} }
@@ -51,7 +52,7 @@ export default function DIYPage() {
return return
} }
if (source === 'designList' && params?.designId) { if (source === 'designList' && params && params.designId) {
const dId = params.designId const dId = params.designId
const list = getDesignList() const list = getDesignList()
const design = list.find(d => d.id === dId) const design = list.find(d => d.id === dId)
@@ -60,16 +61,16 @@ export default function DIYPage() {
setQuantity(design.count) setQuantity(design.count)
const product = getProductById(design.productId) const product = getProductById(design.productId)
if (product) setCategory(product) if (product) setCategory(product)
if (design.designData?.stickers) { if (design.designData ? design.designData.stickers : undefined) {
setStickers(design.designData.stickers) setStickers(design.designData.stickers)
} else if (design.designData?.imageSrc) { } else if (design.designData ? design.designData.imageSrc : undefined) {
// 兼容旧版数据 // 兼容旧版数据
const s: StickerItem = { const s: StickerItem = {
id: 'legacy_' + Date.now(), id: 'legacy_' + Date.now(),
src: design.designData.imageSrc, src: design.designData.imageSrc,
x: design.designData.imagePos?.x || 0, x: design.designData.imagePos ? design.designData.imagePos.x || 0 : 0,
y: design.designData.imagePos?.y || 0, y: design.designData.imagePos ? design.designData.imagePos.y || 0 : 0,
scale: design.designData.imagePos?.scale || 1, scale: design.designData.imagePos ? design.designData.imagePos.scale || 1 : 1,
width: 200, width: 200,
height: 200, height: 200,
isOverlapping: false isOverlapping: false
@@ -255,6 +256,9 @@ export default function DIYPage() {
Taro.navigateTo({ url: `/pages/checkout/index?designId=${designId}` }) 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) { if (!category) {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
@@ -271,8 +275,7 @@ export default function DIYPage() {
<ThemedPageMeta /> <ThemedPageMeta />
<View className='diy-page'> <View className='diy-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}> <View className='page-header surface-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
<View className='star-badge' />
<View className='flex-between' style={{ width: '100%' }}> <View className='flex-between' style={{ width: '100%' }}>
<Text className='back-btn' onTap={goBack}></Text> <Text className='back-btn' onTap={goBack}></Text>
<Text className='page-title'></Text> <Text className='page-title'></Text>
@@ -281,8 +284,7 @@ export default function DIYPage() {
</View> </View>
{/* 画布区域 — catchtouchmove 阻止事件冒泡导致页面滚动 */} {/* 画布区域 — catchtouchmove 阻止事件冒泡导致页面滚动 */}
<View className='canvas-wrapper dashed-card mt-20' catchMove> <View className='canvas-wrapper surface-card mt-20' catchMove>
<View className='star-badge' />
<View className='canvas-area' style={getMaskStyle()}> <View className='canvas-area' style={getMaskStyle()}>
{stickers.map(s => ( {stickers.map(s => (
<Image <Image
@@ -308,8 +310,7 @@ export default function DIYPage() {
</View> </View>
{/* 贴纸列表与控制 */} {/* 贴纸列表与控制 */}
<View className='sticker-panel dashed-card mt-20'> <View className='sticker-panel surface-card mt-20'>
<View className='star-badge' />
<Text className='section-title'></Text> <Text className='section-title'></Text>
<View className='sticker-list'> <View className='sticker-list'>
{stickers.map(s => ( {stickers.map(s => (
@@ -330,15 +331,12 @@ export default function DIYPage() {
{/* 控制工具栏 */} {/* 控制工具栏 */}
{activeStickerId && ( {activeStickerId && (
<View className='toolbar dashed-card mt-20'> <View className='toolbar surface-card mt-20'>
<View className='star-badge' />
<View className='tool-row'> <View className='tool-row'>
<Text className='tool-label'></Text> <Text className='tool-label'></Text>
<View className='flex-center' style={{ gap: '20px' }}> <View className='flex-center' style={{ gap: '20px' }}>
<View className='tool-btn' onTap={() => handleScale(activeStickerId, -0.1)}></View> <View className='tool-btn' onTap={() => handleScale(activeStickerId, -0.1)}></View>
<Text className='tool-value'> <Text className='tool-value'>{Math.round(activeScale * 100)}%</Text>
{Math.round((stickers.find(s => s.id === activeStickerId)?.scale || 1) * 100)}%
</Text>
<View className='tool-btn' onTap={() => handleScale(activeStickerId, 0.1)}></View> <View className='tool-btn' onTap={() => handleScale(activeStickerId, 0.1)}></View>
</View> </View>
</View> </View>
@@ -360,11 +358,11 @@ export default function DIYPage() {
</View> </View>
{/* 底部操作按钮 */} {/* 底部操作按钮 */}
<View className='action-btns mt-20'> <View className='action-bar'>
<View className='btn-gradient' onTap={() => setPreviewMode(true)}> <View className='btn-primary' onTap={() => setPreviewMode(true)}>
<Text></Text> <Text></Text>
</View> </View>
<View className='btn-outline' onTap={addSticker}> <View className='btn-secondary' onTap={addSticker}>
<Text></Text> <Text></Text>
</View> </View>
</View> </View>
@@ -372,8 +370,7 @@ export default function DIYPage() {
{/* 预览弹层 */} {/* 预览弹层 */}
{previewMode && ( {previewMode && (
<View className='preview-overlay'> <View className='preview-overlay'>
<View className='preview-card dashed-card'> <View className='preview-card surface-card'>
<View className='star-badge' />
<Text className='preview-title'></Text> <Text className='preview-title'></Text>
<View className='preview-canvas' style={getMaskStyle()}> <View className='preview-canvas' style={getMaskStyle()}>
{stickers.map(s => ( {stickers.map(s => (
@@ -396,10 +393,10 @@ export default function DIYPage() {
{hasOverlap && <Text className='overlap-hint'></Text>} {hasOverlap && <Text className='overlap-hint'></Text>}
</View> </View>
<View className='preview-actions'> <View className='preview-actions'>
<View className='btn-outline' onTap={() => setPreviewMode(false)}> <View className='btn-secondary' onTap={() => setPreviewMode(false)}>
<Text></Text> <Text></Text>
</View> </View>
<View className={`btn-gradient ${hasOverlap ? 'disabled' : ''}`} onTap={handleComplete}> <View className={`btn-primary ${hasOverlap ? 'disabled' : ''}`} onTap={handleComplete}>
<Text></Text> <Text></Text>
</View> </View>
</View> </View>
@@ -407,7 +404,7 @@ export default function DIYPage() {
</View> </View>
)} )}
<View style={{ height: '40px' }} /> <View className='safe-bottom-placeholder' />
</View> </View>
</View> </View>
) )
+10 -16
View File
@@ -28,7 +28,7 @@
.edit-save { .edit-save {
font-size: 28px; font-size: 28px;
color: var(--accent-pink); color: var(--accent-primary);
font-weight: 600; font-weight: 600;
width: 60px; width: 60px;
text-align: right; text-align: right;
@@ -93,7 +93,7 @@
.slider-fill { .slider-fill {
height: 100%; height: 100%;
border-radius: 4px; border-radius: 4px;
background: linear-gradient(90deg, #ff9a9e, #fecfef); background: linear-gradient(90deg, var(--accent-primary), var(--accent-primary-soft));
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
@@ -104,7 +104,7 @@
height: 32px; height: 32px;
border-radius: 50%; border-radius: 50%;
background: #fff; background: #fff;
border: 4px solid var(--accent-pink); border: 4px solid var(--accent-primary);
position: absolute; position: absolute;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
@@ -132,7 +132,7 @@
font-weight: 500; font-weight: 500;
background: var(--bg-input); background: var(--bg-input);
color: var(--text-primary); color: var(--text-primary);
border: 2px dashed var(--line-star); border: 1rpx solid var(--border);
transition: all 0.2s; transition: all 0.2s;
} }
@@ -142,20 +142,14 @@
} }
.quick-btn.active { .quick-btn.active {
background: rgba(255, 154, 158, 0.12); background: var(--accent-primary-soft);
border-color: var(--accent-pink); border-color: var(--accent-primary);
color: var(--accent-pink); color: var(--accent-primary);
} }
/* 底部操作 */ /* 底部操作栏 — 毛玻璃规则由全局 .action-bar 提供 */
.edit-footer { .edit-footer .btn-primary,
padding: 20px 0 calc(20px + env(safe-area-inset-bottom)); .edit-footer .btn-secondary {
display: flex;
gap: 20px;
}
.edit-footer .btn-gradient,
.edit-footer .btn-outline {
flex: 1; flex: 1;
text-align: center; text-align: center;
padding: 24px 0; padding: 24px 0;
+32 -17
View File
@@ -45,13 +45,15 @@ function TouchSlider({ value, min, max, step = 1, onChange, format }: SliderProp
const handleTouchStart = (e: any) => { const handleTouchStart = (e: any) => {
setDragging(true) setDragging(true)
const x = e.touches?.[0]?.clientX ?? 0 const touch = e.touches && e.touches[0]
const x = touch ? touch.clientX : 0
onChange(computedFromClientX(x)) onChange(computedFromClientX(x))
} }
const handleTouchMove = (e: any) => { const handleTouchMove = (e: any) => {
if (!dragging) return if (!dragging) return
const x = e.touches?.[0]?.clientX ?? 0 const touch = e.touches && e.touches[0]
const x = touch ? touch.clientX : 0
onChange(computedFromClientX(x)) onChange(computedFromClientX(x))
} }
@@ -106,14 +108,15 @@ export default function StickerEditPage() {
query.select('#editCanvas') query.select('#editCanvas')
.fields({ node: true, size: true }) .fields({ node: true, size: true })
.exec((res: any) => { .exec((res: any) => {
const canvas = res?.[0]?.node const resFirst = res && res[0]
const canvas = resFirst ? resFirst.node : undefined
if (!canvas) { if (!canvas) {
setTimeout(() => initCanvas(st), 300) setTimeout(() => initCanvas(st), 300)
return return
} }
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
let cw = res[0]?.width || 300 let cw = (res[0] && res[0].width) || 300
let ch = res[0]?.height || 400 let ch = (res[0] && res[0].height) || 400
if (cw <= 0) cw = 300 if (cw <= 0) cw = 300
if (ch <= 0) ch = 400 if (ch <= 0) ch = 400
const dpr = Taro.getSystemInfoSync().pixelRatio || 1 const dpr = Taro.getSystemInfoSync().pixelRatio || 1
@@ -130,20 +133,21 @@ export default function StickerEditPage() {
}, [canvasReady]) }, [canvasReady])
useEffect(() => { useEffect(() => {
const params = Taro.getCurrentInstance().router?.params const router = Taro.getCurrentInstance().router
const dId = params?.designId as string const params = router ? router.params : undefined
const sId = params?.stickerId as string const dId = params ? params.designId : ''
const sId = params ? params.stickerId : ''
setDesignId(dId) setDesignId(dId)
setStickerId(sId) setStickerId(sId)
const list = getDesignList() const list = getDesignList()
const design = list.find(d => d.id === dId) const design = list.find(d => d.id === dId)
const st = design?.designData?.stickers?.find(s => s.id === sId) const st = design && design.designData && design.designData.stickers ? design.designData.stickers.find(s => s.id === sId) : undefined
if (st) { if (st) {
setSticker(st) setSticker(st)
setBrightness(st.edits?.brightness || 0) setBrightness((st.edits && st.edits.brightness) || 0)
setHue(st.edits?.hue || 0) setHue((st.edits && st.edits.hue) || 0)
setContrast(st.edits?.contrast || 0) setContrast((st.edits && st.edits.contrast) || 0)
initCanvas(st) initCanvas(st)
} }
}, [initCanvas]) }, [initCanvas])
@@ -171,9 +175,9 @@ export default function StickerEditPage() {
const y = (ch - h) / 2 const y = (ch - h) / 2
// 应用滤镜(小程序 Canvas 2D 支持 filter 属性 2.16.0+ // 应用滤镜(小程序 Canvas 2D 支持 filter 属性 2.16.0+
const b = opts?.brightness ?? brightness const b = opts ? opts.brightness : brightness
const hVal = opts?.hue ?? hue const hVal = opts ? opts.hue : hue
const c = opts?.contrast ?? contrast const c = opts ? opts.contrast : contrast
if (b !== 0 || hVal !== 0 || c !== 0) { if (b !== 0 || hVal !== 0 || c !== 0) {
ctx.filter = `brightness(${100 + b}%) hue-rotate(${hVal}deg) contrast(${100 + c}%)` ctx.filter = `brightness(${100 + b}%) hue-rotate(${hVal}deg) contrast(${100 + c}%)`
} else { } else {
@@ -217,7 +221,7 @@ export default function StickerEditPage() {
const dList = getDesignList() const dList = getDesignList()
const dIdx = dList.findIndex(d => d.id === designId) const dIdx = dList.findIndex(d => d.id === designId)
if (dIdx === -1) { setLoading(false); return } if (dIdx === -1) { setLoading(false); return }
const stickers = dList[dIdx].designData?.stickers || [] const stickers = dList[dIdx].designData ? dList[dIdx].designData.stickers || [] : []
const sIdx = stickers.findIndex(s => s.id === stickerId) const sIdx = stickers.findIndex(s => s.id === stickerId)
if (sIdx === -1) { setLoading(false); return } if (sIdx === -1) { setLoading(false); return }
stickers[sIdx] = { stickers[sIdx] = {
@@ -325,7 +329,7 @@ export default function StickerEditPage() {
</View> </View>
{/* 快速操作 */} {/* 快速操作 */}
<View className='edit-tools dashed-card mt-20'> <View className='edit-tools surface-card mt-20'>
<View className='edit-row'> <View className='edit-row'>
<Text className='edit-label'></Text> <Text className='edit-label'></Text>
<View className='edit-quick-actions'> <View className='edit-quick-actions'>
@@ -411,6 +415,17 @@ export default function StickerEditPage() {
<View style={{ flex: 1 }} /> <View style={{ flex: 1 }} />
<View className='action-bar edit-footer'>
<View className='btn-secondary' onTap={() => Taro.navigateBack()}>
<Text></Text>
</View>
<View className='btn-primary' onTap={handleSave}>
<Text></Text>
</View>
</View>
<View className='safe-bottom-placeholder' />
{loading && ( {loading && (
<View className='edit-loading'> <View className='edit-loading'>
<Text className='edit-loading-text'></Text> <Text className='edit-loading-text'></Text>
+8 -8
View File
@@ -1,6 +1,6 @@
.product-page { .product-page {
padding: 0 24px 24px; padding: 0 24px 24px;
padding-bottom: calc(24px + 160px); /* 为底部 action-bar 留空 */ padding-bottom: calc(24px + 200px); /* 为底部毛玻璃 action-bar 留空 */
} }
/* 返回按钮 */ /* 返回按钮 */
@@ -61,7 +61,7 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 20px 0; padding: 20px 0;
border-bottom: 1px dashed rgba(0,0,0,0.06); border-bottom: 1rpx solid var(--border);
} }
.info-row:last-child { .info-row:last-child {
@@ -76,7 +76,7 @@
.info-price { .info-price {
font-size: 40px; font-size: 40px;
font-weight: 800; font-weight: 800;
color: var(--accent-pink); color: var(--accent-primary);
} }
.info-value { .info-value {
@@ -109,7 +109,7 @@
.summary-price { .summary-price {
font-size: 36px; font-size: 36px;
font-weight: 700; font-weight: 700;
color: var(--accent-pink); color: var(--accent-primary);
} }
/* 弹窗 */ /* 弹窗 */
@@ -211,7 +211,7 @@
justify-content: space-between; justify-content: space-between;
margin-bottom: 32px; margin-bottom: 32px;
padding-top: 24px; padding-top: 24px;
border-top: 1px dashed rgba(0,0,0,0.06); border-top: 1rpx solid var(--border);
} }
.total-label { .total-label {
@@ -222,7 +222,7 @@
.total-price { .total-price {
font-size: 36px; font-size: 36px;
font-weight: 800; font-weight: 800;
color: var(--accent-pink); color: var(--accent-primary);
} }
.modal-actions { .modal-actions {
@@ -230,8 +230,8 @@
gap: 20px; gap: 20px;
} }
.modal-actions .btn-outline, .modal-actions .btn-secondary,
.modal-actions .btn-gradient { .modal-actions .btn-primary {
flex: 1; flex: 1;
padding: 20px 0; padding: 20px 0;
} }
+17 -22
View File
@@ -10,7 +10,7 @@ import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import { getProductIconImg } from '../../utils/productConfig' import { getProductIconImg } from '../../utils/productConfig'
const HERO_COLORS = ['#FFE4EC', '#FFF0F5', '#FCE4EC'] const HERO_COLORS = ['var(--accent-primary-soft)', 'var(--bg-muted)', 'var(--accent-secondary-soft)']
export default function ProductPage() { export default function ProductPage() {
const { theme, resolvedTheme } = useThemeContext() const { theme, resolvedTheme } = useThemeContext()
@@ -19,8 +19,9 @@ export default function ProductPage() {
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const [quantity, setQuantity] = useState(1) const [quantity, setQuantity] = useState(1)
const params = Taro.getCurrentInstance().router?.params const router = Taro.getCurrentInstance().router
const productId = params?.id || '' const params = router ? router.params : undefined
const productId = params ? params.id : ''
const product = getProductById(productId) const product = getProductById(productId)
if (!product) { if (!product) {
@@ -28,7 +29,7 @@ export default function ProductPage() {
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<View className='product-page'> <View className='product-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}> <View className='page-header surface-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
<Text className='page-title'></Text> <Text className='page-title'></Text>
</View> </View>
</View> </View>
@@ -63,8 +64,7 @@ export default function ProductPage() {
<ThemedPageMeta /> <ThemedPageMeta />
<View className='product-page'> <View className='product-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}> <View className='page-header surface-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
<View className='star-badge' />
<View className='flex-between'> <View className='flex-between'>
<Text className='back-btn' onTap={() => Taro.navigateBack()}></Text> <Text className='back-btn' onTap={() => Taro.navigateBack()}></Text>
<Text className='page-title'></Text> <Text className='page-title'></Text>
@@ -81,14 +81,13 @@ export default function ProductPage() {
interval={3000} interval={3000}
duration={500} duration={500}
indicatorDots indicatorDots
indicatorColor='rgba(0,0,0,0.2)' indicatorColor='#e0d5c8'
indicatorActiveColor='#ff9a9e' indicatorActiveColor='#d96c6a'
> >
{product.images && product.images.length > 0 ? ( {product.images && product.images.length > 0 ? (
product.images.map((img, idx) => ( product.images.map((img, idx) => (
<SwiperItem key={idx}> <SwiperItem key={idx}>
<View className='hero-card dashed-card'> <View className='hero-card surface-card'>
<View className='star-badge' />
<Image className='hero-image' src={img} mode='aspectFit' /> <Image className='hero-image' src={img} mode='aspectFit' />
</View> </View>
</SwiperItem> </SwiperItem>
@@ -96,8 +95,7 @@ export default function ProductPage() {
) : ( ) : (
HERO_COLORS.map((color, idx) => ( HERO_COLORS.map((color, idx) => (
<SwiperItem key={idx}> <SwiperItem key={idx}>
<View className='hero-card dashed-card' style={{ background: color }}> <View className='hero-card surface-card' style={{ background: color }}>
<View className='star-badge' />
<Image className='hero-icon-img' src={getProductIconImg(product)} mode='aspectFit' /> <Image className='hero-icon-img' src={getProductIconImg(product)} mode='aspectFit' />
<Text className='hero-name'>{product.name}</Text> <Text className='hero-name'>{product.name}</Text>
</View> </View>
@@ -108,8 +106,7 @@ export default function ProductPage() {
</View> </View>
{/* 基本信息 */} {/* 基本信息 */}
<View className='info-section dashed-card mt-20'> <View className='info-section surface-card mt-20'>
<View className='star-badge' />
<View className='info-row'> <View className='info-row'>
<Text className='info-label'></Text> <Text className='info-label'></Text>
<Text className='info-price'>¥{product.price}</Text> <Text className='info-price'>¥{product.price}</Text>
@@ -125,8 +122,7 @@ export default function ProductPage() {
</View> </View>
{/* 详细介绍 */} {/* 详细介绍 */}
<View className='detail-section dashed-card mt-20'> <View className='detail-section surface-card mt-20'>
<View className='star-badge' />
<Text className='section-title'></Text> <Text className='section-title'></Text>
<Text className='detail-text'>{product.description}</Text> <Text className='detail-text'>{product.description}</Text>
</View> </View>
@@ -137,10 +133,10 @@ export default function ProductPage() {
<Text className='summary-label'></Text> <Text className='summary-label'></Text>
<Text className='summary-price'>¥{product.price}</Text> <Text className='summary-price'>¥{product.price}</Text>
</View> </View>
<View className='btn-outline' onTap={handleAddToList}> <View className='btn-secondary' onTap={handleAddToList}>
<Text></Text> <Text></Text>
</View> </View>
<View className='btn-gradient' onTap={goToDIY}> <View className='btn-primary' onTap={goToDIY}>
<Text></Text> <Text></Text>
</View> </View>
</View> </View>
@@ -151,8 +147,7 @@ export default function ProductPage() {
{/* 数量选择弹窗 */} {/* 数量选择弹窗 */}
{showModal && ( {showModal && (
<View className='modal-overlay'> <View className='modal-overlay'>
<View className='modal-card dashed-card'> <View className='modal-card surface-card'>
<View className='star-badge' />
<Text className='modal-title'></Text> <Text className='modal-title'></Text>
<View className='modal-product'> <View className='modal-product'>
<Image className='modal-icon-img' src={getProductIconImg(product)} mode='aspectFit' /> <Image className='modal-icon-img' src={getProductIconImg(product)} mode='aspectFit' />
@@ -171,10 +166,10 @@ export default function ProductPage() {
<Text className='total-price'>¥{(product.price * quantity).toFixed(2)}</Text> <Text className='total-price'>¥{(product.price * quantity).toFixed(2)}</Text>
</View> </View>
<View className='modal-actions'> <View className='modal-actions'>
<View className='btn-outline' onTap={() => setShowModal(false)}> <View className='btn-secondary' onTap={() => setShowModal(false)}>
<Text></Text> <Text></Text>
</View> </View>
<View className='btn-gradient' onTap={confirmAdd}> <View className='btn-primary' onTap={confirmAdd}>
<Text></Text> <Text></Text>
</View> </View>
</View> </View>
+12 -10
View File
@@ -87,7 +87,7 @@
.shop-hero-price { .shop-hero-price {
font-size: 44rpx; font-size: 44rpx;
font-weight: 700; font-weight: 700;
color: #ff9a9e; color: var(--accent-primary);
} }
.shop-hero-original { .shop-hero-original {
@@ -146,7 +146,7 @@
.shop-band-price { .shop-band-price {
font-size: 48rpx; font-size: 48rpx;
font-weight: 700; font-weight: 700;
color: #ff9a9e; color: var(--accent-primary);
} }
.shop-band-original { .shop-band-original {
@@ -183,7 +183,7 @@
bottom: 6rpx; bottom: 6rpx;
width: 6rpx; width: 6rpx;
border-radius: 4rpx; border-radius: 4rpx;
background: linear-gradient(180deg, #ff9a9e, #fecfef); background: linear-gradient(180deg, var(--accent-primary), var(--accent-primary-soft));
} }
.shop-section-text { .shop-section-text {
@@ -212,15 +212,15 @@
border-radius: 40rpx; border-radius: 40rpx;
font-size: 22rpx; font-size: 22rpx;
font-weight: 500; font-weight: 500;
background: rgba(255, 154, 158, 0.12); background: var(--accent-primary-soft);
border: 1rpx solid rgba(255, 154, 158, 0.2); border: 1rpx solid var(--accent-primary);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.shop-tag-text { .shop-tag-text {
color: #ff9a9e; color: var(--accent-primary);
} }
/* --- Spec Table --- */ /* --- Spec Table --- */
@@ -272,7 +272,7 @@
backdrop-filter: blur(16rpx) saturate(1.4); backdrop-filter: blur(16rpx) saturate(1.4);
-webkit-backdrop-filter: blur(16rpx) saturate(1.4); -webkit-backdrop-filter: blur(16rpx) saturate(1.4);
border: 1rpx solid rgba(255,255,255,0.12); border: 1rpx solid rgba(255,255,255,0.12);
box-shadow: 0 2rpx 16rpx rgba(0,0,0,0.25); box-shadow: var(--shadow-card);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -303,6 +303,7 @@
gap: 24rpx; gap: 24rpx;
backdrop-filter: blur(16rpx) saturate(1.2); backdrop-filter: blur(16rpx) saturate(1.2);
-webkit-backdrop-filter: blur(16rpx) saturate(1.2); -webkit-backdrop-filter: blur(16rpx) saturate(1.2);
box-shadow: var(--shadow-card);
transition: background-color 0.4s ease; transition: background-color 0.4s ease;
} }
@@ -315,7 +316,7 @@
.shop-cta-price { .shop-cta-price {
font-size: 40rpx; font-size: 40rpx;
font-weight: 700; font-weight: 700;
color: #ff9a9e; color: var(--accent-primary);
} }
.shop-cta-original { .shop-cta-original {
@@ -327,8 +328,8 @@
.shop-cta-btn { .shop-cta-btn {
flex: 1; flex: 1;
max-width: 260rpx; max-width: 260rpx;
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%); background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-primary-strong) 99%);
color: #5c3a3a; color: #fff;
border-radius: 50rpx; border-radius: 50rpx;
padding: 24rpx 48rpx; padding: 24rpx 48rpx;
font-size: 30rpx; font-size: 30rpx;
@@ -337,6 +338,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-shadow: var(--shadow-card);
} }
.shop-cta-btn-text { .shop-cta-btn-text {
+6 -5
View File
@@ -21,8 +21,9 @@ export default function ShopDetailPage() {
// 顶部为沉浸式商品图,状态栏始终使用白字。 // 顶部为沉浸式商品图,状态栏始终使用白字。
useStatusBar(resolvedTheme, { mode: 'light' }) useStatusBar(resolvedTheme, { mode: 'light' })
const routerParams = Taro.getCurrentInstance().router?.params const routerParams = Taro.getCurrentInstance().router
const productId = routerParams?.id || '' const routerParamsValues = routerParams ? routerParams.params : undefined
const productId = routerParamsValues ? routerParamsValues.id : ''
const product: ProductCategory | undefined = PRODUCTS.find(p => p.id === productId) const product: ProductCategory | undefined = PRODUCTS.find(p => p.id === productId)
useEffect(() => { useEffect(() => {
@@ -35,7 +36,7 @@ export default function ShopDetailPage() {
}, []) }, [])
const handleScroll = useCallback((e: any) => { const handleScroll = useCallback((e: any) => {
const st = e.detail?.scrollTop ?? 0 const st = (e.detail && e.detail.scrollTop) || 0
const total = winHeight * 1.2 const total = winHeight * 1.2
const p = clamp(st / total, 0, 1) const p = clamp(st / total, 0, 1)
setProgress(p) setProgress(p)
@@ -103,7 +104,7 @@ export default function ShopDetailPage() {
} }
}, [progress]) }, [progress])
const tone = useMemo(() => product?.tone || [28, 22, 18], [product]) const tone = useMemo(() => (product ? product.tone : [28, 22, 18]), [product])
if (!product) { if (!product) {
return ( return (
@@ -129,7 +130,7 @@ export default function ShopDetailPage() {
> >
<Image <Image
className="shop-hero-img" className="shop-hero-img"
src={product.images?.[0] || product.iconImg || ''} src={(product.images && product.images[0]) || product.iconImg || ''}
mode="aspectFill" mode="aspectFill"
style={{ style={{
width: '100%', width: '100%',