cart.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. import { get, post } from '@/core/http/http-client'
  2. import { buildRestPath } from '@/core/config/api-paths'
  3. import { STORAGE } from '@/core/constants/storage-keys'
  4. import { decimalAdd, decimalSub, decimalMul } from '@/core/infra/runtime-bridge'
  5. /**
  6. * 从后端返回的各种数据格式中提取购物车数组
  7. * 后端可能返回: 数组 / { list: [] } / { data: [] } / { data: { list: [] } } 等
  8. */
  9. function pickCartRows(payload) {
  10. if (Array.isArray(payload)) return payload
  11. if (!payload || typeof payload !== 'object') return []
  12. if (Array.isArray(payload.list)) return payload.list
  13. if (Array.isArray(payload.data)) return payload.data
  14. if (payload.data && Array.isArray(payload.data.list)) return payload.data.list
  15. if (payload.data && Array.isArray(payload.data.data)) return payload.data.data
  16. return []
  17. }
  18. /**
  19. * 将扁平的購物車数组按店鋪分组
  20. * 输入: [{ shop_id, shop_name, goods_id, ... }, ...]
  21. * 输出: [{ shop_id, shop_name, products: [...], check, choose }, ...]
  22. * 与88live的format逻辑一致
  23. */
  24. function groupByShop(flatList) {
  25. let map = {}
  26. let dest = []
  27. for (let i = 0; i < flatList.length; i++) {
  28. let cart = flatList[i]
  29. cart.checked = false
  30. if (!map[cart.shop_id]) {
  31. dest.push({
  32. shop_id: cart.shop_id,
  33. shop_name: cart.shop_name,
  34. products: [cart],
  35. check: false,
  36. choose: 0
  37. })
  38. map[cart.shop_id] = cart
  39. } else {
  40. for (let j = 0; j < dest.length; j++) {
  41. let dj = dest[j]
  42. if (dj.shop_id == cart.shop_id) {
  43. dj.products.push(cart)
  44. break
  45. }
  46. }
  47. }
  48. }
  49. return dest
  50. }
  51. /**
  52. * 商城购物车状态管理器
  53. * 数据结构: list按店铺分组,每个店铺包含products商品列表
  54. * 选择逻辑: 商品选中 → 店铺选中 → 全选,三级联动
  55. */
  56. export default {
  57. namespaced: true,
  58. state: {
  59. list: [],// 购物车数据,按店铺分组 [{ shop_id, shop_name, products, check, choose }]
  60. cartnum: 0,//购物车商品总数量
  61. allchoose: 0,// 选中的店铺数量
  62. allsum: 0,// 选中商品合计价格
  63. allnum: 0,// 选中商品总数量
  64. status: false,// 全选状态
  65. operate: false// 管理模式(true=编辑模式,false=普通模式
  66. },
  67. mutations: {
  68. replaceList(state, list) {
  69. state.list = list || []
  70. }
  71. },
  72. actions: {
  73. // 切换管理模式(编辑模式/普通模式)
  74. async operate({ state }) {
  75. state.operate = !state.operate
  76. },
  77. //初始化同步购物车:已登录→拉取云端数据,未登录→读取本地缓存
  78. sync({ dispatch, rootState }) {
  79. setTimeout(() => {
  80. if (rootState.user.isLogin) dispatch('pullRemote')
  81. else {
  82. const local = uni.getStorageSync(STORAGE.cart)
  83. if (local) dispatch('normalize', local)
  84. }
  85. }, 1000)
  86. },
  87. // 登录后从云端拉取购物车数据,并与本地合并
  88. async pullRemote({ dispatch }) {
  89. const local = uni.getStorageSync(STORAGE.cart)
  90. try {
  91. const { data } = await post(buildRestPath('cart/synchro'), { cart: local || null })
  92. dispatch('normalize', data)
  93. } catch (e) {
  94. console.warn('[cart] pullRemote', e)
  95. }
  96. },
  97. // 格式化购物车数据:解析后端数据 → 更新state → 计算合计 → 保存本地
  98. normalize({ commit, state }, payload) {
  99. const flatList = pickCartRows(payload)
  100. const dest = groupByShop(flatList)
  101. commit('replaceList', dest)
  102. state.cartnum = 0
  103. state.allchoose = 0
  104. state.allsum = 0
  105. state.allnum = 0
  106. state.status = false
  107. state.operate = false
  108. flatList.forEach((item) => {
  109. if (item) state.cartnum++
  110. })
  111. uni.setStorageSync(STORAGE.cart, flatList)
  112. },
  113. // ==================== 选择操作 ====================
  114. // 选择/取消选择单个商品,根据当前状态分发到choosetrue或choosefalse
  115. async choose({ state, dispatch }, { index, keys }) {
  116. let cart = state.list[index]
  117. let goods = cart.products[keys]
  118. !goods.checked ? dispatch('choosetrue', { cart, goods }) : dispatch('choosefalse', { cart, goods })
  119. },
  120. // 选中商品:更新商品状态 → 检查店铺是否全选 → 检查是否全选 → 更新合计
  121. async choosetrue({ state }, { cart, goods }) {
  122. goods.checked = true
  123. cart.choose++
  124. if (cart.choose === cart.products.length) {
  125. cart.check = true
  126. }
  127. if (cart.check) {
  128. state.allchoose++
  129. if (state.allchoose === state.list.length) {
  130. state.status = true
  131. } else {
  132. state.status = false
  133. }
  134. }
  135. state.allsum = decimalAdd(state.allsum, goods.sum)
  136. state.allnum += goods.number
  137. },
  138. // 取消选中商品:更新商品状态 → 取消店铺选中 → 取消全选 → 更新合计
  139. async choosefalse({ state }, { cart, goods }) {
  140. goods.checked = false
  141. cart.choose--
  142. if (cart.check) {
  143. cart.check = false
  144. state.allchoose--
  145. }
  146. state.status = false
  147. state.allsum = decimalSub(state.allsum, goods.sum)
  148. state.allnum -= goods.number
  149. },
  150. // 选择/取消选择整个店铺,根据当前状态分发到shoptrue或shopfalse
  151. async shopchoose({ dispatch }, cart) {
  152. !cart.check ? dispatch('shoptrue', cart) : dispatch('shopfalse', cart)
  153. },
  154. // 选中店铺:遍历店铺内所有未选中商品,逐一选中
  155. async shoptrue({ dispatch }, cart) {
  156. cart.products.forEach((goods) => {
  157. goods.checked === false && dispatch('choosetrue', { cart, goods })
  158. })
  159. },
  160. // 取消选中店铺:遍历店铺内所有已选中商品,逐一取消选中
  161. async shopfalse({ dispatch }, cart) {
  162. cart.products.forEach((goods) => {
  163. goods.checked === true && dispatch('choosefalse', { cart, goods })
  164. })
  165. },
  166. // 全选/取消全选:切换全选状态,遍历所有店铺执行对应操作
  167. async cartchoose({ state, dispatch }) {
  168. state.status = !state.status
  169. state.status ? state.list.forEach((cart) => dispatch('shoptrue', cart)) : state.list.forEach((cart) => dispatch('shopfalse', cart))
  170. },
  171. // ==================== 数量操作 ====================
  172. // 增加商品数量:数量+1 → 更新小计 → 若已选中则更新合计 → 同步存储
  173. async bcadd({ state, dispatch }, goods) {
  174. goods.number++
  175. goods.sum = decimalAdd(goods.sum, goods.sku.price)
  176. if (goods.checked) {
  177. state.allnum++
  178. state.allsum = decimalAdd(state.allsum, goods.sku.price)
  179. }
  180. dispatch('storage', { type: 'bcadd', goods })
  181. },
  182. // 减少商品数量:数量-1(最少为1) → 更新小计 → 若已选中则更新合计 → 同步存储
  183. async bcsub({ state, dispatch }, goods) {
  184. if (goods.number > 1) {
  185. goods.number--
  186. goods.sum = decimalSub(goods.sum, goods.sku.price)
  187. if (goods.checked) {
  188. state.allnum--
  189. state.allsum = decimalSub(state.allsum, goods.sku.price)
  190. }
  191. dispatch('storage', { type: 'bcsub', goods })
  192. }
  193. },
  194. // ==================== 添加操作 ====================
  195. // 添加商品到购物车:判断店铺是否存在 → 判断商品是否已存在 → 新增或追加数量
  196. async add({ state, dispatch }, goods) {
  197. let isshop = -1
  198. let data = {
  199. goods_id: goods.goods_id,
  200. sku_id: goods.sku_id,
  201. title: goods.title,
  202. image: goods.image,
  203. sku: goods.sku,
  204. number: goods.number,
  205. sum: goods.sum,
  206. checked: false
  207. }
  208. state.list.find((item, index) => {
  209. if (item.shop_id == goods.shop_id) {
  210. isshop = index
  211. }
  212. })
  213. if (isshop == -1) {
  214. // 新店铺:建立新的店铺分组
  215. state.list.push({
  216. shop_id: goods.shop_id,
  217. shop_name: goods.shop_name,
  218. products: [data],
  219. check: false,
  220. choose: 0
  221. })
  222. state.cartnum++
  223. } else {
  224. // 已有店铺:检查商品是否已存在
  225. let products = state.list[isshop].products
  226. let isgoods = -1
  227. products.find((item, index) => {
  228. if (item.goods_id === goods.goods_id && item.sku_id === goods.sku_id) {
  229. isgoods = index
  230. }
  231. })
  232. if (isgoods == -1) {
  233. // 新商品:追加到店铺
  234. dispatch('shopfalse', state.list[isshop])
  235. products.push(data)
  236. state.cartnum++
  237. } else {
  238. // 已有商品:只增加数量
  239. let shop = products[isgoods]
  240. shop.number += goods.number
  241. shop.sum = decimalMul(goods.sku.price, shop.number)
  242. if (shop.checked) {
  243. state.allnum += goods.number
  244. state.allsum = decimalAdd(state.allsum, decimalMul(goods.sku.price, goods.number))
  245. }
  246. }
  247. }
  248. await dispatch('storage', { type: 'add', goods })
  249. },
  250. // ==================== 删除操作 ====================
  251. // 移入关注:将选中商品从购物车移到收藏夹,需登录
  252. async move({ state, dispatch, rootState }) {
  253. if (rootState.user.isLogin) {
  254. dispatch('storage', { type: 'follow' })
  255. } else {
  256. uni.navigateTo({ url: '/pages/passport/signin' })
  257. }
  258. },
  259. // 删除选中商品:全选则清空,否则删除选中项
  260. async del({ state, dispatch }) {
  261. if (state.status) {
  262. dispatch('empty')
  263. } else {
  264. dispatch('storage', { type: 'del' })
  265. }
  266. },
  267. // 清空购物车:重置所有状态
  268. async empty({ state, dispatch }) {
  269. state.list = []
  270. state.cartnum = 0
  271. state.allchoose = 0
  272. state.allsum = 0
  273. state.allnum = 0
  274. state.status = false
  275. state.operate = false
  276. dispatch('storage', { type: 'empty' })
  277. },
  278. // ==================== 结算操作 ====================
  279. // 结算:收集所有选中商品信息,跳转到确认订单页面;未登录则跳转登录页
  280. async settlement({ state, rootState }) {
  281. let data = []
  282. state.list.forEach((cart) => {
  283. cart.products.forEach((goods) => {
  284. if (goods.checked) {
  285. data.push({
  286. goods_id: goods.goods_id,
  287. number: goods.number,
  288. sku_id: goods.sku.id
  289. })
  290. }
  291. })
  292. })
  293. if (rootState.user.isLogin) {
  294. uni.navigateTo({ url: `/pages/order/confirm?type=cart&data=${JSON.stringify(data)}` })
  295. } else {
  296. uni.navigateTo({ url: '/pages/passport/signin' })
  297. }
  298. },
  299. // ==================== 数据同步 ====================
  300. // 统一数据同步处理:整理本地数据 → 同步到云端 → 保存本地缓存
  301. // type取值: empty(清空) / bcadd(加数量) / bcsub(减数量) / add(添加) / del(删除) / follow(移入关注)
  302. async storage({ state, dispatch, rootState }, { type, goods }) {
  303. let cloud = null
  304. let storage = []
  305. let select = []
  306. let unchecked = []
  307. if (type == 'empty') {
  308. cloud = { type }
  309. } else {
  310. // 遍历所有店铺和商品,按类型分类处理
  311. state.list.forEach((cart) => {
  312. cart.products.forEach((item) => {
  313. let data = {
  314. shop_id: cart.shop_id,
  315. shop_name: cart.shop_name,
  316. goods_id: item.goods_id,
  317. title: item.title,
  318. number: item.number,
  319. image: item.image,
  320. sku: item.sku,
  321. sku_id: item.sku_id,
  322. sum: item.sum,
  323. checked: item.checked
  324. }
  325. if (type == 'del' || type == 'follow') {
  326. // 删除或移入关注:分离选中项和未选中项
  327. if (item.checked) {
  328. select.push(data)
  329. } else {
  330. unchecked.push(data)
  331. }
  332. } else {
  333. // 其他操作:直接追加到存储数组
  334. storage.push(data)
  335. }
  336. })
  337. })
  338. state.cartnum = storage.length
  339. // 根据操作类型构建云端请求参数
  340. if (type == 'bcsub' || type == 'bcadd') {
  341. cloud = { type, goods_id: goods.goods_id, sku_id: goods.sku_id, number: goods.number, sum: goods.sum }
  342. } else if (type == 'del' || type == 'follow') {
  343. dispatch('normalize', unchecked)
  344. storage = unchecked
  345. cloud = { type, data: select }
  346. state.operate = false
  347. } else if (type == 'add') {
  348. cloud = { type, data: goods }
  349. state.status = false
  350. }
  351. }
  352. //已登录时同步到云端
  353. if (cloud != null && rootState.user.isLogin) {
  354. try {
  355. const { data } = await post(buildRestPath('cart/storage'), cloud)
  356. if (type == 'follow' && rootState.statistics && rootState.statistics.dynamic) {
  357. rootState.statistics.dynamic.collection = rootState.statistics.dynamic.collection + data
  358. }
  359. } catch (e) {
  360. console.warn('[cart] storage', e)
  361. }
  362. }
  363. uni.setStorageSync(STORAGE.cart, storage)
  364. }
  365. }
  366. }