import { useCallback, useEffect, useState } from 'react'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import PullToRefresh from '@dukang/shared-ui/PullToRefresh'; import type { PartnerAssocSummary, PartnerAssocUserItem, PartnerAssocUserSort, } from '@dukang/shared-types'; import { request } from '../lib/api'; import { downloadBlob, fetchActivityPosterImage, fetchAssocQrcodeImage, } from '../lib/activity-posters'; import { toastError, toastSuccess } from '../lib/toast'; import { blobToDataUrl, isHttpImageUrl, previewSaveableImage } from '../lib/wechat-save-image'; import { isWechatEnv } from '../lib/weixin'; import { usePartnerPageView } from '../lib/usePageView'; import { usePartnerSession } from '../contexts/PartnerSessionContext'; import { isSubAccount } from '../lib/partnerAccess'; type ListRes = { items: PartnerAssocUserItem[]; total: number }; const SORTS: { key: PartnerAssocUserSort; label: string }[] = [ { key: 'boundAt', label: '关联时间' }, { key: 'createdAt', label: '注册时间' }, { key: 'orderCount', label: '订单数' }, ]; function formatUserLabel(u: PartnerAssocUserItem) { const name = u.nickname?.trim() || u.userNo || '—'; const remark = u.partnerRemark?.trim(); return remark ? `${name}(${remark})` : name; } function fmtTime(iso?: string | null) { if (!iso) return '—'; return iso.slice(0, 16).replace('T', ' '); } export default function UsersManagePage() { usePartnerPageView('partner_users_manage_view'); const { account } = usePartnerSession(); const hidePoster = isSubAccount(account); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [summary, setSummary] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [heroUrl, setHeroUrl] = useState(null); const [heroLoading, setHeroLoading] = useState(false); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [keyword, setKeyword] = useState(''); const [appliedKeyword, setAppliedKeyword] = useState(''); const [sort, setSort] = useState('boundAt'); const [editing, setEditing] = useState(null); const [remarkDraft, setRemarkDraft] = useState(''); const [saving, setSaving] = useState(false); useEffect(() => { document.title = '用户管理'; }, []); useEffect(() => () => { if (previewUrl?.startsWith('blob:')) URL.revokeObjectURL(previewUrl); }, [previewUrl]); useEffect(() => { const posterId = hidePoster ? null : summary?.activityPosterId; if (!posterId) { setHeroUrl(null); setHeroLoading(false); return; } let cancelled = false; const acc = { url: null as string | null }; setHeroLoading(true); fetchActivityPosterImage(posterId) .then((blob) => { if (cancelled) return; acc.url = URL.createObjectURL(blob); setHeroUrl(acc.url); }) .catch(() => { if (!cancelled) setHeroUrl(null); }) .finally(() => { if (!cancelled) setHeroLoading(false); }); return () => { cancelled = true; if (acc.url) URL.revokeObjectURL(acc.url); }; }, [hidePoster, summary?.activityPosterId]); const loadSummary = useCallback(async () => { const data = await request('PARTNER_H5', '/partner/assoc'); setSummary(data); }, []); const loadUsers = useCallback(async () => { const qs = new URLSearchParams({ page: '1', pageSize: '50', sort }); const q = appliedKeyword.trim(); if (q) qs.set('keyword', q); const res = await request('PARTNER_H5', `/partner/assoc/users?${qs}`); setItems(res.items ?? []); setTotal(res.total ?? 0); }, [appliedKeyword, sort]); const load = useCallback(async () => { setLoading(true); try { await Promise.all([loadSummary(), loadUsers()]); } catch (e) { toastError(e instanceof Error ? e.message : '加载失败'); } finally { setLoading(false); } }, [loadSummary, loadUsers]); useEffect(() => { void load(); }, [load]); useEffect(() => { const t = window.setTimeout(() => setAppliedKeyword(keyword.trim()), 400); return () => window.clearTimeout(t); }, [keyword]); useEffect(() => { const uid = searchParams.get('userId'); if (uid) navigate(`/users/${uid}/orders`, { replace: true }); }, [searchParams, navigate]); async function downloadMainImage() { const posterId = hidePoster ? null : summary?.activityPosterId; try { if (isWechatEnv() && !posterId && isHttpImageUrl(summary?.qrcodeUrl)) { await previewSaveableImage(summary.qrcodeUrl); return; } const blob = posterId ? await fetchActivityPosterImage(posterId) : await fetchAssocQrcodeImage(); if (isWechatEnv()) { if (previewUrl?.startsWith('blob:')) URL.revokeObjectURL(previewUrl); setPreviewUrl(await blobToDataUrl(blob)); toastSuccess('请长按上方图片保存到相册'); return; } downloadBlob(blob, posterId ? `activity-poster-${posterId}.png` : `partner-assoc-${summary?.partnerId || 'qr'}.png`); toastSuccess('已开始下载'); } catch (e) { if (!posterId && isHttpImageUrl(summary?.qrcodeUrl)) { if (isWechatEnv()) { await previewSaveableImage(summary.qrcodeUrl); return; } setPreviewUrl(summary.qrcodeUrl); toastSuccess('请长按图片保存到相册'); return; } toastError(e instanceof Error ? e.message : '下载失败'); } } async function saveRemark() { if (!editing) return; setSaving(true); try { await request(`PARTNER_H5`, `/partner/assoc/users/${editing.id}/remark`, { method: 'PUT', body: JSON.stringify({ remark: remarkDraft }), }); toastSuccess(remarkDraft.trim() ? '备注已保存' : '已清除备注'); setEditing(null); await loadUsers(); } catch (e) { toastError(e instanceof Error ? e.message : '保存失败'); } finally { setSaving(false); } } return (

用户管理

用户扫码后首次锁定,后续购酒计入关联订单

{summary?.activityPosterId && !hidePoster && (previewUrl || heroUrl) ? ( 活动图 ) : summary?.activityPosterId && !hidePoster && heroLoading ? (

正在生成活动图…

) : previewUrl || summary?.qrcodeUrl ? ( 关联码 ) : (

{loading ? '加载中…' : '关联码尚未生成'}

)} {(() => { const scanCount = summary?.scanCount ?? 0; const userCount = summary?.userCount ?? total; if (scanCount <= 0 && userCount <= 0) return null; return (

已扫码 {scanCount} 人 · 已关联 {userCount} 人

); })()} {isWechatEnv() && (previewUrl || summary?.qrcodeUrl) ? (

微信内请点「{summary?.activityPosterId && !hidePoster ? '下载活动图' : '下载二维码'}」后长按保存

) : null}
{!hidePoster && (
image
活动图
chevron_right
)}

已关联用户

search setKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') void loadUsers(); }} />
{SORTS.map((s) => ( ))}

{loading ? '加载中…' : `共 ${total} 人`}

{!loading && items.length === 0 &&
暂无关联用户
} {items.map((u) => (

{formatUserLabel(u)}

{u.phone || '未绑定手机'}

注册 {fmtTime(u.createdAt)}

))}
{editing && (
setEditing(null)}>
e.stopPropagation()}>

用户备注

仅自己可见,总部看不到