Commit bf9336df9e2f92ddb5cab32692a1440be1be8cf2
1 parent
a16b16b8
收银台微信支付
Showing
8 changed files
with
649 additions
and
232 deletions
apps/web-payment/.env
apps/web-payment/index.html
apps/web-payment/src/api/index.ts
apps/web-payment/src/api/payment.ts
0 → 100644
| 1 | +import { requestClient } from '#/api/request'; | |
| 2 | + | |
| 3 | +export namespace PaymentApi { | |
| 4 | + export interface orderPaymentParams { | |
| 5 | + tradeId: string; | |
| 6 | + pipelineId: string; | |
| 7 | + params: { openId: string }; | |
| 8 | + } | |
| 9 | +} | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * 订单 | |
| 13 | + * @param data | |
| 14 | + * @returns | |
| 15 | + */ | |
| 16 | +export async function orderPayment(data: PaymentApi.orderPaymentParams) { | |
| 17 | + return requestClient.post<any>('/payment/cashier/orderPayment', data); | |
| 18 | +} | |
| 19 | + | |
| 20 | +/** | |
| 21 | + * 获取收银台信息 | |
| 22 | + * @param data | |
| 23 | + * @returns | |
| 24 | + */ | |
| 25 | +export async function orderInfo(token: string) { | |
| 26 | + return requestClient.post<any>(`/payment/cashier/orderInfo?token=${token}`); | |
| 27 | +} | |
| 28 | + | |
| 29 | +/** | |
| 30 | + * | |
| 31 | + * @param pipelineId 收银台支付方式的pipelineId | |
| 32 | + * @param userId userId | |
| 33 | + * @returns | |
| 34 | + */ | |
| 35 | +export async function listUserCards( | |
| 36 | + pipelineId: number | string, | |
| 37 | + userId: number | string, | |
| 38 | +) { | |
| 39 | + return requestClient.post<any>( | |
| 40 | + `/card/payment/listUserCards?pipelineId=${pipelineId}&userId=${userId}`, | |
| 41 | + ); | |
| 42 | +} | ... | ... |
apps/web-payment/src/api/request.ts
apps/web-payment/src/router/routes/core.ts
apps/web-payment/src/views/payment/index.vue
| 1 | 1 | <script setup lang="ts"> |
| 2 | -import { ref } from 'vue'; | |
| 3 | - | |
| 4 | -import { ArrowRight, CreditCard, SuccessFilled } from '@element-plus/icons-vue'; | |
| 5 | -import { ElButton, ElCard, ElDialog, ElIcon, ElMessage } from 'element-plus'; | |
| 6 | - | |
| 2 | +import { computed, ref } from 'vue'; | |
| 3 | +import { useRoute } from 'vue-router'; | |
| 4 | + | |
| 5 | +import { | |
| 6 | + CreditCard, | |
| 7 | + SuccessFilled, | |
| 8 | + Wallet, | |
| 9 | + WarningFilled, | |
| 10 | +} from '@element-plus/icons-vue'; | |
| 11 | +import { ElButton, ElDialog, ElIcon, ElMessage, ElRadio } from 'element-plus'; | |
| 12 | +import qs from 'qs'; | |
| 13 | + | |
| 14 | +import { listUserCards, orderInfo, orderPayment } from '#/api'; | |
| 7 | 15 | import EnvironmentDetector from '#/composables/environmentDetector'; |
| 16 | + | |
| 8 | 17 | // 类型定义 |
| 9 | 18 | interface Card { |
| 10 | - id: string; | |
| 11 | - cardNo: string; | |
| 12 | - balance: number; | |
| 13 | - holderName: string; | |
| 19 | + customerId: number | string; | |
| 20 | + accountId: number | string; | |
| 21 | + cardNo: number | string; | |
| 22 | + name: string; | |
| 23 | + amount: number | string; | |
| 24 | +} | |
| 25 | + | |
| 26 | +interface Pipeline { | |
| 27 | + pipelineId: number; | |
| 28 | + channelId: number; | |
| 29 | + channelName: string; | |
| 14 | 30 | } |
| 15 | 31 | |
| 16 | -// 创建检测器实例 | |
| 17 | 32 | const detector = new EnvironmentDetector(); |
| 18 | 33 | detector.logEnvironment(); |
| 19 | 34 | |
| 20 | -// 模拟的园区卡数据 | |
| 21 | -const mockCards: Card[] = [ | |
| 22 | - { | |
| 23 | - id: '1', | |
| 24 | - cardNo: '6225 8801 2345 6789', | |
| 25 | - balance: 1580.5, | |
| 26 | - holderName: '张三', | |
| 27 | - }, | |
| 28 | - { id: '2', cardNo: '6225 8801 9876 5432', balance: 3200, holderName: '李四' }, | |
| 29 | - { | |
| 30 | - id: '3', | |
| 31 | - cardNo: '6225 8801 1111 2222', | |
| 32 | - balance: 500.8, | |
| 33 | - holderName: '王五', | |
| 34 | - }, | |
| 35 | -]; | |
| 35 | +const route = useRoute(); | |
| 36 | +const loadLoading = ref(false); | |
| 37 | +const token = ref<string>(''); | |
| 38 | +const openId = ref<string>(''); | |
| 39 | +const env = ref(); | |
| 40 | +env.value = detector.env; | |
| 41 | + | |
| 42 | +// 检查参数是否有效 | |
| 43 | +const hasValidParams = computed(() => { | |
| 44 | + return !!(token.value && openId.value); | |
| 45 | +}); | |
| 46 | + | |
| 47 | +const cardList = ref<Card[]>([]); | |
| 48 | +const orderInfoData = ref<any>(null); | |
| 36 | 49 | |
| 37 | 50 | // 响应式数据 |
| 38 | -const amount = ref<number>(128.5); | |
| 51 | +const currentPayType = ref<null | Pipeline>(null); | |
| 39 | 52 | const payBtnShow = ref<boolean>(false); |
| 40 | -const currentPaymentMethod = ref<string>(''); | |
| 53 | +const currentPaymentMethod = ref<null | number>(null); // 改为存储 pipelineId | |
| 41 | 54 | const showCardDialog = ref<boolean>(false); |
| 42 | 55 | const selectedCard = ref<Card | null>(null); |
| 43 | 56 | const loading = ref<boolean>(false); |
| 44 | 57 | const paymentSuccess = ref<boolean>(false); |
| 45 | 58 | |
| 59 | +// 计算显示金额 | |
| 60 | +const displayAmount = computed(() => { | |
| 61 | + return orderInfoData.value?.amount || 0; | |
| 62 | +}); | |
| 63 | + | |
| 64 | +// 获取支付方式列表 | |
| 65 | +const paymentPipelines = computed(() => { | |
| 66 | + return orderInfoData.value?.pipelines || []; | |
| 67 | +}); | |
| 68 | + | |
| 69 | +// 根据渠道名称获取图标和颜色 | |
| 70 | +const getPaymentIcon = (channelName: string) => { | |
| 71 | + const lowerName = channelName.toLowerCase(); | |
| 72 | + if (lowerName.includes('微信')) { | |
| 73 | + return { | |
| 74 | + type: 'wechat', | |
| 75 | + icon: 'wechat', | |
| 76 | + color: '#10b981', | |
| 77 | + activeColor: '#EA4200', | |
| 78 | + bgColor: '#d1fae5', | |
| 79 | + activeBgColor: '#fee2e2', | |
| 80 | + }; | |
| 81 | + } else if (lowerName.includes('园区卡') || lowerName.includes('卡')) { | |
| 82 | + return { | |
| 83 | + type: 'card', | |
| 84 | + icon: 'card', | |
| 85 | + color: '#10b981', | |
| 86 | + activeColor: '#EA4200', | |
| 87 | + bgColor: '#d1fae5', | |
| 88 | + activeBgColor: '#fee2e2', | |
| 89 | + }; | |
| 90 | + } else if (lowerName.includes('支付宝')) { | |
| 91 | + return { | |
| 92 | + type: 'alipay', | |
| 93 | + icon: 'wallet', | |
| 94 | + color: '#1677ff', | |
| 95 | + activeColor: '#EA4200', | |
| 96 | + bgColor: '#e6f4ff', | |
| 97 | + activeBgColor: '#fee2e2', | |
| 98 | + }; | |
| 99 | + } else { | |
| 100 | + return { | |
| 101 | + type: 'other', | |
| 102 | + icon: 'wallet', | |
| 103 | + color: '#8b5cf6', | |
| 104 | + activeColor: '#EA4200', | |
| 105 | + bgColor: '#ede9fe', | |
| 106 | + activeBgColor: '#fee2e2', | |
| 107 | + }; | |
| 108 | + } | |
| 109 | +}; | |
| 110 | + | |
| 111 | +// 获取支付方式描述 | |
| 112 | +const getPaymentDesc = (pipeline: Pipeline) => { | |
| 113 | + if ( | |
| 114 | + pipeline.pipelineId === currentPaymentMethod.value && | |
| 115 | + selectedCard.value | |
| 116 | + ) { | |
| 117 | + return `卡号:${selectedCard.value.cardNo}`; | |
| 118 | + } | |
| 119 | + const channelName = pipeline.channelName.toLowerCase(); | |
| 120 | + if (channelName.includes('微信')) { | |
| 121 | + return '推荐使用'; | |
| 122 | + } else if (channelName.includes('园区卡') || channelName.includes('卡')) { | |
| 123 | + return '点击选择园区卡'; | |
| 124 | + } else if (channelName.includes('支付宝')) { | |
| 125 | + return '快捷支付'; | |
| 126 | + } else { | |
| 127 | + return '安全便捷'; | |
| 128 | + } | |
| 129 | +}; | |
| 130 | + | |
| 46 | 131 | // 方法 |
| 47 | -const handlepayBtnShowClick = (method: string) => { | |
| 48 | - currentPaymentMethod.value = method; | |
| 132 | +const handlepayBtnShowClick = async (pipeline: Pipeline) => { | |
| 133 | + currentPaymentMethod.value = pipeline.pipelineId; | |
| 134 | + currentPayType.value = pipeline; | |
| 49 | 135 | payBtnShow.value = false; |
| 50 | - if (method === 'card') { | |
| 136 | + | |
| 137 | + const channelName = pipeline.channelName.toLowerCase(); | |
| 138 | + | |
| 139 | + // 判断是否是园区卡支付 | |
| 140 | + if (channelName.includes('园区卡') || channelName.includes('卡支付')) { | |
| 141 | + // 如果还没有加载园区卡列表,先加载 | |
| 142 | + if (cardList.value.length === 0) { | |
| 143 | + await getListUserCards(); | |
| 144 | + } | |
| 51 | 145 | showCardDialog.value = true; |
| 52 | 146 | } else { |
| 147 | + // 其他支付方式直接显示支付按钮 | |
| 53 | 148 | payBtnShow.value = true; |
| 54 | 149 | } |
| 55 | 150 | }; |
| ... | ... | @@ -57,144 +152,310 @@ const handlepayBtnShowClick = (method: string) => { |
| 57 | 152 | const handleCardSelect = (card: Card) => { |
| 58 | 153 | selectedCard.value = card; |
| 59 | 154 | showCardDialog.value = false; |
| 60 | - ElMessage.success(`已选择 ${card.holderName} 的园区卡`); | |
| 155 | + ElMessage.success(`已选择 ${card.name} 的园区卡`); | |
| 61 | 156 | payBtnShow.value = true; |
| 62 | - // 园区卡选择后自动支付 | |
| 63 | - // handlePay(); | |
| 157 | +}; | |
| 158 | + | |
| 159 | +const queryPayment = async () => { | |
| 160 | + if (!openId.value || !currentPayType.value) { | |
| 161 | + ElMessage.error('支付参数不完整'); | |
| 162 | + return false; | |
| 163 | + } | |
| 164 | + | |
| 165 | + try { | |
| 166 | + const params = { | |
| 167 | + tradeId: orderInfoData.value.tradeId, | |
| 168 | + pipelineId: currentPayType.value.pipelineId, | |
| 169 | + params: { openId: openId.value }, | |
| 170 | + }; | |
| 171 | + | |
| 172 | + const data = await orderPayment(params); | |
| 173 | + const queryString = qs.stringify(data); | |
| 174 | + | |
| 175 | + // 跳转到微信支付页面 | |
| 176 | + if (typeof jWeixin !== 'undefined' && jWeixin.miniProgram) { | |
| 177 | + jWeixin.miniProgram.redirectTo({ | |
| 178 | + url: `/packageA/pages/wePay/index?${queryString}`, | |
| 179 | + }); | |
| 180 | + } else { | |
| 181 | + console.warn('非微信小程序环境'); | |
| 182 | + // 模拟支付成功 | |
| 183 | + setTimeout(() => { | |
| 184 | + loading.value = false; | |
| 185 | + paymentSuccess.value = true; | |
| 186 | + setTimeout(() => { | |
| 187 | + paymentSuccess.value = false; | |
| 188 | + resetPaymentState(); | |
| 189 | + }, 2000); | |
| 190 | + }, 1500); | |
| 191 | + } | |
| 192 | + } catch (error) { | |
| 193 | + console.error('支付失败:', error); | |
| 194 | + ElMessage.error('支付请求失败,请重试'); | |
| 195 | + loading.value = false; | |
| 196 | + } | |
| 64 | 197 | }; |
| 65 | 198 | |
| 66 | 199 | const handlePay = async () => { |
| 200 | + if (!currentPayType.value) { | |
| 201 | + ElMessage.warning('请先选择支付方式'); | |
| 202 | + return; | |
| 203 | + } | |
| 204 | + | |
| 205 | + const channelName = currentPayType.value.channelName.toLowerCase(); | |
| 206 | + const isCardPayment = | |
| 207 | + channelName.includes('园区卡') || channelName.includes('卡支付'); | |
| 208 | + | |
| 209 | + if (isCardPayment && !selectedCard.value) { | |
| 210 | + ElMessage.warning('请先选择园区卡'); | |
| 211 | + return; | |
| 212 | + } | |
| 213 | + | |
| 67 | 214 | loading.value = true; |
| 68 | 215 | |
| 69 | - // 模拟支付处理 | |
| 70 | - setTimeout(() => { | |
| 71 | - loading.value = false; | |
| 72 | - paymentSuccess.value = true; | |
| 73 | - | |
| 74 | - // 2秒后重置状态 | |
| 75 | - setTimeout(() => { | |
| 76 | - paymentSuccess.value = false; | |
| 77 | - payBtnShow.value = false; | |
| 78 | - selectedCard.value = null; | |
| 79 | - }, 2000); | |
| 80 | - }, 1500); | |
| 216 | + if (isCardPayment) { | |
| 217 | + // 园区卡支付逻辑 | |
| 218 | + 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 | + } | |
| 241 | + } else { | |
| 242 | + // 其他支付方式(微信、支付宝等) | |
| 243 | + await queryPayment(); | |
| 244 | + } | |
| 245 | +}; | |
| 246 | + | |
| 247 | +const resetPaymentState = () => { | |
| 248 | + payBtnShow.value = false; | |
| 249 | + currentPaymentMethod.value = null; | |
| 250 | + selectedCard.value = null; | |
| 251 | + currentPayType.value = null; | |
| 81 | 252 | }; |
| 82 | 253 | |
| 83 | 254 | const handleCloseDialog = () => { |
| 84 | 255 | showCardDialog.value = false; |
| 85 | 256 | payBtnShow.value = false; |
| 86 | 257 | }; |
| 258 | + | |
| 259 | +// 获取订单信息 支付方式等数据 | |
| 260 | +const getOrderInfo = async () => { | |
| 261 | + try { | |
| 262 | + const data = await orderInfo(token.value); | |
| 263 | + orderInfoData.value = data || {}; | |
| 264 | + } catch (error) { | |
| 265 | + console.error('获取订单信息失败:', error); | |
| 266 | + throw error; | |
| 267 | + } | |
| 268 | +}; | |
| 269 | + | |
| 270 | +// 查询园区卡列表 | |
| 271 | +const getListUserCards = async () => { | |
| 272 | + if (!currentPayType.value || !orderInfoData.value.userId) { | |
| 273 | + return; | |
| 274 | + } | |
| 275 | + | |
| 276 | + try { | |
| 277 | + const data = await listUserCards( | |
| 278 | + currentPayType.value.pipelineId, | |
| 279 | + orderInfoData.value.userId, | |
| 280 | + ); | |
| 281 | + cardList.value = data || []; | |
| 282 | + | |
| 283 | + if (cardList.value.length === 0) { | |
| 284 | + ElMessage.warning('未找到可用的园区卡'); | |
| 285 | + } | |
| 286 | + } catch (error) { | |
| 287 | + console.error('获取园区卡列表失败:', error); | |
| 288 | + ElMessage.error('获取园区卡列表失败'); | |
| 289 | + } | |
| 290 | +}; | |
| 291 | + | |
| 292 | +const init = async () => { | |
| 293 | + token.value = (route.query?.token as string) || ''; | |
| 294 | + openId.value = (route.query?.openId as string) || ''; | |
| 295 | + | |
| 296 | + if (!hasValidParams.value) { | |
| 297 | + return; | |
| 298 | + } | |
| 299 | + | |
| 300 | + try { | |
| 301 | + loadLoading.value = true; | |
| 302 | + await getOrderInfo(); | |
| 303 | + } catch { | |
| 304 | + ElMessage.error('加载订单信息失败'); | |
| 305 | + } finally { | |
| 306 | + loadLoading.value = false; | |
| 307 | + } | |
| 308 | +}; | |
| 309 | + | |
| 310 | +init(); | |
| 87 | 311 | </script> |
| 88 | 312 | |
| 89 | 313 | <template> |
| 90 | - <div class="cashier-container"> | |
| 91 | - <div class="cashier-wrapper"> | |
| 92 | - <!-- 头部 --> | |
| 93 | - <div class="header"> | |
| 94 | - <div class="icon-wrapper"> | |
| 95 | - <ElIcon :size="32" color="#fff"> | |
| 96 | - <CreditCard /> | |
| 314 | + <div class="cashier-container" v-loading="loadLoading"> | |
| 315 | + <!-- 无效参数提示 --> | |
| 316 | + <div v-if="!hasValidParams" class="error-container"> | |
| 317 | + <div class="error-content"> | |
| 318 | + <div class="error-icon"> | |
| 319 | + <ElIcon :size="64" color="#f59e0b"> | |
| 320 | + <WarningFilled /> | |
| 97 | 321 | </ElIcon> |
| 98 | 322 | </div> |
| 99 | - <h1 class="title">地利收银台</h1> | |
| 100 | - <p class="subtitle">请选择支付方式完成订单</p> | |
| 323 | + <h3 class="error-title">未查询到有效的订单信息</h3> | |
| 324 | + <p class="error-desc">请确认订单链接是否正确或重新发起支付</p> | |
| 325 | + <div class="error-tips"> | |
| 326 | + <p>可能的原因:</p> | |
| 327 | + <ul> | |
| 328 | + <li>订单链接已过期</li> | |
| 329 | + <li>订单参数不完整</li> | |
| 330 | + <li>网络连接异常</li> | |
| 331 | + </ul> | |
| 332 | + </div> | |
| 101 | 333 | </div> |
| 334 | + </div> | |
| 102 | 335 | |
| 103 | - <!-- 订单金额卡片 --> | |
| 104 | - <ElCard class="amount-card" shadow="always"> | |
| 105 | - <div class="amount-content"> | |
| 106 | - <p class="amount-label">订单金额</p> | |
| 107 | - <p class="amount-value">¥{{ amount.toFixed(2) }}</p> | |
| 336 | + <!-- 正常支付界面 --> | |
| 337 | + <div v-else class="cashier-wrapper"> | |
| 338 | + <div class="cashier-content"> | |
| 339 | + <!-- 头部 --> | |
| 340 | + <div class="header"> | |
| 341 | + <h1 class="title">地利收银台</h1> | |
| 342 | + <p class="subtitle">快捷·安全</p> | |
| 108 | 343 | </div> |
| 109 | - </ElCard> | |
| 110 | - | |
| 111 | - <!-- 支付方式选择 --> | |
| 112 | - <ElCard class="payment-card" shadow="always"> | |
| 113 | - <template #header> | |
| 114 | - <span class="card-header">选择支付方式</span> | |
| 115 | - </template> | |
| 116 | 344 | |
| 117 | - <!-- 微信支付 --> | |
| 118 | - <div | |
| 119 | - class="payment-item" | |
| 120 | - :class="{ active: currentPaymentMethod === 'wechat' }" | |
| 121 | - @click="handlepayBtnShowClick('wechat')" | |
| 122 | - > | |
| 123 | - <div class="payment-item-left"> | |
| 124 | - <div | |
| 125 | - class="payment-icon" | |
| 126 | - :class="{ active: currentPaymentMethod === 'wechat' }" | |
| 127 | - > | |
| 128 | - <svg class="wechat-icon" viewBox="0 0 24 24" fill="currentColor"> | |
| 129 | - <path | |
| 130 | - d="M8.5 9.5c-.8 0-1.5.7-1.5 1.5s.7 1.5 1.5 1.5 1.5-.7 1.5-1.5-.7-1.5-1.5-1.5zm7 0c-.8 0-1.5.7-1.5 1.5s.7 1.5 1.5 1.5 1.5-.7 1.5-1.5-.7-1.5-1.5-1.5zM12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm0 18c-1.7 0-3.3-.5-4.7-1.3l-4.8 1.5 1.5-4.5C3.4 14.5 3 13.3 3 12c0-5 4-9 9-9s9 4 9 9-4 9-9 9z" | |
| 131 | - /> | |
| 132 | - </svg> | |
| 133 | - </div> | |
| 134 | - <div class="payment-info"> | |
| 135 | - <p class="payment-name">微信支付</p> | |
| 136 | - <p class="payment-desc">快捷安全</p> | |
| 137 | - </div> | |
| 345 | + <!-- 订单金额卡片 --> | |
| 346 | + <div class="amount-card"> | |
| 347 | + <div class="amount-content"> | |
| 348 | + <p class="amount-label">订单金额</p> | |
| 349 | + <p class="amount-value">¥{{ displayAmount }}</p> | |
| 350 | + <p class="goods-info">{{ orderInfoData?.goods || '商品信息' }}</p> | |
| 138 | 351 | </div> |
| 139 | - <ElIcon | |
| 140 | - :size="20" | |
| 141 | - :color="currentPaymentMethod === 'wechat' ? '#fff' : '#909399'" | |
| 142 | - > | |
| 143 | - <ArrowRight /> | |
| 144 | - </ElIcon> | |
| 145 | 352 | </div> |
| 146 | 353 | |
| 147 | - <!-- 园区卡支付 --> | |
| 148 | - <div | |
| 149 | - class="payment-item" | |
| 150 | - :class="{ active: currentPaymentMethod === 'card' }" | |
| 151 | - @click="handlepayBtnShowClick('card')" | |
| 152 | - > | |
| 153 | - <div class="payment-item-left"> | |
| 154 | - <div | |
| 155 | - class="payment-icon" | |
| 156 | - :class="{ active: currentPaymentMethod === 'card' }" | |
| 157 | - > | |
| 158 | - <ElIcon | |
| 159 | - :size="28" | |
| 160 | - :color="currentPaymentMethod === 'card' ? '#fff' : '#10b981'" | |
| 354 | + <!-- 支付方式选择 --> | |
| 355 | + <div class="payment-section"> | |
| 356 | + <div class="section-header"> | |
| 357 | + <span class="section-title">选择支付方式</span> | |
| 358 | + </div> | |
| 359 | + | |
| 360 | + <!-- 循环渲染支付方式 --> | |
| 361 | + <div | |
| 362 | + v-for="pipeline in paymentPipelines" | |
| 363 | + :key="pipeline.pipelineId" | |
| 364 | + class="payment-item" | |
| 365 | + :class="{ active: currentPaymentMethod === pipeline.pipelineId }" | |
| 366 | + @click="handlepayBtnShowClick(pipeline)" | |
| 367 | + > | |
| 368 | + <div class="payment-item-left"> | |
| 369 | + <div | |
| 370 | + class="payment-icon" | |
| 371 | + :class="{ | |
| 372 | + active: currentPaymentMethod === pipeline.pipelineId, | |
| 373 | + }" | |
| 374 | + :style="{ | |
| 375 | + backgroundColor: | |
| 376 | + currentPaymentMethod === pipeline.pipelineId | |
| 377 | + ? getPaymentIcon(pipeline.channelName).activeBgColor | |
| 378 | + : getPaymentIcon(pipeline.channelName).bgColor, | |
| 379 | + }" | |
| 161 | 380 | > |
| 162 | - <CreditCard /> | |
| 163 | - </ElIcon> | |
| 164 | - </div> | |
| 165 | - <div class="payment-info"> | |
| 166 | - <p class="payment-name">园区卡支付</p> | |
| 167 | - <p class="payment-desc"> | |
| 168 | - {{ | |
| 169 | - selectedCard | |
| 170 | - ? `卡号:${selectedCard.cardNo}` | |
| 171 | - : '点击选择园区卡' | |
| 172 | - }} | |
| 173 | - </p> | |
| 381 | + <!-- 微信支付图标 --> | |
| 382 | + <svg | |
| 383 | + v-if="getPaymentIcon(pipeline.channelName).icon === 'wechat'" | |
| 384 | + class="payment-svg-icon" | |
| 385 | + viewBox="0 0 24 24" | |
| 386 | + fill="currentColor" | |
| 387 | + :style="{ | |
| 388 | + color: | |
| 389 | + currentPaymentMethod === pipeline.pipelineId | |
| 390 | + ? getPaymentIcon(pipeline.channelName).activeColor | |
| 391 | + : getPaymentIcon(pipeline.channelName).color, | |
| 392 | + }" | |
| 393 | + > | |
| 394 | + <path | |
| 395 | + d="M8.5 9.5c-.8 0-1.5.7-1.5 1.5s.7 1.5 1.5 1.5 1.5-.7 1.5-1.5-.7-1.5-1.5-1.5zm7 0c-.8 0-1.5.7-1.5 1.5s.7 1.5 1.5 1.5 1.5-.7 1.5-1.5-.7-1.5-1.5-1.5zM12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm0 18c-1.7 0-3.3-.5-4.7-1.3l-4.8 1.5 1.5-4.5C3.4 14.5 3 13.3 3 12c0-5 4-9 9-9s9 4 9 9-4 9-9 9z" | |
| 396 | + /> | |
| 397 | + </svg> | |
| 398 | + | |
| 399 | + <!-- 园区卡图标 --> | |
| 400 | + <ElIcon | |
| 401 | + v-else-if=" | |
| 402 | + getPaymentIcon(pipeline.channelName).icon === 'card' | |
| 403 | + " | |
| 404 | + :size="28" | |
| 405 | + :color=" | |
| 406 | + currentPaymentMethod === pipeline.pipelineId | |
| 407 | + ? getPaymentIcon(pipeline.channelName).activeColor | |
| 408 | + : getPaymentIcon(pipeline.channelName).color | |
| 409 | + " | |
| 410 | + > | |
| 411 | + <CreditCard /> | |
| 412 | + </ElIcon> | |
| 413 | + | |
| 414 | + <!-- 其他支付方式图标 --> | |
| 415 | + <ElIcon | |
| 416 | + v-else | |
| 417 | + :size="28" | |
| 418 | + :color=" | |
| 419 | + currentPaymentMethod === pipeline.pipelineId | |
| 420 | + ? getPaymentIcon(pipeline.channelName).activeColor | |
| 421 | + : getPaymentIcon(pipeline.channelName).color | |
| 422 | + " | |
| 423 | + > | |
| 424 | + <Wallet /> | |
| 425 | + </ElIcon> | |
| 426 | + </div> | |
| 427 | + <div class="payment-info"> | |
| 428 | + <p class="payment-name">{{ pipeline.channelName }}</p> | |
| 429 | + <p class="payment-desc">{{ getPaymentDesc(pipeline) }}</p> | |
| 430 | + </div> | |
| 174 | 431 | </div> |
| 432 | + <ElRadio | |
| 433 | + :model-value="currentPaymentMethod" | |
| 434 | + :label="pipeline.pipelineId" | |
| 435 | + size="large" | |
| 436 | + /> | |
| 437 | + </div> | |
| 438 | + | |
| 439 | + <!-- 无支付方式提示 --> | |
| 440 | + <div v-if="paymentPipelines.length === 0" class="no-payment"> | |
| 441 | + <p>暂无可用支付方式</p> | |
| 175 | 442 | </div> |
| 176 | - <ElIcon | |
| 177 | - :size="20" | |
| 178 | - :color="currentPaymentMethod === 'card' ? '#fff' : '#909399'" | |
| 179 | - > | |
| 180 | - <ArrowRight /> | |
| 181 | - </ElIcon> | |
| 182 | 443 | </div> |
| 183 | - </ElCard> | |
| 444 | + </div> | |
| 184 | 445 | |
| 185 | - <!-- 微信支付确认按钮 --> | |
| 186 | - <transition name="slide-fade"> | |
| 446 | + <!-- 底部支付按钮 --> | |
| 447 | + <div class="payment-footer"> | |
| 187 | 448 | <ElButton |
| 188 | - v-if="payBtnShow === true" | |
| 449 | + :disabled="!payBtnShow" | |
| 189 | 450 | class="pay-button" |
| 190 | - type="success" | |
| 451 | + type="primary" | |
| 191 | 452 | size="large" |
| 192 | 453 | :loading="loading" |
| 193 | 454 | @click="handlePay" |
| 194 | 455 | > |
| 195 | - {{ loading ? '处理中...' : `确认支付 ¥${amount.toFixed(2)}` }} | |
| 456 | + {{ loading ? '处理中...' : `确认支付 ¥${displayAmount}` }} | |
| 196 | 457 | </ElButton> |
| 197 | - </transition> | |
| 458 | + </div> | |
| 198 | 459 | </div> |
| 199 | 460 | |
| 200 | 461 | <!-- 园区卡选择弹窗 --> |
| ... | ... | @@ -208,20 +469,25 @@ const handleCloseDialog = () => { |
| 208 | 469 | > |
| 209 | 470 | <div class="card-list-dialog"> |
| 210 | 471 | <div |
| 211 | - v-for="card in mockCards" | |
| 212 | - :key="card.id" | |
| 472 | + v-for="card in cardList" | |
| 473 | + :key="card.cardNo" | |
| 213 | 474 | class="card-item-dialog" |
| 214 | 475 | @click="handleCardSelect(card)" |
| 215 | 476 | > |
| 216 | 477 | <div class="card-item-info"> |
| 217 | - <p class="card-holder">{{ card.holderName }}</p> | |
| 478 | + <p class="card-holder">{{ card.name }}</p> | |
| 218 | 479 | <p class="card-number">{{ card.cardNo }}</p> |
| 219 | 480 | </div> |
| 220 | 481 | <div class="card-balance"> |
| 221 | 482 | <p class="balance-label">余额</p> |
| 222 | - <p class="balance-value">¥{{ card.balance.toFixed(2) }}</p> | |
| 483 | + <p class="balance-value"> | |
| 484 | + ¥{{ (Number(card.amount) / 100).toFixed(2) }} | |
| 485 | + </p> | |
| 223 | 486 | </div> |
| 224 | 487 | </div> |
| 488 | + <div v-if="cardList.length === 0" class="empty-card"> | |
| 489 | + <p>暂无可用园区卡</p> | |
| 490 | + </div> | |
| 225 | 491 | </div> |
| 226 | 492 | <template #footer> |
| 227 | 493 | <ElButton @click="handleCloseDialog">取消</ElButton> |
| ... | ... | @@ -296,39 +562,113 @@ const handleCloseDialog = () => { |
| 296 | 562 | |
| 297 | 563 | .cashier-container { |
| 298 | 564 | min-height: 100vh; |
| 565 | + background: linear-gradient( | |
| 566 | + to bottom, | |
| 567 | + #fff9f1 0%, | |
| 568 | + #ffe9d4 20%, | |
| 569 | + #ffe3c8 29%, | |
| 570 | + #f7f7f7 45%, | |
| 571 | + #f7f7f7 100% | |
| 572 | + ); | |
| 573 | +} | |
| 574 | + | |
| 575 | +// 错误提示容器 | |
| 576 | +.error-container { | |
| 577 | + display: flex; | |
| 578 | + align-items: center; | |
| 579 | + justify-content: center; | |
| 580 | + min-height: 100vh; | |
| 299 | 581 | padding: 20px; |
| 300 | - background: linear-gradient(135deg, #f0fdf4 0%, #d1fae5 100%); | |
| 582 | +} | |
| 301 | 583 | |
| 302 | - @media (min-width: 768px) { | |
| 303 | - padding: 40px; | |
| 584 | +.error-content { | |
| 585 | + max-width: 500px; | |
| 586 | + padding: 40px 30px; | |
| 587 | + text-align: center; | |
| 588 | + background: #fff; | |
| 589 | + border-radius: 16px; | |
| 590 | + box-shadow: 0 4px 20px rgb(0 0 0 / 8%); | |
| 591 | + animation: fadeIn 0.5s ease-out; | |
| 592 | + | |
| 593 | + .error-icon { | |
| 594 | + margin-bottom: 24px; | |
| 595 | + animation: bounceIn 0.6s ease-out; | |
| 596 | + } | |
| 597 | + | |
| 598 | + .error-title { | |
| 599 | + margin: 0 0 12px; | |
| 600 | + font-size: 22px; | |
| 601 | + font-weight: bold; | |
| 602 | + color: #1f2937; | |
| 603 | + } | |
| 604 | + | |
| 605 | + .error-desc { | |
| 606 | + margin: 0 0 24px; | |
| 607 | + font-size: 15px; | |
| 608 | + line-height: 1.6; | |
| 609 | + color: #6b7280; | |
| 610 | + } | |
| 611 | + | |
| 612 | + .error-tips { | |
| 613 | + padding: 20px; | |
| 614 | + text-align: left; | |
| 615 | + background: #fef3c7; | |
| 616 | + border-radius: 12px; | |
| 617 | + | |
| 618 | + p { | |
| 619 | + margin: 0 0 12px; | |
| 620 | + font-size: 14px; | |
| 621 | + font-weight: 600; | |
| 622 | + color: #92400e; | |
| 623 | + } | |
| 624 | + | |
| 625 | + ul { | |
| 626 | + padding-left: 20px; | |
| 627 | + margin: 0; | |
| 628 | + | |
| 629 | + li { | |
| 630 | + margin-bottom: 8px; | |
| 631 | + font-size: 13px; | |
| 632 | + line-height: 1.5; | |
| 633 | + color: #78350f; | |
| 634 | + | |
| 635 | + &:last-child { | |
| 636 | + margin-bottom: 0; | |
| 637 | + } | |
| 638 | + } | |
| 639 | + } | |
| 304 | 640 | } |
| 305 | 641 | } |
| 306 | 642 | |
| 307 | 643 | .cashier-wrapper { |
| 644 | + display: flex; | |
| 645 | + flex-direction: column; | |
| 308 | 646 | max-width: 500px; |
| 647 | + min-height: 100vh; | |
| 309 | 648 | margin: 0 auto; |
| 310 | 649 | } |
| 311 | 650 | |
| 651 | +.cashier-content { | |
| 652 | + flex: 1; | |
| 653 | + padding: 20px; | |
| 654 | + overflow-y: auto; | |
| 655 | + | |
| 656 | + @media (min-width: 768px) { | |
| 657 | + padding: 40px 20px 20px; | |
| 658 | + } | |
| 659 | +} | |
| 660 | + | |
| 312 | 661 | // 头部样式 |
| 313 | 662 | .header { |
| 663 | + display: flex; | |
| 664 | + align-items: end; | |
| 665 | + justify-content: flex-start; | |
| 314 | 666 | margin-bottom: 30px; |
| 315 | 667 | text-align: center; |
| 316 | 668 | animation: fadeIn 0.5s ease-out; |
| 317 | 669 | |
| 318 | - .icon-wrapper { | |
| 319 | - display: inline-flex; | |
| 320 | - align-items: center; | |
| 321 | - justify-content: center; | |
| 322 | - width: 64px; | |
| 323 | - height: 64px; | |
| 324 | - margin-bottom: 16px; | |
| 325 | - background: linear-gradient(135deg, #10b981 0%, #059669 100%); | |
| 326 | - border-radius: 50%; | |
| 327 | - box-shadow: 0 10px 30px rgb(16 185 129 / 30%); | |
| 328 | - } | |
| 329 | - | |
| 330 | 670 | .title { |
| 331 | - margin: 0 0 8px; | |
| 671 | + margin-right: 10px; | |
| 332 | 672 | font-size: 28px; |
| 333 | 673 | font-weight: bold; |
| 334 | 674 | color: #1f2937; |
| ... | ... | @@ -339,7 +679,7 @@ const handleCloseDialog = () => { |
| 339 | 679 | } |
| 340 | 680 | |
| 341 | 681 | .subtitle { |
| 342 | - margin: 0; | |
| 682 | + margin-top: 18px; | |
| 343 | 683 | font-size: 14px; |
| 344 | 684 | color: #6b7280; |
| 345 | 685 | } |
| ... | ... | @@ -347,16 +687,23 @@ const handleCloseDialog = () => { |
| 347 | 687 | |
| 348 | 688 | // 金额卡片 |
| 349 | 689 | .amount-card { |
| 690 | + display: flex; | |
| 691 | + flex-direction: column; | |
| 692 | + align-items: center; | |
| 693 | + justify-content: center; | |
| 694 | + min-height: 100px; | |
| 695 | + padding: 32px; | |
| 350 | 696 | margin-bottom: 24px; |
| 697 | + background: linear-gradient(180deg, #fff8ee 0%, #fff 100%); | |
| 351 | 698 | border: none; |
| 352 | 699 | border-radius: 16px; |
| 353 | 700 | animation: slideUp 0.5s ease-out 0.1s both; |
| 354 | 701 | |
| 355 | - :deep(.el-card__body) { | |
| 356 | - padding: 32px; | |
| 357 | - } | |
| 358 | - | |
| 359 | 702 | .amount-content { |
| 703 | + display: flex; | |
| 704 | + flex-direction: column; | |
| 705 | + align-items: center; | |
| 706 | + justify-content: center; | |
| 360 | 707 | text-align: center; |
| 361 | 708 | |
| 362 | 709 | .amount-label { |
| ... | ... | @@ -366,29 +713,38 @@ const handleCloseDialog = () => { |
| 366 | 713 | } |
| 367 | 714 | |
| 368 | 715 | .amount-value { |
| 369 | - margin: 0; | |
| 370 | - font-size: 48px; | |
| 371 | - font-weight: bold; | |
| 372 | - color: #10b981; | |
| 716 | + margin: 0 0 8px; | |
| 717 | + font-size: 26px; | |
| 718 | + font-weight: 700; | |
| 719 | + color: #ea4200; | |
| 373 | 720 | |
| 374 | 721 | @media (min-width: 768px) { |
| 375 | - font-size: 56px; | |
| 722 | + font-size: 48px; | |
| 376 | 723 | } |
| 377 | 724 | } |
| 725 | + | |
| 726 | + .goods-info { | |
| 727 | + margin: 0; | |
| 728 | + font-size: 14px; | |
| 729 | + color: #6b7280; | |
| 730 | + } | |
| 378 | 731 | } |
| 379 | 732 | } |
| 380 | 733 | |
| 381 | -// 支付方式卡片 | |
| 382 | -.payment-card { | |
| 734 | +// 支付方式区域 | |
| 735 | +.payment-section { | |
| 383 | 736 | margin-bottom: 24px; |
| 384 | - border: none; | |
| 385 | - border-radius: 16px; | |
| 386 | 737 | animation: slideUp 0.5s ease-out 0.2s both; |
| 387 | 738 | |
| 388 | - .card-header { | |
| 389 | - font-size: 16px; | |
| 390 | - font-weight: 600; | |
| 391 | - color: #1f2937; | |
| 739 | + .section-header { | |
| 740 | + padding: 0 4px; | |
| 741 | + margin-bottom: 16px; | |
| 742 | + | |
| 743 | + .section-title { | |
| 744 | + font-size: 16px; | |
| 745 | + font-weight: 600; | |
| 746 | + color: #1f2937; | |
| 747 | + } | |
| 392 | 748 | } |
| 393 | 749 | |
| 394 | 750 | .payment-item { |
| ... | ... | @@ -398,7 +754,8 @@ const handleCloseDialog = () => { |
| 398 | 754 | padding: 16px; |
| 399 | 755 | margin-bottom: 12px; |
| 400 | 756 | cursor: pointer; |
| 401 | - background: #f9fafb; | |
| 757 | + background: #fff; | |
| 758 | + border: 2px solid transparent; | |
| 402 | 759 | border-radius: 12px; |
| 403 | 760 | transition: all 0.3s ease; |
| 404 | 761 | |
| ... | ... | @@ -407,19 +764,11 @@ const handleCloseDialog = () => { |
| 407 | 764 | } |
| 408 | 765 | |
| 409 | 766 | &:hover { |
| 410 | - background: #f3f4f6; | |
| 411 | - transform: translateX(4px); | |
| 767 | + border-color: #fecaca; | |
| 412 | 768 | } |
| 413 | 769 | |
| 414 | 770 | &.active { |
| 415 | - color: white; | |
| 416 | - background: linear-gradient(135deg, #10b981 0%, #059669 100%); | |
| 417 | - box-shadow: 0 8px 24px rgb(16 185 129 / 40%); | |
| 418 | - transform: scale(1.02); | |
| 419 | - | |
| 420 | - .payment-desc { | |
| 421 | - color: rgb(255 255 255 / 80%); | |
| 422 | - } | |
| 771 | + border-color: #ea4200; | |
| 423 | 772 | } |
| 424 | 773 | |
| 425 | 774 | .payment-item-left { |
| ... | ... | @@ -434,29 +783,20 @@ const handleCloseDialog = () => { |
| 434 | 783 | justify-content: center; |
| 435 | 784 | width: 48px; |
| 436 | 785 | height: 48px; |
| 437 | - background: #d1fae5; | |
| 438 | 786 | border-radius: 50%; |
| 439 | 787 | transition: all 0.3s ease; |
| 440 | 788 | |
| 441 | - &.active { | |
| 442 | - background: rgb(255 255 255 / 20%); | |
| 443 | - } | |
| 444 | - | |
| 445 | - .wechat-icon { | |
| 789 | + .payment-svg-icon { | |
| 446 | 790 | width: 28px; |
| 447 | 791 | height: 28px; |
| 448 | - color: #10b981; | |
| 449 | 792 | } |
| 450 | 793 | } |
| 451 | 794 | |
| 452 | - &.active .wechat-icon { | |
| 453 | - color: white; | |
| 454 | - } | |
| 455 | - | |
| 456 | 795 | .payment-name { |
| 457 | 796 | margin: 0 0 4px; |
| 458 | 797 | font-size: 16px; |
| 459 | 798 | font-weight: 600; |
| 799 | + color: #1f2937; | |
| 460 | 800 | } |
| 461 | 801 | |
| 462 | 802 | .payment-desc { |
| ... | ... | @@ -464,27 +804,73 @@ const handleCloseDialog = () => { |
| 464 | 804 | font-size: 13px; |
| 465 | 805 | color: #6b7280; |
| 466 | 806 | } |
| 807 | + | |
| 808 | + :deep(.el-radio) { | |
| 809 | + margin-right: 0; | |
| 810 | + | |
| 811 | + .el-radio__input { | |
| 812 | + .el-radio__inner { | |
| 813 | + width: 20px; | |
| 814 | + height: 20px; | |
| 815 | + border-width: 2px; | |
| 816 | + } | |
| 817 | + } | |
| 818 | + | |
| 819 | + .el-radio__label { | |
| 820 | + display: none; | |
| 821 | + } | |
| 822 | + } | |
| 823 | + | |
| 824 | + &.active :deep(.el-radio) { | |
| 825 | + .el-radio__input.is-checked { | |
| 826 | + .el-radio__inner { | |
| 827 | + background-color: #ea4200; | |
| 828 | + border-color: #ea4200; | |
| 829 | + } | |
| 830 | + } | |
| 831 | + } | |
| 832 | + } | |
| 833 | + | |
| 834 | + .no-payment { | |
| 835 | + padding: 40px 20px; | |
| 836 | + color: #9ca3af; | |
| 837 | + text-align: center; | |
| 838 | + background: #fff; | |
| 839 | + border-radius: 12px; | |
| 467 | 840 | } |
| 468 | 841 | } |
| 469 | 842 | |
| 470 | -// 支付按钮 | |
| 471 | -.pay-button { | |
| 472 | - width: 100%; | |
| 473 | - height: 56px; | |
| 474 | - font-size: 18px; | |
| 475 | - font-weight: 600; | |
| 476 | - background: linear-gradient(135deg, #10b981 0%, #059669 100%); | |
| 477 | - border: none; | |
| 478 | - border-radius: 16px; | |
| 479 | - box-shadow: 0 8px 24px rgb(16 185 129 / 40%); | |
| 843 | +// 底部支付按钮区域 | |
| 844 | +.payment-footer { | |
| 845 | + padding: 16px 20px; | |
| 846 | + padding-bottom: calc(16px + env(safe-area-inset-bottom)); | |
| 847 | + background: #fff; | |
| 848 | + border-top: 1px solid #f0f0f0; | |
| 480 | 849 | |
| 481 | - &:hover { | |
| 482 | - box-shadow: 0 12px 32px rgb(16 185 129 / 50%); | |
| 483 | - transform: translateY(-2px); | |
| 484 | - } | |
| 850 | + .pay-button { | |
| 851 | + width: 100%; | |
| 852 | + height: 42px; | |
| 853 | + font-size: 18px; | |
| 854 | + font-weight: 600; | |
| 855 | + color: #fff; | |
| 856 | + background: #ea4200; | |
| 857 | + border: none; | |
| 858 | + border-radius: 21px; | |
| 859 | + box-shadow: 0 4px 16px rgb(234 66 0 / 30%); | |
| 860 | + | |
| 861 | + &:hover:not(:disabled) { | |
| 862 | + background: #d93d00; | |
| 863 | + box-shadow: 0 6px 20px rgb(234 66 0 / 40%); | |
| 864 | + } | |
| 865 | + | |
| 866 | + &:active:not(:disabled) { | |
| 867 | + transform: scale(0.98); | |
| 868 | + } | |
| 485 | 869 | |
| 486 | - &:active { | |
| 487 | - transform: scale(0.98); | |
| 870 | + &:disabled { | |
| 871 | + cursor: not-allowed; | |
| 872 | + opacity: 0.5; | |
| 873 | + } | |
| 488 | 874 | } |
| 489 | 875 | } |
| 490 | 876 | |
| ... | ... | @@ -512,6 +898,12 @@ const handleCloseDialog = () => { |
| 512 | 898 | gap: 12px; |
| 513 | 899 | max-height: 400px; |
| 514 | 900 | overflow-y: auto; |
| 901 | + | |
| 902 | + .empty-card { | |
| 903 | + padding: 40px 20px; | |
| 904 | + color: #9ca3af; | |
| 905 | + text-align: center; | |
| 906 | + } | |
| 515 | 907 | } |
| 516 | 908 | |
| 517 | 909 | .card-item-dialog { |
| ... | ... | @@ -602,23 +994,4 @@ const handleCloseDialog = () => { |
| 602 | 994 | color: #6b7280; |
| 603 | 995 | } |
| 604 | 996 | } |
| 605 | - | |
| 606 | -// 过渡动画 | |
| 607 | -.slide-fade-enter-active { | |
| 608 | - transition: all 0.3s ease-out; | |
| 609 | -} | |
| 610 | - | |
| 611 | -.slide-fade-leave-active { | |
| 612 | - transition: all 0.2s cubic-bezier(1, 0.5, 0.8, 1); | |
| 613 | -} | |
| 614 | - | |
| 615 | -.slide-fade-enter-from { | |
| 616 | - opacity: 0; | |
| 617 | - transform: translateY(20px); | |
| 618 | -} | |
| 619 | - | |
| 620 | -.slide-fade-leave-to { | |
| 621 | - opacity: 0; | |
| 622 | - transform: translateY(-20px); | |
| 623 | -} | |
| 624 | 997 | </style> | ... | ... |
apps/web-payment/vite.config.mts