用户端小程序修改
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
import { View, Text } from '@tarojs/components';
|
||||||
import { subPageNavBarStyle, useNavBarMetrics } from '../lib/nav-bar';
|
import { subPageNavBarStyle, subPageNavContentStyle, useNavBarMetrics } from '../lib/nav-bar';
|
||||||
|
|
||||||
type PageNavBarProps = {
|
type PageNavBarProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -25,23 +25,25 @@ export default function PageNavBar({
|
|||||||
className={`page-nav-bar${solid ? ' page-nav-bar--solid' : ''}`}
|
className={`page-nav-bar${solid ? ' page-nav-bar--solid' : ''}`}
|
||||||
style={subPageNavBarStyle(metrics)}
|
style={subPageNavBarStyle(metrics)}
|
||||||
>
|
>
|
||||||
|
<Text
|
||||||
|
className={`page-nav-bar__title${titleVisible ? ' page-nav-bar__title--visible' : ''}`}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
<View
|
<View
|
||||||
className="page-nav-bar__content"
|
className="page-nav-bar__content"
|
||||||
style={{ height: `${metrics.navContentHeight}px` }}
|
style={subPageNavContentStyle(metrics)}
|
||||||
>
|
>
|
||||||
{onBack ? (
|
{onBack ? (
|
||||||
<View className="page-nav-bar__btn" onClick={onBack}>
|
<View className="page-nav-bar__btn page-nav-bar__btn--back" onClick={onBack}>
|
||||||
<Text className="page-nav-bar__icon">‹</Text>
|
<Text className="page-nav-bar__icon">‹</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View className="page-nav-bar__btn page-nav-bar__btn--placeholder" />
|
<View className="page-nav-bar__btn page-nav-bar__btn--back page-nav-bar__btn--placeholder" />
|
||||||
|
)}
|
||||||
|
{right ?? (
|
||||||
|
<View className="page-nav-bar__btn page-nav-bar__btn--right page-nav-bar__btn--placeholder" />
|
||||||
)}
|
)}
|
||||||
<Text
|
|
||||||
className={`page-nav-bar__title${titleVisible ? ' page-nav-bar__title--visible' : ''}`}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
{right ?? <View className="page-nav-bar__btn page-nav-bar__btn--placeholder" />}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
import { View, Text } from '@tarojs/components';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { subPageNavBarStyle, useNavBarMetrics } from '../lib/nav-bar';
|
import { subPageNavBarStyle, subPageNavContentStyle, useNavBarMetrics } from '../lib/nav-bar';
|
||||||
|
|
||||||
type SubPageHeaderProps = {
|
type SubPageHeaderProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -28,14 +28,14 @@ export default function SubPageHeader({ title, onBack, right }: SubPageHeaderPro
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="sub-page-header" style={subPageNavBarStyle(metrics)}>
|
<View className="sub-page-header" style={subPageNavBarStyle(metrics)}>
|
||||||
|
<Text className="sub-page-header__title">{title}</Text>
|
||||||
<View
|
<View
|
||||||
className="sub-page-header__content"
|
className="sub-page-header__content"
|
||||||
style={{ height: `${metrics.navContentHeight}px` }}
|
style={subPageNavContentStyle(metrics)}
|
||||||
>
|
>
|
||||||
<View className="sub-page-header__back" onClick={handleBack}>
|
<View className="sub-page-header__back" onClick={handleBack}>
|
||||||
<Text className="sub-page-header__back-icon">‹</Text>
|
<Text className="sub-page-header__back-icon">‹</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text className="sub-page-header__title">{title}</Text>
|
|
||||||
{right ? <View className="sub-page-header__right">{right}</View> : null}
|
{right ? <View className="sub-page-header__right">{right}</View> : null}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
import { View, Text } from '@tarojs/components';
|
||||||
import { navBarStyle, useNavBarMetrics } from '../lib/nav-bar';
|
import { navBarStyle, tabNavContentStyle, useNavBarMetrics } from '../lib/nav-bar';
|
||||||
|
|
||||||
type TabMainHeaderProps = {
|
type TabMainHeaderProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -13,11 +13,11 @@ export default function TabMainHeader({ title, extra }: TabMainHeaderProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="tab-main-header" style={navBarStyle(metrics)}>
|
<View className="tab-main-header" style={navBarStyle(metrics)}>
|
||||||
|
<Text className="tab-main-header__title">{title}</Text>
|
||||||
<View
|
<View
|
||||||
className="tab-main-header__content"
|
className="tab-main-header__content"
|
||||||
style={{ height: `${metrics.navContentHeight}px` }}
|
style={tabNavContentStyle(metrics)}
|
||||||
>
|
>
|
||||||
<Text className="tab-main-header__title">{title}</Text>
|
|
||||||
{extra ? <View className="tab-main-header__extra">{extra}</View> : null}
|
{extra ? <View className="tab-main-header__extra">{extra}</View> : null}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,34 +1,108 @@
|
|||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { request } from './api';
|
import { request } from './api';
|
||||||
|
|
||||||
type MiniProfilePayload = {
|
import type { UserProfile } from './api';
|
||||||
|
|
||||||
|
export type MiniWechatProfile = {
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 小程序授权后拉取微信昵称/头像并上报服务端 */
|
const WX_PROFILE_CACHE_KEY = 'wx_mini_profile_cache';
|
||||||
export async function syncMiniWechatProfile(): Promise<void> {
|
|
||||||
if (process.env.TARO_ENV !== 'weapp') return;
|
|
||||||
|
|
||||||
let profile: MiniProfilePayload | null = null;
|
export function cacheWxProfile(info: MiniWechatProfile) {
|
||||||
|
if (!info.nickname && !info.avatarUrl) return;
|
||||||
try {
|
try {
|
||||||
const res = await Taro.getUserProfile({ desc: '用于完善会员资料' });
|
Taro.setStorageSync(WX_PROFILE_CACHE_KEY, JSON.stringify(info));
|
||||||
profile = {
|
|
||||||
nickname: res.userInfo?.nickName,
|
|
||||||
avatarUrl: res.userInfo?.avatarUrl,
|
|
||||||
};
|
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
/* ignore */
|
||||||
}
|
|
||||||
|
|
||||||
if (!profile.nickname && !profile.avatarUrl) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await request('/auth/wechat/mini-profile', {
|
|
||||||
method: 'POST',
|
|
||||||
data: profile,
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
/* 用户拒绝或上报失败时不阻断主流程 */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
export function navBarStyle(metrics: NavBarMetrics): Record<string, string | number> {
|
||||||
return {
|
return {
|
||||||
paddingTop: `${metrics.statusBarHeight}px`,
|
paddingTop: `${metrics.statusBarHeight}px`,
|
||||||
height: `${metrics.navBarHeight}px`,
|
height: `${metrics.navBarHeight}px`,
|
||||||
paddingLeft: `${metrics.navBarPaddingLeft}px`,
|
|
||||||
paddingRight: `${metrics.navBarPaddingRight}px`,
|
|
||||||
...pageShellCssVars(metrics),
|
...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> {
|
export function subPageNavBarStyle(metrics: NavBarMetrics): Record<string, string | number> {
|
||||||
const paddingSide = Math.max(48, metrics.navBarPaddingRight);
|
const pagePad = 20;
|
||||||
return {
|
return {
|
||||||
paddingTop: `${metrics.statusBarHeight}px`,
|
paddingTop: `${metrics.statusBarHeight}px`,
|
||||||
height: `${metrics.navBarHeight}px`,
|
height: `${metrics.navBarHeight}px`,
|
||||||
paddingLeft: `${paddingSide}px`,
|
|
||||||
paddingRight: `${paddingSide}px`,
|
|
||||||
'--nav-bar-height': `${metrics.navBarHeight}px`,
|
'--nav-bar-height': `${metrics.navBarHeight}px`,
|
||||||
'--nav-content-height': `${metrics.navContentHeight}px`,
|
'--nav-content-height': `${metrics.navContentHeight}px`,
|
||||||
'--nav-status-bar-height': `${metrics.statusBarHeight}px`,
|
'--nav-status-bar-height': `${metrics.statusBarHeight}px`,
|
||||||
'--nav-padding-right': `${paddingSide}px`,
|
'--nav-padding-left': `${pagePad}px`,
|
||||||
'--nav-padding-left': `${paddingSide}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: true; profile?: UserProfile }
|
||||||
| { ok: false; needBindPhone: true; wxSessionKey: string };
|
| { 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();
|
const res = await Taro.login();
|
||||||
if (!res.code) {
|
if (!res.code) {
|
||||||
throw new Error(res.errMsg || '微信授权失败');
|
throw new Error(res.errMsg || '微信授权失败');
|
||||||
@@ -73,7 +75,7 @@ export async function bindWechatForUser(): Promise<WechatBindResult> {
|
|||||||
if (data.needBindPhone && data.wxSessionKey) {
|
if (data.needBindPhone && data.wxSessionKey) {
|
||||||
return { ok: false, needBindPhone: true, wxSessionKey: data.wxSessionKey };
|
return { ok: false, needBindPhone: true, wxSessionKey: data.wxSessionKey };
|
||||||
}
|
}
|
||||||
await syncMiniWechatProfile();
|
await syncMiniWechatProfile(prefetchedWxProfile);
|
||||||
const profile = await fetchUserProfile();
|
const profile = await fetchUserProfile();
|
||||||
return { ok: true, profile };
|
return { ok: true, profile };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
|
import { normalizePhoneInput } from './phone';
|
||||||
|
|
||||||
const USER_PHONE_KEY = 'user_phone';
|
const USER_PHONE_KEY = 'user_phone';
|
||||||
|
|
||||||
|
function isValidMobile(phone: string) {
|
||||||
|
return /^1[3-9]\d{9}$/.test(phone);
|
||||||
|
}
|
||||||
|
|
||||||
export function saveUserPhone(phone: string) {
|
export function saveUserPhone(phone: string) {
|
||||||
const normalized = phone.replace(/\D/g, '').slice(0, 11);
|
const normalized = normalizePhoneInput(phone);
|
||||||
if (!/^1[3-9]\d{9}$/.test(normalized)) return;
|
if (!isValidMobile(normalized)) return;
|
||||||
try {
|
try {
|
||||||
Taro.setStorageSync(USER_PHONE_KEY, normalized);
|
Taro.setStorageSync(USER_PHONE_KEY, normalized);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -20,3 +25,14 @@ export function getStoredUserPhone(): string {
|
|||||||
return '';
|
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 type { WechatLoginResult } from '@dukang/shared-types';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { request } from './api';
|
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> {
|
export async function loginWithWechat(): Promise<WechatLoginResult> {
|
||||||
const res = await Taro.login();
|
const res = await Taro.login();
|
||||||
if (!res.code) {
|
if (!res.code) {
|
||||||
throw new Error(res.errMsg || '微信登录失败,未获取到 code');
|
throw new Error(res.errMsg || '微信登录失败,未获取到 code');
|
||||||
}
|
}
|
||||||
const result = await request<WechatLoginResult>('/auth/login/wechat', {
|
return request<WechatLoginResult>('/auth/login/wechat', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { code: res.code, platform: 'mini' },
|
data: { code: res.code, platform: 'mini' },
|
||||||
});
|
});
|
||||||
if (result.accessToken) {
|
|
||||||
await syncMiniWechatProfile();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { bindWechatForUser } from './pay-wechat';
|
export { bindWechatForUser } from './pay-wechat';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import RegionPicker from '../../components/RegionPicker';
|
|||||||
import { buildAddressListUrl, readCheckoutContext } from '../../lib/checkout-nav';
|
import { buildAddressListUrl, readCheckoutContext } from '../../lib/checkout-nav';
|
||||||
import { DEFAULT_REGION, formatRegion, type RegionSelection } from '../../lib/region-data';
|
import { DEFAULT_REGION, formatRegion, type RegionSelection } from '../../lib/region-data';
|
||||||
import { normalizePhoneInput, validateMobilePhone } from '../../lib/phone';
|
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';
|
import { request, toast, type UserProfile } from '../../lib/api';
|
||||||
|
|
||||||
type AddressForm = {
|
type AddressForm = {
|
||||||
@@ -28,24 +28,23 @@ export default function AddressEditPage() {
|
|||||||
const [pickerOpen, setPickerOpen] = useState(false);
|
const [pickerOpen, setPickerOpen] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [form, setForm] = useState<AddressForm>({
|
const [form, setForm] = useState<AddressForm>(() => ({
|
||||||
receiverName: '',
|
receiverName: '',
|
||||||
phone: '',
|
phone: id ? '' : getStoredUserPhone(),
|
||||||
province: DEFAULT_REGION.province,
|
province: DEFAULT_REGION.province,
|
||||||
city: DEFAULT_REGION.city,
|
city: DEFAULT_REGION.city,
|
||||||
district: DEFAULT_REGION.district,
|
district: DEFAULT_REGION.district,
|
||||||
detail: '',
|
detail: '',
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
});
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) return;
|
if (id) return;
|
||||||
request<UserProfile>('/auth/me')
|
request<UserProfile>('/auth/me')
|
||||||
.then((me) => {
|
.then((me) => {
|
||||||
if (!me.phoneVerified) return;
|
const phone = resolveDefaultUserPhone(me);
|
||||||
const stored = getStoredUserPhone();
|
if (!phone) return;
|
||||||
if (!stored) return;
|
setForm((prev) => (prev.phone ? prev : { ...prev, phone }));
|
||||||
setForm((prev) => (prev.phone ? prev : { ...prev, phone: stored }));
|
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import PageShell from '../../components/PageShell';
|
|||||||
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar';
|
||||||
import { goLogin } from '../../lib/auth-nav';
|
import { goLogin } from '../../lib/auth-nav';
|
||||||
import { isLoggedIn, request, toast } from '../../lib/api';
|
import { isLoggedIn, request, toast } from '../../lib/api';
|
||||||
import { navBarStyle, useNavBarMetrics } from '../../lib/nav-bar';
|
import { navBarStyle, tabNavContentStyle, useNavBarMetrics } from '../../lib/nav-bar';
|
||||||
|
|
||||||
type BenefitSummary = {
|
type BenefitSummary = {
|
||||||
totalBalance: number;
|
totalBalance: number;
|
||||||
@@ -64,18 +64,15 @@ export default function BenefitPage() {
|
|||||||
return (
|
return (
|
||||||
<PageShell variant="tab" className="benefit-page">
|
<PageShell variant="tab" className="benefit-page">
|
||||||
<View className="benefit-header" style={navBarStyle(metrics)}>
|
<View className="benefit-header" style={navBarStyle(metrics)}>
|
||||||
|
<Text className="benefit-header-title">好客权益</Text>
|
||||||
<View
|
<View
|
||||||
className="benefit-header__content"
|
className="benefit-header__content"
|
||||||
style={{ height: `${metrics.navContentHeight}px` }}
|
style={tabNavContentStyle(metrics)}
|
||||||
>
|
>
|
||||||
<View className="benefit-header-city">
|
<View className="benefit-header-city">
|
||||||
<View className="benefit-header-city-pin" />
|
<View className="benefit-header-city-pin" />
|
||||||
<Text>郑州市</Text>
|
<Text>郑州市</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text className="benefit-header-title">好客权益</Text>
|
|
||||||
<View className="benefit-header-btn" onClick={() => toast('消息通知即将开放')}>
|
|
||||||
<Text>🔔</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ import { BRAND_LOGO_WIDE_URL } from '@dukang/shared-types';
|
|||||||
import { finishLoginNavigate } from '../../lib/auth-nav';
|
import { finishLoginNavigate } from '../../lib/auth-nav';
|
||||||
import { bindWechatForUser } from '../../lib/wechat-auth';
|
import { bindWechatForUser } from '../../lib/wechat-auth';
|
||||||
import { fetchUserProfile } from '../../lib/pay-wechat';
|
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 { isLoggedIn, request, saveAuth, toast, type SessionPayload } from '../../lib/api';
|
||||||
import { loginWithWechat } from '../../lib/wechat-auth';
|
import { loginWithWechat } from '../../lib/wechat-auth';
|
||||||
|
|
||||||
@@ -91,18 +97,28 @@ export default function LoginPage() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySessionAndLeave(data: SessionPayload | WechatLoginResult, phone?: string) {
|
function applySessionAndLeave(
|
||||||
|
data: SessionPayload | WechatLoginResult,
|
||||||
|
phone?: string,
|
||||||
|
wxInfo?: MiniWechatProfile | null,
|
||||||
|
) {
|
||||||
if (!data.accessToken) return;
|
if (!data.accessToken) return;
|
||||||
if (phone) saveUserPhone(phone);
|
if (phone) saveUserPhone(phone);
|
||||||
saveAuth({
|
saveAuth({
|
||||||
accessToken: data.accessToken,
|
accessToken: data.accessToken,
|
||||||
refreshToken: data.refreshToken,
|
refreshToken: data.refreshToken,
|
||||||
});
|
});
|
||||||
|
void syncMiniWechatProfile(wxInfo ?? getCachedWxProfile());
|
||||||
|
if (!phone) {
|
||||||
|
void fetchUserProfile()
|
||||||
|
.then((me) => resolveDefaultUserPhone(me))
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
toast('登录成功', 'success');
|
toast('登录成功', 'success');
|
||||||
finishLoginNavigate(returnTo);
|
finishLoginNavigate(returnTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWechatLoginResult(result: WechatLoginResult) {
|
function handleWechatLoginResult(result: WechatLoginResult, wxInfo?: MiniWechatProfile | null) {
|
||||||
if (result.needBindPhone && result.wxSessionKey) {
|
if (result.needBindPhone && result.wxSessionKey) {
|
||||||
setBindMode(true);
|
setBindMode(true);
|
||||||
setWxSessionKey(result.wxSessionKey);
|
setWxSessionKey(result.wxSessionKey);
|
||||||
@@ -111,7 +127,7 @@ export default function LoginPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.accessToken) {
|
if (result.accessToken) {
|
||||||
applySessionAndLeave(result);
|
applySessionAndLeave(result, undefined, wxInfo);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMsg('微信登录未完成,请重试或使用手机号登录');
|
setMsg('微信登录未完成,请重试或使用手机号登录');
|
||||||
@@ -196,8 +212,18 @@ export default function LoginPage() {
|
|||||||
setSentHint('');
|
setSentHint('');
|
||||||
setWxLoading(true);
|
setWxLoading(true);
|
||||||
try {
|
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()) {
|
if (completeMode === 'wechat' && isLoggedIn()) {
|
||||||
const result = await bindWechatForUser();
|
const result = await bindWechatForUser(wxInfo);
|
||||||
if (!result.ok && result.needBindPhone) {
|
if (!result.ok && result.needBindPhone) {
|
||||||
setBindMode(true);
|
setBindMode(true);
|
||||||
setWxSessionKey(result.wxSessionKey);
|
setWxSessionKey(result.wxSessionKey);
|
||||||
@@ -206,6 +232,7 @@ export default function LoginPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
|
await syncMiniWechatProfile(wxInfo);
|
||||||
toast('微信授权成功', 'success');
|
toast('微信授权成功', 'success');
|
||||||
finishLoginNavigate(returnTo);
|
finishLoginNavigate(returnTo);
|
||||||
return;
|
return;
|
||||||
@@ -213,7 +240,7 @@ export default function LoginPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await loginWithWechat();
|
const result = await loginWithWechat();
|
||||||
handleWechatLoginResult(result);
|
handleWechatLoginResult(result, wxInfo);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const raw = e instanceof Error ? e.message : '微信登录失败';
|
const raw = e instanceof Error ? e.message : '微信登录失败';
|
||||||
const hint = /invalid code/i.test(raw)
|
const hint = /invalid code/i.test(raw)
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../co
|
|||||||
import { BRAND_LOGO_MARK_URL } from '@dukang/shared-types';
|
import { BRAND_LOGO_MARK_URL } from '@dukang/shared-types';
|
||||||
import { goLogin } from '../../lib/auth-nav';
|
import { goLogin } from '../../lib/auth-nav';
|
||||||
import { bindWechatForUser } from '../../lib/wechat-auth';
|
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';
|
import { isLoggedIn, logout, request, toast, type UserProfile } from '../../lib/api';
|
||||||
|
|
||||||
const ORDER_SHORTCUTS = [
|
const ORDER_SHORTCUTS = [
|
||||||
@@ -28,22 +34,21 @@ function formatMoney(amount: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MinePage() {
|
export default function MinePage() {
|
||||||
const loggedIn = isLoggedIn();
|
const [authed, setAuthed] = useState(() => isLoggedIn());
|
||||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||||
const [benefitBalance, setBenefitBalance] = useState(0);
|
const [benefitBalance, setBenefitBalance] = useState(0);
|
||||||
const [orderCounts, setOrderCounts] = useState<Record<string, number>>({});
|
const [orderCounts, setOrderCounts] = useState<Record<string, number>>({});
|
||||||
const [wxAuthorize, setWxAuthorize] = useState(true);
|
const [wxAuthorize, setWxAuthorize] = useState(true);
|
||||||
const [bindingWx, setBindingWx] = useState(false);
|
const [bindingWx, setBindingWx] = useState(false);
|
||||||
|
|
||||||
useDidShow(() => {
|
function resetGuestState() {
|
||||||
syncTabBarSelected(3);
|
setProfile(null);
|
||||||
if (isLoggedIn()) {
|
setBenefitBalance(0);
|
||||||
loadProfile();
|
setOrderCounts({});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
function loadProfile() {
|
function loadProfile() {
|
||||||
if (!loggedIn) return;
|
if (!isLoggedIn()) return;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
request<UserProfile>('/auth/me'),
|
request<UserProfile>('/auth/me'),
|
||||||
request<Array<Record<string, unknown>>>('/benefit/coupons').catch(() => []),
|
request<Array<Record<string, unknown>>>('/benefit/coupons').catch(() => []),
|
||||||
@@ -52,7 +57,7 @@ export default function MinePage() {
|
|||||||
),
|
),
|
||||||
])
|
])
|
||||||
.then(([me, coupons, ...totals]) => {
|
.then(([me, coupons, ...totals]) => {
|
||||||
setProfile(me);
|
setProfile(mergeWxDisplayProfile(me));
|
||||||
const balance = (coupons as Array<Record<string, unknown>>).reduce((sum, c) => {
|
const balance = (coupons as Array<Record<string, unknown>>).reduce((sum, c) => {
|
||||||
if (String(c.status) === 'ACTIVE') return sum + Number(c.balance || 0);
|
if (String(c.status) === 'ACTIVE') return sum + Number(c.balance || 0);
|
||||||
return sum;
|
return sum;
|
||||||
@@ -67,30 +72,74 @@ export default function MinePage() {
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useDidShow(() => {
|
||||||
|
syncTabBarSelected(3);
|
||||||
|
const loggedInNow = isLoggedIn();
|
||||||
|
setAuthed(loggedInNow);
|
||||||
|
if (loggedInNow) {
|
||||||
|
loadProfile();
|
||||||
|
} else {
|
||||||
|
resetGuestState();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
request<ClientRuntimeConfig>('/common/client-config')
|
request<ClientRuntimeConfig>('/common/client-config')
|
||||||
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
|
.then((config) => setWxAuthorize(isWxAuthorizeEnabled(config)))
|
||||||
.catch(() => setWxAuthorize(true));
|
.catch(() => setWxAuthorize(true));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadProfile();
|
|
||||||
}, [loggedIn]);
|
|
||||||
|
|
||||||
async function handleAvatarTap() {
|
async function handleAvatarTap() {
|
||||||
if (!loggedIn) {
|
if (!isLoggedIn()) {
|
||||||
goLogin('/pages/mine/index');
|
goLogin('/pages/mine/index');
|
||||||
return;
|
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);
|
setBindingWx(true);
|
||||||
try {
|
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) {
|
if (!result.ok && result.needBindPhone) {
|
||||||
goLogin('/pages/mine/index', { bindMode: '1', wxSessionKey: result.wxSessionKey });
|
goLogin('/pages/mine/index', { bindMode: '1', wxSessionKey: result.wxSessionKey });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.ok) {
|
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();
|
loadProfile();
|
||||||
toast('微信授权成功', 'success');
|
toast('微信授权成功', 'success');
|
||||||
}
|
}
|
||||||
@@ -115,33 +164,46 @@ export default function MinePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAvatarContent(profile: UserProfile | null) {
|
function resolveDisplayProfile(profile: UserProfile | null, hasWechat: boolean) {
|
||||||
if (profile?.avatarUrl) {
|
if (!profile) {
|
||||||
return <Image className="mine-avatar-img" src={profile.avatarUrl} mode="aspectFill" />;
|
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 <Image className="mine-avatar-img" src={displayAvatarUrl} mode="aspectFill" />;
|
||||||
}
|
}
|
||||||
return <Image className="mine-avatar-img" src={BRAND_LOGO_MARK_URL} mode="aspectFit" />;
|
return <Image className="mine-avatar-img" src={BRAND_LOGO_MARK_URL} mode="aspectFit" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!loggedIn) {
|
if (!authed) {
|
||||||
return (
|
return (
|
||||||
<PageShell variant="tab" className="mine-page">
|
<PageShell variant="tab" className="mine-page">
|
||||||
<TabMainHeader title="我的" />
|
<TabMainHeader title="我的" />
|
||||||
<View className="mine-header">
|
<View className="mine-header">
|
||||||
<View className="mine-header-texture" />
|
<View className="mine-header-texture" />
|
||||||
<View className="mine-profile">
|
<View className="mine-profile">
|
||||||
<View className="mine-avatar">
|
<View className="mine-avatar-wrap mine-avatar-wrap--action" onClick={() => goLogin('/pages/mine/index')}>
|
||||||
<Image className="mine-avatar-img" src={BRAND_LOGO_MARK_URL} mode="aspectFit" />
|
<View className="mine-avatar mine-avatar--wx-pending">
|
||||||
|
<Image className="mine-avatar-img" src={BRAND_LOGO_MARK_URL} mode="aspectFit" />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View>
|
||||||
<Text className="mine-profile-name">未登录</Text>
|
<Text className="mine-profile-name">未登录</Text>
|
||||||
<Text className="mine-member-tag">好客会员</Text>
|
<Text className="mine-member-tag">点击头像登录</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View className="mine-login-gate u-card">
|
<View className="mine-login-gate">
|
||||||
<View className="u-empty">登录后管理订单与个人信息</View>
|
<View className="mine-login-gate-hint">登录后管理订单与个人信息</View>
|
||||||
<View
|
<View
|
||||||
className="u-btn u-btn--block"
|
className="mine-login-btn"
|
||||||
onClick={() => goLogin('/pages/mine/index')}
|
onClick={() => goLogin('/pages/mine/index')}
|
||||||
>
|
>
|
||||||
<Text>去登录</Text>
|
<Text>去登录</Text>
|
||||||
@@ -152,9 +214,11 @@ export default function MinePage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nickname = profile?.nickname || '用户';
|
|
||||||
const hasWechat = !!profile?.hasWechat;
|
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 (
|
return (
|
||||||
<PageShell variant="tab" className="mine-page">
|
<PageShell variant="tab" className="mine-page">
|
||||||
@@ -162,12 +226,19 @@ export default function MinePage() {
|
|||||||
<View className="mine-header">
|
<View className="mine-header">
|
||||||
<View className="mine-header-texture" />
|
<View className="mine-header-texture" />
|
||||||
<View className="mine-profile">
|
<View className="mine-profile">
|
||||||
<View className="mine-avatar-wrap" onClick={() => void handleAvatarTap()}>
|
<View
|
||||||
<View className="mine-avatar">
|
className={`mine-avatar-wrap${!hasWechat && canWxBind ? ' mine-avatar-wrap--action' : ''}`}
|
||||||
{renderAvatarContent(profile)}
|
onClick={() => void handleAvatarTap()}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
className={`mine-avatar${hasWechat ? ' mine-avatar--wx-ok' : canWxBind ? ' mine-avatar--wx-pending' : ''}`}
|
||||||
|
>
|
||||||
|
{renderAvatarContent(display.avatarUrl)}
|
||||||
</View>
|
</View>
|
||||||
{!hasWechat && wxAuthorize && process.env.TARO_ENV === 'weapp' ? (
|
{!hasWechat && canWxBind ? (
|
||||||
<Text className="mine-avatar-badge">{bindingWx ? '授权中' : '授权'}</Text>
|
<View className="mine-avatar-status mine-avatar-status--pending">
|
||||||
|
<Text>{bindingWx ? '授权中' : '去授权'}</Text>
|
||||||
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
<View>
|
<View>
|
||||||
@@ -175,7 +246,7 @@ export default function MinePage() {
|
|||||||
<Text className={`mine-member-tag${hasWechat ? ' mine-member-tag--wechat' : ''}`}>
|
<Text className={`mine-member-tag${hasWechat ? ' mine-member-tag--wechat' : ''}`}>
|
||||||
{memberLabel}
|
{memberLabel}
|
||||||
</Text>
|
</Text>
|
||||||
{!hasWechat && wxAuthorize && process.env.TARO_ENV === 'weapp' ? (
|
{!hasWechat && canWxBind ? (
|
||||||
<Text className="mine-wechat-hint">点击头像完成微信授权</Text>
|
<Text className="mine-wechat-hint">点击头像完成微信授权</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -7,18 +7,38 @@
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
padding-left: var(--space-page);
|
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.benefit-header__content {
|
.benefit-header__content {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
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 {
|
.benefit-header-city {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -26,6 +46,8 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
max-width: 30vw;
|
max-width: 30vw;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.benefit-header-city-pin {
|
.benefit-header-city-pin {
|
||||||
@@ -37,23 +59,6 @@
|
|||||||
flex-shrink: 0;
|
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 {
|
.benefit-main {
|
||||||
padding: 16px var(--space-page) 24px;
|
padding: 16px var(--space-page) 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
/* 我的 */
|
/* 我的 */
|
||||||
|
.page-shell.mine-page {
|
||||||
|
min-height: auto;
|
||||||
|
padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px));
|
||||||
|
}
|
||||||
|
|
||||||
.mine-page {
|
.mine-page {
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-header {
|
.mine-header {
|
||||||
position: relative;
|
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%);
|
background: linear-gradient(135deg, #820012 0%, var(--color-heritage-red) 40%, #d4a373 100%);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -34,7 +39,36 @@
|
|||||||
.mine-avatar-wrap {
|
.mine-avatar-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-shrink: 0;
|
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 {
|
.mine-avatar-badge {
|
||||||
@@ -54,26 +88,44 @@
|
|||||||
|
|
||||||
.mine-wechat-hint {
|
.mine-wechat-hint {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 6px;
|
margin-top: 4px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: rgba(255, 255, 255, 0.75);
|
color: rgba(255, 255, 255, 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-avatar {
|
.mine-avatar {
|
||||||
width: 72px;
|
width: 64px;
|
||||||
height: 72px;
|
height: 64px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2px solid rgba(255, 255, 255, 0.35);
|
border: 2px solid rgba(255, 255, 255, 0.35);
|
||||||
background: #fff;
|
background: #fff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-right: 14px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: var(--color-heritage-red);
|
color: var(--color-heritage-red);
|
||||||
font-size: 28px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
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 {
|
.mine-avatar-img {
|
||||||
@@ -84,10 +136,10 @@
|
|||||||
.mine-profile-name {
|
.mine-profile-name {
|
||||||
display: block;
|
display: block;
|
||||||
font-family: var(--font-headline);
|
font-family: var(--font-headline);
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-member-tag {
|
.mine-member-tag {
|
||||||
@@ -100,30 +152,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mine-main {
|
.mine-main {
|
||||||
margin-top: -36px;
|
margin-top: -28px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
padding: 0 var(--space-page) 12px;
|
padding: 0 var(--space-page) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-card {
|
.mine-card {
|
||||||
background: var(--color-card);
|
background: var(--color-card);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
padding: 14px;
|
padding: 12px;
|
||||||
box-shadow: var(--shadow-card);
|
box-shadow: var(--shadow-card);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-card:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-card-head {
|
.mine-card-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-card-title {
|
.mine-card-title {
|
||||||
font-family: var(--font-headline);
|
font-family: var(--font-headline);
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-on-surface);
|
color: var(--color-on-surface);
|
||||||
}
|
}
|
||||||
@@ -137,7 +193,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: linear-gradient(135deg, #fff9e6 0%, #fff0c2 100%);
|
background: linear-gradient(135deg, #fff9e6 0%, #fff0c2 100%);
|
||||||
}
|
}
|
||||||
@@ -163,16 +219,16 @@
|
|||||||
|
|
||||||
.mine-asset-value {
|
.mine-asset-value {
|
||||||
font-family: var(--font-headline);
|
font-family: var(--font-headline);
|
||||||
font-size: 28px;
|
font-size: 24px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-asset-cta {
|
.mine-asset-cta {
|
||||||
padding: 8px 16px;
|
padding: 6px 14px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--color-heritage-red);
|
background: var(--color-heritage-red);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,22 +241,22 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8px;
|
padding: 4px;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 33%;
|
width: 33%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-order-icon {
|
.mine-order-icon {
|
||||||
width: 40px;
|
width: 36px;
|
||||||
height: 40px;
|
height: 36px;
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
background: rgba(166, 29, 36, 0.08);
|
background: rgba(166, 29, 36, 0.08);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: var(--color-heritage-red);
|
color: var(--color-heritage-red);
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-order-badge {
|
.mine-order-badge {
|
||||||
@@ -233,21 +289,21 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px 4px;
|
padding: 6px 4px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-service-icon {
|
.mine-service-icon {
|
||||||
width: 36px;
|
width: 32px;
|
||||||
height: 36px;
|
height: 32px;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
background: var(--color-surface-container-low);
|
background: var(--color-surface-container-low);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: var(--color-heritage-red);
|
color: var(--color-heritage-red);
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-service-label {
|
.mine-service-label {
|
||||||
@@ -257,29 +313,59 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mine-footer {
|
.mine-footer {
|
||||||
text-align: center;
|
display: flex;
|
||||||
padding: 8px 0 4px;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 0 0;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-version {
|
.mine-version {
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-subtle-gray);
|
color: var(--color-subtle-gray);
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-logout {
|
.mine-logout {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
padding: 10px 32px;
|
padding: 6px 18px;
|
||||||
border-radius: var(--radius-full);
|
border-radius: var(--radius-full);
|
||||||
border: 1px solid rgba(166, 29, 36, 0.3);
|
border: 1px solid rgba(166, 29, 36, 0.3);
|
||||||
color: var(--color-heritage-red);
|
color: var(--color-heritage-red);
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-login-gate {
|
.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;
|
position: relative;
|
||||||
z-index: 2;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,23 +34,29 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-main-header__title {
|
.tab-main-header__title {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: var(--nav-padding-left);
|
left: 0;
|
||||||
right: var(--nav-padding-right);
|
right: 0;
|
||||||
|
top: var(--nav-status-bar-height);
|
||||||
|
height: var(--nav-content-height);
|
||||||
|
line-height: var(--nav-content-height);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 0 48px;
|
||||||
|
box-sizing: border-box;
|
||||||
font-family: var(--font-headline);
|
font-family: var(--font-headline);
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-heritage-red);
|
color: var(--color-heritage-red);
|
||||||
line-height: 1.2;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-main-header__extra {
|
.tab-main-header__extra {
|
||||||
@@ -94,11 +100,13 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-nav-bar__btn {
|
.page-nav-bar__btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
@@ -107,6 +115,16 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba(255, 255, 255, 0.8);
|
background: rgba(255, 255, 255, 0.8);
|
||||||
flex-shrink: 0;
|
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 {
|
.page-nav-bar--solid .page-nav-bar__btn {
|
||||||
@@ -119,10 +137,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-nav-bar__icon {
|
.page-nav-bar__icon {
|
||||||
font-size: 28px;
|
font-size: 32px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: var(--color-ink-black);
|
color: var(--color-ink-black);
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
|
margin-top: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-nav-bar__icon--share {
|
.page-nav-bar__icon--share {
|
||||||
@@ -132,19 +151,25 @@
|
|||||||
|
|
||||||
.page-nav-bar__title {
|
.page-nav-bar__title {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: var(--nav-padding-left);
|
left: 0;
|
||||||
right: var(--nav-padding-right);
|
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;
|
opacity: 0;
|
||||||
font-family: var(--font-headline);
|
font-family: var(--font-headline);
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-ink-black);
|
color: var(--color-ink-black);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: center;
|
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-nav-bar__title--visible {
|
.page-nav-bar__title--visible {
|
||||||
@@ -170,10 +195,31 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
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 {
|
.sub-page-header__back {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -181,32 +227,18 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
margin-left: -8px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-page-header__back-icon {
|
.sub-page-header__back-icon {
|
||||||
font-size: 28px;
|
font-size: 32px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: var(--color-heritage-red);
|
color: var(--color-heritage-red);
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
margin-top: -2px;
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-page-header__right {
|
.sub-page-header__right {
|
||||||
@@ -214,6 +246,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-page-body {
|
.sub-page-body {
|
||||||
|
|||||||
@@ -1255,28 +1255,42 @@ export class AuthService {
|
|||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
const nickname = input.nickname?.trim();
|
const nickname = input.nickname?.trim();
|
||||||
if (nickname && this.isDefaultNickname(user.nickname)) {
|
if (nickname) {
|
||||||
data.nickname = nickname.slice(0, 64);
|
data.nickname = nickname.slice(0, 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarUrl = input.avatarUrl?.trim();
|
const avatarUrl = input.avatarUrl?.trim();
|
||||||
if (avatarUrl && !user.avatarResourceId) {
|
if (avatarUrl) {
|
||||||
const avatar = await this.prisma.commonResource.create({
|
if (user.avatarResourceId) {
|
||||||
data: {
|
await this.prisma.commonResource.update({
|
||||||
ownerType: 'USER',
|
where: { id: user.avatarResourceId },
|
||||||
ownerId: userId,
|
data: { url: avatarUrl },
|
||||||
bizType: 'AVATAR',
|
});
|
||||||
mediaType: 'IMAGE',
|
} else {
|
||||||
ossBucket: 'wechat',
|
const avatar = await this.prisma.commonResource.create({
|
||||||
ossKey: `wx-avatar/${user.wxOpenId}`,
|
data: {
|
||||||
url: avatarUrl,
|
ownerType: 'USER',
|
||||||
status: 'ACTIVE',
|
ownerId: userId,
|
||||||
},
|
bizType: 'AVATAR',
|
||||||
});
|
mediaType: 'IMAGE',
|
||||||
data.avatarResourceId = avatar.id;
|
ossBucket: 'wechat',
|
||||||
|
ossKey: `wx-avatar/${user.wxOpenId}`,
|
||||||
|
url: avatarUrl,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
data.avatarResourceId = avatar.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.nickname && !data.avatarResourceId) {
|
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);
|
return this.formatUserProfile(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1719,7 +1733,7 @@ export class AuthService {
|
|||||||
return {
|
return {
|
||||||
id: user.id.toString(),
|
id: user.id.toString(),
|
||||||
userNo: user.userNo,
|
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,
|
phoneVerified: !!user.phoneVerifiedAt,
|
||||||
nickname: user.nickname,
|
nickname: user.nickname,
|
||||||
avatarUrl: user.avatar?.url ?? null,
|
avatarUrl: user.avatar?.url ?? null,
|
||||||
|
|||||||
Reference in New Issue
Block a user