chore(deploy): stop tracking auto-release.env; feat(mini-user): avatar profile sheet
CI / verify (pull_request) Has been cancelled

Remove secrets file from the index (keep local). Mine page can choose avatar and nickname via WeChat components.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-21 10:31:07 +08:00
parent b8adfe98a3
commit 73dbf6effb
3 changed files with 281 additions and 31 deletions
+158 -16
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { View, Text, Image } from '@tarojs/components';
import { View, Text, Image, Button, Input } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import { isWxAuthorizeEnabled, type ClientRuntimeConfig } from '@dukang/shared-types';
import PageShell from '../../components/PageShell';
@@ -10,8 +10,11 @@ import { goLogin } from '../../lib/auth-nav';
import { bindWechatForUser } from '../../lib/wechat-auth';
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';
@@ -42,6 +45,11 @@ export default function MinePage() {
const [orderCounts, setOrderCounts] = useState<Record<string, number>>({});
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);
function resetGuestState() {
setProfile(null);
@@ -135,22 +143,83 @@ export default function MinePage() {
}
return false;
} catch (e) {
toast(e instanceof Error ? e.message : '微信授权失败');
toast(e instanceof Error ? e.message : '授权失败');
return false;
} finally {
setBindingWx(false);
}
}
/** 未绑定微信时点击头像走授权(小程序 openId / H5 公众号 OAuth */
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 (!isLoggedIn()) {
goLogin('/pages/mine/index');
return;
}
if (profile?.hasWechat) return;
await ensureWechatBound();
loadProfile();
if (!isWeapp) {
if (!profile?.hasWechat) {
const ok = await ensureWechatBound();
if (ok) loadProfile();
}
return;
}
if (!profile?.hasWechat) {
const ok = await ensureWechatBound();
if (!ok) return;
}
openProfileSheet();
}
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 {
if (!profile?.hasWechat) {
const ok = await ensureWechatBound();
if (!ok) return;
}
let avatarUrl = draftAvatarUrl;
if (draftAvatarTemp) {
avatarUrl = await uploadAvatarTempFile(draftAvatarTemp);
}
const updated = await uploadMiniWechatProfile({ nickname, avatarUrl });
if (updated) applyProfile({ ...updated, hasWechat: true });
setProfileSheetOpen(false);
toast('头像昵称已更新', 'success');
loadProfile();
} catch (err) {
toast(err instanceof Error ? err.message : '保存失败');
} finally {
setSavingProfile(false);
}
}
function handleService(item: (typeof SERVICES)[number]) {
@@ -213,8 +282,16 @@ export default function MinePage() {
throw new Error('wx profile helpers missing');
}
const nickname = display.nickname || '用户';
const memberLabel = hasWechat ? '好客会员' : canWxAuth ? '未完成授权' : '未完成授权';
void needsWxProfileFill(display);
const needProfileFill = isWeapp && needsWxProfileFill(display);
const memberLabel = needProfileFill
? '点击头像完善资料'
: hasWechat
? '好客会员'
: canWxAuth
? '点击头像授权'
: '好客会员';
const avatarClickable = isWeapp || (!hasWechat && canWxAuth);
const previewAvatar = draftAvatarUrl || display.avatarUrl;
return (
<PageShell variant="tab" className="mine-page no-tab-header">
@@ -223,24 +300,37 @@ export default function MinePage() {
<View className="mine-header-texture" />
<View className="mine-profile">
<View
className={`mine-avatar-wrap${!hasWechat && canWxAuth ? ' mine-avatar-wrap--action' : ''}`}
onClick={!hasWechat && canWxAuth ? () => void handleAvatarTap() : undefined}
className={`mine-avatar-wrap${avatarClickable ? ' mine-avatar-wrap--action' : ''}`}
onClick={avatarClickable ? () => void handleAvatarTap() : undefined}
>
<View
className={`mine-avatar${hasWechat ? ' mine-avatar--wx-ok' : ' mine-avatar--wx-pending'}`}
className={`mine-avatar${
!needProfileFill && hasWechat ? ' mine-avatar--wx-ok' : ' mine-avatar--wx-pending'
}`}
>
{renderAvatarContent(display.avatarUrl)}
</View>
{!hasWechat && canWxAuth ? (
<View className="mine-avatar-status mine-avatar-status--pending">
<Text>{bindingWx ? '授权中' : '去授权'}</Text>
{avatarClickable ? (
<View
className={`mine-avatar-status${
needProfileFill || !hasWechat
? ' mine-avatar-status--pending'
: ' mine-avatar-status--ok'
}`}
>
<Text>
{bindingWx ? '授权中' : needProfileFill || !hasWechat ? '去完善' : '更换'}
</Text>
</View>
) : null}
</View>
<View className="mine-profile-meta">
<View
className="mine-profile-meta"
onClick={avatarClickable ? () => void handleAvatarTap() : undefined}
>
<Text className="mine-profile-name">{nickname}</Text>
<Text className={`mine-member-tag${hasWechat ? ' mine-member-tag--wechat' : ''}`}>
<Text className={`mine-member-tag${hasWechat && !needProfileFill ? ' mine-member-tag--wechat' : ''}`}>
{memberLabel}
</Text>
</View>
@@ -338,6 +428,58 @@ export default function MinePage() {
</View>
{shouldRenderPageTabBar() ? <UserTabBar selected={3} /> : null}
{profileSheetOpen ? (
<View className="mine-profile-sheet-mask" onClick={() => !savingProfile && setProfileSheetOpen(false)}>
<View className="mine-profile-sheet" onClick={(e) => e.stopPropagation()}>
<Text className="mine-profile-sheet-title"></Text>
<Text className="mine-profile-sheet-hint">
</Text>
<Button
className="mine-profile-avatar-btn"
openType="chooseAvatar"
hoverClass="none"
onChooseAvatar={onChooseAvatar}
>
<View className="mine-profile-avatar-preview">
{previewAvatar ? (
<Image className="mine-avatar-img" src={previewAvatar} mode="aspectFill" />
) : (
<Image className="mine-avatar-img" src={BRAND_LOGO_MARK_URL} mode="aspectFit" />
)}
</View>
<Text className="mine-profile-avatar-tip"></Text>
</Button>
<View className="mine-profile-nickname-wrap">
<Text className="mine-profile-nickname-label"></Text>
<Input
className="mine-profile-nickname-input"
type="nickname"
maxlength={32}
placeholder="点击填写昵称"
value={draftNickname}
onInput={(e) => setDraftNickname(e.detail.value)}
onBlur={(e) => setDraftNickname(e.detail.value.trim())}
/>
</View>
<View className="mine-profile-sheet-actions">
<View
className="mine-profile-sheet-cancel"
onClick={() => !savingProfile && setProfileSheetOpen(false)}
>
<Text></Text>
</View>
<View
className={`mine-profile-sheet-save${savingProfile ? ' is-disabled' : ''}`}
onClick={savingProfile ? undefined : () => void saveWxProfile()}
>
<Text>{savingProfile ? '保存中...' : '保存'}</Text>
</View>
</View>
</View>
</View>
) : null}
</PageShell>
);
}