detail.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. <template>
  2. <cwg-page-wrapper>
  3. <view class="order-detail-page">
  4. <view class="content">
  5. <view class="status-card section-card">
  6. <view class="success-icon-wrapper">
  7. <image v-if="orderDetail.orderStatus === 'success'" src="/static/images/vector.png" alt=""
  8. mode="widthFix" />
  9. <image v-else-if="orderDetail.orderStatus === 'failed'" src="/static/images/vector2.png" alt=""
  10. mode="widthFix" />
  11. <image v-else src="/static/images/vector3.png" alt="" mode="widthFix" />
  12. </view>
  13. <text class="status-title">{{ getOrderStatusText(orderDetail.orderStatus) }}</text>
  14. <text v-if="approveDesc" class="success-text">{{ approveDesc }}</text>
  15. </view>
  16. <!-- Approval Progress -->
  17. <!-- <view class="section-card">
  18. <view class="section-header">
  19. <uni-icons type="bars" size="18" color="#2563eb" />
  20. <text class="section-title">{{ t('card.Status.t15') }}</text>
  21. </view>
  22. <view class="approval-timeline">
  23. <view v-for="(step, index) in orderDetail.approvalSteps" :key="index" class="timeline-item">
  24. <view class="timeline-left">
  25. <view
  26. :class="['timeline-dot', step.status === 'completed' ? 'timeline-dot-active' : step.status === 'current' ? 'timeline-dot-current' : '']">
  27. <uni-icons v-if="step.status === 'completed'" type="checkmarkempty" size="14" color="#ffffff" />
  28. </view>
  29. <view v-if="index < orderDetail.approvalSteps.length - 1" class="timeline-line"></view>
  30. </view>
  31. <view class="timeline-right">
  32. <view class="timeline-header">
  33. <text class="timeline-title">{{ step.title }}</text>
  34. <view v-if="step.status === 'completed'" class="timeline-status completed">
  35. <uni-icons type="checkmarkempty" size="12" color="#22c55e" />
  36. <text class="timeline-status-text">{{ t('State.Completed') }}</text>
  37. </view>
  38. <view v-else-if="step.status === 'current'" class="timeline-status current">
  39. <uni-icons type="info" size="12" color="#eab308" />
  40. <text class="timeline-status-text">{{ t('State.Ongoing') }}</text>
  41. </view>
  42. <view v-else class="timeline-status pending">
  43. <text class="timeline-status-text">{{ t('State.ToBeProcessed') }}</text>
  44. </view>
  45. </view>
  46. <text v-if="step.operator" class="timeline-operator">操作人: {{ step.operator }}</text>
  47. <text v-if="step.time" class="timeline-time">{{ step.time }}</text>
  48. <text v-if="step.remark" class="timeline-remark">{{ step.remark }}</text>
  49. </view>
  50. </view>
  51. </view>
  52. </view> -->
  53. <!-- Amount Info -->
  54. <view class="section-card">
  55. <view class="section-header">
  56. <uni-icons type="wallet" size="18" color="#2563eb" />
  57. <text class="section-title">{{ t('card.Form.f55') }}</text>
  58. </view>
  59. <view class="info-list">
  60. <view class="info-row">
  61. <text class="info-label">{{ t('card.Form.f37') }}</text>
  62. <text class="info-value amount-highlight">
  63. {{ orderDetail.amount.toFixed(2) }} <text class="currency">{{ orderDetail.currency || 'USD' }}</text>
  64. </text>
  65. </view>
  66. <view class="info-row">
  67. <text class="info-label">{{ t('card.Form.f30') }}</text>
  68. <text class="info-value">
  69. {{ orderDetail.fee.toFixed(2) }} <text class="currency">{{ orderDetail.currency || 'USD' }}</text>
  70. </text>
  71. </view>
  72. <!-- <view class="divider"></view>
  73. <view class="info-row">
  74. <text class="info-label total-label">{{ t('card.Form.f55') }}</text>
  75. <text class="info-value total-value">
  76. {{ orderDetail.actualAmount.toFixed(2) }} <text class="currency">{{ orderDetail.currency || 'USD'
  77. }}</text>
  78. </text>
  79. </view> -->
  80. </view>
  81. </view>
  82. <!-- Order Info -->
  83. <view class="section-card">
  84. <view class="section-header">
  85. <uni-icons type="list" size="18" color="#2563eb" />
  86. <text class="section-title">{{ t('global.title3') }}</text>
  87. </view>
  88. <view class="info-list">
  89. <view class="info-row" v-if="orderDetail.orderNo">
  90. <text class="info-label">{{ t('card.Form.f35') }}</text>
  91. <view class="info-value-wrapper">
  92. <text class="info-value">{{ formatOrderNo(orderDetail.orderNo) }}</text>
  93. <cwg-icon name="copy" :size="14" color="#9ca3af" @click.stop="copyOrderNo" />
  94. </view>
  95. </view>
  96. <view class="info-row">
  97. <text class="info-label">{{ t('card.Form.f42') }}</text>
  98. <text class="info-value">{{ orderDetail.type || '-' }}</text>
  99. </view>
  100. <view class="info-row">
  101. <text class="info-label">{{ t('card.Form.f33') }}</text>
  102. <text class="info-value">{{ orderDetail.createTime }}</text>
  103. </view>
  104. <view class="info-row" v-if="orderDetail.completeTime">
  105. <text class="info-label">{{ t('State.Complete') }}</text>
  106. <text class="info-value">{{ orderDetail.completeTime || '-' }}</text>
  107. </view>
  108. <view v-if="orderDetail.merchant" class="info-row">
  109. <text class="info-label">{{ t('card.Form.f41') }}</text>
  110. <text class="info-value">{{ orderDetail.merchant }}</text>
  111. </view>
  112. <view v-if="orderDetail.bankCard" class="info-row">
  113. <text class="info-label">{{ t('card.Form.f24') }}</text>
  114. <text class="info-value">{{ orderDetail.bankCard }}</text>
  115. </view>
  116. <view v-if="orderDetail.remark" class="info-row vertical">
  117. <text class="info-label">{{ t('card.Form.f27') }}</text>
  118. <text class="info-value remark-text">{{ orderDetail.remark }}</text>
  119. </view>
  120. </view>
  121. </view>
  122. </view>
  123. </view>
  124. </cwg-page-wrapper>
  125. </template>
  126. <script setup lang="ts">
  127. import { reactive, computed, ref } from 'vue';
  128. import { onLoad, onUnload } from '@dcloudio/uni-app';
  129. import { useI18n } from 'vue-i18n';
  130. import useCardStore from '@/stores/use-card-store';
  131. import useUserStore from '@/stores/use-user-store';
  132. import { ucardApi } from '@/api/ucard';
  133. type OrderStatus = 'success' | 'processing' | 'failed' | 'cancelled';
  134. type ApprovalStatus = 'completed' | 'current' | 'pending';
  135. interface ApprovalStep {
  136. title: string;
  137. status: ApprovalStatus;
  138. operator?: string;
  139. time?: string;
  140. remark?: string;
  141. }
  142. interface OrderDetail {
  143. category?: 'recharge' | 'transaction' | 'deduction';
  144. orderNo: string;
  145. type: string;
  146. amount: number;
  147. fee: number;
  148. actualAmount: number;
  149. currency?: string;
  150. orderStatus: OrderStatus;
  151. statusMessage: string;
  152. createTime: string;
  153. completeTime?: string;
  154. merchant?: string;
  155. bankCard?: string;
  156. remark?: string;
  157. approvalSteps: ApprovalStep[];
  158. }
  159. // 订单详情数据(默认占位,进入页面后会用缓存覆盖)
  160. const orderDetail = reactive<OrderDetail>({
  161. category: 'recharge',
  162. orderNo: '',
  163. type: '',
  164. amount: 0,
  165. fee: 0,
  166. actualAmount: 0,
  167. currency: 'USD',
  168. orderStatus: 'processing',
  169. statusMessage: '',
  170. createTime: '',
  171. completeTime: '',
  172. merchant: '',
  173. bankCard: '',
  174. remark: '',
  175. approvalSteps: []
  176. });
  177. const { t, locale } = useI18n();
  178. const cardStore = useCardStore();
  179. const userStore = useUserStore();
  180. const approveDesc = ref('');
  181. const getApproveDesc = () => {
  182. const d = orderDetail.approveDesc
  183. if (!d) return
  184. const c = userStore.reasonsOptions
  185. const a = c[d || '']
  186. const b = locale.value == 'cn' || locale.value == 'zhHant' ? a.content : a.enContent
  187. if (!b) {
  188. reasonsRefusalList()
  189. }
  190. approveDesc.value = b;
  191. }
  192. async function reasonsRefusalList() {
  193. try {
  194. const res = await ucardApi.reasonsRefusalList();
  195. if (res.code === 200) {
  196. pickFields(res.data);
  197. getApproveDesc()
  198. } else {
  199. uni.$u.toast(res.msg || t("login.msg0"));
  200. }
  201. } catch (error) {
  202. console.log(error, 111);
  203. }
  204. }
  205. function pickFields(source, fields = ['content', 'enContent']) {
  206. const result = {}
  207. Object.entries(source).forEach(([key, value]) => {
  208. result[key] = fields.reduce((acc, f) => {
  209. acc[f] = value[f] ?? null
  210. return acc
  211. }, {})
  212. })
  213. userStore.saveReasonsOptions(result);
  214. }
  215. const formatOrderNo = (orderNo?: string) => {
  216. if (!orderNo) return '--';
  217. if (orderNo.length <= 20) return orderNo;
  218. return orderNo.slice(0, 6) + '...' + orderNo.slice(-4);
  219. }
  220. // 获取订单状态颜色
  221. const getOrderStatusColor = (status: OrderStatus): string => {
  222. switch (status) {
  223. case 'success':
  224. return '#22c55e';
  225. case 'processing':
  226. return '#eab308';
  227. case 'failed':
  228. return '#ef4444';
  229. case 'cancelled':
  230. return '#9ca3af';
  231. default:
  232. return '#9ca3af';
  233. }
  234. };
  235. // 获取订单状态文本
  236. const getOrderStatusText = (status: OrderStatus): string => {
  237. switch (status) {
  238. case 'success':
  239. return t('card.Status.t1'); // 成功
  240. case 'processing':
  241. return t('card.Status.t3'); // 处理中
  242. case 'failed':
  243. return t('card.Status.t2'); // 失败
  244. case 'cancelled':
  245. return t('card.Status.t5'); // 待处理 / 已取消,复用状态文案
  246. default:
  247. return t('card.Status.t5');
  248. }
  249. };
  250. // 复制订单号
  251. const copyOrderNo = () => {
  252. uni.setClipboardData({
  253. data: orderDetail.orderNo,
  254. success: () => {
  255. uni.showToast({
  256. title: t('card.Msg.m8') || '复制成功',
  257. icon: 'success'
  258. });
  259. }
  260. });
  261. };
  262. // 联系客服
  263. const contactService = () => {
  264. uni.showToast({
  265. title: '联系客服',
  266. icon: 'none'
  267. });
  268. };
  269. // 取消订单
  270. const cancelOrder = () => {
  271. uni.showModal({
  272. title: '确认取消',
  273. content: '确定要取消此订单吗?',
  274. success: (res) => {
  275. if (res.confirm) {
  276. uni.showToast({
  277. title: '订单已取消',
  278. icon: 'success'
  279. });
  280. }
  281. }
  282. });
  283. };
  284. // 申诉订单
  285. const appealOrder = () => {
  286. uni.showToast({
  287. title: '提交申诉',
  288. icon: 'none'
  289. });
  290. };
  291. // 删除订单
  292. const deleteOrder = () => {
  293. uni.showModal({
  294. title: '确认删除',
  295. content: '确定要删除此订单吗?删除后无法恢复',
  296. success: (res) => {
  297. if (res.confirm) {
  298. uni.showToast({
  299. title: '订单已删除',
  300. icon: 'success'
  301. });
  302. }
  303. }
  304. });
  305. };
  306. // 页面加载时,从 store 中读取订单详情
  307. onLoad(() => {
  308. const cache = cardStore.orderDetail;
  309. if (cache && typeof cache === 'object') {
  310. Object.assign(orderDetail, cache);
  311. } else {
  312. uni.showToast({
  313. title: '暂无订单数据',
  314. icon: 'none'
  315. });
  316. setTimeout(() => {
  317. uni.navigateBack();
  318. }, 800);
  319. }
  320. });
  321. // 离开页面时清空订单详情
  322. onUnload(() => {
  323. cardStore.clearOrderDetail();
  324. });
  325. </script>
  326. <style scoped lang="scss">
  327. @import "@/uni.scss";
  328. .page-wrapper {
  329. padding: 0;
  330. }
  331. .order-detail-page {
  332. background-color: #f9fafb;
  333. padding-bottom: px2rpx(80);
  334. }
  335. /* Header */
  336. .header {
  337. background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
  338. padding: px2rpx(12) px2rpx(16);
  339. padding-top: calc(px2rpx(12) + env(safe-area-inset-top));
  340. }
  341. .header-nav {
  342. display: flex;
  343. align-items: center;
  344. justify-content: space-between;
  345. }
  346. .back-btn {
  347. width: px2rpx(40);
  348. height: px2rpx(40);
  349. display: flex;
  350. align-items: center;
  351. justify-content: center;
  352. }
  353. .header-title {
  354. color: #ffffff;
  355. font-size: px2rpx(18);
  356. }
  357. .header-action {
  358. width: px2rpx(40);
  359. }
  360. /* Content */
  361. .content {
  362. padding: px2rpx(16);
  363. }
  364. /* Status Card */
  365. .status-card {
  366. background-color: #ffffff;
  367. border-radius: px2rpx(16);
  368. padding: px2rpx(32) px2rpx(24);
  369. margin-bottom: px2rpx(16);
  370. display: flex;
  371. flex-direction: column;
  372. align-items: center;
  373. }
  374. .status-icon-wrapper {
  375. margin-bottom: px2rpx(16);
  376. }
  377. .status-icon {
  378. width: px2rpx(80);
  379. height: px2rpx(80);
  380. border-radius: 50%;
  381. display: flex;
  382. align-items: center;
  383. justify-content: center;
  384. }
  385. .status-icon-success {
  386. background-color: #f0fdf4;
  387. }
  388. .status-icon-processing {
  389. background-color: #fefce8;
  390. }
  391. .status-icon-failed {
  392. background-color: #fef2f2;
  393. }
  394. .status-icon-cancelled {
  395. background-color: #f9fafb;
  396. }
  397. .status-title {
  398. font-size: px2rpx(22);
  399. color: #111827;
  400. margin-bottom: px2rpx(8);
  401. }
  402. .status-subtitle {
  403. font-size: px2rpx(14);
  404. color: #6b7280;
  405. text-align: center;
  406. }
  407. /* Section Card */
  408. .section-card {
  409. background-color: #ffffff;
  410. border-radius: px2rpx(12);
  411. padding: px2rpx(16);
  412. margin-bottom: px2rpx(16);
  413. }
  414. .section-header {
  415. display: flex;
  416. align-items: center;
  417. gap: px2rpx(8);
  418. margin-bottom: px2rpx(16);
  419. }
  420. .section-title {
  421. font-size: px2rpx(16);
  422. color: #111827;
  423. }
  424. /* Approval Timeline */
  425. .approval-timeline {
  426. display: flex;
  427. flex-direction: column;
  428. }
  429. .timeline-item {
  430. display: flex;
  431. gap: px2rpx(12);
  432. }
  433. .timeline-left {
  434. display: flex;
  435. flex-direction: column;
  436. align-items: center;
  437. flex-shrink: 0;
  438. }
  439. .timeline-dot {
  440. width: px2rpx(24);
  441. height: px2rpx(24);
  442. border-radius: 50%;
  443. background-color: #f3f4f6;
  444. border: 2px solid #e5e7eb;
  445. display: flex;
  446. align-items: center;
  447. justify-content: center;
  448. flex-shrink: 0;
  449. }
  450. .timeline-dot-active {
  451. background-color: #22c55e;
  452. border-color: #22c55e;
  453. }
  454. .timeline-dot-current {
  455. background-color: #eab308;
  456. border-color: #eab308;
  457. animation: pulse 2s infinite;
  458. }
  459. @keyframes pulse {
  460. 0%,
  461. 100% {
  462. opacity: 1;
  463. }
  464. 50% {
  465. opacity: 0.7;
  466. }
  467. }
  468. .timeline-line {
  469. width: px2rpx(2);
  470. flex: 1;
  471. background-color: #e5e7eb;
  472. margin: px2rpx(4) 0;
  473. }
  474. .timeline-right {
  475. flex: 1;
  476. padding-bottom: px2rpx(24);
  477. }
  478. .timeline-header {
  479. display: flex;
  480. align-items: center;
  481. justify-content: space-between;
  482. margin-bottom: px2rpx(6);
  483. }
  484. .timeline-title {
  485. font-size: px2rpx(15);
  486. color: #111827;
  487. }
  488. .timeline-status {
  489. display: flex;
  490. align-items: center;
  491. gap: px2rpx(4);
  492. padding: px2rpx(2) px2rpx(8);
  493. border-radius: px2rpx(12);
  494. }
  495. .timeline-status.completed {
  496. background-color: #f0fdf4;
  497. }
  498. .timeline-status.current {
  499. background-color: #fefce8;
  500. }
  501. .timeline-status.pending {
  502. background-color: #f9fafb;
  503. }
  504. .timeline-status-text {
  505. font-size: px2rpx(12);
  506. }
  507. .timeline-status.completed .timeline-status-text {
  508. color: #22c55e;
  509. }
  510. .timeline-status.current .timeline-status-text {
  511. color: #eab308;
  512. }
  513. .timeline-status.pending .timeline-status-text {
  514. color: #9ca3af;
  515. }
  516. .timeline-operator {
  517. font-size: px2rpx(13);
  518. color: #6b7280;
  519. display: block;
  520. margin-bottom: px2rpx(4);
  521. }
  522. .timeline-time {
  523. font-size: px2rpx(12);
  524. color: #9ca3af;
  525. display: block;
  526. margin-bottom: px2rpx(4);
  527. }
  528. .timeline-remark {
  529. font-size: px2rpx(13);
  530. color: #6b7280;
  531. display: block;
  532. margin-top: px2rpx(6);
  533. padding: px2rpx(8);
  534. background-color: #f9fafb;
  535. border-radius: px2rpx(6);
  536. }
  537. /* Info List */
  538. .info-list {
  539. display: flex;
  540. flex-direction: column;
  541. gap: px2rpx(12);
  542. }
  543. .info-row {
  544. display: flex;
  545. align-items: center;
  546. justify-content: space-between;
  547. gap: px2rpx(12);
  548. }
  549. .info-row.vertical {
  550. flex-direction: column;
  551. align-items: flex-start;
  552. }
  553. .info-label {
  554. font-size: px2rpx(14);
  555. color: #6b7280;
  556. flex-shrink: 0;
  557. }
  558. .info-value {
  559. font-size: px2rpx(14);
  560. color: #111827;
  561. text-align: right;
  562. word-break: break-all;
  563. }
  564. .info-value-wrapper {
  565. display: flex;
  566. align-items: center;
  567. gap: px2rpx(8);
  568. flex: 1;
  569. justify-content: flex-end;
  570. }
  571. .amount-highlight {
  572. font-size: px2rpx(20);
  573. color: #2563eb;
  574. }
  575. .total-label {
  576. font-size: px2rpx(15);
  577. color: #111827;
  578. }
  579. .total-value {
  580. font-size: px2rpx(18);
  581. color: #ef4444;
  582. }
  583. .remark-text {
  584. text-align: left;
  585. color: #6b7280;
  586. line-height: 1.6;
  587. }
  588. .divider {
  589. height: px2rpx(1);
  590. background-color: #f3f4f6;
  591. margin: px2rpx(4) 0;
  592. }
  593. /* Service Card */
  594. .service-card {
  595. background-color: #ffffff;
  596. border-radius: px2rpx(12);
  597. padding: px2rpx(16);
  598. display: flex;
  599. align-items: center;
  600. gap: px2rpx(12);
  601. margin-bottom: px2rpx(16);
  602. }
  603. .service-text {
  604. flex: 1;
  605. font-size: px2rpx(15);
  606. color: #111827;
  607. }
  608. /* Bottom Actions */
  609. .bottom-actions {
  610. position: fixed;
  611. bottom: 0;
  612. left: 0;
  613. right: 0;
  614. background-color: #ffffff;
  615. border-top: 1px solid #e5e7eb;
  616. padding: px2rpx(12) px2rpx(16);
  617. padding-bottom: calc(px2rpx(12) + env(safe-area-inset-bottom));
  618. display: flex;
  619. gap: px2rpx(12);
  620. }
  621. .action-btn {
  622. flex: 1;
  623. height: px2rpx(44);
  624. border-radius: px2rpx(8);
  625. display: flex;
  626. align-items: center;
  627. justify-content: center;
  628. }
  629. .cancel-btn {
  630. background-color: #f3f4f6;
  631. }
  632. .cancel-btn .btn-text {
  633. color: #6b7280;
  634. }
  635. .appeal-btn {
  636. background-color: #2563eb;
  637. }
  638. .appeal-btn .btn-text {
  639. color: #ffffff;
  640. }
  641. .delete-btn {
  642. background-color: #f3f4f6;
  643. }
  644. .delete-btn .btn-text {
  645. color: #ef4444;
  646. }
  647. .btn-text {
  648. font-size: px2rpx(15);
  649. }
  650. .currency {
  651. font-size: px2rpx(12);
  652. }
  653. </style>