feat(app): 添加流量统计功能和服务器配置管理
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m47s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m48s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m29s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m53s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 1m14s
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) Successful in 1m36s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m51s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 1m17s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m50s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 1m18s

- 在WebServer中添加TrafficStore存储接口
- 将Web配置从根级别移动到Server.Web子结构下
- 移除Web配置中的BindAddr字段并调整默认值逻辑
- 在前端HomeView中替换模拟流量数据显示真实统计数据
- 添加流量统计API接口(/traffic/stats和/traffic/hourly)
- 实现SQLite数据库流量统计表创建和CRUD操作
- 在Relay包中添加带流量统计的数据转发功能
- 在设置页面添加服务器配置编辑和保存功能
- 创建流量统计处理器和相关数据模型定义
This commit is contained in:
Flik
2026-01-22 19:53:40 +08:00
parent 06dfcfaff3
commit d1058f9e89
14 changed files with 634 additions and 74 deletions

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { CloudDownloadOutline, RefreshOutline, ServerOutline } from '@vicons/ionicons5'
import { CloudDownloadOutline, RefreshOutline, ServerOutline, SettingsOutline, SaveOutline } from '@vicons/ionicons5'
import GlassTag from '../components/GlassTag.vue'
import { useToast } from '../composables/useToast'
import { useConfirm } from '../composables/useConfirm'
import {
getVersionInfo, checkServerUpdate, applyServerUpdate,
type UpdateInfo, type VersionInfo
getServerConfig, updateServerConfig,
type UpdateInfo, type VersionInfo, type ServerConfigResponse
} from '../api'
const message = useToast()
@@ -18,6 +19,20 @@ const loading = ref(true)
const checkingServer = ref(false)
const updatingServer = ref(false)
// 服务器配置
const serverConfig = ref<ServerConfigResponse | null>(null)
const configLoading = ref(false)
const savingConfig = ref(false)
// 配置表单
const configForm = ref({
bind_addr: '',
heartbeat_sec: 30,
heartbeat_timeout: 90,
web_username: '',
web_password: ''
})
const loadVersionInfo = async () => {
try {
const { data } = await getVersionInfo()
@@ -29,6 +44,53 @@ const loadVersionInfo = async () => {
}
}
const loadServerConfig = async () => {
configLoading.value = true
try {
const { data } = await getServerConfig()
serverConfig.value = data
// 填充表单
configForm.value = {
bind_addr: data.server.bind_addr,
heartbeat_sec: data.server.heartbeat_sec,
heartbeat_timeout: data.server.heartbeat_timeout,
web_username: data.web.username,
web_password: ''
}
} 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: {
bind_addr: configForm.value.bind_addr,
heartbeat_sec: configForm.value.heartbeat_sec,
heartbeat_timeout: configForm.value.heartbeat_timeout
},
web: {
username: configForm.value.web_username
}
}
// 只有填写了密码才更新
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
}
}
const handleCheckServerUpdate = async () => {
checkingServer.value = true
try {
@@ -83,6 +145,7 @@ const formatBytes = (bytes: number): string => {
onMounted(() => {
loadVersionInfo()
loadServerConfig()
})
</script>
@@ -140,6 +203,87 @@ onMounted(() => {
</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-group">
<label class="form-label">服务器地址</label>
<input
v-model="configForm.bind_addr"
type="text"
class="glass-input"
placeholder="0.0.0.0"
/>
<span class="form-hint">服务器监听地址修改后需重启生效</span>
</div>
<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-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>
<!-- Server Update Card -->
<div class="glass-card">
<div class="card-header">
@@ -406,4 +550,71 @@ onMounted(() => {
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: rgba(255, 255, 255, 0.7);
font-weight: 500;
}
.form-hint {
font-size: 11px;
color: rgba(255, 255, 255, 0.4);
}
.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: rgba(255, 255, 255, 0.1);
margin: 8px 0;
}
.form-actions {
margin-top: 8px;
}
/* Glass Input */
.glass-input {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 8px;
padding: 10px 14px;
color: white;
font-size: 14px;
outline: none;
transition: all 0.2s;
}
.glass-input:focus {
border-color: rgba(96, 165, 250, 0.5);
background: rgba(255, 255, 255, 0.12);
}
.glass-input::placeholder {
color: rgba(255, 255, 255, 0.3);
}
</style>