index.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982
  1. <template>
  2. <view class="plaza-page">
  3. <!-- 顶部标题栏 -->
  4. <view class="header">
  5. <view class="header-background">
  6. <view class="feather feather-left">🪶</view>
  7. <view class="feather feather-right">🪶</view>
  8. </view>
  9. <view class="header-content">
  10. <text class="header-title">💕 缘分广场</text>
  11. <text class="header-desc">两只羽毛相遇,编织爱情故事</text>
  12. </view>
  13. </view>
  14. <!-- 动态列表 -->
  15. <scroll-view
  16. class="dynamic-list"
  17. scroll-y
  18. @scrolltolower="loadMore"
  19. refresher-enabled
  20. :refresher-triggered="refreshing"
  21. @refresherrefresh="onRefresh"
  22. >
  23. <!-- 列表内容 -->
  24. <view class="list-content">
  25. <!-- 动态卡片 -->
  26. <view
  27. v-for="item in dynamicList"
  28. :key="item.dynamicId"
  29. class="dynamic-card"
  30. @click="goToDetail(item.dynamicId)"
  31. @longpress="showReportMenu(item)"
  32. >
  33. <!-- 用户信息 -->
  34. <view class="user-info">
  35. <image
  36. :src="getAvatar(item)"
  37. class="avatar"
  38. mode="aspectFill"
  39. ></image>
  40. <view class="user-details">
  41. <text class="nickname">{{ getNickname(item) }}</text>
  42. <text class="time">{{ formatTime(item.createdAt) }}</text>
  43. </view>
  44. </view>
  45. <!-- 动态内容 -->
  46. <view class="dynamic-content">
  47. <text class="content-text">{{ item.content }}</text>
  48. </view>
  49. <!-- 媒体内容(图片) -->
  50. <view v-if="item.mediaUrls && item.mediaUrls.length > 0" class="media-container">
  51. <view
  52. :class="['image-grid', getGridClass(item)]"
  53. >
  54. <image
  55. v-for="(url, index) in item.mediaUrls.slice(0, 9)"
  56. :key="index"
  57. :src="url"
  58. class="media-image"
  59. mode="aspectFill"
  60. @click.stop="previewImage(item.mediaUrls, index)"
  61. ></image>
  62. </view>
  63. </view>
  64. <!-- 互动栏 -->
  65. <view class="action-bar">
  66. <view class="action-item" @click.stop="handleLike(item)">
  67. <text :class="['icon', !!item.isLiked ? 'icon-liked' : '']">
  68. {{ !!item.isLiked ? '❤️' : '🤍' }}
  69. </text>
  70. <text class="action-text">{{ item.likeCount || 0 }}</text>
  71. </view>
  72. <view class="action-item">
  73. <text class="icon">💬</text>
  74. <text class="action-text">{{ item.commentCount || 0 }}</text>
  75. </view>
  76. <view class="action-item" @click.stop="handleFavorite(item)">
  77. <text :class="['icon', !!item.isFavorited ? 'icon-favorited' : '']">
  78. {{ !!item.isFavorited ? '⭐' : '☆' }}
  79. </text>
  80. <text class="action-text">{{ item.favoriteCount || 0 }}</text>
  81. </view>
  82. </view>
  83. </view>
  84. <!-- 加载提示 -->
  85. <view class="loading-tip" v-if="loading">
  86. <text>加载中...</text>
  87. </view>
  88. <!-- 没有更多数据 -->
  89. <view class="loading-tip" v-if="noMore && dynamicList.length > 0">
  90. <text>没有更多了~</text>
  91. </view>
  92. <!-- 空状态 -->
  93. <view class="empty-state" v-if="!loading && dynamicList.length === 0">
  94. <view class="empty-feathers">
  95. <text class="feather-icon">🪶</text>
  96. <text class="feather-icon">🪶</text>
  97. </view>
  98. <text class="empty-text">还没有缘分动态</text>
  99. <text class="empty-hint">快来分享你的美好时光,遇见有趣的灵魂吧~</text>
  100. <view class="empty-action" @click="goToPublish">
  101. <text>💕 发布第一条动态</text>
  102. </view>
  103. </view>
  104. </view>
  105. </scroll-view>
  106. <!-- 发布按钮 -->
  107. <view class="publish-btn" @click="goToPublish">
  108. <view class="publish-bg">
  109. <text class="publish-icon">💕</text>
  110. <text class="publish-text">分享心动</text>
  111. </view>
  112. </view>
  113. <!-- 举报菜单弹窗 -->
  114. <view v-if="showMenu" class="report-menu-popup">
  115. <view class="menu-mask" @click="hideReportMenu"></view>
  116. <view class="menu-content">
  117. <view class="menu-item" @click="handleReport">
  118. <text class="menu-icon">🚩</text>
  119. <text class="menu-text">举报此动态</text>
  120. </view>
  121. <view class="menu-item cancel" @click="hideReportMenu">
  122. <text class="menu-text">取消</text>
  123. </view>
  124. </view>
  125. </view>
  126. <!-- 底部导航栏 -->
  127. <view class="tabbar">
  128. <view class="tabbar-item" @click="switchTab('index')">
  129. <text class="tabbar-icon">🏠</text>
  130. <text class="tabbar-text">首页</text>
  131. </view>
  132. <view class="tabbar-item active" @click="switchTab('plaza')">
  133. <text class="tabbar-icon">💕</text>
  134. <text class="tabbar-text">动态</text>
  135. </view>
  136. <view class="tabbar-item" @click="switchTab('recommend')">
  137. <text class="tabbar-icon">👍</text>
  138. <text class="tabbar-text">推荐</text>
  139. </view>
  140. <!-- <view class="tabbar-item" @click="switchTab('message')">
  141. <text class="tabbar-icon">💬</text>
  142. <text class="tabbar-text">消息</text>
  143. <view v-if="unreadCount > 0" class="tabbar-badge">{{ unreadCount }}</view>
  144. </view> -->
  145. <view class="tabbar-item" @click="switchTab('mine')">
  146. <text class="tabbar-icon">👤</text>
  147. <text class="tabbar-text">我的</text>
  148. </view>
  149. </view>
  150. </view>
  151. </template>
  152. <script>
  153. import api from '@/utils/api.js'
  154. export default {
  155. data() {
  156. return {
  157. dynamicList: [],
  158. pageNum: 1,
  159. pageSize: 10,
  160. loading: false,
  161. refreshing: false,
  162. noMore: false,
  163. showMenu: false, // 显示举报菜单
  164. selectedDynamic: null, // 选中的动态
  165. currentUserId: uni.getStorageInfoSync("userId"), // 当前用户ID,实际应从存储获取
  166. defaultAvatar: 'http://via.placeholder.com/100x100.png?text=头像',
  167. likingMap: {}, // 记录正在点赞的动态ID,防止重复点击
  168. favoritingMap: {} // 记录正在收藏的动态ID,防止重复点击
  169. }
  170. },
  171. computed: {
  172. unreadCount() {
  173. return this.$store.getters.getTotalUnread || 0;
  174. }
  175. },
  176. onLoad() {
  177. // 从存储中获取当前用户ID
  178. const storedUserId = uni.getStorageSync('userId')
  179. if (storedUserId !== null && storedUserId !== undefined && storedUserId !== '') {
  180. this.currentUserId = parseInt(storedUserId)
  181. } else {
  182. // 尝试从 userInfo 中获取
  183. const userInfo = uni.getStorageSync('userInfo')
  184. if (userInfo && (userInfo.userId || userInfo.id || userInfo.user_id)) {
  185. this.currentUserId = parseInt(userInfo.userId || userInfo.id || userInfo.user_id)
  186. } else {
  187. // 未登录,设置为 null
  188. this.currentUserId = null
  189. }
  190. }
  191. this.loadDynamicList()
  192. // 监听详情页更新事件,实时同步点赞/收藏状态
  193. uni.$on('dynamic-updated', (payload) => {
  194. if (!payload || !payload.dynamicId) return
  195. const idx = this.dynamicList.findIndex(x => x.dynamicId === payload.dynamicId)
  196. if (idx !== -1) {
  197. const item = this.dynamicList[idx]
  198. // 使用 hasOwnProperty 严格检查属性是否存在,只更新事件中明确包含的字段
  199. // 这样可以避免误更新不相关的字段(如点赞时误更新收藏状态)
  200. // 根据 dynamic_likes 表判断:有记录显示 ❤️,无记录显示 🤍
  201. if (payload.hasOwnProperty('isLiked') && payload.isLiked !== undefined && payload.isLiked !== null) {
  202. this.$set(item, 'isLiked', payload.isLiked === true || payload.isLiked === 1 || payload.isLiked === 'true' || payload.isLiked === '1')
  203. }
  204. if (payload.hasOwnProperty('likeCount') && payload.likeCount !== undefined && payload.likeCount !== null) {
  205. this.$set(item, 'likeCount', Number(payload.likeCount) || 0)
  206. }
  207. // 只有当 isFavorited 明确存在于 payload 中时才更新(避免误更新)
  208. // 根据 dynamic_favorites 表判断:有记录显示 ⭐,无记录显示 ☆
  209. if (payload.hasOwnProperty('isFavorited') && payload.isFavorited !== undefined && payload.isFavorited !== null) {
  210. this.$set(item, 'isFavorited', payload.isFavorited === true || payload.isFavorited === 1 || payload.isFavorited === 'true' || payload.isFavorited === '1')
  211. }
  212. if (payload.hasOwnProperty('favoriteCount') && payload.favoriteCount !== undefined && payload.favoriteCount !== null) {
  213. this.$set(item, 'favoriteCount', Number(payload.favoriteCount) || 0)
  214. }
  215. }
  216. })
  217. // 监听发布页插入新动态
  218. uni.$on('dynamic-insert', (d) => {
  219. if (!d) return
  220. // 仅在第一页顶部插入
  221. this.dynamicList = [d, ...this.dynamicList]
  222. })
  223. },
  224. onShow() {
  225. // 返回列表页时自动刷新,确保状态一致
  226. // 重新获取当前用户ID(可能在其他页面登录/登出)
  227. const storedUserId = uni.getStorageSync('userId')
  228. if (storedUserId !== null && storedUserId !== undefined && storedUserId !== '') {
  229. this.currentUserId = parseInt(storedUserId)
  230. } else {
  231. // 尝试从 userInfo 中获取
  232. const userInfo = uni.getStorageSync('userInfo')
  233. if (userInfo && (userInfo.userId || userInfo.id || userInfo.user_id)) {
  234. this.currentUserId = parseInt(userInfo.userId || userInfo.id || userInfo.user_id)
  235. } else {
  236. // 未登录,设置为 null
  237. this.currentUserId = null
  238. }
  239. }
  240. this.pageNum = 1
  241. this.noMore = false
  242. this.dynamicList = []
  243. this.loadDynamicList()
  244. },
  245. methods: {
  246. // 兼容WXML:获取头像
  247. getAvatar(item) {
  248. if (item && item.user && item.user.avatarUrl) {
  249. return item.user.avatarUrl
  250. }
  251. return this.defaultAvatar
  252. },
  253. // 兼容WXML:获取昵称
  254. getNickname(item) {
  255. if (item && item.user && item.user.nickname) {
  256. return item.user.nickname
  257. }
  258. return '匿名用户'
  259. },
  260. // 兼容WXML:图片栅格class
  261. getGridClass(item) {
  262. const len = (item && item.mediaUrls && item.mediaUrls.length) ? item.mediaUrls.length : 0
  263. const n = Math.min(len, 3)
  264. return 'grid-' + n
  265. },
  266. // 加载动态列表
  267. async loadDynamicList() {
  268. if (this.loading || this.noMore) return
  269. this.loading = true
  270. try {
  271. const res = await api.dynamic.getRecommendList({
  272. pageNum: this.pageNum,
  273. pageSize: this.pageSize,
  274. userId: this.currentUserId
  275. })
  276. if (res && res.records) {
  277. // 处理数据,确保布尔值正确转换(根据 dynamic_likes 和 dynamic_favorites 表判断)
  278. // 后端返回的 isLiked 和 isFavorited 应该已经是 Boolean,但为了兼容性,确保正确转换
  279. const newData = res.records.map(item => ({
  280. ...item,
  281. // 如果 dynamic_likes 表中有记录,显示 ❤️,否则显示 🤍
  282. isLiked: item.isLiked === true || item.isLiked === 1 || item.isLiked === 'true' || item.isLiked === '1',
  283. // 如果 dynamic_favorites 表中有记录,显示 ⭐,否则显示 ☆
  284. isFavorited: item.isFavorited === true || item.isFavorited === 1 || item.isFavorited === 'true' || item.isFavorited === '1'
  285. }))
  286. if (this.pageNum === 1) {
  287. this.dynamicList = newData
  288. } else {
  289. this.dynamicList = [...this.dynamicList, ...newData]
  290. }
  291. // 判断是否还有更多数据
  292. if (res.current >= res.pages) {
  293. this.noMore = true
  294. } else {
  295. this.pageNum++
  296. }
  297. }
  298. } catch (error) {
  299. console.error('加载动态列表失败:', error)
  300. uni.showToast({
  301. title: '加载失败',
  302. icon: 'none'
  303. })
  304. } finally {
  305. this.loading = false
  306. this.refreshing = false
  307. }
  308. },
  309. // 下拉刷新
  310. onRefresh() {
  311. this.refreshing = true
  312. this.pageNum = 1
  313. this.noMore = false
  314. this.dynamicList = []
  315. this.loadDynamicList()
  316. },
  317. // 上拉加载更多
  318. loadMore() {
  319. if (!this.noMore && !this.loading) {
  320. this.loadDynamicList()
  321. }
  322. },
  323. // 处理点赞
  324. async handleLike(item) {
  325. // 防止重复点击:如果正在处理该动态的点赞请求,直接返回
  326. if (this.likingMap[item.dynamicId]) {
  327. return
  328. }
  329. try {
  330. // 标记为正在处理
  331. this.$set(this.likingMap, item.dynamicId, true)
  332. // 根据 dynamic_likes 表判断是否点赞(有记录显示 ❤️,无记录显示 🤍)
  333. const isLiked = item.isLiked === true || item.isLiked === 1 || item.isLiked === 'true' || item.isLiked === '1'
  334. const originalCount = item.likeCount || 0
  335. const newIsLiked = !isLiked
  336. const newLikeCount = isLiked ? Math.max(0, originalCount - 1) : originalCount + 1
  337. // 立即更新UI(乐观更新),使用 $set 确保响应式更新
  338. this.$set(item, 'isLiked', newIsLiked)
  339. this.$set(item, 'likeCount', newLikeCount)
  340. // 发送请求
  341. if (isLiked) {
  342. // 取消点赞
  343. await api.dynamic.unlike(item.dynamicId)
  344. } else {
  345. // 点赞
  346. await api.dynamic.like(item.dynamicId)
  347. }
  348. } catch (error) {
  349. console.error('点赞操作失败:', error)
  350. // 请求失败,恢复原状态
  351. const currentIsLiked = item.isLiked === true || item.isLiked === 1 || item.isLiked === 'true' || item.isLiked === '1'
  352. this.$set(item, 'isLiked', !currentIsLiked)
  353. this.$set(item, 'likeCount', !currentIsLiked ? (item.likeCount || 0) + 1 : Math.max(0, (item.likeCount || 0) - 1))
  354. uni.showToast({
  355. title: '操作失败,请重试',
  356. icon: 'none'
  357. })
  358. } finally {
  359. // 延迟300毫秒后释放锁,防止快速点击
  360. setTimeout(() => {
  361. this.$set(this.likingMap, item.dynamicId, false)
  362. }, 300)
  363. }
  364. },
  365. // 处理收藏
  366. async handleFavorite(item) {
  367. // 防止重复点击:如果正在处理该动态的收藏请求,直接返回
  368. if (this.favoritingMap[item.dynamicId]) {
  369. return
  370. }
  371. try {
  372. // 标记为正在处理
  373. this.$set(this.favoritingMap, item.dynamicId, true)
  374. // 根据 dynamic_favorites 表判断是否收藏(有记录显示 ⭐,无记录显示 ☆)
  375. const isFavorited = item.isFavorited === true || item.isFavorited === 1 || item.isFavorited === 'true' || item.isFavorited === '1'
  376. const originalCount = item.favoriteCount || 0
  377. const newIsFavorited = !isFavorited
  378. const newFavoriteCount = isFavorited ? Math.max(0, originalCount - 1) : originalCount + 1
  379. // 立即更新UI(乐观更新),使用 $set 确保响应式更新
  380. this.$set(item, 'isFavorited', newIsFavorited)
  381. this.$set(item, 'favoriteCount', newFavoriteCount)
  382. if (isFavorited) {
  383. // 取消收藏
  384. await api.dynamic.unfavorite(item.dynamicId)
  385. } else {
  386. // 收藏
  387. await api.dynamic.favorite(item.dynamicId)
  388. uni.showToast({
  389. title: '收藏成功',
  390. icon: 'success'
  391. })
  392. }
  393. } catch (error) {
  394. console.error('收藏操作失败:', error)
  395. // 请求失败,恢复原状态
  396. const currentIsFavorited = item.isFavorited === true || item.isFavorited === 1 || item.isFavorited === 'true' || item.isFavorited === '1'
  397. this.$set(item, 'isFavorited', !currentIsFavorited)
  398. this.$set(item, 'favoriteCount', !currentIsFavorited ? (item.favoriteCount || 0) + 1 : Math.max(0, (item.favoriteCount || 0) - 1))
  399. uni.showToast({
  400. title: '操作失败,请重试',
  401. icon: 'none'
  402. })
  403. } finally {
  404. // 延迟300毫秒后释放锁,防止快速点击
  405. setTimeout(() => {
  406. this.$set(this.favoritingMap, item.dynamicId, false)
  407. }, 300)
  408. }
  409. },
  410. // 预览图片
  411. previewImage(urls, current) {
  412. uni.previewImage({
  413. urls: urls,
  414. current: current
  415. })
  416. },
  417. // 跳转到详情页
  418. goToDetail(dynamicId) {
  419. uni.navigateTo({
  420. url: `/pages/plaza/detail?id=${dynamicId}`
  421. })
  422. },
  423. // 跳转到发布页
  424. goToPublish() {
  425. console.log('点击了发布按钮');
  426. uni.navigateTo({
  427. url: '/pages/plaza/publish',
  428. fail: (err) => {
  429. console.error('跳转失败:', err);
  430. }
  431. })
  432. },
  433. // 显示举报菜单
  434. showReportMenu(item) {
  435. // 震动反馈
  436. uni.vibrateShort({
  437. type: 'light'
  438. })
  439. this.selectedDynamic = item
  440. this.showMenu = true
  441. },
  442. // 隐藏举报菜单
  443. hideReportMenu() {
  444. this.showMenu = false
  445. this.selectedDynamic = null
  446. },
  447. // 处理举报
  448. handleReport() {
  449. this.hideReportMenu()
  450. if (!this.selectedDynamic || !this.selectedDynamic.dynamicId) {
  451. uni.showToast({
  452. title: '动态ID不存在',
  453. icon: 'none'
  454. })
  455. return
  456. }
  457. // 跳转到举报页面
  458. uni.navigateTo({
  459. url: `/pages/plaza/report?id=${this.selectedDynamic.dynamicId}`
  460. })
  461. },
  462. // 格式化时间
  463. formatTime(timeStr) {
  464. if (!timeStr) return ''
  465. const time = new Date(timeStr)
  466. const now = new Date()
  467. const diff = now - time
  468. // 1分钟内
  469. if (diff < 60000) {
  470. return '刚刚'
  471. }
  472. // 1小时内
  473. if (diff < 3600000) {
  474. return Math.floor(diff / 60000) + '分钟前'
  475. }
  476. // 24小时内
  477. if (diff < 86400000) {
  478. return Math.floor(diff / 3600000) + '小时前'
  479. }
  480. // 7天内
  481. if (diff < 604800000) {
  482. return Math.floor(diff / 86400000) + '天前'
  483. }
  484. // 超过7天显示日期
  485. const year = time.getFullYear()
  486. const month = String(time.getMonth() + 1).padStart(2, '0')
  487. const day = String(time.getDate()).padStart(2, '0')
  488. if (year === now.getFullYear()) {
  489. return `${month}-${day}`
  490. }
  491. return `${year}-${month}-${day}`
  492. },
  493. // 切换Tab
  494. switchTab(tab) {
  495. if (tab === 'plaza') return
  496. const tabPages = {
  497. index: '/pages/index/index',
  498. recommend: '/pages/recommend/index',
  499. message: '/subpkg-message/index/index',
  500. mine: '/pages/mine/index'
  501. }
  502. if (tabPages[tab]) {
  503. uni.redirectTo({
  504. url: tabPages[tab]
  505. })
  506. }
  507. }
  508. }
  509. }
  510. </script>
  511. <style lang="scss" scoped>
  512. .plaza-page {
  513. padding-top: 60px;
  514. min-height: 100vh;
  515. background: #FFF0F5; // 淡淡粉色背景
  516. // 顶部标题栏
  517. .header {
  518. position: fixed;
  519. top: 0;
  520. left: 0;
  521. right: 0;
  522. height: 88rpx; // 更紧凑,减小头部占用
  523. background: transparent; // 移除重色背景,突出可爱风淡粉
  524. display: flex;
  525. flex-direction: column;
  526. align-items: center;
  527. justify-content: center;
  528. padding-top: 0; // 移除顶部安全区额外留白
  529. z-index: 999;
  530. box-shadow: none;
  531. overflow: visible;
  532. position: relative;
  533. .header-background { display: none; }
  534. .header-content {
  535. text-align: center;
  536. z-index: 1;
  537. .header-subtitle {
  538. font-size: 22rpx;
  539. color: #E91E63;
  540. display: block;
  541. margin-bottom: 8rpx;
  542. letter-spacing: 2rpx;
  543. }
  544. .header-title {
  545. font-size: 36rpx;
  546. font-weight: 700;
  547. color: #D81B60;
  548. text-shadow: none;
  549. display: block;
  550. margin-bottom: 2rpx; // 缩小标题与描述间距
  551. }
  552. .header-desc {
  553. font-size: 20rpx;
  554. color: #AD1457;
  555. display: block;
  556. letter-spacing: 1rpx;
  557. }
  558. }
  559. }
  560. // 动态列表
  561. .dynamic-list {
  562. margin-top: 88rpx; // 同步减少顶部外边距
  563. padding-bottom: 120rpx;
  564. height: calc(100vh - 88rpx);
  565. .list-content {
  566. padding: 12rpx; // 再收紧列表内边距
  567. }
  568. // 动态卡片
  569. .dynamic-card {
  570. background: #FFFFFF; // 纯白卡片
  571. border-radius: 20rpx;
  572. padding: 24rpx; // 收紧卡片内边距
  573. margin-bottom: 14rpx; // 卡片之间更紧凑
  574. box-shadow: 0 4rpx 12rpx rgba(233, 30, 99, 0.07);
  575. border: 1rpx solid rgba(255, 182, 193, 0.22);
  576. transition: all 0.3s ease;
  577. position: relative;
  578. overflow: hidden;
  579. // 用户信息
  580. .user-info {
  581. display: flex;
  582. align-items: center;
  583. margin-bottom: 24rpx;
  584. .avatar {
  585. width: 88rpx;
  586. height: 88rpx;
  587. border-radius: 50%;
  588. margin-right: 24rpx;
  589. border: 3rpx solid #FFB3BA;
  590. box-shadow: 0 4rpx 12rpx rgba(233, 30, 99, 0.2);
  591. }
  592. .user-details {
  593. flex: 1;
  594. .nickname {
  595. display: block;
  596. font-size: 30rpx;
  597. font-weight: 600;
  598. color: #2D1B69;
  599. margin-bottom: 8rpx;
  600. }
  601. .time {
  602. font-size: 24rpx;
  603. color: #8B5A96;
  604. opacity: 0.8;
  605. }
  606. }
  607. }
  608. // 动态内容
  609. .dynamic-content {
  610. margin-bottom: 20rpx;
  611. .content-text {
  612. font-size: 28rpx;
  613. line-height: 1.6;
  614. color: #333333;
  615. word-wrap: break-word;
  616. }
  617. }
  618. // 媒体容器
  619. .media-container {
  620. margin-bottom: 16rpx;
  621. .image-grid {
  622. display: grid;
  623. gap: 10rpx;
  624. &.grid-1 {
  625. grid-template-columns: 1fr;
  626. .media-image {
  627. height: 400rpx;
  628. border-radius: 12rpx;
  629. }
  630. }
  631. &.grid-2,
  632. &.grid-3 {
  633. grid-template-columns: repeat(3, 1fr);
  634. .media-image {
  635. height: 180rpx;
  636. border-radius: 12rpx;
  637. }
  638. }
  639. .media-image {
  640. width: 100%;
  641. background-color: #F5F5F5;
  642. }
  643. }
  644. }
  645. // 互动栏
  646. .action-bar {
  647. display: flex;
  648. align-items: center;
  649. padding-top: 20rpx;
  650. border-top: 1rpx solid #F5F5F5;
  651. .action-item {
  652. flex: 1;
  653. display: flex;
  654. align-items: center;
  655. justify-content: center;
  656. .icon {
  657. font-size: 40rpx;
  658. margin-right: 8rpx;
  659. &.icon-liked {
  660. animation: heartBeat 0.3s ease-in-out;
  661. }
  662. &.icon-favorited {
  663. animation: starShine 0.3s ease-in-out;
  664. }
  665. }
  666. .action-text {
  667. font-size: 24rpx;
  668. color: #666666;
  669. }
  670. }
  671. }
  672. }
  673. // 加载提示
  674. .loading-tip {
  675. text-align: center;
  676. padding: 40rpx 0;
  677. font-size: 24rpx;
  678. color: #999999;
  679. }
  680. // 空状态
  681. .empty-state {
  682. display: flex;
  683. flex-direction: column;
  684. align-items: center;
  685. justify-content: center;
  686. padding: 200rpx 60rpx;
  687. .empty-icon {
  688. font-size: 120rpx;
  689. margin-bottom: 30rpx;
  690. }
  691. .empty-text {
  692. font-size: 32rpx;
  693. color: #333333;
  694. margin-bottom: 16rpx;
  695. }
  696. .empty-hint {
  697. font-size: 24rpx;
  698. color: #999999;
  699. }
  700. }
  701. }
  702. // 发布按钮
  703. .publish-btn {
  704. position: fixed;
  705. bottom: 200rpx;
  706. right: 40rpx;
  707. z-index: 998;
  708. .publish-bg {
  709. width: 140rpx;
  710. height: 140rpx;
  711. background: linear-gradient(135deg, #FF6B9D 0%, #E91E63 25%, #C2185B 50%, #FF8A95 75%, #FFB3BA 100%);
  712. border-radius: 50%;
  713. display: flex;
  714. flex-direction: column;
  715. align-items: center;
  716. justify-content: center;
  717. box-shadow: 0 12rpx 36rpx rgba(233, 30, 99, 0.5);
  718. transition: all 0.3s ease;
  719. border: 4rpx solid rgba(255, 255, 255, 0.8);
  720. animation: pulse-love 3s ease-in-out infinite;
  721. &:active {
  722. transform: scale(0.9);
  723. }
  724. .publish-icon {
  725. font-size: 42rpx;
  726. margin-bottom: 4rpx;
  727. }
  728. .publish-text {
  729. font-size: 18rpx;
  730. color: rgba(255, 255, 255, 0.9);
  731. font-weight: 500;
  732. letter-spacing: 1rpx;
  733. }
  734. }
  735. }
  736. // 举报菜单弹窗
  737. .report-menu-popup {
  738. position: fixed;
  739. top: 0;
  740. left: 0;
  741. right: 0;
  742. bottom: 0;
  743. z-index: 9998;
  744. .menu-mask {
  745. position: absolute;
  746. top: 0;
  747. left: 0;
  748. right: 0;
  749. bottom: 0;
  750. background-color: rgba(0, 0, 0, 0.5);
  751. }
  752. .menu-content {
  753. position: absolute;
  754. bottom: 0;
  755. left: 0;
  756. right: 0;
  757. background-color: #FFFFFF;
  758. border-radius: 30rpx 30rpx 0 0;
  759. padding: 20rpx 30rpx;
  760. padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
  761. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  762. animation: slideUp 0.3s ease;
  763. .menu-item {
  764. height: 100rpx;
  765. display: flex;
  766. align-items: center;
  767. justify-content: center;
  768. border-radius: 12rpx;
  769. margin-bottom: 16rpx;
  770. transition: background-color 0.3s ease;
  771. &:not(.cancel) {
  772. background: #FFF5F7;
  773. &:active {
  774. background: #FFE8EC;
  775. }
  776. }
  777. &.cancel {
  778. background: #F5F5F5;
  779. margin-top: 8rpx;
  780. &:active {
  781. background: #EEEEEE;
  782. }
  783. }
  784. .menu-icon {
  785. font-size: 36rpx;
  786. margin-right: 12rpx;
  787. }
  788. .menu-text {
  789. font-size: 30rpx;
  790. color: #333333;
  791. font-weight: 500;
  792. }
  793. }
  794. }
  795. }
  796. // 底部导航栏
  797. .tabbar {
  798. position: fixed;
  799. bottom: 0;
  800. left: 0;
  801. right: 0;
  802. display: flex;
  803. background-color: #FFFFFF;
  804. border-top: 1rpx solid #F0F0F0;
  805. padding-bottom: constant(safe-area-inset-bottom);
  806. padding-bottom: env(safe-area-inset-bottom);
  807. z-index: 999;
  808. box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
  809. .tabbar-item {
  810. flex: 1;
  811. display: flex;
  812. flex-direction: column;
  813. align-items: center;
  814. justify-content: center;
  815. padding: 15rpx 0;
  816. position: relative;
  817. .tabbar-icon {
  818. font-size: 44rpx;
  819. margin-bottom: 5rpx;
  820. }
  821. .tabbar-text {
  822. font-size: 22rpx;
  823. color: #666666;
  824. }
  825. &.active {
  826. .tabbar-text {
  827. color: #E91E63;
  828. font-weight: bold;
  829. }
  830. }
  831. }
  832. }
  833. }
  834. // 动画
  835. @keyframes heartBeat {
  836. 0%, 100% {
  837. transform: scale(1);
  838. }
  839. 50% {
  840. transform: scale(1.2);
  841. }
  842. }
  843. @keyframes starShine {
  844. 0%, 100% {
  845. transform: scale(1) rotate(0deg);
  846. }
  847. 50% {
  848. transform: scale(1.2) rotate(15deg);
  849. }
  850. }
  851. @keyframes slideUp {
  852. from {
  853. transform: translateY(100%);
  854. }
  855. to {
  856. transform: translateY(0);
  857. }
  858. }
  859. .tabbar-badge {
  860. position: absolute;
  861. top: 8rpx;
  862. right: 50%;
  863. margin-right: -40rpx;
  864. min-width: 32rpx;
  865. height: 32rpx;
  866. line-height: 32rpx;
  867. padding: 0 6rpx;
  868. background-color: #FA5151;
  869. border-radius: 16rpx;
  870. font-size: 20rpx;
  871. color: #FFFFFF;
  872. text-align: center;
  873. }
  874. </style>