diff --git a/admin-web/src/utils/api.js b/admin-web/src/utils/api.js
index 8191510..0af0262 100644
--- a/admin-web/src/utils/api.js
+++ b/admin-web/src/utils/api.js
@@ -1,4 +1,4 @@
-const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://ghxiangqin.com/api/v1'
+export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'https://ghxiangqin.com/api/v1'
export async function apiRequest(url, options = {}) {
const token = localStorage.getItem('admin_token') || ''
diff --git a/admin-web/src/views/activities/ActivityCreateView.vue b/admin-web/src/views/activities/ActivityCreateView.vue
index b0ce0c7..64c7f8b 100644
--- a/admin-web/src/views/activities/ActivityCreateView.vue
+++ b/admin-web/src/views/activities/ActivityCreateView.vue
@@ -2,10 +2,14 @@
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
-import { apiRequest } from '../../utils/api'
+import { API_BASE_URL, apiRequest } from '../../utils/api'
const router = useRouter()
const submitting = ref(false)
+const coverUploading = ref(false)
+const descriptionUploading = ref(false)
+const descriptionEditor = ref(null)
+const resizingState = ref(null)
const form = reactive({
title: '',
description: '',
@@ -24,8 +28,208 @@ const form = reactive({
is_published: false
})
+function getEditorHtml() {
+ return descriptionEditor.value?.innerHTML || ''
+}
+
+function syncDescriptionFromEditor() {
+ form.description = getEditorHtml()
+}
+
+function onDescriptionInput() {
+ syncDescriptionFromEditor()
+}
+
+function insertHtmlAtCursor(html) {
+ const editor = descriptionEditor.value
+ if (!editor) return
+ editor.focus()
+
+ const selection = window.getSelection()
+ if (!selection || selection.rangeCount === 0) {
+ editor.insertAdjacentHTML('beforeend', html)
+ syncDescriptionFromEditor()
+ return
+ }
+
+ const range = selection.getRangeAt(0)
+ if (!editor.contains(range.commonAncestorContainer)) {
+ editor.insertAdjacentHTML('beforeend', html)
+ syncDescriptionFromEditor()
+ return
+ }
+
+ range.deleteContents()
+ const fragment = range.createContextualFragment(html)
+ const lastNode = fragment.lastChild
+ range.insertNode(fragment)
+ if (lastNode) {
+ range.setStartAfter(lastNode)
+ range.setEndAfter(lastNode)
+ selection.removeAllRanges()
+ selection.addRange(range)
+ }
+ syncDescriptionFromEditor()
+}
+
+function parseImageSizeValue(value, fallback) {
+ const match = String(value || '').match(/\d+/)
+ return match ? Number(match[0]) : fallback
+}
+
+function updateResizableImage(image, widthPx) {
+ const nextWidth = Math.max(180, Math.min(widthPx, 720))
+ image.dataset.width = String(nextWidth)
+ image.style.width = `${nextWidth}px`
+ image.style.maxWidth = '100%'
+ image.style.height = 'auto'
+ image.style.display = 'block'
+ image.style.margin = '0 auto'
+ image.style.objectFit = 'contain'
+}
+
+function clearImageResizeOverlay() {
+ const editor = descriptionEditor.value
+ if (!editor) return
+ editor.querySelector('.image-resize-overlay')?.remove()
+ resizingState.value = null
+}
+
+function showImageResizeOverlay(image) {
+ const editor = descriptionEditor.value
+ if (!editor) return
+ clearImageResizeOverlay()
+
+ const wrapper = image.closest('.activity-image-wrap')
+ if (!wrapper) return
+
+ const overlay = document.createElement('div')
+ overlay.className = 'image-resize-overlay'
+ const handle = document.createElement('span')
+ handle.className = 'image-resize-handle'
+ overlay.appendChild(handle)
+ wrapper.appendChild(overlay)
+
+ const syncOverlay = () => {
+ overlay.style.width = `${image.offsetWidth}px`
+ overlay.style.height = `${image.offsetHeight}px`
+ }
+
+ syncOverlay()
+
+ const onPointerDown = (event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ resizingState.value = { image, overlay }
+ const originX = event.clientX
+ const originWidth = parseImageSizeValue(image.dataset.width, image.getBoundingClientRect().width)
+
+ const moveHandler = (moveEvent) => {
+ const delta = moveEvent.clientX - originX
+ updateResizableImage(image, originWidth + delta)
+ syncOverlay()
+ }
+
+ const upHandler = () => {
+ document.removeEventListener('pointermove', moveHandler)
+ document.removeEventListener('pointerup', upHandler)
+ syncDescriptionFromEditor()
+ clearImageResizeOverlay()
+ }
+
+ document.addEventListener('pointermove', moveHandler)
+ document.addEventListener('pointerup', upHandler)
+ }
+
+ handle.addEventListener('pointerdown', onPointerDown)
+ overlay.addEventListener('pointerdown', (event) => event.stopPropagation())
+ editor.addEventListener('pointerdown', clearImageResizeOverlay, { once: true })
+}
+
+function prepareResizableImage(image) {
+ if (!image.dataset.width) {
+ const naturalWidth = image.naturalWidth || image.getBoundingClientRect().width || 360
+ updateResizableImage(image, Math.min(Math.max(naturalWidth, 280), 720))
+ } else {
+ updateResizableImage(image, parseImageSizeValue(image.dataset.width, 360))
+ }
+
+ image.classList.add('activity-image')
+ image.contentEditable = 'false'
+ image.addEventListener('click', (event) => {
+ event.preventDefault()
+ event.stopPropagation()
+ showImageResizeOverlay(image)
+ })
+}
+
+async function uploadImage(file) {
+ const formData = new FormData()
+ formData.append('file', file)
+ const token = localStorage.getItem('admin_token') || ''
+ const response = await fetch(`${API_BASE_URL}/upload/activity-image`, {
+ method: 'POST',
+ headers: {
+ Authorization: token ? `Bearer ${token}` : ''
+ },
+ body: formData
+ })
+ const text = await response.text()
+ let data = null
+ if (text) {
+ try {
+ data = JSON.parse(text)
+ } catch {
+ throw new Error(text || response.statusText || '图片上传失败')
+ }
+ }
+ if (!response.ok || !data || data.code !== 0) {
+ throw new Error(data?.detail || data?.message || response.statusText || '图片上传失败')
+ }
+ return data.data.image_url
+}
+
+async function handleCoverChange(event) {
+ const file = event.target.files?.[0]
+ if (!file) return
+ coverUploading.value = true
+ try {
+ form.cover_image = await uploadImage(file)
+ ElMessage.success('封面图上传成功')
+ } catch (error) {
+ ElMessage.error(error.message)
+ } finally {
+ coverUploading.value = false
+ event.target.value = ''
+ }
+}
+
+async function handleDescriptionImageUpload(event) {
+ const file = event.target.files?.[0]
+ if (!file) return
+ descriptionUploading.value = true
+ try {
+ const url = await uploadImage(file)
+ insertHtmlAtCursor(`

`)
+ const editor = descriptionEditor.value
+ const images = editor?.querySelectorAll('.activity-image')
+ const image = images?.[images.length - 1]
+ if (image) {
+ prepareResizableImage(image)
+ clearImageResizeOverlay()
+ }
+ ElMessage.success('图片插入成功')
+ } catch (error) {
+ ElMessage.error(error.message)
+ } finally {
+ descriptionUploading.value = false
+ event.target.value = ''
+ }
+}
+
async function createActivity() {
submitting.value = true
+ syncDescriptionFromEditor()
try {
await apiRequest('/admin/activities', {
method: 'POST',
@@ -68,7 +272,17 @@ function goBack() {
-
+
+
封面图
+
+
+ 支持 JPG / PNG,上传后自动填入封面地址
+
+
+
![封面预览]()
+
+
尚未上传封面图
+
-
-
-
-
+
+
+
+
-
+
+
+
点击图片后可拖动右下角手柄调整大小。
+
+
取消
创建活动
@@ -170,4 +417,148 @@ function goBack() {
.full-row {
grid-column: 1 / -1;
}
+
+.field-label {
+ font-weight: 600;
+ color: #334155;
+ margin-bottom: 10px;
+}
+
+.cover-upload,
+.description-box,
+.numeric-field {
+ padding: 16px;
+ border: 1px solid #e5e7eb;
+ border-radius: 16px;
+ background: #fafafa;
+}
+
+.numeric-field {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.field-hint {
+ font-size: 13px;
+ color: #64748b;
+ line-height: 1.6;
+}
+
+.cover-upload-row,
+.field-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.upload-note {
+ color: #64748b;
+ font-size: 13px;
+}
+
+.cover-preview {
+ margin-top: 12px;
+}
+
+.cover-preview img {
+ width: 100%;
+ max-height: 240px;
+ object-fit: cover;
+ border-radius: 14px;
+ display: block;
+}
+
+.empty-preview {
+ margin-top: 12px;
+ padding: 20px;
+ border-radius: 14px;
+ text-align: center;
+ color: #94a3b8;
+ background: #fff;
+ border: 1px dashed #d1d5db;
+}
+
+.image-upload-button {
+ position: relative;
+ overflow: hidden;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 10px 14px;
+ border-radius: 10px;
+ background: #0f172a;
+ color: #fff;
+ cursor: pointer;
+ font-size: 14px;
+}
+
+.image-upload-button.disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.image-upload-button input {
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+ cursor: inherit;
+}
+
+.description-editor {
+ min-height: 220px;
+ padding: 14px;
+ border-radius: 14px;
+ border: 1px solid #dbe1ea;
+ background: #fff;
+ line-height: 1.8;
+ outline: none;
+}
+
+.description-editor :deep(.activity-image-wrap) {
+ position: relative;
+ display: block;
+ width: fit-content;
+ max-width: 100%;
+ margin: 12px auto;
+}
+
+.description-editor :deep(.activity-image) {
+ display: block;
+ width: auto;
+ max-width: min(70%, 720px);
+ max-height: 320px;
+ object-fit: contain;
+ border-radius: 12px;
+ margin: 0 auto;
+}
+
+.description-editor :deep(.image-resize-overlay) {
+ position: absolute;
+ inset: 0;
+ border: 2px solid #3b82f6;
+ border-radius: 14px;
+ pointer-events: none;
+ box-sizing: border-box;
+}
+
+.description-editor :deep(.image-resize-handle) {
+ position: absolute;
+ right: -7px;
+ bottom: -7px;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: #2563eb;
+ border: 2px solid #fff;
+ box-shadow: 0 2px 8px rgba(37, 99, 235, 0.35);
+ cursor: nwse-resize;
+ pointer-events: auto;
+}
+
+.description-editor:empty::before {
+ content: attr(data-placeholder);
+ color: #94a3b8;
+}
diff --git a/backend/app/routers/upload.py b/backend/app/routers/upload.py
index 6c6b402..6bd8b06 100644
--- a/backend/app/routers/upload.py
+++ b/backend/app/routers/upload.py
@@ -2,8 +2,12 @@ from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
-from app.dependencies import get_current_user_id
-from app.services.upload_service import upload_user_avatar, upload_user_profile_image
+from app.dependencies import get_current_admin, get_current_user_id
+from app.services.upload_service import (
+ upload_activity_image,
+ upload_user_avatar,
+ upload_user_profile_image,
+)
from app.services.user_service import get_user_by_id
router = APIRouter()
@@ -43,3 +47,17 @@ async def upload_profile_image(
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
return {"code": 0, "message": "ok", "data": data}
+
+
+@router.post("/activity-image")
+async def upload_activity_cover_image(
+ file: UploadFile = File(...),
+ current_user_id: int = Depends(get_current_user_id),
+) -> dict:
+ _ = current_user_id
+ try:
+ data = await upload_activity_image(file)
+ except ValueError as exc:
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
+
+ return {"code": 0, "message": "ok", "data": data}
diff --git a/backend/app/services/activity_match_service.py b/backend/app/services/activity_match_service.py
index 19300d5..3cbd449 100644
--- a/backend/app/services/activity_match_service.py
+++ b/backend/app/services/activity_match_service.py
@@ -28,19 +28,11 @@ def _format_datetime(value: datetime | None) -> str | None:
return value.isoformat() if value else None
-def _birth_year_range(birth_year: int | None) -> str | None:
- if not birth_year:
- return None
- start = birth_year - 2
- end = birth_year + 2
- return f"{start}-{end}"
-
-
def _candidate_item(user: User, selected: bool = False) -> dict:
return {
"user_id": user.id,
"nickname": user.nickname,
- "birth_year_range": _birth_year_range(user.birth_year),
+ "birth_year": user.birth_year,
"city": user.city,
"personality_tags": user.personality_tags if isinstance(user.personality_tags, list) else [],
"avatar_blur_url": user.avatar_blur_url,
diff --git a/backend/app/services/upload_service.py b/backend/app/services/upload_service.py
index 51fa3d3..66443f0 100644
--- a/backend/app/services/upload_service.py
+++ b/backend/app/services/upload_service.py
@@ -17,6 +17,7 @@ from app.models.user import User
ALLOWED_IMAGE_TYPES = {"jpeg", "png"}
UPLOAD_ROOT = Path(__file__).resolve().parent.parent / "static" / "uploads"
MAX_PROFILE_IMAGES = 6
+MAX_ACTIVITY_IMAGE_SIZE = 5 * 1024 * 1024
def _validate_image(content: bytes) -> str:
@@ -76,10 +77,7 @@ def _append_profile_image(existing_images: list[str] | None, avatar_url: str) ->
async def upload_user_avatar(session: AsyncSession, user: User, file: UploadFile) -> dict:
content = await file.read()
- if not content:
- raise ValueError("上传文件不能为空")
- if len(content) > 5 * 1024 * 1024:
- raise ValueError("图片大小不能超过 5MB")
+ _validate_upload_content(content)
main_path, blur_path, timestamp = _save_optimized_variants(content)
@@ -110,13 +108,44 @@ async def upload_user_avatar(session: AsyncSession, user: User, file: UploadFile
Path(blur_path).unlink(missing_ok=True)
-async def upload_user_profile_image(session: AsyncSession, user: User, file: UploadFile) -> dict:
- content = await file.read()
+def _validate_upload_content(content: bytes) -> None:
if not content:
raise ValueError("上传文件不能为空")
- if len(content) > 5 * 1024 * 1024:
+ if len(content) > MAX_ACTIVITY_IMAGE_SIZE:
raise ValueError("图片大小不能超过 5MB")
+
+def _store_generic_image(content: bytes, folder: str, filename: str) -> str:
+ _validate_image(content)
+ target_dir = UPLOAD_ROOT / folder
+ target_dir.mkdir(parents=True, exist_ok=True)
+ target_path = target_dir / filename
+ with Image.open(BytesIO(content)) as image:
+ image = image.convert("RGB")
+ image.thumbnail((1600, 1600))
+ image.save(target_path, format="JPEG", quality=88)
+ return str(target_path)
+
+
+async def upload_activity_image(file: UploadFile, *, folder: str = "activities") -> dict:
+ content = await file.read()
+ _validate_upload_content(content)
+
+ timestamp = str(int(time()))
+ filename = f"{timestamp}_{uuid4().hex}.jpg"
+ stored_path = _store_generic_image(content, folder, filename)
+ url_path = f"/static/uploads/{folder}/{filename}"
+ return {
+ "image_url": url_path,
+ "local_file": filename,
+ "stored_path": stored_path,
+ }
+
+
+async def upload_user_profile_image(session: AsyncSession, user: User, file: UploadFile) -> dict:
+ content = await file.read()
+ _validate_upload_content(content)
+
main_path, blur_path, timestamp = _save_optimized_variants(content)
try:
diff --git a/miniprogram/pages/activity-detail/activity-detail.js b/miniprogram/pages/activity-detail/activity-detail.js
index 995d3af..20d5663 100644
--- a/miniprogram/pages/activity-detail/activity-detail.js
+++ b/miniprogram/pages/activity-detail/activity-detail.js
@@ -10,12 +10,39 @@ Page({
submitting: false
},
+ parseLaunchOptions(options = {}) {
+ const normalized = { ...options }
+
+ if (normalized.scene) {
+ const sceneText = decodeURIComponent(normalized.scene)
+ sceneText.split('&').forEach((segment) => {
+ const [rawKey, ...rest] = segment.split('=')
+ const key = rawKey || ''
+ if (!key) {
+ return
+ }
+ normalized[key] = rest.join('=')
+ })
+ }
+
+ const rawId = normalized.id || normalized.activity_id || ''
+ const parsedId = rawId !== '' && !Number.isNaN(Number(rawId)) ? Number(rawId) : null
+ const shareToken = normalized.share_token || ''
+
+ return {
+ id: parsedId,
+ shareToken
+ }
+ },
+
onLoad(options) {
- this.setData({
- id: options.id ? Number(options.id) : null,
- shareToken: options.share_token || ''
- })
- this.loadDetail()
+ const { id, shareToken } = this.parseLaunchOptions(options)
+ this.setData({ id, shareToken })
+ if (id || shareToken) {
+ this.loadDetail()
+ return
+ }
+ wx.showToast({ title: '活动链接无效', icon: 'none' })
},
onShow() {
@@ -96,7 +123,12 @@ Page({
wx.showToast({ title: '当前阶段暂不可进入匹配', icon: 'none' })
return
}
- wx.navigateTo({ url: `/pages/match/match?source=activity&activity_id=${this.data.id}` })
+
+ wx.setStorageSync('match_entry_context', {
+ source: 'activity',
+ activity_id: this.data.id
+ })
+ wx.switchTab({ url: '/pages/match/match' })
},
goMatchResults() {
diff --git a/miniprogram/pages/activity-detail/activity-detail.wxml b/miniprogram/pages/activity-detail/activity-detail.wxml
index 565cf64..ab21173 100644
--- a/miniprogram/pages/activity-detail/activity-detail.wxml
+++ b/miniprogram/pages/activity-detail/activity-detail.wxml
@@ -31,9 +31,11 @@
-
-
-
diff --git a/miniprogram/pages/activity-detail/activity-detail.wxss b/miniprogram/pages/activity-detail/activity-detail.wxss
index 4575896..213408d 100644
--- a/miniprogram/pages/activity-detail/activity-detail.wxss
+++ b/miniprogram/pages/activity-detail/activity-detail.wxss
@@ -76,11 +76,28 @@
left: 0;
right: 0;
bottom: 0;
- padding: 24rpx;
+ padding: 20rpx 24rpx calc(20rpx + env(safe-area-inset-bottom));
background: #ffffff;
box-shadow: 0 -12rpx 32rpx rgba(0, 0, 0, 0.06);
}
+.bottom-actions {
+ display: flex;
+ gap: 16rpx;
+ margin-bottom: 16rpx;
+}
+
+.action-btn {
+ flex: 1;
+ border-radius: 999rpx;
+ font-size: 28rpx;
+ line-height: 1.2;
+}
+
+.primary-action {
+ width: 100%;
+}
+
.primary-btn {
background: #e8593c;
color: #ffffff;
diff --git a/miniprogram/pages/activity/activity.wxml b/miniprogram/pages/activity/activity.wxml
index 725c1be..6711a31 100644
--- a/miniprogram/pages/activity/activity.wxml
+++ b/miniprogram/pages/activity/activity.wxml
@@ -23,18 +23,18 @@
{{item.locationText}}
{{item.start_time}}
-
+
diff --git a/miniprogram/pages/home/home.wxml b/miniprogram/pages/home/home.wxml
index c5e9770..5cd2eeb 100644
--- a/miniprogram/pages/home/home.wxml
+++ b/miniprogram/pages/home/home.wxml
@@ -37,8 +37,8 @@
@@ -57,7 +57,7 @@
- 暂无优质推荐
+ 暂无用户
diff --git a/miniprogram/pages/match/match.js b/miniprogram/pages/match/match.js
index 554c891..1b97f69 100644
--- a/miniprogram/pages/match/match.js
+++ b/miniprogram/pages/match/match.js
@@ -37,10 +37,22 @@ Page({
},
onLoad(options) {
+ const storedEntry = wx.getStorageSync('match_entry_context') || {}
+ const source = options.source || storedEntry.source || 'manual'
+ const activityId = options.activity_id
+ ? Number(options.activity_id)
+ : storedEntry.activity_id
+ ? Number(storedEntry.activity_id)
+ : null
+
this.setData({
- source: options.source || 'manual',
- activityId: options.activity_id ? Number(options.activity_id) : null
+ source,
+ activityId
})
+
+ if (storedEntry.source || storedEntry.activity_id) {
+ wx.removeStorageSync('match_entry_context')
+ }
},
onShareAppMessage() {
@@ -63,7 +75,7 @@ Page({
if (this.data.source === 'activity' && this.data.activityId) {
await this.loadActivityMatchState()
}
- this.loadCandidates()
+ await this.loadCandidates()
},
syncOperateState() {
@@ -135,7 +147,8 @@ Page({
this.setData({ loading: true })
try {
let candidates = []
- if (this.data.source === 'activity' && this.data.activityId) {
+ if (this.data.activityId) {
+ this.setData({ source: 'activity' })
candidates = await request({ url: `/activities/${this.data.activityId}/candidates` })
} else {
candidates = await request({ url: `/matches/candidates?source=${this.data.source}` })
@@ -157,6 +170,9 @@ Page({
aiSuggestionCount: 0
})
this.syncDeckCandidates()
+ if (this.data.source === 'activity') {
+ wx.showToast({ title: normalizedCandidates.length ? '已进入活动匹配' : '暂无可匹配对象', icon: 'none' })
+ }
} catch (err) {
console.error('Load candidates failed:', err)
wx.showToast({ title: err.message || '候选加载失败', icon: 'none' })
@@ -170,7 +186,7 @@ Page({
if (!this.ensureCanOperate()) {
return
}
- if (this.data.aiMatching || this.data.source === 'activity') {
+ if (this.data.aiMatching || this.data.activityId || this.data.source === 'activity') {
return
}
this.setData({ loading: true, source: 'ai', aiMatching: true })
@@ -233,7 +249,7 @@ Page({
...item,
avatar_blur_url: resolveImageUrl(item.avatar_blur_url || ''),
nicknameText: item.nickname || '匿名用户',
- birthYearRangeText: item.birth_year_range || '年龄未知',
+ birthYearText: item.birth_year != null ? String(item.birth_year) : '出生年份未知',
cityText: item.city || '城市未知',
personality_tags: personalityTags,
matchScoreText: hasMatchScore ? safeMatchScore.toFixed(1) : '',
@@ -449,8 +465,8 @@ Page({
},
getAiStatusText() {
- if (this.data.source === 'activity') {
- return this.data.stageText || '仅展示当前活动内异性嘉宾'
+ if (this.data.activityId || this.data.source === 'activity') {
+ return this.data.stageText || '仅展示当前活动内可匹配嘉宾'
}
if (this.data.aiMatching) {
return 'AI 正在推荐中'
diff --git a/miniprogram/pages/match/match.wxml b/miniprogram/pages/match/match.wxml
index 6f80ce6..c1f641a 100644
--- a/miniprogram/pages/match/match.wxml
+++ b/miniprogram/pages/match/match.wxml
@@ -1,21 +1,29 @@
-
-
-
- {{source === 'activity' ? (activityTitle || '活动专属推荐') : '今日推荐'}}
- {{source === 'activity' ? '仅展示当前活动内的异性嘉宾,右滑加入我的选择。' : '优先展示同城、兴趣重叠和价值观更接近的候选人。'}}
+
+
+ 活动模式
+ {{activityTitle || '活动专属匹配'}}
+
+
+
+ {{activityMatchText}}
+
+
+
+ 我的选择与结果
- {{aiMatching ? '匹配中...' : '匹配推荐'}}
- {{selectionSummaryText}}
- {{activityMatchText}}
- 左滑跳过 · 右滑选择 · 点击卡片查看资料
- {{getAiStatusText()}} · 共 {{aiSuggestionCount}} 人
-
-
- 我的选择与结果
+
+
+ 匹配
+ 优先展示同城、兴趣重叠和价值观更接近的候选人。
+
+ {{aiMatching ? '匹配中...' : '随机展示'}}
+ {{getAiStatusText()}} · 共 {{aiSuggestionCount}} 人
+ 左滑跳过 · 右滑选择 · 点击卡片查看资料
+
正在加载推荐候选人...
@@ -56,7 +64,7 @@
{{currentCandidate.nicknameText}}
- {{currentCandidate.birthYearRangeText}} · {{currentCandidate.cityText}}
+ {{currentCandidate.birthYearText}} · {{currentCandidate.cityText}}
diff --git a/miniprogram/pages/match/match.wxss b/miniprogram/pages/match/match.wxss
index 4216841..e44c7d4 100644
--- a/miniprogram/pages/match/match.wxss
+++ b/miniprogram/pages/match/match.wxss
@@ -7,6 +7,70 @@
flex-direction: column;
}
+.match-page-activity {
+ padding-top: 20rpx;
+}
+
+.activity-mode-panel {
+ margin-bottom: 24rpx;
+ padding: 28rpx 28rpx 24rpx;
+ border-radius: 30rpx;
+ background: linear-gradient(180deg, #fff3eb 0%, #ffffff 100%);
+ box-shadow: 0 16rpx 40rpx rgba(232, 89, 60, 0.08);
+ border: 1rpx solid rgba(232, 89, 60, 0.08);
+}
+
+.activity-mode-pill {
+ display: inline-flex;
+ align-items: center;
+ padding: 8rpx 18rpx;
+ border-radius: 999rpx;
+ background: rgba(232, 89, 60, 0.1);
+ color: #d95c3d;
+ font-size: 22rpx;
+ font-weight: 600;
+}
+
+.activity-mode-title {
+ margin-top: 14rpx;
+ font-size: 38rpx;
+ font-weight: 700;
+ color: #2f1b16;
+}
+
+.activity-mode-desc {
+ margin-top: 10rpx;
+ color: #7d655d;
+ line-height: 1.6;
+ font-size: 24rpx;
+}
+
+.activity-mode-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12rpx;
+ margin-top: 18rpx;
+}
+
+.activity-mode-meta-item {
+ padding: 10rpx 16rpx;
+ border-radius: 999rpx;
+ background: #fff8f5;
+ color: #a06a5d;
+ font-size: 22rpx;
+}
+
+.activity-mode-actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 18rpx;
+}
+
+.activity-mode-btn {
+ padding: 0 26rpx;
+ font-size: 24rpx;
+}
+
.hero-box {
display: flex;
align-items: center;
@@ -24,6 +88,11 @@
font-size: 24rpx;
}
+.match-page-activity .swipe-tip,
+.match-page-activity .ai-status {
+ display: none;
+}
+
.ai-status {
margin: 0 8rpx 24rpx;
color: #b07a69;
diff --git a/miniprogram/pages/my-matches/my-matches.js b/miniprogram/pages/my-matches/my-matches.js
index 83cc011..df41a66 100644
--- a/miniprogram/pages/my-matches/my-matches.js
+++ b/miniprogram/pages/my-matches/my-matches.js
@@ -16,9 +16,20 @@ Page({
},
onLoad(options) {
+ const storedEntry = wx.getStorageSync('match_entry_context') || {}
+ const activityId = options.activity_id
+ ? Number(options.activity_id)
+ : storedEntry.activity_id
+ ? Number(storedEntry.activity_id)
+ : null
+
this.setData({
- activityId: options.activity_id ? Number(options.activity_id) : null
+ activityId
})
+
+ if (storedEntry.activity_id) {
+ wx.removeStorageSync('match_entry_context')
+ }
},
onShow() {
diff --git a/miniprogram/pages/my-matches/my-matches.wxml b/miniprogram/pages/my-matches/my-matches.wxml
index 3e70c11..803b692 100644
--- a/miniprogram/pages/my-matches/my-matches.wxml
+++ b/miniprogram/pages/my-matches/my-matches.wxml
@@ -1,4 +1,9 @@
+
+