157 lines
5.8 KiB
TypeScript
157 lines
5.8 KiB
TypeScript
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>
|
||
);
|
||
}
|