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 # 应用命名空间,用于缓存、store等功能的前缀,确保隔离 4 # 应用命名空间,用于缓存、store等功能的前缀,确保隔离
5 VITE_APP_NAMESPACE=vben-web-payment 5 VITE_APP_NAMESPACE=vben-web-payment
apps/web-payment/index.html
@@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
17 </head> 17 </head>
18 <body> 18 <body>
19 <div id="app"></div> 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 <script type="module" src="/src/main.ts"></script> 21 <script type="module" src="/src/main.ts"></script>
21 </body> 22 </body>
22 </html> 23 </html>
apps/web-payment/src/api/index.ts
1 export * from './core'; 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,7 +76,7 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
76 defaultResponseInterceptor({ 76 defaultResponseInterceptor({
77 codeField: 'code', 77 codeField: 'code',
78 dataField: 'data', 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,7 +40,7 @@ const coreRoutes: RouteRecordRaw[] = [
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: 'lucide:area-chart',
43 - title: '收银台', 43 + title: '订单支付',
44 }, 44 },
45 }, 45 },
46 ]; 46 ];
apps/web-payment/src/views/payment/index.vue
1 <script setup lang="ts"> 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 import EnvironmentDetector from '#/composables/environmentDetector'; 15 import EnvironmentDetector from '#/composables/environmentDetector';
  16 +
