operations.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. <template>
  2. <cwg-page-wrapper>
  3. <cwg-header :showBack="true" :title="title[type]">
  4. <view v-if="type == 2" @click="goRechargeRecord">
  5. <cwg-icon name="icon_history" :size="24" color="#000" />
  6. </view>
  7. </cwg-header>
  8. <view class="page page-shadow">
  9. <u-form ref="formRef" class="kyc-form">
  10. <template v-if="type == '1'">
  11. <cwg-input v-model:value="infoForm.cardNumber1" fkey="cardNumber1" :required="true"
  12. :label="`${t('card.Form.f24')}:`" :rules="rules.cardNumber1" @change="handleChange" />
  13. <cwg-input v-model:value="infoForm.activeCode" fkey="activeCode" :required="true"
  14. :label="`${t('card.Form.f26')}:`" :rules="rules.activeCode" @change="handleChange" />
  15. <cwg-input v-model:value="infoForm.pin" fkey="pin" :required="true" :maxlength="6" :label="t('card.Info.s26')"
  16. :rules="rules.pin" @change="handleChange" />
  17. <cwg-input v-model:value="infoForm.password" fkey="password" :required="true" :maxlength="6"
  18. :label="`${t('card.Btn.Confirm')}:`" :rules="rules.password" @change="handleChange" />
  19. <view class="pwd">
  20. <view calss="lis" v-t="'card.vaildate.v32'" :class="{ fit: rule1 }"></view>
  21. <view calss="lis" v-t="'card.vaildate.v33'" :class="{ fit: rule2 }"></view>
  22. </view>
  23. </template>
  24. <template v-if="type == '2'">
  25. <cwg-input v-model:value="infoForm.cardNumber1" fkey="cardNumber1" :required="true"
  26. :label="`${t('card.Form.f24')}:`" :rules="rules.cardNumber1" @change="handleChange" />
  27. <cwg-input v-model:value="infoForm.amount" fkey="amount" type="number" :label="`${t('card.Form.f28')}:`"
  28. :required="true" :min="rechargeMinQuota" :max="rechargeMaxQuota" :placeholder="t('card.vaildate.v26')"
  29. @change="handleChange">
  30. </cwg-input>
  31. <view class="balance-info">
  32. <text class="balance-key">{{ t("card.Form.f56") }}</text>
  33. <text class="balance-value">{{ userBalance }} USD</text>
  34. <view class="all-btn" @click="allBalance">{{
  35. t("card.Form.f57")
  36. }}</view>
  37. </view>
  38. <view class="balance-info">
  39. <text class="balance-key">{{ t("card.Form.f58") }}</text>
  40. <text class="balance-value">{{ exchangeRate }}%</text>
  41. <text></text>
  42. </view>
  43. <view class="balance-info">
  44. <text class="balance-key">{{ t("card.Form.f59") }}</text>
  45. <text class="balance-value">{{ fee }}</text>
  46. <text></text>
  47. </view>
  48. </template>
  49. <template v-if="type == '3'">
  50. <cwg-input v-model:value="infoForm.cardNumber1" fkey="cardNumber1" :required="true"
  51. :label="`${t('card.Form.f24')}:`" :rules="rules.cardNumber1" @change="handleChange" />
  52. <cwg-input v-model:value="infoForm.pin" fkey="pin" :required="true" :label="t('card.Info.s26')" :maxlength="6"
  53. :rules="rules.pin" @change="handleChange" />
  54. <cwg-input v-model:value="infoForm.password" fkey="password" :required="true" :maxlength="6"
  55. :label="`${t('card.Btn.Confirm')}:`" :rules="rules.password" @change="handleChange" />
  56. <view class="pwd">
  57. <view calss="lis" v-t="'card.vaildate.v32'" :class="{ fit: rule1 }"></view>
  58. <view calss="lis" v-t="'card.vaildate.v33'" :class="{ fit: rule2 }"></view>
  59. </view>
  60. </template>
  61. <template v-if="type == '4' || type == '5'">
  62. <cwg-input v-model:value="infoForm.cardNumber1" fkey="cardNumber1" :required="true"
  63. :label="`${t('card.Form.f24')}:`" :rules="rules.cardNumber1" @change="handleChange" />
  64. <cwg-input v-model:value="infoForm.clientRemark" fkey="clientRemark" :label="`${t('card.Form.f27')}:`"
  65. @change="handleChange" />
  66. </template>
  67. <view v-if="infoForm.authStatus != 1" class="fixed-btn">
  68. <view class="cwg-button">
  69. <u-button type="primary" block @click="infoSubmit">{{
  70. t("card.Btn.Submit")
  71. }}</u-button>
  72. </view>
  73. </view>
  74. </u-form>
  75. </view>
  76. </cwg-page-wrapper>
  77. </template>
  78. <script setup lang="ts">
  79. import { ref, onMounted, watch, computed } from "vue";
  80. import { showToast } from "@/utils/toast";
  81. import { useI18n } from "vue-i18n";
  82. import useRouter from "@/hooks/useRouter";
  83. import { onLoad } from '@dcloudio/uni-app'
  84. import { ucardApi } from "@/api/ucard";
  85. import config from "@/config";
  86. const { t } = useI18n();
  87. const router = useRouter();
  88. const form = ref({});
  89. const formRef = ref();
  90. const infoForm = ref({
  91. password: undefined,
  92. cardNumber1: undefined,
  93. cardNo: undefined,
  94. activeCode: undefined,
  95. pin: undefined,
  96. amount: undefined,
  97. clientRemark: undefined,
  98. });
  99. const title = ref({
  100. 1: t('card.Btn.b1'),
  101. 2: t('card.Btn.b3'),
  102. 3: t('card.Btn.b4'),
  103. 4: t('card.Btn.b5'),
  104. 5: t('card.Btn.b6')
  105. })
  106. const type = ref()
  107. const id = ref()
  108. onLoad((options) => {
  109. id.value = options.id
  110. type.value = options.type
  111. })
  112. // 动态限制
  113. const exchangeRate = ref(0);
  114. const rechargeMinQuota = ref(0);
  115. const rechargeMaxQuota = ref(0);
  116. const rechargeFixedFee = ref(0);
  117. const userBalance = ref(0);
  118. const pictLoading = ref(false);
  119. const rules = {
  120. pin: [
  121. { required: true, message: t("card.vaildate.v23") },
  122. { pattern: config.Pattern.Pin, message: t("card.vaildate.v23") },
  123. ],
  124. password: [
  125. { required: true, message: t("card.vaildate.v31") },
  126. { pattern: config.Pattern.Pin, message: t("card.vaildate.v31") },
  127. ],
  128. cardNumber1: [
  129. {
  130. required: true,
  131. message: t("card.vaildate.v22"),
  132. trigger: "blur",
  133. },
  134. ],
  135. amount: [
  136. {
  137. required: true,
  138. message: t("card.vaildate.v26"),
  139. trigger: "blur",
  140. },
  141. {
  142. validator: (rule, value, callback) => validateAmount(value, callback),
  143. trigger: "blur",
  144. },
  145. ],
  146. activeCode: [
  147. {
  148. required: true,
  149. message: t("card.vaildate.v24"),
  150. trigger: "blur",
  151. },
  152. ],
  153. };
  154. const formData = ref<typeof infoForm.value>({ id } as any);
  155. const headerTitleMap: Record<number, string> = {
  156. 1: "card.tab10",
  157. 2: "card.tab11",
  158. 3: "card.tab12",
  159. 4: "card.tab13",
  160. 5: "card.tab14",
  161. };
  162. // 根据 type 自动取标题
  163. const maxRecharge = computed(() => {
  164. const balance = Number(userBalance.value) || 0;
  165. const rate = Number(exchangeRate.value) / 100;
  166. if (balance <= 0) return 0;
  167. return Math.floor((balance / (1 + rate)) * 100) / 100;
  168. });
  169. const fee = computed(() => {
  170. if (!formData.value.amount) {
  171. return 0;
  172. }
  173. if (rechargeFixedFee.value) {
  174. return rechargeFixedFee.value;
  175. }
  176. const amount = Math.ceil(formData.value.amount);
  177. return ((amount * exchangeRate.value) / 100).toFixed(2);
  178. });
  179. function goRechargeRecord() {
  180. router.push(`/pages/recharge-record/list?cardNo=${formData.value?.cardNo}`);
  181. }
  182. // 根据 type 自动取标题
  183. const headerTitle = computed(() => {
  184. const key = Number(type.value);
  185. const i18nKey = headerTitleMap[key];
  186. return i18nKey ? t(i18nKey) : t("common.unknown");
  187. });
  188. const showBack = computed(() => {
  189. return ["/", "/cards", "/finance", "/mine", "/login"].includes(route.path);
  190. });
  191. // 计算属性 rule1
  192. const rule1 = computed(() => {
  193. if (!formData.value.pin) return false;
  194. return /^(\d)\d{5}$/.test(formData.value.pin);
  195. });
  196. // 计算属性 rule2
  197. const rule2 = computed(() => {
  198. if (!formData.value.pin) return false;
  199. return config.Pattern.Pin.test(formData.value.pin);
  200. });
  201. async function infoSubmit() {
  202. if (formData.value.cardNumber1 != infoForm.value.cardNumber) {
  203. showToast(t("card.vaildate.v22"));
  204. return;
  205. }
  206. try {
  207. switch (type.value) {
  208. case "1":
  209. await formRef.value?.validate(["cardNumber1", "pin", "activeCode"]);
  210. ucardActivate();
  211. break;
  212. case "2":
  213. await formRef.value?.validate(["cardNumber1", "amount"]);
  214. ucardRecharge();
  215. break;
  216. case "3":
  217. await formRef.value?.validate(["cardNumber1", "pin"]);
  218. ucardResetPassword();
  219. break;
  220. case "4":
  221. await formRef.value?.validate(["cardNumber1"]);
  222. ucardFreeze();
  223. break;
  224. case "5":
  225. await formRef.value?.validate(["cardNumber1"]);
  226. ucardUnfreeze();
  227. break;
  228. }
  229. } catch (error) { }
  230. }
  231. function backActivity() {
  232. setTimeout(() => {
  233. router.back();
  234. }, 3000);
  235. }
  236. async function ucardActivate() {
  237. if (formData.value.pin != formData.value.password) {
  238. showToast(t("card.Msg.m11"));
  239. return;
  240. }
  241. const res = await ucardApi.ucardActivate(formData.value);
  242. if (res.code == 200) {
  243. showToast(t("card.Msg.m3"));
  244. backActivity();
  245. } else {
  246. showToast(res.msg);
  247. }
  248. }
  249. async function ucardResetPassword() {
  250. if (formData.value.pin != formData.value.password) {
  251. showToast(t("card.Msg.m11"));
  252. return;
  253. }
  254. const res = await ucardApi.ucardResetPassword(formData.value);
  255. if (res.code == 200) {
  256. showToast(t("card.Msg.m6"));
  257. backActivity();
  258. } else {
  259. showToast(res.msg);
  260. }
  261. }
  262. async function ucardFreeze() {
  263. const res = await ucardApi.ucardFreeze(formData.value);
  264. if (res.code == 200) {
  265. showToast(t("card.Msg.m4"));
  266. backActivity();
  267. } else {
  268. showToast(res.msg);
  269. }
  270. }
  271. async function ucardUnfreeze() {
  272. const res = await ucardApi.ucardUnfreeze(formData.value);
  273. if (res.code == 200) {
  274. showToast(t("card.Msg.m5"));
  275. backActivity();
  276. } else {
  277. showToast(res.msg);
  278. }
  279. }
  280. async function getCardInfo() {
  281. try {
  282. if (!id) return;
  283. const res = await ucardApi.getCardInfo({ id: id.value });
  284. infoForm.value = res.data;
  285. formData.value = res.data;
  286. exchangeRate.value = res.data.rechargeFeeRate;
  287. rechargeMaxQuota.value = res.data.rechargeMaxQuota;
  288. rechargeMinQuota.value = res.data.rechargeMinQuota;
  289. rechargeFixedFee.value = res.data.rechargeFixedFee;
  290. } catch (error) {
  291. console.log(error);
  292. }
  293. }
  294. // 一键填充最大金额
  295. function allBalance() {
  296. infoForm.value.amount = maxRecharge.value;
  297. }
  298. // 金额验证函数(配合 Vant <Field /> 的 rules)
  299. function validateAmount(value: string | number) {
  300. const num = Number(value);
  301. if (!num || num <= 0) {
  302. return t("card.vaildate.v34"); // 请输入有效金额
  303. } else if (maxRecharge.value === 0) {
  304. return `${t("card.Form.f56")} 0 USD`;
  305. } else if (num > maxRecharge.value) {
  306. return `${t("card.vaildate.v35")} ${maxRecharge.value} USD`;
  307. } else if (num < rechargeMinQuota.value) {
  308. return `${t("card.vaildate.v36")} ${rechargeMinQuota.value} USD`;
  309. } else if (num >= rechargeMaxQuota.value) {
  310. return `${t("card.vaildate.v37")} ${rechargeMaxQuota.value} USD`;
  311. }
  312. return true;
  313. }
  314. // 获取钱包余额
  315. async function walletBalance() {
  316. try {
  317. const res = await ucardApi.walletBalance();
  318. if (res.code === 200) {
  319. userBalance.value = res.data.balance || 0;
  320. } else {
  321. showToast(res.msg);
  322. userBalance.value = 0;
  323. }
  324. } catch (err) {
  325. userBalance.value = 0;
  326. }
  327. }
  328. // 银行卡充值
  329. async function ucardRecharge() {
  330. const amount = Number(formData.value.amount);
  331. const cardNo = formData.value.cardNo;
  332. if (validateAmount(amount) !== true) {
  333. showToast(validateAmount(amount));
  334. return;
  335. }
  336. pictLoading.value = true;
  337. try {
  338. const res = await ucardApi.ucardRecharge({ amount, cardNo });
  339. if (res.code === 200) {
  340. showToast(t("card.Msg.m2"));
  341. setTimeout(() => {
  342. router.push(`/cards`);
  343. }, 1000);
  344. } else {
  345. showToast(res.msg);
  346. }
  347. } catch (err) {
  348. console.log(err);
  349. } finally {
  350. pictLoading.value = false;
  351. }
  352. }
  353. onMounted(() => {
  354. getCardInfo();
  355. if (type.value == "2") {
  356. walletBalance();
  357. }
  358. });
  359. function handleChange(value: any) {
  360. formData.value = { ...formData.value, [value.key]: value.value };
  361. }
  362. </script>
  363. <style scoped lang="scss">
  364. @import "@/uni.scss";
  365. .pointer-none {
  366. pointer-events: none;
  367. }
  368. .f {
  369. display: flex;
  370. align-items: flex-end;
  371. gap: px2rpx(12);
  372. .l {
  373. flex: 1;
  374. }
  375. .r {
  376. width: px2rpx(273);
  377. }
  378. }
  379. .pwd {
  380. line-height: px2rpx(20);
  381. li {
  382. list-style-type: disc;
  383. line-height: 1.2;
  384. margin: px2rpx(4) 0 px2rpx(4) px2rpx(20);
  385. color: #c0c4cc;
  386. }
  387. .fit {
  388. color: #009deb;
  389. }
  390. }
  391. .balance-info {
  392. width: 100%;
  393. color: #bdbdbd;
  394. font-size: px2rpx(14);
  395. margin-bottom: px2rpx(44);
  396. display: flex;
  397. align-items: center;
  398. /* justify-content: space-between; */
  399. }
  400. .balance-value {
  401. width: px2rpx(140);
  402. color: var(--white);
  403. font-weight: bold;
  404. margin: 0 px2rpx(8);
  405. }
  406. .balance-key {
  407. color: #8e8a8a;
  408. width: px2rpx(130);
  409. }
  410. .all-btn {
  411. min-width: px2rpx(30);
  412. display: flex;
  413. padding: px2rpx(4) px2rpx(12);
  414. justify-content: center;
  415. align-items: center;
  416. gap: px2rpx(10);
  417. border-radius: px2rpx(35);
  418. border: 1px solid #ea002a;
  419. color: var(--Brand-color, #ea002a);
  420. font-family: Roboto;
  421. font-size: px2rpx(14);
  422. font-style: normal;
  423. font-weight: 600;
  424. line-height: px2rpx(20);
  425. letter-spacing: px2rpx(0.07);
  426. }
  427. </style>