duqing2026 commited on
Commit
d2cc112
·
1 Parent(s): e501942

升级优化

Browse files
Files changed (3) hide show
  1. app.py +52 -17
  2. templates/export_theme.html +13 -10
  3. templates/index.html +64 -9
app.py CHANGED
@@ -1,8 +1,27 @@
1
- from flask import Flask, render_template, request, send_file, make_response
2
  import io
 
 
 
 
 
3
 
4
  app = Flask(__name__)
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  @app.route('/')
7
  def index():
8
  return render_template('index.html')
@@ -12,28 +31,44 @@ def preview():
12
  """
13
  Returns the HTML for the preview iframe.
14
  """
15
- data = request.json
16
- return render_template('export_theme.html', **data)
 
 
 
 
 
 
 
 
 
17
 
18
  @app.route('/download', methods=['POST'])
19
  def download():
20
  """
21
  Returns the HTML as a downloadable file.
22
  """
23
- data = request.json
24
- html_content = render_template('export_theme.html', **data)
25
-
26
- # Create a file-like object
27
- mem = io.BytesIO()
28
- mem.write(html_content.encode('utf-8'))
29
- mem.seek(0)
30
-
31
- return send_file(
32
- mem,
33
- as_attachment=True,
34
- download_name='404.html',
35
- mimetype='text/html'
36
- )
 
 
 
 
 
 
37
 
38
  if __name__ == '__main__':
 
39
  app.run(debug=True, port=7860, host='0.0.0.0')
 
1
+ from flask import Flask, render_template, request, send_file
2
  import io
3
+ import logging
4
+
5
+ # Configure logging
6
+ logging.basicConfig(level=logging.INFO)
7
+ logger = logging.getLogger(__name__)
8
 
9
  app = Flask(__name__)
10
 
11
+ # Default configuration to prevent missing data errors
12
+ DEFAULTS = {
13
+ 'theme': 'modern',
14
+ 'title': '404',
15
+ 'message': 'Oops! The page you are looking for has vanished into the void.',
16
+ 'buttonText': 'Back to Safety',
17
+ 'buttonLink': '/',
18
+ 'bgColor': '#ffffff',
19
+ 'textColor': '#1f2937',
20
+ 'accentColor': '#ef4444',
21
+ 'illustration': 'ghost',
22
+ 'showGame': False
23
+ }
24
+
25
  @app.route('/')
26
  def index():
27
  return render_template('index.html')
 
31
  """
32
  Returns the HTML for the preview iframe.
33
  """
34
+ try:
35
+ # Get JSON data, defaulting to empty dict if None
36
+ data = request.get_json(silent=True) or {}
37
+
38
+ # Merge defaults with provided data (data overrides defaults)
39
+ context = {**DEFAULTS, **data}
40
+
41
+ return render_template('export_theme.html', **context)
42
+ except Exception as e:
43
+ logger.error(f"Preview error: {e}")
44
+ return f"Internal Server Error: {str(e)}", 500
45
 
46
  @app.route('/download', methods=['POST'])
47
  def download():
48
  """
49
  Returns the HTML as a downloadable file.
50
  """
51
+ try:
52
+ data = request.get_json(silent=True) or {}
53
+ context = {**DEFAULTS, **data}
54
+
55
+ html_content = render_template('export_theme.html', **context)
56
+
57
+ # Create a file-like object
58
+ mem = io.BytesIO()
59
+ mem.write(html_content.encode('utf-8'))
60
+ mem.seek(0)
61
+
62
+ return send_file(
63
+ mem,
64
+ as_attachment=True,
65
+ download_name='404.html',
66
+ mimetype='text/html'
67
+ )
68
+ except Exception as e:
69
+ logger.error(f"Download error: {e}")
70
+ return f"Internal Server Error: {str(e)}", 500
71
 
72
  if __name__ == '__main__':
73
+ # Use port 7860 for Hugging Face Spaces compatibility
74
  app.run(debug=True, port=7860, host='0.0.0.0')
templates/export_theme.html CHANGED
@@ -3,12 +3,12 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>{{ title }}</title>
7
  <style>
8
  :root {
9
- --bg-color: {{ bgColor }};
10
- --text-color: {{ textColor }};
11
- --accent-color: {{ accentColor }};
12
  }
13
 
14
  body {
@@ -150,13 +150,16 @@
150
  <div class="ghost">💔</div>
151
  {% elif illustration == 'planet' %}
152
  <div class="ghost">🪐</div>
 
 
 
153
  {% endif %}
154
  </div>
155
 
156
- <h1>{{ title }}</h1>
157
- <p>{{ message }}</p>
158
 
159
- <a href="{{ buttonLink }}" class="btn">{{ buttonText }}</a>
160
 
161
  {% if showGame %}
162
  <div class="game-toggle" onclick="toggleGame()">Play Snake while you wait?</div>
@@ -243,17 +246,17 @@
243
  }
