mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 支持手动添加 Claude 账户 Access Token
- 添加 OAuth 和手动输入两种账户添加方式 - 支持直接输入 Access Token 和 Refresh Token - 新增账户编辑功能,可更新 Token 和代理设置 - 优化 Token 刷新失败时的回退机制 - 改进用户体验,支持手动维护长期有效的 Token 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
20
README.md
20
README.md
@@ -237,28 +237,16 @@ npm run service:status
|
||||
4. 设置使用限制(可选)
|
||||
5. 保存,记下生成的Key
|
||||
|
||||
### 4. 开始使用API
|
||||
### 4. 开始使用Claude code
|
||||
|
||||
现在你可以用自己的服务替换官方API了:
|
||||
|
||||
**原来的请求:**
|
||||
**设置环境变量:**
|
||||
```bash
|
||||
curl https://api.anthropic.com/v1/messages \
|
||||
-H "x-api-key: 官方的key" \
|
||||
-H "content-type: application/json" \
|
||||
-d '{"model":"claude-3-sonnet-20240229","messages":[{"role":"user","content":"你好"}]}'
|
||||
export ANTHROPIC_BASE_URL="http://127.0.0.1:3000/api/" # 根据实际填写你服务器的ip地址或者域名
|
||||
export ANTHROPIC_AUTH_TOKEN="后台创建的API密钥"
|
||||
```
|
||||
|
||||
**现在的请求:**
|
||||
```bash
|
||||
curl http://你的域名:3000/api/v1/messages \
|
||||
-H "x-api-key: cr_你创建的key" \
|
||||
-H "content-type: application/json" \
|
||||
-d '{"model":"claude-3-sonnet-20240229","messages":[{"role":"user","content":"你好"}]}'
|
||||
```
|
||||
|
||||
就是把域名换一下,API Key换成你自己生成的,其他都一样。
|
||||
|
||||
---
|
||||
|
||||
## 🔧 日常维护
|
||||
|
||||
@@ -107,7 +107,7 @@ class ClaudeAccountService {
|
||||
const refreshToken = this._decryptSensitiveData(accountData.refreshToken);
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new Error('No refresh token available');
|
||||
throw new Error('No refresh token available - manual token update required');
|
||||
}
|
||||
|
||||
// 创建代理agent
|
||||
@@ -183,9 +183,20 @@ class ClaudeAccountService {
|
||||
const now = Date.now();
|
||||
|
||||
if (!expiresAt || now >= (expiresAt - 10000)) { // 10秒提前刷新
|
||||
logger.info(`🔄 Token expired/expiring for account ${accountId}, refreshing...`);
|
||||
const refreshResult = await this.refreshAccountToken(accountId);
|
||||
return refreshResult.accessToken;
|
||||
logger.info(`🔄 Token expired/expiring for account ${accountId}, attempting refresh...`);
|
||||
try {
|
||||
const refreshResult = await this.refreshAccountToken(accountId);
|
||||
return refreshResult.accessToken;
|
||||
} catch (refreshError) {
|
||||
logger.warn(`⚠️ Token refresh failed for account ${accountId}: ${refreshError.message}`);
|
||||
// 如果刷新失败,仍然尝试使用当前token(可能是手动添加的长期有效token)
|
||||
const currentToken = this._decryptSensitiveData(accountData.accessToken);
|
||||
if (currentToken) {
|
||||
logger.info(`🔄 Using current token for account ${accountId} (refresh failed)`);
|
||||
return currentToken;
|
||||
}
|
||||
throw refreshError;
|
||||
}
|
||||
}
|
||||
|
||||
const accessToken = this._decryptSensitiveData(accountData.accessToken);
|
||||
@@ -240,7 +251,7 @@ class ClaudeAccountService {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
|
||||
const allowedUpdates = ['name', 'description', 'email', 'password', 'refreshToken', 'proxy', 'isActive'];
|
||||
const allowedUpdates = ['name', 'description', 'email', 'password', 'refreshToken', 'proxy', 'isActive', 'claudeAiOauth'];
|
||||
const updatedData = { ...accountData };
|
||||
|
||||
for (const [field, value] of Object.entries(updates)) {
|
||||
@@ -249,6 +260,18 @@ class ClaudeAccountService {
|
||||
updatedData[field] = this._encryptSensitiveData(value);
|
||||
} else if (field === 'proxy') {
|
||||
updatedData[field] = value ? JSON.stringify(value) : '';
|
||||
} else if (field === 'claudeAiOauth') {
|
||||
// 更新 Claude AI OAuth 数据
|
||||
if (value) {
|
||||
updatedData.claudeAiOauth = this._encryptSensitiveData(JSON.stringify(value));
|
||||
updatedData.accessToken = this._encryptSensitiveData(value.accessToken);
|
||||
updatedData.refreshToken = this._encryptSensitiveData(value.refreshToken);
|
||||
updatedData.expiresAt = value.expiresAt.toString();
|
||||
updatedData.scopes = value.scopes.join(' ');
|
||||
updatedData.status = 'active';
|
||||
updatedData.errorMessage = '';
|
||||
updatedData.lastRefreshAt = new Date().toISOString();
|
||||
}
|
||||
} else {
|
||||
updatedData[field] = value.toString();
|
||||
}
|
||||
|
||||
201
web/admin/app.js
201
web/admin/app.js
@@ -140,6 +140,25 @@ const app = createApp({
|
||||
accountForm: {
|
||||
name: '',
|
||||
description: '',
|
||||
addType: 'oauth', // 'oauth' 或 'manual'
|
||||
accessToken: '',
|
||||
refreshToken: '',
|
||||
proxyType: '',
|
||||
proxyHost: '',
|
||||
proxyPort: '',
|
||||
proxyUsername: '',
|
||||
proxyPassword: ''
|
||||
},
|
||||
|
||||
// 编辑账户相关
|
||||
showEditAccountModal: false,
|
||||
editAccountLoading: false,
|
||||
editAccountForm: {
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
accessToken: '',
|
||||
refreshToken: '',
|
||||
proxyType: '',
|
||||
proxyHost: '',
|
||||
proxyPort: '',
|
||||
@@ -294,11 +313,123 @@ const app = createApp({
|
||||
this.resetAccountForm();
|
||||
},
|
||||
|
||||
// 打开编辑账户模态框
|
||||
openEditAccountModal(account) {
|
||||
this.editAccountForm = {
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
description: account.description || '',
|
||||
accessToken: '',
|
||||
refreshToken: '',
|
||||
proxyType: account.proxy ? account.proxy.type : '',
|
||||
proxyHost: account.proxy ? account.proxy.host : '',
|
||||
proxyPort: account.proxy ? account.proxy.port : '',
|
||||
proxyUsername: account.proxy ? account.proxy.username : '',
|
||||
proxyPassword: account.proxy ? account.proxy.password : ''
|
||||
};
|
||||
this.showEditAccountModal = true;
|
||||
},
|
||||
|
||||
// 关闭编辑账户模态框
|
||||
closeEditAccountModal() {
|
||||
this.showEditAccountModal = false;
|
||||
this.editAccountForm = {
|
||||
id: '',
|
||||
name: '',
|
||||
description: '',
|
||||
accessToken: '',
|
||||
refreshToken: '',
|
||||
proxyType: '',
|
||||
proxyHost: '',
|
||||
proxyPort: '',
|
||||
proxyUsername: '',
|
||||
proxyPassword: ''
|
||||
};
|
||||
},
|
||||
|
||||
// 更新账户
|
||||
async updateAccount() {
|
||||
this.editAccountLoading = true;
|
||||
try {
|
||||
// 构建更新数据
|
||||
let updateData = {
|
||||
name: this.editAccountForm.name,
|
||||
description: this.editAccountForm.description
|
||||
};
|
||||
|
||||
// 只在有值时才更新 token
|
||||
if (this.editAccountForm.accessToken.trim()) {
|
||||
// 构建新的 OAuth 数据
|
||||
const newOauthData = {
|
||||
accessToken: this.editAccountForm.accessToken,
|
||||
refreshToken: this.editAccountForm.refreshToken || '',
|
||||
expiresAt: Date.now() + (365 * 24 * 60 * 60 * 1000), // 默认设置1年后过期
|
||||
scopes: ['user:inference']
|
||||
};
|
||||
updateData.claudeAiOauth = newOauthData;
|
||||
}
|
||||
|
||||
// 更新代理配置
|
||||
if (this.editAccountForm.proxyType) {
|
||||
updateData.proxy = {
|
||||
type: this.editAccountForm.proxyType,
|
||||
host: this.editAccountForm.proxyHost,
|
||||
port: parseInt(this.editAccountForm.proxyPort),
|
||||
username: this.editAccountForm.proxyUsername || null,
|
||||
password: this.editAccountForm.proxyPassword || null
|
||||
};
|
||||
} else {
|
||||
updateData.proxy = null;
|
||||
}
|
||||
|
||||
const response = await fetch(`/admin/claude-accounts/${this.editAccountForm.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + this.authToken
|
||||
},
|
||||
body: JSON.stringify(updateData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showToast('账户更新成功!', 'success', '更新成功');
|
||||
this.closeEditAccountModal();
|
||||
await this.loadAccounts();
|
||||
} else {
|
||||
this.showToast(data.message || 'Account update failed', 'error', 'Update Failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating account:', error);
|
||||
|
||||
let errorMessage = '更新失败,请检查网络连接';
|
||||
|
||||
if (error.response) {
|
||||
try {
|
||||
const errorData = await error.response.json();
|
||||
errorMessage = errorData.message || errorMessage;
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse error response:', parseError);
|
||||
}
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
this.showToast(errorMessage, 'error', '网络错误', 8000);
|
||||
} finally {
|
||||
this.editAccountLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 重置账户表单
|
||||
resetAccountForm() {
|
||||
this.accountForm = {
|
||||
name: '',
|
||||
description: '',
|
||||
addType: 'oauth',
|
||||
accessToken: '',
|
||||
refreshToken: '',
|
||||
proxyType: '',
|
||||
proxyHost: '',
|
||||
proxyPort: '',
|
||||
@@ -462,6 +593,76 @@ const app = createApp({
|
||||
}
|
||||
},
|
||||
|
||||
// 创建手动账户
|
||||
async createManualAccount() {
|
||||
this.createAccountLoading = true;
|
||||
try {
|
||||
// 构建代理配置
|
||||
let proxy = null;
|
||||
if (this.accountForm.proxyType) {
|
||||
proxy = {
|
||||
type: this.accountForm.proxyType,
|
||||
host: this.accountForm.proxyHost,
|
||||
port: parseInt(this.accountForm.proxyPort),
|
||||
username: this.accountForm.proxyUsername || null,
|
||||
password: this.accountForm.proxyPassword || null
|
||||
};
|
||||
}
|
||||
|
||||
// 构建手动 OAuth 数据
|
||||
const manualOauthData = {
|
||||
accessToken: this.accountForm.accessToken,
|
||||
refreshToken: this.accountForm.refreshToken || '',
|
||||
expiresAt: Date.now() + (365 * 24 * 60 * 60 * 1000), // 默认设置1年后过期
|
||||
scopes: ['user:inference'] // 默认权限
|
||||
};
|
||||
|
||||
// 创建账户
|
||||
const createResponse = await fetch('/admin/claude-accounts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + this.authToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: this.accountForm.name,
|
||||
description: this.accountForm.description,
|
||||
claudeAiOauth: manualOauthData,
|
||||
proxy: proxy
|
||||
})
|
||||
});
|
||||
|
||||
const createData = await createResponse.json();
|
||||
|
||||
if (createData.success) {
|
||||
this.showToast('手动账户创建成功!', 'success', '账户创建成功');
|
||||
this.closeCreateAccountModal();
|
||||
await this.loadAccounts();
|
||||
} else {
|
||||
this.showToast(createData.message || 'Account creation failed', 'error', 'Creation Failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating manual account:', error);
|
||||
|
||||
let errorMessage = '创建失败,请检查网络连接';
|
||||
|
||||
if (error.response) {
|
||||
try {
|
||||
const errorData = await error.response.json();
|
||||
errorMessage = errorData.message || errorMessage;
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse error response:', parseError);
|
||||
}
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
this.showToast(errorMessage, 'error', '网络错误', 8000);
|
||||
} finally {
|
||||
this.createAccountLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// 根据当前标签页加载数据
|
||||
loadCurrentTabData() {
|
||||
|
||||
@@ -720,6 +720,12 @@
|
||||
{{ account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleDateString() : '从未使用' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm space-x-2">
|
||||
<button
|
||||
@click="openEditAccountModal(account)"
|
||||
class="text-blue-600 hover:text-blue-900 font-medium hover:bg-blue-50 px-3 py-1 rounded-lg transition-colors"
|
||||
>
|
||||
<i class="fas fa-edit mr-1"></i>编辑
|
||||
</button>
|
||||
<button
|
||||
@click="deleteAccount(account.id)"
|
||||
class="text-red-600 hover:text-red-900 font-medium hover:bg-red-50 px-3 py-1 rounded-lg transition-colors"
|
||||
@@ -1826,6 +1832,30 @@
|
||||
<!-- 步骤1: 基本信息和代理设置 -->
|
||||
<div v-if="oauthStep === 1">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">添加方式</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="accountForm.addType"
|
||||
value="oauth"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">OAuth 授权 (推荐)</span>
|
||||
</label>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
v-model="accountForm.addType"
|
||||
value="manual"
|
||||
class="mr-2"
|
||||
>
|
||||
<span class="text-sm text-gray-700">手动输入 Access Token</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">账户名称</label>
|
||||
<input
|
||||
@@ -1847,6 +1877,42 @@
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 手动输入 Token 字段 -->
|
||||
<div v-if="accountForm.addType === 'manual'" class="space-y-4 bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||||
<div class="flex items-start gap-3 mb-4">
|
||||
<div class="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
|
||||
<i class="fas fa-info text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="font-semibold text-blue-900 mb-2">手动输入 Token</h5>
|
||||
<p class="text-sm text-blue-800 mb-2">请输入有效的 Claude Access Token。如果您有 Refresh Token,建议也一并填写以支持自动刷新。</p>
|
||||
<p class="text-xs text-blue-600">💡 如果未填写 Refresh Token,Token 过期后需要手动更新。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">Access Token *</label>
|
||||
<textarea
|
||||
v-model="accountForm.accessToken"
|
||||
rows="4"
|
||||
class="form-input w-full resize-none font-mono text-sm"
|
||||
placeholder="sk-ant-oat01-..."
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">Refresh Token (可选)</label>
|
||||
<textarea
|
||||
v-model="accountForm.refreshToken"
|
||||
rows="4"
|
||||
class="form-input w-full resize-none font-mono text-sm"
|
||||
placeholder="sk-ant-ort01-..."
|
||||
></textarea>
|
||||
<p class="text-xs text-gray-500 mt-2">如果有 Refresh Token,填写后系统可以自动刷新过期的 Access Token</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-6">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">代理设置 (可选)</label>
|
||||
<p class="text-sm text-gray-500 mb-4">如果需要使用代理访问Claude服务,请配置代理信息。OAuth授权也将通过此代理进行。</p>
|
||||
@@ -1913,6 +1979,7 @@
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
v-if="accountForm.addType === 'oauth'"
|
||||
type="button"
|
||||
@click="nextOAuthStep()"
|
||||
:disabled="!accountForm.name"
|
||||
@@ -1920,6 +1987,17 @@
|
||||
>
|
||||
下一步 <i class="fas fa-arrow-right ml-2"></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="accountForm.addType === 'manual'"
|
||||
type="button"
|
||||
@click="createManualAccount()"
|
||||
:disabled="!accountForm.name || !accountForm.accessToken || createAccountLoading"
|
||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||
>
|
||||
<div v-if="createAccountLoading" class="loading-spinner mr-2"></div>
|
||||
<i v-else class="fas fa-check mr-2"></i>
|
||||
{{ createAccountLoading ? '创建中...' : '创建账户' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2031,6 +2109,163 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑 Claude 账户模态框 -->
|
||||
<div v-if="showEditAccountModal" class="fixed inset-0 modal z-50 flex items-center justify-center p-4">
|
||||
<div class="modal-content w-full max-w-2xl p-8 mx-auto max-h-[90vh] overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl flex items-center justify-center">
|
||||
<i class="fas fa-edit text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900">编辑 Claude 账户</h3>
|
||||
</div>
|
||||
<button
|
||||
@click="closeEditAccountModal"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">账户名称</label>
|
||||
<input
|
||||
v-model="editAccountForm.name"
|
||||
type="text"
|
||||
required
|
||||
class="form-input w-full"
|
||||
placeholder="为账户设置一个易识别的名称"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">描述 (可选)</label>
|
||||
<textarea
|
||||
v-model="editAccountForm.description"
|
||||
rows="3"
|
||||
class="form-input w-full resize-none"
|
||||
placeholder="账户用途说明..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Token 更新区域 -->
|
||||
<div class="bg-amber-50 p-4 rounded-lg border border-amber-200">
|
||||
<div class="flex items-start gap-3 mb-4">
|
||||
<div class="w-8 h-8 bg-amber-500 rounded-lg flex items-center justify-center flex-shrink-0 mt-1">
|
||||
<i class="fas fa-key text-white text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="font-semibold text-amber-900 mb-2">更新 Token</h5>
|
||||
<p class="text-sm text-amber-800 mb-2">可以更新 Access Token 和 Refresh Token。为了安全起见,不会显示当前的 Token 值。</p>
|
||||
<p class="text-xs text-amber-600">💡 留空表示不更新该字段。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">Access Token</label>
|
||||
<textarea
|
||||
v-model="editAccountForm.accessToken"
|
||||
rows="4"
|
||||
class="form-input w-full resize-none font-mono text-sm"
|
||||
placeholder="留空表示不更新,否则输入新的 Access Token"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">Refresh Token</label>
|
||||
<textarea
|
||||
v-model="editAccountForm.refreshToken"
|
||||
rows="4"
|
||||
class="form-input w-full resize-none font-mono text-sm"
|
||||
placeholder="留空表示不更新,否则输入新的 Refresh Token"
|
||||
></textarea>
|
||||
<p class="text-xs text-gray-500 mt-2">如果有 Refresh Token,填写后系统可以自动刷新过期的 Access Token</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 代理设置 -->
|
||||
<div class="border-t pt-6">
|
||||
<label class="block text-sm font-semibold text-gray-700 mb-3">代理设置 (可选)</label>
|
||||
<p class="text-sm text-gray-500 mb-4">如果需要修改代理设置,请更新代理信息。</p>
|
||||
<select
|
||||
v-model="editAccountForm.proxyType"
|
||||
class="form-input w-full"
|
||||
>
|
||||
<option value="">不使用代理</option>
|
||||
<option value="socks5">SOCKS5 代理</option>
|
||||
<option value="http">HTTP 代理</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="editAccountForm.proxyType" class="space-y-4 pl-4 border-l-2 border-blue-200">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">代理主机</label>
|
||||
<input
|
||||
v-model="editAccountForm.proxyHost"
|
||||
type="text"
|
||||
class="form-input w-full"
|
||||
placeholder="127.0.0.1"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">代理端口</label>
|
||||
<input
|
||||
v-model="editAccountForm.proxyPort"
|
||||
type="number"
|
||||
class="form-input w-full"
|
||||
placeholder="1080"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">用户名 (可选)</label>
|
||||
<input
|
||||
v-model="editAccountForm.proxyUsername"
|
||||
type="text"
|
||||
class="form-input w-full"
|
||||
placeholder="代理用户名"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">密码 (可选)</label>
|
||||
<input
|
||||
v-model="editAccountForm.proxyPassword"
|
||||
type="password"
|
||||
class="form-input w-full"
|
||||
placeholder="代理密码"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 pt-6">
|
||||
<button
|
||||
type="button"
|
||||
@click="closeEditAccountModal"
|
||||
class="flex-1 px-6 py-3 bg-gray-100 text-gray-700 rounded-xl font-semibold hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="updateAccount()"
|
||||
:disabled="!editAccountForm.name || editAccountLoading"
|
||||
class="btn btn-primary flex-1 py-3 px-6 font-semibold"
|
||||
>
|
||||
<div v-if="editAccountLoading" class="loading-spinner mr-2"></div>
|
||||
<i v-else class="fas fa-save mr-2"></i>
|
||||
{{ editAccountLoading ? '更新中...' : '保存修改' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast 通知组件 -->
|
||||
<div v-for="(toast, index) in toasts" :key="toast.id"
|
||||
:class="['toast rounded-2xl p-4 shadow-2xl backdrop-blur-sm',
|
||||
|
||||
Reference in New Issue
Block a user