This commit is contained in:
2026-04-28 22:04:24 +08:00
parent 80ee99e564
commit 71c940ab46
156 changed files with 5700 additions and 304 deletions

3
web/.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
dist
.env

View File

@@ -10,7 +10,7 @@ server {
}
location /api/ {
proxy_pass http://host.docker.internal:3000;
proxy_pass http://server:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}

View File

@@ -5,7 +5,6 @@ function authHeaders() { return { 'Authorization': `Bearer ${token()}`, 'Content
export interface SmsMessage {
id: number;
user_id: number;
phone_number: string;
contact_name: string | null;
content: string;
@@ -31,21 +30,11 @@ async function parseError(res: Response): Promise<string> {
}
}
export async function login(username: string, password: string) {
const res = await fetch(`${BASE}/auth/login`, {
export async function verifyToken(t: string) {
const res = await fetch(`${BASE}/auth/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify({ username, password }),
});
if (!res.ok) throw new Error(await parseError(res));
return res.json();
}
export async function register(username: string, password: string) {
const res = await fetch(`${BASE}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify({ username, password }),
body: JSON.stringify({ token: t }),
});
if (!res.ok) throw new Error(await parseError(res));
return res.json();

View File

@@ -1,31 +1,20 @@
import { useState } from 'react';
import { login, register } from '../api';
import { verifyToken } from '../api';
import './Login.css';
interface Props { onLogin: (token: string) => void }
export default function Login({ onLogin }: Props) {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [tokenInput, setTokenInput] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
async function doLogin() {
async function doVerify() {
setError(''); setLoading(true);
try {
const data = await login(username, password);
localStorage.setItem('token', data.token);
onLogin(data.token);
} catch (e: any) { setError(e.message); }
setLoading(false);
}
async function doRegister() {
setError(''); setLoading(true);
try {
const data = await register(username, password);
localStorage.setItem('token', data.token);
onLogin(data.token);
await verifyToken(tokenInput);
localStorage.setItem('token', tokenInput);
onLogin(tokenInput);
} catch (e: any) { setError(e.message); }
setLoading(false);
}
@@ -42,19 +31,11 @@ export default function Login({ onLogin }: Props) {
<div className="login-fields">
<div className="field">
<span className="material-symbols-outlined field-icon">person</span>
<span className="material-symbols-outlined field-icon">key</span>
<input
type="text" placeholder="用户名" value={username}
onChange={e => setUsername(e.target.value)}
onKeyDown={e => e.key === 'Enter' && doLogin()}
/>
</div>
<div className="field">
<span className="material-symbols-outlined field-icon">lock</span>
<input
type="password" placeholder="密码" value={password}
onChange={e => setPassword(e.target.value)}
onKeyDown={e => e.key === 'Enter' && doLogin()}
type="password" placeholder="Access Token" value={tokenInput}
onChange={e => setTokenInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && doVerify()}
/>
</div>
</div>
@@ -67,13 +48,9 @@ export default function Login({ onLogin }: Props) {
)}
<div className="login-buttons">
<button className="btn-primary" onClick={doLogin} disabled={loading}>
<button className="btn-primary" onClick={doVerify} disabled={loading}>
<span className="material-symbols-outlined">login</span>
</button>
<button className="btn-ghost" onClick={doRegister} disabled={loading}>
<span className="material-symbols-outlined">person_add</span>
</button>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useState, useCallback } from 'react';
import { fetchSms, deleteSms, SmsMessage } from '../api';
import './SmsList.css';
@@ -6,39 +6,31 @@ interface Props { onLogout: () => void }
export default function SmsList({ onLogout }: Props) {
const [messages, setMessages] = useState<SmsMessage[]>([]);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [phone, setPhone] = useState('');
const [type, setType] = useState('');
const [detail, setDetail] = useState<SmsMessage | null>(null);
async function load(p: number) {
async function load() {
setLoading(true);
try {
const params: Record<string, string> = { page: String(p), limit: '20' };
const params: Record<string, string> = { limit: '999999' };
if (phone) params.phone = phone;
if (type) params.type = type;
const data = await fetchSms(params);
setMessages(data.messages);
setTotalPages(data.totalPages);
setTotal(data.total);
} catch (e) { console.error(e); }
setLoading(false);
}
useEffect(() => {
load(1);
setPage(1);
}, [phone, type]);
useEffect(() => { load(page); }, [page]);
useEffect(() => { load(); }, []);
async function handleDelete(id: number) {
await deleteSms(id);
if (detail?.id === id) setDetail(null);
load(page);
load();
}
function formatDate(d: string) {
@@ -69,14 +61,14 @@ export default function SmsList({ onLogout }: Props) {
<input
placeholder="按手机号筛选..." value={phone}
onChange={e => setPhone(e.target.value)}
onKeyDown={e => e.key === 'Enter' && (setPage(1), load(1))}
onKeyDown={e => e.key === 'Enter' && load()}
/>
<select value={type} onChange={e => setType(e.target.value)}>
<option value=""></option>
<option value="received"></option>
<option value="sent"></option>
</select>
<button className="btn-primary" onClick={() => { setPage(1); load(1); }}>
<button className="btn-primary" onClick={load}>
<span className="material-symbols-outlined">search</span>
</button>
@@ -159,19 +151,6 @@ export default function SmsList({ onLogout }: Props) {
</aside>
)}
</main>
<footer className="pagination">
<span className="pagination-info"> {total} </span>
<div className="pagination-ctrl">
<button className="btn-icon" disabled={page <= 1} onClick={() => setPage(p => p - 1)}>
<span className="material-symbols-outlined">chevron_left</span>
</button>
<span className="page-num">{page} / {totalPages || 1}</span>
<button className="btn-icon" disabled={page >= totalPages} onClick={() => setPage(p => p + 1)}>
<span className="material-symbols-outlined">chevron_right</span>
</button>
</div>
</footer>
</div>
);
}