index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. <template>
  2. <view class="app-center-page">
  3. <!-- 自定义顶栏:头像 + 应用中心 + 搜索 -->
  4. <view class="custom-header">
  5. <view class="header-left" @click="onAvatarClick">
  6. <UserAvatar
  7. :name="currentUser.name"
  8. :id="currentUser.id"
  9. :src="currentUser.avatar"
  10. :size="80"
  11. unit="rpx"
  12. />
  13. <view class="org-info">
  14. <text class="user-name">应用中心</text>
  15. </view>
  16. </view>
  17. <view class="header-right">
  18. <view class="icon-btn" @click="onSearch">
  19. <image class="icon-img" src="/static/icons/search.svg" mode="aspectFit" />
  20. </view>
  21. </view>
  22. </view>
  23. <scroll-view
  24. class="app-center-scroll"
  25. scroll-y
  26. :refresher-enabled="refresherEnabled"
  27. :refresher-triggered="refresherTriggered"
  28. @refresherrefresh="onRefresh"
  29. >
  30. <!-- 最近使用标题 + 4 列网格(最多展示 8 个) -->
  31. <view class="recent-section">
  32. <text class="section-title">最近使用</text>
  33. <view class="quick-grid">
  34. <view v-for="app in quickApps" :key="app.id" class="app-tile" @click="openApp(app)">
  35. <view class="tile-icon" :style="{ background: app.iconBg || defaultIconBg }">
  36. <image class="tile-icon-img" :src="app.iconPath" mode="aspectFit" />
  37. <view
  38. v-if="app.badge"
  39. class="tile-badge"
  40. :style="{ background: app.badgeBg || '#fbbf24', color: app.badgeColor || '#fff' }"
  41. >
  42. {{ app.badge }}
  43. </view>
  44. </view>
  45. <text class="tile-name tile-name-2">{{ app.name }}</text>
  46. </view>
  47. </view>
  48. </view>
  49. <view class="all-section">
  50. <!-- 水平分类 Tab -->
  51. <view class="category-bar">
  52. <scroll-view
  53. class="category-scroll"
  54. scroll-x
  55. scroll-with-animation
  56. :show-scrollbar="false"
  57. :scroll-into-view="scrollIntoTabId"
  58. >
  59. <view class="category-list">
  60. <view
  61. v-for="(cat, catIdx) in categories"
  62. :key="cat.id"
  63. :id="'cat-tab-' + catIdx"
  64. class="category-item"
  65. :class="{ active: cat.id === activeCategoryId }"
  66. @click="onCategoryTabClick(catIdx)"
  67. >
  68. <text class="category-name">{{ cat.name }}</text>
  69. <view v-if="cat.id === activeCategoryId" class="category-underline" />
  70. </view>
  71. </view>
  72. </scroll-view>
  73. </view>
  74. </view>
  75. <!-- 全部应用 swiper 4 列网格(横向滑动切分类) -->
  76. <swiper
  77. class="apps-swiper"
  78. :current="currentCategoryIndex"
  79. @change="onSwiperChange"
  80. :circular="false"
  81. >
  82. <swiper-item v-for="cat in categories" :key="cat.id">
  83. <view class="apps-grid">
  84. <view
  85. v-for="app in getAppsByCategory(cat.id)"
  86. :key="app.id"
  87. class="app-tile app-tile-4"
  88. @click="openApp(app)"
  89. >
  90. <view class="tile-icon" :style="{ background: app.iconBg || defaultIconBg }">
  91. <image class="tile-icon-img" :src="app.iconPath" mode="aspectFit" />
  92. <view
  93. v-if="app.badge"
  94. class="tile-badge"
  95. :style="{ background: app.badgeBg || '#fbbf24', color: app.badgeColor || '#fff' }"
  96. >
  97. {{ app.badge }}
  98. </view>
  99. </view>
  100. <text class="tile-name tile-name-2">{{ app.name }}</text>
  101. </view>
  102. </view>
  103. </swiper-item>
  104. </swiper>
  105. </scroll-view>
  106. </view>
  107. </template>
  108. <script>
  109. import UserAvatar from '../../components/UserAvatar.vue'
  110. import { getToken, getLaunchpadApps, ssoLogin, getUserIdFromToken } from '../../utils/api'
  111. import { openEmbeddedOrSystemBrowser } from '../../utils/openUrlPreference'
  112. const USER_KEY = 'current_user'
  113. const RECENT_APPS_KEY_PREFIX = 'launchpad_recent_apps_v1_'
  114. const RECENT_APPS_MAX = 8
  115. /** 渐变色库与 `components/SystemAvatar.vue` 保持一致 */
  116. const AVATAR_GRADIENT_PAIRS = [
  117. ['#1e3a8a', '#93c5fd'],
  118. ['#166534', '#86efac'],
  119. ['#c2410c', '#fdba74'],
  120. ['#b91c1c', '#fca5a5'],
  121. ['#5b21b6', '#c4b5fd'],
  122. ['#0f766e', '#5eead4'],
  123. ['#3730a3', '#a5b4fc'],
  124. ['#0d9488', '#2dd4bf'],
  125. ['#b45309', '#fcd34d'],
  126. ['#be123c', '#fda4af'],
  127. ['#0369a1', '#7dd3fc'],
  128. ['#4d7c0f', '#bef264'],
  129. ['#86198f', '#e879f9'],
  130. ['#db2777', '#fbcfe8'],
  131. ['#047857', '#6ee7b7'],
  132. ['#6d28d9', '#ddd6fe'],
  133. ['#1e40af', '#93c5fd'],
  134. ['#ea580c', '#fed7aa'],
  135. ['#0e7490', '#99f6e4'],
  136. ['#881337', '#fbcfe8']
  137. ]
  138. function getGradientIndexByName(name) {
  139. const s = String(name || '').trim() || 'SYSTEM'
  140. let hash = 0
  141. for (let i = 0; i < s.length; i++) {
  142. hash = ((hash << 5) - hash + s.charCodeAt(i)) | 0
  143. }
  144. return Math.abs(hash) % AVATAR_GRADIENT_PAIRS.length
  145. }
  146. function getGradientBgByName(name) {
  147. const idx = getGradientIndexByName(name)
  148. const pair = AVATAR_GRADIENT_PAIRS[idx]
  149. return pair ? `linear-gradient(135deg, ${pair[0]} 0%, ${pair[1]} 100%)` : ''
  150. }
  151. export default {
  152. components: { UserAvatar },
  153. data() {
  154. return {
  155. currentUser: { name: '', id: '', avatar: '' },
  156. defaultIconBg: 'linear-gradient(135deg, #e6f4ea 0%, #d4edda 100%)',
  157. launchpadLoading: false,
  158. // 下拉刷新(避免首屏阶段重复触发)
  159. refresherEnabled: false,
  160. refresherTriggered: false,
  161. // 顶部最近使用(本地按 usedAt 倒序,最多 8 个)
  162. quickApps: [],
  163. categories: [{ id: 'recent', name: '全部应用' }],
  164. activeCategoryId: 'recent',
  165. // 全部应用网格数据:来自接口返回的 items
  166. allApps: [],
  167. /** 横向分类 Tab 与 swiper 同步时滚入视口(与 chat 页 scroll-into-view 用法一致) */
  168. scrollIntoTabId: ''
  169. }
  170. },
  171. async onLoad() {
  172. this.loadCurrentUser()
  173. this.refresherEnabled = false
  174. try {
  175. await this.fetchLaunchpadApps()
  176. } finally {
  177. this.refresherEnabled = true
  178. }
  179. },
  180. async onShow() {
  181. this.loadCurrentUser()
  182. await this.fetchLaunchpadApps()
  183. },
  184. computed: {
  185. currentCategoryIndex() {
  186. const idx = this.categories.findIndex((c) => c.id === this.activeCategoryId)
  187. return idx >= 0 ? idx : 0
  188. },
  189. filteredApps() {
  190. // 根据当前 Tab 过滤;如果没有命中则回退显示全部(避免空白)
  191. const active = String(this.activeCategoryId || '')
  192. // “全部应用”Tab 的 id 约定为 'recent'
  193. if (active === 'recent') return this.allApps
  194. const list = this.allApps.filter((a) => String(a.category || '') === active)
  195. return list.length ? list : this.allApps
  196. }
  197. },
  198. methods: {
  199. async onRefresh() {
  200. this.refresherTriggered = true
  201. try {
  202. await this.fetchLaunchpadApps()
  203. } finally {
  204. this.refresherTriggered = false
  205. }
  206. },
  207. /** 当前登录用户 id,用于分用户存储「最近使用」 */
  208. getRecentAppsStorageKey() {
  209. let uid = ''
  210. try {
  211. const raw = uni.getStorageSync(USER_KEY)
  212. if (raw && typeof raw === 'object') {
  213. const id = raw.id ?? raw.user_id
  214. if (id != null && id !== '') uid = String(id)
  215. }
  216. } catch (e) {}
  217. if (!uid) {
  218. const token = getToken()
  219. if (token) uid = String(getUserIdFromToken(token) || '')
  220. }
  221. return uid ? RECENT_APPS_KEY_PREFIX + uid : ''
  222. },
  223. loadRecentApps() {
  224. const key = this.getRecentAppsStorageKey()
  225. if (!key) return []
  226. try {
  227. const raw = uni.getStorageSync(key)
  228. if (Array.isArray(raw)) return raw
  229. if (raw && typeof raw === 'object' && Array.isArray(raw.items)) return raw.items
  230. } catch (e) {}
  231. return []
  232. },
  233. saveRecentApps(list) {
  234. const key = this.getRecentAppsStorageKey()
  235. if (!key) return
  236. try {
  237. uni.setStorageSync(key, Array.isArray(list) ? list : [])
  238. } catch (e) {}
  239. },
  240. refreshQuickApps() {
  241. // 只用已拉取到的 `allApps` + 本地 recent 存储来刷新顶部“最近使用”
  242. // 避免每次点击应用都发起网络请求
  243. const appList = Array.isArray(this.allApps) ? this.allApps : []
  244. if (!appList.length) return
  245. const recentList = this.loadRecentApps()
  246. .filter((x) => x && x.appId != null)
  247. .map((x) => ({ appId: String(x.appId), usedAt: Number(x.usedAt) || 0 }))
  248. .sort((a, b) => (b.usedAt || 0) - (a.usedAt || 0))
  249. const appById = new Map(appList.map((a) => [String(a.id), a]))
  250. const recentApps = []
  251. const recentSeen = new Set()
  252. for (const r of recentList) {
  253. const id = String(r.appId || '')
  254. if (!id || recentSeen.has(id)) continue
  255. if (!appById.has(id)) continue
  256. recentSeen.add(id)
  257. recentApps.push(appById.get(id))
  258. if (recentApps.length >= RECENT_APPS_MAX) break
  259. }
  260. // 不足 8 个时,用接口里的其他应用补齐(但 recents 仍保持按时间倒序)
  261. if (recentApps.length < RECENT_APPS_MAX) {
  262. const rest = appList.filter((a) => !recentSeen.has(String(a.id)))
  263. recentApps.push(...rest.slice(0, RECENT_APPS_MAX - recentApps.length))
  264. }
  265. this.quickApps = recentApps
  266. },
  267. recordRecentApp(appId) {
  268. const id = String(appId ?? '')
  269. if (!id) return
  270. if (!this.getRecentAppsStorageKey()) return
  271. const now = Date.now()
  272. const list = this.loadRecentApps()
  273. // 去重 + 更新为最新,按 usedAt 倒序保留最多 8 个
  274. const next = list
  275. .filter((x) => x && x.appId != null)
  276. .map((x) => ({ appId: String(x.appId), usedAt: Number(x.usedAt) || 0 }))
  277. .filter((x) => x.appId !== id)
  278. next.unshift({ appId: id, usedAt: now })
  279. next.sort((a, b) => (b.usedAt || 0) - (a.usedAt || 0))
  280. this.saveRecentApps(next.slice(0, RECENT_APPS_MAX))
  281. this.refreshQuickApps()
  282. },
  283. async fetchLaunchpadApps() {
  284. this.launchpadLoading = true
  285. try {
  286. const token = getToken()
  287. if (!token) {
  288. this.quickApps = []
  289. this.allApps = []
  290. this.categories = [{ id: 'recent', name: '全部应用' }]
  291. this.activeCategoryId = 'recent'
  292. return
  293. }
  294. const res = await getLaunchpadApps()
  295. const items = Array.isArray(res && res.items) ? res.items : []
  296. const activeItems = items.filter((i) => i && i.is_active !== false)
  297. const appList = activeItems.map((it) => {
  298. const categoryId = it.category_id == null || it.category_id === '' ? 'uncat' : String(it.category_id)
  299. return {
  300. id: it.app_id,
  301. name: it.app_name,
  302. // 与聊天列表 SystemAvatar 保持一致
  303. iconPath: '/static/icons/application.svg',
  304. iconBg: getGradientBgByName(it.app_name),
  305. category: categoryId,
  306. categoryName: it.category_name || '未分类',
  307. description: it.description || '',
  308. protocolType: it.protocol_type,
  309. mappedKey: it.mapped_key,
  310. mappedEmail: it.mapped_email,
  311. isActive: it.is_active
  312. }
  313. })
  314. this.allApps = appList
  315. // 从本地最近列表拼出顶部“最近使用”
  316. const recentList = this.loadRecentApps()
  317. .filter((x) => x && x.appId != null)
  318. .map((x) => ({ appId: String(x.appId), usedAt: Number(x.usedAt) || 0 }))
  319. .sort((a, b) => (b.usedAt || 0) - (a.usedAt || 0))
  320. const appById = new Map(appList.map((a) => [String(a.id), a]))
  321. const recentApps = []
  322. const recentSeen = new Set()
  323. for (const r of recentList) {
  324. const id = String(r.appId || '')
  325. if (!id || recentSeen.has(id)) continue
  326. if (!appById.has(id)) continue
  327. recentSeen.add(id)
  328. recentApps.push(appById.get(id))
  329. if (recentApps.length >= RECENT_APPS_MAX) break
  330. }
  331. // 不足 8 个时,用接口里的其他应用补齐(但 recents 仍保持按时间倒序)
  332. if (recentApps.length < RECENT_APPS_MAX) {
  333. const rest = appList.filter((a) => !recentSeen.has(String(a.id)))
  334. recentApps.push(...rest.slice(0, RECENT_APPS_MAX - recentApps.length))
  335. }
  336. this.quickApps = recentApps
  337. const catMap = new Map()
  338. for (const a of appList) {
  339. if (!catMap.has(a.category)) {
  340. catMap.set(a.category, { id: a.category, name: a.categoryName || '未分类' })
  341. }
  342. }
  343. this.categories = [{ id: 'recent', name: '全部应用' }, ...Array.from(catMap.values())]
  344. this.activeCategoryId = 'recent'
  345. } catch (e) {
  346. // utils/api.js 的 request 已经做了网络、401/403 登录态提示
  347. uni.showToast({ title: '加载应用失败', icon: 'none' })
  348. } finally {
  349. this.launchpadLoading = false
  350. }
  351. },
  352. loadCurrentUser() {
  353. try {
  354. const raw = uni.getStorageSync(USER_KEY)
  355. if (raw && typeof raw === 'object') {
  356. this.currentUser = {
  357. name: raw.name || raw.nickname || '',
  358. id: String(raw.id ?? raw.user_id ?? ''),
  359. avatar: raw.avatar || raw.avatar_url || ''
  360. }
  361. }
  362. } catch (e) {}
  363. },
  364. onAvatarClick() {
  365. uni.navigateTo({ url: '/pages/profile/index' })
  366. },
  367. onSearch() {
  368. uni.navigateTo({ url: '/pages/search-center/index' })
  369. },
  370. // 先清空再设锚点,避免同 id 时不滚动(与 pages/chat/index.vue 一致)
  371. scrollCategoryTabIntoView(catIdx) {
  372. const idx = Number(catIdx)
  373. if (!Number.isFinite(idx) || idx < 0) return
  374. const anchor = 'cat-tab-' + idx
  375. this.scrollIntoTabId = ''
  376. this.$nextTick(() => {
  377. this.scrollIntoTabId = anchor
  378. setTimeout(() => {
  379. this.scrollIntoTabId = anchor
  380. }, 50)
  381. })
  382. },
  383. onCategoryTabClick(catIdx) {
  384. const cat = this.categories[catIdx]
  385. if (!cat) return
  386. this.activeCategoryId = cat.id
  387. this.scrollCategoryTabIntoView(catIdx)
  388. },
  389. // swiper 滑动切换分类 -> 同步更新 Tab 高亮
  390. onSwiperChange(e) {
  391. const idx = e && e.detail ? e.detail.current : 0
  392. const cat = this.categories[idx]
  393. if (cat) this.activeCategoryId = cat.id
  394. this.scrollCategoryTabIntoView(idx)
  395. },
  396. // 根据分类 id 获取该页要渲染的应用列表
  397. getAppsByCategory(catId) {
  398. const active = String(catId || '')
  399. if (active === 'recent') return this.allApps
  400. const list = this.allApps.filter((a) => String(a.category || '') === active)
  401. return list.length ? list : this.allApps
  402. },
  403. async openApp(app) {
  404. if (!app || !app.id) return
  405. uni.showLoading({ title: '打开中...' })
  406. try {
  407. const res = await ssoLogin(app.id)
  408. const redirectUrl = res && (res.redirect_url || res.redirectUrl)
  409. if (!redirectUrl) {
  410. uni.showToast({ title: '打开失败', icon: 'none' })
  411. return
  412. }
  413. // 登录成功并获得 redirect_url 后,把该应用记录为最新使用
  414. this.recordRecentApp(app.id)
  415. openEmbeddedOrSystemBrowser(redirectUrl, app.name || '应用')
  416. } catch (e) {
  417. uni.showToast({ title: '打开失败', icon: 'none' })
  418. } finally {
  419. uni.hideLoading()
  420. }
  421. }
  422. }
  423. }
  424. </script>
  425. <style scoped>
  426. .app-center-page {
  427. min-height: 100vh;
  428. height: 100vh;
  429. display: flex;
  430. flex-direction: column;
  431. background: #fff;
  432. }
  433. .app-center-scroll {
  434. flex: 1;
  435. min-height: 0;
  436. height: 0;
  437. /* 与消息/通讯录一致:用框架 tabBar 区域高度,避免重复加 safe-area 造成大块白底或与原生 Tab 错位遮挡 */
  438. padding-bottom: var(--window-bottom, 50px);
  439. box-sizing: border-box;
  440. }
  441. /* 顶部安全区:与消息/通讯录页面 custom-header 对齐 */
  442. .custom-header {
  443. display: flex;
  444. align-items: center;
  445. justify-content: space-between;
  446. padding: 24rpx 24rpx 24rpx 32rpx;
  447. padding-top: 88rpx;
  448. padding-top: max(88rpx, calc(24rpx + constant(safe-area-inset-top)));
  449. padding-top: max(88rpx, calc(24rpx + env(safe-area-inset-top)));
  450. background: #fff;
  451. border-bottom: 1rpx solid #eee;
  452. }
  453. .header-left {
  454. display: flex;
  455. align-items: center;
  456. flex: 1;
  457. min-width: 0;
  458. }
  459. .org-info {
  460. margin-left: 24rpx;
  461. display: flex;
  462. flex-direction: column;
  463. min-width: 0;
  464. }
  465. .user-name {
  466. font-size: 32rpx;
  467. font-weight: 600;
  468. color: #111827;
  469. }
  470. .header-right {
  471. display: flex;
  472. align-items: center;
  473. gap: 24rpx;
  474. }
  475. .icon-btn {
  476. width: 64rpx;
  477. height: 64rpx;
  478. display: flex;
  479. align-items: center;
  480. justify-content: center;
  481. }
  482. .icon-img {
  483. width: 44rpx;
  484. height: 44rpx;
  485. opacity: 0.85;
  486. }
  487. /* 最近使用:整体与顶栏拉开一点距离 */
  488. .recent-section {
  489. padding-top: 28rpx;
  490. }
  491. /* 常用应用 4 列网格 */
  492. .quick-grid {
  493. display: flex;
  494. flex-wrap: wrap;
  495. gap: 24rpx;
  496. /* 与顶部自定义顶栏的头像左侧对齐:顶栏左 padding = 32rpx */
  497. padding: 24rpx 24rpx 40rpx 32rpx;
  498. }
  499. .app-tile {
  500. width: calc((100% - 24rpx * 3) / 4);
  501. display: flex;
  502. flex-direction: column;
  503. align-items: center;
  504. box-sizing: border-box;
  505. }
  506. .tile-icon {
  507. width: 96rpx;
  508. height: 96rpx;
  509. border-radius: 22rpx;
  510. display: flex;
  511. align-items: center;
  512. justify-content: center;
  513. position: relative;
  514. }
  515. .tile-icon-img {
  516. width: 52rpx;
  517. height: 52rpx;
  518. /* 与聊天列表 SystemAvatar 一致:确保图标居中显示为白色 */
  519. filter: brightness(0) invert(1);
  520. }
  521. .tile-badge {
  522. position: absolute;
  523. bottom: 12rpx;
  524. left: 12rpx;
  525. height: 28rpx;
  526. padding: 0 10rpx;
  527. border-radius: 14rpx;
  528. font-size: 20rpx;
  529. line-height: 28rpx;
  530. font-weight: 600;
  531. }
  532. .tile-name {
  533. margin-top: 14rpx;
  534. font-size: 26rpx;
  535. color: #111827;
  536. text-align: center;
  537. width: 100%;
  538. overflow: hidden;
  539. text-overflow: ellipsis;
  540. white-space: nowrap;
  541. }
  542. .tile-name-2 {
  543. white-space: normal;
  544. line-height: 1.2;
  545. display: -webkit-box;
  546. -webkit-line-clamp: 2;
  547. -webkit-box-orient: vertical;
  548. overflow: hidden;
  549. }
  550. /* 全部应用(与上方「最近使用」拉开间距) */
  551. .all-section {
  552. /* 与顶部自定义顶栏的头像左侧对齐:顶栏左 padding = 32rpx */
  553. padding: 32rpx 24rpx 0 32rpx;
  554. }
  555. .section-title {
  556. font-size: 34rpx;
  557. font-weight: 700;
  558. color: #111827;
  559. /* 与顶部自定义顶栏的头像左侧对齐:顶栏左 padding = 32rpx */
  560. padding-left: 32rpx;
  561. margin-bottom: 18rpx;
  562. }
  563. /* 分类 Tab 行 */
  564. .category-bar {
  565. display: flex;
  566. align-items: center;
  567. gap: 16rpx;
  568. }
  569. .category-scroll {
  570. flex: 1;
  571. min-width: 0;
  572. }
  573. .category-list {
  574. display: flex;
  575. align-items: flex-end;
  576. white-space: nowrap;
  577. /* 最后一项滚到最右时不贴边裁切 */
  578. padding-right: 24rpx;
  579. box-sizing: border-box;
  580. }
  581. .category-item {
  582. display: flex;
  583. flex-direction: column;
  584. align-items: center;
  585. padding: 0 22rpx;
  586. flex: 0 0 auto; /* 防止 flex 在容器宽度不足时压缩导致文字换行 */
  587. }
  588. .category-name {
  589. font-size: 28rpx;
  590. color: #6b7280;
  591. white-space: nowrap; /* 不允许换行,过长则由外层横向 scroll-view 滚动显示 */
  592. }
  593. .category-item.active .category-name {
  594. color: #1f6feb;
  595. font-weight: 600;
  596. }
  597. .category-underline {
  598. width: 56rpx;
  599. height: 6rpx;
  600. border-radius: 8rpx;
  601. background: #1f6feb;
  602. margin-top: 12rpx;
  603. }
  604. .apps-swiper {
  605. width: 100%;
  606. min-height: 600rpx; /* swiper 需要固定/最小高度才能正常展示 */
  607. }
  608. /* 全部应用宫格 */
  609. .apps-grid {
  610. display: flex;
  611. flex-wrap: wrap;
  612. gap: 24rpx;
  613. padding: 24rpx 24rpx 0;
  614. }
  615. </style>