瑶都万能墙
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

796 lines
18 KiB

2 weeks ago
  1. <template>
  2. <view class="profile-page">
  3. <!-- 顶部导航 -->
  4. <!-- <navbar
  5. :title="userProfile.nickName || '用户主页'"
  6. :leftClick="true"
  7. :moreClick="showMoreOptions"
  8. bgColor="transparent"
  9. color="#fff"
  10. @leftClick="$utils.navigateBack"
  11. /> -->
  12. <!-- 用户信息头部 -->
  13. <view class="profile-header">
  14. <!-- 背景装饰 -->
  15. <view class="header-bg"></view>
  16. <!-- 返回按钮 -->
  17. <view class="back-btn" @click="$utils.navigateBack">
  18. <uv-icon name="arrow-left" size="30rpx" color="#fff"></uv-icon>
  19. </view>
  20. <!-- 用户基本信息 -->
  21. <view class="user-info">
  22. <view class="user-avatar">
  23. <image :src="userProfile.headImage" mode="aspectFill" @click="previewAvatar"></image>
  24. <!-- VIP标识 -->
  25. <view class="vip-badge" v-if="userProfile.isPay">
  26. <text>{{ getVipLevel(userProfile.isPay) }}</text>
  27. </view>
  28. </view>
  29. <view class="user-details">
  30. <view class="username">{{ userProfile.nickName }}</view>
  31. <view class="user-tags">
  32. <!-- 性别年龄 -->
  33. <view class="tag gender-tag" v-if="userProfile.sex">
  34. <uv-icon :name="sexIcons[userProfile.sex]" size="24rpx" :color="sexColors[userProfile.sex]"></uv-icon>
  35. <text>{{ userProfile.sex }}</text>
  36. <text v-if="userProfile.yearDate">{{ getAge() }}</text>
  37. </view>
  38. <!-- 地址 -->
  39. <view class="tag location-tag" v-if="userProfile.address">
  40. <uv-icon name="map-pin" size="20rpx" color="#666"></uv-icon>
  41. <text>{{ userProfile.address }}</text>
  42. </view>
  43. <!-- 认证状态 -->
  44. <view class="tag auth-tag" v-if="userProfile.idCardOpen">
  45. <uv-icon name="checkmark-circle-fill" size="20rpx" color="#52c41a"></uv-icon>
  46. <text>{{ getAuthText(userProfile.idCardOpen) }}</text>
  47. </view>
  48. </view>
  49. <!-- 学校信息 -->
  50. <view class="school-info" v-if="userProfile.czSchool || userProfile.gzSchool">
  51. <text v-if="userProfile.czSchool">🎓 {{ userProfile.czSchool }}</text>
  52. <text v-if="userProfile.gzSchool">🏫 {{ userProfile.gzSchool }}</text>
  53. </view>
  54. </view>
  55. </view>
  56. <!-- 数据统计 -->
  57. <view class="stats-row">
  58. <view class="stat-item" @click="showFans">
  59. <view class="stat-number">{{ userProfile.intentionNum || 0 }}</view>
  60. <view class="stat-label">粉丝</view>
  61. </view>
  62. <view class="stat-item" @click="showFollowing">
  63. <view class="stat-number">{{ userProfile.followNum || 0 }}</view>
  64. <view class="stat-label">关注</view>
  65. </view>
  66. <view class="stat-item" @click="showLikes">
  67. <view class="stat-number">{{ userProfile.likeNum || 0 }}</view>
  68. <view class="stat-label">获赞</view>
  69. </view>
  70. </view>
  71. <!-- 操作按钮 -->
  72. <view class="action-buttons" v-if="!isCurrentUser">
  73. <button class="follow-btn" :class="{followed: isFollowed}" @click="toggleFollow">
  74. <uv-icon :name="isFollowed ? 'checkmark' : 'plus'" size="24rpx"></uv-icon>
  75. <text>{{ isFollowed ? '已关注' : '关注' }}</text>
  76. </button>
  77. <button class="message-btn" @click="sendMessage">
  78. <uv-icon name="chat" size="24rpx"></uv-icon>
  79. <text>私信</text>
  80. </button>
  81. </view>
  82. </view>
  83. <!-- 内容标签页 -->
  84. <view class="content-tabs">
  85. <uv-tabs
  86. :list="contentTabs"
  87. :current="currentTabIndex"
  88. :activeStyle="{color: '#333', fontWeight: 600}"
  89. lineColor="#5baaff"
  90. lineHeight="6rpx"
  91. lineWidth="40rpx"
  92. keyName="name"
  93. @click="onTabClick"
  94. />
  95. </view>
  96. <!-- 内容区域 -->
  97. <view class="content-container">
  98. <!-- 帖子 - 瀑布流展示 -->
  99. <view v-if="currentTabIndex === 0" class="posts-content">
  100. <waterfallContainer
  101. v-if="postsList.length > 0"
  102. :list="postsList"
  103. @item-click="onPostClick"
  104. @item-like="onPostLike"
  105. />
  106. </view>
  107. <!-- 租房信息 -->
  108. <view v-else-if="currentTabIndex === 1" class="renting-content">
  109. <rentingItem
  110. v-for="(item, index) in rentingList"
  111. :key="index"
  112. :item="item"
  113. @click="onRentingClick(item)"
  114. />
  115. </view>
  116. <!-- 招聘信息 -->
  117. <view v-else-if="currentTabIndex === 2" class="work-content">
  118. <workItem
  119. v-for="(item, index) in workList"
  120. :key="index"
  121. :item="item"
  122. @click="onWorkClick(item)"
  123. />
  124. </view>
  125. <!-- 店铺信息 -->
  126. <view v-else-if="currentTabIndex === 3" class="shop-content">
  127. <gourmetItem
  128. v-for="(item, index) in shopList"
  129. :key="index"
  130. :item="item"
  131. @click="onShopClick(item)"
  132. />
  133. </view>
  134. <!-- 加载更多提示 -->
  135. <view v-if="showLoadMore" class="load-more-state">
  136. <uv-loading-icon size="32"></uv-loading-icon>
  137. <text>加载更多...</text>
  138. </view>
  139. <!-- 没有更多数据提示 -->
  140. <view v-if="showNoMore" class="no-more-state">
  141. <text> 没有更多了 </text>
  142. </view>
  143. </view>
  144. <!-- 空状态 -->
  145. <view v-if="showEmptyState" class="empty-state">
  146. <uv-empty
  147. :text="getEmptyText()"
  148. :icon="getEmptyIcon()"
  149. iconSize="120"
  150. ></uv-empty>
  151. </view>
  152. <!-- 初始加载状态 -->
  153. <view v-if="loading && currentList.length === 0" class="loading-state">
  154. <uv-loading-icon size="40"></uv-loading-icon>
  155. <text>加载中...</text>
  156. </view>
  157. </view>
  158. </template>
  159. <script>
  160. import navbar from '@/components/base/navbar.vue'
  161. import waterfallContainer from '@/components/list/square/waterfallContainer.vue'
  162. import rentingItem from '@/components/list/renting/rentingItem.vue'
  163. import workItem from '@/components/list/work/workItem.vue'
  164. import gourmetItem from '@/components/list/gourmet/gourmetItem.vue'
  165. import { mapState } from 'vuex'
  166. export default {
  167. components: {
  168. navbar,
  169. waterfallContainer,
  170. rentingItem,
  171. workItem,
  172. gourmetItem
  173. },
  174. data() {
  175. return {
  176. userId: '', // 要查看的用户ID
  177. userProfile: {}, // 用户资料
  178. currentTabIndex: 0, // 当前标签页索引
  179. contentTabs: [
  180. { name: '帖子', icon: 'grid' },
  181. { name: '租房', icon: 'home' },
  182. { name: '招聘', icon: 'search' },
  183. { name: '店铺', icon: 'shop' }
  184. ],
  185. postsList: [], // 帖子列表
  186. rentingList: [], // 租房列表
  187. workList: [], // 招聘列表
  188. shopList: [], // 店铺列表
  189. loading: false, // 初始加载状态
  190. loadingMore: false, // 加载更多状态
  191. isFollowed: false, // 是否已关注
  192. // 分页参数
  193. pageParams: {
  194. postsList: { pageNum: 1, hasMore: true },
  195. rentingList: { pageNum: 1, hasMore: true },
  196. workList: { pageNum: 1, hasMore: true },
  197. shopList: { pageNum: 1, hasMore: true }
  198. },
  199. sexIcons: {
  200. '男': 'mars',
  201. '女': 'venus',
  202. '其他': 'transgender'
  203. },
  204. sexColors: {
  205. '男': '#4A90E2',
  206. '女': '#FF69B4',
  207. '其他': '#999'
  208. }
  209. }
  210. },
  211. computed: {
  212. ...mapState(['userInfo']),
  213. // 是否是当前登录用户
  214. isCurrentUser() {
  215. return this.userInfo.id === this.userId
  216. },
  217. // 当前显示的列表
  218. currentList() {
  219. const listMap = ['postsList', 'rentingList', 'workList', 'shopList']
  220. return this[listMap[this.currentTabIndex]] || []
  221. },
  222. // 当前列表的分页参数
  223. currentPageParams() {
  224. const listMap = ['postsList', 'rentingList', 'workList', 'shopList']
  225. return this.pageParams[listMap[this.currentTabIndex]]
  226. },
  227. // 是否显示空状态
  228. showEmptyState() {
  229. return !this.loading && this.currentList.length === 0
  230. },
  231. // 是否显示加载更多
  232. showLoadMore() {
  233. return this.currentList.length > 0 && this.currentPageParams.hasMore && this.loadingMore
  234. },
  235. // 是否显示没有更多
  236. showNoMore() {
  237. return this.currentList.length > 0 && !this.currentPageParams.hasMore
  238. }
  239. },
  240. onLoad(options) {
  241. this.userId = options.userId || this.userInfo.id
  242. this.loadUserProfile()
  243. this.loadUserContent(false)
  244. },
  245. onPullDownRefresh() {
  246. this.loadUserProfile()
  247. this.refreshUserContent()
  248. },
  249. onReachBottom() {
  250. this.loadMoreContent()
  251. },
  252. methods: {
  253. // 显示更多选项
  254. showMoreOptions() {
  255. uni.showActionSheet({
  256. itemList: ['举报用户', '拉黑用户'],
  257. success: (res) => {
  258. if (res.tapIndex === 0) {
  259. this.reportUser()
  260. } else if (res.tapIndex === 1) {
  261. this.blockUser()
  262. }
  263. }
  264. })
  265. },
  266. // 预览头像
  267. previewAvatar() {
  268. if (this.userProfile.headImage) {
  269. uni.previewImage({
  270. urls: [this.userProfile.headImage]
  271. })
  272. }
  273. },
  274. // 获取VIP等级
  275. getVipLevel(level) {
  276. const levels = ['', 'VIP', 'SVIP']
  277. return levels[level] || ''
  278. },
  279. // 获取年龄
  280. getAge() {
  281. if (!this.userProfile.yearDate) return ''
  282. const birthYear = parseInt(this.userProfile.yearDate)
  283. const currentYear = new Date().getFullYear()
  284. return currentYear - birthYear
  285. },
  286. // 获取认证文本
  287. getAuthText(status) {
  288. const authTexts = ['审核中', '个人认证', '店铺认证']
  289. return authTexts[status] || '未认证'
  290. },
  291. // 标签页切换
  292. onTabClick(item) {
  293. this.currentTabIndex = item.index
  294. this.refreshUserContent()
  295. },
  296. // 切换关注状态
  297. toggleFollow() {
  298. if (!uni.getStorageSync('token')) {
  299. uni.showToast({
  300. title: '请先登录',
  301. icon: 'none'
  302. })
  303. return
  304. }
  305. this.isFollowed = !this.isFollowed
  306. // 这里调用关注/取消关注API
  307. this.$api(this.isFollowed ? 'followUser' : 'unfollowUser', {
  308. userId: this.userId
  309. }, res => {
  310. if (res.code === 200) {
  311. uni.showToast({
  312. title: this.isFollowed ? '关注成功' : '取消关注',
  313. icon: 'success'
  314. })
  315. }
  316. })
  317. },
  318. // 发送私信
  319. sendMessage() {
  320. if (!uni.getStorageSync('token')) {
  321. uni.showToast({
  322. title: '请先登录',
  323. icon: 'none'
  324. })
  325. return
  326. }
  327. uni.navigateTo({
  328. url: `/pages_order/chat/chatDetail?userId=${this.userId}`
  329. })
  330. },
  331. // 显示粉丝列表
  332. showFans() {
  333. uni.navigateTo({
  334. url: `/pages_order/profile/fansList?userId=${this.userId}&type=fans`
  335. })
  336. },
  337. // 显示关注列表
  338. showFollowing() {
  339. uni.navigateTo({
  340. url: `/pages_order/profile/fansList?userId=${this.userId}&type=following`
  341. })
  342. },
  343. // 显示点赞列表
  344. showLikes() {
  345. uni.showToast({
  346. title: '功能开发中',
  347. icon: 'none'
  348. })
  349. },
  350. // 加载用户资料
  351. loadUserProfile() {
  352. this.loading = true
  353. this.$api('getUserProfile', { userId: this.userId }, res => {
  354. this.loading = false
  355. uni.stopPullDownRefresh()
  356. if (res.code === 200) {
  357. this.userProfile = res.result
  358. // 检查是否已关注(如果不是当前用户)
  359. if (!this.isCurrentUser) {
  360. this.checkFollowStatus()
  361. }
  362. }
  363. })
  364. },
  365. // 检查关注状态
  366. checkFollowStatus() {
  367. this.$api('checkFollowStatus', { userId: this.userId }, res => {
  368. if (res.code === 200) {
  369. this.isFollowed = res.result.isFollowed
  370. }
  371. })
  372. },
  373. // 刷新用户内容(重置分页)
  374. refreshUserContent() {
  375. const listMap = ['postsList', 'rentingList', 'workList', 'shopList']
  376. const currentListKey = listMap[this.currentTabIndex]
  377. // 重置分页参数
  378. this.pageParams[currentListKey].pageNum = 1
  379. this.pageParams[currentListKey].hasMore = true
  380. // 清空当前列表
  381. this[currentListKey] = []
  382. // 加载第一页数据
  383. this.loadUserContent(false)
  384. },
  385. // 加载用户内容
  386. loadUserContent(isLoadMore = false) {
  387. const apiMap = [
  388. 'getUserPosts', // 帖子
  389. 'getUserRenting', // 租房
  390. 'getUserWork', // 招聘
  391. 'getUserShop' // 店铺
  392. ]
  393. const listMap = ['postsList', 'rentingList', 'workList', 'shopList']
  394. const currentListKey = listMap[this.currentTabIndex]
  395. const currentPageParams = this.pageParams[currentListKey]
  396. // 设置加载状态
  397. if (isLoadMore) {
  398. this.loadingMore = true
  399. } else {
  400. this.loading = true
  401. }
  402. this.$api(apiMap[this.currentTabIndex], {
  403. userId: this.userId,
  404. pageNum: currentPageParams.pageNum,
  405. pageSize: 20
  406. }, res => {
  407. // 清除加载状态
  408. this.loading = false
  409. this.loadingMore = false
  410. uni.stopPullDownRefresh()
  411. if (res.code === 200) {
  412. const newData = res.result.records || res.result || []
  413. if (isLoadMore) {
  414. // 加载更多:追加数据
  415. this[currentListKey] = [...this[currentListKey], ...newData]
  416. } else {
  417. // 首次加载:替换数据
  418. this[currentListKey] = newData
  419. }
  420. // 更新分页状态
  421. if (newData.length < 20) {
  422. // 数据不足一页,说明没有更多了
  423. this.pageParams[currentListKey].hasMore = false
  424. } else {
  425. // 还有更多数据,页码+1
  426. this.pageParams[currentListKey].pageNum += 1
  427. }
  428. } else {
  429. // 请求失败,恢复分页参数
  430. if (isLoadMore && currentPageParams.pageNum > 1) {
  431. this.pageParams[currentListKey].pageNum -= 1
  432. }
  433. }
  434. })
  435. },
  436. // 加载更多内容
  437. loadMoreContent() {
  438. // 检查是否有更多数据和是否正在加载
  439. if (!this.currentPageParams.hasMore || this.loadingMore || this.loading) {
  440. return
  441. }
  442. // 检查当前列表是否有数据
  443. if (this.currentList.length === 0) {
  444. return
  445. }
  446. this.loadUserContent(true)
  447. },
  448. // 点击帖子
  449. onPostClick(item) {
  450. this.$utils.navigateTo(`/pages_order/post/postDetail?id=${item.id}`)
  451. },
  452. // 点赞帖子
  453. onPostLike(item) {
  454. console.log('点赞帖子:', item.id)
  455. },
  456. // 点击租房
  457. onRentingClick(item) {
  458. this.$utils.navigateTo(`/pages_order/renting/rentingDetail?id=${item.id}`)
  459. },
  460. // 点击招聘
  461. onWorkClick(item) {
  462. this.$utils.navigateTo(`/pages_order/work/workDetail?id=${item.id}`)
  463. },
  464. // 点击店铺
  465. onShopClick(item) {
  466. this.$utils.navigateTo(`/pages_order/gourmet/gourmetDetail?id=${item.id}`)
  467. },
  468. // 举报用户
  469. reportUser() {
  470. uni.showToast({
  471. title: '举报功能开发中',
  472. icon: 'none'
  473. })
  474. },
  475. // 拉黑用户
  476. blockUser() {
  477. uni.showModal({
  478. title: '确认拉黑该用户?',
  479. success: (res) => {
  480. if (res.confirm) {
  481. // 调用拉黑API
  482. uni.showToast({
  483. title: '拉黑功能开发中',
  484. icon: 'none'
  485. })
  486. }
  487. }
  488. })
  489. },
  490. // 获取空状态文本
  491. getEmptyText() {
  492. const emptyTexts = ['暂无帖子', '暂无租房信息', '暂无招聘信息', '暂无店铺信息']
  493. return emptyTexts[this.currentTabIndex] || '暂无数据'
  494. },
  495. // 获取空状态图标
  496. getEmptyIcon() {
  497. const emptyIcons = ['list', 'home', 'search', 'shop']
  498. return emptyIcons[this.currentTabIndex] || 'list'
  499. }
  500. }
  501. }
  502. </script>
  503. <style scoped lang="scss">
  504. .profile-page {
  505. background-color: #f5f5f5;
  506. min-height: 100vh;
  507. }
  508. .profile-header {
  509. position: relative;
  510. padding: 40rpx 30rpx 40rpx;
  511. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  512. padding-top: 180rpx;
  513. .header-bg {
  514. position: absolute;
  515. top: 0;
  516. left: 0;
  517. right: 0;
  518. bottom: 0;
  519. background: linear-gradient(135deg, rgba(102, 126, 234, 0.9) 0%, rgba(118, 75, 162, 0.9) 100%);
  520. }
  521. .back-btn {
  522. position: absolute;
  523. top: 80rpx;
  524. left: 30rpx;
  525. z-index: 3;
  526. width: 70rpx;
  527. height: 70rpx;
  528. display: flex;
  529. align-items: center;
  530. justify-content: center;
  531. background: rgba(255, 255, 255, 0.2);
  532. border-radius: 50%;
  533. backdrop-filter: blur(10rpx);
  534. }
  535. .user-info {
  536. position: relative;
  537. z-index: 2;
  538. display: flex;
  539. margin-bottom: 40rpx;
  540. .user-avatar {
  541. position: relative;
  542. margin-right: 30rpx;
  543. image {
  544. width: 160rpx;
  545. height: 160rpx;
  546. border-radius: 80rpx;
  547. border: 6rpx solid rgba(255, 255, 255, 0.3);
  548. }
  549. .vip-badge {
  550. position: absolute;
  551. bottom: -10rpx;
  552. left: 50%;
  553. transform: translateX(-50%);
  554. background: linear-gradient(45deg, #FFD700, #FFA500);
  555. color: #333;
  556. padding: 6rpx 16rpx;
  557. border-radius: 20rpx;
  558. font-size: 20rpx;
  559. font-weight: 600;
  560. }
  561. }
  562. .user-details {
  563. flex: 1;
  564. color: #fff;
  565. .username {
  566. font-size: 36rpx;
  567. font-weight: 600;
  568. margin-bottom: 16rpx;
  569. }
  570. .user-tags {
  571. display: flex;
  572. flex-wrap: wrap;
  573. gap: 12rpx;
  574. margin-bottom: 16rpx;
  575. .tag {
  576. display: flex;
  577. align-items: center;
  578. background: rgba(255, 255, 255, 0.2);
  579. padding: 8rpx 16rpx;
  580. border-radius: 20rpx;
  581. font-size: 22rpx;
  582. text {
  583. margin-left: 6rpx;
  584. }
  585. }
  586. }
  587. .school-info {
  588. font-size: 24rpx;
  589. opacity: 0.9;
  590. text {
  591. display: block;
  592. margin-bottom: 6rpx;
  593. }
  594. }
  595. }
  596. }
  597. .stats-row {
  598. position: relative;
  599. z-index: 2;
  600. display: flex;
  601. justify-content: space-around;
  602. margin-bottom: 40rpx;
  603. .stat-item {
  604. text-align: center;
  605. color: #fff;
  606. .stat-number {
  607. font-size: 40rpx;
  608. font-weight: 600;
  609. margin-bottom: 8rpx;
  610. }
  611. .stat-label {
  612. font-size: 24rpx;
  613. opacity: 0.8;
  614. }
  615. }
  616. }
  617. .action-buttons {
  618. position: relative;
  619. z-index: 2;
  620. display: flex;
  621. gap: 20rpx;
  622. button {
  623. flex: 1;
  624. display: flex;
  625. align-items: center;
  626. justify-content: center;
  627. padding: 20rpx 0;
  628. border-radius: 50rpx;
  629. font-size: 28rpx;
  630. border: none;
  631. text {
  632. margin-left: 8rpx;
  633. }
  634. }
  635. .follow-btn {
  636. background: #fff;
  637. color: #333;
  638. &.followed {
  639. background: rgba(255, 255, 255, 0.3);
  640. color: #fff;
  641. }
  642. }
  643. .message-btn {
  644. background: rgba(255, 255, 255, 0.3);
  645. color: #fff;
  646. }
  647. }
  648. }
  649. .content-tabs {
  650. background: #fff;
  651. border-bottom: 1rpx solid #eee;
  652. padding: 0 20rpx;
  653. }
  654. .content-container {
  655. min-height: 400rpx;
  656. .posts-content,
  657. .renting-content,
  658. .work-content,
  659. .shop-content {
  660. padding: 20rpx 0;
  661. }
  662. }
  663. .empty-state {
  664. padding: 100rpx 0;
  665. text-align: center;
  666. }
  667. .loading-state {
  668. display: flex;
  669. flex-direction: column;
  670. align-items: center;
  671. padding: 60rpx 0;
  672. text {
  673. margin-top: 20rpx;
  674. font-size: 28rpx;
  675. color: #999;
  676. }
  677. }
  678. .load-more-state {
  679. display: flex;
  680. flex-direction: column;
  681. align-items: center;
  682. padding: 40rpx 0;
  683. text {
  684. margin-top: 15rpx;
  685. font-size: 24rpx;
  686. color: #666;
  687. }
  688. }
  689. .no-more-state {
  690. padding: 30rpx 0;
  691. text-align: center;
  692. text {
  693. font-size: 24rpx;
  694. color: #ccc;
  695. }
  696. }
  697. </style>