diff --git a/apps/mini-user/src/components/PageNavBar.tsx b/apps/mini-user/src/components/PageNavBar.tsx index 02de225..8203bb6 100644 --- a/apps/mini-user/src/components/PageNavBar.tsx +++ b/apps/mini-user/src/components/PageNavBar.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react'; import { View, Text } from '@tarojs/components'; -import { subPageNavBarStyle, useNavBarMetrics } from '../lib/nav-bar'; +import { subPageNavBarStyle, subPageNavContentStyle, useNavBarMetrics } from '../lib/nav-bar'; type PageNavBarProps = { title: string; @@ -25,23 +25,25 @@ export default function PageNavBar({ className={`page-nav-bar${solid ? ' page-nav-bar--solid' : ''}`} style={subPageNavBarStyle(metrics)} > + + {title} + {onBack ? ( - + ) : ( - + + )} + {right ?? ( + )} - - {title} - - {right ?? } ); diff --git a/apps/mini-user/src/components/SubPageHeader.tsx b/apps/mini-user/src/components/SubPageHeader.tsx index 0e274e6..cf78328 100644 --- a/apps/mini-user/src/components/SubPageHeader.tsx +++ b/apps/mini-user/src/components/SubPageHeader.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react'; import { View, Text } from '@tarojs/components'; import Taro from '@tarojs/taro'; -import { subPageNavBarStyle, useNavBarMetrics } from '../lib/nav-bar'; +import { subPageNavBarStyle, subPageNavContentStyle, useNavBarMetrics } from '../lib/nav-bar'; type SubPageHeaderProps = { title: string; @@ -28,14 +28,14 @@ export default function SubPageHeader({ title, onBack, right }: SubPageHeaderPro return ( + {title} - {title} {right ? {right} : null} diff --git a/apps/mini-user/src/components/TabMainHeader.tsx b/apps/mini-user/src/components/TabMainHeader.tsx index 131e0dc..25ae63d 100644 --- a/apps/mini-user/src/components/TabMainHeader.tsx +++ b/apps/mini-user/src/components/TabMainHeader.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react'; import { View, Text } from '@tarojs/components'; -import { navBarStyle, useNavBarMetrics } from '../lib/nav-bar'; +import { navBarStyle, tabNavContentStyle, useNavBarMetrics } from '../lib/nav-bar'; type TabMainHeaderProps = { title: string; @@ -13,11 +13,11 @@ export default function TabMainHeader({ title, extra }: TabMainHeaderProps) { return ( + {title} - {title} {extra ? {extra} : null} diff --git a/apps/mini-user/src/lib/mini-wechat-profile.ts b/apps/mini-user/src/lib/mini-wechat-profile.ts index aea719b..0821958 100644 --- a/apps/mini-user/src/lib/mini-wechat-profile.ts +++ b/apps/mini-user/src/lib/mini-wechat-profile.ts @@ -1,34 +1,108 @@ import Taro from '@tarojs/taro'; import { request } from './api'; -type MiniProfilePayload = { +import type { UserProfile } from './api'; + +export type MiniWechatProfile = { nickname?: string; avatarUrl?: string; }; -/** 小程序授权后拉取微信昵称/头像并上报服务端 */ -export async function syncMiniWechatProfile(): Promise { - if (process.env.TARO_ENV !== 'weapp') return; +const WX_PROFILE_CACHE_KEY = 'wx_mini_profile_cache'; - let profile: MiniProfilePayload | null = null; +export function cacheWxProfile(info: MiniWechatProfile) { + if (!info.nickname && !info.avatarUrl) return; try { - const res = await Taro.getUserProfile({ desc: '用于完善会员资料' }); - profile = { - nickname: res.userInfo?.nickName, - avatarUrl: res.userInfo?.avatarUrl, - }; + Taro.setStorageSync(WX_PROFILE_CACHE_KEY, JSON.stringify(info)); } catch { - return; - } - - if (!profile.nickname && !profile.avatarUrl) return; - - try { - await request('/auth/wechat/mini-profile', { - method: 'POST', - data: profile, - }); - } catch { - /* 用户拒绝或上报失败时不阻断主流程 */ + /* ignore */ } } + +export function getCachedWxProfile(): MiniWechatProfile | null { + try { + const raw = Taro.getStorageSync(WX_PROFILE_CACHE_KEY); + if (!raw) return null; + const parsed = JSON.parse(String(raw)) as MiniWechatProfile; + if (!parsed?.nickname && !parsed?.avatarUrl) return null; + return parsed; + } catch { + return null; + } +} + +export function isDefaultMiniNickname(nickname?: string | null): boolean { + if (!nickname || nickname === '访客') return true; + return /^用户\d{4}$/.test(nickname); +} + +export function mergeWxDisplayProfile(profile: UserProfile): UserProfile { + if (!profile.hasWechat) return profile; + const cached = getCachedWxProfile(); + if (!cached) return profile; + return { + ...profile, + nickname: + cached.nickname || + (!isDefaultMiniNickname(profile.nickname) ? profile.nickname : undefined) || + profile.nickname || + '微信用户', + avatarUrl: profile.avatarUrl || cached.avatarUrl || null, + }; +} + +/** 用户点击触发:拉取微信昵称/头像 */ +export async function fetchMiniWechatUserInfo(): Promise { + if (process.env.TARO_ENV !== 'weapp') { + throw new Error('请在微信小程序中授权'); + } + const res = await Taro.getUserProfile({ desc: '用于完善会员资料' }); + const info: MiniWechatProfile = { + nickname: res.userInfo?.nickName?.trim(), + avatarUrl: res.userInfo?.avatarUrl?.trim(), + }; + if (!info.nickname && !info.avatarUrl) { + throw new Error('未获取到微信头像或昵称'); + } + cacheWxProfile(info); + return info; +} + +export async function uploadMiniWechatProfile(info: MiniWechatProfile): Promise { + if (!info.nickname && !info.avatarUrl) return null; + try { + const updated = await request<{ + nickname?: string | null; + avatarUrl?: string | null; + }>('/auth/wechat/mini-profile', { + method: 'POST', + data: info, + }); + if (updated?.nickname || updated?.avatarUrl) { + cacheWxProfile({ + nickname: updated.nickname ?? info.nickname, + avatarUrl: updated.avatarUrl ?? info.avatarUrl, + }); + } + return info; + } catch { + return null; + } +} + +/** 绑定后上报微信资料(优先使用已拉取的信息,避免重复弹窗) */ +export async function syncMiniWechatProfile(prefetched?: MiniWechatProfile | null): Promise { + if (process.env.TARO_ENV !== 'weapp') return null; + + let info = prefetched ?? null; + if (!info) { + try { + info = await fetchMiniWechatUserInfo(); + } catch { + return getCachedWxProfile(); + } + } + + await uploadMiniWechatProfile(info); + return info; +} diff --git a/apps/mini-user/src/lib/nav-bar.ts b/apps/mini-user/src/lib/nav-bar.ts index 5af75bf..cdbc4c8 100644 --- a/apps/mini-user/src/lib/nav-bar.ts +++ b/apps/mini-user/src/lib/nav-bar.ts @@ -77,29 +77,45 @@ export function pageShellCssVars(metrics: NavBarMetrics): Record }; } -/** 顶栏自身样式:statusBar padding + 总高 + 左右对称胶囊避让 */ +/** 顶栏自身样式:statusBar padding + 总高 + CSS 变量 */ export function navBarStyle(metrics: NavBarMetrics): Record { return { paddingTop: `${metrics.statusBarHeight}px`, height: `${metrics.navBarHeight}px`, - paddingLeft: `${metrics.navBarPaddingLeft}px`, - paddingRight: `${metrics.navBarPaddingRight}px`, ...pageShellCssVars(metrics), }; } -/** 子页顶栏:左侧为返回按钮预留与右侧胶囊等宽空间 */ +/** Tab 顶栏内容行:左右留白(标题单独全屏居中) */ +export function tabNavContentStyle(metrics: NavBarMetrics): Record { + return { + height: `${metrics.navContentHeight}px`, + paddingLeft: '20px', + paddingRight: `${metrics.navBarPaddingRight}px`, + boxSizing: 'border-box', + }; +} + +/** 子页/内页顶栏:标题全屏居中,内容区单独留白 */ export function subPageNavBarStyle(metrics: NavBarMetrics): Record { - const paddingSide = Math.max(48, metrics.navBarPaddingRight); + const pagePad = 20; return { paddingTop: `${metrics.statusBarHeight}px`, height: `${metrics.navBarHeight}px`, - paddingLeft: `${paddingSide}px`, - paddingRight: `${paddingSide}px`, '--nav-bar-height': `${metrics.navBarHeight}px`, '--nav-content-height': `${metrics.navContentHeight}px`, '--nav-status-bar-height': `${metrics.statusBarHeight}px`, - '--nav-padding-right': `${paddingSide}px`, - '--nav-padding-left': `${paddingSide}px`, + '--nav-padding-left': `${pagePad}px`, + '--nav-padding-right': `${metrics.navBarPaddingRight}px`, + }; +} + +/** 子页顶栏内容行:左侧页边距 + 右侧避让胶囊 */ +export function subPageNavContentStyle(metrics: NavBarMetrics): Record { + return { + height: `${metrics.navContentHeight}px`, + paddingLeft: '20px', + paddingRight: `${metrics.navBarPaddingRight}px`, + boxSizing: 'border-box', }; } diff --git a/apps/mini-user/src/lib/pay-wechat.ts b/apps/mini-user/src/lib/pay-wechat.ts index 2815c8f..2d5ad16 100644 --- a/apps/mini-user/src/lib/pay-wechat.ts +++ b/apps/mini-user/src/lib/pay-wechat.ts @@ -61,7 +61,9 @@ export type WechatBindResult = | { ok: true; profile?: UserProfile } | { ok: false; needBindPhone: true; wxSessionKey: string }; -export async function bindWechatForUser(): Promise { +export async function bindWechatForUser( + prefetchedWxProfile?: { nickname?: string; avatarUrl?: string } | null, +): Promise { const res = await Taro.login(); if (!res.code) { throw new Error(res.errMsg || '微信授权失败'); @@ -73,7 +75,7 @@ export async function bindWechatForUser(): Promise { if (data.needBindPhone && data.wxSessionKey) { return { ok: false, needBindPhone: true, wxSessionKey: data.wxSessionKey }; } - await syncMiniWechatProfile(); + await syncMiniWechatProfile(prefetchedWxProfile); const profile = await fetchUserProfile(); return { ok: true, profile }; } diff --git a/apps/mini-user/src/lib/user-phone.ts b/apps/mini-user/src/lib/user-phone.ts index a873b4e..af399d3 100644 --- a/apps/mini-user/src/lib/user-phone.ts +++ b/apps/mini-user/src/lib/user-phone.ts @@ -1,10 +1,15 @@ import Taro from '@tarojs/taro'; +import { normalizePhoneInput } from './phone'; const USER_PHONE_KEY = 'user_phone'; +function isValidMobile(phone: string) { + return /^1[3-9]\d{9}$/.test(phone); +} + export function saveUserPhone(phone: string) { - const normalized = phone.replace(/\D/g, '').slice(0, 11); - if (!/^1[3-9]\d{9}$/.test(normalized)) return; + const normalized = normalizePhoneInput(phone); + if (!isValidMobile(normalized)) return; try { Taro.setStorageSync(USER_PHONE_KEY, normalized); } catch { @@ -20,3 +25,14 @@ export function getStoredUserPhone(): string { return ''; } } + +/** 新增地址等场景:优先本地缓存,其次资料里的已验证手机号 */ +export function resolveDefaultUserPhone(profile?: { phone?: string | null; phoneVerified?: boolean } | null) { + const stored = getStoredUserPhone(); + if (isValidMobile(stored)) return stored; + if (!profile?.phoneVerified || !profile.phone) return ''; + const fromProfile = normalizePhoneInput(profile.phone); + if (!isValidMobile(fromProfile)) return ''; + saveUserPhone(fromProfile); + return fromProfile; +} diff --git a/apps/mini-user/src/lib/wechat-auth.ts b/apps/mini-user/src/lib/wechat-auth.ts index 2810253..cb763a9 100644 --- a/apps/mini-user/src/lib/wechat-auth.ts +++ b/apps/mini-user/src/lib/wechat-auth.ts @@ -1,22 +1,17 @@ import type { WechatLoginResult } from '@dukang/shared-types'; import Taro from '@tarojs/taro'; import { request } from './api'; -import { syncMiniWechatProfile } from './mini-wechat-profile'; -/** 小程序微信授权登录:Taro.login → /auth/login/wechat */ +/** 小程序微信授权登录:Taro.login → /auth/login/wechat(资料上报由调用方 saveAuth 后执行) */ export async function loginWithWechat(): Promise { const res = await Taro.login(); if (!res.code) { throw new Error(res.errMsg || '微信登录失败,未获取到 code'); } - const result = await request('/auth/login/wechat', { + return request('/auth/login/wechat', { method: 'POST', data: { code: res.code, platform: 'mini' }, }); - if (result.accessToken) { - await syncMiniWechatProfile(); - } - return result; } export { bindWechatForUser } from './pay-wechat'; diff --git a/apps/mini-user/src/pages/address-edit/index.tsx b/apps/mini-user/src/pages/address-edit/index.tsx index 63e9c2d..9680506 100644 --- a/apps/mini-user/src/pages/address-edit/index.tsx +++ b/apps/mini-user/src/pages/address-edit/index.tsx @@ -7,7 +7,7 @@ import RegionPicker from '../../components/RegionPicker'; import { buildAddressListUrl, readCheckoutContext } from '../../lib/checkout-nav'; import { DEFAULT_REGION, formatRegion, type RegionSelection } from '../../lib/region-data'; import { normalizePhoneInput, validateMobilePhone } from '../../lib/phone'; -import { getStoredUserPhone } from '../../lib/user-phone'; +import { getStoredUserPhone, resolveDefaultUserPhone } from '../../lib/user-phone'; import { request, toast, type UserProfile } from '../../lib/api'; type AddressForm = { @@ -28,24 +28,23 @@ export default function AddressEditPage() { const [pickerOpen, setPickerOpen] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); - const [form, setForm] = useState({ + const [form, setForm] = useState(() => ({ receiverName: '', - phone: '', + phone: id ? '' : getStoredUserPhone(), province: DEFAULT_REGION.province, city: DEFAULT_REGION.city, district: DEFAULT_REGION.district, detail: '', isDefault: true, - }); + })); useEffect(() => { if (id) return; request('/auth/me') .then((me) => { - if (!me.phoneVerified) return; - const stored = getStoredUserPhone(); - if (!stored) return; - setForm((prev) => (prev.phone ? prev : { ...prev, phone: stored })); + const phone = resolveDefaultUserPhone(me); + if (!phone) return; + setForm((prev) => (prev.phone ? prev : { ...prev, phone })); }) .catch(() => {}); }, [id]); diff --git a/apps/mini-user/src/pages/benefit/index.tsx b/apps/mini-user/src/pages/benefit/index.tsx index a908d45..1e46e1b 100644 --- a/apps/mini-user/src/pages/benefit/index.tsx +++ b/apps/mini-user/src/pages/benefit/index.tsx @@ -5,7 +5,7 @@ import PageShell from '../../components/PageShell'; import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar'; import { goLogin } from '../../lib/auth-nav'; import { isLoggedIn, request, toast } from '../../lib/api'; -import { navBarStyle, useNavBarMetrics } from '../../lib/nav-bar'; +import { navBarStyle, tabNavContentStyle, useNavBarMetrics } from '../../lib/nav-bar'; type BenefitSummary = { totalBalance: number; @@ -64,18 +64,15 @@ export default function BenefitPage() { return ( + 好客权益 郑州市 - 好客权益 - toast('消息通知即将开放')}> - 🔔 - diff --git a/apps/mini-user/src/pages/login/index.tsx b/apps/mini-user/src/pages/login/index.tsx index 6c8e28b..ad49a56 100644 --- a/apps/mini-user/src/pages/login/index.tsx +++ b/apps/mini-user/src/pages/login/index.tsx @@ -13,7 +13,13 @@ import { BRAND_LOGO_WIDE_URL } from '@dukang/shared-types'; import { finishLoginNavigate } from '../../lib/auth-nav'; import { bindWechatForUser } from '../../lib/wechat-auth'; import { fetchUserProfile } from '../../lib/pay-wechat'; -import { saveUserPhone } from '../../lib/user-phone'; +import { saveUserPhone, resolveDefaultUserPhone } from '../../lib/user-phone'; +import { + fetchMiniWechatUserInfo, + getCachedWxProfile, + syncMiniWechatProfile, + type MiniWechatProfile, +} from '../../lib/mini-wechat-profile'; import { isLoggedIn, request, saveAuth, toast, type SessionPayload } from '../../lib/api'; import { loginWithWechat } from '../../lib/wechat-auth'; @@ -91,18 +97,28 @@ export default function LoginPage() { return true; } - function applySessionAndLeave(data: SessionPayload | WechatLoginResult, phone?: string) { + function applySessionAndLeave( + data: SessionPayload | WechatLoginResult, + phone?: string, + wxInfo?: MiniWechatProfile | null, + ) { if (!data.accessToken) return; if (phone) saveUserPhone(phone); saveAuth({ accessToken: data.accessToken, refreshToken: data.refreshToken, }); + void syncMiniWechatProfile(wxInfo ?? getCachedWxProfile()); + if (!phone) { + void fetchUserProfile() + .then((me) => resolveDefaultUserPhone(me)) + .catch(() => {}); + } toast('登录成功', 'success'); finishLoginNavigate(returnTo); } - function handleWechatLoginResult(result: WechatLoginResult) { + function handleWechatLoginResult(result: WechatLoginResult, wxInfo?: MiniWechatProfile | null) { if (result.needBindPhone && result.wxSessionKey) { setBindMode(true); setWxSessionKey(result.wxSessionKey); @@ -111,7 +127,7 @@ export default function LoginPage() { return; } if (result.accessToken) { - applySessionAndLeave(result); + applySessionAndLeave(result, undefined, wxInfo); return; } setMsg('微信登录未完成,请重试或使用手机号登录'); @@ -196,8 +212,18 @@ export default function LoginPage() { setSentHint(''); setWxLoading(true); try { + let wxInfo: MiniWechatProfile | null = null; + if (process.env.TARO_ENV === 'weapp') { + try { + wxInfo = await fetchMiniWechatUserInfo(); + } catch (e) { + toast(e instanceof Error ? e.message : '需要授权微信头像和昵称'); + return; + } + } + if (completeMode === 'wechat' && isLoggedIn()) { - const result = await bindWechatForUser(); + const result = await bindWechatForUser(wxInfo); if (!result.ok && result.needBindPhone) { setBindMode(true); setWxSessionKey(result.wxSessionKey); @@ -206,6 +232,7 @@ export default function LoginPage() { return; } if (result.ok) { + await syncMiniWechatProfile(wxInfo); toast('微信授权成功', 'success'); finishLoginNavigate(returnTo); return; @@ -213,7 +240,7 @@ export default function LoginPage() { return; } const result = await loginWithWechat(); - handleWechatLoginResult(result); + handleWechatLoginResult(result, wxInfo); } catch (e) { const raw = e instanceof Error ? e.message : '微信登录失败'; const hint = /invalid code/i.test(raw) diff --git a/apps/mini-user/src/pages/mine/index.tsx b/apps/mini-user/src/pages/mine/index.tsx index 11e3b96..6ef4727 100644 --- a/apps/mini-user/src/pages/mine/index.tsx +++ b/apps/mini-user/src/pages/mine/index.tsx @@ -8,6 +8,12 @@ import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../co import { BRAND_LOGO_MARK_URL } from '@dukang/shared-types'; import { goLogin } from '../../lib/auth-nav'; import { bindWechatForUser } from '../../lib/wechat-auth'; +import { fetchUserProfile } from '../../lib/pay-wechat'; +import { + fetchMiniWechatUserInfo, + getCachedWxProfile, + mergeWxDisplayProfile, +} from '../../lib/mini-wechat-profile'; import { isLoggedIn, logout, request, toast, type UserProfile } from '../../lib/api'; const ORDER_SHORTCUTS = [ @@ -28,22 +34,21 @@ function formatMoney(amount: number) { } export default function MinePage() { - const loggedIn = isLoggedIn(); + const [authed, setAuthed] = useState(() => isLoggedIn()); const [profile, setProfile] = useState(null); const [benefitBalance, setBenefitBalance] = useState(0); const [orderCounts, setOrderCounts] = useState>({}); const [wxAuthorize, setWxAuthorize] = useState(true); const [bindingWx, setBindingWx] = useState(false); - useDidShow(() => { - syncTabBarSelected(3); - if (isLoggedIn()) { - loadProfile(); - } - }); + function resetGuestState() { + setProfile(null); + setBenefitBalance(0); + setOrderCounts({}); + } function loadProfile() { - if (!loggedIn) return; + if (!isLoggedIn()) return; Promise.all([ request('/auth/me'), request>>('/benefit/coupons').catch(() => []), @@ -52,7 +57,7 @@ export default function MinePage() { ), ]) .then(([me, coupons, ...totals]) => { - setProfile(me); + setProfile(mergeWxDisplayProfile(me)); const balance = (coupons as Array>).reduce((sum, c) => { if (String(c.status) === 'ACTIVE') return sum + Number(c.balance || 0); return sum; @@ -67,30 +72,74 @@ export default function MinePage() { .catch(() => {}); } + useDidShow(() => { + syncTabBarSelected(3); + const loggedInNow = isLoggedIn(); + setAuthed(loggedInNow); + if (loggedInNow) { + loadProfile(); + } else { + resetGuestState(); + } + }); + useEffect(() => { request('/common/client-config') .then((config) => setWxAuthorize(isWxAuthorizeEnabled(config))) .catch(() => setWxAuthorize(true)); }, []); - useEffect(() => { - loadProfile(); - }, [loggedIn]); - async function handleAvatarTap() { - if (!loggedIn) { + if (!isLoggedIn()) { goLogin('/pages/mine/index'); return; } - if (profile?.hasWechat || !wxAuthorize || process.env.TARO_ENV !== 'weapp') return; + if (bindingWx) return; + + let current = profile; + if (!current) { + try { + current = await fetchUserProfile(); + setProfile(current); + } catch { + /* ignore */ + } + } + if (current?.hasWechat) return; + + if (!wxAuthorize) { + toast('当前环境未开启微信授权'); + return; + } + if (process.env.TARO_ENV !== 'weapp') { + toast('请在微信小程序中完成微信授权'); + return; + } + setBindingWx(true); try { - const result = await bindWechatForUser(); + let wxInfo = null; + try { + wxInfo = await fetchMiniWechatUserInfo(); + } catch (e) { + toast(e instanceof Error ? e.message : '需要授权微信头像和昵称'); + return; + } + + const result = await bindWechatForUser(wxInfo); if (!result.ok && result.needBindPhone) { goLogin('/pages/mine/index', { bindMode: '1', wxSessionKey: result.wxSessionKey }); return; } if (result.ok) { + const merged = mergeWxDisplayProfile({ + ...(result.profile ?? {}), + id: result.profile?.id ?? profile?.id ?? '', + hasWechat: true, + nickname: result.profile?.nickname || wxInfo.nickname || profile?.nickname, + avatarUrl: result.profile?.avatarUrl || wxInfo.avatarUrl || profile?.avatarUrl, + }); + setProfile(merged); loadProfile(); toast('微信授权成功', 'success'); } @@ -115,33 +164,46 @@ export default function MinePage() { } } - function renderAvatarContent(profile: UserProfile | null) { - if (profile?.avatarUrl) { - return ; + function resolveDisplayProfile(profile: UserProfile | null, hasWechat: boolean) { + if (!profile) { + return { nickname: '用户', avatarUrl: null as string | null }; + } + const merged = hasWechat ? mergeWxDisplayProfile(profile) : profile; + return { + nickname: merged.nickname || '用户', + avatarUrl: merged.avatarUrl || null, + }; + } + + function renderAvatarContent(displayAvatarUrl: string | null) { + if (displayAvatarUrl) { + return ; } return ; } - if (!loggedIn) { + if (!authed) { return ( - - + goLogin('/pages/mine/index')}> + + + 未登录 - 好客会员 + 点击头像登录 - - 登录后管理订单与个人信息 + + 登录后管理订单与个人信息 goLogin('/pages/mine/index')} > 去登录 @@ -152,9 +214,11 @@ export default function MinePage() { ); } - const nickname = profile?.nickname || '用户'; const hasWechat = !!profile?.hasWechat; - const memberLabel = hasWechat ? '微信会员' : '未授权微信'; + const canWxBind = wxAuthorize && process.env.TARO_ENV === 'weapp'; + const display = resolveDisplayProfile(profile, hasWechat); + const nickname = display.nickname; + const memberLabel = hasWechat ? '好客会员' : canWxBind ? '微信未授权' : '未授权微信'; return ( @@ -162,12 +226,19 @@ export default function MinePage() { - void handleAvatarTap()}> - - {renderAvatarContent(profile)} + void handleAvatarTap()} + > + + {renderAvatarContent(display.avatarUrl)} - {!hasWechat && wxAuthorize && process.env.TARO_ENV === 'weapp' ? ( - {bindingWx ? '授权中' : '授权'} + {!hasWechat && canWxBind ? ( + + {bindingWx ? '授权中' : '去授权'} + ) : null} @@ -175,7 +246,7 @@ export default function MinePage() { {memberLabel} - {!hasWechat && wxAuthorize && process.env.TARO_ENV === 'weapp' ? ( + {!hasWechat && canWxBind ? ( 点击头像完成微信授权 ) : null} diff --git a/apps/mini-user/src/styles/benefit.css b/apps/mini-user/src/styles/benefit.css index 95ccf54..2f1775c 100644 --- a/apps/mini-user/src/styles/benefit.css +++ b/apps/mini-user/src/styles/benefit.css @@ -7,18 +7,38 @@ position: sticky; top: 0; z-index: 50; - padding-left: var(--space-page); background: var(--color-background); box-sizing: border-box; } .benefit-header__content { + position: relative; display: flex; align-items: center; - justify-content: space-between; width: 100%; } +.benefit-header-title { + position: absolute; + left: 0; + right: 0; + top: var(--nav-status-bar-height); + height: var(--nav-content-height); + line-height: var(--nav-content-height); + text-align: center; + padding: 0 48px; + box-sizing: border-box; + font-family: var(--font-headline); + font-size: 16px; + font-weight: 700; + color: var(--color-heritage-red); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + z-index: 1; +} + .benefit-header-city { display: flex; align-items: center; @@ -26,6 +46,8 @@ font-size: 12px; font-weight: 500; max-width: 30vw; + position: relative; + z-index: 2; } .benefit-header-city-pin { @@ -37,23 +59,6 @@ flex-shrink: 0; } -.benefit-header-title { - font-family: var(--font-headline); - font-size: 18px; - font-weight: 700; - color: var(--color-heritage-red); -} - -.benefit-header-btn { - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - color: var(--color-on-surface-variant); - font-size: 18px; -} - .benefit-main { padding: 16px var(--space-page) 24px; } diff --git a/apps/mini-user/src/styles/mine.css b/apps/mini-user/src/styles/mine.css index 3944385..7111fe8 100644 --- a/apps/mini-user/src/styles/mine.css +++ b/apps/mini-user/src/styles/mine.css @@ -1,11 +1,16 @@ /* 我的 */ +.page-shell.mine-page { + min-height: auto; + padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); +} + .mine-page { background: var(--color-background); } .mine-header { position: relative; - padding: 20px var(--space-page) 52px; + padding: 16px var(--space-page) 44px; background: linear-gradient(135deg, #820012 0%, var(--color-heritage-red) 40%, #d4a373 100%); overflow: hidden; } @@ -34,7 +39,36 @@ .mine-avatar-wrap { position: relative; flex-shrink: 0; - margin-right: 14px; + margin-right: 12px; +} + +.mine-avatar-wrap--action { + cursor: pointer; +} + +.mine-avatar-status { + position: absolute; + left: 50%; + bottom: -4px; + transform: translateX(-50%); + padding: 2px 8px; + border-radius: 999px; + font-size: 9px; + font-weight: 600; + line-height: 1.3; + white-space: nowrap; + border: 1px solid rgba(255, 255, 255, 0.85); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12); +} + +.mine-avatar-status--ok { + background: #07c160; + color: #fff; +} + +.mine-avatar-status--pending { + background: #fff; + color: var(--color-heritage-red); } .mine-avatar-badge { @@ -54,26 +88,44 @@ .mine-wechat-hint { display: block; - margin-top: 6px; + margin-top: 4px; font-size: 10px; color: rgba(255, 255, 255, 0.75); } .mine-avatar { - width: 72px; - height: 72px; + width: 64px; + height: 64px; border-radius: 50%; border: 2px solid rgba(255, 255, 255, 0.35); background: #fff; overflow: hidden; flex-shrink: 0; - margin-right: 14px; display: flex; align-items: center; justify-content: center; color: var(--color-heritage-red); - font-size: 28px; + font-size: 24px; font-weight: 700; + box-sizing: border-box; +} + +.mine-avatar--wx-ok { + border-color: #07c160; + box-shadow: 0 0 0 2px rgba(7, 193, 96, 0.25); +} + +.mine-avatar--wx-pending { + border-color: rgba(255, 255, 255, 0.9); + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.35); +} + +.mine-avatar-wrap .mine-avatar { + margin-right: 0; +} + +.mine-profile > .mine-avatar { + margin-right: 12px; } .mine-avatar-img { @@ -84,10 +136,10 @@ .mine-profile-name { display: block; font-family: var(--font-headline); - font-size: 20px; + font-size: 18px; font-weight: 600; color: #fff; - margin-bottom: 4px; + margin-bottom: 2px; } .mine-member-tag { @@ -100,30 +152,34 @@ } .mine-main { - margin-top: -36px; + margin-top: -28px; position: relative; z-index: 2; - padding: 0 var(--space-page) 12px; + padding: 0 var(--space-page) 0; } .mine-card { background: var(--color-card); border-radius: var(--radius-lg); - padding: 14px; + padding: 12px; box-shadow: var(--shadow-card); - margin-bottom: 10px; + margin-bottom: 8px; +} + +.mine-card:last-of-type { + margin-bottom: 0; } .mine-card-head { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 14px; + margin-bottom: 10px; } .mine-card-title { font-family: var(--font-headline); - font-size: 16px; + font-size: 15px; font-weight: 700; color: var(--color-on-surface); } @@ -137,7 +193,7 @@ display: flex; align-items: center; justify-content: space-between; - padding: 16px; + padding: 12px; border-radius: var(--radius-md); background: linear-gradient(135deg, #fff9e6 0%, #fff0c2 100%); } @@ -163,16 +219,16 @@ .mine-asset-value { font-family: var(--font-headline); - font-size: 28px; + font-size: 24px; font-weight: 700; } .mine-asset-cta { - padding: 8px 16px; + padding: 6px 14px; border-radius: 999px; background: var(--color-heritage-red); color: #fff; - font-size: 13px; + font-size: 12px; font-weight: 600; } @@ -185,22 +241,22 @@ display: flex; flex-direction: column; align-items: center; - padding: 8px; + padding: 4px; position: relative; width: 33%; } .mine-order-icon { - width: 40px; - height: 40px; - border-radius: 12px; + width: 36px; + height: 36px; + border-radius: 10px; background: rgba(166, 29, 36, 0.08); display: flex; align-items: center; justify-content: center; color: var(--color-heritage-red); - font-size: 18px; - margin-bottom: 6px; + font-size: 16px; + margin-bottom: 4px; } .mine-order-badge { @@ -233,21 +289,21 @@ display: flex; flex-direction: column; align-items: center; - padding: 12px 4px; + padding: 6px 4px; box-sizing: border-box; } .mine-service-icon { - width: 36px; - height: 36px; - border-radius: 10px; + width: 32px; + height: 32px; + border-radius: 8px; background: var(--color-surface-container-low); display: flex; align-items: center; justify-content: center; color: var(--color-heritage-red); - font-size: 16px; - margin-bottom: 6px; + font-size: 14px; + margin-bottom: 4px; } .mine-service-label { @@ -257,29 +313,59 @@ } .mine-footer { - text-align: center; - padding: 8px 0 4px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 0 0; + margin-top: 8px; } .mine-version { - display: block; font-size: 12px; color: var(--color-subtle-gray); - margin-bottom: 8px; } .mine-logout { display: inline-flex; - padding: 10px 32px; + padding: 6px 18px; border-radius: var(--radius-full); border: 1px solid rgba(166, 29, 36, 0.3); color: var(--color-heritage-red); - font-size: 14px; + font-size: 13px; font-weight: 600; } .mine-login-gate { - margin-top: -32px; + margin-top: -24px; + margin-left: var(--space-page); + margin-right: var(--space-page); + margin-bottom: 8px; + padding: 16px; position: relative; z-index: 2; + background: var(--color-card); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + box-sizing: border-box; +} + +.mine-login-gate-hint { + text-align: center; + padding: 24px 0 16px; + color: var(--color-subtle-gray); + font-size: 14px; +} + +.mine-login-btn { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + box-sizing: border-box; + padding: 12px 18px; + border-radius: var(--radius-md); + background: var(--color-heritage-red); + color: #fff; + font-size: 15px; + font-weight: 600; } diff --git a/apps/mini-user/src/styles/nav-bar.css b/apps/mini-user/src/styles/nav-bar.css index 55676c0..fb92f73 100644 --- a/apps/mini-user/src/styles/nav-bar.css +++ b/apps/mini-user/src/styles/nav-bar.css @@ -34,23 +34,29 @@ position: relative; display: flex; align-items: center; - justify-content: center; + justify-content: flex-end; width: 100%; } .tab-main-header__title { position: absolute; - left: var(--nav-padding-left); - right: var(--nav-padding-right); + left: 0; + right: 0; + top: var(--nav-status-bar-height); + height: var(--nav-content-height); + line-height: var(--nav-content-height); text-align: center; + padding: 0 48px; + box-sizing: border-box; font-family: var(--font-headline); - font-size: 18px; + font-size: 16px; font-weight: 700; color: var(--color-heritage-red); - line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + pointer-events: none; + z-index: 1; } .tab-main-header__extra { @@ -94,11 +100,13 @@ position: relative; display: flex; align-items: center; - justify-content: space-between; width: 100%; } .page-nav-bar__btn { + position: absolute; + top: 50%; + transform: translateY(-50%); width: 40px; height: 40px; border-radius: 999px; @@ -107,6 +115,16 @@ justify-content: center; background: rgba(255, 255, 255, 0.8); flex-shrink: 0; + z-index: 2; +} + +.page-nav-bar__btn--back { + left: 0; +} + +.page-nav-bar__btn--right { + left: auto; + right: 0; } .page-nav-bar--solid .page-nav-bar__btn { @@ -119,10 +137,11 @@ } .page-nav-bar__icon { - font-size: 28px; + font-size: 32px; line-height: 1; color: var(--color-ink-black); font-weight: 300; + margin-top: -2px; } .page-nav-bar__icon--share { @@ -132,19 +151,25 @@ .page-nav-bar__title { position: absolute; - left: var(--nav-padding-left); - right: var(--nav-padding-right); + left: 0; + right: 0; + top: var(--nav-status-bar-height); + height: var(--nav-content-height); + line-height: var(--nav-content-height); + text-align: center; + padding: 0 48px; + box-sizing: border-box; opacity: 0; font-family: var(--font-headline); - font-size: 16px; + font-size: 15px; font-weight: 600; color: var(--color-ink-black); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - text-align: center; transition: opacity 0.3s; pointer-events: none; + z-index: 1; } .page-nav-bar__title--visible { @@ -170,10 +195,31 @@ position: relative; display: flex; align-items: center; - justify-content: center; + justify-content: flex-end; width: 100%; } +.sub-page-header__title { + position: absolute; + left: 0; + right: 0; + top: var(--nav-status-bar-height); + height: var(--nav-content-height); + line-height: var(--nav-content-height); + text-align: center; + padding: 0 48px; + box-sizing: border-box; + font-family: var(--font-headline); + font-size: 16px; + font-weight: 700; + color: var(--color-heritage-red); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + pointer-events: none; + z-index: 1; +} + .sub-page-header__back { position: absolute; left: 0; @@ -181,32 +227,18 @@ transform: translateY(-50%); width: 40px; height: 40px; - margin-left: -8px; display: flex; align-items: center; justify-content: center; + z-index: 2; } .sub-page-header__back-icon { - font-size: 28px; + font-size: 32px; line-height: 1; color: var(--color-heritage-red); font-weight: 300; -} - -.sub-page-header__title { - position: absolute; - left: var(--nav-padding-left); - right: var(--nav-padding-right); - text-align: center; - font-family: var(--font-headline); - font-size: 18px; - font-weight: 700; - color: var(--color-heritage-red); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - pointer-events: none; + margin-top: -2px; } .sub-page-header__right { @@ -214,6 +246,7 @@ right: 0; top: 50%; transform: translateY(-50%); + z-index: 2; } .sub-page-body { diff --git a/server/dukang-api/src/modules/iam/auth.service.ts b/server/dukang-api/src/modules/iam/auth.service.ts index 1d51709..3cc250f 100644 --- a/server/dukang-api/src/modules/iam/auth.service.ts +++ b/server/dukang-api/src/modules/iam/auth.service.ts @@ -1255,28 +1255,42 @@ export class AuthService { } = {}; const nickname = input.nickname?.trim(); - if (nickname && this.isDefaultNickname(user.nickname)) { + if (nickname) { data.nickname = nickname.slice(0, 64); } const avatarUrl = input.avatarUrl?.trim(); - if (avatarUrl && !user.avatarResourceId) { - const avatar = await this.prisma.commonResource.create({ - data: { - ownerType: 'USER', - ownerId: userId, - bizType: 'AVATAR', - mediaType: 'IMAGE', - ossBucket: 'wechat', - ossKey: `wx-avatar/${user.wxOpenId}`, - url: avatarUrl, - status: 'ACTIVE', - }, - }); - data.avatarResourceId = avatar.id; + if (avatarUrl) { + if (user.avatarResourceId) { + await this.prisma.commonResource.update({ + where: { id: user.avatarResourceId }, + data: { url: avatarUrl }, + }); + } else { + const avatar = await this.prisma.commonResource.create({ + data: { + ownerType: 'USER', + ownerId: userId, + bizType: 'AVATAR', + mediaType: 'IMAGE', + ossBucket: 'wechat', + ossKey: `wx-avatar/${user.wxOpenId}`, + url: avatarUrl, + status: 'ACTIVE', + }, + }); + data.avatarResourceId = avatar.id; + } } if (!data.nickname && !data.avatarResourceId) { + if (avatarUrl && user.avatarResourceId) { + const refreshed = await this.prisma.user.findUnique({ + where: { id: userId }, + include: { avatar: true }, + }); + return this.formatUserProfile(refreshed ?? user); + } return this.formatUserProfile(user); } @@ -1719,7 +1733,7 @@ export class AuthService { return { id: user.id.toString(), userNo: user.userNo, - phone: user.phone ? user.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : null, + phone: user.phone ?? null, phoneVerified: !!user.phoneVerifiedAt, nickname: user.nickname, avatarUrl: user.avatar?.url ?? null,