gcx/frontend/admin-web/src/views/users/SmsLogPage.tsx

169 lines
5.6 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import { t } from '@/i18n/locales';
import { useApi } from '@/lib/use-api';
/**
* SMS 发送日志查看页面
*
* 管理员可按手机号搜索 SMS 发送记录
*/
interface SmsLogItem {
id: string;
phone: string;
type: string;
status: string;
provider: string;
createdAt: string;
}
interface SmsLogsResponse {
items: SmsLogItem[];
total: number;
}
const loadingBox: React.CSSProperties = {
display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: 40, color: 'var(--color-text-tertiary)', font: 'var(--text-body)',
};
export const SmsLogPage: React.FC = () => {
const [phone, setPhone] = useState('');
const [searchPhone, setSearchPhone] = useState('');
const [limit] = useState(20);
const [offset] = useState(0);
const { data, isLoading, error } = useApi<SmsLogsResponse>(
searchPhone ? '/api/v1/admin/sms/logs' : null,
{ params: { phone: searchPhone, limit, offset } },
);
const logs = data?.items ?? [];
const handleSearch = () => {
setSearchPhone(phone.trim());
};
const typeBadge = (type: string) => {
const map: Record<string, { bg: string; color: string; label: string }> = {
REGISTER: { bg: 'var(--color-primary)', color: 'white', label: t('sms_type_register') },
LOGIN: { bg: 'var(--color-info)', color: 'white', label: t('sms_type_login') },
RESET_PASSWORD: { bg: 'var(--color-warning)', color: 'white', label: t('sms_type_reset') },
CHANGE_PHONE: { bg: 'var(--color-success)', color: 'white', label: t('sms_type_change_phone') },
};
const m = map[type] ?? { bg: 'var(--color-gray-400)', color: 'white', label: type };
return (
<span style={{
padding: '2px 8px',
borderRadius: 'var(--radius-full)',
background: `${m.bg}20`,
color: m.bg,
font: 'var(--text-label-sm)',
fontWeight: 600,
}}>{m.label}</span>
);
};
const statusBadge = (status: string) => {
const isSuccess = status === 'SUCCESS' || status === 'SENT';
return (
<span style={{
padding: '2px 8px',
borderRadius: 'var(--radius-full)',
background: isSuccess ? 'var(--color-success-light)' : 'var(--color-error-light)',
color: isSuccess ? 'var(--color-success)' : 'var(--color-error)',
font: 'var(--text-label-sm)',
fontWeight: 600,
}}>{isSuccess ? t('sms_status_sent') : t('sms_status_failed')}</span>
);
};
return (
<div>
<h1 style={{ font: 'var(--text-h1)', marginBottom: 24 }}>{t('sms_log_title')}</h1>
{/* Search */}
<div style={{ display: 'flex', gap: 12, marginBottom: 20 }}>
<input
placeholder={t('sms_search_placeholder')}
value={phone}
onChange={e => setPhone(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleSearch()}
style={{
flex: 1,
maxWidth: 360,
height: 40,
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-sm)',
padding: '0 16px',
font: 'var(--text-body)',
}}
/>
<button
onClick={handleSearch}
style={{
padding: '8px 20px',
border: 'none',
borderRadius: 'var(--radius-sm)',
background: 'var(--color-primary)',
color: 'white',
cursor: 'pointer',
font: 'var(--text-body)',
}}
>{t('search')}</button>
</div>
{/* Hint when no search */}
{!searchPhone && (
<div style={loadingBox}>{t('sms_search_hint')}</div>
)}
{/* Table */}
{searchPhone && (error ? (
<div style={loadingBox}>Error: {error.message}</div>
) : isLoading ? (
<div style={loadingBox}>Loading...</div>
) : (
<div style={{
background: 'var(--color-surface)',
borderRadius: 'var(--radius-md)',
border: '1px solid var(--color-border-light)',
overflow: 'hidden',
}}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: 'var(--color-gray-50)', borderBottom: '1px solid var(--color-border)' }}>
{[t('sms_log_phone'), t('sms_log_type'), t('sms_log_status'), t('sms_log_provider'), t('sms_log_time')].map(h => (
<th key={h} style={{
font: 'var(--text-label-sm)',
color: 'var(--color-text-tertiary)',
padding: '12px 14px',
textAlign: 'left',
}}>{h}</th>
))}
</tr>
</thead>
<tbody>
{logs.length === 0 ? (
<tr>
<td colSpan={5} style={{ ...loadingBox, padding: '24px 14px' }}>{t('sms_no_logs')}</td>
</tr>
) : logs.map(log => (
<tr key={log.id} style={{ borderBottom: '1px solid var(--color-border-light)' }}>
<td style={{ font: 'var(--text-body-sm)', padding: '12px 14px' }}>{log.phone}</td>
<td style={{ padding: '12px 14px' }}>{typeBadge(log.type)}</td>
<td style={{ padding: '12px 14px' }}>{statusBadge(log.status)}</td>
<td style={{ font: 'var(--text-body-sm)', color: 'var(--color-text-tertiary)', padding: '12px 14px' }}>{log.provider}</td>
<td style={{ font: 'var(--text-body-sm)', color: 'var(--color-text-tertiary)', padding: '12px 14px' }}>{log.createdAt}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
);
};