pay.vue 16 KB

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