pay.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749
  1. <template>
  2. <view class="pay-container">
  3. <!-- 顶部导航栏 -->
  4. <view class="nav-bar">
  5. <view class="nav-title">付款</view>
  6. </view>
  7. <!-- 支付金额区域 -->
  8. <view class="amount-section">
  9. <view class="amount-label">支付金额</view>
  10. <view class="amount-display">
  11. <text class="currency">¥</text>
  12. <text class="amount">{{ displayAmount }}</text>
  13. </view>
  14. <view class="amount-tip">请输入金额</view>
  15. </view>
  16. <!-- 支付方式展示 -->
  17. <view class="payment-methods">
  18. <view class="method-list">
  19. <view class="method-item active">
  20. <view class="method-icon alipay">
  21. <text class="iconfont icon-zhifubao"></text>
  22. </view>
  23. <view class="method-info">
  24. <text class="method-name">支付宝</text>
  25. <text class="method-desc">安全便捷的支付方式</text>
  26. </view>
  27. </view>
  28. </view>
  29. </view>
  30. <!-- 数字键盘 - 使用直接事件绑定,确保兼容性 -->
  31. <view class="keyboard-section">
  32. <view class="keyboard">
  33. <view class="keyboard-row" v-for="(row, rowIndex) in keyboardKeys" :key="rowIndex">
  34. <view
  35. class="key-item"
  36. v-for="(key, keyIndex) in row"
  37. :key="`${rowIndex}-${keyIndex}`"
  38. :class="{
  39. 'key-number': key.type === 'number',
  40. 'key-delete': key.type === 'delete',
  41. 'key-dot': key.type === 'dot',
  42. 'key-confirm': key.type === 'confirm'
  43. }"
  44. @click="handleKeyPress(key)"
  45. @touchstart="handleTouchStart"
  46. @touchend="handleTouchEnd"
  47. >
  48. <text v-if="key.type === 'delete'" class="iconfont icon-delete"></text>
  49. <text v-else-if="key.type === 'confirm'">确认</text>
  50. <text v-else>{{ key.value }}</text>
  51. </view>
  52. </view>
  53. </view>
  54. </view>
  55. <!-- 支付按钮 -->
  56. <view class="pay-button-section">
  57. <button
  58. class="pay-button"
  59. :class="{ disabled: !canPay }"
  60. :disabled="!canPay"
  61. @click="showConfirmDialog"
  62. >
  63. 支付
  64. </button>
  65. </view>
  66. <!-- 确认支付弹框 -->
  67. <view class="confirm-dialog" v-if="showConfirm" @click="hideConfirmDialog">
  68. <view class="dialog-content" @click.stop>
  69. <view class="dialog-header">
  70. <text class="dialog-title">提示</text>
  71. <view class="close-btn" @click="hideConfirmDialog">
  72. <text class="iconfont icon-close">×</text>
  73. </view>
  74. </view>
  75. <view class="dialog-body">
  76. <view class="confirm-amount-section">
  77. <text class="confirm-label">支付金额</text>
  78. <view class="confirm-amount-display">
  79. <text class="confirm-currency">¥</text>
  80. <text class="confirm-amount">{{ displayAmount }}</text>
  81. </view>
  82. </view>
  83. <view class="confirm-tips">
  84. <text class="tip-text">请确认金额无误</text>
  85. </view>
  86. </view>
  87. <view class="dialog-footer">
  88. <button class="cancel-btn" @click="hideConfirmDialog">取消</button>
  89. <button class="confirm-btn" @click="handlePay">确认</button>
  90. </view>
  91. </view>
  92. </view>
  93. </view>
  94. </template>
  95. <script>
  96. import { getGhsData } from '@/api/ghs';
  97. // 优化:使用常量避免重复创建
  98. const KEY_TYPES = {
  99. NUMBER: 'number',
  100. DELETE: 'delete',
  101. DOT: 'dot',
  102. CONFIRM: 'confirm'
  103. };
  104. const MAX_INTEGER_LENGTH = 8;
  105. const MAX_DECIMAL_LENGTH = 2;
  106. export default {
  107. name: 'pay',
  108. data() {
  109. return {
  110. currentAmount: '0',
  111. displayAmount: '0',
  112. // 优化:使用静态数据,避免重复创建
  113. keyboardKeys: [
  114. [
  115. { type: KEY_TYPES.NUMBER, value: '1' },
  116. { type: KEY_TYPES.NUMBER, value: '2' },
  117. { type: KEY_TYPES.NUMBER, value: '3' }
  118. ],
  119. [
  120. { type: KEY_TYPES.NUMBER, value: '4' },
  121. { type: KEY_TYPES.NUMBER, value: '5' },
  122. { type: KEY_TYPES.NUMBER, value: '6' }
  123. ],
  124. [
  125. { type: KEY_TYPES.NUMBER, value: '7' },
  126. { type: KEY_TYPES.NUMBER, value: '8' },
  127. { type: KEY_TYPES.NUMBER, value: '9' }
  128. ],
  129. [
  130. { type: KEY_TYPES.DOT, value: '.' },
  131. { type: KEY_TYPES.NUMBER, value: '0' },
  132. { type: KEY_TYPES.DELETE, value: '' }
  133. ]
  134. ],
  135. // 优化:使用更高效的状态管理
  136. amountBuffer: '0',
  137. updateScheduled: false,
  138. updateTimer: null,
  139. // 优化:缓存计算结果
  140. _cachedCanPay: false,
  141. _lastAmount: '0',
  142. // 弹框状态
  143. showConfirm: false,
  144. id:0,
  145. salt:''
  146. }
  147. },
  148. computed: {
  149. // 优化:使用缓存的计算属性
  150. canPay() {
  151. // 只有当金额变化时才重新计算
  152. if (this.currentAmount !== this._lastAmount) {
  153. const amount = parseFloat(this.currentAmount);
  154. this._cachedCanPay = amount > 0 && amount <= 99999999.99;
  155. this._lastAmount = this.currentAmount;
  156. }
  157. return this._cachedCanPay;
  158. }
  159. },
  160. onLoad(options) {
  161. if (options.id) {
  162. this.id = options.id;
  163. }
  164. if (options.salt) {
  165. this.salt = options.salt;
  166. }
  167. if (this.id && this.salt) {
  168. getGhsData({id:this.id,salt:this.salt}).then(res => {
  169. console.log(res)
  170. })
  171. }
  172. },
  173. methods: {
  174. // 优化:添加触摸反馈
  175. handleTouchStart(e) {
  176. const target = e.currentTarget;
  177. if (target && target.style) {
  178. target.style.transform = 'scale(0.95)';
  179. }
  180. },
  181. handleTouchEnd(e) {
  182. const target = e.currentTarget;
  183. if (target && target.style) {
  184. setTimeout(() => {
  185. target.style.transform = 'scale(1)';
  186. }, 100);
  187. }
  188. },
  189. // 优化:简化的键盘处理逻辑
  190. handleKeyPress(key) {
  191. // 防抖处理
  192. if (this.updateTimer) {
  193. clearTimeout(this.updateTimer);
  194. }
  195. if (key.type === KEY_TYPES.NUMBER) {
  196. this.addNumber(key.value);
  197. } else if (key.type === KEY_TYPES.DELETE) {
  198. this.deleteNumber();
  199. } else if (key.type === KEY_TYPES.DOT) {
  200. this.addDecimal();
  201. }
  202. // 使用 requestAnimationFrame 优化更新
  203. this.scheduleUpdate();
  204. },
  205. // 优化:使用 requestAnimationFrame 调度更新
  206. scheduleUpdate() {
  207. if (!this.updateScheduled) {
  208. this.updateScheduled = true;
  209. requestAnimationFrame(() => {
  210. this.updateDisplay();
  211. this.updateScheduled = false;
  212. });
  213. }
  214. },
  215. // 优化:更高效的数字添加逻辑
  216. addNumber(num) {
  217. const buffer = this.amountBuffer;
  218. // 快速路径:如果当前是0,直接替换
  219. if (buffer === '0') {
  220. this.amountBuffer = num;
  221. this.currentAmount = num;
  222. return;
  223. }
  224. // 检查长度限制
  225. const parts = buffer.split('.');
  226. const integerPart = parts[0];
  227. const decimalPart = parts[1];
  228. // 整数部分限制
  229. if (!decimalPart && integerPart.length >= MAX_INTEGER_LENGTH) {
  230. return;
  231. }
  232. // 小数部分限制
  233. if (decimalPart && decimalPart.length >= MAX_DECIMAL_LENGTH) {
  234. return;
  235. }
  236. // 添加数字
  237. this.amountBuffer = buffer + num;
  238. this.currentAmount = this.amountBuffer;
  239. },
  240. // 优化:更高效的小数点添加逻辑
  241. addDecimal() {
  242. if (!this.amountBuffer.includes('.')) {
  243. this.amountBuffer += '.';
  244. this.currentAmount = this.amountBuffer;
  245. }
  246. },
  247. // 优化:更高效的删除逻辑
  248. deleteNumber() {
  249. const buffer = this.amountBuffer;
  250. if (buffer.length > 1) {
  251. this.amountBuffer = buffer.slice(0, -1);
  252. } else {
  253. this.amountBuffer = '0';
  254. }
  255. this.currentAmount = this.amountBuffer;
  256. },
  257. // 优化:更高效的显示更新逻辑
  258. updateDisplay() {
  259. const amount = this.currentAmount;
  260. // 快速路径:如果是0,直接显示
  261. if (amount === '0') {
  262. this.displayAmount = '0';
  263. return;
  264. }
  265. // 检查是否包含小数点
  266. if (amount.includes('.')) {
  267. const numAmount = parseFloat(amount) || 0;
  268. this.displayAmount = numAmount.toFixed(2);
  269. } else {
  270. // 整数显示
  271. this.displayAmount = amount;
  272. }
  273. },
  274. // 优化:更高效的金额设置
  275. setAmount(amount) {
  276. const numAmount = parseFloat(amount);
  277. if (!isNaN(numAmount) && numAmount > 0) {
  278. const amountStr = numAmount.toString();
  279. this.amountBuffer = amountStr;
  280. this.currentAmount = amountStr;
  281. this.updateDisplay();
  282. }
  283. },
  284. // 显示确认弹框
  285. showConfirmDialog() {
  286. if (!this.canPay) return;
  287. this.showConfirm = true;
  288. },
  289. // 隐藏确认弹框
  290. hideConfirmDialog() {
  291. this.showConfirm = false;
  292. },
  293. // 处理支付
  294. async handlePay() {
  295. if (!this.canPay) return;
  296. const amount = parseFloat(this.currentAmount);
  297. // 隐藏弹框
  298. this.hideConfirmDialog();
  299. },
  300. // 请求支付
  301. async requestPayment(amount) {
  302. },
  303. // 生成订单ID
  304. generateOrderId() {
  305. const timestamp = Date.now();
  306. const random = Math.floor(Math.random() * 10000);
  307. return `ORDER${timestamp}${random}`;
  308. }
  309. }
  310. }
  311. </script>
  312. <style lang="scss" scoped>
  313. .pay-container {
  314. min-height: 100vh;
  315. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  316. position: relative;
  317. }
  318. .nav-bar {
  319. display: flex;
  320. align-items: center;
  321. justify-content: center;
  322. padding: 20rpx 30rpx;
  323. background: rgba(255, 255, 255, 0.1);
  324. backdrop-filter: blur(10px);
  325. .nav-title {
  326. color: #fff;
  327. font-size: 36rpx;
  328. font-weight: 600;
  329. }
  330. }
  331. .amount-section {
  332. padding: 60rpx 40rpx;
  333. text-align: center;
  334. .amount-label {
  335. color: rgba(255, 255, 255, 0.8);
  336. font-size: 28rpx;
  337. margin-bottom: 20rpx;
  338. }
  339. .amount-display {
  340. display: flex;
  341. align-items: baseline;
  342. justify-content: center;
  343. margin-bottom: 20rpx;
  344. .currency {
  345. color: #fff;
  346. font-size: 48rpx;
  347. font-weight: 600;
  348. margin-right: 10rpx;
  349. }
  350. .amount {
  351. color: #fff;
  352. font-size: 80rpx;
  353. font-weight: 700;
  354. font-family: 'Arial', sans-serif;
  355. }
  356. }
  357. .amount-tip {
  358. color: rgba(255, 255, 255, 0.6);
  359. font-size: 24rpx;
  360. }
  361. }
  362. .payment-methods {
  363. background: rgba(255, 255, 255, 0.95);
  364. margin: 0 30rpx 20rpx;
  365. border-radius: 15rpx;
  366. padding: 20rpx 30rpx;
  367. .method-list {
  368. .method-item {
  369. display: flex;
  370. align-items: center;
  371. padding: 20rpx 0;
  372. transition: all 0.3s ease;
  373. &.active {
  374. background: rgba(102, 126, 234, 0.1);
  375. border-radius: 12rpx;
  376. padding: 20rpx 15rpx;
  377. margin: 0 -15rpx;
  378. }
  379. .method-icon {
  380. width: 60rpx;
  381. height: 60rpx;
  382. border-radius: 12rpx;
  383. display: flex;
  384. align-items: center;
  385. justify-content: center;
  386. margin-right: 20rpx;
  387. &.alipay {
  388. background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
  389. }
  390. .iconfont {
  391. color: #fff;
  392. font-size: 32rpx;
  393. }
  394. }
  395. .method-info {
  396. flex: 1;
  397. .method-name {
  398. display: block;
  399. font-size: 28rpx;
  400. font-weight: 600;
  401. color: #333;
  402. margin-bottom: 4rpx;
  403. }
  404. .method-desc {
  405. display: block;
  406. font-size: 22rpx;
  407. color: #999;
  408. }
  409. }
  410. .method-check {
  411. width: 50rpx;
  412. height: 50rpx;
  413. display: flex;
  414. align-items: center;
  415. justify-content: center;
  416. .iconfont {
  417. color: #1677ff;
  418. font-size: 32rpx;
  419. }
  420. }
  421. }
  422. }
  423. }
  424. .keyboard-section {
  425. position: fixed;
  426. bottom: 160rpx;
  427. left: 0;
  428. right: 0;
  429. background: rgba(255, 255, 255, 0.95);
  430. backdrop-filter: blur(10px);
  431. padding: 30rpx;
  432. .keyboard {
  433. .keyboard-row {
  434. display: flex;
  435. margin-bottom: 20rpx;
  436. &:last-child {
  437. margin-bottom: 0;
  438. }
  439. .key-item {
  440. flex: 1;
  441. height: 120rpx;
  442. margin: 0 10rpx;
  443. border-radius: 20rpx;
  444. display: flex;
  445. align-items: center;
  446. justify-content: center;
  447. font-size: 40rpx;
  448. font-weight: 600;
  449. transition: transform 0.08s ease;
  450. user-select: none;
  451. -webkit-user-select: none;
  452. cursor: pointer;
  453. will-change: transform;
  454. &.key-number {
  455. background: #fff;
  456. color: #333;
  457. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
  458. }
  459. &.key-delete {
  460. background: #ff6b6b;
  461. color: #fff;
  462. }
  463. &.key-dot {
  464. background: #fff;
  465. color: #333;
  466. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
  467. }
  468. &.key-confirm {
  469. background: #667eea;
  470. color: #fff;
  471. }
  472. }
  473. }
  474. }
  475. }
  476. .pay-button-section {
  477. position: fixed;
  478. bottom: 0;
  479. left: 0;
  480. right: 0;
  481. padding: 30rpx;
  482. background: rgba(255, 255, 255, 0.95);
  483. backdrop-filter: blur(10px);
  484. .pay-button {
  485. width: 100%;
  486. height: 100rpx;
  487. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  488. color: #fff;
  489. font-size: 36rpx;
  490. font-weight: 600;
  491. border-radius: 50rpx;
  492. border: none;
  493. transition: all 0.3s ease;
  494. &.disabled {
  495. background: #ccc;
  496. color: #999;
  497. }
  498. &:not(.disabled):active {
  499. transform: scale(0.98);
  500. }
  501. }
  502. }
  503. // 确认支付弹框样式
  504. .confirm-dialog {
  505. position: fixed;
  506. top: 0;
  507. left: 0;
  508. right: 0;
  509. bottom: 0;
  510. background: rgba(0, 0, 0, 0.6);
  511. display: flex;
  512. align-items: center;
  513. justify-content: center;
  514. z-index: 9999;
  515. animation: fadeIn 0.3s ease;
  516. .dialog-content {
  517. width: 85%;
  518. max-width: 700rpx;
  519. background: #fff;
  520. border-radius: 20rpx;
  521. overflow: hidden;
  522. animation: slideUp 0.3s ease;
  523. .dialog-header {
  524. display: flex;
  525. align-items: center;
  526. justify-content: space-between;
  527. padding: 30rpx 40rpx 20rpx;
  528. border-bottom: 1rpx solid #f0f0f0;
  529. .dialog-title {
  530. font-size: 36rpx;
  531. font-weight: 600;
  532. color: #333;
  533. }
  534. .close-btn {
  535. width: 60rpx;
  536. height: 60rpx;
  537. display: flex;
  538. align-items: center;
  539. justify-content: center;
  540. border-radius: 50%;
  541. background: #f5f5f5;
  542. cursor: pointer;
  543. transition: all 0.2s ease;
  544. &:active {
  545. background: #e0e0e0;
  546. transform: scale(0.95);
  547. }
  548. .iconfont {
  549. font-size: 32rpx;
  550. color: #666;
  551. font-weight: bold;
  552. }
  553. }
  554. }
  555. .dialog-body {
  556. padding: 40rpx;
  557. .confirm-amount-section {
  558. text-align: center;
  559. margin-bottom: 30rpx;
  560. .confirm-label {
  561. display: block;
  562. font-size: 28rpx;
  563. color: #666;
  564. margin-bottom: 20rpx;
  565. }
  566. .confirm-amount-display {
  567. display: flex;
  568. align-items: baseline;
  569. justify-content: center;
  570. .confirm-currency {
  571. font-size: 60rpx;
  572. font-weight: 600;
  573. color: #ff6b6b;
  574. margin-right: 10rpx;
  575. }
  576. .confirm-amount {
  577. font-size: 100rpx;
  578. font-weight: 700;
  579. color: #ff6b6b;
  580. font-family: 'Arial', sans-serif;
  581. text-shadow: 0 2rpx 10rpx rgba(255, 107, 107, 0.3);
  582. }
  583. }
  584. }
  585. .confirm-tips {
  586. text-align: center;
  587. .tip-text {
  588. font-size: 26rpx;
  589. color: #999;
  590. line-height: 1.5;
  591. }
  592. }
  593. }
  594. .dialog-footer {
  595. display: flex;
  596. border-top: 1rpx solid #f0f0f0;
  597. padding: 30rpx 40rpx;
  598. gap: 20rpx;
  599. .cancel-btn, .confirm-btn {
  600. flex: 1;
  601. height: 100rpx;
  602. border: none;
  603. font-size: 32rpx;
  604. font-weight: 600;
  605. transition: all 0.2s ease;
  606. border-radius: 50rpx;
  607. display: flex;
  608. align-items: center;
  609. justify-content: center;
  610. &:active {
  611. transform: scale(0.98);
  612. }
  613. }
  614. .cancel-btn {
  615. background: #f5f5f5;
  616. color: #666;
  617. border: 2rpx solid #e0e0e0;
  618. &:active {
  619. background: #e0e0e0;
  620. border-color: #d0d0d0;
  621. }
  622. }
  623. .confirm-btn {
  624. background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%);
  625. color: #fff;
  626. &:active {
  627. background: linear-gradient(135deg, #ff5252 0%, #ff4444 100%);
  628. }
  629. }
  630. }
  631. }
  632. }
  633. // 弹框动画
  634. @keyframes fadeIn {
  635. from {
  636. opacity: 0;
  637. }
  638. to {
  639. opacity: 1;
  640. }
  641. }
  642. @keyframes slideUp {
  643. from {
  644. opacity: 0;
  645. transform: translateY(50rpx) scale(0.9);
  646. }
  647. to {
  648. opacity: 1;
  649. transform: translateY(0) scale(1);
  650. }
  651. }
  652. </style>