feat: 前端完整源码纳入 Go 仓库(web/,标题已改为 系统调优 1.0)

This commit is contained in:
shishi
2026-08-09 12:32:54 +08:00
parent e00157ac2e
commit 0b9dd22f24
11 changed files with 1040 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
__pycache__
*.pyc
*.log
__pycache__/
.venv
.idea
test
dist/*
dist/
.DS_Store
/node_modules/
/www/
+51
View File
@@ -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/)
+1
View File
@@ -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
View File
@@ -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>
+39
View File
@@ -0,0 +1,39 @@
// ===================== API 封装 =====================
// GM 应用内通过 $gm.request 调用后端 RPCsocket 传输)
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

+54
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
import 'gm-app-sdk';
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
+190
View File
@@ -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' },
}
+3
View File
@@ -0,0 +1,3 @@
:root {
}
+15
View File
@@ -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'
}
})