细节优化.
|
|
@ -5,3 +5,6 @@ VITE_APP_TITLE=相宜相亲后台管理系统(开发)
|
|||
|
||||
# API基础地址
|
||||
VITE_API_BASE_URL=http://localhost:5000/api
|
||||
|
||||
# 静态资源服务器地址(AppApi,用于图片等静态资源)
|
||||
VITE_STATIC_BASE_URL=http://localhost:5001
|
||||
|
|
|
|||
|
|
@ -5,3 +5,6 @@ VITE_APP_TITLE=相宜相亲后台管理系统
|
|||
|
||||
# API基础地址 - 生产环境请修改为实际地址
|
||||
VITE_API_BASE_URL=https://api.example.com
|
||||
|
||||
# 静态资源服务器地址 - 生产环境请修改为实际地址
|
||||
VITE_STATIC_BASE_URL=https://static.example.com
|
||||
|
|
|
|||
22
admin/src/api/config.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取默认头像配置
|
||||
*/
|
||||
export function getDefaultAvatar() {
|
||||
return request.get('/admin/config/defaultAvatar')
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认头像
|
||||
*/
|
||||
export function setDefaultAvatar(avatarUrl: string) {
|
||||
return request.post('/admin/config/defaultAvatar', { avatarUrl })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有系统配置
|
||||
*/
|
||||
export function getAllConfigs() {
|
||||
return request.get('/admin/config/all')
|
||||
}
|
||||
|
|
@ -57,3 +57,12 @@ export function getUserStatistics(): Promise<UserStatistics> {
|
|||
export function createTestUsers(count: number, gender?: number): Promise<number[]> {
|
||||
return request.post('/admin/users/test-users', { count, gender })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户(硬删除)
|
||||
* @param id 用户ID
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function deleteUser(id: number): Promise<void> {
|
||||
return request.delete(`/admin/users/${id}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,6 +234,12 @@ export const asyncRoutes: RouteRecordRaw[] = [
|
|||
name: 'OperationLog',
|
||||
component: () => import('@/views/system/log.vue'),
|
||||
meta: { title: '操作日志' }
|
||||
},
|
||||
{
|
||||
path: 'config',
|
||||
name: 'SystemConfig',
|
||||
component: () => import('@/views/system/config.vue'),
|
||||
meta: { title: '系统配置' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@
|
|||
* 图片URL处理工具
|
||||
*/
|
||||
|
||||
// 获取后端基础地址(去掉 /api 后缀)
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || ''
|
||||
const SERVER_BASE = API_BASE.replace(/\/api$/, '')
|
||||
// 获取静态资源服务器地址(AppApi,用于图片等静态资源)
|
||||
const STATIC_BASE = import.meta.env.VITE_STATIC_BASE_URL || 'http://localhost:5001'
|
||||
|
||||
/**
|
||||
* 处理图片URL,将相对路径转换为完整URL
|
||||
|
|
@ -17,8 +16,8 @@ export function getFullImageUrl(url: string | undefined | null): string {
|
|||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url
|
||||
}
|
||||
// 相对路径,拼接服务器地址
|
||||
return `${SERVER_BASE}${url.startsWith('/') ? '' : '/'}${url}`
|
||||
// 相对路径,拼接静态资源服务器地址
|
||||
return `${STATIC_BASE}${url.startsWith('/') ? '' : '/'}${url}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
202
admin/src/views/system/config.vue
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
<template>
|
||||
<div class="config-container">
|
||||
<el-card class="config-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>系统配置</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="configForm" label-width="120px" class="config-form">
|
||||
<!-- 默认头像设置 -->
|
||||
<el-form-item label="默认头像">
|
||||
<div class="avatar-upload">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleAvatarSuccess"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img v-if="configForm.defaultAvatar" :src="getFullUrl(configForm.defaultAvatar)" class="avatar" />
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<div class="avatar-tip">
|
||||
<p>建议尺寸:200x200像素</p>
|
||||
<p>支持格式:JPG、PNG</p>
|
||||
<p>新用户注册时将使用此头像作为默认头像</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveConfig" :loading="saving">
|
||||
保存配置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { getDefaultAvatar, setDefaultAvatar } from '@/api/config'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api'
|
||||
// 去掉 /api 后缀获取服务器根地址
|
||||
const serverUrl = apiBaseUrl.replace(/\/api$/, '')
|
||||
|
||||
const configForm = ref({
|
||||
defaultAvatar: ''
|
||||
})
|
||||
|
||||
const saving = ref(false)
|
||||
|
||||
const uploadUrl = computed(() => `${apiBaseUrl}/admin/upload`)
|
||||
|
||||
const uploadHeaders = computed(() => ({
|
||||
Authorization: `Bearer ${userStore.token}`
|
||||
}))
|
||||
|
||||
const getFullUrl = (url) => {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http')) return url
|
||||
return `${serverUrl}${url}`
|
||||
}
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const res = await getDefaultAvatar()
|
||||
// request 拦截器已经处理了 code,直接返回 data
|
||||
if (res) {
|
||||
configForm.value.defaultAvatar = res.avatarUrl || ''
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAvatarSuccess = (response) => {
|
||||
if (response.code === 0 && response.data) {
|
||||
configForm.value.defaultAvatar = response.data.url
|
||||
ElMessage.success('上传成功')
|
||||
} else {
|
||||
ElMessage.error(response.message || '上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const beforeAvatarUpload = (file) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const saveConfig = async () => {
|
||||
if (!configForm.value.defaultAvatar) {
|
||||
ElMessage.warning('请先上传默认头像')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
// request 拦截器会处理错误,成功时直接返回
|
||||
await setDefaultAvatar(configForm.value.defaultAvatar)
|
||||
ElMessage.success('保存成功')
|
||||
} catch (error) {
|
||||
// 错误已在拦截器中处理
|
||||
console.error('保存失败:', error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.config-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.config-card {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.config-form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.avatar-upload {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.avatar-uploader :deep(.el-upload) {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-uploader :deep(.el-upload:hover) {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.avatar-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-tip {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.avatar-tip p {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -89,6 +89,78 @@ const formatMoney = (amount: number) => {
|
|||
return `¥${amount.toFixed(2)}`
|
||||
}
|
||||
|
||||
// 学历映射
|
||||
const educationMap: Record<number, string> = {
|
||||
1: '高中',
|
||||
2: '中专',
|
||||
3: '大专',
|
||||
4: '本科',
|
||||
5: '研究生',
|
||||
6: '博士及以上'
|
||||
}
|
||||
|
||||
// 婚姻状态映射
|
||||
const marriageStatusMap: Record<number, string> = {
|
||||
1: '未婚',
|
||||
2: '离异未育',
|
||||
3: '离异已育'
|
||||
}
|
||||
|
||||
// 格式化学历数组
|
||||
const formatEducation = (education: string | number[] | undefined) => {
|
||||
if (!education) return '-'
|
||||
|
||||
// 如果是字符串,尝试解析为数组
|
||||
let eduArray: number[] = []
|
||||
if (typeof education === 'string') {
|
||||
try {
|
||||
eduArray = JSON.parse(education)
|
||||
} catch {
|
||||
return education || '-'
|
||||
}
|
||||
} else if (Array.isArray(education)) {
|
||||
eduArray = education
|
||||
}
|
||||
|
||||
if (!eduArray || eduArray.length === 0) return '-'
|
||||
return eduArray.map(e => educationMap[e] || e).join('、')
|
||||
}
|
||||
|
||||
// 格式化婚姻状态数组
|
||||
const formatMarriageStatus = (status: string | number[] | undefined) => {
|
||||
if (!status) return '-'
|
||||
|
||||
// 如果是字符串,尝试解析为数组
|
||||
let statusArray: number[] = []
|
||||
if (typeof status === 'string') {
|
||||
try {
|
||||
statusArray = JSON.parse(status)
|
||||
} catch {
|
||||
return status || '-'
|
||||
}
|
||||
} else if (Array.isArray(status)) {
|
||||
statusArray = status
|
||||
}
|
||||
|
||||
if (!statusArray || statusArray.length === 0) return '-'
|
||||
return statusArray.map(s => marriageStatusMap[s] || s).join('、')
|
||||
}
|
||||
|
||||
// 格式化月收入
|
||||
const formatIncome = (min: number | undefined, max: number | undefined) => {
|
||||
const incomeMap: Record<number, string> = {
|
||||
1: '5000以下',
|
||||
2: '5000-10000',
|
||||
3: '10000-20000',
|
||||
4: '20000-50000',
|
||||
5: '50000以上'
|
||||
}
|
||||
// 优先显示最小值(期望最低收入)
|
||||
if (min) return incomeMap[min] || '-'
|
||||
if (max) return incomeMap[max] || '-'
|
||||
return '不限'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchUserDetail()
|
||||
})
|
||||
|
|
@ -193,7 +265,7 @@ onMounted(() => {
|
|||
{{ userDetail.genderText || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="城市">
|
||||
{{ userDetail.city || '-' }}
|
||||
{{ userDetail.profile?.workCity || userDetail.city || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="会员等级">
|
||||
<el-tag
|
||||
|
|
@ -364,16 +436,16 @@ onMounted(() => {
|
|||
{{ userDetail.requirement.heightMin || '-' }} ~ {{ userDetail.requirement.heightMax || '-' }}cm
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="期望学历">
|
||||
{{ userDetail.requirement.education || '-' }}
|
||||
{{ formatEducation(userDetail.requirement.education) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="期望工作城市">
|
||||
{{ userDetail.requirement.workCity || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="期望月收入">
|
||||
{{ userDetail.requirement.incomeMin || '-' }} ~ {{ userDetail.requirement.incomeMax || '-' }}
|
||||
{{ formatIncome(userDetail.requirement.incomeMin, userDetail.requirement.incomeMax) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="期望婚姻状态">
|
||||
{{ userDetail.requirement.marriageStatus || '-' }}
|
||||
{{ formatMarriageStatus(userDetail.requirement.marriageStatus) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="是否要求有房">
|
||||
{{ userDetail.requirement.requireHouse ? '是' : '否' }}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { View, Edit, Plus } from '@element-plus/icons-vue'
|
||||
import { View, Edit, Plus, Delete } from '@element-plus/icons-vue'
|
||||
import SearchForm from '@/components/SearchForm/index.vue'
|
||||
import Pagination from '@/components/Pagination/index.vue'
|
||||
import StatusTag from '@/components/StatusTag/index.vue'
|
||||
import { getUserList, updateUserStatus, createTestUsers } from '@/api/user'
|
||||
import { getUserList, updateUserStatus, createTestUsers, deleteUser } from '@/api/user'
|
||||
import { getFullImageUrl } from '@/utils/image'
|
||||
import type { UserListItem, UserQueryParams } from '@/types/user.d'
|
||||
|
||||
|
|
@ -151,6 +151,29 @@ const handleToggleStatus = async (row: UserListItem) => {
|
|||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const handleDeleteUser = async (row: UserListItem) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除用户「${row.nickname || row.xiangQinNo}」吗?此操作不可恢复!`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}
|
||||
)
|
||||
|
||||
await deleteUser(row.userId)
|
||||
ElMessage.success('删除成功')
|
||||
fetchUserList()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除用户失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return '-'
|
||||
|
|
@ -438,7 +461,7 @@ onMounted(() => {
|
|||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
width="240"
|
||||
fixed="right"
|
||||
align="center"
|
||||
>
|
||||
|
|
@ -459,6 +482,14 @@ onMounted(() => {
|
|||
>
|
||||
{{ row.status === 1 ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
:icon="Delete"
|
||||
@click="handleDeleteUser(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import { get, post, del } from './request'
|
|||
import { getToken } from '../utils/storage'
|
||||
|
||||
// API 基础地址
|
||||
const BASE_URL = 'http://localhost:5000/api/app'
|
||||
const BASE_URL = 'http://localhost:5001/api/app'
|
||||
// 静态资源服务器地址
|
||||
const STATIC_URL = 'http://localhost:5001'
|
||||
|
||||
/**
|
||||
* 提交/更新用户资料
|
||||
|
|
@ -59,54 +61,79 @@ export async function getMyProfile() {
|
|||
* Requirements: 4.4
|
||||
*
|
||||
* @param {Array<string>} filePaths - 本地文件路径数组
|
||||
* @returns {Promise<Object>} 上传结果
|
||||
* @returns {Promise<Object>} 上传结果,包含 photos 数组 [{url: string}]
|
||||
*/
|
||||
export async function uploadPhotos(filePaths) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = getToken()
|
||||
|
||||
// 使用 uni.uploadFile 上传文件
|
||||
const uploadTasks = filePaths.map((filePath, index) => {
|
||||
return new Promise((res, rej) => {
|
||||
const token = getToken()
|
||||
|
||||
// 逐个上传文件(uni.uploadFile 不支持一次上传多个文件)
|
||||
const uploadedPhotos = []
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
try {
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${BASE_URL}/profile/photos`,
|
||||
filePath: filePath,
|
||||
name: 'files',
|
||||
name: 'files', // 后端期望的参数名
|
||||
header: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
success: (uploadRes) => {
|
||||
console.log('上传响应:', uploadRes)
|
||||
if (uploadRes.statusCode === 200) {
|
||||
try {
|
||||
const data = JSON.parse(uploadRes.data)
|
||||
res(data)
|
||||
// 后端返回格式: { code: 0, message: "success", data: {...} }
|
||||
if (data.code === 0 && data.data) {
|
||||
resolve(data.data)
|
||||
} else {
|
||||
reject(new Error(data.message || '上传失败'))
|
||||
}
|
||||
} catch (e) {
|
||||
rej(new Error('解析响应失败'))
|
||||
console.error('解析响应失败:', e, uploadRes.data)
|
||||
reject(new Error('解析响应失败'))
|
||||
}
|
||||
} else if (uploadRes.statusCode === 401) {
|
||||
rej(new Error('未授权,请重新登录'))
|
||||
reject(new Error('未授权,请重新登录'))
|
||||
} else {
|
||||
rej(new Error('上传失败'))
|
||||
// 尝试解析错误信息
|
||||
try {
|
||||
const errData = JSON.parse(uploadRes.data)
|
||||
reject(new Error(errData.message || '上传失败'))
|
||||
} catch {
|
||||
reject(new Error(`上传失败: ${uploadRes.statusCode}`))
|
||||
}
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
rej(new Error('网络连接失败'))
|
||||
console.error('上传网络错误:', err)
|
||||
reject(new Error('网络连接失败'))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// 并行上传所有文件
|
||||
Promise.all(uploadTasks)
|
||||
.then(results => {
|
||||
// 合并所有上传结果
|
||||
const photos = results.flatMap(r => r.data?.photos || [])
|
||||
resolve({ success: true, data: { photos } })
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
|
||||
// 后端返回 { photos: [{id, photoUrl}], totalCount: number }
|
||||
// 转换为前端需要的格式,拼接完整URL
|
||||
if (result.photos && result.photos.length > 0) {
|
||||
result.photos.forEach(photo => {
|
||||
// 如果是相对路径,拼接服务器地址
|
||||
const fullUrl = photo.photoUrl.startsWith('http') ? photo.photoUrl : `${STATIC_URL}${photo.photoUrl}`
|
||||
uploadedPhotos.push({ id: photo.id, url: fullUrl })
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传照片失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
photos: uploadedPhotos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -120,9 +147,33 @@ export async function deletePhoto(photoId) {
|
|||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理未提交资料的孤立照片
|
||||
* 进入填写资料页面时调用,如果用户没有Profile记录,清理之前上传的照片
|
||||
*
|
||||
* @returns {Promise<Object>} 清理结果
|
||||
*/
|
||||
export async function cleanupOrphanPhotos() {
|
||||
const response = await post('/profile/photos/cleanup')
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新头像
|
||||
*
|
||||
* @param {string} avatarUrl - 头像URL
|
||||
* @returns {Promise<Object>} 更新结果
|
||||
*/
|
||||
export async function updateAvatar(avatarUrl) {
|
||||
const response = await post('/profile/avatar', { avatarUrl })
|
||||
return response
|
||||
}
|
||||
|
||||
export default {
|
||||
createOrUpdate,
|
||||
getMyProfile,
|
||||
uploadPhotos,
|
||||
deletePhoto
|
||||
deletePhoto,
|
||||
cleanupOrphanPhotos,
|
||||
updateAvatar
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ import { get, post } from './request'
|
|||
* WHEN a user visits the home page, THE XiangYi_MiniApp SHALL display recommended user list
|
||||
* Requirements: 2.3
|
||||
*
|
||||
* @param {number} pageIndex - 页码
|
||||
* @param {number} pageSize - 每页数量
|
||||
* @returns {Promise<Object>} 推荐用户列表
|
||||
*/
|
||||
export async function getRecommend() {
|
||||
const response = await get('/users/recommend')
|
||||
export async function getRecommend(pageIndex = 1, pageSize = 10) {
|
||||
const response = await get('/users/recommend', { pageIndex, pageSize }, { needAuth: false })
|
||||
return response
|
||||
}
|
||||
|
||||
|
|
@ -57,8 +59,44 @@ export async function search(params = {}) {
|
|||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户头像
|
||||
*
|
||||
* @param {string} avatar - 头像URL
|
||||
* @returns {Promise<Object>} 更新结果
|
||||
*/
|
||||
export async function updateAvatar(avatar) {
|
||||
const response = await post('/users/avatar', { avatar })
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户昵称
|
||||
*
|
||||
* @param {string} nickname - 昵称
|
||||
* @returns {Promise<Object>} 更新结果
|
||||
*/
|
||||
export async function updateNickname(nickname) {
|
||||
const response = await post('/users/nickname', { nickname })
|
||||
return response
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密微信手机号
|
||||
*
|
||||
* @param {string} code - 微信返回的 code
|
||||
* @returns {Promise<Object>} 解密结果,包含 phone 字段
|
||||
*/
|
||||
export async function decryptPhone(code) {
|
||||
const response = await post('/users/phone/decrypt', { code })
|
||||
return response
|
||||
}
|
||||
|
||||
export default {
|
||||
getRecommend,
|
||||
getUserDetail,
|
||||
search
|
||||
search,
|
||||
updateAvatar,
|
||||
updateNickname,
|
||||
decryptPhone
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,56 +1,125 @@
|
|||
<template>
|
||||
<view class="user-card" @click="handleCardClick">
|
||||
<!-- 照片区域 -->
|
||||
<view class="photo-wrapper">
|
||||
<image
|
||||
v-if="isPhotoPublic && firstPhoto"
|
||||
class="user-photo"
|
||||
:src="firstPhoto"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="photo-placeholder">
|
||||
<view class="blur-overlay"></view>
|
||||
<text class="private-text">私密照片</text>
|
||||
<!-- 今天看过标识 -->
|
||||
<view v-if="viewedToday" class="viewed-tag">
|
||||
<text>今天看过</text>
|
||||
</view>
|
||||
|
||||
<!-- 不公开照片样式:两列信息布局 -->
|
||||
<view v-if="!isPhotoPublic || !firstPhoto" class="card-no-photo">
|
||||
<!-- 标题行 -->
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 徽章区域 -->
|
||||
<view class="badges">
|
||||
<view v-if="isMember" class="badge member-badge">会员</view>
|
||||
<view v-if="isRealName" class="badge realname-badge">已实名</view>
|
||||
|
||||
<!-- 信息区域:两列布局 -->
|
||||
<view class="info-grid">
|
||||
<view class="info-item">
|
||||
<text class="label">年龄</text>
|
||||
<text class="value">{{ age }}岁</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">身高</text>
|
||||
<text class="value">{{ height }}cm</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">学历</text>
|
||||
<text class="value">{{ educationName }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">体重</text>
|
||||
<text class="value">{{ weight ? weight + 'kg' : '未填写' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">收入</text>
|
||||
<text class="value">{{ incomeText }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">职业</text>
|
||||
<text class="value">{{ occupation || '未填写' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">现居</text>
|
||||
<text class="value">{{ workCity || '未填写' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">家乡</text>
|
||||
<text class="value">{{ hometown || '未填写' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 今天看过标识 -->
|
||||
<view v-if="viewedToday" class="viewed-today">
|
||||
<text>今天看过</text>
|
||||
|
||||
<!-- 简介 -->
|
||||
<view class="intro-section" v-if="intro">
|
||||
<text class="intro-text">{{ intro }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-buttons" @click.stop>
|
||||
<button class="btn-detail" @click="handleCardClick">查看详细资料</button>
|
||||
<button class="btn-contact" @click="handleContact">联系对方</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户信息区域 -->
|
||||
<view class="user-info">
|
||||
<view class="info-row">
|
||||
<text class="nickname">{{ nickname }}</text>
|
||||
<text class="age">{{ age }}岁</text>
|
||||
|
||||
<!-- 公开照片样式:左信息右照片 -->
|
||||
<view v-else class="card-with-photo">
|
||||
<!-- 标题行 -->
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-row secondary">
|
||||
<text class="location">{{ workCity }}</text>
|
||||
<text class="divider">|</text>
|
||||
<text class="height">{{ height }}cm</text>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-row">
|
||||
<!-- 左侧信息 -->
|
||||
<view class="info-left">
|
||||
<view class="info-item">
|
||||
<text class="label">现居</text>
|
||||
<text class="value">{{ workCity || '未填写' }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">学历</text>
|
||||
<text class="value">{{ educationName }}</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">身高</text>
|
||||
<text class="value">{{ height }}cm</text>
|
||||
</view>
|
||||
<view class="info-item">
|
||||
<text class="label">职业</text>
|
||||
<text class="value">{{ occupation || '未填写' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧照片 -->
|
||||
<view class="photo-right">
|
||||
<image class="user-photo" :src="fullPhotoUrl" mode="aspectFill" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="info-row secondary">
|
||||
<text class="education">{{ educationName }}</text>
|
||||
<text class="divider">|</text>
|
||||
<text class="occupation">{{ occupation }}</text>
|
||||
|
||||
<!-- 简介 -->
|
||||
<view class="intro-section" v-if="intro">
|
||||
<text class="intro-text">{{ intro }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-buttons" @click.stop>
|
||||
<button class="btn-detail" @click="handleCardClick">查看详细资料</button>
|
||||
<button class="btn-contact" @click="handleContact">联系对方</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮区域 -->
|
||||
<view class="action-buttons" @click.stop>
|
||||
<button class="btn-contact" @click="handleContact">联系TA</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getFullImageUrl } from '@/utils/image.js'
|
||||
|
||||
export default {
|
||||
name: 'UserCard',
|
||||
props: {
|
||||
|
|
@ -66,18 +135,34 @@ export default {
|
|||
type: String,
|
||||
default: ''
|
||||
},
|
||||
gender: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
age: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
birthYear: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
workCity: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hometown: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
weight: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
education: {
|
||||
type: Number,
|
||||
default: 0
|
||||
|
|
@ -90,6 +175,14 @@ export default {
|
|||
type: String,
|
||||
default: ''
|
||||
},
|
||||
monthlyIncome: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
intro: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isMember: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
|
|
@ -112,6 +205,24 @@ export default {
|
|||
}
|
||||
},
|
||||
emits: ['click', 'contact'],
|
||||
computed: {
|
||||
genderText() {
|
||||
return this.gender === 1 ? '男' : '女'
|
||||
},
|
||||
fullPhotoUrl() {
|
||||
return getFullImageUrl(this.firstPhoto)
|
||||
},
|
||||
incomeText() {
|
||||
if (!this.monthlyIncome) return '未填写'
|
||||
if (this.monthlyIncome < 5000) return '5千以下'
|
||||
if (this.monthlyIncome < 8000) return '5千~8千/月'
|
||||
if (this.monthlyIncome < 10000) return '8千~1万/月'
|
||||
if (this.monthlyIncome < 15000) return '1万~1.5万/月'
|
||||
if (this.monthlyIncome < 20000) return '1.5万~2万/月'
|
||||
if (this.monthlyIncome < 30000) return '2万~3万/月'
|
||||
return '3万以上/月'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleCardClick() {
|
||||
this.$emit('click', this.userId)
|
||||
|
|
@ -126,139 +237,187 @@ export default {
|
|||
<style lang="scss" scoped>
|
||||
.user-card {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.photo-wrapper {
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 24rpx;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400rpx;
|
||||
|
||||
.user-photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.photo-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #e0e0e0 0%, #c0c0c0 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
.blur-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.private-text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.badges {
|
||||
position: absolute;
|
||||
top: 16rpx;
|
||||
left: 16rpx;
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
|
||||
.badge {
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.member-badge {
|
||||
background: linear-gradient(135deg, #ffd700 0%, #ffb800 100%);
|
||||
}
|
||||
|
||||
.realname-badge {
|
||||
background: linear-gradient(135deg, #4cd964 0%, #34c759 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.viewed-today {
|
||||
position: absolute;
|
||||
bottom: 16rpx;
|
||||
right: 16rpx;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
|
||||
text {
|
||||
color: #fff;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
}
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
padding: 20rpx;
|
||||
// 今天看过标签
|
||||
.viewed-tag {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
right: 24rpx;
|
||||
background: #ff6b6b;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: #999;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.age {
|
||||
font-size: 28rpx;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 0 8rpx;
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
padding: 0 20rpx 20rpx;
|
||||
|
||||
.btn-contact {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%);
|
||||
text {
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 标题行
|
||||
.title-row {
|
||||
margin-bottom: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
|
||||
.gender-year {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.title-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
|
||||
.tag {
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
|
||||
&.tag-realname {
|
||||
background: #e8f5e9;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
&.tag-member {
|
||||
background: linear-gradient(135deg, #fff3e0 0%, #ffe0b2 100%);
|
||||
color: #ff9800;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 不公开照片样式
|
||||
.card-no-photo {
|
||||
.info-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.info-item {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
width: 80rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 公开照片样式
|
||||
.card-with-photo {
|
||||
.content-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.info-left {
|
||||
flex: 1;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
width: 80rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.photo-right {
|
||||
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;
|
||||
border-radius: 36rpx;
|
||||
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 #ddd;
|
||||
}
|
||||
|
||||
.btn-contact {
|
||||
background: linear-gradient(135deg, #ff8a8a 0%, #ff6b6b 100%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name" : "src",
|
||||
"name" : "相宜亲家",
|
||||
"appid" : "__UNI__39EAECC",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "",
|
||||
"appid" : "wx21b4110b18b31831",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,31 +4,44 @@
|
|||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "相宜相亲"
|
||||
"navigationBarTitleText": "相宜相亲",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/message/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "消息"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mine/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/edit",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "填写资料"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/personal",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "个人资料",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "用户详情"
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "详情资料"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -96,6 +109,13 @@
|
|||
"style": {
|
||||
"navigationBarTitleText": "我解锁的"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
|
|
|||
|
|
@ -52,9 +52,6 @@
|
|||
<!-- 推荐标题栏 -->
|
||||
<view class="section-header">
|
||||
<text class="section-title">今日推荐</text>
|
||||
<view class="section-more" @click="handleRefreshRecommend">
|
||||
<text>换一批</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
|
@ -73,16 +70,25 @@
|
|||
</view>
|
||||
|
||||
<!-- 可滚动的用户列表区域 -->
|
||||
<scroll-view class="user-scroll-area" scroll-y :style="{ height: scrollHeight }"
|
||||
@scrolltolower="handleScrollToLower">
|
||||
<scroll-view
|
||||
class="user-scroll-area"
|
||||
scroll-y
|
||||
:style="{ height: scrollHeight }"
|
||||
refresher-enabled
|
||||
:refresher-triggered="isRefreshing"
|
||||
@refresherrefresh="handleRefresh"
|
||||
@scrolltolower="handleScrollToLower"
|
||||
>
|
||||
<!-- 用户列表 -->
|
||||
<view class="user-list" v-if="recommendList.length > 0">
|
||||
<UserCard v-for="user in recommendList" :key="user.userId" :userId="user.userId"
|
||||
:nickname="user.nickname" :avatar="user.avatar" :age="user.age" :workCity="user.workCity"
|
||||
:height="user.height" :education="user.education" :educationName="user.educationName"
|
||||
:occupation="user.occupation" :isMember="user.isMember" :isRealName="user.isRealName"
|
||||
:isPhotoPublic="user.isPhotoPublic" :firstPhoto="user.firstPhoto" :viewedToday="user.viewedToday"
|
||||
@click="handleUserClick" @contact="handleUserContact" />
|
||||
: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="handleUserClick" @contact="handleUserContact" />
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
|
|
@ -140,6 +146,12 @@
|
|||
const listLoading = ref(false)
|
||||
const noMoreData = ref(false)
|
||||
const scrollHeight = ref('300px')
|
||||
const isRefreshing = ref(false)
|
||||
|
||||
// 分页参数
|
||||
const pageIndex = ref(1)
|
||||
const pageSize = 10
|
||||
const total = ref(0)
|
||||
|
||||
// 数据
|
||||
const recommendList = ref([])
|
||||
|
|
@ -228,13 +240,32 @@
|
|||
}
|
||||
|
||||
// 加载推荐用户列表
|
||||
const loadRecommendList = async () => {
|
||||
const loadRecommendList = async (isLoadMore = false) => {
|
||||
if (listLoading.value) return
|
||||
if (isLoadMore && noMoreData.value) return
|
||||
|
||||
listLoading.value = true
|
||||
try {
|
||||
const res = await getRecommend()
|
||||
if (res && res.code === 0) {
|
||||
recommendList.value = res.data?.list || res.data || []
|
||||
noMoreData.value = true // 推荐列表一次性加载
|
||||
if (!isLoadMore) {
|
||||
pageIndex.value = 1
|
||||
recommendList.value = []
|
||||
noMoreData.value = false
|
||||
}
|
||||
|
||||
const res = await getRecommend(pageIndex.value, pageSize)
|
||||
if (res && res.code === 0 && res.data) {
|
||||
const items = res.data.items || []
|
||||
total.value = res.data.total || 0
|
||||
|
||||
if (isLoadMore) {
|
||||
recommendList.value = [...recommendList.value, ...items]
|
||||
} else {
|
||||
recommendList.value = items
|
||||
}
|
||||
|
||||
// 判断是否还有更多数据
|
||||
noMoreData.value = recommendList.value.length >= total.value
|
||||
pageIndex.value++
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载推荐列表失败:', error)
|
||||
|
|
@ -266,10 +297,8 @@
|
|||
loadMemberAdConfig()
|
||||
])
|
||||
|
||||
// 如果已登录,加载推荐列表
|
||||
if (userStore.isLoggedIn) {
|
||||
await loadRecommendList()
|
||||
}
|
||||
// 加载推荐列表(无论是否登录都加载)
|
||||
await loadRecommendList()
|
||||
|
||||
// 检查弹窗显示
|
||||
checkPopups()
|
||||
|
|
@ -435,10 +464,21 @@
|
|||
})
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
// 滚动到底部 - 加载更多
|
||||
const handleScrollToLower = () => {
|
||||
// 可以在这里加载更多数据
|
||||
console.log('滚动到底部')
|
||||
if (!noMoreData.value && !listLoading.value) {
|
||||
loadRecommendList(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
isRefreshing.value = true
|
||||
try {
|
||||
await loadRecommendList(false)
|
||||
} finally {
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 页面显示时检查弹窗
|
||||
|
|
@ -451,6 +491,7 @@
|
|||
listLoading,
|
||||
noMoreData,
|
||||
scrollHeight,
|
||||
isRefreshing,
|
||||
banners,
|
||||
kingKongs,
|
||||
showMemberAd,
|
||||
|
|
@ -460,6 +501,8 @@
|
|||
showDailyPopup,
|
||||
dailyPopup,
|
||||
recommendList,
|
||||
initPage,
|
||||
loadRecommendList,
|
||||
handleSearchClick,
|
||||
handleBannerClick,
|
||||
handleKingKongClick,
|
||||
|
|
@ -471,14 +514,16 @@
|
|||
handleRefreshRecommend,
|
||||
handleUserClick,
|
||||
handleUserContact,
|
||||
handleScrollToLower
|
||||
handleScrollToLower,
|
||||
handleRefresh
|
||||
}
|
||||
},
|
||||
// 下拉刷新
|
||||
onPullDownRefresh() {
|
||||
this.initPage && this.initPage().finally(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
async onPullDownRefresh() {
|
||||
if (this.loadRecommendList) {
|
||||
await this.loadRecommendList()
|
||||
}
|
||||
uni.stopPullDownRefresh()
|
||||
},
|
||||
// 页面显示
|
||||
onShow() {
|
||||
|
|
|
|||
267
miniapp/pages/login/index.vue
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
<template>
|
||||
<view class="login-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<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">登录</text>
|
||||
<view class="navbar-placeholder"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Logo区域 -->
|
||||
<view class="logo-section" :style="{ marginTop: (statusBarHeight + 44) + 'px' }">
|
||||
<view class="logo-wrapper">
|
||||
<image src="/static/logo.png" mode="aspectFit" class="logo-img" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部区域 -->
|
||||
<view class="bottom-section">
|
||||
<!-- 登录按钮 -->
|
||||
<button class="btn-login" @click="handleLogin">一键注册/登录</button>
|
||||
|
||||
<!-- 协议勾选 -->
|
||||
<view class="agreement-row" @click="toggleAgreement">
|
||||
<image
|
||||
:src="isAgreed ? '/static/ic_check_s.png' : '/static/ic_check.png'"
|
||||
mode="aspectFit"
|
||||
class="check-icon"
|
||||
/>
|
||||
<text class="agreement-text">
|
||||
注册即同意<text class="link" @click.stop="handleUserAgreement">《用户协议》</text>和<text class="link" @click.stop="handlePrivacyPolicy">《隐私政策》</text>
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/store/user.js'
|
||||
import { login } from '@/api/auth.js'
|
||||
|
||||
export default {
|
||||
name: 'LoginPage',
|
||||
setup() {
|
||||
const userStore = useUserStore()
|
||||
const statusBarHeight = ref(20)
|
||||
const isAgreed = ref(false)
|
||||
|
||||
// 获取系统信息
|
||||
const getSystemInfo = () => {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
statusBarHeight.value = res.statusBarHeight || 20
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
// 切换协议勾选
|
||||
const toggleAgreement = () => {
|
||||
isAgreed.value = !isAgreed.value
|
||||
}
|
||||
|
||||
// 微信登录
|
||||
const handleLogin = async () => {
|
||||
if (!isAgreed.value) {
|
||||
uni.showToast({ title: '请先同意用户协议和隐私政策', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
|
||||
if (loginRes.code) {
|
||||
uni.showLoading({ title: '登录中...' })
|
||||
const res = await login(loginRes.code)
|
||||
uni.hideLoading()
|
||||
|
||||
if (res && res.code === 0 && res.data) {
|
||||
userStore.login({
|
||||
token: res.data.token,
|
||||
userInfo: {
|
||||
userId: res.data.userId,
|
||||
nickname: res.data.nickname,
|
||||
avatar: res.data.avatar,
|
||||
xiangQinNo: res.data.xiangQinNo,
|
||||
isProfileCompleted: res.data.isProfileCompleted,
|
||||
isMember: res.data.isMember,
|
||||
memberLevel: res.data.memberLevel,
|
||||
isRealName: res.data.isRealName
|
||||
}
|
||||
})
|
||||
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||
// 清除所有页面,返回首页
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/index/index' })
|
||||
}, 1000)
|
||||
} else {
|
||||
uni.showToast({ title: res?.message || '登录失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('登录失败:', error)
|
||||
uni.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
// 用户协议
|
||||
const handleUserAgreement = () => {
|
||||
uni.showToast({ title: '功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
// 隐私政策
|
||||
const handlePrivacyPolicy = () => {
|
||||
uni.showToast({ title: '功能开发中', icon: 'none' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSystemInfo()
|
||||
})
|
||||
|
||||
return {
|
||||
statusBarHeight,
|
||||
isAgreed,
|
||||
handleBack,
|
||||
toggleAgreement,
|
||||
handleLogin,
|
||||
handleUserAgreement,
|
||||
handlePrivacyPolicy
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f6fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// 自定义导航栏
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background-color: #f5f6fa;
|
||||
|
||||
.navbar-content {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24rpx;
|
||||
|
||||
.navbar-back {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.back-icon {
|
||||
font-size: 48rpx;
|
||||
color: #333;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.navbar-placeholder {
|
||||
width: 60rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Logo区域
|
||||
.logo-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding-top: 120rpx;
|
||||
|
||||
.logo-wrapper {
|
||||
width: 360rpx;
|
||||
height: 360rpx;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
|
||||
.logo-img {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部区域
|
||||
.bottom-section {
|
||||
padding: 60rpx 48rpx 80rpx;
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
background: linear-gradient(135deg, #FFBDC2 0%, #FF8A93 100%);
|
||||
border-radius: 48rpx;
|
||||
font-size: 34rpx;
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-weight: 500;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 32rpx;
|
||||
|
||||
.check-icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
|
||||
.link {
|
||||
color: #FF8A93;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,106 +2,147 @@
|
|||
<view class="message-page">
|
||||
<!-- 页面加载状态 -->
|
||||
<Loading type="page" :loading="pageLoading" />
|
||||
|
||||
<!-- 互动统计区域 -->
|
||||
<view class="interact-section">
|
||||
<view class="interact-grid">
|
||||
<view class="interact-item" @click="navigateTo('/pages/interact/viewedMe')">
|
||||
<view class="interact-count">{{ interactCounts.viewedMe || 0 }}</view>
|
||||
<view class="interact-label">看过我</view>
|
||||
</view>
|
||||
<view class="interact-item" @click="navigateTo('/pages/interact/favoritedMe')">
|
||||
<view class="interact-count">{{ interactCounts.favoritedMe || 0 }}</view>
|
||||
<view class="interact-label">收藏我</view>
|
||||
</view>
|
||||
<view class="interact-item" @click="navigateTo('/pages/interact/unlockedMe')">
|
||||
<view class="interact-count">{{ interactCounts.unlockedMe || 0 }}</view>
|
||||
<view class="interact-label">解锁我</view>
|
||||
</view>
|
||||
|
||||
<!-- 顶部背景图 -->
|
||||
<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">
|
||||
<text class="navbar-title">消息</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天会话列表 -->
|
||||
<view class="session-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">聊天消息</text>
|
||||
</view>
|
||||
|
||||
<!-- 会话列表 -->
|
||||
<view class="session-list" v-if="sessions.length > 0">
|
||||
<view
|
||||
class="session-item"
|
||||
v-for="session in sessions"
|
||||
:key="session.sessionId"
|
||||
@click="handleSessionClick(session)"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<view class="session-avatar">
|
||||
<image
|
||||
class="avatar-image"
|
||||
:src="session.targetAvatar || defaultAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<!-- 未读徽章 -->
|
||||
<view class="unread-badge" v-if="session.unreadCount > 0">
|
||||
<text>{{ session.unreadCount > 99 ? '99+' : session.unreadCount }}</text>
|
||||
|
||||
<!-- 固定头部区域 -->
|
||||
<view class="fixed-header" :style="{ top: navbarHeight + 'px' }">
|
||||
<!-- 互动统计区域 -->
|
||||
<view class="interact-section">
|
||||
<view class="interact-grid">
|
||||
<view class="interact-item" @click="navigateTo('/pages/interact/viewedMe')">
|
||||
<view class="interact-icon viewed">
|
||||
<image src="/static/ic_seen.png" mode="aspectFit" class="icon-img" />
|
||||
</view>
|
||||
<text class="interact-label">看过我</text>
|
||||
<view class="interact-badge" v-if="interactCounts.viewedMe > 0">
|
||||
<text>+{{ interactCounts.viewedMe }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会话信息 -->
|
||||
<view class="session-info">
|
||||
<view class="session-header">
|
||||
<text class="session-nickname">{{ session.targetNickname }}</text>
|
||||
<text class="session-time">{{ formatTime(session.lastMessageTime) }}</text>
|
||||
<view class="interact-item" @click="navigateTo('/pages/interact/favoritedMe')">
|
||||
<view class="interact-icon favorited">
|
||||
<image src="/static/ic_collection.png" mode="aspectFit" class="icon-img" />
|
||||
</view>
|
||||
<view class="session-content">
|
||||
<text class="last-message">{{ session.lastMessage || '暂无消息' }}</text>
|
||||
<text class="interact-label">收藏我</text>
|
||||
<view class="interact-badge" v-if="interactCounts.favoritedMe > 0">
|
||||
<text>+{{ interactCounts.favoritedMe }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="interact-item" @click="navigateTo('/pages/interact/unlockedMe')">
|
||||
<view class="interact-icon unlocked">
|
||||
<image src="/static/ic_unlock.png" mode="aspectFit" class="icon-img" />
|
||||
</view>
|
||||
<text class="interact-label">解锁我</text>
|
||||
<view class="interact-badge" v-if="interactCounts.unlockedMe > 0">
|
||||
<text>+{{ interactCounts.unlockedMe }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<Empty
|
||||
v-else-if="!listLoading"
|
||||
text="暂无聊天消息"
|
||||
buttonText="去相亲"
|
||||
buttonUrl="/pages/index/index"
|
||||
/>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<Loading
|
||||
type="more"
|
||||
:loading="listLoading"
|
||||
:noMore="noMoreData && sessions.length > 0"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 消息列表区域(可滚动) -->
|
||||
<scroll-view
|
||||
class="message-scroll"
|
||||
scroll-y
|
||||
:style="{ height: scrollHeight + 'px' }"
|
||||
refresher-enabled
|
||||
:refresher-triggered="isRefreshing"
|
||||
@refresherrefresh="handleRefresh"
|
||||
>
|
||||
<view class="message-section">
|
||||
<!-- 系统消息入口 -->
|
||||
<view class="system-message-item" @click="navigateTo('/pages/message/system')">
|
||||
<view class="system-avatar">
|
||||
<image :src="defaultAvatar" mode="aspectFill" class="avatar-img" />
|
||||
</view>
|
||||
<view class="system-info">
|
||||
<text class="system-title">系统消息</text>
|
||||
</view>
|
||||
<view class="system-arrow">
|
||||
<text class="arrow-icon">〉</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天会话列表 -->
|
||||
<view class="session-list" v-if="sessions.length > 0">
|
||||
<view
|
||||
class="session-item"
|
||||
v-for="session in sessions"
|
||||
:key="session.sessionId"
|
||||
@click="handleSessionClick(session)"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<view class="session-avatar">
|
||||
<image
|
||||
class="avatar-img"
|
||||
:src="session.targetAvatar || defaultAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<!-- 未读徽章 -->
|
||||
<view class="unread-badge" v-if="session.unreadCount > 0">
|
||||
<text>{{ session.unreadCount > 99 ? '99+' : session.unreadCount }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 会话信息 -->
|
||||
<view class="session-info">
|
||||
<view class="session-header">
|
||||
<text class="session-nickname">{{ session.targetNickname }}{{ session.relationship ? `(${session.relationship})` : '' }}</text>
|
||||
<text class="session-time">{{ formatTime(session.lastMessageTime) }}</text>
|
||||
</view>
|
||||
<view class="session-content">
|
||||
<text class="last-message">{{ session.lastMessage || '暂无消息' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useChatStore } from '@/store/chat.js'
|
||||
import { useUserStore } from '@/store/user.js'
|
||||
import { getSessions } from '@/api/chat.js'
|
||||
import { getViewedMe, getFavoritedMe, getUnlockedMe } from '@/api/interact.js'
|
||||
import { formatTimestamp } from '@/utils/format.js'
|
||||
import Loading from '@/components/Loading/index.vue'
|
||||
import Empty from '@/components/Empty/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'MessagePage',
|
||||
components: {
|
||||
Loading,
|
||||
Empty
|
||||
Loading
|
||||
},
|
||||
setup() {
|
||||
const chatStore = useChatStore()
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 页面状态
|
||||
const pageLoading = ref(true)
|
||||
const listLoading = ref(false)
|
||||
const noMoreData = ref(false)
|
||||
|
||||
const isRefreshing = ref(false)
|
||||
|
||||
// 系统信息
|
||||
const statusBarHeight = ref(20)
|
||||
const navbarHeight = ref(64)
|
||||
const scrollHeight = ref(500)
|
||||
|
||||
// 数据
|
||||
const sessions = ref([])
|
||||
const interactCounts = ref({
|
||||
|
|
@ -109,20 +150,51 @@ export default {
|
|||
favoritedMe: 0,
|
||||
unlockedMe: 0
|
||||
})
|
||||
|
||||
|
||||
// 默认头像
|
||||
const defaultAvatar = '/static/logo.png'
|
||||
|
||||
|
||||
// 获取系统信息
|
||||
const getSystemInfo = () => {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
statusBarHeight.value = res.statusBarHeight || 20
|
||||
// 导航栏高度 = 状态栏高度 + 44px
|
||||
navbarHeight.value = statusBarHeight.value + 44
|
||||
|
||||
// 计算滚动区域高度
|
||||
// 需要等待固定头部渲染后再计算
|
||||
setTimeout(() => {
|
||||
calcScrollHeight(res.windowHeight)
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 计算滚动区域高度
|
||||
const calcScrollHeight = (windowHeight) => {
|
||||
const query = uni.createSelectorQuery()
|
||||
query.select('.fixed-header').boundingClientRect((rect) => {
|
||||
if (rect) {
|
||||
// 窗口高度 - 导航栏高度 - 固定头部高度 - tabbar高度
|
||||
const tabbarHeight = 50
|
||||
scrollHeight.value = windowHeight - navbarHeight.value - rect.height - tabbarHeight
|
||||
}
|
||||
}).exec()
|
||||
}
|
||||
|
||||
// 加载互动统计
|
||||
const loadInteractCounts = async () => {
|
||||
// 未登录时不加载
|
||||
if (!userStore.isLoggedIn) return
|
||||
|
||||
try {
|
||||
// 并行请求三个接口获取数量
|
||||
const [viewedRes, favoritedRes, unlockedRes] = await Promise.all([
|
||||
getViewedMe(1, 1),
|
||||
getFavoritedMe(1, 1),
|
||||
getUnlockedMe(1, 1)
|
||||
])
|
||||
|
||||
|
||||
interactCounts.value = {
|
||||
viewedMe: viewedRes?.data?.total || 0,
|
||||
favoritedMe: favoritedRes?.data?.total || 0,
|
||||
|
|
@ -132,9 +204,12 @@ export default {
|
|||
console.error('加载互动统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 加载会话列表
|
||||
const loadSessions = async () => {
|
||||
// 未登录时不加载
|
||||
if (!userStore.isLoggedIn) return
|
||||
|
||||
listLoading.value = true
|
||||
try {
|
||||
const res = await getSessions()
|
||||
|
|
@ -149,7 +224,7 @@ export default {
|
|||
listLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 初始化页面
|
||||
const initPage = async () => {
|
||||
pageLoading.value = true
|
||||
|
|
@ -164,50 +239,65 @@ export default {
|
|||
pageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
isRefreshing.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
loadInteractCounts(),
|
||||
loadSessions()
|
||||
])
|
||||
} finally {
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp) => {
|
||||
return formatTimestamp(timestamp)
|
||||
}
|
||||
|
||||
|
||||
// 导航到互动页面
|
||||
const navigateTo = (url) => {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
|
||||
// 点击会话
|
||||
const handleSessionClick = (session) => {
|
||||
chatStore.setCurrentSession(session.sessionId)
|
||||
uni.navigateTo({
|
||||
url: `/pages/chat/index?sessionId=${session.sessionId}&targetUserId=${session.targetUserId}`
|
||||
uni.navigateTo({
|
||||
url: `/pages/chat/index?sessionId=${session.sessionId}&targetUserId=${session.targetUserId}`
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getSystemInfo()
|
||||
initPage()
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
pageLoading,
|
||||
listLoading,
|
||||
noMoreData,
|
||||
isRefreshing,
|
||||
statusBarHeight,
|
||||
navbarHeight,
|
||||
scrollHeight,
|
||||
sessions,
|
||||
interactCounts,
|
||||
defaultAvatar,
|
||||
formatTime,
|
||||
navigateTo,
|
||||
handleSessionClick
|
||||
handleSessionClick,
|
||||
handleRefresh,
|
||||
initPage,
|
||||
loadInteractCounts,
|
||||
loadSessions
|
||||
}
|
||||
},
|
||||
// 下拉刷新
|
||||
onPullDownRefresh() {
|
||||
this.initPage && this.initPage().finally(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
// 页面显示时刷新数据
|
||||
onShow() {
|
||||
// 每次显示页面时刷新数据
|
||||
if (this.loadInteractCounts) {
|
||||
this.loadInteractCounts()
|
||||
}
|
||||
|
|
@ -220,142 +310,280 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.message-page {
|
||||
min-height: 100vh;
|
||||
background-color: #f8f8f8;
|
||||
padding-bottom: 120rpx;
|
||||
height: 100vh;
|
||||
background-color: #f5f6fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 互动统计区域
|
||||
.interact-section {
|
||||
background-color: #fff;
|
||||
padding: 30rpx 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
// 顶部背景图
|
||||
.top-bg {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 400rpx;
|
||||
z-index: 1;
|
||||
|
||||
.interact-grid {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
|
||||
.interact-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16rpx 40rpx;
|
||||
|
||||
.interact-count {
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
color: #ff6b6b;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.interact-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
.bg-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// 聊天会话区域
|
||||
.session-section {
|
||||
background-color: #fff;
|
||||
|
||||
.section-header {
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
// 自定义导航栏
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
|
||||
.navbar-content {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.navbar-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.session-list {
|
||||
.session-item {
|
||||
}
|
||||
|
||||
// 固定头部区域
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
// 互动统计区域
|
||||
.interact-section {
|
||||
position: relative;
|
||||
padding: 24rpx 40rpx 32rpx;
|
||||
|
||||
.interact-grid {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.interact-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24rpx 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.session-avatar {
|
||||
position: relative;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
|
||||
.interact-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16rpx;
|
||||
|
||||
.icon-img {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
}
|
||||
|
||||
.unread-badge {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
min-width: 36rpx;
|
||||
height: 36rpx;
|
||||
padding: 0 8rpx;
|
||||
background-color: #ff4d4f;
|
||||
border-radius: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&.viewed {
|
||||
background: linear-gradient(135deg, #e8e0ff 0%, #d4c4ff 100%);
|
||||
}
|
||||
|
||||
&.favorited {
|
||||
background: linear-gradient(135deg, #ffe0e8 0%, #ffc4d4 100%);
|
||||
}
|
||||
|
||||
&.unlocked {
|
||||
background: linear-gradient(135deg, #fff3e0 0%, #ffe4c4 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.session-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.session-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.session-nickname {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 400rpx;
|
||||
}
|
||||
|
||||
.session-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.interact-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.interact-badge {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
min-width: 44rpx;
|
||||
height: 44rpx;
|
||||
padding: 0 12rpx;
|
||||
border-radius: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.session-content {
|
||||
.last-message {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(1) .interact-badge {
|
||||
background: #9b7bff;
|
||||
}
|
||||
|
||||
&:nth-child(2) .interact-badge {
|
||||
background: #ff6b8a;
|
||||
}
|
||||
|
||||
&:nth-child(3) .interact-badge {
|
||||
background: #ffb347;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 消息滚动区域
|
||||
.message-scroll {
|
||||
flex: 1;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 100rpx;
|
||||
}
|
||||
|
||||
// 消息列表区域
|
||||
.message-section {
|
||||
background-color: #F3F3F3;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
// 系统消息入口
|
||||
.system-message-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
// background: linear-gradient(90deg, #f8fbff 0%, #fff 100%);
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
|
||||
.system-avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
margin-right: 24rpx;
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.system-info {
|
||||
flex: 1;
|
||||
|
||||
.system-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #52c41a;
|
||||
}
|
||||
}
|
||||
|
||||
.system-arrow {
|
||||
.arrow-icon {
|
||||
font-size: 32rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 聊天会话列表
|
||||
.session-list {
|
||||
.session-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 28rpx 32rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.session-avatar {
|
||||
position: relative;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.unread-badge {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
min-width: 36rpx;
|
||||
height: 36rpx;
|
||||
padding: 0 8rpx;
|
||||
background-color: #ff4d4f;
|
||||
border-radius: 18rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
text {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.session-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.session-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.session-nickname {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 400rpx;
|
||||
}
|
||||
|
||||
.session-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.session-content {
|
||||
.last-message {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,35 @@
|
|||
<template>
|
||||
<view class="profile-edit-page">
|
||||
<!-- 步骤指示器 -->
|
||||
<view class="step-indicator">
|
||||
<view
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
class="step-item"
|
||||
:class="{ active: currentStep === index, completed: currentStep > index }"
|
||||
>
|
||||
<view class="step-dot">{{ index + 1 }}</view>
|
||||
<text class="step-label">{{ step.label }}</text>
|
||||
<!-- 固定头部区域 -->
|
||||
<view class="fixed-header">
|
||||
<!-- 自定义导航栏(带渐变背景) -->
|
||||
<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">填写资料</text>
|
||||
<view class="navbar-placeholder"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 步骤指示器(白色背景,不在渐变内) -->
|
||||
<view class="step-indicator">
|
||||
<view
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
class="step-item"
|
||||
:class="{ active: currentStep === index, completed: currentStep > index }"
|
||||
>
|
||||
<view class="step-dot">{{ index + 1 }}</view>
|
||||
<text class="step-label">{{ step.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 占位区域 -->
|
||||
<view class="header-placeholder" :style="{ height: headerHeight + 'px' }"></view>
|
||||
|
||||
<!-- 步骤内容 -->
|
||||
<view class="step-content">
|
||||
<!-- 步骤1: 基础信息 -->
|
||||
|
|
@ -99,6 +116,7 @@
|
|||
:value="formData.nickname"
|
||||
disabled
|
||||
placeholder="填写关系和姓氏后自动生成"
|
||||
placeholder-class="input-placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
|
|
@ -139,6 +157,21 @@
|
|||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 籍贯 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">籍贯</text>
|
||||
<picker
|
||||
mode="region"
|
||||
:value="[formData.homeProvince, formData.homeCity, formData.homeDistrict]"
|
||||
@change="handleHomeLocationChange"
|
||||
>
|
||||
<view class="picker-value">
|
||||
{{ homeLocationText || '请选择' }}
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 步骤2: 详细信息 -->
|
||||
|
|
@ -458,6 +491,21 @@
|
|||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 期望月收入 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">期望月收入</text>
|
||||
<picker
|
||||
:value="requirementIncomeMinIndex"
|
||||
:range="incomeRangeOptions"
|
||||
@change="handleIncomeMinChange"
|
||||
>
|
||||
<view class="picker-value">
|
||||
{{ getIncomeLabel(formData.requirement.monthlyIncomeMin) }}
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 步骤5: 联系方式 -->
|
||||
|
|
@ -474,17 +522,21 @@
|
|||
/>
|
||||
</view>
|
||||
|
||||
<!-- 手机号验证提示 -->
|
||||
<!-- 手机号验证 -->
|
||||
<view class="form-item">
|
||||
<text class="form-label">手机号验证</text>
|
||||
<button
|
||||
v-if="!phoneVerified"
|
||||
class="verify-btn"
|
||||
:disabled="!formData.weChatNo"
|
||||
@click="handlePhoneVerify"
|
||||
open-type="getPhoneNumber"
|
||||
@getphonenumber="handleGetPhoneNumber"
|
||||
>
|
||||
{{ phoneVerified ? '已验证' : '点击验证手机号' }}
|
||||
点击验证手机号
|
||||
</button>
|
||||
<text v-if="!formData.weChatNo" class="form-tip">请先填写微信号</text>
|
||||
<view v-else class="phone-verified">
|
||||
<text class="phone-number">{{ formData.phone }}</text>
|
||||
<text class="verified-tag">已验证</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -520,11 +572,31 @@
|
|||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { useUserStore } from '@/store/user.js'
|
||||
import { createOrUpdate, getMyProfile, uploadPhotos, deletePhoto } from '@/api/profile.js'
|
||||
import { createOrUpdate, getMyProfile, uploadPhotos, deletePhoto, cleanupOrphanPhotos } from '@/api/profile.js'
|
||||
import { generateNickname, getBirthYearRange } from '@/utils/format.js'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 状态栏高度
|
||||
const statusBarHeight = ref(20)
|
||||
const headerHeight = ref(120)
|
||||
|
||||
// 获取系统信息
|
||||
const getSystemInfo = () => {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
statusBarHeight.value = res.statusBarHeight || 20
|
||||
// 计算头部总高度:状态栏 + 导航栏(44px) + 步骤指示器(约100px)
|
||||
headerHeight.value = statusBarHeight.value + 44 + 100
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
// 步骤配置
|
||||
const steps = [
|
||||
{ label: '基础信息' },
|
||||
|
|
@ -542,25 +614,29 @@ const phoneVerified = ref(false)
|
|||
const formData = reactive({
|
||||
photos: [],
|
||||
isPhotoPublic: true,
|
||||
relationship: 0,
|
||||
relationship: 1, // 默认选择"父亲"
|
||||
surname: '',
|
||||
nickname: '',
|
||||
childGender: 0,
|
||||
childGender: 1, // 默认选择"男"
|
||||
birthYear: 0,
|
||||
education: 0,
|
||||
education: 1, // 默认选择"高中及以下"
|
||||
workProvince: '',
|
||||
workCity: '',
|
||||
workDistrict: '',
|
||||
homeProvince: '',
|
||||
homeCity: '',
|
||||
homeDistrict: '',
|
||||
occupation: '',
|
||||
monthlyIncome: 0,
|
||||
monthlyIncome: 1, // 默认选择"5000以下"
|
||||
height: 0,
|
||||
weight: 0,
|
||||
houseStatus: 0,
|
||||
carStatus: 0,
|
||||
marriageStatus: 0,
|
||||
houseStatus: 5, // 默认选择"租房"
|
||||
carStatus: 2, // 默认选择"无车"
|
||||
marriageStatus: 1, // 默认选择"未婚"
|
||||
expectMarryTime: 0,
|
||||
introduction: '',
|
||||
weChatNo: '',
|
||||
phone: '', // 验证后的手机号
|
||||
requirement: {
|
||||
ageMin: 0,
|
||||
ageMax: 0,
|
||||
|
|
@ -587,11 +663,12 @@ const relationshipOptions = [
|
|||
]
|
||||
|
||||
const educationOptions = [
|
||||
{ value: 1, label: '高中及以下' },
|
||||
{ value: 2, label: '大专' },
|
||||
{ value: 3, label: '本科' },
|
||||
{ value: 4, label: '硕士' },
|
||||
{ value: 5, label: '博士' }
|
||||
{ value: 1, label: '高中' },
|
||||
{ value: 2, label: '中专' },
|
||||
{ value: 3, label: '大专' },
|
||||
{ value: 4, label: '本科' },
|
||||
{ value: 5, label: '研究生' },
|
||||
{ value: 6, label: '博士及以上' }
|
||||
]
|
||||
|
||||
const incomeOptions = [
|
||||
|
|
@ -603,30 +680,30 @@ const incomeOptions = [
|
|||
]
|
||||
|
||||
const houseOptions = [
|
||||
{ value: 1, label: '无房' },
|
||||
{ value: 2, label: '有房贷' },
|
||||
{ value: 3, label: '有房无贷' },
|
||||
{ value: 4, label: '多套房' }
|
||||
{ value: 1, label: '现居地已购房' },
|
||||
{ value: 2, label: '家乡已购房' },
|
||||
{ value: 3, label: '婚后购房' },
|
||||
{ value: 4, label: '父母同住' },
|
||||
{ value: 5, label: '租房' },
|
||||
{ value: 6, label: '近期有购房计划' }
|
||||
]
|
||||
|
||||
const carOptions = [
|
||||
{ value: 1, label: '无车' },
|
||||
{ value: 2, label: '有车贷' },
|
||||
{ value: 3, label: '有车无贷' }
|
||||
{ value: 1, label: '已购车' },
|
||||
{ value: 2, label: '无车' },
|
||||
{ value: 3, label: '近期购车' }
|
||||
]
|
||||
|
||||
const marriageOptions = [
|
||||
{ value: 1, label: '未婚' },
|
||||
{ value: 2, label: '离异无孩' },
|
||||
{ value: 3, label: '离异有孩' },
|
||||
{ value: 4, label: '丧偶' }
|
||||
{ value: 2, label: '离异未育' },
|
||||
{ value: 3, label: '离异已育' }
|
||||
]
|
||||
|
||||
const expectMarryOptions = [
|
||||
{ value: 1, label: '1年内' },
|
||||
{ value: 2, label: '2年内' },
|
||||
{ value: 3, label: '3年内' },
|
||||
{ value: 4, label: '看缘分' }
|
||||
{ value: 1, label: '尽快结婚' },
|
||||
{ value: 2, label: '一到两年内' },
|
||||
{ value: 3, label: '孩子满意就结婚' }
|
||||
]
|
||||
|
||||
// 出生年份选项 (Property 9: Birth Year Range)
|
||||
|
|
@ -668,6 +745,22 @@ const heightRangeOptions = computed(() => {
|
|||
return options
|
||||
})
|
||||
|
||||
// 月收入范围选项
|
||||
const incomeRangeOptions = ['不限', '5000以下', '5000-10000', '10000-20000', '20000-50000', '50000以上']
|
||||
|
||||
// 获取月收入标签
|
||||
const getIncomeLabel = (value) => {
|
||||
if (!value) return '不限'
|
||||
const labels = {
|
||||
1: '5000以下',
|
||||
2: '5000-10000',
|
||||
3: '10000-20000',
|
||||
4: '20000-50000',
|
||||
5: '50000以上'
|
||||
}
|
||||
return labels[value] || '不限'
|
||||
}
|
||||
|
||||
// 选中索引计算
|
||||
const relationshipIndex = computed(() => {
|
||||
const idx = relationshipOptions.findIndex(o => o.value === formData.relationship)
|
||||
|
|
@ -740,6 +833,11 @@ const requirementHeightMaxIndex = computed(() => {
|
|||
return heightRangeOptions.value.indexOf(formData.requirement.heightMax)
|
||||
})
|
||||
|
||||
const requirementIncomeMinIndex = computed(() => {
|
||||
if (!formData.requirement.monthlyIncomeMin) return 0
|
||||
return formData.requirement.monthlyIncomeMin
|
||||
})
|
||||
|
||||
// 地点文本
|
||||
const workLocationText = computed(() => {
|
||||
if (formData.workProvince && formData.workCity) {
|
||||
|
|
@ -748,6 +846,13 @@ const workLocationText = computed(() => {
|
|||
return ''
|
||||
})
|
||||
|
||||
const homeLocationText = computed(() => {
|
||||
if (formData.homeProvince && formData.homeCity) {
|
||||
return `${formData.homeProvince} ${formData.homeCity} ${formData.homeDistrict || ''}`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const city1Text = computed(() => {
|
||||
if (formData.requirement.city1Province && formData.requirement.city1City) {
|
||||
return `${formData.requirement.city1Province} ${formData.requirement.city1City}`
|
||||
|
|
@ -792,6 +897,13 @@ const handleWorkLocationChange = (e) => {
|
|||
formData.workDistrict = district || ''
|
||||
}
|
||||
|
||||
const handleHomeLocationChange = (e) => {
|
||||
const [province, city, district] = e.detail.value
|
||||
formData.homeProvince = province
|
||||
formData.homeCity = city
|
||||
formData.homeDistrict = district || ''
|
||||
}
|
||||
|
||||
const handleIncomeChange = (e) => {
|
||||
formData.monthlyIncome = incomeOptions[e.detail.value].value
|
||||
}
|
||||
|
|
@ -848,6 +960,11 @@ const handleHeightMaxChange = (e) => {
|
|||
formData.requirement.heightMax = val === '不限' ? 0 : val
|
||||
}
|
||||
|
||||
const handleIncomeMinChange = (e) => {
|
||||
const idx = e.detail.value
|
||||
formData.requirement.monthlyIncomeMin = idx === 0 ? 0 : idx
|
||||
}
|
||||
|
||||
const handleCity1Change = (e) => {
|
||||
const [province, city] = e.detail.value
|
||||
formData.requirement.city1Province = province
|
||||
|
|
@ -929,14 +1046,17 @@ const handleChoosePhoto = () => {
|
|||
uploadRes.data.photos.forEach(photo => {
|
||||
if (formData.photos.length < 5) {
|
||||
formData.photos.push({
|
||||
id: photo.id,
|
||||
url: photo.photoUrl
|
||||
id: photo.id || null, // 新上传的照片可能没有 id
|
||||
url: photo.url || photo.photoUrl
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
console.error('上传照片错误:', error)
|
||||
uni.showToast({ title: error.message || '上传失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
|
|
@ -958,16 +1078,39 @@ const handleDeletePhoto = async (index) => {
|
|||
formData.photos.splice(index, 1)
|
||||
}
|
||||
|
||||
// 手机号验证 (Property 11: WeChat Field Validation)
|
||||
const handlePhoneVerify = () => {
|
||||
if (!formData.weChatNo) {
|
||||
uni.showToast({ title: '请先填写微信号', icon: 'none' })
|
||||
// 手机号验证 - 处理微信获取手机号回调
|
||||
const handleGetPhoneNumber = async (e) => {
|
||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
uni.showToast({ title: '取消授权', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 调用微信获取手机号
|
||||
// 实际实现需要使用 button 的 open-type="getPhoneNumber"
|
||||
uni.showToast({ title: '请使用微信授权获取手机号', icon: 'none' })
|
||||
// 获取到 code,发送给后端解密
|
||||
const code = e.detail.code
|
||||
if (!code) {
|
||||
uni.showToast({ title: '获取手机号失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '验证中...' })
|
||||
|
||||
try {
|
||||
const { decryptPhone } = await import('@/api/user.js')
|
||||
const res = await decryptPhone(code)
|
||||
|
||||
if (res && res.code === 0 && res.data?.phone) {
|
||||
formData.phone = res.data.phone
|
||||
phoneVerified.value = true
|
||||
uni.showToast({ title: '验证成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: res?.message || '验证失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('验证手机号失败:', error)
|
||||
uni.showToast({ title: '验证失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤验证
|
||||
|
|
@ -1081,9 +1224,9 @@ const handleSubmit = async () => {
|
|||
|
||||
const res = await createOrUpdate(submitData)
|
||||
|
||||
if (res && res.success) {
|
||||
if (res && res.code === 0) {
|
||||
// 更新用户状态
|
||||
userStore.setProfileCompleted(true)
|
||||
userStore.updateUserInfo({ isProfileCompleted: true })
|
||||
|
||||
uni.showToast({ title: '资料提交成功', icon: 'success' })
|
||||
|
||||
|
|
@ -1094,6 +1237,7 @@ const handleSubmit = async () => {
|
|||
uni.showToast({ title: res?.message || '提交失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交资料失败:', error)
|
||||
uni.showToast({ title: '提交失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
submitting.value = false
|
||||
|
|
@ -1104,36 +1248,40 @@ const handleSubmit = async () => {
|
|||
const loadProfile = async () => {
|
||||
try {
|
||||
const res = await getMyProfile()
|
||||
if (res && res.success && res.data) {
|
||||
if (res && res.code === 0 && res.data) {
|
||||
const profile = res.data
|
||||
|
||||
// 填充表单数据
|
||||
formData.isPhotoPublic = profile.isPhotoPublic ?? true
|
||||
formData.relationship = profile.relationship || 0
|
||||
formData.relationship = profile.relationship || 1
|
||||
formData.surname = profile.surname || ''
|
||||
formData.nickname = profile.nickname || ''
|
||||
formData.childGender = profile.childGender || 0
|
||||
formData.childGender = profile.childGender || 1
|
||||
formData.birthYear = profile.birthYear || 0
|
||||
formData.education = profile.education || 0
|
||||
formData.education = profile.education || 1
|
||||
formData.workProvince = profile.workProvince || ''
|
||||
formData.workCity = profile.workCity || ''
|
||||
formData.workDistrict = profile.workDistrict || ''
|
||||
formData.homeProvince = profile.homeProvince || ''
|
||||
formData.homeCity = profile.homeCity || ''
|
||||
formData.homeDistrict = profile.homeDistrict || ''
|
||||
formData.occupation = profile.occupation || ''
|
||||
formData.monthlyIncome = profile.monthlyIncome || 0
|
||||
formData.monthlyIncome = profile.monthlyIncome || 1
|
||||
formData.height = profile.height || 0
|
||||
formData.weight = profile.weight || 0
|
||||
formData.houseStatus = profile.houseStatus || 0
|
||||
formData.carStatus = profile.carStatus || 0
|
||||
formData.marriageStatus = profile.marriageStatus || 0
|
||||
formData.houseStatus = profile.houseStatus || 5
|
||||
formData.carStatus = profile.carStatus || 2
|
||||
formData.marriageStatus = profile.marriageStatus || 1
|
||||
formData.expectMarryTime = profile.expectMarryTime || 0
|
||||
formData.introduction = profile.introduction || ''
|
||||
formData.weChatNo = profile.weChatNo || ''
|
||||
|
||||
// 照片
|
||||
// 照片 - 需要拼接完整URL
|
||||
const STATIC_URL = 'http://localhost:5001'
|
||||
if (profile.photos && profile.photos.length > 0) {
|
||||
formData.photos = profile.photos.map(p => ({
|
||||
id: p.id,
|
||||
url: p.photoUrl
|
||||
url: p.photoUrl.startsWith('http') ? p.photoUrl : `${STATIC_URL}${p.photoUrl}`
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -1156,13 +1304,27 @@ const loadProfile = async () => {
|
|||
marriageStatus: profile.requirement.marriageStatus || []
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 用户没有 Profile 记录,清理之前可能上传的孤立照片
|
||||
try {
|
||||
await cleanupOrphanPhotos()
|
||||
} catch (e) {
|
||||
console.log('清理孤立照片:', e.message)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载资料失败:', error)
|
||||
// 加载失败也尝试清理孤立照片
|
||||
try {
|
||||
await cleanupOrphanPhotos()
|
||||
} catch (e) {
|
||||
console.log('清理孤立照片:', e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSystemInfo()
|
||||
loadProfile()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1174,8 +1336,63 @@ onMounted(() => {
|
|||
padding-bottom: 140rpx;
|
||||
}
|
||||
|
||||
// 步骤指示器
|
||||
// 固定头部区域
|
||||
.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
// 自定义导航栏(水平渐变背景)
|
||||
.custom-navbar {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: linear-gradient(90deg, #FFDEE0 0%, #FF939C 100%);
|
||||
|
||||
.navbar-content {
|
||||
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: 64rpx;
|
||||
color: #333;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.navbar-placeholder {
|
||||
width: 80rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 占位区域
|
||||
.header-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 步骤指示器(白色背景)
|
||||
.step-indicator {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx 40rpx;
|
||||
|
|
@ -1202,16 +1419,17 @@ onMounted(() => {
|
|||
|
||||
.step-label {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.step-dot {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ff5252 100%);
|
||||
background: #FF6B6B;
|
||||
color: #fff;
|
||||
}
|
||||
.step-label {
|
||||
color: #ff6b6b;
|
||||
color: #FF6B6B;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1230,6 +1448,8 @@ onMounted(() => {
|
|||
// 表单区域
|
||||
.step-content {
|
||||
padding: 20rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
|
|
@ -1271,6 +1491,11 @@ onMounted(() => {
|
|||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
|
||||
&[disabled] {
|
||||
color: #999;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
|
|
@ -1503,12 +1728,37 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
// 已验证手机号显示
|
||||
.phone-verified {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 80rpx;
|
||||
padding: 0 20rpx;
|
||||
background-color: #f0fff0;
|
||||
border-radius: 12rpx;
|
||||
|
||||
.phone-number {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.verified-tag {
|
||||
font-size: 24rpx;
|
||||
color: #4cd964;
|
||||
padding: 4rpx 12rpx;
|
||||
background-color: #e8f8e8;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部按钮
|
||||
.bottom-buttons {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 20rpx 30rpx;
|
||||
|
|
@ -1539,3 +1789,11 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 非 scoped 样式,用于 placeholder-class -->
|
||||
<style lang="scss">
|
||||
.input-placeholder {
|
||||
color: #999 !important;
|
||||
font-size: 28rpx !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
432
miniapp/pages/profile/personal.vue
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
<template>
|
||||
<view class="personal-page">
|
||||
<!-- 顶部背景图 -->
|
||||
<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">个人资料</text>
|
||||
<view class="navbar-placeholder"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 头像区域 -->
|
||||
<view class="avatar-section" :style="{ marginTop: (statusBarHeight + 44) + 'px' }">
|
||||
<view class="avatar-wrapper" @click="handleChangeAvatar">
|
||||
<image
|
||||
class="avatar-img"
|
||||
:src="userInfo.avatar || defaultAvatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="avatar-edit-icon">
|
||||
<text>✎</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view class="form-section">
|
||||
<view class="form-item">
|
||||
<text class="form-label">用户名</text>
|
||||
<view class="form-input">
|
||||
<input
|
||||
type="text"
|
||||
:value="nickname"
|
||||
@input="e => nickname = e.detail.value"
|
||||
placeholder="请输入用户名"
|
||||
maxlength="20"
|
||||
class="input-field"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">相亲编号</text>
|
||||
<view class="form-input disabled" @click="handleCopyXiangQinNo">
|
||||
<text>{{ userInfo.xiangQinNo || '未设置' }}</text>
|
||||
<text class="copy-icon">复制</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<view class="btn-section">
|
||||
<button class="btn-save" @click="handleSave">保存</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useUserStore } from '@/store/user.js'
|
||||
import { updateAvatar, updateNickname } from '@/api/user.js'
|
||||
import { getMyProfile } from '@/api/profile.js'
|
||||
|
||||
export default {
|
||||
name: 'PersonalPage',
|
||||
setup() {
|
||||
const userStore = useUserStore()
|
||||
const defaultAvatar = '/static/logo.png'
|
||||
const statusBarHeight = ref(20)
|
||||
const nickname = ref('')
|
||||
|
||||
const userInfo = computed(() => ({
|
||||
avatar: userStore.avatar,
|
||||
xiangQinNo: userStore.xiangQinNo,
|
||||
nickname: userStore.nickname
|
||||
}))
|
||||
|
||||
// 监听 store 中的 nickname 变化,同步到本地
|
||||
watch(() => userStore.nickname, (newVal) => {
|
||||
if (newVal && !nickname.value) {
|
||||
nickname.value = newVal
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 获取系统信息
|
||||
const getSystemInfo = () => {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
statusBarHeight.value = res.statusBarHeight || 20
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
// 更换头像
|
||||
const handleChangeAvatar = () => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempFilePath = res.tempFilePaths[0]
|
||||
|
||||
uni.showLoading({ title: '上传中...' })
|
||||
|
||||
try {
|
||||
// 上传图片
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
uni.uploadFile({
|
||||
url: `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:5001/api'}/app/upload`,
|
||||
filePath: tempFilePath,
|
||||
name: 'file',
|
||||
header: {
|
||||
Authorization: `Bearer ${userStore.token}`
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(JSON.parse(res.data))
|
||||
} else {
|
||||
reject(new Error('上传失败'))
|
||||
}
|
||||
},
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
|
||||
if (uploadRes.code === 0 && uploadRes.data?.url) {
|
||||
// 调用后端API保存头像到数据库
|
||||
const saveRes = await updateAvatar(uploadRes.data.url)
|
||||
if (saveRes && saveRes.code === 0) {
|
||||
userStore.updateUserInfo({ avatar: uploadRes.data.url })
|
||||
uni.showToast({ title: '头像更新成功', icon: 'success' })
|
||||
} else {
|
||||
uni.showToast({ title: saveRes?.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: uploadRes.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传头像失败:', error)
|
||||
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 复制相亲编号
|
||||
const handleCopyXiangQinNo = () => {
|
||||
const xiangQinNo = userStore.xiangQinNo
|
||||
if (!xiangQinNo) {
|
||||
uni.showToast({ title: '暂无编号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.setClipboardData({
|
||||
data: xiangQinNo,
|
||||
success: () => {
|
||||
uni.showToast({ title: '已复制', icon: 'success' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 保存
|
||||
const handleSave = async () => {
|
||||
// 检查昵称是否有变化
|
||||
if (nickname.value && nickname.value !== userStore.nickname) {
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
try {
|
||||
const res = await updateNickname(nickname.value)
|
||||
if (res && res.code === 0) {
|
||||
userStore.updateUserInfo({ nickname: nickname.value })
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1000)
|
||||
} else {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: res?.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
console.error('保存失败:', error)
|
||||
uni.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化昵称 - 从API获取最新数据
|
||||
const initNickname = async () => {
|
||||
userStore.restoreFromStorage()
|
||||
|
||||
// 从 API 获取用户资料
|
||||
try {
|
||||
const res = await getMyProfile()
|
||||
if (res && res.code === 0 && res.data) {
|
||||
// 优先使用 nickname,其次使用 surname(姓氏)
|
||||
const profileNickname = res.data.nickname || res.data.surname || ''
|
||||
if (profileNickname) {
|
||||
nickname.value = profileNickname
|
||||
userStore.updateUserInfo({ nickname: profileNickname })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户资料失败:', error)
|
||||
// API 失败时尝试从 store 获取
|
||||
if (userStore.nickname) {
|
||||
nickname.value = userStore.nickname
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getSystemInfo()
|
||||
initNickname()
|
||||
})
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
defaultAvatar,
|
||||
statusBarHeight,
|
||||
nickname,
|
||||
handleBack,
|
||||
handleChangeAvatar,
|
||||
handleCopyXiangQinNo,
|
||||
handleSave,
|
||||
initNickname
|
||||
}
|
||||
},
|
||||
// 页面显示时重新初始化
|
||||
onShow() {
|
||||
if (this.initNickname) {
|
||||
this.initNickname()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.personal-page {
|
||||
height: 100vh;
|
||||
background-color: #f5f6fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 顶部背景图
|
||||
.top-bg {
|
||||
position: absolute;
|
||||
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: 100;
|
||||
|
||||
.navbar-content {
|
||||
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: 64rpx;
|
||||
color: #333;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.navbar-placeholder {
|
||||
width: 60rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 头像区域
|
||||
.avatar-section {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 60rpx 0;
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #87ceeb 0%, #5fb3d4 100%);
|
||||
}
|
||||
|
||||
.avatar-edit-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
background: linear-gradient(135deg, #ffb5b5 0%, #ff9a9a 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 4rpx solid #fff;
|
||||
|
||||
text {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单区域
|
||||
.form-section {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 0 32rpx;
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
background: #f0f0f0;
|
||||
border-radius: 16rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
text {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
font-size: 26rpx;
|
||||
color: #ff9a9a;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #f0f0f0;
|
||||
|
||||
text {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存按钮
|
||||
.btn-section {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 60rpx 32rpx;
|
||||
|
||||
.btn-save {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
background: linear-gradient(135deg, #ffb5b5 0%, #ff9a9a 100%);
|
||||
border-radius: 48rpx;
|
||||
font-size: 34rpx;
|
||||
color: #fff;
|
||||
border: none;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
BIN
miniapp/static/butler.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
miniapp/static/ic_agreement1.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
miniapp/static/ic_agreement2.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
miniapp/static/ic_check.png
Normal file
|
After Width: | Height: | Size: 855 B |
BIN
miniapp/static/ic_check_s.png
Normal file
|
After Width: | Height: | Size: 749 B |
BIN
miniapp/static/ic_collection.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
miniapp/static/ic_exit.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
miniapp/static/ic_seen.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
miniapp/static/ic_unlock.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 25 KiB |
BIN
miniapp/static/real_name.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
miniapp/static/title_bg.png
Normal file
|
After Width: | Height: | Size: 275 KiB |
|
|
@ -0,0 +1,86 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using XiangYi.Application.DTOs.Responses;
|
||||
using XiangYi.Application.Interfaces;
|
||||
|
||||
namespace XiangYi.AdminApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 后台系统配置控制器
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/admin/config")]
|
||||
[Authorize]
|
||||
public class AdminConfigController : ControllerBase
|
||||
{
|
||||
private readonly ISystemConfigService _configService;
|
||||
private readonly ILogger<AdminConfigController> _logger;
|
||||
|
||||
public AdminConfigController(
|
||||
ISystemConfigService configService,
|
||||
ILogger<AdminConfigController> logger)
|
||||
{
|
||||
_configService = configService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取默认头像
|
||||
/// </summary>
|
||||
[HttpGet("defaultAvatar")]
|
||||
public async Task<ApiResponse<DefaultAvatarResponse>> GetDefaultAvatar()
|
||||
{
|
||||
var avatarUrl = await _configService.GetDefaultAvatarAsync();
|
||||
return ApiResponse<DefaultAvatarResponse>.Success(new DefaultAvatarResponse
|
||||
{
|
||||
AvatarUrl = avatarUrl
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置默认头像
|
||||
/// </summary>
|
||||
[HttpPost("defaultAvatar")]
|
||||
public async Task<ApiResponse> SetDefaultAvatar([FromBody] SetDefaultAvatarRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.AvatarUrl))
|
||||
{
|
||||
return ApiResponse.Error(40001, "头像URL不能为空");
|
||||
}
|
||||
|
||||
var result = await _configService.SetDefaultAvatarAsync(request.AvatarUrl);
|
||||
return result ? ApiResponse.Success("设置成功") : ApiResponse.Error(40001, "设置失败");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有系统配置
|
||||
/// </summary>
|
||||
[HttpGet("all")]
|
||||
public async Task<ApiResponse<Dictionary<string, string>>> GetAllConfigs()
|
||||
{
|
||||
var configs = await _configService.GetAllConfigsAsync();
|
||||
return ApiResponse<Dictionary<string, string>>.Success(configs);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 默认头像响应
|
||||
/// </summary>
|
||||
public class DefaultAvatarResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 头像URL
|
||||
/// </summary>
|
||||
public string? AvatarUrl { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置默认头像请求
|
||||
/// </summary>
|
||||
public class SetDefaultAvatarRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 头像URL
|
||||
/// </summary>
|
||||
public string AvatarUrl { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
@ -98,6 +98,19 @@ public class AdminUserController : ControllerBase
|
|||
var adminIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
return long.TryParse(adminIdClaim, out var adminId) ? adminId : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除用户(硬删除)
|
||||
/// </summary>
|
||||
/// <param name="id">用户ID</param>
|
||||
/// <returns>操作结果</returns>
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<ApiResponse> DeleteUser(long id)
|
||||
{
|
||||
var adminId = GetCurrentAdminId();
|
||||
var result = await _adminUserService.DeleteUserAsync(id, adminId);
|
||||
return result ? ApiResponse.Success("删除成功") : ApiResponse.Error(40001, "删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -101,6 +101,19 @@ public class ProfileController : ControllerBase
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理未提交资料用户的照片
|
||||
/// 当用户进入填写资料页面时调用,清理之前未提交的孤立照片
|
||||
/// </summary>
|
||||
/// <returns>清理结果</returns>
|
||||
[HttpPost("photos/cleanup")]
|
||||
public async Task<ApiResponse> CleanupPhotos()
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
var count = await _profileService.CleanupPhotosIfNoProfileAsync(userId);
|
||||
return ApiResponse.Success($"已清理{count}张照片");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除照片
|
||||
/// </summary>
|
||||
|
|
|
|||
135
server/src/XiangYi.AppApi/Controllers/UploadController.cs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using XiangYi.Application.DTOs.Responses;
|
||||
using XiangYi.Infrastructure.Storage;
|
||||
|
||||
namespace XiangYi.AppApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 小程序文件上传控制器
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/app/upload")]
|
||||
[Authorize]
|
||||
public class UploadController : ControllerBase
|
||||
{
|
||||
private readonly IStorageProvider _storageProvider;
|
||||
private readonly ILogger<UploadController> _logger;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
// 允许的图片类型
|
||||
private static readonly string[] AllowedImageTypes = { ".jpg", ".jpeg", ".png", ".gif", ".webp" };
|
||||
// 最大文件大小 5MB
|
||||
private const long MaxFileSize = 5 * 1024 * 1024;
|
||||
|
||||
public UploadController(
|
||||
IStorageProvider storageProvider,
|
||||
ILogger<UploadController> logger,
|
||||
IWebHostEnvironment environment)
|
||||
{
|
||||
_storageProvider = storageProvider;
|
||||
_logger = logger;
|
||||
_environment = environment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传图片
|
||||
/// </summary>
|
||||
/// <param name="file">图片文件</param>
|
||||
/// <returns>图片URL</returns>
|
||||
[HttpPost]
|
||||
public async Task<ApiResponse<UploadResult>> Upload(IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
return ApiResponse<UploadResult>.Error(40001, "请选择要上传的文件");
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
if (file.Length > MaxFileSize)
|
||||
{
|
||||
return ApiResponse<UploadResult>.Error(40002, "文件大小不能超过5MB");
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
if (!AllowedImageTypes.Contains(extension))
|
||||
{
|
||||
return ApiResponse<UploadResult>.Error(40003, "只支持 jpg、jpeg、png、gif、webp 格式的图片");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 生成文件名
|
||||
var fileName = $"{Guid.NewGuid():N}{extension}";
|
||||
var folder = $"app/{DateTime.Now:yyyyMMdd}";
|
||||
|
||||
// 上传文件
|
||||
string url;
|
||||
using var stream = file.OpenReadStream();
|
||||
var relativePath = await _storageProvider.UploadAsync(stream, fileName, folder);
|
||||
|
||||
// 如果返回的是相对路径,转换为完整URL
|
||||
if (relativePath.StartsWith("/"))
|
||||
{
|
||||
var request = HttpContext.Request;
|
||||
var baseUrl = $"{request.Scheme}://{request.Host}";
|
||||
url = $"{baseUrl}{relativePath}";
|
||||
}
|
||||
else
|
||||
{
|
||||
url = relativePath;
|
||||
}
|
||||
|
||||
_logger.LogInformation("文件上传成功: {FileName} -> {Url}", file.FileName, url);
|
||||
|
||||
return ApiResponse<UploadResult>.Success(new UploadResult
|
||||
{
|
||||
Url = url,
|
||||
FileName = file.FileName
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "文件上传失败: {FileName}", file.FileName);
|
||||
return ApiResponse<UploadResult>.Error(50001, "文件上传失败");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存到本地
|
||||
/// </summary>
|
||||
private async Task<string> SaveToLocalAsync(IFormFile file, string fileName, string folder)
|
||||
{
|
||||
var uploadPath = Path.Combine(_environment.WebRootPath ?? "wwwroot", "uploads", folder);
|
||||
if (!Directory.Exists(uploadPath))
|
||||
{
|
||||
Directory.CreateDirectory(uploadPath);
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(uploadPath, fileName);
|
||||
using var stream = new FileStream(filePath, FileMode.Create);
|
||||
await file.CopyToAsync(stream);
|
||||
|
||||
// 返回完整URL
|
||||
var request = HttpContext.Request;
|
||||
var baseUrl = $"{request.Scheme}://{request.Host}";
|
||||
return $"{baseUrl}/uploads/{folder}/{fileName}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传结果
|
||||
/// </summary>
|
||||
public class UploadResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件URL
|
||||
/// </summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 原始文件名
|
||||
/// </summary>
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ using XiangYi.Application.DTOs.Requests;
|
|||
using XiangYi.Application.DTOs.Responses;
|
||||
using XiangYi.Application.Interfaces;
|
||||
using XiangYi.Core.Constants;
|
||||
using XiangYi.Infrastructure.WeChat;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace XiangYi.AppApi.Controllers;
|
||||
|
|
@ -20,6 +21,7 @@ public class UserController : ControllerBase
|
|||
private readonly ISearchService _searchService;
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly IInteractService _interactService;
|
||||
private readonly IWeChatService _weChatService;
|
||||
private readonly ILogger<UserController> _logger;
|
||||
|
||||
public UserController(
|
||||
|
|
@ -27,25 +29,30 @@ public class UserController : ControllerBase
|
|||
ISearchService searchService,
|
||||
IProfileService profileService,
|
||||
IInteractService interactService,
|
||||
IWeChatService weChatService,
|
||||
ILogger<UserController> logger)
|
||||
{
|
||||
_recommendService = recommendService;
|
||||
_searchService = searchService;
|
||||
_profileService = profileService;
|
||||
_interactService = interactService;
|
||||
_weChatService = weChatService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取每日推荐列表
|
||||
/// </summary>
|
||||
/// <param name="pageIndex">页码</param>
|
||||
/// <param name="pageSize">每页数量</param>
|
||||
/// <returns>推荐用户列表</returns>
|
||||
[HttpGet("recommend")]
|
||||
public async Task<ApiResponse<List<RecommendUserResponse>>> GetRecommend()
|
||||
[AllowAnonymous]
|
||||
public async Task<ApiResponse<PagedResult<RecommendUserResponse>>> GetRecommend([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 10)
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
var result = await _recommendService.GetDailyRecommendAsync(userId);
|
||||
return ApiResponse<List<RecommendUserResponse>>.Success(result);
|
||||
var result = await _recommendService.GetDailyRecommendAsync(userId, pageIndex, pageSize);
|
||||
return ApiResponse<PagedResult<RecommendUserResponse>>.Success(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -106,6 +113,90 @@ public class UserController : ControllerBase
|
|||
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
return long.TryParse(userIdClaim, out var userId) ? userId : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新头像
|
||||
/// </summary>
|
||||
/// <param name="request">头像请求</param>
|
||||
/// <returns>更新结果</returns>
|
||||
[HttpPost("avatar")]
|
||||
public async Task<ApiResponse> UpdateAvatar([FromBody] UpdateAvatarRequest request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.Avatar))
|
||||
{
|
||||
return ApiResponse.Error(ErrorCodes.InvalidParameter, "头像URL不能为空");
|
||||
}
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
var success = await _profileService.UpdateAvatarAsync(userId, request.Avatar);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return ApiResponse.Error(ErrorCodes.UserNotFound, "用户不存在");
|
||||
}
|
||||
|
||||
return ApiResponse.Success("头像更新成功");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新昵称
|
||||
/// </summary>
|
||||
/// <param name="request">昵称请求</param>
|
||||
/// <returns>更新结果</returns>
|
||||
[HttpPost("nickname")]
|
||||
public async Task<ApiResponse> UpdateNickname([FromBody] UpdateNicknameRequest request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.Nickname))
|
||||
{
|
||||
return ApiResponse.Error(ErrorCodes.InvalidParameter, "昵称不能为空");
|
||||
}
|
||||
|
||||
if (request.Nickname.Length > 20)
|
||||
{
|
||||
return ApiResponse.Error(ErrorCodes.InvalidParameter, "昵称不能超过20个字符");
|
||||
}
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
var success = await _profileService.UpdateNicknameAsync(userId, request.Nickname);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return ApiResponse.Error(ErrorCodes.UserNotFound, "用户不存在");
|
||||
}
|
||||
|
||||
return ApiResponse.Success("昵称更新成功");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解密微信手机号
|
||||
/// </summary>
|
||||
/// <param name="request">解密请求</param>
|
||||
/// <returns>手机号</returns>
|
||||
[HttpPost("phone/decrypt")]
|
||||
public async Task<ApiResponse<DecryptPhoneResponse>> DecryptPhone([FromBody] DecryptPhoneRequest request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.Code))
|
||||
{
|
||||
return ApiResponse<DecryptPhoneResponse>.Error(ErrorCodes.InvalidParameter, "code不能为空");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var phone = await _weChatService.GetPhoneNumberAsync(request.Code);
|
||||
|
||||
if (string.IsNullOrEmpty(phone))
|
||||
{
|
||||
return ApiResponse<DecryptPhoneResponse>.Error(ErrorCodes.WeChatServiceError, "获取手机号失败");
|
||||
}
|
||||
|
||||
return ApiResponse<DecryptPhoneResponse>.Success(new DecryptPhoneResponse { Phone = phone });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "解密手机号失败");
|
||||
return ApiResponse<DecryptPhoneResponse>.Error(ErrorCodes.WeChatServiceError, "获取手机号失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -118,3 +209,47 @@ public class UserDetailRequest
|
|||
/// </summary>
|
||||
public long UserId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新头像请求
|
||||
/// </summary>
|
||||
public class UpdateAvatarRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 头像URL
|
||||
/// </summary>
|
||||
public string Avatar { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新昵称请求
|
||||
/// </summary>
|
||||
public class UpdateNicknameRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 昵称
|
||||
/// </summary>
|
||||
public string Nickname { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解密手机号请求
|
||||
/// </summary>
|
||||
public class DecryptPhoneRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 微信返回的code
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解密手机号响应
|
||||
/// </summary>
|
||||
public class DecryptPhoneResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 手机号
|
||||
/// </summary>
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,10 @@
|
|||
},
|
||||
"ConnectionStrings": {
|
||||
"BizDb": "Server=192.168.195.15;Database=XiangYi_Biz;User Id=sa;Password=Dbt@com@123;TrustServerCertificate=true;",
|
||||
"Redis": "localhost:6379,defaultDatabase=0"
|
||||
"Redis": "192.168.195.15:6379,defaultDatabase=0"
|
||||
},
|
||||
"Redis": {
|
||||
"ConnectionString": "localhost:6379,defaultDatabase=0",
|
||||
"ConnectionString": "192.168.195.15:6379,defaultDatabase=0",
|
||||
"DefaultDatabase": 0,
|
||||
"KeyPrefix": "xiangyi:app:",
|
||||
"DefaultExpireSeconds": 3600,
|
||||
|
|
@ -34,14 +34,23 @@
|
|||
"ExpireMinutes": 10080
|
||||
},
|
||||
"WeChat": {
|
||||
"AppId": "",
|
||||
"AppSecret": ""
|
||||
"MiniProgram": {
|
||||
"AppId": "wx21b4110b18b31831",
|
||||
"AppSecret": "fe3b5aa5715820cd66af3d42d55efad6"
|
||||
}
|
||||
},
|
||||
"TencentCos": {
|
||||
"SecretId": "",
|
||||
"SecretKey": "",
|
||||
"Region": "ap-guangzhou",
|
||||
"BucketName": ""
|
||||
"Storage": {
|
||||
"Provider": "Local",
|
||||
"Local": {
|
||||
"BasePath": "wwwroot/uploads",
|
||||
"BaseUrl": "/uploads"
|
||||
},
|
||||
"TencentCos": {
|
||||
"SecretId": "",
|
||||
"SecretKey": "",
|
||||
"Region": "ap-guangzhou",
|
||||
"BucketName": ""
|
||||
}
|
||||
},
|
||||
"AliyunSms": {
|
||||
"AccessKeyId": "",
|
||||
|
|
|
|||
|
|
@ -264,12 +264,28 @@ public class RequirementResponse
|
|||
public class UploadPhotoResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 上传成功的照片URL列表
|
||||
/// 上传成功的照片列表(包含ID和URL)
|
||||
/// </summary>
|
||||
public List<string> PhotoUrls { get; set; } = new();
|
||||
public List<UploadedPhotoInfo> Photos { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 当前照片总数
|
||||
/// </summary>
|
||||
public int TotalCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传的照片信息
|
||||
/// </summary>
|
||||
public class UploadedPhotoInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 照片ID
|
||||
/// </summary>
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 照片URL
|
||||
/// </summary>
|
||||
public string PhotoUrl { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,21 +20,41 @@ public class RecommendUserResponse
|
|||
/// </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>
|
||||
/// 身高(cm)
|
||||
/// </summary>
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 体重(kg)
|
||||
/// </summary>
|
||||
public int Weight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 学历
|
||||
/// </summary>
|
||||
|
|
@ -50,6 +70,16 @@ public class RecommendUserResponse
|
|||
/// </summary>
|
||||
public string? Occupation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 月收入
|
||||
/// </summary>
|
||||
public int MonthlyIncome { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 个人简介
|
||||
/// </summary>
|
||||
public string? Intro { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否会员
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ public static class ServiceCollectionExtensions
|
|||
services.AddScoped<IReportService, ReportService>();
|
||||
services.AddScoped<ISearchService, SearchService>();
|
||||
services.AddScoped<ISensitiveWordService, SensitiveWordService>();
|
||||
services.AddScoped<ISystemConfigService, SystemConfigService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,4 +44,12 @@ public interface IAdminUserService
|
|||
/// <param name="gender">性别:1男 2女,null则随机</param>
|
||||
/// <returns>创建的用户ID列表</returns>
|
||||
Task<List<long>> CreateTestUsersAsync(int count, int? gender = null);
|
||||
|
||||
/// <summary>
|
||||
/// 删除用户(硬删除,仅用于测试数据清理)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="adminId">操作管理员ID</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> DeleteUserAsync(long userId, long adminId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,4 +62,27 @@ public interface IProfileService
|
|||
/// <param name="uploadCount">要上传的数量</param>
|
||||
/// <returns>是否可以上传</returns>
|
||||
Task<bool> CanUploadPhotosAsync(long userId, int uploadCount);
|
||||
|
||||
/// <summary>
|
||||
/// 更新用户头像
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="avatarUrl">头像URL</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> UpdateAvatarAsync(long userId, string avatarUrl);
|
||||
|
||||
/// <summary>
|
||||
/// 更新用户昵称
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="nickname">昵称</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Task<bool> UpdateNicknameAsync(long userId, string nickname);
|
||||
|
||||
/// <summary>
|
||||
/// 清理没有关联Profile的孤立照片
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <returns>清理的照片数量</returns>
|
||||
Task<int> CleanupPhotosIfNoProfileAsync(long userId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ namespace XiangYi.Application.Interfaces;
|
|||
public interface IRecommendService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取用户的每日推荐列表
|
||||
/// 获取用户的每日推荐列表(分页)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <returns>推荐用户列表</returns>
|
||||
Task<List<RecommendUserResponse>> GetDailyRecommendAsync(long userId);
|
||||
/// <param name="userId">用户ID,0表示未登录</param>
|
||||
/// <param name="pageIndex">页码</param>
|
||||
/// <param name="pageSize">每页数量</param>
|
||||
/// <returns>分页推荐用户列表</returns>
|
||||
Task<PagedResult<RecommendUserResponse>> GetDailyRecommendAsync(long userId, int pageIndex = 1, int pageSize = 10);
|
||||
|
||||
/// <summary>
|
||||
/// 为指定用户生成每日推荐列表
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
namespace XiangYi.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// 系统配置服务接口
|
||||
/// </summary>
|
||||
public interface ISystemConfigService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取配置值
|
||||
/// </summary>
|
||||
Task<string?> GetConfigValueAsync(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 设置配置值
|
||||
/// </summary>
|
||||
Task<bool> SetConfigValueAsync(string key, string value, string? description = null);
|
||||
|
||||
/// <summary>
|
||||
/// 获取默认头像URL
|
||||
/// </summary>
|
||||
Task<string?> GetDefaultAvatarAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 设置默认头像URL
|
||||
/// </summary>
|
||||
Task<bool> SetDefaultAvatarAsync(string avatarUrl);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有配置
|
||||
/// </summary>
|
||||
Task<Dictionary<string, string>> GetAllConfigsAsync();
|
||||
}
|
||||
|
|
@ -418,6 +418,27 @@ public class AdminUserService : IAdminUserService
|
|||
return createdIds;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> DeleteUserAsync(long userId, long adminId)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
|
||||
}
|
||||
|
||||
// 删除用户相关数据
|
||||
await _photoRepository.DeleteAsync(p => p.UserId == userId);
|
||||
await _requirementRepository.DeleteAsync(r => r.UserId == userId);
|
||||
await _profileRepository.DeleteAsync(p => p.UserId == userId);
|
||||
await _userRepository.DeleteAsync(userId);
|
||||
|
||||
_logger.LogInformation("管理员删除用户: AdminId={AdminId}, UserId={UserId}, Nickname={Nickname}",
|
||||
adminId, userId, user.Nickname);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Private Helper Methods
|
||||
|
||||
private static AdminUserListDto MapToUserListDto(User user, UserProfile? profile)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ public class AuthService : IAuthService
|
|||
private readonly IRepository<User> _userRepository;
|
||||
private readonly IWeChatService _weChatService;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly ISystemConfigService _systemConfigService;
|
||||
private readonly JwtOptions _jwtOptions;
|
||||
private readonly ILogger<AuthService> _logger;
|
||||
private static readonly Random _random = new();
|
||||
|
|
@ -32,12 +33,14 @@ public class AuthService : IAuthService
|
|||
IRepository<User> userRepository,
|
||||
IWeChatService weChatService,
|
||||
ICacheService cacheService,
|
||||
ISystemConfigService systemConfigService,
|
||||
IOptions<JwtOptions> jwtOptions,
|
||||
ILogger<AuthService> logger)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_weChatService = weChatService;
|
||||
_cacheService = cacheService;
|
||||
_systemConfigService = systemConfigService;
|
||||
_jwtOptions = jwtOptions.Value;
|
||||
_logger = logger;
|
||||
}
|
||||
|
|
@ -66,11 +69,15 @@ public class AuthService : IAuthService
|
|||
isNewUser = true;
|
||||
var xiangQinNo = await GenerateXiangQinNoAsync();
|
||||
|
||||
// 获取默认头像
|
||||
var defaultAvatar = await _systemConfigService.GetDefaultAvatarAsync();
|
||||
|
||||
existingUser = new User
|
||||
{
|
||||
OpenId = weChatResult.OpenId,
|
||||
UnionId = weChatResult.UnionId,
|
||||
XiangQinNo = xiangQinNo,
|
||||
Avatar = defaultAvatar,
|
||||
Status = 1,
|
||||
ContactCount = 2,
|
||||
CreateTime = DateTime.Now,
|
||||
|
|
|
|||
|
|
@ -165,14 +165,32 @@ public class ProfileService : IProfileService
|
|||
// 获取资料
|
||||
var profiles = await _profileRepository.GetListAsync(p => p.UserId == userId);
|
||||
var profile = profiles.FirstOrDefault();
|
||||
|
||||
// 获取照片(无论是否有 Profile 都获取)
|
||||
var photos = await _photoRepository.GetListAsync(p => p.UserId == userId);
|
||||
|
||||
// 如果没有 Profile,但有照片,返回只包含照片的响应
|
||||
if (profile == null)
|
||||
{
|
||||
// 如果有照片,返回包含照片的空资料
|
||||
if (photos.Any())
|
||||
{
|
||||
return new ProfileResponse
|
||||
{
|
||||
UserId = userId,
|
||||
Nickname = user.Nickname,
|
||||
XiangQinNo = user.XiangQinNo,
|
||||
Photos = photos.OrderBy(p => p.Sort).Select(p => new PhotoResponse
|
||||
{
|
||||
Id = p.Id,
|
||||
PhotoUrl = p.PhotoUrl,
|
||||
Sort = p.Sort
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取照片
|
||||
var photos = await _photoRepository.GetListAsync(p => p.UserId == userId);
|
||||
|
||||
// 获取择偶要求
|
||||
var requirements = await _requirementRepository.GetListAsync(r => r.UserId == userId);
|
||||
var requirement = requirements.FirstOrDefault();
|
||||
|
|
@ -236,7 +254,7 @@ public class ProfileService : IProfileService
|
|||
}
|
||||
|
||||
// 3. 上传照片
|
||||
var uploadedUrls = new List<string>();
|
||||
var uploadedPhotos = new List<UploadedPhotoInfo>();
|
||||
var sort = currentCount;
|
||||
|
||||
foreach (var stream in photoStreams)
|
||||
|
|
@ -256,15 +274,19 @@ public class ProfileService : IProfileService
|
|||
};
|
||||
await _photoRepository.AddAsync(photo);
|
||||
|
||||
uploadedUrls.Add(photoUrl);
|
||||
uploadedPhotos.Add(new UploadedPhotoInfo
|
||||
{
|
||||
Id = photo.Id,
|
||||
PhotoUrl = photoUrl
|
||||
});
|
||||
}
|
||||
|
||||
_logger.LogInformation("用户上传照片成功: UserId={UserId}, Count={Count}", userId, uploadedUrls.Count);
|
||||
_logger.LogInformation("用户上传照片成功: UserId={UserId}, Count={Count}", userId, uploadedPhotos.Count);
|
||||
|
||||
return new UploadPhotoResponse
|
||||
{
|
||||
PhotoUrls = uploadedUrls,
|
||||
TotalCount = currentCount + uploadedUrls.Count
|
||||
Photos = uploadedPhotos,
|
||||
TotalCount = currentCount + uploadedPhotos.Count
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -443,4 +465,69 @@ public class ProfileService : IProfileService
|
|||
// 注:即使是会员,也需要通过交换请求才能查看不公开的照片
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> UpdateAvatarAsync(long userId, string avatarUrl)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
user.Avatar = avatarUrl;
|
||||
user.UpdateTime = DateTime.Now;
|
||||
await _userRepository.UpdateAsync(user);
|
||||
|
||||
_logger.LogInformation("用户头像更新成功: UserId={UserId}, Avatar={Avatar}", userId, avatarUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> UpdateNicknameAsync(long userId, string nickname)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
user.Nickname = nickname;
|
||||
user.UpdateTime = DateTime.Now;
|
||||
await _userRepository.UpdateAsync(user);
|
||||
|
||||
_logger.LogInformation("用户昵称更新成功: UserId={UserId}, Nickname={Nickname}", userId, nickname);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> CleanupPhotosIfNoProfileAsync(long userId)
|
||||
{
|
||||
// 检查用户是否有 Profile 记录
|
||||
var profiles = await _profileRepository.GetListAsync(p => p.UserId == userId);
|
||||
if (profiles.Any())
|
||||
{
|
||||
// 有 Profile,不需要清理
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 没有 Profile,清理所有照片
|
||||
var photos = await _photoRepository.GetListAsync(p => p.UserId == userId);
|
||||
if (!photos.Any())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var count = photos.Count;
|
||||
foreach (var photo in photos)
|
||||
{
|
||||
// 删除云存储文件
|
||||
await _storageProvider.DeleteAsync(photo.PhotoUrl);
|
||||
// 删除数据库记录
|
||||
await _photoRepository.DeleteAsync(photo.Id);
|
||||
}
|
||||
|
||||
_logger.LogInformation("清理用户孤立照片: UserId={UserId}, Count={Count}", userId, count);
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,10 +76,16 @@ public class RecommendService : IRecommendService
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<RecommendUserResponse>> GetDailyRecommendAsync(long userId)
|
||||
public async Task<PagedResult<RecommendUserResponse>> GetDailyRecommendAsync(long userId, int pageIndex = 1, int pageSize = 10)
|
||||
{
|
||||
var today = DateTime.Today;
|
||||
|
||||
// 未登录用户,返回默认推荐(随机推荐已审核通过的用户)
|
||||
if (userId <= 0)
|
||||
{
|
||||
return await GetDefaultRecommendAsync(pageIndex, pageSize);
|
||||
}
|
||||
|
||||
// 获取今日推荐列表
|
||||
var recommends = await _recommendRepository.GetListAsync(
|
||||
r => r.UserId == userId && r.RecommendDate == today);
|
||||
|
|
@ -92,9 +98,16 @@ public class RecommendService : IRecommendService
|
|||
r => r.UserId == userId && r.RecommendDate == today);
|
||||
}
|
||||
|
||||
var total = recommends.Count;
|
||||
var pagedRecommends = recommends
|
||||
.OrderBy(r => r.Sort)
|
||||
.Skip((pageIndex - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToList();
|
||||
|
||||
var result = new List<RecommendUserResponse>();
|
||||
|
||||
foreach (var recommend in recommends.OrderBy(r => r.Sort))
|
||||
foreach (var recommend in pagedRecommends)
|
||||
{
|
||||
var recommendUser = await _userRepository.GetByIdAsync(recommend.RecommendUserId);
|
||||
if (recommendUser == null || recommendUser.Status != 1)
|
||||
|
|
@ -117,12 +130,18 @@ public class RecommendService : IRecommendService
|
|||
UserId = recommendUser.Id,
|
||||
Nickname = recommendUser.Nickname,
|
||||
Avatar = recommendUser.Avatar,
|
||||
Gender = recommendUser.Gender ?? 0,
|
||||
Age = DateTime.Now.Year - profile.BirthYear,
|
||||
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,
|
||||
MonthlyIncome = profile.MonthlyIncome,
|
||||
Intro = profile.Introduction,
|
||||
IsMember = recommendUser.IsMember,
|
||||
IsRealName = recommendUser.IsRealName,
|
||||
IsPhotoPublic = profile.IsPhotoPublic,
|
||||
|
|
@ -134,7 +153,85 @@ public class RecommendService : IRecommendService
|
|||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
return new PagedResult<RecommendUserResponse>
|
||||
{
|
||||
Items = result,
|
||||
Total = total,
|
||||
PageIndex = pageIndex,
|
||||
PageSize = pageSize
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取默认推荐列表(未登录用户)
|
||||
/// </summary>
|
||||
private async Task<PagedResult<RecommendUserResponse>> GetDefaultRecommendAsync(int pageIndex, int pageSize)
|
||||
{
|
||||
// 获取已完成资料、状态正常的用户
|
||||
var allUsers = await _userRepository.GetListAsync(
|
||||
u => u.IsProfileCompleted && u.Status == 1);
|
||||
|
||||
// 过滤出审核通过的用户
|
||||
var validUsers = new List<(Core.Entities.Biz.User User, Core.Entities.Biz.UserProfile Profile)>();
|
||||
foreach (var user in allUsers.OrderByDescending(u => u.CreateTime))
|
||||
{
|
||||
var profiles = await _profileRepository.GetListAsync(p => p.UserId == user.Id);
|
||||
var profile = profiles.FirstOrDefault();
|
||||
if (profile != null && profile.AuditStatus == (int)AuditStatus.Approved)
|
||||
{
|
||||
validUsers.Add((user, profile));
|
||||
}
|
||||
}
|
||||
|
||||
var total = validUsers.Count;
|
||||
var pagedUsers = validUsers
|
||||
.Skip((pageIndex - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToList();
|
||||
|
||||
var result = new List<RecommendUserResponse>();
|
||||
var sort = (pageIndex - 1) * pageSize;
|
||||
|
||||
foreach (var (user, profile) in pagedUsers)
|
||||
{
|
||||
var photos = await _photoRepository.GetListAsync(p => p.UserId == user.Id);
|
||||
var firstPhoto = profile.IsPhotoPublic ? photos.OrderBy(p => p.Sort).FirstOrDefault()?.PhotoUrl : null;
|
||||
|
||||
result.Add(new RecommendUserResponse
|
||||
{
|
||||
UserId = user.Id,
|
||||
Nickname = user.Nickname,
|
||||
Avatar = user.Avatar,
|
||||
Gender = user.Gender ?? 0,
|
||||
Age = DateTime.Now.Year - profile.BirthYear,
|
||||
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,
|
||||
MonthlyIncome = profile.MonthlyIncome,
|
||||
Intro = profile.Introduction,
|
||||
IsMember = user.IsMember,
|
||||
IsRealName = user.IsRealName,
|
||||
IsPhotoPublic = profile.IsPhotoPublic,
|
||||
FirstPhoto = firstPhoto,
|
||||
ViewedToday = false,
|
||||
MatchScore = 0,
|
||||
Sort = sort++,
|
||||
IsNewUser = user.CreateTime > DateTime.Now.AddMinutes(-NewUserPriorityMinutes)
|
||||
});
|
||||
}
|
||||
|
||||
return new PagedResult<RecommendUserResponse>
|
||||
{
|
||||
Items = result,
|
||||
Total = total,
|
||||
PageIndex = pageIndex,
|
||||
PageSize = pageSize
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using XiangYi.Application.Interfaces;
|
||||
using XiangYi.Core.Entities.Biz;
|
||||
using XiangYi.Core.Interfaces;
|
||||
|
||||
namespace XiangYi.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 系统配置服务实现
|
||||
/// </summary>
|
||||
public class SystemConfigService : ISystemConfigService
|
||||
{
|
||||
private readonly IRepository<SystemConfig> _configRepository;
|
||||
private readonly ILogger<SystemConfigService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 默认头像配置键
|
||||
/// </summary>
|
||||
public const string DefaultAvatarKey = "default_avatar";
|
||||
|
||||
public SystemConfigService(
|
||||
IRepository<SystemConfig> configRepository,
|
||||
ILogger<SystemConfigService> logger)
|
||||
{
|
||||
_configRepository = configRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> GetConfigValueAsync(string key)
|
||||
{
|
||||
var configs = await _configRepository.GetListAsync(c => c.ConfigKey == key);
|
||||
return configs.FirstOrDefault()?.ConfigValue;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> SetConfigValueAsync(string key, string value, string? description = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var configs = await _configRepository.GetListAsync(c => c.ConfigKey == key);
|
||||
var config = configs.FirstOrDefault();
|
||||
|
||||
if (config != null)
|
||||
{
|
||||
config.ConfigValue = value;
|
||||
config.UpdateTime = DateTime.Now;
|
||||
if (description != null)
|
||||
{
|
||||
config.Description = description;
|
||||
}
|
||||
await _configRepository.UpdateAsync(config);
|
||||
}
|
||||
else
|
||||
{
|
||||
config = new SystemConfig
|
||||
{
|
||||
ConfigKey = key,
|
||||
ConfigValue = value,
|
||||
ConfigType = "system",
|
||||
Description = description,
|
||||
CreateTime = DateTime.Now,
|
||||
UpdateTime = DateTime.Now
|
||||
};
|
||||
await _configRepository.AddAsync(config);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "设置配置失败: {Key}", key);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> GetDefaultAvatarAsync()
|
||||
{
|
||||
return await GetConfigValueAsync(DefaultAvatarKey);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> SetDefaultAvatarAsync(string avatarUrl)
|
||||
{
|
||||
return await SetConfigValueAsync(DefaultAvatarKey, avatarUrl, "新用户默认头像");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Dictionary<string, string>> GetAllConfigsAsync()
|
||||
{
|
||||
var configs = await _configRepository.GetListAsync(c => true);
|
||||
return configs.ToDictionary(c => c.ConfigKey, c => c.ConfigValue);
|
||||
}
|
||||
}
|
||||