From e557dbcb89762c3d2f4be95418445c03a41e90db Mon Sep 17 00:00:00 2001 From: lhmin0604 Date: Sat, 12 Sep 2026 03:51:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(r2):=20=E9=98=B6=E6=AE=B53=20=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E6=95=B0=E6=8D=AE=E6=BA=90=E5=88=87=E6=8D=A2=E5=88=B0?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E7=AB=AF=20API=EF=BC=88=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E9=99=8D=E7=BA=A7=E6=9C=AC=E5=9C=B0=E7=BC=93=E5=AD=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/pages/address/index.tsx | 43 +++++++++---- src/pages/designList/index.tsx | 21 +++++-- src/pages/diy/index.tsx | 94 ++++++++++++++++++++--------- src/pages/diy/stickerEdit/index.tsx | 17 +++++- src/pages/product/index.tsx | 26 ++++++-- src/types/index.ts | 11 +++- 6 files changed, 155 insertions(+), 57 deletions(-) diff --git a/src/pages/address/index.tsx b/src/pages/address/index.tsx index 40dbf51..730b225 100644 --- a/src/pages/address/index.tsx +++ b/src/pages/address/index.tsx @@ -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) => { diff --git a/src/pages/designList/index.tsx b/src/pages/designList/index.tsx index 4d5e1d2..60dc465 100644 --- a/src/pages/designList/index.tsx +++ b/src/pages/designList/index.tsx @@ -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 = { 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>(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() { {/* 列表 */} {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' diff --git a/src/pages/diy/index.tsx b/src/pages/diy/index.tsx index 9455ed3..eea61c4 100644 --- a/src/pages/diy/index.tsx +++ b/src/pages/diy/index.tsx @@ -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}` }) diff --git a/src/pages/diy/stickerEdit/index.tsx b/src/pages/diy/stickerEdit/index.tsx index 0811ccd..aecd3b7 100644 --- a/src/pages/diy/stickerEdit/index.tsx +++ b/src/pages/diy/stickerEdit/index.tsx @@ -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() }, diff --git a/src/pages/product/index.tsx b/src/pages/product/index.tsx index 8756ee3..46a8d0b 100644 --- a/src/pages/product/index.tsx +++ b/src/pages/product/index.tsx @@ -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 = () => { diff --git a/src/types/index.ts b/src/types/index.ts index eefbce1..3f55427 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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