index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  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. orderInfo.value.orderNo = result.orderNo;
  297. uni.hideLoading();
  298. // 使用 WeixinJSBridge.invoke 调起支付
  299. const currentResult = result; // 保存引用,避免闭包问题
  300. const onBridgeReady = () => {
  301. if (!currentResult || !currentResult.payParams) {
  302. console.error('[WeChat Pay] payParams 不存在:', currentResult);
  303. uni.showToast({ title: '支付参数异常,请重试', icon: 'none' });
  304. paying.value = false;
  305. return;
  306. }
  307. (window as any).WeixinJSBridge.invoke(
  308. 'getBrandWXPayRequest',
  309. {
  310. "appId": currentResult.payParams.appId,
  311. "timeStamp": currentResult.payParams.timeStamp,
  312. "nonceStr": currentResult.payParams.nonceStr,
  313. "package": currentResult.payParams.package,
  314. "signType": currentResult.payParams.signType || "RSA",
  315. "paySign": currentResult.payParams.paySign,
  316. },
  317. async (res: any) => {
  318. console.log('[WeChat Pay] WeixinJSBridge 回调:', JSON.stringify(res));
  319. // 官方要求:必须调用查询订单API确认最终状态
  320. const orderStatus = await checkWechatOrderStatus(currentResult.orderNo);
  321. paying.value = false;
  322. if (res.err_msg === "get_brand_wcpay_request:ok") {
  323. if (orderStatus === 'SUCCESS') {
  324. uni.showModal({
  325. title: '支付成功',
  326. content: productType.value === 'token-pack' ? '积分包购买成功!' : '恭喜您订阅成功!',
  327. showCancel: false,
  328. success: () => uni.switchTab({ url: '/pages/mine/index' })
  329. });
  330. } else {
  331. uni.showToast({ title: '订单确认中,请稍后查看', icon: 'none' });
  332. }
  333. } else if (res.err_msg === "get_brand_wcpay_request:cancel") {
  334. uni.showToast({ title: '已取消支付', icon: 'none' });
  335. } else if (orderStatus === 'SUCCESS') {
  336. // 桥回调失败但订单已支付(延迟回调)
  337. uni.showModal({
  338. title: '支付成功',
  339. content: productType.value === 'token-pack' ? '积分包购买成功!' : '恭喜您订阅成功!',
  340. showCancel: false,
  341. success: () => uni.switchTab({ url: '/pages/mine/index' })
  342. });
  343. } else {
  344. uni.showModal({
  345. title: '支付失败',
  346. content: '微信支付调起失败,是否使用扫码支付?',
  347. confirmText: '扫码支付',
  348. cancelText: '取消',
  349. success: (modalRes: any) => {
  350. if (modalRes.confirm && currentResult.orderNo) {
  351. inWechat.value = false;
  352. handlePay();
  353. }
  354. }
  355. });
  356. }
  357. }
  358. );
  359. };
  360. // WeixinJSBridge 可能尚未就绪,监听就绪事件
  361. if (typeof (window as any).WeixinJSBridge === 'undefined') {
  362. document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
  363. } else {
  364. onBridgeReady();
  365. }
  366. } catch (error: any) {
  367. paying.value = false;
  368. uni.hideLoading();
  369. console.error('[WeChat Pay] 创建JSAPI支付失败:', error);
  370. if (error.message && error.message.includes('openid')) {
  371. localStorage.removeItem('wx_openid');
  372. openid.value = '';
  373. await redirectToWechatOAuth();
  374. return;
  375. }
  376. uni.showToast({ title: error.message || '支付失败', icon: 'none' });
  377. }
  378. }
  379. async function handlePay() {
  380. // 防重处理:2秒内禁止重复点击(官方要求按钮防抖)
  381. const now = Date.now();
  382. if (now - lastPayClick.value < 2000) {
  383. console.log('[Payment] 防重:忽略重复点击');
  384. return;
  385. }
  386. lastPayClick.value = now;
  387. if (!planId.value && productType.value !== 'token-pack') {
  388. uni.showToast({ title: '参数错误', icon: 'none' });
  389. return;
  390. }
  391. // 公众号内微信支付 → 走 JSAPI
  392. if (inWechat.value && paymentMethod.value === 'wechat') {
  393. await handleWechatJsapiPay();
  394. return;
  395. }
  396. try {
  397. uni.showLoading({ title: '创建订单...' });
  398. // #ifdef H5
  399. const returnUrl = `${window.location.origin}/#/pages/payment-result/index`;
  400. // #endif
  401. // #ifndef H5
  402. // App 端使用相对路径
  403. const returnUrl = `/#/pages/payment-result/index`;
  404. // #endif
  405. let orderResult: any;
  406. if (productType.value === 'token-pack') {
  407. orderResult = await post<any>('/payment/token-packs/create', {
  408. quantity: tokenPackQuantity.value,
  409. paymentMethod: paymentMethod.value,
  410. returnUrl
  411. });
  412. } else {
  413. orderResult = await post<any>('/payment/create', {
  414. planId: planId.value,
  415. paymentMethod: paymentMethod.value,
  416. returnUrl
  417. });
  418. }
  419. uni.hideLoading();
  420. orderInfo.value.orderNo = orderResult.orderNo;
  421. // 微信支付 - 显示二维码
  422. if (paymentMethod.value === 'wechat' && orderResult.qrcode) {
  423. uni.navigateTo({
  424. url: `/pages/alipay-qrcode/index?orderNo=${orderResult.orderNo}&amount=${orderResult.amount}&planName=${encodeURIComponent(orderResult.planName || orderInfo.value.planName)}&qrcode=${encodeURIComponent(orderResult.qrcode)}&method=wechat`
  425. });
  426. return;
  427. }
  428. // 支付宝 H5 跳转
  429. // #ifdef H5
  430. if (orderResult.paymentUrl) {
  431. uni.showToast({ title: '即将跳转支付宝...', icon: 'none' });
  432. setTimeout(() => {
  433. window.location.href = orderResult.paymentUrl!;
  434. }, 1000);
  435. }
  436. // #endif
  437. // 开发环境模拟
  438. // #ifndef H5
  439. uni.showModal({
  440. title: '模拟支付',
  441. content: `确认支付 ¥${orderResult.amount}?\n(开发环境模拟)`,
  442. success: async (res) => {
  443. if (res.confirm) {
  444. await mockPay(orderResult.orderNo);
  445. }
  446. },
  447. });
  448. // #endif
  449. } catch (error: any) {
  450. uni.hideLoading();
  451. console.error('创建订单失败:', error);
  452. uni.showToast({ title: error.message || '创建订单失败', icon: 'none' });
  453. }
  454. }
  455. async function mockPay(orderNo: string) {
  456. try {
  457. uni.showLoading({ title: '支付中...' });
  458. await post('/payment/mock', { orderNo });
  459. uni.hideLoading();
  460. uni.showModal({
  461. title: '支付成功',
  462. content: productType.value === 'token-pack' ? '积分包购买成功!' : '恭喜您订阅成功!',
  463. showCancel: false,
  464. success: () => {
  465. uni.switchTab({ url: '/pages/mine/index' });
  466. }
  467. });
  468. } catch (error) {
  469. uni.hideLoading();
  470. uni.showToast({ title: '支付失败', icon: 'none' });
  471. }
  472. }
  473. function goBack() {
  474. const pages = getCurrentPages();
  475. if (pages.length > 1) {
  476. uni.navigateBack();
  477. } else {
  478. uni.switchTab({ url: '/pages/member/index' });
  479. }
  480. }
  481. </script>
  482. <style scoped>
  483. .page {
  484. min-height: 100vh;
  485. background: #f5f5f5;
  486. }
  487. .nav-bar {
  488. position: fixed;
  489. top: 0;
  490. left: 0;
  491. right: 0;
  492. height: 88rpx;
  493. padding-top: env(safe-area-inset-top);
  494. background: #ffffff;
  495. border-bottom: 1px solid #e5e7eb;
  496. z-index: 100;
  497. }
  498. .nav-content {
  499. height: 100%;
  500. display: flex;
  501. align-items: center;
  502. padding: 0 24rpx;
  503. }
  504. .nav-left {
  505. width: 80rpx;
  506. display: flex;
  507. align-items: center;
  508. }
  509. .back-icon {
  510. font-size: 36rpx;
  511. color: #333;
  512. }
  513. .page-title {
  514. flex: 1;
  515. text-align: center;
  516. font-size: 32rpx;
  517. font-weight: 500;
  518. color: #333;
  519. margin-right: 80rpx;
  520. }
  521. .content {
  522. padding-top: calc(120rpx + env(safe-area-inset-top));
  523. padding-left: 32rpx;
  524. padding-right: 32rpx;
  525. }
  526. .order-card {
  527. background: #ffffff;
  528. border-radius: 16rpx;
  529. padding: 32rpx;
  530. margin-bottom: 32rpx;
  531. }
  532. .order-title {
  533. font-size: 28rpx;
  534. font-weight: 600;
  535. color: #1f2937;
  536. margin-bottom: 24rpx;
  537. display: block;
  538. }
  539. .order-info {
  540. display: flex;
  541. justify-content: space-between;
  542. align-items: center;
  543. padding: 16rpx 0;
  544. border-bottom: 1px solid #f3f4f6;
  545. }
  546. .order-info:last-child {
  547. border-bottom: none;
  548. }
  549. .order-info.highlight {
  550. margin-top: 16rpx;
  551. padding-top: 24rpx;
  552. border-top: 2rpx dashed #e5e7eb;
  553. border-bottom: none;
  554. }
  555. .info-label {
  556. font-size: 26rpx;
  557. color: #9ca3af;
  558. }
  559. .info-value {
  560. font-size: 26rpx;
  561. color: #1f2937;
  562. }
  563. .info-value.price {
  564. font-size: 36rpx;
  565. font-weight: 700;
  566. color: #ef4444;
  567. }
  568. .payment-section {
  569. background: #ffffff;
  570. border-radius: 16rpx;
  571. padding: 32rpx;
  572. margin-bottom: 32rpx;
  573. }
  574. .section-title {
  575. font-size: 28rpx;
  576. font-weight: 600;
  577. color: #1f2937;
  578. margin-bottom: 24rpx;
  579. display: block;
  580. }
  581. .payment-methods {
  582. display: flex;
  583. flex-direction: column;
  584. gap: 16rpx;
  585. }
  586. .payment-method {
  587. display: flex;
  588. align-items: center;
  589. padding: 24rpx;
  590. background: #f9fafb;
  591. border: 3rpx solid #e5e7eb;
  592. border-radius: 12rpx;
  593. transition: all 0.3s;
  594. }
  595. .payment-method.active {
  596. border-color: #667eea;
  597. background: #f0f2ff;
  598. }
  599. .method-radio {
  600. width: 40rpx;
  601. height: 40rpx;
  602. border: 3rpx solid #d1d5db;
  603. border-radius: 50%;
  604. margin-right: 16rpx;
  605. display: flex;
  606. align-items: center;
  607. justify-content: center;
  608. }
  609. .payment-method.active .method-radio {
  610. border-color: #667eea;
  611. }
  612. .radio-inner {
  613. width: 24rpx;
  614. height: 24rpx;
  615. background: #667eea;
  616. border-radius: 50%;
  617. }
  618. .method-icon {
  619. font-size: 36rpx;
  620. margin-right: 12rpx;
  621. }
  622. .method-name {
  623. font-size: 28rpx;
  624. color: #1f2937;
  625. flex: 1;
  626. }
  627. .method-badge {
  628. font-size: 22rpx;
  629. color: #ffffff;
  630. background: #10b981;
  631. padding: 4rpx 12rpx;
  632. border-radius: 8rpx;
  633. margin-left: 8rpx;
  634. }
  635. .wechat-notice {
  636. display: flex;
  637. align-items: center;
  638. padding: 16rpx 20rpx;
  639. background: #ecfdf5;
  640. border: 1rpx solid #a7f3d0;
  641. border-radius: 10rpx;
  642. margin-bottom: 20rpx;
  643. }
  644. .wechat-notice-icon {
  645. font-size: 32rpx;
  646. margin-right: 12rpx;
  647. }
  648. .wechat-notice-text {
  649. font-size: 24rpx;
  650. color: #065f46;
  651. }
  652. .actions {
  653. display: flex;
  654. gap: 24rpx;
  655. margin-bottom: 32rpx;
  656. }
  657. .btn-primary,
  658. .btn-secondary {
  659. flex: 1;
  660. height: 88rpx;
  661. border-radius: 44rpx;
  662. display: flex;
  663. align-items: center;
  664. justify-content: center;
  665. font-size: 30rpx;
  666. border: none;
  667. }
  668. .btn-primary {
  669. background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
  670. color: #ffffff;
  671. }
  672. .btn-secondary {
  673. background: #ffffff;
  674. color: #6b7280;
  675. border: 2rpx solid #e5e7eb;
  676. }
  677. .tips {
  678. background: #fef3c7;
  679. border-radius: 16rpx;
  680. padding: 24rpx;
  681. margin-bottom: 48rpx;
  682. }
  683. .tips-title {
  684. font-size: 24rpx;
  685. font-weight: 600;
  686. color: #92400e;
  687. display: block;
  688. margin-bottom: 12rpx;
  689. }
  690. .tips-text {
  691. font-size: 22rpx;
  692. color: #78350f;
  693. display: block;
  694. margin-bottom: 6rpx;
  695. }
  696. </style>