mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-11 19:57:41 +08:00
feat: add account editing functionality with UI modal and backend handler
This commit is contained in:
@@ -6,6 +6,7 @@ import ApiKeysPanel from './ApiKeysPanel'
|
||||
import AccountsTable from './AccountsTable'
|
||||
import AddKeyModal from './AddKeyModal'
|
||||
import AddAccountModal from './AddAccountModal'
|
||||
import EditAccountModal from './EditAccountModal'
|
||||
|
||||
export default function AccountManagerContainer({ config, onRefresh, onMessage, authFetch }) {
|
||||
const { t } = useI18n()
|
||||
@@ -35,7 +36,14 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
|
||||
closeKeyModal,
|
||||
editingKey,
|
||||
showAddAccount,
|
||||
setShowAddAccount,
|
||||
openAddAccount,
|
||||
closeAddAccount,
|
||||
showEditAccount,
|
||||
editingAccount,
|
||||
editAccount,
|
||||
setEditAccount,
|
||||
openEditAccount,
|
||||
closeEditAccount,
|
||||
newKey,
|
||||
setNewKey,
|
||||
copiedKey,
|
||||
@@ -52,6 +60,7 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
|
||||
addKey,
|
||||
deleteKey,
|
||||
addAccount,
|
||||
updateAccount,
|
||||
deleteAccount,
|
||||
testAccount,
|
||||
testAllAccounts,
|
||||
@@ -121,7 +130,8 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
|
||||
resolveAccountIdentifier={resolveAccountIdentifier}
|
||||
proxies={config?.proxies || []}
|
||||
onTestAll={testAllAccounts}
|
||||
onShowAddAccount={() => setShowAddAccount(true)}
|
||||
onShowAddAccount={openAddAccount}
|
||||
onEditAccount={openEditAccount}
|
||||
onTestAccount={testAccount}
|
||||
onDeleteAccount={deleteAccount}
|
||||
onDeleteAllSessions={deleteAllSessions}
|
||||
@@ -151,9 +161,20 @@ export default function AccountManagerContainer({ config, onRefresh, onMessage,
|
||||
newAccount={newAccount}
|
||||
setNewAccount={setNewAccount}
|
||||
loading={loading}
|
||||
onClose={() => setShowAddAccount(false)}
|
||||
onClose={closeAddAccount}
|
||||
onAdd={addAccount}
|
||||
/>
|
||||
|
||||
<EditAccountModal
|
||||
show={showEditAccount}
|
||||
t={t}
|
||||
editingAccount={editingAccount}
|
||||
editAccount={editAccount}
|
||||
setEditAccount={setEditAccount}
|
||||
loading={loading}
|
||||
onClose={closeEditAccount}
|
||||
onSave={updateAccount}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { ChevronLeft, ChevronRight, Check, Copy, Play, Plus, Trash2, FolderX } from 'lucide-react'
|
||||
import { ChevronLeft, ChevronRight, Check, Copy, Pencil, Play, Plus, Trash2, FolderX } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
export default function AccountsTable({
|
||||
@@ -20,6 +20,7 @@ export default function AccountsTable({
|
||||
proxies,
|
||||
onTestAll,
|
||||
onShowAddAccount,
|
||||
onEditAccount,
|
||||
onTestAccount,
|
||||
onDeleteAccount,
|
||||
onDeleteAllSessions,
|
||||
@@ -180,6 +181,14 @@ export default function AccountsTable({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={() => onEditAccount(acc)}
|
||||
disabled={!id}
|
||||
className="p-1 lg:p-1.5 text-muted-foreground hover:text-primary hover:bg-primary/10 rounded-md transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
title={id ? t('accountManager.editAccountTitle') : t('accountManager.invalidIdentifier')}
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5 lg:w-4 lg:h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onTestAccount(id)}
|
||||
disabled={testing[id]}
|
||||
|
||||
65
webui/src/features/account/EditAccountModal.jsx
Normal file
65
webui/src/features/account/EditAccountModal.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
export default function EditAccountModal({
|
||||
show,
|
||||
t,
|
||||
editingAccount,
|
||||
editAccount,
|
||||
setEditAccount,
|
||||
loading,
|
||||
onClose,
|
||||
onSave,
|
||||
}) {
|
||||
if (!show || !editingAccount) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in">
|
||||
<div className="bg-card w-full max-w-md rounded-xl border border-border shadow-2xl overflow-hidden animate-in zoom-in-95">
|
||||
<div className="p-4 border-b border-border flex justify-between items-start gap-4">
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-semibold">{t('accountManager.modalEditAccountTitle')}</h3>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{t('accountManager.editAccountHint')}</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="text-muted-foreground hover:text-foreground">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="rounded-lg border border-border bg-muted/20 px-3 py-2">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-1">{t('accountManager.accountIdentifierLabel')}</div>
|
||||
<code className="text-sm font-mono text-foreground break-all">{editingAccount.identifier}</code>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">{t('accountManager.nameOptional')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input-field"
|
||||
placeholder={t('accountManager.namePlaceholder')}
|
||||
value={editAccount.name}
|
||||
onChange={e => setEditAccount({ ...editAccount, name: e.target.value })}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1.5">{t('accountManager.remarkOptional')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className="input-field"
|
||||
placeholder={t('accountManager.remarkPlaceholder')}
|
||||
value={editAccount.remark}
|
||||
onChange={e => setEditAccount({ ...editAccount, remark: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<button onClick={onClose} className="px-4 py-2 rounded-lg border border-border hover:bg-secondary transition-colors text-sm font-medium">{t('actions.cancel')}</button>
|
||||
<button onClick={onSave} disabled={loading} className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium disabled:opacity-50">
|
||||
{loading ? t('accountManager.editAccountLoading') : t('accountManager.editAccountAction')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,9 +4,12 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
|
||||
const [showAddKey, setShowAddKey] = useState(false)
|
||||
const [editingKey, setEditingKey] = useState(null)
|
||||
const [showAddAccount, setShowAddAccount] = useState(false)
|
||||
const [showEditAccount, setShowEditAccount] = useState(false)
|
||||
const [editingAccount, setEditingAccount] = useState(null)
|
||||
const [newKey, setNewKey] = useState({ key: '', name: '', remark: '' })
|
||||
const [copiedKey, setCopiedKey] = useState(null)
|
||||
const [newAccount, setNewAccount] = useState({ name: '', remark: '', email: '', mobile: '', password: '' })
|
||||
const [editAccount, setEditAccount] = useState({ name: '', remark: '' })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [testing, setTesting] = useState({})
|
||||
const [testingAll, setTestingAll] = useState(false)
|
||||
@@ -38,6 +41,42 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
|
||||
setNewKey({ key: '', name: '', remark: '' })
|
||||
}
|
||||
|
||||
const openAddAccount = () => {
|
||||
setShowEditAccount(false)
|
||||
setEditingAccount(null)
|
||||
setEditAccount({ name: '', remark: '' })
|
||||
setNewAccount({ name: '', remark: '', email: '', mobile: '', password: '' })
|
||||
setShowAddAccount(true)
|
||||
}
|
||||
|
||||
const closeAddAccount = () => {
|
||||
setShowAddAccount(false)
|
||||
setNewAccount({ name: '', remark: '', email: '', mobile: '', password: '' })
|
||||
}
|
||||
|
||||
const openEditAccount = (account) => {
|
||||
const identifier = resolveAccountIdentifier(account)
|
||||
if (!identifier) {
|
||||
onMessage('error', t('accountManager.invalidIdentifier'))
|
||||
return
|
||||
}
|
||||
setShowAddAccount(false)
|
||||
setEditingAccount({
|
||||
identifier,
|
||||
})
|
||||
setEditAccount({
|
||||
name: account?.name || '',
|
||||
remark: account?.remark || '',
|
||||
})
|
||||
setShowEditAccount(true)
|
||||
}
|
||||
|
||||
const closeEditAccount = () => {
|
||||
setShowEditAccount(false)
|
||||
setEditingAccount(null)
|
||||
setEditAccount({ name: '', remark: '' })
|
||||
}
|
||||
|
||||
const addKey = async () => {
|
||||
const isEditing = Boolean(editingKey?.key)
|
||||
if (!isEditing && !newKey.key.trim()) {
|
||||
@@ -104,8 +143,7 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
|
||||
})
|
||||
if (res.ok) {
|
||||
onMessage('success', t('accountManager.addAccountSuccess'))
|
||||
setNewAccount({ name: '', remark: '', email: '', mobile: '', password: '' })
|
||||
setShowAddAccount(false)
|
||||
closeAddAccount()
|
||||
fetchAccounts(1)
|
||||
onRefresh()
|
||||
} else {
|
||||
@@ -119,6 +157,35 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
|
||||
}
|
||||
}
|
||||
|
||||
const updateAccount = async () => {
|
||||
const identifier = String(editingAccount?.identifier || '').trim()
|
||||
if (!identifier) {
|
||||
onMessage('error', t('accountManager.invalidIdentifier'))
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await apiFetch(`/admin/accounts/${encodeURIComponent(identifier)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(editAccount),
|
||||
})
|
||||
if (res.ok) {
|
||||
onMessage('success', t('accountManager.updateAccountSuccess'))
|
||||
closeEditAccount()
|
||||
fetchAccounts()
|
||||
onRefresh()
|
||||
} else {
|
||||
const data = await res.json()
|
||||
onMessage('error', data.detail || t('messages.requestFailed'))
|
||||
}
|
||||
} catch (e) {
|
||||
onMessage('error', t('messages.networkError'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAccount = async (id) => {
|
||||
const identifier = String(id || '').trim()
|
||||
if (!identifier) {
|
||||
@@ -285,7 +352,14 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
|
||||
closeKeyModal,
|
||||
editingKey,
|
||||
showAddAccount,
|
||||
setShowAddAccount,
|
||||
openAddAccount,
|
||||
closeAddAccount,
|
||||
showEditAccount,
|
||||
editingAccount,
|
||||
editAccount,
|
||||
setEditAccount,
|
||||
openEditAccount,
|
||||
closeEditAccount,
|
||||
newKey,
|
||||
setNewKey,
|
||||
copiedKey,
|
||||
@@ -302,6 +376,7 @@ export function useAccountActions({ apiFetch, t, onMessage, onRefresh, config, f
|
||||
addKey,
|
||||
deleteKey,
|
||||
addAccount,
|
||||
updateAccount,
|
||||
deleteAccount,
|
||||
testAccount,
|
||||
testAllAccounts,
|
||||
|
||||
Reference in New Issue
Block a user