index.vue 26 KB

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