fix(diy): 草稿持久化 + 进入工作台即设计中 + 编辑页空态兜底

修正工作流批次①(问题 2+3+4):
- 问题2: 贴纸变化防抖 800ms 落缓存+服务端静默同步,
  useDidHide/卸载前强制 flush;中途退出不再丢布局
- 问题3: 进入工作台(新建/继续)即 PATCH designing(SUBMITTED),
  取代原「确认完成才提交」触发点
- 问题4: 草稿实时写缓存后贴纸编辑页始终能读到 stickers;
  另加 notFound 空态(未找到贴纸提示 + 返回工作台),
  handleEditSticker 跳转前 flushDraft 关闭防抖竞态窗口

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-12 19:07:07 +08:00
co-authored by Claude
parent 395fa4116c
commit eea6c3feef
10 changed files with 123 additions and 10 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+62 -3
View File
@@ -1,6 +1,6 @@
import { View, Text, Image } from '@tarojs/components' import { View, Text, Image } from '@tarojs/components'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { useState, useEffect } from 'react' import { useState, useEffect, useRef } from 'react'
import './index.scss' import './index.scss'
import { getProductById } from '../../utils/productConfig' import { getProductById } from '../../utils/productConfig'
import { getDesignList, setDesignList, updateDesign, type DesignItem, type StickerItem } from '../../utils/store' import { getDesignList, setDesignList, updateDesign, type DesignItem, type StickerItem } from '../../utils/store'
@@ -27,7 +27,7 @@ export default function DIYPage() {
const [previewMode, setPreviewMode] = useState(false) const [previewMode, setPreviewMode] = useState(false)
const [hasOverlap, setHasOverlap] = useState(false) const [hasOverlap, setHasOverlap] = useState(false)
/** 进入 DIY 即创建清单条目:服务端创建(DRAFT,阶段0 决策#6),失败降级本地 */ /** 进入 DIY 即创建清单条目,并推进到「设计中」(问题3 修正:进入工作台即 SUBMITTED),失败降级本地 */
const createDesignEntry = (product: { id: string; name: string; price: number; icon: string }, count: number) => { const createDesignEntry = (product: { id: string; name: string; price: number; icon: string }, count: number) => {
createDesign({ createDesign({
productId: product.id, productId: product.id,
@@ -38,6 +38,13 @@ export default function DIYPage() {
.then(item => { .then(item => {
setDesignList([item, ...getDesignList()]) // 回写缓存,checkout 等页面仍读缓存 setDesignList([item, ...getDesignList()]) // 回写缓存,checkout 等页面仍读缓存
setDesignId(item.id) setDesignId(item.id)
return updateDesignApi(item.id, { status: 'designing' })
.then(updated => {
setDesignList(getDesignList().map(d => d.id === item.id ? { ...d, status: updated.status } : d))
})
.catch(() => {
setDesignList(getDesignList().map(d => d.id === item.id ? { ...d, status: 'designing' as const } : d))
})
}) })
.catch(() => { .catch(() => {
const newDesign: DesignItem = { const newDesign: DesignItem = {
@@ -47,7 +54,7 @@ export default function DIYPage() {
productIcon: product.icon, productIcon: product.icon,
unitPrice: product.price, unitPrice: product.price,
count, count,
status: 'undesigned', status: 'designing',
createdAt: new Date().toISOString().slice(0, 10) createdAt: new Date().toISOString().slice(0, 10)
} }
setDesignList([...getDesignList(), newDesign]) setDesignList([...getDesignList(), newDesign])
@@ -97,6 +104,12 @@ export default function DIYPage() {
} }
setStickers([s]) setStickers([s])
} }
// 问题3 修正:继续设计同样进入「设计中」状态(单向状态机,undesigned → designing
if (design.status === 'undesigned') {
updateDesign(dId, { status: 'designing' })
updateDesignApi(dId, { status: 'designing' }).catch(() => {})
}
} }
return return
} }
@@ -111,6 +124,51 @@ export default function DIYPage() {
} }
}, []) }, [])
// ---------- 草稿持久化(修正工作流 问题2----------
// 贴纸变化防抖 800ms:本地缓存即时同步 + 服务端静默同步;不推状态(SUBMITTED 只由显式动作触发)
const draftTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const latestRef = useRef({ designId: '', quantity: 1, category: null as any, stickers: [] as StickerItem[] })
latestRef.current = { designId, quantity, category, stickers }
const saveDraft = () => {
const { designId: dId, quantity: qty, category: cat, stickers: st } = latestRef.current
if (!dId || !cat) return
// WCD 红线:基于缓存中服务端最新 designData 合并(保留 wordcloud 分组),只覆盖本页编辑字段
const cached = getDesignList().find(d => d.id === dId)
const designData = {
...(cached && cached.designData ? cached.designData : {}),
version: 1 as const,
category: { id: cat.id, mask: cat.mask, ...(cat.tone ? { tone: cat.tone } : {}) },
stickers: st
}
updateDesign(dId, { designData }) // 本地缓存即时同步(问题4:贴纸编辑页从这里读取)
updateDesignApi(dId, {
item: { productId: cat.id, productName: cat.name, unitPrice: cat.price, count: qty, designData }
}).catch(() => {}) // 草稿静默降级:失败仅落缓存
}
const flushDraft = () => {
if (draftTimerRef.current) {
clearTimeout(draftTimerRef.current)
draftTimerRef.current = null
}
saveDraft()
}
// 隐藏页面(切后台/跳转)时强制落盘,防止防抖窗口内的改动丢失
Taro.useDidHide(flushDraft)
// 卸载时同样落盘
useEffect(() => flushDraft, [])
// eslint-disable-next-line react-hooks/exhaustive-deps
const skipDraftRef = useRef(true)
useEffect(() => {
if (skipDraftRef.current) { skipDraftRef.current = false; return }
if (draftTimerRef.current) clearTimeout(draftTimerRef.current)
draftTimerRef.current = setTimeout(saveDraft, 800)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stickers])
// 矩形碰撞检测 // 矩形碰撞检测
const checkOverlap = (list: StickerItem[]) => { const checkOverlap = (list: StickerItem[]) => {
const newList = list.map(s => ({ ...s, isOverlapping: false })) const newList = list.map(s => ({ ...s, isOverlapping: false }))
@@ -217,6 +275,7 @@ export default function DIYPage() {
const handleEditSticker = () => { const handleEditSticker = () => {
if (!activeStickerId || !designId) return if (!activeStickerId || !designId) return
flushDraft() // 问题4:跳编辑页前清掉防抖窗口,缓存里保证有最新贴纸
Taro.navigateTo({ Taro.navigateTo({
url: `/pages/diy/stickerEdit/index?designId=${designId}&stickerId=${activeStickerId}` url: `/pages/diy/stickerEdit/index?designId=${designId}&stickerId=${activeStickerId}`
}) })
+27
View File
@@ -226,3 +226,30 @@
.mt-20 { .mt-20 {
margin-top: 24rpx; margin-top: 24rpx;
} }
/* 空态(贴纸未找到) */
.edit-empty {
margin: 24rpx 24rpx 0;
padding: 60rpx 40rpx;
border-radius: 24rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.edit-empty-title {
font-size: 32rpx;
font-weight: 600;
}
.edit-empty-desc {
font-size: 26rpx;
opacity: 0.65;
text-align: center;
}
.edit-empty .btn-primary {
padding: 16rpx 60rpx;
border-radius: 44rpx;
}
+27
View File
@@ -90,6 +90,7 @@ export default function StickerEditPage() {
const [designId, setDesignId] = useState('') const [designId, setDesignId] = useState('')
const [stickerId, setStickerId] = useState('') const [stickerId, setStickerId] = useState('')
const [sticker, setSticker] = useState<StickerItem | null>(null) const [sticker, setSticker] = useState<StickerItem | null>(null)
const [notFound, setNotFound] = useState(false)
// 编辑参数 // 编辑参数
const [brightness, setBrightness] = useState(0) const [brightness, setBrightness] = useState(0)
@@ -152,6 +153,9 @@ export default function StickerEditPage() {
setHue((st.edits && st.edits.hue) || 0) setHue((st.edits && st.edits.hue) || 0)
setContrast((st.edits && st.edits.contrast) || 0) setContrast((st.edits && st.edits.contrast) || 0)
initCanvas(st) initCanvas(st)
} else {
// 问题4 留意点:找不到贴纸时给出空态,而不是停在空白 Canvas
setNotFound(true)
} }
}, [initCanvas]) }, [initCanvas])
@@ -312,6 +316,29 @@ export default function StickerEditPage() {
const hPct = `${((hue + 180) / 360) * 100}%` const hPct = `${((hue + 180) / 360) * 100}%`
const cPct = `${((contrast + 100) / 200) * 100}%` const cPct = `${((contrast + 100) / 200) * 100}%`
if (notFound) {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<ScrollTopMask title="编辑贴纸" targetSelector=".edit-header" showBack />
<View className='sticker-edit-page'>
<View className='edit-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<Text className='edit-back' onTap={() => Taro.navigateBack()}></Text>
<Text className='edit-title'></Text>
<Text className='edit-save' />
</View>
<View className='edit-empty surface-card mt-20'>
<Text className='edit-empty-title'></Text>
<Text className='edit-empty-desc'></Text>
<View className='btn-primary' onTap={() => Taro.navigateBack()}>
<Text></Text>
</View>
</View>
</View>
</View>
)
}
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />