聊天搜索修改

This commit is contained in:
18631081161 2026-01-23 19:38:29 +08:00
parent 185343822c
commit 82acdee645
17 changed files with 2353 additions and 801 deletions

View File

@ -76,3 +76,14 @@ export function deleteUser(id: number): Promise<void> {
export function updateContactCount(id: number, contactCount: number): Promise<void> {
return request.put(`/admin/users/${id}/contact-count`, { contactCount })
}
/**
*
* @param id ID
* @param memberLevel 0 1 2 3
* @param memberExpireTime 23
* @returns
*/
export function updateMemberLevel(id: number, memberLevel: number, memberExpireTime?: string): Promise<void> {
return request.put(`/admin/users/${id}/member-level`, { memberLevel, memberExpireTime })
}

View File

@ -8,7 +8,7 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft } from '@element-plus/icons-vue'
import StatusTag from '@/components/StatusTag/index.vue'
import { getUserDetail, updateUserStatus, updateContactCount } from '@/api/user'
import { getUserDetail, updateUserStatus, updateContactCount, updateMemberLevel } from '@/api/user'
import { getFullImageUrl } from '@/utils/image'
import type { UserDetail } from '@/types/user.d'
@ -106,6 +106,54 @@ const handleEditContactCount = async () => {
}
}
//
const memberLevelOptions = [
{ value: 0, label: '非会员' },
{ value: 1, label: '不限时会员' },
{ value: 2, label: '诚意会员' },
{ value: 3, label: '家庭版会员' }
]
//
const memberLevelDialogVisible = ref(false)
const memberLevelForm = ref({
memberLevel: 0,
memberExpireTime: ''
})
//
const handleEditMemberLevel = () => {
if (!userDetail.value) return
memberLevelForm.value = {
memberLevel: userDetail.value.memberLevel,
memberExpireTime: userDetail.value.memberExpireTime ? userDetail.value.memberExpireTime.substring(0, 10) : ''
}
memberLevelDialogVisible.value = true
}
//
const handleConfirmMemberLevel = async () => {
try {
const { memberLevel, memberExpireTime } = memberLevelForm.value
// 23
if (memberLevel > 1 && !memberExpireTime) {
ElMessage.warning('请选择会员到期时间')
return
}
await updateMemberLevel(
userId.value,
memberLevel,
memberLevel > 1 ? memberExpireTime : undefined
)
ElMessage.success('修改成功')
memberLevelDialogVisible.value = false
fetchUserDetail()
} catch (error) {
console.error('修改会员等级失败:', error)
ElMessage.error('修改失败')
}
}
//
const formatTime = (time: string) => {
if (!time) return '-'
@ -303,6 +351,15 @@ onMounted(() => {
{{ userDetail.memberLevelText }}
</el-tag>
<span v-else>非会员</span>
<el-button
type="primary"
link
size="small"
style="margin-left: 8px;"
@click="handleEditMemberLevel"
>
修改
</el-button>
</el-descriptions-item>
<el-descriptions-item label="会员到期时间">
{{ formatTime(userDetail.memberExpireTime) }}
@ -591,6 +648,58 @@ onMounted(() => {
v-else-if="!loading"
description="用户不存在"
/>
<!-- 修改会员等级对话框 -->
<el-dialog
v-model="memberLevelDialogVisible"
title="修改会员等级"
width="400px"
>
<el-form label-width="100px">
<el-form-item label="会员等级">
<el-select
v-model="memberLevelForm.memberLevel"
placeholder="请选择会员等级"
style="width: 100%;"
>
<el-option
v-for="item in memberLevelOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item
v-if="memberLevelForm.memberLevel > 1"
label="到期时间"
>
<el-date-picker
v-model="memberLevelForm.memberExpireTime"
type="date"
placeholder="选择到期日期"
value-format="YYYY-MM-DD"
style="width: 100%;"
/>
</el-form-item>
<el-form-item v-if="memberLevelForm.memberLevel === 1">
<el-text type="info">
不限时会员无需设置到期时间
</el-text>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="memberLevelDialogVisible = false">
取消
</el-button>
<el-button
type="primary"
@click="handleConfirmMemberLevel"
>
确定
</el-button>
</template>
</el-dialog>
</div>
</template>

View File

@ -341,16 +341,19 @@ onMounted(() => {
stripe
border
style="width: 100%"
table-layout="auto"
>
<el-table-column
prop="xiangQinNo"
label="相亲编号"
width="120"
min-width="120"
fixed="left"
align="center"
/>
<el-table-column
label="用户信息"
width="200"
min-width="180"
align="center"
>
<template #default="{ row }">
<div class="user-info">
@ -372,17 +375,18 @@ onMounted(() => {
<el-table-column
prop="genderText"
label="性别"
width="80"
min-width="70"
align="center"
/>
<el-table-column
prop="city"
label="城市"
width="100"
min-width="90"
align="center"
/>
<el-table-column
label="状态"
width="80"
min-width="70"
align="center"
>
<template #default="{ row }">
@ -397,7 +401,7 @@ onMounted(() => {
</el-table-column>
<el-table-column
label="会员等级"
width="100"
min-width="90"
align="center"
>
<template #default="{ row }">
@ -412,7 +416,7 @@ onMounted(() => {
</el-table-column>
<el-table-column
label="实名认证"
width="100"
min-width="90"
align="center"
>
<template #default="{ row }">
@ -425,7 +429,7 @@ onMounted(() => {
</el-table-column>
<el-table-column
label="资料状态"
width="100"
min-width="90"
align="center"
>
<template #default="{ row }">
@ -440,12 +444,13 @@ onMounted(() => {
<el-table-column
prop="contactCount"
label="联系次数"
width="100"
min-width="90"
align="center"
/>
<el-table-column
label="注册时间"
width="170"
min-width="160"
align="center"
>
<template #default="{ row }">
{{ formatTime(row.createTime) }}
@ -453,7 +458,8 @@ onMounted(() => {
</el-table-column>
<el-table-column
label="最后登录"
width="170"
min-width="160"
align="center"
>
<template #default="{ row }">
{{ formatTime(row.lastLoginTime) }}
@ -461,35 +467,37 @@ onMounted(() => {
</el-table-column>
<el-table-column
label="操作"
width="240"
width="200"
fixed="right"
align="center"
>
<template #default="{ row }">
<el-button
type="primary"
link
:icon="View"
@click="handleViewDetail(row)"
>
详情
</el-button>
<el-button
:type="row.status === 1 ? 'danger' : 'success'"
link
:icon="Edit"
@click="handleToggleStatus(row)"
>
{{ row.status === 1 ? '禁用' : '启用' }}
</el-button>
<el-button
type="danger"
link
:icon="Delete"
@click="handleDeleteUser(row)"
>
删除
</el-button>
<div style="display: flex; justify-content: center; gap: 8px; flex-wrap: nowrap;">
<el-button
type="primary"
link
:icon="View"
@click="handleViewDetail(row)"
>
详情
</el-button>
<el-button
:type="row.status === 1 ? 'danger' : 'success'"
link
:icon="Edit"
@click="handleToggleStatus(row)"
>
{{ row.status === 1 ? '禁用' : '启用' }}
</el-button>
<el-button
type="danger"
link
:icon="Delete"
@click="handleDeleteUser(row)"
>
删除
</el-button>
</div>
</template>
</el-table-column>
</el-table>

View File

@ -102,7 +102,7 @@ export async function exchangePhoto(sessionId, receiverId) {
* @returns {Promise<Object>} 响应结果
*/
export async function respondExchange(requestMessageId, accept) {
const response = await post('/chat/respondExchange', { requestMessageId, accept })
const response = await post('/chat/respondExchange', { requestMessageId, isAgreed: accept })
return response
}

View File

@ -8,15 +8,16 @@
<!-- 性别选择弹窗 -->
<template v-if="type === 'gender'">
<view class="popup-title">请选择您想看的性别</view>
<view class="popup-title">您想找儿媳还是找女婿</view>
<view class="popup-subtitle">登录资料后推荐更精准</view>
<view class="gender-options">
<view class="gender-option male" @click="handleGenderSelect(1)">
<view class="gender-icon"></view>
<text>看男生</text>
</view>
<view class="gender-option female" @click="handleGenderSelect(2)">
<view class="gender-option male" @click="handleGenderSelect(2)">
<view class="gender-icon"></view>
<text>看女生</text>
<text>找儿媳</text>
</view>
<view class="gender-option female" @click="handleGenderSelect(1)">
<view class="gender-icon"></view>
<text>找女婿</text>
</view>
</view>
</template>
@ -244,6 +245,13 @@ export default {
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 20rpx;
}
.popup-subtitle {
font-size: 26rpx;
color: #FF5F5F;
text-align: center;
margin-bottom: 30rpx;
}
@ -327,20 +335,20 @@ export default {
}
&.male {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
.gender-icon {
color: #2196f3;
}
}
&.female {
background: linear-gradient(135deg, #fce4ec 0%, #f8bbd9 100%);
.gender-icon {
color: #e91e63;
}
}
&.female {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
.gender-icon {
color: #2196f3;
}
}
}
}
}

View File

@ -11,8 +11,8 @@
<view class="title-row">
<text class="gender-year">{{ genderText }} · {{ birthYear }}</text>
<view class="title-tags">
<text v-if="isRealName" class="tag tag-realname">已实名</text>
<text v-if="isMember" class="tag tag-member">会员</text>
<text v-if="isRealName" class="tag tag-realname">已实名</text>
</view>
</view>
@ -70,8 +70,8 @@
<view class="title-row">
<text class="gender-year">{{ genderText }} · {{ birthYear }}</text>
<view class="title-tags">
<text v-if="isRealName" class="tag tag-realname">已实名</text>
<text v-if="isMember" class="tag tag-member">会员</text>
<text v-if="isRealName" class="tag tag-realname">已实名</text>
</view>
</view>

View File

@ -95,6 +95,7 @@
{
"path": "pages/interact/viewedMe",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "看过我"
}
},
@ -107,6 +108,7 @@
{
"path": "pages/interact/favoritedMe",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "收藏我"
}
},
@ -119,6 +121,7 @@
{
"path": "pages/interact/unlockedMe",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "解锁我"
}
},

View File

@ -121,15 +121,80 @@
:key="message.id"
:id="'msg-' + message.id"
class="message-item"
:class="{ 'mine': message.isMine }"
:class="{ 'mine': message.isMine, 'exchange-item': message.messageType === MessageType.EXCHANGE_WECHAT || message.messageType === MessageType.EXCHANGE_PHOTO }"
>
<!-- 时间分隔 -->
<view v-if="shouldShowTime(message, index)" class="time-divider">
<text>{{ formatMessageTime(message.createTime) }}</text>
</view>
<!-- 消息内容 -->
<view class="message-content">
<!-- 交换微信请求卡片 - 独立全宽显示 -->
<view
v-if="message.messageType === MessageType.EXCHANGE_WECHAT"
class="exchange-card"
>
<view class="exchange-card-header">
<text>{{ getExchangeTitle(message) }}</text>
</view>
<!-- 待处理状态 - 显示操作按钮 -->
<view v-if="showExchangeActions(message)" class="exchange-card-actions">
<button class="action-btn reject-btn" @click="handleRespondExchange(message.id, false)">拒绝</button>
<button class="action-btn accept-btn" @click="handleRespondExchange(message.id, true)">同意</button>
</view>
<!-- 已接受 - 显示微信号 -->
<view v-else-if="message.status === ExchangeStatus.ACCEPTED" class="exchange-card-result accepted">
<text class="result-label">{{ targetNickname }}{{ targetRelationship || '本人' }}的微信号</text>
<text class="wechat-no">{{ message.exchangedContent || 'abcv123123' }}</text>
<button class="copy-btn" @click="handleCopyWeChat(message.exchangedContent)">点击复制微信号</button>
</view>
<!-- 已拒绝 -->
<view v-else-if="message.status === ExchangeStatus.REJECTED" class="exchange-card-result rejected">
<text>{{ getExchangeRejectText(message, 'wechat') }}</text>
</view>
<!-- 等待中 -->
<view v-else class="exchange-card-result pending">
<text>等待对方回应...</text>
</view>
</view>
<!-- 交换照片请求卡片 - 独立全宽显示 -->
<view
v-else-if="message.messageType === MessageType.EXCHANGE_PHOTO"
class="exchange-card"
>
<view class="exchange-card-header">
<text>{{ getExchangePhotoTitle(message) }}</text>
</view>
<!-- 待处理状态 - 显示操作按钮 -->
<view v-if="showExchangeActions(message)" class="exchange-card-actions">
<button class="action-btn reject-btn" @click="handleRespondExchange(message.id, false)">拒绝</button>
<button class="action-btn accept-btn" @click="handleRespondExchange(message.id, true)">同意</button>
</view>
<!-- 已接受 - 显示照片 -->
<view v-else-if="message.status === ExchangeStatus.ACCEPTED" class="exchange-card-result accepted">
<text class="result-label">{{ targetNickname }}{{ targetRelationship || '本人' }}希望与您发送孩子的照片共{{ message.photoCount || 5 }}</text>
<view class="photo-preview" v-if="message.photos && message.photos.length > 0">
<image
v-for="(photo, idx) in message.photos.slice(0, 3)"
:key="idx"
:src="photo"
mode="aspectFill"
@click="previewPhotos(message.photos, idx)"
/>
</view>
</view>
<!-- 已拒绝 -->
<view v-else-if="message.status === ExchangeStatus.REJECTED" class="exchange-card-result rejected">
<text>{{ getExchangeRejectText(message, 'photo') }}</text>
</view>
<!-- 等待中 -->
<view v-else class="exchange-card-result pending">
<text>等待对方回应...</text>
</view>
</view>
<!-- 普通消息内容 -->
<view v-else class="message-content">
<!-- 对方头像 -->
<image
v-if="!message.isMine"
@ -178,71 +243,6 @@
</view>
</view>
<!-- 交换微信请求卡片 -->
<view
v-else-if="message.messageType === MessageType.EXCHANGE_WECHAT"
class="exchange-card"
>
<view class="exchange-card-header">
<text>{{ getExchangeTitle(message) }}</text>
</view>
<!-- 待处理状态 - 显示操作按钮 -->
<view v-if="showExchangeActions(message)" class="exchange-card-actions">
<button class="action-btn reject-btn" @click="handleRespondExchange(message.id, false)">拒绝</button>
<button class="action-btn accept-btn" @click="handleRespondExchange(message.id, true)">同意</button>
</view>
<!-- 已接受 - 显示微信号 -->
<view v-else-if="message.status === ExchangeStatus.ACCEPTED" class="exchange-card-result accepted">
<text class="result-label">{{ targetNickname }}{{ targetRelationship || '本人' }}的微信号</text>
<text class="wechat-no">{{ message.exchangedContent || 'abcv123123' }}</text>
<button class="copy-btn" @click="handleCopyWeChat(message.exchangedContent)">点击复制微信号</button>
</view>
<!-- 已拒绝 -->
<view v-else-if="message.status === ExchangeStatus.REJECTED" class="exchange-card-result rejected">
<text>{{ message.isMine ? targetNickname : '您' }}拒绝了交换微信号</text>
</view>
<!-- 等待中 -->
<view v-else class="exchange-card-result pending">
<text>等待对方回应...</text>
</view>
</view>
<!-- 交换照片请求卡片 -->
<view
v-else-if="message.messageType === MessageType.EXCHANGE_PHOTO"
class="exchange-card"
>
<view class="exchange-card-header">
<text>{{ getExchangePhotoTitle(message) }}</text>
</view>
<!-- 待处理状态 - 显示操作按钮 -->
<view v-if="showExchangeActions(message)" class="exchange-card-actions">
<button class="action-btn reject-btn" @click="handleRespondExchange(message.id, false)">拒绝</button>
<button class="action-btn accept-btn" @click="handleRespondExchange(message.id, true)">同意</button>
</view>
<!-- 已接受 - 显示照片 -->
<view v-else-if="message.status === ExchangeStatus.ACCEPTED" class="exchange-card-result accepted">
<text class="result-label">{{ targetNickname }}{{ targetRelationship || '本人' }}的照片共{{ message.photoCount || 5 }}</text>
<view class="photo-preview" v-if="message.photos && message.photos.length > 0">
<image
v-for="(photo, idx) in message.photos.slice(0, 3)"
:key="idx"
:src="photo"
mode="aspectFill"
@click="previewPhotos(message.photos, idx)"
/>
</view>
</view>
<!-- 已拒绝 -->
<view v-else-if="message.status === ExchangeStatus.REJECTED" class="exchange-card-result rejected">
<text>{{ message.isMine ? targetNickname : '您' }}拒绝了交换孩子照片</text>
</view>
<!-- 等待中 -->
<view v-else class="exchange-card-result pending">
<text>等待对方回应...</text>
</view>
</view>
<!-- 消息状态 -->
<view v-if="message.isMine && message.messageType === MessageType.TEXT" class="message-status">
<text v-if="message.status === MessageStatus.SENDING" class="status sending">发送中</text>
@ -470,7 +470,10 @@ const loadMessages = async (isLoadMore = false) => {
if (res && res.code === 0 && res.data) {
const serverMessages = res.data.items || []
const newMessages = serverMessages.map((msg) => {
const newMessages = serverMessages
// 5=7=
.filter((msg) => msg.messageType !== MessageType.EXCHANGE_WECHAT_RESULT && msg.messageType !== MessageType.EXCHANGE_PHOTO_RESULT)
.map((msg) => {
// MessageId / IsSelf 使 id / isMine
const mapped = {
...msg,
@ -854,11 +857,37 @@ const showExchangeActions = (message) => {
}
const getExchangeTitle = (message) => {
return message.isMine ? '您发起了交换微信请求' : '对方想和您交换微信'
if (message.isMine) {
return '您发起了交换微信请求'
}
//
const name = targetNickname.value
if (name && name.includes('')) {
return `${name}希望与您交换微信号`
}
return `${name}${targetRelationship.value || '本人'})希望与您交换微信号`
}
const getExchangePhotoTitle = (message) => {
return message.isMine ? '您发起了交换照片请求' : '对方想和您交换照片'
if (message.isMine) {
return '您发起了交换照片请求'
}
const name = targetNickname.value
if (name && name.includes('')) {
return `${name}希望与您交换孩子照片`
}
return `${name}${targetRelationship.value || '本人'})希望与您交换孩子照片`
}
const getExchangeRejectText = (message, type) => {
let name = targetNickname.value
if (name && !name.includes('')) {
name = `${name}${targetRelationship.value || '本人'}`
}
if (type === 'wechat') {
return message.isMine ? `${name}拒绝了交换微信号` : '您拒绝了交换微信号'
}
return message.isMine ? `${name}拒绝了交换孩子照片` : '您拒绝了交换孩子照片'
}
const getExchangeStatusClass = (message) => {
@ -1073,6 +1102,11 @@ onMounted(async () => {
const handleReceiveMessage = (message) => {
console.log('[Chat] 收到新消息:', message)
// 5=7=
if (message.messageType === MessageType.EXCHANGE_WECHAT_RESULT || message.messageType === MessageType.EXCHANGE_PHOTO_RESULT) {
return
}
//
if (message.sessionId !== sessionId.value) {
return
@ -1491,80 +1525,100 @@ onUnmounted(() => {
}
}
// -
&.exchange-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 40rpx;
.time-divider {
width: 100%;
margin-bottom: 20rpx;
}
}
//
.exchange-card {
background: #fff;
background: #FFFFFF;
border-radius: 16rpx;
overflow: hidden;
min-width: 400rpx;
width: 100%;
max-width: 600rpx;
.exchange-card-header {
padding: 24rpx;
background: #f8f8f8;
border-bottom: 1rpx solid #f0f0f0;
padding: 30rpx 24rpx;
text-align: center;
text {
font-size: 26rpx;
color: #666;
font-size: 28rpx;
color: #333;
font-weight: 500;
}
}
.exchange-card-actions {
display: flex;
padding: 24rpx;
gap: 24rpx;
padding: 20rpx 60rpx 30rpx;
gap: 40rpx;
justify-content: center;
.action-btn {
flex: 1;
height: 72rpx;
line-height: 72rpx;
width: 180rpx;
height: 64rpx;
line-height: 64rpx;
font-size: 28rpx;
border-radius: 36rpx;
border: none;
border-radius: 32rpx;
padding: 0;
&::after {
border: none;
}
&.reject-btn {
background: #f5f5f5;
background: #fff;
color: #666;
border: 2rpx solid #CCCCCC;
}
&.accept-btn {
background: linear-gradient(135deg, #ff9a9a 0%, #ff6b6b 100%);
background: linear-gradient(135deg, #FFB5B5 0%, #FF9A9A 100%);
color: #fff;
border: none;
}
}
}
.exchange-card-result {
padding: 24rpx;
padding: 20rpx 24rpx 30rpx;
text-align: center;
.result-label {
display: block;
font-size: 26rpx;
color: #666;
margin-bottom: 16rpx;
font-size: 28rpx;
color: #333;
margin-bottom: 20rpx;
line-height: 1.5;
}
.wechat-no {
display: block;
font-size: 32rpx;
font-weight: 600;
font-weight: 500;
color: #333;
margin-bottom: 20rpx;
margin-bottom: 24rpx;
}
.copy-btn {
width: 100%;
height: 72rpx;
line-height: 72rpx;
background: linear-gradient(135deg, #4cd964 0%, #34c759 100%);
color: #fff;
font-size: 28rpx;
border-radius: 36rpx;
border: none;
width: 280rpx;
height: 64rpx;
line-height: 64rpx;
background: #fff;
color: #666;
font-size: 26rpx;
border-radius: 32rpx;
border: 2rpx solid #CCCCCC;
margin: 0 auto;
&::after {
border: none;
@ -1573,20 +1627,21 @@ onUnmounted(() => {
.photo-preview {
display: flex;
gap: 12rpx;
margin-top: 16rpx;
gap: 16rpx;
margin-top: 20rpx;
justify-content: center;
image {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
width: 180rpx;
height: 180rpx;
border-radius: 12rpx;
}
}
&.rejected {
text {
color: #999;
font-size: 26rpx;
color: #333;
font-size: 28rpx;
}
}

View File

@ -3,8 +3,24 @@
<!-- 页面加载状态 -->
<Loading type="page" :loading="pageLoading" />
<!-- 顶部背景图 -->
<view class="top-bg">
<image src="/static/title_bg.png" mode="aspectFill" class="bg-img" />
</view>
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="navbar-back" @click="handleBack">
<text class="back-icon"></text>
</view>
<text class="navbar-title">{{ activeTab === 'favoritedMe' ? '收藏我' : '我收藏的' }}</text>
<view class="navbar-placeholder"></view>
</view>
</view>
<!-- Tab切换 -->
<view class="tab-header">
<view class="tab-header" :style="{ top: (statusBarHeight + 44) + 'px' }">
<view
class="tab-item"
:class="{ active: activeTab === 'favoritedMe' }"
@ -21,72 +37,107 @@
</view>
</view>
<!-- 用户列表 -->
<view class="user-list" v-if="list.length > 0">
<view
class="user-item"
v-for="item in list"
:key="item.id"
@click="handleUserClick(item.userId)"
>
<!-- 头像 -->
<view class="user-avatar">
<image
class="avatar-image"
:src="item.avatar || defaultAvatar"
mode="aspectFill"
/>
</view>
<!-- 用户信息 -->
<view class="user-info">
<view class="user-header">
<text class="user-nickname">{{ item.nickname }}</text>
<view class="user-badges">
<view v-if="item.isMember" class="badge member-badge">会员</view>
<view v-if="item.isRealName" class="badge realname-badge">已实名</view>
<!-- 可滚动内容区域 -->
<scroll-view
class="content-scroll"
scroll-y
:style="{
top: (statusBarHeight + 44 + 80) + 'px',
height: 'calc(100vh - ' + (statusBarHeight + 44 + 80) + 'px)'
}"
@scrolltolower="handleScrollToLower"
>
<!-- 用户列表 -->
<view class="user-list" v-if="list.length > 0">
<view
class="user-card"
v-for="item in list"
:key="item.id || item.userId"
@click="handleUserClick(item.userId)"
>
<!-- 时间信息 -->
<view class="time-row">
<text class="time-text">{{ formatViewTime(item.createTime) }}{{ activeTab === 'favoritedMe' ? '收藏了我' : '收藏' }}</text>
</view>
<!-- 性别年份 -->
<view class="title-row">
<text class="gender-year">{{ item.gender === 1 ? '男' : '女' }} · {{ item.birthYear || getYearFromAge(item.age) }}</text>
</view>
<!-- 内容区域 -->
<view class="content-row">
<!-- 左侧信息 -->
<view class="info-section" :class="{ 'full-width': !item.isPhotoPublic || !item.firstPhoto }">
<view class="info-grid">
<view class="info-item">
<text class="label">现居</text>
<text class="value">{{ item.workCity || '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">身高</text>
<text class="value">{{ item.height ? item.height + 'cm' : '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">学历</text>
<text class="value">{{ item.educationName || '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">体重</text>
<text class="value">{{ item.weight ? item.weight + 'kg' : '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">职业</text>
<text class="value">{{ item.occupation || '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">家乡</text>
<text class="value">{{ item.hometown || '未填写' }}</text>
</view>
</view>
</view>
<!-- 右侧照片 -->
<view class="photo-section" v-if="item.isPhotoPublic && item.firstPhoto">
<image class="user-photo" :src="getFullUrl(item.firstPhoto)" mode="aspectFill" />
</view>
</view>
<view class="user-detail">
<text>{{ item.age }}</text>
<text class="divider">|</text>
<text>{{ item.workCity }}</text>
<text class="divider">|</text>
<text>{{ item.height }}cm</text>
<!-- 简介 -->
<view class="intro-section" v-if="item.intro">
<text class="intro-text">{{ item.intro }}</text>
</view>
<view class="user-time">
<text>{{ formatTime(item.createTime) }}</text>
<!-- 操作按钮 -->
<view class="action-buttons" @click.stop>
<button class="btn-detail" @click="handleUserClick(item.userId)">查看详细资料</button>
<button class="btn-contact" @click="handleContact(item.userId)">联系对方</button>
</view>
</view>
<!-- 操作按钮 -->
<view class="user-action" @click.stop="handleContact(item.userId)">
<text>联系TA</text>
</view>
</view>
</view>
<!-- 空状态 -->
<Empty
v-else-if="!listLoading"
:text="activeTab === 'favoritedMe' ? '暂无人收藏你' : '你还没有收藏别人'"
buttonText="去相亲"
buttonUrl="/pages/index/index"
/>
<!-- 加载更多 -->
<Loading
type="more"
:loading="listLoading"
:noMore="noMoreData && list.length > 0"
/>
<!-- 空状态 -->
<Empty
v-else-if="!listLoading"
:text="activeTab === 'favoritedMe' ? '暂无人收藏你' : '你还没有收藏别人'"
buttonText="去相亲"
buttonUrl="/pages/index/index"
/>
<!-- 加载更多 -->
<Loading
type="more"
:loading="listLoading"
:noMore="noMoreData && list.length > 0"
/>
</scroll-view>
</view>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { getFavoritedMe, getMyFavorite } from '@/api/interact.js'
import { formatTimestamp } from '@/utils/format.js'
import { getFullImageUrl } from '@/utils/image.js'
import { useUserStore } from '@/store/user.js'
import { useConfigStore } from '@/store/config.js'
import Loading from '@/components/Loading/index.vue'
@ -102,6 +153,9 @@ export default {
const userStore = useUserStore()
const configStore = useConfigStore()
//
const statusBarHeight = ref(20)
//
const pageLoading = ref(true)
const listLoading = ref(false)
@ -116,6 +170,56 @@ export default {
// configStore
const defaultAvatar = computed(() => configStore.defaultAvatar || '/static/logo.png')
//
const getSystemInfo = () => {
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20
}
})
}
//
const handleBack = () => {
uni.navigateBack()
}
// URL
const getFullUrl = (url) => {
return getFullImageUrl(url)
}
//
const getYearFromAge = (age) => {
if (!age) return ''
return new Date().getFullYear() - age
}
// - "15:21""15:21""01-20 15:21"
const formatViewTime = (timeStr) => {
if (!timeStr) return ''
const date = new Date(timeStr)
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000)
const targetDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const timeStr2 = `${hours}:${minutes}`
if (targetDate.getTime() === today.getTime()) {
return `今天${timeStr2}`
} else if (targetDate.getTime() === yesterday.getTime()) {
return `昨天${timeStr2}`
} else {
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${month}-${day} ${timeStr2}`
}
}
//
const loadList = async (refresh = false) => {
if (refresh) {
@ -157,6 +261,7 @@ export default {
const initPage = async () => {
pageLoading.value = true
try {
getSystemInfo()
await loadList(true)
} finally {
pageLoading.value = false
@ -168,18 +273,15 @@ export default {
if (activeTab.value === tab) return
activeTab.value = tab
//
uni.setNavigationBarTitle({
title: tab === 'favoritedMe' ? '收藏我' : '我收藏的'
})
//
loadList(true)
}
//
const formatTime = (timestamp) => {
return formatTimestamp(timestamp)
//
const handleScrollToLower = () => {
if (!noMoreData.value && !listLoading.value) {
loadList()
}
}
//
@ -212,16 +314,21 @@ export default {
})
return {
statusBarHeight,
pageLoading,
listLoading,
noMoreData,
activeTab,
list,
defaultAvatar,
handleBack,
switchTab,
formatTime,
formatViewTime,
getFullUrl,
getYearFromAge,
handleUserClick,
handleContact,
handleScrollToLower,
loadList
}
},
@ -230,156 +337,270 @@ export default {
this.loadList && this.loadList(true).finally(() => {
uni.stopPullDownRefresh()
})
},
//
onReachBottom() {
if (!this.noMoreData && !this.listLoading) {
this.loadList && this.loadList()
}
}
}
</script>
<style lang="scss" scoped>
.interact-page {
min-height: 100vh;
background-color: #f8f8f8;
height: 100vh;
background-color: #f5f5f5;
overflow: hidden;
}
//
.top-bg {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 400rpx;
z-index: 0;
.bg-img {
width: 100%;
height: 100%;
}
}
//
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 200;
.navbar-content {
position: relative;
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
.navbar-back {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
.back-icon {
font-size: 56rpx;
color: #333;
font-weight: 300;
}
}
.navbar-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.navbar-placeholder {
width: 80rpx;
}
}
}
// Tab
.tab-header {
position: fixed;
left: 0;
right: 0;
z-index: 100;
display: flex;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
padding: 20rpx 40rpx;
gap: 24rpx;
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 28rpx 0;
position: relative;
padding: 20rpx 40rpx;
border-radius: 40rpx;
background-color: #fff;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
text {
font-size: 30rpx;
color: #666;
color: #2F2F2F;
}
&.active {
text {
color: #ff6b6b;
font-weight: 600;
}
background-color: #FFF0F0;
position: relative;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 107, 0.2);
&::after {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: #ff6b6b;
border-radius: 2rpx;
border-radius: 40rpx;
padding: 2rpx;
background: linear-gradient(to right, #FFCBCB, #FF7B7B);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
text {
color: #FF5F5F;
font-weight: 500;
}
}
}
}
//
.content-scroll {
position: fixed;
left: 0;
right: 0;
z-index: 1;
}
//
.user-list {
padding: 0 20rpx;
padding: 20rpx;
}
//
.user-card {
background: linear-gradient(to bottom, #FFEBEA 0%, #FFFFFF 100%);
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
}
//
.time-row {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.user-item {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #fff;
border-radius: 16rpx;
margin-top: 20rpx;
&:active {
background-color: #f8f8f8;
.time-text {
font-size: 26rpx;
color: #666;
}
}
//
.title-row {
margin-bottom: 24rpx;
.gender-year {
font-size: 40rpx;
font-weight: 600;
color: #ff6b6b;
}
}
//
.content-row {
display: flex;
justify-content: space-between;
}
//
.info-section {
flex: 1;
&.full-width {
.info-grid {
display: flex;
flex-wrap: wrap;
}
.user-avatar {
width: 120rpx;
height: 120rpx;
margin-right: 24rpx;
flex-shrink: 0;
.avatar-image {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
}
.user-info {
flex: 1;
overflow: hidden;
.user-header {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.user-nickname {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-right: 12rpx;
}
.user-badges {
display: flex;
gap: 8rpx;
.badge {
padding: 2rpx 8rpx;
border-radius: 6rpx;
font-size: 20rpx;
color: #fff;
}
.member-badge {
background: linear-gradient(135deg, #ffd700 0%, #ffb800 100%);
}
.realname-badge {
background: linear-gradient(135deg, #4cd964 0%, #34c759 100%);
}
}
}
.user-detail {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
.divider {
margin: 0 8rpx;
color: #ddd;
}
}
.user-time {
font-size: 24rpx;
color: #999;
}
}
.user-action {
padding: 12rpx 24rpx;
background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%);
border-radius: 30rpx;
flex-shrink: 0;
text {
font-size: 26rpx;
color: #fff;
}
.info-item {
width: 50%;
}
}
}
.info-grid {
.info-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.label {
font-size: 28rpx;
color: #999;
width: 80rpx;
flex-shrink: 0;
}
.value {
font-size: 28rpx;
color: #333;
margin-left: 16rpx;
}
}
}
//
.photo-section {
width: 200rpx;
height: 200rpx;
margin-left: 24rpx;
flex-shrink: 0;
.user-photo {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
}
//
.intro-section {
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 1rpx solid #f5f5f5;
.intro-text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
//
.action-buttons {
display: flex;
gap: 24rpx;
margin-top: 32rpx;
button {
flex: 1;
height: 80rpx;
line-height: 80rpx;
font-size: 30rpx;
border-radius: 40rpx;
border: none;
&::after {
border: none;
}
}
.btn-detail {
background: #fff;
color: #333;
border: 2rpx solid #FFCBCB;
}
.btn-contact {
background: linear-gradient(to right, #FFBDC2, #FF8A93);
color: #fff;
}
}
</style>

View File

@ -3,8 +3,24 @@
<!-- 页面加载状态 -->
<Loading type="page" :loading="pageLoading" />
<!-- 顶部背景图 -->
<view class="top-bg">
<image src="/static/title_bg.png" mode="aspectFill" class="bg-img" />
</view>
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="navbar-back" @click="handleBack">
<text class="back-icon"></text>
</view>
<text class="navbar-title">{{ activeTab === 'unlockedMe' ? '解锁我' : '我解锁的' }}</text>
<view class="navbar-placeholder"></view>
</view>
</view>
<!-- Tab切换 -->
<view class="tab-header">
<view class="tab-header" :style="{ top: (statusBarHeight + 44) + 'px' }">
<view
class="tab-item"
:class="{ active: activeTab === 'unlockedMe' }"
@ -21,72 +37,107 @@
</view>
</view>
<!-- 用户列表 -->
<view class="user-list" v-if="list.length > 0">
<view
class="user-item"
v-for="item in list"
:key="item.id"
@click="handleUserClick(item.userId)"
>
<!-- 头像 -->
<view class="user-avatar">
<image
class="avatar-image"
:src="item.avatar || defaultAvatar"
mode="aspectFill"
/>
</view>
<!-- 用户信息 -->
<view class="user-info">
<view class="user-header">
<text class="user-nickname">{{ item.nickname }}</text>
<view class="user-badges">
<view v-if="item.isMember" class="badge member-badge">会员</view>
<view v-if="item.isRealName" class="badge realname-badge">已实名</view>
<!-- 可滚动内容区域 -->
<scroll-view
class="content-scroll"
scroll-y
:style="{
top: (statusBarHeight + 44 + 80) + 'px',
height: 'calc(100vh - ' + (statusBarHeight + 44 + 80) + 'px)'
}"
@scrolltolower="handleScrollToLower"
>
<!-- 用户列表 -->
<view class="user-list" v-if="list.length > 0">
<view
class="user-card"
v-for="item in list"
:key="item.id || item.userId"
@click="handleUserClick(item.userId)"
>
<!-- 时间信息 -->
<view class="time-row">
<text class="time-text">{{ formatViewTime(item.createTime) }}{{ activeTab === 'unlockedMe' ? '解锁了我' : '解锁' }}</text>
</view>
<!-- 性别年份 -->
<view class="title-row">
<text class="gender-year">{{ item.gender === 1 ? '男' : '女' }} · {{ item.birthYear || getYearFromAge(item.age) }}</text>
</view>
<!-- 内容区域 -->
<view class="content-row">
<!-- 左侧信息 -->
<view class="info-section" :class="{ 'full-width': !item.isPhotoPublic || !item.firstPhoto }">
<view class="info-grid">
<view class="info-item">
<text class="label">现居</text>
<text class="value">{{ item.workCity || '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">身高</text>
<text class="value">{{ item.height ? item.height + 'cm' : '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">学历</text>
<text class="value">{{ item.educationName || '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">体重</text>
<text class="value">{{ item.weight ? item.weight + 'kg' : '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">职业</text>
<text class="value">{{ item.occupation || '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">家乡</text>
<text class="value">{{ item.hometown || '未填写' }}</text>
</view>
</view>
</view>
<!-- 右侧照片 -->
<view class="photo-section" v-if="item.isPhotoPublic && item.firstPhoto">
<image class="user-photo" :src="getFullUrl(item.firstPhoto)" mode="aspectFill" />
</view>
</view>
<view class="user-detail">
<text>{{ item.age }}</text>
<text class="divider">|</text>
<text>{{ item.workCity }}</text>
<text class="divider">|</text>
<text>{{ item.height }}cm</text>
<!-- 简介 -->
<view class="intro-section" v-if="item.intro">
<text class="intro-text">{{ item.intro }}</text>
</view>
<view class="user-time">
<text>{{ formatTime(item.createTime) }}</text>
<!-- 操作按钮 -->
<view class="action-buttons" @click.stop>
<button class="btn-detail" @click="handleUserClick(item.userId)">查看详细资料</button>
<button class="btn-contact" @click="handleContact(item.userId)">联系对方</button>
</view>
</view>
<!-- 操作按钮 -->
<view class="user-action" @click.stop="handleContact(item.userId)">
<text>联系TA</text>
</view>
</view>
</view>
<!-- 空状态 -->
<Empty
v-else-if="!listLoading"
:text="activeTab === 'unlockedMe' ? '暂无人解锁你' : '你还没有解锁别人'"
buttonText="去相亲"
buttonUrl="/pages/index/index"
/>
<!-- 加载更多 -->
<Loading
type="more"
:loading="listLoading"
:noMore="noMoreData && list.length > 0"
/>
<!-- 空状态 -->
<Empty
v-else-if="!listLoading"
:text="activeTab === 'unlockedMe' ? '暂无人解锁你' : '你还没有解锁别人'"
buttonText="去相亲"
buttonUrl="/pages/index/index"
/>
<!-- 加载更多 -->
<Loading
type="more"
:loading="listLoading"
:noMore="noMoreData && list.length > 0"
/>
</scroll-view>
</view>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { getUnlockedMe, getMyUnlocked } from '@/api/interact.js'
import { formatTimestamp } from '@/utils/format.js'
import { getFullImageUrl } from '@/utils/image.js'
import { useUserStore } from '@/store/user.js'
import { useConfigStore } from '@/store/config.js'
import Loading from '@/components/Loading/index.vue'
@ -102,6 +153,9 @@ export default {
const userStore = useUserStore()
const configStore = useConfigStore()
//
const statusBarHeight = ref(20)
//
const pageLoading = ref(true)
const listLoading = ref(false)
@ -116,6 +170,56 @@ export default {
// configStore
const defaultAvatar = computed(() => configStore.defaultAvatar || '/static/logo.png')
//
const getSystemInfo = () => {
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20
}
})
}
//
const handleBack = () => {
uni.navigateBack()
}
// URL
const getFullUrl = (url) => {
return getFullImageUrl(url)
}
//
const getYearFromAge = (age) => {
if (!age) return ''
return new Date().getFullYear() - age
}
// - "15:21""15:21""01-20 15:21"
const formatViewTime = (timeStr) => {
if (!timeStr) return ''
const date = new Date(timeStr)
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000)
const targetDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const timeStr2 = `${hours}:${minutes}`
if (targetDate.getTime() === today.getTime()) {
return `今天${timeStr2}`
} else if (targetDate.getTime() === yesterday.getTime()) {
return `昨天${timeStr2}`
} else {
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${month}-${day} ${timeStr2}`
}
}
//
const loadList = async (refresh = false) => {
if (refresh) {
@ -157,6 +261,7 @@ export default {
const initPage = async () => {
pageLoading.value = true
try {
getSystemInfo()
await loadList(true)
} finally {
pageLoading.value = false
@ -168,18 +273,15 @@ export default {
if (activeTab.value === tab) return
activeTab.value = tab
//
uni.setNavigationBarTitle({
title: tab === 'unlockedMe' ? '解锁我' : '我解锁的'
})
//
loadList(true)
}
//
const formatTime = (timestamp) => {
return formatTimestamp(timestamp)
//
const handleScrollToLower = () => {
if (!noMoreData.value && !listLoading.value) {
loadList()
}
}
//
@ -212,16 +314,21 @@ export default {
})
return {
statusBarHeight,
pageLoading,
listLoading,
noMoreData,
activeTab,
list,
defaultAvatar,
handleBack,
switchTab,
formatTime,
formatViewTime,
getFullUrl,
getYearFromAge,
handleUserClick,
handleContact,
handleScrollToLower,
loadList
}
},
@ -230,156 +337,270 @@ export default {
this.loadList && this.loadList(true).finally(() => {
uni.stopPullDownRefresh()
})
},
//
onReachBottom() {
if (!this.noMoreData && !this.listLoading) {
this.loadList && this.loadList()
}
}
}
</script>
<style lang="scss" scoped>
.interact-page {
min-height: 100vh;
background-color: #f8f8f8;
height: 100vh;
background-color: #f5f5f5;
overflow: hidden;
}
//
.top-bg {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 400rpx;
z-index: 0;
.bg-img {
width: 100%;
height: 100%;
}
}
//
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 200;
.navbar-content {
position: relative;
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
.navbar-back {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
.back-icon {
font-size: 56rpx;
color: #333;
font-weight: 300;
}
}
.navbar-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.navbar-placeholder {
width: 80rpx;
}
}
}
// Tab
.tab-header {
position: fixed;
left: 0;
right: 0;
z-index: 100;
display: flex;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
padding: 20rpx 40rpx;
gap: 24rpx;
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 28rpx 0;
position: relative;
padding: 20rpx 40rpx;
border-radius: 40rpx;
background-color: #fff;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
text {
font-size: 30rpx;
color: #666;
color: #2F2F2F;
}
&.active {
text {
color: #ff6b6b;
font-weight: 600;
}
background-color: #FFF0F0;
position: relative;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 107, 0.2);
&::after {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: #ff6b6b;
border-radius: 2rpx;
border-radius: 40rpx;
padding: 2rpx;
background: linear-gradient(to right, #FFCBCB, #FF7B7B);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
text {
color: #FF5F5F;
font-weight: 500;
}
}
}
}
//
.content-scroll {
position: fixed;
left: 0;
right: 0;
z-index: 1;
}
//
.user-list {
padding: 0 20rpx;
padding: 20rpx;
}
//
.user-card {
background: linear-gradient(to bottom, #FFEBEA 0%, #FFFFFF 100%);
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
}
//
.time-row {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.user-item {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #fff;
border-radius: 16rpx;
margin-top: 20rpx;
&:active {
background-color: #f8f8f8;
.time-text {
font-size: 26rpx;
color: #666;
}
}
//
.title-row {
margin-bottom: 24rpx;
.gender-year {
font-size: 40rpx;
font-weight: 600;
color: #ff6b6b;
}
}
//
.content-row {
display: flex;
justify-content: space-between;
}
//
.info-section {
flex: 1;
&.full-width {
.info-grid {
display: flex;
flex-wrap: wrap;
}
.user-avatar {
width: 120rpx;
height: 120rpx;
margin-right: 24rpx;
flex-shrink: 0;
.avatar-image {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
}
.user-info {
flex: 1;
overflow: hidden;
.user-header {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.user-nickname {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-right: 12rpx;
}
.user-badges {
display: flex;
gap: 8rpx;
.badge {
padding: 2rpx 8rpx;
border-radius: 6rpx;
font-size: 20rpx;
color: #fff;
}
.member-badge {
background: linear-gradient(135deg, #ffd700 0%, #ffb800 100%);
}
.realname-badge {
background: linear-gradient(135deg, #4cd964 0%, #34c759 100%);
}
}
}
.user-detail {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
.divider {
margin: 0 8rpx;
color: #ddd;
}
}
.user-time {
font-size: 24rpx;
color: #999;
}
}
.user-action {
padding: 12rpx 24rpx;
background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%);
border-radius: 30rpx;
flex-shrink: 0;
text {
font-size: 26rpx;
color: #fff;
}
.info-item {
width: 50%;
}
}
}
.info-grid {
.info-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.label {
font-size: 28rpx;
color: #999;
width: 80rpx;
flex-shrink: 0;
}
.value {
font-size: 28rpx;
color: #333;
margin-left: 16rpx;
}
}
}
//
.photo-section {
width: 200rpx;
height: 200rpx;
margin-left: 24rpx;
flex-shrink: 0;
.user-photo {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
}
//
.intro-section {
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 1rpx solid #f5f5f5;
.intro-text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
//
.action-buttons {
display: flex;
gap: 24rpx;
margin-top: 32rpx;
button {
flex: 1;
height: 80rpx;
line-height: 80rpx;
font-size: 30rpx;
border-radius: 40rpx;
border: none;
&::after {
border: none;
}
}
.btn-detail {
background: #fff;
color: #333;
border: 2rpx solid #FFCBCB;
}
.btn-contact {
background: linear-gradient(to right, #FFBDC2, #FF8A93);
color: #fff;
}
}
</style>

View File

@ -3,8 +3,24 @@
<!-- 页面加载状态 -->
<Loading type="page" :loading="pageLoading" />
<!-- 顶部背景图 -->
<view class="top-bg">
<image src="/static/title_bg.png" mode="aspectFill" class="bg-img" />
</view>
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="navbar-back" @click="handleBack">
<text class="back-icon"></text>
</view>
<text class="navbar-title">{{ activeTab === 'viewedMe' ? '看过我' : '我看过的' }}</text>
<view class="navbar-placeholder"></view>
</view>
</view>
<!-- Tab切换 -->
<view class="tab-header">
<view class="tab-header" :style="{ top: (statusBarHeight + 44) + 'px' }">
<view
class="tab-item"
:class="{ active: activeTab === 'viewedMe' }"
@ -21,72 +37,108 @@
</view>
</view>
<!-- 用户列表 -->
<view class="user-list" v-if="list.length > 0">
<view
class="user-item"
v-for="item in list"
:key="item.id"
@click="handleUserClick(item.userId)"
>
<!-- 头像 -->
<view class="user-avatar">
<image
class="avatar-image"
:src="item.avatar || defaultAvatar"
mode="aspectFill"
/>
</view>
<!-- 用户信息 -->
<view class="user-info">
<view class="user-header">
<text class="user-nickname">{{ item.nickname }}</text>
<view class="user-badges">
<view v-if="item.isMember" class="badge member-badge">会员</view>
<view v-if="item.isRealName" class="badge realname-badge">已实名</view>
<!-- 可滚动内容区域 -->
<scroll-view
class="content-scroll"
scroll-y
:style="{
top: (statusBarHeight + 44 + 80) + 'px',
height: 'calc(100vh - ' + (statusBarHeight + 44 + 80) + 'px)'
}"
@scrolltolower="handleScrollToLower"
>
<!-- 用户列表 -->
<view class="user-list" v-if="list.length > 0">
<view
class="user-card"
v-for="item in list"
:key="item.id || item.userId"
@click="handleUserClick(item.userId)"
>
<!-- 时间信息 -->
<view class="time-row">
<text class="time-text">{{ formatViewTime(item.lastViewTime) }}{{ activeTab === 'viewedMe' ? '看过我' : '我看过' }}</text>
<text class="view-count" v-if="activeTab === 'viewedMe' && item.viewCount > 1"> 共看过我{{ item.viewCount }}</text>
</view>
<!-- 性别年份 -->
<view class="title-row">
<text class="gender-year">{{ item.gender === 1 ? '男' : '女' }} · {{ item.birthYear || getYearFromAge(item.age) }}</text>
</view>
<!-- 内容区域 -->
<view class="content-row">
<!-- 左侧信息 -->
<view class="info-section" :class="{ 'full-width': !item.isPhotoPublic || !item.firstPhoto }">
<view class="info-grid">
<view class="info-item">
<text class="label">现居</text>
<text class="value">{{ item.workCity || '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">身高</text>
<text class="value">{{ item.height ? item.height + 'cm' : '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">学历</text>
<text class="value">{{ item.educationName || '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">体重</text>
<text class="value">{{ item.weight ? item.weight + 'kg' : '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">职业</text>
<text class="value">{{ item.occupation || '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">家乡</text>
<text class="value">{{ item.hometown || '未填写' }}</text>
</view>
</view>
</view>
<!-- 右侧照片 -->
<view class="photo-section" v-if="item.isPhotoPublic && item.firstPhoto">
<image class="user-photo" :src="getFullUrl(item.firstPhoto)" mode="aspectFill" />
</view>
</view>
<view class="user-detail">
<text>{{ item.age }}</text>
<text class="divider">|</text>
<text>{{ item.workCity }}</text>
<text class="divider">|</text>
<text>{{ item.height }}cm</text>
<!-- 简介 -->
<view class="intro-section" v-if="item.intro">
<text class="intro-text">{{ item.intro }}</text>
</view>
<view class="user-time">
<text>{{ formatTime(item.createTime) }}</text>
<!-- 操作按钮 -->
<view class="action-buttons" @click.stop>
<button class="btn-detail" @click="handleUserClick(item.userId)">查看详细资料</button>
<button class="btn-contact" @click="handleContact(item.userId)">联系对方</button>
</view>
</view>
<!-- 操作按钮 -->
<view class="user-action" @click.stop="handleContact(item.userId)">
<text>联系TA</text>
</view>
</view>
</view>
<!-- 空状态 -->
<Empty
v-else-if="!listLoading"
:text="activeTab === 'viewedMe' ? '暂无人看过你' : '你还没有看过别人'"
buttonText="去相亲"
buttonUrl="/pages/index/index"
/>
<!-- 加载更多 -->
<Loading
type="more"
:loading="listLoading"
:noMore="noMoreData && list.length > 0"
/>
<!-- 空状态 -->
<Empty
v-else-if="!listLoading"
:text="activeTab === 'viewedMe' ? '暂无人看过你' : '你还没有看过别人'"
buttonText="去相亲"
buttonUrl="/pages/index/index"
/>
<!-- 加载更多 -->
<Loading
type="more"
:loading="listLoading"
:noMore="noMoreData && list.length > 0"
/>
</scroll-view>
</view>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { getViewedMe, getMyViewed } from '@/api/interact.js'
import { formatTimestamp } from '@/utils/format.js'
import { getFullImageUrl } from '@/utils/image.js'
import { useUserStore } from '@/store/user.js'
import { useConfigStore } from '@/store/config.js'
import Loading from '@/components/Loading/index.vue'
@ -102,6 +154,9 @@ export default {
const userStore = useUserStore()
const configStore = useConfigStore()
//
const statusBarHeight = ref(20)
//
const pageLoading = ref(true)
const listLoading = ref(false)
@ -116,6 +171,56 @@ export default {
// configStore
const defaultAvatar = computed(() => configStore.defaultAvatar || '/static/logo.png')
//
const getSystemInfo = () => {
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20
}
})
}
//
const handleBack = () => {
uni.navigateBack()
}
// URL
const getFullUrl = (url) => {
return getFullImageUrl(url)
}
//
const getYearFromAge = (age) => {
if (!age) return ''
return new Date().getFullYear() - age
}
// - "15:21""15:21""01-20 15:21"
const formatViewTime = (timeStr) => {
if (!timeStr) return ''
const date = new Date(timeStr)
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000)
const targetDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const timeStr2 = `${hours}:${minutes}`
if (targetDate.getTime() === today.getTime()) {
return `今天${timeStr2}`
} else if (targetDate.getTime() === yesterday.getTime()) {
return `昨天${timeStr2}`
} else {
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${month}-${day} ${timeStr2}`
}
}
//
const loadList = async (refresh = false) => {
if (refresh) {
@ -157,6 +262,7 @@ export default {
const initPage = async () => {
pageLoading.value = true
try {
getSystemInfo()
await loadList(true)
} finally {
pageLoading.value = false
@ -168,18 +274,15 @@ export default {
if (activeTab.value === tab) return
activeTab.value = tab
//
uni.setNavigationBarTitle({
title: tab === 'viewedMe' ? '看过我' : '我看过的'
})
//
loadList(true)
}
//
const formatTime = (timestamp) => {
return formatTimestamp(timestamp)
//
const handleScrollToLower = () => {
if (!noMoreData.value && !listLoading.value) {
loadList()
}
}
//
@ -212,16 +315,21 @@ export default {
})
return {
statusBarHeight,
pageLoading,
listLoading,
noMoreData,
activeTab,
list,
defaultAvatar,
handleBack,
switchTab,
formatTime,
formatViewTime,
getFullUrl,
getYearFromAge,
handleUserClick,
handleContact,
handleScrollToLower,
loadList
}
},
@ -230,156 +338,276 @@ export default {
this.loadList && this.loadList(true).finally(() => {
uni.stopPullDownRefresh()
})
},
//
onReachBottom() {
if (!this.noMoreData && !this.listLoading) {
this.loadList && this.loadList()
}
}
}
</script>
<style lang="scss" scoped>
.interact-page {
min-height: 100vh;
background-color: #f8f8f8;
height: 100vh;
background-color: #f5f5f5;
overflow: hidden;
}
//
.top-bg {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 400rpx;
z-index: 0;
.bg-img {
width: 100%;
height: 100%;
}
}
//
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 200;
.navbar-content {
position: relative;
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
.navbar-back {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
.back-icon {
font-size: 56rpx;
color: #333;
font-weight: 300;
}
}
.navbar-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.navbar-placeholder {
width: 80rpx;
}
}
}
// Tab
.tab-header {
position: fixed;
left: 0;
right: 0;
z-index: 100;
display: flex;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
padding: 20rpx 40rpx;
gap: 24rpx;
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 28rpx 0;
position: relative;
padding: 20rpx 40rpx;
border-radius: 40rpx;
background-color: #fff;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
text {
font-size: 30rpx;
color: #666;
color: #2F2F2F;
}
&.active {
text {
color: #ff6b6b;
font-weight: 600;
}
background-color: #FFF0F0;
position: relative;
box-shadow: 0 4rpx 16rpx rgba(255, 107, 107, 0.2);
&::after {
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: #ff6b6b;
border-radius: 2rpx;
border-radius: 40rpx;
padding: 2rpx;
background: linear-gradient(to right, #FFCBCB, #FF7B7B);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
text {
color: #FF5F5F;
font-weight: 500;
}
}
}
}
//
.content-scroll {
position: fixed;
left: 0;
right: 0;
z-index: 1;
}
//
.user-list {
padding: 0 20rpx;
padding: 20rpx;
}
//
.user-card {
background: linear-gradient(to bottom, #FFEBEA 0%, #FFFFFF 100%);
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
}
//
.time-row {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.user-item {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #fff;
border-radius: 16rpx;
margin-top: 20rpx;
&:active {
background-color: #f8f8f8;
.time-text {
font-size: 26rpx;
color: #666;
}
.view-count {
font-size: 26rpx;
color: #999;
margin-left: 16rpx;
}
}
//
.title-row {
margin-bottom: 24rpx;
.gender-year {
font-size: 40rpx;
font-weight: 600;
color: #ff6b6b;
}
}
//
.content-row {
display: flex;
justify-content: space-between;
}
//
.info-section {
flex: 1;
&.full-width {
.info-grid {
display: flex;
flex-wrap: wrap;
}
.user-avatar {
width: 120rpx;
height: 120rpx;
margin-right: 24rpx;
flex-shrink: 0;
.avatar-image {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
}
.user-info {
flex: 1;
overflow: hidden;
.user-header {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.user-nickname {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-right: 12rpx;
}
.user-badges {
display: flex;
gap: 8rpx;
.badge {
padding: 2rpx 8rpx;
border-radius: 6rpx;
font-size: 20rpx;
color: #fff;
}
.member-badge {
background: linear-gradient(135deg, #ffd700 0%, #ffb800 100%);
}
.realname-badge {
background: linear-gradient(135deg, #4cd964 0%, #34c759 100%);
}
}
}
.user-detail {
font-size: 26rpx;
color: #666;
margin-bottom: 8rpx;
.divider {
margin: 0 8rpx;
color: #ddd;
}
}
.user-time {
font-size: 24rpx;
color: #999;
}
}
.user-action {
padding: 12rpx 24rpx;
background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%);
border-radius: 30rpx;
flex-shrink: 0;
text {
font-size: 26rpx;
color: #fff;
}
.info-item {
width: 50%;
}
}
}
.info-grid {
.info-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.label {
font-size: 28rpx;
color: #999;
width: 80rpx;
flex-shrink: 0;
}
.value {
font-size: 28rpx;
color: #333;
margin-left: 16rpx;
}
}
}
//
.photo-section {
width: 200rpx;
height: 200rpx;
margin-left: 24rpx;
flex-shrink: 0;
.user-photo {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
}
//
.intro-section {
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 1rpx solid #f5f5f5;
.intro-text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
//
.action-buttons {
display: flex;
gap: 24rpx;
margin-top: 32rpx;
button {
flex: 1;
height: 80rpx;
line-height: 80rpx;
font-size: 30rpx;
border-radius: 40rpx;
border: none;
&::after {
border: none;
}
}
.btn-detail {
background: #fff;
color: #333;
border: 2rpx solid #FFCBCB;
}
.btn-contact {
background: linear-gradient(to right, #FFBDC2, #FF8A93);
color: #fff;
}
}
</style>

View File

@ -6,23 +6,60 @@
<view class="navbar-back" @click="handleBack">
<text class="back-icon"></text>
</view>
<text class="navbar-title">搜索结果</text>
<view class="navbar-placeholder"></view>
</view>
</view>
<!-- 顶部背景区域 -->
<view class="header-section" :style="{ paddingTop: (statusBarHeight + 44) + 'px' }">
<view class="search-date">搜索日期 {{ searchDate }}</view>
<view class="search-title">
<text>根据您的要求</text>
<text class="highlight">精准搜索出</text>
</view>
<view class="search-count">
<text class="count-number">{{ totalCount }}+</text>
<text class="count-text">位合适的亲家</text>
</view>
<!-- 搜索条件标签 -->
<view class="search-tags">
<view class="tag" v-if="searchParams.ageMin || searchParams.ageMax">
年龄{{ searchParams.ageMin || 18 }}-{{ searchParams.ageMax || 60 }}
</view>
<view class="tag" v-if="searchParams.heightMin || searchParams.heightMax">
身高{{ searchParams.heightMin || 150 }}cm及以上
</view>
<view class="tag" v-if="searchParams.cities && searchParams.cities.length > 0">
地区{{ searchParams.cities[0] }}
</view>
<view class="tag" v-if="searchParams.carStatus">
{{ searchParams.carStatus === 1 ? '有车' : '车产不限' }}
</view>
<view class="tag" v-if="searchParams.houseStatus">
{{ searchParams.houseStatus === 1 ? '有房' : '房产不限' }}
</view>
<view class="tag" v-if="searchParams.marriageStatus">
{{ getMarriageText(searchParams.marriageStatus) }}
</view>
<view class="tag" v-if="searchParams.monthlyIncome">
{{ getIncomeText(searchParams.monthlyIncome) }}
</view>
<view class="tag" v-if="searchParams.education && searchParams.education.length > 0">
{{ getEducationText(searchParams.education[0]) }}
</view>
<view class="tag more" v-if="hasMoreTags" @click="showAllTags = !showAllTags">
{{ showAllTags ? '收起' : '更多...' }}
</view>
</view>
</view>
<!-- 内容区域 -->
<scroll-view
class="content-scroll"
scroll-y
:style="{ paddingTop: (statusBarHeight + 44) + 'px' }"
@scrolltolower="loadMore"
>
<!-- 结果统计 -->
<view class="result-header" v-if="!loading">
<text class="result-count">共找到 {{ totalCount }} 位符合条件的用户</text>
</view>
<!-- 加载中 -->
<Loading v-if="loading" type="page" :loading="loading" />
@ -34,46 +71,92 @@
<view
v-for="user in results"
:key="user.userId"
class="result-item-wrapper"
class="result-card"
@click="handleCardClick(user.userId)"
>
<!-- 非会员模糊遮罩 -->
<view v-if="!isMember" class="blur-mask">
<view class="blur-content">
<text class="blur-text">开通会员查看完整信息</text>
<button class="btn-member" @click="goToMember">立即开通</button>
<!-- 性别年份和标签 -->
<view class="card-header">
<view class="gender-year">
<text :class="{ male: user.gender === 1 }">{{ user.gender === 1 ? '男' : '女' }}·{{ user.birthYear || '99' }}</text>
</view>
<view class="user-tags">
<view class="tag vip" v-if="user.isMember">
<image src="/static/vip-icon.png" mode="aspectFit" class="vip-icon" />
<text>xx会员</text>
</view>
<view class="tag realname" v-if="user.isRealName">已实名</view>
</view>
</view>
<UserCard
:userId="user.userId"
:nickname="user.nickname"
:avatar="user.avatar"
:gender="user.gender"
:age="user.age"
:birthYear="user.birthYear"
:workCity="user.workCity"
:hometown="user.hometown"
:height="user.height"
:weight="user.weight"
:education="user.education"
:educationName="user.educationName"
:occupation="user.occupation"
:monthlyIncome="user.monthlyIncome"
:intro="user.intro"
:isMember="user.isMember"
:isRealName="user.isRealName"
:isPhotoPublic="user.isPhotoPublic"
:firstPhoto="user.firstPhoto"
:viewedToday="user.viewedToday"
@click="handleCardClick"
@contact="handleContact"
/>
<!-- 详细信息网格 -->
<view class="info-grid">
<view class="info-row">
<view class="info-item">
<text class="label">年龄</text>
<text class="value">{{ user.age }}</text>
</view>
<view class="info-item">
<text class="label">身高</text>
<text class="value">{{ user.height || '-' }}cm</text>
</view>
</view>
<view class="info-row">
<view class="info-item">
<text class="label">学历</text>
<text class="value">{{ user.educationName || getEducationText(user.education) }}</text>
</view>
<view class="info-item">
<text class="label">体重</text>
<text class="value">{{ user.weight || '-' }}kg</text>
</view>
</view>
<view class="info-row">
<view class="info-item">
<text class="label">收入</text>
<text class="value">{{ getIncomeText(user.monthlyIncome) }}</text>
</view>
<view class="info-item">
<text class="label">职业</text>
<text class="value">{{ user.occupation || '未填写' }}</text>
</view>
</view>
<view class="info-row">
<view class="info-item">
<text class="label">现居</text>
<text class="value">{{ user.workCity || '未填写' }}</text>
</view>
<view class="info-item">
<text class="label">家乡</text>
<text class="value">{{ user.hometown || '未填写' }}</text>
</view>
</view>
</view>
<!-- 简介 -->
<view class="intro-section" v-if="user.intro">
<text class="intro-text">{{ user.intro }}</text>
</view>
<!-- 联系按钮 -->
<view class="contact-btn" @click.stop="handleContact(user.userId)">
<text class="lock-icon">🔒</text>
<text>开通诚意会员 联系对方</text>
</view>
</view>
</view>
<!-- 加载更多 -->
<Loading v-if="results.length > 0" type="more" :loading="loadingMore" :noMore="!hasMore" />
<!-- 底部占位 -->
<view class="bottom-placeholder"></view>
</scroll-view>
<!-- 底部会员按钮 -->
<view class="bottom-bar" v-if="!isMember">
<button class="member-btn" @click="goToMember">开通诚意会员</button>
<text class="member-tip" @click="goToMember">查看诚意会员其他特权</text>
</view>
</view>
</template>
@ -81,7 +164,6 @@
import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/store/user.js'
import { search } from '@/api/user.js'
import UserCard from '@/components/UserCard/index.vue'
import Empty from '@/components/Empty/index.vue'
import Loading from '@/components/Loading/index.vue'
@ -96,6 +178,24 @@ const isMember = computed(() => userStore.isMember)
//
const searchParams = ref({})
//
const searchDate = ref('')
//
const showAllTags = ref(false)
const hasMoreTags = computed(() => {
let count = 0
if (searchParams.value.ageMin || searchParams.value.ageMax) count++
if (searchParams.value.heightMin || searchParams.value.heightMax) count++
if (searchParams.value.cities?.length > 0) count++
if (searchParams.value.carStatus) count++
if (searchParams.value.houseStatus) count++
if (searchParams.value.marriageStatus) count++
if (searchParams.value.monthlyIncome) count++
if (searchParams.value.education?.length > 0) count++
return count > 6
})
//
const loading = ref(true)
const loadingMore = ref(false)
@ -105,6 +205,34 @@ const pageIndex = ref(1)
const pageSize = ref(10)
const hasMore = ref(true)
//
const educationMap = {
1: '高中',
2: '中专',
3: '大专',
4: '本科',
5: '研究生',
6: '博士及以上'
}
const incomeMap = {
1: '5千以下',
2: '5千-8千/月',
3: '8千-1万/月',
4: '1万-2万/月',
5: '2万以上'
}
const marriageMap = {
1: '未婚',
2: '离异',
3: '丧偶'
}
const getEducationText = (value) => educationMap[value] || '学历不限'
const getIncomeText = (value) => incomeMap[value] || '收入不限'
const getMarriageText = (value) => marriageMap[value] || '婚史不限'
//
const getSystemInfo = () => {
uni.getSystemInfo({
@ -187,11 +315,6 @@ const loadMore = () => {
//
const handleCardClick = (userId) => {
if (!isMember.value) {
uni.showToast({ title: '开通会员查看完整信息', icon: 'none' })
return
}
uni.navigateTo({
url: `/pages/profile/detail?userId=${userId}`
})
@ -200,7 +323,7 @@ const handleCardClick = (userId) => {
//
const handleContact = (userId) => {
if (!isMember.value) {
uni.showToast({ title: '开通会员联系TA', icon: 'none' })
goToMember()
return
}
@ -231,8 +354,18 @@ const goToMember = () => {
})
}
//
const formatDate = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
onMounted(() => {
getSystemInfo()
searchDate.value = formatDate()
//
const pages = getCurrentPages()
@ -252,6 +385,7 @@ onMounted(() => {
})
</script>
<style lang="scss" scoped>
.result-page {
min-height: 100vh;
@ -265,7 +399,7 @@ onMounted(() => {
left: 0;
right: 0;
z-index: 100;
background: linear-gradient(135deg, #ffb5b5 0%, #ff9a9a 100%);
background: transparent;
.navbar-content {
height: 44px;
@ -283,37 +417,82 @@ onMounted(() => {
.back-icon {
font-size: 64rpx;
color: #fff;
color: #333;
font-weight: 400;
}
}
.navbar-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.navbar-placeholder {
width: 80rpx;
}
}
}
//
.content-scroll {
min-height: 100vh;
padding-bottom: 40rpx;
//
.header-section {
background: linear-gradient(180deg, #FFD4D4 0%, #FFE8E8 50%, #f5f6fa 100%);
padding: 30rpx 32rpx 40rpx;
text-align: center;
.search-date {
font-size: 24rpx;
color: #FF6B6B;
margin-bottom: 16rpx;
}
.search-title {
font-size: 40rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
.highlight {
color: #FF5F5F;
}
}
.search-count {
margin-bottom: 30rpx;
.count-number {
font-size: 48rpx;
font-weight: 700;
color: #FF5F5F;
}
.count-text {
font-size: 36rpx;
font-weight: 600;
color: #FF5F5F;
}
}
.search-tags {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
justify-content: center;
.tag {
padding: 12rpx 24rpx;
background: #FFF0F0;
border-radius: 30rpx;
font-size: 24rpx;
color: #666;
border: 1rpx solid #FFD4D4;
&.more {
color: #FF5F5F;
background: #fff;
}
}
}
}
//
.result-header {
padding: 24rpx 32rpx;
.result-count {
font-size: 26rpx;
color: #999;
}
//
.content-scroll {
min-height: calc(100vh - 400rpx);
padding-bottom: 200rpx;
}
//
@ -321,49 +500,168 @@ onMounted(() => {
padding: 0 24rpx;
}
.result-item-wrapper {
position: relative;
//
.result-card {
background: #fff;
border-radius: 24rpx;
padding: 30rpx;
margin-bottom: 24rpx;
.blur-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
z-index: 10;
.card-header {
display: flex;
align-items: center;
justify-content: center;
border-radius: 24rpx;
justify-content: space-between;
margin-bottom: 24rpx;
.blur-content {
text-align: center;
.blur-text {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
}
.btn-member {
width: 240rpx;
height: 72rpx;
line-height: 72rpx;
background: linear-gradient(135deg, #ffd700 0%, #ffb800 100%);
color: #fff;
font-size: 28rpx;
border-radius: 36rpx;
border: none;
.gender-year {
text {
font-size: 36rpx;
font-weight: 600;
color: #FF5F5F;
&::after {
border: none;
&.male {
color: #4A90D9;
}
}
}
.user-tags {
display: flex;
gap: 12rpx;
.tag {
display: flex;
align-items: center;
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
&.vip {
background: linear-gradient(135deg, #FFE4B5 0%, #FFD700 100%);
color: #8B4513;
.vip-icon {
width: 24rpx;
height: 24rpx;
margin-right: 6rpx;
}
}
&.realname {
background: #E8F5E9;
color: #4CAF50;
}
}
}
}
.info-grid {
background: #FFF9F9;
border-radius: 16rpx;
padding: 20rpx;
margin-bottom: 20rpx;
.info-row {
display: flex;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.info-item {
flex: 1;
display: flex;
.label {
font-size: 26rpx;
color: #999;
width: 80rpx;
}
.value {
font-size: 26rpx;
color: #333;
flex: 1;
}
}
}
}
.intro-section {
margin-bottom: 24rpx;
.intro-text {
font-size: 26rpx;
color: #666;
line-height: 1.6;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
.contact-btn {
display: flex;
align-items: center;
justify-content: center;
height: 80rpx;
background: linear-gradient(90deg, #F6C47A 0%, #FFE7C3 50%, #F6C47A 100%);
border: none;
border-radius: 40rpx;
.lock-icon {
font-size: 28rpx;
margin-right: 12rpx;
}
text {
font-size: 28rpx;
color: #333;
}
}
}
//
.bottom-placeholder {
height: 40rpx;
}
//
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 20rpx 32rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
align-items: center;
.member-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
background: linear-gradient(135deg, #FFB5B5 0%, #FF9A9A 100%);
color: #fff;
font-size: 32rpx;
font-weight: 500;
border-radius: 44rpx;
border: none;
margin-bottom: 16rpx;
&::after {
border: none;
}
}
.member-tip {
font-size: 24rpx;
color: #999;
}
}
</style>

View File

@ -125,6 +125,20 @@ public class AdminUserController : ControllerBase
var result = await _adminUserService.UpdateContactCountAsync(id, request.ContactCount, adminId);
return result ? ApiResponse.Success("更新成功") : ApiResponse.Error(40001, "更新失败");
}
/// <summary>
/// 更新用户会员等级
/// </summary>
/// <param name="id">用户ID</param>
/// <param name="request">会员等级更新请求</param>
/// <returns>操作结果</returns>
[HttpPut("{id}/member-level")]
public async Task<ApiResponse> UpdateMemberLevel(long id, [FromBody] UpdateMemberLevelRequest request)
{
var adminId = GetCurrentAdminId();
var result = await _adminUserService.UpdateMemberLevelAsync(id, request.MemberLevel, request.MemberExpireTime, adminId);
return result ? ApiResponse.Success("更新成功") : ApiResponse.Error(40001, "更新失败");
}
}
/// <summary>
@ -153,3 +167,19 @@ public class UpdateContactCountRequest
/// </summary>
public int ContactCount { get; set; }
}
/// <summary>
/// 更新会员等级请求
/// </summary>
public class UpdateMemberLevelRequest
{
/// <summary>
/// 会员等级0非会员 1不限时会员 2诚意会员 3家庭版会员
/// </summary>
public int MemberLevel { get; set; }
/// <summary>
/// 会员到期时间等级2、3需要
/// </summary>
public DateTime? MemberExpireTime { get; set; }
}

View File

@ -20,16 +20,81 @@ public class ViewRecordResponse
/// </summary>
public string? Avatar { get; set; }
/// <summary>
/// 性别 1男 2女
/// </summary>
public int? Gender { get; set; }
/// <summary>
/// 年龄
/// </summary>
public int? Age { get; set; }
/// <summary>
/// 出生年份
/// </summary>
public int? BirthYear { get; set; }
/// <summary>
/// 工作城市
/// </summary>
public string? WorkCity { get; set; }
/// <summary>
/// 家乡
/// </summary>
public string? Hometown { get; set; }
/// <summary>
/// 身高
/// </summary>
public int? Height { get; set; }
/// <summary>
/// 体重
/// </summary>
public int? Weight { get; set; }
/// <summary>
/// 学历
/// </summary>
public int? Education { get; set; }
/// <summary>
/// 学历名称
/// </summary>
public string? EducationName { get; set; }
/// <summary>
/// 职业
/// </summary>
public string? Occupation { get; set; }
/// <summary>
/// 简介
/// </summary>
public string? Intro { get; set; }
/// <summary>
/// 是否会员
/// </summary>
public bool IsMember { get; set; }
/// <summary>
/// 是否实名
/// </summary>
public bool IsRealName { get; set; }
/// <summary>
/// 是否公开照片
/// </summary>
public bool IsPhotoPublic { get; set; }
/// <summary>
/// 首张照片
/// </summary>
public string? FirstPhoto { get; set; }
/// <summary>
/// 浏览次数
/// </summary>
@ -61,16 +126,81 @@ public class FavoriteRecordResponse
/// </summary>
public string? Avatar { get; set; }
/// <summary>
/// 性别 1男 2女
/// </summary>
public int? Gender { get; set; }
/// <summary>
/// 年龄
/// </summary>
public int? Age { get; set; }
/// <summary>
/// 出生年份
/// </summary>
public int? BirthYear { get; set; }
/// <summary>
/// 工作城市
/// </summary>
public string? WorkCity { get; set; }
/// <summary>
/// 家乡
/// </summary>
public string? Hometown { get; set; }
/// <summary>
/// 身高
/// </summary>
public int? Height { get; set; }
/// <summary>
/// 体重
/// </summary>
public int? Weight { get; set; }
/// <summary>
/// 学历
/// </summary>
public int? Education { get; set; }
/// <summary>
/// 学历名称
/// </summary>
public string? EducationName { get; set; }
/// <summary>
/// 职业
/// </summary>
public string? Occupation { get; set; }
/// <summary>
/// 简介
/// </summary>
public string? Intro { get; set; }
/// <summary>
/// 是否会员
/// </summary>
public bool IsMember { get; set; }
/// <summary>
/// 是否实名
/// </summary>
public bool IsRealName { get; set; }
/// <summary>
/// 是否公开照片
/// </summary>
public bool IsPhotoPublic { get; set; }
/// <summary>
/// 首张照片
/// </summary>
public string? FirstPhoto { get; set; }
/// <summary>
/// 收藏时间
/// </summary>
@ -118,16 +248,81 @@ public class UnlockRecordResponse
/// </summary>
public string? Avatar { get; set; }
/// <summary>
/// 性别 1男 2女
/// </summary>
public int? Gender { get; set; }
/// <summary>
/// 年龄
/// </summary>
public int? Age { get; set; }
/// <summary>
/// 出生年份
/// </summary>
public int? BirthYear { get; set; }
/// <summary>
/// 工作城市
/// </summary>
public string? WorkCity { get; set; }
/// <summary>
/// 家乡
/// </summary>
public string? Hometown { get; set; }
/// <summary>
/// 身高
/// </summary>
public int? Height { get; set; }
/// <summary>
/// 体重
/// </summary>
public int? Weight { get; set; }
/// <summary>
/// 学历
/// </summary>
public int? Education { get; set; }
/// <summary>
/// 学历名称
/// </summary>
public string? EducationName { get; set; }
/// <summary>
/// 职业
/// </summary>
public string? Occupation { get; set; }
/// <summary>
/// 简介
/// </summary>
public string? Intro { get; set; }
/// <summary>
/// 是否会员
/// </summary>
public bool IsMember { get; set; }
/// <summary>
/// 是否实名
/// </summary>
public bool IsRealName { get; set; }
/// <summary>
/// 是否公开照片
/// </summary>
public bool IsPhotoPublic { get; set; }
/// <summary>
/// 首张照片
/// </summary>
public string? FirstPhoto { get; set; }
/// <summary>
/// 解锁时间
/// </summary>

View File

@ -61,4 +61,14 @@ public interface IAdminUserService
/// <param name="adminId">操作管理员ID</param>
/// <returns>是否成功</returns>
Task<bool> UpdateContactCountAsync(long userId, int contactCount, long adminId);
/// <summary>
/// 更新用户会员等级
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="memberLevel">会员等级0非会员 1不限时会员 2诚意会员 3家庭版会员</param>
/// <param name="memberExpireTime">会员到期时间等级2、3需要</param>
/// <param name="adminId">操作管理员ID</param>
/// <returns>是否成功</returns>
Task<bool> UpdateMemberLevelAsync(long userId, int memberLevel, DateTime? memberExpireTime, long adminId);
}

View File

@ -465,6 +465,51 @@ public class AdminUserService : IAdminUserService
return result > 0;
}
/// <inheritdoc />
public async Task<bool> UpdateMemberLevelAsync(long userId, int memberLevel, DateTime? memberExpireTime, long adminId)
{
var user = await _userRepository.GetByIdAsync(userId);
if (user == null || user.IsDeleted)
{
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
}
// 验证会员等级
if (memberLevel < 0 || memberLevel > 3)
{
throw new BusinessException(ErrorCodes.InvalidParameter, "无效的会员等级");
}
var oldLevel = user.MemberLevel;
var oldExpireTime = user.MemberExpireTime;
user.MemberLevel = memberLevel;
user.IsMember = memberLevel > 0;
// 不限时会员不需要到期时间
if (memberLevel == 1)
{
user.MemberExpireTime = null;
}
else if (memberLevel > 1)
{
user.MemberExpireTime = memberExpireTime;
}
else
{
user.MemberExpireTime = null;
}
user.UpdateTime = DateTime.Now;
var result = await _userRepository.UpdateAsync(user);
_logger.LogInformation("管理员更新用户会员等级: AdminId={AdminId}, UserId={UserId}, OldLevel={OldLevel}, NewLevel={NewLevel}, ExpireTime={ExpireTime}",
adminId, userId, oldLevel, memberLevel, memberExpireTime);
return result > 0;
}
#region Private Helper Methods
private static AdminUserListDto MapToUserListDto(User user, UserProfile? profile)

View File

@ -19,6 +19,7 @@ public class InteractService : IInteractService
private readonly IRepository<UserUnlock> _unlockRepository;
private readonly IRepository<ChatSession> _sessionRepository;
private readonly IRepository<UserProfile> _profileRepository;
private readonly IRepository<UserPhoto> _photoRepository;
private readonly ILogger<InteractService> _logger;
public InteractService(
@ -28,6 +29,7 @@ public class InteractService : IInteractService
IRepository<UserUnlock> unlockRepository,
IRepository<ChatSession> sessionRepository,
IRepository<UserProfile> profileRepository,
IRepository<UserPhoto> photoRepository,
ILogger<InteractService> logger)
{
_userRepository = userRepository;
@ -36,6 +38,7 @@ public class InteractService : IInteractService
_unlockRepository = unlockRepository;
_sessionRepository = sessionRepository;
_profileRepository = profileRepository;
_photoRepository = photoRepository;
_logger = logger;
}
@ -115,14 +118,29 @@ public class InteractService : IInteractService
{
var user = await _userRepository.GetByIdAsync(item.UserId);
var profile = (await _profileRepository.GetListAsync(p => p.UserId == item.UserId)).FirstOrDefault();
var photos = await _photoRepository.GetListAsync(p => p.UserId == item.UserId);
var firstPhoto = photos.OrderBy(p => p.Sort).FirstOrDefault();
responses.Add(new ViewRecordResponse
{
UserId = item.UserId,
Nickname = user?.Nickname,
Avatar = user?.Avatar,
Gender = profile?.ChildGender ?? user?.Gender,
Age = profile?.BirthYear != null ? DateTime.Now.Year - profile.BirthYear : null,
BirthYear = profile?.BirthYear,
WorkCity = profile?.WorkCity,
Hometown = profile?.HomeCity,
Height = profile?.Height,
Weight = profile?.Weight,
Education = profile?.Education,
EducationName = GetEducationName(profile?.Education),
Occupation = profile?.Occupation,
Intro = profile?.Introduction,
IsMember = user?.IsMember ?? false,
IsRealName = user?.IsRealName ?? false,
IsPhotoPublic = profile?.IsPhotoPublic ?? false,
FirstPhoto = firstPhoto?.PhotoUrl,
ViewCount = item.ViewCount,
LastViewTime = item.LastViewTime
});
@ -152,14 +170,29 @@ public class InteractService : IInteractService
{
var user = await _userRepository.GetByIdAsync(item.TargetUserId);
var profile = (await _profileRepository.GetListAsync(p => p.UserId == item.TargetUserId)).FirstOrDefault();
var photos = await _photoRepository.GetListAsync(p => p.UserId == item.TargetUserId);
var firstPhoto = photos.OrderBy(p => p.Sort).FirstOrDefault();
responses.Add(new ViewRecordResponse
{
UserId = item.TargetUserId,
Nickname = user?.Nickname,
Avatar = user?.Avatar,
Gender = profile?.ChildGender ?? user?.Gender,
Age = profile?.BirthYear != null ? DateTime.Now.Year - profile.BirthYear : null,
BirthYear = profile?.BirthYear,
WorkCity = profile?.WorkCity,
Hometown = profile?.HomeCity,
Height = profile?.Height,
Weight = profile?.Weight,
Education = profile?.Education,
EducationName = GetEducationName(profile?.Education),
Occupation = profile?.Occupation,
Intro = profile?.Introduction,
IsMember = user?.IsMember ?? false,
IsRealName = user?.IsRealName ?? false,
IsPhotoPublic = profile?.IsPhotoPublic ?? false,
FirstPhoto = firstPhoto?.PhotoUrl,
ViewCount = item.ViewCount,
LastViewTime = item.LastViewTime
});
@ -174,6 +207,23 @@ public class InteractService : IInteractService
};
}
/// <summary>
/// 获取学历名称
/// </summary>
private static string? GetEducationName(int? education)
{
return education switch
{
1 => "高中",
2 => "中专",
3 => "大专",
4 => "本科",
5 => "研究生",
6 => "博士及以上",
_ => null
};
}
#endregion
#region
@ -254,14 +304,29 @@ public class InteractService : IInteractService
{
var user = await _userRepository.GetByIdAsync(item.UserId);
var profile = (await _profileRepository.GetListAsync(p => p.UserId == item.UserId)).FirstOrDefault();
var photos = await _photoRepository.GetListAsync(p => p.UserId == item.UserId);
var firstPhoto = photos.OrderBy(p => p.Sort).FirstOrDefault();
responses.Add(new FavoriteRecordResponse
{
UserId = item.UserId,
Nickname = user?.Nickname,
Avatar = user?.Avatar,
Gender = profile?.ChildGender ?? user?.Gender,
Age = profile?.BirthYear != null ? DateTime.Now.Year - profile.BirthYear : null,
BirthYear = profile?.BirthYear,
WorkCity = profile?.WorkCity,
Hometown = profile?.HomeCity,
Height = profile?.Height,
Weight = profile?.Weight,
Education = profile?.Education,
EducationName = GetEducationName(profile?.Education),
Occupation = profile?.Occupation,
Intro = profile?.Introduction,
IsMember = user?.IsMember ?? false,
IsRealName = user?.IsRealName ?? false,
IsPhotoPublic = profile?.IsPhotoPublic ?? false,
FirstPhoto = firstPhoto?.PhotoUrl,
CreateTime = item.CreateTime
});
}
@ -290,14 +355,29 @@ public class InteractService : IInteractService
{
var user = await _userRepository.GetByIdAsync(item.TargetUserId);
var profile = (await _profileRepository.GetListAsync(p => p.UserId == item.TargetUserId)).FirstOrDefault();
var photos = await _photoRepository.GetListAsync(p => p.UserId == item.TargetUserId);
var firstPhoto = photos.OrderBy(p => p.Sort).FirstOrDefault();
responses.Add(new FavoriteRecordResponse
{
UserId = item.TargetUserId,
Nickname = user?.Nickname,
Avatar = user?.Avatar,
Gender = profile?.ChildGender ?? user?.Gender,
Age = profile?.BirthYear != null ? DateTime.Now.Year - profile.BirthYear : null,
BirthYear = profile?.BirthYear,
WorkCity = profile?.WorkCity,
Hometown = profile?.HomeCity,
Height = profile?.Height,
Weight = profile?.Weight,
Education = profile?.Education,
EducationName = GetEducationName(profile?.Education),
Occupation = profile?.Occupation,
Intro = profile?.Introduction,
IsMember = user?.IsMember ?? false,
IsRealName = user?.IsRealName ?? false,
IsPhotoPublic = profile?.IsPhotoPublic ?? false,
FirstPhoto = firstPhoto?.PhotoUrl,
CreateTime = item.CreateTime
});
}
@ -413,14 +493,29 @@ public class InteractService : IInteractService
{
var user = await _userRepository.GetByIdAsync(item.UserId);
var profile = (await _profileRepository.GetListAsync(p => p.UserId == item.UserId)).FirstOrDefault();
var photos = await _photoRepository.GetListAsync(p => p.UserId == item.UserId);
var firstPhoto = photos.OrderBy(p => p.Sort).FirstOrDefault();
responses.Add(new UnlockRecordResponse
{
UserId = item.UserId,
Nickname = user?.Nickname,
Avatar = user?.Avatar,
Gender = profile?.ChildGender ?? user?.Gender,
Age = profile?.BirthYear != null ? DateTime.Now.Year - profile.BirthYear : null,
BirthYear = profile?.BirthYear,
WorkCity = profile?.WorkCity,
Hometown = profile?.HomeCity,
Height = profile?.Height,
Weight = profile?.Weight,
Education = profile?.Education,
EducationName = GetEducationName(profile?.Education),
Occupation = profile?.Occupation,
Intro = profile?.Introduction,
IsMember = user?.IsMember ?? false,
IsRealName = user?.IsRealName ?? false,
IsPhotoPublic = profile?.IsPhotoPublic ?? false,
FirstPhoto = firstPhoto?.PhotoUrl,
CreateTime = item.CreateTime
});
}
@ -449,14 +544,29 @@ public class InteractService : IInteractService
{
var user = await _userRepository.GetByIdAsync(item.TargetUserId);
var profile = (await _profileRepository.GetListAsync(p => p.UserId == item.TargetUserId)).FirstOrDefault();
var photos = await _photoRepository.GetListAsync(p => p.UserId == item.TargetUserId);
var firstPhoto = photos.OrderBy(p => p.Sort).FirstOrDefault();
responses.Add(new UnlockRecordResponse
{
UserId = item.TargetUserId,
Nickname = user?.Nickname,
Avatar = user?.Avatar,
Gender = profile?.ChildGender ?? user?.Gender,
Age = profile?.BirthYear != null ? DateTime.Now.Year - profile.BirthYear : null,
BirthYear = profile?.BirthYear,
WorkCity = profile?.WorkCity,
Hometown = profile?.HomeCity,
Height = profile?.Height,
Weight = profile?.Weight,
Education = profile?.Education,
EducationName = GetEducationName(profile?.Education),
Occupation = profile?.Occupation,
Intro = profile?.Introduction,
IsMember = user?.IsMember ?? false,
IsRealName = user?.IsRealName ?? false,
IsPhotoPublic = profile?.IsPhotoPublic ?? false,
FirstPhoto = firstPhoto?.PhotoUrl,
CreateTime = item.CreateTime
});
}