duqing2026 commited on
Commit
847febc
·
0 Parent(s):

Initial commit: Nginx Config Studio

Browse files
Files changed (7) hide show
  1. Dockerfile +19 -0
  2. README.md +64 -0
  3. app.py +27 -0
  4. requirements.txt +2 -0
  5. static/css/style.css +27 -0
  6. static/js/app.js +159 -0
  7. templates/index.html +214 -0
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ # Create a non-root user for Hugging Face Spaces
11
+ RUN useradd -m -u 1000 user
12
+ USER user
13
+
14
+ ENV HOME=/home/user \
15
+ PATH=/home/user/.local/bin:$PATH
16
+
17
+ EXPOSE 7860
18
+
19
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Nginx Config Studio
3
+ emoji: 🛠️
4
+ colorFrom: gray
5
+ colorTo: green
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ short_description: 可视化 Nginx 配置生成器
10
+ ---
11
+
12
+ # Nginx 配置大师 (Nginx Config Studio)
13
+
14
+ 一款专为开发者和运维人员设计的**可视化 Nginx 配置文件生成器**。通过直观的图形界面,快速生成安全、高性能的 `nginx.conf` 配置,避免繁琐的语法错误。
15
+
16
+ ## ✨ 核心功能
17
+
18
+ * **可视化配置**:支持 Server、Location、Upstream 等核心指令的图形化编辑。
19
+ * **反向代理管理**:轻松添加多个代理路径,支持 WebSocket、Host 头转发等常用设置。
20
+ * **安全加固**:一键开启 HSTS、隐藏版本号、防点击劫持等安全选项(基于 OWASP 最佳实践)。
21
+ * **性能优化**:内置 Gzip 压缩、静态资源缓存、Client Body 限制等优化预设。
22
+ * **实时预览**:右侧代码框实时显示生成的配置文件,支持语法高亮。
23
+ * **一键导出**:支持复制到剪贴板或下载 `.conf` 文件。
24
+
25
+ ## 🛠️ 技术栈
26
+
27
+ * **Frontend**: Vue 3 + Tailwind CSS (使用 Baomitu CDN 加速) + Prism.js (语法高亮)
28
+ * **Backend**: Flask (Python)
29
+ * **Deployment**: Docker (兼容 Hugging Face Spaces)
30
+
31
+ ## 🚀 快速开始
32
+
33
+ ### Docker 部署 (推荐)
34
+
35
+ 本项目已配置 Dockerfile,可直接部署到 Hugging Face Spaces 或本地 Docker 环境。
36
+
37
+ 1. **构建镜像**:
38
+ ```bash
39
+ docker build -t nginx-config-studio .
40
+ ```
41
+
42
+ 2. **运行容器**:
43
+ ```bash
44
+ docker run -p 7860:7860 nginx-config-studio
45
+ ```
46
+
47
+ 3. **访问应用**:
48
+ 打开浏览器访问 `http://localhost:7860`
49
+
50
+ ### 本地开发
51
+
52
+ 1. 安装依赖:
53
+ ```bash
54
+ pip install -r requirements.txt
55
+ ```
56
+
57
+ 2. 运行应用:
58
+ ```bash
59
+ python app.py
60
+ ```
61
+
62
+ ## 📝 许可证
63
+
64
+ MIT License
app.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, send_from_directory
2
+ import os
3
+
4
+ app = Flask(__name__)
5
+
6
+ @app.route('/')
7
+ def index():
8
+ return render_template('index.html')
9
+
10
+ @app.route('/health')
11
+ def health():
12
+ return "OK", 200
13
+
14
+ @app.route('/static/<path:path>')
15
+ def send_static(path):
16
+ return send_from_directory('static', path)
17
+
18
+ @app.errorhandler(404)
19
+ def page_not_found(e):
20
+ return render_template('index.html'), 404
21
+
22
+ @app.errorhandler(500)
23
+ def internal_server_error(e):
24
+ return "Internal Server Error: " + str(e), 500
25
+
26
+ if __name__ == '__main__':
27
+ app.run(host='0.0.0.0', port=7860, debug=False)
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ flask==3.0.0
2
+ gunicorn==21.2.0
static/css/style.css ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Custom Scrollbar */
2
+ .custom-scrollbar::-webkit-scrollbar {
3
+ width: 8px;
4
+ height: 8px;
5
+ }
6
+
7
+ .custom-scrollbar::-webkit-scrollbar-track {
8
+ background: #0f172a;
9
+ }
10
+
11
+ .custom-scrollbar::-webkit-scrollbar-thumb {
12
+ background: #334155;
13
+ border-radius: 4px;
14
+ }
15
+
16
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
17
+ background: #475569;
18
+ }
19
+
20
+ /* Prism Overrides for Dark Mode integration */
21
+ code[class*="language-"],
22
+ pre[class*="language-"] {
23
+ text-shadow: none !important;
24
+ font-family: 'Menlo', 'Monaco', 'Courier New', monospace !important;
25
+ font-size: 14px !important;
26
+ line-height: 1.5 !important;
27
+ }
static/js/app.js ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { createApp, ref, computed, watch, nextTick, onMounted } = Vue;
2
+
3
+ const app = createApp({
4
+ delimiters: ['[[', ']]'],
5
+ setup() {
6
+ const currentTab = ref('server');
7
+
8
+ const tabs = [
9
+ { id: 'server', name: '基础设置', icon: 'fa-solid fa-sliders' },
10
+ { id: 'proxy', name: '反向代理', icon: 'fa-solid fa-route' },
11
+ { id: 'security', name: '安全防护', icon: 'fa-solid fa-shield-halved' },
12
+ { id: 'performance', name: '性能优化', icon: 'fa-solid fa-gauge-high' }
13
+ ];
14
+
15
+ const config = ref({
16
+ port: 80,
17
+ serverName: 'example.com',
18
+ root: '/var/www/html',
19
+ index: 'index.html index.htm',
20
+ forceHttps: false,
21
+ hideVersion: true,
22
+ hsts: false,
23
+ preventClickjacking: true,
24
+ gzip: true,
25
+ clientMaxBodySize: 10,
26
+ cacheStatic: true,
27
+ cacheExtensions: 'jpg jpeg png gif ico css js',
28
+ locations: [
29
+ { path: '/', proxyPass: '', websocket: false, tryFiles: '$uri $uri/ /index.html' },
30
+ { path: '/api', proxyPass: 'http://localhost:3000', websocket: false, tryFiles: '' }
31
+ ]
32
+ });
33
+
34
+ const addLocation = () => {
35
+ config.value.locations.push({ path: '/new-path', proxyPass: 'http://localhost:8080', websocket: false, tryFiles: '' });
36
+ };
37
+
38
+ const removeLocation = (index) => {
39
+ config.value.locations.splice(index, 1);
40
+ };
41
+
42
+ const generatedConfig = computed(() => {
43
+ const c = config.value;
44
+ let lines = [];
45
+
46
+ // Header
47
+ lines.push(`server {`);
48
+ lines.push(` listen ${c.port};`);
49
+ lines.push(` server_name ${c.serverName};`);
50
+ if (c.root) lines.push(` root ${c.root};`);
51
+ if (c.index) lines.push(` index ${c.index};`);
52
+ lines.push(``);
53
+
54
+ // Security
55
+ if (c.hideVersion) lines.push(` # Hide Nginx version\n server_tokens off;`);
56
+ if (c.forceHttps) lines.push(` # Force HTTPS (Redirect)\n if ($scheme != "https") {\n return 301 https://$host$request_uri;\n }`);
57
+
58
+ let headers = [];
59
+ if (c.hsts) headers.push('add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;');
60
+ if (c.preventClickjacking) headers.push('add_header X-Frame-Options "SAMEORIGIN" always;');
61
+
62
+ if (headers.length > 0) {
63
+ lines.push(` # Security Headers`);
64
+ headers.forEach(h => lines.push(` ${h}`));
65
+ lines.push(``);
66
+ }
67
+
68
+ // Performance
69
+ if (c.clientMaxBodySize) lines.push(` client_max_body_size ${c.clientMaxBodySize}M;`);
70
+
71
+ if (c.gzip) {
72
+ lines.push(` # Gzip Compression`);
73
+ lines.push(` gzip on;`);
74
+ lines.push(` gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;`);
75
+ lines.push(``);
76
+ }
77
+
78
+ if (c.cacheStatic) {
79
+ lines.push(` # Static File Caching`);
80
+ lines.push(` location ~* \\.(${c.cacheExtensions.split(' ').join('|')})$ {`);
81
+ lines.push(` expires 30d;`);
82
+ lines.push(` access_log off;`);
83
+ lines.push(` }`);
84
+ lines.push(``);
85
+ }
86
+
87
+ // Locations
88
+ c.locations.forEach(loc => {
89
+ lines.push(` location ${loc.path} {`);
90
+ if (loc.proxyPass) {
91
+ lines.push(` proxy_pass ${loc.proxyPass};`);
92
+ lines.push(` proxy_set_header Host $host;`);
93
+ lines.push(` proxy_set_header X-Real-IP $remote_addr;`);
94
+ lines.push(` proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;`);
95
+ if (loc.websocket) {
96
+ lines.push(` # WebSocket Support`);
97
+ lines.push(` proxy_http_version 1.1;`);
98
+ lines.push(` proxy_set_header Upgrade $http_upgrade;`);
99
+ lines.push(` proxy_set_header Connection "upgrade";`);
100
+ }
101
+ } else if (loc.tryFiles) {
102
+ lines.push(` try_files ${loc.tryFiles};`);
103
+ }
104
+ lines.push(` }`);
105
+ lines.push(``);
106
+ });
107
+
108
+ lines.push(`}`);
109
+ return lines.join('\n');
110
+ });
111
+
112
+ const highlightCode = () => {
113
+ nextTick(() => {
114
+ const codeBlock = document.getElementById('code-block');
115
+ if (codeBlock && window.Prism) {
116
+ codeBlock.textContent = generatedConfig.value; // Update text content explicitly for Prism
117
+ window.Prism.highlightElement(codeBlock);
118
+ }
119
+ });
120
+ };
121
+
122
+ watch(generatedConfig, () => {
123
+ highlightCode();
124
+ });
125
+
126
+ onMounted(() => {
127
+ highlightCode();
128
+ });
129
+
130
+ const copyConfig = () => {
131
+ navigator.clipboard.writeText(generatedConfig.value).then(() => {
132
+ alert('配置已复制到剪贴板!');
133
+ });
134
+ };
135
+
136
+ const downloadConfig = () => {
137
+ const blob = new Blob([generatedConfig.value], { type: 'text/plain' });
138
+ const url = window.URL.createObjectURL(blob);
139
+ const a = document.createElement('a');
140
+ a.href = url;
141
+ a.download = 'nginx.conf';
142
+ a.click();
143
+ window.URL.revokeObjectURL(url);
144
+ };
145
+
146
+ return {
147
+ tabs,
148
+ currentTab,
149
+ config,
150
+ addLocation,
151
+ removeLocation,
152
+ generatedConfig,
153
+ copyConfig,
154
+ downloadConfig
155
+ };
156
+ }
157
+ });
158
+
159
+ app.mount('#app');
templates/index.html ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Nginx 配置大师 | Nginx Config Studio</title>
7
+ <script src="https://lib.baomitu.com/vue/3.3.4/vue.global.prod.min.js"></script>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <link href="https://lib.baomitu.com/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
10
+ <script src="https://lib.baomitu.com/prism/1.29.0/prism.min.js"></script>
11
+ <script src="https://lib.baomitu.com/prism/1.29.0/components/prism-nginx.min.js"></script>
12
+ <link href="https://lib.baomitu.com/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
13
+ <style>
14
+ body { font-family: 'Inter', system-ui, -apple-system, sans-serif; }
15
+ .code-preview { max-height: calc(100vh - 200px); overflow-y: auto; }
16
+ [v-cloak] { display: none; }
17
+ </style>
18
+ </head>
19
+ <body class="bg-gray-900 text-gray-100 h-screen overflow-hidden">
20
+ <div id="app" v-cloak class="h-full flex flex-col">
21
+ <!-- Header -->
22
+ <header class="bg-gray-800 border-b border-gray-700 h-16 flex items-center justify-between px-6 shrink-0">
23
+ <div class="flex items-center gap-3">
24
+ <i class="fa-solid fa-server text-green-500 text-2xl"></i>
25
+ <h1 class="text-xl font-bold tracking-wide">Nginx Config Studio <span class="text-xs bg-green-900 text-green-300 px-2 py-0.5 rounded ml-2">v1.0</span></h1>
26
+ </div>
27
+ <div class="flex items-center gap-4">
28
+ <button @click="copyConfig" class="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg text-sm transition flex items-center gap-2">
29
+ <i class="fa-solid fa-copy"></i> 复制配置
30
+ </button>
31
+ <button @click="downloadConfig" class="bg-green-600 hover:bg-green-500 text-white px-4 py-2 rounded-lg text-sm font-medium transition flex items-center gap-2 shadow-lg shadow-green-900/20">
32
+ <i class="fa-solid fa-download"></i> 下载 .conf
33
+ </button>
34
+ </div>
35
+ </header>
36
+
37
+ <!-- Main Content -->
38
+ <div class="flex-1 flex overflow-hidden">
39
+ <!-- Sidebar / Config Form -->
40
+ <div class="w-1/2 lg:w-5/12 border-r border-gray-700 flex flex-col bg-gray-800/50">
41
+ <!-- Tabs -->
42
+ <div class="flex border-b border-gray-700">
43
+ <button v-for="tab in tabs" :key="tab.id"
44
+ @click="currentTab = tab.id"
45
+ :class="['flex-1 py-3 text-sm font-medium border-b-2 transition-colors duration-200',
46
+ currentTab === tab.id ? 'border-green-500 text-green-400 bg-gray-800' : 'border-transparent text-gray-400 hover:text-gray-200 hover:bg-gray-700/50']">
47
+ <i :class="[tab.icon, 'mr-2']"></i> [[ tab.name ]]
48
+ </button>
49
+ </div>
50
+
51
+ <!-- Form Content -->
52
+ <div class="flex-1 overflow-y-auto p-6 space-y-6">
53
+
54
+ <!-- Server Settings -->
55
+ <div v-if="currentTab === 'server'" class="space-y-4">
56
+ <div class="grid grid-cols-2 gap-4">
57
+ <div class="form-group">
58
+ <label class="block text-xs font-medium text-gray-400 mb-1">监听端口 (Listen Port)</label>
59
+ <input v-model="config.port" type="number" class="w-full bg-gray-900 border border-gray-700 rounded p-2 text-white focus:border-green-500 focus:outline-none transition">
60
+ </div>
61
+ <div class="form-group">
62
+ <label class="block text-xs font-medium text-gray-400 mb-1">域名 (Server Name)</label>
63
+ <input v-model="config.serverName" type="text" placeholder="example.com" class="w-full bg-gray-900 border border-gray-700 rounded p-2 text-white focus:border-green-500 focus:outline-none transition">
64
+ </div>
65
+ </div>
66
+ <div class="form-group">
67
+ <label class="block text-xs font-medium text-gray-400 mb-1">项目根目录 (Root Directory)</label>
68
+ <input v-model="config.root" type="text" placeholder="/var/www/html" class="w-full bg-gray-900 border border-gray-700 rounded p-2 text-white focus:border-green-500 focus:outline-none transition">
69
+ </div>
70
+ <div class="form-group">
71
+ <label class="block text-xs font-medium text-gray-400 mb-1">默认文件 (Index)</label>
72
+ <input v-model="config.index" type="text" placeholder="index.html index.htm" class="w-full bg-gray-900 border border-gray-700 rounded p-2 text-white focus:border-green-500 focus:outline-none transition">
73
+ </div>
74
+ </div>
75
+
76
+ <!-- Proxy Settings -->
77
+ <div v-if="currentTab === 'proxy'" class="space-y-4">
78
+ <div class="flex justify-between items-center mb-2">
79
+ <h3 class="text-sm font-bold text-gray-300">反向代理规则</h3>
80
+ <button @click="addLocation" class="text-xs bg-blue-600 hover:bg-blue-500 text-white px-2 py-1 rounded transition">
81
+ <i class="fa-solid fa-plus"></i> 添加规则
82
+ </button>
83
+ </div>
84
+
85
+ <div v-for="(loc, index) in config.locations" :key="index" class="bg-gray-900 p-4 rounded border border-gray-700 relative group">
86
+ <button @click="removeLocation(index)" class="absolute top-2 right-2 text-gray-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition">
87
+ <i class="fa-solid fa-times"></i>
88
+ </button>
89
+ <div class="grid grid-cols-2 gap-3">
90
+ <div>
91
+ <label class="block text-xs text-gray-500 mb-1">路径 (Path)</label>
92
+ <input v-model="loc.path" type="text" placeholder="/" class="w-full bg-gray-800 border border-gray-600 rounded p-1.5 text-sm text-white">
93
+ </div>
94
+ <div>
95
+ <label class="block text-xs text-gray-500 mb-1">目标 (Proxy Pass)</label>
96
+ <input v-model="loc.proxyPass" type="text" placeholder="http://localhost:3000" class="w-full bg-gray-800 border border-gray-600 rounded p-1.5 text-sm text-white">
97
+ </div>
98
+ </div>
99
+ <div class="mt-2 flex items-center gap-2">
100
+ <input type="checkbox" v-model="loc.websocket" :id="'ws-'+index" class="rounded bg-gray-700 border-gray-600 text-green-500 focus:ring-offset-gray-900">
101
+ <label :for="'ws-'+index" class="text-xs text-gray-400">支持 WebSocket</label>
102
+ </div>
103
+ </div>
104
+
105
+ <div v-if="config.locations.length === 0" class="text-center text-gray-500 py-8 text-sm">
106
+ 暂无代理规则,点击上方添加
107
+ </div>
108
+ </div>
109
+
110
+ <!-- Security Settings -->
111
+ <div v-if="currentTab === 'security'" class="space-y-4">
112
+ <div class="bg-gray-900 p-4 rounded border border-gray-700 space-y-3">
113
+ <div class="flex items-center justify-between">
114
+ <div>
115
+ <h4 class="text-sm font-medium text-gray-200">强制 HTTPS</h4>
116
+ <p class="text-xs text-gray-500">重定向所有 HTTP 请求到 HTTPS</p>
117
+ </div>
118
+ <label class="relative inline-flex items-center cursor-pointer">
119
+ <input type="checkbox" v-model="config.forceHttps" class="sr-only peer">
120
+ <div class="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-green-600"></div>
121
+ </label>
122
+ </div>
123
+
124
+ <div class="flex items-center justify-between border-t border-gray-800 pt-3">
125
+ <div>
126
+ <h4 class="text-sm font-medium text-gray-200">隐藏版本号</h4>
127
+ <p class="text-xs text-gray-500">server_tokens off</p>
128
+ </div>
129
+ <label class="relative inline-flex items-center cursor-pointer">
130
+ <input type="checkbox" v-model="config.hideVersion" class="sr-only peer">
131
+ <div class="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-green-600"></div>
132
+ </label>
133
+ </div>
134
+
135
+ <div class="flex items-center justify-between border-t border-gray-800 pt-3">
136
+ <div>
137
+ <h4 class="text-sm font-medium text-gray-200">HSTS</h4>
138
+ <p class="text-xs text-gray-500">HTTP Strict Transport Security</p>
139
+ </div>
140
+ <label class="relative inline-flex items-center cursor-pointer">
141
+ <input type="checkbox" v-model="config.hsts" class="sr-only peer">
142
+ <div class="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-green-600"></div>
143
+ </label>
144
+ </div>
145
+
146
+ <div class="flex items-center justify-between border-t border-gray-800 pt-3">
147
+ <div>
148
+ <h4 class="text-sm font-medium text-gray-200">防止点击劫持</h4>
149
+ <p class="text-xs text-gray-500">X-Frame-Options: SAMEORIGIN</p>
150
+ </div>
151
+ <label class="relative inline-flex items-center cursor-pointer">
152
+ <input type="checkbox" v-model="config.preventClickjacking" class="sr-only peer">
153
+ <div class="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-green-600"></div>
154
+ </label>
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <!-- Performance Settings -->
160
+ <div v-if="currentTab === 'performance'" class="space-y-4">
161
+ <div class="bg-gray-900 p-4 rounded border border-gray-700 space-y-3">
162
+ <div class="flex items-center justify-between">
163
+ <div>
164
+ <h4 class="text-sm font-medium text-gray-200">Gzip 压缩</h4>
165
+ <p class="text-xs text-gray-500">开启 Gzip 减少传输体积</p>
166
+ </div>
167
+ <label class="relative inline-flex items-center cursor-pointer">
168
+ <input type="checkbox" v-model="config.gzip" class="sr-only peer">
169
+ <div class="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-green-600"></div>
170
+ </label>
171
+ </div>
172
+
173
+ <div class="pt-3 border-t border-gray-800">
174
+ <label class="block text-xs font-medium text-gray-400 mb-1">最大上传限制 (Client Max Body Size)</label>
175
+ <div class="flex items-center">
176
+ <input v-model="config.clientMaxBodySize" type="number" class="w-20 bg-gray-800 border border-gray-600 rounded-l p-1.5 text-white text-sm text-center">
177
+ <span class="bg-gray-700 border border-l-0 border-gray-600 rounded-r p-1.5 text-sm text-gray-300">MB</span>
178
+ </div>
179
+ </div>
180
+
181
+ <div class="pt-3 border-t border-gray-800">
182
+ <div class="flex items-center justify-between mb-2">
183
+ <h4 class="text-sm font-medium text-gray-200">静态资源缓存</h4>
184
+ <label class="relative inline-flex items-center cursor-pointer">
185
+ <input type="checkbox" v-model="config.cacheStatic" class="sr-only peer">
186
+ <div class="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-green-600"></div>
187
+ </label>
188
+ </div>
189
+ <div v-if="config.cacheStatic" class="pl-4 border-l-2 border-green-900">
190
+ <label class="block text-xs text-gray-500 mb-1">缓存文件类型</label>
191
+ <input v-model="config.cacheExtensions" type="text" class="w-full bg-gray-800 border border-gray-600 rounded p-1.5 text-xs text-gray-300">
192
+ </div>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+
199
+ <!-- Preview Panel -->
200
+ <div class="flex-1 bg-gray-950 flex flex-col min-w-0">
201
+ <div class="h-10 bg-gray-900 border-b border-gray-800 flex items-center px-4 justify-between">
202
+ <span class="text-xs font-mono text-gray-400">nginx.conf</span>
203
+ <span class="text-xs text-gray-600">Read Only</span>
204
+ </div>
205
+ <div class="flex-1 overflow-auto relative custom-scrollbar">
206
+ <pre class="language-nginx h-full m-0 rounded-none !bg-gray-950 !text-sm"><code id="code-block" class="language-nginx">[[ generatedConfig ]]</code></pre>
207
+ </div>
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ <script src="/static/js/app.js"></script>
213
+ </body>
214
+ </html>