8 // 类型定义 17 // 类型定义
9 interface Card { 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 const detector = new EnvironmentDetector(); 32 const detector = new EnvironmentDetector();
18 detector.logEnvironment(); 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 const payBtnShow = ref<boolean>(false); 52 const payBtnShow = ref<boolean>(false);
40 -const currentPaymentMethod = ref<string>(''); 53 +const currentPaymentMethod = ref<null | number>(null); // 改为存储 pipelineId
41 const showCardDialog = ref<boolean>(false); 54 const showCardDialog = ref<boolean>(false);
42 const selectedCard = ref<Card | null>(null); 55 const selectedCard = ref<Card | null>(null);
43 const loading = ref<boolean>(false); 56 const loading = ref<boolean>(false);
44 const paymentSuccess = ref<boolean>(false); 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 payBtnShow.value = false; 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 showCardDialog.value = true; 145 showCardDialog.value = true;
52 } else { 146 } else {
  147 + // 其他支付方式直接显示支付按钮
53 payBtnShow.value = true; 148 payBtnShow.value = true;
54 } 149 }
55 }; 150 };
@@ -57,144 +152,310 @@ const handlepayBtnShowClick = (method: string) =&gt; { @@ -57,144 +152,310 @@ const handlepayBtnShowClick = (method: string) =&gt; {
57 const handleCardSelect = (card: Card) => { 152 const handleCardSelect = (card: Card) => {
58 selectedCard.value = card; 153 selectedCard.value = card;
59 showCardDialog.value = false; 154 showCardDialog.value = false;
60 - ElMessage.success(`已选择 ${card.holderName} 的园区卡`); 155 + ElMessage.success(`已选择 ${card.name} 的园区卡`);
61 payBtnShow.value = true; 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 const handlePay = async () => { 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 loading.value = true; 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 const handleCloseDialog = () => { 254 const handleCloseDialog = () => {
84 showCardDialog.value = false; 255 showCardDialog.value = false;
85 payBtnShow.value = false; 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 </script> 311 </script>
88 312
89 <template> 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 </ElIcon> 321 </ElIcon>
98 </div> 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 </div> 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 </div> 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 </div> 351 </div>
139 - <ElIcon  
140 - :size="20"  
141 - :color="currentPaymentMethod === 'wechat' ? '#fff' : '#909399'"  
142 - >  
143 - <ArrowRight />  
144 - </ElIcon>  
145 </div> 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 </div> 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 </div> 442 </div>
176 - <ElIcon  
177 - :size="20"  
178 - :color="currentPaymentMethod === 'card' ? '#fff' : '#909399'"  
179 - >  
180 - <ArrowRight />  
181 - </ElIcon>  
182 </div> 443 </div>
183 - </ElCard> 444 + </div>
184 445
185 - <!-- 微信支付确认按钮 -->  
186 - <transition name="slide-fade"> 446 + <!-- 底部支付按钮 -->
  447 + <div class="payment-footer">
187 <ElButton 448 <ElButton
188 - v-if="payBtnShow === true" 449 + :disabled="!payBtnShow"
189 class="pay-button" 450 class="pay-button"
190 - type="success" 451 + type="primary"
191 size="large" 452 size="large"
192 :loading="loading" 453 :loading="loading"
193 @click="handlePay" 454 @click="handlePay"
194 > 455 >
195 - {{ loading ? '处理中...' : `确认支付 ¥${amount.toFixed(2)}` }} 456 + {{ loading ? '处理中...' : `确认支付 ¥${displayAmount}` }}
196 </ElButton> 457 </ElButton>
197 - </transition> 458 + </div>
198 </div> 459 </div>
199 460
200 <!-- 园区卡选择弹窗 --> 461 <!-- 园区卡选择弹窗 -->
@@ -208,20 +469,25 @@ const handleCloseDialog = () =&gt; { @@ -208,20 +469,25 @@ const handleCloseDialog = () =&gt; {
208 > 469 >
209 <div class="card-list-dialog"> 470 <div class="card-list-dialog">
210 <div 471 <div
211 - v-for="card in mockCards"  
212 - :key="card.id" 472 + v-for="card in cardList"
  473 + :key="card.cardNo"
213 class="card-item-dialog" 474 class="card-item-dialog"
214 @click="handleCardSelect(card)" 475 @click="handleCardSelect(card)"
215 > 476 >
216 <div class="card-item-info"> 477 <div class="card-item-info">
217 - <p class="card-holder">{{ card.holderName }}</p> 478 + <p class="card-holder">{{ card.name }}</p>
218 <p class="card-number">{{ card.cardNo }}</p> 479 <p class="card-number">{{ card.cardNo }}</p>
219 </div> 480 </div>
220 <div class="card-balance"> 481 <div class="card-balance">
221 <p class="balance-label">余额</p> 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 </div> 486 </div>
224 </div> 487 </div>
  488 + <div v-if="cardList.length === 0" class="empty-card">
  489 + <p>暂无可用园区卡</p>
  490 + </div>
225 </div> 491 </div>
226 <template #footer> 492 <template #footer>
227 <ElButton @click="handleCloseDialog">取消</ElButton> 493 <ElButton @click="handleCloseDialog">取消</ElButton>
@@ -296,39 +562,113 @@ const handleCloseDialog = () =&gt; { @@ -296,39 +562,113 @@ const handleCloseDialog = () =&gt; {
296 562
297 .cashier-container { 563 .cashier-container {
298 min-height: 100vh; 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 padding: 20px; 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 .cashier-wrapper { 643 .cashier-wrapper {
  644 + display: flex;
  645 + flex-direction: column;
308 max-width: 500px; 646 max-width: 500px;
  647 + min-height: 100vh;
309 margin: 0 auto; 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 .header { 662 .header {
  663 + display: flex;
  664 + align-items: end;
  665 + justify-content: flex-start;
314 margin-bottom: 30px; 666 margin-bottom: 30px;
315 text-align: center; 667 text-align: center;
316 animation: fadeIn 0.5s ease-out; 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 .title { 670 .title {
331 - margin: 0 0 8px; 671 + margin-right: 10px;
332 font-size: 28px; 672 font-size: 28px;
333 font-weight: bold; 673 font-weight: bold;
334 color: #1f2937; 674 color: #1f2937;
@@ -339,7 +679,7 @@ const handleCloseDialog = () =&gt; { @@ -339,7 +679,7 @@ const handleCloseDialog = () =&gt; {
339 } 679 }
340 680
341 .subtitle { 681 .subtitle {
342 - margin: 0; 682 + margin-top: 18px;
343 font-size: 14px; 683 font-size: 14px;
344 color: #6b7280; 684 color: #6b7280;
345 } 685 }
@@ -347,16 +687,23 @@ const handleCloseDialog = () =&gt; { @@ -347,16 +687,23 @@ const handleCloseDialog = () =&gt; {
347 687
348 // 金额卡片 688 // 金额卡片
349 .amount-card { 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 margin-bottom: 24px; 696 margin-bottom: 24px;
  697 + background: linear-gradient(180deg, #fff8ee 0%, #fff 100%);
351 border: none; 698 border: none;
352 border-radius: 16px; 699 border-radius: 16px;
353 animation: slideUp 0.5s ease-out 0.1s both; 700 animation: slideUp 0.5s ease-out 0.1s both;
354 701
355 - :deep(.el-card__body) {  
356 - padding: 32px;  
357 - }  
358 -  
359 .amount-content { 702 .amount-content {
  703 + display: flex;
  704 + flex-direction: column;
  705 + align-items: center;
  706 + justify-content: center;
360 text-align: center; 707 text-align: center;
361 708
362 .amount-label { 709 .amount-label {
@@ -366,29 +713,38 @@ const handleCloseDialog = () =&gt; { @@ -366,29 +713,38 @@ const handleCloseDialog = () =&gt; {
366 } 713 }
367 714
368 .amount-value { 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 @media (min-width: 768px) { 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 margin-bottom: 24px; 736 margin-bottom: 24px;
384 - border: none;  
385 - border-radius: 16px;  
386 animation: slideUp 0.5s ease-out 0.2s both; 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 .payment-item { 750 .payment-item {
@@ -398,7 +754,8 @@ const handleCloseDialog = () =&gt; { @@ -398,7 +754,8 @@ const handleCloseDialog = () =&gt; {
398 padding: 16px; 754 padding: 16px;
399 margin-bottom: 12px; 755 margin-bottom: 12px;
400 cursor: pointer; 756 cursor: pointer;
401 - background: #f9fafb; 757 + background: #fff;
  758 + border: 2px solid transparent;
402 border-radius: 12px; 759 border-radius: 12px;
403 transition: all 0.3s ease; 760 transition: all 0.3s ease;
404 761
@@ -407,19 +764,11 @@ const handleCloseDialog = () =&gt; { @@ -407,19 +764,11 @@ const handleCloseDialog = () =&gt; {
407 } 764 }
408 765
409 &:hover { 766 &:hover {
410 - background: #f3f4f6;  
411 - transform: translateX(4px); 767 + border-color: #fecaca;
412 } 768 }
413 769
414 &.active { 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 .payment-item-left { 774 .payment-item-left {
@@ -434,29 +783,20 @@ const handleCloseDialog = () =&gt; { @@ -434,29 +783,20 @@ const handleCloseDialog = () =&gt; {
434 justify-content: center; 783 justify-content: center;
435 width: 48px; 784 width: 48px;
436 height: 48px; 785 height: 48px;
437 - background: #d1fae5;  
438 border-radius: 50%; 786 border-radius: 50%;
439 transition: all 0.3s ease; 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 width: 28px; 790 width: 28px;
447 height: 28px; 791 height: 28px;
448 - color: #10b981;  
449 } 792 }
450 } 793 }
451 794
452 - &.active .wechat-icon {  
453 - color: white;  
454 - }  
455 -  
456 .payment-name { 795 .payment-name {
457 margin: 0 0 4px; 796 margin: 0 0 4px;
458 font-size: 16px; 797 font-size: 16px;
459 font-weight: 600; 798 font-weight: 600;
  799 + color: #1f2937;
460 } 800 }
461 801
462 .payment-desc { 802 .payment-desc {
@@ -464,27 +804,73 @@ const handleCloseDialog = () =&gt; { @@ -464,27 +804,73 @@ const handleCloseDialog = () =&gt; {
464 font-size: 13px; 804 font-size: 13px;
465 color: #6b7280; 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,6 +898,12 @@ const handleCloseDialog = () =&gt; {
512 gap: 12px; 898 gap: 12px;
513 max-height: 400px; 899 max-height: 400px;
514 overflow-y: auto; 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 .card-item-dialog { 909 .card-item-dialog {
@@ -602,23 +994,4 @@ const handleCloseDialog = () =&gt; { @@ -602,23 +994,4 @@ const handleCloseDialog = () =&gt; {
602 color: #6b7280; 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 </style> 997 </style>
apps/web-payment/vite.config.mts
@@ -17,7 +17,7 @@ export default defineConfig(async () =&gt; { @@ -17,7 +17,7 @@ export default defineConfig(async () =&gt; {
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://localhost:5320/api', 20 + target: 'http://10.28.3.24:8686',
21 ws: true, 21 ws: true,
22 }, 22 },
23 }, 23 },