244
 
245
  // Draw
246
- ctx.fillStyle = '{{ bgColor }}';
247
  ctx.fillRect(0, 0, canvas.width, canvas.height);
248
 
249
  // Snake
250
- ctx.fillStyle = '{{ accentColor }}';
251
  for (let i = 0; i < snake.length; i++) {
252
  ctx.fillRect(snake[i].x * gridSize, snake[i].y * gridSize, gridSize - 2, gridSize - 2);
253
  }
254
 
255
  // Food
256
- ctx.fillStyle = '{{ textColor }}';
257
  ctx.fillRect(food.x * gridSize, food.y * gridSize, gridSize - 2, gridSize - 2);
258
  }
259
 
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ title | default('404') }}</title>
7
  <style>
8
  :root {
9
+ --bg-color: {{ bgColor | default('#ffffff') }};
10
+ --text-color: {{ textColor | default('#1f2937') }};
11
+ --accent-color: {{ accentColor | default('#ef4444') }};
12
  }
13
 
14
  body {
 
150
  <div class="ghost">💔</div>
151
  {% elif illustration == 'planet' %}
152
  <div class="ghost">🪐</div>
153
+ {% else %}
154
+ <!-- Default fallback if illustration matches nothing -->
155
+ <div class="ghost">👻</div>
156
  {% endif %}
157
  </div>
158
 
159
+ <h1>{{ title | default('404') }}</h1>
160
+ <p>{{ message | default('Page not found') }}</p>
161
 
162
+ <a href="{{ buttonLink | default('/') }}" class="btn">{{ buttonText | default('Go Home') }}</a>
163
 
164
  {% if showGame %}
165
  <div class="game-toggle" onclick="toggleGame()">Play Snake while you wait?</div>
 
246
  }
247
 
248
  // Draw
249
+ ctx.fillStyle = '{{ bgColor | default("#ffffff") }}';
250
  ctx.fillRect(0, 0, canvas.width, canvas.height);
251
 
252
  // Snake
253
+ ctx.fillStyle = '{{ accentColor | default("#ef4444") }}';
254
  for (let i = 0; i < snake.length; i++) {
255
  ctx.fillRect(snake[i].x * gridSize, snake[i].y * gridSize, gridSize - 2, gridSize - 2);
256
  }
257
 
258
  // Food
259
+ ctx.fillStyle = '{{ textColor | default("#1f2937") }}';
260
  ctx.fillRect(food.x * gridSize, food.y * gridSize, gridSize - 2, gridSize - 2);
261
  }
262
 
templates/index.html CHANGED
@@ -25,18 +25,44 @@
25
  height: 200%;
26
  cursor: pointer;
27
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  </style>
29
  </head>
30
  <body class="bg-gray-50 text-gray-800 h-screen overflow-hidden">
 
31
  <div id="app" v-cloak class="flex h-full">
32
  <!-- Sidebar / Config -->
33
- <div class="w-1/3 min-w-[350px] bg-white border-r border-gray-200 flex flex-col h-full z-10 shadow-lg">
34
  <div class="p-5 border-b border-gray-100 flex items-center justify-between">
35
- <h1 class="text-xl font-bold text-gray-900"><i class="fas fa-exclamation-triangle text-red-500 mr-2"></i>Error Page Studio</h1>
 
 
 
36
  <span class="text-xs bg-red-100 text-red-600 px-2 py-1 rounded-full">Beta</span>
37
  </div>
38
 
39
  <div class="flex-1 overflow-y-auto p-5 space-y-6">
 
 
 
 
 
 
 
 
40
  <!-- Theme Selection -->
41
  <div>
42
  <label class="block text-sm font-medium text-gray-700 mb-3">Theme Style</label>
@@ -111,7 +137,7 @@
111
  :key="ill.id"
112
  @click="config.illustration = ill.id"
113
  :class="{'ring-2 ring-red-500 bg-red-50': config.illustration === ill.id}"
114
- class="p-2 rounded-lg border border-gray-200 text-2xl flex items-center justify-center hover:bg-gray-50"
115
  >
116
  {{ ill.icon }}
117
  </button>
@@ -136,8 +162,12 @@
136
  <button
137
  @click="download"
138
  class="w-full flex items-center justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all"
 
 
139
  >
140
- <i class="fas fa-download mr-2"></i> Download HTML
 
 
141
  </button>
142
  </div>
143
  </div>
@@ -145,8 +175,8 @@
145
  <!-- Preview Area -->
146
  <div class="flex-1 bg-gray-100 flex flex-col relative overflow-hidden">
147
  <div class="absolute top-4 right-4 z-10 flex space-x-2 bg-white rounded-lg shadow p-1">
