Files
pdf-translate-web/templates/admin/llm_config.html
coder db98c2b82c refactor: 备用大模型整合到大模型配置页
- 备用大模型不再单独页面,直接在大模型配置页显示
- 新增/编辑使用模态框,更简洁高效
- 移除独立的 backup_llm.html 和 backup_llm_form.html
- 统一侧边栏导航,移除备用大模型链接
- API 改为只返回 JSON,不再渲染页面
2026-04-16 15:58:17 +08:00

353 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>大模型配置 - 后台管理</title>
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body { background-color: #f5f5f5; }
.sidebar { position: fixed; top: 0; left: 0; height: 100vh; width: 250px; background: #343a40; padding-top: 60px; }
.sidebar .nav-link { color: #adb5bd; padding: 12px 20px; }
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: #fff; background: rgba(255,255,255,0.1); }
.main-content { margin-left: 250px; padding: 20px; }
.table th, .table td { font-size: 0.9rem; }
.status-badge { font-size: 0.75rem; }
</style>
</head>
<body>
<nav class="sidebar">
<div class="position-absolute top-0 w-100 p-3 border-bottom border-secondary">
<h5 class="text-white mb-0"><i class="bi bi-gear-fill"></i> 后台管理</h5>
</div>
<ul class="nav flex-column">
<li class="nav-item"><a class="nav-link " href="{{ url_for('admin.dashboard') }}"><i class="bi bi-speedometer2"></i> 数据概览</a></li>
<li class="nav-item"><a class="nav-link " href="{{ url_for('admin.users') }}"><i class="bi bi-people"></i> 用户管理</a></li>
<li class="nav-item"><a class="nav-link " href="{{ url_for('admin.translations') }}"><i class="bi bi-file-text"></i> 翻译记录</a></li>
<li class="nav-item"><a class="nav-link " href="{{ url_for('admin.cache_list') }}"><i class="bi bi-database"></i> 缓存管理</a></li>
<li class="nav-item"><a class="nav-link " href="{{ url_for('admin.packages') }}"><i class="bi bi-box-seam"></i> 数据包套餐</a></li>
<li class="nav-item"><a class="nav-link " href="{{ url_for('admin.stats') }}"><i class="bi bi-bar-chart"></i> 统计报表</a></li>
<li class="nav-item"><a class="nav-link " href="{{ url_for('admin.logs') }}"><i class="bi bi-list-check"></i> 操作日志</a></li>
<li class="nav-item"><a class="nav-link active" href="{{ url_for('admin.llm_config') }}"><i class="bi bi-cpu"></i> 大模型配置</a></li>
<li class="nav-item"><a class="nav-link " href="{{ url_for('admin.user_types') }}"><i class="bi bi-person-badge"></i> 用户类型</a></li>
<li class="nav-item"><a class="nav-link " href="{{ url_for('admin.membership_plans') }}"><i class="bi bi-credit-card"></i> 会员套餐</a></li>
<li class="nav-item"><a class="nav-link " href="{{ url_for('admin.settings') }}"><i class="bi bi-sliders"></i> 系统配置</a></li>
</ul>
<div class="position-absolute bottom-0 w-100 p-3 border-top border-secondary">
<a href="/" class="btn btn-outline-light btn-sm w-100"><i class="bi bi-house"></i> 返回前台</a>
</div>
</nav>
<main class="main-content">
<!-- 主配置 -->
<div class="card">
<div class="card-header">
<h6 class="mb-0"><i class="bi bi-cpu"></i> 主大模型接口配置</h6>
</div>
<div class="card-body">
<form id="llmConfigForm">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">API 地址</label>
<input type="text" class="form-control" name="api_base" value="{{ config.api_base }}" placeholder="http://localhost:1234/v1">
<small class="text-muted">LLM服务的API endpoint</small>
</div>
<div class="mb-3">
<label class="form-label">API Key</label>
<input type="text" class="form-control" name="api_key" value="{{ config.api_key }}" placeholder="sk-xxx">
<small class="text-muted">如果不需要可留空</small>
</div>
<div class="mb-3">
<label class="form-label">模型名称</label>
<input type="text" class="form-control" name="model" value="{{ config.model }}" placeholder="qwen/qwen3.5-35b">
<small class="text-muted">使用的模型ID</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">最大输出Token</label>
<input type="number" class="form-control" name="max_tokens" value="{{ config.max_tokens }}" placeholder="8000">
</div>
<div class="mb-3">
<label class="form-label">分块大小</label>
<input type="number" class="form-control" name="chunk_size" value="{{ config.chunk_size }}" placeholder="2000">
</div>
<div class="mb-3">
<label class="form-label">超时时间(秒)</label>
<input type="number" class="form-control" name="timeout" value="{{ config.timeout }}" placeholder="180">
</div>
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> 保存配置</button>
<button type="button" class="btn btn-outline-secondary" onclick="testMainConnection()"><i class="bi bi-plug"></i> 测试连接</button>
<button type="button" class="btn btn-outline-warning" onclick="resetToDefault()"><i class="bi bi-arrow-counterclockwise"></i> 恢复默认</button>
</div>
</form>
<div id="mainTestResult" class="mt-3" style="display:none;"></div>
</div>
</div>
<!-- 备用大模型 -->
<div class="card mt-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-cloud"></i> 备用大模型接口</h6>
<button class="btn btn-sm btn-primary" onclick="showAddModal()"><i class="bi bi-plus-lg"></i> 新增</button>
</div>
<div class="card-body">
<table class="table table-sm table-hover">
<thead class="table-light">
<tr>
<th style="width: 40px;">#</th>
<th>服务商</th>
<th>API地址</th>
<th>模型</th>
<th style="width: 70px;">状态</th>
<th style="width: 130px;">操作</th>
</tr>
</thead>
<tbody>
{% for item in backup_configs %}
<tr id="backup-row-{{ item.id }}">
<td>{{ item.sort_order }}</td>
<td>
<strong>{{ item.provider_name }}</strong>
{% if item.description %}<br><small class="text-muted">{{ item.description }}</small>{% endif %}
</td>
<td><code style="font-size: 0.8rem;">{{ item.api_base }}</code></td>
<td><code style="font-size: 0.8rem;">{{ item.model or '默认' }}</code></td>
<td>
{% if item.is_active %}
<span class="badge bg-success status-badge">启用</span>
{% else %}
<span class="badge bg-secondary status-badge">禁用</span>
{% endif %}
</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="testBackup({{ item.id }})" title="测试"><i class="bi bi-plug"></i></button>
<button class="btn btn-sm btn-outline-secondary" onclick="showEditModal({{ item.id }})" title="编辑"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-outline-warning" onclick="toggleBackup({{ item.id }})" title="切换"><i class="bi bi-toggle2"></i></button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteBackup({{ item.id }})" title="删除"><i class="bi bi-trash"></i></button>
</td>
</tr>
{% else %}
<tr><td colspan="6" class="text-center text-muted py-3">暂无备用大模型配置</td></tr>
{% endfor %}
</tbody>
</table>
<div id="backupTestResult" class="mt-2" style="display:none;"></div>
</div>
</div>
</main>
<!-- 新增/编辑模态框 -->
<div class="modal fade" id="backupModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title" id="modalTitle">新增备用大模型</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="backupForm">
<input type="hidden" id="backup_id" name="id">
<div class="mb-3">
<label class="form-label">服务商名称 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="provider_name" name="provider_name" required placeholder="如: OpenAI">
</div>
<div class="mb-3">
<label class="form-label">API地址 <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="api_base" name="api_base" required placeholder="https://api.openai.com/v1">
</div>
<div class="mb-3">
<label class="form-label">API Key</label>
<input type="text" class="form-control" id="api_key" name="api_key" placeholder="sk-xxx可选">
</div>
<div class="mb-3">
<label class="form-label">默认模型</label>
<input type="text" class="form-control" id="model" name="model" placeholder="gpt-4">
</div>
<div class="mb-3">
<label class="form-label">备注</label>
<input type="text" class="form-control" id="description" name="description">
</div>
<div class="row">
<div class="col-6">
<label class="form-label">排序</label>
<input type="number" class="form-control" id="sort_order" name="sort_order" value="0">
</div>
<div class="col-6">
<label class="form-label">状态</label>
<select class="form-select" id="is_active" name="is_active">
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="saveBackup()"><i class="bi bi-check-lg"></i> 保存</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 主配置
document.getElementById('llmConfigForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = {};
formData.forEach((value, key) => {
data[key] = ['max_tokens', 'chunk_size', 'timeout'].includes(key) ? parseInt(value) || 0 : value;
});
fetch('/admin/llm_config/save', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
})
.then(r => r.json())
.then(res => alert(res.success ? '配置已保存!' : '保存失败: ' + res.error))
.catch(err => alert('请求失败: ' + err));
});
function testMainConnection() {
const formData = new FormData(document.getElementById('llmConfigForm'));
const data = {};
formData.forEach((value, key) => {
data[key] = ['max_tokens', 'chunk_size', 'timeout'].includes(key) ? parseInt(value) || 0 : value;
});
const div = document.getElementById('mainTestResult');
div.style.display = 'block';
div.innerHTML = '<div class="alert alert-info"><i class="bi bi-hourglass-split"></i> 测试中...</div>';
fetch('/admin/llm_config/test', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
})
.then(r => r.json())
.then(res => {
div.innerHTML = res.success
? `<div class="alert alert-success"><i class="bi bi-check-circle"></i> 连接成功!模型: ${res.model}</div>`
: `<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 连接失败: ${res.error}</div>`;
})
.catch(err => div.innerHTML = `<div class="alert alert-danger">请求失败: ${err}</div>`);
}
function resetToDefault() {
if (confirm('确定恢复默认配置?')) {
fetch('/admin/llm_config/reset', {method: 'POST'})
.then(r => r.json())
.then(res => res.success ? location.reload() : alert('恢复失败: ' + res.error));
}
}
// 备用大模型
const backupData = {{ backup_configs | tojson }};
const modal = new bootstrap.Modal(document.getElementById('backupModal'));
function showAddModal() {
document.getElementById('modalTitle').textContent = '新增备用大模型';
document.getElementById('backup_id').value = '';
document.getElementById('provider_name').value = '';
document.getElementById('api_base').value = '';
document.getElementById('api_key').value = '';
document.getElementById('model').value = '';
document.getElementById('description').value = '';
document.getElementById('sort_order').value = '0';
document.getElementById('is_active').value = 'true';
modal.show();
}
function showEditModal(id) {
const item = backupData.find(b => b.id === id);
if (!item) return;
document.getElementById('modalTitle').textContent = '编辑备用大模型';
document.getElementById('backup_id').value = id;
document.getElementById('provider_name').value = item.provider_name;
document.getElementById('api_base').value = item.api_base;
document.getElementById('api_key').value = item.api_key || '';
document.getElementById('model').value = item.model || '';
document.getElementById('description').value = item.description || '';
document.getElementById('sort_order').value = item.sort_order;
document.getElementById('is_active').value = item.is_active ? 'true' : 'false';
modal.show();
}
function saveBackup() {
const id = document.getElementById('backup_id').value;
const data = {
provider_name: document.getElementById('provider_name').value,
api_base: document.getElementById('api_base').value,
api_key: document.getElementById('api_key').value,
model: document.getElementById('model').value,
description: document.getElementById('description').value,
sort_order: parseInt(document.getElementById('sort_order').value) || 0,
is_active: document.getElementById('is_active').value === 'true'
};
const url = id ? `/admin/backup-llm/${id}/edit` : '/admin/backup-llm/add';
fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
})
.then(r => r.json())
.then(res => {
if (res.success) {
modal.hide();
location.reload();
} else {
alert('保存失败: ' + (res.error || '未知错误'));
}
})
.catch(err => alert('请求失败: ' + err));
}
function testBackup(id) {
const div = document.getElementById('backupTestResult');
div.style.display = 'block';
div.innerHTML = '<div class="alert alert-info"><i class="bi bi-hourglass-split"></i> 测试中...</div>';
fetch(`/admin/backup-llm/${id}/test`, {method: 'POST'})
.then(r => r.json())
.then(res => {
div.innerHTML = res.success
? `<div class="alert alert-success"><i class="bi bi-check-circle"></i> ${res.provider} 连接成功</div>`
: `<div class="alert alert-danger"><i class="bi bi-x-circle"></i> 连接失败: ${res.error}</div>`;
})
.catch(err => div.innerHTML = `<div class="alert alert-danger">请求失败: ${err}</div>`);
}
function toggleBackup(id) {
fetch(`/admin/backup-llm/${id}/toggle`, {method: 'POST'})
.then(r => r.json())
.then(res => res.success ? location.reload() : alert('操作失败'));
}
function deleteBackup(id) {
if (confirm('确定删除此备用接口?')) {
fetch(`/admin/backup-llm/${id}/delete`, {method: 'POST'})
.then(r => r.json())
.then(res => {
if (res.success) document.getElementById(`backup-row-${id}`).remove();
else alert('删除失败');
});
}
}
</script>
</body>
</html>