diff --git a/admin-web/src/views/admins/AdminsView.vue b/admin-web/src/views/admins/AdminsView.vue index 12110c7..b99e476 100644 --- a/admin-web/src/views/admins/AdminsView.vue +++ b/admin-web/src/views/admins/AdminsView.vue @@ -98,30 +98,32 @@ onMounted(loadAdmins) 创建管理员 -
+

管理员列表

刷新
- - - - - - - - - - - +
+ + + + + + + + + + + +
@@ -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; } } diff --git a/backend/app/schemas/match.py b/backend/app/schemas/match.py index 0b59f08..d86a8c5 100644 --- a/backend/app/schemas/match.py +++ b/backend/app/schemas/match.py @@ -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 diff --git a/backend/app/services/match_service.py b/backend/app/services/match_service.py index ca791bb..85dd64d 100644 --- a/backend/app/services/match_service.py +++ b/backend/app/services/match_service.py @@ -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 diff --git a/backend/app/services/upload_service.py b/backend/app/services/upload_service.py index 162246f..2502c2f 100644 --- a/backend/app/services/upload_service.py +++ b/backend/app/services/upload_service.py @@ -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: diff --git a/miniprogram/pages/home/home.js b/miniprogram/pages/home/home.js index 3ce3bc0..4a6720e 100644 --- a/miniprogram/pages/home/home.js +++ b/miniprogram/pages/home/home.js @@ -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(' / ') : '' }) }, diff --git a/miniprogram/pages/home/home.wxml b/miniprogram/pages/home/home.wxml index 92f1b4f..c5e9770 100644 --- a/miniprogram/pages/home/home.wxml +++ b/miniprogram/pages/home/home.wxml @@ -1,17 +1,19 @@ @@ -38,19 +40,24 @@ 优质推荐 更多推荐 - - - - - 用户 - - {{item.nicknameText}} - {{item.cityText}} - {{item.introText}} - - - - + + + + + + + 用户 + + {{member.nicknameText}} + {{member.cityText}} + {{member.introText}} + + + + + + + 暂无优质推荐 diff --git a/miniprogram/pages/home/home.wxss b/miniprogram/pages/home/home.wxss index a36e178..f0ad04e 100644 --- a/miniprogram/pages/home/home.wxss +++ b/miniprogram/pages/home/home.wxss @@ -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; } } diff --git a/miniprogram/pages/match-detail/match-detail.js b/miniprogram/pages/match-detail/match-detail.js index 498438d..a333b21 100644 --- a/miniprogram/pages/match-detail/match-detail.js +++ b/miniprogram/pages/match-detail/match-detail.js @@ -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: { diff --git a/miniprogram/pages/match/match.js b/miniprogram/pages/match/match.js index bd4e3f1..242bd61 100644 --- a/miniprogram/pages/match/match.js +++ b/miniprogram/pages/match/match.js @@ -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' }) } diff --git a/miniprogram/pages/my-matches/my-matches.js b/miniprogram/pages/my-matches/my-matches.js index 6a2921d..c524d49 100644 --- a/miniprogram/pages/my-matches/my-matches.js +++ b/miniprogram/pages/my-matches/my-matches.js @@ -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}` }) diff --git a/miniprogram/pages/my-matches/my-matches.wxml b/miniprogram/pages/my-matches/my-matches.wxml index 0f54610..775b14c 100644 --- a/miniprogram/pages/my-matches/my-matches.wxml +++ b/miniprogram/pages/my-matches/my-matches.wxml @@ -1,15 +1,17 @@ - + 用户 {{item.other_user.nicknameText}} - 查看信息 + {{item.match_status_text}} {{item.other_user.cityText}} · {{item.other_user.educationText}} {{item.other_user.hobbiesText}} + 状态说明:{{item.fail_reason}} + 失败原因:{{item.fail_reason}} 匹配时间:{{item.matched_at}} diff --git a/miniprogram/pages/my-matches/my-matches.wxss b/miniprogram/pages/my-matches/my-matches.wxss index 55a039a..dd6a589 100644 --- a/miniprogram/pages/my-matches/my-matches.wxss +++ b/miniprogram/pages/my-matches/my-matches.wxss @@ -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; diff --git a/miniprogram/pages/profile-edit/profile-edit.js b/miniprogram/pages/profile-edit/profile-edit.js index eca6dcb..2a0eeae 100644 --- a/miniprogram/pages/profile-edit/profile-edit.js +++ b/miniprogram/pages/profile-edit/profile-edit.js @@ -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() diff --git a/miniprogram/pages/profile/profile.js b/miniprogram/pages/profile/profile.js index 99bd423..acb2374 100644 --- a/miniprogram/pages/profile/profile.js +++ b/miniprogram/pages/profile/profile.js @@ -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] || '未知' } }) }, diff --git a/miniprogram/sitemap.json b/miniprogram/sitemap.json index f70da99..e3368f5 100644 --- a/miniprogram/sitemap.json +++ b/miniprogram/sitemap.json @@ -1,3 +1,8 @@ { - "rules": [] -} + "rules": [ + { + "action": "allow", + "page": "*" + } + ] +} \ No newline at end of file