diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx index 0e9ab9e..3bc334e 100644 --- a/apps/admin-web/src/App.tsx +++ b/apps/admin-web/src/App.tsx @@ -28,6 +28,7 @@ import UserLogsPage from './pages/UserLogsPage'; import HqLogsPage from './pages/HqLogsPage'; import ThirdPartyLogsPage from './pages/ThirdPartyLogsPage'; import StoreLogsPage from './pages/StoreLogsPage'; +import PartnerLogsPage from './pages/PartnerLogsPage'; function RequireAuth({ children }: { children: React.ReactNode }) { if (!getToken()) return ; @@ -67,6 +68,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx index 1ce7031..5a71690 100644 --- a/apps/admin-web/src/layouts/AdminLayout.tsx +++ b/apps/admin-web/src/layouts/AdminLayout.tsx @@ -74,6 +74,7 @@ const MENU_ITEMS: MenuProps['items'] = [ children: [ { key: '/logs/users', label: '用户日志' }, { key: '/logs/stores', label: '商户日志' }, + { key: '/logs/partners', label: '合伙人日志' }, { key: '/logs/hq', label: 'HQ 操作日志' }, { key: '/logs/third-party', label: '第三方日志' }, ], diff --git a/apps/admin-web/src/lib/partner-log.ts b/apps/admin-web/src/lib/partner-log.ts new file mode 100644 index 0000000..9d9d545 --- /dev/null +++ b/apps/admin-web/src/lib/partner-log.ts @@ -0,0 +1,6 @@ +export { + PARTNER_LOG_CATEGORY_OPTIONS, + PARTNER_LOG_CATEGORY_LABELS, + resolvePartnerLogCategory, + type PartnerLogCategory, +} from '@dukang/shared-types'; diff --git a/apps/admin-web/src/pages/PartnerLogsPage.tsx b/apps/admin-web/src/pages/PartnerLogsPage.tsx new file mode 100644 index 0000000..9580874 --- /dev/null +++ b/apps/admin-web/src/pages/PartnerLogsPage.tsx @@ -0,0 +1,222 @@ +import { useEffect, useState } from 'react'; +import { Button, Descriptions, Drawer, Form, Input, Segmented, Space, Table, Tag, Typography } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { useSearchParams } from 'react-router-dom'; +import { + PARTNER_LOG_CATEGORY_OPTIONS, + PARTNER_LOG_CATEGORY_LABELS, + resolvePartnerLogCategory, + type PartnerLogCategory, +} from '../lib/partner-log'; +import { request } from '../lib/api'; +import { fmtTime } from '../lib/constants'; +import { useAdminList } from '../lib/useAdminList'; + +type Row = { + id: string; + partnerId: string; + partnerAccountId: string | null; + accountName: string | null; + accountPhone: string | null; + companyName: string | null; + category: PartnerLogCategory | null; + eventName: string; + clientApp: string | null; + refType: string | null; + refId: string | null; + extraJson: Record | null; + createdAt: string; +}; + +function summarizeExtra(json: Record | null) { + if (!json) return '—'; + const text = JSON.stringify(json); + return text.length > 80 ? `${text.slice(0, 80)}…` : text; +} + +export default function PartnerLogsPage() { + const [searchParams, setSearchParams] = useSearchParams(); + const [form] = Form.useForm(); + const [category, setCategory] = useState(searchParams.get('category') ?? ''); + const [filters, setFilters] = useState>(() => ({ + partnerId: searchParams.get('partnerId') ?? '', + partnerAccountId: searchParams.get('partnerAccountId') ?? '', + phone: searchParams.get('phone') ?? '', + companyName: searchParams.get('companyName') ?? '', + eventName: searchParams.get('eventName') ?? '', + })); + const { data, loading, page, pageSize, setPage, setPageSize } = useAdminList( + '/admin/logs/partners', + () => { + const qs = new URLSearchParams(); + if (filters.partnerId) qs.set('partnerId', filters.partnerId); + if (filters.partnerAccountId) qs.set('partnerAccountId', filters.partnerAccountId); + if (filters.phone) qs.set('phone', filters.phone); + if (filters.companyName) qs.set('companyName', filters.companyName); + if (filters.eventName) qs.set('eventName', filters.eventName); + if (category) qs.set('category', category); + return qs; + }, + [filters, category], + ); + const [detail, setDetail] = useState | null>(null); + const [drawerOpen, setDrawerOpen] = useState(false); + + useEffect(() => { + form.setFieldsValue(filters); + }, [form, filters]); + + const columns: ColumnsType = [ + { title: '时间', dataIndex: 'createdAt', width: 160, render: fmtTime }, + { + title: '合伙人', + width: 180, + render: (_, r) => ( +
+
{r.companyName || '—'}
+
{r.partnerId}
+
+ ), + }, + { + title: '账号', + width: 150, + render: (_, r) => ( +
+
{r.accountName || '—'}
+
{r.accountPhone || r.partnerAccountId || '—'}
+
+ ), + }, + { + title: '分类', + dataIndex: 'category', + width: 100, + render: (v: PartnerLogCategory | null, r) => ( + {PARTNER_LOG_CATEGORY_LABELS[v ?? ''] || resolvePartnerLogCategory(r.eventName) || '其他'} + ), + }, + { title: '事件', dataIndex: 'eventName', width: 180 }, + { + title: '关联', + width: 120, + render: (_, r) => (r.refType && r.refId ? `${r.refType}#${r.refId}` : '—'), + }, + { + title: '摘要', + ellipsis: true, + render: (_, r) => summarizeExtra(r.extraJson), + }, + { + title: '操作', + width: 80, + render: (_, row) => ( + + ), + }, + ]; + + return ( +
+ + 合伙人日志 + + ({ value: o.value, label: o.label }))} + value={category} + onChange={(v) => { + setCategory(String(v)); + setPage(1); + setSearchParams((prev) => { + const next = new URLSearchParams(prev); + if (v) next.set('category', String(v)); + else next.delete('category'); + return next; + }); + }} + style={{ marginBottom: 16 }} + /> +
{ + setFilters(values); + setPage(1); + }} + > + + + + + + + + + + + + + + + + + + +
+ { + setPage(p); + setPageSize(ps); + }, + }} + scroll={{ x: 1100 }} + /> + setDrawerOpen(false)} width={520}> + {detail && ( + + {Object.entries(detail).map(([k, v]) => ( + + {typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v ?? '—')} + + ))} + + )} + + + ); +} diff --git a/apps/h5-partner/src/pages/LoginPage.tsx b/apps/h5-partner/src/pages/LoginPage.tsx index eeb810c..9628b3a 100644 --- a/apps/h5-partner/src/pages/LoginPage.tsx +++ b/apps/h5-partner/src/pages/LoginPage.tsx @@ -23,12 +23,26 @@ function loadRememberedPhone(): { phone: string; remember: boolean } { } } +function formatPartnerError(e: unknown): string { + const text = e instanceof Error ? e.message : '操作失败'; + if (text.includes('合伙人账号不存在') || text.includes('未找到合伙人账号')) { + return '未找到合伙人账号'; + } + return text; +} + export default function LoginPage() { const navigate = useNavigate(); const [params] = useSearchParams(); const quick = params.get('quick') === '1'; const remembered = loadRememberedPhone(); + const [step, setStep] = useState<'phone' | 'code'>('phone'); const [phone, setPhone] = useState(remembered.phone || '13700000001'); + const [confirmedProfile, setConfirmedProfile] = useState<{ + maskedPhone: string; + name?: string; + companyName?: string; + } | null>(null); const [code, setCode] = useState('123456'); const [rememberAccount, setRememberAccount] = useState(remembered.remember); const [agreed, setAgreed] = useState(true); @@ -77,6 +91,37 @@ export default function LoginPage() { } } + function backToPhoneStep() { + setStep('phone'); + setConfirmedProfile(null); + setCode(''); + setMsg(''); + setCodeCooldown(0); + } + + async function confirmPhone() { + if (!ensureAgreed()) return; + setLoading(true); + setMsg(''); + try { + const data = await request<{ ok: boolean; maskedPhone: string; name?: string; companyName?: string }>( + 'PARTNER_H5', + '/partner/auth/phone/check', + { method: 'POST', body: JSON.stringify({ phone }) }, + ); + setConfirmedProfile({ + maskedPhone: data.maskedPhone, + name: data.name, + companyName: data.companyName, + }); + setStep('code'); + } catch (e) { + setMsg(formatPartnerError(e)); + } finally { + setLoading(false); + } + } + async function sendCode() { if (!ensureAgreed()) return; setMsg(''); @@ -97,7 +142,7 @@ export default function LoginPage() { }); }, 1000); } catch (e) { - setMsg(e instanceof Error ? e.message : '发送失败'); + setMsg(formatPartnerError(e)); } } @@ -106,10 +151,6 @@ export default function LoginPage() { setLoading(true); setMsg(''); try { - await request('PARTNER_H5', '/partner/auth/sms/send', { - method: 'POST', - body: JSON.stringify({ phone, scene: 'PARTNER_LOGIN' }), - }); const data = await request<{ accessToken: string }>('PARTNER_H5', '/partner/auth/login/sms', { method: 'POST', body: JSON.stringify({ phone, code }), @@ -123,7 +164,7 @@ export default function LoginPage() { } navigate('/'); } catch (e) { - setMsg(e instanceof Error ? e.message : '登录失败'); + setMsg(formatPartnerError(e)); } finally { setLoading(false); } @@ -203,39 +244,83 @@ export default function LoginPage() {

城市合伙人登录

-
- smartphone - setPhone(e.target.value)} /> -
-
-
- shield - setCode(e.target.value)} /> -
- -
- - + {step === 'phone' ? ( + <> +
+ smartphone + { + setPhone(e.target.value); + setMsg(''); + }} + /> +
+ + + ) : ( + <> +
+
+
+
+ {confirmedProfile?.name || '合伙人账号'} + {confirmedProfile?.companyName ? ( + + {confirmedProfile.companyName} + + ) : null} +
+
+ {confirmedProfile?.maskedPhone || phone} +
+
+ +
+
+
+
+ shield + setCode(e.target.value)} /> +
+ +
+ + + + )} {msg &&

{msg}

} -
其他登录方式
- + {step === 'phone' && ( + <> +
其他登录方式
+ + + )}