148
- <button @click="viewMode = 'desktop'" :class="{'text-red-500 bg-red-50': viewMode === 'desktop'}" class="p-2 rounded hover:bg-gray-50 transition-colors"><i class="fas fa-desktop"></i></button>
149
- <button @click="viewMode = 'mobile'" :class="{'text-red-500 bg-red-50': viewMode === 'mobile'}" class="p-2 rounded hover:bg-gray-50 transition-colors"><i class="fas fa-mobile-alt"></i></button>
150
  </div>
151
 
152
  <div class="flex-1 flex items-center justify-center p-8 overflow-hidden">
@@ -163,13 +193,14 @@
163
  <iframe id="preview-frame" class="w-full h-full border-none"></iframe>
164
 
165
  <!-- Loading Overlay -->
166
- <div v-if="loading" class="absolute inset-0 bg-white/50 flex items-center justify-center backdrop-blur-sm">
167
  <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-red-500"></div>
168
  </div>
169
  </div>
170
  </div>
171
  </div>
172
  </div>
 
173
 
174
  <script>
175
  const { createApp, ref, watch, onMounted } = Vue;
@@ -178,6 +209,9 @@
178
  setup() {
179
  const viewMode = ref('desktop');
180
  const loading = ref(false);
 
 
 
181
  const config = ref({
182
  theme: 'modern',
183
  title: '404',
@@ -202,19 +236,28 @@
202
 
203
  const updatePreview = async () => {
204
  loading.value = true;
 
 
205
  try {
206
  const response = await fetch('/preview', {
207
  method: 'POST',
208
  headers: { 'Content-Type': 'application/json' },
209
  body: JSON.stringify(config.value)
210
  });
 
211
  const html = await response.text();
 
 
 
 
 
212
  const frame = document.getElementById('preview-frame');
213
  if (frame) {
214
  frame.srcdoc = html;
215
  }
216
  } catch (error) {
217
  console.error('Preview error:', error);
 
218
  } finally {
219
  loading.value = false;
220
  }
@@ -226,12 +269,20 @@
226
  };
227
 
228
  const download = async () => {
 
 
229
  try {
230
  const response = await fetch('/download', {
231
  method: 'POST',
232
  headers: { 'Content-Type': 'application/json' },
233
  body: JSON.stringify(config.value)
234
  });
 
 
 
 
 
 
235
  const blob = await response.blob();
236
  const url = window.URL.createObjectURL(blob);
237
  const a = document.createElement('a');
@@ -242,7 +293,10 @@
242
  document.body.removeChild(a);
243
  } catch (error) {
244
  console.error('Download error:', error);
245
- alert('Download failed. Please try again.');
 
 
 
246
  }
247
  };
248
 
@@ -257,7 +311,8 @@
257
  viewMode,
258
  illustrations,
259
  loading,
260
- download
 
261
  };
262
  }
263
  }).mount('#app');
 
25
  height: 200%;
26
  cursor: pointer;
27
  }
28
+ /* Custom scrollbar for sidebar */
29
+ ::-webkit-scrollbar {
30
+ width: 6px;
31
+ }
32
+ ::-webkit-scrollbar-track {
33
+ background: #f1f1f1;
34
+ }
35
+ ::-webkit-scrollbar-thumb {
36
+ background: #d1d5db;
37
+ border-radius: 3px;
38
+ }
39
+ ::-webkit-scrollbar-thumb:hover {
40
+ background: #9ca3af;
41
+ }
42
  </style>
43
  </head>
44
  <body class="bg-gray-50 text-gray-800 h-screen overflow-hidden">
45
+ {% raw %}
46
  <div id="app" v-cloak class="flex h-full">
47
  <!-- Sidebar / Config -->
48
+ <div class="w-1/3 min-w-[320px] max-w-[450px] bg-white border-r border-gray-200 flex flex-col h-full z-10 shadow-lg transition-all duration-300">
49
  <div class="p-5 border-b border-gray-100 flex items-center justify-between">
50
+ <h1 class="text-xl font-bold text-gray-900 flex items-center">
51
+ <i class="fas fa-exclamation-triangle text-red-500 mr-2"></i>
52
+ <span>Error Page Studio</span>
53
+ </h1>
54
  <span class="text-xs bg-red-100 text-red-600 px-2 py-1 rounded-full">Beta</span>
55
  </div>
56
 
57
  <div class="flex-1 overflow-y-auto p-5 space-y-6">
58
+
59
+ <!-- Error Message Display -->
60
+ <div v-if="errorMessage" class="bg-red-50 text-red-700 p-3 rounded-md text-sm mb-4 border border-red-200 flex items-start">
61
+ <i class="fas fa-times-circle mt-0.5 mr-2"></i>
62
+ <div class="flex-1">{{ errorMessage }}</div>
63
+ <button @click="errorMessage = ''" class="text-red-500 hover:text-red-700"><i class="fas fa-times"></i></button>
64
+ </div>
65
+
66
  <!-- Theme Selection -->
