index.vue 26 KB

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