用户端小程序修改
This commit is contained in:
@@ -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<void> {
|
||||
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<MiniWechatProfile> {
|
||||
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<MiniWechatProfile | null> {
|
||||
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<MiniWechatProfile | null> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -77,29 +77,45 @@ export function pageShellCssVars(metrics: NavBarMetrics): Record<string, string>
|
||||
};
|
||||
}
|
||||
|
||||
/** 顶栏自身样式:statusBar padding + 总高 + 左右对称胶囊避让 */
|
||||
/** 顶栏自身样式:statusBar padding + 总高 + CSS 变量 */
|
||||
export function navBarStyle(metrics: NavBarMetrics): Record<string, string | number> {
|
||||
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<string, string | number> {
|
||||
return {
|
||||
height: `${metrics.navContentHeight}px`,
|
||||
paddingLeft: '20px',
|
||||
paddingRight: `${metrics.navBarPaddingRight}px`,
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
}
|
||||
|
||||
/** 子页/内页顶栏:标题全屏居中,内容区单独留白 */
|
||||
export function subPageNavBarStyle(metrics: NavBarMetrics): Record<string, string | number> {
|
||||
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<string, string | number> {
|
||||
return {
|
||||
height: `${metrics.navContentHeight}px`,
|
||||
paddingLeft: '20px',
|
||||
paddingRight: `${metrics.navBarPaddingRight}px`,
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,7 +61,9 @@ export type WechatBindResult =
|
||||
| { ok: true; profile?: UserProfile }
|
||||
| { ok: false; needBindPhone: true; wxSessionKey: string };
|
||||
|
||||
export async function bindWechatForUser(): Promise<WechatBindResult> {
|
||||
export async function bindWechatForUser(
|
||||
prefetchedWxProfile?: { nickname?: string; avatarUrl?: string } | null,
|
||||
): Promise<WechatBindResult> {
|
||||
const res = await Taro.login();
|
||||
if (!res.code) {
|
||||
throw new Error(res.errMsg || '微信授权失败');
|
||||
@@ -73,7 +75,7 @@ export async function bindWechatForUser(): Promise<WechatBindResult> {
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<WechatLoginResult> {
|
||||
const res = await Taro.login();
|
||||
if (!res.code) {
|
||||
throw new Error(res.errMsg || '微信登录失败,未获取到 code');
|
||||
}
|
||||
const result = await request<WechatLoginResult>('/auth/login/wechat', {
|
||||
return request<WechatLoginResult>('/auth/login/wechat', {
|
||||
method: 'POST',
|
||||
data: { code: res.code, platform: 'mini' },
|
||||
});
|
||||
if (result.accessToken) {
|
||||
await syncMiniWechatProfile();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export { bindWechatForUser } from './pay-wechat';
|
||||
|
||||
Reference in New Issue
Block a user