index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. <template>
  2. <view class="today-recommend-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. <!-- 页面内容 -->
  12. <view class="page-content">
  13. <!-- 推荐用户卡片 -->
  14. <view class="recommend-list">
  15. <view class="recommend-card" v-for="(user, index) in recommendUsers" :key="index">
  16. <view class="card-header">
  17. <image class="user-avatar" :src="user.avatar" mode="aspectFill"></image>
  18. <view class="user-basic-info">
  19. <view class="user-name-row">
  20. <text class="user-name">{{ user.nickname }}</text>
  21. <view class="vip-badge" v-if="user.isVip">VIP</view>
  22. </view>
  23. <view class="user-details">
  24. <text class="user-detail" v-if="user.age && user.age !== '未知'">{{ user.age }}岁</text>
  25. <text class="user-detail" v-if="user.height">{{ user.height }}cm</text>
  26. <text class="user-detail" v-if="user.location && user.location !== '未填写'">{{ user.location }}</text>
  27. </view>
  28. </view>
  29. </view>
  30. <view class="card-content">
  31. <view class="user-info-grid">
  32. <view class="info-item" v-if="user.job && user.job !== '未填写'">
  33. <text class="info-label">职业</text>
  34. <text class="info-value">{{ user.job }}</text>
  35. </view>
  36. <view class="info-item" v-if="user.education && user.education !== '未填写'">
  37. <text class="info-label">学历</text>
  38. <text class="info-value">{{ user.education }}</text>
  39. </view>
  40. <view class="info-item" v-if="user.constellation && user.constellation !== '未填写'">
  41. <text class="info-label">星座</text>
  42. <text class="info-value">{{ user.constellation }}</text>
  43. </view>
  44. <view class="info-item" v-if="user.salary && user.salary !== '未填写'">
  45. <text class="info-label">月收入</text>
  46. <text class="info-value">{{ user.salary }}</text>
  47. </view>
  48. </view>
  49. <view class="user-tags">
  50. <text class="tag" v-for="(tag, tagIndex) in user.hobbies" :key="tagIndex">{{ tag }}</text>
  51. </view>
  52. <view class="personal-intro" v-if="user.introduction">
  53. <text class="intro-label">个人简介:</text>
  54. <text class="intro-text">{{ user.introduction }}</text>
  55. </view>
  56. </view>
  57. <view class="card-actions">
  58. <!-- <view class="action-btn pass-btn" @click="handlePass(user, index)">-->
  59. <!-- <text class="action-icon">👎</text>-->
  60. <!-- <text class="action-text">不感兴趣</text>-->
  61. <!-- </view>-->
  62. <view class="action-btn like-btn" @click="handleLike(user, index)">
  63. <text class="action-icon">❤️</text>
  64. <text class="action-text">喜欢</text>
  65. </view>
  66. <!-- <view class="action-btn chat-btn" @click="handleChat(user)">
  67. <text class="action-icon">💬</text>
  68. <text class="action-text">打招呼</text>
  69. </view> -->
  70. </view>
  71. </view>
  72. </view>
  73. <!-- 暂无推荐 -->
  74. <view class="no-recommend" v-if="recommendUsers.length === 0">
  75. <text class="no-recommend-icon">💭</text>
  76. <text class="no-recommend-title">今日暂无新推荐</text>
  77. <text class="no-recommend-tip">明天再来看看吧~</text>
  78. <view class="complete-profile-btn" @click="goToProfile">
  79. <text class="btn-text">完善资料获得更多推荐</text>
  80. </view>
  81. </view>
  82. <!-- 底部占位 -->
  83. <view class="bottom-placeholder"></view>
  84. </view>
  85. </view>
  86. </template>
  87. <script>
  88. import userAuth from '@/utils/userAuth.js'
  89. import api from '@/utils/api.js'
  90. import { DEFAULT_IMAGES, EDUCATION_TEXT, SALARY_RANGE_TEXT } from '@/config/index.js'
  91. export default {
  92. data() {
  93. return {
  94. currentUserId: null,
  95. currentUserGender: null,
  96. // 推荐用户数据
  97. recommendUsers: []
  98. }
  99. },
  100. onLoad() {
  101. this.currentUserId = userAuth.getUserId()
  102. // 加载今日推荐
  103. this.loadTodayRecommend()
  104. },
  105. methods: {
  106. // 加载今日推荐
  107. async loadTodayRecommend() {
  108. try {
  109. uni.showLoading({
  110. title: '加载推荐中...'
  111. })
  112. const userId = this.currentUserId || parseInt(uni.getStorageSync('userId'))
  113. if (!userId) {
  114. uni.hideLoading()
  115. uni.showToast({
  116. title: '请先登录',
  117. icon: 'none'
  118. })
  119. return
  120. }
  121. await this.loadCurrentUserInfo()
  122. const data = await api.recommend.getUsers({
  123. userId,
  124. oppoOnly: 1,
  125. limit: 50
  126. })
  127. uni.hideLoading()
  128. if (data && data.length > 0) {
  129. const sortedUsers = data.filter(u => u.userId !== userId && u.compatibilityScore > 0 && (!this.currentUserGender || u.gender !== this.currentUserGender)).sort((a, b) => (b.compatibilityScore || 0) - (a.compatibilityScore || 0)).slice(0, 10)
  130. this.recommendUsers = sortedUsers.map(u => this.formatUserData(u))
  131. if (this.recommendUsers.length > 0) uni.showToast({
  132. title: `为您推荐了${this.recommendUsers.length}位高匹配度会员`,
  133. icon: 'success'
  134. })
  135. else uni.showToast({
  136. title: '暂无推荐用户',
  137. icon: 'none'
  138. })
  139. } else uni.showToast({
  140. title: '暂无推荐用户',
  141. icon: 'none'
  142. })
  143. } catch (e) {
  144. uni.hideLoading()
  145. console.error('获取推荐用户失败:', e)
  146. uni.showToast({
  147. title: '加载失败,请重试',
  148. icon: 'none'
  149. })
  150. }
  151. },
  152. // 加载当前用户信息
  153. async loadCurrentUserInfo() {
  154. try {
  155. const s = uni.getStorageSync('userInfo')
  156. if (s && s.gender !== undefined) this.currentUserGender = parseInt(s.gender)
  157. if (this.currentUserId) {
  158. const u = await api.user.getDetailInfo(this.currentUserId)
  159. if (u && u.gender !== undefined) this.currentUserGender = parseInt(u.gender)
  160. }
  161. } catch (e) {
  162. console.error('加载失败:', e)
  163. }
  164. },
  165. // 格式化用户数据
  166. formatUserData(u) {
  167. return {
  168. ...u,
  169. id: u.userId || u.id,
  170. avatar: this.validateImageUrl(u.avatarUrl || u.avatar),
  171. nickname: u.nickname || '用户',
  172. age: this.calculateAge(u.birthDate || u.birth_date) || u.age || '未知',
  173. height: u.height || null,
  174. location: this.formatLocation(u),
  175. job: u.jobTitle || u.job || '未填写',
  176. education: this.formatEducation(u.education || u.education_lev || u.educationLevel),
  177. constellation: u.star || u.constellation || '未填写',
  178. salary: this.formatSalary(u.salaryRange),
  179. hobbies: this.parseHobbies(u.hobby),
  180. introduction: u.introduction || u.selfIntro || '',
  181. isVip: u.isVip || false,
  182. compatibilityScore: u.compatibilityScore || 0
  183. }
  184. },
  185. // 计算年龄
  186. calculateAge(d) {
  187. if (!d) return null
  188. try {
  189. const b = new Date(d),
  190. t = new Date()
  191. let a = t.getFullYear() - b.getFullYear()
  192. const m = t.getMonth() - b.getMonth()
  193. if (m < 0 || (m === 0 && t.getDate() < b.getDate())) a--
  194. return a > 0 && a < 120 ? a : null
  195. } catch (e) {
  196. return null
  197. }
  198. },
  199. // 格式化位置
  200. formatLocation(u) {
  201. const p = []
  202. if (u.provinceName) p.push(u.provinceName)
  203. if (u.cityName) p.push(u.cityName)
  204. if (p.length === 0 && u.location) return u.location
  205. if (p.length === 0 && u.city) return u.city
  206. return p.join(' ') || '未填写'
  207. },
  208. // 格式化学历
  209. formatEducation(e) {
  210. if (!e) return '未填写'
  211. return EDUCATION_TEXT[e] || e
  212. },
  213. // 格式化月收入
  214. formatSalary(s) {
  215. if (!s) return '未填写'
  216. return SALARY_RANGE_TEXT[s] || s
  217. },
  218. // 解析兴趣爱好
  219. parseHobbies(h) {
  220. if (!h) return []
  221. try {
  222. const hs = typeof h === 'string' ? JSON.parse(h) : h
  223. return Array.isArray(hs) ? hs.slice(0, 5) : []
  224. } catch (e) {
  225. return []
  226. }
  227. },
  228. // 验证图片地址
  229. validateImageUrl(url) {
  230. if (!url || url === 'null' || url === 'undefined') return DEFAULT_IMAGES.avatar
  231. let c = String(url).trim()
  232. const i = c.indexOf('http')
  233. if (i > 0) c = c.slice(i)
  234. if (!c) return DEFAULT_IMAGES.avatar
  235. if (c.startsWith('/')) return `http://115.190.125.125:9000${c}`
  236. if (c.startsWith('http://') || c.startsWith('https://')) return c
  237. return DEFAULT_IMAGES.avatar
  238. },
  239. // 格式化匹配度
  240. formatMatchScore(s) {
  241. if (!s) return '0.0'
  242. return Number(s).toFixed(1)
  243. },
  244. // 处理不感兴趣
  245. // async handlePass(user, index) {
  246. // uni.showModal({
  247. // title: '确认操作',
  248. // content: `确定对${user.nickname}不感兴趣吗?`,
  249. // success: async (res) => {
  250. // if (res.confirm) {
  251. // try {
  252. // await api.recommend.feedback({
  253. // userId: this.currentUserId,
  254. // targetUserId: user.userId || user.id,
  255. // type: 'dislike'
  256. // })
  257. // uni.showToast({
  258. // title: '已记录您的偏好',
  259. // icon: 'success'
  260. // })
  261. // this.recommendUsers.splice(index, 1)
  262. // } catch (e) {
  263. // console.error('失败:', e)
  264. // uni.showToast({
  265. // title: '操作失败,请重试',
  266. // icon: 'none'
  267. // })
  268. // }
  269. // }
  270. // }
  271. // })
  272. // },
  273. // 处理喜欢
  274. async handleLike(user, index) {
  275. try {
  276. await api.recommend.feedback({
  277. userId: this.currentUserId,
  278. targetUserId: user.userId || user.id,
  279. type: 'like'
  280. })
  281. } catch (e) {
  282. console.error('失败:', e)
  283. }
  284. uni.showToast({
  285. title: `已喜欢${user.nickname} ❤️`,
  286. icon: 'success'
  287. })
  288. setTimeout(() => {
  289. this.recommendUsers.splice(index, 1)
  290. }, 800)
  291. },
  292. // 处理打招呼
  293. handleChat(user) {
  294. const greetings = [
  295. '你好,很高兴认识你!',
  296. 'Hi,看了你的资料很不错呢~',
  297. '你好,我们聊聊天吧!',
  298. 'Hello,可以交个朋友吗?'
  299. ]
  300. const randomGreeting = greetings[Math.floor(Math.random() * greetings.length)]
  301. uni.showModal({
  302. title: `向${user.nickname}打招呼`,
  303. content: `发送消息:"${randomGreeting}"`,
  304. showCancel: true,
  305. cancelText: '自定义',
  306. confirmText: '发送',
  307. success: (res) => {
  308. if (res.confirm) {
  309. uni.navigateTo({
  310. url: `/pages/message/chat?targetUserId=${user.userId||user.id}&targetUserName=${encodeURIComponent(user.nickname)}&message=${encodeURIComponent(randomGreeting)}`
  311. })
  312. } else {
  313. uni.navigateTo({
  314. url: `/pages/message/chat?targetUserId=${user.userId||user.id}&targetUserName=${encodeURIComponent(user.nickname)}`
  315. })
  316. }
  317. }
  318. })
  319. },
  320. // 跳转到个人资料
  321. goToProfile() {
  322. uni.navigateTo({
  323. url: '/pages/profile/index'
  324. })
  325. },
  326. // 返回
  327. goBack() {
  328. uni.navigateBack({
  329. fail: () => {
  330. // 返回失败时跳转首页
  331. uni.navigateTo({
  332. url: '/pages/index/index'
  333. })
  334. }
  335. })
  336. }
  337. }
  338. }
  339. </script>
  340. <style lang="scss" scoped>
  341. .today-recommend-page {
  342. min-height: 100vh;
  343. background: linear-gradient(180deg, #FFE5EE 0%, #FFF9F9 100%);
  344. padding-top: 90rpx;
  345. padding-bottom: 40rpx;
  346. }
  347. /* 自定义导航栏 */
  348. .custom-navbar {
  349. position: fixed;
  350. top: 0;
  351. left: 0;
  352. right: 0;
  353. height: 90rpx;
  354. display: flex;
  355. align-items: center;
  356. justify-content: space-between;
  357. padding: 0 20rpx;
  358. background: linear-gradient(135deg, #FF6B9D 0%, #FF8FAB 100%);
  359. z-index: 999;
  360. .navbar-left,
  361. .navbar-right {
  362. width: 80rpx;
  363. }
  364. .back-icon {
  365. font-size: 40rpx;
  366. color: #FFFFFF;
  367. font-weight: bold;
  368. }
  369. .navbar-title {
  370. flex: 1;
  371. text-align: center;
  372. font-size: 32rpx;
  373. font-weight: bold;
  374. color: #FFFFFF;
  375. }
  376. }
  377. /* 页面内容 */
  378. .page-content {
  379. padding: 20rpx 30rpx;
  380. }
  381. /* 推荐列表 */
  382. .recommend-list {
  383. .recommend-card {
  384. background: #FFFFFF;
  385. border-radius: 25rpx;
  386. margin-bottom: 30rpx;
  387. overflow: hidden;
  388. box-shadow: 0 8rpx 25rpx rgba(255, 107, 157, 0.15);
  389. border: 2rpx solid rgba(255, 107, 157, 0.1);
  390. .card-header {
  391. padding: 30rpx;
  392. display: flex;
  393. align-items: center;
  394. position: relative;
  395. background: linear-gradient(135deg, #FFF9FC 0%, #FFFFFF 100%);
  396. .user-avatar {
  397. width: 120rpx;
  398. height: 120rpx;
  399. border-radius: 60rpx;
  400. margin-right: 25rpx;
  401. border: 4rpx solid #FFE5EE;
  402. box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.2);
  403. }
  404. .user-basic-info {
  405. flex: 1;
  406. .user-name-row {
  407. display: flex;
  408. align-items: center;
  409. margin-bottom: 15rpx;
  410. .user-name {
  411. font-size: 34rpx;
  412. font-weight: bold;
  413. color: #333333;
  414. margin-right: 15rpx;
  415. }
  416. .vip-badge {
  417. background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
  418. color: #FFFFFF;
  419. font-size: 20rpx;
  420. font-weight: bold;
  421. padding: 6rpx 15rpx;
  422. border-radius: 20rpx;
  423. box-shadow: 0 2rpx 8rpx rgba(255, 165, 0, 0.3);
  424. }
  425. }
  426. .user-details {
  427. display: flex;
  428. gap: 20rpx;
  429. .user-detail {
  430. font-size: 26rpx;
  431. color: #666666;
  432. background: rgba(255, 107, 157, 0.1);
  433. padding: 8rpx 16rpx;
  434. border-radius: 15rpx;
  435. }
  436. }
  437. }
  438. .online-status {
  439. position: absolute;
  440. top: 20rpx;
  441. right: 20rpx;
  442. display: flex;
  443. align-items: center;
  444. gap: 8rpx;
  445. .status-dot {
  446. width: 16rpx;
  447. height: 16rpx;
  448. border-radius: 50%;
  449. background: #CCCCCC;
  450. }
  451. .status-text {
  452. font-size: 22rpx;
  453. color: #999999;
  454. }
  455. &.online {
  456. .status-dot {
  457. background: #4CAF50;
  458. animation: blink 2s infinite;
  459. }
  460. .status-text {
  461. color: #4CAF50;
  462. }
  463. }
  464. }
  465. }
  466. .card-content {
  467. padding: 0 30rpx 20rpx;
  468. .user-info-grid {
  469. display: grid;
  470. grid-template-columns: 1fr 1fr;
  471. gap: 20rpx;
  472. margin-bottom: 25rpx;
  473. .info-item {
  474. background: #F8F9FA;
  475. padding: 20rpx;
  476. border-radius: 15rpx;
  477. .info-label {
  478. font-size: 22rpx;
  479. color: #999999;
  480. display: block;
  481. margin-bottom: 8rpx;
  482. }
  483. .info-value {
  484. font-size: 26rpx;
  485. color: #333333;
  486. font-weight: 500;
  487. }
  488. }
  489. }
  490. .user-tags {
  491. display: flex;
  492. flex-wrap: wrap;
  493. gap: 12rpx;
  494. margin-bottom: 25rpx;
  495. .tag {
  496. background: linear-gradient(135deg, #FFE5EE 0%, #FFD0DC 100%);
  497. color: #FF6B8A;
  498. font-size: 22rpx;
  499. padding: 10rpx 18rpx;
  500. border-radius: 20rpx;
  501. font-weight: 500;
  502. }
  503. }
  504. .personal-intro {
  505. background: rgba(255, 107, 157, 0.05);
  506. padding: 20rpx;
  507. border-radius: 15rpx;
  508. border-left: 4rpx solid #FF6B9D;
  509. .intro-label {
  510. font-size: 24rpx;
  511. color: #FF6B9D;
  512. font-weight: bold;
  513. display: block;
  514. margin-bottom: 10rpx;
  515. }
  516. .intro-text {
  517. font-size: 26rpx;
  518. color: #666666;
  519. line-height: 1.6;
  520. }
  521. }
  522. }
  523. .card-actions {
  524. display: flex;
  525. padding: 25rpx 30rpx 30rpx;
  526. gap: 15rpx;
  527. background: linear-gradient(135deg, #FAFAFA 0%, #F5F5F5 100%);
  528. .action-btn {
  529. flex: 1;
  530. display: flex;
  531. flex-direction: column;
  532. align-items: center;
  533. justify-content: center;
  534. padding: 25rpx 0;
  535. border-radius: 20rpx;
  536. font-weight: 500;
  537. transition: all 0.3s;
  538. .action-icon {
  539. font-size: 36rpx;
  540. margin-bottom: 8rpx;
  541. }
  542. .action-text {
  543. font-size: 24rpx;
  544. }
  545. &.pass-btn {
  546. background: #FFFFFF;
  547. color: #666666;
  548. border: 2rpx solid #E9ECEF;
  549. &:active {
  550. background: #F8F9FA;
  551. transform: scale(0.95);
  552. }
  553. }
  554. &.like-btn {
  555. background: linear-gradient(135deg, #FF6B9D 0%, #FF8FAB 100%);
  556. color: #FFFFFF;
  557. box-shadow: 0 6rpx 20rpx rgba(255, 107, 157, 0.3);
  558. &:active {
  559. transform: scale(0.95);
  560. }
  561. }
  562. &.chat-btn {
  563. background: linear-gradient(135deg, #4CAF50 0%, #66BB6A 100%);
  564. color: #FFFFFF;
  565. box-shadow: 0 6rpx 20rpx rgba(76, 175, 80, 0.3);
  566. &:active {
  567. transform: scale(0.95);
  568. }
  569. }
  570. }
  571. }
  572. }
  573. }
  574. @keyframes blink {
  575. 0%, 100% { opacity: 1; }
  576. 50% { opacity: 0.5; }
  577. }
  578. /* 暂无推荐 */
  579. .no-recommend {
  580. text-align: center;
  581. padding: 100rpx 40rpx;
  582. .no-recommend-icon {
  583. font-size: 120rpx;
  584. display: block;
  585. margin-bottom: 30rpx;
  586. }
  587. .no-recommend-title {
  588. font-size: 36rpx;
  589. color: #333333;
  590. font-weight: bold;
  591. display: block;
  592. margin-bottom: 15rpx;
  593. }
  594. .no-recommend-tip {
  595. font-size: 28rpx;
  596. color: #666666;
  597. display: block;
  598. margin-bottom: 50rpx;
  599. }
  600. .complete-profile-btn {
  601. background: linear-gradient(135deg, #FF6B9D 0%, #FF8FAB 100%);
  602. color: #FFFFFF;
  603. padding: 25rpx 45rpx;
  604. border-radius: 35rpx;
  605. box-shadow: 0 8rpx 20rpx rgba(255, 107, 157, 0.3);
  606. display: inline-block;
  607. .btn-text {
  608. font-size: 28rpx;
  609. font-weight: bold;
  610. }
  611. &:active {
  612. transform: scale(0.95);
  613. }
  614. }
  615. }
  616. .bottom-placeholder {
  617. height: 60rpx;
  618. }
  619. </style>