Commit bf9336df9e2f92ddb5cab32692a1440be1be8cf2

Authored by tianwu
1 parent a16b16b8

收银台微信支付

apps/web-payment/.env
1 1 # 应用标题
2   -VITE_APP_TITLE=Vben Admin Ele
  2 +VITE_APP_TITLE=收银台
3 3  
4 4 # 应用命名空间,用于缓存、store等功能的前缀,确保隔离
5 5 VITE_APP_NAMESPACE=vben-web-payment
... ...
apps/web-payment/index.html
... ... @@ -17,6 +17,7 @@
17 17 </head>
18 18 <body>
19 19 <div id="app"></div>
  20 + <script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.6.0.js" ></script>
20 21 <script type="module" src="/src/main.ts"></script>
21 22 </body>
22 23 </html>
... ...
apps/web-payment/src/api/index.ts
1 1 export * from './core';
  2 +export * from './payment';
... ...
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
... ... @@ -76,7 +76,7 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
76 76 defaultResponseInterceptor({
77 77 codeField: 'code',
78 78 dataField: 'data',
79   - successCode: 0,
  79 + successCode: '200',
80 80 }),
81 81 );
82 82  
... ...
apps/web-payment/src/router/routes/core.ts
... ... @@ -40,7 +40,7 @@ const coreRoutes: RouteRecordRaw[] = [
40 40 component: () => import('#/views/payment/index.vue'),
41 41 meta: {
42 42 icon: 'lucide:area-chart',
43   - title: '收银台',
  43 + title: '订单支付',
44 44 },
45 45 },
46 46 ];
... ...
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) =&gt; {
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 = () =&gt; {
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 = () =&gt; {
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 = () =&gt; {
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 = () =&gt; {
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 = () =&gt; {
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 = () =&gt; {
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 = () =&gt; {
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 = () =&gt; {
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 = () =&gt; {
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 = () =&gt; {
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 = () =&gt; {
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
... ... @@ -17,7 +17,7 @@ export default defineConfig(async () =&gt; {
17 17 changeOrigin: true,
18 18 rewrite: (path) => path.replace(/^\/api/, ''),
19 19 // mock代理目标地址
20   - target: 'http://localhost:5320/api',
  20 + target: 'http://10.28.3.24:8686',
21 21 ws: true,
22 22 },
23 23 },
... ...