feat: 前端完整源码纳入 Go 仓库(web/,标题已改为 系统调优 1.0)
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.log
|
||||||
|
__pycache__/
|
||||||
|
.venv
|
||||||
|
.idea
|
||||||
|
test
|
||||||
|
dist/*
|
||||||
|
dist/
|
||||||
|
.DS_Store
|
||||||
|
/node_modules/
|
||||||
|
/www/
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Vue 3 + Vite 项目模板
|
||||||
|
|
||||||
|
这是一个基于 Vue 3 和 Vite 的项目模板,旨在帮助开发者快速搭建和开发现代前端应用。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **Vue 3**: 使用 Vue 3 的组合式 API 和 `<script setup>` 语法。
|
||||||
|
- **Vite**: 快速构建工具,支持热更新和快速开发。
|
||||||
|
- **TypeScript**: 可选支持,为项目提供类型检查和更好的开发体验。
|
||||||
|
- **CSS**: 使用原生 CSS 或预处理器(如 SCSS)进行样式管理。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
- vue-demo
|
||||||
|
- index.html # 主入口 HTML 文件
|
||||||
|
- package.json # 项目依赖和脚本配置
|
||||||
|
- public # 静态资源目录
|
||||||
|
- src # 源代码目录
|
||||||
|
- assets # 静态资源目录
|
||||||
|
- App.vue # 根组件
|
||||||
|
- main.js # 应用入口文件
|
||||||
|
- style.css # 全局样式文件
|
||||||
|
- components # 组件目录
|
||||||
|
- vite.config.js # Vite 配置文件
|
||||||
|
- yarn.lock # Yarn 包管理器锁文件
|
||||||
|
|
||||||
|
## 功能介绍
|
||||||
|
|
||||||
|
- **组件化开发**: 使用 Vue 3 的单文件组件 (SFC) 进行模块化开发。
|
||||||
|
- **热更新**: Vite 提供了快速的热更新功能,开发过程中无需手动刷新页面。
|
||||||
|
- **TypeScript 支持**: 可选支持 TypeScript,提升代码质量和开发效率。
|
||||||
|
- **样式管理**: 支持原生 CSS 和预处理器(如 SCSS),方便管理样式。
|
||||||
|
|
||||||
|
## API 接口说明
|
||||||
|
|
||||||
|
本项目目前未包含后端 API,但可以通过以下方式集成后端服务:
|
||||||
|
|
||||||
|
1. 使用 Axios 或 Fetch 进行 HTTP 请求。
|
||||||
|
2. 配置 Vite 的代理功能,解决跨域问题。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 请确保已安装 Node.js 和 Yarn。
|
||||||
|
- 如果需要 TypeScript 支持,请在创建项目时选择相关选项。
|
||||||
|
- 生产构建时,请确保已正确配置环境变量。
|
||||||
|
|
||||||
|
|
||||||
|
## 学习资源
|
||||||
|
|
||||||
|
- [Vue 3 官方文档](https://v3.vuejs.org/)
|
||||||
|
- [Vite 官方文档](https://vitejs.dev/)
|
||||||
|
- [TypeScript 官方文档](https://www.typescriptlang.org/)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
+669
@@ -0,0 +1,669 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-root" :class="{ 'unrooted': !rootOk }">
|
||||||
|
<!-- 非 root 强制提示层 -->
|
||||||
|
<div v-if="!rootOk && rootChecked" class="root-banner">
|
||||||
|
<div class="root-banner-inner">
|
||||||
|
<span class="root-icon"><Icon name="lock" :size="48" /></span>
|
||||||
|
<div class="root-msg">
|
||||||
|
<h3>需要 root 权限</h3>
|
||||||
|
<p>系统调优需要修改内核参数,当前进程不是 root 用户,无法执行调优操作。</p>
|
||||||
|
<p class="root-hint">请以 root 身份运行 GA 服务后重试,或联系管理员授权。</p>
|
||||||
|
</div>
|
||||||
|
<button class="btn ghost" @click="checkRoot">重新检测</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主界面 -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- 顶栏 -->
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="brand">
|
||||||
|
<img src="./assets/icon.png" class="brand-logo" alt="logo" />
|
||||||
|
<div>
|
||||||
|
<h1>系统调优 <span class="ver">1.0</span></h1>
|
||||||
|
<p class="subtitle">服务器性能一键优化 · 安全备份回滚</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="topbar-right">
|
||||||
|
<span v-if="hw.memory_gb" class="chip chip-info"><Icon name="memory" :size="13" /> {{ hw.memory_gb }}G</span>
|
||||||
|
<span v-if="hw.cpu_cores" class="chip chip-info"><Icon name="cpu" :size="13" /> {{ hw.cpu_cores }}核</span>
|
||||||
|
<span v-if="hw.disk_type !== 'unknown'" class="chip" :class="hw.disk_type==='ssd'?'chip-green':'chip-yellow'">
|
||||||
|
<Icon name="hardDrive" :size="13" /> {{ hw.disk_type === 'ssd' ? 'SSD' : 'HDD' }}
|
||||||
|
</span>
|
||||||
|
<span v-if="hw.is_container" class="chip chip-yellow"><Icon name="box" :size="13" /> 容器</span>
|
||||||
|
<button class="btn ghost" @click="reloadAll"><Icon name="refresh" :size="13" /> 刷新</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 标签导航 -->
|
||||||
|
<nav class="tabs">
|
||||||
|
<button v-for="t in tabs" :key="t.key" class="tab" :class="{ active: tab === t.key }" @click="switchTab(t.key)">
|
||||||
|
<Icon :name="t.icon" :size="15" /> {{ t.name }}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- ================= 页面内容 ================= -->
|
||||||
|
<main class="content">
|
||||||
|
<!-- ① 一键优化 -->
|
||||||
|
<section v-show="tab==='preset'">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-title"><Icon name="zap" :size="18" /> 一键优化</h2>
|
||||||
|
<p class="card-desc">选择场景自动应用推荐参数组合,<b>可随时切换方案或回滚</b></p>
|
||||||
|
<div v-if="currentPreset.name" class="preset-current">
|
||||||
|
<Icon name="check" :size="14" /> 当前生效方案:<b>{{ currentPreset.name }}</b>
|
||||||
|
<span v-if="currentPreset.applied_at" class="muted">({{ currentPreset.applied_at }} 应用)</span>
|
||||||
|
</div>
|
||||||
|
<div class="preset-grid">
|
||||||
|
<div v-for="(p, key) in presets" :key="key" class="preset-card" :class="{ chosen: chosenPreset === key, active: currentPreset.key === key }" @click="chosenPreset = key">
|
||||||
|
<h3>{{ p.name }}</h3>
|
||||||
|
<span v-if="currentPreset.key === key" class="preset-active-badge"><Icon name="check" :size="11" /> 使用中</span>
|
||||||
|
<p>{{ p.desc }}</p>
|
||||||
|
<div class="preset-params">
|
||||||
|
<span v-for="k in Object.keys(p.params)" :key="k" class="param-chip">{{ k }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn primary" @click="applyPreset" :disabled="loading || !chosenPreset">
|
||||||
|
<Icon name="zap" :size="14" /> 应用「{{ chosenPreset ? presets[chosenPreset].name : '未选择' }}」
|
||||||
|
</button>
|
||||||
|
<span v-if="currentPreset.key && chosenPreset && chosenPreset !== currentPreset.key" class="muted">点击应用将切换到新方案</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ② Swappiness -->
|
||||||
|
<section v-show="tab==='swappiness'">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-title"><Icon name="memory" :size="18" /> Swappiness 调优</h2>
|
||||||
|
<p class="card-desc">控制内核使用 swap 的积极程度(0-100)</p>
|
||||||
|
<div v-if="!swapEnabled" class="swap-disabled">
|
||||||
|
<div class="swap-disabled-icon"><Icon name="x" :size="22" /></div>
|
||||||
|
<b>服务器未开启 Swap / 虚拟内存</b>
|
||||||
|
<p>Swappiness 只在系统使用 swap 时才有意义。请先配置 Swap / 虚拟内存:</p>
|
||||||
|
<p class="swap-guide">打开面板「应用中心」→ 搜索并安装「运维中心」→ 打开应用 → 进入「内存管理」界面 → 配置 Swap / 虚拟内存</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="swappiness-box">
|
||||||
|
<div class="swap-current">
|
||||||
|
当前值:<b class="text-green">{{ swapCurrent ?? '-' }}</b>
|
||||||
|
<span class="swap-tip">服务器推荐 10 · 数据库推荐 0-1 · 桌面 60</span>
|
||||||
|
</div>
|
||||||
|
<input type="range" min="0" max="100" v-model.number="swapValue" class="slider" />
|
||||||
|
<div class="swap-labels"><span>0(尽量不用swap)</span><span>100(积极swap)</span></div>
|
||||||
|
<div class="swap-value-big">{{ swapValue }}</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn primary" @click="applySwap" :disabled="loading"><Icon name="memory" :size="14" /> 应用 Swappiness</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ③ TCP 加速 -->
|
||||||
|
<section v-show="tab==='tcp'">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-title"><Icon name="rocket" :size="18" /> TCP 加速</h2>
|
||||||
|
<p class="card-desc">开启 BBR 拥塞控制 + 优化 TCP 参数</p>
|
||||||
|
<div class="tcp-status">
|
||||||
|
<div class="tcp-item">
|
||||||
|
<span>当前拥塞算法</span>
|
||||||
|
<b class="text-green">{{ tcpInfo['net.ipv4.tcp_congestion_control'] || '-' }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="tcp-item">
|
||||||
|
<span>队列规则</span>
|
||||||
|
<b>{{ tcpInfo['net.core.default_qdisc'] || '-' }}</b>
|
||||||
|
</div>
|
||||||
|
<div class="tcp-item">
|
||||||
|
<span>BBR 状态</span>
|
||||||
|
<b class="tcp-bbr" :class="bbrOn ? 'text-green' : 'text-red'">
|
||||||
|
<Icon :name="bbrOn ? 'check' : 'x'" :size="16" />
|
||||||
|
{{ bbrOn ? '已开启' : '未开启' }}
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button v-if="!bbrOn" class="btn primary" @click="enableBBR" :disabled="loading || !tcpInfo.bbr_available">
|
||||||
|
<Icon name="rocket" :size="14" /> 开启 BBR
|
||||||
|
</button>
|
||||||
|
<button v-else class="btn danger-btn" @click="disableBBR" :disabled="loading">
|
||||||
|
<Icon name="power" :size="14" /> 关闭 BBR
|
||||||
|
</button>
|
||||||
|
<span v-if="!tcpInfo.bbr_available" class="muted">当前内核不支持 BBR</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ④ 内核参数 -->
|
||||||
|
<section v-show="tab==='params'">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-title"><Icon name="settings" :size="18" /> 内核参数</h2>
|
||||||
|
<div class="param-toolbar">
|
||||||
|
<div class="search-box">
|
||||||
|
<Icon name="search" :size="14" class="search-icon" />
|
||||||
|
<input v-model="paramSearch" class="input" placeholder="搜索参数名 / 说明..." />
|
||||||
|
</div>
|
||||||
|
<label class="unlock-toggle">
|
||||||
|
<input type="checkbox" v-model="dangerUnlocked" />
|
||||||
|
<span>解锁危险参数(风险自担)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-for="g in filteredGroups" :key="g.key" class="param-group">
|
||||||
|
<h3 class="param-group-title"><Icon :name="g.icon" :size="14" /> {{ g.name }}</h3>
|
||||||
|
<table class="param-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>参数</th><th>说明</th><th>当前值</th><th>风险</th><th>操作</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="([key, info]) in groupParams(g)" :key="key">
|
||||||
|
<td class="mono">{{ key }}</td>
|
||||||
|
<td class="param-desc">
|
||||||
|
<b>{{ info.name }}</b>
|
||||||
|
<small>{{ info.desc }}</small>
|
||||||
|
</td>
|
||||||
|
<td class="mono current-val">{{ paramValues[key] ?? '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="risk-badge" :class="'risk-' + info.risk">{{ riskLabel(info.risk) }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn sm" :class="isDanger(key) && !dangerUnlocked ? 'disabled' : ''"
|
||||||
|
:disabled="isDanger(key) && !dangerUnlocked"
|
||||||
|
@click="editParam(key, info)">编辑</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ⑤ 备份回滚 -->
|
||||||
|
<section v-show="tab==='backup'">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-title"><Icon name="shield" :size="18" /> 备份与回滚</h2>
|
||||||
|
<p class="card-desc">初始快照记录首次使用时的配置,可随时一键恢复到任意快照</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="btn primary" @click="createSnapshot" :disabled="loading"><Icon name="camera" :size="14" /> 立即备份当前状态</button>
|
||||||
|
</div>
|
||||||
|
<table class="param-table" v-if="snapshots.length">
|
||||||
|
<thead><tr><th>快照</th><th>类型</th><th>时间</th><th>原因</th><th>参数数</th><th>操作</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="s in snapshots" :key="s.name">
|
||||||
|
<td class="mono">{{ s.name }}</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="s.kind==='init'" class="risk-badge risk-safe">初始</span>
|
||||||
|
<span v-else class="risk-badge risk-warn">手动</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ s.created_at }}</td>
|
||||||
|
<td>{{ s.reason }}</td>
|
||||||
|
<td>{{ s.param_count }}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn sm danger-btn" @click="restoreSnapshot(s)"><Icon name="rotate" :size="13" /> 恢复到此</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p v-else class="muted">暂无快照,点击「立即备份」创建</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ⑥ 操作日志 -->
|
||||||
|
<section v-show="tab==='logs'">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-title"><Icon name="fileText" :size="18" /> 操作日志</h2>
|
||||||
|
<p class="card-desc">记录每次调优操作,出问题可追溯</p>
|
||||||
|
<table class="param-table" v-if="logs.length">
|
||||||
|
<thead><tr><th>时间</th><th>操作</th><th>详情</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(l, i) in logs" :key="i">
|
||||||
|
<td class="mono">{{ l.time }}</td>
|
||||||
|
<td>{{ l.action }}</td>
|
||||||
|
<td class="mono small">{{ JSON.stringify(Object.fromEntries(Object.entries(l).filter(([k]) => !['time','action'].includes(k)))) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p v-else class="muted">暂无操作记录</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { call, toast } from './api'
|
||||||
|
import { PARAM_GROUPS, ALL_PARAMS, searchParams, RISK_META } from './params'
|
||||||
|
import { Icon } from './icons'
|
||||||
|
|
||||||
|
// ===== 状态 =====
|
||||||
|
const tab = ref('preset')
|
||||||
|
const rootOk = ref(false)
|
||||||
|
const rootChecked = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const hw = ref({})
|
||||||
|
const presets = ref({})
|
||||||
|
const chosenPreset = ref('')
|
||||||
|
const currentPreset = ref({ key: null, name: null, applied_at: '' })
|
||||||
|
const swapEnabled = ref(true)
|
||||||
|
const swapCurrent = ref(null)
|
||||||
|
const swapValue = ref(10)
|
||||||
|
const tcpInfo = ref({})
|
||||||
|
const paramSearch = ref('')
|
||||||
|
const dangerUnlocked = ref(false)
|
||||||
|
const paramValues = ref({})
|
||||||
|
const snapshots = ref([])
|
||||||
|
const logs = ref([])
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ key: 'preset', icon: 'zap', name: '一键优化' },
|
||||||
|
{ key: 'swappiness', icon: 'memory', name: 'Swappiness' },
|
||||||
|
{ key: 'tcp', icon: 'rocket', name: 'TCP加速' },
|
||||||
|
{ key: 'params', icon: 'settings', name: '内核参数' },
|
||||||
|
{ key: 'backup', icon: 'shield', name: '备份回滚' },
|
||||||
|
{ key: 'logs', icon: 'fileText', name: '操作日志' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// ===== 工具 =====
|
||||||
|
const riskLabel = r => RISK_META[r]?.label || r
|
||||||
|
const isDanger = key => ALL_PARAMS[key]?.risk === 'danger'
|
||||||
|
const groupParams = g => {
|
||||||
|
const all = searchParams(paramSearch.value)
|
||||||
|
return all.filter(([k]) => PARAM_GROUPS.find(x => x.key === g.key)?.params[k])
|
||||||
|
}
|
||||||
|
const filteredGroups = computed(() => {
|
||||||
|
const kw = paramSearch.value.trim()
|
||||||
|
if (!kw) return PARAM_GROUPS
|
||||||
|
return PARAM_GROUPS.filter(g => groupParams(g).length > 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ===== 加载 =====
|
||||||
|
async function checkRoot() {
|
||||||
|
try {
|
||||||
|
const r = await call('tuner_check_root')
|
||||||
|
rootOk.value = r.ok
|
||||||
|
} catch (e) {
|
||||||
|
rootOk.value = false
|
||||||
|
}
|
||||||
|
rootChecked.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runDetect() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await call('tuner_detect')
|
||||||
|
hw.value = r.hardware
|
||||||
|
tcpInfo.value = r.tcp
|
||||||
|
// swap 状态 + 当前预设(来自 detect 返回)
|
||||||
|
swapEnabled.value = r.swap ? r.swap.swap_enabled : true
|
||||||
|
if (r.preset) {
|
||||||
|
currentPreset.value = { key: r.preset.current, name: r.preset.name, applied_at: r.preset.applied_at || '' }
|
||||||
|
if (r.preset.current && !chosenPreset.value) chosenPreset.value = r.preset.current
|
||||||
|
}
|
||||||
|
swapCurrent.value = await call('tuner_get_param', { key: 'vm.swappiness' }).then(x => x.value).catch(() => null)
|
||||||
|
swapValue.value = parseInt(swapCurrent.value || '10', 10)
|
||||||
|
// 拉取知识库中所有参数当前值
|
||||||
|
const keys = Object.keys(ALL_PARAMS)
|
||||||
|
const values = {}
|
||||||
|
await Promise.all(keys.map(async k => {
|
||||||
|
try { values[k] = (await call('tuner_get_param', { key: k })).value } catch (e) { values[k] = '-' }
|
||||||
|
}))
|
||||||
|
paramValues.value = values
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('检测失败: ' + e.message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadAll() {
|
||||||
|
await Promise.all([loadPresets(), loadSnapshots(), loadLogs()])
|
||||||
|
await runDetect()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPresets() {
|
||||||
|
try { presets.value = await call('tuner_presets') } catch (e) { toast.error('加载预设失败') }
|
||||||
|
}
|
||||||
|
async function loadSnapshots() {
|
||||||
|
try { snapshots.value = await call('tuner_snapshots') } catch (e) {}
|
||||||
|
}
|
||||||
|
async function loadLogs() {
|
||||||
|
try { logs.value = await call('tuner_audit_logs') } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(k) {
|
||||||
|
tab.value = k
|
||||||
|
if (k === 'backup') loadSnapshots()
|
||||||
|
if (k === 'logs') loadLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 计算属性 =====
|
||||||
|
const bbrOn = computed(() => {
|
||||||
|
const algo = (tcpInfo.value['net.ipv4.tcp_congestion_control'] || '').toString().trim().toLowerCase()
|
||||||
|
return algo === 'bbr'
|
||||||
|
})
|
||||||
|
|
||||||
|
// ===== 操作 =====
|
||||||
|
async function applyPreset() {
|
||||||
|
if (!chosenPreset.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await call('tuner_apply_preset', { preset_key: chosenPreset.value })
|
||||||
|
toast.success(`已应用「${r.preset_name}」,成功 ${r.ok_count} 项`)
|
||||||
|
await runDetect()
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('应用失败: ' + e.message)
|
||||||
|
} finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applySwap() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await call('tuner_set_swappiness', { value: swapValue.value })
|
||||||
|
toast.success(`Swappiness 已设为 ${r.value}(当前 ${r.current})`)
|
||||||
|
swapCurrent.value = r.current
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('设置失败: ' + e.message)
|
||||||
|
} finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enableBBR() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await call('tuner_enable_bbr')
|
||||||
|
toast.success('BBR 已开启')
|
||||||
|
await runDetect()
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('开启失败: ' + e.message)
|
||||||
|
} finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disableBBR() {
|
||||||
|
if (!window.$gm || !$gm.dialog) {
|
||||||
|
if (!confirm('确定关闭 BBR?将恢复为默认拥塞算法 cubic')) return
|
||||||
|
doDisableBBR()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$gm.dialog.warning({
|
||||||
|
title: '关闭 BBR',
|
||||||
|
content: '确定关闭 BBR?将恢复为默认拥塞算法 cubic',
|
||||||
|
positiveText: '确认关闭',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: () => doDisableBBR(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doDisableBBR() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await call('tuner_disable_bbr')
|
||||||
|
toast.success('BBR 已关闭')
|
||||||
|
await runDetect()
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('关闭失败: ' + e.message)
|
||||||
|
} finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function editParam(key, info) {
|
||||||
|
const current = paramValues.value[key] ?? ''
|
||||||
|
const isDangerParam = info.risk === 'danger'
|
||||||
|
const note = isDangerParam && !dangerUnlocked.value ? '\n\n[危险参数] 请先在上方解锁!' : ''
|
||||||
|
const v = prompt(`修改 ${key}\n当前值: ${current}\n推荐: ${info.recommend}\n示例: ${info.example}${note}`, current)
|
||||||
|
if (v === null || v === '') return
|
||||||
|
if (isDangerParam && !dangerUnlocked.value) {
|
||||||
|
toast.warning('危险参数需先解锁')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
call('tuner_set_param', { key, value: v, unlocked: dangerUnlocked.value })
|
||||||
|
.then(r => {
|
||||||
|
toast.success(`${key} = ${r.current}`)
|
||||||
|
paramValues.value[key] = r.current
|
||||||
|
})
|
||||||
|
.catch(e => toast.error('修改失败: ' + e.message))
|
||||||
|
.finally(() => { loading.value = false })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSnapshot() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await call('tuner_create_snapshot', { reason: 'manual' })
|
||||||
|
toast.success(`已创建快照 ${r.name}`)
|
||||||
|
await loadSnapshots()
|
||||||
|
} catch (e) { toast.error('备份失败: ' + e.message) } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreSnapshot(s) {
|
||||||
|
if (!window.$gm || !$gm.dialog) {
|
||||||
|
if (!confirm(`确定回滚到快照 ${s.name}?`)) return
|
||||||
|
doRestore(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$gm.dialog.warning({
|
||||||
|
title: '回滚确认',
|
||||||
|
content: `确定回滚到快照「${s.name}」?\n回滚前会自动备份当前状态,可随时恢复。`,
|
||||||
|
positiveText: '确认回滚',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: () => doRestore(s),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doRestore(s) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const r = await call('tuner_restore_snapshot', { snapshot_name: s.name })
|
||||||
|
toast.success(`已回滚:恢复 ${r.restored_count} 个参数${r.failed_count ? `,${r.failed_count} 个失败` : ''}`)
|
||||||
|
await loadSnapshots()
|
||||||
|
await runDetect()
|
||||||
|
} catch (e) { toast.error('回滚失败: ' + e.message) } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 初始化 =====
|
||||||
|
function ensureInit() {
|
||||||
|
call('tuner_init_status').then(st => {
|
||||||
|
if (st.initialized) return
|
||||||
|
const doInit = async () => {
|
||||||
|
try {
|
||||||
|
const r = await call('tuner_init')
|
||||||
|
toast.success(r.already ? '已存在初始快照' : `初始化完成,已保存当前配置(${r.snapshot.param_count} 个参数)`)
|
||||||
|
await loadSnapshots()
|
||||||
|
} catch (e) {
|
||||||
|
toast.error('初始化失败: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (window.$gm && $gm.dialog) {
|
||||||
|
$gm.dialog.warning({
|
||||||
|
title: '首次使用初始化',
|
||||||
|
content: '系统调优需要先保存当前服务器的参数配置作为初始快照,之后可随时一键恢复。是否现在初始化?',
|
||||||
|
positiveText: '立即初始化',
|
||||||
|
negativeText: '稍后再说',
|
||||||
|
onPositiveClick: doInit,
|
||||||
|
})
|
||||||
|
} else if (confirm('系统调优需要先保存当前服务器的参数配置作为初始快照,之后可随时一键恢复。是否现在初始化?')) {
|
||||||
|
doInit()
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 初始化 =====
|
||||||
|
onMounted(async () => {
|
||||||
|
await checkRoot()
|
||||||
|
if (rootOk.value) {
|
||||||
|
await reloadAll()
|
||||||
|
ensureInit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 深色科技风 */
|
||||||
|
:root {
|
||||||
|
--bg: #0d1424;
|
||||||
|
--card: #141d33;
|
||||||
|
--card2: #1a2540;
|
||||||
|
--border: #243252;
|
||||||
|
--text: #dbe4f5;
|
||||||
|
--muted: #7c8db0;
|
||||||
|
--primary: #34d399;
|
||||||
|
--primary-dim: #1f9d74;
|
||||||
|
--danger: #ef4444;
|
||||||
|
--warn: #f59e0b;
|
||||||
|
--mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
||||||
|
*:focus { outline: none; }
|
||||||
|
button, a, input, select, textarea { -webkit-tap-highlight-color: transparent; }
|
||||||
|
button:focus, button:focus-visible, a:focus, a:focus-visible { outline: none; box-shadow: none; }
|
||||||
|
body { background: var(--bg); color: var(--text); font-family: -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif; font-size: 14px; }
|
||||||
|
|
||||||
|
.app-root { min-height: 100vh; padding: 16px; max-width: 1200px; margin: 0 auto; }
|
||||||
|
|
||||||
|
/* 非 root 提示 */
|
||||||
|
.root-banner {
|
||||||
|
position: fixed; inset: 0; background: rgba(10, 14, 28, 0.92);
|
||||||
|
display: flex; align-items: center; justify-content: center; z-index: 999;
|
||||||
|
}
|
||||||
|
.root-banner-inner {
|
||||||
|
background: var(--card); border: 1px solid var(--danger); border-radius: 16px;
|
||||||
|
padding: 32px 40px; max-width: 480px; text-align: center;
|
||||||
|
}
|
||||||
|
.root-icon { font-size: 48px; }
|
||||||
|
.root-msg h3 { color: var(--danger); font-size: 20px; margin: 12px 0 8px; }
|
||||||
|
.root-msg p { color: var(--muted); line-height: 1.7; margin-bottom: 6px; }
|
||||||
|
.root-hint { font-size: 12px; }
|
||||||
|
|
||||||
|
/* 顶栏 */
|
||||||
|
.topbar { display: flex; justify-content: space-between; align-items: center; padding: 12px 4px 16px; flex-wrap: wrap; gap: 10px; }
|
||||||
|
.brand { display: flex; align-items: center; gap: 12px; }
|
||||||
|
.brand-logo { width: 44px; height: 44px; border-radius: 10px; }
|
||||||
|
.brand h1 { font-size: 20px; font-weight: 700; }
|
||||||
|
.ver { background: var(--primary); color: #06251b; border-radius: 6px; padding: 1px 7px; font-size: 12px; vertical-align: 2px; }
|
||||||
|
.subtitle { color: var(--muted); font-size: 12px; margin-top: 2px; }
|
||||||
|
.topbar-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||||
|
|
||||||
|
.icon { display: inline-block; vertical-align: -3px; flex-shrink: 0; }
|
||||||
|
.chip { display: inline-flex; align-items: center; gap: 4px; border-radius: 8px; padding: 3px 10px; font-size: 12px; background: var(--card2); border: 1px solid var(--border); }
|
||||||
|
.chip-green { color: var(--primary); border-color: var(--primary-dim); }
|
||||||
|
.chip-yellow { color: var(--warn); border-color: var(--warn); }
|
||||||
|
|
||||||
|
/* 标签 */
|
||||||
|
.tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 16px; }
|
||||||
|
.tab {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
background: var(--card); border: 1px solid var(--border); color: var(--muted);
|
||||||
|
padding: 8px 16px; border-radius: 10px; cursor: pointer; font-size: 13px; transition: all .15s;
|
||||||
|
}
|
||||||
|
.tab:hover { color: var(--text); border-color: #31436b; }
|
||||||
|
.tab.active { background: linear-gradient(135deg, #0f3d2e, #14532d); color: var(--primary); border-color: var(--primary-dim); font-weight: 600; }
|
||||||
|
|
||||||
|
/* 卡片 */
|
||||||
|
.card { background: var(--card); border: 1px solid var(--border); border-radius: 14px; padding: 20px; margin-bottom: 16px; }
|
||||||
|
.card-title { display: flex; align-items: center; gap: 8px; font-size: 17px; margin-bottom: 4px; }
|
||||||
|
.card-title .icon { color: var(--primary); }
|
||||||
|
.card-desc { color: var(--muted); font-size: 13px; margin-bottom: 16px; }
|
||||||
|
.card-desc b { color: var(--warn); }
|
||||||
|
|
||||||
|
/* 预设 */
|
||||||
|
.preset-current {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
background: rgba(52, 211, 153, 0.1); border: 1px solid var(--primary-dim);
|
||||||
|
color: var(--primary); border-radius: 10px; padding: 8px 14px; font-size: 13px; margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.preset-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; margin-bottom: 16px; }
|
||||||
|
.preset-card {
|
||||||
|
background: var(--card2); border: 2px solid var(--border); border-radius: 12px; padding: 16px;
|
||||||
|
cursor: pointer; transition: all .15s; position: relative;
|
||||||
|
}
|
||||||
|
.preset-card:hover { border-color: #31436b; }
|
||||||
|
.preset-card.chosen { border-color: var(--primary); background: rgba(52, 211, 153, 0.08); }
|
||||||
|
.preset-card.active { border-color: var(--primary); box-shadow: 0 0 0 1px var(--primary); }
|
||||||
|
.preset-active-badge {
|
||||||
|
position: absolute; top: 12px; right: 12px;
|
||||||
|
display: inline-flex; align-items: center; gap: 3px;
|
||||||
|
background: var(--primary); color: #06251b; font-size: 11px; font-weight: 600;
|
||||||
|
border-radius: 6px; padding: 2px 7px;
|
||||||
|
}
|
||||||
|
.preset-card h3 { font-size: 15px; margin-bottom: 6px; }
|
||||||
|
.preset-card p { color: var(--muted); font-size: 12px; line-height: 1.6; margin-bottom: 10px; }
|
||||||
|
.preset-params { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||||
|
.param-chip { background: rgba(52, 211, 153, 0.12); color: var(--primary); border-radius: 6px; padding: 2px 7px; font-size: 10px; font-family: var(--mono); }
|
||||||
|
|
||||||
|
/* Swappiness */
|
||||||
|
.swappiness-box { max-width: 560px; }
|
||||||
|
.swap-current { margin-bottom: 16px; }
|
||||||
|
.swap-current b { font-size: 22px; }
|
||||||
|
.swap-tip { color: var(--muted); font-size: 12px; margin-left: 10px; }
|
||||||
|
.slider { width: 100%; accent-color: var(--primary); margin: 8px 0; }
|
||||||
|
.swap-labels { display: flex; justify-content: space-between; color: var(--muted); font-size: 11px; }
|
||||||
|
.swap-value-big { text-align: center; font-size: 42px; font-weight: 800; color: var(--primary); margin: 8px 0 14px; font-family: var(--mono); }
|
||||||
|
.swap-disabled {
|
||||||
|
max-width: 560px; text-align: center; padding: 32px 20px;
|
||||||
|
background: var(--card2); border: 1px dashed var(--border); border-radius: 12px;
|
||||||
|
}
|
||||||
|
.swap-disabled-icon {
|
||||||
|
width: 48px; height: 48px; margin: 0 auto 12px; border-radius: 50%;
|
||||||
|
background: rgba(248, 113, 113, 0.12); color: var(--danger);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.swap-disabled b { font-size: 16px; }
|
||||||
|
.swap-disabled p { color: var(--muted); font-size: 13px; margin: 10px 0 4px; line-height: 1.7; }
|
||||||
|
.swap-guide {
|
||||||
|
background: rgba(52, 211, 153, 0.08); border: 1px solid var(--primary-dim);
|
||||||
|
border-radius: 8px; padding: 10px 14px; margin: 10px auto 16px !important;
|
||||||
|
max-width: 420px; color: var(--primary) !important; font-size: 12.5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TCP */
|
||||||
|
.tcp-status { display: flex; gap: 24px; margin-bottom: 16px; flex-wrap: wrap; }
|
||||||
|
.tcp-item { background: var(--card2); border: 1px solid var(--border); border-radius: 10px; padding: 12px 18px; }
|
||||||
|
.tcp-item span { display: block; color: var(--muted); font-size: 12px; margin-bottom: 4px; }
|
||||||
|
.tcp-item b { font-size: 16px; }
|
||||||
|
.tcp-bbr { display: inline-flex; align-items: center; gap: 5px; }
|
||||||
|
|
||||||
|
/* 参数表 */
|
||||||
|
.param-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 14px; flex-wrap: wrap; }
|
||||||
|
.search-box { position: relative; display: flex; align-items: center; }
|
||||||
|
.search-icon { position: absolute; left: 10px; color: var(--muted); }
|
||||||
|
.input {
|
||||||
|
background: var(--card2); border: 1px solid var(--border); color: var(--text); border-radius: 8px;
|
||||||
|
padding: 8px 12px 8px 32px; font-size: 13px; width: 260px; outline: none;
|
||||||
|
}
|
||||||
|
.input:focus { border-color: var(--primary-dim); }
|
||||||
|
.unlock-toggle { color: var(--warn); font-size: 13px; display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
|
||||||
|
.unlock-toggle input { accent-color: var(--warn); width: 16px; height: 16px; }
|
||||||
|
.param-group { margin-bottom: 18px; }
|
||||||
|
.param-group-title { display: flex; align-items: center; gap: 6px; font-size: 14px; color: var(--muted); margin-bottom: 8px; }
|
||||||
|
.param-group-title .icon { color: var(--primary); }
|
||||||
|
.param-table { width: 100%; border-collapse: collapse; }
|
||||||
|
.param-table th { text-align: left; color: var(--muted); font-size: 12px; padding: 8px; border-bottom: 1px solid var(--border); }
|
||||||
|
.param-table td { padding: 8px; border-bottom: 1px solid rgba(36, 50, 82, 0.5); vertical-align: top; }
|
||||||
|
.param-desc small { display: block; color: var(--muted); font-size: 11px; margin-top: 2px; line-height: 1.5; }
|
||||||
|
.mono { font-family: var(--mono); font-size: 12px; }
|
||||||
|
.current-val { color: var(--primary); }
|
||||||
|
.risk-badge { border-radius: 6px; padding: 2px 8px; font-size: 11px; }
|
||||||
|
.risk-safe { background: rgba(52, 211, 153, 0.15); color: var(--primary); }
|
||||||
|
.risk-warn { background: rgba(245, 158, 11, 0.15); color: var(--warn); }
|
||||||
|
.risk-danger { background: rgba(239, 68, 68, 0.15); color: var(--danger); }
|
||||||
|
|
||||||
|
/* 按钮 */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
background: var(--card2); border: 1px solid var(--border); color: var(--text);
|
||||||
|
padding: 8px 18px; border-radius: 9px; cursor: pointer; font-size: 13px; transition: all .15s;
|
||||||
|
}
|
||||||
|
.btn:hover:not(:disabled) { border-color: var(--primary-dim); color: var(--primary); }
|
||||||
|
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.btn.primary { background: linear-gradient(135deg, #10b981, #059669); border: none; color: #04241a; font-weight: 600; }
|
||||||
|
.btn.primary:hover:not(:disabled) { filter: brightness(1.1); }
|
||||||
|
.btn.ghost { background: transparent; }
|
||||||
|
.btn.sm { padding: 4px 10px; font-size: 12px; }
|
||||||
|
.btn.danger-btn { border-color: var(--danger); color: var(--danger); }
|
||||||
|
.btn.danger-btn:hover:not(:disabled) { background: rgba(239, 68, 68, 0.12); }
|
||||||
|
.btn.disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
|
.actions { display: flex; gap: 10px; align-items: center; margin-top: 6px; flex-wrap: wrap; }
|
||||||
|
|
||||||
|
.text-green { color: var(--primary); }
|
||||||
|
.text-red { color: var(--danger); }
|
||||||
|
.muted { color: var(--muted); font-size: 13px; }
|
||||||
|
.small { font-size: 11px; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// ===================== API 封装 =====================
|
||||||
|
// GM 应用内通过 $gm.request 调用后端 RPC(socket 传输)
|
||||||
|
const ORG = 'user_TGPJp0KLpNsa5TAo'
|
||||||
|
const APP = 'ddhv'
|
||||||
|
|
||||||
|
export function call(method, params = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const doCall = () => {
|
||||||
|
$gm.request.post(`/api/call/${ORG}/${APP}/${method}`, {
|
||||||
|
version: '1.0.0',
|
||||||
|
transport: 'socket',
|
||||||
|
params,
|
||||||
|
}).then(res => {
|
||||||
|
// 后端统一返回 {code, data, msg}
|
||||||
|
const body = res && res.data !== undefined ? res.data : res
|
||||||
|
if (body && body.code === 200) {
|
||||||
|
resolve(body.data)
|
||||||
|
} else {
|
||||||
|
reject(new Error((body && body.msg) || '请求失败'))
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (window.$gm && $gm.request) {
|
||||||
|
doCall()
|
||||||
|
} else {
|
||||||
|
// 开发环境无 SDK 时降级(便于浏览器直接调试)
|
||||||
|
reject(new Error('GM SDK 未初始化(请在面板内打开应用)'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息提示(兼容 SDK 缺失环境)
|
||||||
|
export const toast = {
|
||||||
|
success(msg) { if (window.$gm && $gm.message) $gm.message.success(msg); else alert('[OK] ' + msg) },
|
||||||
|
error(msg) { if (window.$gm && $gm.message) $gm.message.error(msg); else alert('[失败] ' + msg) },
|
||||||
|
warning(msg) { if (window.$gm && $gm.message) $gm.message.warning(msg); else alert('[警告] ' + msg) },
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,54 @@
|
|||||||
|
// ===================== SVG 图标库 =====================
|
||||||
|
// Feather 风格线性图标(stroke=currentColor,随文字颜色变化)
|
||||||
|
// 用法:<Icon :name="'zap'" :size="16" />
|
||||||
|
|
||||||
|
import { h } from 'vue'
|
||||||
|
|
||||||
|
export const paths = {
|
||||||
|
// 通用
|
||||||
|
zap: '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>',
|
||||||
|
memory: '<rect x="3" y="9" width="18" height="8" rx="1"/><line x1="7" y1="9" x2="7" y2="15"/><line x1="11" y1="9" x2="11" y2="15"/><line x1="15" y1="9" x2="15" y2="15"/><line x1="19" y1="9" x2="19" y2="15"/><line x1="9" y1="3" x2="9" y2="5"/><line x1="13" y1="3" x2="13" y2="5"/><line x1="17" y1="3" x2="17" y2="5"/>',
|
||||||
|
rocket: '<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>',
|
||||||
|
settings: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>',
|
||||||
|
shield: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
|
||||||
|
fileText: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>',
|
||||||
|
cpu: '<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="14" x2="23" y2="14"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="14" x2="4" y2="14"/>',
|
||||||
|
hardDrive: '<line x1="22" y1="12" x2="2" y2="12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/><line x1="6" y1="16" x2="6.01" y2="16"/><line x1="10" y1="16" x2="10.01" y2="16"/>',
|
||||||
|
box: '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
|
||||||
|
lock: '<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
||||||
|
search: '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
|
||||||
|
refresh: '<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>',
|
||||||
|
check: '<polyline points="20 6 9 17 4 12"/>',
|
||||||
|
x: '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
|
||||||
|
camera: '<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/>',
|
||||||
|
rotate: '<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>',
|
||||||
|
globe: '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>',
|
||||||
|
gauge: '<path d="M12 15l3.5-3.5"/><path d="M20.3 18a10 10 0 1 0-16.6 0"/>',
|
||||||
|
activity: '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>',
|
||||||
|
power: '<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/><line x1="12" y1="2" x2="12" y2="12"/>',
|
||||||
|
external: '<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Icon = {
|
||||||
|
name: 'Icon',
|
||||||
|
props: {
|
||||||
|
name: { type: String, required: true },
|
||||||
|
size: { type: [Number, String], default: 16 },
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h('svg', {
|
||||||
|
class: 'icon',
|
||||||
|
width: this.size,
|
||||||
|
height: this.size,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
'stroke-width': 2,
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
'stroke-linejoin': 'round',
|
||||||
|
innerHTML: paths[this.name] || '',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Icon
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import 'gm-app-sdk';
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import './style.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
createApp(App).mount('#app')
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// ===================== 内核参数知识库 =====================
|
||||||
|
// 每个参数:中文名、说明、风险等级(risk: safe/warn/danger)、推荐值、单位/格式提示
|
||||||
|
// 用于可视化展示与编辑引导
|
||||||
|
|
||||||
|
export const PARAM_GROUPS = [
|
||||||
|
{
|
||||||
|
key: 'network',
|
||||||
|
name: '网络',
|
||||||
|
icon: 'globe',
|
||||||
|
params: {
|
||||||
|
'net.ipv4.tcp_congestion_control': {
|
||||||
|
name: 'TCP 拥塞控制算法',
|
||||||
|
desc: '控制 TCP 在高延迟/丢包下的发送策略。BBR 由 Google 开发,对高带宽长距离链路提升显著',
|
||||||
|
risk: 'safe', recommend: 'bbr', example: 'cubic / bbr',
|
||||||
|
},
|
||||||
|
'net.core.default_qdisc': {
|
||||||
|
name: '默认队列规则',
|
||||||
|
desc: '与 BBR 配套使用的排队规则,fq(fair queue) 可平滑突发流量、降低延迟',
|
||||||
|
risk: 'safe', recommend: 'fq', example: 'pfifo_fast / fq',
|
||||||
|
},
|
||||||
|
'net.ipv4.tcp_fastopen': {
|
||||||
|
name: 'TCP Fast Open',
|
||||||
|
desc: '允许在 TCP 握手期间携带数据,减少一次 RTT。3 = 客户端+服务端都启用',
|
||||||
|
risk: 'warn', recommend: '3', example: '0=关 / 1=客户端 / 2=服务端 / 3=全开',
|
||||||
|
},
|
||||||
|
'net.ipv4.tcp_slow_start_after_idle': {
|
||||||
|
name: '空闲后慢启动',
|
||||||
|
desc: '连接空闲后是否重置拥塞窗口。设为 0 可让长连接保持高吞吐,对 Web 服务友好',
|
||||||
|
risk: 'safe', recommend: '0', example: '0 / 1',
|
||||||
|
},
|
||||||
|
'net.ipv4.tcp_mtu_probing': {
|
||||||
|
name: 'MTU 探测',
|
||||||
|
desc: '探测路径 MTU,避免分片。1 = 在 DF 标志下探测',
|
||||||
|
risk: 'safe', recommend: '1', example: '0 / 1',
|
||||||
|
},
|
||||||
|
'net.core.somaxconn': {
|
||||||
|
name: '最大连接队列',
|
||||||
|
desc: 'socket 监听队列上限,高并发服务建议调大(配合应用 backlog 配置)',
|
||||||
|
risk: 'safe', recommend: '1024~4096', example: '128~65535',
|
||||||
|
},
|
||||||
|
'net.ipv4.tcp_max_syn_backlog': {
|
||||||
|
name: 'SYN 半连接队列',
|
||||||
|
desc: '未完成三次握手的连接队列长度,抗 SYN 洪水、高并发连接有用',
|
||||||
|
risk: 'safe', recommend: '4096~8192', example: '128~65535',
|
||||||
|
},
|
||||||
|
'net.ipv4.tcp_syncookies': {
|
||||||
|
name: 'SYN Cookies',
|
||||||
|
desc: 'SYN 洪水防护,正常情况下保持 1 开启即可',
|
||||||
|
risk: 'safe', recommend: '1', example: '0 / 1',
|
||||||
|
},
|
||||||
|
'net.ipv4.tcp_tw_reuse': {
|
||||||
|
name: 'TIME_WAIT 复用',
|
||||||
|
desc: '允许复用 TIME_WAIT 状态的连接。[NAT 环境下可能引发连接异常,需谨慎]',
|
||||||
|
risk: 'danger', recommend: '1(谨慎)', example: '0 / 1',
|
||||||
|
},
|
||||||
|
'net.ipv4.ip_local_port_range': {
|
||||||
|
name: '本地端口范围',
|
||||||
|
desc: '发起连接可用的本地端口区间,范围越大并发出站连接越多',
|
||||||
|
risk: 'warn', recommend: '1024 65535', example: '1024 65535',
|
||||||
|
},
|
||||||
|
'net.core.rmem_max': {
|
||||||
|
name: '最大接收缓冲区',
|
||||||
|
desc: '所有协议的最大接收缓冲。过大浪费内存,需与内存匹配',
|
||||||
|
risk: 'danger', recommend: '16777216(16MB)', example: '整数(字节)',
|
||||||
|
},
|
||||||
|
'net.core.wmem_max': {
|
||||||
|
name: '最大发送缓冲区',
|
||||||
|
desc: '所有协议的最大发送缓冲。过大浪费内存',
|
||||||
|
risk: 'danger', recommend: '16777216(16MB)', example: '整数(字节)',
|
||||||
|
},
|
||||||
|
'net.ipv4.tcp_rmem': {
|
||||||
|
name: 'TCP 接收缓冲范围',
|
||||||
|
desc: 'TCP 接收缓冲的动态范围(最小值 默认值 最大值),不当设置影响吞吐',
|
||||||
|
risk: 'danger', recommend: '4096 87380 16777216', example: '三个整数',
|
||||||
|
},
|
||||||
|
'net.ipv4.tcp_wmem': {
|
||||||
|
name: 'TCP 发送缓冲范围',
|
||||||
|
desc: 'TCP 发送缓冲的动态范围,不当设置影响吞吐',
|
||||||
|
risk: 'danger', recommend: '4096 16384 16777216', example: '三个整数',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'memory',
|
||||||
|
name: '内存',
|
||||||
|
icon: 'memory',
|
||||||
|
params: {
|
||||||
|
'vm.swappiness': {
|
||||||
|
name: 'Swappiness 交换倾向',
|
||||||
|
desc: '内核使用 swap 的积极程度(0-100)。服务器建议 10,数据库 0-1,桌面 60。调优核心参数',
|
||||||
|
risk: 'danger', recommend: '10(服务器) / 0-1(数据库)', example: '0~100',
|
||||||
|
},
|
||||||
|
'vm.vfs_cache_pressure': {
|
||||||
|
name: '缓存回收压力',
|
||||||
|
desc: '内核回收目录/文件缓存(dentry/inode)的倾向。值越低缓存保留越久,但内存不足时可能 OOM',
|
||||||
|
risk: 'danger', recommend: '50~100', example: '0~1000',
|
||||||
|
},
|
||||||
|
'vm.dirty_ratio': {
|
||||||
|
name: '脏页比例(全局)',
|
||||||
|
desc: '进程写入时脏页占内存比例上限,超过后强制写回。调高提升写吞吐但崩溃丢数据风险增大',
|
||||||
|
risk: 'warn', recommend: '20~30', example: '0~100(百分比)',
|
||||||
|
},
|
||||||
|
'vm.dirty_background_ratio': {
|
||||||
|
name: '脏页后台写回阈值',
|
||||||
|
desc: '脏页达到该比例时后台开始写回。建议为 dirty_ratio 的一半左右',
|
||||||
|
risk: 'warn', recommend: '10~15', example: '0~100(百分比)',
|
||||||
|
},
|
||||||
|
'vm.overcommit_memory': {
|
||||||
|
name: '内存过载策略',
|
||||||
|
desc: '0=启发式 1=总是允许 2=禁止过载。数据库/大内存应用建议评估后再改',
|
||||||
|
risk: 'danger', recommend: '0(默认)', example: '0 / 1 / 2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'fs',
|
||||||
|
name: '文件系统',
|
||||||
|
icon: 'hardDrive',
|
||||||
|
params: {
|
||||||
|
'fs.file-max': {
|
||||||
|
name: '系统最大文件句柄',
|
||||||
|
desc: '全系统可打开的文件描述符总数上限。高并发服务需要调大',
|
||||||
|
risk: 'safe', recommend: '65535~1048576', example: '整数',
|
||||||
|
},
|
||||||
|
'fs.inotify.max_user_watches': {
|
||||||
|
name: 'inotify 监听上限',
|
||||||
|
desc: '单个用户可创建的 inotify 监听数量。文件监听类应用(如 Node 热重载)不够会报错',
|
||||||
|
risk: 'safe', recommend: '524288', example: '整数',
|
||||||
|
},
|
||||||
|
'fs.aio-max-nr': {
|
||||||
|
name: '异步 IO 上限',
|
||||||
|
desc: '系统级异步 IO 请求数上限,数据库/高 IO 应用需要',
|
||||||
|
risk: 'safe', recommend: '1048576', example: '整数',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'kernel',
|
||||||
|
name: '内核',
|
||||||
|
icon: 'settings',
|
||||||
|
params: {
|
||||||
|
'kernel.pid_max': {
|
||||||
|
name: 'PID 上限',
|
||||||
|
desc: '系统最大进程数。进程多的服务器(容器/常驻任务)建议调大',
|
||||||
|
risk: 'safe', recommend: '4194304(64位)', example: '整数',
|
||||||
|
},
|
||||||
|
'kernel.threads-max': {
|
||||||
|
name: '线程上限',
|
||||||
|
desc: '系统最大线程数',
|
||||||
|
risk: 'safe', recommend: '跟随 pid_max', example: '整数',
|
||||||
|
},
|
||||||
|
'kernel.core_uses_pid': {
|
||||||
|
name: 'core 文件带 PID',
|
||||||
|
desc: 'core dump 文件名附加 PID,方便多进程崩溃定位',
|
||||||
|
risk: 'safe', recommend: '1', example: '0 / 1',
|
||||||
|
},
|
||||||
|
'kernel.sysrq': {
|
||||||
|
name: 'SysRq 魔术键',
|
||||||
|
desc: '紧急系统操作快捷键开关。服务器建议 1,云主机可能被限制',
|
||||||
|
risk: 'warn', recommend: '1', example: '0 / 1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 拍平所有参数(供搜索)
|
||||||
|
export const ALL_PARAMS = (() => {
|
||||||
|
const map = {}
|
||||||
|
for (const g of PARAM_GROUPS) {
|
||||||
|
for (const [key, info] of Object.entries(g.params)) {
|
||||||
|
map[key] = { ...info, group: g.name, groupKey: g.key }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
})()
|
||||||
|
|
||||||
|
export function searchParams(keyword) {
|
||||||
|
const kw = (keyword || '').trim().toLowerCase()
|
||||||
|
if (!kw) return Object.entries(ALL_PARAMS)
|
||||||
|
return Object.entries(ALL_PARAMS).filter(([key, info]) =>
|
||||||
|
key.toLowerCase().includes(kw) || info.name.includes(kw) || info.desc.includes(kw)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 风险徽章
|
||||||
|
export const RISK_META = {
|
||||||
|
safe: { label: '安全', color: '#34d399' },
|
||||||
|
warn: { label: '谨慎', color: '#f59e0b' },
|
||||||
|
danger: { label: '危险', color: '#ef4444' },
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
:root {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
base: './',
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
allowedHosts: true,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'www'
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user