67
  <div>
68
  <label class="block text-sm font-medium text-gray-700 mb-3">Theme Style</label>
 
137
  :key="ill.id"
138
  @click="config.illustration = ill.id"
139
  :class="{'ring-2 ring-red-500 bg-red-50': config.illustration === ill.id}"
140
+ class="p-2 rounded-lg border border-gray-200 text-2xl flex items-center justify-center hover:bg-gray-50 transition-colors"
141
  >
142
  {{ ill.icon }}
143
  </button>
 
162
  <button
163
  @click="download"
164
  class="w-full flex items-center justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-all"
165
+ :disabled="loading"
166
+ :class="{'opacity-75 cursor-not-allowed': loading}"
167
  >
168
+ <i class="fas fa-download mr-2"></i>
169
+ <span v-if="loading">Processing...</span>
170
+ <span v-else>Download HTML</span>
171
  </button>
172
  </div>
173
  </div>
 
175
  <!-- Preview Area -->
176
  <div class="flex-1 bg-gray-100 flex flex-col relative overflow-hidden">
177
  <div class="absolute top-4 right-4 z-10 flex space-x-2 bg-white rounded-lg shadow p-1">
178
+ <button @click="viewMode = 'desktop'" :class="{'text-red-500 bg-red-50': viewMode === 'desktop'}" class="p-2 rounded hover:bg-gray-50 transition-colors" title="Desktop View"><i class="fas fa-desktop"></i></button>
179
+ <button @click="viewMode = 'mobile'" :class="{'text-red-500 bg-red-50': viewMode === 'mobile'}" class="p-2 rounded hover:bg-gray-50 transition-colors" title="Mobile View"><i class="fas fa-mobile-alt"></i></button>
180
  </div>
181
 
182
  <div class="flex-1 flex items-center justify-center p-8 overflow-hidden">
 
193
  <iframe id="preview-frame" class="w-full h-full border-none"></iframe>
194
 
195
  <!-- Loading Overlay -->
196
+ <div v-if="loading" class="absolute inset-0 bg-white/50 flex items-center justify-center backdrop-blur-sm z-20">
197
  <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-red-500"></div>
198
  </div>
199
  </div>
200
  </div>
201
  </div>
202
  </div>
203
+ {% endraw %}
204
 
205
  <script>
206
  const { createApp, ref, watch, onMounted } = Vue;
 
209
  setup() {
210
  const viewMode = ref('desktop');
211
  const loading = ref(false);
212
+ const errorMessage = ref('');
213
+
214
+ // Initialize with safe defaults
215
  const config = ref({
216
  theme: 'modern',
217
  title: '404',
 
236
 
237
  const updatePreview = async () => {
238
  loading.value = true;
239
+ errorMessage.value = '';
240
+
241
  try {
242
  const response = await fetch('/preview', {
243
  method: 'POST',
244
  headers: { 'Content-Type': 'application/json' },
245
  body: JSON.stringify(config.value)
246
  });
247
+
248
  const html = await response.text();
249
+
250
+ if (!response.ok) {
251
+ throw new Error(html || 'Server returned an error');
252
+ }
253
+
254
  const frame = document.getElementById('preview-frame');
255
  if (frame) {
256
  frame.srcdoc = html;
257
  }
258
  } catch (error) {
259
  console.error('Preview error:', error);
260
+ errorMessage.value = 'Preview failed: ' + error.message;
261
  } finally {
262
  loading.value = false;
263
  }
 
269
  };
270
 
271
  const download = async () => {
272
+ loading.value = true;
273
+ errorMessage.value = '';
274
  try {
275
  const response = await fetch('/download', {
276
  method: 'POST',
277
  headers: { 'Content-Type': 'application/json' },
278
  body: JSON.stringify(config.value)
279
  });
280
+
281
+ if (!response.ok) {
282
+ const errText = await response.text();
283
+ throw new Error(errText || 'Download failed');
284
+ }
285
+
286
  const blob = await response.blob();
287
  const url = window.URL.createObjectURL(blob);
288
  const a = document.createElement('a');
 
293
  document.body.removeChild(a);
294
  } catch (error) {
295
  console.error('Download error:', error);
296
+ errorMessage.value = 'Download failed: ' + error.message;
297
+ alert('Download failed. Please check the console or try again.');
298
+ } finally {
299
+ loading.value = false;
300
  }
301
  };
302
 
 
311
  viewMode,
312
  illustrations,
313
  loading,
314
+ download,
315
+ errorMessage
316
  };
317
  }
318
  }).mount('#app');