Commit 532a824366bb118847256793fa923954f7199782
1 parent
bf9336df
收银台页面
Showing
12 changed files
with
884 additions
and
51 deletions
apps/web-payment/public/fail.png
0 → 100644
3.96 KB
apps/web-payment/public/favicon.ico
No preview for this file type
apps/web-payment/public/success.png
0 → 100644
1.72 KB
apps/web-payment/src/api/payment.ts
| @@ -2,14 +2,25 @@ import { requestClient } from '#/api/request'; | @@ -2,14 +2,25 @@ import { requestClient } from '#/api/request'; | ||
| 2 | 2 | ||
| 3 | export namespace PaymentApi { | 3 | export namespace PaymentApi { |
| 4 | export interface orderPaymentParams { | 4 | export interface orderPaymentParams { |
| 5 | - tradeId: string; | ||
| 6 | - pipelineId: string; | ||
| 7 | - params: { openId: string }; | 5 | + tradeId: number | string; |
| 6 | + pipelineId: number | string; | ||
| 7 | + params: { | ||
| 8 | + accountId?: number | string; | ||
| 9 | + cardNo?: number | string; | ||
| 10 | + openId?: number | string; | ||
| 11 | + password?: number | string; | ||
| 12 | + }; | ||
| 8 | } | 13 | } |
| 9 | } | 14 | } |
| 10 | 15 | ||
| 16 | +// 换取openId | ||
| 17 | +export async function getOpenId(pipelineId: number | string, code: string) { | ||
| 18 | + return requestClient.post<any>( | ||
| 19 | + `/wechat/payment/openId.do?pipelineId=${pipelineId}&code=${code}`, | ||
| 20 | + ); | ||
| 21 | +} | ||
| 11 | /** | 22 | /** |
| 12 | - * 订单 | 23 | + * 园区卡支付 微信支付共用接口 |
| 13 | * @param data | 24 | * @param data |
| 14 | * @returns | 25 | * @returns |
| 15 | */ | 26 | */ |
apps/web-payment/src/router/routes/core.ts
| @@ -39,10 +39,19 @@ const coreRoutes: RouteRecordRaw[] = [ | @@ -39,10 +39,19 @@ const coreRoutes: RouteRecordRaw[] = [ | ||
| 39 | path: '/payment', | 39 | path: '/payment', |
| 40 | component: () => import('#/views/payment/index.vue'), | 40 | component: () => import('#/views/payment/index.vue'), |
| 41 | meta: { | 41 | meta: { |
| 42 | - icon: 'lucide:area-chart', | 42 | + icon: '', |
| 43 | title: '订单支付', | 43 | title: '订单支付', |
| 44 | }, | 44 | }, |
| 45 | }, | 45 | }, |
| 46 | + { | ||
| 47 | + name: 'PaymentSuccess', | ||
| 48 | + path: '/paymentSuccess', | ||
| 49 | + component: () => import('#/views/payment/PaySuccess.vue'), | ||
| 50 | + meta: { | ||
| 51 | + icon: '', | ||
| 52 | + title: '支付成功', | ||
| 53 | + }, | ||
| 54 | + }, | ||
| 46 | ]; | 55 | ]; |
| 47 | 56 | ||
| 48 | export { coreRoutes, fallbackNotFoundRoute }; | 57 | export { coreRoutes, fallbackNotFoundRoute }; |
apps/web-payment/src/views/payment/component/PasswordInput.vue
0 → 100644
| 1 | +<script setup lang="ts"> | ||
| 2 | +import { nextTick, ref, watch } from 'vue'; | ||
| 3 | + | ||
| 4 | +interface Props { | ||
| 5 | + modelValue: boolean; | ||
| 6 | + title?: string; | ||
| 7 | + length?: number; | ||
| 8 | +} | ||
| 9 | + | ||
| 10 | +interface Emits { | ||
| 11 | + (e: 'update:modelValue', value: boolean): void; | ||
| 12 | + (e: 'complete', password: string): void; | ||
| 13 | + (e: 'cancel'): void; | ||
| 14 | +} | ||
| 15 | + | ||
| 16 | +const props = withDefaults(defineProps<Props>(), { | ||
| 17 | + title: '请输入支付密码', | ||
| 18 | + length: 6, | ||
| 19 | +}); | ||
| 20 | + | ||
| 21 | +const emit = defineEmits<Emits>(); | ||
| 22 | + | ||
| 23 | +// 密码输入框数组 | ||
| 24 | +const passwordValues = ref<string[]>( | ||
| 25 | + Array.from({ length: props.length }).fill(''), | ||
| 26 | +); | ||
| 27 | +const inputRefs = ref<HTMLInputElement[]>([]); | ||
| 28 | +const currentIndex = ref(0); | ||
| 29 | + | ||
| 30 | +// 设置输入框引用 | ||
| 31 | +const setInputRef = (el: any, index: number) => { | ||
| 32 | + if (el) { | ||
| 33 | + inputRefs.value[index] = el; | ||
| 34 | + } | ||
| 35 | +}; | ||
| 36 | + | ||
| 37 | +// 处理输入 | ||
| 38 | +const handleInput = (index: number, event: Event) => { | ||
| 39 | + const target = event.target as HTMLInputElement; | ||
| 40 | + let value = target.value; | ||
| 41 | + | ||
| 42 | + // 只允许输入数字 | ||
| 43 | + value = value.replaceAll(/\D/g, ''); | ||
| 44 | + | ||
| 45 | + // 只保留第一个字符 | ||
| 46 | + if (value.length > 1) { | ||
| 47 | + value = value.charAt(0); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + passwordValues.value[index] = value; | ||
| 51 | + target.value = value; | ||
| 52 | + | ||
| 53 | + // 如果输入了数字,自动聚焦到下一个输入框 | ||
| 54 | + if (value && index < props.length - 1) { | ||
| 55 | + currentIndex.value = index + 1; | ||
| 56 | + nextTick(() => { | ||
| 57 | + inputRefs.value[index + 1]?.focus(); | ||
| 58 | + }); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + // 检查是否已完成输入 | ||
| 62 | + checkComplete(); | ||
| 63 | +}; | ||
| 64 | + | ||
| 65 | +// 处理键盘事件 | ||
| 66 | +const handleKeydown = (index: number, event: KeyboardEvent) => { | ||
| 67 | + // 处理退格键 | ||
| 68 | + if (event.key === 'Backspace') { | ||
| 69 | + event.preventDefault(); | ||
| 70 | + | ||
| 71 | + if (passwordValues.value[index]) { | ||
| 72 | + // 如果当前框有值,清空当前框 | ||
| 73 | + passwordValues.value[index] = ''; | ||
| 74 | + (event.target as HTMLInputElement).value = ''; | ||
| 75 | + } else if (index > 0) { | ||
| 76 | + // 如果当前框没值,回退到上一个框并清空 | ||
| 77 | + currentIndex.value = index - 1; | ||
| 78 | + passwordValues.value[index - 1] = ''; | ||
| 79 | + nextTick(() => { | ||
| 80 | + const prevInput = inputRefs.value[index - 1]; | ||
| 81 | + if (prevInput) { | ||
| 82 | + prevInput.value = ''; | ||
| 83 | + prevInput.focus(); | ||
| 84 | + } | ||
| 85 | + }); | ||
| 86 | + } | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + // 禁用左右箭头键 | ||
| 90 | + if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') { | ||
| 91 | + event.preventDefault(); | ||
| 92 | + } | ||
| 93 | +}; | ||
| 94 | + | ||
| 95 | +// 处理粘贴事件 | ||
| 96 | +const handlePaste = (index: number, event: ClipboardEvent) => { | ||
| 97 | + event.preventDefault(); | ||
| 98 | + const pasteData = event.clipboardData?.getData('text') || ''; | ||
| 99 | + const numbers = pasteData.replaceAll(/\D/g, ''); | ||
| 100 | + | ||
| 101 | + if (numbers) { | ||
| 102 | + // 从当前位置开始填充 | ||
| 103 | + for (let i = 0; i < numbers.length && index + i < props.length; i++) { | ||
| 104 | + passwordValues.value[index + i] = numbers[i]; | ||
| 105 | + if (inputRefs.value[index + i]) { | ||
| 106 | + inputRefs.value[index + i].value = numbers[i]; | ||
| 107 | + } | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + // 聚焦到最后一个填充的位置或下一个空位 | ||
| 111 | + const nextEmptyIndex = passwordValues.value.findIndex( | ||
| 112 | + (v, i) => i >= index && !v, | ||
| 113 | + ); | ||
| 114 | + if (nextEmptyIndex === -1) { | ||
| 115 | + currentIndex.value = props.length - 1; | ||
| 116 | + nextTick(() => { | ||
| 117 | + inputRefs.value[props.length - 1]?.focus(); | ||
| 118 | + }); | ||
| 119 | + } else { | ||
| 120 | + currentIndex.value = nextEmptyIndex; | ||
| 121 | + nextTick(() => { | ||
| 122 | + inputRefs.value[nextEmptyIndex]?.focus(); | ||
| 123 | + }); | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + checkComplete(); | ||
| 127 | + } | ||
| 128 | +}; | ||
| 129 | + | ||
| 130 | +// 处理点击输入框 - 禁止点击跳转 | ||
| 131 | +const handleFocus = (index: number, event: FocusEvent) => { | ||
| 132 | + event.preventDefault(); | ||
| 133 | + // 找到第一个空的输入框位置 | ||
| 134 | + const firstEmptyIndex = passwordValues.value.findIndex((v) => !v); | ||
| 135 | + const targetIndex = | ||
| 136 | + firstEmptyIndex === -1 ? props.length - 1 : firstEmptyIndex; | ||
| 137 | + | ||
| 138 | + // 如果点击的不是当前应该输入的位置,则重新聚焦到正确位置 | ||
| 139 | + if (index !== targetIndex) { | ||
| 140 | + nextTick(() => { | ||
| 141 | + inputRefs.value[targetIndex]?.focus(); | ||
| 142 | + }); | ||
| 143 | + } | ||
| 144 | + currentIndex.value = targetIndex; | ||
| 145 | +}; | ||
| 146 | + | ||
| 147 | +// 检查是否完成输入 | ||
| 148 | +const checkComplete = () => { | ||
| 149 | + const password = passwordValues.value.join(''); | ||
| 150 | + if (password.length === props.length) { | ||
| 151 | + // 延迟一点点,让用户看到最后一个数字输入 | ||
| 152 | + setTimeout(() => { | ||
| 153 | + emit('complete', password); | ||
| 154 | + }, 100); | ||
| 155 | + } | ||
| 156 | +}; | ||
| 157 | + | ||
| 158 | +// 重置密码 | ||
| 159 | +const reset = () => { | ||
| 160 | + passwordValues.value = Array.from({ length: props.length }).fill(''); | ||
| 161 | + inputRefs.value.forEach((input) => { | ||
| 162 | + if (input) input.value = ''; | ||
| 163 | + }); | ||
| 164 | + currentIndex.value = 0; | ||
| 165 | + nextTick(() => { | ||
| 166 | + inputRefs.value[0]?.focus(); | ||
| 167 | + }); | ||
| 168 | +}; | ||
| 169 | + | ||
| 170 | +// 关闭弹窗 | ||
| 171 | +const handleClose = () => { | ||
| 172 | + emit('update:modelValue', false); | ||
| 173 | + emit('cancel'); | ||
| 174 | +}; | ||
| 175 | + | ||
| 176 | +// 点击遮罩关闭 | ||
| 177 | +const handleMaskClick = (event: MouseEvent) => { | ||
| 178 | + if (event.target === event.currentTarget) { | ||
| 179 | + handleClose(); | ||
| 180 | + } | ||
| 181 | +}; | ||
| 182 | + | ||
| 183 | +// 监听弹窗打开,自动清空并聚焦第一个输入框 | ||
| 184 | +watch( | ||
| 185 | + () => props.modelValue, | ||
| 186 | + (newVal) => { | ||
| 187 | + if (newVal) { | ||
| 188 | + nextTick(() => { | ||
| 189 | + reset(); | ||
| 190 | + }); | ||
| 191 | + } | ||
| 192 | + }, | ||
| 193 | +); | ||
| 194 | + | ||
| 195 | +// 暴露方法给父组件 | ||
| 196 | +defineExpose({ | ||
| 197 | + reset, | ||
| 198 | +}); | ||
| 199 | +</script> | ||
| 200 | + | ||
| 201 | +<template> | ||
| 202 | + <Teleport to="body"> | ||
| 203 | + <Transition name="modal"> | ||
| 204 | + <div | ||
| 205 | + v-if="modelValue" | ||
| 206 | + class="password-modal-overlay" | ||
| 207 | + @click="handleMaskClick" | ||
| 208 | + > | ||
| 209 | + <Transition name="slide-up"> | ||
| 210 | + <div v-if="modelValue" class="password-modal-content"> | ||
| 211 | + <!-- 头部 --> | ||
| 212 | + <div class="modal-header"> | ||
| 213 | + <h3 class="modal-title">{{ title }}</h3> | ||
| 214 | + <button class="close-btn" @click="handleClose"> | ||
| 215 | + <svg | ||
| 216 | + width="24" | ||
| 217 | + height="24" | ||
| 218 | + viewBox="0 0 24 24" | ||
| 219 | + fill="none" | ||
| 220 | + stroke="currentColor" | ||
| 221 | + stroke-width="2" | ||
| 222 | + stroke-linecap="round" | ||
| 223 | + stroke-linejoin="round" | ||
| 224 | + > | ||
| 225 | + <line x1="18" y1="6" x2="6" y2="18" /> | ||
| 226 | + <line x1="6" y1="6" x2="18" y2="18" /> | ||
| 227 | + </svg> | ||
| 228 | + </button> | ||
| 229 | + </div> | ||
| 230 | + | ||
| 231 | + <!-- 主体内容 --> | ||
| 232 | + <div class="modal-body"> | ||
| 233 | + <div class="password-input-group"> | ||
| 234 | + <input | ||
| 235 | + v-for="(value, index) in passwordValues" | ||
| 236 | + :key="index" | ||
| 237 | + :ref="(el) => setInputRef(el, index)" | ||
| 238 | + type="tel" | ||
| 239 | + inputmode="numeric" | ||
| 240 | + maxlength="1" | ||
| 241 | + class="password-input" | ||
| 242 | + :class="{ | ||
| 243 | + active: currentIndex === index, | ||
| 244 | + filled: passwordValues[index], | ||
| 245 | + }" | ||
| 246 | + @input="handleInput(index, $event)" | ||
| 247 | + @keydown="handleKeydown(index, $event)" | ||
| 248 | + @paste="handlePaste(index, $event)" | ||
| 249 | + @focus="handleFocus(index, $event)" | ||
| 250 | + @mousedown.prevent | ||
| 251 | + /> | ||
| 252 | + </div> | ||
| 253 | + | ||
| 254 | + <div class="password-tips"> | ||
| 255 | + <p>为了您的资金安全,请输入支付密码</p> | ||
| 256 | + </div> | ||
| 257 | + </div> | ||
| 258 | + | ||
| 259 | + <!-- 底部 --> | ||
| 260 | + <div class="modal-footer"> | ||
| 261 | + <button class="cancel-btn" @click="handleClose">取消</button> | ||
| 262 | + </div> | ||
| 263 | + </div> | ||
| 264 | + </Transition> | ||
| 265 | + </div> | ||
| 266 | + </Transition> | ||
| 267 | + </Teleport> | ||
| 268 | +</template> | ||
| 269 | + | ||
| 270 | +<style scoped lang="scss"> | ||
| 271 | +// 脉动动画 | ||
| 272 | +@keyframes pulse { | ||
| 273 | + 0%, | ||
| 274 | + 100% { | ||
| 275 | + box-shadow: 0 0 0 3px rgb(234 66 0 / 10%); | ||
| 276 | + } | ||
| 277 | + | ||
| 278 | + 50% { | ||
| 279 | + box-shadow: 0 0 0 6px rgb(234 66 0 / 5%); | ||
| 280 | + } | ||
| 281 | +} | ||
| 282 | + | ||
| 283 | +// 移动端优化 | ||
| 284 | +@media (max-width: 767px) { | ||
| 285 | + .password-input-group { | ||
| 286 | + gap: 6px; | ||
| 287 | + } | ||
| 288 | + | ||
| 289 | + .password-input { | ||
| 290 | + max-width: 45px; | ||
| 291 | + height: 48px; | ||
| 292 | + font-size: 22px; | ||
| 293 | + } | ||
| 294 | +} | ||
| 295 | + | ||
| 296 | +.modal-enter-active, | ||
| 297 | +.modal-leave-active { | ||
| 298 | + transition: opacity 0.3s ease; | ||
| 299 | +} | ||
| 300 | + | ||
| 301 | +.modal-enter-from, | ||
| 302 | +.modal-leave-to { | ||
| 303 | + opacity: 0; | ||
| 304 | +} | ||
| 305 | + | ||
| 306 | +// 内容区域滑入动画 | ||
| 307 | +.slide-up-enter-active { | ||
| 308 | + transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1); | ||
| 309 | +} | ||
| 310 | + | ||
| 311 | +.slide-up-leave-active { | ||
| 312 | + transition: transform 0.25s cubic-bezier(0.4, 0, 1, 1); | ||
| 313 | +} | ||
| 314 | + | ||
| 315 | +.slide-up-enter-from { | ||
| 316 | + transform: translateY(100%); | ||
| 317 | +} | ||
| 318 | + | ||
| 319 | +.slide-up-leave-to { | ||
| 320 | + transform: translateY(100%); | ||
| 321 | +} | ||
| 322 | + | ||
| 323 | +// 遮罩层 | ||
| 324 | +.password-modal-overlay { | ||
| 325 | + position: fixed; | ||
| 326 | + top: 0; | ||
| 327 | + left: 0; | ||
| 328 | + z-index: 9999; | ||
| 329 | + display: flex; | ||
| 330 | + align-items: flex-end; | ||
| 331 | + justify-content: center; | ||
| 332 | + width: 100%; | ||
| 333 | + height: 100%; | ||
| 334 | + background-color: rgb(0 0 0 / 50%); | ||
| 335 | +} | ||
| 336 | + | ||
| 337 | +// 弹窗内容 | ||
| 338 | +.password-modal-content { | ||
| 339 | + position: relative; | ||
| 340 | + width: 100%; | ||
| 341 | + max-width: 100%; | ||
| 342 | + background: #fff; | ||
| 343 | + border-radius: 20px 20px 0 0; | ||
| 344 | + box-shadow: 0 -4px 20px rgb(0 0 0 / 10%); | ||
| 345 | +} | ||
| 346 | + | ||
| 347 | +// 头部 | ||
| 348 | +.modal-header { | ||
| 349 | + position: relative; | ||
| 350 | + padding: 20px 20px 10px; | ||
| 351 | + border-bottom: 1px solid #f0f0f0; | ||
| 352 | + | ||
| 353 | + .modal-title { | ||
| 354 | + margin: 0; | ||
| 355 | + font-size: 18px; | ||
| 356 | + font-weight: 600; | ||
| 357 | + color: #1f2937; | ||
| 358 | + text-align: center; | ||
| 359 | + } | ||
| 360 | + | ||
| 361 | + .close-btn { | ||
| 362 | + position: absolute; | ||
| 363 | + top: 20px; | ||
| 364 | + right: 20px; | ||
| 365 | + display: flex; | ||
| 366 | + align-items: center; | ||
| 367 | + justify-content: center; | ||
| 368 | + width: 32px; | ||
| 369 | + height: 32px; | ||
| 370 | + padding: 0; | ||
| 371 | + color: #909399; | ||
| 372 | + cursor: pointer; | ||
| 373 | + outline: none; | ||
| 374 | + background: transparent; | ||
| 375 | + border: none; | ||
| 376 | + transition: all 0.3s ease; | ||
| 377 | + | ||
| 378 | + &:hover { | ||
| 379 | + color: #606266; | ||
| 380 | + background: #f5f5f5; | ||
| 381 | + border-radius: 50%; | ||
| 382 | + } | ||
| 383 | + | ||
| 384 | + &:active { | ||
| 385 | + transform: scale(0.95); | ||
| 386 | + } | ||
| 387 | + | ||
| 388 | + svg { | ||
| 389 | + display: block; | ||
| 390 | + } | ||
| 391 | + } | ||
| 392 | +} | ||
| 393 | + | ||
| 394 | +// 主体 | ||
| 395 | +.modal-body { | ||
| 396 | + display: flex; | ||
| 397 | + flex-direction: column; | ||
| 398 | + align-items: center; | ||
| 399 | + padding: 30px 20px 20px; | ||
| 400 | +} | ||
| 401 | + | ||
| 402 | +.password-input-group { | ||
| 403 | + display: flex; | ||
| 404 | + gap: 8px; | ||
| 405 | + justify-content: center; | ||
| 406 | + width: 100%; | ||
| 407 | + max-width: 360px; | ||
| 408 | + margin-bottom: 24px; | ||
| 409 | + | ||
| 410 | + @media (min-width: 768px) { | ||
| 411 | + gap: 12px; | ||
| 412 | + max-width: 420px; | ||
| 413 | + } | ||
| 414 | +} | ||
| 415 | + | ||
| 416 | +.password-input { | ||
| 417 | + flex: 1; | ||
| 418 | + max-width: 50px; | ||
| 419 | + height: 50px; | ||
| 420 | + font-size: 24px; | ||
| 421 | + font-weight: 600; | ||
| 422 | + color: #1f2937; | ||
| 423 | + text-align: center; | ||
| 424 | + appearance: none; | ||
| 425 | + cursor: pointer; | ||
| 426 | + caret-color: transparent; | ||
| 427 | + user-select: none; | ||
| 428 | + outline: none; | ||
| 429 | + background: #f9fafb; | ||
| 430 | + border: 2px solid #e5e7eb; | ||
| 431 | + border-radius: 12px; | ||
| 432 | + transition: all 0.3s ease; | ||
| 433 | + | ||
| 434 | + @media (min-width: 768px) { | ||
| 435 | + max-width: 60px; | ||
| 436 | + height: 60px; | ||
| 437 | + font-size: 28px; | ||
| 438 | + } | ||
| 439 | + | ||
| 440 | + &:focus { | ||
| 441 | + background: #fff; | ||
| 442 | + border-color: #ea4200; | ||
| 443 | + box-shadow: 0 0 0 3px rgb(234 66 0 / 10%); | ||
| 444 | + } | ||
| 445 | + | ||
| 446 | + &.active { | ||
| 447 | + background: #fff; | ||
| 448 | + border-color: #ea4200; | ||
| 449 | + animation: pulse 1.5s ease-in-out infinite; | ||
| 450 | + } | ||
| 451 | + | ||
| 452 | + &.filled { | ||
| 453 | + // 显示为星号 | ||
| 454 | + -webkit-text-security: disc; | ||
| 455 | + text-security: disc; | ||
| 456 | + font-family: text-security-disc; | ||
| 457 | + background: #fff; | ||
| 458 | + border-color: #ea4200; | ||
| 459 | + } | ||
| 460 | + | ||
| 461 | + // iOS 样式重置 | ||
| 462 | + &::-webkit-inner-spin-button, | ||
| 463 | + &::-webkit-outer-spin-button { | ||
| 464 | + margin: 0; | ||
| 465 | + appearance: none; | ||
| 466 | + } | ||
| 467 | + | ||
| 468 | + // 禁用选中效果 | ||
| 469 | + &::selection { | ||
| 470 | + background: transparent; | ||
| 471 | + } | ||
| 472 | +} | ||
| 473 | + | ||
| 474 | +.password-tips { | ||
| 475 | + text-align: center; | ||
| 476 | + | ||
| 477 | + p { | ||
| 478 | + margin: 0; | ||
| 479 | + font-size: 13px; | ||
| 480 | + line-height: 1.5; | ||
| 481 | + color: #6b7280; | ||
| 482 | + } | ||
| 483 | +} | ||
| 484 | + | ||
| 485 | +// 底部 | ||
| 486 | +.modal-footer { | ||
| 487 | + display: flex; | ||
| 488 | + justify-content: center; | ||
| 489 | + width: 100%; | ||
| 490 | + padding: 0 20px 20px; | ||
| 491 | + padding-bottom: calc(20px + env(safe-area-inset-bottom)); | ||
| 492 | + | ||
| 493 | + .cancel-btn { | ||
| 494 | + width: 100%; | ||
| 495 | + max-width: 360px; | ||
| 496 | + height: 44px; | ||
| 497 | + font-size: 16px; | ||
| 498 | + font-weight: 500; | ||
| 499 | + color: #6b7280; | ||
| 500 | + cursor: pointer; | ||
| 501 | + outline: none; | ||
| 502 | + background: #f3f4f6; | ||
| 503 | + border: none; | ||
| 504 | + border-radius: 12px; | ||
| 505 | + transition: all 0.3s ease; | ||
| 506 | + | ||
| 507 | + &:hover { | ||
| 508 | + color: #374151; | ||
| 509 | + background: #e5e7eb; | ||
| 510 | + } | ||
| 511 | + | ||
| 512 | + &:active { | ||
| 513 | + transform: scale(0.98); | ||
| 514 | + } | ||
| 515 | + } | ||
| 516 | +} | ||
| 517 | +</style> |
apps/web-payment/src/views/payment/index.vue
| 1 | <script setup lang="ts"> | 1 | <script setup lang="ts"> |
| 2 | import { computed, ref } from 'vue'; | 2 | import { computed, ref } from 'vue'; |
| 3 | -import { useRoute } from 'vue-router'; | 3 | +import { useRoute, useRouter } from 'vue-router'; |
| 4 | + | ||
| 5 | +import { fenToYuan } from '@vben/utils'; | ||
| 4 | 6 | ||
| 5 | import { | 7 | import { |
| 6 | CreditCard, | 8 | CreditCard, |
| @@ -11,9 +13,10 @@ import { | @@ -11,9 +13,10 @@ import { | ||
| 11 | import { ElButton, ElDialog, ElIcon, ElMessage, ElRadio } from 'element-plus'; | 13 | import { ElButton, ElDialog, ElIcon, ElMessage, ElRadio } from 'element-plus'; |
| 12 | import qs from 'qs'; | 14 | import qs from 'qs'; |
| 13 | 15 | ||
| 14 | -import { listUserCards, orderInfo, orderPayment } from '#/api'; | 16 | +import { getOpenId, listUserCards, orderInfo, orderPayment } from '#/api'; |
| 15 | import EnvironmentDetector from '#/composables/environmentDetector'; | 17 | import EnvironmentDetector from '#/composables/environmentDetector'; |
| 16 | 18 | ||
| 19 | +import PasswordInput from './component/PasswordInput.vue'; | ||
| 17 | // 类型定义 | 20 | // 类型定义 |
| 18 | interface Card { | 21 | interface Card { |
| 19 | customerId: number | string; | 22 | customerId: number | string; |
| @@ -33,15 +36,20 @@ const detector = new EnvironmentDetector(); | @@ -33,15 +36,20 @@ const detector = new EnvironmentDetector(); | ||
| 33 | detector.logEnvironment(); | 36 | detector.logEnvironment(); |
| 34 | 37 | ||
| 35 | const route = useRoute(); | 38 | const route = useRoute(); |
| 39 | +const router = useRouter(); | ||
| 36 | const loadLoading = ref(false); | 40 | const loadLoading = ref(false); |
| 41 | +const showPasswordDialog = ref(false); | ||
| 37 | const token = ref<string>(''); | 42 | const token = ref<string>(''); |
| 38 | const openId = ref<string>(''); | 43 | const openId = ref<string>(''); |
| 44 | +const code = ref<string>(''); | ||
| 39 | const env = ref(); | 45 | const env = ref(); |
| 40 | env.value = detector.env; | 46 | env.value = detector.env; |
| 41 | 47 | ||
| 48 | +const REDIRECT_CHANNEL_ID = 29; | ||
| 49 | + | ||
| 42 | // 检查参数是否有效 | 50 | // 检查参数是否有效 |
| 43 | const hasValidParams = computed(() => { | 51 | const hasValidParams = computed(() => { |
| 44 | - return !!(token.value && openId.value); | 52 | + return !!(token.value && code.value); |
| 45 | }); | 53 | }); |
| 46 | 54 | ||
| 47 | const cardList = ref<Card[]>([]); | 55 | const cardList = ref<Card[]>([]); |
| @@ -55,10 +63,12 @@ const showCardDialog = ref<boolean>(false); | @@ -55,10 +63,12 @@ const showCardDialog = ref<boolean>(false); | ||
| 55 | const selectedCard = ref<Card | null>(null); | 63 | const selectedCard = ref<Card | null>(null); |
| 56 | const loading = ref<boolean>(false); | 64 | const loading = ref<boolean>(false); |
| 57 | const paymentSuccess = ref<boolean>(false); | 65 | const paymentSuccess = ref<boolean>(false); |
| 66 | +const payErrorDialog = ref<boolean>(false); | ||
| 67 | +const errorMessage = ref<string>('支付失败'); | ||
| 58 | 68 | ||
| 59 | // 计算显示金额 | 69 | // 计算显示金额 |
| 60 | const displayAmount = computed(() => { | 70 | const displayAmount = computed(() => { |
| 61 | - return orderInfoData.value?.amount || 0; | 71 | + return fenToYuan(orderInfoData.value?.amount || 0); |
| 62 | }); | 72 | }); |
| 63 | 73 | ||
| 64 | // 获取支付方式列表 | 74 | // 获取支付方式列表 |
| @@ -108,6 +118,20 @@ const getPaymentIcon = (channelName: string) => { | @@ -108,6 +118,20 @@ const getPaymentIcon = (channelName: string) => { | ||
| 108 | } | 118 | } |
| 109 | }; | 119 | }; |
| 110 | 120 | ||
| 121 | +const jumpTest = () => { | ||
| 122 | + // jWeixin.miniProgram.redirectTo({ | ||
| 123 | + // url: `/packageA/pages/wePay/index?amount=${displayAmount.value}&businessType=3&redirect=${true}`, | ||
| 124 | + // }); | ||
| 125 | + router.push({ | ||
| 126 | + path: '/paymentSuccess', | ||
| 127 | + query: { | ||
| 128 | + amount: displayAmount.value, | ||
| 129 | + success: 'true', | ||
| 130 | + payType: '园区卡支付', | ||
| 131 | + }, | ||
| 132 | + }); | ||
| 133 | +}; | ||
| 134 | + | ||
| 111 | // 获取支付方式描述 | 135 | // 获取支付方式描述 |
| 112 | const getPaymentDesc = (pipeline: Pipeline) => { | 136 | const getPaymentDesc = (pipeline: Pipeline) => { |
| 113 | if ( | 137 | if ( |
| @@ -128,6 +152,19 @@ const getPaymentDesc = (pipeline: Pipeline) => { | @@ -128,6 +152,19 @@ const getPaymentDesc = (pipeline: Pipeline) => { | ||
| 128 | } | 152 | } |
| 129 | }; | 153 | }; |
| 130 | 154 | ||
| 155 | +const loadOpenId = async () => { | ||
| 156 | + try { | ||
| 157 | + if (openId.value) { | ||
| 158 | + payBtnShow.value = true; | ||
| 159 | + return; | ||
| 160 | + } | ||
| 161 | + const data = await getOpenId(currentPayType.value?.pipelineId, code.value); | ||
| 162 | + openId.value = data || null; | ||
| 163 | + // 其他支付方式直接显示支付按钮\ | ||
| 164 | + payBtnShow.value = true; | ||
| 165 | + } catch {} | ||
| 166 | +}; | ||
| 167 | + | ||
| 131 | // 方法 | 168 | // 方法 |
| 132 | const handlepayBtnShowClick = async (pipeline: Pipeline) => { | 169 | const handlepayBtnShowClick = async (pipeline: Pipeline) => { |
| 133 | currentPaymentMethod.value = pipeline.pipelineId; | 170 | currentPaymentMethod.value = pipeline.pipelineId; |
| @@ -144,8 +181,11 @@ const handlepayBtnShowClick = async (pipeline: Pipeline) => { | @@ -144,8 +181,11 @@ const handlepayBtnShowClick = async (pipeline: Pipeline) => { | ||
| 144 | } | 181 | } |
| 145 | showCardDialog.value = true; | 182 | showCardDialog.value = true; |
| 146 | } else { | 183 | } else { |
| 147 | - // 其他支付方式直接显示支付按钮 | ||
| 148 | - payBtnShow.value = true; | 184 | + if (pipeline.channelId === REDIRECT_CHANNEL_ID) { |
| 185 | + payBtnShow.value = true; | ||
| 186 | + return; | ||
| 187 | + } | ||
| 188 | + await loadOpenId(); | ||
| 149 | } | 189 | } |
| 150 | }; | 190 | }; |
| 151 | 191 | ||
| @@ -156,12 +196,73 @@ const handleCardSelect = (card: Card) => { | @@ -156,12 +196,73 @@ const handleCardSelect = (card: Card) => { | ||
| 156 | payBtnShow.value = true; | 196 | payBtnShow.value = true; |
| 157 | }; | 197 | }; |
| 158 | 198 | ||
| 199 | +// 处理密码输入取消 | ||
| 200 | +const handlePasswordCancel = () => { | ||
| 201 | + showPasswordDialog.value = false; | ||
| 202 | + loading.value = false; | ||
| 203 | +}; | ||
| 204 | + | ||
| 205 | +const handlePasswordComplete = async (password: string) => { | ||
| 206 | + showPasswordDialog.value = false; | ||
| 207 | + try { | ||
| 208 | + const params = { | ||
| 209 | + tradeId: orderInfoData.value.tradeId, | ||
| 210 | + pipelineId: currentPayType.value.pipelineId, | ||
| 211 | + params: { | ||
| 212 | + accountId: selectedCard.value?.accountId, | ||
| 213 | + cardNo: selectedCard.value?.cardNo, | ||
| 214 | + password, | ||
| 215 | + }, | ||
| 216 | + }; | ||
| 217 | + | ||
| 218 | + const data = await orderPayment(params); | ||
| 219 | + router.push({ | ||
| 220 | + path: '/paymentSuccess', | ||
| 221 | + query: { | ||
| 222 | + amount: displayAmount.value, | ||
| 223 | + success: 'true', | ||
| 224 | + payType: '园区卡支付', | ||
| 225 | + }, | ||
| 226 | + }); | ||
| 227 | + } catch (error) { | ||
| 228 | + errorMessage.value = error?.message || '支付失败'; | ||
| 229 | + payErrorDialog.value = true; | ||
| 230 | + console.error(error); | ||
| 231 | + } finally { | ||
| 232 | + loading.value = false; | ||
| 233 | + } | ||
| 234 | +}; | ||
| 235 | + | ||
| 159 | const queryPayment = async () => { | 236 | const queryPayment = async () => { |
| 160 | - if (!openId.value || !currentPayType.value) { | 237 | + if ( |
| 238 | + !(currentPayType.value.channelId === REDIRECT_CHANNEL_ID) && | ||
| 239 | + !openId.value | ||
| 240 | + ) { | ||
| 161 | ElMessage.error('支付参数不完整'); | 241 | ElMessage.error('支付参数不完整'); |
| 162 | return false; | 242 | return false; |
| 163 | } | 243 | } |
| 164 | - | 244 | + // 跳转云商户支付 |
| 245 | + if (currentPayType.value.channelId === REDIRECT_CHANNEL_ID) { | ||
| 246 | + const params = { | ||
| 247 | + tradeId: orderInfoData.value.tradeId, | ||
| 248 | + amount: orderInfoData.value.amount, | ||
| 249 | + pipelineId: currentPayType.value.pipelineId, | ||
| 250 | + goods: orderInfoData.value.goods, | ||
| 251 | + payType: currentPayType.value.channelName, | ||
| 252 | + redirect: true, | ||
| 253 | + payee: orderInfoData.value.mchName, | ||
| 254 | + redirectUrl: orderInfoData.value.redirectUrl, | ||
| 255 | + }; | ||
| 256 | + const queryString = qs.stringify(params); | ||
| 257 | + if (typeof jWeixin !== 'undefined' && jWeixin.miniProgram) { | ||
| 258 | + jWeixin.miniProgram.navigateTo({ | ||
| 259 | + url: `/packageA/pages/wxPay/index?${queryString}`, | ||
| 260 | + }); | ||
| 261 | + loading.value = false; | ||
| 262 | + } | ||
| 263 | + return false; | ||
| 264 | + } | ||
| 265 | + // redirect: currentPayType.value.channelId === REDIRECT_CHANNEL_ID | ||
| 165 | try { | 266 | try { |
| 166 | const params = { | 267 | const params = { |
| 167 | tradeId: orderInfoData.value.tradeId, | 268 | tradeId: orderInfoData.value.tradeId, |
| @@ -170,19 +271,27 @@ const queryPayment = async () => { | @@ -170,19 +271,27 @@ const queryPayment = async () => { | ||
| 170 | }; | 271 | }; |
| 171 | 272 | ||
| 172 | const data = await orderPayment(params); | 273 | const data = await orderPayment(params); |
| 173 | - const queryString = qs.stringify(data); | 274 | + const pramsData = { |
| 275 | + ...data, | ||
| 276 | + goods: orderInfoData.value.goods, | ||
| 277 | + amount: orderInfoData.value.amount, | ||
| 278 | + payType: currentPayType.value.channelName, | ||
| 279 | + redirectUrl: orderInfoData.value.redirectUrl, | ||
| 280 | + payee: orderInfoData.value.mchName, | ||
| 281 | + }; | ||
| 282 | + const queryString = qs.stringify(pramsData); | ||
| 174 | 283 | ||
| 175 | // 跳转到微信支付页面 | 284 | // 跳转到微信支付页面 |
| 176 | if (typeof jWeixin !== 'undefined' && jWeixin.miniProgram) { | 285 | if (typeof jWeixin !== 'undefined' && jWeixin.miniProgram) { |
| 177 | - jWeixin.miniProgram.redirectTo({ | ||
| 178 | - url: `/packageA/pages/wePay/index?${queryString}`, | 286 | + jWeixin.miniProgram.navigateTo({ |
| 287 | + url: `/packageA/pages/wxPay/index?${queryString}`, | ||
| 179 | }); | 288 | }); |
| 289 | + loading.value = false; | ||
| 180 | } else { | 290 | } else { |
| 181 | console.warn('非微信小程序环境'); | 291 | console.warn('非微信小程序环境'); |
| 182 | // 模拟支付成功 | 292 | // 模拟支付成功 |
| 183 | setTimeout(() => { | 293 | setTimeout(() => { |
| 184 | loading.value = false; | 294 | loading.value = false; |
| 185 | - paymentSuccess.value = true; | ||
| 186 | setTimeout(() => { | 295 | setTimeout(() => { |
| 187 | paymentSuccess.value = false; | 296 | paymentSuccess.value = false; |
| 188 | resetPaymentState(); | 297 | resetPaymentState(); |
| @@ -216,28 +325,8 @@ const handlePay = async () => { | @@ -216,28 +325,8 @@ const handlePay = async () => { | ||
| 216 | if (isCardPayment) { | 325 | if (isCardPayment) { |
| 217 | // 园区卡支付逻辑 | 326 | // 园区卡支付逻辑 |
| 218 | try { | 327 | try { |
| 219 | - const params = { | ||
| 220 | - tradeId: orderInfoData.value.tradeId, | ||
| 221 | - pipelineId: currentPayType.value.pipelineId, | ||
| 222 | - params: { | ||
| 223 | - openId: openId.value, | ||
| 224 | - cardNo: selectedCard.value?.cardNo, | ||
| 225 | - }, | ||
| 226 | - }; | ||
| 227 | - | ||
| 228 | - const data = await orderPayment(params); | ||
| 229 | - loading.value = false; | ||
| 230 | - paymentSuccess.value = true; | ||
| 231 | - | ||
| 232 | - setTimeout(() => { | ||
| 233 | - paymentSuccess.value = false; | ||
| 234 | - resetPaymentState(); | ||
| 235 | - }, 2000); | ||
| 236 | - } catch (error) { | ||
| 237 | - console.error('园区卡支付失败:', error); | ||
| 238 | - ElMessage.error('支付失败,请重试'); | ||
| 239 | - loading.value = false; | ||
| 240 | - } | 328 | + showPasswordDialog.value = true; |
| 329 | + } catch {} | ||
| 241 | } else { | 330 | } else { |
| 242 | // 其他支付方式(微信、支付宝等) | 331 | // 其他支付方式(微信、支付宝等) |
| 243 | await queryPayment(); | 332 | await queryPayment(); |
| @@ -292,16 +381,17 @@ const getListUserCards = async () => { | @@ -292,16 +381,17 @@ const getListUserCards = async () => { | ||
| 292 | const init = async () => { | 381 | const init = async () => { |
| 293 | token.value = (route.query?.token as string) || ''; | 382 | token.value = (route.query?.token as string) || ''; |
| 294 | openId.value = (route.query?.openId as string) || ''; | 383 | openId.value = (route.query?.openId as string) || ''; |
| 295 | - | ||
| 296 | - if (!hasValidParams.value) { | 384 | + code.value = (route.query?.code as string) || ''; |
| 385 | + if (!token.value) { | ||
| 297 | return; | 386 | return; |
| 298 | } | 387 | } |
| 299 | - | ||
| 300 | try { | 388 | try { |
| 301 | loadLoading.value = true; | 389 | loadLoading.value = true; |
| 302 | await getOrderInfo(); | 390 | await getOrderInfo(); |
| 303 | - } catch { | ||
| 304 | - ElMessage.error('加载订单信息失败'); | 391 | + } catch (error) { |
| 392 | + // ElMessage.error(error?.message || '获取订单信息失败'); | ||
| 393 | + errorMessage.value = error?.message || '获取订单信息失败'; | ||
| 394 | + payErrorDialog.value = true; | ||
| 305 | } finally { | 395 | } finally { |
| 306 | loadLoading.value = false; | 396 | loadLoading.value = false; |
| 307 | } | 397 | } |
| @@ -442,7 +532,6 @@ init(); | @@ -442,7 +532,6 @@ init(); | ||
| 442 | </div> | 532 | </div> |
| 443 | </div> | 533 | </div> |
| 444 | </div> | 534 | </div> |
| 445 | - | ||
| 446 | <!-- 底部支付按钮 --> | 535 | <!-- 底部支付按钮 --> |
| 447 | <div class="payment-footer"> | 536 | <div class="payment-footer"> |
| 448 | <ElButton | 537 | <ElButton |
| @@ -480,9 +569,7 @@ init(); | @@ -480,9 +569,7 @@ init(); | ||
| 480 | </div> | 569 | </div> |
| 481 | <div class="card-balance"> | 570 | <div class="card-balance"> |
| 482 | <p class="balance-label">余额</p> | 571 | <p class="balance-label">余额</p> |
| 483 | - <p class="balance-value"> | ||
| 484 | - ¥{{ (Number(card.amount) / 100).toFixed(2) }} | ||
| 485 | - </p> | 572 | + <p class="balance-value">¥{{ card.amount }}</p> |
| 486 | </div> | 573 | </div> |
| 487 | </div> | 574 | </div> |
| 488 | <div v-if="cardList.length === 0" class="empty-card"> | 575 | <div v-if="cardList.length === 0" class="empty-card"> |
| @@ -494,6 +581,39 @@ init(); | @@ -494,6 +581,39 @@ init(); | ||
| 494 | </template> | 581 | </template> |
| 495 | </ElDialog> | 582 | </ElDialog> |
| 496 | 583 | ||
| 584 | + <ElDialog | ||
| 585 | + v-model="payErrorDialog" | ||
| 586 | + title="" | ||
| 587 | + width="90%" | ||
| 588 | + :style="{ maxWidth: '500px' }" | ||
| 589 | + class="card-dialog" | ||
| 590 | + :close-on-click-modal="true" | ||
| 591 | + > | ||
| 592 | + <div class="color-[#333] items-center"> | ||
| 593 | + <img src="/fail.png" class="m-auto block h-[64px] w-[64px]" /> | ||
| 594 | + <div class="py-[20px] text-center text-[20px] font-bold">支付失败</div> | ||
| 595 | + <div class="w-full overflow-y-auto text-left text-[18px] leading-6"> | ||
| 596 | + {{ errorMessage }} | ||
| 597 | + </div> | ||
| 598 | + <div class="mt-10"> | ||
| 599 | + <ElButton | ||
| 600 | + plain | ||
| 601 | + @click="payErrorDialog = false" | ||
| 602 | + class="confirm-button" | ||
| 603 | + > | ||
| 604 | + 确 认 | ||
| 605 | + </ElButton> | ||
| 606 | + </div> | ||
| 607 | + </div> | ||
| 608 | + </ElDialog> | ||
| 609 | + | ||
| 610 | + <!-- 支付密码输入弹窗 --> | ||
| 611 | + <PasswordInput | ||
| 612 | + v-model="showPasswordDialog" | ||
| 613 | + @complete="handlePasswordComplete" | ||
| 614 | + @cancel="handlePasswordCancel" | ||
| 615 | + /> | ||
| 616 | + | ||
| 497 | <!-- 支付成功对话框 --> | 617 | <!-- 支付成功对话框 --> |
| 498 | <ElDialog | 618 | <ElDialog |
| 499 | v-model="paymentSuccess" | 619 | v-model="paymentSuccess" |
| @@ -994,4 +1114,29 @@ init(); | @@ -994,4 +1114,29 @@ init(); | ||
| 994 | color: #6b7280; | 1114 | color: #6b7280; |
| 995 | } | 1115 | } |
| 996 | } | 1116 | } |
| 1117 | + | ||
| 1118 | +.confirm-button { | ||
| 1119 | + width: 100%; | ||
| 1120 | + height: 42px; | ||
| 1121 | + font-size: 18px; | ||
| 1122 | + font-weight: 600; | ||
| 1123 | + color: #ea4200; | ||
| 1124 | + background: #fff; | ||
| 1125 | + border: 1px solid #ea4200; | ||
| 1126 | + border-radius: 21px; | ||
| 1127 | + | ||
| 1128 | + &:hover:not(:disabled) { | ||
| 1129 | + background: #f7f7f7; | ||
| 1130 | + box-shadow: 0 6px 20px rgb(234 66 0 / 40%); | ||
| 1131 | + } | ||
| 1132 | + | ||
| 1133 | + &:active:not(:disabled) { | ||
| 1134 | + transform: scale(0.98); | ||
| 1135 | + } | ||
| 1136 | + | ||
| 1137 | + &:disabled { | ||
| 1138 | + cursor: not-allowed; | ||
| 1139 | + opacity: 0.5; | ||
| 1140 | + } | ||
| 1141 | +} | ||
| 997 | </style> | 1142 | </style> |
apps/web-payment/src/views/payment/payFailed.vue
0 → 100644
apps/web-payment/src/views/payment/paySuccess.vue
0 → 100644
| 1 | +<script setup lang="ts"> | ||
| 2 | +import { onMounted, ref } from 'vue'; | ||
| 3 | +import { useRoute } from 'vue-router'; | ||
| 4 | + | ||
| 5 | +import { ElButton } from 'element-plus'; | ||
| 6 | + | ||
| 7 | +import EnvironmentDetector from '#/composables/environmentDetector'; | ||
| 8 | + | ||
| 9 | +const route = useRoute(); | ||
| 10 | +const loadLoading = ref(false); | ||
| 11 | + | ||
| 12 | +const queryData = ref<any>({}); | ||
| 13 | +const detector = ref(); | ||
| 14 | +const init = async () => { | ||
| 15 | + queryData.value = route.query || {}; | ||
| 16 | +}; | ||
| 17 | + | ||
| 18 | +const handleBack = () => { | ||
| 19 | + if ( | ||
| 20 | + typeof jWeixin !== 'undefined' && | ||
| 21 | + jWeixin.miniProgram && | ||
| 22 | + detector.value?.env.isMiniProgram | ||
| 23 | + ) { | ||
| 24 | + jWeixin.miniProgram.switchTab({ | ||
| 25 | + url: `/pages/newhome/newhome`, | ||
| 26 | + }); | ||
| 27 | + } | ||
| 28 | +}; | ||
| 29 | + | ||
| 30 | +onMounted(() => { | ||
| 31 | + init(); | ||
| 32 | + detector.value = new EnvironmentDetector(); | ||
| 33 | +}); | ||
| 34 | +</script> | ||
| 35 | + | ||
| 36 | +<template> | ||
| 37 | + <div class="cashier-container" v-loading="loadLoading"> | ||
| 38 | + {{ detector?.env }} | ||
| 39 | + <!-- 正常支付界面 --> | ||
| 40 | + <div class="cashier-wrapper"> | ||
| 41 | + <div class="pt-[40px]"> | ||
| 42 | + <div class="flex flex-col items-center justify-center"> | ||
| 43 | + <img src="/success.png" class="w-[64px]" /> | ||
| 44 | + <p class="color-[#49250B] mt-2 text-[18px] font-bold">支付成功</p> | ||
| 45 | + </div> | ||
| 46 | + <div class="color-[#333] flex flex-col gap-4 p-[40px]"> | ||
| 47 | + <div class="flex justify-between"> | ||
| 48 | + <div>支付方式</div> | ||
| 49 | + <div>{{ queryData?.payType }}</div> | ||
| 50 | + </div> | ||
| 51 | + <div class="flex justify-between"> | ||
| 52 | + <div>支付金额</div> | ||
| 53 | + <div>{{ queryData?.amount }} 元</div> | ||
| 54 | + </div> | ||
| 55 | + <div class="mt-10 flex justify-between"> | ||
| 56 | + <ElButton | ||
| 57 | + class="pay-button" | ||
| 58 | + plain | ||
| 59 | + type="primary" | ||
| 60 | + size="large" | ||
| 61 | + @click="handleBack" | ||
| 62 | + > | ||
| 63 | + 完成 | ||
| 64 | + </ElButton> | ||
| 65 | + </div> | ||
| 66 | + </div> | ||
| 67 | + </div> | ||
| 68 | + </div> | ||
| 69 | + </div> | ||
| 70 | +</template> | ||
| 71 | + | ||
| 72 | +<style scoped lang="scss"> | ||
| 73 | +.cashier-container { | ||
| 74 | + min-height: 100vh; | ||
| 75 | + background: linear-gradient( | ||
| 76 | + to bottom, | ||
| 77 | + #fff9f1 0%, | ||
| 78 | + #ffe9d4 20%, | ||
| 79 | + #ffe3c8 29%, | ||
| 80 | + #fff 45%, | ||
| 81 | + #fff 100% | ||
| 82 | + ); | ||
| 83 | +} | ||
| 84 | + | ||
| 85 | +.cashier-wrapper { | ||
| 86 | + display: flex; | ||
| 87 | + flex-direction: column; | ||
| 88 | + max-width: 500px; | ||
| 89 | + min-height: 100vh; | ||
| 90 | + margin: 0 auto; | ||
| 91 | +} | ||
| 92 | + | ||
| 93 | +.pay-button { | ||
| 94 | + width: 100%; | ||
| 95 | + height: 42px; | ||
| 96 | + font-size: 18px; | ||
| 97 | + font-weight: 600; | ||
| 98 | + color: #ea4200; | ||
| 99 | + background: #fff; | ||
| 100 | + border: 1px solid #ea4200; | ||
| 101 | + border-radius: 21px; | ||
| 102 | + | ||
| 103 | + &:hover:not(:disabled) { | ||
| 104 | + background: #f7f7f7; | ||
| 105 | + box-shadow: 0 6px 20px rgb(234 66 0 / 40%); | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + &:active:not(:disabled) { | ||
| 109 | + transform: scale(0.98); | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + &:disabled { | ||
| 113 | + cursor: not-allowed; | ||
| 114 | + opacity: 0.5; | ||
| 115 | + } | ||
| 116 | +} | ||
| 117 | +</style> |
apps/web-payment/vite.config.mts
| @@ -17,7 +17,7 @@ export default defineConfig(async () => { | @@ -17,7 +17,7 @@ export default defineConfig(async () => { | ||
| 17 | changeOrigin: true, | 17 | changeOrigin: true, |
| 18 | rewrite: (path) => path.replace(/^\/api/, ''), | 18 | rewrite: (path) => path.replace(/^\/api/, ''), |
| 19 | // mock代理目标地址 | 19 | // mock代理目标地址 |
| 20 | - target: 'http://10.28.3.24:8686', | 20 | + target: 'http://10.28.3.34:8686', |
| 21 | ws: true, | 21 | ws: true, |
| 22 | }, | 22 | }, |
| 23 | }, | 23 | }, |
packages/@core/base/shared/src/utils/util.ts
| @@ -42,3 +42,36 @@ export function getNestedValue<T>(obj: T, path: string): any { | @@ -42,3 +42,36 @@ export function getNestedValue<T>(obj: T, path: string): any { | ||
| 42 | 42 | ||
| 43 | return current; | 43 | return current; |
| 44 | } | 44 | } |
| 45 | + | ||
| 46 | +/** | ||
| 47 | + * 分转元 + 千分位格式化(TS 严格模式安全) | ||
| 48 | + * @param amount 分 | ||
| 49 | + * @param decimals 小数位,默认 2 | ||
| 50 | + */ | ||
| 51 | +export function fenToYuan( | ||
| 52 | + amount: bigint | number | string, | ||
| 53 | + decimals: number = 2, | ||
| 54 | +): string { | ||
| 55 | + if (amount === null || amount === undefined || amount === '') { | ||
| 56 | + return (0).toFixed(decimals); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + const num = Number(amount); | ||
| 60 | + if (Number.isNaN(num)) { | ||
| 61 | + return (0).toFixed(decimals); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + const isNegative = num < 0; | ||
| 65 | + const yuan = Math.abs(num) / 100; | ||
| 66 | + | ||
| 67 | + const fixed = yuan.toFixed(decimals); | ||
| 68 | + | ||
| 69 | + // 👇 关键修复点 | ||
| 70 | + const parts = fixed.split('.'); | ||
| 71 | + const integer = parts[0] ?? '0'; | ||
| 72 | + const decimal = parts[1]; | ||
| 73 | + | ||
| 74 | + const thousand = integer.replaceAll(/\B(?=(\d{3})+(?!\d))/g, ','); | ||
| 75 | + | ||
| 76 | + return `${isNegative ? '-' : ''}${thousand}${decimal ? `.${decimal}` : ''}`; | ||
| 77 | +} |
packages/effects/request/src/request-client/preset-interceptors.ts
| @@ -9,7 +9,7 @@ import axios from 'axios'; | @@ -9,7 +9,7 @@ import axios from 'axios'; | ||
| 9 | export const defaultResponseInterceptor = ({ | 9 | export const defaultResponseInterceptor = ({ |
| 10 | codeField = 'code', | 10 | codeField = 'code', |
| 11 | dataField = 'data', | 11 | dataField = 'data', |
| 12 | - successCode = 0, | 12 | + successCode = 200, |
| 13 | }: { | 13 | }: { |
| 14 | /** 响应数据中代表访问结果的字段名 */ | 14 | /** 响应数据中代表访问结果的字段名 */ |
| 15 | codeField: string; | 15 | codeField: string; |
| @@ -131,6 +131,7 @@ export const errorMessageResponseInterceptor = ( | @@ -131,6 +131,7 @@ export const errorMessageResponseInterceptor = ( | ||
| 131 | } | 131 | } |
| 132 | 132 | ||
| 133 | let errorMessage = ''; | 133 | let errorMessage = ''; |
| 134 | + debugger; | ||
| 134 | const status = error?.response?.status; | 135 | const status = error?.response?.status; |
| 135 | 136 | ||
| 136 | switch (status) { | 137 | switch (status) { |
| @@ -158,7 +159,7 @@ export const errorMessageResponseInterceptor = ( | @@ -158,7 +159,7 @@ export const errorMessageResponseInterceptor = ( | ||
| 158 | errorMessage = $t('ui.fallback.http.internalServerError'); | 159 | errorMessage = $t('ui.fallback.http.internalServerError'); |
| 159 | } | 160 | } |
| 160 | } | 161 | } |
| 161 | - makeErrorMessage?.(errorMessage, error); | 162 | + // makeErrorMessage?.(errorMessage, error); |
| 162 | return Promise.reject(error); | 163 | return Promise.reject(error); |
| 163 | }, | 164 | }, |
| 164 | }; | 165 | }; |