巴青农资商城

orderDisplay.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. import { resolveFileUrl } from '@/utils/image'
  2. import { formatPrice } from '@/utils/format'
  3. import { parseCartSpecText } from '@/utils/cartSpec'
  4. import { listAftersales } from '@/api/orderAftersale'
  5. import {
  6. ORDER_ACTION,
  7. ORDER_ACTION_LABEL,
  8. REVIEW_ITEM_STATUS,
  9. AFTERSALE_APPLY_TYPE_OPTIONS,
  10. AFTERSALE_TAB,
  11. ORDER_STATUS,
  12. ORDER_AFTERSALE_CARD_LABEL,
  13. ORDER_AFTERSALE_CARD_STATUS
  14. } from '@/constants/order'
  15. /** 订单级不再展示的评价按钮(改在商品行) */
  16. const ORDER_LEVEL_REVIEW_CODES = [ORDER_ACTION.REVIEW, ORDER_ACTION.VIEW_REVIEW]
  17. const GOODS_PLACEHOLDER = '/static/logo.png'
  18. const SHOP_PLACEHOLDER = '/static/logo.png'
  19. /** 后端凭证图可能是逗号拼接字符串,统一转成数组 */
  20. function normalizePicList(raw) {
  21. if (!raw) return []
  22. if (Array.isArray(raw)) return raw.filter(Boolean)
  23. if (typeof raw === 'string') {
  24. return raw.split(',').map((s) => s.trim()).filter(Boolean)
  25. }
  26. return []
  27. }
  28. function mapAftersaleApplyTypeText(applyType) {
  29. const hit = AFTERSALE_APPLY_TYPE_OPTIONS.find((opt) => opt.value === applyType)
  30. return hit ? hit.label : applyType || ''
  31. }
  32. function mapAftersaleStatusText(status) {
  33. if (status === '2') return '售后完结'
  34. if (status === '1') return '商家处理中'
  35. return status || ''
  36. }
  37. function mapAftersaleProgressText(stage) {
  38. if (stage === 'FINISHED') return '商家处理 → 售后完结(当前:售后完结)'
  39. return '商家处理 → 售后完结(当前:商家处理)'
  40. }
  41. function mapFirstItem(item) {
  42. if (!item) return null
  43. const specText = (item.goodsSpec || item.specText || '').trim() || '默认'
  44. return {
  45. orderItemId: item.itemId || item.orderItemId,
  46. goodsId: item.goodsId,
  47. goodsName: item.goodsName || '',
  48. displayPic: resolveFileUrl(item.goodsImage || item.mainPic) || GOODS_PLACEHOLDER,
  49. specText,
  50. specList: parseCartSpecText(specText),
  51. quantity: Number(item.quantity) || 1,
  52. unitPrice: item.unitPrice,
  53. priceText: formatPrice(item.unitPrice),
  54. reviewStatus: item.reviewStatus,
  55. reviewId: item.reviewId
  56. }
  57. }
  58. function mapOrderItemRow(row) {
  59. if (!row) return null
  60. const specText = (row.goodsSpec || row.specText || '').trim() || '默认'
  61. return {
  62. orderItemId: row.orderItemId || row.itemId,
  63. goodsId: row.goodsId,
  64. goodsName: row.goodsName || '',
  65. displayPic: resolveFileUrl(row.goodsImage || row.mainPic) || GOODS_PLACEHOLDER,
  66. specText,
  67. specList: parseCartSpecText(specText),
  68. serviceDesc: row.serviceDesc || '',
  69. quantity: Number(row.quantity) || 1,
  70. unitPrice: row.unitPrice || row.salePrice,
  71. priceText: formatPrice(row.unitPrice || row.salePrice),
  72. lineAmount: row.lineAmount || row.subtotal,
  73. lineAmountText: formatPrice(row.lineAmount || row.subtotal),
  74. buyerRemark: row.buyerRemark || '',
  75. reviewStatus: row.reviewStatus,
  76. reviewId: row.reviewId
  77. }
  78. }
  79. /** 订单已有进行中/已完结售后时,不再展示「申请售后」 */
  80. export function hasOrderAftersale(aftersaleStatus) {
  81. const status = aftersaleStatus != null ? String(aftersaleStatus) : ''
  82. return (
  83. status === ORDER_AFTERSALE_CARD_STATUS.IN_PROGRESS ||
  84. status === ORDER_AFTERSALE_CARD_STATUS.FINISHED
  85. )
  86. }
  87. function mapActions(actions, aftersaleStatus) {
  88. const hideAftersale = hasOrderAftersale(aftersaleStatus)
  89. return (actions || [])
  90. .filter((code) => !ORDER_LEVEL_REVIEW_CODES.includes(code))
  91. .filter((code) => !(hideAftersale && code === ORDER_ACTION.AFTERSALE))
  92. .map((code) => ({
  93. code,
  94. label: ORDER_ACTION_LABEL[code] || code
  95. }))
  96. .filter((item) => item.label)
  97. }
  98. /** 详情页补查订单售后态(列表接口有 aftersaleStatus,详情接口暂无) */
  99. export async function resolveOrderAftersaleStatus(orderId) {
  100. if (!orderId) return null
  101. try {
  102. const inProgressRes = await listAftersales({
  103. tab: AFTERSALE_TAB.IN_PROGRESS,
  104. pageNum: 1,
  105. pageSize: 100
  106. })
  107. const inProgressRows = inProgressRes.rows || []
  108. if (inProgressRows.some((row) => String(row.orderId) === String(orderId))) {
  109. return ORDER_AFTERSALE_CARD_STATUS.IN_PROGRESS
  110. }
  111. const finishedRes = await listAftersales({
  112. tab: AFTERSALE_TAB.FINISHED,
  113. pageNum: 1,
  114. pageSize: 100
  115. })
  116. const finishedRows = finishedRes.rows || []
  117. if (finishedRows.some((row) => String(row.orderId) === String(orderId))) {
  118. return ORDER_AFTERSALE_CARD_STATUS.FINISHED
  119. }
  120. } catch (e) {
  121. // 查询失败时不阻断详情展示
  122. }
  123. return null
  124. }
  125. /** 交易成功商品行:待评价→评价,已评价→查看评价 */
  126. export function getItemReviewAction(item) {
  127. if (!item || !item.reviewStatus) return null
  128. if (item.reviewStatus === REVIEW_ITEM_STATUS.PENDING) {
  129. return {
  130. code: ORDER_ACTION.REVIEW,
  131. label: ORDER_ACTION_LABEL[ORDER_ACTION.REVIEW]
  132. }
  133. }
  134. if (item.reviewStatus === REVIEW_ITEM_STATUS.DONE) {
  135. return {
  136. code: ORDER_ACTION.VIEW_REVIEW,
  137. label: ORDER_ACTION_LABEL[ORDER_ACTION.VIEW_REVIEW]
  138. }
  139. }
  140. return null
  141. }
  142. /** 评价列表「待评价」:订单行展平为待评价商品行 */
  143. export function flattenPendingReviewItems(rows) {
  144. const result = []
  145. for (const row of rows || []) {
  146. const card = mapOrderListRow(row)
  147. if (!card) continue
  148. for (const item of card.items || []) {
  149. if (item.reviewStatus !== REVIEW_ITEM_STATUS.PENDING) continue
  150. result.push({
  151. key: `pending-${card.orderId}-${item.orderItemId}`,
  152. orderId: card.orderId,
  153. item
  154. })
  155. }
  156. }
  157. return result
  158. }
  159. /** 评价列表「已评价」行 → 紧凑卡片模型 */
  160. export function mapReviewDoneRow(row) {
  161. if (!row) return null
  162. const item = mapFirstItem(row.firstItem)
  163. const content = (row.content || '').trim()
  164. return {
  165. key: `done-${row.reviewId || row.orderId}-${item?.orderItemId || 0}`,
  166. reviewId: row.reviewId,
  167. orderId: row.orderId,
  168. orderItemId: item?.orderItemId,
  169. score: Number(row.score) || 0,
  170. content,
  171. contentBrief: trimText(content, 48, '此用户未填写评价内容'),
  172. reviewTime: row.reviewTime || row.createTime || '',
  173. item
  174. }
  175. }
  176. function trimText(text, maxLen, emptyFallback = '') {
  177. const s = (text || '').trim()
  178. if (!s) return emptyFallback
  179. if (s.length <= maxLen) return s
  180. return `${s.slice(0, maxLen)}…`
  181. }
  182. /**
  183. * 列表卡片状态文案(MO-L4 调整)
  184. * 仅「售后处理中」覆盖主状态;售后已完结后订单应变已关闭,列表不再展示「售后已完成」
  185. */
  186. export function mapOrderCardStatusText(orderStatusText, aftersaleStatus, orderStatus) {
  187. if (aftersaleStatus === ORDER_AFTERSALE_CARD_STATUS.IN_PROGRESS) {
  188. return ORDER_AFTERSALE_CARD_LABEL[aftersaleStatus]
  189. }
  190. if (aftersaleStatus === ORDER_AFTERSALE_CARD_STATUS.FINISHED) {
  191. if (orderStatus === ORDER_STATUS.CLOSED) {
  192. return orderStatusText || '已关闭'
  193. }
  194. // 售后已完结但订单主状态未同步(异常数据)时,仍按已关闭展示
  195. return '已关闭'
  196. }
  197. return orderStatusText || ''
  198. }
  199. /** 列表行 VO → 卡片模型 */
  200. export function mapOrderListRow(row) {
  201. if (!row) return null
  202. const rawItems = Array.isArray(row.items) && row.items.length
  203. ? row.items
  204. : row.firstItem
  205. ? [row.firstItem]
  206. : []
  207. const items = rawItems.map(mapFirstItem).filter(Boolean)
  208. const firstItem = items[0] || mapFirstItem(row.firstItem)
  209. const orderStatusText = row.orderStatusText || ''
  210. const aftersaleStatus = row.aftersaleStatus || null
  211. return {
  212. orderId: row.orderId,
  213. orderNo: row.orderNo || '',
  214. orderStatus: row.orderStatus,
  215. orderStatusText,
  216. aftersaleStatus,
  217. statusText: mapOrderCardStatusText(orderStatusText, aftersaleStatus, row.orderStatus),
  218. statusIsAftersale: aftersaleStatus === ORDER_AFTERSALE_CARD_STATUS.IN_PROGRESS,
  219. amountLabel: row.orderStatus === ORDER_STATUS.PENDING_PAY ? '应付' : '实付',
  220. shopId: row.shopId,
  221. shopName: row.shopName || '',
  222. shopAvatar: resolveFileUrl(row.shopAvatar) || SHOP_PLACEHOLDER,
  223. payAmount: row.payAmount,
  224. payAmountText: formatPrice(row.payAmount),
  225. goodsAmount: row.goodsAmount,
  226. freightAmount: row.freightAmount,
  227. createTime: row.createTime || '',
  228. itemCount: Number(row.itemCount) || items.length,
  229. items,
  230. firstItem,
  231. payRemainSeconds: row.payRemainSeconds,
  232. reviewStatus: row.reviewStatus,
  233. actions: mapActions(row.actions, aftersaleStatus)
  234. }
  235. }
  236. /** 详情 VO → 页面模型;options.aftersaleStatus 用于补全售后态并过滤操作按钮 */
  237. export function mapOrderDetail(data, options = {}) {
  238. if (!data) return null
  239. const items = (data.items || []).map(mapOrderItemRow).filter(Boolean)
  240. const latestTrace = data.latestTrace || null
  241. const aftersaleStatus = data.aftersaleStatus || options.aftersaleStatus || null
  242. return {
  243. orderId: data.orderId,
  244. orderNo: data.orderNo || '',
  245. orderStatus: data.orderStatus,
  246. statusText: data.orderStatusText || '',
  247. payStatus: data.payStatus,
  248. payType: data.payType,
  249. payTypeText: data.payTypeText || '',
  250. shopId: data.shopId,
  251. shopName: data.shopName || '',
  252. shopAvatar: resolveFileUrl(data.shopAvatar) || SHOP_PLACEHOLDER,
  253. consigneeName: data.consigneeName || '',
  254. consigneeMobile: data.consigneeMobile || '',
  255. consigneeAddress: data.consigneeAddress || '',
  256. goodsAmount: data.goodsAmount,
  257. goodsAmountText: formatPrice(data.goodsAmount),
  258. freightAmount: data.freightAmount,
  259. freightAmountText: formatPrice(data.freightAmount),
  260. freightDesc: data.freightDesc || '',
  261. payAmount: data.payAmount,
  262. payAmountText: formatPrice(data.payAmount),
  263. createTime: data.createTime || '',
  264. payTime: data.payTime || '',
  265. payExpireTime: data.payExpireTime || '',
  266. payRemainSeconds: data.payRemainSeconds,
  267. finishTime: data.finishTime || '',
  268. shipTime: data.shipTime || '',
  269. closeType: data.closeType,
  270. closeTypeText: data.closeTypeText || '',
  271. closeReason: data.closeReason || '',
  272. deliveryType: data.deliveryType,
  273. logisticsCompany: data.logisticsCompany || '',
  274. trackingNo: data.trackingNo || '',
  275. vehicleNo: data.vehicleNo || '',
  276. courierName: data.courierName || '',
  277. courierMobile: data.courierMobile || '',
  278. latestTrace: latestTrace
  279. ? {
  280. traceType: latestTrace.traceType,
  281. traceTime: latestTrace.traceTime || '',
  282. content: latestTrace.content || ''
  283. }
  284. : null,
  285. items,
  286. reviewStatus: data.reviewStatus,
  287. reviewId: data.reviewId,
  288. aftersaleStatus,
  289. actions: mapActions(data.actions, aftersaleStatus)
  290. }
  291. }
  292. /** 评价详情 */
  293. export function mapOrderReview(data) {
  294. if (!data) return null
  295. const pics = (data.pics || []).map((p) => resolveFileUrl(p) || p).filter(Boolean)
  296. return {
  297. score: Number(data.score) || 0,
  298. content: data.content || '',
  299. pics,
  300. createTime: data.createTime || '',
  301. replyContent: data.replyContent || '',
  302. replyTime: data.replyTime || ''
  303. }
  304. }
  305. /** 售后列表行 */
  306. export function mapAftersaleListRow(row) {
  307. if (!row) return null
  308. const specText = (row.goodsSpec || row.specText || '').trim() || '默认'
  309. return {
  310. aftersaleId: row.aftersaleId,
  311. aftersaleNo: row.aftersaleNo || '',
  312. aftersaleStatus: row.aftersaleStatus,
  313. statusText:
  314. row.aftersaleStatusText ||
  315. row.statusText ||
  316. mapAftersaleStatusText(row.aftersaleStatus),
  317. applyType: row.applyType,
  318. applyTypeText: row.applyTypeText || mapAftersaleApplyTypeText(row.applyType),
  319. applyReason: row.applyReason || '',
  320. applyAmount: row.applyAmount,
  321. applyAmountText: formatPrice(row.applyAmount),
  322. createTime: row.createTime || '',
  323. goodsName: row.goodsName || '',
  324. displayPic: resolveFileUrl(row.goodsImage || row.mainPic) || GOODS_PLACEHOLDER,
  325. specList: parseCartSpecText(specText),
  326. orderId: row.orderId,
  327. orderNo: row.orderNo || ''
  328. }
  329. }
  330. /** 售后详情 */
  331. export function mapAftersaleDetail(data) {
  332. if (!data) return null
  333. const info = data.info || data
  334. const specText = (info.goodsSpec || info.specText || '').trim() || '默认'
  335. const progressStage = data.progressStage || info.progressStage || ''
  336. const aftersaleStatus = info.aftersaleStatus || data.aftersaleStatus
  337. return {
  338. aftersaleId: info.aftersaleId || data.aftersaleId,
  339. aftersaleNo: info.aftersaleNo || data.aftersaleNo || '',
  340. aftersaleStatus,
  341. statusText:
  342. info.aftersaleStatusText ||
  343. info.statusText ||
  344. mapAftersaleStatusText(aftersaleStatus),
  345. progress: data.progress || info.progress || progressStage,
  346. progressText:
  347. data.progressText ||
  348. info.progressText ||
  349. mapAftersaleProgressText(progressStage),
  350. applyType: info.applyType,
  351. applyTypeText: info.applyTypeText || mapAftersaleApplyTypeText(info.applyType),
  352. applyReason: info.applyReason || '',
  353. returnQuantity: info.returnQuantity,
  354. applyAmount: info.applyAmount,
  355. applyAmountText: formatPrice(info.applyAmount),
  356. description: info.description || '',
  357. evidencePics: normalizePicList(info.evidencePics).map((p) => resolveFileUrl(p) || p),
  358. createTime: info.createTime || '',
  359. finishTime: info.finishTime || '',
  360. processResult: data.processResult || info.processResult || '',
  361. orderId: info.orderId,
  362. orderNo: info.orderNo || '',
  363. orderItemId: info.orderItemId,
  364. goodsName: info.goodsName || '',
  365. displayPic: resolveFileUrl(info.goodsImage || info.mainPic) || GOODS_PLACEHOLDER,
  366. specList: parseCartSpecText(specText),
  367. quantity: Number(info.quantity) || 1
  368. }
  369. }
  370. /** 待支付倒计时文案 */
  371. export function formatPayCountdown(seconds) {
  372. if (seconds == null || seconds <= 0) return '已超时'
  373. const h = Math.floor(seconds / 3600)
  374. const m = Math.floor((seconds % 3600) / 60)
  375. const s = seconds % 60
  376. if (h > 0) {
  377. return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
  378. }
  379. return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
  380. }