index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838
  1. <template>
  2. <view class="page-container">
  3. <uv-navbar title="洗车机" bgColor="#C6171E" leftIconColor="#FFFFFF" :titleStyle="{ color: '#FFFFFF' }" :placeholder="true" @leftClick="handleNavigateBack"></uv-navbar>
  4. <scroll-view class="content-scroll" scroll-y="true">
  5. <!-- 设备信息卡片 -->
  6. <view class="device-info-card">
  7. <view class="device-number">
  8. <text class="number-text">洗车机 No.{{ state.device?.shortId }}</text>
  9. <text v-if="state.device?.hasPa" class="pa-badge">PA</text>
  10. </view>
  11. <view class="device-status" :class="'status-' + (state.device?.state || 'idle')">
  12. <view class="status-dot"></view>
  13. <text>{{ fmtDictName('WashDevice.state',state.device?.state) }}</text>
  14. </view>
  15. <view class="device-functions">
  16. <view
  17. class="function-item"
  18. v-for="f in state.device?.functionList"
  19. :key="f"
  20. >
  21. <uv-icon name="checkbox-mark" size="14" color="#C6171E"></uv-icon>
  22. <text>{{ f }}</text>
  23. </view>
  24. </view>
  25. </view>
  26. <!-- 价格公示入口 -->
  27. <view class="price-entry" v-if="state.device?.price" @click="pricePopupRef?.open()">
  28. <view class="price-entry-left">
  29. <text class="price-icon">¥</text>
  30. <text class="price-text">收费标准</text>
  31. </view>
  32. <uv-icon name="arrow-right" size="16" color="#C0C4CC"></uv-icon>
  33. </view>
  34. <!-- 价格公示弹窗 -->
  35. <uv-popup ref="pricePopupRef" mode="center" :round="0" :closeable="false" customStyle="background:transparent;">
  36. <PriceTable :price="state.device?.price" @close="pricePopupRef?.close()" />
  37. </uv-popup>
  38. <!-- 停车费提示 -->
  39. <view class="parking-notice" v-if="state.parkingFee">
  40. <view class="parking-notice-left">
  41. <text class="parking-icon">P</text>
  42. <text class="parking-text">{{ state.parkingFee }}</text>
  43. </view>
  44. </view>
  45. <!-- 操作按钮卡片 -->
  46. <view class="control-card">
  47. <view class="control-wrapper">
  48. <view
  49. class="control-button"
  50. :class="{
  51. 'control-button--active': state.device?.state === 'busy',
  52. 'control-button--disabled': !isDeviceOperable
  53. }"
  54. @click="isDeviceOperable && debounceStartStopDevice()"
  55. >
  56. <view class="button-icon">
  57. <view v-if="state.device?.state==='idle'" class="icon-play">
  58. <uv-icon name="play-circle-fill" size="56" color="#FFFFFF"></uv-icon>
  59. </view>
  60. <view v-else-if="state.device?.state==='busy'" class="icon-stop">
  61. <uv-icon name="pause-circle-fill" size="56" color="#FFFFFF"></uv-icon>
  62. </view>
  63. <view v-else class="icon-disabled">
  64. <uv-icon name="error-circle-fill" size="56" color="#FFFFFF"></uv-icon>
  65. </view>
  66. </view>
  67. <text class="button-text" v-if="state.device?.state==='idle'">启动设备</text>
  68. <text class="button-text" v-else-if="state.device?.state==='busy'">停止设备</text>
  69. <text class="button-text" v-else>设备{{ fmtDictName('WashDevice.state', state.device?.state) }}</text>
  70. <text class="button-tip" v-if="state.device?.state==='idle'">点击开始洗车</text>
  71. <text class="button-tip" v-else-if="state.device?.state==='busy'">点击结束洗车</text>
  72. <text class="button-tip" v-else>当前无法操作</text>
  73. </view>
  74. </view>
  75. </view>
  76. <!-- 操作指南卡片 -->
  77. <view class="guide-card">
  78. <view class="card-header">
  79. <view class="header-dot"></view>
  80. <text class="header-title">操作指南</text>
  81. <uv-icon name="question-circle" size="18" color="#909399"></uv-icon>
  82. </view>
  83. <view class="guide-list">
  84. <view class="guide-step" v-for="(step, index) in guideSteps" :key="index">
  85. <view class="step-number">{{ index + 1 }}</view>
  86. <text class="step-text">{{ step }}</text>
  87. </view>
  88. </view>
  89. </view>
  90. <!-- 余额充值卡片 -->
  91. <view class="balance-card" v-if="state.isLogin" @click="handleGotoRechage">
  92. <view class="balance-left">
  93. <view class="balance-icon">
  94. <uv-icon name="bag" size="22" color="#C6171E"></uv-icon>
  95. </view>
  96. <view class="balance-info">
  97. <text class="balance-label">账户余额</text>
  98. <view class="balance-amount">
  99. <text class="amount-symbol">¥</text>
  100. <text class="amount-number">{{ fmtMoney(state.balance) }}</text>
  101. </view>
  102. </view>
  103. </view>
  104. <view class="balance-right">
  105. <text class="recharge-text">去充值</text>
  106. <uv-icon name="arrow-right" size="16" color="#C6171E"></uv-icon>
  107. </view>
  108. </view>
  109. <view style="height: 40rpx;"></view>
  110. </scroll-view>
  111. <login-bar class="w100 text-center"></login-bar>
  112. </view>
  113. </template>
  114. <script setup lang="ts">
  115. import {onHide, onLoad, onShow} from "@dcloudio/uni-app";
  116. import {computed, reactive, ref} from "vue";
  117. import {debounce, fmtDictName} from "@/utils/common";
  118. import {get, post} from "@/utils/https";
  119. import {checkLogin, fetchToken, tryLogin} from "@/utils/auth";
  120. import {fmtMoney} from "@/utils/common";
  121. import LoginBar from "@/components/login-bar/index.vue";
  122. import PriceTable from "@/components/price-table/index.vue";
  123. const guideSteps = [
  124. '点击上方【启动设备】按钮启动设备',
  125. '设备启动后,在设备功能面板按下功能按键选择服务项目',
  126. '洗车过程中可按功能按键暂停,暂停期间停止计费',
  127. '洗车结束后,按结算按键或【停止设备】按钮结束计费',
  128. '请及时将车辆驶离工位,方便后续用户使用'
  129. ]
  130. const initState = () => ({
  131. isLogin:false,
  132. balance:0,
  133. device: {
  134. functions: [],
  135. deviceName: '',
  136. state: '',
  137. stationId: ''
  138. },
  139. parkingFee: '',
  140. time: "00:00:00",
  141. start: new Date(),
  142. deviceId: null,
  143. stationId: null
  144. })
  145. const state = reactive(initState())
  146. const timerId = ref<ReturnType<typeof setInterval> | null>(null)
  147. const pricePopupRef = ref()
  148. const isDeviceOperable = computed(() => {
  149. const s = state.device?.state
  150. return s === 'idle' || s === 'busy'
  151. })
  152. const loadBalance = () => {
  153. get('/account/balance').then((res: any) => {
  154. state.balance = res.balance
  155. const gd = getApp<any>().globalData
  156. gd.user = Object.assign({}, gd.user, { balance: res.balance })
  157. })
  158. }
  159. onHide(() => {
  160. if (timerId.value) {
  161. clearInterval(timerId.value);
  162. timerId.value = null;
  163. }
  164. Object.assign(state, initState());
  165. })
  166. onLoad((options: any) => {
  167. let id = options?.shortId;
  168. state.stationId = options?.stationId;
  169. if (!id) {
  170. let query = decodeURIComponent(options.q);
  171. let scanTime = options.scancode_time;
  172. if (query) {
  173. id = query.split("#")[1].split("?")[0]
  174. state.deviceId = id;
  175. } else {
  176. return;
  177. }
  178. }
  179. getApp<any>().globalData.deviceId = id;
  180. state.deviceId = id;
  181. state.device = getApp<any>().globalData.last.device;
  182. checkLogin().then((token) => {
  183. state.isLogin =true;
  184. setTimeout(() => {
  185. loadDeviceDetail(id);
  186. loadBalance()
  187. }, 200)
  188. }).catch(e => {
  189. console.error("onLoad 校验登录失败,自动跳转登录页")
  190. })
  191. });
  192. onShow((options:any) => {
  193. addListener();
  194. checkLogin().then(() => {
  195. if (!state.deviceId) {
  196. let deviceId = getApp<any>().globalData.deviceId;
  197. if (deviceId) {
  198. loadDeviceDetail(deviceId);
  199. }
  200. } else {
  201. loadDeviceDetail(state.deviceId);
  202. }
  203. state.isLogin =true;
  204. loadBalance()
  205. }).catch(e => {
  206. console.error("校验登录失败,自动跳转登录页")
  207. setTimeout(()=>{
  208. uni.navigateTo({
  209. url: `/pages-user/login/index?shortId=${!!state.deviceId?state.deviceId:''}&stationId=${!!state.stationId?state.stationId:''}&redirectUrl=/pages-wash/device/index`
  210. })
  211. },1000)
  212. })
  213. })
  214. const addListener = () => {
  215. uni.$on('login', function (data) {
  216. state.isLogin =true;
  217. if (state.deviceId && data.isLogin) {
  218. loadDeviceDetail(state.deviceId);
  219. loadBalance()
  220. }
  221. })
  222. uni.$on('logout', function (data) {
  223. })
  224. }
  225. const removeListener = () => {
  226. uni.$off('logout');
  227. uni.$off('login');
  228. }
  229. const loadDeviceDetail = (id: any) => {
  230. state.deviceId = id;
  231. let ft = fetchToken();
  232. get(`/wash-device/queryDevice/${id}`).then((res: any) => {
  233. if (res.currentUserId && res.currentUserId != getApp<any>().globalData.user.id) {
  234. uni.showToast({
  235. title: '设备已被占用',
  236. icon: 'error'
  237. })
  238. setTimeout(() => {
  239. uni.switchTab({
  240. url: '/pages/index/index'
  241. })
  242. }, 2000)
  243. }
  244. res.functionList = res.functions?.split("|") || []
  245. state.device = res;
  246. loadParkingFee(res.stationId);
  247. getApp<any>().globalData.deviceId = id;
  248. }).catch(e => {
  249. console.error(e)
  250. })
  251. }
  252. const handleNavigateBack = () => {
  253. const pages = getCurrentPages()
  254. if (pages.length > 1) {
  255. uni.navigateBack()
  256. } else {
  257. uni.switchTab({ url: '/pages/index/index' })
  258. }
  259. }
  260. const debounceStartStopDevice = debounce(() => {
  261. handleClickDevice();
  262. }, 600)
  263. const handleClickDevice = () => {
  264. if(!state.isLogin){
  265. return;
  266. }
  267. uni.showModal({
  268. title: '提示',
  269. content: state.device?.state === 'idle' ? '确定启动设备开始洗车吗?' : '确定停止设备终止本次服务吗?',
  270. success: (res) => {
  271. if (res.confirm) {
  272. getApp<any>().globalData.refresh =true;
  273. if (state.device?.state === 'idle') {
  274. uni.showLoading({
  275. title: "启动中",
  276. mask: true,
  277. });
  278. post(`/wash-device/startDevice/${state.device?.shortId}`).then((res: any) => {
  279. uni.hideLoading();
  280. uni.showToast({
  281. title: '设备启动成功'
  282. })
  283. state.device.state = 'busy'
  284. })
  285. } else {
  286. uni.showLoading({
  287. title: "停止中",
  288. mask: true,
  289. });
  290. post(`/wash-device/stopDevice/${state.device?.shortId}`).then((res: any) => {
  291. uni.hideLoading();
  292. uni.showToast({
  293. title: '设备停机成功'
  294. })
  295. state.device.state = 'idle'
  296. loadBalance()
  297. })
  298. }
  299. }
  300. }
  301. });
  302. }
  303. const countTime = () => {
  304. if (timerId.value) {
  305. clearInterval(timerId.value);
  306. }
  307. timerId.value = setInterval(() => {
  308. let delta = new Date().getTime() - state.start.getTime();
  309. delta = delta / 1000;
  310. let hour = (delta / 3600).toFixed(0);
  311. let min = ((delta % 3600) / 60).toFixed(0);
  312. let second = ((delta % 3600) % 60).toFixed(0);
  313. state.time = [
  314. Number(hour) > 9 ? hour : `0${hour}`,
  315. Number(min) > 9 ? min : `0${min}`,
  316. Number(second) > 9 ? second : `0${second}`
  317. ].join(":")
  318. }, 1000)
  319. }
  320. const loadParkingFee = (stationId: string) => {
  321. if (!stationId) return
  322. post('/wash-station/listStation', { stationId, pageSize: 1 }).then((res: any) => {
  323. const list = res?.list || res?.records || []
  324. if (list.length > 0) {
  325. state.parkingFee = list[0].parkingFee || ''
  326. }
  327. })
  328. }
  329. const handleGotoRechage = () => {
  330. uni.navigateTo({
  331. url: `/pages-user/wallet/recharge?stationId=${state.device.stationId}`
  332. })
  333. }
  334. </script>
  335. <style lang="scss" scoped>
  336. .page-container {
  337. width: 100vw;
  338. height: 100vh;
  339. background: $uni-bg-color-page;
  340. display: flex;
  341. flex-direction: column;
  342. }
  343. .content-scroll {
  344. flex: 1;
  345. height: 100%;
  346. width: 100%;
  347. }
  348. // 设备信息卡片
  349. .device-info-card {
  350. margin: 24rpx 30rpx;
  351. @include card-interactive(24rpx);
  352. padding: 32rpx;
  353. text-align: center;
  354. .device-number {
  355. display: flex;
  356. align-items: center;
  357. justify-content: center;
  358. gap: 8rpx;
  359. margin-bottom: 20rpx;
  360. .number-text {
  361. font-size: 32rpx;
  362. font-weight: $uni-font-weight-semibold;
  363. color: $uni-text-color;
  364. }
  365. .pa-badge {
  366. display: inline-block;
  367. margin-left: 12rpx;
  368. padding: 4rpx 12rpx;
  369. background: #C6171E;
  370. color: #FFFFFF;
  371. border-radius: 6rpx;
  372. font-size: 22rpx;
  373. font-weight: 700;
  374. line-height: 1;
  375. vertical-align: middle;
  376. }
  377. }
  378. .device-status {
  379. display: inline-flex;
  380. align-items: center;
  381. gap: 8rpx;
  382. padding: 8rpx 20rpx;
  383. border-radius: 20rpx;
  384. font-size: 26rpx;
  385. font-weight: $uni-font-weight-medium;
  386. margin-bottom: 24rpx;
  387. .status-dot {
  388. width: 12rpx;
  389. height: 12rpx;
  390. border-radius: 50%;
  391. animation: pulse 2s infinite;
  392. }
  393. &.status-idle {
  394. background: rgba($uni-color-success, 0.1);
  395. color: $uni-color-success;
  396. .status-dot {
  397. background: $uni-color-success;
  398. }
  399. }
  400. &.status-busy {
  401. background: rgba($uni-color-warning, 0.1);
  402. color: $uni-color-warning;
  403. .status-dot {
  404. background: $uni-color-warning;
  405. }
  406. }
  407. &.status-fault {
  408. background: rgba($uni-color-error, 0.1);
  409. color: $uni-color-error;
  410. .status-dot {
  411. background: $uni-color-error;
  412. }
  413. }
  414. }
  415. .device-functions {
  416. display: flex;
  417. flex-wrap: wrap;
  418. justify-content: center;
  419. gap: 12rpx;
  420. .function-item {
  421. display: flex;
  422. align-items: center;
  423. gap: 6rpx;
  424. padding: 6rpx 16rpx;
  425. background: rgba($uni-color-primary, 0.08);
  426. color: $uni-color-primary;
  427. border-radius: 16rpx;
  428. font-size: 24rpx;
  429. font-weight: $uni-font-weight-medium;
  430. }
  431. }
  432. }
  433. @keyframes pulse {
  434. 0%, 100% {
  435. opacity: 1;
  436. }
  437. 50% {
  438. opacity: 0.5;
  439. }
  440. }
  441. @keyframes idle-glow {
  442. 0%, 100% {
  443. opacity: 0;
  444. transform: scale(1);
  445. }
  446. 50% {
  447. opacity: 1;
  448. transform: scale(1.04);
  449. }
  450. }
  451. // 价格公示入口
  452. .price-entry {
  453. margin: 0 30rpx 24rpx;
  454. padding: 20rpx 28rpx;
  455. background: rgba($uni-color-primary, 0.05);
  456. border: 1rpx solid rgba($uni-color-primary, 0.12);
  457. border-radius: 16rpx;
  458. display: flex;
  459. align-items: center;
  460. justify-content: space-between;
  461. &-left {
  462. display: flex;
  463. align-items: center;
  464. gap: 12rpx;
  465. flex: 1;
  466. }
  467. .price-icon {
  468. width: 36rpx;
  469. height: 36rpx;
  470. line-height: 36rpx;
  471. text-align: center;
  472. font-size: 20rpx;
  473. font-weight: $uni-font-weight-bold;
  474. color: #fff;
  475. background: $uni-color-primary;
  476. border-radius: 6rpx;
  477. flex-shrink: 0;
  478. }
  479. .price-text {
  480. font-size: 26rpx;
  481. color: $uni-text-color-secondary;
  482. }
  483. }
  484. // 停车费提示
  485. .parking-notice {
  486. margin: 0 30rpx 24rpx;
  487. padding: 20rpx 28rpx;
  488. background: rgba($uni-color-primary, 0.05);
  489. border: 1rpx solid rgba($uni-color-primary, 0.12);
  490. border-radius: 16rpx;
  491. display: flex;
  492. align-items: center;
  493. &-left {
  494. display: flex;
  495. align-items: center;
  496. gap: 12rpx;
  497. flex: 1;
  498. }
  499. .parking-icon {
  500. width: 36rpx;
  501. height: 36rpx;
  502. line-height: 36rpx;
  503. text-align: center;
  504. font-size: 22rpx;
  505. font-weight: $uni-font-weight-bold;
  506. color: #fff;
  507. background: $uni-color-primary;
  508. border-radius: 6rpx;
  509. flex-shrink: 0;
  510. }
  511. .parking-text {
  512. font-size: 26rpx;
  513. color: $uni-text-color-secondary;
  514. line-height: 1.5;
  515. flex: 1;
  516. }
  517. }
  518. // 控制按钮卡片
  519. .control-card {
  520. margin: 0 30rpx 24rpx;
  521. @include card-interactive(24rpx);
  522. padding: 48rpx 32rpx;
  523. .control-wrapper {
  524. display: flex;
  525. justify-content: center;
  526. align-items: center;
  527. .control-button {
  528. width: 300rpx;
  529. height: 300rpx;
  530. border-radius: 50%;
  531. background: linear-gradient(135deg, $uni-color-primary 0%, $uni-color-primary-light 100%);
  532. display: flex;
  533. flex-direction: column;
  534. justify-content: center;
  535. align-items: center;
  536. gap: 6rpx;
  537. box-shadow: 0 16rpx 40rpx rgba(198, 23, 30, 0.35);
  538. transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  539. position: relative;
  540. overflow: hidden;
  541. &::before {
  542. content: '';
  543. position: absolute;
  544. top: 0;
  545. left: 0;
  546. right: 0;
  547. bottom: 0;
  548. background: rgba(255, 255, 255, 0.1);
  549. border-radius: 50%;
  550. transform: scale(0);
  551. transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
  552. }
  553. &::after {
  554. content: '';
  555. position: absolute;
  556. top: -8rpx;
  557. left: -8rpx;
  558. right: -8rpx;
  559. bottom: -8rpx;
  560. border-radius: 50%;
  561. border: 2rpx solid rgba(198, 23, 30, 0.15);
  562. opacity: 0;
  563. transition: opacity 2s ease;
  564. }
  565. &:not(.control-button--active)::after {
  566. animation: idle-glow 2.5s ease-in-out infinite;
  567. }
  568. &:active {
  569. transform: scale(0.95);
  570. box-shadow: 0 8rpx 24rpx rgba(198, 23, 30, 0.35);
  571. &::before {
  572. transform: scale(1);
  573. }
  574. }
  575. .button-icon {
  576. margin-bottom: 2rpx;
  577. display: flex;
  578. align-items: center;
  579. justify-content: center;
  580. .icon-play,
  581. .icon-stop {
  582. display: flex;
  583. align-items: center;
  584. justify-content: center;
  585. line-height: 1;
  586. }
  587. }
  588. .button-text {
  589. font-size: 34rpx;
  590. font-weight: $uni-font-weight-semibold;
  591. color: $uni-text-color-inverse;
  592. letter-spacing: 0.5rpx;
  593. }
  594. .button-tip {
  595. font-size: 24rpx;
  596. color: rgba(255, 255, 255, 0.75);
  597. }
  598. &.control-button--active {
  599. background: linear-gradient(135deg, $uni-color-primary-light 0%, #FF6B6B 100%);
  600. box-shadow: 0 16rpx 40rpx rgba(232, 69, 69, 0.35);
  601. &:active {
  602. box-shadow: 0 8rpx 24rpx rgba(232, 69, 69, 0.35);
  603. }
  604. }
  605. &.control-button--disabled {
  606. background: linear-gradient(135deg, #b0b0b0 0%, #909399 100%);
  607. box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
  608. cursor: not-allowed;
  609. &:active {
  610. transform: none;
  611. box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
  612. }
  613. &::after {
  614. display: none;
  615. }
  616. }
  617. }
  618. }
  619. }
  620. // 操作指南卡片
  621. .guide-card {
  622. margin: 0 30rpx 24rpx;
  623. @include card-interactive(24rpx);
  624. overflow: hidden;
  625. .card-header {
  626. display: flex;
  627. align-items: center;
  628. gap: 12rpx;
  629. padding: 28rpx;
  630. border-bottom: 1rpx solid $uni-border-color-light;
  631. .header-dot {
  632. width: 10rpx;
  633. height: 10rpx;
  634. background: $uni-color-primary;
  635. border-radius: 50%;
  636. }
  637. .header-title {
  638. font-size: 30rpx;
  639. font-weight: $uni-font-weight-semibold;
  640. color: $uni-text-color;
  641. flex: 1;
  642. }
  643. }
  644. .guide-list {
  645. padding: 24rpx 28rpx;
  646. .guide-step {
  647. display: flex;
  648. align-items: flex-start;
  649. gap: 16rpx;
  650. margin-bottom: 20rpx;
  651. &:last-child {
  652. margin-bottom: 0;
  653. }
  654. .step-number {
  655. width: 40rpx;
  656. height: 40rpx;
  657. border-radius: 50%;
  658. background: $uni-color-primary;
  659. color: $uni-text-color-inverse;
  660. font-size: 24rpx;
  661. font-weight: $uni-font-weight-semibold;
  662. display: flex;
  663. align-items: center;
  664. justify-content: center;
  665. flex-shrink: 0;
  666. margin-top: 4rpx;
  667. }
  668. .step-text {
  669. flex: 1;
  670. font-size: 26rpx;
  671. color: $uni-text-color-secondary;
  672. line-height: 1.6;
  673. padding-top: 8rpx;
  674. }
  675. }
  676. }
  677. }
  678. // 余额充值卡片
  679. .balance-card {
  680. margin: 0 30rpx 24rpx;
  681. @include card-interactive(24rpx);
  682. padding: 28rpx 32rpx;
  683. display: flex;
  684. justify-content: space-between;
  685. align-items: center;
  686. .balance-left {
  687. display: flex;
  688. align-items: center;
  689. gap: 16rpx;
  690. flex: 1;
  691. .balance-icon {
  692. width: 56rpx;
  693. height: 56rpx;
  694. border-radius: 50%;
  695. background: rgba($uni-color-primary, 0.08);
  696. display: flex;
  697. align-items: center;
  698. justify-content: center;
  699. }
  700. .balance-info {
  701. display: flex;
  702. flex-direction: column;
  703. gap: 6rpx;
  704. .balance-label {
  705. font-size: 26rpx;
  706. color: $uni-text-color-hint;
  707. }
  708. .balance-amount {
  709. display: flex;
  710. align-items: baseline;
  711. gap: 4rpx;
  712. .amount-symbol {
  713. font-size: 26rpx;
  714. color: $uni-color-primary;
  715. font-weight: $uni-font-weight-medium;
  716. }
  717. .amount-number {
  718. font-size: 32rpx;
  719. color: $uni-color-primary;
  720. font-weight: $uni-font-weight-semibold;
  721. }
  722. }
  723. }
  724. }
  725. .balance-right {
  726. display: flex;
  727. align-items: center;
  728. gap: 8rpx;
  729. .recharge-text {
  730. font-size: 26rpx;
  731. color: $uni-color-primary;
  732. font-weight: $uni-font-weight-medium;
  733. }
  734. }
  735. }
  736. </style>