Files
GoTunnel/web/src/views/SettingsView.vue
Flik 98a5525e6d
Some checks failed
Build Multi-Platform Binaries / build-frontend (push) Successful in 27s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m24s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m33s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m18s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m40s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 1m16s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m40s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Failing after 1m39s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m49s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 1m26s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m53s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 1m16s
feat(client): 添加服务端版本检查和客户端更新逻辑
- 引入 getVersionInfo API 和服务端版本获取功能
- 实现版本比较算法用于判断更新需求
- 添加服务端版本加载和目标版本计算逻辑
- 更新客户端版本显示为可更新目标版本
- 优化样式表移除不透明背景设置
- 调整进度条外观样式增强视觉效果
2026-01-23 08:40:54 +08:00

551 lines
12 KiB
Vue
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.
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ServerOutline, SettingsOutline, SaveOutline } from '@vicons/ionicons5'
import { useToast } from '../composables/useToast'
import {
getVersionInfo, getServerConfig, updateServerConfig,
type VersionInfo, type ServerConfigResponse
} from '../api'
const message = useToast()
const versionInfo = ref<VersionInfo | null>(null)
const loading = ref(true)
// 服务器配置
const serverConfig = ref<ServerConfigResponse | null>(null)
const configLoading = ref(false)
const savingConfig = ref(false)
// 配置表单
const configForm = ref({
heartbeat_sec: 30,
heartbeat_timeout: 90,
web_username: '',
web_password: '',
plugin_store_url: ''
})
const loadVersionInfo = async () => {
try {
const { data } = await getVersionInfo()
versionInfo.value = data
} catch (e) {
console.error('Failed to load version info', e)
} finally {
loading.value = false
}
}
const loadServerConfig = async () => {
configLoading.value = true
try {
const { data } = await getServerConfig()
serverConfig.value = data
// 填充表单
configForm.value = {
heartbeat_sec: data.server.heartbeat_sec,
heartbeat_timeout: data.server.heartbeat_timeout,
web_username: data.web.username,
web_password: '',
plugin_store_url: data.plugin_store.url
}
} catch (e) {
console.error('Failed to load server config', e)
} finally {
configLoading.value = false
}
}
const handleSaveConfig = async () => {
savingConfig.value = true
try {
const updateReq: any = {
server: {
heartbeat_sec: configForm.value.heartbeat_sec,
heartbeat_timeout: configForm.value.heartbeat_timeout
},
web: {
username: configForm.value.web_username
},
plugin_store: {
url: configForm.value.plugin_store_url
}
}
// 只有填写了密码才更新
if (configForm.value.web_password) {
updateReq.web.password = configForm.value.web_password
}
await updateServerConfig(updateReq)
message.success('配置已保存,部分配置需要重启服务后生效')
configForm.value.web_password = ''
} catch (e: any) {
message.error(e.response?.data || '保存配置失败')
} finally {
savingConfig.value = false
}
}
onMounted(() => {
loadVersionInfo()
loadServerConfig()
})
</script>
<template>
<div class="settings-page">
<!-- Particles -->
<div class="particles">
<div class="particle particle-1"></div>
<div class="particle particle-2"></div>
<div class="particle particle-3"></div>
</div>
<div class="settings-content">
<!-- Header -->
<div class="page-header">
<h1 class="page-title">系统设置</h1>
<p class="page-subtitle">管理服务端配置和系统更新</p>
</div>
<!-- Version Info Card -->
<div class="glass-card">
<div class="card-header">
<h3>版本信息</h3>
<ServerOutline class="header-icon" />
</div>
<div class="card-body">
<div v-if="loading" class="loading-state">加载中...</div>
<div v-else-if="versionInfo" class="info-grid">
<div class="info-item">
<span class="info-label">版本号</span>
<span class="info-value">{{ versionInfo.version }}</span>
</div>
<div class="info-item">
<span class="info-label">Git 提交</span>
<span class="info-value mono">{{ versionInfo.git_commit?.slice(0, 8) || 'N/A' }}</span>
</div>
<div class="info-item">
<span class="info-label">构建时间</span>
<span class="info-value">{{ versionInfo.build_time || 'N/A' }}</span>
</div>
<div class="info-item">
<span class="info-label">Go 版本</span>
<span class="info-value">{{ versionInfo.go_version }}</span>
</div>
<div class="info-item">
<span class="info-label">操作系统</span>
<span class="info-value">{{ versionInfo.os }}</span>
</div>
<div class="info-item">
<span class="info-label">架构</span>
<span class="info-value">{{ versionInfo.arch }}</span>
</div>
</div>
<div v-else class="empty-state">无法加载版本信息</div>
</div>
</div>
<!-- Server Config Card -->
<div class="glass-card">
<div class="card-header">
<h3>服务器配置</h3>
<SettingsOutline class="header-icon" />
</div>
<div class="card-body">
<div v-if="configLoading" class="loading-state">加载中...</div>
<div v-else-if="serverConfig" class="config-form">
<div class="form-row">
<div class="form-group">
<label class="form-label">心跳间隔 ()</label>
<input
v-model.number="configForm.heartbeat_sec"
type="number"
class="glass-input"
min="1"
max="300"
/>
</div>
<div class="form-group">
<label class="form-label">心跳超时 ()</label>
<input
v-model.number="configForm.heartbeat_timeout"
type="number"
class="glass-input"
min="1"
max="600"
/>
</div>
</div>
<div class="form-divider"></div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Web 用户名</label>
<input
v-model="configForm.web_username"
type="text"
class="glass-input"
placeholder="admin"
/>
</div>
<div class="form-group">
<label class="form-label">Web 密码</label>
<input
v-model="configForm.web_password"
type="password"
class="glass-input"
placeholder="留空则不修改"
/>
</div>
</div>
<div class="form-divider"></div>
<div class="form-group">
<label class="form-label">插件商店地址</label>
<input
v-model="configForm.plugin_store_url"
type="text"
class="glass-input"
placeholder="https://git.92coco.cn/flik/GoTunnel-Plugins/raw/branch/main/store.json"
/>
<span class="form-hint">插件商店的 API 地址留空使用默认地址</span>
</div>
<div class="form-actions">
<button
class="glass-btn primary"
:disabled="savingConfig"
@click="handleSaveConfig"
>
<SaveOutline class="btn-icon" />
保存配置
</button>
</div>
</div>
<div v-else class="empty-state">无法加载配置信息</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.settings-page {
min-height: calc(100vh - 108px);
background: transparent;
position: relative;
overflow: hidden;
padding: 32px;
}
/* Hide particles */
.particles {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.particle {
position: absolute;
border-radius: 50%;
opacity: 0.15;
filter: blur(60px);
animation: float 20s ease-in-out infinite;
}
.particle-1 {
width: 350px;
height: 350px;
background: var(--color-accent);
top: -80px;
right: -80px;
}
.particle-2 {
width: 280px;
height: 280px;
background: #8b5cf6;
bottom: -40px;
left: -40px;
animation-delay: -5s;
}
.particle-3 {
width: 220px;
height: 220px;
background: var(--color-success);
top: 40%;
left: 30%;
animation-delay: -10s;
}
@keyframes float {
0%, 100% { transform: translate(0, 0) scale(1); }
25% { transform: translate(30px, -30px) scale(1.05); }
50% { transform: translate(-20px, 20px) scale(0.95); }
75% { transform: translate(-30px, -20px) scale(1.02); }
}
.settings-content {
position: relative;
z-index: 10;
max-width: 900px;
margin: 0 auto;
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-size: 28px;
font-weight: 700;
color: var(--color-text-primary);
margin: 0 0 8px 0;
}
.page-subtitle {
color: var(--color-text-secondary);
margin: 0;
font-size: 14px;
}
/* Glass Card */
.glass-card {
background: var(--color-bg-tertiary);
border-radius: 12px;
border: 1px solid var(--color-border);
margin-bottom: 20px;
}
.card-header {
padding: 16px 20px;
border-bottom: 1px solid var(--color-border-light);
display: flex;
justify-content: space-between;
align-items: center;
}
.card-header h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--color-text-primary);
}
.card-body {
padding: 20px;
}
/* Info Grid */
.info-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
@media (max-width: 600px) {
.info-grid { grid-template-columns: repeat(2, 1fr); }
}
.info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-label {
font-size: 12px;
color: var(--color-text-muted);
}
.info-value {
font-size: 14px;
color: var(--color-text-primary);
font-weight: 500;
}
.info-value.mono {
font-family: monospace;
}
/* States */
.loading-state, .empty-state {
text-align: center;
padding: 32px;
color: var(--color-text-muted);
}
/* Update Alert */
.update-alert {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
font-size: 13px;
}
.update-alert.success {
background: rgba(0, 186, 124, 0.15);
border: 1px solid rgba(0, 186, 124, 0.3);
color: var(--color-success);
}
.update-alert.info {
background: rgba(29, 155, 240, 0.15);
border: 1px solid rgba(29, 155, 240, 0.3);
color: var(--color-info);
}
/* Download Info */
.download-info {
color: var(--color-text-secondary);
font-size: 13px;
margin-bottom: 12px;
}
/* Release Note */
.release-note {
margin-bottom: 16px;
}
.note-label {
display: block;
font-size: 12px;
color: var(--color-text-muted);
margin-bottom: 6px;
}
.release-note pre {
margin: 0;
white-space: pre-wrap;
font-size: 12px;
color: var(--color-text-secondary);
background: var(--color-bg-elevated);
padding: 12px;
border-radius: 8px;
max-height: 150px;
overflow-y: auto;
}
/* Glass Button */
.glass-btn {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 8px 16px;
color: var(--color-text-primary);
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
gap: 6px;
}
.glass-btn:hover:not(:disabled) {
background: var(--color-border);
}
.glass-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.glass-btn.small {
padding: 6px 12px;
font-size: 12px;
}
.glass-btn.primary {
background: var(--color-accent);
border: none;
}
.glass-btn.primary:hover:not(:disabled) {
background: var(--color-accent-hover);
}
/* Icon styles */
.header-icon {
width: 20px;
height: 20px;
color: var(--color-text-muted);
}
.btn-icon {
width: 14px;
height: 14px;
}
/* Config Form */
.config-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-size: 13px;
color: var(--color-text-secondary);
font-weight: 500;
}
.form-hint {
font-size: 11px;
color: var(--color-text-muted);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 500px) {
.form-row {
grid-template-columns: 1fr;
}
}
.form-divider {
height: 1px;
background: var(--color-border-light);
margin: 8px 0;
}
.form-actions {
margin-top: 8px;
}
/* Glass Input */
.glass-input {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 10px 14px;
color: var(--color-text-primary);
font-size: 14px;
outline: none;
transition: all 0.15s;
}
.glass-input:focus {
border-color: var(--color-accent);
}
.glass-input::placeholder {
color: var(--color-text-muted);
}
</style>