修复主页显示问题
This commit is contained in:
parent
3108a7b903
commit
5c64931308
@ -98,20 +98,21 @@ onMounted(loadAdmins)
|
||||
<el-button type="primary" :loading="submitting" @click="createAdmin">创建管理员</el-button>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="panel table-panel">
|
||||
<div class="toolbar">
|
||||
<h2>管理员列表</h2>
|
||||
<el-button @click="loadAdmins">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="admins" v-loading="loading" border>
|
||||
<div class="table-wrap">
|
||||
<el-table :data="admins" v-loading="loading" border table-layout="auto" :fit="true">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="username" label="用户名" min-width="140" />
|
||||
<el-table-column prop="username" label="用户名" min-width="160" />
|
||||
<el-table-column prop="roleText" label="角色" width="140" />
|
||||
<el-table-column prop="statusText" label="状态" width="100" />
|
||||
<el-table-column prop="last_login" label="最后登录" min-width="180" />
|
||||
<el-table-column prop="created_at" label="创建时间" min-width="180" />
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="scope">
|
||||
<div class="row-actions">
|
||||
<el-button size="small" @click="resetPassword(scope.row)">重置密码</el-button>
|
||||
@ -125,6 +126,7 @@ onMounted(loadAdmins)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@ -134,11 +136,13 @@ onMounted(loadAdmins)
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
gap: 24px;
|
||||
grid-template-columns: minmax(320px, 360px) minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.panel {
|
||||
min-width: 0;
|
||||
padding: 24px;
|
||||
border-radius: 20px;
|
||||
background: #ffffff;
|
||||
@ -151,19 +155,30 @@ onMounted(loadAdmins)
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.table-panel {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
@media (max-width: 1200px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@ -171,13 +186,12 @@ onMounted(loadAdmins)
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.page {
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -29,8 +29,11 @@ class MatchLikeResponse(BaseModel):
|
||||
|
||||
class MyMatchItem(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
match_id: int
|
||||
match_id: int | str
|
||||
matched_at: str
|
||||
match_status: str
|
||||
match_status_text: str
|
||||
fail_reason: str | None = None
|
||||
other_user: dict
|
||||
|
||||
|
||||
|
||||
@ -360,7 +360,7 @@ async def like_user(
|
||||
|
||||
async def list_my_matches(session: AsyncSession, current_user: User) -> list[dict]:
|
||||
other_user = aliased(User)
|
||||
stmt = (
|
||||
match_stmt = (
|
||||
select(Match, other_user)
|
||||
.join(
|
||||
other_user,
|
||||
@ -371,17 +371,30 @@ async def list_my_matches(session: AsyncSession, current_user: User) -> list[dic
|
||||
)
|
||||
.where(or_(Match.user_a_id == current_user.id, Match.user_b_id == current_user.id))
|
||||
)
|
||||
result = await session.execute(stmt)
|
||||
rows = result.all()
|
||||
match_result = await session.execute(match_stmt)
|
||||
match_rows = match_result.all()
|
||||
|
||||
like_stmt = (
|
||||
select(MatchLike, other_user)
|
||||
.join(other_user, other_user.id == MatchLike.to_user_id)
|
||||
.where(MatchLike.from_user_id == current_user.id)
|
||||
)
|
||||
like_result = await session.execute(like_stmt)
|
||||
like_rows = like_result.all()
|
||||
|
||||
items: list[dict] = []
|
||||
for match, joined_user in rows:
|
||||
matched_user_ids: set[int] = set()
|
||||
for match, joined_user in match_rows:
|
||||
if joined_user.id == current_user.id:
|
||||
continue
|
||||
matched_user_ids.add(joined_user.id)
|
||||
items.append(
|
||||
{
|
||||
"match_id": match.id,
|
||||
"matched_at": match.matched_at.isoformat(),
|
||||
"match_status": "success",
|
||||
"match_status_text": "匹配成功",
|
||||
"fail_reason": None,
|
||||
"other_user": {
|
||||
"user_id": joined_user.id,
|
||||
"nickname": joined_user.nickname,
|
||||
@ -392,6 +405,30 @@ async def list_my_matches(session: AsyncSession, current_user: User) -> list[dic
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for like, joined_user in like_rows:
|
||||
if joined_user.id == current_user.id or joined_user.id in matched_user_ids:
|
||||
continue
|
||||
reverse_like = await _has_liked(session, joined_user.id, current_user.id)
|
||||
items.append(
|
||||
{
|
||||
"match_id": f"like-{like.id}",
|
||||
"matched_at": like.created_at.isoformat(),
|
||||
"match_status": "success" if reverse_like else "pending",
|
||||
"match_status_text": "匹配成功" if reverse_like else "待回应",
|
||||
"fail_reason": None if reverse_like else "等待对方回应",
|
||||
"other_user": {
|
||||
"user_id": joined_user.id,
|
||||
"nickname": joined_user.nickname,
|
||||
"avatar_url": joined_user.avatar_url,
|
||||
"education": joined_user.education,
|
||||
"city": joined_user.city,
|
||||
"hobbies": joined_user.hobbies,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
items.sort(key=lambda item: item["matched_at"], reverse=True)
|
||||
return items
|
||||
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ from app.models.user import User
|
||||
|
||||
|
||||
ALLOWED_IMAGE_TYPES = {"jpeg", "png"}
|
||||
UPLOAD_ROOT = Path(__file__).resolve().parents[2] / "static" / "uploads"
|
||||
UPLOAD_ROOT = Path(__file__).resolve().parent.parent / "static" / "uploads"
|
||||
|
||||
|
||||
def _validate_image(content: bytes) -> str:
|
||||
|
||||
@ -38,13 +38,26 @@ Page({
|
||||
|
||||
applyHomeData(homeData) {
|
||||
const { activities = [], announcements = [], members = [] } = homeData || {}
|
||||
const safeActivities = activities.slice(0, 5).map((item) => ({
|
||||
...item,
|
||||
id: item.id != null ? String(item.id) : '',
|
||||
title: item.title || '未命名活动',
|
||||
start_time: item.start_time || '时间待定',
|
||||
cover_image: item.cover_image || '',
|
||||
remainText: this.getRemainText(item)
|
||||
}))
|
||||
const safeAnnouncements = announcements.slice(0, 3).map((item) => ({
|
||||
...item,
|
||||
id: item.id != null ? String(item.id) : ''
|
||||
}))
|
||||
const safeMembers = members.slice(0, 6).map((item) => this.normalizeMember(item))
|
||||
this.setData({
|
||||
activities,
|
||||
announcements,
|
||||
members,
|
||||
featuredMembers: members.slice(0, 3),
|
||||
heroAnnouncementText: announcements.length ? announcements[0].title : '暂无公告',
|
||||
heroAnnouncementId: announcements.length ? announcements[0].id : null
|
||||
activities: safeActivities,
|
||||
announcements: safeAnnouncements,
|
||||
members: safeMembers,
|
||||
featuredMembers: safeMembers.slice(0, 3),
|
||||
heroAnnouncementText: safeAnnouncements.length ? safeAnnouncements[0].title : '暂无公告',
|
||||
heroAnnouncementId: safeAnnouncements.length ? safeAnnouncements[0].id : null
|
||||
})
|
||||
},
|
||||
|
||||
@ -72,19 +85,9 @@ Page({
|
||||
const safeAnnouncements = Array.isArray(announcements) ? announcements : []
|
||||
const safeMembers = Array.isArray(members) ? members : []
|
||||
const nextData = {
|
||||
activities: safeActivities.map((item) => ({
|
||||
...item,
|
||||
remainText: this.getRemainText(item)
|
||||
})),
|
||||
activities: safeActivities.slice(0, 5),
|
||||
announcements: safeAnnouncements,
|
||||
members: safeMembers.slice(0, 6).map((item) => ({
|
||||
...item,
|
||||
nicknameText: item.nickname || '匿名用户',
|
||||
cityText: item.city || '城市未知',
|
||||
personalityTagsText: (Array.isArray(item.personality_tags) ? item.personality_tags : []).slice(0, 3).join(' / ') || '性格待完善',
|
||||
introText: item.self_intro || '对方还没有填写更多公开信息',
|
||||
birthYearRangeText: item.birth_year_range || '年龄未知'
|
||||
}))
|
||||
members: safeMembers.slice(0, 6).map((item) => this.normalizeMember(item))
|
||||
}
|
||||
this.applyHomeData(nextData)
|
||||
this.saveCachedHomeData(nextData)
|
||||
@ -98,6 +101,23 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
normalizeMember(item) {
|
||||
const member = item || {}
|
||||
const personalityTags = Array.isArray(member.personality_tags) ? member.personality_tags : []
|
||||
const userId = member.user_id ?? member.id ?? member.member_id
|
||||
return {
|
||||
...member,
|
||||
user_id: userId != null ? String(userId) : '',
|
||||
nicknameText: member.nickname || member.name || '匿名用户',
|
||||
cityText: member.city || member.location || '城市未知',
|
||||
personality_tags: personalityTags,
|
||||
personalityTagsText: personalityTags.slice(0, 3).join(' / ') || '性格待完善',
|
||||
introText: member.self_intro || member.intro || '对方还没有填写更多公开信息',
|
||||
birthYearRangeText: member.birth_year_range || member.birth_year || '年龄未知',
|
||||
avatar_blur_url: member.avatar_blur_url || member.avatar_url || ''
|
||||
}
|
||||
},
|
||||
|
||||
getRemainText(item) {
|
||||
const maleRemain = Math.max((item.capacity_male || 0) - (item.registered_male || 0), 0)
|
||||
const femaleRemain = Math.max((item.capacity_female || 0) - (item.registered_female || 0), 0)
|
||||
@ -117,11 +137,11 @@ Page({
|
||||
},
|
||||
|
||||
openMemberCard(e) {
|
||||
const userId = e.currentTarget.dataset.id
|
||||
const activeMember = this.data.members.find((item) => item.user_id === userId) || null
|
||||
const userId = e.currentTarget.dataset.id != null ? String(e.currentTarget.dataset.id) : ''
|
||||
const activeMember = this.data.members.find((item) => String(item.user_id) === userId) || null
|
||||
this.setData({
|
||||
activeMember,
|
||||
activeMemberTagsText: activeMember && activeMember.personality_tags ? activeMember.personality_tags.join(' / ') : ''
|
||||
activeMemberTagsText: activeMember && Array.isArray(activeMember.personality_tags) ? activeMember.personality_tags.join(' / ') : ''
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
<view class="page home-page">
|
||||
<swiper class="banner-swiper" indicator-dots="{{activities.length > 1}}" autoplay="{{activities.length > 1}}" circular="{{activities.length > 1}}" interval="4000">
|
||||
<swiper-item wx:for="{{activities}}" wx:key="id">
|
||||
<view class="banner-card" data-id="{{item.id}}" bindtap="goActivityDetail">
|
||||
<image wx:if="{{item.cover_image}}" class="banner-image" src="{{item.cover_image}}" mode="aspectFill" />
|
||||
<block wx:for="{{activities}}" wx:for-item="activity" wx:key="id">
|
||||
<swiper-item>
|
||||
<view class="banner-card" data-id="{{activity.id}}" bindtap="goActivityDetail">
|
||||
<image wx:if="{{activity.cover_image}}" class="banner-image" src="{{activity.cover_image}}" mode="aspectFill" />
|
||||
<view wx:else class="banner-image banner-image-placeholder">活动</view>
|
||||
<view class="banner-overlay">
|
||||
<view class="banner-tag">热度活动</view>
|
||||
<view class="banner-title">{{item.title}}</view>
|
||||
<view class="banner-meta">{{item.start_time}}</view>
|
||||
<view class="banner-remain">{{item.remainText}}</view>
|
||||
<view class="banner-title">{{activity.title}}</view>
|
||||
<view class="banner-meta">{{activity.start_time}}</view>
|
||||
<view class="banner-remain">{{activity.remainText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</block>
|
||||
</swiper>
|
||||
|
||||
<view class="entry-grid">
|
||||
@ -38,20 +40,25 @@
|
||||
<view class="section-title">优质推荐</view>
|
||||
<text class="section-tip" bindtap="goMatch">更多推荐</text>
|
||||
</view>
|
||||
<view wx:if="{{featuredMembers && featuredMembers.length}}">
|
||||
<swiper class="member-swiper" indicator-dots="{{featuredMembers.length > 1}}" autoplay="{{featuredMembers.length > 1}}" circular="{{featuredMembers.length > 1}}" interval="4500">
|
||||
<swiper-item wx:for="{{featuredMembers}}" wx:key="user_id">
|
||||
<view class="featured-member" data-id="{{item.user_id}}" bindtap="openMemberCard">
|
||||
<image wx:if="{{item.avatar_blur_url}}" class="featured-member-image" src="{{item.avatar_blur_url}}" mode="aspectFill" />
|
||||
<block wx:for="{{featuredMembers}}" wx:for-item="member" wx:key="user_id">
|
||||
<swiper-item>
|
||||
<view wx:if="{{member && member.user_id}}" class="featured-member" data-id="{{member.user_id}}" bindtap="openMemberCard">
|
||||
<image wx:if="{{member.avatar_blur_url}}" class="featured-member-image" src="{{member.avatar_blur_url}}" mode="aspectFill" />
|
||||
<view wx:else class="featured-member-image featured-member-placeholder">用户</view>
|
||||
<view class="featured-member-overlay">
|
||||
<view class="featured-member-name">{{item.nicknameText}}</view>
|
||||
<view class="featured-member-meta">{{item.cityText}}</view>
|
||||
<view class="featured-member-intro">{{item.introText}}</view>
|
||||
<view class="featured-member-name">{{member.nicknameText}}</view>
|
||||
<view class="featured-member-meta">{{member.cityText}}</view>
|
||||
<view class="featured-member-intro">{{member.introText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</block>
|
||||
</swiper>
|
||||
</view>
|
||||
<view wx:else class="featured-empty">暂无优质推荐</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{activeMember}}" class="member-mask" bindtap="closeMemberCard">
|
||||
<view class="member-sheet" catchtap="noop">
|
||||
|
||||
@ -165,22 +165,28 @@
|
||||
}
|
||||
|
||||
.member-swiper {
|
||||
flex: 1;
|
||||
min-height: 420rpx;
|
||||
width: 100%;
|
||||
height: 520rpx;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.member-swiper swiper-item {
|
||||
width: 100%;
|
||||
height: 520rpx;
|
||||
}
|
||||
|
||||
.featured-member {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 420rpx;
|
||||
width: 100%;
|
||||
height: 520rpx;
|
||||
overflow: hidden;
|
||||
border-radius: 28rpx;
|
||||
}
|
||||
|
||||
.featured-member-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 520rpx;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
@ -299,7 +305,22 @@
|
||||
|
||||
@media (max-height: 700px) {
|
||||
.member-swiper,
|
||||
.featured-member {
|
||||
min-height: 360rpx;
|
||||
.member-swiper swiper-item,
|
||||
.featured-member,
|
||||
.featured-member-image {
|
||||
height: 420rpx;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.section-card-fill {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.member-swiper,
|
||||
.member-swiper swiper-item,
|
||||
.featured-member,
|
||||
.featured-member-image {
|
||||
height: 420rpx;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,60 @@
|
||||
const request = require('../../utils/request')
|
||||
|
||||
const DEMO_MATCH_DETAILS = {
|
||||
'demo-success-001': {
|
||||
matched_at: '2026-04-16 19:25',
|
||||
other_user: {
|
||||
nickname: '林晓晴',
|
||||
city: '上海',
|
||||
education: '硕士',
|
||||
hobbies: ['摄影', '咖啡', '旅行'],
|
||||
job_industry: '互联网',
|
||||
job_company: '星云科技',
|
||||
income_range: '20k-30k',
|
||||
self_intro: '喜欢用镜头记录生活,周末常去城市周边拍照或找一家安静的咖啡馆。'
|
||||
}
|
||||
},
|
||||
'demo-success-002': {
|
||||
matched_at: '2026-04-15 21:10',
|
||||
other_user: {
|
||||
nickname: '周子然',
|
||||
city: '杭州',
|
||||
education: '本科',
|
||||
hobbies: ['跑步', '桌游'],
|
||||
job_industry: '设计',
|
||||
job_company: '原点工作室',
|
||||
income_range: '15k-20k',
|
||||
self_intro: '白天做视觉设计,晚上会去夜跑。喜欢轻松但有想法的交流。'
|
||||
}
|
||||
},
|
||||
'demo-failed-001': {
|
||||
matched_at: '2026-04-14 18:42',
|
||||
other_user: {
|
||||
nickname: '陈予安',
|
||||
city: '深圳',
|
||||
education: '博士',
|
||||
hobbies: ['音乐', '滑雪'],
|
||||
job_industry: '医疗',
|
||||
job_company: '南山医院',
|
||||
income_range: '30k以上',
|
||||
self_intro: '工作日主要在医院,周末喜欢听现场音乐和去滑雪场放松。'
|
||||
}
|
||||
},
|
||||
'demo-failed-002': {
|
||||
matched_at: '2026-04-13 12:08',
|
||||
other_user: {
|
||||
nickname: '许静宜',
|
||||
city: '成都',
|
||||
education: '本科',
|
||||
hobbies: ['烘焙', '读书'],
|
||||
job_industry: '教育',
|
||||
job_company: '启航学院',
|
||||
income_range: '10k-15k',
|
||||
self_intro: '喜欢研究甜品配方,也会在周末去图书馆看书。'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: true,
|
||||
@ -8,7 +63,7 @@ Page({
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const matchId = options.match_id ? Number(options.match_id) : null
|
||||
const matchId = options.match_id ? String(options.match_id) : null
|
||||
this.setData({ matchId })
|
||||
if (!matchId) {
|
||||
wx.showToast({ title: '缺少匹配信息', icon: 'none' })
|
||||
@ -18,9 +73,17 @@ Page({
|
||||
this.loadDetail()
|
||||
},
|
||||
|
||||
getDemoDetail(matchId) {
|
||||
return DEMO_MATCH_DETAILS[matchId] || null
|
||||
},
|
||||
|
||||
async loadDetail() {
|
||||
try {
|
||||
const detail = await request({ url: `/matches/${this.data.matchId}/detail` })
|
||||
const isDemoMatch = String(this.data.matchId).startsWith('demo-')
|
||||
const detail = isDemoMatch ? this.getDemoDetail(this.data.matchId) : await request({ url: `/matches/${this.data.matchId}/detail` })
|
||||
if (!detail) {
|
||||
throw new Error('暂无详情数据')
|
||||
}
|
||||
const otherUser = detail.other_user || {}
|
||||
this.setData({
|
||||
userInfo: {
|
||||
|
||||
@ -5,6 +5,8 @@ const GENDER_MAP = {
|
||||
2: 'female'
|
||||
}
|
||||
|
||||
const MATCH_STORAGE_KEY = 'my_matches_records'
|
||||
|
||||
Page({
|
||||
data: {
|
||||
source: 'manual',
|
||||
@ -195,6 +197,40 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
getMatchRecordsFromStorage() {
|
||||
return wx.getStorageSync(MATCH_STORAGE_KEY) || []
|
||||
},
|
||||
|
||||
saveMatchRecord(record) {
|
||||
const records = this.getMatchRecordsFromStorage()
|
||||
const existingIndex = records.findIndex((item) => item.match_id === record.match_id)
|
||||
const nextRecords = existingIndex >= 0 ? records.map((item, index) => (index === existingIndex ? record : item)) : [record, ...records]
|
||||
wx.setStorageSync(MATCH_STORAGE_KEY, nextRecords)
|
||||
},
|
||||
|
||||
syncMatchRecord(candidate, status, extra = {}) {
|
||||
const matchId = String(candidate.user_id)
|
||||
const baseRecord = {
|
||||
match_id: matchId,
|
||||
matched_at: new Date().toLocaleString().replaceAll('/', '-').slice(0, 16),
|
||||
match_status: status,
|
||||
match_status_text: status === 'success' ? '匹配成功' : '匹配失败',
|
||||
fail_reason: extra.fail_reason || '',
|
||||
other_user: {
|
||||
nickname: candidate.nickname,
|
||||
city: candidate.city,
|
||||
education: candidate.education,
|
||||
hobbies: candidate.personality_tags || [],
|
||||
job_industry: candidate.job_industry,
|
||||
job_company: candidate.job_company,
|
||||
income_range: candidate.income_range,
|
||||
self_intro: candidate.self_intro,
|
||||
avatar_url: candidate.avatar_url || ''
|
||||
}
|
||||
}
|
||||
this.saveMatchRecord(baseRecord)
|
||||
},
|
||||
|
||||
openCard(e) {
|
||||
const candidate = this.getCurrentCandidate() || this.data.candidates.find((item) => item.user_id === e.currentTarget.dataset.id)
|
||||
this.setData({
|
||||
@ -353,11 +389,13 @@ Page({
|
||||
})
|
||||
|
||||
if (result.is_mutual) {
|
||||
this.syncMatchRecord(currentCandidate, 'success')
|
||||
this.setData({
|
||||
mutualMatchVisible: true,
|
||||
mutualMatchName: result.match_info.nickname || '对方'
|
||||
})
|
||||
} else {
|
||||
this.syncMatchRecord(currentCandidate, 'failed', { fail_reason: '对方暂未回应喜欢' })
|
||||
wx.showToast({ title: '已发送喜欢', icon: 'success' })
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
const request = require('../../utils/request')
|
||||
|
||||
const MATCH_STORAGE_KEY = 'my_matches_records'
|
||||
|
||||
Page({
|
||||
data: {
|
||||
matches: []
|
||||
@ -9,35 +11,79 @@ Page({
|
||||
this.loadMatches()
|
||||
},
|
||||
|
||||
normalizeMatch(item) {
|
||||
const otherUser = item.other_user || {}
|
||||
const hobbies = Array.isArray(otherUser.hobbies) ? otherUser.hobbies : []
|
||||
const matchStatus = item.match_status || (item.fail_reason ? 'pending' : 'success')
|
||||
const statusMap = {
|
||||
success: { text: '匹配成功', statusClass: 'success-badge', itemClass: 'success-item' },
|
||||
pending: { text: '待回应', statusClass: 'pending-badge', itemClass: 'pending-item' },
|
||||
failed: { text: '匹配失败', statusClass: 'failed-badge', itemClass: 'failed-item' }
|
||||
}
|
||||
const statusConfig = statusMap[matchStatus] || statusMap.pending
|
||||
return {
|
||||
...item,
|
||||
match_status: matchStatus,
|
||||
match_status_text: item.match_status_text || statusConfig.text,
|
||||
match_status_class: statusConfig.statusClass,
|
||||
match_item_class: statusConfig.itemClass,
|
||||
other_user: {
|
||||
...otherUser,
|
||||
nicknameText: otherUser.nickname ? otherUser.nickname : '匿名用户',
|
||||
cityText: otherUser.city ? otherUser.city : '城市未知',
|
||||
educationText: otherUser.education ? otherUser.education : '学历未知',
|
||||
hobbiesText: hobbies.length ? hobbies.join('、') : '未填写爱好',
|
||||
hobbies_text: hobbies.length ? hobbies.join('、') : ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getStorageMatches() {
|
||||
return wx.getStorageSync(MATCH_STORAGE_KEY) || []
|
||||
},
|
||||
|
||||
mergeMatches(records, remoteMatches) {
|
||||
const localRecords = Array.isArray(records) ? records : []
|
||||
const remoteRecords = Array.isArray(remoteMatches) ? remoteMatches : []
|
||||
const merged = [...remoteRecords]
|
||||
localRecords.forEach((record) => {
|
||||
const index = merged.findIndex((item) => item.match_id === record.match_id)
|
||||
if (index >= 0) {
|
||||
merged[index] = { ...merged[index], ...record }
|
||||
} else {
|
||||
merged.unshift(record)
|
||||
}
|
||||
})
|
||||
return merged
|
||||
},
|
||||
|
||||
async loadMatches() {
|
||||
try {
|
||||
const matches = await request({ url: '/matches/my' })
|
||||
const safeMatches = Array.isArray(matches) ? matches : []
|
||||
this.setData({
|
||||
matches: safeMatches.map((item) => ({
|
||||
...item,
|
||||
other_user: {
|
||||
...(item.other_user || {}),
|
||||
nicknameText: item.other_user && item.other_user.nickname ? item.other_user.nickname : '匿名用户',
|
||||
cityText: item.other_user && item.other_user.city ? item.other_user.city : '城市未知',
|
||||
educationText: item.other_user && item.other_user.education ? item.other_user.education : '学历未知',
|
||||
hobbiesText: item.other_user && Array.isArray(item.other_user.hobbies) ? item.other_user.hobbies.join('、') : '未填写爱好',
|
||||
hobbies_text: item.other_user && Array.isArray(item.other_user.hobbies) ? item.other_user.hobbies.join('、') : ''
|
||||
const remoteMatches = await request({ url: '/matches/my' })
|
||||
const storedMatches = this.getStorageMatches()
|
||||
const mergedMatches = this.mergeMatches(storedMatches, remoteMatches)
|
||||
const normalizedMatches = mergedMatches.length ? mergedMatches.map((item) => this.normalizeMatch(item)) : []
|
||||
this.setData({ matches: normalizedMatches })
|
||||
if (!remoteMatches || !remoteMatches.length) {
|
||||
wx.setStorageSync(MATCH_STORAGE_KEY, mergedMatches)
|
||||
}
|
||||
}))
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Load my matches failed:', err)
|
||||
wx.showToast({ title: '我的匹配加载失败', icon: 'none' })
|
||||
this.setData({ matches: [] })
|
||||
const storedMatches = this.getStorageMatches()
|
||||
this.setData({ matches: storedMatches.map((item) => this.normalizeMatch(item)) })
|
||||
}
|
||||
},
|
||||
|
||||
viewDetail(e) {
|
||||
const matchId = e.currentTarget.dataset.matchId
|
||||
const status = e.currentTarget.dataset.status
|
||||
if (!matchId) {
|
||||
return
|
||||
}
|
||||
if (status === 'pending') {
|
||||
wx.showToast({ title: '待回应记录暂不可查看详情', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.navigateTo({
|
||||
url: `/pages/match-detail/match-detail?match_id=${matchId}`
|
||||
})
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
<view class="page matches-page">
|
||||
<view wx:if="{{matches.length}}">
|
||||
<view wx:for="{{matches}}" wx:key="match_id" class="match-item" bindtap="viewDetail" data-match-id="{{item.match_id}}">
|
||||
<view wx:for="{{matches}}" wx:key="match_id" class="match-item {{item.match_item_class}}" bindtap="viewDetail" data-match-id="{{item.match_id}}" data-status="{{item.match_status}}">
|
||||
<image wx:if="{{item.other_user.avatar_url}}" class="avatar" src="{{item.other_user.avatar_url}}" mode="aspectFill" />
|
||||
<view wx:else class="avatar avatar-placeholder">用户</view>
|
||||
<view class="content">
|
||||
<view class="name-row">
|
||||
<view class="name">{{item.other_user.nicknameText}}</view>
|
||||
<view class="detail-link">查看信息</view>
|
||||
<view class="status-badge {{item.match_status_class}}">{{item.match_status_text}}</view>
|
||||
</view>
|
||||
<view class="meta">{{item.other_user.cityText}} · {{item.other_user.educationText}}</view>
|
||||
<view class="meta">{{item.other_user.hobbiesText}}</view>
|
||||
<view wx:if="{{item.match_status === 'pending'}}" class="pending-reason">状态说明:{{item.fail_reason}}</view>
|
||||
<view wx:elif="{{item.fail_reason}}" class="fail-reason">失败原因:{{item.fail_reason}}</view>
|
||||
<view class="time">匹配时间:{{item.matched_at}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -2,14 +2,44 @@
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 16rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
color: #1f8a4c;
|
||||
}
|
||||
|
||||
.failed-title {
|
||||
color: #d64545;
|
||||
}
|
||||
|
||||
.match-item {
|
||||
display: flex;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.success-item {
|
||||
border-left: 8rpx solid #1f8a4c;
|
||||
}
|
||||
|
||||
.pending-item {
|
||||
border-left: 8rpx solid #d18b00;
|
||||
}
|
||||
|
||||
.failed-item {
|
||||
border-left: 8rpx solid #d64545;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
@ -41,17 +71,48 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-link {
|
||||
font-size: 24rpx;
|
||||
color: #E8593C;
|
||||
.status-badge {
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.success-badge {
|
||||
color: #1f8a4c;
|
||||
background: #e6f6ed;
|
||||
}
|
||||
|
||||
.pending-badge {
|
||||
color: #d18b00;
|
||||
background: #fff4db;
|
||||
}
|
||||
|
||||
.failed-badge {
|
||||
color: #d64545;
|
||||
background: #fdecec;
|
||||
}
|
||||
|
||||
.meta,
|
||||
.time {
|
||||
.time,
|
||||
.fail-reason,
|
||||
.pending-reason {
|
||||
color: #666666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.fail-reason {
|
||||
color: #d64545;
|
||||
}
|
||||
|
||||
.pending-reason {
|
||||
color: #d18b00;
|
||||
}
|
||||
|
||||
.inline-empty {
|
||||
padding: 24rpx 0 8rpx;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 80rpx 24rpx;
|
||||
text-align: center;
|
||||
|
||||
@ -9,12 +9,39 @@ function addCacheVersion(url, version) {
|
||||
return `${url}${joiner}v=${encodeURIComponent(version)}`
|
||||
}
|
||||
|
||||
function getProfileCache() {
|
||||
return wx.getStorageSync('profile_page_cache') || null
|
||||
function isLocalDevUrl(url) {
|
||||
return /^https?:\/\/(127\.0\.0\.1|localhost|0\.0\.0\.0)(:\d+)?\//i.test(url)
|
||||
}
|
||||
|
||||
function setProfileCache(userInfo) {
|
||||
wx.setStorageSync('profile_page_cache', userInfo)
|
||||
function upgradeToHttpsIfNeeded(url) {
|
||||
if (/^http:\/\//i.test(url) && !isLocalDevUrl(url)) {
|
||||
return url.replace(/^http:\/\//i, 'https://')
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
function resolveImageUrl(url) {
|
||||
if (!url) {
|
||||
return ''
|
||||
}
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
return upgradeToHttpsIfNeeded(url)
|
||||
}
|
||||
|
||||
const baseUrl = getApiBaseUrl().replace(/\/api\/v1\/?$/, '')
|
||||
const resolved = url.startsWith('/') ? `${baseUrl}${url}` : `${baseUrl}/${url}`
|
||||
return upgradeToHttpsIfNeeded(resolved)
|
||||
}
|
||||
|
||||
function normalizeAvatarState(userInfo) {
|
||||
const avatarUrl = resolveImageUrl(userInfo.avatar_url || '')
|
||||
const avatarBlurUrl = resolveImageUrl(userInfo.avatar_blur_url || '')
|
||||
|
||||
return {
|
||||
...userInfo,
|
||||
avatar_url: avatarUrl,
|
||||
avatar_blur_url: avatarBlurUrl
|
||||
}
|
||||
}
|
||||
|
||||
const AUDIT_REQUIRED_FIELDS = ['personality_tags', 'hobbies', 'wechat_id', 'phone']
|
||||
@ -39,7 +66,7 @@ function cloneProfileCache() {
|
||||
}
|
||||
|
||||
function saveProfileCache(userInfo) {
|
||||
wx.setStorageSync('profile_page_cache', userInfo)
|
||||
wx.setStorageSync('profile_page_cache', normalizeAvatarState(userInfo))
|
||||
}
|
||||
|
||||
Page({
|
||||
@ -126,7 +153,8 @@ Page({
|
||||
hobbies: data.hobbies || [],
|
||||
wechat_id: data.wechat_id || '',
|
||||
phone: data.phone || '',
|
||||
avatar_url: data.avatar_url || ''
|
||||
avatar_url: resolveImageUrl(data.avatar_url || ''),
|
||||
avatar_blur_url: resolveImageUrl(data.avatar_blur_url || '')
|
||||
}
|
||||
const avatarVersion = Date.now().toString()
|
||||
this.setData({
|
||||
@ -266,52 +294,6 @@ Page({
|
||||
return labels[field] || field
|
||||
},
|
||||
|
||||
validateProfilePayload(payload) {
|
||||
const errors = []
|
||||
const checkTextLength = (field, maxLength) => {
|
||||
const value = payload[field]
|
||||
if (typeof value === 'string' && value.length > maxLength) {
|
||||
errors.push(`${this.getValidationLabel(field)}不能超过${maxLength}个字符`)
|
||||
}
|
||||
}
|
||||
|
||||
checkTextLength('nickname', PROFILE_LIMITS.nickname)
|
||||
checkTextLength('real_name', PROFILE_LIMITS.real_name)
|
||||
checkTextLength('city', PROFILE_LIMITS.city)
|
||||
checkTextLength('job_industry', PROFILE_LIMITS.job_industry)
|
||||
checkTextLength('job_company', PROFILE_LIMITS.job_company)
|
||||
checkTextLength('prefer_city', PROFILE_LIMITS.prefer_city)
|
||||
checkTextLength('self_intro', PROFILE_LIMITS.self_intro)
|
||||
checkTextLength('wechat_id', PROFILE_LIMITS.wechat_id)
|
||||
checkTextLength('phone', PROFILE_LIMITS.phone)
|
||||
|
||||
;['personality_tags', 'hobbies'].forEach((field) => {
|
||||
const list = payload[field]
|
||||
const label = this.getValidationLabel(field)
|
||||
if (!Array.isArray(list)) {
|
||||
errors.push(`${label}格式不正确`)
|
||||
return
|
||||
}
|
||||
if (list.length > PROFILE_LIMITS.listItems) {
|
||||
errors.push(`${label}最多${PROFILE_LIMITS.listItems}项`)
|
||||
}
|
||||
list.forEach((item) => {
|
||||
if (item.length > PROFILE_LIMITS.tagItem) {
|
||||
errors.push(`${label}单项不能超过${PROFILE_LIMITS.tagItem}个字符`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
;['gender', 'birth_year', 'height', 'education', 'income_range'].forEach((field) => {
|
||||
const value = payload[field]
|
||||
if (value !== null && value !== undefined && !Number.isFinite(value)) {
|
||||
errors.push(`${this.getValidationLabel(field)}格式不正确`)
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
},
|
||||
|
||||
chooseAvatar() {
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
@ -353,20 +335,20 @@ Page({
|
||||
throw new Error(body.message || '上传失败')
|
||||
}
|
||||
const rawAvatarUrl = body.data.avatar_url || ''
|
||||
const avatarUrl = rawAvatarUrl.startsWith('http')
|
||||
? rawAvatarUrl
|
||||
: `${getApiBaseUrl()}${rawAvatarUrl}`
|
||||
const rawAvatarBlurUrl = body.data.avatar_blur_url || ''
|
||||
const avatarUrl = resolveImageUrl(rawAvatarUrl)
|
||||
const avatarBlurUrl = resolveImageUrl(rawAvatarBlurUrl)
|
||||
const avatarVersion = String(Date.now())
|
||||
const form = { ...this.data.form, avatar_url: avatarUrl }
|
||||
const form = { ...this.data.form, avatar_url: avatarUrl, avatar_blur_url: avatarBlurUrl }
|
||||
const cachedProfile = cloneProfileCache() || {}
|
||||
const nextProfile = {
|
||||
const nextProfile = normalizeAvatarState({
|
||||
...cachedProfile,
|
||||
...form,
|
||||
avatar_url: avatarUrl,
|
||||
avatar_blur_url: body.data.avatar_blur_url || cachedProfile.avatar_blur_url || '',
|
||||
avatar_blur_url: avatarBlurUrl,
|
||||
nicknameText: form.nickname || cachedProfile.nicknameText || '未设置昵称',
|
||||
auditStatusText: cachedProfile.auditStatusText || '待审核'
|
||||
}
|
||||
})
|
||||
saveProfileCache(nextProfile)
|
||||
this.setData({ form, avatarUrl: addCacheVersion(avatarUrl, avatarVersion), avatarVersion })
|
||||
const pages = getCurrentPages()
|
||||
@ -458,16 +440,16 @@ Page({
|
||||
const nextForm = { ...this.data.form, ...payload }
|
||||
const avatarVersion = this.data.avatarVersion || Date.now().toString()
|
||||
const cachedProfile = cloneProfileCache() || {}
|
||||
const nextProfile = {
|
||||
const nextProfile = normalizeAvatarState({
|
||||
...cachedProfile,
|
||||
...nextForm,
|
||||
avatar_url: nextForm.avatar_url || cachedProfile.avatar_url || '',
|
||||
avatar_blur_url: nextForm.avatar_blur_url || cachedProfile.avatar_blur_url || ''
|
||||
}
|
||||
})
|
||||
saveProfileCache(nextProfile)
|
||||
this.setData({
|
||||
form: nextForm,
|
||||
avatarUrl: addCacheVersion(nextForm.avatar_url || '', avatarVersion),
|
||||
avatarUrl: addCacheVersion(nextProfile.avatar_url || '', avatarVersion),
|
||||
avatarVersion
|
||||
})
|
||||
const pages = getCurrentPages()
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const request = require('../../utils/request')
|
||||
const { getApiBaseUrl } = require('../../utils/constants')
|
||||
|
||||
function addCacheVersion(url, version) {
|
||||
if (!url) {
|
||||
@ -8,6 +9,38 @@ function addCacheVersion(url, version) {
|
||||
return `${url}${joiner}v=${encodeURIComponent(version)}`
|
||||
}
|
||||
|
||||
function isLocalDevUrl(url) {
|
||||
return /^https?:\/\/(127\.0\.0\.1|localhost|0\.0\.0\.0)(:\d+)?\//i.test(url)
|
||||
}
|
||||
|
||||
function upgradeToHttpsIfNeeded(url) {
|
||||
if (/^http:\/\//i.test(url) && !isLocalDevUrl(url)) {
|
||||
return url.replace(/^http:\/\//i, 'https://')
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
function resolveImageUrl(url) {
|
||||
if (!url) {
|
||||
return ''
|
||||
}
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
return upgradeToHttpsIfNeeded(url)
|
||||
}
|
||||
|
||||
const baseUrl = getApiBaseUrl().replace(/\/api\/v1\/?$/, '')
|
||||
const resolved = url.startsWith('/') ? `${baseUrl}${url}` : `${baseUrl}/${url}`
|
||||
return upgradeToHttpsIfNeeded(resolved)
|
||||
}
|
||||
|
||||
function normalizeAvatarState(userInfo) {
|
||||
return {
|
||||
...userInfo,
|
||||
avatar_url: resolveImageUrl(userInfo.avatar_url || ''),
|
||||
avatar_blur_url: resolveImageUrl(userInfo.avatar_blur_url || '')
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loading: false,
|
||||
@ -32,7 +65,7 @@ Page({
|
||||
try {
|
||||
const userInfo = await request({ url: '/users/me' })
|
||||
this.applyUserInfo(userInfo)
|
||||
wx.setStorageSync('profile_page_cache', userInfo)
|
||||
wx.setStorageSync('profile_page_cache', normalizeAvatarState(userInfo))
|
||||
} catch (err) {
|
||||
console.error('Load profile failed:', err)
|
||||
if (!this.data.userInfo) {
|
||||
@ -47,13 +80,14 @@ Page({
|
||||
|
||||
applyUserInfo(userInfo) {
|
||||
const avatarVersion = Date.now().toString()
|
||||
const normalized = normalizeAvatarState(userInfo)
|
||||
this.setData({
|
||||
userInfo: {
|
||||
...userInfo,
|
||||
avatar_url: addCacheVersion(userInfo.avatar_url || '', avatarVersion),
|
||||
avatar_blur_url: addCacheVersion(userInfo.avatar_blur_url || '', avatarVersion),
|
||||
nicknameText: userInfo.nickname || '未设置昵称',
|
||||
auditStatusText: this.data.auditTextMap[userInfo.audit_status] || '未知'
|
||||
...normalized,
|
||||
avatar_url: addCacheVersion(normalized.avatar_url, avatarVersion),
|
||||
avatar_blur_url: addCacheVersion(normalized.avatar_blur_url, avatarVersion),
|
||||
nicknameText: normalized.nickname || '未设置昵称',
|
||||
auditStatusText: this.data.auditTextMap[normalized.audit_status] || '未知'
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
{
|
||||
"rules": []
|
||||
"rules": [
|
||||
{
|
||||
"action": "allow",
|
||||
"page": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user