208 lines
6.2 KiB
TypeScript
208 lines
6.2 KiB
TypeScript
import { create } from 'zustand'
|
||
import Taro from '@tarojs/taro'
|
||
import {
|
||
addCartApi,
|
||
clearCartApi,
|
||
deleteCartItemApi,
|
||
getCartApi,
|
||
getCartSummaryApi,
|
||
updateCartItemApi,
|
||
} from '@/services/cart'
|
||
import type { CartMutationResult } from '@/services/cart'
|
||
import { getToken } from '@/utils/request'
|
||
import type { CartItem, CartSummary } from '@/types/cart'
|
||
|
||
/** 存储 key */
|
||
const STORAGE_KEY = 'cart_data'
|
||
|
||
/** 汇总请求序号(并发时仅采用最后一次响应) */
|
||
let summarySeq = 0
|
||
/** 汇总防抖校准定时器(列表加减停止 800ms 后整体拉取一次,以服务端为准) */
|
||
let summaryTimer: ReturnType<typeof setTimeout> | null = null
|
||
|
||
/** 数值 → 2 位小数字符串(与服务端金额/数量口径一致) */
|
||
function to2(n: number): string {
|
||
return (Math.round(n * 100) / 100).toFixed(2)
|
||
}
|
||
|
||
/** 持久化的购物车快照(服务端为准,此处仅作展示缓存) */
|
||
interface StoredCart {
|
||
items: CartItem[]
|
||
totalCount: number
|
||
totalQuantity: string
|
||
totalAmount: string
|
||
}
|
||
|
||
/** 从本地存储恢复购物车快照 */
|
||
function loadFromStorage(): StoredCart | null {
|
||
try {
|
||
const stored = Taro.getStorageSync(STORAGE_KEY)
|
||
if (stored && Array.isArray(stored.items)) {
|
||
return stored as StoredCart
|
||
}
|
||
} catch {
|
||
// noop
|
||
}
|
||
return null
|
||
}
|
||
|
||
interface CartState {
|
||
items: CartItem[]
|
||
/** 总项数 */
|
||
totalCount: number
|
||
/** 可购项总数量(服务端字符串金额/数量) */
|
||
totalQuantity: string
|
||
/** 可购项总金额(服务端重算) */
|
||
totalAmount: string
|
||
/** 是否已从服务端同步过(避免每次展示都闪烁 loading) */
|
||
loaded: boolean
|
||
loading: boolean
|
||
/** 拉取购物车(以服务端为准,金额一律服务端重算) */
|
||
fetchCart: () => Promise<void>
|
||
/** 加购(返回合并后的购物车行,供列表页回写 cart_id/cart_quantity) */
|
||
addItem: (productId: number, quantity: number) => Promise<CartMutationResult>
|
||
/** 修改数量 */
|
||
updateQuantity: (id: number, quantity: number) => Promise<void>
|
||
/** 删除单项 */
|
||
removeItem: (id: number) => Promise<void>
|
||
/** 清空购物车 */
|
||
clearCart: () => Promise<void>
|
||
/** 下单成功后本地清空(不请求接口) */
|
||
clearLocal: () => void
|
||
/** 用接口附带的汇总块(首页/商品列表响应的 cart 字段)直接更新悬浮球 */
|
||
setSummary: (summary: CartSummary) => void
|
||
/** 拉取轻量汇总(需登录;未登录跳过,避免 401 跳转) */
|
||
fetchSummary: () => Promise<void>
|
||
/** 列表行内加减后本地增减悬浮球(乐观展示),并防抖调 fetchSummary 校准 */
|
||
applyDelta: (delta: { quantity: number; amount: number; count?: number }) => void
|
||
}
|
||
|
||
/** 空的购物车快照 */
|
||
const EMPTY_SNAPSHOT = {
|
||
items: [] as CartItem[],
|
||
totalCount: 0,
|
||
totalQuantity: '0.00',
|
||
totalAmount: '0.00',
|
||
}
|
||
|
||
const useCartStore = create<CartState>((set, get) => {
|
||
const cached = loadFromStorage()
|
||
|
||
/** 持久化购物车快照 */
|
||
const persist = (
|
||
snapshot: Pick<CartState, 'items' | 'totalCount' | 'totalQuantity' | 'totalAmount'>,
|
||
) => {
|
||
try {
|
||
Taro.setStorageSync(STORAGE_KEY, snapshot)
|
||
} catch {
|
||
// storage 写入失败不阻塞
|
||
}
|
||
}
|
||
|
||
/** 写入悬浮球汇总并持久化(列表项快照保持不变) */
|
||
const applySummary = (summary: CartSummary) => {
|
||
const next = {
|
||
items: get().items,
|
||
totalCount: summary.total_count,
|
||
totalQuantity: summary.total_quantity,
|
||
totalAmount: summary.total_amount,
|
||
}
|
||
set(next)
|
||
persist(next)
|
||
}
|
||
|
||
return {
|
||
...EMPTY_SNAPSHOT,
|
||
items: cached?.items ?? [],
|
||
totalCount: cached?.totalCount ?? 0,
|
||
totalQuantity: cached?.totalQuantity ?? '0.00',
|
||
totalAmount: cached?.totalAmount ?? '0.00',
|
||
loaded: false,
|
||
loading: false,
|
||
|
||
/** 拉取购物车(服务端为准) */
|
||
fetchCart: async () => {
|
||
if (get().loading) return
|
||
set({ loading: true })
|
||
try {
|
||
const res = await getCartApi()
|
||
const { items, total_count, total_quantity, total_amount } = res.data
|
||
const next = {
|
||
items,
|
||
totalCount: total_count,
|
||
totalQuantity: total_quantity,
|
||
totalAmount: total_amount,
|
||
}
|
||
set({ ...next, loaded: true })
|
||
persist(next)
|
||
} finally {
|
||
set({ loading: false })
|
||
}
|
||
},
|
||
|
||
/** 加购:服务端校验上架与等级价,成功后重新同步 */
|
||
addItem: async (productId, quantity) => {
|
||
const res = await addCartApi({ product_id: productId, quantity })
|
||
await get().fetchCart()
|
||
return res.data
|
||
},
|
||
|
||
/** 修改数量 */
|
||
updateQuantity: async (id, quantity) => {
|
||
await updateCartItemApi(id, quantity)
|
||
await get().fetchCart()
|
||
},
|
||
|
||
/** 删除单项 */
|
||
removeItem: async (id) => {
|
||
await deleteCartItemApi(id)
|
||
await get().fetchCart()
|
||
},
|
||
|
||
/** 清空购物车 */
|
||
clearCart: async () => {
|
||
await clearCartApi()
|
||
set({ ...EMPTY_SNAPSHOT, loaded: true })
|
||
persist(EMPTY_SNAPSHOT)
|
||
},
|
||
|
||
/** 下单成功后本地清空 */
|
||
clearLocal: () => {
|
||
set(EMPTY_SNAPSHOT)
|
||
persist(EMPTY_SNAPSHOT)
|
||
},
|
||
|
||
/** 写入接口附带的汇总块(首页/商品列表) */
|
||
setSummary: (summary) => {
|
||
applySummary(summary)
|
||
},
|
||
|
||
/** 拉取轻量汇总(并发时仅采用最后一次响应) */
|
||
fetchSummary: async () => {
|
||
// 未登录无汇总(接口固定 401,会触发清理登录态),直接跳过
|
||
if (!getToken()) return
|
||
const seq = ++summarySeq
|
||
const res = await getCartSummaryApi()
|
||
if (seq !== summarySeq) return // 已有更新的请求,丢弃本次响应
|
||
applySummary(res.data)
|
||
},
|
||
|
||
/** 列表加减后的本地增减:即时反馈,防抖后以服务端汇总校准 */
|
||
applyDelta: ({ quantity, amount, count = 0 }) => {
|
||
const s = get()
|
||
applySummary({
|
||
total_count: Math.max(0, s.totalCount + count),
|
||
total_quantity: to2(Math.max(0, Number(s.totalQuantity) + quantity)),
|
||
total_amount: to2(Math.max(0, Number(s.totalAmount) + amount)),
|
||
})
|
||
if (summaryTimer) clearTimeout(summaryTimer)
|
||
summaryTimer = setTimeout(() => {
|
||
summaryTimer = null
|
||
get().fetchSummary().catch(() => {})
|
||
}, 800)
|
||
},
|
||
}
|
||
})
|
||
|
||
export default useCartStore
|