metalExchange.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. <template>
  2. <view class="withdraw">
  3. <view class="withdrawContent">
  4. <view class="tabs">
  5. <view
  6. v-for="item in tabsList"
  7. :key="item.key"
  8. class="tabs-item"
  9. :class="[tabsIndex === item.key ? 'active' : '']"
  10. @click="tabsChange(item)"
  11. >
  12. {{ item.title }}
  13. </view>
  14. </view>
  15. <!-- 提料克重输入模块 -->
  16. <view class="gold-box">
  17. <view class="gold-item">
  18. <view class="header">
  19. <h3 class="title">提料克重</h3>
  20. </view>
  21. <view class="input-box">
  22. <input
  23. v-model.number="extract"
  24. placeholder="请输入克数"
  25. type="digit"
  26. class="t-input"
  27. @input="onKeyInput"
  28. />
  29. </view>
  30. <view class="infoMoney" style="font-size: 16px">
  31. <view v-if="is_out">
  32. <text class="info-money-num" style="color: #ff1e0f">
  33. 输入克重超过可提现克重,账户克重{{ accountWeight }}克
  34. </text>
  35. </view>
  36. <view v-else-if="is_lowest">
  37. <text class="info-money-num" style="color: #ff1e0f">
  38. 最低{{ lowest }}克起兑换,账户克重{{
  39. accountWeight
  40. }}克,且最多两位小数
  41. </text>
  42. </view>
  43. <view v-else class="infoMoneyNum">
  44. <view> 手续费:{{ totalFee }}元,{{ feePerGram }}元/g </view>
  45. <view>账户克重:{{ accountWeight }}克</view>
  46. </view>
  47. <!-- <view class="infoTip">*默认融成小圆饼寄出</view> -->
  48. </view>
  49. <view class="remark-box">
  50. <view class="header">
  51. <h3 class="title">备注</h3>
  52. </view>
  53. <view class="input-box" style="margin-top: 20rpx">
  54. <input
  55. v-model="remark"
  56. placeholder="请输入备注(如特殊需求、收货说明等)"
  57. type="text"
  58. class="t-input"
  59. maxlength="50"
  60. />
  61. </view>
  62. </view>
  63. </view>
  64. </view>
  65. <view class="order-list" @click="gotoOrderList">
  66. 订单/预约列表 {{ ">" }}
  67. </view>
  68. <!-- 提交按钮 -->
  69. <view class="withdraw-bottom">
  70. <view
  71. :class="['submitBtn', is_post ? '' : 'submitBtnActive']"
  72. style="margin-top: 10px"
  73. :style="{
  74. opacity: isSubmitting ? 0.7 : 1,
  75. pointerEvents: isSubmitting ? 'none' : 'auto',
  76. }"
  77. >
  78. <button @click="handleShowModel">
  79. <text v-if="!isSubmitting">前往预约</text>
  80. <text v-if="isSubmitting">提交中...</text>
  81. </button>
  82. </view>
  83. </view>
  84. <view>
  85. <up-parse :content="content"></up-parse>
  86. </view>
  87. </view>
  88. <!-- 预约日期选择弹窗 -->
  89. <appointmentCalendar
  90. ref="calendarRef"
  91. :metalType="tabsIndex"
  92. :reservedWeight="extract"
  93. @confirm="onDateConfirm"
  94. />
  95. </view>
  96. </template>
  97. <script setup>
  98. import { ref, computed, nextTick, watch } from "vue";
  99. import { onLoad } from "@dcloudio/uni-app";
  100. import { storeToRefs } from 'pinia';
  101. // 导入API/组件/Store
  102. import { quotaByWeight, createReservation } from "@/api/vault";
  103. import appointmentCalendar from "@/components/appointmentCalendar";
  104. import { useAppStore } from "@/stores/app";
  105. import { agreementGetoneApi } from "@/api/user";
  106. import { toLogin } from "@/libs/login.js";
  107. // 初始化Store
  108. const appStore = useAppStore();
  109. const { wxConfig, isWxConfigReady } = storeToRefs(appStore);
  110. // 核心响应式数据
  111. const tabsList = ref([
  112. { key: 1, label: "gold", title: "黄金" },
  113. { key: 2, label: "platinum", title: "铂金" },
  114. { key: 3, label: "silver", title: "白银" },
  115. ]);
  116. const tabsIndex = ref(1);
  117. const extract = ref(0);
  118. const is_out = ref(false);
  119. const is_lowest = ref(false);
  120. const isSubmitting = ref(false);
  121. const metalConfigs = ref([]);
  122. const remark = ref("");
  123. // 日历预约数据
  124. const calendarRef = ref(null);
  125. const selectedAppointmentDate = ref(null);
  126. // 金属类型映射
  127. const typeMap = { 1: "au", 2: "pt", 3: "ag" };
  128. // 获取协议
  129. const content = ref("");
  130. function agreementGetoneFn() {
  131. // 资产说明
  132. agreementGetoneApi({ name: "metal_exchange_content" }).then((res) => {
  133. content.value = res.data?.content;
  134. });
  135. }
  136. // 动态计算参数
  137. const feePerGram = computed(() => {
  138. if (!metalConfigs.value.length) return "0.00";
  139. const currentType = typeMap[tabsIndex.value];
  140. const currentConfig = metalConfigs.value.find(
  141. (item) => item.metalType === currentType
  142. );
  143. return currentConfig?.feePerGram
  144. ? Number(currentConfig.feePerGram).toFixed(2)
  145. : "0.00";
  146. });
  147. const lowest = computed(() => {
  148. if (!metalConfigs.value.length) return 0;
  149. const currentType = typeMap[tabsIndex.value];
  150. const currentConfig = metalConfigs.value.find(
  151. (item) => item.metalType === currentType
  152. );
  153. return currentConfig?.minimumWithdrawalWeight
  154. ? Number(currentConfig.minimumWithdrawalWeight)
  155. : 0;
  156. });
  157. const totalFee = computed(() => {
  158. const fee = Number(feePerGram.value);
  159. const weight = Number(extract.value);
  160. return (fee * weight).toFixed(2);
  161. });
  162. const accountWeight = computed(() => {
  163. const weightMap = {
  164. 1: appStore.userInfo.goldBalance || 0,
  165. 2: appStore.userInfo.ptBalance || 0,
  166. 3: appStore.userInfo.agBalance || 0,
  167. };
  168. return Number(weightMap[tabsIndex.value]).toFixed(2);
  169. });
  170. // 页面加载
  171. onLoad(() => {
  172. if (appStore.isLogin) {
  173. agreementGetoneFn();
  174. metalConfigs.value = appStore.wxConfig?.metalConfigs || [];
  175. } else {
  176. toLogin();
  177. }
  178. });
  179. // 监听wxConfig就绪
  180. watch(
  181. isWxConfigReady,
  182. (isReady) => {
  183. if (isReady) {
  184. metalConfigs.value = appStore.wxConfig?.metalConfigs || [];
  185. }
  186. },
  187. { immediate: true }
  188. );
  189. // 切换金属类型
  190. const tabsChange = (item) => {
  191. tabsIndex.value = item.key;
  192. onKeyInput();
  193. };
  194. // 输入克重验证
  195. const onKeyInput = () => {
  196. is_lowest.value = false;
  197. is_out.value = false;
  198. if (extract.value === null || extract.value === "" || isNaN(extract.value)) {
  199. extract.value = 0;
  200. is_lowest.value = true;
  201. return;
  202. }
  203. const inputWeight = Number(extract.value);
  204. const minWeight = lowest.value;
  205. const accountWeightNum = Number(accountWeight.value);
  206. const isBelowMin = inputWeight < minWeight;
  207. const hasMoreDecimals = /\.\d{3,}$/.test(extract.value.toString());
  208. if (isBelowMin || hasMoreDecimals) is_lowest.value = true;
  209. if (inputWeight > accountWeightNum) is_out.value = true;
  210. };
  211. // 订单列表跳转
  212. const gotoOrderList = () => {
  213. uni.navigateTo({
  214. url: "/pages/users/vault/storeMetal/metalExchangeList",
  215. });
  216. };
  217. // 参数校验
  218. const validateSubmitParams = () => {
  219. if (!appStore.isLogin) return { valid: false, msg: "请先登录" };
  220. if (is_out.value || is_lowest.value || extract.value <= 0)
  221. return { valid: false, msg: "请输入有效克重" };
  222. return { valid: true };
  223. };
  224. // 点击提交按钮
  225. const handleShowModel = async () => {
  226. if (!appStore.userInfo.realNameVerified) {
  227. uni.showToast({ title: "请先进行实名认证", icon: "none", duration: 2000 });
  228. uni.navigateTo({ url: "/pages/users/face_detect/index" });
  229. return;
  230. }
  231. const check = validateSubmitParams();
  232. if (!check.valid) {
  233. uni.showToast({ title: check.msg, icon: "none", duration: 2000 });
  234. return;
  235. }
  236. calendarRef.value?.open();
  237. };
  238. // 日期确认回调
  239. const onDateConfirm = async (date) => {
  240. const params = {
  241. metalType: tabsIndex.value,
  242. reservationDate: date,
  243. reservedWeight: extract.value,
  244. remark: remark.value,
  245. };
  246. try {
  247. isSubmitting.value = true;
  248. uni.showLoading({ title: "提交预约...", mask: true });
  249. await createReservation(params);
  250. uni.hideLoading();
  251. uni.showToast({ title: "预约成功", icon: "success" });
  252. setTimeout(() => {
  253. gotoOrderList();
  254. }, 1000);
  255. extract.value = 0;
  256. } catch (err) {
  257. isSubmitting.value = false;
  258. uni.hideLoading();
  259. uni.showToast({ title: err || "提交失败,请重试", icon: "none" });
  260. } finally {
  261. isSubmitting.value = false;
  262. }
  263. };
  264. </script>
  265. <style lang="scss" scoped>
  266. $item-value-color: #dca537;
  267. $card-bcolor: #f8f8f8;
  268. page {
  269. min-height: 95vh;
  270. background-color: #f7f7f7;
  271. }
  272. .tabs {
  273. display: flex;
  274. height: 80rpx;
  275. border-radius: 40rpx;
  276. padding: 0 20rpx;
  277. color: #fff;
  278. font-size: 38rpx;
  279. font-weight: 300;
  280. .tabs-item {
  281. width: 50%;
  282. height: 100%;
  283. display: flex;
  284. justify-content: center;
  285. position: relative;
  286. color: #000000;
  287. }
  288. .active::after {
  289. position: absolute;
  290. bottom: 18rpx;
  291. left: 50%;
  292. transform: translateX(-50%);
  293. content: "";
  294. z-index: 20;
  295. display: block;
  296. width: 65rpx;
  297. height: 6rpx;
  298. background-color: #f8c007;
  299. }
  300. }
  301. .order-list {
  302. margin-top: 50rpx;
  303. text-align: center;
  304. color: #767676;
  305. font-size: 30rpx;
  306. }
  307. .withdraw-bottom {
  308. width: 100%;
  309. margin-top: 10rpx;
  310. margin-bottom: 100rpx;
  311. display: flex;
  312. justify-content: center;
  313. }
  314. .header {
  315. padding-left: 5px;
  316. position: relative;
  317. display: flex;
  318. align-items: center;
  319. justify-content: space-between;
  320. padding: 10px 10px;
  321. border-radius: 5px;
  322. font-size: 18px;
  323. .live-gold {
  324. font-weight: 0;
  325. font-size: 30rpx;
  326. .price {
  327. color: #dcbe81;
  328. margin-left: 10rpx;
  329. }
  330. }
  331. .item {
  332. display: flex;
  333. align-items: center;
  334. margin-top: 15px;
  335. .targe {
  336. display: flex;
  337. justify-content: center;
  338. align-items: center;
  339. color: #fff;
  340. width: 35px;
  341. height: 35px;
  342. border-radius: 50%;
  343. background-color: #cc9933;
  344. }
  345. .address {
  346. width: 440rpx;
  347. margin: 0 10px;
  348. font-size: 28rpx;
  349. .receive-address {
  350. letter-spacing: 9px;
  351. }
  352. }
  353. .end {
  354. display: flex;
  355. justify-content: center;
  356. align-items: center;
  357. .copy {
  358. color: #888888;
  359. border-radius: 3px;
  360. border: 1px solid #888888;
  361. font-size: 24rpx;
  362. padding: 6rpx 23rpx;
  363. }
  364. }
  365. }
  366. &::before {
  367. position: absolute;
  368. top: 50%;
  369. transform: translatey(-50%);
  370. left: 0;
  371. content: "";
  372. width: 2px;
  373. height: 15px;
  374. background-color: #daa520;
  375. }
  376. .title {
  377. font-weight: 500;
  378. font-size: 32rpx;
  379. font-family: "黑体";
  380. }
  381. }
  382. .gold-box {
  383. padding: 30rpx 20rpx;
  384. }
  385. .gold-item {
  386. margin: 0 0 25rpx 0;
  387. }
  388. .input-box {
  389. display: flex;
  390. background-color: #ededed;
  391. border-radius: 5px;
  392. height: 90rpx;
  393. align-items: center;
  394. justify-content: space-around;
  395. font-size: 28rpx;
  396. padding-left: 20rpx;
  397. margin-top: 50rpx;
  398. input {
  399. width: 90%;
  400. }
  401. }
  402. .address {
  403. width: 690rpx;
  404. max-height: 180rpx;
  405. margin: 40rpx 0;
  406. padding: 28rpx;
  407. box-sizing: border-box;
  408. border-radius: 30rpx;
  409. .addressCon {
  410. width: 596rpx;
  411. font-size: 26rpx;
  412. color: #666;
  413. .name {
  414. font-size: 30rpx;
  415. color: #282828;
  416. font-weight: bold;
  417. margin-bottom: 10rpx;
  418. .phone {
  419. margin-left: 50rpx;
  420. }
  421. }
  422. .default {
  423. margin-right: 12rpx;
  424. }
  425. .setaddress {
  426. color: #333;
  427. font-size: 28rpx;
  428. }
  429. }
  430. .iconfont {
  431. font-size: 35rpx;
  432. color: #707070;
  433. }
  434. }
  435. .content-top {
  436. border-radius: 30rpx;
  437. background: $card-bcolor;
  438. margin-bottom: 20rpx;
  439. .section {
  440. .section-title {
  441. font-size: 32rpx;
  442. font-weight: bold;
  443. margin-bottom: 24rpx;
  444. position: relative;
  445. padding-left: 20rpx;
  446. &::after {
  447. position: absolute;
  448. top: 0;
  449. left: 0;
  450. height: 100%;
  451. width: 3px;
  452. background: $item-value-color;
  453. content: "";
  454. }
  455. }
  456. .courier-list {
  457. display: flex;
  458. justify-content: space-between;
  459. padding-bottom: 16rpx;
  460. .courier-item {
  461. display: flex;
  462. flex-direction: column;
  463. align-items: center;
  464. min-width: 160rpx;
  465. margin-right: 24rpx;
  466. padding: 24rpx 20rpx;
  467. border: 2rpx solid #eee;
  468. border-radius: 12rpx;
  469. cursor: pointer;
  470. position: relative;
  471. .gou {
  472. position: absolute;
  473. width: 40rpx;
  474. height: 40rpx;
  475. right: -7rpx;
  476. bottom: -7rpx;
  477. border-radius: 50%;
  478. right: -6rpx;
  479. bottom: -6rpx;
  480. }
  481. &.active {
  482. border-color: #dbb870;
  483. }
  484. .courier-logo {
  485. width: 120rpx;
  486. height: 120rpx;
  487. margin-bottom: 8rpx;
  488. }
  489. .courier-name {
  490. font-size: 26rpx;
  491. }
  492. }
  493. }
  494. }
  495. }
  496. .withdraw {
  497. height: 100%;
  498. background-size: 100% 40%;
  499. background: $uni-bg-primary !important;
  500. .withdrawContent {
  501. height: 90%;
  502. padding: 45rpx 40rpx;
  503. background-color: #f7f7f7;
  504. top: 150rpx;
  505. position: relative;
  506. border-radius: 50rpx 50rpx 0 0;
  507. }
  508. .withdrawBody {
  509. background-color: #fff;
  510. padding: 20px 30px;
  511. font-size: 14px;
  512. .inputMoney {
  513. display: flex;
  514. align-items: center;
  515. justify-content: center;
  516. font-weight: 600;
  517. border-bottom: 1px solid #eaeef1;
  518. .rmb {
  519. font-size: 16px;
  520. }
  521. .tInput {
  522. height: 1.9em;
  523. font-size: 2.5em;
  524. border: none;
  525. position: relative;
  526. left: 3.5%;
  527. outline: none;
  528. }
  529. }
  530. }
  531. }
  532. .infoMoney {
  533. margin-top: 10px;
  534. font-size: 12px;
  535. margin-bottom: 20px;
  536. .infoMoneyNum {
  537. color: #b2b2b2;
  538. font-size: 26rpx;
  539. }
  540. .infoTip {
  541. color: red;
  542. font-size: 23rpx;
  543. margin-top: 10rpx;
  544. }
  545. }
  546. .agreement {
  547. display: flex;
  548. align-items: center;
  549. justify-content: center;
  550. padding: 10px;
  551. .chooseIcon {
  552. width: 16px;
  553. height: 16px;
  554. }
  555. .agreementText {
  556. font-size: 14px;
  557. margin-left: 10px;
  558. color: #999999;
  559. .agreementLink {
  560. color: #dca12b;
  561. }
  562. }
  563. }
  564. .submitBtn {
  565. button {
  566. background-color: #dca12b;
  567. color: #fff;
  568. width: 380rpx;
  569. height: 72rpx;
  570. display: flex;
  571. font-size: 30rpx;
  572. justify-content: center;
  573. align-items: center;
  574. border-radius: 30rpx;
  575. }
  576. }
  577. .submitBtnActive {
  578. button {
  579. color: #fff;
  580. background: #ffe079;
  581. box-shadow: 0 10rpx 8rpx rgba(207, 6, 6, 0.05);
  582. }
  583. }
  584. .signContent {
  585. background-color: #f8f8f8;
  586. padding: 20px;
  587. box-sizing: border-box;
  588. display: flex;
  589. justify-content: center;
  590. align-items: center;
  591. flex-direction: column;
  592. border-radius: 20px 20px 0 0;
  593. .scrollView {
  594. background-color: #fff;
  595. padding: 4px;
  596. height: 300px;
  597. overflow-y: hidden;
  598. border: 1px solid #dfdfdf;
  599. }
  600. .confirmBtn {
  601. margin-top: 10px;
  602. color: #fff;
  603. padding: 4px 20px;
  604. border-radius: 20px;
  605. background: linear-gradient(to right, #8ed187, #5dd665);
  606. }
  607. }
  608. </style>