Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { ClientApp } from '@dukang/shared-types';
|
import { ClientApp } from '@dukang/shared-types';
|
||||||
import { goLogin, forceReloadAfterAccountMerge } from './auth-nav';
|
import { forceReloadAfterAccountMerge } from './auth-nav';
|
||||||
|
|
||||||
function resolveApiBase(): string {
|
function resolveApiBase(): string {
|
||||||
const origin =
|
const origin =
|
||||||
@@ -54,10 +54,6 @@ export function isLoggedIn(): boolean {
|
|||||||
return !!getToken();
|
return !!getToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function redirectToLogin() {
|
|
||||||
goLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logout() {
|
export function logout() {
|
||||||
clearAuth();
|
clearAuth();
|
||||||
Taro.reLaunch({ url: '/pages/home/index' });
|
Taro.reLaunch({ url: '/pages/home/index' });
|
||||||
@@ -76,17 +72,6 @@ function parseBody(data: unknown): { code?: number; message?: string } {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOnLoginPage(): boolean {
|
|
||||||
try {
|
|
||||||
const pages = Taro.getCurrentPages();
|
|
||||||
const cur = pages[pages.length - 1] as { route?: string } | undefined;
|
|
||||||
const route = cur?.route || '';
|
|
||||||
return route.includes('pages/login');
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 统一请求:注入 CLIENT_APP + Bearer,解包 { code, message, data } */
|
/** 统一请求:注入 CLIENT_APP + Bearer,解包 { code, message, data } */
|
||||||
export async function request<T = unknown>(path: string, options: ReqOptions = {}): Promise<T> {
|
export async function request<T = unknown>(path: string, options: ReqOptions = {}): Promise<T> {
|
||||||
const header: Record<string, string> = {
|
const header: Record<string, string> = {
|
||||||
@@ -115,8 +100,6 @@ export async function request<T = unknown>(path: string, options: ReqOptions = {
|
|||||||
if (/账号已合并/.test(mergedMsg)) {
|
if (/账号已合并/.test(mergedMsg)) {
|
||||||
// 合并后旧 JWT 失效:强制刷新,不引导「重新登录」
|
// 合并后旧 JWT 失效:强制刷新,不引导「重新登录」
|
||||||
forceReloadAfterAccountMerge();
|
forceReloadAfterAccountMerge();
|
||||||
} else if (!isOnLoginPage()) {
|
|
||||||
redirectToLogin();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(body?.message || '登录已过期,请重新登录');
|
throw new Error(body?.message || '登录已过期,请重新登录');
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ const TAB_PAGES = new Set([
|
|||||||
'/pages/mine/index',
|
'/pages/mine/index',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
let loginNavigationPending = false;
|
||||||
|
|
||||||
|
function isLoginPageActive(): boolean {
|
||||||
|
const pages = Taro.getCurrentPages();
|
||||||
|
const current = pages[pages.length - 1] as { route?: string } | undefined;
|
||||||
|
return !!current?.route?.includes('pages/login/');
|
||||||
|
}
|
||||||
|
|
||||||
function currentPagePath(): string {
|
function currentPagePath(): string {
|
||||||
const pages = Taro.getCurrentPages();
|
const pages = Taro.getCurrentPages();
|
||||||
const cur = pages[pages.length - 1] as
|
const cur = pages[pages.length - 1] as
|
||||||
@@ -25,6 +33,7 @@ function currentPagePath(): string {
|
|||||||
|
|
||||||
/** 跳转登录页;默认带回当前页作为 return */
|
/** 跳转登录页;默认带回当前页作为 return */
|
||||||
export function goLogin(returnPath?: string, extras?: Record<string, string>) {
|
export function goLogin(returnPath?: string, extras?: Record<string, string>) {
|
||||||
|
if (loginNavigationPending || isLoginPageActive()) return;
|
||||||
const returnTo = returnPath ?? currentPagePath();
|
const returnTo = returnPath ?? currentPagePath();
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (returnTo) parts.push(`return=${encodeURIComponent(returnTo)}`);
|
if (returnTo) parts.push(`return=${encodeURIComponent(returnTo)}`);
|
||||||
@@ -34,9 +43,15 @@ export function goLogin(returnPath?: string, extras?: Record<string, string>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const url = parts.length ? `/pages/login/index?${parts.join('&')}` : '/pages/login/index';
|
const url = parts.length ? `/pages/login/index?${parts.join('&')}` : '/pages/login/index';
|
||||||
Taro.navigateTo({ url }).catch(() => {
|
loginNavigationPending = true;
|
||||||
Taro.redirectTo({ url });
|
void Taro.navigateTo({ url })
|
||||||
});
|
.catch(() => Taro.redirectTo({ url }))
|
||||||
|
.finally(() => {
|
||||||
|
// 等路由栈稳定后再释放,拦截同一轮请求触发的重复登录跳转。
|
||||||
|
setTimeout(() => {
|
||||||
|
loginNavigationPending = false;
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登录成功后回到 return 页,或回退 / 首页 */
|
/** 登录成功后回到 return 页,或回退 / 首页 */
|
||||||
|
|||||||
@@ -6,6 +6,17 @@ export type MiniWechatProfile = {
|
|||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MiniWechatProfileUpdate = MiniWechatProfile & {
|
||||||
|
avatarResourceId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UploadedAvatarResource = {
|
||||||
|
resourceId: string;
|
||||||
|
url: string;
|
||||||
|
bucket: string;
|
||||||
|
ossKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
const WX_PROFILE_CACHE_KEY = 'wx_mini_profile_cache';
|
const WX_PROFILE_CACHE_KEY = 'wx_mini_profile_cache';
|
||||||
|
|
||||||
export function cacheWxProfile(info: MiniWechatProfile) {
|
export function cacheWxProfile(info: MiniWechatProfile) {
|
||||||
@@ -55,8 +66,8 @@ export function mergeWxDisplayProfile(profile: UserProfile): UserProfile {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 上传 chooseAvatar 临时文件到 OSS,返回永久 URL */
|
/** 上传 chooseAvatar 临时文件到 OSS,并返回已登记到当前用户的真实资源。 */
|
||||||
export async function uploadAvatarTempFile(tempFilePath: string): Promise<string> {
|
export async function uploadAvatarTempFile(tempFilePath: string): Promise<UploadedAvatarResource> {
|
||||||
const { API_BASE, CLIENT_APP, getToken } = await import('./api');
|
const { API_BASE, CLIENT_APP, getToken } = await import('./api');
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (!token) throw new Error('请先登录');
|
if (!token) throw new Error('请先登录');
|
||||||
@@ -75,7 +86,11 @@ export async function uploadAvatarTempFile(tempFilePath: string): Promise<string
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let body: { code?: number; message?: string; data?: { url?: string } } = {};
|
let body: {
|
||||||
|
code?: number;
|
||||||
|
message?: string;
|
||||||
|
data?: { resourceId?: string; url?: string; bucket?: string; ossKey?: string };
|
||||||
|
} = {};
|
||||||
try {
|
try {
|
||||||
body = JSON.parse(String(res.data || '{}')) as typeof body;
|
body = JSON.parse(String(res.data || '{}')) as typeof body;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -84,14 +99,26 @@ export async function uploadAvatarTempFile(tempFilePath: string): Promise<string
|
|||||||
if (res.statusCode === 401 || body.code === 401) {
|
if (res.statusCode === 401 || body.code === 401) {
|
||||||
throw new Error(body.message || '登录已过期,请重新登录');
|
throw new Error(body.message || '登录已过期,请重新登录');
|
||||||
}
|
}
|
||||||
if (res.statusCode >= 400 || body.code !== 0 || !body.data?.url) {
|
if (
|
||||||
|
res.statusCode >= 400 ||
|
||||||
|
body.code !== 0 ||
|
||||||
|
!body.data?.resourceId ||
|
||||||
|
!body.data.url ||
|
||||||
|
!body.data.bucket ||
|
||||||
|
!body.data.ossKey
|
||||||
|
) {
|
||||||
throw new Error(body.message || '头像上传失败');
|
throw new Error(body.message || '头像上传失败');
|
||||||
}
|
}
|
||||||
return body.data.url;
|
return {
|
||||||
|
resourceId: body.data.resourceId,
|
||||||
|
url: body.data.url,
|
||||||
|
bucket: body.data.bucket,
|
||||||
|
ossKey: body.data.ossKey,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadMiniWechatProfile(info: MiniWechatProfile): Promise<UserProfile | null> {
|
export async function uploadMiniWechatProfile(info: MiniWechatProfileUpdate): Promise<UserProfile | null> {
|
||||||
if (!info.nickname && !info.avatarUrl) return null;
|
if (!info.nickname && !info.avatarUrl && !info.avatarResourceId) return null;
|
||||||
const { request } = await import('./api');
|
const { request } = await import('./api');
|
||||||
const updated = await request<UserProfile>('/auth/wechat/mini-profile', {
|
const updated = await request<UserProfile>('/auth/wechat/mini-profile', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -124,6 +151,7 @@ export async function syncMiniWechatProfile(
|
|||||||
if (process.env.TARO_ENV !== 'weapp') return null;
|
if (process.env.TARO_ENV !== 'weapp') return null;
|
||||||
const info = prefetched?.nickname || prefetched?.avatarUrl ? prefetched : getCachedWxProfile();
|
const info = prefetched?.nickname || prefetched?.avatarUrl ? prefetched : getCachedWxProfile();
|
||||||
if (!info?.nickname && !info?.avatarUrl) return null;
|
if (!info?.nickname && !info?.avatarUrl) return null;
|
||||||
await uploadMiniWechatProfile(info);
|
// 缓存头像 URL 可能来自历史微信资料,未经过当前 OSS 上传登记;这里只同步昵称。
|
||||||
|
if (info.nickname) await uploadMiniWechatProfile({ nickname: info.nickname });
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { View, Text } from '@tarojs/components';
|
import { View, Text } from '@tarojs/components';
|
||||||
import Taro, { useDidShow } from '@tarojs/taro';
|
import Taro, { useDidShow } from '@tarojs/taro';
|
||||||
import PageShell from '../../components/PageShell';
|
import PageShell from '../../components/PageShell';
|
||||||
@@ -21,9 +21,9 @@ type Product = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AROMA_TABS = [
|
const AROMA_TABS = [
|
||||||
{ key: 'QINGXIANG', label: '清香型', open: true },
|
{ key: 'QINGXIANG', label: '清香型' },
|
||||||
{ key: 'JIANGXIANG', label: '酱香型', open: false },
|
{ key: 'JIANGXIANG', label: '酱香型' },
|
||||||
{ key: 'NONGXIANG', label: '浓香型', open: false },
|
{ key: 'NONGXIANG', label: '浓香型' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
@@ -49,20 +49,26 @@ export default function HomePage() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [cityCode]);
|
}, [cityCode]);
|
||||||
|
|
||||||
function onAromaTabClick(key: string, open: boolean) {
|
const availableAromas = useMemo(
|
||||||
if (!open) {
|
() =>
|
||||||
toast('暂未开放');
|
AROMA_TABS.filter((item) =>
|
||||||
return;
|
products.some((product) => product.aromaType === item.key),
|
||||||
|
),
|
||||||
|
[products],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading || availableAromas.length === 0) return;
|
||||||
|
if (!availableAromas.some((item) => item.key === tab)) {
|
||||||
|
setTab(availableAromas[0].key);
|
||||||
}
|
}
|
||||||
setTab(key);
|
}, [availableAromas, loading, tab]);
|
||||||
}
|
|
||||||
|
|
||||||
function openProductDetail(id: string) {
|
function openProductDetail(id: string) {
|
||||||
Taro.navigateTo({ url: `/pages/product-detail/index?id=${id}` });
|
Taro.navigateTo({ url: `/pages/product-detail/index?id=${id}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered = products.filter((p) => p.aromaType === tab);
|
const filtered = products.filter((p) => p.aromaType === tab);
|
||||||
const onSale = tab === 'QINGXIANG';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell variant="tab" className="home-page no-tab-header">
|
<PageShell variant="tab" className="home-page no-tab-header">
|
||||||
@@ -70,11 +76,11 @@ export default function HomePage() {
|
|||||||
|
|
||||||
<View className="home-aroma-nav">
|
<View className="home-aroma-nav">
|
||||||
<View className="home-aroma-tabs">
|
<View className="home-aroma-tabs">
|
||||||
{AROMA_TABS.map((t) => (
|
{availableAromas.map((t) => (
|
||||||
<Text
|
<Text
|
||||||
key={t.key}
|
key={t.key}
|
||||||
className={`home-aroma-tab${tab === t.key ? ' home-aroma-tab--active' : ''}${!t.open ? ' home-aroma-tab--muted' : ''}`}
|
className={`home-aroma-tab${tab === t.key ? ' home-aroma-tab--active' : ''}`}
|
||||||
onClick={() => onAromaTabClick(t.key, t.open)}
|
onClick={() => setTab(t.key)}
|
||||||
>
|
>
|
||||||
{t.label}
|
{t.label}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -85,12 +91,10 @@ export default function HomePage() {
|
|||||||
|
|
||||||
<View className="home-product-list">
|
<View className="home-product-list">
|
||||||
{loading ? <View className="home-empty">加载中…</View> : null}
|
{loading ? <View className="home-empty">加载中…</View> : null}
|
||||||
{!loading && !onSale ? <View className="home-empty">该香型暂未上线,敬请期待</View> : null}
|
{!loading && products.length === 0 ? (
|
||||||
{!loading && onSale && filtered.length === 0 ? (
|
<View className="home-empty">当前城市暂无在售商品</View>
|
||||||
<View className="home-empty">暂无商品</View>
|
|
||||||
) : null}
|
) : null}
|
||||||
{!loading &&
|
{!loading &&
|
||||||
onSale &&
|
|
||||||
filtered.map((p) => {
|
filtered.map((p) => {
|
||||||
const images = getProductImages(p);
|
const images = getProductImages(p);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -150,6 +150,19 @@ export default function LoginPage() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cancelLogin() {
|
||||||
|
const pages = Taro.getCurrentPages();
|
||||||
|
if (pages.length > 1) {
|
||||||
|
Taro.navigateBack().catch(() => {
|
||||||
|
Taro.switchTab({ url: '/pages/home/index' });
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Taro.switchTab({ url: '/pages/home/index' }).catch(() => {
|
||||||
|
Taro.reLaunch({ url: '/pages/home/index' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function applySessionAndLeave(
|
function applySessionAndLeave(
|
||||||
data: SessionPayload | WechatLoginResult,
|
data: SessionPayload | WechatLoginResult,
|
||||||
phoneValue?: string,
|
phoneValue?: string,
|
||||||
@@ -370,6 +383,12 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell variant="plain" className="login-page">
|
<PageShell variant="plain" className="login-page">
|
||||||
|
<View className="login-nav">
|
||||||
|
<View className="login-nav-back" onClick={cancelLogin}>
|
||||||
|
<Text className="login-nav-back-icon">‹</Text>
|
||||||
|
<Text>返回</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
<View className="login-header">
|
<View className="login-header">
|
||||||
<View className="login-logo-wrap">
|
<View className="login-logo-wrap">
|
||||||
<View className="login-logo">
|
<View className="login-logo">
|
||||||
@@ -523,6 +542,11 @@ export default function LoginPage() {
|
|||||||
<WechatLoginButton loading={wxLoading} label="授权登录" onClick={() => void wechatLogin()} />
|
<WechatLoginButton loading={wxLoading} label="授权登录" onClick={() => void wechatLogin()} />
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<View className="login-cancel-btn" onClick={cancelLogin}>
|
||||||
|
<Text>暂不登录,继续浏览</Text>
|
||||||
|
</View>
|
||||||
|
<Text className="login-cancel-hint">无需登录也可浏览商品和门店</Text>
|
||||||
</View>
|
</View>
|
||||||
</PageShell>
|
</PageShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,11 +50,13 @@ export default function MinePage() {
|
|||||||
const [draftAvatarUrl, setDraftAvatarUrl] = useState('');
|
const [draftAvatarUrl, setDraftAvatarUrl] = useState('');
|
||||||
const [draftNickname, setDraftNickname] = useState('');
|
const [draftNickname, setDraftNickname] = useState('');
|
||||||
const [savingProfile, setSavingProfile] = useState(false);
|
const [savingProfile, setSavingProfile] = useState(false);
|
||||||
|
const [profileLoadError, setProfileLoadError] = useState('');
|
||||||
|
|
||||||
function resetGuestState() {
|
function resetGuestState() {
|
||||||
setProfile(null);
|
setProfile(null);
|
||||||
setBenefitBalance(0);
|
setBenefitBalance(0);
|
||||||
setOrderCounts({});
|
setOrderCounts({});
|
||||||
|
setProfileLoadError('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyProfile(me: UserProfile) {
|
function applyProfile(me: UserProfile) {
|
||||||
@@ -63,6 +65,7 @@ export default function MinePage() {
|
|||||||
|
|
||||||
function loadProfile() {
|
function loadProfile() {
|
||||||
if (!isLoggedIn()) return;
|
if (!isLoggedIn()) return;
|
||||||
|
setProfileLoadError('');
|
||||||
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(() => []),
|
||||||
@@ -83,7 +86,16 @@ export default function MinePage() {
|
|||||||
});
|
});
|
||||||
setOrderCounts(counts);
|
setOrderCounts(counts);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch((error) => {
|
||||||
|
if (!isLoggedIn()) {
|
||||||
|
setAuthed(false);
|
||||||
|
resetGuestState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const message = error instanceof Error ? error.message : '个人资料加载失败';
|
||||||
|
setProfileLoadError(message);
|
||||||
|
toast('个人资料加载失败,请点击重试');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useDidShow(() => {
|
useDidShow(() => {
|
||||||
@@ -162,22 +174,20 @@ export default function MinePage() {
|
|||||||
|
|
||||||
/** 点击头像:绑定账号后用 chooseAvatar + nickname 获取头像昵称 */
|
/** 点击头像:绑定账号后用 chooseAvatar + nickname 获取头像昵称 */
|
||||||
async function handleAvatarTap() {
|
async function handleAvatarTap() {
|
||||||
|
if (bindingWx || savingProfile) return;
|
||||||
if (!isLoggedIn()) {
|
if (!isLoggedIn()) {
|
||||||
goLogin('/pages/mine/index');
|
goLogin('/pages/mine/index');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isWeapp) {
|
if (isWeapp) {
|
||||||
if (!profile?.hasWechat) {
|
openProfileSheet();
|
||||||
const ok = await ensureWechatBound();
|
|
||||||
if (ok) loadProfile();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!profile?.hasWechat) {
|
if (!profile?.hasWechat) {
|
||||||
const ok = await ensureWechatBound();
|
const ok = await ensureWechatBound();
|
||||||
if (!ok) return;
|
if (ok) loadProfile();
|
||||||
}
|
}
|
||||||
openProfileSheet();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onChooseAvatar(e: { detail?: { avatarUrl?: string } }) {
|
async function onChooseAvatar(e: { detail?: { avatarUrl?: string } }) {
|
||||||
@@ -202,16 +212,18 @@ export default function MinePage() {
|
|||||||
}
|
}
|
||||||
setSavingProfile(true);
|
setSavingProfile(true);
|
||||||
try {
|
try {
|
||||||
if (!profile?.hasWechat) {
|
|
||||||
const ok = await ensureWechatBound();
|
|
||||||
if (!ok) return;
|
|
||||||
}
|
|
||||||
let avatarUrl = draftAvatarUrl;
|
let avatarUrl = draftAvatarUrl;
|
||||||
|
let avatarResourceId: string | undefined;
|
||||||
if (draftAvatarTemp) {
|
if (draftAvatarTemp) {
|
||||||
avatarUrl = await uploadAvatarTempFile(draftAvatarTemp);
|
const uploaded = await uploadAvatarTempFile(draftAvatarTemp);
|
||||||
|
avatarUrl = uploaded.url;
|
||||||
|
avatarResourceId = uploaded.resourceId;
|
||||||
}
|
}
|
||||||
const updated = await uploadMiniWechatProfile({ nickname, avatarUrl });
|
const updated = await uploadMiniWechatProfile({
|
||||||
if (updated) applyProfile({ ...updated, hasWechat: true });
|
nickname,
|
||||||
|
...(avatarResourceId ? { avatarUrl, avatarResourceId } : {}),
|
||||||
|
});
|
||||||
|
if (updated) applyProfile(updated);
|
||||||
setProfileSheetOpen(false);
|
setProfileSheetOpen(false);
|
||||||
toast('头像昵称已更新', 'success');
|
toast('头像昵称已更新', 'success');
|
||||||
loadProfile();
|
loadProfile();
|
||||||
@@ -262,7 +274,9 @@ export default function MinePage() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View className="mine-login-gate">
|
<View className="mine-login-gate">
|
||||||
<View className="mine-login-gate-hint">登录后管理订单与个人信息</View>
|
<View className="mine-login-gate-hint">
|
||||||
|
登录后管理订单与个人信息;无需登录也可浏览商品和门店
|
||||||
|
</View>
|
||||||
<View className="mine-login-btn" onClick={() => goLogin('/pages/mine/index')}>
|
<View className="mine-login-btn" onClick={() => goLogin('/pages/mine/index')}>
|
||||||
<Text>去登录</Text>
|
<Text>去登录</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -283,9 +297,10 @@ export default function MinePage() {
|
|||||||
}
|
}
|
||||||
const nickname = display.nickname || '用户';
|
const nickname = display.nickname || '用户';
|
||||||
const needProfileFill = isWeapp && needsWxProfileFill(display);
|
const needProfileFill = isWeapp && needsWxProfileFill(display);
|
||||||
|
const avatarProfileReady = isWeapp ? !needProfileFill : hasWechat;
|
||||||
const memberLabel = needProfileFill
|
const memberLabel = needProfileFill
|
||||||
? '点击头像完善资料'
|
? '点击头像完善资料'
|
||||||
: hasWechat
|
: isWeapp || hasWechat
|
||||||
? '好客会员'
|
? '好客会员'
|
||||||
: canWxAuth
|
: canWxAuth
|
||||||
? '点击头像授权'
|
? '点击头像授权'
|
||||||
@@ -305,7 +320,7 @@ export default function MinePage() {
|
|||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
className={`mine-avatar${
|
className={`mine-avatar${
|
||||||
!needProfileFill && hasWechat ? ' mine-avatar--wx-ok' : ' mine-avatar--wx-pending'
|
avatarProfileReady ? ' mine-avatar--wx-ok' : ' mine-avatar--wx-pending'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{renderAvatarContent(display.avatarUrl)}
|
{renderAvatarContent(display.avatarUrl)}
|
||||||
@@ -313,13 +328,11 @@ export default function MinePage() {
|
|||||||
{avatarClickable ? (
|
{avatarClickable ? (
|
||||||
<View
|
<View
|
||||||
className={`mine-avatar-status${
|
className={`mine-avatar-status${
|
||||||
needProfileFill || !hasWechat
|
avatarProfileReady ? ' mine-avatar-status--ok' : ' mine-avatar-status--pending'
|
||||||
? ' mine-avatar-status--pending'
|
|
||||||
: ' mine-avatar-status--ok'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Text>
|
<Text>
|
||||||
{bindingWx ? '授权中' : needProfileFill || !hasWechat ? '去完善' : '更换'}
|
{bindingWx ? '授权中' : avatarProfileReady ? '更换' : '去完善'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -330,9 +343,20 @@ export default function MinePage() {
|
|||||||
onClick={avatarClickable ? () => void handleAvatarTap() : undefined}
|
onClick={avatarClickable ? () => void handleAvatarTap() : undefined}
|
||||||
>
|
>
|
||||||
<Text className="mine-profile-name">{nickname}</Text>
|
<Text className="mine-profile-name">{nickname}</Text>
|
||||||
<Text className={`mine-member-tag${hasWechat && !needProfileFill ? ' mine-member-tag--wechat' : ''}`}>
|
<Text className={`mine-member-tag${avatarProfileReady ? ' mine-member-tag--wechat' : ''}`}>
|
||||||
{memberLabel}
|
{memberLabel}
|
||||||
</Text>
|
</Text>
|
||||||
|
{profileLoadError ? (
|
||||||
|
<Text
|
||||||
|
className="mine-profile-retry"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
loadProfile();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
资料加载失败,点击重试
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -81,10 +81,6 @@
|
|||||||
border-bottom-color: var(--color-heritage-red);
|
border-bottom-color: var(--color-heritage-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-aroma-tab--muted {
|
|
||||||
opacity: 0.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-product-list {
|
.home-product-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -326,3 +326,51 @@
|
|||||||
.login-agreement-link {
|
.login-agreement-link {
|
||||||
color: var(--color-heritage-red);
|
color: var(--color-heritage-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-nav {
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 8px var(--space-page) 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-nav-back {
|
||||||
|
min-width: 72px;
|
||||||
|
min-height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-nav-back-icon {
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-cancel-btn {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 48px;
|
||||||
|
margin-top: 20px;
|
||||||
|
border: 1px solid var(--color-heritage-red);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--color-heritage-red);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-cancel-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-on-surface-variant);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -149,6 +149,14 @@
|
|||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mine-profile-retry {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.mine-main {
|
.mine-main {
|
||||||
margin-top: -28px;
|
margin-top: -28px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { ClientConfigController } from './client-config.controller';
|
|||||||
import { WechatLocationService } from './wechat-location.service';
|
import { WechatLocationService } from './wechat-location.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [IamModule, IntegrationsModule, SystemConfigModule, forwardRef(() => AnalyticsModule)],
|
imports: [forwardRef(() => IamModule), IntegrationsModule, SystemConfigModule, forwardRef(() => AnalyticsModule)],
|
||||||
controllers: [
|
controllers: [
|
||||||
ResourceController,
|
ResourceController,
|
||||||
EventController,
|
EventController,
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ export class ResourceService {
|
|||||||
});
|
});
|
||||||
throw new BadRequestException(message);
|
throw new BadRequestException(message);
|
||||||
}
|
}
|
||||||
|
if (dto.bizType === 'AVATAR' && !file.mimetype?.startsWith('image/')) {
|
||||||
|
throw new BadRequestException('头像仅支持图片文件');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.oss.putObject({
|
const result = await this.oss.putObject({
|
||||||
@@ -125,6 +128,24 @@ export class ResourceService {
|
|||||||
externalNo: result.ossKey,
|
externalNo: result.ossKey,
|
||||||
status: 'SUCCESS',
|
status: 'SUCCESS',
|
||||||
});
|
});
|
||||||
|
if (actor?.refType === 'USER' && dto.bizType === 'AVATAR' && dto.mediaType === 'IMAGE') {
|
||||||
|
const resource = await this.prisma.commonResource.create({
|
||||||
|
data: {
|
||||||
|
ownerType: 'USER',
|
||||||
|
ownerId: actor.refId,
|
||||||
|
bizType: 'AVATAR',
|
||||||
|
mediaType: 'IMAGE',
|
||||||
|
ossBucket: result.bucket,
|
||||||
|
ossKey: result.ossKey,
|
||||||
|
url: result.url,
|
||||||
|
fileName: file.originalname || 'avatar',
|
||||||
|
fileSize: BigInt(file.size),
|
||||||
|
mimeType: file.mimetype,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return serializeBigInt({ ...result, resourceId: resource.id });
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await logOssUpload(this.prisma, {
|
await logOssUpload(this.prisma, {
|
||||||
@@ -138,6 +159,37 @@ export class ResourceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOwnedActiveAvatar(resourceId: bigint, userId: bigint) {
|
||||||
|
const resource = await this.prisma.commonResource.findFirst({
|
||||||
|
where: {
|
||||||
|
id: resourceId,
|
||||||
|
ownerType: 'USER',
|
||||||
|
ownerId: userId,
|
||||||
|
bizType: 'AVATAR',
|
||||||
|
mediaType: 'IMAGE',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!resource) throw new BadRequestException('头像资源无效或不属于当前用户');
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOwnedActiveAvatarByUrl(url: string, userId: bigint) {
|
||||||
|
const resource = await this.prisma.commonResource.findFirst({
|
||||||
|
where: {
|
||||||
|
url,
|
||||||
|
ownerType: 'USER',
|
||||||
|
ownerId: userId,
|
||||||
|
bizType: 'AVATAR',
|
||||||
|
mediaType: 'IMAGE',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
});
|
||||||
|
if (!resource) throw new BadRequestException('头像资源无效或不属于当前用户');
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
async register(dto: RegisterResourceDto) {
|
async register(dto: RegisterResourceDto) {
|
||||||
const resource = await this.prisma.commonResource.create({
|
const resource = await this.prisma.commonResource.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto';
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
|
forwardRef,
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
@@ -22,6 +23,7 @@ import { serializeBigInt } from '../../common/decorators/current-user.decorator'
|
|||||||
import { verifyPassword } from '../../common/crypto/password.util';
|
import { verifyPassword } from '../../common/crypto/password.util';
|
||||||
import { AnalyticsService } from '../analytics/analytics.service';
|
import { AnalyticsService } from '../analytics/analytics.service';
|
||||||
import { UserAddressService } from './user-address.service';
|
import { UserAddressService } from './user-address.service';
|
||||||
|
import { ResourceService } from '../common/resource.service';
|
||||||
|
|
||||||
import type { User } from '@prisma/client';
|
import type { User } from '@prisma/client';
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ export class AuthService {
|
|||||||
private readonly analyticsService: AnalyticsService,
|
private readonly analyticsService: AnalyticsService,
|
||||||
private readonly smsCodeStore: SmsCodeStore,
|
private readonly smsCodeStore: SmsCodeStore,
|
||||||
private readonly userAddressService: UserAddressService,
|
private readonly userAddressService: UserAddressService,
|
||||||
|
@Inject(forwardRef(() => ResourceService)) private readonly resourceService: ResourceService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private assertMobilePhone(phone: string) {
|
private assertMobilePhone(phone: string) {
|
||||||
@@ -1407,12 +1410,9 @@ export class AuthService {
|
|||||||
|
|
||||||
async updateMiniWechatProfile(
|
async updateMiniWechatProfile(
|
||||||
userId: bigint,
|
userId: bigint,
|
||||||
input: { nickname?: string; avatarUrl?: string },
|
input: { nickname?: string; avatarUrl?: string; avatarResourceId?: string },
|
||||||
) {
|
) {
|
||||||
const user = await this.assertActiveUser(userId);
|
const user = await this.assertActiveUser(userId);
|
||||||
if (!user.wxOpenId) {
|
|
||||||
throw new BadRequestException('请先完成微信授权');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: {
|
const data: {
|
||||||
nickname?: string;
|
nickname?: string;
|
||||||
@@ -1425,37 +1425,23 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const avatarUrl = input.avatarUrl?.trim();
|
const avatarUrl = input.avatarUrl?.trim();
|
||||||
if (avatarUrl) {
|
const avatarResourceId = input.avatarResourceId?.trim();
|
||||||
if (user.avatarResourceId) {
|
if (avatarResourceId) {
|
||||||
await this.prisma.commonResource.update({
|
let resourceId: bigint;
|
||||||
where: { id: user.avatarResourceId },
|
try {
|
||||||
data: { url: avatarUrl },
|
resourceId = BigInt(avatarResourceId);
|
||||||
});
|
} catch {
|
||||||
} else {
|
throw new BadRequestException('头像资源编号无效');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
const avatar = await this.resourceService.getOwnedActiveAvatar(resourceId, userId);
|
||||||
|
data.avatarResourceId = avatar.id;
|
||||||
|
} else if (avatarUrl) {
|
||||||
|
// 兼容已发布旧客户端:只接受刚由当前用户上传并登记过的真实资源 URL。
|
||||||
|
const avatar = await this.resourceService.getOwnedActiveAvatarByUrl(avatarUrl, userId);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,10 @@ export class MiniWechatProfileDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
avatarResourceId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CheckPartnerPhoneDto {
|
export class CheckPartnerPhoneDto {
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
|
|||||||
import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
|
import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
|
||||||
import { ShopStoreGuard } from '../../common/guards/shop-store.guard';
|
import { ShopStoreGuard } from '../../common/guards/shop-store.guard';
|
||||||
import { StoreMembershipService } from '../../common/guards/store-membership.service';
|
import { StoreMembershipService } from '../../common/guards/store-membership.service';
|
||||||
|
import { CommonModule } from '../common/common.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
IntegrationsModule,
|
IntegrationsModule,
|
||||||
|
forwardRef(() => CommonModule),
|
||||||
forwardRef(() => AnalyticsModule),
|
forwardRef(() => AnalyticsModule),
|
||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
secret: process.env.JWT_SECRET || 'dukang-prev1-dev-secret',
|
secret: process.env.JWT_SECRET || 'dukang-prev1-dev-secret',
|
||||||
|
|||||||
Reference in New Issue
Block a user