user-detail.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. <template>
  2. <view class="user-detail-page">
  3. <!-- 自定义导航栏 -->
  4. <view class="custom-navbar">
  5. <view class="navbar-left" @click="goBack">
  6. <text class="back-icon">←</text>
  7. </view>
  8. <view class="navbar-title">用户详情</view>
  9. <view class="navbar-right"></view>
  10. </view>
  11. <scroll-view scroll-y class="page-content" v-if="userInfo">
  12. <!-- 用户头像和基本信息 -->
  13. <view class="user-header">
  14. <image
  15. :src="userInfo.avatar || userInfo.avatarUrl || '/static/close.png'"
  16. class="user-avatar"
  17. mode="aspectFill"
  18. @error="onAvatarError"
  19. />
  20. <view class="user-basic">
  21. <view class="user-name-row">
  22. <text class="user-name">{{ userInfo.nickname || '未设置' }}</text>
  23. <view class="score-badge" v-if="compatibilityScore">
  24. <text>匹配度 {{ fmtScore(compatibilityScore) }}</text>
  25. </view>
  26. </view>
  27. <view class="user-meta">
  28. <text v-if="userInfo.genderText">{{ userInfo.genderText }}</text>
  29. <text v-if="userInfo.age">{{ userInfo.age }}岁</text>
  30. <text v-if="userInfo.height">{{ userInfo.height }}cm</text>
  31. <text v-if="userInfo.educationText">{{ userInfo.educationText }}</text>
  32. <text v-if="userInfo.salaryText">{{ userInfo.salaryText }}</text>
  33. </view>
  34. <view class="user-tags" v-if="hasTags">
  35. <text v-if="userInfo.star" class="tag">{{ userInfo.star }}</text>
  36. <text v-if="userInfo.animal" class="tag">{{ userInfo.animal }}</text>
  37. <text v-if="userInfo.jobTitle" class="tag">{{ userInfo.jobTitle }}</text>
  38. <text v-for="t in parseHobby(userInfo.hobby)" :key="t" class="tag">{{ t }}</text>
  39. </view>
  40. </view>
  41. </view>
  42. <!-- 详细信息 -->
  43. <view class="detail-section">
  44. <view class="section-title">详细信息</view>
  45. <view class="detail-grid">
  46. <view class="detail-item" v-if="userInfo.schoolName">
  47. <text class="detail-label">毕业院校</text>
  48. <text class="detail-value">{{ userInfo.schoolName }}</text>
  49. </view>
  50. <view class="detail-item" v-if="userInfo.company">
  51. <text class="detail-label">工作单位</text>
  52. <text class="detail-value">{{ userInfo.company }}</text>
  53. </view>
  54. <view class="detail-item" v-if="userInfo.maritalText">
  55. <text class="detail-label">婚姻状况</text>
  56. <text class="detail-value">{{ userInfo.maritalText }}</text>
  57. </view>
  58. <view class="detail-item" v-if="userInfo.houseText">
  59. <text class="detail-label">房产</text>
  60. <text class="detail-value">{{ userInfo.houseText }}</text>
  61. </view>
  62. <view class="detail-item" v-if="userInfo.carText">
  63. <text class="detail-label">车产</text>
  64. <text class="detail-value">{{ userInfo.carText }}</text>
  65. </view>
  66. <view class="detail-item" v-if="userInfo.weight">
  67. <text class="detail-label">体重</text>
  68. <text class="detail-value">{{ userInfo.weight }}kg</text>
  69. </view>
  70. </view>
  71. </view>
  72. <!-- 自我简介 -->
  73. <view class="detail-section" v-if="userInfo.introduction">
  74. <view class="section-title">自我简介</view>
  75. <view class="introduction-content">
  76. <text>{{ userInfo.introduction }}</text>
  77. </view>
  78. </view>
  79. <!-- 动态区域 -->
  80. <view class="dynamic-section">
  81. <view class="section-header">
  82. <text class="section-title">动态</text>
  83. <text class="section-more" @click="viewAllDynamics" v-if="dynamicList.length > 0">
  84. 查看全部({{ totalDynamics }}) >
  85. </text>
  86. </view>
  87. <!-- 动态列表 -->
  88. <view v-if="loadingDynamics" class="loading-tip">
  89. <text>加载动态中...</text>
  90. </view>
  91. <view v-else-if="dynamicList.length === 0" class="empty-dynamics">
  92. <text class="empty-icon">📝</text>
  93. <text class="empty-text">该用户还没有发布动态</text>
  94. </view>
  95. <view v-else class="dynamic-list">
  96. <view
  97. v-for="(item, index) in dynamicList"
  98. :key="item.dynamicId || index"
  99. class="dynamic-item"
  100. @click="viewDynamicDetail(item)"
  101. >
  102. <!-- 动态内容 -->
  103. <view class="dynamic-content" v-if="item.content">
  104. <text class="dynamic-text">{{ item.content }}</text>
  105. </view>
  106. <!-- 动态媒体(照片/视频) -->
  107. <view class="dynamic-media" v-if="item.mediaUrls && item.mediaUrls.length > 0">
  108. <view :class="['media-grid', getGridClass(item)]">
  109. <image
  110. v-for="(url, idx) in item.mediaUrls.slice(0, 9)"
  111. :key="idx"
  112. :src="url"
  113. class="media-image"
  114. mode="aspectFill"
  115. @click.stop="previewMedia(item.mediaUrls, idx)"
  116. />
  117. </view>
  118. </view>
  119. <!-- 动态信息 -->
  120. <view class="dynamic-info">
  121. <text class="dynamic-time">{{ formatTime(item.createdAt) }}</text>
  122. <view class="dynamic-stats">
  123. <text class="stat-item">❤️ {{ item.likeCount || 0 }}</text>
  124. <text class="stat-item comment-stat" @click.stop="viewDynamicComments(item)">💬 {{ item.commentCount || 0 }}</text>
  125. </view>
  126. </view>
  127. </view>
  128. </view>
  129. </view>
  130. </scroll-view>
  131. <!-- 加载中 -->
  132. <view v-else class="loading-container">
  133. <text>加载中...</text>
  134. </view>
  135. </view>
  136. </template>
  137. <script>
  138. import api from '@/utils/api.js'
  139. export default {
  140. data() {
  141. return {
  142. userId: null,
  143. userInfo: null,
  144. compatibilityScore: null,
  145. dynamicList: [],
  146. loadingDynamics: false,
  147. totalDynamics: 0,
  148. pageNum: 1,
  149. pageSize: 6
  150. }
  151. },
  152. computed: {
  153. hasTags() {
  154. return this.userInfo && (
  155. this.userInfo.star ||
  156. this.userInfo.animal ||
  157. this.userInfo.jobTitle ||
  158. this.userInfo.hobby
  159. )
  160. }
  161. },
  162. onLoad(options) {
  163. if (options.userId) {
  164. this.userId = parseInt(options.userId)
  165. if (options.score) {
  166. this.compatibilityScore = parseFloat(options.score)
  167. }
  168. this.loadUserInfo()
  169. this.loadUserDynamics()
  170. } else {
  171. uni.showToast({
  172. title: '用户ID无效',
  173. icon: 'none'
  174. })
  175. setTimeout(() => {
  176. uni.navigateBack()
  177. }, 1500)
  178. }
  179. },
  180. methods: {
  181. // 加载用户信息
  182. async loadUserInfo() {
  183. try {
  184. uni.showLoading({ title: '加载中...' })
  185. const detail = await api.user.getDetailInfo(this.userId)
  186. if (detail) {
  187. // 确保photos是数组
  188. if (!detail.photos || !Array.isArray(detail.photos)) {
  189. detail.photos = []
  190. if (detail.avatar) {
  191. detail.photos.push(detail.avatar)
  192. }
  193. }
  194. this.userInfo = detail
  195. } else {
  196. uni.showToast({
  197. title: '用户信息不存在',
  198. icon: 'none'
  199. })
  200. setTimeout(() => {
  201. uni.navigateBack()
  202. }, 1500)
  203. }
  204. } catch (e) {
  205. console.error('获取用户信息失败:', e)
  206. uni.showToast({
  207. title: '获取用户信息失败',
  208. icon: 'none'
  209. })
  210. } finally {
  211. uni.hideLoading()
  212. }
  213. },
  214. // 加载用户动态
  215. async loadUserDynamics() {
  216. if (!this.userId) return
  217. this.loadingDynamics = true
  218. try {
  219. const currentUserId = uni.getStorageSync('userId')
  220. const params = {
  221. pageNum: this.pageNum,
  222. pageSize: this.pageSize,
  223. currentUserId: currentUserId ? parseInt(currentUserId) : null
  224. }
  225. const result = await api.dynamic.getUserDynamics(this.userId, params)
  226. console.log('用户动态API返回:', result)
  227. // 处理PageResult格式:{ records: [], total: 0, current: 1, size: 10 }
  228. let list = []
  229. let total = 0
  230. if (result) {
  231. // PageResult格式:{ records: [], total: 0 }
  232. if (result.records && Array.isArray(result.records)) {
  233. list = result.records
  234. total = result.total || 0
  235. }
  236. // 兼容格式:{ list: [], total: 0 }
  237. else if (result.list && Array.isArray(result.list)) {
  238. list = result.list
  239. total = result.total || result.list.length
  240. }
  241. // 直接是数组
  242. else if (Array.isArray(result)) {
  243. list = result
  244. total = result.length
  245. }
  246. }
  247. // 处理mediaUrls字段(可能是字符串需要解析)
  248. list = list.map(item => {
  249. if (item.mediaUrls && typeof item.mediaUrls === 'string') {
  250. try {
  251. item.mediaUrls = JSON.parse(item.mediaUrls)
  252. } catch (e) {
  253. console.error('解析mediaUrls失败:', e)
  254. item.mediaUrls = []
  255. }
  256. }
  257. if (!item.mediaUrls || !Array.isArray(item.mediaUrls)) {
  258. item.mediaUrls = []
  259. }
  260. return item
  261. })
  262. this.dynamicList = list
  263. this.totalDynamics = total
  264. console.log('处理后的动态列表:', this.dynamicList)
  265. } catch (e) {
  266. console.error('获取用户动态失败:', e)
  267. uni.showToast({
  268. title: '加载动态失败',
  269. icon: 'none'
  270. })
  271. } finally {
  272. this.loadingDynamics = false
  273. }
  274. },
  275. // 查看全部动态
  276. viewAllDynamics() {
  277. uni.navigateTo({
  278. url: `/pages/recommend/user-dynamics?userId=${this.userId}`
  279. })
  280. },
  281. // 查看动态详情
  282. viewDynamicDetail(item) {
  283. if (!item || !item.dynamicId) {
  284. console.error('动态项无效:', item)
  285. uni.showToast({
  286. title: '动态信息无效',
  287. icon: 'none'
  288. })
  289. return
  290. }
  291. uni.navigateTo({
  292. url: `/pages/plaza/detail?dynamicId=${item.dynamicId}`
  293. })
  294. },
  295. // 查看动态评论
  296. viewDynamicComments(item) {
  297. if (!item || !item.dynamicId) {
  298. console.error('动态项无效:', item)
  299. uni.showToast({
  300. title: '动态信息无效',
  301. icon: 'none'
  302. })
  303. return
  304. }
  305. uni.navigateTo({
  306. url: `/pages/plaza/detail?dynamicId=${item.dynamicId}&scrollToComment=true`
  307. })
  308. },
  309. // 预览媒体
  310. previewMedia(urls, index) {
  311. if (!urls || urls.length === 0) return
  312. uni.previewImage({
  313. urls: urls,
  314. current: index
  315. })
  316. },
  317. // 获取网格类名
  318. getGridClass(item) {
  319. const count = item.mediaUrls ? item.mediaUrls.length : 0
  320. if (count === 1) return 'grid-1'
  321. if (count === 2 || count === 4) return 'grid-2'
  322. return 'grid-3'
  323. },
  324. // 格式化时间
  325. formatTime(timeStr) {
  326. if (!timeStr) return ''
  327. const date = new Date(timeStr)
  328. const now = new Date()
  329. const diff = now - date
  330. const minutes = Math.floor(diff / 60000)
  331. const hours = Math.floor(diff / 3600000)
  332. const days = Math.floor(diff / 86400000)
  333. if (minutes < 1) return '刚刚'
  334. if (minutes < 60) return `${minutes}分钟前`
  335. if (hours < 24) return `${hours}小时前`
  336. if (days < 7) return `${days}天前`
  337. return date.toLocaleDateString()
  338. },
  339. // 格式化匹配度
  340. fmtScore(s) {
  341. return s ? Number(s).toFixed(1) : '0.0'
  342. },
  343. // 解析兴趣爱好
  344. parseHobby(h) {
  345. try {
  346. const arr = typeof h === 'string' ? JSON.parse(h) : h
  347. return Array.isArray(arr) ? arr.slice(0, 6) : []
  348. } catch {
  349. return []
  350. }
  351. },
  352. // 头像加载错误
  353. onAvatarError() {
  354. if (this.userInfo) {
  355. this.userInfo.avatar = '/static/close.png'
  356. this.userInfo.avatarUrl = '/static/close.png'
  357. }
  358. },
  359. // 返回
  360. goBack() {
  361. uni.navigateBack()
  362. }
  363. }
  364. }
  365. </script>
  366. <style lang="scss" scoped>
  367. .user-detail-page {
  368. min-height: 100vh;
  369. background: #F5F5F5;
  370. padding-top: 90rpx;
  371. }
  372. /* 自定义导航栏 */
  373. .custom-navbar {
  374. position: fixed;
  375. top: 0;
  376. left: 0;
  377. right: 0;
  378. height: 90rpx;
  379. display: flex;
  380. align-items: center;
  381. justify-content: space-between;
  382. padding: 0 20rpx;
  383. background: #FFFFFF;
  384. border-bottom: 2rpx solid #E0E0E0;
  385. z-index: 999;
  386. }
  387. .navbar-left,
  388. .navbar-right {
  389. width: 80rpx;
  390. }
  391. .back-icon {
  392. font-size: 40rpx;
  393. color: #333;
  394. font-weight: bold;
  395. }
  396. .navbar-title {
  397. flex: 1;
  398. text-align: center;
  399. font-size: 32rpx;
  400. font-weight: bold;
  401. color: #333;
  402. }
  403. /* 页面内容 */
  404. .page-content {
  405. height: calc(100vh - 90rpx);
  406. padding: 20rpx;
  407. }
  408. /* 用户头部 */
  409. .user-header {
  410. background: #FFFFFF;
  411. border-radius: 16rpx;
  412. padding: 30rpx;
  413. margin-bottom: 20rpx;
  414. display: flex;
  415. align-items: flex-start;
  416. border: 2rpx solid #E0E0E0;
  417. }
  418. .user-avatar {
  419. width: 160rpx;
  420. height: 160rpx;
  421. border-radius: 80rpx;
  422. margin-right: 24rpx;
  423. border: 4rpx solid #FFE5F1;
  424. background: #F5F5F5;
  425. }
  426. .user-basic {
  427. flex: 1;
  428. }
  429. .user-name-row {
  430. display: flex;
  431. align-items: center;
  432. margin-bottom: 16rpx;
  433. flex-wrap: wrap;
  434. gap: 12rpx;
  435. }
  436. .user-name {
  437. font-size: 36rpx;
  438. font-weight: bold;
  439. color: #333;
  440. }
  441. .score-badge {
  442. background: #FFE5F1;
  443. color: #E91E63;
  444. padding: 6rpx 16rpx;
  445. border-radius: 20rpx;
  446. font-size: 24rpx;
  447. font-weight: 600;
  448. }
  449. .user-meta {
  450. display: flex;
  451. flex-wrap: wrap;
  452. gap: 16rpx;
  453. margin-bottom: 16rpx;
  454. font-size: 26rpx;
  455. color: #666;
  456. }
  457. .user-tags {
  458. display: flex;
  459. flex-wrap: wrap;
  460. gap: 12rpx;
  461. }
  462. .tag {
  463. background: #F5F5F5;
  464. color: #666;
  465. padding: 8rpx 16rpx;
  466. border-radius: 20rpx;
  467. font-size: 24rpx;
  468. border: 1rpx solid #E0E0E0;
  469. }
  470. /* 详细信息区域 */
  471. .detail-section {
  472. background: #FFFFFF;
  473. border-radius: 16rpx;
  474. padding: 24rpx;
  475. margin-bottom: 20rpx;
  476. border: 2rpx solid #E0E0E0;
  477. }
  478. .section-title {
  479. font-size: 28rpx;
  480. font-weight: 600;
  481. color: #333;
  482. margin-bottom: 20rpx;
  483. padding-bottom: 12rpx;
  484. border-bottom: 2rpx solid #F0F0F0;
  485. }
  486. .detail-grid {
  487. display: grid;
  488. grid-template-columns: repeat(2, 1fr);
  489. gap: 20rpx;
  490. }
  491. .detail-item {
  492. display: flex;
  493. flex-direction: column;
  494. }
  495. .detail-label {
  496. font-size: 24rpx;
  497. color: #999;
  498. margin-bottom: 8rpx;
  499. }
  500. .detail-value {
  501. font-size: 26rpx;
  502. color: #333;
  503. font-weight: 500;
  504. }
  505. .introduction-content {
  506. background: #F8F9FA;
  507. padding: 20rpx;
  508. border-radius: 8rpx;
  509. border-left: 4rpx solid #E91E63;
  510. line-height: 1.8;
  511. font-size: 26rpx;
  512. color: #666;
  513. }
  514. /* 动态区域 */
  515. .dynamic-section {
  516. background: #FFFFFF;
  517. border-radius: 16rpx;
  518. padding: 24rpx;
  519. margin-bottom: 20rpx;
  520. border: 2rpx solid #E0E0E0;
  521. }
  522. .section-header {
  523. display: flex;
  524. align-items: center;
  525. justify-content: space-between;
  526. margin-bottom: 20rpx;
  527. padding-bottom: 12rpx;
  528. border-bottom: 2rpx solid #F0F0F0;
  529. }
  530. .section-more {
  531. font-size: 24rpx;
  532. color: #E91E63;
  533. }
  534. .dynamic-list {
  535. display: flex;
  536. flex-direction: column;
  537. gap: 20rpx;
  538. }
  539. .dynamic-item {
  540. padding: 20rpx;
  541. background: #F8F9FA;
  542. border-radius: 12rpx;
  543. border: 1rpx solid #E0E0E0;
  544. }
  545. .dynamic-content {
  546. margin-bottom: 16rpx;
  547. }
  548. .dynamic-text {
  549. font-size: 26rpx;
  550. color: #333;
  551. line-height: 1.6;
  552. display: -webkit-box;
  553. -webkit-box-orient: vertical;
  554. -webkit-line-clamp: 3;
  555. overflow: hidden;
  556. }
  557. .dynamic-media {
  558. margin-bottom: 16rpx;
  559. }
  560. .media-grid {
  561. display: grid;
  562. gap: 8rpx;
  563. }
  564. .grid-1 {
  565. grid-template-columns: 1fr;
  566. }
  567. .grid-2 {
  568. grid-template-columns: repeat(2, 1fr);
  569. }
  570. .grid-3 {
  571. grid-template-columns: repeat(3, 1fr);
  572. }
  573. .media-image {
  574. width: 100%;
  575. height: 200rpx;
  576. border-radius: 8rpx;
  577. background: #F5F5F5;
  578. }
  579. .dynamic-info {
  580. display: flex;
  581. justify-content: space-between;
  582. align-items: center;
  583. font-size: 24rpx;
  584. color: #999;
  585. }
  586. .dynamic-stats {
  587. display: flex;
  588. gap: 20rpx;
  589. }
  590. .stat-item {
  591. font-size: 24rpx;
  592. color: #999;
  593. }
  594. .comment-stat {
  595. cursor: pointer;
  596. transition: opacity 0.2s;
  597. }
  598. .comment-stat:active {
  599. opacity: 0.6;
  600. }
  601. .empty-dynamics {
  602. text-align: center;
  603. padding: 60rpx 20rpx;
  604. }
  605. .empty-icon {
  606. font-size: 80rpx;
  607. display: block;
  608. margin-bottom: 20rpx;
  609. }
  610. .empty-text {
  611. font-size: 26rpx;
  612. color: #999;
  613. }
  614. .loading-tip {
  615. text-align: center;
  616. padding: 40rpx;
  617. color: #999;
  618. font-size: 26rpx;
  619. }
  620. .loading-container {
  621. display: flex;
  622. align-items: center;
  623. justify-content: center;
  624. height: 100vh;
  625. font-size: 28rpx;
  626. color: #999;
  627. }
  628. </style>