@@ -0,0 +1,63 @@
|
||||
import { Button, Text, View } from '@tarojs/components';
|
||||
|
||||
type PhoneQuickLoginButtonProps = {
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
/** 须已主动勾选协议后才挂载 getPhoneNumber,避免未同意即拉起授权 */
|
||||
agreed: boolean;
|
||||
onRequireAgree: () => void;
|
||||
onGetPhoneNumber: (phoneCode: string) => void;
|
||||
onFail?: (message: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* 小程序手机号快捷登录(open-type=getPhoneNumber)。
|
||||
* 文案不得使用「微信」字样或仿官方图标,以符合审核要求。
|
||||
*/
|
||||
export default function PhoneQuickLoginButton({
|
||||
loading = false,
|
||||
disabled = false,
|
||||
agreed,
|
||||
onRequireAgree,
|
||||
onGetPhoneNumber,
|
||||
onFail,
|
||||
}: PhoneQuickLoginButtonProps) {
|
||||
const inactive = loading || disabled;
|
||||
const className = `login-phone-quick-btn${inactive ? ' login-phone-quick-btn--disabled' : ''}`;
|
||||
const label = loading ? '登录中...' : '手机号快捷登录';
|
||||
|
||||
if (!agreed) {
|
||||
return (
|
||||
<View className={className} onClick={inactive ? undefined : onRequireAgree}>
|
||||
<Text className="login-phone-quick-btn__text">{label}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={className}
|
||||
openType={inactive ? undefined : 'getPhoneNumber'}
|
||||
hoverClass="none"
|
||||
onGetPhoneNumber={(e) => {
|
||||
if (inactive) return;
|
||||
const detail = e.detail as {
|
||||
errMsg?: string;
|
||||
code?: string;
|
||||
errno?: number;
|
||||
};
|
||||
if (!detail?.code) {
|
||||
const denied =
|
||||
detail?.errMsg?.includes('deny') ||
|
||||
detail?.errMsg?.includes('cancel') ||
|
||||
detail?.errno === 103;
|
||||
onFail?.(denied ? '已取消手机号授权' : detail?.errMsg || '获取手机号失败');
|
||||
return;
|
||||
}
|
||||
onGetPhoneNumber(detail.code);
|
||||
}}
|
||||
>
|
||||
<Text className="login-phone-quick-btn__text">{label}</Text>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -3,22 +3,16 @@ import { View, Text } from '@tarojs/components';
|
||||
type WechatLoginButtonProps = {
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
/** 默认「授权登录」,避免使用「微信」字样与官方风格图标 */
|
||||
label?: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
function WechatIcon() {
|
||||
return (
|
||||
<View className="wechat-login-icon" aria-hidden>
|
||||
<View className="wechat-login-icon__big" />
|
||||
<View className="wechat-login-icon__small" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/** 微信授权一键登录按钮(对齐 h5-user login-wechat-btn) */
|
||||
/** 授权登录按钮(无微信品牌元素,满足小程序审核) */
|
||||
export default function WechatLoginButton({
|
||||
loading = false,
|
||||
disabled = false,
|
||||
label = '授权登录',
|
||||
onClick,
|
||||
}: WechatLoginButtonProps) {
|
||||
const inactive = loading || disabled;
|
||||
@@ -28,10 +22,7 @@ export default function WechatLoginButton({
|
||||
className={`login-wechat-btn${inactive ? ' login-wechat-btn--disabled' : ''}`}
|
||||
onClick={inactive ? undefined : onClick}
|
||||
>
|
||||
<WechatIcon />
|
||||
<Text className="login-wechat-btn__text">
|
||||
{loading ? '授权中...' : '微信一键授权'}
|
||||
</Text>
|
||||
<Text className="login-wechat-btn__text">{loading ? '授权中...' : label}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@dukang/shared-types';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import WechatLoginButton from '../../components/WechatLoginButton';
|
||||
import PhoneQuickLoginButton from '../../components/PhoneQuickLoginButton';
|
||||
import { BRAND_LOGO_WIDE_URL } from '@dukang/shared-types';
|
||||
import { finishLoginNavigate, forceReloadAfterAccountMerge } from '../../lib/auth-nav';
|
||||
import {
|
||||
@@ -24,6 +25,8 @@ import {
|
||||
} from '../../lib/mini-wechat-profile';
|
||||
import { isLoggedIn, request, saveAuth, toast, type SessionPayload } from '../../lib/api';
|
||||
|
||||
const IS_WEAPP = process.env.TARO_ENV === 'weapp';
|
||||
|
||||
function normalizePhone(value: string) {
|
||||
return value.replace(/\D/g, '').slice(0, 11);
|
||||
}
|
||||
@@ -32,6 +35,44 @@ function isValidPhone(phone: string) {
|
||||
return /^1[3-9]\d{9}$/.test(phone);
|
||||
}
|
||||
|
||||
function AgreementRow({
|
||||
agreed,
|
||||
onToggle,
|
||||
}: {
|
||||
agreed: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
return (
|
||||
<View className="login-agreement" onClick={onToggle}>
|
||||
<View className={`login-agreement-check${agreed ? ' login-agreement-check--on' : ''}`}>
|
||||
{agreed ? <Text>✓</Text> : null}
|
||||
</View>
|
||||
<Text className="login-agreement-text">
|
||||
请阅读并勾选同意
|
||||
<Text
|
||||
className="login-agreement-link"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
Taro.navigateTo({ url: '/pages/user-agreement/index' });
|
||||
}}
|
||||
>
|
||||
《用户服务协议》
|
||||
</Text>
|
||||
和
|
||||
<Text
|
||||
className="login-agreement-link"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
Taro.navigateTo({ url: '/pages/privacy-policy/index' });
|
||||
}}
|
||||
>
|
||||
《隐私政策》
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const returnTo = router.params.return || '';
|
||||
@@ -44,8 +85,10 @@ export default function LoginPage() {
|
||||
const [code, setCode] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [wxLoading, setWxLoading] = useState(false);
|
||||
const [phoneQuickLoading, setPhoneQuickLoading] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [cooldown, setCooldown] = useState(0);
|
||||
/** 须用户主动勾选,禁止默认同意 */
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
const [sentHint, setSentHint] = useState('');
|
||||
@@ -53,6 +96,7 @@ export default function LoginPage() {
|
||||
const [wxSessionKey, setWxSessionKey] = useState<string | null>(initialWxSessionKey);
|
||||
const [wxAuthorize, setWxAuthorize] = useState(true);
|
||||
const [completeMode, setCompleteMode] = useState<'phone' | 'wechat' | null>(null);
|
||||
const [showSmsForm, setShowSmsForm] = useState(!IS_WEAPP);
|
||||
|
||||
useEffect(() => {
|
||||
request<ClientRuntimeConfig>('/common/client-config')
|
||||
@@ -65,7 +109,6 @@ export default function LoginPage() {
|
||||
setCompleteMode(null);
|
||||
return;
|
||||
}
|
||||
// 完善资料场景才拉 profile;普通登录勿抢跑 /auth/me,避免旧 token 401 与短信登录竞态
|
||||
if (!needPhone && !needWechat) {
|
||||
setCompleteMode(null);
|
||||
return;
|
||||
@@ -76,6 +119,7 @@ export default function LoginPage() {
|
||||
if (cancelled) return;
|
||||
if (needPhone && !me.phoneVerified) {
|
||||
setCompleteMode('phone');
|
||||
setShowSmsForm(true);
|
||||
return;
|
||||
}
|
||||
if (needWechat && !me.hasWechat) {
|
||||
@@ -100,7 +144,7 @@ export default function LoginPage() {
|
||||
|
||||
function ensureAgreed() {
|
||||
if (!agreed) {
|
||||
setMsg('请先勾选并同意用户协议');
|
||||
setMsg('请先阅读并勾选同意《用户服务协议》和《隐私政策》');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -108,18 +152,18 @@ export default function LoginPage() {
|
||||
|
||||
function applySessionAndLeave(
|
||||
data: SessionPayload | WechatLoginResult,
|
||||
phone?: string,
|
||||
phoneValue?: string,
|
||||
wxInfo?: MiniWechatProfile | null,
|
||||
successToast = '登录成功',
|
||||
) {
|
||||
if (!data.accessToken) return;
|
||||
if (phone) saveUserPhone(phone);
|
||||
if (phoneValue) saveUserPhone(phoneValue);
|
||||
saveAuth({
|
||||
accessToken: data.accessToken,
|
||||
refreshToken: data.refreshToken,
|
||||
});
|
||||
void syncMiniWechatProfile(wxInfo ?? getCachedWxProfile());
|
||||
if (!phone) {
|
||||
if (!phoneValue) {
|
||||
void fetchUserProfile()
|
||||
.then((me) => resolveDefaultUserPhone(me))
|
||||
.catch(() => {});
|
||||
@@ -133,7 +177,6 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
function handleWechatLoginResult(result: WechatLoginResult, wxInfo?: MiniWechatProfile | null) {
|
||||
// 微信授权成功即登录;手机号改为下单页可选绑定
|
||||
if (result.accessToken) {
|
||||
applySessionAndLeave(result, undefined, wxInfo);
|
||||
return;
|
||||
@@ -141,11 +184,49 @@ export default function LoginPage() {
|
||||
if (result.needBindPhone && result.wxSessionKey) {
|
||||
setBindMode(true);
|
||||
setWxSessionKey(result.wxSessionKey);
|
||||
setMsg('微信授权成功,可绑定手机号(也可跳过,稍后在下单时再绑定)');
|
||||
setShowSmsForm(true);
|
||||
setMsg('授权成功,可绑定手机号(也可稍后在下单时再绑定)');
|
||||
setSentHint('');
|
||||
return;
|
||||
}
|
||||
setMsg('微信登录未完成,请重试或使用手机号登录');
|
||||
setMsg('登录未完成,请重试或使用手机号登录');
|
||||
}
|
||||
|
||||
async function onPhoneQuickLogin(phoneCode: string) {
|
||||
if (!ensureAgreed()) return;
|
||||
setPhoneQuickLoading(true);
|
||||
setMsg('');
|
||||
setSentHint('');
|
||||
try {
|
||||
let loginCode: string | undefined;
|
||||
try {
|
||||
const loginRes = await Taro.login();
|
||||
loginCode = loginRes.code || undefined;
|
||||
} catch {
|
||||
/* openId 绑定失败不阻断手机号登录 */
|
||||
}
|
||||
const data = await request<WechatLoginResult>('/auth/login/wechat-phone', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
phoneCode,
|
||||
...(loginCode ? { loginCode } : {}),
|
||||
platform: 'mini',
|
||||
},
|
||||
});
|
||||
if (!data?.accessToken) {
|
||||
setMsg('登录成功但未返回令牌,请重试');
|
||||
return;
|
||||
}
|
||||
const profilePhone =
|
||||
typeof data.user === 'object' && data.user && 'phone' in data.user
|
||||
? String((data.user as { phone?: string }).phone || '')
|
||||
: '';
|
||||
applySessionAndLeave(data, profilePhone || undefined);
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '手机号快捷登录失败');
|
||||
} finally {
|
||||
setPhoneQuickLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onSendCode() {
|
||||
@@ -200,7 +281,6 @@ export default function LoginPage() {
|
||||
return;
|
||||
}
|
||||
if (completeMode === 'phone' && isLoggedIn()) {
|
||||
// bind 返回新 session(合并账号后旧 guest JWT 立刻失效),必须落盘后再离开
|
||||
const data = await request<SessionPayload>('/auth/phone/bind', {
|
||||
method: 'POST',
|
||||
data: { phone: normalized, code: code.trim() },
|
||||
@@ -234,7 +314,6 @@ export default function LoginPage() {
|
||||
setSentHint('');
|
||||
setWxLoading(true);
|
||||
try {
|
||||
// 登录仅绑定 openId;资料展示走账号昵称/头像
|
||||
const wxInfo = getCachedWxProfile();
|
||||
|
||||
if (completeMode === 'wechat' && isLoggedIn()) {
|
||||
@@ -246,12 +325,13 @@ export default function LoginPage() {
|
||||
setBindMode(true);
|
||||
setWxSessionKey(result.wxSessionKey);
|
||||
setCompleteMode('phone');
|
||||
setShowSmsForm(true);
|
||||
setMsg('请绑定手机号完成认证');
|
||||
return;
|
||||
}
|
||||
if (result.ok) {
|
||||
if (wxInfo) await syncMiniWechatProfile(wxInfo);
|
||||
toast('微信授权成功', 'success');
|
||||
toast('授权成功', 'success');
|
||||
finishLoginNavigate(returnTo);
|
||||
return;
|
||||
}
|
||||
@@ -260,11 +340,11 @@ export default function LoginPage() {
|
||||
const result = await loginWithWechat();
|
||||
if (result) handleWechatLoginResult(result, wxInfo);
|
||||
} catch (e) {
|
||||
const raw = e instanceof Error ? e.message : '微信登录失败';
|
||||
const raw = e instanceof Error ? e.message : '授权登录失败';
|
||||
const hint = /invalid code/i.test(raw)
|
||||
? process.env.TARO_ENV === 'weapp'
|
||||
? '微信 code 无效:请确认后端 WX_MINI_APP_ID 与小程序 appid 一致,或本地联调使用 MOCK_WECHAT'
|
||||
: '微信授权失败:请确认公众号 WX_APP_ID / 网页授权域名配置正确'
|
||||
? '授权失败:请确认后端小程序 AppID 配置正确'
|
||||
: '授权失败:请确认公众号网页授权域名配置正确'
|
||||
: raw;
|
||||
setMsg(hint);
|
||||
} finally {
|
||||
@@ -274,18 +354,19 @@ export default function LoginPage() {
|
||||
|
||||
const displayMsg = msg || sentHint;
|
||||
const codeDisabled = cooldown > 0 || sending;
|
||||
const showWechatLogin =
|
||||
(completeMode === 'wechat' || (!bindMode && !completeMode)) &&
|
||||
(process.env.TARO_ENV === 'weapp' || wxAuthorize);
|
||||
const showSmsForm = completeMode !== 'wechat';
|
||||
const showAuthLogin =
|
||||
(completeMode === 'wechat' || (!IS_WEAPP && !bindMode && !completeMode)) &&
|
||||
(IS_WEAPP || wxAuthorize);
|
||||
const showPhoneQuick =
|
||||
IS_WEAPP && completeMode !== 'wechat' && !bindMode && completeMode !== 'phone';
|
||||
const cardTitle =
|
||||
completeMode === 'phone'
|
||||
? '验证手机号'
|
||||
: bindMode
|
||||
? '绑定手机号'
|
||||
: completeMode === 'wechat'
|
||||
? '微信授权'
|
||||
: '手机验证码登录';
|
||||
? '授权登录'
|
||||
: '手机号快捷登录';
|
||||
|
||||
return (
|
||||
<PageShell variant="plain" className="login-page">
|
||||
@@ -301,7 +382,7 @@ export default function LoginPage() {
|
||||
{completeMode === 'phone'
|
||||
? '建议绑定手机号'
|
||||
: completeMode === 'wechat'
|
||||
? '完成微信授权'
|
||||
? '完成授权登录'
|
||||
: '欢迎来到杜康好客'}
|
||||
</Text>
|
||||
<Text className="login-welcome-sub">
|
||||
@@ -317,77 +398,100 @@ export default function LoginPage() {
|
||||
<View className="login-main">
|
||||
{completeMode === 'wechat' ? (
|
||||
<View className="login-card">
|
||||
<Text className="login-card-title">微信一键授权</Text>
|
||||
<Text className="login-card-title">授权登录</Text>
|
||||
<Text className="login-msg login-msg--hint" style={{ marginBottom: 16 }}>
|
||||
使用微信支付前需授权微信账号
|
||||
使用支付功能前需完成授权登录
|
||||
</Text>
|
||||
<View className="login-agreement" onClick={() => setAgreed((v) => !v)}>
|
||||
<View className={`login-agreement-check${agreed ? ' login-agreement-check--on' : ''}`}>
|
||||
{agreed ? <Text>✓</Text> : null}
|
||||
</View>
|
||||
<Text className="login-agreement-text">
|
||||
我已阅读并同意
|
||||
<Text
|
||||
className="login-agreement-link"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
Taro.navigateTo({ url: '/pages/user-agreement/index' });
|
||||
}}
|
||||
>
|
||||
《用户协议》
|
||||
</Text>
|
||||
和
|
||||
<Text
|
||||
className="login-agreement-link"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
Taro.navigateTo({ url: '/pages/privacy-policy/index' });
|
||||
}}
|
||||
>
|
||||
《隐私政策》
|
||||
</Text>
|
||||
<AgreementRow agreed={agreed} onToggle={() => setAgreed((v) => !v)} />
|
||||
{displayMsg ? (
|
||||
<Text className={`login-msg${sentHint && !msg ? ' login-msg--hint' : ''}`}>
|
||||
{displayMsg}
|
||||
</Text>
|
||||
</View>
|
||||
{showWechatLogin ? (
|
||||
<WechatLoginButton loading={wxLoading} onClick={() => void wechatLogin()} />
|
||||
) : null}
|
||||
{showAuthLogin ? (
|
||||
<WechatLoginButton loading={wxLoading} label="授权登录" onClick={() => void wechatLogin()} />
|
||||
) : null}
|
||||
</View>
|
||||
) : (
|
||||
<View className="login-card">
|
||||
<Text className="login-card-title">{cardTitle}</Text>
|
||||
|
||||
<View className="login-field">
|
||||
<Text className="login-field-prefix">+86</Text>
|
||||
<Input
|
||||
className="login-field-input"
|
||||
type="number"
|
||||
maxlength={11}
|
||||
placeholder="请输入手机号"
|
||||
value={phone}
|
||||
onInput={(e) => {
|
||||
setPhone(normalizePhone(e.detail.value));
|
||||
setMsg('');
|
||||
setSentHint('');
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<AgreementRow agreed={agreed} onToggle={() => setAgreed((v) => !v)} />
|
||||
|
||||
<View className="login-field">
|
||||
<Input
|
||||
className="login-field-input"
|
||||
type="number"
|
||||
maxlength={6}
|
||||
placeholder="请输入验证码"
|
||||
value={code}
|
||||
onInput={(e) => setCode(e.detail.value.replace(/\D/g, '').slice(0, 6))}
|
||||
{showPhoneQuick ? (
|
||||
<PhoneQuickLoginButton
|
||||
loading={phoneQuickLoading}
|
||||
agreed={agreed}
|
||||
onRequireAgree={() => ensureAgreed()}
|
||||
onGetPhoneNumber={(phoneCode) => void onPhoneQuickLogin(phoneCode)}
|
||||
onFail={(message) => setMsg(message)}
|
||||
/>
|
||||
<Text
|
||||
className={`login-get-code${codeDisabled ? ' login-get-code--disabled' : ''}`}
|
||||
onClick={() => void onSendCode()}
|
||||
>
|
||||
{sending ? '发送中...' : cooldown > 0 ? `${cooldown}s 后重新获取` : '获取验证码'}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{showPhoneQuick ? (
|
||||
<View className="login-divider" style={{ marginTop: 20 }}>
|
||||
<View className="login-divider-line" />
|
||||
<Text
|
||||
className="login-divider-text"
|
||||
onClick={() => setShowSmsForm((v) => !v)}
|
||||
>
|
||||
{showSmsForm ? '收起验证码登录' : '使用验证码登录'}
|
||||
</Text>
|
||||
<View className="login-divider-line" />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{(showSmsForm || !showPhoneQuick) && (
|
||||
<>
|
||||
<View className="login-field" style={showPhoneQuick ? { marginTop: 8 } : undefined}>
|
||||
<Text className="login-field-prefix">+86</Text>
|
||||
<Input
|
||||
className="login-field-input"
|
||||
type="number"
|
||||
maxlength={11}
|
||||
placeholder="请输入手机号"
|
||||
value={phone}
|
||||
onInput={(e) => {
|
||||
setPhone(normalizePhone(e.detail.value));
|
||||
setMsg('');
|
||||
setSentHint('');
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="login-field">
|
||||
<Input
|
||||
className="login-field-input"
|
||||
type="number"
|
||||
maxlength={6}
|
||||
placeholder="请输入验证码"
|
||||
value={code}
|
||||
onInput={(e) => setCode(e.detail.value.replace(/\D/g, '').slice(0, 6))}
|
||||
/>
|
||||
<Text
|
||||
className={`login-get-code${codeDisabled ? ' login-get-code--disabled' : ''}`}
|
||||
onClick={() => void onSendCode()}
|
||||
>
|
||||
{sending ? '发送中...' : cooldown > 0 ? `${cooldown}s 后重新获取` : '获取验证码'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className={`login-sms-btn${loading ? ' login-sms-btn--disabled' : ''}`}
|
||||
onClick={loading ? undefined : () => void login()}
|
||||
>
|
||||
<Text className="login-sms-btn__text">
|
||||
{loading
|
||||
? '处理中...'
|
||||
: completeMode === 'phone'
|
||||
? '完成验证'
|
||||
: bindMode
|
||||
? '绑定并登录'
|
||||
: '验证码登录'}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{displayMsg ? (
|
||||
<Text className={`login-msg${sentHint && !msg ? ' login-msg--hint' : ''}`}>
|
||||
@@ -395,48 +499,6 @@ export default function LoginPage() {
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<View className="login-agreement" onClick={() => setAgreed((v) => !v)}>
|
||||
<View className={`login-agreement-check${agreed ? ' login-agreement-check--on' : ''}`}>
|
||||
{agreed ? <Text>✓</Text> : null}
|
||||
</View>
|
||||
<Text className="login-agreement-text">
|
||||
我已阅读并同意
|
||||
<Text
|
||||
className="login-agreement-link"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
Taro.navigateTo({ url: '/pages/user-agreement/index' });
|
||||
}}
|
||||
>
|
||||
《用户协议》
|
||||
</Text>
|
||||
和
|
||||
<Text
|
||||
className="login-agreement-link"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
Taro.navigateTo({ url: '/pages/privacy-policy/index' });
|
||||
}}
|
||||
>
|
||||
《隐私政策》
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View
|
||||
className={`login-sms-btn${loading ? ' login-sms-btn--disabled' : ''}`}
|
||||
onClick={loading ? undefined : () => void login()}
|
||||
>
|
||||
<Text className="login-sms-btn__text">
|
||||
{loading
|
||||
? '处理中...'
|
||||
: completeMode === 'phone'
|
||||
? '完成验证'
|
||||
: bindMode
|
||||
? '绑定并登录'
|
||||
: '登录'}
|
||||
</Text>
|
||||
</View>
|
||||
{completeMode === 'phone' ? (
|
||||
<View
|
||||
className="login-skip-bind"
|
||||
@@ -451,14 +513,14 @@ export default function LoginPage() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{showWechatLogin && completeMode !== 'wechat' ? (
|
||||
{showAuthLogin && completeMode !== 'wechat' ? (
|
||||
<>
|
||||
<View className="login-divider">
|
||||
<View className="login-divider-line" />
|
||||
<Text className="login-divider-text">或者</Text>
|
||||
<View className="login-divider-line" />
|
||||
</View>
|
||||
<WechatLoginButton loading={wxLoading} onClick={() => void wechatLogin()} />
|
||||
<WechatLoginButton loading={wxLoading} label="授权登录" onClick={() => void wechatLogin()} />
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -213,7 +213,7 @@ export default function MinePage() {
|
||||
throw new Error('wx profile helpers missing');
|
||||
}
|
||||
const nickname = display.nickname || '用户';
|
||||
const memberLabel = hasWechat ? '好客会员' : canWxAuth ? '微信未授权' : '未授权微信';
|
||||
const memberLabel = hasWechat ? '好客会员' : canWxAuth ? '未完成授权' : '未完成授权';
|
||||
void needsWxProfileFill(display);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { getLegalDocument } from '@dukang/shared-types';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import SubPageHeader from '../../components/SubPageHeader';
|
||||
@@ -8,21 +8,25 @@ export default function PrivacyPolicyPage() {
|
||||
return (
|
||||
<PageShell variant="sub" className="legal-page">
|
||||
<SubPageHeader title={doc.title} />
|
||||
<View className="sub-page-body inset-page">
|
||||
<ScrollView scrollY className="legal-scroll">
|
||||
<Text className="legal-updated">更新日期:{doc.updatedAt}</Text>
|
||||
<Text className="legal-intro">{doc.intro}</Text>
|
||||
{doc.sections.map((section) => (
|
||||
<View key={section.heading} className="legal-section">
|
||||
<Text className="legal-heading">{section.heading}</Text>
|
||||
{section.paragraphs.map((p, i) => (
|
||||
<Text key={`${section.heading}-${i}`} className="legal-paragraph">
|
||||
{p}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
<View className="sub-page-body inset-page legal-body">
|
||||
<Text className="legal-updated" selectable>
|
||||
更新日期:{doc.updatedAt}
|
||||
</Text>
|
||||
<Text className="legal-intro" selectable>
|
||||
{doc.intro}
|
||||
</Text>
|
||||
{doc.sections.map((section) => (
|
||||
<View key={section.heading} className="legal-section">
|
||||
<Text className="legal-heading" selectable>
|
||||
{section.heading}
|
||||
</Text>
|
||||
{section.paragraphs.map((p, i) => (
|
||||
<Text key={`${section.heading}-${i}`} className="legal-paragraph" selectable>
|
||||
{p}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</PageShell>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default definePageConfig({
|
||||
navigationBarTitleText: '用户协议',
|
||||
navigationBarTitleText: '用户服务协议',
|
||||
navigationStyle: 'custom',
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { View, Text, ScrollView } from '@tarojs/components';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import { getLegalDocument } from '@dukang/shared-types';
|
||||
import PageShell from '../../components/PageShell';
|
||||
import SubPageHeader from '../../components/SubPageHeader';
|
||||
@@ -8,21 +8,25 @@ export default function UserAgreementPage() {
|
||||
return (
|
||||
<PageShell variant="sub" className="legal-page">
|
||||
<SubPageHeader title={doc.title} />
|
||||
<View className="sub-page-body inset-page">
|
||||
<ScrollView scrollY className="legal-scroll">
|
||||
<Text className="legal-updated">更新日期:{doc.updatedAt}</Text>
|
||||
<Text className="legal-intro">{doc.intro}</Text>
|
||||
{doc.sections.map((section) => (
|
||||
<View key={section.heading} className="legal-section">
|
||||
<Text className="legal-heading">{section.heading}</Text>
|
||||
{section.paragraphs.map((p, i) => (
|
||||
<Text key={`${section.heading}-${i}`} className="legal-paragraph">
|
||||
{p}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
<View className="sub-page-body inset-page legal-body">
|
||||
<Text className="legal-updated" selectable>
|
||||
更新日期:{doc.updatedAt}
|
||||
</Text>
|
||||
<Text className="legal-intro" selectable>
|
||||
{doc.intro}
|
||||
</Text>
|
||||
{doc.sections.map((section) => (
|
||||
<View key={section.heading} className="legal-section">
|
||||
<Text className="legal-heading" selectable>
|
||||
{section.heading}
|
||||
</Text>
|
||||
{section.paragraphs.map((p, i) => (
|
||||
<Text key={`${section.heading}-${i}`} className="legal-paragraph" selectable>
|
||||
{p}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</PageShell>
|
||||
);
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
.legal-page .legal-scroll {
|
||||
height: 100%;
|
||||
.legal-page .legal-body {
|
||||
min-height: calc(100vh - var(--nav-bar-height, 88px));
|
||||
padding-top: 8px;
|
||||
padding-bottom: 40px;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 32px;
|
||||
background: var(--color-background, #f7f4ef);
|
||||
}
|
||||
|
||||
.legal-updated {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--color-on-surface-variant, #8d706e);
|
||||
color: #8d706e;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.legal-intro {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: var(--color-on-surface, #1f1a17);
|
||||
line-height: 1.75;
|
||||
color: #1f1a17;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@@ -27,14 +30,15 @@
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-on-surface, #1f1a17);
|
||||
color: #1f1a17;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.legal-paragraph {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: var(--color-on-surface-variant, #5c504c);
|
||||
line-height: 1.75;
|
||||
color: #3d3530;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@@ -234,7 +234,8 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.login-wechat-btn {
|
||||
.login-wechat-btn,
|
||||
.login-phone-quick-btn {
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
border: none;
|
||||
@@ -250,52 +251,33 @@
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 8px 24px rgba(166, 29, 36, 0.1);
|
||||
}
|
||||
|
||||
.login-wechat-btn:active {
|
||||
.login-wechat-btn:active,
|
||||
.login-phone-quick-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.login-wechat-btn--disabled {
|
||||
.login-wechat-btn--disabled,
|
||||
.login-phone-quick-btn--disabled {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-wechat-btn__text {
|
||||
.login-wechat-btn__text,
|
||||
.login-phone-quick-btn__text {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.wechat-login-icon {
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wechat-login-icon__big,
|
||||
.wechat-login-icon__small {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.wechat-login-icon__big {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
left: 0;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.wechat-login-icon__small {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
right: 0;
|
||||
bottom: 3px;
|
||||
/* 重置小程序 Button 默认样式,避免绿边/微信绿 */
|
||||
.login-phone-quick-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
|
||||
Reference in New Issue
Block a user