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,