fufeigemini / page /src /views /BackendView.vue
Leeflour's picture
Upload 197 files
d0dd276 verified
<template>
<div class="backend-view">
<div class="header">
<div class="title-section">
<h1>后端实例管理</h1>
<p class="subtitle">管理和切换不同的 Hajimi 后端实例</p>
</div>
<BackendSwitcher @openManager="scrollToManager" />
</div>
<div class="content">
<!-- 概览卡片 -->
<div class="overview-cards">
<div class="card">
<div class="card-icon">🏠</div>
<div class="card-content">
<h3>{{ backendStore.backends.length }}</h3>
<p>总实例数</p>
</div>
</div>
<div class="card">
<div class="card-icon"></div>
<div class="card-content">
<h3>{{ backendStore.connectedBackendsCount }}</h3>
<p>已连接</p>
</div>
</div>
<div class="card">
<div class="card-icon"></div>
<div class="card-content">
<h3>{{ backendStore.activeBackend?.name || '无' }}</h3>
<p>当前活跃</p>
</div>
</div>
<div class="card">
<div class="card-icon">🔄</div>
<div class="card-content">
<h3>{{ formatTime(backendStore.activeBackend?.lastConnected) || '从未' }}</h3>
<p>最后连接</p>
</div>
</div>
</div>
<!-- 快速操作 -->
<div class="quick-actions">
<h2>快速操作</h2>
<div class="action-buttons">
<button @click="testAllConnections" :disabled="testingAll" class="btn btn-primary">
<span class="icon">🔍</span>
{{ testingAll ? '测试中...' : '测试所有连接' }}
</button>
<button @click="showAddModal = true" class="btn btn-success">
<span class="icon"></span>
添加新实例
</button>
<button @click="exportConfig" class="btn btn-outline">
<span class="icon">📤</span>
导出配置
</button>
<button @click="importConfig" class="btn btn-outline">
<span class="icon">📥</span>
导入配置
</button>
<button @click="showUserGuide" class="btn btn-outline">
<span class="icon"></span>
使用指南
</button>
</div>
</div>
<!-- 后端管理器 -->
<div ref="managerRef">
<BackendManager />
</div>
</div>
<!-- 添加实例快速模态框 -->
<div v-if="showAddModal" class="modal-overlay" @click="closeAddModal">
<div class="modal quick-add-modal" @click.stop>
<div class="modal-header">
<h4>快速添加后端实例</h4>
<button @click="closeAddModal" class="close-btn">×</button>
</div>
<div class="modal-body">
<div class="preset-buttons">
<button
v-for="preset in presets"
:key="preset.id"
@click="usePreset(preset)"
class="preset-btn"
>
<div class="preset-icon">{{ preset.icon }}</div>
<div class="preset-info">
<h5>{{ preset.name }}</h5>
<p>{{ preset.description }}</p>
</div>
</button>
</div>
<div class="divider">或手动添加</div>
<div class="form-group">
<label>服务器地址</label>
<input
v-model="quickAddUrl"
type="url"
placeholder="https://your-hajimi-instance.com"
class="form-control"
@keyup.enter="quickAddInstance"
/>
</div>
</div>
<div class="modal-footer">
<button @click="closeAddModal" class="btn btn-secondary">取消</button>
<button
@click="quickAddInstance"
:disabled="!quickAddUrl.trim()"
class="btn btn-primary"
>
添加
</button>
</div>
</div>
</div>
<!-- 隐藏的文件输入 -->
<input
ref="fileInput"
type="file"
accept=".json"
style="display: none"
@change="handleFileImport"
/>
<!-- 用户指南 -->
<UserGuide ref="userGuideRef" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useBackendStore } from '@/stores/backend'
import BackendManager from '@/components/backend/BackendManager.vue'
import BackendSwitcher from '@/components/backend/BackendSwitcher.vue'
import UserGuide from '@/components/backend/UserGuide.vue'
const backendStore = useBackendStore()
// 状态
const testingAll = ref(false)
const showAddModal = ref(false)
const quickAddUrl = ref('')
const managerRef = ref(null)
const fileInput = ref(null)
const userGuideRef = ref(null)
// 预设配置
const presets = [
{
id: 'localhost',
name: '本地开发',
icon: '🏠',
description: '本地开发服务器 (localhost:7860)',
baseUrl: 'http://localhost:7860'
},
{
id: 'production',
name: '生产服务器',
icon: '🚀',
description: '生产环境服务器',
baseUrl: 'https://'
},
{
id: 'staging',
name: '测试服务器',
icon: '🧪',
description: '测试环境服务器',
baseUrl: 'https://'
}
]
// 测试所有连接
async function testAllConnections() {
testingAll.value = true
try {
await backendStore.testAllConnections()
} catch (error) {
console.error('批量测试失败:', error)
} finally {
testingAll.value = false
}
}
// 使用预设
function usePreset(preset) {
if (preset.id === 'localhost') {
quickAddUrl.value = preset.baseUrl
} else {
quickAddUrl.value = preset.baseUrl
}
}
// 快速添加实例
function quickAddInstance() {
if (!quickAddUrl.value.trim()) return
try {
const url = new URL(quickAddUrl.value)
const name = url.hostname
backendStore.addBackend({
name: name,
baseUrl: quickAddUrl.value.trim(),
password: '',
description: `通过快速添加创建: ${url.hostname}`
})
backendStore.saveToStorage()
closeAddModal()
} catch (error) {
alert('请输入有效的 URL 地址')
}
}
// 关闭添加模态框
function closeAddModal() {
showAddModal.value = false
quickAddUrl.value = ''
}
// 滚动到管理器
function scrollToManager() {
if (managerRef.value) {
managerRef.value.scrollIntoView({ behavior: 'smooth' })
}
}
// 导出配置
function exportConfig() {
const config = {
backends: backendStore.backends,
activeBackendId: backendStore.activeBackendId,
exportTime: new Date().toISOString()
}
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `hajimi-backends-${new Date().toISOString().split('T')[0]}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
// 导入配置
function importConfig() {
fileInput.value?.click()
}
// 处理文件导入
function handleFileImport(event) {
const file = event.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = (e) => {
try {
const config = JSON.parse(e.target.result)
if (config.backends && Array.isArray(config.backends)) {
// 合并配置,保留本地实例
const localBackend = backendStore.backends.find(b => b.id === 'local')
const importedBackends = config.backends.filter(b => b.id !== 'local')
backendStore.backends.splice(0, backendStore.backends.length)
if (localBackend) {
backendStore.backends.push(localBackend)
}
backendStore.backends.push(...importedBackends)
backendStore.saveToStorage()
alert(`成功导入 ${importedBackends.length} 个后端实例`)
} else {
alert('无效的配置文件格式')
}
} catch (error) {
alert('配置文件解析失败: ' + error.message)
}
}
reader.readAsText(file)
// 清空文件输入
event.target.value = ''
}
// 显示用户指南
function showUserGuide() {
userGuideRef.value?.show()
}
// 格式化时间
function formatTime(isoString) {
if (!isoString) return ''
const date = new Date(isoString)
return date.toLocaleString('zh-CN')
}
// 初始化
onMounted(() => {
testAllConnections()
})
</script>
<style scoped>
.backend-view {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
min-height: 100vh;
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #e9ecef;
}
.title-section h1 {
margin: 0 0 8px 0;
color: #333;
font-size: 28px;
font-weight: 600;
}
.subtitle {
margin: 0;
color: #666;
font-size: 16px;
}
.content {
display: flex;
flex-direction: column;
gap: 30px;
}
.overview-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #f0f0f0;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
}
.card-icon {
font-size: 24px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 10px;
}
.card-content h3 {
margin: 0 0 4px 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.card-content p {
margin: 0;
font-size: 14px;
color: #666;
}
.quick-actions h2 {
margin: 0 0 16px 0;
color: #333;
font-size: 20px;
}
.action-buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.btn {
padding: 10px 16px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #0056b3;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover:not(:disabled) {
background: #1e7e34;
}
.btn-outline {
background: transparent;
color: #666;
border: 1px solid #ddd;
}
.btn-outline:hover:not(:disabled) {
background: #f8f9fa;
border-color: #007bff;
color: #007bff;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover:not(:disabled) {
background: #545b62;
}
.icon {
font-size: 16px;
}
/* 快速添加模态框 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal {
background: white;
border-radius: 12px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow: auto;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.quick-add-modal {
max-width: 600px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px;
border-bottom: 1px solid #e9ecef;
}
.modal-header h4 {
margin: 0;
color: #333;
font-size: 18px;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
}
.close-btn:hover {
background: #f8f9fa;
color: #333;
}
.modal-body {
padding: 24px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 24px;
border-top: 1px solid #e9ecef;
}
.preset-buttons {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.preset-btn {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid #e9ecef;
border-radius: 8px;
background: white;
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
width: 100%;
}
.preset-btn:hover {
border-color: #007bff;
background: #f8f9ff;
}
.preset-icon {
font-size: 24px;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 8px;
}
.preset-info h5 {
margin: 0 0 4px 0;
color: #333;
font-size: 16px;
}
.preset-info p {
margin: 0;
color: #666;
font-size: 14px;
}
.divider {
text-align: center;
color: #666;
font-size: 14px;
margin: 24px 0;
position: relative;
}
.divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #e9ecef;
z-index: 0;
}
.divider span {
background: white;
padding: 0 16px;
position: relative;
z-index: 1;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
font-size: 14px;
}
.form-control {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.form-control:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
@media (max-width: 768px) {
.backend-view {
padding: 16px;
}
.header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.overview-cards {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
.card {
padding: 16px;
gap: 12px;
}
.card-icon {
font-size: 20px;
width: 40px;
height: 40px;
}
.card-content h3 {
font-size: 20px;
}
.action-buttons {
flex-direction: column;
}
.btn {
justify-content: center;
}
}
</style>