feat: 初始化 ConfTemplate 云原生配置文件生成器

- Vue 3 + Element Plus + Monaco Editor 技术栈
- 支持 NGINX、Redis、MySQL、K8s Deployment、K8s Service 五个组件
- 侧边栏搜索过滤、动态表单联动、实时代码预览
- 一键复制/下载/重置,localStorage 状态持久化
- 纯前端静态页面,零后端依赖
This commit is contained in:
cnbugs
2026-07-18 19:19:14 +08:00
commit 5070664593
17 changed files with 5084 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
dist/
.DS_Store
*.local
*.log
+212
View File
@@ -0,0 +1,212 @@
# ⚙️ ConfTemplate — 云原生配置文件生成器
> 一站式可视化生成 NGINX、Redis、MySQL、Kubernetes 等主流中间件的标准配置文件,告别手写配置的语法烦恼。
![Vue 3](https://img.shields.io/badge/Vue-3-42b883?logo=vue.js)
![Element Plus](https://img.shields.io/badge/Element_Plus-2.x-409eff)
![Monaco Editor](https://img.shields.io/badge/Monaco_Editor-0.49-1e90ff)
![License](https://img.shields.io/badge/License-MIT-green)
---
## 📸 界面预览
三栏布局:**左侧边栏导航** → **中间表单配置****右侧代码预览**
- 侧边栏支持搜索过滤,快速定位目标中间件
- 表单支持下拉框、输入框、开关、数字输入等多种组件
- 代码预览基于 Monaco EditorVS Code 内核),支持语法高亮
- 一键复制、一键下载、一键重置
---
## ✨ 核心特性
| 特性 | 说明 |
|------|------|
| 🔍 搜索过滤 | 侧边栏支持关键字搜索,输入 `k8s` 即可过滤出 Kubernetes 相关模板 |
| 📋 动态表单 | 基于 JSON Schema 驱动,自动渲染专属配置表单 |
| 🔗 联动逻辑 | 选项变更时动态展示/隐藏关联参数(如开启 HTTPS 后显示证书路径) |
| 💡 最佳实践预设 | 所有参数内置生产环境推荐默认值 |
| 📝 实时预览 | 表单修改即时反映到右侧代码,Monaco Editor 专业语法高亮 |
| 📋 一键复制 | 生成的配置一键复制到系统剪贴板 |
| 💾 一键下载 | 直接保存为 `.conf` / `.cnf` / `.yaml` 本地文件 |
| 🔄 一键重置 | 快速恢复当前组件的默认模板状态 |
| 💾 状态持久化 | 表单数据自动保存到 localStorage,刷新页面不丢失 |
| 🚫 零后端 | 纯前端静态页面,无需 API、无需数据库、无需登录 |
---
## 🧩 一期支持组件
| 分类 | 组件 | 输出格式 | 核心配置项 |
|------|------|----------|------------|
| 代理与 Web | **NGINX** | `.conf` | HTTP/HTTPS 端口、域名、SSL 证书、反向代理、Gzip、CORS |
| 缓存 | **Redis** | `.conf` | 单机/哨兵/集群模式、端口、内存限制、RDB/AOF 持久化、密码 |
| 数据库 | **MySQL** | `.cnf` | 版本(5.7/8.0)、Buffer Pool 自动计算、字符集、慢查询、主从复制 |
| 云原生 | **K8s Deployment** | `.yaml` | 镜像、副本数、资源限制、健康检查、滚动更新策略 |
| 云原生 | **K8s Service** | `.yaml` | ClusterIP/NodePort/LoadBalancer、端口映射、会话保持 |
---
## 🚀 快速开始
### 环境要求
- Node.js >= 18
- npm >= 9
### 安装与运行
```bash
# 克隆项目
git clone <your-repo-url>
cd conf-template
# 安装依赖
npm install
# 启动开发服务器
npm run dev
```
浏览器访问 `http://localhost:3000`
### 生产构建
```bash
# 构建产物输出到 dist/ 目录
npm run build
# 预览构建产物
npm run preview
```
构建产物为纯静态文件,可直接部署到 Nginx、GitHub Pages、Vercel、Cloudflare Pages 等任意静态托管服务。
---
## 📁 项目结构
```
conf-template/
├── index.html # 入口 HTML
├── package.json # 项目依赖与脚本
├── vite.config.js # Vite 构建配置
├── src/
│ ├── main.js # Vue 应用入口
│ ├── App.vue # 主布局(三栏)
│ ├── components/
│ │ ├── Sidebar.vue # 侧边栏导航 + 搜索过滤
│ │ ├── ConfigForm.vue # 左侧动态表单面板
│ │ └── CodePreview.vue # 右侧 Monaco Editor 代码预览
│ └── schemas/
│ ├── index.js # Schema 注册中心 + 工具函数
│ ├── nginx.js # NGINX Schema 定义 + 模板生成
│ ├── redis.js # Redis Schema 定义 + 模板生成
│ ├── mysql.js # MySQL Schema 定义 + 模板生成
│ ├── k8s-deployment.js # K8s Deployment Schema + YAML 生成
│ └── k8s-service.js # K8s Service Schema + YAML 生成
└── dist/ # 生产构建产物(gitignore
```
---
## 🔧 如何扩展新组件
每个中间件由两部分组成:**Schema 定义** + **模板生成函数**
### 1. 创建 Schema 文件
`src/schemas/` 下新建文件,如 `postgresql.js`
```js
export const postgresqlSchema = {
id: 'postgresql',
name: 'PostgreSQL',
icon: 'Coin',
category: '数据库',
description: '世界上最先进的开源关系型数据库',
format: 'conf',
fileName: 'postgresql.conf',
groups: [
{
title: '基础配置',
fields: [
{
key: 'port',
label: '监听端口',
type: 'number', // number | text | select | switch
min: 1,
max: 65535,
default: 5432,
},
// ...更多字段
],
},
],
}
export function generatePostgresqlConf(config) {
// 根据 config 对象生成配置文件文本
return `port = ${config.port}\n...`
}
```
### 2. 字段类型说明
| type | 渲染组件 | 额外属性 |
|------|----------|----------|
| `select` | 下拉框 | `options: [{label, value}]` |
| `number` | 数字输入 | `min`, `max`, `step` |
| `text` | 文本输入 | `placeholder` |
| `switch` | 开关 | — |
### 3. 联动字段
通过 `dependsOn` 实现条件显示:
```js
{
key: 'sslCert',
label: 'SSL 证书路径',
type: 'text',
dependsOn: { key: 'enableSsl', value: true }, // 仅当 enableSsl === true 时显示
}
```
### 4. 注册到 Schema 中心
`src/schemas/index.js` 中导入并注册:
```js
import { postgresqlSchema, generatePostgresqlConf } from './postgresql'
export const schemas = {
// ...已有组件
postgresql: postgresqlSchema,
}
export const generators = {
// ...已有生成器
postgresql: generatePostgresqlConf,
}
```
---
## 🛠 技术栈
| 技术 | 用途 |
|------|------|
| [Vue 3](https://vuejs.org/) | 核心视图框架,原生双向绑定 |
| [Element Plus](https://element-plus.org/) | UI 组件库(表单、按钮、提示等) |
| [Monaco Editor](https://microsoft.github.io/monaco-editor/) | 代码编辑器(VS Code 内核) |
| [Vite](https://vitejs.dev/) | 构建工具,秒级 HMR |
| localStorage | 用户配置本地持久化 |
---
## 📄 License
[MIT](LICENSE)
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ConfTemplate - 云原生配置文件生成器</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚙️</text></svg>" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+2122
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "conf-template",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.0",
"@vitejs/plugin-vue": "^5.2.4",
"element-plus": "^2.7.0",
"monaco-editor": "^0.49.0",
"unplugin-auto-import": "^0.18.6",
"unplugin-vue-components": "^0.27.5",
"vite": "^5.4.21",
"vue": "^3.4.0",
"vue-router": "^4.3.0"
}
}
+109
View File
@@ -0,0 +1,109 @@
<template>
<div class="app-layout">
<Sidebar v-model="activeSchemaId" />
<div class="main-content">
<ConfigForm
:key="activeSchemaId"
:schema="currentSchema"
@update:config="onConfigUpdate"
/>
</div>
<div class="preview-content">
<CodePreview :code="generatedCode" :schema="currentSchema" />
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import Sidebar from './components/Sidebar.vue'
import ConfigForm from './components/ConfigForm.vue'
import CodePreview from './components/CodePreview.vue'
import { schemas, generators, getDefaultValues } from './schemas'
const activeSchemaId = ref('nginx')
const currentConfig = ref({})
const currentSchema = computed(() => schemas[activeSchemaId.value])
const generatedCode = computed(() => {
const gen = generators[activeSchemaId.value]
if (!gen) return ''
return gen(currentConfig.value)
})
function onConfigUpdate(config) {
currentConfig.value = config
}
// Initialize with defaults
currentConfig.value = getDefaultValues(schemas[activeSchemaId.value])
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #app {
height: 100%;
width: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.app-layout {
display: flex;
height: 100vh;
width: 100vw;
overflow: hidden;
}
.main-content {
flex: 0 0 420px;
min-width: 380px;
max-width: 480px;
border-right: 1px solid #ebeef5;
overflow: hidden;
}
.preview-content {
flex: 1;
min-width: 0;
overflow: hidden;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #d0d0d0;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #b0b0b0;
}
/* Element Plus overrides */
.el-form-item {
margin-bottom: 18px;
}
.el-input-number {
width: 100%;
}
.el-select {
width: 100%;
}
</style>
+192
View File
@@ -0,0 +1,192 @@
<template>
<div class="code-preview">
<div class="preview-header">
<div class="preview-header-left">
<el-icon><Document /></el-icon>
<span class="preview-filename">{{ schema.fileName }}</span>
<el-tag size="small" type="info">{{ schema.format.toUpperCase() }}</el-tag>
</div>
<div class="preview-header-actions">
<el-button-group>
<el-tooltip content="复制到剪贴板" placement="top">
<el-button :icon="CopyDocument" @click="handleCopy" />
</el-tooltip>
<el-tooltip content="下载文件" placement="top">
<el-button :icon="Download" @click="handleDownload" />
</el-tooltip>
</el-button-group>
</div>
</div>
<div ref="editorContainer" class="editor-container"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { CopyDocument, Download } from '@element-plus/icons-vue'
import * as monaco from 'monaco-editor'
import { getLanguage } from '../schemas'
const props = defineProps({
code: { type: String, default: '' },
schema: { type: Object, required: true },
})
const editorContainer = ref(null)
let editor = null
// Monaco editor theme
function defineTheme() {
monaco.editor.defineTheme('confTheme', {
base: 'vs',
inherit: true,
rules: [
{ token: 'comment', foreground: '6a737d', fontStyle: 'italic' },
{ token: 'keyword', foreground: 'd73a49' },
{ token: 'string', foreground: '032f62' },
{ token: 'number', foreground: '005cc5' },
{ token: 'type', foreground: '6f42c1' },
{ token: 'delimiter', foreground: '24292e' },
],
colors: {
'editor.background': '#fafbfc',
'editor.lineHighlightBackground': '#f6f8fa',
'editorLineNumber.foreground': '#bfc3c8',
'editorLineNumber.activeForeground': '#24292e',
'editorGutter.background': '#fafbfc',
'editor.inactiveSelectionBackground': '#28a74520',
'editor.selectionBackground': '#28a74530',
},
})
}
onMounted(async () => {
defineTheme()
editor = monaco.editor.create(editorContainer.value, {
value: props.code,
language: getLanguage(props.schema.format),
theme: 'confTheme',
readOnly: true,
minimap: { enabled: false },
scrollBeyondLastLine: false,
lineNumbers: 'on',
renderLineHighlight: 'line',
fontSize: 13,
lineHeight: 20,
fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', 'Cascadia Code', Consolas, monospace",
padding: { top: 12, bottom: 12 },
scrollbar: {
verticalScrollbarSize: 8,
horizontalScrollbarSize: 8,
},
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
overviewRulerBorder: false,
wordWrap: 'on',
automaticLayout: true,
})
})
watch(
() => props.code,
(newCode) => {
if (editor) {
const model = editor.getModel()
if (model) {
monaco.editor.setModelLanguage(model, getLanguage(props.schema.format))
model.setValue(newCode)
}
}
},
)
watch(
() => props.schema.format,
(newFormat) => {
if (editor) {
const model = editor.getModel()
if (model) {
monaco.editor.setModelLanguage(model, getLanguage(newFormat))
}
}
},
)
onBeforeUnmount(() => {
if (editor) {
editor.dispose()
}
})
async function handleCopy() {
try {
await navigator.clipboard.writeText(props.code)
ElMessage.success('已复制到剪贴板')
} catch {
// Fallback
const textarea = document.createElement('textarea')
textarea.value = props.code
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
ElMessage.success('已复制到剪贴板')
}
}
function handleDownload() {
const blob = new Blob([props.code], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = props.schema.fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success(`已下载 ${props.schema.fileName}`)
}
</script>
<style scoped>
.code-preview {
height: 100%;
display: flex;
flex-direction: column;
background: #fafbfc;
}
.preview-header {
padding: 10px 16px;
background: #fff;
border-bottom: 1px solid #ebeef5;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.preview-header-left {
display: flex;
align-items: center;
gap: 8px;
color: #303133;
}
.preview-filename {
font-size: 13px;
font-weight: 600;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
.preview-header-actions .el-button-group .el-button {
border-color: #dcdfe6;
}
.editor-container {
flex: 1;
min-height: 0;
}
</style>
+258
View File
@@ -0,0 +1,258 @@
<template>
<div class="config-form">
<div class="form-header">
<div class="form-header-info">
<el-icon :size="22"><component :is="schema.icon" /></el-icon>
<div>
<h2>{{ schema.name }} 配置</h2>
<span class="form-header-desc">{{ schema.description }}</span>
</div>
</div>
<div class="form-header-actions">
<el-tooltip content="重置为默认值" placement="top">
<el-button :icon="RefreshLeft" circle @click="handleReset" />
</el-tooltip>
</div>
</div>
<el-scrollbar class="form-scroll">
<el-form
ref="formRef"
:model="formData"
label-position="top"
class="form-body"
>
<div v-for="group in visibleGroups" :key="group.title" class="form-group">
<div class="form-group-title">
<span>{{ group.title }}</span>
</div>
<template v-for="field in group.fields" :key="field.key">
<el-form-item
v-if="isFieldVisible(field)"
:label="field.label"
:prop="field.key"
:rules="getFieldRules(field)"
class="form-field"
>
<template #label="{ label }">
<span class="field-label">{{ label }}</span>
<el-tooltip v-if="field.tip" :content="field.tip" placement="top">
<el-icon class="field-tip-icon"><InfoFilled /></el-icon>
</el-tooltip>
</template>
<!-- Select -->
<el-select
v-if="field.type === 'select'"
v-model="formData[field.key]"
style="width: 100%"
filterable
>
<el-option
v-for="opt in field.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<!-- Number -->
<el-input-number
v-else-if="field.type === 'number'"
v-model="formData[field.key]"
:min="field.min"
:max="field.max"
:step="field.step || 1"
controls-position="right"
style="width: 100%"
/>
<!-- Switch -->
<el-switch
v-else-if="field.type === 'switch'"
v-model="formData[field.key]"
active-text="开启"
inactive-text="关闭"
/>
<!-- Text -->
<el-input
v-else
v-model="formData[field.key]"
:placeholder="field.placeholder || ''"
clearable
/>
</el-form-item>
</template>
</div>
</el-form>
</el-scrollbar>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { RefreshLeft } from '@element-plus/icons-vue'
import { getDefaultValues } from '../schemas'
const props = defineProps({
schema: { type: Object, required: true },
})
const emit = defineEmits(['update:config'])
const formRef = ref(null)
const formData = ref({})
function initForm() {
formData.value = getDefaultValues(props.schema)
// Load from localStorage
const saved = localStorage.getItem(`conf-${props.schema.id}`)
if (saved) {
try {
const parsed = JSON.parse(saved)
formData.value = { ...formData.value, ...parsed }
} catch (e) {
// ignore
}
}
}
onMounted(initForm)
watch(
() => props.schema.id,
() => initForm(),
)
watch(
formData,
(val) => {
emit('update:config', { ...val })
// Save to localStorage
localStorage.setItem(`conf-${props.schema.id}`, JSON.stringify(val))
},
{ deep: true, immediate: true },
)
function isFieldVisible(field) {
if (!field.dependsOn) return true
const dep = field.dependsOn
const val = formData.value[dep.key]
if ('value' in dep) return val === dep.value
if ('valueNotIn' in dep) return !dep.valueNotIn.includes(val)
return true
}
const visibleGroups = computed(() => {
return props.schema.groups.map((group) => ({
...group,
fields: group.fields.filter(isFieldVisible),
})).filter((group) => group.fields.length > 0)
})
function getFieldRules(field) {
if (!field.required) return []
return [{ required: true, message: `${field.label}不能为空`, trigger: 'blur' }]
}
function handleReset() {
const defaults = getDefaultValues(props.schema)
formData.value = { ...defaults }
localStorage.removeItem(`conf-${props.schema.id}`)
}
</script>
<style scoped>
.config-form {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
}
.form-header {
padding: 16px 20px;
border-bottom: 1px solid #ebeef5;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.form-header-info {
display: flex;
align-items: center;
gap: 10px;
}
.form-header-info h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #303133;
}
.form-header-desc {
font-size: 12px;
color: #909399;
}
.form-scroll {
flex: 1;
}
.form-body {
padding: 16px 20px 40px;
}
.form-group {
margin-bottom: 8px;
}
.form-group-title {
font-size: 14px;
font-weight: 600;
color: #303133;
padding: 8px 0;
margin-bottom: 4px;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
gap: 6px;
}
.form-group-title::before {
content: '';
width: 3px;
height: 14px;
background: #667eea;
border-radius: 2px;
}
.form-field {
margin-bottom: 18px;
}
.field-label {
font-size: 13px;
font-weight: 500;
}
.field-tip-icon {
margin-left: 4px;
color: #909399;
cursor: help;
font-size: 14px;
}
:deep(.el-form-item__label) {
padding-bottom: 4px !important;
}
:deep(.el-input-number .el-input__wrapper) {
padding-left: 11px;
padding-right: 40px;
}
</style>
+239
View File
@@ -0,0 +1,239 @@
<template>
<div class="sidebar">
<div class="sidebar-header">
<div class="logo">
<el-icon :size="28"><Setting /></el-icon>
<h1>ConfTemplate</h1>
</div>
<p class="subtitle">云原生配置文件生成器</p>
</div>
<div class="search-box">
<el-input
v-model="searchQuery"
placeholder="搜索中间件..."
prefix-icon="Search"
clearable
size="default"
/>
</div>
<div class="nav-list">
<template v-for="cat in filteredCategories" :key="cat.name">
<div class="nav-category">
<el-icon><component :is="cat.icon" /></el-icon>
<span>{{ cat.name }}</span>
</div>
<div
v-for="itemId in cat.items"
:key="itemId"
class="nav-item"
:class="{ active: modelValue === itemId }"
@click="$emit('update:modelValue', itemId)"
>
<el-icon><component :is="schemas[itemId].icon" /></el-icon>
<div class="nav-item-info">
<span class="nav-item-name">{{ schemas[itemId].name }}</span>
<span class="nav-item-desc">{{ schemas[itemId].description }}</span>
</div>
<el-tag size="small" type="info">{{ schemas[itemId].format }}</el-tag>
</div>
</template>
<div v-if="filteredCategories.length === 0" class="no-results">
<el-empty description="未找到匹配的中间件" :image-size="60" />
</div>
</div>
<div class="sidebar-footer">
<el-text size="small" type="info">v1.0.0 · 纯前端生成</el-text>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { schemas, categories } from '../schemas'
const props = defineProps({
modelValue: String,
})
defineEmits(['update:modelValue'])
const searchQuery = ref('')
const filteredCategories = computed(() => {
const q = searchQuery.value.toLowerCase().trim()
if (!q) return categories
return categories
.map((cat) => ({
...cat,
items: cat.items.filter((id) => {
const s = schemas[id]
return (
s.name.toLowerCase().includes(q) ||
s.description.toLowerCase().includes(q) ||
s.category.toLowerCase().includes(q) ||
id.toLowerCase().includes(q)
)
}),
}))
.filter((cat) => cat.items.length > 0)
})
</script>
<style scoped>
.sidebar {
width: 280px;
min-width: 280px;
height: 100vh;
background: #1a1a2e;
color: #e0e0e0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 20px 16px 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.logo {
display: flex;
align-items: center;
gap: 10px;
color: #fff;
}
.logo h1 {
font-size: 18px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 12px;
color: #888;
margin: 6px 0 0;
}
.search-box {
padding: 12px 16px;
}
.search-box :deep(.el-input__wrapper) {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: none;
border-radius: 8px;
}
.search-box :deep(.el-input__inner) {
color: #e0e0e0;
}
.search-box :deep(.el-input__inner::placeholder) {
color: #666;
}
.nav-list {
flex: 1;
overflow-y: auto;
padding: 4px 8px;
}
.nav-list::-webkit-scrollbar {
width: 4px;
}
.nav-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.nav-category {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 8px 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #666;
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
margin: 2px 0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.06);
}
.nav-item.active {
background: rgba(102, 126, 234, 0.15);
color: #667eea;
}
.nav-item.active::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 60%;
background: #667eea;
border-radius: 0 2px 2px 0;
}
.nav-item-info {
flex: 1;
min-width: 0;
}
.nav-item-name {
display: block;
font-size: 13px;
font-weight: 500;
color: #e0e0e0;
}
.nav-item.active .nav-item-name {
color: #667eea;
}
.nav-item-desc {
display: block;
font-size: 11px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.no-results {
padding: 40px 0;
}
.sidebar-footer {
padding: 12px 16px;
text-align: center;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
</style>
+14
View File
@@ -0,0 +1,14 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus, { size: 'default' })
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app')
+65
View File
@@ -0,0 +1,65 @@
import { nginxSchema, generateNginxConf } from './nginx'
import { redisSchema, generateRedisConf } from './redis'
import { mysqlSchema, generateMysqlConf } from './mysql'
import { k8sDeploymentSchema, generateK8sDeploymentYaml } from './k8s-deployment'
import { k8sServiceSchema, generateK8sServiceYaml } from './k8s-service'
export const schemas = {
nginx: nginxSchema,
redis: redisSchema,
mysql: mysqlSchema,
'k8s-deployment': k8sDeploymentSchema,
'k8s-service': k8sServiceSchema,
}
export const generators = {
nginx: generateNginxConf,
redis: generateRedisConf,
mysql: generateMysqlConf,
'k8s-deployment': generateK8sDeploymentYaml,
'k8s-service': generateK8sServiceYaml,
}
export const categories = [
{
name: '代理与Web',
icon: 'Monitor',
items: ['nginx'],
},
{
name: '缓存',
icon: 'Coin',
items: ['redis'],
},
{
name: '数据库',
icon: 'Coin',
items: ['mysql'],
},
{
name: '云原生',
icon: 'Box',
items: ['k8s-deployment', 'k8s-service'],
},
]
export function getDefaultValues(schema) {
const defaults = {}
for (const group of schema.groups) {
for (const field of group.fields) {
defaults[field.key] = field.default
}
}
return defaults
}
export function getLanguage(format) {
const map = {
conf: 'ini',
cnf: 'ini',
yaml: 'yaml',
json: 'json',
toml: 'toml',
}
return map[format] || 'plaintext'
}
+416
View File
@@ -0,0 +1,416 @@
export const k8sDeploymentSchema = {
id: 'k8s-deployment',
name: 'K8s Deployment',
icon: 'Box',
category: '云原生',
description: 'Kubernetes 无状态应用部署控制器',
format: 'yaml',
fileName: 'deployment.yaml',
groups: [
{
title: '基础信息',
fields: [
{
key: 'appName',
label: '应用名称',
type: 'text',
placeholder: 'my-app',
default: 'my-app',
required: true,
tip: '将用作 Deployment 和 Label 的名称',
},
{
key: 'namespace',
label: '命名空间',
type: 'text',
placeholder: 'default',
default: 'default',
},
{
key: 'image',
label: '容器镜像地址',
type: 'text',
placeholder: 'nginx:1.24',
default: 'nginx:1.24',
required: true,
},
{
key: 'replicas',
label: '副本数',
type: 'number',
min: 1,
max: 100,
default: 3,
tip: '建议生产环境至少 2 副本',
},
],
},
{
title: '容器端口',
fields: [
{
key: 'containerPort',
label: '容器端口',
type: 'number',
min: 1,
max: 65535,
default: 80,
},
{
key: 'protocol',
label: '协议',
type: 'select',
options: [
{ label: 'TCP', value: 'TCP' },
{ label: 'UDP', value: 'UDP' },
],
default: 'TCP',
},
{
key: 'portName',
label: '端口名称',
type: 'text',
placeholder: 'http',
default: 'http',
tip: 'Service 匹配时使用',
},
],
},
{
title: '资源限制',
fields: [
{
key: 'cpuRequest',
label: 'CPU Requests',
type: 'select',
options: [
{ label: '50m', value: '50m' },
{ label: '100m', value: '100m' },
{ label: '200m', value: '200m' },
{ label: '500m', value: '500m' },
{ label: '1', value: '1' },
{ label: '2', value: '2' },
],
default: '100m',
},
{
key: 'cpuLimit',
label: 'CPU Limits',
type: 'select',
options: [
{ label: '200m', value: '200m' },
{ label: '500m', value: '500m' },
{ label: '1', value: '1' },
{ label: '2', value: '2' },
{ label: '4', value: '4' },
],
default: '500m',
},
{
key: 'memoryRequest',
label: 'Memory Requests',
type: 'select',
options: [
{ label: '64Mi', value: '64Mi' },
{ label: '128Mi', value: '128Mi' },
{ label: '256Mi', value: '256Mi' },
{ label: '512Mi', value: '512Mi' },
{ label: '1Gi', value: '1Gi' },
],
default: '128Mi',
},
{
key: 'memoryLimit',
label: 'Memory Limits',
type: 'select',
options: [
{ label: '128Mi', value: '128Mi' },
{ label: '256Mi', value: '256Mi' },
{ label: '512Mi', value: '512Mi' },
{ label: '1Gi', value: '1Gi' },
{ label: '2Gi', value: '2Gi' },
],
default: '256Mi',
},
],
},
{
title: '健康检查',
fields: [
{
key: 'enableLivenessProbe',
label: '存活探针 (Liveness)',
type: 'switch',
default: true,
tip: '检测容器是否存活,失败则重启',
},
{
key: 'livenessProbeType',
label: '探针方式',
type: 'select',
options: [
{ label: 'HTTP GET', value: 'httpGet' },
{ label: 'TCP Socket', value: 'tcpSocket' },
{ label: 'Exec Command', value: 'exec' },
],
default: 'httpGet',
dependsOn: { key: 'enableLivenessProbe', value: true },
},
{
key: 'livenessPath',
label: '健康检查路径',
type: 'text',
placeholder: '/healthz',
default: '/healthz',
dependsOn: { key: 'livenessProbeType', value: 'httpGet' },
},
{
key: 'livenessPort',
label: '探针端口',
type: 'number',
min: 1,
max: 65535,
default: 80,
dependsOn: { key: 'enableLivenessProbe', value: true },
},
{
key: 'enableReadinessProbe',
label: '就绪探针 (Readiness)',
type: 'switch',
default: true,
tip: '检测容器是否就绪接收流量',
},
{
key: 'readinessProbeType',
label: '探针方式',
type: 'select',
options: [
{ label: 'HTTP GET', value: 'httpGet' },
{ label: 'TCP Socket', value: 'tcpSocket' },
{ label: 'Exec Command', value: 'exec' },
],
default: 'httpGet',
dependsOn: { key: 'enableReadinessProbe', value: true },
},
{
key: 'readinessPath',
label: '就绪检查路径',
type: 'text',
placeholder: '/ready',
default: '/ready',
dependsOn: { key: 'readinessProbeType', value: 'httpGet' },
},
{
key: 'readinessPort',
label: '探针端口',
type: 'number',
min: 1,
max: 65535,
default: 80,
dependsOn: { key: 'enableReadinessProbe', value: true },
},
],
},
{
title: '部署策略',
fields: [
{
key: 'strategy',
label: '更新策略',
type: 'select',
options: [
{ label: 'RollingUpdate - 滚动更新 (推荐)', value: 'RollingUpdate' },
{ label: 'Recreate - 先删后建', value: 'Recreate' },
],
default: 'RollingUpdate',
},
{
key: 'maxSurge',
label: '最大超出副本数',
type: 'select',
options: [
{ label: '1', value: '1' },
{ label: '25%', value: '25%' },
{ label: '50%', value: '50%' },
{ label: '100%', value: '100%' },
],
default: '25%',
dependsOn: { key: 'strategy', value: 'RollingUpdate' },
},
{
key: 'maxUnavailable',
label: '最大不可用副本数',
type: 'select',
options: [
{ label: '0 (零停机)', value: '0' },
{ label: '1', value: '1' },
{ label: '25%', value: '25%' },
],
default: '0',
dependsOn: { key: 'strategy', value: 'RollingUpdate' },
},
],
},
{
title: '标签与注解',
fields: [
{
key: 'appLabel',
label: 'app 标签值',
type: 'text',
default: '',
tip: '留空则使用应用名称',
},
{
key: 'versionLabel',
label: 'version 标签',
type: 'text',
placeholder: 'v1',
default: 'v1',
},
{
key: 'imagePullPolicy',
label: '镜像拉取策略',
type: 'select',
options: [
{ label: 'IfNotPresent (推荐)', value: 'IfNotPresent' },
{ label: 'Always', value: 'Always' },
{ label: 'Never', value: 'Never' },
],
default: 'IfNotPresent',
},
],
},
],
}
function toYaml(obj, indent = 0) {
const lines = []
const prefix = ' '.repeat(indent)
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined || value === '') continue
if (Array.isArray(value)) {
lines.push(`${prefix}${key}:`)
for (const item of value) {
if (typeof item === 'object' && item !== null) {
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
if (entries.length === 0) continue
const first = entries[0]
lines.push(`${prefix} - ${first[0]}: ${first[1]}`)
for (let i = 1; i < entries.length; i++) {
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`)
}
} else {
lines.push(`${prefix} - ${item}`)
}
}
} else if (typeof value === 'object') {
lines.push(`${prefix}${key}:`)
lines.push(toYaml(value, indent + 1))
} else {
lines.push(`${prefix}${key}: ${value}`)
}
}
return lines.join('\n')
}
export function generateK8sDeploymentYaml(config) {
const labels = {
app: config.appLabel || config.appName,
}
if (config.versionLabel) {
labels.version = config.versionLabel
}
const container = {
name: config.appName,
image: config.image,
imagePullPolicy: config.imagePullPolicy,
ports: [{
name: config.portName,
containerPort: config.containerPort,
protocol: config.protocol,
}],
resources: {
requests: {
cpu: config.cpuRequest,
memory: config.memoryRequest,
},
limits: {
cpu: config.cpuLimit,
memory: config.memoryLimit,
},
},
}
const probeConfig = (type, path, port) => {
if (type === 'httpGet') {
return { httpGet: { path, port, scheme: 'HTTP' } }
}
if (type === 'tcpSocket') {
return { tcpSocket: { port } }
}
return { exec: { command: ['cat', '/tmp/healthy'] } }
}
if (config.enableLivenessProbe) {
container.livenessProbe = {
...probeConfig(config.livenessProbeType, config.livenessPath, config.livenessPort),
initialDelaySeconds: 15,
periodSeconds: 20,
timeoutSeconds: 5,
failureThreshold: 3,
}
}
if (config.enableReadinessProbe) {
container.readinessProbe = {
...probeConfig(config.readinessProbeType, config.readinessPath, config.readinessPort),
initialDelaySeconds: 5,
periodSeconds: 10,
timeoutSeconds: 3,
failureThreshold: 3,
}
}
const deployment = {
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: {
name: config.appName,
namespace: config.namespace,
labels,
},
spec: {
replicas: config.replicas,
selector: {
matchLabels: { app: labels.app },
},
strategy: config.strategy === 'RollingUpdate'
? {
type: 'RollingUpdate',
rollingUpdate: {
maxSurge: config.maxSurge,
maxUnavailable: config.maxUnavailable,
},
}
: { type: 'Recreate' },
template: {
metadata: { labels },
spec: {
containers: [container],
},
},
},
}
const header = `# K8s Deployment - 由 ConfTemplate 生成
# 生成时间: ${new Date().toLocaleString('zh-CN')}
# 部署命令: kubectl apply -f ${config.fileName || 'deployment.yaml'}
`
return header + '\n' + toYaml(deployment)
}
+220
View File
@@ -0,0 +1,220 @@
export const k8sServiceSchema = {
id: 'k8s-service',
name: 'K8s Service',
icon: 'Connection',
category: '云原生',
description: 'Kubernetes 服务发现与负载均衡',
format: 'yaml',
fileName: 'service.yaml',
groups: [
{
title: '基础信息',
fields: [
{
key: 'serviceName',
label: 'Service 名称',
type: 'text',
placeholder: 'my-app-svc',
default: 'my-app-svc',
required: true,
},
{
key: 'namespace',
label: '命名空间',
type: 'text',
placeholder: 'default',
default: 'default',
},
{
key: 'appLabel',
label: '关联应用标签 (Selector)',
type: 'text',
placeholder: 'my-app',
default: 'my-app',
required: true,
tip: '需匹配 Deployment 中 Pod 的 app 标签',
},
],
},
{
title: 'Service 类型与端口',
fields: [
{
key: 'serviceType',
label: 'Service 类型',
type: 'select',
options: [
{ label: 'ClusterIP - 集群内部访问', value: 'ClusterIP' },
{ label: 'NodePort - 节点端口暴露', value: 'NodePort' },
{ label: 'LoadBalancer - 云负载均衡', value: 'LoadBalancer' },
],
default: 'ClusterIP',
tip: 'ClusterIP: 仅集群内访问; NodePort: 通过节点IP+端口访问; LoadBalancer: 云厂商 LB',
},
{
key: 'port',
label: 'Service 端口',
type: 'number',
min: 1,
max: 65535,
default: 80,
tip: 'Service 暴露的端口',
},
{
key: 'targetPort',
label: '目标容器端口',
type: 'number',
min: 1,
max: 65535,
default: 80,
tip: '需与 Deployment 中容器端口一致',
},
{
key: 'protocol',
label: '协议',
type: 'select',
options: [
{ label: 'TCP', value: 'TCP' },
{ label: 'UDP', value: 'UDP' },
],
default: 'TCP',
},
{
key: 'portName',
label: '端口名称',
type: 'text',
placeholder: 'http',
default: 'http',
},
{
key: 'nodePort',
label: 'NodePort 端口',
type: 'number',
min: 30000,
max: 32767,
default: 30080,
dependsOn: { key: 'serviceType', value: 'NodePort' },
tip: 'NodePort 范围: 30000-32767',
},
],
},
{
title: '会话保持与高级选项',
fields: [
{
key: 'sessionAffinity',
label: '会话保持',
type: 'switch',
default: false,
tip: '启用后同一客户端请求会转发到同一 Pod',
},
{
key: 'sessionTimeout',
label: '会话超时 (秒)',
type: 'number',
min: 1,
max: 86400,
default: 10800,
dependsOn: { key: 'sessionAffinity', value: true },
},
{
key: 'externalTrafficPolicy',
label: '外部流量策略',
type: 'select',
options: [
{ label: 'Cluster - 跨节点负载均衡 (默认)', value: 'Cluster' },
{ label: 'Local - 保留源 IP', value: 'Local' },
],
default: 'Cluster',
dependsOn: { key: 'serviceType', valueNotIn: ['ClusterIP'] },
tip: 'Local 可保留客户端真实 IP',
},
],
},
],
}
function toYaml(obj, indent = 0) {
const lines = []
const prefix = ' '.repeat(indent)
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === undefined || value === '') continue
if (Array.isArray(value)) {
lines.push(`${prefix}${key}:`)
for (const item of value) {
if (typeof item === 'object' && item !== null) {
const entries = Object.entries(item).filter(([, v]) => v !== null && v !== undefined && v !== '')
if (entries.length === 0) continue
const first = entries[0]
lines.push(`${prefix} - ${first[0]}: ${first[1]}`)
for (let i = 1; i < entries.length; i++) {
lines.push(`${prefix} ${entries[i][0]}: ${entries[i][1]}`)
}
} else {
lines.push(`${prefix} - ${item}`)
}
}
} else if (typeof value === 'object') {
lines.push(`${prefix}${key}:`)
lines.push(toYaml(value, indent + 1))
} else {
lines.push(`${prefix}${key}: ${value}`)
}
}
return lines.join('\n')
}
export function generateK8sServiceYaml(config) {
const portSpec = {
name: config.portName,
port: config.port,
targetPort: config.targetPort,
protocol: config.protocol,
}
if (config.serviceType === 'NodePort') {
portSpec.nodePort = config.nodePort
}
const service = {
apiVersion: 'v1',
kind: 'Service',
metadata: {
name: config.serviceName,
namespace: config.namespace,
labels: {
app: config.appLabel,
},
},
spec: {
type: config.serviceType,
selector: {
app: config.appLabel,
},
ports: [portSpec],
},
}
if (config.sessionAffinity) {
service.spec.sessionAffinity = 'ClientIP'
service.spec.sessionAffinityConfig = {
clientIP: {
timeoutSeconds: config.sessionTimeout,
},
}
}
if (config.serviceType !== 'ClusterIP' && config.externalTrafficPolicy) {
service.spec.externalTrafficPolicy = config.externalTrafficPolicy
}
const header = `# K8s Service - 由 ConfTemplate 生成
# 生成时间: ${new Date().toLocaleString('zh-CN')}
# 部署命令: kubectl apply -f ${config.fileName || 'service.yaml'}
`
return header + '\n' + toYaml(service)
}
+518
View File
@@ -0,0 +1,518 @@
export const mysqlSchema = {
id: 'mysql',
name: 'MySQL',
icon: 'Coin',
category: '数据库',
description: '世界上最流行的开源关系型数据库',
format: 'cnf',
fileName: 'my.cnf',
groups: [
{
title: '基础配置',
fields: [
{
key: 'version',
label: 'MySQL 版本',
type: 'select',
options: [
{ label: 'MySQL 8.0 (推荐)', value: '8.0' },
{ label: 'MySQL 5.7', value: '5.7' },
],
default: '8.0',
},
{
key: 'port',
label: '监听端口',
type: 'number',
min: 1,
max: 65535,
default: 3306,
},
{
key: 'bindAddress',
label: '绑定地址',
type: 'text',
default: '0.0.0.0',
},
{
key: 'datadir',
label: '数据目录',
type: 'text',
default: '/var/lib/mysql',
},
{
key: 'socket',
label: 'Socket 文件路径',
type: 'text',
default: '/var/run/mysqld/mysqld.sock',
},
{
key: 'characterSet',
label: '字符集',
type: 'select',
options: [
{ label: 'utf8mb4 (推荐,支持完整 Unicode)', value: 'utf8mb4' },
{ label: 'utf8', value: 'utf8' },
{ label: 'latin1', value: 'latin1' },
],
default: 'utf8mb4',
},
{
key: 'collation',
label: '排序规则',
type: 'select',
options: [
{ label: 'utf8mb4_general_ci (通用,性能好)', value: 'utf8mb4_general_ci' },
{ label: 'utf8mb4_unicode_ci (Unicode 标准)', value: 'utf8mb4_unicode_ci' },
{ label: 'utf8mb4_0900_ai_ci (MySQL 8.0 默认)', value: 'utf8mb4_0900_ai_ci' },
],
default: 'utf8mb4_general_ci',
},
{
key: 'timezone',
label: '时区',
type: 'select',
options: [
{ label: '+08:00 (中国标准时间)', value: '+08:00' },
{ label: 'SYSTEM (跟随系统)', value: 'SYSTEM' },
{ label: '+00:00 (UTC)', value: '+00:00' },
],
default: '+08:00',
},
],
},
{
title: '内存与性能',
fields: [
{
key: 'serverMemory',
label: '服务器总内存',
type: 'select',
options: [
{ label: '2GB', value: '2' },
{ label: '4GB', value: '4' },
{ label: '8GB', value: '8' },
{ label: '16GB', value: '16' },
{ label: '32GB', value: '32' },
{ label: '64GB', value: '64' },
],
default: '8',
tip: '用于自动计算 Buffer Pool 等参数',
},
{
key: 'maxConnections',
label: '最大连接数',
type: 'number',
min: 10,
max: 10000,
default: 500,
},
{
key: 'innodbBufferPoolSize',
label: 'InnoDB Buffer Pool 大小',
type: 'select',
options: [
{ label: '自动计算 (推荐)', value: 'auto' },
{ label: '256M', value: '256M' },
{ label: '512M', value: '512M' },
{ label: '1G', value: '1G' },
{ label: '2G', value: '2G' },
{ label: '4G', value: '4G' },
{ label: '8G', value: '8G' },
{ label: '16G', value: '16G' },
],
default: 'auto',
tip: '通常设为服务器内存的 60-80%',
},
{
key: 'innodbLogFileSize',
label: 'Redo Log 文件大小',
type: 'select',
options: [
{ label: '256M', value: '256M' },
{ label: '512M', value: '512M' },
{ label: '1G', value: '1G' },
],
default: '256M',
},
{
key: 'innodbFlushLogAtTrxCommit',
label: '事务日志刷盘策略',
type: 'select',
options: [
{ label: '1 - 每次事务提交都刷盘 (最安全)', value: '1' },
{ label: '2 - 每秒刷盘 (性能与安全平衡)', value: '2' },
{ label: '0 - 由操作系统决定 (性能最高)', value: '0' },
],
default: '1',
},
{
key: 'innodbIoCapacity',
label: 'IO 容量 (IOPS)',
type: 'select',
options: [
{ label: '200 (HDD 机械硬盘)', value: '200' },
{ label: '1000 (普通 SSD)', value: '1000' },
{ label: '2000 (高性能 SSD)', value: '2000' },
{ label: '10000 (NVMe SSD)', value: '10000' },
],
default: '2000',
},
],
},
{
title: '查询优化',
fields: [
{
key: 'queryCacheType',
label: '查询缓存 (仅 5.7)',
type: 'select',
options: [
{ label: '关闭 (MySQL 8.0 已移除)', value: '0' },
{ label: '按需开启', value: '1' },
{ label: '全部开启', value: '2' },
],
default: '0',
dependsOn: { key: 'version', value: '5.7' },
},
{
key: 'tmpTableSize',
label: '临时表最大大小',
type: 'select',
options: [
{ label: '16M', value: '16M' },
{ label: '32M', value: '32M' },
{ label: '64M', value: '64M' },
{ label: '128M', value: '128M' },
],
default: '64M',
},
{
key: 'maxHeapTableSize',
label: '内存临时表最大大小',
type: 'select',
options: [
{ label: '16M', value: '16M' },
{ label: '32M', value: '32M' },
{ label: '64M', value: '64M' },
{ label: '128M', value: '128M' },
],
default: '64M',
},
{
key: 'sortBufferSize',
label: '排序缓冲区大小',
type: 'select',
options: [
{ label: '256K', value: '256K' },
{ label: '512K', value: '512K' },
{ label: '1M', value: '1M' },
{ label: '2M', value: '2M' },
{ label: '4M', value: '4M' },
],
default: '1M',
},
{
key: 'joinBufferSize',
label: 'JOIN 缓冲区大小',
type: 'select',
options: [
{ label: '256K', value: '256K' },
{ label: '512K', value: '512K' },
{ label: '1M', value: '1M' },
{ label: '2M', value: '2M' },
],
default: '512K',
},
],
},
{
title: '日志配置',
fields: [
{
key: 'enableSlowQueryLog',
label: '开启慢查询日志',
type: 'switch',
default: true,
},
{
key: 'slowQueryLogTime',
label: '慢查询阈值 (秒)',
type: 'number',
min: 0,
max: 60,
default: 2,
dependsOn: { key: 'enableSlowQueryLog', value: true },
tip: '超过该时间的 SQL 会被记录',
},
{
key: 'enableGeneralLog',
label: '开启通用查询日志',
type: 'switch',
default: false,
tip: '记录所有 SQL,生产环境不建议开启',
},
{
key: 'enableBinlog',
label: '开启 Binlog',
type: 'switch',
default: true,
tip: '用于主从复制和数据恢复',
},
{
key: 'binlogFormat',
label: 'Binlog 格式',
type: 'select',
options: [
{ label: 'ROW (推荐,数据一致性最好)', value: 'ROW' },
{ label: 'STATEMENT (记录 SQL 语句)', value: 'STATEMENT' },
{ label: 'MIXED (混合模式)', value: 'MIXED' },
],
default: 'ROW',
dependsOn: { key: 'enableBinlog', value: true },
},
{
key: 'binlogExpireDays',
label: 'Binlog 过期天数',
type: 'number',
min: 1,
max: 365,
default: 15,
dependsOn: { key: 'enableBinlog', value: true },
},
],
},
{
title: '主从复制',
fields: [
{
key: 'enableReplication',
label: '开启主从复制',
type: 'switch',
default: false,
},
{
key: 'serverId',
label: 'Server ID',
type: 'number',
min: 1,
max: 4294967295,
default: 1,
dependsOn: { key: 'enableReplication', value: true },
tip: '每个 MySQL 实例必须唯一',
required: true,
},
{
key: 'replicationRole',
label: '角色',
type: 'select',
options: [
{ label: 'Master (主库)', value: 'master' },
{ label: 'Slave (从库)', value: 'slave' },
],
default: 'master',
dependsOn: { key: 'enableReplication', value: true },
},
{
key: 'masterHost',
label: '主库地址',
type: 'text',
placeholder: '192.168.1.100',
default: '',
dependsOn: { key: 'replicationRole', value: 'slave' },
required: true,
},
{
key: 'masterPort',
label: '主库端口',
type: 'number',
min: 1,
max: 65535,
default: 3306,
dependsOn: { key: 'replicationRole', value: 'slave' },
},
{
key: 'replicateDoDb',
label: '复制的数据库',
type: 'text',
placeholder: 'mydb (留空复制所有)',
default: '',
dependsOn: { key: 'enableReplication', value: true },
},
],
},
],
}
function calculateBufferPoolSize(serverMemory) {
const mem = parseInt(serverMemory)
if (mem <= 2) return '512M'
if (mem <= 4) return '2G'
if (mem <= 8) return '5G'
if (mem <= 16) return '10G'
if (mem <= 32) return '20G'
return '40G'
}
export function generateMysqlConf(config) {
const lines = []
lines.push(`# MySQL 配置文件 - 由 ConfTemplate 生成`)
lines.push(`# MySQL ${config.version} | 生成时间: ${new Date().toLocaleString('zh-CN')}`)
lines.push(``)
const bufferSize = config.innodbBufferPoolSize === 'auto'
? calculateBufferPoolSize(config.serverMemory)
: config.innodbBufferPoolSize
lines.push(`[mysqld]`)
lines.push(``)
// 基础
lines.push(`# ======================== 基础配置 ========================`)
lines.push(`port = ${config.port}`)
lines.push(`bind-address = ${config.bindAddress}`)
lines.push(`datadir = ${config.datadir}`)
lines.push(`socket = ${config.socket}`)
lines.push(`pid-file = /var/run/mysqld/mysqld.pid`)
lines.push(`default-storage-engine = InnoDB`)
lines.push(`character-set-server = ${config.characterSet}`)
lines.push(`collation-server = ${config.collation}`)
lines.push(`default-time-zone = '${config.timezone}'`)
lines.push(`lower_case_table_names = 1`)
lines.push(`skip-name-resolve`)
lines.push(`skip-external-locking`)
if (config.enableReplication) {
lines.push(`server-id = ${config.serverId}`)
}
lines.push(``)
// 连接
lines.push(`# ======================== 连接配置 ========================`)
lines.push(`max_connections = ${config.maxConnections}`)
lines.push(`max_connect_errors = 100000`)
lines.push(`wait_timeout = 600`)
lines.push(`interactive_timeout = 600`)
lines.push(`connect_timeout = 10`)
lines.push(`max_allowed_packet = 64M`)
lines.push(`back_log = 256`)
lines.push(``)
// 内存与性能
lines.push(`# ======================== 内存与性能 ========================`)
lines.push(`innodb_buffer_pool_size = ${bufferSize}`)
lines.push(`innodb_buffer_pool_instances = ${parseInt(bufferSize) >= 1 ? Math.min(parseInt(bufferSize), 8) : 1}`)
lines.push(`innodb_log_file_size = ${config.innodbLogFileSize}`)
lines.push(`innodb_log_buffer_size = 64M`)
lines.push(`innodb_flush_log_at_trx_commit = ${config.innodbFlushLogAtTrxCommit}`)
lines.push(`innodb_io_capacity = ${config.innodbIoCapacity}`)
lines.push(`innodb_io_capacity_max = ${parseInt(config.innodbIoCapacity) * 2}`)
lines.push(`innodb_flush_method = O_DIRECT`)
lines.push(`innodb_file_per_table = 1`)
lines.push(`innodb_open_files = 65535`)
lines.push(`innodb_read_io_threads = 4`)
lines.push(`innodb_write_io_threads = 4`)
lines.push(`innodb_purge_threads = 4`)
lines.push(`innodb_thread_concurrency = 0`)
lines.push(``)
// 查询优化
lines.push(`# ======================== 查询优化 ========================`)
lines.push(`tmp_table_size = ${config.tmpTableSize}`)
lines.push(`max_heap_table_size = ${config.maxHeapTableSize}`)
lines.push(`sort_buffer_size = ${config.sortBufferSize}`)
lines.push(`join_buffer_size = ${config.joinBufferSize}`)
lines.push(`read_buffer_size = 256K`)
lines.push(`read_rnd_buffer_size = 512K`)
lines.push(`thread_cache_size = 64`)
lines.push(`table_open_cache = 4096`)
lines.push(`table_definition_cache = 2048`)
lines.push(``)
if (config.version === '5.7' && config.queryCacheType !== '0') {
lines.push(`query_cache_type = ${config.queryCacheType}`)
lines.push(`query_cache_size = 64M`)
lines.push(``)
}
// 日志
lines.push(`# ======================== 日志配置 ========================`)
if (config.enableSlowQueryLog) {
lines.push(`slow_query_log = 1`)
lines.push(`slow_query_log_file = /var/log/mysql/slow.log`)
lines.push(`long_query_time = ${config.slowQueryLogTime}`)
lines.push(`log_queries_not_using_indexes = 1`)
lines.push(`min_examined_row_limit = 100`)
} else {
lines.push(`slow_query_log = 0`)
}
if (config.enableGeneralLog) {
lines.push(`general_log = 1`)
lines.push(`general_log_file = /var/log/mysql/general.log`)
}
lines.push(`log-error = /var/log/mysql/error.log`)
if (config.enableBinlog) {
lines.push(`log_bin = /var/log/mysql/mysql-bin`)
lines.push(`binlog_format = ${config.binlogFormat}`)
lines.push(`expire_logs_days = ${config.binlogExpireDays}`)
lines.push(`max_binlog_size = 256M`)
lines.push(`binlog_cache_size = 4M`)
lines.push(`sync_binlog = 1`)
}
lines.push(``)
// 主从复制
if (config.enableReplication) {
lines.push(`# ======================== 主从复制 ========================`)
if (config.replicationRole === 'master') {
lines.push(`# Master 配置`)
lines.push(`binlog_do_db = ${config.replicateDoDb || '(所有数据库)'}`)
lines.push(``)
lines.push(`# 在主库创建复制用户:`)
lines.push(`# CREATE USER 'repl'@'%' IDENTIFIED BY 'strong_password';`)
lines.push(`# GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';`)
lines.push(`# FLUSH PRIVILEGES;`)
} else {
lines.push(`# Slave 配置`)
lines.push(`relay-log = /var/log/mysql/relay-bin`)
lines.push(`read_only = 1`)
lines.push(`super_read_only = 1`)
lines.push(`log_slave_updates = 1`)
if (config.replicateDoDb) {
lines.push(`replicate-do-db = ${config.replicateDoDb}`)
}
lines.push(``)
lines.push(`# 在从库执行以下命令配置主库信息:`)
lines.push(`# CHANGE MASTER TO`)
lines.push(`# MASTER_HOST='${config.masterHost || 'MASTER_IP'}',`)
lines.push(`# MASTER_PORT=${config.masterPort},`)
lines.push(`# MASTER_USER='repl',`)
lines.push(`# MASTER_PASSWORD='strong_password',`)
lines.push(`# MASTER_AUTO_POSITION=1;`)
lines.push(`# START SLAVE;`)
}
lines.push(``)
}
// 安全
lines.push(`# ======================== 安全配置 ========================`)
lines.push(`sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION`)
if (config.version === '8.0') {
lines.push(`authentication_policy = mysql_native_password`)
}
lines.push(``)
lines.push(`[client]`)
lines.push(`port = ${config.port}`)
lines.push(`socket = ${config.socket}`)
lines.push(`default-character-set = ${config.characterSet}`)
return lines.join('\n')
}
+339
View File
@@ -0,0 +1,339 @@
export const nginxSchema = {
id: 'nginx',
name: 'NGINX',
icon: 'Monitor',
category: '代理与Web',
description: '高性能 HTTP 和反向代理服务器',
format: 'conf',
fileName: 'nginx.conf',
groups: [
{
title: '基础配置',
fields: [
{
key: 'workerProcesses',
label: 'Worker 进程数',
type: 'select',
options: [
{ label: 'auto (自动检测)', value: 'auto' },
{ label: '1', value: '1' },
{ label: '2', value: '2' },
{ label: '4', value: '4' },
{ label: '8', value: '8' },
],
default: 'auto',
tip: '通常设置为 CPU 核心数',
},
{
key: 'workerConnections',
label: '单 Worker 最大连接数',
type: 'number',
min: 512,
max: 65535,
step: 512,
default: 1024,
tip: '每个 Worker 进程能处理的最大并发连接数',
},
],
},
{
title: 'HTTP 服务',
fields: [
{
key: 'httpPort',
label: 'HTTP 端口',
type: 'number',
min: 1,
max: 65535,
default: 80,
},
{
key: 'serverName',
label: '域名 (server_name)',
type: 'text',
placeholder: 'example.com',
default: 'localhost',
required: true,
tip: '支持通配符,如 *.example.com',
},
{
key: 'enableGzip',
label: '开启 Gzip 压缩',
type: 'switch',
default: true,
tip: '压缩响应体,减少传输体积',
},
{
key: 'gzipTypes',
label: 'Gzip 压缩类型',
type: 'text',
default: 'text/plain text/css application/json application/javascript text/xml',
dependsOn: { key: 'enableGzip', value: true },
tip: 'MIME 类型,空格分隔',
},
],
},
{
title: 'HTTPS / SSL',
fields: [
{
key: 'enableHttps',
label: '开启 HTTPS',
type: 'switch',
default: false,
},
{
key: 'httpsPort',
label: 'HTTPS 端口',
type: 'number',
min: 1,
max: 65535,
default: 443,
dependsOn: { key: 'enableHttps', value: true },
},
{
key: 'sslCertificate',
label: 'SSL 证书路径',
type: 'text',
placeholder: '/etc/nginx/ssl/cert.pem',
default: '/etc/nginx/ssl/cert.pem',
dependsOn: { key: 'enableHttps', value: true },
required: true,
},
{
key: 'sslCertificateKey',
label: 'SSL 私钥路径',
type: 'text',
placeholder: '/etc/nginx/ssl/key.pem',
default: '/etc/nginx/ssl/key.pem',
dependsOn: { key: 'enableHttps', value: true },
required: true,
},
{
key: 'sslProtocols',
label: 'TLS 版本',
type: 'select',
options: [
{ label: 'TLSv1.2 + TLSv1.3 (推荐)', value: 'TLSv1.2 TLSv1.3' },
{ label: 'TLSv1.1 + TLSv1.2 + TLSv1.3', value: 'TLSv1.1 TLSv1.2 TLSv1.3' },
{ label: '仅 TLSv1.3', value: 'TLSv1.3' },
],
default: 'TLSv1.2 TLSv1.3',
dependsOn: { key: 'enableHttps', value: true },
},
],
},
{
title: '反向代理',
fields: [
{
key: 'enableProxy',
label: '开启反向代理',
type: 'switch',
default: false,
tip: '将请求转发到后端应用服务器',
},
{
key: 'proxyTarget',
label: '后端地址',
type: 'text',
placeholder: 'http://127.0.0.1:8080',
default: 'http://127.0.0.1:8080',
dependsOn: { key: 'enableProxy', value: true },
required: true,
},
{
key: 'proxyPath',
label: '代理路径前缀',
type: 'text',
placeholder: '/',
default: '/',
dependsOn: { key: 'enableProxy', value: true },
},
],
},
{
title: '高级选项',
fields: [
{
key: 'enableCors',
label: '开启跨域 (CORS)',
type: 'switch',
default: false,
tip: '添加跨域资源共享响应头',
},
{
key: 'clientMaxBodySize',
label: '请求体最大限制',
type: 'select',
options: [
{ label: '1m', value: '1m' },
{ label: '10m', value: '10m' },
{ label: '50m', value: '50m' },
{ label: '100m', value: '100m' },
{ label: '1g', value: '1g' },
],
default: '10m',
tip: '文件上传大小限制',
},
{
key: 'accessLog',
label: '开启访问日志',
type: 'switch',
default: true,
},
{
key: 'keepaliveTimeout',
label: 'Keep-Alive 超时 (秒)',
type: 'number',
min: 5,
max: 300,
default: 65,
},
],
},
],
}
export function generateNginxConf(config) {
const lines = []
lines.push(`# NGINX 配置文件 - 由 ConfTemplate 生成`)
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
lines.push(``)
lines.push(`user nginx;`)
lines.push(`worker_processes ${config.workerProcesses};`)
lines.push(`error_log /var/log/nginx/error.log warn;`)
lines.push(`pid /var/run/nginx.pid;`)
lines.push(``)
lines.push(`events {`)
lines.push(` worker_connections ${config.workerConnections};`)
lines.push(`}`)
lines.push(``)
lines.push(`http {`)
lines.push(` include /etc/nginx/mime.types;`)
lines.push(` default_type application/octet-stream;`)
lines.push(``)
if (config.accessLog) {
lines.push(` log_format main '$remote_addr - $remote_user [$time_local] "$request" '`)
lines.push(` '$status $body_bytes_sent "$http_referer" '`)
lines.push(` '"$http_user_agent" "$http_x_forwarded_for"';`)
lines.push(``)
lines.push(` access_log /var/log/nginx/access.log main;`)
lines.push(``)
}
lines.push(` sendfile on;`)
lines.push(` tcp_nopush on;`)
lines.push(` tcp_nodelay on;`)
lines.push(` keepalive_timeout ${config.keepaliveTimeout};`)
lines.push(` types_hash_max_size 2048;`)
lines.push(` client_max_body_size ${config.clientMaxBodySize};`)
lines.push(``)
if (config.enableGzip) {
lines.push(` # Gzip 压缩配置`)
lines.push(` gzip on;`)
lines.push(` gzip_vary on;`)
lines.push(` gzip_proxied any;`)
lines.push(` gzip_comp_level 6;`)
lines.push(` gzip_buffers 16 8k;`)
lines.push(` gzip_http_version 1.1;`)
lines.push(` gzip_min_length 256;`)
lines.push(` gzip_types ${config.gzipTypes};`)
lines.push(``)
}
lines.push(` # 上游服务器${config.enableProxy ? '' : ' (示例,未启用)'}`)
lines.push(` # upstream backend {`)
lines.push(` # server 127.0.0.1:8080;`)
lines.push(` # }`)
lines.push(``)
lines.push(` server {`)
lines.push(` listen ${config.httpPort};`)
if (config.enableHttps) {
lines.push(` listen ${config.httpsPort} ssl http2;`)
}
lines.push(` server_name ${config.serverName};`)
lines.push(``)
if (config.enableHttps) {
lines.push(` # SSL 证书配置`)
lines.push(` ssl_certificate ${config.sslCertificate};`)
lines.push(` ssl_certificate_key ${config.sslCertificateKey};`)
lines.push(` ssl_protocols ${config.sslProtocols};`)
lines.push(` ssl_ciphers HIGH:!aNULL:!MD5;`)
lines.push(` ssl_prefer_server_ciphers on;`)
lines.push(` ssl_session_cache shared:SSL:10m;`)
lines.push(` ssl_session_timeout 10m;`)
lines.push(``)
// HTTP to HTTPS redirect
lines.push(` # HTTP 自动跳转 HTTPS`)
lines.push(` }`)
lines.push(``)
lines.push(` server {`)
lines.push(` listen 80;`)
lines.push(` server_name ${config.serverName};`)
lines.push(` return 301 https://$host$request_uri;`)
lines.push(` }`)
lines.push(``)
lines.push(` server {`)
lines.push(` listen ${config.httpsPort} ssl http2;`)
lines.push(` server_name ${config.serverName};`)
lines.push(` ssl_certificate ${config.sslCertificate};`)
lines.push(` ssl_certificate_key ${config.sslCertificateKey};`)
lines.push(``)
}
if (config.enableCors) {
lines.push(` # 跨域配置 (CORS)`)
lines.push(` add_header 'Access-Control-Allow-Origin' '*' always;`)
lines.push(` add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;`)
lines.push(` add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;`)
lines.push(` add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;`)
lines.push(``)
lines.push(` if ($request_method = 'OPTIONS') {`)
lines.push(` add_header 'Access-Control-Allow-Origin' '*';`)
lines.push(` add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';`)
lines.push(` add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';`)
lines.push(` add_header 'Access-Control-Max-Age' 1728000;`)
lines.push(` add_header 'Content-Type' 'text/plain; charset=utf-8';`)
lines.push(` add_header 'Content-Length' 0;`)
lines.push(` return 204;`)
lines.push(` }`)
lines.push(``)
}
lines.push(` location / {`)
if (config.enableProxy) {
lines.push(` proxy_pass ${config.proxyTarget};`)
lines.push(` proxy_http_version 1.1;`)
lines.push(` proxy_set_header Upgrade $http_upgrade;`)
lines.push(` proxy_set_header Connection 'upgrade';`)
lines.push(` proxy_set_header Host $host;`)
lines.push(` proxy_set_header X-Real-IP $remote_addr;`)
lines.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`)
lines.push(` proxy_set_header X-Forwarded-Proto $scheme;`)
lines.push(` proxy_cache_bypass $http_upgrade;`)
} else {
lines.push(` root /usr/share/nginx/html;`)
lines.push(` index index.html index.htm;`)
}
lines.push(` }`)
lines.push(``)
lines.push(` error_page 404 /404.html;`)
lines.push(` error_page 500 502 503 504 /50x.html;`)
lines.push(` location = /50x.html {`)
lines.push(` root /usr/share/nginx/html;`)
lines.push(` }`)
lines.push(` }`)
lines.push(`}`)
return lines.join('\n')
}
+319
View File
@@ -0,0 +1,319 @@
export const redisSchema = {
id: 'redis',
name: 'Redis',
icon: 'Coin',
category: '缓存',
description: '高性能内存键值存储与缓存',
format: 'conf',
fileName: 'redis.conf',
groups: [
{
title: '基础配置',
fields: [
{
key: 'port',
label: '监听端口',
type: 'number',
min: 1,
max: 65535,
default: 6379,
},
{
key: 'bind',
label: '绑定地址',
type: 'text',
default: '0.0.0.0',
tip: '0.0.0.0 允许所有地址连接,生产环境建议限制',
},
{
key: 'daemonize',
label: '守护进程模式',
type: 'switch',
default: true,
tip: '后台运行 Redis',
},
{
key: 'requirepass',
label: '访问密码',
type: 'text',
placeholder: '留空表示无密码',
default: '',
tip: '生产环境务必设置强密码',
},
],
},
{
title: '内存管理',
fields: [
{
key: 'maxmemory',
label: '最大内存限制',
type: 'select',
options: [
{ label: '128mb', value: '128mb' },
{ label: '256mb', value: '256mb' },
{ label: '512mb', value: '512mb' },
{ label: '1gb', value: '1gb' },
{ label: '2gb', value: '2gb' },
{ label: '4gb', value: '4gb' },
{ label: '8gb', value: '8gb' },
{ label: '不限制', value: '0' },
],
default: '256mb',
tip: 'Redis 最大可用内存',
},
{
key: 'maxmemoryPolicy',
label: '内存淘汰策略',
type: 'select',
options: [
{ label: 'noeviction - 不淘汰,内存满时写入报错', value: 'noeviction' },
{ label: 'allkeys-lru - 所有键中淘汰最近最少使用 (推荐)', value: 'allkeys-lru' },
{ label: 'volatile-lru - 仅从设置了过期时间的键中淘汰 LRU', value: 'volatile-lru' },
{ label: 'allkeys-random - 所有键中随机淘汰', value: 'allkeys-random' },
{ label: 'volatile-ttl - 淘汰最近将过期的键', value: 'volatile-ttl' },
],
default: 'allkeys-lru',
dependsOn: { key: 'maxmemory', valueNotIn: ['0'] },
},
],
},
{
title: '持久化配置',
fields: [
{
key: 'enableRdb',
label: 'RDB 快照',
type: 'switch',
default: true,
tip: '定期将内存数据快照写入磁盘',
},
{
key: 'rdbSaveRules',
label: 'RDB 保存规则',
type: 'select',
options: [
{ label: '默认策略 (推荐)', value: 'default' },
{ label: '保守策略 (数据安全优先)', value: 'conservative' },
{ label: '激进策略 (性能优先)', value: 'aggressive' },
],
default: 'default',
dependsOn: { key: 'enableRdb', value: true },
},
{
key: 'enableAof',
label: 'AOF 持久化',
type: 'switch',
default: false,
tip: '记录每次写操作,数据安全性更高',
},
{
key: 'aofFsync',
label: 'AOF 同步策略',
type: 'select',
options: [
{ label: 'everysec - 每秒同步 (推荐)', value: 'everysec' },
{ label: 'always - 每次写入同步 (最安全,性能最低)', value: 'always' },
{ label: 'no - 由操作系统决定 (性能最高)', value: 'no' },
],
default: 'everysec',
dependsOn: { key: 'enableAof', value: true },
},
],
},
{
title: '运行模式',
fields: [
{
key: 'mode',
label: '运行模式',
type: 'select',
options: [
{ label: '单机模式 (Standalone)', value: 'standalone' },
{ label: '哨兵模式 (Sentinel)', value: 'sentinel' },
{ label: '集群模式 (Cluster)', value: 'cluster' },
],
default: 'standalone',
},
{
key: 'sentinelPort',
label: '哨兵端口',
type: 'number',
min: 1,
max: 65535,
default: 26379,
dependsOn: { key: 'mode', value: 'sentinel' },
},
{
key: 'sentinelMasterName',
label: '主节点名称',
type: 'text',
default: 'mymaster',
dependsOn: { key: 'mode', value: 'sentinel' },
},
{
key: 'sentinelDownAfter',
label: '主观下线时间 (毫秒)',
type: 'number',
min: 1000,
max: 60000,
step: 1000,
default: 5000,
dependsOn: { key: 'mode', value: 'sentinel' },
},
{
key: 'clusterNodeTimeout',
label: '集群节点超时 (毫秒)',
type: 'number',
min: 1000,
max: 30000,
step: 1000,
default: 5000,
dependsOn: { key: 'mode', value: 'cluster' },
},
],
},
{
title: '连接与性能',
fields: [
{
key: 'timeout',
label: '空闲连接超时 (秒)',
type: 'number',
min: 0,
max: 3600,
default: 300,
tip: '0 表示不超时',
},
{
key: 'maxclients',
label: '最大客户端连接数',
type: 'number',
min: 1,
max: 100000,
default: 10000,
},
{
key: 'tcpKeepalive',
label: 'TCP Keep-Alive (秒)',
type: 'number',
min: 0,
max: 3600,
default: 300,
tip: '0 表示不开启',
},
],
},
],
}
export function generateRedisConf(config) {
const lines = []
lines.push(`# Redis 配置文件 - 由 ConfTemplate 生成`)
lines.push(`# 生成时间: ${new Date().toLocaleString('zh-CN')}`)
lines.push(``)
// 基础配置
lines.push(`################################## 基础配置 ##################################`)
lines.push(``)
lines.push(`port ${config.port}`)
lines.push(`bind ${config.bind}`)
lines.push(`daemonize ${config.daemonize ? 'yes' : 'no'}`)
lines.push(`pidfile /var/run/redis_${config.port}.pid`)
lines.push(`loglevel notice`)
lines.push(`logfile /var/log/redis/redis_${config.port}.log`)
lines.push(`databases 16`)
if (config.requirepass) {
lines.push(``)
lines.push(`# 安全 - 访问密码`)
lines.push(`requirepass ${config.requirepass}`)
}
lines.push(``)
// 内存管理
lines.push(`################################## 内存管理 ##################################`)
lines.push(``)
if (config.maxmemory === '0') {
lines.push(`# maxmemory <bytes> # 未设置内存限制`)
} else {
lines.push(`maxmemory ${config.maxmemory}`)
lines.push(`maxmemory-policy ${config.maxmemoryPolicy}`)
}
lines.push(``)
// 持久化
lines.push(`################################## 持久化 ##################################`)
lines.push(``)
if (config.enableRdb) {
lines.push(`# RDB 快照配置`)
lines.push(`dbfilename dump.rdb`)
lines.push(`dir /var/lib/redis`)
if (config.rdbSaveRules === 'default') {
lines.push(`save 900 1`)
lines.push(`save 300 10`)
lines.push(`save 60 10000`)
} else if (config.rdbSaveRules === 'conservative') {
lines.push(`save 3600 1`)
lines.push(`save 300 100`)
lines.push(`save 60 10000`)
} else {
lines.push(`save 60 1000`)
lines.push(`save 10 10000`)
}
lines.push(`rdbcompression yes`)
lines.push(`rdbchecksum yes`)
} else {
lines.push(`# RDB 快照已禁用`)
lines.push(`save ""`)
}
lines.push(``)
if (config.enableAof) {
lines.push(`# AOF 持久化配置`)
lines.push(`appendonly yes`)
lines.push(`appendfilename "appendonly.aof"`)
lines.push(`appendfsync ${config.aofFsync}`)
lines.push(`auto-aof-rewrite-percentage 100`)
lines.push(`auto-aof-rewrite-min-size 64mb`)
} else {
lines.push(`# AOF 持久化已禁用`)
lines.push(`appendonly no`)
}
lines.push(``)
// 运行模式
if (config.mode === 'sentinel') {
lines.push(`################################## 哨兵配置 ##################################`)
lines.push(`# 以下为 sentinel.conf 内容,需单独放置`)
lines.push(``)
lines.push(`port ${config.sentinelPort}`)
lines.push(`sentinel monitor ${config.sentinelMasterName} 127.0.0.1 ${config.port} 2`)
lines.push(`sentinel down-after-milliseconds ${config.sentinelMasterName} ${config.sentinelDownAfter}`)
lines.push(`sentinel failover-timeout ${config.sentinelMasterName} 60000`)
lines.push(`sentinel parallel-syncs ${config.sentinelMasterName} 1`)
lines.push(``)
} else if (config.mode === 'cluster') {
lines.push(`################################## 集群配置 ##################################`)
lines.push(``)
lines.push(`cluster-enabled yes`)
lines.push(`cluster-config-file nodes-${config.port}.conf`)
lines.push(`cluster-node-timeout ${config.clusterNodeTimeout}`)
lines.push(`cluster-announce-ip 127.0.0.1`)
lines.push(`cluster-announce-port ${config.port}`)
lines.push(`cluster-announce-bus-port ${config.port + 10000}`)
lines.push(``)
}
// 连接与性能
lines.push(`################################## 连接与性能 ##################################`)
lines.push(``)
lines.push(`timeout ${config.timeout}`)
lines.push(`tcp-keepalive ${config.tcpKeepalive}`)
lines.push(`maxclients ${config.maxclients}`)
lines.push(`tcp-backlog 511`)
return lines.join('\n')
}
+21
View File
@@ -0,0 +1,21 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
server: {
host: '0.0.0.0',
port: 3000,
},
})