index.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. <template>
  2. <view class="page">
  3. <!-- 顶部导航栏 -->
  4. <view class="nav-bar">
  5. <view class="nav-content">
  6. <view class="nav-left" @click="goBack">
  7. <text class="back-icon">←</text>
  8. </view>
  9. <text class="page-title">确认支付</text>
  10. </view>
  11. </view>
  12. <!-- 支付内容 -->
  13. <view class="content">
  14. <!-- 订单信息 -->
  15. <view class="order-card">
  16. <text class="order-title">订单信息</text>
  17. <view class="order-info">
  18. <text class="info-label">商品</text>
  19. <text class="info-value">{{ orderInfo.planName || '-' }}</text>
  20. </view>
  21. <view class="order-info">
  22. <text class="info-label">订单号</text>
  23. <text class="info-value">{{ orderInfo.orderNo || '-' }}</text>
  24. </view>
  25. <view class="order-info highlight">
  26. <text class="info-label">应付金额</text>
  27. <text class="info-value price">¥{{ orderInfo.amount || 0 }}</text>
  28. </view>
  29. </view>
  30. <!-- 支付方式选择 -->
  31. <view class="payment-section">
  32. <text class="section-title">选择支付方式</text>
  33. <!-- 公众号环境提示 -->
  34. <view class="wechat-notice" v-if="inWechat">
  35. <text class="wechat-notice-icon">💚</text>
  36. <text class="wechat-notice-text">检测到微信环境,将自动使用微信支付</text>
  37. </view>
  38. <view class="payment-methods">
  39. <view
  40. class="payment-method"
  41. :class="{ active: paymentMethod === 'alipay' }"
  42. @click="paymentMethod = 'alipay'"
  43. >
  44. <view class="method-radio">
  45. <view class="radio-inner" v-if="paymentMethod === 'alipay'"></view>
  46. </view>
  47. <text class="method-icon">💙</text>
  48. <text class="method-name">支付宝</text>
  49. </view>
  50. <view
  51. class="payment-method"
  52. :class="{ active: paymentMethod === 'wechat' }"
  53. @click="paymentMethod = 'wechat'"
  54. >
  55. <view class="method-radio">
  56. <view class="radio-inner" v-if="paymentMethod === 'wechat'"></view>
  57. </view>
  58. <text class="method-icon">🟢</text>
  59. <text class="method-name">微信支付</text>
  60. <text class="method-badge" v-if="inWechat">推荐</text>
  61. </view>
  62. </view>
  63. </view>
  64. <!-- 操作按钮 -->
  65. <view class="actions">
  66. <button class="btn-secondary" @click="goBack">取消</button>
  67. <button class="btn-primary" @click="handlePay">确认支付</button>
  68. </view>
  69. <!-- 提示 -->
  70. <view class="tips">
  71. <text class="tips-title">温馨提示</text>
  72. <text class="tips-text">• 请在24小时内完成支付</text>
  73. <text class="tips-text">• 支付成功后权益将自动到账</text>
  74. <text class="tips-text">• 如支付遇到问题,请联系客服</text>
  75. </view>
  76. </view>
  77. </view>
  78. </template>
  79. <script setup lang="ts">
  80. import { ref, onMounted } from 'vue';
  81. import { post, get } from '../../utils/request';
  82. // ==================== 微信浏览器检测 ====================
  83. function isInWechat(): boolean {
  84. // #ifdef H5
  85. const ua = navigator.userAgent.toLowerCase();
  86. return ua.includes('micromessenger');
  87. // #endif
  88. // #ifndef H5
  89. return false;
  90. // #endif
  91. }
  92. // 获取不含 hash 的基础URL(用于OAuth redirect_uri)
  93. function getBaseUrl(): string {
  94. // #ifdef H5
  95. return window.location.origin + window.location.pathname;
  96. // #endif
  97. return '';
  98. }
  99. // 保存当前页面上下文到 sessionStorage(OAuth 来回会丢失 hash 路由状态)
  100. function savePageContext() {
  101. try {
  102. sessionStorage.setItem('wx_oauth_return', window.location.href);
  103. } catch (_) {}
  104. }
  105. // 恢复页面上下文
  106. function restorePageContext(): string | null {
  107. try {
  108. return sessionStorage.getItem('wx_oauth_return');
  109. } catch (_) { return null; }
  110. }
  111. const orderInfo = ref<{
  112. orderNo: string;
  113. amount: number;
  114. planName: string;
  115. }>({
  116. orderNo: '',
  117. amount: 0,
  118. planName: ''
  119. });
  120. const planId = ref<number>(0);
  121. const productType = ref<string>(''); // 'subscription' | 'token-pack'
  122. const tokenPackQuantity = ref<number>(0);
  123. const paymentMethod = ref<'alipay' | 'wechat'>('alipay');
  124. const inWechat = ref(false);
  125. const openid = ref('');
  126. const paying = ref(false);
  127. const lastPayClick = ref(0); // 防重:记录上次点击时间
  128. let checkTimer: ReturnType<typeof setInterval> | null = null;
  129. // ==================== 微信 OAuth 处理 ====================
  130. async function handleWechatOAuth() {
  131. // #ifdef H5
  132. // OAuth 回调已在 App.vue 全局处理,这里只需读取缓存的 openid
  133. const cachedOpenid = localStorage.getItem('wx_openid');
  134. if (cachedOpenid) {
  135. openid.value = cachedOpenid;
  136. console.log('[WeChat Pay] 使用缓存的 openid');
  137. return;
  138. }
  139. // 双重保险:检查 URL search 中的 code(如果 App.vue 未拦截到)
  140. const searchParams = new URLSearchParams(window.location.search);
  141. const code = searchParams.get('code');
  142. if (code) {
  143. uni.showLoading({ title: '授权中...' });
  144. try {
  145. const result = await post<{ openid: string }>('/payment/wechat/openid', { code });
  146. openid.value = result.openid;
  147. localStorage.setItem('wx_openid', result.openid);
  148. console.log('[WeChat Pay] openid 获取成功,自动重新发起支付');
  149. // 清除 URL 中的 code/state
  150. searchParams.delete('code');
  151. searchParams.delete('state');
  152. const newSearch = searchParams.toString();
  153. const newUrl = window.location.origin + window.location.pathname
  154. + (newSearch ? '?' + newSearch : '');
  155. window.history.replaceState({}, '', newUrl);
  156. // 自动重新发起支付(不用用户再点一次)
  157. setTimeout(() => handleWechatJsapiPay(), 100);
  158. } catch (e) {
  159. console.error('[WeChat Pay] 获取 openid 失败:', e);
  160. } finally {
  161. uni.hideLoading();
  162. }
  163. }
  164. // #endif
  165. }
  166. // 跳转到微信 OAuth 授权
  167. async function redirectToWechatOAuth() {
  168. // #ifdef H5
  169. try {
  170. // 保存当前完整 URL(含 hash 路由参数),OAuth 回来时恢复
  171. savePageContext();
  172. // 用不含 # 的 baseUrl 作为 redirect_uri,避免微信报"非法字符"
  173. const baseUrl = getBaseUrl();
  174. const result = await get<{ oauthUrl: string; state: string }>('/payment/wechat/oauth-url', {
  175. redirect: baseUrl
  176. });
  177. window.location.href = result.oauthUrl;
  178. } catch (e) {
  179. console.error('[WeChat Pay] 获取OAuth链接失败:', e);
  180. }
  181. // #endif
  182. }
  183. onMounted(async () => {
  184. const pages = getCurrentPages();
  185. const currentPage = pages[pages.length - 1] as any;
  186. // #ifdef APP-PLUS
  187. const options = currentPage?.$page?.options || {};
  188. // #endif
  189. // #ifndef APP-PLUS
  190. const options = currentPage?.options || {};
  191. // #endif
  192. // 检查产品类型
  193. productType.value = options.productType || 'subscription';
  194. if (productType.value === 'token-pack') {
  195. // 积分包支付
  196. tokenPackQuantity.value = parseInt(options.quantity) || 1;
  197. orderInfo.value.planName = decodeURIComponent(options.planName || '积分包');
  198. orderInfo.value.amount = parseFloat(options.amount) || 0;
  199. } else if (options.planId) {
  200. planId.value = parseInt(options.planId);
  201. orderInfo.value.planName = decodeURIComponent(options.planName || '会员订阅');
  202. orderInfo.value.amount = parseFloat(options.amount) || 0;
  203. }
  204. // 检测是否在微信浏览器中
  205. if (isInWechat()) {
  206. inWechat.value = true;
  207. // 默认选中微信支付
  208. paymentMethod.value = 'wechat';
  209. // 处理 OAuth 回调
  210. await handleWechatOAuth();
  211. // 微信环境不预创建订单(JSAPI 支付时另外创建)
  212. } else {
  213. // 非微信环境:进入页面时直接创建订单
  214. if (productType.value === 'token-pack' || planId.value) {
  215. await createOrder();
  216. }
  217. }
  218. });
  219. async function createOrder() {
  220. try {
  221. uni.showLoading({ title: '创建订单...' });
  222. const returnUrl = `${window.location.origin}/#/pages/payment-result/index`;
  223. let result: any;
  224. if (productType.value === 'token-pack') {
  225. // 积分包订单
  226. result = await post<{
  227. orderNo: string;
  228. amount: number;
  229. paymentUrl?: string;
  230. qrcode?: string;
  231. planName?: string;
  232. }>('/payment/token-packs/create', {
  233. quantity: tokenPackQuantity.value,
  234. paymentMethod: paymentMethod.value,
  235. returnUrl
  236. });
  237. } else {
  238. // 套餐订阅订单
  239. result = await post<{
  240. orderNo: string;
  241. amount: number;
  242. paymentUrl?: string;
  243. qrcode?: string;
  244. planName?: string;
  245. }>('/payment/create', {
  246. planId: planId.value,
  247. paymentMethod: paymentMethod.value,
  248. returnUrl
  249. });
  250. }
  251. orderInfo.value.orderNo = result.orderNo;
  252. uni.hideLoading();
  253. } catch (error) {
  254. uni.hideLoading();
  255. console.error('创建订单失败:', error);
  256. }
  257. }
  258. // 查询微信订单状态(用于支付后确认)
  259. async function checkWechatOrderStatus(orderNo: string): Promise<string | null> {
  260. try {
  261. const result = await get<any>(`/payment/wechat/query/${orderNo}`);
  262. const state = result?.data?.trade_state || result?.trade_state || null;
  263. console.log('[WeChat Pay] 订单查询结果:', orderNo, state);
  264. return state;
  265. } catch (e) {
  266. console.error('[WeChat Pay] 订单查询失败:', e);
  267. return null;
  268. }
  269. }
  270. // ==================== 微信 JSAPI 支付(公众号内) ====================
  271. // 使用 WeixinJSBridge.invoke 直接调起(官方 V3 推荐方式,无需 JSSDK 配置)
  272. // 参考: https://pay.weixin.qq.com/doc/v3/merchant/4012791870
  273. async function handleWechatJsapiPay() {
  274. // 如果没有 openid,需要先授权
  275. if (!openid.value) {
  276. await redirectToWechatOAuth();
  277. return;
  278. }
  279. paying.value = true;
  280. uni.showLoading({ title: '拉起支付...' });
  281. try {
  282. // 创建 JSAPI 支付订单
  283. let result: any;
  284. if (productType.value === 'token-pack') {
  285. result = await post<any>('/payment/wechat/jsapi', {
  286. productType: 'token-pack',
  287. quantity: tokenPackQuantity.value,
  288. openid: openid.value
  289. });
  290. } else {
  291. result = await post<any>('/payment/wechat/jsapi', {
  292. planId: planId.value,
  293. openid: openid.value
  294. });
  295. }
  296. // 注意:post 函数返回的是 response.data,即 { code, message, data: {...} }
  297. const responseData = result.data || result; // 兼容直接返回 data 的情况
  298. orderInfo.value.orderNo = responseData.orderNo;
  299. uni.hideLoading();
  300. // 使用 WeixinJSBridge.invoke 调起支付(微信官方推荐方式)
  301. const currentResult = responseData; // 保存引用,避免闭包问题
  302. console.log('[WeChat Pay] 准备调起 WeixinJSBridge, payParams:', JSON.stringify(currentResult.payParams));
  303. // 检查 WeixinJSBridge 是否存在
  304. if (typeof (window as any).WeixinJSBridge === 'undefined') {
  305. console.error('[WeChat Pay] WeixinJSBridge 不存在,等待注入...');
  306. // 如果 WeixinJSBridge 还未注入,监听注入事件
  307. if (document.addEventListener) {
  308. document.addEventListener('WeixinJSBridgeReady', () => {
  309. console.log('[WeChat Pay] WeixinJSBridge 已注入,开始调起支付');
  310. invokeWeixinJSBridge(currentResult);
  311. }, false);
  312. } else if ((document as any).attachEvent) {
  313. (document as any).attachEvent('WeixinJSBridgeReady', () => {
  314. console.log('[WeChat Pay] WeixinJSBridge 已注入,开始调起支付');
  315. invokeWeixinJSBridge(currentResult);
  316. });
  317. (document as any).attachEvent('onWeixinJSBridgeReady', () => {
  318. console.log('[WeChat Pay] WeixinJSBridge 已注入,开始调起支付');
  319. invokeWeixinJSBridge(currentResult);
  320. });
  321. }
  322. return;
  323. }
  324. // WeixinJSBridge 已存在,直接调起
  325. invokeWeixinJSBridge(currentResult);
  326. } catch (error: any) {
  327. paying.value = false;
  328. uni.hideLoading();
  329. console.error('[WeChat Pay] 创建JSAPI支付失败:', error);
  330. if (error.message && error.message.includes('openid')) {
  331. localStorage.removeItem('wx_openid');
  332. openid.value = '';
  333. await redirectToWechatOAuth();
  334. return;
  335. }
  336. uni.showToast({ title: error.message || '支付失败', icon: 'none' });
  337. }
  338. }
  339. // 调用 WeixinJSBridge.invoke
  340. function invokeWeixinJSBridge(payResult: any) {
  341. const payParams = payResult.payParams;
  342. if (!payParams || !payParams.package) {
  343. console.error('[WeChat Pay] payParams 异常:', payParams);
  344. uni.showToast({ title: '支付参数异常', icon: 'none' });
  345. paying.value = false;
  346. return;
  347. }
  348. console.log('[WeChat Pay] 调用 WeixinJSBridge.invoke, params:', JSON.stringify(payParams));
  349. (window as any).WeixinJSBridge.invoke(
  350. 'getBrandWCPayRequest',
  351. {
  352. appId: payParams.appId,
  353. timeStamp: payParams.timeStamp,
  354. nonceStr: payParams.nonceStr,
  355. package: payParams.package,
  356. signType: payParams.signType || 'RSA',
  357. paySign: payParams.paySign,
  358. },
  359. async (res: any) => {
  360. console.log('[WeChat Pay] WeixinJSBridge 回调:', JSON.stringify(res));
  361. paying.value = false;
  362. if (res.err_msg === 'get_brand_wcpay_request:ok') {
  363. // 支付成功(前端返回,需调用查询API确认)
  364. console.log('[WeChat Pay] 前端返回支付成功,查询订单状态...');
  365. const orderStatus = await checkWechatOrderStatus(payResult.orderNo);
  366. if (orderStatus === 'SUCCESS') {
  367. uni.showModal({
  368. title: '支付成功',
  369. content: productType.value === 'token-pack' ? '积分包购买成功!' : '恭喜您订阅成功!',
  370. showCancel: false,
  371. success: () => uni.switchTab({ url: '/pages/mine/index' })
  372. });
  373. } else {
  374. // 前端返回成功但订单未成功(延迟回调)
  375. uni.showToast({ title: '订单确认中,请稍后查看', icon: 'none' });
  376. }
  377. } else {
  378. // 支付失败或取消
  379. console.log('[WeChat Pay] 支付失败或取消:', res.err_msg);
  380. // 官方要求:必须调用查询订单API确认最终状态
  381. const orderStatus = await checkWechatOrderStatus(payResult.orderNo);
  382. if (orderStatus === 'SUCCESS') {
  383. // 桥回调失败但订单已支付(延迟回调)
  384. uni.showModal({
  385. title: '支付成功',
  386. content: productType.value === 'token-pack' ? '积分包购买成功!' : '恭喜您订阅成功!',
  387. showCancel: false,
  388. success: () => uni.switchTab({ url: '/pages/mine/index' })
  389. });
  390. } else {
  391. // 用户取消或真正失败
  392. const isCancel = res.err_msg === 'get_brand_wcpay_request:cancel';
  393. uni.showModal({
  394. title: isCancel ? '支付取消' : '支付失败',
  395. content: isCancel ? '您已取消支付' : `支付失败 (${res.err_msg || '未知错误'})`,
  396. showCancel: !isCancel,
  397. confirmText: '重新支付',
  398. cancelText: '取消',
  399. success: (modalRes: any) => {
  400. if (modalRes.confirm) {
  401. // 重新调起支付
  402. paying.value = false;
  403. handleWechatJsapiPay();
  404. }
  405. }
  406. });
  407. }
  408. }
  409. }
  410. );
  411. }
  412. async function handlePay() {
  413. // 防重处理:2秒内禁止重复点击(官方要求按钮防抖)
  414. const now = Date.now();
  415. if (now - lastPayClick.value < 2000) {
  416. console.log('[Payment] 防重:忽略重复点击');
  417. return;
  418. }
  419. lastPayClick.value = now;
  420. if (!planId.value && productType.value !== 'token-pack') {
  421. uni.showToast({ title: '参数错误', icon: 'none' });
  422. return;
  423. }
  424. // 公众号内微信支付 → 走 JSAPI
  425. if (inWechat.value && paymentMethod.value === 'wechat') {
  426. await handleWechatJsapiPay();
  427. return;
  428. }
  429. try {
  430. uni.showLoading({ title: '创建订单...' });
  431. // #ifdef H5
  432. const returnUrl = `${window.location.origin}/#/pages/payment-result/index`;
  433. // #endif
  434. // #ifndef H5
  435. // App 端使用相对路径
  436. const returnUrl = `/#/pages/payment-result/index`;
  437. // #endif
  438. let orderResult: any;
  439. if (productType.value === 'token-pack') {
  440. orderResult = await post<any>('/payment/token-packs/create', {
  441. quantity: tokenPackQuantity.value,
  442. paymentMethod: paymentMethod.value,
  443. returnUrl
  444. });
  445. } else {
  446. orderResult = await post<any>('/payment/create', {
  447. planId: planId.value,
  448. paymentMethod: paymentMethod.value,
  449. returnUrl
  450. });
  451. }
  452. uni.hideLoading();
  453. orderInfo.value.orderNo = orderResult.orderNo;
  454. // 微信支付 - 显示二维码
  455. if (paymentMethod.value === 'wechat' && orderResult.qrcode) {
  456. uni.navigateTo({
  457. url: `/pages/alipay-qrcode/index?orderNo=${orderResult.orderNo}&amount=${orderResult.amount}&planName=${encodeURIComponent(orderResult.planName || orderInfo.value.planName)}&qrcode=${encodeURIComponent(orderResult.qrcode)}&method=wechat`
  458. });
  459. return;
  460. }
  461. // 支付宝 H5 跳转
  462. // #ifdef H5
  463. if (orderResult.paymentUrl) {
  464. uni.showToast({ title: '即将跳转支付宝...', icon: 'none' });
  465. setTimeout(() => {
  466. window.location.href = orderResult.paymentUrl!;
  467. }, 1000);
  468. }
  469. // #endif
  470. // 开发环境模拟
  471. // #ifndef H5
  472. uni.showModal({
  473. title: '模拟支付',
  474. content: `确认支付 ¥${orderResult.amount}?\n(开发环境模拟)`,
  475. success: async (res) => {
  476. if (res.confirm) {
  477. await mockPay(orderResult.orderNo);
  478. }
  479. },
  480. });
  481. // #endif
  482. } catch (error: any) {
  483. uni.hideLoading();
  484. console.error('创建订单失败:', error);
  485. uni.showToast({ title: error.message || '创建订单失败', icon: 'none' });
  486. }
  487. }
  488. async function mockPay(orderNo: string) {
  489. try {
  490. uni.showLoading({ title: '支付中...' });
  491. await post('/payment/mock', { orderNo });
  492. uni.hideLoading();
  493. uni.showModal({
  494. title: '支付成功',
  495. content: productType.value === 'token-pack' ? '积分包购买成功!' : '恭喜您订阅成功!',
  496. showCancel: false,
  497. success: () => {
  498. uni.switchTab({ url: '/pages/mine/index' });
  499. }
  500. });
  501. } catch (error) {
  502. uni.hideLoading();
  503. uni.showToast({ title: '支付失败', icon: 'none' });
  504. }
  505. }
  506. function goBack() {
  507. const pages = getCurrentPages();
  508. if (pages.length > 1) {
  509. uni.navigateBack();
  510. } else {
  511. uni.switchTab({ url: '/pages/member/index' });
  512. }
  513. }
  514. </script>
  515. <style scoped>
  516. .page {
  517. min-height: 100vh;
  518. background: #f5f5f5;
  519. }
  520. .nav-bar {
  521. position: fixed;
  522. top: 0;
  523. left: 0;
  524. right: 0;
  525. height: 88rpx;
  526. padding-top: env(safe-area-inset-top);
  527. background: #ffffff;
  528. border-bottom: 1px solid #e5e7eb;
  529. z-index: 100;
  530. }
  531. .nav-content {
  532. height: 100%;
  533. display: flex;
  534. align-items: center;
  535. padding: 0 24rpx;
  536. }
  537. .nav-left {
  538. width: 80rpx;
  539. display: flex;
  540. align-items: center;
  541. }
  542. .back-icon {
  543. font-size: 36rpx;
  544. color: #333;
  545. }
  546. .page-title {
  547. flex: 1;
  548. text-align: center;
  549. font-size: 32rpx;
  550. font-weight: 500;
  551. color: #333;
  552. margin-right: 80rpx;
  553. }
  554. .content {
  555. padding-top: calc(120rpx + env(safe-area-inset-top));
  556. padding-left: 32rpx;
  557. padding-right: 32rpx;
  558. }
  559. .order-card {
  560. background: #ffffff;
  561. border-radius: 16rpx;
  562. padding: 32rpx;
  563. margin-bottom: 32rpx;
  564. }
  565. .order-title {
  566. font-size: 28rpx;
  567. font-weight: 600;
  568. color: #1f2937;
  569. margin-bottom: 24rpx;
  570. display: block;
  571. }
  572. .order-info {
  573. display: flex;
  574. justify-content: space-between;
  575. align-items: center;
  576. padding: 16rpx 0;
  577. border-bottom: 1px solid #f3f4f6;
  578. }
  579. .order-info:last-child {
  580. border-bottom: none;
  581. }
  582. .order-info.highlight {
  583. margin-top: 16rpx;
  584. padding-top: 24rpx;
  585. border-top: 2rpx dashed #e5e7eb;
  586. border-bottom: none;
  587. }
  588. .info-label {
  589. font-size: 26rpx;
  590. color: #9ca3af;
  591. }
  592. .info-value {
  593. font-size: 26rpx;
  594. color: #1f2937;
  595. }
  596. .info-value.price {
  597. font-size: 36rpx;
  598. font-weight: 700;
  599. color: #ef4444;
  600. }
  601. .payment-section {
  602. background: #ffffff;
  603. border-radius: 16rpx;
  604. padding: 32rpx;
  605. margin-bottom: 32rpx;
  606. }
  607. .section-title {
  608. font-size: 28rpx;
  609. font-weight: 600;
  610. color: #1f2937;
  611. margin-bottom: 24rpx;
  612. display: block;
  613. }
  614. .payment-methods {
  615. display: flex;
  616. flex-direction: column;
  617. gap: 16rpx;
  618. }
  619. .payment-method {
  620. display: flex;
  621. align-items: center;
  622. padding: 24rpx;
  623. background: #f9fafb;
  624. border: 3rpx solid #e5e7eb;
  625. border-radius: 12rpx;
  626. transition: all 0.3s;
  627. }
  628. .payment-method.active {
  629. border-color: #667eea;
  630. background: #f0f2ff;
  631. }
  632. .method-radio {
  633. width: 40rpx;
  634. height: 40rpx;
  635. border: 3rpx solid #d1d5db;
  636. border-radius: 50%;
  637. margin-right: 16rpx;
  638. display: flex;
  639. align-items: center;
  640. justify-content: center;
  641. }
  642. .payment-method.active .method-radio {
  643. border-color: #667eea;
  644. }
  645. .radio-inner {
  646. width: 24rpx;
  647. height: 24rpx;
  648. background: #667eea;
  649. border-radius: 50%;
  650. }
  651. .method-icon {
  652. font-size: 36rpx;
  653. margin-right: 12rpx;
  654. }
  655. .method-name {
  656. font-size: 28rpx;
  657. color: #1f2937;
  658. flex: 1;
  659. }
  660. .method-badge {
  661. font-size: 22rpx;
  662. color: #ffffff;
  663. background: #10b981;
  664. padding: 4rpx 12rpx;
  665. border-radius: 8rpx;
  666. margin-left: 8rpx;
  667. }
  668. .wechat-notice {
  669. display: flex;
  670. align-items: center;
  671. padding: 16rpx 20rpx;
  672. background: #ecfdf5;
  673. border: 1rpx solid #a7f3d0;
  674. border-radius: 10rpx;
  675. margin-bottom: 20rpx;
  676. }
  677. .wechat-notice-icon {
  678. font-size: 32rpx;
  679. margin-right: 12rpx;
  680. }
  681. .wechat-notice-text {
  682. font-size: 24rpx;
  683. color: #065f46;
  684. }
  685. .actions {
  686. display: flex;
  687. gap: 24rpx;
  688. margin-bottom: 32rpx;
  689. }
  690. .btn-primary,
  691. .btn-secondary {
  692. flex: 1;
  693. height: 88rpx;
  694. border-radius: 44rpx;
  695. display: flex;
  696. align-items: center;
  697. justify-content: center;
  698. font-size: 30rpx;
  699. border: none;
  700. }
  701. .btn-primary {
  702. background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
  703. color: #ffffff;
  704. }
  705. .btn-secondary {
  706. background: #ffffff;
  707. color: #6b7280;
  708. border: 2rpx solid #e5e7eb;
  709. }
  710. .tips {
  711. background: #fef3c7;
  712. border-radius: 16rpx;
  713. padding: 24rpx;
  714. margin-bottom: 48rpx;
  715. }
  716. .tips-title {
  717. font-size: 24rpx;
  718. font-weight: 600;
  719. color: #92400e;
  720. display: block;
  721. margin-bottom: 12rpx;
  722. }
  723. .tips-text {
  724. font-size: 22rpx;
  725. color: #78350f;
  726. display: block;
  727. margin-bottom: 6rpx;
  728. }
  729. </style>