1071 lines
36 KiB
HTML
1071 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>模型管理</title>
|
||
<!-- Fonts -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<!-- Lucide Icons -->
|
||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
||
|
||
<style>
|
||
:root {
|
||
--primary: #2563EB;
|
||
--primary-hover: #1D4ED8;
|
||
--secondary: #3B82F6;
|
||
--background: #F8FAFC;
|
||
--surface: #FFFFFF;
|
||
--text: #1E293B;
|
||
--text-muted: #64748B;
|
||
--border: #E2E8F0;
|
||
--success: #10B981;
|
||
--warning: #F59E0B;
|
||
--error: #EF4444;
|
||
--glass-bg: rgba(255, 255, 255, 0.8);
|
||
--glass-blur: 12px;
|
||
}
|
||
|
||
.dark {
|
||
--primary: #3B82F6;
|
||
--primary-hover: #60A5FA;
|
||
--secondary: #60A5FA;
|
||
--background: #0F172A;
|
||
--surface: #1E293B;
|
||
--text: #F1F5F9;
|
||
--text-muted: #94A3B8;
|
||
--border: #334155;
|
||
--glass-bg: rgba(30, 41, 59, 0.8);
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Open Sans', sans-serif;
|
||
background: var(--background);
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
transition: background-color 0.3s ease, color 0.3s ease;
|
||
}
|
||
|
||
/* ===== Header ===== */
|
||
.header {
|
||
height: 64px;
|
||
background: var(--glass-bg);
|
||
backdrop-filter: blur(var(--glass-blur));
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 24px;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 10;
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.back-btn {
|
||
width: 38px;
|
||
height: 38px;
|
||
border-radius: 10px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.back-btn:hover {
|
||
background: var(--background);
|
||
}
|
||
|
||
.header-logo {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 10px;
|
||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
}
|
||
|
||
.header-title {
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.header-btn {
|
||
width: 38px;
|
||
height: 38px;
|
||
border-radius: 10px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.header-btn:hover {
|
||
background: var(--background);
|
||
}
|
||
|
||
.header-btn.primary {
|
||
background: var(--primary);
|
||
color: white;
|
||
padding: 0 16px;
|
||
width: auto;
|
||
gap: 8px;
|
||
}
|
||
|
||
.header-btn.primary:hover {
|
||
background: var(--primary-hover);
|
||
}
|
||
|
||
/* ===== Main Content ===== */
|
||
.main-content {
|
||
max-width: 900px;
|
||
margin: 0 auto;
|
||
padding: 32px 24px;
|
||
}
|
||
|
||
.page-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.page-title {
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.page-subtitle {
|
||
font-size: 14px;
|
||
color: var(--text-muted);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* ===== Model List ===== */
|
||
.model-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.model-card {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.model-card:hover {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 4px 20px rgba(37, 99, 235, 0.1);
|
||
}
|
||
|
||
.model-card-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 14px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.model-card-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 12px;
|
||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.model-card-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.model-card-name {
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.model-card-provider {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
background: var(--background);
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
display: inline-block;
|
||
}
|
||
|
||
.model-card-details {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
padding: 14px;
|
||
background: var(--background);
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.model-detail-item {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.model-detail-label {
|
||
color: var(--text-muted);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.model-detail-value {
|
||
font-family: 'Monaco', 'Consolas', monospace;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.model-card-actions {
|
||
display: flex;
|
||
gap: 10px;
|
||
padding-top: 14px;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.model-card-action {
|
||
flex: 1;
|
||
padding: 10px 14px;
|
||
border-radius: 10px;
|
||
border: 1px solid var(--border);
|
||
background: transparent;
|
||
color: var(--text);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.model-card-action:hover {
|
||
background: var(--background);
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.model-card-action.set-default {
|
||
background: rgba(37, 99, 235, 0.1);
|
||
border-color: var(--primary);
|
||
color: var(--primary);
|
||
}
|
||
|
||
.model-card-action.delete:hover {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
border-color: var(--error);
|
||
color: var(--error);
|
||
}
|
||
|
||
.model-card-action.default {
|
||
background: var(--success);
|
||
border-color: var(--success);
|
||
color: white;
|
||
}
|
||
|
||
/* ===== Empty State ===== */
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 80px 20px;
|
||
}
|
||
|
||
.empty-state-icon {
|
||
width: 80px;
|
||
height: 80px;
|
||
margin: 0 auto 20px;
|
||
border-radius: 20px;
|
||
background: var(--background);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.empty-state-title {
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.empty-state-subtitle {
|
||
font-size: 14px;
|
||
color: var(--text-muted);
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
/* ===== Modal ===== */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
backdrop-filter: blur(4px);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 100;
|
||
}
|
||
|
||
.modal-overlay.active {
|
||
display: flex;
|
||
}
|
||
|
||
.modal {
|
||
background: var(--surface);
|
||
border-radius: 16px;
|
||
width: 90%;
|
||
max-width: 540px;
|
||
max-height: 85vh;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.modal-header {
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid var(--border);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.modal-title {
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.modal-close {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text-muted);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-close:hover {
|
||
background: var(--background);
|
||
color: var(--text);
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 24px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.form-group:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.form-label {
|
||
display: block;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--text-muted);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.form-input,
|
||
.form-select {
|
||
width: 100%;
|
||
padding: 12px 14px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
font-size: 14px;
|
||
background: var(--background);
|
||
color: var(--text);
|
||
outline: none;
|
||
transition: border-color 0.15s ease;
|
||
}
|
||
|
||
.form-input:focus,
|
||
.form-select:focus {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||
}
|
||
|
||
.form-input::placeholder {
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.form-hint {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.modal-footer {
|
||
padding: 16px 24px;
|
||
border-top: 1px solid var(--border);
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: var(--background);
|
||
border: 1px solid var(--border);
|
||
color: var(--text);
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--primary);
|
||
border: none;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: var(--primary-hover);
|
||
}
|
||
|
||
/* ===== Delete Confirmation Modal ===== */
|
||
.delete-confirm-content {
|
||
text-align: center;
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.delete-confirm-icon {
|
||
width: 56px;
|
||
height: 56px;
|
||
margin: 0 auto 16px;
|
||
border-radius: 50%;
|
||
background: rgba(239, 68, 68, 0.1);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--error);
|
||
}
|
||
|
||
.delete-confirm-title {
|
||
font-family: 'Poppins', sans-serif;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.delete-confirm-text {
|
||
font-size: 14px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* ===== Checkbox ===== */
|
||
.checkbox-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 12px;
|
||
background: var(--background);
|
||
border-radius: 10px;
|
||
border: 1px solid var(--border);
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.checkbox-wrapper:hover {
|
||
border-color: var(--primary);
|
||
}
|
||
|
||
.checkbox-wrapper input[type="checkbox"] {
|
||
width: 18px;
|
||
height: 18px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.checkbox-wrapper label {
|
||
flex: 1;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* ===== Scrollbar ===== */
|
||
::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: var(--border);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: var(--text-muted);
|
||
}
|
||
|
||
/* ===== Responsive ===== */
|
||
@media (max-width: 768px) {
|
||
.header {
|
||
padding: 0 16px;
|
||
}
|
||
|
||
.main-content {
|
||
padding: 20px 16px;
|
||
}
|
||
|
||
.model-card-details {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.model-card-actions {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.model-card-action {
|
||
flex: 1 1 calc(50% - 5px);
|
||
min-width: 120px;
|
||
}
|
||
|
||
.page-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
}
|
||
|
||
.header-btn.primary span {
|
||
display: none;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Header -->
|
||
<header class="header">
|
||
<div class="header-left">
|
||
<button class="back-btn" id="back-btn" title="返回">
|
||
<i data-lucide="arrow-left" style="width: 20px; height: 20px;"></i>
|
||
</button>
|
||
<div class="header-logo">
|
||
<i data-lucide="cpu" style="width: 20px; height: 20px;"></i>
|
||
</div>
|
||
<div>
|
||
<div class="header-title">模型管理</div>
|
||
</div>
|
||
</div>
|
||
<div class="header-right">
|
||
<button class="header-btn" id="theme-toggle" title="切换主题">
|
||
<i data-lucide="moon" style="width: 18px; height: 18px;"></i>
|
||
</button>
|
||
<button class="header-btn primary" id="add-model-btn">
|
||
<i data-lucide="plus" style="width: 16px; height: 16px;"></i>
|
||
<span>添加模型</span>
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Main Content -->
|
||
<main class="main-content">
|
||
<div class="page-header">
|
||
<div>
|
||
<h1 class="page-title">我的模型</h1>
|
||
<p class="page-subtitle">管理您的大语言模型配置</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Model List -->
|
||
<div class="model-list" id="model-list">
|
||
<!-- 动态生成 -->
|
||
</div>
|
||
</main>
|
||
|
||
<!-- Add/Edit Model Modal -->
|
||
<div class="modal-overlay" id="model-modal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3 class="modal-title" id="model-modal-title">添加模型</h3>
|
||
<button class="modal-close" id="model-modal-close">
|
||
<i data-lucide="x" style="width: 18px; height: 18px;"></i>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-group">
|
||
<label class="form-label" for="model-name">模型名称 *</label>
|
||
<input type="text" id="model-name" class="form-input" placeholder="例如:GPT-4、Claude 3.5">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="model-provider">服务提供商</label>
|
||
<select id="model-provider" class="form-select">
|
||
<option value="OpenAI">OpenAI</option>
|
||
<option value="Anthropic">Anthropic</option>
|
||
<option value="通义千问">通义千问</option>
|
||
<option value="DeepSeek">DeepSeek</option>
|
||
<option value="OpenRouter">OpenRouter</option>
|
||
<option value="其他">其他</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="model-model">模型标识符 *</label>
|
||
<input type="text" id="model-model" class="form-input" placeholder="例如:gpt-4、claude-3-5-sonnet-20241022">
|
||
<p class="form-hint">用于 API 调用的模型名称</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="model-server">模型服务器地址</label>
|
||
<input type="text" id="model-server" class="form-input" placeholder="例如:https://api.openai.com/v1">
|
||
<p class="form-hint">API 服务器的基础 URL</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label" for="model-api-key">API Key</label>
|
||
<input type="password" id="model-api-key" class="form-input" placeholder="输入您的 API Key">
|
||
<p class="form-hint">您的 API 访问密钥</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="checkbox-wrapper">
|
||
<input type="checkbox" id="model-is-default">
|
||
<label for="model-is-default">设为默认模型</label>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" id="model-modal-cancel">取消</button>
|
||
<button class="btn btn-primary" id="model-modal-save">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Delete Confirmation Modal -->
|
||
<div class="modal-overlay" id="delete-modal">
|
||
<div class="modal" style="max-width: 400px;">
|
||
<div class="modal-body">
|
||
<div class="delete-confirm-content">
|
||
<div class="delete-confirm-icon">
|
||
<i data-lucide="alert-triangle" style="width: 28px; height: 28px;"></i>
|
||
</div>
|
||
<h3 class="delete-confirm-title">确认删除</h3>
|
||
<p class="delete-confirm-text">确定要删除这个模型配置吗?此操作无法撤销。</p>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-secondary" id="delete-modal-cancel">取消</button>
|
||
<button class="btn btn-primary" id="delete-modal-confirm" style="background: var(--error);">删除</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Initialize Lucide icons
|
||
lucide.createIcons();
|
||
|
||
class ModelManager {
|
||
constructor() {
|
||
this.elements = {
|
||
themeToggle: document.getElementById('theme-toggle'),
|
||
backBtn: document.getElementById('back-btn'),
|
||
addModelBtn: document.getElementById('add-model-btn'),
|
||
modelList: document.getElementById('model-list'),
|
||
// Model Modal
|
||
modelModal: document.getElementById('model-modal'),
|
||
modelModalTitle: document.getElementById('model-modal-title'),
|
||
modelModalClose: document.getElementById('model-modal-close'),
|
||
modelModalCancel: document.getElementById('model-modal-cancel'),
|
||
modelModalSave: document.getElementById('model-modal-save'),
|
||
modelName: document.getElementById('model-name'),
|
||
modelProvider: document.getElementById('model-provider'),
|
||
modelModel: document.getElementById('model-model'),
|
||
modelServer: document.getElementById('model-server'),
|
||
modelApiKey: document.getElementById('model-api-key'),
|
||
modelIsDefault: document.getElementById('model-is-default'),
|
||
// Delete Modal
|
||
deleteModal: document.getElementById('delete-modal'),
|
||
deleteModalCancel: document.getElementById('delete-modal-cancel'),
|
||
deleteModalConfirm: document.getElementById('delete-modal-confirm')
|
||
};
|
||
|
||
this.models = [];
|
||
this.editingModelId = null;
|
||
this.deletingModelId = null;
|
||
|
||
this.initializeEventListeners();
|
||
this.loadTheme();
|
||
this.loadModels();
|
||
}
|
||
|
||
initializeEventListeners() {
|
||
// Theme toggle
|
||
this.elements.themeToggle.addEventListener('click', () => this.toggleTheme());
|
||
|
||
// Back button
|
||
this.elements.backBtn.addEventListener('click', () => {
|
||
window.history.back();
|
||
});
|
||
|
||
// Add model
|
||
this.elements.addModelBtn.addEventListener('click', () => this.openModelModal());
|
||
|
||
// Model modal
|
||
this.elements.modelModalClose.addEventListener('click', () => this.closeModelModal());
|
||
this.elements.modelModalCancel.addEventListener('click', () => this.closeModelModal());
|
||
this.elements.modelModalSave.addEventListener('click', () => this.saveModel());
|
||
this.elements.modelModal.addEventListener('click', (e) => {
|
||
if (e.target === this.elements.modelModal) this.closeModelModal();
|
||
});
|
||
|
||
// Delete modal
|
||
this.elements.deleteModalCancel.addEventListener('click', () => this.closeDeleteModal());
|
||
this.elements.deleteModalConfirm.addEventListener('click', () => this.confirmDelete());
|
||
}
|
||
|
||
loadTheme() {
|
||
const theme = localStorage.getItem('theme') || 'light';
|
||
if (theme === 'dark') {
|
||
document.documentElement.classList.add('dark');
|
||
}
|
||
this.updateThemeIcon(theme === 'dark');
|
||
}
|
||
|
||
toggleTheme() {
|
||
document.documentElement.classList.toggle('dark');
|
||
const isDark = document.documentElement.classList.contains('dark');
|
||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||
this.updateThemeIcon(isDark);
|
||
}
|
||
|
||
updateThemeIcon(isDark) {
|
||
const icon = this.elements.themeToggle.querySelector('i');
|
||
if (!icon) return;
|
||
icon.setAttribute('data-lucide', isDark ? 'sun' : 'moon');
|
||
lucide.createIcons();
|
||
}
|
||
|
||
loadModels() {
|
||
const stored = localStorage.getItem('model-list');
|
||
if (stored) {
|
||
try {
|
||
this.models = JSON.parse(stored);
|
||
} catch (e) {
|
||
this.models = [];
|
||
}
|
||
}
|
||
this.renderModels();
|
||
}
|
||
|
||
saveModels() {
|
||
localStorage.setItem('model-list', JSON.stringify(this.models));
|
||
}
|
||
|
||
renderModels() {
|
||
const list = this.elements.modelList;
|
||
|
||
if (this.models.length === 0) {
|
||
list.innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon">
|
||
<i data-lucide="cpu" style="width: 40px; height: 40px;"></i>
|
||
</div>
|
||
<h2 class="empty-state-title">暂无模型配置</h2>
|
||
<p class="empty-state-subtitle">点击上方按钮添加您的第一个模型</p>
|
||
</div>
|
||
`;
|
||
lucide.createIcons();
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = '';
|
||
this.models.forEach(model => {
|
||
const card = this.createModelCard(model);
|
||
list.appendChild(card);
|
||
});
|
||
|
||
lucide.createIcons();
|
||
}
|
||
|
||
createModelCard(model) {
|
||
const card = document.createElement('div');
|
||
card.className = 'model-card';
|
||
|
||
const isDefault = model.isDefault;
|
||
|
||
// Mask API key for display
|
||
const maskApiKey = (key) => {
|
||
if (!key) return '未设置';
|
||
if (key.length <= 8) return '*'.repeat(key.length);
|
||
return key.substring(0, 4) + '*'.repeat(key.length - 8) + key.substring(key.length - 4);
|
||
};
|
||
|
||
card.innerHTML = `
|
||
<div class="model-card-header">
|
||
<div class="model-card-icon">
|
||
<i data-lucide="cpu" style="width: 24px; height: 24px;"></i>
|
||
</div>
|
||
<div class="model-card-info">
|
||
<div class="model-card-name">${this.escapeHtml(model.name)} ${isDefault ? '<span style="color: var(--success); font-size: 12px;">(默认)</span>' : ''}</div>
|
||
<span class="model-card-provider">${this.escapeHtml(model.provider || '自定义')}</span>
|
||
</div>
|
||
</div>
|
||
<div class="model-card-details">
|
||
<div class="model-detail-item">
|
||
<div class="model-detail-label">模型标识</div>
|
||
<div class="model-detail-value">${this.escapeHtml(model.model)}</div>
|
||
</div>
|
||
<div class="model-detail-item">
|
||
<div class="model-detail-label">服务器</div>
|
||
<div class="model-detail-value">${this.escapeHtml(model.server || '默认')}</div>
|
||
</div>
|
||
<div class="model-detail-item">
|
||
<div class="model-detail-label">API Key</div>
|
||
<div class="model-detail-value">${maskApiKey(model.apiKey)}</div>
|
||
</div>
|
||
</div>
|
||
<div class="model-card-actions">
|
||
${!isDefault ? `
|
||
<button class="model-card-action set-default" data-action="set-default" data-model-id="${model.id}">
|
||
<i data-lucide="star" style="width: 14px; height: 14px;"></i>
|
||
设为默认
|
||
</button>
|
||
` : `
|
||
<button class="model-card-action default" disabled>
|
||
<i data-lucide="check" style="width: 14px; height: 14px;"></i>
|
||
默认模型
|
||
</button>
|
||
`}
|
||
<button class="model-card-action" data-action="edit" data-model-id="${model.id}">
|
||
<i data-lucide="pencil" style="width: 14px; height: 14px;"></i>
|
||
编辑
|
||
</button>
|
||
<button class="model-card-action delete" data-action="delete" data-model-id="${model.id}">
|
||
<i data-lucide="trash-2" style="width: 14px; height: 14px;"></i>
|
||
删除
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
// Event listeners
|
||
card.addEventListener('click', (e) => {
|
||
const action = e.target.closest('[data-action]')?.dataset.action;
|
||
const modelId = e.target.closest('[data-model-id]')?.dataset.modelId;
|
||
|
||
if (action === 'set-default' && modelId) {
|
||
e.stopPropagation();
|
||
this.setDefaultModel(modelId);
|
||
} else if (action === 'edit' && modelId) {
|
||
e.stopPropagation();
|
||
this.openEditModal(modelId);
|
||
} else if (action === 'delete' && modelId) {
|
||
e.stopPropagation();
|
||
this.openDeleteModal(modelId);
|
||
}
|
||
});
|
||
|
||
return card;
|
||
}
|
||
|
||
setDefaultModel(modelId) {
|
||
this.models.forEach(m => m.isDefault = (m.id === modelId));
|
||
this.saveModels();
|
||
this.renderModels();
|
||
}
|
||
|
||
openModelModal() {
|
||
this.editingModelId = null;
|
||
this.elements.modelModalTitle.textContent = '添加模型';
|
||
this.elements.modelName.value = '';
|
||
this.elements.modelProvider.value = 'OpenAI';
|
||
this.elements.modelModel.value = '';
|
||
this.elements.modelServer.value = '';
|
||
this.elements.modelApiKey.value = '';
|
||
this.elements.modelIsDefault.checked = this.models.length === 0;
|
||
this.elements.modelModal.classList.add('active');
|
||
this.elements.modelName.focus();
|
||
}
|
||
|
||
openEditModal(modelId) {
|
||
const model = this.models.find(m => m.id === modelId);
|
||
if (!model) return;
|
||
|
||
this.editingModelId = modelId;
|
||
this.elements.modelModalTitle.textContent = '编辑模型';
|
||
this.elements.modelName.value = model.name;
|
||
this.elements.modelProvider.value = model.provider || 'OpenAI';
|
||
this.elements.modelModel.value = model.model;
|
||
this.elements.modelServer.value = model.server || '';
|
||
this.elements.modelApiKey.value = model.apiKey || '';
|
||
this.elements.modelIsDefault.checked = model.isDefault || false;
|
||
this.elements.modelModal.classList.add('active');
|
||
this.elements.modelName.focus();
|
||
}
|
||
|
||
closeModelModal() {
|
||
this.elements.modelModal.classList.remove('active');
|
||
this.editingModelId = null;
|
||
}
|
||
|
||
saveModel() {
|
||
const name = this.elements.modelName.value.trim();
|
||
const model = this.elements.modelModel.value.trim();
|
||
const provider = this.elements.modelProvider.value;
|
||
const server = this.elements.modelServer.value.trim();
|
||
const apiKey = this.elements.modelApiKey.value.trim();
|
||
const isDefault = this.elements.modelIsDefault.checked;
|
||
|
||
if (!name) {
|
||
alert('请输入模型名称');
|
||
return;
|
||
}
|
||
|
||
if (!model) {
|
||
alert('请输入模型标识符');
|
||
return;
|
||
}
|
||
|
||
const now = Date.now();
|
||
|
||
if (this.editingModelId) {
|
||
// 编辑现有模型
|
||
const modelConfig = this.models.find(m => m.id === this.editingModelId);
|
||
if (modelConfig) {
|
||
modelConfig.name = name;
|
||
modelConfig.provider = provider;
|
||
modelConfig.model = model;
|
||
modelConfig.server = server;
|
||
modelConfig.apiKey = apiKey;
|
||
modelConfig.isDefault = isDefault;
|
||
modelConfig.updatedAt = now;
|
||
}
|
||
|
||
// 如果设为默认,取消其他模型的默认状态
|
||
if (isDefault) {
|
||
this.models.forEach(m => {
|
||
if (m.id !== this.editingModelId) m.isDefault = false;
|
||
});
|
||
}
|
||
} else {
|
||
// 添加新模型
|
||
// 如果设为默认,取消其他模型的默认状态
|
||
if (isDefault) {
|
||
this.models.forEach(m => m.isDefault = false);
|
||
}
|
||
|
||
const newModel = {
|
||
id: this.generateUUID(),
|
||
name: name,
|
||
provider: provider,
|
||
model: model,
|
||
server: server,
|
||
apiKey: apiKey,
|
||
isDefault: isDefault || this.models.length === 0,
|
||
createdAt: now,
|
||
updatedAt: now
|
||
};
|
||
this.models.unshift(newModel);
|
||
}
|
||
|
||
this.saveModels();
|
||
this.renderModels();
|
||
this.closeModelModal();
|
||
}
|
||
|
||
openDeleteModal(modelId) {
|
||
this.deletingModelId = modelId;
|
||
this.elements.deleteModal.classList.add('active');
|
||
}
|
||
|
||
closeDeleteModal() {
|
||
this.elements.deleteModal.classList.remove('active');
|
||
this.deletingModelId = null;
|
||
}
|
||
|
||
confirmDelete() {
|
||
if (!this.deletingModelId) return;
|
||
|
||
const deletingModel = this.models.find(m => m.id === this.deletingModelId);
|
||
|
||
this.models = this.models.filter(m => m.id !== this.deletingModelId);
|
||
|
||
// 如果删除的是默认模型,设置第一个为默认
|
||
if (deletingModel?.isDefault && this.models.length > 0) {
|
||
this.models[0].isDefault = true;
|
||
}
|
||
|
||
this.saveModels();
|
||
this.renderModels();
|
||
this.closeDeleteModal();
|
||
}
|
||
|
||
generateUUID() {
|
||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||
const r = Math.random() * 16 | 0;
|
||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||
return v.toString(16);
|
||
});
|
||
}
|
||
|
||
escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
}
|
||
|
||
// Initialize app
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
new ModelManager();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|