feat: 初始化 ConfTemplate 云原生配置文件生成器
- Vue 3 + Element Plus + Monaco Editor 技术栈 - 支持 NGINX、Redis、MySQL、K8s Deployment、K8s Service 五个组件 - 侧边栏搜索过滤、动态表单联动、实时代码预览 - 一键复制/下载/重置,localStorage 状态持久化 - 纯前端静态页面,零后端依赖
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
*.local
|
||||
*.log
|
||||
@@ -0,0 +1,212 @@
|
||||
# ⚙️ ConfTemplate — 云原生配置文件生成器
|
||||
|
||||
> 一站式可视化生成 NGINX、Redis、MySQL、Kubernetes 等主流中间件的标准配置文件,告别手写配置的语法烦恼。
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
## 📸 界面预览
|
||||
|
||||
三栏布局:**左侧边栏导航** → **中间表单配置** → **右侧代码预览**
|
||||
|
||||
- 侧边栏支持搜索过滤,快速定位目标中间件
|
||||
- 表单支持下拉框、输入框、开关、数字输入等多种组件
|
||||
- 代码预览基于 Monaco Editor(VS 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
@@ -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>
|
||||
Generated
+2122
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
@@ -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')
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user