feat: add account editing functionality with UI modal and backend handler

This commit is contained in:
CJACK.
2026-04-22 17:20:44 +00:00
parent f14969eca5
commit 77484bf813
11 changed files with 293 additions and 11 deletions

View File

@@ -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>
)
}

View File

@@ -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]}

View 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>
)
}

View File

@@ -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,

View File

@@ -98,6 +98,7 @@
"addKeySuccess": "API key added successfully.",
"updateKeySuccess": "API key updated successfully.",
"addAccountSuccess": "Account added successfully.",
"updateAccountSuccess": "Account metadata updated successfully.",
"requiredFields": "Password and email/mobile are required.",
"deleteKeyConfirm": "Are you sure you want to delete this API key?",
"deleteAccountConfirm": "Are you sure you want to delete this account?",
@@ -111,16 +112,17 @@
"accountsUnit": "accounts",
"threadsUnit": "threads",
"apiKeysTitle": "API Keys",
"apiKeysDesc": "Manage the API access key pool",
"apiKeysDesc": "Manage the API access key pool. Click the pencil icon on each row to edit name and remark.",
"addKey": "Add key",
"editKeyTitle": "Edit key",
"editAccountTitle": "Edit account",
"copied": "Copied",
"copyFailed": "Copy failed",
"copyKeyTitle": "Copy key",
"deleteKeyTitle": "Delete key",
"noApiKeys": "No API keys found.",
"accountsTitle": "DeepSeek Accounts",
"accountsDesc": "Manage the DeepSeek account pool",
"accountsDesc": "Manage the DeepSeek account pool and edit name/remark.",
"testAll": "Refresh all tokens",
"addAccount": "Add account",
"testingAllAccounts": "Refreshing tokens for all accounts...",
@@ -131,6 +133,7 @@
"noAccounts": "No accounts found.",
"modalAddKeyTitle": "Add API key",
"modalEditKeyTitle": "Edit API key",
"modalEditAccountTitle": "Edit account details",
"newKeyLabel": "New key value",
"newKeyPlaceholder": "Enter a custom API key",
"keyLabel": "Key value",
@@ -142,6 +145,10 @@
"addKeyAction": "Add key",
"editKeyLoading": "Saving...",
"editKeyAction": "Save changes",
"editAccountHint": "Only name and remark can be changed here. The account identifier stays the same.",
"accountIdentifierLabel": "Account identifier",
"editAccountLoading": "Saving...",
"editAccountAction": "Save changes",
"modalAddAccountTitle": "Add DeepSeek account",
"nameOptional": "Name (optional)",
"namePlaceholder": "e.g. Primary Account A",

View File

@@ -98,6 +98,7 @@
"addKeySuccess": "API 密钥添加成功",
"updateKeySuccess": "API 密钥更新成功",
"addAccountSuccess": "账号添加成功",
"updateAccountSuccess": "账号信息更新成功",
"requiredFields": "需要填写密码以及邮箱或手机号",
"deleteKeyConfirm": "确定要删除此 API 密钥吗?",
"deleteAccountConfirm": "确定要删除此账号吗?",
@@ -111,16 +112,17 @@
"accountsUnit": "个账号",
"threadsUnit": "线程",
"apiKeysTitle": "API 密钥",
"apiKeysDesc": "管理 API 访问密钥池",
"apiKeysDesc": "管理 API 访问密钥池,点每行右侧铅笔可修改名称和备注",
"addKey": "添加密钥",
"editKeyTitle": "编辑密钥",
"editAccountTitle": "编辑账号",
"copied": "已复制",
"copyFailed": "复制失败",
"copyKeyTitle": "复制密钥",
"deleteKeyTitle": "删除密钥",
"noApiKeys": "未找到 API 密钥",
"accountsTitle": "DeepSeek 账号",
"accountsDesc": "管理 DeepSeek 账号池",
"accountsDesc": "管理 DeepSeek 账号池,支持修改名称和备注",
"testAll": "刷新全部 Token",
"addAccount": "添加账号",
"testingAllAccounts": "正在刷新所有账号 Token...",
@@ -131,6 +133,7 @@
"noAccounts": "未找到任何账号",
"modalAddKeyTitle": "添加 API 密钥",
"modalEditKeyTitle": "编辑 API 密钥",
"modalEditAccountTitle": "编辑账号信息",
"newKeyLabel": "新密钥值",
"newKeyPlaceholder": "输入自定义 API 密钥",
"keyLabel": "密钥值",
@@ -142,6 +145,10 @@
"addKeyAction": "添加密钥",
"editKeyLoading": "保存中...",
"editKeyAction": "保存修改",
"editAccountHint": "这里只能修改名称和备注,账号标识保持不变。",
"accountIdentifierLabel": "账号标识",
"editAccountLoading": "保存中...",
"editAccountAction": "保存修改",
"modalAddAccountTitle": "添加 DeepSeek 账号",
"nameOptional": "名称(可选)",
"namePlaceholder": "例如:主账号 A",