修复主页显示问题

This commit is contained in:
taiyi 2026-04-17 21:28:56 +08:00
parent 3108a7b903
commit 5c64931308
15 changed files with 511 additions and 178 deletions

View File

@ -98,30 +98,32 @@ 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>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" min-width="140" />
<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">
<template #default="scope">
<div class="row-actions">
<el-button size="small" @click="resetPassword(scope.row)">重置密码</el-button>
<el-button size="small" type="warning" @click="toggleStatus(scope.row)">
{{ scope.row.is_active === 1 ? '停用' : '启用' }}
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<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="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="240" fixed="right">
<template #default="scope">
<div class="row-actions">
<el-button size="small" @click="resetPassword(scope.row)">重置密码</el-button>
<el-button size="small" type="warning" @click="toggleStatus(scope.row)">
{{ scope.row.is_active === 1 ? '停用' : '启用' }}
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</div>
@ -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>

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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(' / ') : ''
})
},

View File

@ -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" />
<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>
<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">{{activity.title}}</view>
<view class="banner-meta">{{activity.start_time}}</view>
<view class="banner-remain">{{activity.remainText}}</view>
</view>
</view>
</view>
</swiper-item>
</swiper-item>
</block>
</swiper>
<view class="entry-grid">
@ -38,19 +40,24 @@
<view class="section-title">优质推荐</view>
<text class="section-tip" bindtap="goMatch">更多推荐</text>
</view>
<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" />
<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>
</view>
</swiper-item>
</swiper>
<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">
<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">{{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 File

@ -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;
}
}

View File

@ -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: {

View File

@ -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' })
}

View File

@ -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}`
})

View File

@ -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>

View File

@ -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;

View File

@ -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()

View File

@ -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] || '未知'
}
})
},

View File

@ -1,3 +1,8 @@
{
"rules": []
}
"rules": [
{
"action": "allow",
"page": "*"
}
]
}