operations.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. <template>
  2. <cwg-page-wrapper :isHeaderFixed="true">
  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" :rules="rules" :model="infoForm" 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')}:`" rulesKey="cardNumber1" @change="handleChange" />
  13. <cwg-input v-model:value="infoForm.activeCode" fkey="activeCode" :required="true"
  14. :label="`${t('card.Form.f26')}:`" rulesKey="activeCode" @change="handleChange" />
  15. <cwg-input v-model:value="infoForm.pin" fkey="pin" :required="true" :maxlength="6" :label="t('card.Info.s26')"
  16. rulesKey="pin" @change="handleChange" />
  17. <cwg-input v-model:value="infoForm.password" fkey="password" :required="true" :maxlength="6"
  18. :label="`${t('card.Info.s26_1')}`" rulesKey="password" @change="handleChange" />
  19. <view class="pwd">
  20. <view class="lis" v-t="'card.vaildate.v32'" :class="{ fit: rule1 }"></view>
  21. <view class="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')}:`" rulesKey="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')}:`" rulesKey="cardNumber1" @change="handleChange" />
  52. <cwg-input v-model:value="infoForm.pin" fkey="pin" :required="true" :label="t('card.Info.s26')" :maxlength="6"
  53. rulesKey="pin" @change="handleChange" />
  54. <cwg-input v-model:value="infoForm.password" fkey="password" :required="true" :maxlength="6"
  55. :label="`${t('card.Info.s26_1')}`" rulesKey="password" @change="handleChange" />
  56. <view class="pwd">
  57. <view class="lis" v-t="'card.vaildate.v32'" :class="{ fit: rule1 }"></view>
  58. <view class="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')}:`" rulesKey="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(btn[type])
  71. }}</u-button>
  72. </view>
  73. </view>
  74. </u-form>
  75. </view>
  76. <cwg-SuccessPrompt v-model:show="showSuccessPrompt" :title="t(title[type])" :desc="t('card.Msg.m2')"
  77. :btn-click="btnClick" />
  78. </cwg-page-wrapper>
  79. </template>
  80. <script setup lang="ts">
  81. import { ref, onMounted, watch, computed } from "vue";
  82. import { showToast } from "@/utils/toast";
  83. import { useI18n } from "vue-i18n";
  84. import useRouter from "@/hooks/useRouter";
  85. import { onLoad } from '@dcloudio/uni-app'
  86. import { ucardApi } from "@/api/ucard";
  87. import config from "@/config";
  88. import { Validators } from "@/utils/validators";
  89. const { t } = useI18n();
  90. const router = useRouter();
  91. const form = ref({});
  92. const formRef = ref(null);
  93. const infoForm = ref({
  94. password: undefined,
  95. cardNumber1: undefined,
  96. cardNo: undefined,
  97. activeCode: undefined,
  98. pin: undefined,
  99. amount: undefined,
  100. clientRemark: undefined,
  101. });
  102. const title = ref({
  103. 1: t('card.Btn.b1'),
  104. 2: t('card.Btn.b3'),
  105. 3: t('card.Btn.b4'),
  106. 4: t('card.Btn.b5'),
  107. 5: t('card.Btn.b6')
  108. })
  109. const btn = ref({
  110. 1: t('card.Btn.Activate'),
  111. 2: t('card.Btn.Recharge'),
  112. 3: t('card.Btn.Submit'),
  113. 4: t('card.Btn.Freeze'),
  114. 5: t('card.Btn.Unfreeze')
  115. })
  116. const type = ref()
  117. const id = ref()
  118. onLoad((options) => {
  119. id.value = options.id
  120. type.value = options.type
  121. })
  122. const showSuccessPrompt = ref(false);
  123. const btnClick = () => {
  124. router.push('/pages/card/index');
  125. };
  126. // 动态限制
  127. const exchangeRate = ref(0);
  128. const rechargeMinQuota = ref(0);
  129. const rechargeMaxQuota = ref(0);
  130. const rechargeFixedFee = ref(0);
  131. const userBalance = ref(0);
  132. const pictLoading = ref(false);
  133. function validatePin(s) {
  134. if (!/^\d{6}$/.test(s)) return false;
  135. if (/(.)\1\1/.test(s)) return false;
  136. const isSequential = (str) => {
  137. let inc = true;
  138. let dec = true;
  139. for (let i = 1; i < str.length; i++) {
  140. if (str[i] - str[i - 1] !== 1) inc = false;
  141. if (str[i - 1] - str[i] !== 1) dec = false;
  142. }
  143. return inc || dec;
  144. };
  145. if (isSequential(s)) return false;
  146. if (/^(\d{2})\1\1$/.test(s)) return false;
  147. if (/^(\d{3})\1$/.test(s)) return false;
  148. return true;
  149. }
  150. function validatePina(a: any, b?: any, c?: any) {
  151. if (typeof c === "function") {
  152. const value = b;
  153. const callback = c;
  154. const val = String(value ?? "").trim();
  155. if (validatePin(val)) {
  156. return callback();
  157. } else {
  158. return callback(new Error(t("card.vaildate.v23")));
  159. }
  160. }
  161. }
  162. function validatePassword(a: any, b?: any, c?: any) {
  163. if (typeof c === "function") {
  164. const value = b;
  165. const callback = c;
  166. const val = String(value ?? "").trim();
  167. if (validatePin(val)) {
  168. if (formData.value.pin != formData.value.password) {
  169. callback(new Error(t("vaildate.password.same")));
  170. return;
  171. } else {
  172. return callback();
  173. }
  174. } else {
  175. callback(new Error(t("vaildate.password.same")));
  176. }
  177. }
  178. }
  179. const rules = {
  180. pin: [
  181. Validators.required(t("card.vaildate.v23")),
  182. Validators.custom(validatePina),
  183. ],
  184. password: [
  185. Validators.required(t("card.vaildate.v31")),
  186. Validators.custom(validatePassword)
  187. ],
  188. cardNumber1: [
  189. {
  190. required: true,
  191. message: t("card.vaildate.v41"),
  192. trigger: "blur",
  193. },
  194. ],
  195. amount: [
  196. {
  197. required: true,
  198. message: t("card.vaildate.v26"),
  199. trigger: "blur",
  200. },
  201. {
  202. validator: (rule, value, callback) => validateAmount(value, callback),
  203. trigger: "blur",
  204. },
  205. ],
  206. activeCode: [
  207. {
  208. required: true,
  209. message: t("card.vaildate.v24"),
  210. trigger: "blur",
  211. },
  212. ],
  213. };
  214. const formData = ref<typeof infoForm.value>({ id } as any);
  215. const headerTitleMap: Record<number, string> = {
  216. 1: "card.tab10",
  217. 2: "card.tab11",
  218. 3: "card.tab12",
  219. 4: "card.tab13",
  220. 5: "card.tab14",
  221. };
  222. // 根据 type 自动取标题
  223. const maxRecharge = computed(() => {
  224. const balance = Number(userBalance.value) || 0;
  225. const rate = Number(exchangeRate.value) / 100;
  226. if (balance <= 0) return 0;
  227. return Math.floor((balance / (1 + rate)) * 100) / 100;
  228. });
  229. const fee = computed(() => {
  230. if (!formData.value.amount) {
  231. return 0;
  232. }
  233. if (rechargeFixedFee.value) {
  234. return rechargeFixedFee.value;
  235. }
  236. const amount = Math.ceil(formData.value.amount);
  237. return ((amount * exchangeRate.value) / 100).toFixed(2);
  238. });
  239. function goRechargeRecord() {
  240. router.push(`/pages/recharge-record/list?cardNo=${formData.value?.cardNo}`);
  241. }
  242. // 计算属性 rule1
  243. const rule1 = computed(() => {
  244. if (!formData.value.pin) return false;
  245. return /^(\d)\d{5}$/.test(formData.value.pin);
  246. });
  247. // 计算属性 rule2
  248. const rule2 = computed(() => {
  249. if (!formData.value.pin) return false;
  250. return config.Pattern.Pin.test(formData.value.pin);
  251. });
  252. async function infoSubmit() {
  253. if (formData.value.cardNumber1 != infoForm.value.cardNumber) {
  254. showToast(t("card.vaildate.v41"));
  255. return;
  256. }
  257. if (pictLoading.value) {
  258. return
  259. }
  260. pictLoading.value = true;
  261. try {
  262. switch (type.value) {
  263. case "1":
  264. await formRef.value?.validate();
  265. ucardActivate();
  266. break;
  267. case "2":
  268. await formRef.value?.validate();
  269. ucardRecharge();
  270. break;
  271. case "3":
  272. await formRef.value?.validate();
  273. ucardResetPassword();
  274. break;
  275. case "4":
  276. await formRef.value?.validate();
  277. ucardFreeze();
  278. break;
  279. case "5":
  280. await formRef.value?.validate();
  281. ucardUnfreeze();
  282. break;
  283. }
  284. } catch (error) {
  285. pictLoading.value = false;
  286. }
  287. }
  288. function backActivity() {
  289. setTimeout(() => {
  290. router.back();
  291. }, 3000);
  292. }
  293. async function ucardActivate() {
  294. if (formData.value.pin != formData.value.password) {
  295. showToast(t("card.Msg.m11"));
  296. return;
  297. }
  298. const res = await ucardApi.ucardActivate(formData.value);
  299. if (res.code == 200) {
  300. showToast(t("card.Msg.m3"));
  301. backActivity();
  302. } else {
  303. showToast(res.msg);
  304. }
  305. pictLoading.value = false;
  306. }
  307. async function ucardResetPassword() {
  308. const res = await ucardApi.ucardResetPassword(formData.value);
  309. if (res.code == 200) {
  310. showToast(t("card.Msg.m6"));
  311. backActivity();
  312. } else {
  313. showToast(res.msg);
  314. }
  315. pictLoading.value = false;
  316. }
  317. async function ucardFreeze() {
  318. const res = await ucardApi.ucardFreeze(formData.value);
  319. if (res.code == 200) {
  320. showToast(t("card.Msg.m4"));
  321. backActivity();
  322. } else {
  323. showToast(res.msg);
  324. }
  325. pictLoading.value = false;
  326. }
  327. async function ucardUnfreeze() {
  328. const res = await ucardApi.ucardUnfreeze(formData.value);
  329. if (res.code == 200) {
  330. showToast(t("card.Msg.m5"));
  331. backActivity();
  332. } else {
  333. showToast(res.msg);
  334. }
  335. pictLoading.value = false;
  336. }
  337. async function getCardInfo() {
  338. try {
  339. if (!id) return;
  340. const res = await ucardApi.getCardInfo({ id: id.value });
  341. // 只更新后端返回的字段,避免整体替换导致输入被清空
  342. Object.assign(infoForm.value, res.data);
  343. Object.assign(formData.value, res.data);
  344. exchangeRate.value = res.data.rechargeFeeRate;
  345. rechargeMaxQuota.value = res.data.rechargeMaxQuota;
  346. rechargeMinQuota.value = res.data.rechargeMinQuota;
  347. rechargeFixedFee.value = res.data.rechargeFixedFee;
  348. } catch (error) {
  349. console.log(error);
  350. }
  351. }
  352. // 一键填充最大金额
  353. function allBalance() {
  354. infoForm.value.amount = maxRecharge.value;
  355. }
  356. // 金额验证函数(配合 Vant <Field /> 的 rules)
  357. function validateAmount(value: string | number) {
  358. const num = Number(value);
  359. if (!num || num <= 0) {
  360. return t("card.vaildate.v34"); // 请输入有效金额
  361. } else if (maxRecharge.value === 0) {
  362. return `${t("card.Form.f56")} 0 USD`;
  363. } else if (num > maxRecharge.value) {
  364. return `${t("card.vaildate.v35")} ${maxRecharge.value} USD`;
  365. } else if (num < rechargeMinQuota.value) {
  366. return `${t("card.vaildate.v36")} ${rechargeMinQuota.value} USD`;
  367. } else if (num >= rechargeMaxQuota.value) {
  368. return `${t("card.vaildate.v37")} ${rechargeMaxQuota.value} USD`;
  369. }
  370. return true;
  371. }
  372. // 获取钱包余额
  373. async function walletBalance() {
  374. try {
  375. const res = await ucardApi.walletBalance();
  376. if (res.code === 200) {
  377. userBalance.value = res.data.balance || 0;
  378. } else {
  379. showToast(res.msg);
  380. userBalance.value = 0;
  381. }
  382. } catch (err) {
  383. userBalance.value = 0;
  384. }
  385. }
  386. // 银行卡充值
  387. async function ucardRecharge() {
  388. const amount = Number(formData.value.amount);
  389. const cardNo = formData.value.cardNo;
  390. if (validateAmount(amount) !== true) {
  391. showToast(validateAmount(amount));
  392. return;
  393. }
  394. try {
  395. const res = await ucardApi.ucardRecharge({ amount, cardNo });
  396. if (res.code === 200) {
  397. // showToast(t("card.Msg.m2"));
  398. showSuccessPrompt.value = true;
  399. } else {
  400. showToast(res.msg);
  401. }
  402. } catch (err) {
  403. console.log(err);
  404. } finally {
  405. pictLoading.value = false;
  406. }
  407. }
  408. onMounted(() => {
  409. getCardInfo();
  410. if (type.value == "2") {
  411. walletBalance();
  412. }
  413. });
  414. function handleChange(value: any) {
  415. formData.value = { ...formData.value, [value.key]: value.value };
  416. }
  417. </script>
  418. <style scoped lang="scss">
  419. @import "@/uni.scss";
  420. .pointer-none {
  421. pointer-events: none;
  422. }
  423. .f {
  424. display: flex;
  425. align-items: flex-end;
  426. gap: px2rpx(12);
  427. .l {
  428. flex: 1;
  429. }
  430. .r {
  431. width: px2rpx(273);
  432. }
  433. }
  434. .pwd {
  435. line-height: px2rpx(20);
  436. .lis {
  437. list-style-type: disc;
  438. line-height: 1.2;
  439. margin: px2rpx(4) 0 px2rpx(4) px2rpx(10);
  440. color: #c0c4cc;
  441. position: relative;
  442. &::after {
  443. content: '';
  444. position: absolute;
  445. left: px2rpx(-6);
  446. top: px2rpx(6);
  447. width: px2rpx(4);
  448. height: px2rpx(4);
  449. border-radius: 50%;
  450. background: #c0c4cc;
  451. }
  452. }
  453. .fit {
  454. color: var(--main-yellow);
  455. &::after {
  456. background: var(--main-yellow);
  457. }
  458. }
  459. }
  460. .balance-info {
  461. width: 100%;
  462. color: #bdbdbd;
  463. font-size: px2rpx(14);
  464. margin-bottom: px2rpx(44);
  465. display: flex;
  466. align-items: center;
  467. /* justify-content: space-between; */
  468. }
  469. .balance-value {
  470. width: px2rpx(140);
  471. color: var(--white);
  472. font-weight: bold;
  473. margin: 0 px2rpx(8);
  474. }
  475. .balance-key {
  476. color: #8e8a8a;
  477. width: px2rpx(130);
  478. }
  479. .all-btn {
  480. min-width: px2rpx(30);
  481. display: flex;
  482. padding: px2rpx(4) px2rpx(12);
  483. justify-content: center;
  484. align-items: center;
  485. gap: px2rpx(10);
  486. border-radius: px2rpx(35);
  487. border: 1px solid #ea002a;
  488. color: var(--Brand-color, #ea002a);
  489. font-family: Roboto;
  490. font-size: px2rpx(14);
  491. font-style: normal;
  492. font-weight: 600;
  493. line-height: px2rpx(20);
  494. letter-spacing: px2rpx(0.07);
  495. }
  496. </style>