import { useEffect, useMemo, useState } from 'react'; import { View, Text, Image, Button, Input, ScrollView } from '@tarojs/components'; import Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimeline } from '@tarojs/taro'; import { isWxAuthorizeEnabled, type ClientRuntimeConfig, } from '@dukang/shared-types'; import PageShell from '../../components/PageShell'; import TabMainHeader from '../../components/TabMainHeader'; import WechatShareReady from '../../components/WechatShareReady'; import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar'; import BenefitFigure from '../../components/BenefitFigure'; import { goLogin } from '../../lib/auth-nav'; import { bindWechatForUser } from '../../lib/wechat-auth'; import { applyBrandFromClientConfig, getBrandAssetsSync, loadBrandAssets, } from '../../lib/brand-assets'; import { fetchMiniWechatUserInfo, isDefaultMiniNickname, mergeWxDisplayProfile, needsWxProfileFill, uploadAvatarTempFile, uploadMiniWechatProfile, } from '../../lib/mini-wechat-profile'; import { isLoggedIn, logout, request, toast, type UserProfile } from '../../lib/api'; import { isWechatEnv } from '../../lib/weixin'; import { APP_VERSION_LABEL } from '../../lib/client-version'; import { maskPhone } from '../../lib/phone'; import { applyShareFromClientConfig, buildSceneSharePayload, toWeappShareMessage, toWeappShareTimeline, } from '../../lib/wechat-share'; import iconPendingPay from '../../assets/icons/待付款.png'; import iconPaid from '../../assets/icons/已付款.png'; import iconCompleted from '../../assets/icons/已完成.png'; import iconAddress from '../../assets/icons/地址管理.png'; import iconStores from '../../assets/icons/可用门店.png'; import iconCs from '../../assets/icons/联系客服.png'; import iconQualification from '../../assets/icons/资质公示.png'; import iconAbout from '../../assets/icons/关于我们.png'; import { formatMoney } from '../../lib/money'; const ORDER_SHORTCUTS = [ { tab: 'pending_pay', icon: iconPendingPay, label: '待付款' }, { tab: 'paid', icon: iconPaid, label: '已付款' }, { tab: 'completed', icon: iconCompleted, label: '已完成' }, ] as const; const SERVICES = [ { icon: iconAddress, label: '地址管理', url: '/pages/addresses/index' }, { icon: iconStores, label: '可用门店', tab: '/pages/stores/index' }, { icon: iconCs, label: '联系客服', url: '/pages/customer-service/index' }, { icon: iconQualification, label: '资质公示', action: 'qualification' as const }, { icon: iconAbout, label: '关于我们', action: 'about' as const }, { icon: iconAbout, label: '发票管理', url: '/pages/invoice-titles/index' }, ] as const; const isWeapp = process.env.TARO_ENV === 'weapp'; export default function MinePage() { const [authed, setAuthed] = useState(() => isLoggedIn()); const [profile, setProfile] = useState(null); const [benefitBalance, setBenefitBalance] = useState(0); const [orderCounts, setOrderCounts] = useState>({}); const [wxAuthorize, setWxAuthorize] = useState(true); const [bindingWx, setBindingWx] = useState(false); const [profileSheetOpen, setProfileSheetOpen] = useState(false); const [draftAvatarTemp, setDraftAvatarTemp] = useState(''); const [draftAvatarUrl, setDraftAvatarUrl] = useState(''); const [draftNickname, setDraftNickname] = useState(''); const [savingProfile, setSavingProfile] = useState(false); const [profileLoadError, setProfileLoadError] = useState(''); const [qualificationOpen, setQualificationOpen] = useState(false); const [brandMarkUrl, setBrandMarkUrl] = useState(() => getBrandAssetsSync().brandLogoMarkUrl); const [qualificationUrl, setQualificationUrl] = useState( () => getBrandAssetsSync().qualificationDisclosureUrl, ); const [shareTick, setShareTick] = useState(0); function resetGuestState() { setProfile(null); setBenefitBalance(0); setOrderCounts({}); setProfileLoadError(''); } function applyProfile(me: UserProfile) { setProfile(mergeWxDisplayProfile(me)); } function loadProfile() { if (!isLoggedIn()) return Promise.resolve(); setProfileLoadError(''); return Promise.all([ request('/auth/me'), request>>('/benefit/coupons').catch(() => []), ...ORDER_SHORTCUTS.map((s) => request<{ total: number }>(`/trade/orders?tab=${s.tab}&pageSize=1`).catch(() => ({ total: 0 })), ), ]) .then(([me, coupons, ...totals]) => { applyProfile(me); const balance = (coupons as Array>).reduce((sum, c) => { if (String(c.status) === 'ACTIVE') return sum + Number(c.balance || 0); return sum; }, 0); setBenefitBalance(balance); const counts: Record = {}; ORDER_SHORTCUTS.forEach((s, i) => { counts[s.tab] = (totals[i] as { total: number })?.total ?? 0; }); setOrderCounts(counts); }) .catch((error) => { if (!isLoggedIn()) { setAuthed(false); resetGuestState(); return; } const message = error instanceof Error ? error.message : '个人资料加载失败'; setProfileLoadError(message); toast('个人资料加载失败,请点击重试'); }); } useDidShow(() => { syncTabBarSelected(3); const loggedInNow = isLoggedIn(); setAuthed(loggedInNow); if (loggedInNow) { loadProfile(); } else { resetGuestState(); } }); usePullDownRefresh(() => { const loggedInNow = isLoggedIn(); setAuthed(loggedInNow); if (!loggedInNow) { resetGuestState(); Taro.stopPullDownRefresh(); return; } void loadProfile().finally(() => Taro.stopPullDownRefresh()); }); useEffect(() => { request('/common/client-config') .then((config) => { setWxAuthorize(isWxAuthorizeEnabled(config)); const brand = applyBrandFromClientConfig(config); applyShareFromClientConfig(config); setBrandMarkUrl(brand.brandLogoMarkUrl); setQualificationUrl(brand.qualificationDisclosureUrl); setShareTick((n) => n + 1); }) .catch(() => { setWxAuthorize(true); void loadBrandAssets(); }); }, []); const sharePayload = useMemo( () => buildSceneSharePayload('mine', { path: '/pages/mine/index', }), [shareTick], ); useShareAppMessage(() => toWeappShareMessage(sharePayload)); useShareTimeline(() => toWeappShareTimeline(sharePayload)); async function ensureWechatBound(): Promise { if (profile?.hasWechat) return true; if (!wxAuthorize) { toast('当前环境未开启微信授权'); return false; } setBindingWx(true); try { if (!isWeapp) { if (!isWechatEnv()) { toast('请在微信内打开后授权'); return false; } const result = await bindWechatForUser(); if (!result.ok && 'redirecting' in result && result.redirecting) return false; if (!result.ok && 'needBindPhone' in result && result.needBindPhone) { goLogin('/pages/mine/index', { bindMode: '1', wxSessionKey: result.wxSessionKey, }); return false; } if (result.ok && result.profile) { applyProfile({ ...result.profile, hasWechat: true }); return true; } return false; } const result = await bindWechatForUser(null); if (!result.ok && 'needBindPhone' in result && result.needBindPhone) { goLogin('/pages/mine/index', { bindMode: '1', wxSessionKey: result.wxSessionKey }); return false; } if (result.ok && result.profile) { applyProfile({ ...result.profile, hasWechat: true }); return true; } return false; } catch (e) { toast(e instanceof Error ? e.message : '授权失败'); return false; } finally { setBindingWx(false); } } function openProfileSheet(me?: UserProfile | null) { const base = mergeWxDisplayProfile( me || profile || { id: '', nickname: null, avatarUrl: null, hasWechat: false }, ); setDraftAvatarTemp(''); setDraftAvatarUrl(base.avatarUrl || ''); setDraftNickname(isDefaultMiniNickname(base.nickname) ? '' : base.nickname || ''); setProfileSheetOpen(true); } /** 点击头像:绑定账号后用 chooseAvatar + nickname 获取头像昵称 */ async function handleAvatarTap() { if (bindingWx || savingProfile) { toast(savingProfile ? '资料保存中…' : '请稍候…'); return; } if (!isLoggedIn()) { goLogin('/pages/mine/index'); return; } if (isWeapp) { openProfileSheet(); return; } if (!profile?.hasWechat) { const ok = await ensureWechatBound(); if (ok) loadProfile(); } return; } async function onChooseAvatar(e: { detail?: { avatarUrl?: string } }) { const tempPath = e.detail?.avatarUrl?.trim(); if (!tempPath) { toast('未获取到头像,请重试'); return; } setDraftAvatarTemp(tempPath); setDraftAvatarUrl(tempPath); } async function saveWxProfile() { const nickname = draftNickname.trim(); if (!nickname) { toast('请填写昵称'); return; } if (!draftAvatarTemp && !draftAvatarUrl) { toast('请选择头像'); return; } setSavingProfile(true); try { let avatarUrl = draftAvatarUrl; let avatarResourceId: string | undefined; if (draftAvatarTemp) { const uploaded = await uploadAvatarTempFile(draftAvatarTemp); avatarUrl = uploaded.url; avatarResourceId = uploaded.resourceId; } const updated = await uploadMiniWechatProfile({ nickname, ...(avatarResourceId ? { avatarUrl, avatarResourceId } : {}), }); if (updated) applyProfile(updated); setProfileSheetOpen(false); toast('头像昵称已更新', 'success'); loadProfile(); } catch (err) { toast(err instanceof Error ? err.message : '保存失败'); } finally { setSavingProfile(false); } } function handleService(item: (typeof SERVICES)[number]) { if ('url' in item && item.url) { Taro.navigateTo({ url: item.url }); return; } if ('tab' in item && item.tab) { Taro.switchTab({ url: item.tab }); return; } if ('action' in item && item.action === 'qualification') { setQualificationOpen(true); return; } if ('action' in item && item.action === 'about') { toast('杜康好客 · 传承千年酒文化'); } } function renderAvatarContent(displayAvatarUrl: string | null) { if (displayAvatarUrl) { return ; } return ; } if (!authed) { return ( goLogin('/pages/mine/index')}> 未登录 点击头像登录 登录后管理订单与个人信息;无需登录也可浏览商品和门店 goLogin('/pages/mine/index')}> 去登录 {shouldRenderPageTabBar() ? : null} ); } const hasWechat = !!profile?.hasWechat; const canWxAuth = wxAuthorize && (isWeapp || isWechatEnv()); const display = mergeWxDisplayProfile( profile || { id: '', nickname: null, avatarUrl: null, hasWechat: false }, ); // 强制保留 common 导出,避免开发者工具「旧页 + 新 common」混用时报 is not a function if (typeof needsWxProfileFill !== 'function' || typeof fetchMiniWechatUserInfo !== 'function') { throw new Error('wx profile helpers missing'); } const nickname = display.nickname || '用户'; const needProfileFill = isWeapp && needsWxProfileFill(display); const avatarProfileReady = isWeapp ? !needProfileFill : hasWechat; const maskedPhone = profile?.phone ? maskPhone(String(profile.phone)) : ''; // 昵称下优先展示脱敏手机号;无手机号时再提示完善资料/授权 const memberLabel = maskedPhone || (needProfileFill ? '点击头像完善资料' : !isWeapp && !hasWechat && canWxAuth ? '点击头像授权' : '未绑定手机'); const avatarClickable = isWeapp || (!hasWechat && canWxAuth); const previewAvatar = draftAvatarUrl || display.avatarUrl; return ( void handleAvatarTap() : undefined} > {renderAvatarContent(display.avatarUrl)} {avatarClickable ? ( {bindingWx ? '授权中' : avatarProfileReady ? '更换' : '去完善'} ) : null} void handleAvatarTap() : undefined} > {nickname} {memberLabel} {profileLoadError ? ( { event.stopPropagation(); loadProfile(); }} > 资料加载失败,点击重试 ) : null} 我的资产 Taro.navigateTo({ url: '/pages/benefit-detail/index' })} > 查看明细 › 好客权益余额 Taro.navigateTo({ url: '/pages/redeem/index' })} > 去使用 我的订单 Taro.navigateTo({ url: '/pages/orders/index' })} > 全部订单 › {ORDER_SHORTCUTS.map((item) => { const count = orderCounts[item.tab] ?? 0; return ( Taro.navigateTo({ url: `/pages/orders/index?tab=${item.tab}` }) } > {count > 0 ? ( {count > 99 ? '99+' : count} ) : null} {item.label} ); })} 常用服务 {SERVICES.map((item) => ( handleService(item)} > {item.label} ))} 杜康好客 {APP_VERSION_LABEL} logout()}> 退出登录 {shouldRenderPageTabBar() ? : null} {profileSheetOpen ? ( {/* 遮罩单独绑 tap,勿在含 chooseAvatar 的祖先上用 stopPropagation(会编译成 catchtap 导致选头像无反应) */} !savingProfile && setProfileSheetOpen(false)} /> 完善头像与昵称 点击头像选择,并填写昵称后保存(用于个人中心展示) 昵称 setDraftNickname(e.detail.value)} onBlur={(e) => setDraftNickname(e.detail.value.trim())} /> !savingProfile && setProfileSheetOpen(false)} > 取消 { if (!savingProfile) void saveWxProfile(); }} > {savingProfile ? '保存中…' : '保存'} ) : null} {qualificationOpen ? ( setQualificationOpen(false)} > 点击任意处关闭 ) : null} ); }