feat(admin): HQ手机号编辑、合伙人子账号日志与子账号H5体验

支持 HQ 账户改手机号(格式校验)、合伙人日志包含子账号且子账号不显示主账号信息;改手机号时清除微信绑定。合伙人 H5 子账号页签与我的页、录入门店底部按钮及上传失败提示。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 13:29:09 +08:00
parent b8623f59f0
commit 63af3321dd
16 changed files with 259 additions and 40 deletions
+3
View File
@@ -10,6 +10,7 @@ import StoreCreatePage from './pages/StoreCreatePage';
import OrderListPage from './pages/OrderListPage';
import OrderDetailPage from './pages/OrderDetailPage';
import CenterPage from './pages/CenterPage';
import PartnerMePage from './pages/PartnerMePage';
import BillsPage from './pages/BillsPage';
import SettlementPage from './pages/SettlementPage';
import ReshipPage from './pages/ReshipPage';
@@ -17,6 +18,7 @@ import WeeklyReportPage from './pages/WeeklyReportPage';
import ProxyOrderPage from './pages/ProxyOrderPage';
import StaffListPage from './pages/StaffListPage';
import StaffCreatePage from './pages/StaffCreatePage';
import LeaderboardPage from './pages/LeaderboardPage';
function PrimaryRoutes() {
return (
@@ -50,6 +52,7 @@ function SubAccountRoutes() {
<Route path="/stores" element={<StoreListPage />} />
<Route path="/stores/new" element={<StoreCreatePage />} />
<Route path="/stores/:id" element={<StoreDetailPage />} />
<Route path="/me" element={<PartnerMePage />} />
</Route>
<Route path="*" element={<Navigate to="/stores/new?step=1" replace />} />
</Routes>
@@ -10,6 +10,7 @@ import {
} from '../lib/wechat-auth';
import { formatChooseImageFailMessage } from '@dukang/weixin-sdk';
import { isWechatEnv, weixinSdk } from '../lib/weixin';
import { toastError } from '../lib/toast';
import type { ClientRuntimeConfig } from '@dukang/shared-types';
type OssUploadFieldProps = {
@@ -61,6 +62,11 @@ export default function OssUploadField({
const [profile, setProfile] = useState<PartnerProfile | null>(null);
const [clientConfig, setClientConfig] = useState<ClientRuntimeConfig | null>(null);
function showUploadError(text: string) {
setError(text);
toastError(text);
}
const resolvedAccept =
accept ?? (mediaType === 'VIDEO' ? 'video/*' : mediaType === 'FILE' ? 'image/*,.pdf' : 'image/*');
const inWechat = isWechatEnv();
@@ -91,7 +97,7 @@ export default function OssUploadField({
async function uploadSelectedFile(file: File) {
if (file.size > DEFAULT_MAX_MB * 1024 * 1024) {
const text = `文件不能超过 ${DEFAULT_MAX_MB}MB`;
setError(text);
showUploadError(text);
return;
}
setUploading(true);
@@ -100,7 +106,7 @@ export default function OssUploadField({
const result = await enqueueUpload(() => uploadFileToOss(file, { bizType, mediaType }));
onChange?.(result.url);
} catch (e) {
setError(e instanceof Error ? e.message : '上传失败');
showUploadError(e instanceof Error ? e.message : '上传失败');
} finally {
setUploading(false);
if (inputRef.current) inputRef.current.value = '';
@@ -114,7 +120,7 @@ export default function OssUploadField({
await authorizePartnerWechat();
} catch (e) {
const text = e instanceof Error ? e.message : '微信授权失败';
setError(text);
showUploadError(text);
setAuthorizing(false);
}
}
@@ -149,7 +155,7 @@ export default function OssUploadField({
if (useWechatPicker && needsAuth) {
const text = '请先完成微信授权后再上传照片';
setError(text);
showUploadError(text);
return;
}
@@ -159,7 +165,7 @@ export default function OssUploadField({
} catch (e) {
const msg = e instanceof Error ? e.message : '无法打开相册';
if (/cancel/i.test(msg)) return;
setError(formatWechatUploadError(e));
showUploadError(formatWechatUploadError(e));
}
return;
}
@@ -1,27 +1,14 @@
import { NavLink, Outlet } from 'react-router-dom';
import { usePartnerSession } from '../contexts/PartnerSessionContext';
const TABS = [
{ to: '/stores/new', icon: 'add_business', label: '录入新店' },
{ to: '/stores', end: true, icon: 'store', label: '我的门店' },
{ to: '/stores', end: true, icon: 'store', label: '门店管理' },
{ to: '/stores/new', icon: 'add_business', label: '录入门店' },
{ to: '/me', icon: 'person', label: '我的' },
] as const;
export default function SubAccountLayout() {
const { account, logout } = usePartnerSession();
return (
<>
<header className="header app-page-header partner-sub-header">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<div>
<h1 className="app-page-title" style={{ fontSize: 18 }}>{account?.name || '拓店账号'}</h1>
<p className="label-md text-muted">{account?.companyName || '门店入驻'}</p>
</div>
<button type="button" className="partner-menu-icon" onClick={logout} aria-label="退出登录">
<span className="material-symbols-outlined">logout</span>
</button>
</div>
</header>
<Outlet />
<nav className="app-tabbar">
{TABS.map((tab) => (
+1 -1
View File
@@ -21,7 +21,7 @@ export function partnerHomePath(account: PartnerMe | null | undefined): string {
return isSubAccount(account) ? '/stores/new?step=1' : '/';
}
export const SUB_ACCOUNT_ALLOWED_PREFIXES = ['/stores', '/login'];
export const SUB_ACCOUNT_ALLOWED_PREFIXES = ['/stores', '/me', '/login'];
export function isSubAccountPath(pathname: string): boolean {
if (pathname === '/login') return true;
+102
View File
@@ -0,0 +1,102 @@
import { useEffect, useState } from 'react';
import { PARTNER_STAFF_ROLE_LABELS, type PartnerStaffRole } from '@dukang/shared-types';
import { request } from '../lib/api';
import { usePartnerSession } from '../contexts/PartnerSessionContext';
import { toastError, toastSuccess } from '../lib/toast';
export default function PartnerMePage() {
const { account, refresh, logout } = usePartnerSession();
const [name, setName] = useState(account?.name ?? '');
const [saving, setSaving] = useState(false);
useEffect(() => {
setName(account?.name ?? '');
}, [account?.name]);
const roleLabel = account?.staffRole
? PARTNER_STAFF_ROLE_LABELS[account.staffRole as PartnerStaffRole] || account.staffRole
: '拓店账号';
async function handleSave() {
const trimmed = name.trim();
if (!trimmed) {
toastError('请输入姓名');
return;
}
if (trimmed === account?.name) {
toastSuccess('已保存');
return;
}
setSaving(true);
try {
await request('PARTNER_H5', '/partner/me', {
method: 'PUT',
body: JSON.stringify({ name: trimmed }),
});
await refresh();
toastSuccess('已保存');
} catch (e) {
toastError(e instanceof Error ? e.message : '保存失败');
} finally {
setSaving(false);
}
}
return (
<div className="page partner-me-page">
<section className="partner-profile-card" style={{ margin: '16px var(--space-page)' }}>
<div className="partner-profile-avatar">
<span className="material-symbols-outlined">person</span>
</div>
<div style={{ flex: 1 }}>
<h2 className="headline-lg" style={{ fontSize: 20 }}>{account?.name || '我的'}</h2>
<p className="label-md text-muted" style={{ marginTop: 4 }}>{roleLabel}</p>
{account?.companyName ? (
<p className="body-md text-muted" style={{ marginTop: 4 }}>{account.companyName}</p>
) : null}
</div>
</section>
<section className="partner-form-section" style={{ padding: '0 var(--space-page)' }}>
<label className="partner-form-label" htmlFor="me-name"></label>
<input
id="me-name"
className="partner-form-input"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="请输入姓名"
maxLength={32}
/>
<label className="partner-form-label" style={{ marginTop: 16 }}></label>
<input
className="partner-form-input"
value={account?.phone ?? ''}
readOnly
disabled
/>
<p className="label-md text-muted" style={{ marginTop: 4 }}></p>
</section>
<div style={{ padding: '24px var(--space-page) 100px' }}>
<button
type="button"
className="partner-btn-primary"
style={{ width: '100%' }}
disabled={saving}
onClick={() => void handleSave()}
>
{saving ? '保存中…' : '保存'}
</button>
<button
type="button"
className="partner-btn-outline"
style={{ width: '100%', marginTop: 12 }}
onClick={logout}
>
退
</button>
</div>
</div>
);
}
@@ -20,6 +20,7 @@ import { fetchPartnerCities, type OpenCityOption } from '../lib/upload';
import { isWechatEnv, weixinSdk } from '../lib/weixin';
import { usePartnerSession } from '../contexts/PartnerSessionContext';
import { isSubAccount } from '../lib/partnerAccess';
import {
@@ -79,6 +80,8 @@ export default function StoreCreatePage() {
const { account, refresh } = usePartnerSession();
const subAccountLayout = isSubAccount(account);
const accountId = account?.id;
const wechatReady = !!account?.hasWechat;
@@ -499,7 +502,7 @@ export default function StoreCreatePage() {
return (
<div className="partner-page-sticky">
<div className={`partner-page-sticky${subAccountLayout ? ' partner-page-sticky--tabbar' : ''}`}>
<PageHeader title="录入新门店" onBack={() => navigate('/stores')} />
@@ -915,7 +918,7 @@ export default function StoreCreatePage() {
<footer className="partner-sticky-footer">
<footer className={`partner-sticky-footer${subAccountLayout ? ' partner-sticky-footer--above-tabbar' : ''}`}>
{step > 1 && (
+9
View File
@@ -1290,6 +1290,15 @@ nav.app-tabbar .app-tabbar-label {
background: var(--color-surface);
}
.partner-page-sticky--tabbar {
padding-bottom: calc(88px + 56px + env(safe-area-inset-bottom, 0px));
}
.partner-sticky-footer--above-tabbar {
bottom: calc(56px + env(safe-area-inset-bottom, 0px));
z-index: 90;
}
.partner-stepper {
--step-circle-size: 32px;
padding: 16px var(--space-page) 0;