Files
dukang/apps/h5-partner/src/pages/StaffListPage.tsx
T
2026-07-09 23:52:00 +08:00

157 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import PageHeader from '@dukang/shared-ui/PageHeader';
import {
AccountStatus,
PARTNER_STAFF_ROLE_LABELS,
PartnerStaffRole,
type PartnerStaffItem,
} from '@dukang/shared-types';
import { deletePartnerStaff, listPartnerStaff, updatePartnerStaff } from '../lib/staff';
export default function StaffListPage() {
const navigate = useNavigate();
const [staff, setStaff] = useState<PartnerStaffItem[]>([]);
const [q, setQ] = useState('');
const [error, setError] = useState('');
const [busyId, setBusyId] = useState<string | null>(null);
const loadStaff = useCallback(() => {
return listPartnerStaff().then(setStaff).catch((e) => {
setError(e instanceof Error ? e.message : '加载失败');
});
}, []);
useEffect(() => {
void loadStaff();
}, [loadStaff]);
const filtered = useMemo(() => staff.filter((item) => {
if (!q.trim()) return true;
const keyword = q.trim();
return item.name.includes(keyword) || item.phone.includes(keyword);
}), [staff, q]);
async function toggleStatus(item: PartnerStaffItem) {
const next = item.status === AccountStatus.ACTIVE ? AccountStatus.DISABLED : AccountStatus.ACTIVE;
setBusyId(item.id);
setError('');
try {
await updatePartnerStaff(item.id, { status: next });
await loadStaff();
} catch (e) {
setError(e instanceof Error ? e.message : '操作失败');
} finally {
setBusyId(null);
}
}
async function removeStaff(item: PartnerStaffItem) {
const ok = window.confirm(`确认删除子账号「${item.name}」?`);
if (!ok) return;
setBusyId(item.id);
setError('');
try {
await deletePartnerStaff(item.id);
await loadStaff();
} catch (e) {
setError(e instanceof Error ? e.message : '删除失败');
} finally {
setBusyId(null);
}
}
async function editStaff(item: PartnerStaffItem) {
const name = window.prompt('修改姓名', item.name);
if (name == null || !name.trim()) return;
const roleOptions = Object.values(PartnerStaffRole);
const roleLabels = roleOptions.map((r) => PARTNER_STAFF_ROLE_LABELS[r]).join(' / ');
const roleInput = window.prompt(`分配角色(${roleLabels}`, item.staffRole);
if (roleInput == null) return;
const staffRole = roleOptions.find((r) => r === roleInput || PARTNER_STAFF_ROLE_LABELS[r] === roleInput);
if (!staffRole) {
setError('无效的角色');
return;
}
setBusyId(item.id);
setError('');
try {
await updatePartnerStaff(item.id, { name: name.trim(), staffRole });
await loadStaff();
} catch (e) {
setError(e instanceof Error ? e.message : '保存失败');
} finally {
setBusyId(null);
}
}
return (
<div className="page-no-tab partner-staff-page">
<PageHeader title="子账号管理" onBack={() => navigate('/center')} />
<div style={{ padding: '0 20px 16px' }}>
<div className="partner-search">
<span className="material-symbols-outlined">search</span>
<input placeholder="搜索姓名或手机号" value={q} onChange={(e) => setQ(e.target.value)} />
</div>
</div>
{error && <p className="partner-form-error" role="alert" style={{ margin: '0 20px 12px' }}>{error}</p>}
<div style={{ padding: '0 20px 100px' }}>
{filtered.length === 0 && (
<div className="empty">
<p>暂无子账号</p>
<p className="label-md text-muted">点击下方按钮添加首个子账号</p>
</div>
)}
{filtered.map((item) => {
const active = item.status === AccountStatus.ACTIVE;
const busy = busyId === item.id;
return (
<div key={item.id} className="partner-staff-card">
<div className="partner-staff-avatar">{item.name.slice(0, 1)}</div>
<div className="partner-staff-info">
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<span className="headline-md">{item.name}</span>
<span className="partner-role-badge partner-role-badge--subtle">
{PARTNER_STAFF_ROLE_LABELS[item.staffRole]}
</span>
</div>
<p className="label-md text-muted">{item.phone}</p>
</div>
<div className="partner-staff-actions">
<button
type="button"
className={`partner-toggle${active ? ' partner-toggle--on' : ''}`}
disabled={busy}
aria-label={active ? '禁用' : '启用'}
onClick={() => void toggleStatus(item)}
>
<span className="partner-toggle-thumb" />
</button>
<div style={{ display: 'flex', gap: 12 }}>
<button type="button" className="partner-icon-btn" disabled={busy} onClick={() => void editStaff(item)}>
<span className="material-symbols-outlined">edit</span>
</button>
<button type="button" className="partner-icon-btn partner-icon-btn--danger" disabled={busy} onClick={() => void removeStaff(item)}>
<span className="material-symbols-outlined">delete</span>
</button>
</div>
</div>
</div>
);
})}
</div>
<div className="partner-sticky-footer">
<Link to="/center/staff/new" className="partner-btn-primary" style={{ display: 'flex', textDecoration: 'none' }}>
<span className="material-symbols-outlined">person_add</span>
添加子账号
</Link>
</div>
</div>
);
}