mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-08 22:44:43 +08:00
release: v1.1.5
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { UserCog, Key, LogIn, Monitor, Clock, Globe } from 'lucide-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Clock, Globe, LogIn, Monitor, UserCog } from 'lucide-react'
|
||||
import {
|
||||
changePassword,
|
||||
changeUsername,
|
||||
@@ -20,7 +20,6 @@ export default function Settings() {
|
||||
const [oldPwd, setOldPwd] = useState('')
|
||||
const [newPwd, setNewPwd] = useState('')
|
||||
const [newUsername, setNewUsername] = useState('')
|
||||
const [pwdForUser, setPwdForUser] = useState('')
|
||||
|
||||
const fetchLogs = useCallback(async () => {
|
||||
try {
|
||||
@@ -33,30 +32,45 @@ export default function Settings() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => { fetchLogs(); const t = setInterval(fetchLogs, 15000); return () => clearInterval(t) }, [fetchLogs])
|
||||
useEffect(() => {
|
||||
fetchLogs()
|
||||
const timer = setInterval(fetchLogs, 15000)
|
||||
return () => clearInterval(timer)
|
||||
}, [fetchLogs])
|
||||
|
||||
const handleSaveAccount = async () => {
|
||||
if (!oldPwd) { dialog.alert('提示', '请输入当前密码以确认修改'); return }
|
||||
if (!newPwd && !newUsername) { dialog.alert('提示', '至少填写新密码或新用户名中的一项'); return }
|
||||
if (newPwd && newPwd.length < 6) { dialog.alert('提示', '新密码至少 6 位'); return }
|
||||
if (newUsername && newUsername.length < 3) { dialog.alert('提示', '用户名至少 3 位'); return }
|
||||
if (!oldPwd) {
|
||||
dialog.alert('提示', '请输入当前密码以确认修改')
|
||||
return
|
||||
}
|
||||
if (!newPwd && !newUsername) {
|
||||
dialog.alert('提示', '至少填写新密码或新用户名中的一项')
|
||||
return
|
||||
}
|
||||
if (newPwd && newPwd.length < 6) {
|
||||
dialog.alert('提示', '新密码至少 6 位')
|
||||
return
|
||||
}
|
||||
if (newUsername && newUsername.length < 3) {
|
||||
dialog.alert('提示', '用户名至少 3 位')
|
||||
return
|
||||
}
|
||||
|
||||
let results: string[] = []
|
||||
const results: string[] = []
|
||||
try {
|
||||
// 先改用户名(用旧密码验证),再改密码,否则改完密码后旧密码就失效了
|
||||
if (newUsername) {
|
||||
const res = await changeUsername(newUsername, oldPwd)
|
||||
if (res.data.success) results.push('用户名已修改')
|
||||
else results.push('用户名修改失败')
|
||||
results.push(res.data.success ? '用户名已修改' : '用户名修改失败')
|
||||
}
|
||||
if (newPwd) {
|
||||
const res = await changePassword(oldPwd, newPwd)
|
||||
if (res.data.success) results.push('密码已修改')
|
||||
else results.push('密码修改失败')
|
||||
results.push(res.data.success ? '密码已修改' : '密码修改失败')
|
||||
}
|
||||
if (results.length > 0) {
|
||||
dialog.alert('完成', results.join(',') + '。下次登录生效')
|
||||
setOldPwd(''); setNewPwd(''); setNewUsername('')
|
||||
dialog.alert('完成', `${results.join(',')}。下次登录生效`)
|
||||
setOldPwd('')
|
||||
setNewPwd('')
|
||||
setNewUsername('')
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const e = err as { response?: { data?: { message?: string } } }
|
||||
@@ -67,48 +81,48 @@ export default function Settings() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-black"></div>
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-black"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(logs.length / pageSize)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-black">面板设置</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">账号管理与登录日志</p>
|
||||
<p className="mt-1 text-sm text-gray-500">账号管理与登录日志</p>
|
||||
</div>
|
||||
|
||||
{/* Account Settings */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-5">
|
||||
<h2 className="text-sm font-semibold text-black mb-4 flex items-center gap-2">
|
||||
<UserCog className="w-4 h-4" />账号设置
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-5">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-sm font-semibold text-black">
|
||||
<UserCog className="h-4 w-4" />账号设置
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">当前用户名</label>
|
||||
<input type="text" value={username || ''} disabled className="w-full px-3 py-2 border border-gray-200 rounded-md text-sm text-gray-400 bg-gray-50" />
|
||||
<label className="mb-1 block text-xs text-gray-500">当前用户名</label>
|
||||
<input type="text" value={username || ''} disabled className="w-full rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-400" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">新用户名(留空则不修改)</label>
|
||||
<input type="text" value={newUsername} onChange={(e) => setNewUsername(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm text-black bg-white" placeholder="至少 3 位" />
|
||||
<label className="mb-1 block text-xs text-gray-500">新用户名(留空则不修改)</label>
|
||||
<input type="text" value={newUsername} onChange={(e) => setNewUsername(e.target.value)} className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-black" placeholder="至少 3 位" />
|
||||
</div>
|
||||
<div className="border-t border-gray-100 pt-3">
|
||||
<label className="block text-xs text-gray-500 mb-1">新密码(留空则不修改)</label>
|
||||
<input type="password" value={newPwd} onChange={(e) => setNewPwd(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm text-black bg-white" placeholder="至少 6 位" />
|
||||
<label className="mb-1 block text-xs text-gray-500">新密码(留空则不修改)</label>
|
||||
<input type="password" value={newPwd} onChange={(e) => setNewPwd(e.target.value)} className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-black" placeholder="至少 6 位" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-gray-500 mb-1">当前密码(验证身份)</label>
|
||||
<input type="password" value={oldPwd} onChange={(e) => setOldPwd(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm text-black bg-white" placeholder="输入当前密码以确认修改" />
|
||||
<label className="mb-1 block text-xs text-gray-500">当前密码(验证身份)</label>
|
||||
<input type="password" value={oldPwd} onChange={(e) => setOldPwd(e.target.value)} className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-black" placeholder="输入当前密码以确认修改" />
|
||||
</div>
|
||||
<button onClick={handleSaveAccount} className="w-full px-4 py-2 bg-black text-white rounded-md text-sm hover:bg-gray-800">保存修改</button>
|
||||
<button onClick={handleSaveAccount} className="w-full rounded-md bg-black px-4 py-2 text-sm text-white hover:bg-gray-800">保存修改</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Login Logs */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-5">
|
||||
<h2 className="text-sm font-semibold text-black mb-4 flex items-center gap-2">
|
||||
<LogIn className="w-4 h-4" />登录日志
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-5">
|
||||
<h2 className="mb-4 flex items-center gap-2 text-sm font-semibold text-black">
|
||||
<LogIn className="h-4 w-4" />登录日志
|
||||
</h2>
|
||||
{logs.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">暂无登录记录</p>
|
||||
@@ -117,23 +131,23 @@ export default function Settings() {
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-gray-400 border-b border-gray-100">
|
||||
<th className="text-left py-2 font-medium w-40"><span className="inline-flex items-center gap-1"><Clock className="w-3 h-3" />时间</span></th>
|
||||
<th className="text-left py-2 font-medium">用户名</th>
|
||||
<th className="text-left py-2 font-medium"><span className="inline-flex items-center gap-1"><Globe className="w-3 h-3" />IP</span></th>
|
||||
<th className="text-left py-2 font-medium"><span className="inline-flex items-center gap-1"><Monitor className="w-3 h-3" />设备</span></th>
|
||||
<th className="text-left py-2 font-medium">结果</th>
|
||||
<tr className="border-b border-gray-100 text-gray-400">
|
||||
<th className="w-40 py-2 text-left font-medium"><span className="inline-flex items-center gap-1"><Clock className="h-3 w-3" />时间</span></th>
|
||||
<th className="py-2 text-left font-medium">用户名</th>
|
||||
<th className="py-2 text-left font-medium"><span className="inline-flex items-center gap-1"><Globe className="h-3 w-3" />IP</span></th>
|
||||
<th className="py-2 text-left font-medium"><span className="inline-flex items-center gap-1"><Monitor className="h-3 w-3" />设备</span></th>
|
||||
<th className="py-2 text-left font-medium">结果</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{logs.slice((logPage - 1) * pageSize, logPage * pageSize).map((log, i) => (
|
||||
<tr key={i}>
|
||||
<td className="py-1.5 text-gray-500 font-mono whitespace-nowrap">{log.time}</td>
|
||||
{logs.slice((logPage - 1) * pageSize, logPage * pageSize).map((log, index) => (
|
||||
<tr key={`${log.time}-${index}`}>
|
||||
<td className="whitespace-nowrap py-1.5 font-mono text-gray-500">{log.time}</td>
|
||||
<td className="py-1.5 text-gray-700">{log.username}</td>
|
||||
<td className="py-1.5 text-gray-500 font-mono">{log.ip}</td>
|
||||
<td className="py-1.5 text-gray-500 max-w-[180px] truncate" title={log.user_agent}>{formatUA(log.user_agent)}</td>
|
||||
<td className="py-1.5 font-mono text-gray-500">{log.ip}</td>
|
||||
<td className="max-w-[180px] truncate py-1.5 text-gray-500" title={log.user_agent}>{formatUA(log.user_agent)}</td>
|
||||
<td className="py-1.5">
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${log.success ? 'bg-gray-100 text-gray-700' : 'bg-red-50 text-red-600'}`}>
|
||||
<span className={`rounded px-1.5 py-0.5 text-xs ${log.success ? 'bg-gray-100 text-gray-700' : 'bg-red-50 text-red-600'}`}>
|
||||
{log.success ? '成功' : '失败'}
|
||||
</span>
|
||||
</td>
|
||||
@@ -143,23 +157,22 @@ export default function Settings() {
|
||||
</table>
|
||||
</div>
|
||||
{logs.length > pageSize && (
|
||||
<div className="flex items-center justify-between mt-3 pt-3 border-t border-gray-100">
|
||||
<span className="text-xs text-gray-400">共 {logs.length} 条,第 {logPage}/{Math.ceil(logs.length / pageSize)} 页</span>
|
||||
<div className="mt-3 flex items-center justify-between border-t border-gray-100 pt-3">
|
||||
<span className="text-xs text-gray-400">共 {logs.length} 条,第 {logPage}/{totalPages} 页</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={() => setLogPage(1)} disabled={logPage === 1} className="px-2 py-1 text-xs border border-gray-200 rounded hover:bg-gray-50 disabled:opacity-30">首页</button>
|
||||
<button onClick={() => setLogPage(p => Math.max(1, p - 1))} disabled={logPage === 1} className="px-2 py-1 text-xs border border-gray-200 rounded hover:bg-gray-50 disabled:opacity-30">上一页</button>
|
||||
{Array.from({length: Math.min(5, Math.ceil(logs.length / pageSize))}, (_, i) => {
|
||||
const totalPages = Math.ceil(logs.length / pageSize)
|
||||
<button onClick={() => setLogPage(1)} disabled={logPage === 1} className="rounded border border-gray-200 px-2 py-1 text-xs hover:bg-gray-50 disabled:opacity-30">首页</button>
|
||||
<button onClick={() => setLogPage(p => Math.max(1, p - 1))} disabled={logPage === 1} className="rounded border border-gray-200 px-2 py-1 text-xs hover:bg-gray-50 disabled:opacity-30">上一页</button>
|
||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||
let start = Math.max(1, logPage - 2)
|
||||
if (start + 4 > totalPages) start = Math.max(1, totalPages - 4)
|
||||
const page = start + i
|
||||
if (page > totalPages) return null
|
||||
return (
|
||||
<button key={page} onClick={() => setLogPage(page)} className={`w-7 h-7 text-xs rounded ${page === logPage ? 'bg-black text-white' : 'border border-gray-200 hover:bg-gray-50'}`}>{page}</button>
|
||||
<button key={page} onClick={() => setLogPage(page)} className={`h-7 w-7 rounded text-xs ${page === logPage ? 'bg-black text-white' : 'border border-gray-200 hover:bg-gray-50'}`}>{page}</button>
|
||||
)
|
||||
})}
|
||||
<button onClick={() => setLogPage(p => Math.min(Math.ceil(logs.length / pageSize), p + 1))} disabled={logPage >= Math.ceil(logs.length / pageSize)} className="px-2 py-1 text-xs border border-gray-200 rounded hover:bg-gray-50 disabled:opacity-30">下一页</button>
|
||||
<button onClick={() => setLogPage(Math.ceil(logs.length / pageSize))} disabled={logPage >= Math.ceil(logs.length / pageSize)} className="px-2 py-1 text-xs border border-gray-200 rounded hover:bg-gray-50 disabled:opacity-30">末页</button>
|
||||
<button onClick={() => setLogPage(p => Math.min(totalPages, p + 1))} disabled={logPage >= totalPages} className="rounded border border-gray-200 px-2 py-1 text-xs hover:bg-gray-50 disabled:opacity-30">下一页</button>
|
||||
<button onClick={() => setLogPage(totalPages)} disabled={logPage >= totalPages} className="rounded border border-gray-200 px-2 py-1 text-xs hover:bg-gray-50 disabled:opacity-30">末页</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -171,7 +184,6 @@ export default function Settings() {
|
||||
}
|
||||
|
||||
function formatUA(ua: string): string {
|
||||
// Extract browser/OS info from UA string
|
||||
const parts: string[] = []
|
||||
if (ua.includes('Windows NT')) parts.push('Windows')
|
||||
else if (ua.includes('Mac OS X')) parts.push('macOS')
|
||||
|
||||
Reference in New Issue
Block a user