Merge commit 'fcf1d45522e3ba6e6faf2590b60f2db1ef90e73c' into dev_jacy
This commit is contained in:
@@ -13,7 +13,8 @@ import BillsPage from './pages/BillsPage';
|
||||
import ReshipPage from './pages/ReshipPage';
|
||||
import WeeklyReportPage from './pages/WeeklyReportPage';
|
||||
import LeaderboardPage from './pages/LeaderboardPage';
|
||||
import { handlePartnerWechatCallback, savePartnerWechatAuth } from './lib/wechat-auth';
|
||||
import { handlePartnerWechatCallback, handlePartnerWechatLoginResult } from './lib/wechat-auth';
|
||||
import { isLoggedIn } from './lib/api';
|
||||
import { isWechatEnv } from './lib/weixin';
|
||||
|
||||
function WechatOAuthHandler() {
|
||||
@@ -25,7 +26,7 @@ function WechatOAuthHandler() {
|
||||
if (location.pathname === '/login') return;
|
||||
void handlePartnerWechatCallback()
|
||||
.then((result) => {
|
||||
if (!result || !savePartnerWechatAuth(result)) return;
|
||||
if (!result || !handlePartnerWechatLoginResult(result)) return;
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.delete('code');
|
||||
params.delete('state');
|
||||
@@ -59,7 +60,7 @@ export default function App() {
|
||||
<Route path="/leaderboard" element={<LeaderboardPage />} />
|
||||
<Route path="/stores/:id" element={<StoreDetailPage />} />
|
||||
<Route path="/orders/:id" element={<OrderDetailPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
<Route path="*" element={<Navigate to={isLoggedIn() ? '/' : '/login'} replace />} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react';
|
||||
import { clearAuth, isLoggedIn, request } from '../lib/api';
|
||||
|
||||
export type PartnerAccount = {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
isPrimary?: boolean;
|
||||
companyName?: string;
|
||||
hasWechat?: boolean;
|
||||
};
|
||||
|
||||
type PartnerSessionValue = {
|
||||
account: PartnerAccount | null;
|
||||
loading: boolean;
|
||||
loggedIn: boolean;
|
||||
refresh: () => Promise<void>;
|
||||
logout: () => void;
|
||||
};
|
||||
|
||||
const PartnerSessionContext = createContext<PartnerSessionValue | null>(null);
|
||||
|
||||
export function PartnerSessionProvider({ children }: { children: ReactNode }) {
|
||||
const [account, setAccount] = useState<PartnerAccount | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!isLoggedIn()) {
|
||||
setAccount(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await request<PartnerAccount>('PARTNER_H5', '/partner/me');
|
||||
setAccount(data);
|
||||
} catch {
|
||||
setAccount(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
clearAuth();
|
||||
setAccount(null);
|
||||
window.location.href = '/login';
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
return (
|
||||
<PartnerSessionContext.Provider
|
||||
value={{ account, loading, loggedIn: isLoggedIn(), refresh, logout }}
|
||||
>
|
||||
{children}
|
||||
</PartnerSessionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePartnerSession(): PartnerSessionValue {
|
||||
const ctx = useContext(PartnerSessionContext);
|
||||
if (!ctx) throw new Error('usePartnerSession 必须在 PartnerSessionProvider 内使用');
|
||||
return ctx;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
|
||||
import { NavLink, Navigate, Outlet } from 'react-router-dom';
|
||||
import { isLoggedIn } from '../lib/api';
|
||||
|
||||
const TABS = [
|
||||
@@ -8,10 +8,8 @@ const TABS = [
|
||||
] as const;
|
||||
|
||||
export default function TabLayout() {
|
||||
const navigate = useNavigate();
|
||||
if (!isLoggedIn()) {
|
||||
navigate('/login');
|
||||
return null;
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -9,7 +9,14 @@ export async function request<T>(clientApp: string, path: string, options: Reque
|
||||
};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
const res = await fetch(`${apiBase}${path}`, { ...options, headers });
|
||||
const json = await res.json();
|
||||
const json = await res.json().catch(() => ({ code: res.status, message: '网络异常' }));
|
||||
if (res.status === 401 || json.code === 401) {
|
||||
clearAuth();
|
||||
if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) {
|
||||
window.location.href = '/login';
|
||||
}
|
||||
throw new Error(json.message || '登录已过期,请重新登录');
|
||||
}
|
||||
if (json.code !== 0) throw new Error(json.message || '请求失败');
|
||||
return json.data as T;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { WechatLoginResult } from '@dukang/shared-types';
|
||||
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
|
||||
import { isWechatEnv, weixinSdk } from './weixin';
|
||||
import { request, saveAuth } from './api';
|
||||
|
||||
@@ -19,20 +20,46 @@ export function needsWechatAuth(profile: PartnerProfile | null): boolean {
|
||||
return isWechatEnv() && !!profile && !profile.hasWechat;
|
||||
}
|
||||
|
||||
export function savePartnerWechatAuth(result: WechatLoginResult): boolean {
|
||||
/** 处理微信登录/绑定结果,返回是否已拿到 token 可进入首页 */
|
||||
export function handlePartnerWechatLoginResult(result: WechatLoginResult): boolean {
|
||||
if (!result.accessToken) return false;
|
||||
saveAuth({ accessToken: result.accessToken });
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function authorizePartnerWechat(): Promise<WechatLoginResult | void> {
|
||||
if (!isWechatEnv()) {
|
||||
throw new Error('请在微信内打开以完成授权');
|
||||
}
|
||||
return weixinSdk.login();
|
||||
}
|
||||
|
||||
export async function handlePartnerWechatCallback(): Promise<WechatLoginResult | null> {
|
||||
if (!isWechatEnv()) return null;
|
||||
return weixinSdk.handleOAuthCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信授权登录(对齐 C 端:仅微信内置浏览器走 OAuth)。
|
||||
* 返回 true = 已登录;void = 已跳转授权页等待回调。
|
||||
*/
|
||||
export async function loginPartnerWithWechat(): Promise<boolean | void> {
|
||||
if (!isWechatEnv()) {
|
||||
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
|
||||
}
|
||||
const result = await weixinSdk.login();
|
||||
if (result) return handlePartnerWechatLoginResult(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信登录成功后于微信内自动发起 OAuth,将 openId 绑定到当前合伙人账号(便于同一微信后续免登)。
|
||||
*/
|
||||
export async function bindPartnerWechatAfterSmsLogin(): Promise<void> {
|
||||
if (!isWechatEnv()) return;
|
||||
await weixinSdk.login();
|
||||
}
|
||||
|
||||
export async function authorizePartnerWechat(): Promise<WechatLoginResult | void> {
|
||||
if (!isWechatEnv()) {
|
||||
throw new Error(WECHAT_INAPP_REQUIRED_MSG);
|
||||
}
|
||||
return weixinSdk.login();
|
||||
}
|
||||
|
||||
/** @deprecated 使用 handlePartnerWechatLoginResult */
|
||||
export function savePartnerWechatAuth(result: WechatLoginResult): boolean {
|
||||
return handlePartnerWechatLoginResult(result);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,15 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import { PartnerSessionProvider } from './contexts/PartnerSessionContext';
|
||||
import './styles.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode><BrowserRouter><App /></BrowserRouter></React.StrictMode>,
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<PartnerSessionProvider>
|
||||
<App />
|
||||
</PartnerSessionProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { clearAuth, isLoggedIn, request } from '../lib/api';
|
||||
import { isLoggedIn, request } from '../lib/api';
|
||||
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||
|
||||
export default function CenterPage() {
|
||||
const navigate = useNavigate();
|
||||
const [me, setMe] = useState<Record<string, unknown> | null>(null);
|
||||
const { account, logout } = usePartnerSession();
|
||||
const me = account as unknown as Record<string, unknown> | null;
|
||||
const [bills, setBills] = useState<Array<Record<string, unknown>>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn()) { navigate('/login'); return; }
|
||||
request('PARTNER_H5', '/partner/me').then(setMe);
|
||||
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/settlement/bills').then(setBills);
|
||||
}, [navigate]);
|
||||
|
||||
@@ -134,7 +135,7 @@ export default function CenterPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button type="button" className="partner-logout-btn" onClick={() => { clearAuth(); navigate('/login'); }}>
|
||||
<button type="button" className="partner-logout-btn" onClick={logout}>
|
||||
<span className="material-symbols-outlined">logout</span>
|
||||
退出登录
|
||||
</button>
|
||||
|
||||
@@ -2,37 +2,109 @@ import { useEffect, useState } from 'react';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { request, saveAuth } from '../lib/api';
|
||||
import {
|
||||
authorizePartnerWechat,
|
||||
bindPartnerWechatAfterSmsLogin,
|
||||
handlePartnerWechatCallback,
|
||||
savePartnerWechatAuth,
|
||||
handlePartnerWechatLoginResult,
|
||||
loginPartnerWithWechat,
|
||||
} from '../lib/wechat-auth';
|
||||
import { isWechatEnv } from '../lib/weixin';
|
||||
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
|
||||
|
||||
const REMEMBER_PHONE_KEY = 'partner_remember_phone';
|
||||
const REMEMBER_FLAG_KEY = 'partner_remember_account';
|
||||
|
||||
function loadRememberedPhone(): { phone: string; remember: boolean } {
|
||||
try {
|
||||
const remember = localStorage.getItem(REMEMBER_FLAG_KEY) === '1';
|
||||
const phone = remember ? localStorage.getItem(REMEMBER_PHONE_KEY) || '' : '';
|
||||
return { phone, remember };
|
||||
} catch {
|
||||
return { phone: '', remember: false };
|
||||
}
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const [params] = useSearchParams();
|
||||
const quick = params.get('quick') === '1';
|
||||
const [phone, setPhone] = useState('13700000001');
|
||||
const remembered = loadRememberedPhone();
|
||||
const [phone, setPhone] = useState(remembered.phone || '13700000001');
|
||||
const [code, setCode] = useState('123456');
|
||||
const [rememberAccount, setRememberAccount] = useState(remembered.remember);
|
||||
const [agreed, setAgreed] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [wechatLoading, setWechatLoading] = useState(false);
|
||||
const [codeCooldown, setCodeCooldown] = useState(0);
|
||||
const [wxLoading, setWxLoading] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
const [codeCooldown, setCodeCooldown] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWechatEnv()) return;
|
||||
void handlePartnerWechatCallback()
|
||||
.then((result) => {
|
||||
if (!result) return;
|
||||
if (savePartnerWechatAuth(result)) {
|
||||
navigate('/');
|
||||
}
|
||||
if (handlePartnerWechatLoginResult(result)) navigate('/');
|
||||
})
|
||||
.catch((e) => setMsg(e instanceof Error ? e.message : '微信登录失败'));
|
||||
.catch((e) => setMsg(formatWechatError(e)));
|
||||
}, [navigate]);
|
||||
|
||||
function formatWechatError(e: unknown): string {
|
||||
const text = e instanceof Error ? e.message : '微信登录失败';
|
||||
if (text.includes('首次登录') || text.includes('手机验证码')) {
|
||||
return '该微信尚未绑定合伙人账号,请先使用手机验证码登录,登录后将自动关联微信';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function ensureAgreed() {
|
||||
if (!agreed) {
|
||||
setMsg('请先勾选并同意用户协议');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function persistRememberAccount(nextPhone: string) {
|
||||
try {
|
||||
if (rememberAccount) {
|
||||
localStorage.setItem(REMEMBER_FLAG_KEY, '1');
|
||||
localStorage.setItem(REMEMBER_PHONE_KEY, nextPhone);
|
||||
} else {
|
||||
localStorage.removeItem(REMEMBER_FLAG_KEY);
|
||||
localStorage.removeItem(REMEMBER_PHONE_KEY);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
async function sendCode() {
|
||||
if (!ensureAgreed()) return;
|
||||
setMsg('');
|
||||
try {
|
||||
await request('PARTNER_H5', '/partner/auth/sms/send', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone, scene: 'PARTNER_LOGIN' }),
|
||||
});
|
||||
setMsg('验证码已发送(Mock: 123456)');
|
||||
setCodeCooldown(60);
|
||||
const timer = setInterval(() => {
|
||||
setCodeCooldown((s) => {
|
||||
if (s <= 1) {
|
||||
clearInterval(timer);
|
||||
return 0;
|
||||
}
|
||||
return s - 1;
|
||||
});
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '发送失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function login() {
|
||||
if (!ensureAgreed()) return;
|
||||
setLoading(true);
|
||||
setMsg('');
|
||||
try {
|
||||
await request('PARTNER_H5', '/partner/auth/sms/send', {
|
||||
method: 'POST',
|
||||
@@ -43,43 +115,38 @@ export default function LoginPage() {
|
||||
body: JSON.stringify({ phone, code }),
|
||||
});
|
||||
saveAuth(data);
|
||||
persistRememberAccount(phone);
|
||||
if (isWechatEnv()) {
|
||||
setMsg('登录成功,正在关联微信…');
|
||||
await bindPartnerWechatAfterSmsLogin();
|
||||
return;
|
||||
}
|
||||
navigate('/');
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '登录失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function wechatLogin() {
|
||||
if (!ensureAgreed()) return;
|
||||
setMsg('');
|
||||
if (!isWechatEnv()) {
|
||||
setMsg('请在微信内打开以使用微信一键登录');
|
||||
setMsg(WECHAT_INAPP_REQUIRED_MSG);
|
||||
return;
|
||||
}
|
||||
setWechatLoading(true);
|
||||
setWxLoading(true);
|
||||
try {
|
||||
await authorizePartnerWechat();
|
||||
const ok = await loginPartnerWithWechat();
|
||||
if (ok) navigate('/');
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '微信登录失败');
|
||||
setWechatLoading(false);
|
||||
setMsg(formatWechatError(e));
|
||||
} finally {
|
||||
setWxLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function sendCode() {
|
||||
if (codeCooldown > 0) return;
|
||||
request('PARTNER_H5', '/partner/auth/sms/send', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone, scene: 'PARTNER_LOGIN' }),
|
||||
}).then(() => {
|
||||
setCodeCooldown(60);
|
||||
const timer = setInterval(() => {
|
||||
setCodeCooldown((s) => {
|
||||
if (s <= 1) { clearInterval(timer); return 0; }
|
||||
return s - 1;
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
if (quick) {
|
||||
return (
|
||||
<div className="partner-auth-page partner-auth-page--quick">
|
||||
@@ -105,6 +172,7 @@ export default function LoginPage() {
|
||||
</section>
|
||||
|
||||
<nav style={{ width: '100%', maxWidth: 384, marginTop: 16 }}>
|
||||
{msg && <p className="partner-auth-msg" style={{ color: 'var(--color-error, #d33)', fontSize: 13, textAlign: 'center', marginBottom: 12 }}>{msg}</p>}
|
||||
<button type="button" className="partner-btn-primary" onClick={login} disabled={loading}>
|
||||
<span>{loading ? '登录中...' : '一键登录'}</span>
|
||||
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
|
||||
@@ -148,28 +216,28 @@ export default function LoginPage() {
|
||||
{codeCooldown > 0 ? `${codeCooldown}s 后重发` : '获取验证码'}
|
||||
</button>
|
||||
</div>
|
||||
<label className="partner-checkbox-row">
|
||||
<input type="checkbox" defaultChecked />
|
||||
<label className="partner-remember-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rememberAccount}
|
||||
onChange={(e) => setRememberAccount(e.target.checked)}
|
||||
/>
|
||||
<span>记住账号</span>
|
||||
</label>
|
||||
<button type="button" className="partner-btn-primary" onClick={login} disabled={loading}>
|
||||
<span>{loading ? '登录中...' : '登录'}</span>
|
||||
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
|
||||
</button>
|
||||
{msg && <p className="partner-auth-msg" style={{ color: 'var(--color-error, #d33)', fontSize: 13, textAlign: 'center' }}>{msg}</p>}
|
||||
<div className="partner-auth-divider"><span>其他登录方式</span></div>
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-outline"
|
||||
style={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}
|
||||
disabled={wechatLoading || loading}
|
||||
onClick={() => void wechatLogin()}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="#07C160"><path d="M8.25 4.5C4.52 4.5 1.5 7.04 1.5 10.17c0 1.78.98 3.37 2.5 4.48l-.63 1.88 2.19-1.09c.84.24 1.74.38 2.69.38.25 0 .5 0 .75-.03-.16-.53-.25-1.09-.25-1.66 0-3.13 3.02-5.67 6.75-5.67.57 0 1.13.06 1.66.17C15.17 6.13 12 4.5 8.25 4.5zm10.5 6.33c-3.11 0-5.62 2.12-5.62 4.73 0 2.61 2.51 4.73 5.62 4.73.79 0 1.54-.14 2.24-.38l1.83.91-.53-1.57c1.27-.92 2.08-2.25 2.08-3.73 0-2.61-2.51-4.73-5.62-4.73z" /></svg>
|
||||
{wechatLoading ? '跳转授权中…' : '微信一键登录'}
|
||||
<button type="button" className="partner-btn-wechat" onClick={wechatLogin} disabled={wxLoading}>
|
||||
<svg viewBox="0 0 24 24" fill="#07C160" aria-hidden>
|
||||
<path d="M8.25 4.5C4.52 4.5 1.5 7.04 1.5 10.17c0 1.78.98 3.37 2.5 4.48l-.63 1.88 2.19-1.09c.84.24 1.74.38 2.69.38.25 0 .5 0 .75-.03-.16-.53-.25-1.09-.25-1.66 0-3.13 3.02-5.67 6.75-5.67.57 0 1.13.06 1.66.17C15.17 6.13 12 4.5 8.25 4.5zm10.5 6.33c-3.11 0-5.62 2.12-5.62 4.73 0 2.61 2.51 4.73 5.62 4.73.79 0 1.54-.14 2.24-.38l1.83.91-.53-1.57c1.27-.92 2.08-2.25 2.08-3.73 0-2.61-2.51-4.73-5.62-4.73z" />
|
||||
</svg>
|
||||
<span>{wxLoading ? '登录中...' : '微信一键授权'}</span>
|
||||
</button>
|
||||
{msg && <p className="partner-form-error" role="alert">{msg}</p>}
|
||||
<label className="partner-checkbox-row">
|
||||
<input type="checkbox" />
|
||||
<input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} />
|
||||
<span>
|
||||
我已阅读并同意 <span className="text-primary" style={{ fontWeight: 600 }}>《用户协议》</span> 与 <span className="text-primary" style={{ fontWeight: 600 }}>《隐私政策》</span>
|
||||
</span>
|
||||
|
||||
+140
-12
@@ -6,6 +6,10 @@ html, body, #root {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
/* ── Partner auth ── */
|
||||
.partner-auth-page {
|
||||
min-height: 100vh;
|
||||
@@ -277,6 +281,58 @@ html, body, #root {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.partner-btn-wechat {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border: 1px solid rgba(226, 190, 188, 0.2);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-container-low);
|
||||
color: var(--color-on-surface);
|
||||
font-family: var(--font-headline);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: background 0.2s, transform 0.1s;
|
||||
}
|
||||
|
||||
.partner-btn-wechat:hover {
|
||||
background: var(--color-surface-container-high);
|
||||
}
|
||||
|
||||
.partner-btn-wechat:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.partner-btn-wechat:disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.partner-btn-wechat svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.partner-remember-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-on-surface-variant);
|
||||
}
|
||||
|
||||
.partner-remember-row input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
.partner-link {
|
||||
display: block;
|
||||
text-align: center;
|
||||
@@ -681,30 +737,75 @@ html, body, #root {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Partner bottom tabbar ── */
|
||||
.app-tabbar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
/* ── Partner bottom tabbar(锁定尺寸,仅切换颜色)── */
|
||||
nav.app-tabbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-items: stretch;
|
||||
padding: 8px 0 calc(8px + env(safe-area-inset-bottom, 0px));
|
||||
justify-content: stretch;
|
||||
padding: 0;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
height: calc(56px + env(safe-area-inset-bottom, 0px));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.app-tabbar-item {
|
||||
flex: 1;
|
||||
nav.app-tabbar .app-tabbar-item {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
padding: 4px 2px;
|
||||
max-width: 33.333%;
|
||||
height: 56px;
|
||||
padding: 6px 0 4px;
|
||||
border-radius: 0;
|
||||
gap: 2px;
|
||||
color: var(--color-subtle-gray);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
transform: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
nav.app-tabbar .app-tabbar-item.active {
|
||||
color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
nav.app-tabbar .app-tabbar-item:active,
|
||||
nav.app-tabbar .app-tabbar-item:focus,
|
||||
nav.app-tabbar .app-tabbar-item:focus-visible {
|
||||
transform: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
nav.app-tabbar .app-tabbar-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
|
||||
.app-tabbar-label {
|
||||
max-width: 100%;
|
||||
nav.app-tabbar .app-tabbar-label {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
height: 14px;
|
||||
line-height: 14px;
|
||||
margin: 0;
|
||||
padding: 0 1px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ── Store create stepper ── */
|
||||
@@ -1997,3 +2098,30 @@ html, body, #root {
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
iOS / Android 机型适配:顶部安全区(刘海 / 状态栏)
|
||||
viewport-fit=cover 下内容会延伸到状态栏,需为吸顶头部补 inset。
|
||||
───────────────────────────────────────────── */
|
||||
.partner-home-header,
|
||||
.header.app-page-header,
|
||||
.app-page-header,
|
||||
.page-header {
|
||||
padding-top: env(safe-area-inset-top, 0px);
|
||||
height: auto;
|
||||
min-height: calc(56px + env(safe-area-inset-top, 0px));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 登录 / 快捷登录页顶部留出状态栏空间,避免品牌区贴到刘海 */
|
||||
.partner-auth-page {
|
||||
padding-top: calc(48px + env(safe-area-inset-top, 0px));
|
||||
}
|
||||
|
||||
/* 全屏容器铺满机身背景,安全区外也保持底色一致 */
|
||||
html,
|
||||
body {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 5175,
|
||||
proxy: { '/api': 'http://localhost:3000' },
|
||||
proxy: {
|
||||
'/api': process.env.VITE_API_TARGET ?? 'http://localhost:3000',
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user