init
This commit is contained in:
3
web/.dockerignore
Normal file
3
web/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user