feat(r2): 阶段3 页面数据源切换到服务端 API(失败降级本地缓存)
- designList:列表/批量删除走 API,新增生产中(processing)徽标与筛选 tab
- address:增删改查走 API,默认唯一性由服务端事务保证
- product:加入清单走 createDesign(服务端 DRAFT)
- diy:进入即创建清单条目;保存时 category 裁剪 {id,mask,tone}、
designData 与服务端数据合并(保护 wordcloud 分组,WCD 红线)、
贴纸经 persistDesignMedia 持久化后整包提交(状态→designing)
- stickerEdit:贴纸编辑合并后整包提交
- types:DesignDataV1.category 收窄为契约 §2 冻结形状 {id, mask, tone}
- 全部写入点:API 成功回写本地缓存(checkout 等未迁移页面不受影响)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+30
-13
@@ -2,7 +2,8 @@ import { View, Text, Image, Input, Picker } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState, useEffect } from 'react'
|
||||
import './index.scss'
|
||||
import { getAddressList, addAddress, updateAddress, deleteAddress } from '../../utils/store'
|
||||
import { getAddressList, setAddressList, addAddress, updateAddress, deleteAddress } from '../../utils/store'
|
||||
import { fetchAddresses, createAddress, updateAddress as updateAddressApi, deleteAddress as deleteAddressApi } from '../../utils/api'
|
||||
import type { AddressItem } from '../../types'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
@@ -26,7 +27,15 @@ export default function AddressPage() {
|
||||
const [detail, setDetail] = useState('')
|
||||
const [isDefault, setIsDefault] = useState(false)
|
||||
|
||||
const load = () => setList(getAddressList())
|
||||
const load = () => {
|
||||
// API 优先,成功后回写本地缓存;失败降级读缓存
|
||||
fetchAddresses()
|
||||
.then(data => {
|
||||
setList(data)
|
||||
setAddressList(data)
|
||||
})
|
||||
.catch(() => setList(getAddressList()))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
@@ -62,8 +71,9 @@ export default function AddressPage() {
|
||||
content: '删除后将无法恢复该地址',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
deleteAddress(id)
|
||||
load()
|
||||
deleteAddressApi(id)
|
||||
.catch(() => deleteAddress(id)) // 失败降级本地删除
|
||||
.finally(load)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -80,15 +90,22 @@ export default function AddressPage() {
|
||||
return
|
||||
}
|
||||
const payload = { name, phone, region, detail, isDefault }
|
||||
if (editing) {
|
||||
updateAddress(editing.id, payload)
|
||||
} else {
|
||||
addAddress(payload)
|
||||
}
|
||||
Taro.showToast({ title: '保存成功', icon: 'success' })
|
||||
setShowForm(false)
|
||||
resetForm()
|
||||
load()
|
||||
const save = editing
|
||||
? updateAddressApi(editing.id, payload)
|
||||
: createAddress(payload)
|
||||
save
|
||||
.then(() => Taro.showToast({ title: '保存成功', icon: 'success' }))
|
||||
.catch(() => {
|
||||
// 服务端不可达时降级本地保存,联网后进入页面会刷新
|
||||
if (editing) updateAddress(editing.id, payload)
|
||||
else addAddress(payload)
|
||||
Taro.showToast({ title: '网络不可用,已暂存到本地', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
setShowForm(false)
|
||||
resetForm()
|
||||
load()
|
||||
})
|
||||
}
|
||||
|
||||
const onRegionChange = (e: any) => {
|
||||
|
||||
@@ -2,7 +2,8 @@ import { View, Text, ScrollView, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import './index.scss'
|
||||
import { getDesignList, removeDesigns, type DesignItem } from '../../utils/store'
|
||||
import { getDesignList, setDesignList, removeDesigns, type DesignItem } from '../../utils/store'
|
||||
import { fetchDesignList, deleteDesigns } from '../../utils/api'
|
||||
import { PRODUCT_ICON_MAP } from '../../utils/productConfig'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
@@ -17,6 +18,7 @@ const STATUS_TABS = [
|
||||
{ code: 'toDesign', label: '待设计' },
|
||||
{ code: 'undesigned', label: '未设计' },
|
||||
{ code: 'designing', label: '设计中' },
|
||||
{ code: 'processing', label: '生产中' },
|
||||
{ code: 'ordered', label: '已下单' }
|
||||
]
|
||||
|
||||
@@ -25,6 +27,7 @@ const VISIBLE_STATUS_TABS = STATUS_TABS.filter(tab => tab.code !== 'undesigned')
|
||||
const STATUS_STYLE: Record<string, { label: string; cls: string }> = {
|
||||
undesigned: { label: '未设计', cls: 'badge-pink' },
|
||||
designing: { label: '设计中', cls: 'badge-blue' },
|
||||
processing: { label: '生产中', cls: 'badge-warning' },
|
||||
ordered: { label: '已下单', cls: 'badge-green' }
|
||||
}
|
||||
|
||||
@@ -38,7 +41,13 @@ export default function DesignListPage() {
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||
|
||||
const load = useCallback(() => {
|
||||
setList(getDesignList())
|
||||
// API 优先,成功后回写本地缓存(checkout 等页面仍读缓存);失败降级读缓存
|
||||
fetchDesignList()
|
||||
.then(data => {
|
||||
setList(data)
|
||||
setDesignList(data)
|
||||
})
|
||||
.catch(() => setList(getDesignList()))
|
||||
}, [])
|
||||
|
||||
const init = useCallback(() => {
|
||||
@@ -100,10 +109,12 @@ export default function DesignListPage() {
|
||||
content: `确定删除选中的 ${selected.size} 个条目吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
removeDesigns(Array.from(selected))
|
||||
const ids = Array.from(selected)
|
||||
setSelected(new Set())
|
||||
setManaging(false)
|
||||
load()
|
||||
deleteDesigns(ids)
|
||||
.catch(() => removeDesigns(ids)) // 失败降级本地删除
|
||||
.finally(load)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -164,7 +175,7 @@ export default function DesignListPage() {
|
||||
{/* 列表 */}
|
||||
<ScrollView className='design-list' scrollY>
|
||||
{filtered.map(item => {
|
||||
const style = STATUS_STYLE[item.status]
|
||||
const style = STATUS_STYLE[item.status] || STATUS_STYLE.undesigned
|
||||
const rawIcon = (item.productIcon || '').startsWith('/icon/') || /^https?:/i.test(item.productIcon || '')
|
||||
? item.productIcon
|
||||
: PRODUCT_ICON_MAP[item.productId] || '/icon/四角星.svg'
|
||||
|
||||
+64
-30
@@ -4,7 +4,7 @@ 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 { persistDesignMedia } from '../../utils/api'
|
||||
import { createDesign, updateDesign as updateDesignApi, persistDesignMedia } from '../../utils/api'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
@@ -27,6 +27,34 @@ export default function DIYPage() {
|
||||
const [previewMode, setPreviewMode] = useState(false)
|
||||
const [hasOverlap, setHasOverlap] = useState(false)
|
||||
|
||||
/** 进入 DIY 即创建清单条目:服务端创建(DRAFT,阶段0 决策#6),失败降级本地 */
|
||||
const createDesignEntry = (product: { id: string; name: string; price: number; icon: string }, count: number) => {
|
||||
createDesign({
|
||||
productId: product.id,
|
||||
productName: product.name,
|
||||
unitPrice: product.price,
|
||||
count
|
||||
})
|
||||
.then(item => {
|
||||
setDesignList([item, ...getDesignList()]) // 回写缓存,checkout 等页面仍读缓存
|
||||
setDesignId(item.id)
|
||||
})
|
||||
.catch(() => {
|
||||
const newDesign: DesignItem = {
|
||||
id: 'DSG' + Date.now(),
|
||||
productId: product.id,
|
||||
productName: product.name,
|
||||
productIcon: product.icon,
|
||||
unitPrice: product.price,
|
||||
count,
|
||||
status: 'undesigned',
|
||||
createdAt: new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
setDesignList([...getDesignList(), newDesign])
|
||||
setDesignId(newDesign.id)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const router = Taro.getCurrentInstance().router
|
||||
const params = router ? router.params : undefined
|
||||
@@ -37,20 +65,9 @@ export default function DIYPage() {
|
||||
const product = getProductById(productId)
|
||||
if (product) {
|
||||
setCategory(product)
|
||||
setQuantity(Number(params ? params.quantity : undefined) || 1)
|
||||
const newDesign: DesignItem = {
|
||||
id: 'DSG' + Date.now(),
|
||||
productId: product.id,
|
||||
productName: product.name,
|
||||
productIcon: product.icon,
|
||||
unitPrice: product.price,
|
||||
count: Number(params ? params.quantity : undefined) || 1,
|
||||
status: 'designing',
|
||||
createdAt: new Date().toISOString().slice(0, 10)
|
||||
}
|
||||
const list = getDesignList()
|
||||
setDesignList([...list, newDesign])
|
||||
setDesignId(newDesign.id)
|
||||
const count = Number(params ? params.quantity : undefined) || 1
|
||||
setQuantity(count)
|
||||
createDesignEntry(product, count)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -89,19 +106,7 @@ export default function DIYPage() {
|
||||
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)
|
||||
createDesignEntry(found, 1)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
@@ -247,10 +252,39 @@ export default function DIYPage() {
|
||||
Taro.showToast({ title: '贴纸不能重叠', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (designId) {
|
||||
if (designId && category) {
|
||||
// 贴纸/底图持久化(决策#4,R4 负责):本地图 → COS 持久 URL,保证后端 WCD 打包可下载素材
|
||||
const data = await persistDesignMedia({ stickers, category, version: 1 })
|
||||
updateDesign(designId, { designData: data })
|
||||
// category 裁剪为 {id, mask, tone}(阶段0 决策#2;契约约束#3:mask 必须保留)
|
||||
const trimmedCategory = {
|
||||
id: category.id,
|
||||
mask: category.mask,
|
||||
...(category.tone ? { tone: category.tone } : {})
|
||||
}
|
||||
// WCD 红线:基于服务端已有 designData 合并(保留 R4 写入的 wordcloud 分组等),只覆盖本页编辑的字段
|
||||
const cached = getDesignList().find(d => d.id === designId)
|
||||
const designData = {
|
||||
...(cached && cached.designData ? cached.designData : {}),
|
||||
version: 1 as const,
|
||||
category: trimmedCategory,
|
||||
stickers: data.stickers || stickers
|
||||
}
|
||||
// 本地缓存同步(checkout 仍读缓存)
|
||||
updateDesign(designId, { status: 'designing', designData })
|
||||
try {
|
||||
await updateDesignApi(designId, {
|
||||
status: 'designing',
|
||||
item: {
|
||||
productId: category.id,
|
||||
productName: category.name,
|
||||
unitPrice: category.price,
|
||||
count: quantity,
|
||||
designData
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
Taro.showToast({ title: '网络不可用,已暂存到本地', icon: 'none' })
|
||||
}
|
||||
}
|
||||
setPreviewMode(false)
|
||||
Taro.navigateTo({ url: `/pages/checkout/index?designId=${designId}` })
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { sketchImage } from '../../../utils/api'
|
||||
import { sketchImage, updateDesign as updateDesignApi } from '../../../utils/api'
|
||||
import { useThemeContext } from '../../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../../hooks/useStatusBar'
|
||||
@@ -232,7 +232,20 @@ export default function StickerEditPage() {
|
||||
src: newSrc,
|
||||
edits: { brightness, hue, contrast }
|
||||
}
|
||||
updateDesign(designId, { designData: { ...dList[dIdx].designData, stickers } })
|
||||
const d = dList[dIdx]
|
||||
// WCD 红线:基于服务端已有 designData 合并(保留 wordcloud 分组),只覆盖 stickers
|
||||
const designData = { ...d.designData, stickers }
|
||||
updateDesign(designId, { designData }) // 本地缓存同步
|
||||
// 服务端保存(items 全量替换,必须带全 4 个基本字段);失败暂存本地
|
||||
updateDesignApi(designId, {
|
||||
item: {
|
||||
productId: d.productId,
|
||||
productName: d.productName,
|
||||
unitPrice: d.unitPrice,
|
||||
count: d.count,
|
||||
designData
|
||||
}
|
||||
}).catch(() => Taro.showToast({ title: '网络不可用,已暂存到本地', icon: 'none' }))
|
||||
Taro.showToast({ title: '保存成功', icon: 'success' })
|
||||
Taro.navigateBack()
|
||||
},
|
||||
|
||||
@@ -3,7 +3,8 @@ import Taro from '@tarojs/taro'
|
||||
import { useState } from 'react'
|
||||
import './index.scss'
|
||||
import { getProductById } from '../../utils/productConfig'
|
||||
import { addDesign } from '../../utils/store'
|
||||
import { addDesign, getDesignList, setDesignList } from '../../utils/store'
|
||||
import { createDesign } from '../../utils/api'
|
||||
import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
@@ -60,10 +61,25 @@ export default function ProductPage() {
|
||||
}
|
||||
|
||||
const confirmAdd = () => {
|
||||
addDesign(product, quantity)
|
||||
setShowModal(false)
|
||||
Taro.showToast({ title: `已加入设计清单 x${quantity}`, icon: 'success' })
|
||||
setTimeout(() => Taro.navigateBack(), 1200)
|
||||
// 写服务端(需登录,创建即 DRAFT),成功后回写本地缓存;失败降级本地
|
||||
createDesign({
|
||||
productId: product.id,
|
||||
productName: product.name,
|
||||
unitPrice: product.price,
|
||||
count: quantity
|
||||
})
|
||||
.then(item => {
|
||||
setDesignList([item, ...getDesignList()])
|
||||
Taro.showToast({ title: `已加入设计清单 x${quantity}`, icon: 'success' })
|
||||
})
|
||||
.catch(() => {
|
||||
addDesign(product, quantity)
|
||||
Taro.showToast({ title: `网络不可用,已加入本地清单 x${quantity}`, icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
setShowModal(false)
|
||||
setTimeout(() => Taro.navigateBack(), 1200)
|
||||
})
|
||||
}
|
||||
|
||||
const goToDIY = () => {
|
||||
|
||||
+9
-2
@@ -77,8 +77,15 @@ export interface StickerItem {
|
||||
export interface DesignDataV1 {
|
||||
/** 结构版本;旧数据缺省视为 v1 */
|
||||
version?: 1
|
||||
/** 商品品类:mask 是画布尺寸来源,说明见 docs/mask-config-guide.md */
|
||||
category?: ProductCategory
|
||||
/**
|
||||
* 商品品类(契约 §2 冻结:仅 {id, mask, tone},DIY 保存时裁剪);
|
||||
* mask 是画布尺寸来源,说明见 docs/mask-config-guide.md
|
||||
*/
|
||||
category?: {
|
||||
id: string
|
||||
mask: MaskConfig
|
||||
tone?: [number, number, number]
|
||||
}
|
||||
/** 底图(持久 URL),WCD 打包的画布底 */
|
||||
background?: {
|
||||
src: string
|
||||
|
||||
Reference in New Issue
Block a user