You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

595 lines
22 KiB

  1. <template>
  2. <div>
  3. <AppHero
  4. kicker="AI ONLINE POINTS"
  5. title="点数充值"
  6. desc="点数长期有效,多端通用。小程序、H5、PC 端共享同一份点数。"
  7. >
  8. <template #aside>
  9. <!-- 余额卡 -->
  10. <div class="w-full rounded-3xl border border-[var(--c-e8eef7)] bg-[var(--c-ffffff)]/80 p-5 shadow-[0_18px_34px_var(--sh-0-92-190-100)] backdrop-blur lg:w-[260px]">
  11. <div class="text-xs font-bold text-[var(--c-8a97a8)]">我的点数</div>
  12. <div class="mt-2 flex items-end gap-1.5">
  13. <span class="text-4xl font-extrabold leading-none text-[var(--c-0b8cff)]">{{ balanceText }}</span>
  14. <span v-if="balanceUnit" class="pb-1 text-sm font-bold text-[var(--c-8a97a8)]">{{ balanceUnit }}</span>
  15. </div>
  16. <NuxtLink to="/bill" class="mt-3 inline-flex items-center gap-1 text-xs font-bold text-[var(--c-0b8cff)] hover:underline">
  17. 查看账单明细
  18. <UIcon name="i-lucide-chevron-right" class="size-3.5" />
  19. </NuxtLink>
  20. </div>
  21. </template>
  22. </AppHero>
  23. <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
  24. <div class="lg:grid lg:grid-cols-12 lg:gap-8">
  25. <!-- ===== 左:套餐选择 ===== -->
  26. <div class="lg:col-span-7">
  27. <div class="rounded-2xl bg-[var(--c-ffffff)] p-5 shadow-[0_8px_28px_var(--sh-34-68-112-50)] lg:p-6">
  28. <div class="flex items-baseline">
  29. <h2 class="text-base font-extrabold text-[var(--c-172033)] lg:text-lg">选择充值套餐</h2>
  30. <span class="ml-auto text-xs text-[var(--c-8a95a6)]">点数长期有效</span>
  31. </div>
  32. <!-- 加载中 -->
  33. <div v-if="loading" class="grid grid-cols-2 gap-3 pt-4 sm:grid-cols-3">
  34. <div v-for="i in 3" :key="i" class="h-[124px] animate-pulse rounded-2xl bg-[var(--c-f1f5fa)]" />
  35. </div>
  36. <!-- 套餐网格 -->
  37. <div v-else-if="packages.length" class="grid grid-cols-2 gap-3 pt-4 sm:grid-cols-3">
  38. <button
  39. v-for="pkg in packages"
  40. :key="pkg.id"
  41. type="button"
  42. class="recharge-package relative flex min-h-[124px] flex-col items-center justify-center gap-2 rounded-2xl border-2 p-4 transition-all"
  43. :class="{ 'is-active': current && current.id === pkg.id, 'is-recommended': isRecommendedPackage(pkg) }"
  44. @click="selectPackage(pkg)"
  45. >
  46. <span
  47. v-if="pkg.badge"
  48. class="recharge-package-badge absolute left-1/2 top-2 -translate-x-1/2 whitespace-nowrap rounded-full bg-[var(--c-0b8cff)]/10 px-2 py-0.5 text-[10px] font-extrabold text-[var(--c-0b8cff)]"
  49. >{{ pkg.badge }}</span>
  50. <span class="recharge-package-points mt-4 text-xl font-extrabold text-[var(--c-172033)]">{{ pkg.points }}点</span>
  51. <span v-if="isRecommendedPackage(pkg)" class="recharge-package-copy">日常创作首选</span>
  52. <span class="recharge-package-price text-sm font-extrabold text-[var(--c-0b8cff)]">¥{{ pkg.price_yuan }}</span>
  53. <span v-if="pkg.old_price_yuan" class="recharge-package-offer flex items-center gap-1.5 text-xs">
  54. <span class="recharge-package-old-price text-[var(--c-a0aabb)] line-through">¥{{ pkg.old_price_yuan }}</span>
  55. <span
  56. v-if="pkg.bonus_percent"
  57. class="recharge-package-bonus rounded-full bg-[var(--c-edfdf4)] px-1.5 py-0.5 font-extrabold text-[var(--c-159a54)]"
  58. >多送{{ pkg.bonus_percent }}%</span>
  59. </span>
  60. </button>
  61. </div>
  62. <!-- 空态 -->
  63. <div v-else class="py-14 text-center">
  64. <UIcon name="i-lucide-package-x" class="mx-auto mb-3 size-10 text-[var(--c-a0aabb)]" />
  65. <div class="text-sm text-[var(--c-8a95a6)]">暂无可用套餐</div>
  66. </div>
  67. <!-- 确认充值 -->
  68. <button
  69. type="button"
  70. class="mt-5 flex h-12 w-full items-center justify-center rounded-full bg-gradient-to-r from-[var(--c-0b8cff)] to-[var(--c-3aa9ff)] text-base font-extrabold text-white shadow-[0_12px_24px_var(--sh-11-140-255-220)] transition-opacity disabled:cursor-not-allowed disabled:opacity-50 lg:h-14"
  71. :disabled="!current || payState === 'loading'"
  72. @click="startPay"
  73. >
  74. {{ buyText }}
  75. </button>
  76. </div>
  77. <!-- 权益 -->
  78. <div class="mt-5 grid grid-cols-1 gap-3 sm:grid-cols-2">
  79. <div class="rounded-2xl bg-[var(--c-0b8cff)]/[0.07] p-4">
  80. <div class="text-sm font-extrabold text-[var(--c-0b8cff)]">点数长期有效</div>
  81. <div class="mt-1.5 text-xs leading-relaxed text-[var(--c-66758a)]">购买后可用于所有 AI 图像工具</div>
  82. </div>
  83. <div class="rounded-2xl bg-[var(--c-0b8cff)]/[0.07] p-4">
  84. <div class="text-sm font-extrabold text-[var(--c-0b8cff)]">多端通用</div>
  85. <div class="mt-1.5 text-xs leading-relaxed text-[var(--c-66758a)]">小程序、H5、PC 端共享点数</div>
  86. </div>
  87. </div>
  88. <!-- 购买须知 -->
  89. <div class="mt-5 rounded-2xl bg-[var(--c-ffffff)] p-5 text-sm leading-relaxed text-[var(--c-8a95a6)] shadow-[0_8px_28px_var(--sh-34-68-112-40)]">
  90. <div class="mb-1 font-bold text-[var(--c-7f8896)]">购买须知:</div>
  91. <div>1、购买后长期有效,不支持退款;</div>
  92. <div>2、同一账号点数可在小程序、H5、PC端使用;</div>
  93. <div>3、点数适用于 AI 图片生成、高清修复、证件照、AI绘画等功能。</div>
  94. </div>
  95. </div>
  96. <!-- ===== 右:扫码支付(吸顶) ===== -->
  97. <div class="mt-5 lg:col-span-5 lg:mt-0">
  98. <div class="space-y-4 lg:sticky lg:top-20">
  99. <div class="rounded-2xl bg-[var(--c-ffffff)] p-6 text-center shadow-[0_8px_28px_var(--sh-34-68-112-50)]">
  100. <!-- 未开始 -->
  101. <template v-if="payState === 'idle'">
  102. <UIcon :name="payIdleIcon" class="mx-auto mb-4 size-12 text-[var(--c-a0aabb)]" />
  103. <div class="mb-1 text-base font-bold text-[var(--c-172033)]">{{ payIdleTitle }}</div>
  104. <div class="text-sm text-[var(--c-a0aabb)]">{{ payIdleDesc1 }}</div>
  105. <div class="text-sm text-[var(--c-a0aabb)]">{{ payIdleDesc2 }}</div>
  106. </template>
  107. <!-- 生成中 / 待支付 / 成功 -->
  108. <template v-else>
  109. <div class="mb-1 text-base font-bold text-[var(--c-172033)]">
  110. {{ payPanelTitle }}
  111. </div>
  112. <div class="mb-4 text-sm text-[var(--c-8a95a6)]">
  113. {{ payStateText }}
  114. </div>
  115. <div class="mx-auto mb-4 flex h-[200px] w-[200px] items-center justify-center rounded-2xl bg-[var(--c-f7f9fd)]">
  116. <div v-if="payState === 'loading'" class="flex flex-col items-center gap-2">
  117. <span class="size-7 animate-spin rounded-full border-2 border-[var(--c-0b8cff)] border-t-transparent" />
  118. <span class="text-xs text-[var(--c-a0aabb)]">生成二维码...</span>
  119. </div>
  120. <div v-else-if="payState === 'success'" class="flex flex-col items-center gap-2">
  121. <UIcon name="i-lucide-circle-check" class="size-14 text-[var(--c-17a65a)]" />
  122. <span class="text-sm font-bold text-[var(--c-17a65a)]">+{{ current?.points || 0 }} 点已到账</span>
  123. </div>
  124. <div v-else-if="isMobileClient && payState === 'pending'" class="flex flex-col items-center gap-2">
  125. <UIcon name="i-lucide-smartphone" class="size-12 text-[var(--c-0b8cff)]" />
  126. <span class="text-xs font-bold text-[var(--c-0b8cff)]">等待支付完成...</span>
  127. </div>
  128. <ClientOnly v-else-if="codeUrl">
  129. <vue-qr :text="codeUrl" :size="180" :margin="8" />
  130. </ClientOnly>
  131. <div v-else class="flex flex-col items-center gap-2">
  132. <UIcon name="i-lucide-triangle-alert" class="size-9 text-[var(--c-f04438)]" />
  133. <span class="text-xs text-[var(--c-f04438)]">生成失败,请重试</span>
  134. </div>
  135. </div>
  136. <div v-if="payState !== 'success'" class="text-xs text-[var(--c-a0aabb)]">
  137. 支付金额:¥{{ current?.price_yuan || '0.00' }}
  138. </div>
  139. <div class="mt-4 flex justify-center gap-3">
  140. <button
  141. v-if="payState !== 'success'"
  142. class="rounded-full px-4 py-2 text-xs text-[var(--c-8a95a6)] transition-colors hover:text-[var(--c-172033)]"
  143. @click="cancelPay"
  144. >
  145. 取消支付
  146. </button>
  147. <button
  148. v-else
  149. class="rounded-full bg-[var(--c-0b8cff)] px-6 py-2 text-xs font-bold text-white transition-colors hover:bg-[var(--c-0a7ce0)]"
  150. @click="finishPay"
  151. >
  152. 完成
  153. </button>
  154. </div>
  155. </template>
  156. </div>
  157. </div>
  158. </div>
  159. </div>
  160. </div>
  161. </div>
  162. </template>
  163. <script setup>
  164. import VueQr from 'vue-qr'
  165. useSeoMeta({
  166. title: '点数充值 - AI在线',
  167. description: '选择套餐充值点数,点数长期有效、多端通用',
  168. })
  169. const { userInfo, isLogin, showLogin, refreshBalance } = useUser()
  170. const { post } = useApi()
  171. const { reportCv } = usePvcv()
  172. const toast = useToast()
  173. const route = useRoute()
  174. const { clientId, isMobileClient, isWechatClient } = useClientDevice()
  175. const wxPayOpenId = useCookie('wechat_pay_open_id', { maxAge: 30 * 24 * 3600, path: '/' })
  176. const WECHAT_MP_APPID = 'wxd2aa05b2249b16ce'
  177. const packages = ref([])
  178. const current = ref(null)
  179. const loading = ref(true)
  180. const balance = reactive({ points: 0 })
  181. // idle | loading | pending | success | error
  182. const payState = ref('idle')
  183. const codeUrl = ref('')
  184. const orderNo = ref('')
  185. let pollingTimer = null
  186. const balanceText = computed(() => (Number(balance.points) < 0 ? '不限' : String(Number(balance.points) || 0)))
  187. const balanceUnit = computed(() => (balanceText.value === '不限' ? '' : '点'))
  188. const buyText = computed(() => {
  189. if (!current.value) return '请选择套餐'
  190. return `确认充值(${current.value.price_yuan}元)`
  191. })
  192. const payIdleIcon = computed(() => (isMobileClient.value ? 'i-lucide-smartphone' : 'i-lucide-qr-code'))
  193. const payIdleTitle = computed(() => {
  194. if (!isMobileClient.value) return '微信扫码支付'
  195. return isWechatClient.value ? '微信内支付' : '手机微信支付'
  196. })
  197. const payIdleDesc1 = computed(() => {
  198. if (!isMobileClient.value) return '选择套餐后点击「确认充值」'
  199. return isWechatClient.value ? '选择套餐后将拉起微信支付' : '选择套餐后将跳转微信支付页'
  200. })
  201. const payIdleDesc2 = computed(() => {
  202. if (!isMobileClient.value) return '这里会生成支付二维码'
  203. return '支付完成后点数会自动到账'
  204. })
  205. const payPanelTitle = computed(() => {
  206. if (payState.value === 'success') return '充值成功'
  207. if (!isMobileClient.value) return '微信扫码支付'
  208. return isWechatClient.value ? '微信内支付' : '手机微信支付'
  209. })
  210. const payStateText = computed(() => {
  211. if (payState.value === 'loading') return isMobileClient.value ? '正在创建订单并准备拉起支付...' : '正在创建订单...'
  212. if (payState.value === 'pending') {
  213. if (!isMobileClient.value) return '请使用微信扫描二维码完成支付'
  214. return isWechatClient.value ? '请在微信支付弹窗中完成支付' : '请在打开的微信支付页面完成支付'
  215. }
  216. if (payState.value === 'success') return '点数已到账,可以开始创作了'
  217. return '订单创建失败'
  218. })
  219. function showMsg(msg, color = 'error') {
  220. toast.add({ title: msg, color })
  221. }
  222. function isRecommendedPackage(pkg = {}) {
  223. return String(pkg.badge || '').includes('推荐')
  224. }
  225. // ==================== 套餐 & 余额 ====================
  226. async function loadPackages() {
  227. loading.value = true
  228. try {
  229. const res = await post('/point/packagelist')
  230. packages.value = res.list || []
  231. current.value = packages.value.find(p => p.badge === '推荐') || packages.value[0] || null
  232. } catch (e) {
  233. packages.value = []
  234. showMsg(e.message || '套餐加载失败')
  235. } finally {
  236. loading.value = false
  237. }
  238. }
  239. async function loadBalance() {
  240. if (!isLogin.value) return
  241. try {
  242. const res = await post('/point/balance')
  243. balance.points = res.data?.points || 0
  244. userInfo.value = { ...userInfo.value, balance: balance.points }
  245. } catch {
  246. // 忽略
  247. }
  248. }
  249. function selectPackage(pkg) {
  250. if (payState.value === 'pending' || payState.value === 'loading') cancelPay()
  251. current.value = pkg
  252. }
  253. // ==================== 支付(PC 扫码 / H5 微信支付) ====================
  254. function isInvalidOpenId(value) {
  255. return !value || value === 'undefined' || value === 'null'
  256. }
  257. function cleanOAuthQuery() {
  258. if (!import.meta.client) return
  259. const url = new URL(window.location.href)
  260. url.searchParams.delete('code')
  261. url.searchParams.delete('state')
  262. window.history.replaceState({}, document.title, url.toString())
  263. }
  264. function getWechatAuthUrl(packageId) {
  265. const url = new URL(window.location.href)
  266. url.searchParams.delete('code')
  267. url.searchParams.delete('state')
  268. const redirectUri = encodeURIComponent(url.toString())
  269. return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${WECHAT_MP_APPID}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=pointbuy_${packageId}#wechat_redirect`
  270. }
  271. function getQueryValue(value) {
  272. return Array.isArray(value) ? value[0] : value
  273. }
  274. function invokeWechatPay(payInfo) {
  275. if (!import.meta.client) return
  276. const payload = {
  277. appId: payInfo.appId || payInfo.appid || WECHAT_MP_APPID,
  278. timeStamp: String(payInfo.timeStamp || payInfo.timestamp || ''),
  279. nonceStr: payInfo.nonceStr || payInfo.noncestr || '',
  280. package: payInfo.package || (payInfo.prepay_id ? `prepay_id=${payInfo.prepay_id}` : ''),
  281. signType: payInfo.signType || 'RSA',
  282. paySign: payInfo.paySign || payInfo.paysign || '',
  283. }
  284. const onReady = () => {
  285. window.WeixinJSBridge.invoke('getBrandWCPayRequest', payload, (res = {}) => {
  286. if (res.err_msg === 'get_brand_wcpay_request:ok') {
  287. pollOnce()
  288. } else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
  289. cancelPay()
  290. showMsg('支付已取消', 'warning')
  291. } else {
  292. payState.value = 'error'
  293. showMsg('支付失败,请重试')
  294. }
  295. })
  296. }
  297. if (typeof window.WeixinJSBridge === 'undefined') {
  298. document.addEventListener('WeixinJSBridgeReady', onReady, false)
  299. } else {
  300. onReady()
  301. }
  302. }
  303. async function handleWechatOAuthReturn() {
  304. if (!import.meta.client || !isMobileClient.value || !isWechatClient.value) return
  305. const code = getQueryValue(route.query.code)
  306. const state = String(getQueryValue(route.query.state) || '')
  307. if (!code || !state.startsWith('pointbuy_')) return
  308. try {
  309. const res = await post('/common/getopenid', { code, client: 2 })
  310. const openId = res.data?.wx_open_id || ''
  311. if (!openId) throw new Error('获取 openid 失败')
  312. wxPayOpenId.value = openId
  313. cleanOAuthQuery()
  314. const packageId = Number(state.replace('pointbuy_', ''))
  315. const target = packages.value.find(item => Number(item.id) === packageId)
  316. if (target) current.value = target
  317. if (current.value) await startPay({ skipOauth: true })
  318. } catch (e) {
  319. cleanOAuthQuery()
  320. showMsg(e.message || '微信支付身份获取失败,请重新点击充值')
  321. }
  322. }
  323. async function startPay(options = {}) {
  324. if (!current.value) return
  325. reportCv('point_recharge_click', { page_key: 'recharge' })
  326. if (!isLogin.value) {
  327. showLogin.value = true
  328. return
  329. }
  330. if (isMobileClient.value && isWechatClient.value && isInvalidOpenId(wxPayOpenId.value) && !options.skipOauth) {
  331. window.location.href = getWechatAuthUrl(current.value.id)
  332. return
  333. }
  334. payState.value = 'loading'
  335. codeUrl.value = ''
  336. try {
  337. const payload = {
  338. package_id: current.value.id,
  339. client: clientId.value,
  340. base_in_wechat: isWechatClient.value ? 1 : 0,
  341. }
  342. if (isMobileClient.value && isWechatClient.value) {
  343. payload.open_id = wxPayOpenId.value
  344. }
  345. const res = await post('/point/createorder', payload)
  346. const data = res.data || {}
  347. orderNo.value = data.order_no || ''
  348. reportCv('point_recharge_order', { page_key: 'recharge' })
  349. if (data.mode === 'wxpay_jsapi' || data.package || data.paySign) {
  350. payState.value = 'pending'
  351. startPolling()
  352. invokeWechatPay(data)
  353. return
  354. }
  355. const h5Url = data.h5_url || data.h5Url || ''
  356. if (data.mode === 'wxpay_h5' && h5Url) {
  357. payState.value = 'pending'
  358. startPolling()
  359. window.location.href = h5Url
  360. return
  361. }
  362. const url = data.code_url || data.codeUrl || data.qr_code || ''
  363. if (url) {
  364. codeUrl.value = url
  365. payState.value = 'pending'
  366. startPolling()
  367. } else {
  368. payState.value = 'error'
  369. showMsg(res.msg || '支付暂不可用')
  370. }
  371. } catch (e) {
  372. payState.value = 'error'
  373. showMsg(e.message || '创建订单失败')
  374. }
  375. }
  376. // 轮询订单状态:服务端会主动查微信并流转订单 + 幂等入账
  377. async function pollOnce() {
  378. if (!orderNo.value) return
  379. try {
  380. const res = await post('/point/orderstatus', { order_no: orderNo.value })
  381. const d = res.data || {}
  382. if (Number(d.status) === 20) {
  383. clearInterval(pollingTimer)
  384. payState.value = 'success'
  385. balance.points = d.balance ?? balance.points
  386. refreshBalance()
  387. reportCv('point_recharge_paid', { page_key: 'recharge' })
  388. }
  389. } catch {
  390. // 单次失败不中断轮询
  391. }
  392. }
  393. function startPolling() {
  394. clearInterval(pollingTimer)
  395. pollingTimer = setInterval(async () => {
  396. await pollOnce()
  397. }, 2000)
  398. }
  399. function cancelPay() {
  400. clearInterval(pollingTimer)
  401. payState.value = 'idle'
  402. codeUrl.value = ''
  403. orderNo.value = ''
  404. }
  405. function finishPay() {
  406. cancelPay()
  407. loadBalance()
  408. }
  409. onMounted(async () => {
  410. await loadPackages()
  411. await loadBalance()
  412. await handleWechatOAuthReturn()
  413. })
  414. watch(isLogin, async (loggedIn) => {
  415. if (!loggedIn) {
  416. cancelPay()
  417. balance.points = 0
  418. }
  419. await loadPackages()
  420. if (loggedIn) await loadBalance()
  421. })
  422. onUnmounted(() => clearInterval(pollingTimer))
  423. </script>
  424. <style scoped>
  425. .recharge-package {
  426. overflow: hidden;
  427. border-color: transparent;
  428. background: var(--c-f7f9fd);
  429. box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04);
  430. transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
  431. }
  432. .recharge-package:hover {
  433. border-color: var(--c-e5edf7);
  434. }
  435. .recharge-package::before {
  436. content: '';
  437. position: absolute;
  438. top: -34px;
  439. right: -34px;
  440. width: 92px;
  441. height: 92px;
  442. border-radius: 999px;
  443. background: transparent;
  444. }
  445. .recharge-package::after {
  446. content: '';
  447. position: absolute;
  448. right: 14px;
  449. bottom: 10px;
  450. left: 14px;
  451. height: 2px;
  452. border-radius: 999px;
  453. background: transparent;
  454. }
  455. .recharge-package > * {
  456. position: relative;
  457. z-index: 1;
  458. }
  459. .recharge-package.is-recommended {
  460. border-color: rgba(246, 196, 83, 0.82);
  461. color: #fff;
  462. background:
  463. radial-gradient(circle at 82% 8%, rgba(255, 231, 163, 0.28), transparent 32%),
  464. radial-gradient(circle at 8% 92%, rgba(202, 138, 4, 0.18), transparent 36%),
  465. linear-gradient(160deg, #1c1917 0%, #111827 52%, #020617 100%);
  466. box-shadow:
  467. 0 18px 42px rgba(15, 23, 42, 0.26),
  468. 0 0 0 4px rgba(246, 196, 83, 0.12);
  469. }
  470. .recharge-package.is-recommended::before {
  471. background: radial-gradient(circle, rgba(255, 216, 106, 0.32), transparent 68%);
  472. }
  473. .recharge-package.is-active {
  474. border-color: var(--c-0b8cff);
  475. background: linear-gradient(180deg, #f4faff 0%, #ffffff 68%);
  476. box-shadow:
  477. 0 16px 34px rgba(11, 140, 255, 0.18),
  478. 0 0 0 4px var(--sh-11-140-255-100);
  479. }
  480. .recharge-package.is-recommended.is-active {
  481. background:
  482. radial-gradient(circle at 82% 8%, rgba(255, 231, 163, 0.38), transparent 32%),
  483. radial-gradient(circle at 8% 92%, rgba(202, 138, 4, 0.22), transparent 38%),
  484. linear-gradient(160deg, #27201a 0%, #111827 50%, #020617 100%);
  485. border-color: #f6c453;
  486. box-shadow:
  487. 0 20px 48px rgba(15, 23, 42, 0.32),
  488. 0 0 0 5px rgba(246, 196, 83, 0.16),
  489. inset 0 1px 0 rgba(255, 255, 255, 0.86);
  490. }
  491. .recharge-package.is-recommended.is-active::after {
  492. background: linear-gradient(90deg, transparent, rgba(255, 216, 106, 0.72), transparent);
  493. box-shadow: 0 0 10px rgba(255, 216, 106, 0.3);
  494. }
  495. .recharge-package.is-recommended .recharge-package-badge {
  496. color: #2a1803;
  497. background: linear-gradient(135deg, #fff1b8 0%, #f6c453 50%, #d89a22 100%);
  498. box-shadow: 0 6px 16px rgba(216, 154, 34, 0.32);
  499. }
  500. .recharge-package-copy {
  501. margin-top: -0.25rem;
  502. font-size: 11px;
  503. font-weight: 800;
  504. line-height: 1;
  505. color: #3b8bd8;
  506. }
  507. .recharge-package.is-recommended .recharge-package-points {
  508. color: #fff;
  509. text-shadow: 0 1px 5px rgba(0, 0, 0, 0.32);
  510. }
  511. .recharge-package.is-recommended .recharge-package-copy {
  512. color: rgba(255, 220, 138, 0.92);
  513. }
  514. .recharge-package.is-recommended .recharge-package-price {
  515. color: #ffd76a;
  516. text-shadow: 0 1px 6px rgba(216, 154, 34, 0.22);
  517. }
  518. .recharge-package.is-recommended .recharge-package-old-price {
  519. color: rgba(255, 255, 255, 0.45);
  520. }
  521. .recharge-package.is-recommended .recharge-package-bonus {
  522. color: #7a4a00;
  523. background: linear-gradient(135deg, rgba(255, 241, 184, 0.96), rgba(255, 215, 106, 0.96));
  524. }
  525. </style>