index.vue 18 KB

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