feat: 支持从小程序获取微信头像和昵称同步到 H5

- 后端 UserProfileUpdate 新增 avatar_url 字段
- LoginPage 检测 URL 参数 name/avatar 并调用 updateUserProfile
- ProfilePage 头像卡片显示真实头像(有 avatar_url 时)
- ProfilePage 增加 基本信息-姓名 编辑字段
This commit is contained in:
2026-05-27 20:24:56 +08:00
parent 150dafc72f
commit bceb4f4619
3 changed files with 43 additions and 4 deletions

View File

@@ -27,6 +27,7 @@ class UserProfileUpdate(BaseModel):
multiple_pregnancy: Optional[str] = None multiple_pregnancy: Optional[str] = None
pregnancy_count: Optional[int] = None pregnancy_count: Optional[int] = None
emergency_contact: Optional[EmergencyContactSchema] = None emergency_contact: Optional[EmergencyContactSchema] = None
avatar_url: Optional[str] = None
class OnboardingRequest(BaseModel): class OnboardingRequest(BaseModel):

View File

@@ -3,7 +3,7 @@ import { useNavigate } from "react-router";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { Heart } from "lucide-react"; import { Heart } from "lucide-react";
import { useUser } from "../context/UserContext"; import { useUser } from "../context/UserContext";
import { devLogin, createDummyToken } from "../services/api"; import { devLogin, createDummyToken, updateUserProfile } from "../services/api";
import { debugLog, clearDebugLogs } from "../services/debug"; import { debugLog, clearDebugLogs } from "../services/debug";
export function LoginPage() { export function LoginPage() {
@@ -35,6 +35,18 @@ export function LoginPage() {
setTokenAndUser(token, { id: String(uid), openid: openidParam, phone }); setTokenAndUser(token, { id: String(uid), openid: openidParam, phone });
debugLog(`setTokenAndUser: id=${uid}, openid=${openidParam}, phone=${phone}`); debugLog(`setTokenAndUser: id=${uid}, openid=${openidParam}, phone=${phone}`);
// 同步昵称和头像到后端
const nameParam = params.get('name');
const avatarParam = params.get('avatar');
if (nameParam || avatarParam) {
const updateData: Record<string, unknown> = {};
if (nameParam) updateData.name = decodeURIComponent(nameParam);
if (avatarParam) updateData.avatar_url = decodeURIComponent(avatarParam);
updateUserProfile(updateData).then(() => {
debugLog(`synced: name=${nameParam}, avatar=${(avatarParam || '').substring(0, 30)}...`);
}).catch((e) => debugLog(`sync error: ${e.message}`));
}
fetchProfile().then((dto) => { fetchProfile().then((dto) => {
if (dto) debugLog(`fetchProfile: name=${dto.name}, phone=${dto.phone}, onboarded=${dto.is_onboarded}`); if (dto) debugLog(`fetchProfile: name=${dto.name}, phone=${dto.phone}, onboarded=${dto.is_onboarded}`);
navigate(dto?.is_onboarded ? "/home" : "/onboarding", { replace: true }); navigate(dto?.is_onboarded ? "/home" : "/onboarding", { replace: true });

View File

@@ -32,6 +32,7 @@ interface UserInfo {
phone: string; phone: string;
email: string; email: string;
address: string; address: string;
avatar_url: string | null;
lastPeriod: string; lastPeriod: string;
dueDate: string; dueDate: string;
currentWeek: number; currentWeek: number;
@@ -59,6 +60,7 @@ export function ProfilePage() {
phone: user.phone || "未设置", phone: user.phone || "未设置",
email: "", email: "",
address: "", address: "",
avatar_url: user.avatar_url || null,
lastPeriod: user.lastPeriodDate || "", lastPeriod: user.lastPeriodDate || "",
dueDate: user.dueDate || "", dueDate: user.dueDate || "",
currentWeek: user.currentWeek, currentWeek: user.currentWeek,
@@ -232,9 +234,13 @@ export function ProfilePage() {
<Card className="bg-gradient-to-br from-morandi-400 to-morandi-500 border-0 text-white p-6 rounded-3xl shadow-sm"> <Card className="bg-gradient-to-br from-morandi-400 to-morandi-500 border-0 text-white p-6 rounded-3xl shadow-sm">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="relative"> <div className="relative">
{userInfo.avatar_url ? (
<img src={userInfo.avatar_url} alt="头像" className="w-20 h-20 rounded-full object-cover" />
) : (
<div className="w-20 h-20 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center"> <div className="w-20 h-20 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center">
<User className="w-10 h-10 text-white" /> <User className="w-10 h-10 text-white" />
</div> </div>
)}
<button className="absolute bottom-0 right-0 w-6 h-6 bg-white rounded-full flex items-center justify-center shadow-sm"> <button className="absolute bottom-0 right-0 w-6 h-6 bg-white rounded-full flex items-center justify-center shadow-sm">
<Camera className="w-3 h-3 text-morandi-500" /> <Camera className="w-3 h-3 text-morandi-500" />
</button> </button>
@@ -252,6 +258,26 @@ export function ProfilePage() {
</Card> </Card>
</motion.div> </motion.div>
{/* 基本信息 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.05 }}
>
<h2 className="text-base font-semibold text-gray-800 mb-3 flex items-center gap-2">
<User className="w-5 h-5 text-morandi-500" />
基本信息
</h2>
<Card className="bg-white rounded-2xl p-5 shadow-sm border-0">
<InfoItem
label="姓名"
value={userInfo.name}
field="name"
icon={User}
/>
</Card>
</motion.div>
{/* 孕期信息 */} {/* 孕期信息 */}
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}