index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959
  1. <template>
  2. <view class="page">
  3. <!-- 用户信息卡片 -->
  4. <view class="user-card">
  5. <view class="user-info">
  6. <image v-if="userStore.userInfo?.avatar" :src="userStore.userInfo.avatar" class="avatar" mode="aspectFill" />
  7. <view v-else class="avatar-placeholder">
  8. <text class="avatar-text">👤</text>
  9. </view>
  10. <view class="user-text">
  11. <text class="nickname">{{ userStore.userInfo?.nickname || '未登录' }}</text>
  12. <view class="member-badge" :class="memberClass">
  13. <text class="member-text">{{ memberText }}</text>
  14. </view>
  15. </view>
  16. </view>
  17. <!-- 积分余额(两行展示) -->
  18. <view class="credits-section" v-if="tokenBalance">
  19. <view class="credit-row">
  20. <view class="credit-item">
  21. <text class="credit-label">📋 订阅积分</text>
  22. <text class="credit-value">{{ formatNumber(tokenBalance.subscriptionRemaining ?? tokenBalance.remainingTokens) }}</text>
  23. <text class="credit-sub" v-if="tokenBalance.subscriptionTokens">/ {{ formatNumber(tokenBalance.subscriptionTokens) }} 总量</text>
  24. </view>
  25. <view class="credit-divider" />
  26. <view class="credit-item">
  27. <text class="credit-label">🎁 积分包</text>
  28. <text class="credit-value">{{ formatNumber(tokenBalance.packRemaining ?? 0) }}</text>
  29. <text class="credit-sub" v-if="tokenBalance.packTokens > 0">/ {{ formatNumber(tokenBalance.packTokens) }} 总量</text>
  30. <text class="credit-sub" v-else>暂无</text>
  31. </view>
  32. </view>
  33. <!-- 进度条 -->
  34. <view class="credit-bar-row">
  35. <view class="credit-bar" :style="{ flex: tokenBalance.subscriptionTokens || 1 }">
  36. <view class="credit-bar-fill sub" :style="{ width: subscriptionPercent + '%' }"></view>
  37. </view>
  38. <view class="credit-bar-gap" />
  39. <view class="credit-bar" :style="{ flex: tokenBalance.packTokens || 1 }">
  40. <view class="credit-bar-fill pack" :style="{ width: packPercent + '%' }"></view>
  41. </view>
  42. </view>
  43. </view>
  44. <!-- 使用统计 -->
  45. <view class="stats-row" v-if="userStore.memberStatus">
  46. <view class="stat-item">
  47. <text class="stat-value">{{ userStore.memberStatus.quota.dailyRemaining }}</text>
  48. <text class="stat-label">今日次数</text>
  49. </view>
  50. <view class="stat-divider" />
  51. <view class="stat-item">
  52. <text class="stat-value">{{ userStore.memberStatus.quota.wordLimit === 0 ? '∞' : userStore.memberStatus.quota.wordLimit }}</text>
  53. <text class="stat-label">字数上限</text>
  54. </view>
  55. </view>
  56. </view>
  57. <!-- 今日创作概况 -->
  58. <view class="today-card" v-if="userStore.isLoggedIn">
  59. <view class="today-header">
  60. <text class="today-title">今日创作概况</text>
  61. <text class="today-date">{{ todayDate }}</text>
  62. </view>
  63. <view class="today-stats">
  64. <view class="today-stat-item">
  65. <text class="today-stat-value">{{ todayStats.audioCount }}</text>
  66. <text class="today-stat-label">生成音频</text>
  67. </view>
  68. <view class="today-stat-divider" />
  69. <view class="today-stat-item">
  70. <text class="today-stat-value">{{ todayStats.bookCount }}</text>
  71. <text class="today-stat-label">生成书籍</text>
  72. </view>
  73. <view class="today-stat-divider" />
  74. <view class="today-stat-item">
  75. <text class="today-stat-value">{{ todayStats.usageQuota }}</text>
  76. <text class="today-stat-label">使用额度</text>
  77. </view>
  78. </view>
  79. </view>
  80. <!-- 最近的创作 -->
  81. <view class="recent-card" v-if="recentCreations.length > 0">
  82. <view class="recent-header">
  83. <text class="recent-title">最近的创作</text>
  84. <text class="recent-more" @click="goHistory">查看更多 ›</text>
  85. </view>
  86. <view class="recent-list">
  87. <view
  88. v-for="item in recentCreations"
  89. :key="item._id"
  90. class="recent-item"
  91. hover-class="recent-item-hover"
  92. @click="goToDetail(item)"
  93. >
  94. <view class="recent-cover" :style="{ background: getCoverGradient(item.voiceId) }">
  95. <text class="cover-letter">{{ getTitleLetter(item.title) }}</text>
  96. <text class="cover-title-small">{{ item.title }}</text>
  97. </view>
  98. <view class="recent-info">
  99. <text class="recent-title-text">{{ item.title }}</text>
  100. <text class="recent-meta">{{ item.wordCount }}字 · {{ formatDate(item.createdAt) }}</text>
  101. </view>
  102. <text class="recent-arrow">›</text>
  103. </view>
  104. </view>
  105. </view>
  106. <!-- 升级会员推广卡片 -->
  107. <view class="upgrade-card" v-if="userStore.isLoggedIn && !userStore.isMember" @click="goMember">
  108. <view class="upgrade-left">
  109. <text class="upgrade-icon">👑</text>
  110. <view class="upgrade-text">
  111. <text class="upgrade-title">升级会员</text>
  112. <text class="upgrade-desc">解锁更多创作次数和高级音色</text>
  113. </view>
  114. </view>
  115. <text class="upgrade-btn">立即开通</text>
  116. </view>
  117. <!-- 功能入口 -->
  118. <view class="menu-section">
  119. <view class="menu-item" @click="goBookGenerator">
  120. <text class="menu-icon">📖</text>
  121. <text class="menu-text">书籍生成</text>
  122. <text class="menu-arrow">›</text>
  123. </view>
  124. <view class="menu-item" @click="goMember">
  125. <text class="menu-icon">👑</text>
  126. <text class="menu-text">会员中心</text>
  127. <text class="menu-arrow">›</text>
  128. </view>
  129. <view class="menu-item" @click="goSettings">
  130. <text class="menu-icon">⚙️</text>
  131. <text class="menu-text">设置</text>
  132. <text class="menu-arrow">›</text>
  133. </view>
  134. <view class="menu-item" @click="goTokenPacks">
  135. <text class="menu-icon">🎁</text>
  136. <text class="menu-text">购买积分包</text>
  137. <text class="menu-arrow">›</text>
  138. </view>
  139. <view class="menu-item" @click="goInvite">
  140. <text class="menu-icon">🎁</text>
  141. <text class="menu-text">邀请好友 · 双方得10000积分</text>
  142. <text class="menu-arrow">›</text>
  143. </view>
  144. <view class="menu-item" @click="goOrders">
  145. <text class="menu-icon">📋</text>
  146. <text class="menu-text">订单记录</text>
  147. <text class="menu-arrow">›</text>
  148. </view>
  149. <view class="menu-item" @click="goBilling">
  150. <text class="menu-icon">💰</text>
  151. <text class="menu-text">消费账单</text>
  152. <text class="menu-arrow">›</text>
  153. </view>
  154. <view class="menu-item" @click="goHistory">
  155. <text class="menu-icon">📜</text>
  156. <text class="menu-text">历史记录</text>
  157. <text class="menu-arrow">›</text>
  158. </view>
  159. <view class="menu-item" @click="showAbout">
  160. <text class="menu-icon">ℹ️</text>
  161. <text class="menu-text">关于我们</text>
  162. <text class="menu-arrow">›</text>
  163. </view>
  164. <view class="menu-item" @click="showFeedback">
  165. <text class="menu-icon">💬</text>
  166. <text class="menu-text">意见反馈</text>
  167. <text class="menu-arrow">›</text>
  168. </view>
  169. <view class="menu-item" @click="goTextToVideo">
  170. <text class="menu-icon">🎬</text>
  171. <text class="menu-text">文本生成视频</text>
  172. <text class="menu-tag">新功能</text>
  173. <text class="menu-arrow">›</text>
  174. </view>
  175. </view>
  176. <!-- 退出登录 -->
  177. <view class="logout-section" v-if="userStore.isLoggedIn">
  178. <button class="logout-btn" @click="handleLogout">退出登录</button>
  179. </view>
  180. </view>
  181. </template>
  182. <script setup lang="ts">
  183. import { computed, ref, onMounted } from 'vue';
  184. import { onShow } from '@dcloudio/uni-app';
  185. import { useUserStore } from '../../store/user';
  186. import { get } from '../../utils/request';
  187. import { formatNumber } from '../../utils/format';
  188. import type { AudioItem } from '../../types';
  189. import { getCoverGradient, getTitleLetter } from '../../composables/useCoverStyle';
  190. import { getTodayStartISO } from '../../utils/time';
  191. const userStore = useUserStore();
  192. // 积分余额
  193. const tokenBalance = ref<any>(null);
  194. const subscriptionPercent = computed(() => {
  195. if (!tokenBalance.value || !tokenBalance.value.subscriptionTokens) return 100;
  196. return tokenBalance.value.subscriptionTokens > 0
  197. ? Math.round((tokenBalance.value.subscriptionRemaining || 0) / tokenBalance.value.subscriptionTokens * 100)
  198. : 100;
  199. });
  200. const packPercent = computed(() => {
  201. if (!tokenBalance.value || !tokenBalance.value.packTokens) return 0;
  202. return tokenBalance.value.packTokens > 0
  203. ? Math.round((tokenBalance.value.packRemaining || 0) / tokenBalance.value.packTokens * 100)
  204. : 0;
  205. });
  206. // 今日创作概况
  207. const todayStats = ref({
  208. audioCount: 0,
  209. bookCount: 0,
  210. usageQuota: 0
  211. });
  212. // 最近的创作
  213. const recentCreations = ref<AudioItem[]>([]);
  214. // 今日日期
  215. const todayDate = computed(() => {
  216. const now = new Date();
  217. return `${now.getMonth() + 1}月${now.getDate()}日`;
  218. });
  219. // 计算属性
  220. const memberClass = computed(() => {
  221. const level = userStore.memberStatus?.level || 0;
  222. return `level-${level}`;
  223. });
  224. const memberText = computed(() => {
  225. return userStore.memberStatus?.levelName || '免费用户';
  226. });
  227. // 获取积分余额
  228. async function loadTokenBalance() {
  229. try {
  230. tokenBalance.value = await get('/subscription/balance');
  231. } catch (error) {
  232. console.error('获取积分余额失败:', error);
  233. }
  234. }
  235. // 获取今日创作概况
  236. async function fetchTodayStats() {
  237. try {
  238. const startDate = getTodayStartISO();
  239. // 获取今日音频数
  240. const audioResult = await get<{ list: AudioItem[]; total: number }>('/history', {
  241. startDate,
  242. pageSize: 100
  243. });
  244. // 获取今日书籍数(从书籍生成记录获取)
  245. let bookCount = 0;
  246. try {
  247. const bookResult = await get<{ list: any[]; total: number }>('/book-generator/list', {
  248. startDate,
  249. pageSize: 100
  250. });
  251. bookCount = bookResult.total || 0;
  252. } catch (e) {
  253. // 书籍接口可能不存在
  254. console.log('[Mine] 书籍接口不存在', e);
  255. }
  256. todayStats.value = {
  257. audioCount: audioResult.total || 0,
  258. bookCount,
  259. usageQuota: userStore.memberStatus?.quota.dailyRemaining || 0
  260. };
  261. } catch (error) {
  262. console.log('[Mine] 获取今日概况失败', error);
  263. }
  264. }
  265. // 获取最近的创作
  266. async function fetchRecentCreations() {
  267. try {
  268. const result = await get<{ list: AudioItem[]; total: number }>('/history', {
  269. pageSize: 3,
  270. sortBy: 'createdAt',
  271. sortOrder: 'desc'
  272. });
  273. recentCreations.value = (result.list || []).slice(0, 3);
  274. } catch (error) {
  275. console.log('[Mine] 获取最近创作失败', error);
  276. }
  277. }
  278. // 格式化日期
  279. function formatDate(dateStr: string): string {
  280. const date = new Date(dateStr);
  281. const now = new Date();
  282. const diff = now.getTime() - date.getTime();
  283. const oneDay = 24 * 60 * 60 * 1000;
  284. if (diff < oneDay) return '今天';
  285. if (diff < 2 * oneDay) return '昨天';
  286. return `${date.getMonth() + 1}/${date.getDate()}`;
  287. }
  288. // 跳转到历史记录页
  289. function goToDetail(item: AudioItem) {
  290. uni.navigateTo({ url: '/pages/history/index' });
  291. }
  292. // 页面显示
  293. onShow(() => {
  294. if (userStore.isLoggedIn) {
  295. userStore.fetchMemberStatus();
  296. loadTokenBalance();
  297. fetchTodayStats();
  298. fetchRecentCreations();
  299. } else {
  300. // 未登录,跳转到登录页
  301. uni.navigateTo({ url: '/pages/login/index' });
  302. }
  303. });
  304. // 跳转会员页
  305. function goMember() {
  306. uni.navigateTo({ url: '/pages/member/index' });
  307. }
  308. // 跳转设置页
  309. function goSettings() {
  310. uni.navigateTo({ url: '/pages/settings/index' });
  311. }
  312. // 跳转日志监控页
  313. function goLogs() {
  314. uni.navigateTo({ url: '/pages/logs/index' });
  315. }
  316. // 跳转 Token 包购买
  317. function goTokenPacks() {
  318. uni.navigateTo({ url: '/pages/token-packs/index' });
  319. }
  320. // 跳转邀请页
  321. function goInvite() {
  322. uni.navigateTo({ url: '/pages/invite/index' });
  323. }
  324. // 跳转订单记录
  325. function goOrders() {
  326. uni.navigateTo({ url: '/pages/orders/index' });
  327. }
  328. // 跳转消费账单
  329. function goBilling() {
  330. uni.navigateTo({ url: '/pages/billing/index' });
  331. }
  332. // 跳转历史页
  333. function goHistory() {
  334. uni.switchTab({ url: '/pages/history/index' });
  335. }
  336. // 跳转文本生成视频页
  337. function goTextToVideo() {
  338. uni.navigateTo({ url: '/pages/text-to-video/index' });
  339. }
  340. // 跳转书籍生成页
  341. function goBookGenerator() {
  342. uni.switchTab({ url: '/pages/book-generator/index' });
  343. }
  344. // 关于我们
  345. function showAbout() {
  346. uni.showModal({
  347. title: '关于声工坊',
  348. content: '声工坊是一款智能文本转语音工具,帮助您轻松制作高质量音频内容。\n\n版本: 1.0.0',
  349. showCancel: false,
  350. });
  351. }
  352. // 意见反馈
  353. function showFeedback() {
  354. uni.showModal({
  355. title: '意见反馈',
  356. content: '如有问题或建议,请联系客服:\n邮箱:support@example.com',
  357. showCancel: false,
  358. });
  359. }
  360. // 退出登录
  361. function handleLogout() {
  362. uni.showModal({
  363. title: '确认退出',
  364. content: '确定要退出登录吗?',
  365. success: (res) => {
  366. if (res.confirm) {
  367. userStore.logout();
  368. uni.showToast({ title: '已退出登录', icon: 'success' });
  369. // 关闭所有页面,打开登录页
  370. uni.reLaunch({ url: '/pages/login/index' });
  371. }
  372. },
  373. });
  374. }
  375. </script>
  376. <style scoped>
  377. .page {
  378. min-height: 100vh;
  379. background: var(--color-bg-page, #f3f4f6);
  380. padding-bottom: 120rpx;
  381. }
  382. /* --- 用户信息卡片 --- */
  383. .user-card {
  384. margin: 24rpx 24rpx 20rpx;
  385. background: linear-gradient(135deg, var(--color-primary-600, #4f46e5) 0%, #7c3aed 100%);
  386. border-radius: var(--radius-lg, 24rpx);
  387. padding: 40rpx 32rpx 32rpx;
  388. box-shadow: var(--shadow-primary, 0 4rpx 20rpx rgba(79, 70, 229, 0.25));
  389. position: relative;
  390. overflow: hidden;
  391. }
  392. .user-card::before {
  393. content: '';
  394. position: absolute;
  395. top: -40%;
  396. right: -20%;
  397. width: 280rpx;
  398. height: 280rpx;
  399. background: rgba(255, 255, 255, 0.04);
  400. border-radius: 50%;
  401. pointer-events: none;
  402. }
  403. .user-card::after {
  404. content: '';
  405. position: absolute;
  406. bottom: -30%;
  407. left: -10%;
  408. width: 200rpx;
  409. height: 200rpx;
  410. background: rgba(255, 255, 255, 0.03);
  411. border-radius: 50%;
  412. pointer-events: none;
  413. }
  414. .user-info {
  415. display: flex;
  416. align-items: center;
  417. margin-bottom: 28rpx;
  418. position: relative;
  419. z-index: 1;
  420. }
  421. .avatar {
  422. width: 96rpx;
  423. height: 96rpx;
  424. border-radius: 50%;
  425. border: 3rpx solid rgba(255, 255, 255, 0.4);
  426. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15);
  427. }
  428. .avatar-placeholder {
  429. width: 96rpx;
  430. height: 96rpx;
  431. border-radius: 50%;
  432. background: rgba(255, 255, 255, 0.2);
  433. display: flex;
  434. align-items: center;
  435. justify-content: center;
  436. border: 3rpx solid rgba(255, 255, 255, 0.4);
  437. }
  438. .avatar-text {
  439. font-size: 48rpx;
  440. }
  441. .user-text {
  442. margin-left: 24rpx;
  443. }
  444. .nickname {
  445. display: block;
  446. font-size: var(--text-xl, 36rpx);
  447. font-weight: 700;
  448. color: #ffffff;
  449. margin-bottom: 8rpx;
  450. letter-spacing: -0.3rpx;
  451. }
  452. .member-badge {
  453. display: inline-block;
  454. padding: 4rpx 16rpx;
  455. border-radius: 20rpx;
  456. background: rgba(255, 255, 255, 0.2);
  457. backdrop-filter: blur(8px);
  458. }
  459. .member-badge.level-1 {
  460. background: linear-gradient(135deg, rgba(249, 115, 22, 0.9), rgba(251, 146, 60, 0.9));
  461. }
  462. .member-badge.level-2 {
  463. background: linear-gradient(135deg, rgba(251, 191, 36, 0.9), rgba(252, 211, 77, 0.9));
  464. }
  465. .member-text {
  466. font-size: 22rpx;
  467. color: #ffffff;
  468. font-weight: 600;
  469. }
  470. /* 积分余额区域 */
  471. .credits-section {
  472. background: rgba(255, 255, 255, 0.12);
  473. border-radius: var(--radius-md, 16rpx);
  474. padding: 20rpx 24rpx;
  475. margin-bottom: 12rpx;
  476. backdrop-filter: blur(8px);
  477. }
  478. .credit-row {
  479. display: flex;
  480. align-items: center;
  481. }
  482. .credit-item {
  483. flex: 1;
  484. text-align: center;
  485. }
  486. .credit-label {
  487. display: block;
  488. font-size: 20rpx;
  489. color: rgba(255, 255, 255, 0.7);
  490. margin-bottom: 6rpx;
  491. }
  492. .credit-value {
  493. display: block;
  494. font-size: 34rpx;
  495. font-weight: 700;
  496. color: #ffffff;
  497. letter-spacing: -0.5rpx;
  498. }
  499. .credit-sub {
  500. display: block;
  501. font-size: 18rpx;
  502. color: rgba(255, 255, 255, 0.5);
  503. margin-top: 2rpx;
  504. }
  505. .credit-divider {
  506. width: 1rpx;
  507. height: 60rpx;
  508. background: rgba(255, 255, 255, 0.2);
  509. }
  510. /* 双进度条 */
  511. .credit-bar-row {
  512. display: flex;
  513. align-items: center;
  514. margin-top: 14rpx;
  515. gap: 8rpx;
  516. }
  517. .credit-bar {
  518. height: 5rpx;
  519. background: rgba(255, 255, 255, 0.2);
  520. border-radius: 3rpx;
  521. overflow: hidden;
  522. }
  523. .credit-bar-gap {
  524. width: 6rpx;
  525. }
  526. .credit-bar-fill {
  527. height: 100%;
  528. border-radius: 3rpx;
  529. transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
  530. }
  531. .credit-bar-fill.sub {
  532. background: #34d399;
  533. }
  534. .credit-bar-fill.pack {
  535. background: #fbbf24;
  536. }
  537. /* 使用统计 */
  538. .stats-row {
  539. display: flex;
  540. align-items: center;
  541. background: rgba(255, 255, 255, 0.1);
  542. border-radius: var(--radius-md, 16rpx);
  543. padding: 20rpx 24rpx;
  544. backdrop-filter: blur(8px);
  545. }
  546. .stat-item {
  547. flex: 1;
  548. text-align: center;
  549. }
  550. .stat-value {
  551. display: block;
  552. font-size: 32rpx;
  553. font-weight: 700;
  554. color: #ffffff;
  555. letter-spacing: -0.5rpx;
  556. }
  557. .stat-label {
  558. display: block;
  559. font-size: 20rpx;
  560. color: rgba(255, 255, 255, 0.7);
  561. margin-top: 4rpx;
  562. }
  563. .stat-divider {
  564. width: 1rpx;
  565. height: 60rpx;
  566. background: rgba(255, 255, 255, 0.2);
  567. }
  568. /* --- 今日创作概况 --- */
  569. .today-card {
  570. margin: 0 24rpx 20rpx;
  571. background: var(--color-bg-card, #ffffff);
  572. border-radius: var(--radius-lg, 24rpx);
  573. padding: 32rpx;
  574. box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
  575. }
  576. .today-header {
  577. display: flex;
  578. justify-content: space-between;
  579. align-items: center;
  580. margin-bottom: 28rpx;
  581. }
  582. .today-title {
  583. font-size: var(--text-base, 28rpx);
  584. font-weight: 700;
  585. color: var(--color-text-primary, #111827);
  586. letter-spacing: -0.2rpx;
  587. }
  588. .today-date {
  589. font-size: var(--text-sm, 24rpx);
  590. color: var(--color-text-tertiary, #9ca3af);
  591. }
  592. .today-stats {
  593. display: flex;
  594. align-items: center;
  595. }
  596. .today-stat-item {
  597. flex: 1;
  598. text-align: center;
  599. }
  600. .today-stat-value {
  601. display: block;
  602. font-size: 40rpx;
  603. font-weight: 800;
  604. color: var(--color-primary-600, #4f46e5);
  605. letter-spacing: -0.5rpx;
  606. }
  607. .today-stat-label {
  608. display: block;
  609. font-size: 22rpx;
  610. color: var(--color-text-secondary, #4b5563);
  611. margin-top: 6rpx;
  612. }
  613. .today-stat-divider {
  614. width: 1rpx;
  615. height: 60rpx;
  616. background: var(--color-bg-divider, #e5e7eb);
  617. }
  618. /* --- 最近的创作 --- */
  619. .recent-card {
  620. margin: 0 24rpx 20rpx;
  621. background: var(--color-bg-card, #ffffff);
  622. border-radius: var(--radius-lg, 24rpx);
  623. padding: 32rpx;
  624. box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
  625. }
  626. .recent-header {
  627. display: flex;
  628. justify-content: space-between;
  629. align-items: center;
  630. margin-bottom: 20rpx;
  631. }
  632. .recent-title {
  633. font-size: var(--text-base, 28rpx);
  634. font-weight: 700;
  635. color: var(--color-text-primary, #111827);
  636. }
  637. .recent-more {
  638. font-size: var(--text-sm, 24rpx);
  639. color: var(--color-primary-600, #4f46e5);
  640. font-weight: 500;
  641. }
  642. .recent-list {
  643. display: flex;
  644. flex-direction: column;
  645. }
  646. .recent-item {
  647. display: flex;
  648. align-items: center;
  649. padding: 18rpx 12rpx;
  650. border-radius: var(--radius-sm, 10rpx);
  651. margin-bottom: 4rpx;
  652. transition: background 0.15s;
  653. }
  654. .recent-item:last-child {
  655. margin-bottom: 0;
  656. }
  657. .recent-item-hover {
  658. background-color: var(--color-bg-surface, #f9fafb);
  659. }
  660. .recent-cover {
  661. width: 76rpx;
  662. height: 76rpx;
  663. border-radius: var(--radius-sm, 10rpx);
  664. display: flex;
  665. align-items: center;
  666. justify-content: center;
  667. flex-shrink: 0;
  668. position: relative;
  669. overflow: hidden;
  670. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
  671. }
  672. .cover-letter {
  673. position: absolute;
  674. top: -6rpx;
  675. left: 4rpx;
  676. font-size: 52rpx;
  677. font-weight: 900;
  678. color: rgba(255, 255, 255, 0.12);
  679. line-height: 1;
  680. pointer-events: none;
  681. }
  682. .cover-title-small {
  683. position: absolute;
  684. bottom: 4rpx;
  685. left: 8rpx;
  686. right: 8rpx;
  687. font-size: 16rpx;
  688. color: rgba(255, 255, 255, 0.8);
  689. font-weight: 500;
  690. overflow: hidden;
  691. text-overflow: ellipsis;
  692. white-space: nowrap;
  693. }
  694. .cover-icon {
  695. font-size: 32rpx;
  696. }
  697. .recent-info {
  698. flex: 1;
  699. margin-left: 18rpx;
  700. overflow: hidden;
  701. }
  702. .recent-title-text {
  703. display: block;
  704. font-size: var(--text-base, 28rpx);
  705. font-weight: 500;
  706. color: var(--color-text-primary, #111827);
  707. white-space: nowrap;
  708. overflow: hidden;
  709. text-overflow: ellipsis;
  710. }
  711. .recent-meta {
  712. display: block;
  713. font-size: 22rpx;
  714. color: var(--color-text-tertiary, #9ca3af);
  715. margin-top: 4rpx;
  716. }
  717. .recent-arrow {
  718. font-size: 28rpx;
  719. color: var(--color-text-placeholder, #d1d5db);
  720. margin-left: 16rpx;
  721. }
  722. /* --- 升级会员卡片 --- */
  723. .upgrade-card {
  724. margin: 0 24rpx 20rpx;
  725. background: linear-gradient(135deg, #fef3c7, #fde68a);
  726. border-radius: var(--radius-lg, 24rpx);
  727. padding: 24rpx 28rpx;
  728. display: flex;
  729. align-items: center;
  730. justify-content: space-between;
  731. box-shadow: 0 4rpx 16rpx rgba(251, 191, 36, 0.15);
  732. transition: transform 0.15s;
  733. }
  734. .upgrade-card:active {
  735. transform: scale(0.98);
  736. }
  737. .upgrade-left {
  738. display: flex;
  739. align-items: center;
  740. }
  741. .upgrade-icon {
  742. font-size: 36rpx;
  743. margin-right: 16rpx;
  744. }
  745. .upgrade-text {
  746. display: flex;
  747. flex-direction: column;
  748. }
  749. .upgrade-title {
  750. display: block;
  751. font-size: var(--text-base, 28rpx);
  752. font-weight: 700;
  753. color: #92400e;
  754. }
  755. .upgrade-desc {
  756. display: block;
  757. font-size: 22rpx;
  758. color: #b45309;
  759. margin-top: 2rpx;
  760. }
  761. .upgrade-btn {
  762. font-size: 24rpx;
  763. color: #ffffff;
  764. background: linear-gradient(135deg, #f59e0b, #d97706);
  765. padding: 14rpx 28rpx;
  766. border-radius: 24rpx;
  767. font-weight: 600;
  768. box-shadow: 0 2rpx 8rpx rgba(217, 119, 6, 0.3);
  769. }
  770. /* --- 功能菜单 --- */
  771. .menu-section {
  772. margin: 24rpx;
  773. background: var(--color-bg-card, #ffffff);
  774. border-radius: var(--radius-lg, 24rpx);
  775. overflow: hidden;
  776. box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
  777. }
  778. .menu-item {
  779. display: flex;
  780. align-items: center;
  781. padding: 28rpx 32rpx;
  782. transition: background 0.15s;
  783. }
  784. .menu-item:active {
  785. background: var(--color-bg-surface, #f9fafb);
  786. }
  787. .menu-icon {
  788. font-size: 36rpx;
  789. margin-right: 20rpx;
  790. width: 48rpx;
  791. text-align: center;
  792. }
  793. .menu-text {
  794. flex: 1;
  795. font-size: var(--text-base, 28rpx);
  796. color: var(--color-text-primary, #111827);
  797. font-weight: 500;
  798. }
  799. .menu-tag {
  800. font-size: 18rpx;
  801. padding: 4rpx 12rpx;
  802. background: linear-gradient(135deg, #f97316, #fb923c);
  803. color: #ffffff;
  804. border-radius: 6rpx;
  805. margin-right: 12rpx;
  806. font-weight: 600;
  807. }
  808. .menu-arrow {
  809. font-size: 28rpx;
  810. color: var(--color-text-placeholder, #d1d5db);
  811. }
  812. .logout-section {
  813. margin: 40rpx 24rpx;
  814. }
  815. .logout-btn {
  816. width: 100%;
  817. height: 88rpx;
  818. background: var(--color-bg-card, #ffffff);
  819. border-radius: var(--radius-md, 16rpx);
  820. border: 1.5rpx solid var(--color-error, #ef4444);
  821. display: flex;
  822. align-items: center;
  823. justify-content: center;
  824. box-shadow: var(--shadow-xs, 0 1rpx 2rpx rgba(0,0,0,0.04));
  825. transition: all 0.15s;
  826. }
  827. .logout-btn:active {
  828. background: var(--color-error-bg, #fef2f2);
  829. }
  830. .logout-btn::after {
  831. border: none;
  832. }
  833. .logout-btn text {
  834. font-size: var(--text-base, 28rpx);
  835. color: var(--color-error, #ef4444);
  836. font-weight: 500;
  837. }
  838. </style>