withdraw-detail.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. <template>
  2. <cwg-page-wrapper>
  3. <view class="order-detail-page">
  4. <view class="content">
  5. <view class="section-card">
  6. <view class="success-icon-wrap">
  7. <image v-if="detailData.transactionStatus == 3" src="/static/images/vector.png" alt="" mode="widthFix" />
  8. <image v-else-if="detailData.transactionStatus == 4 || detailData.transactionStatus == 5"
  9. src="/static/images/vector2.png" alt="" mode="widthFix" />
  10. <image v-else src="/static/images/vector3.png" alt="" mode="widthFix" />
  11. <text class="success-text">{{ t(withdrawStatus[detailData.transactionStatus]) }}</text>
  12. <text v-if="approveDesc" class="success-text">{{ approveDesc }}</text>
  13. </view>
  14. </view>
  15. <view class="section-card">
  16. <view class="section-header">
  17. <uni-icons type="wallet" size="18" color="#2563eb" />
  18. <text class="section-title">{{ t('Ib.Report.Title3') }}</text>
  19. </view>
  20. <view class="info-list">
  21. <view class="info-row" v-if="detailData.merchantOrderNo">
  22. <text class="info-label">{{ t('card.Form.f35') }}</text>
  23. <view class="info-value-wrapper">
  24. <text class="info-value">{{ detailData.merchantOrderNo }}</text>
  25. <cwg-icon name="copy" :size="14" color="#9ca3af" @click.stop="copyOrderNo" />
  26. </view>
  27. </view>
  28. <view class="info-row">
  29. <text class="info-label">{{ t('WalletApply.p3') }}</text>
  30. <text class="info-value amount-highlight">
  31. {{ detailData.amount }} USDT
  32. </text>
  33. </view>
  34. <view class="info-row">
  35. <text class="info-label">{{ t('card.Form.f31') }}</text>
  36. <text class="info-value">
  37. {{ detailData.receivedAmount || '0' }}
  38. <text class="currency">{{ detailData.receivedCurrency }}</text>
  39. </text>
  40. </view>
  41. <view class="info-row">
  42. <text class="info-label">{{ t('WalletApply.p8') }}</text>
  43. <text class="info-value">
  44. {{ detailData.contractAddress }}
  45. </text>
  46. </view>
  47. <view class="info-row">
  48. <text class="info-label">{{ t('WalletApply.p9') }}</text>
  49. <text class="info-value">
  50. {{ detailData.fromAddress }}
  51. </text>
  52. </view>
  53. <view class="info-row">
  54. <text class="info-label">{{ t('WalletApply.p1') }}</text>
  55. <text class="info-value">
  56. {{ formatOrderNo(detailData.address) }}
  57. <cwg-icon class="footer-order-icon" name="copy" :size="14" color="#9ca3af"
  58. @click.stop="copyOrderNo(detailData.address)" />
  59. </text>
  60. </view>
  61. <view class="divider"></view>
  62. <view class="info-row">
  63. <text class="info-label">{{ t('card.Form.f27') }}</text>
  64. <text class="info-value">
  65. <text v-if="detailData.remark">{{ detailData.remark }}</text>
  66. <text v-else-if="detailData.note">{{ detailData.note }}</text>
  67. <text v-else-if="approveDesc">{{ approveDesc }}</text>
  68. <text v-else>--</text>
  69. </text>
  70. </view>
  71. <view class="info-row">
  72. <text class="info-label">{{ t('Label.ApplyTime') }}</text>
  73. <text class="info-value">
  74. <text>{{ detailData.addTime }}</text>
  75. </text>
  76. </view>
  77. <view class="info-row" v-if="detailData.approveTime">
  78. <text class="info-label">{{ t('WalletApply.p10') }}</text>
  79. <text class="info-value">
  80. <text>{{ detailData.approveTime }}</text>
  81. </text>
  82. </view>
  83. <view class="info-row">
  84. <text class="info-label">{{ t('global.p22') }}</text>
  85. <text class="info-value">
  86. <text>{{ t(withdrawApprovalText[detailData.status]) }}</text>
  87. </text>
  88. </view>
  89. <view class="info-row">
  90. <text class="info-label">{{ t('card.Form.f45') }}</text>
  91. <text class="info-value">
  92. <text>{{ t(withdrawStatus[detailData.transactionStatus]) }}</text>
  93. </text>
  94. </view>
  95. </view>
  96. </view>
  97. </view>
  98. <view class="bottom-actions" v-if="(detailData.transactionStatus == 1 && detailData.transactionStatus != 5)">
  99. <view class="action-btn cancel-btn" @click="cancelOrder">
  100. <text class="btn-text">{{ t('global.GlobalOrder.CancelOrder') }}</text>
  101. </view>
  102. </view>
  103. </view>
  104. </cwg-page-wrapper>
  105. </template>
  106. <script setup lang="ts">
  107. import { ref, watch, nextTick, computed } from 'vue'
  108. import { onLoad, onUnload } from '@dcloudio/uni-app';
  109. import { useI18n } from 'vue-i18n';
  110. import useCardStore from '@/stores/use-card-store';
  111. import { ucardApi } from "@/api/ucard";
  112. import { withdrawApprovalText, withdrawStatus } from '@/utils/dataMap';
  113. import useUserStore from "@/stores/use-user-store";
  114. const { t, locale } = useI18n();
  115. const userStore = useUserStore();
  116. const approveDesc = ref('');
  117. const getApproveDesc = () => {
  118. const d = detailData.value.approveDesc
  119. if (!d) return
  120. const c = userStore.reasonsOptions
  121. const a = c[d || '']
  122. const b = locale.value == 'cn' || locale.value == 'zhHant' ? a.content : a.enContent
  123. if (!b) {
  124. reasonsRefusalList()
  125. }
  126. approveDesc.value = b;
  127. }
  128. const formatOrderNo = (orderNo?: string) => {
  129. if (!orderNo) return '--';
  130. if (orderNo.length <= 16) return orderNo;
  131. return orderNo.slice(0, 6) + '...' + orderNo.slice(-4);
  132. };
  133. const copyOrderNo = (orderNo?: string) => {
  134. if (!orderNo) return;
  135. uni.setClipboardData({
  136. data: orderNo,
  137. success: () => {
  138. uni.showToast({
  139. title: t('card.Msg.m8') || '复制成功',
  140. icon: 'success'
  141. });
  142. }
  143. });
  144. };
  145. async function reasonsRefusalList() {
  146. try {
  147. const res = await ucardApi.reasonsRefusalList();
  148. if (res.code === 200) {
  149. pickFields(res.data);
  150. getApproveDesc()
  151. } else {
  152. uni.$u.toast(res.msg || t("login.msg0"));
  153. }
  154. } catch (error) {
  155. console.log(error, 111);
  156. }
  157. }
  158. function pickFields(source, fields = ['content', 'enContent']) {
  159. const result = {}
  160. Object.entries(source).forEach(([key, value]) => {
  161. result[key] = fields.reduce((acc, f) => {
  162. acc[f] = value[f] ?? null
  163. return acc
  164. }, {})
  165. })
  166. userStore.saveReasonsOptions(result);
  167. }
  168. const cardStore = useCardStore();
  169. // 取消订单
  170. const cancelOrder = () => {
  171. uni.showModal({
  172. title: t('Msg.SystemPrompt'),
  173. cancelText: t('common.cancel'),
  174. confirmText: t('common.confirm'),
  175. content: t('global.GlobalOrder.ConfirmCancelOrder'),
  176. success: async (e) => {
  177. if (e.confirm) {
  178. const res = await ucardApi.getBlockchainWithdrawCancel({ id: detailData.value.id })
  179. if (res.code == 200) {
  180. // 刷新订单详情
  181. uni.showToast({
  182. title: t('global.GlobalOrder.CancelOrderSuccess'),
  183. icon: 'success'
  184. });
  185. detailData.value.transactionStatus = 5;
  186. }
  187. }
  188. }
  189. });
  190. };
  191. const detailData = ref<any>({})
  192. // 页面加载时,先使用缓存数据渲染,然后获取最新数据
  193. onLoad((e) => {
  194. const cachedData = cardStore.orderDetail
  195. if (cachedData) {
  196. try {
  197. const clonedData = JSON.parse(JSON.stringify(cachedData))
  198. Object.assign(detailData.value, clonedData)
  199. } catch (error) {
  200. console.error('加载缓存数据失败:', error)
  201. }
  202. }
  203. });
  204. // 离开页面时清空订单详情
  205. onUnload(() => {
  206. cardStore.clearOrderDetail();
  207. });
  208. </script>
  209. <style scoped lang="scss">
  210. @import "@/uni.scss";
  211. .page-wrapper {
  212. padding: 0;
  213. }
  214. .success-icon-wrap {
  215. display: flex;
  216. justify-content: center;
  217. align-items: center;
  218. flex-wrap: wrap;
  219. background-color: #ffffff;
  220. image {
  221. width: 100%;
  222. }
  223. .success-text {
  224. width: 100%;
  225. text-align: center;
  226. font-size: px2rpx(18);
  227. color: #111827;
  228. margin-bottom: px2rpx(24);
  229. }
  230. }
  231. .order-detail-page {
  232. background-color: #f9fafb;
  233. padding-bottom: px2rpx(80);
  234. }
  235. /* Header */
  236. .header {
  237. background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
  238. padding: px2rpx(12) px2rpx(16);
  239. padding-top: calc(px2rpx(12) + env(safe-area-inset-top));
  240. }
  241. .header-nav {
  242. display: flex;
  243. align-items: center;
  244. justify-content: space-between;
  245. }
  246. .back-btn {
  247. width: px2rpx(40);
  248. height: px2rpx(40);
  249. display: flex;
  250. align-items: center;
  251. justify-content: center;
  252. }
  253. .header-title {
  254. color: #ffffff;
  255. font-size: px2rpx(18);
  256. }
  257. .header-action {
  258. width: px2rpx(40);
  259. }
  260. /* Content */
  261. .content {
  262. padding: px2rpx(16);
  263. }
  264. /* Status Card */
  265. .status-card {
  266. background-color: #ffffff;
  267. border-radius: px2rpx(16);
  268. padding: px2rpx(32) px2rpx(24);
  269. margin-bottom: px2rpx(16);
  270. display: flex;
  271. flex-direction: column;
  272. align-items: center;
  273. }
  274. .status-icon-wrapper {
  275. margin-bottom: px2rpx(16);
  276. }
  277. .status-icon {
  278. width: px2rpx(80);
  279. height: px2rpx(80);
  280. border-radius: 50%;
  281. display: flex;
  282. align-items: center;
  283. justify-content: center;
  284. }
  285. .status-icon-success {
  286. background-color: #f0fdf4;
  287. }
  288. .status-icon-processing {
  289. background-color: #fefce8;
  290. }
  291. .status-icon-failed {
  292. background-color: #fef2f2;
  293. }
  294. .status-icon-cancelled {
  295. background-color: #f9fafb;
  296. }
  297. .status-title {
  298. font-size: px2rpx(22);
  299. color: #111827;
  300. margin-bottom: px2rpx(8);
  301. }
  302. .status-subtitle {
  303. font-size: px2rpx(14);
  304. color: #6b7280;
  305. text-align: center;
  306. }
  307. /* Section Card */
  308. .section-card {
  309. background-color: #ffffff;
  310. border-radius: px2rpx(12);
  311. padding: px2rpx(16);
  312. margin-bottom: px2rpx(16);
  313. }
  314. .section-header {
  315. display: flex;
  316. align-items: center;
  317. gap: px2rpx(8);
  318. margin-bottom: px2rpx(16);
  319. }
  320. .section-title {
  321. font-size: px2rpx(16);
  322. color: #111827;
  323. }
  324. /* Approval Timeline */
  325. .approval-timeline {
  326. display: flex;
  327. flex-direction: column;
  328. }
  329. .timeline-item {
  330. display: flex;
  331. gap: px2rpx(12);
  332. }
  333. .timeline-left {
  334. display: flex;
  335. flex-direction: column;
  336. align-items: center;
  337. flex-shrink: 0;
  338. }
  339. .timeline-dot {
  340. width: px2rpx(24);
  341. height: px2rpx(24);
  342. border-radius: 50%;
  343. background-color: #f3f4f6;
  344. border: 2px solid #e5e7eb;
  345. display: flex;
  346. align-items: center;
  347. justify-content: center;
  348. flex-shrink: 0;
  349. }
  350. .timeline-dot-active {
  351. background-color: #22c55e;
  352. border-color: #22c55e;
  353. }
  354. .timeline-dot-current {
  355. background-color: #eab308;
  356. border-color: #eab308;
  357. animation: pulse 2s infinite;
  358. }
  359. @keyframes pulse {
  360. 0%,
  361. 100% {
  362. opacity: 1;
  363. }
  364. 50% {
  365. opacity: 0.7;
  366. }
  367. }
  368. .timeline-line {
  369. width: px2rpx(2);
  370. flex: 1;
  371. background-color: #e5e7eb;
  372. margin: px2rpx(4) 0;
  373. }
  374. .timeline-right {
  375. flex: 1;
  376. padding-bottom: px2rpx(24);
  377. }
  378. .timeline-header {
  379. display: flex;
  380. align-items: center;
  381. justify-content: space-between;
  382. margin-bottom: px2rpx(6);
  383. }
  384. .timeline-title {
  385. font-size: px2rpx(15);
  386. color: #111827;
  387. }
  388. .timeline-status {
  389. display: flex;
  390. align-items: center;
  391. gap: px2rpx(4);
  392. padding: px2rpx(2) px2rpx(8);
  393. border-radius: px2rpx(12);
  394. }
  395. .timeline-status.completed {
  396. background-color: #f0fdf4;
  397. }
  398. .timeline-status.current {
  399. background-color: #fefce8;
  400. }
  401. .timeline-status.pending {
  402. background-color: #f9fafb;
  403. }
  404. .timeline-status-text {
  405. font-size: px2rpx(12);
  406. }
  407. .timeline-status.completed .timeline-status-text {
  408. color: #22c55e;
  409. }
  410. .timeline-status.current .timeline-status-text {
  411. color: #eab308;
  412. }
  413. .timeline-status.pending .timeline-status-text {
  414. color: #9ca3af;
  415. }
  416. .timeline-operator {
  417. font-size: px2rpx(13);
  418. color: #6b7280;
  419. display: block;
  420. margin-bottom: px2rpx(4);
  421. }
  422. .timeline-time {
  423. font-size: px2rpx(12);
  424. color: #9ca3af;
  425. display: block;
  426. margin-bottom: px2rpx(4);
  427. }
  428. .timeline-remark {
  429. font-size: px2rpx(13);
  430. color: #6b7280;
  431. display: block;
  432. margin-top: px2rpx(6);
  433. padding: px2rpx(8);
  434. background-color: #f9fafb;
  435. border-radius: px2rpx(6);
  436. }
  437. /* Info List */
  438. .info-list {
  439. display: flex;
  440. flex-direction: column;
  441. gap: px2rpx(12);
  442. }
  443. .info-row {
  444. display: flex;
  445. align-items: center;
  446. justify-content: space-between;
  447. gap: px2rpx(12);
  448. padding: px2rpx(4) 0;
  449. }
  450. .info-row.vertical {
  451. flex-direction: column;
  452. align-items: flex-start;
  453. }
  454. .info-label {
  455. font-size: px2rpx(14);
  456. color: #6b7280;
  457. flex-shrink: 0;
  458. }
  459. .info-value {
  460. font-size: px2rpx(14);
  461. color: #111827;
  462. text-align: right;
  463. word-break: break-all;
  464. }
  465. .info-value-wrapper {
  466. display: flex;
  467. align-items: center;
  468. gap: px2rpx(8);
  469. flex: 1;
  470. justify-content: flex-end;
  471. }
  472. .amount-highlight {
  473. font-size: px2rpx(20);
  474. color: #2563eb;
  475. }
  476. .total-label {
  477. font-size: px2rpx(15);
  478. color: #111827;
  479. }
  480. .total-value {
  481. font-size: px2rpx(18);
  482. color: #ef4444;
  483. }
  484. .remark-text {
  485. text-align: left;
  486. color: #6b7280;
  487. line-height: 1.6;
  488. }
  489. .divider {
  490. height: px2rpx(1);
  491. background-color: #f3f4f6;
  492. margin: px2rpx(4) 0;
  493. }
  494. /* Service Card */
  495. .service-card {
  496. background-color: #ffffff;
  497. border-radius: px2rpx(12);
  498. padding: px2rpx(16);
  499. display: flex;
  500. align-items: center;
  501. gap: px2rpx(12);
  502. margin-bottom: px2rpx(16);
  503. }
  504. .service-text {
  505. flex: 1;
  506. font-size: px2rpx(15);
  507. color: #111827;
  508. }
  509. /* Bottom Actions */
  510. .bottom-actions {
  511. position: fixed;
  512. bottom: 0;
  513. left: 0;
  514. right: 0;
  515. background-color: #ffffff;
  516. border-top: 1px solid #e5e7eb;
  517. padding: px2rpx(12) px2rpx(16);
  518. padding-bottom: calc(px2rpx(12) + env(safe-area-inset-bottom));
  519. display: flex;
  520. gap: px2rpx(12);
  521. }
  522. .action-btn {
  523. flex: 1;
  524. height: px2rpx(44);
  525. border-radius: px2rpx(8);
  526. display: flex;
  527. align-items: center;
  528. justify-content: center;
  529. }
  530. .cancel-btn {
  531. background-color: #f3f4f6;
  532. }
  533. .cancel-btn .btn-text {
  534. color: #6b7280;
  535. }
  536. .appeal-btn {
  537. background-color: #2563eb;
  538. }
  539. .appeal-btn .btn-text {
  540. color: #ffffff;
  541. }
  542. .delete-btn {
  543. background-color: #f3f4f6;
  544. }
  545. .delete-btn .btn-text {
  546. color: #ef4444;
  547. }
  548. .btn-text {
  549. font-size: px2rpx(15);
  550. }
  551. .currency {
  552. font-size: px2rpx(12);
  553. }
  554. </style>