巴青农资商城

orderDisplay.js 14KB

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