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)
创建管理员
-
+
管理员列表
刷新
-
-
-
-
-
-
-
-
-
-
- 重置密码
-
- {{ scope.row.is_active === 1 ? '停用' : '启用' }}
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ 重置密码
+
+ {{ scope.row.is_active === 1 ? '停用' : '启用' }}
+
+
+
+
+
+
@@ -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 @@
-
-
-
- 活动
-
- 热度活动
- {{item.title}}
- {{item.start_time}}
- {{item.remainText}}
+
+
+
+
+ 活动
+
+ 热度活动
+ {{activity.title}}
+ {{activity.start_time}}
+ {{activity.remainText}}
+
-
-
+
+
@@ -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