Files
xin-procurement-weapp/src/stores/cart/useCartStore.ts
T
2026-08-27 18:08:14 +08:00

208 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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