duqing2026's picture
升级优化
d2cc112
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Error Page Studio | 404 页面设计工坊</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
[v-cloak] { display: none; }
.color-input-wrapper {
position: relative;
overflow: hidden;
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid #e5e7eb;
}
.color-input {
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
cursor: pointer;
}
/* Custom scrollbar for sidebar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
</style>
</head>
<body class="bg-gray-50 text-gray-800 h-screen overflow-hidden">
{% raw %}
<div id="app" v-cloak class="flex h-full">
<!-- Sidebar / Config -->
<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">
<div class="p-5 border-b border-gray-100 flex items-center justify-between">
<h1 class="text-xl font-bold text-gray-900 flex items-center">
<i class="fas fa-exclamation-triangle text-red-500 mr-2"></i>
<span>Error Page Studio</span>
</h1>
<span class="text-xs bg-red-100 text-red-600 px-2 py-1 rounded-full">Beta</span>
</div>
<div class="flex-1 overflow-y-auto p-5 space-y-6">
<!-- Error Message Display -->
<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">
<i class="fas fa-times-circle mt-0.5 mr-2"></i>
<div class="flex-1">{{ errorMessage }}</div>
<button @click="errorMessage = ''" class="text-red-500 hover:text-red-700"><i class="fas fa-times"></i></button>
</div>
<!-- Theme Selection -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">Theme Style</label>
<div class="grid grid-cols-3 gap-2">
<button
v-for="t in ['modern', 'retro', 'minimal']"
:key="t"
@click="config.theme = t"
:class="{'ring-2 ring-red-500 bg-red-50': config.theme === t, 'bg-gray-50 hover:bg-gray-100': config.theme !== t}"
class="p-2 rounded-lg border border-gray-200 text-sm capitalize transition-all"
>
{{ t }}
</button>
</div>
</div>
<!-- Colors -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">Colors</label>
<div class="flex space-x-6">
<div class="flex flex-col items-center">
<div class="color-input-wrapper" :style="{ backgroundColor: config.bgColor }">
<input type="color" v-model="config.bgColor" class="color-input">
</div>
<span class="text-xs mt-1 text-gray-500">Bg</span>
</div>
<div class="flex flex-col items-center">
<div class="color-input-wrapper" :style="{ backgroundColor: config.textColor }">
<input type="color" v-model="config.textColor" class="color-input">
</div>
<span class="text-xs mt-1 text-gray-500">Text</span>
</div>
<div class="flex flex-col items-center">
<div class="color-input-wrapper" :style="{ backgroundColor: config.accentColor }">
<input type="color" v-model="config.accentColor" class="color-input">
</div>
<span class="text-xs mt-1 text-gray-500">Accent</span>
</div>
</div>
</div>
<!-- Content -->
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Heading</label>
<input type="text" v-model="config.title" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-red-500 focus:border-red-500 text-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Message</label>
<textarea v-model="config.message" rows="2" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-red-500 focus:border-red-500 text-sm"></textarea>
</div>
</div>
<!-- Button -->
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Button Text</label>
<input type="text" v-model="config.buttonText" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Button Link</label>
<input type="text" v-model="config.buttonLink" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm">
</div>
</div>
<!-- Illustration -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">Illustration</label>
<div class="grid grid-cols-4 gap-2">
<button
v-for="ill in illustrations"
:key="ill.id"
@click="config.illustration = ill.id"
:class="{'ring-2 ring-red-500 bg-red-50': config.illustration === ill.id}"
class="p-2 rounded-lg border border-gray-200 text-2xl flex items-center justify-center hover:bg-gray-50 transition-colors"
>
{{ ill.icon }}
</button>
</div>
</div>
<!-- Game -->
<div class="flex items-center justify-between bg-gray-50 p-3 rounded-lg border border-gray-200">
<div>
<span class="block text-sm font-medium text-gray-900">Embedded Game</span>
<span class="block text-xs text-gray-500">Add Snake game for retention</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" v-model="config.showGame" class="sr-only peer">
<div class="w-11 h-6 bg-gray-200 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-red-500"></div>
</label>
</div>
</div>
<!-- Footer Actions -->
<div class="p-5 border-t border-gray-200 bg-gray-50">
<button
@click="download"
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"
:disabled="loading"
:class="{'opacity-75 cursor-not-allowed': loading}"
>
<i class="fas fa-download mr-2"></i>
<span v-if="loading">Processing...</span>
<span v-else>Download HTML</span>
</button>
</div>
</div>
<!-- Preview Area -->
<div class="flex-1 bg-gray-100 flex flex-col relative overflow-hidden">
<div class="absolute top-4 right-4 z-10 flex space-x-2 bg-white rounded-lg shadow p-1">
<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>
<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>
</div>
<div class="flex-1 flex items-center justify-center p-8 overflow-hidden">
<div
class="bg-white shadow-2xl transition-all duration-300 overflow-hidden relative"
:style="{
width: viewMode === 'desktop' ? '100%' : '375px',
height: viewMode === 'desktop' ? '100%' : '667px',
maxHeight: '100%',
borderRadius: viewMode === 'desktop' ? '8px' : '20px',
border: viewMode === 'mobile' ? '8px solid #333' : 'none'
}"
>
<iframe id="preview-frame" class="w-full h-full border-none"></iframe>
<!-- Loading Overlay -->
<div v-if="loading" class="absolute inset-0 bg-white/50 flex items-center justify-center backdrop-blur-sm z-20">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-red-500"></div>
</div>
</div>
</div>
</div>
</div>
{% endraw %}
<script>
const { createApp, ref, watch, onMounted } = Vue;
createApp({
setup() {
const viewMode = ref('desktop');
const loading = ref(false);
const errorMessage = ref('');
// Initialize with safe defaults
const config = ref({
theme: 'modern',
title: '404',
message: 'Oops! The page you are looking for has vanished into the void.',
buttonText: 'Back to Safety',
buttonLink: '/',
bgColor: '#ffffff',
textColor: '#1f2937',
accentColor: '#ef4444',
showGame: false,
illustration: 'ghost'
});
const illustrations = [
{ id: 'ghost', icon: '👻' },
{ id: 'robot', icon: '🤖' },
{ id: 'broken', icon: '💔' },
{ id: 'planet', icon: '🪐' }
];
let debounceTimer;
const updatePreview = async () => {
loading.value = true;
errorMessage.value = '';
try {
const response = await fetch('/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config.value)
});
const html = await response.text();
if (!response.ok) {
throw new Error(html || 'Server returned an error');
}
const frame = document.getElementById('preview-frame');
if (frame) {
frame.srcdoc = html;
}
} catch (error) {
console.error('Preview error:', error);
errorMessage.value = 'Preview failed: ' + error.message;
} finally {
loading.value = false;
}
};
const debouncedUpdate = () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(updatePreview, 500);
};
const download = async () => {
loading.value = true;
errorMessage.value = '';
try {
const response = await fetch('/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config.value)
});
if (!response.ok) {
const errText = await response.text();
throw new Error(errText || 'Download failed');
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '404.html';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
} catch (error) {
console.error('Download error:', error);
errorMessage.value = 'Download failed: ' + error.message;
alert('Download failed. Please check the console or try again.');
} finally {
loading.value = false;
}
};
watch(config, debouncedUpdate, { deep: true });
onMounted(() => {
updatePreview();
});
return {
config,
viewMode,
illustrations,
loading,
download,
errorMessage
};
}
}).mount('#app');
</script>
</body>
</html>