index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  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. // TODO: 跳转到聊天页面
  309. uni.showToast({
  310. title: '聊天功能开发中',
  311. icon: 'none'
  312. })
  313. }
  314. }
  315. })
  316. }, 1000)
  317. }
  318. }, 800)
  319. },
  320. // 处理打招呼
  321. handleChat(user) {
  322. console.log('打招呼:', user.nickname)
  323. const greetings = [
  324. '你好,很高兴认识你!',
  325. 'Hi,看了你的资料很不错呢~',
  326. '你好,我们聊聊天吧!',
  327. 'Hello,可以交个朋友吗?'
  328. ]
  329. const randomGreeting = greetings[Math.floor(Math.random() * greetings.length)]
  330. uni.showModal({
  331. title: `向${user.nickname}打招呼`,
  332. content: `发送消息:"${randomGreeting}"`,
  333. showCancel: true,
  334. cancelText: '自定义',
  335. confirmText: '发送',
  336. success: (res) => {
  337. if (res.confirm) {
  338. uni.navigateTo({
  339. url: `/pages/message/chat?targetUserId=${user.userId||user.id}&targetUserName=${encodeURIComponent(user.nickname)}&message=${encodeURIComponent(randomGreeting)}`
  340. })
  341. } else {
  342. uni.navigateTo({
  343. url: `/pages/message/chat?targetUserId=${user.userId||user.id}&targetUserName=${encodeURIComponent(user.nickname)}`
  344. })
  345. }
  346. }
  347. })
  348. },
  349. // 跳转到个人资料
  350. goToProfile() {
  351. uni.navigateTo({
  352. url: '/pages/profile/index'
  353. })
  354. },
  355. // 返回
  356. goBack() {
  357. uni.navigateBack({
  358. fail: () => {
  359. // 返回失败时跳转首页
  360. uni.navigateTo({
  361. url: '/pages/index/index'
  362. })
  363. }
  364. })
  365. }
  366. }
  367. }
  368. </script>
  369. <style lang="scss" scoped>
  370. .today-recommend-page {
  371. min-height: 100vh;
  372. background: linear-gradient(180deg, #FFE5EE 0%, #FFF9F9 100%);
  373. padding-top: 90rpx;
  374. padding-bottom: 40rpx;
  375. }
  376. /* 自定义导航栏 */
  377. .custom-navbar {
  378. position: fixed;
  379. top: 0;
  380. left: 0;
  381. right: 0;
  382. height: 90rpx;
  383. display: flex;
  384. align-items: center;
  385. justify-content: space-between;
  386. padding: 0 20rpx;
  387. background: linear-gradient(135deg, #FF6B9D 0%, #FF8FAB 100%);
  388. z-index: 999;
  389. .navbar-left,
  390. .navbar-right {
  391. width: 80rpx;
  392. }
  393. .back-icon {
  394. font-size: 40rpx;
  395. color: #FFFFFF;
  396. font-weight: bold;
  397. }
  398. .navbar-title {
  399. flex: 1;
  400. text-align: center;
  401. font-size: 32rpx;
  402. font-weight: bold;
  403. color: #FFFFFF;
  404. }
  405. }
  406. /* 页面内容 */
  407. .page-content {
  408. padding: 20rpx 30rpx;
  409. }
  410. /* 推荐列表 */
  411. .recommend-list {
  412. .recommend-card {
  413. background: #FFFFFF;
  414. border-radius: 25rpx;
  415. margin-bottom: 30rpx;
  416. overflow: hidden;
  417. box-shadow: 0 8rpx 25rpx rgba(255, 107, 157, 0.15);
  418. border: 2rpx solid rgba(255, 107, 157, 0.1);
  419. .card-header {
  420. padding: 30rpx;
  421. display: flex;
  422. align-items: center;
  423. position: relative;
  424. background: linear-gradient(135deg, #FFF9FC 0%, #FFFFFF 100%);
  425. .user-avatar {
  426. width: 120rpx;
  427. height: 120rpx;
  428. border-radius: 60rpx;
  429. margin-right: 25rpx;
  430. border: 4rpx solid #FFE5EE;
  431. box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.2);
  432. }
  433. .user-basic-info {
  434. flex: 1;
  435. .user-name-row {
  436. display: flex;
  437. align-items: center;
  438. margin-bottom: 15rpx;
  439. .user-name {
  440. font-size: 34rpx;
  441. font-weight: bold;
  442. color: #333333;
  443. margin-right: 15rpx;
  444. }
  445. .vip-badge {
  446. background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
  447. color: #FFFFFF;
  448. font-size: 20rpx;
  449. font-weight: bold;
  450. padding: 6rpx 15rpx;
  451. border-radius: 20rpx;
  452. box-shadow: 0 2rpx 8rpx rgba(255, 165, 0, 0.3);
  453. }
  454. }
  455. .user-details {
  456. display: flex;
  457. gap: 20rpx;
  458. .user-detail {
  459. font-size: 26rpx;
  460. color: #666666;
  461. background: rgba(255, 107, 157, 0.1);
  462. padding: 8rpx 16rpx;
  463. border-radius: 15rpx;
  464. }
  465. }
  466. }
  467. .online-status {
  468. position: absolute;
  469. top: 20rpx;
  470. right: 20rpx;
  471. display: flex;
  472. align-items: center;
  473. gap: 8rpx;
  474. .status-dot {
  475. width: 16rpx;
  476. height: 16rpx;
  477. border-radius: 50%;
  478. background: #CCCCCC;
  479. }
  480. .status-text {
  481. font-size: 22rpx;
  482. color: #999999;
  483. }
  484. &.online {
  485. .status-dot {
  486. background: #4CAF50;
  487. animation: blink 2s infinite;
  488. }
  489. .status-text {
  490. color: #4CAF50;
  491. }
  492. }
  493. }
  494. }
  495. .card-content {
  496. padding: 0 30rpx 20rpx;
  497. .user-info-grid {
  498. display: grid;
  499. grid-template-columns: 1fr 1fr;
  500. gap: 20rpx;
  501. margin-bottom: 25rpx;
  502. .info-item {
  503. background: #F8F9FA;
  504. padding: 20rpx;
  505. border-radius: 15rpx;
  506. .info-label {
  507. font-size: 22rpx;
  508. color: #999999;
  509. display: block;
  510. margin-bottom: 8rpx;
  511. }
  512. .info-value {
  513. font-size: 26rpx;
  514. color: #333333;
  515. font-weight: 500;
  516. }
  517. }
  518. }
  519. .user-tags {
  520. display: flex;
  521. flex-wrap: wrap;
  522. gap: 12rpx;
  523. margin-bottom: 25rpx;
  524. .tag {
  525. background: linear-gradient(135deg, #FFE5EE 0%, #FFD0DC 100%);
  526. color: #FF6B8A;
  527. font-size: 22rpx;
  528. padding: 10rpx 18rpx;
  529. border-radius: 20rpx;
  530. font-weight: 500;
  531. }
  532. }
  533. .personal-intro {
  534. background: rgba(255, 107, 157, 0.05);
  535. padding: 20rpx;
  536. border-radius: 15rpx;
  537. border-left: 4rpx solid #FF6B9D;
  538. .intro-label {
  539. font-size: 24rpx;
  540. color: #FF6B9D;
  541. font-weight: bold;
  542. display: block;
  543. margin-bottom: 10rpx;
  544. }
  545. .intro-text {
  546. font-size: 26rpx;
  547. color: #666666;
  548. line-height: 1.6;
  549. }
  550. }
  551. }
  552. .card-actions {
  553. display: flex;
  554. padding: 25rpx 30rpx 30rpx;
  555. gap: 15rpx;
  556. background: linear-gradient(135deg, #FAFAFA 0%, #F5F5F5 100%);
  557. .action-btn {
  558. flex: 1;
  559. display: flex;
  560. flex-direction: column;
  561. align-items: center;
  562. justify-content: center;
  563. padding: 25rpx 0;
  564. border-radius: 20rpx;
  565. font-weight: 500;
  566. transition: all 0.3s;
  567. .action-icon {
  568. font-size: 36rpx;
  569. margin-bottom: 8rpx;
  570. }
  571. .action-text {
  572. font-size: 24rpx;
  573. }
  574. &.pass-btn {
  575. background: #FFFFFF;
  576. color: #666666;
  577. border: 2rpx solid #E9ECEF;
  578. &:active {
  579. background: #F8F9FA;
  580. transform: scale(0.95);
  581. }
  582. }
  583. &.like-btn {
  584. background: linear-gradient(135deg, #FF6B9D 0%, #FF8FAB 100%);
  585. color: #FFFFFF;
  586. box-shadow: 0 6rpx 20rpx rgba(255, 107, 157, 0.3);
  587. &:active {
  588. transform: scale(0.95);
  589. }
  590. }
  591. &.chat-btn {
  592. background: linear-gradient(135deg, #4CAF50 0%, #66BB6A 100%);
  593. color: #FFFFFF;
  594. box-shadow: 0 6rpx 20rpx rgba(76, 175, 80, 0.3);
  595. &:active {
  596. transform: scale(0.95);
  597. }
  598. }
  599. }
  600. }
  601. }
  602. }
  603. @keyframes blink {
  604. 0%, 100% { opacity: 1; }
  605. 50% { opacity: 0.5; }
  606. }
  607. /* 暂无推荐 */
  608. .no-recommend {
  609. text-align: center;
  610. padding: 100rpx 40rpx;
  611. .no-recommend-icon {
  612. font-size: 120rpx;
  613. display: block;
  614. margin-bottom: 30rpx;
  615. }
  616. .no-recommend-title {
  617. font-size: 36rpx;
  618. color: #333333;
  619. font-weight: bold;
  620. display: block;
  621. margin-bottom: 15rpx;
  622. }
  623. .no-recommend-tip {
  624. font-size: 28rpx;
  625. color: #666666;
  626. display: block;
  627. margin-bottom: 50rpx;
  628. }
  629. .complete-profile-btn {
  630. background: linear-gradient(135deg, #FF6B9D 0%, #FF8FAB 100%);
  631. color: #FFFFFF;
  632. padding: 25rpx 45rpx;
  633. border-radius: 35rpx;
  634. box-shadow: 0 8rpx 20rpx rgba(255, 107, 157, 0.3);
  635. display: inline-block;
  636. .btn-text {
  637. font-size: 28rpx;
  638. font-weight: bold;
  639. }
  640. &:active {
  641. transform: scale(0.95);
  642. }
  643. }
  644. }
  645. .bottom-placeholder {
  646. height: 60rpx;
  647. }
  648. </style>