Spaces:
Sleeping
Sleeping
Commit ·
a30d178
1
Parent(s): f6413ef
Fix Jinja2 error, add auto-save & JSON export, optimize Dockerfile
Browse files- Dockerfile +4 -11
- templates/builder.html +71 -3
Dockerfile
CHANGED
|
@@ -11,20 +11,13 @@ COPY . /app
|
|
| 11 |
|
| 12 |
# Create a non-root user and switch to it
|
| 13 |
RUN useradd -m -u 1000 user
|
| 14 |
-
USER user
|
| 15 |
-
ENV PATH="/home/user/.local/bin:$PATH"
|
| 16 |
-
|
| 17 |
-
# Ensure the user owns the application files
|
| 18 |
-
# (Although COPY . /app creates files owned by root, for read-only it's usually fine,
|
| 19 |
-
# but for safety and potential write needs, we should fix permissions or COPY with chown)
|
| 20 |
-
# However, standard practice in HF Spaces is often just running as user 1000.
|
| 21 |
-
# Let's use the --chown flag in COPY if possible, but standard COPY followed by chmod is also an option.
|
| 22 |
-
# The simplest way for this file:
|
| 23 |
|
| 24 |
-
|
| 25 |
RUN chown -R user:user /app
|
|
|
|
| 26 |
USER user
|
|
|
|
| 27 |
|
| 28 |
EXPOSE 7860
|
| 29 |
|
| 30 |
-
CMD ["
|
|
|
|
| 11 |
|
| 12 |
# Create a non-root user and switch to it
|
| 13 |
RUN useradd -m -u 1000 user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
# Set ownership
|
| 16 |
RUN chown -R user:user /app
|
| 17 |
+
|
| 18 |
USER user
|
| 19 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 20 |
|
| 21 |
EXPOSE 7860
|
| 22 |
|
| 23 |
+
CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
|
templates/builder.html
CHANGED
|
@@ -17,6 +17,7 @@
|
|
| 17 |
<body class="bg-gray-50 text-gray-800 h-screen flex flex-col overflow-hidden">
|
| 18 |
|
| 19 |
<div id="app" class="flex-1 flex flex-col h-full">
|
|
|
|
| 20 |
<!-- Header -->
|
| 21 |
<header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6 shrink-0 z-10">
|
| 22 |
<div class="flex items-center gap-3">
|
|
@@ -25,11 +26,22 @@
|
|
| 25 |
</div>
|
| 26 |
<h1 class="text-xl font-bold text-gray-900 tracking-tight">Interactive Checklist Pro</h1>
|
| 27 |
</div>
|
| 28 |
-
<div class="flex items-center gap-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
<button @click="downloadHTML" :disabled="isExporting" class="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-5 py-2 rounded-lg font-medium transition-colors shadow-sm disabled:opacity-50">
|
| 30 |
<i class="ph ph-download-simple" v-if="!isExporting"></i>
|
| 31 |
<i class="ph ph-spinner animate-spin" v-else></i>
|
| 32 |
-
|
| 33 |
</button>
|
| 34 |
</div>
|
| 35 |
</header>
|
|
@@ -186,9 +198,10 @@
|
|
| 186 |
</div>
|
| 187 |
</div>
|
| 188 |
</div>
|
|
|
|
| 189 |
|
| 190 |
<script>
|
| 191 |
-
const { createApp, ref, computed } = Vue;
|
| 192 |
|
| 193 |
createApp({
|
| 194 |
setup() {
|
|
@@ -217,6 +230,22 @@
|
|
| 217 |
]
|
| 218 |
});
|
| 219 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
const isExporting = ref(false);
|
| 221 |
|
| 222 |
const themeColors = [
|
|
@@ -333,4 +362,43 @@
|
|
| 333 |
}).mount('#app');
|
| 334 |
</script>
|
| 335 |
</body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
</html>
|
|
|
|
| 17 |
<body class="bg-gray-50 text-gray-800 h-screen flex flex-col overflow-hidden">
|
| 18 |
|
| 19 |
<div id="app" class="flex-1 flex flex-col h-full">
|
| 20 |
+
{% raw %}
|
| 21 |
<!-- Header -->
|
| 22 |
<header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6 shrink-0 z-10">
|
| 23 |
<div class="flex items-center gap-3">
|
|
|
|
| 26 |
</div>
|
| 27 |
<h1 class="text-xl font-bold text-gray-900 tracking-tight">Interactive Checklist Pro</h1>
|
| 28 |
</div>
|
| 29 |
+
<div class="flex items-center gap-2">
|
| 30 |
+
<div class="flex items-center gap-1 mr-2 border-r border-gray-200 pr-3">
|
| 31 |
+
<button @click="importJSON" class="text-gray-500 hover:text-indigo-600 p-2 rounded-lg transition" title="导入配置 (Import JSON)">
|
| 32 |
+
<i class="ph ph-upload-simple text-lg"></i>
|
| 33 |
+
</button>
|
| 34 |
+
<button @click="exportJSON" class="text-gray-500 hover:text-indigo-600 p-2 rounded-lg transition" title="备份配置 (Export JSON)">
|
| 35 |
+
<i class="ph ph-floppy-disk text-lg"></i>
|
| 36 |
+
</button>
|
| 37 |
+
<button @click="clearStorage" class="text-gray-400 hover:text-red-600 p-2 rounded-lg transition" title="重置 (Reset)">
|
| 38 |
+
<i class="ph ph-arrow-counter-clockwise text-lg"></i>
|
| 39 |
+
</button>
|
| 40 |
+
</div>
|
| 41 |
<button @click="downloadHTML" :disabled="isExporting" class="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-5 py-2 rounded-lg font-medium transition-colors shadow-sm disabled:opacity-50">
|
| 42 |
<i class="ph ph-download-simple" v-if="!isExporting"></i>
|
| 43 |
<i class="ph ph-spinner animate-spin" v-else></i>
|
| 44 |
+
<span v-text="isExporting ? '生成中...' : '导出单文件 HTML'"></span>
|
| 45 |
</button>
|
| 46 |
</div>
|
| 47 |
</header>
|
|
|
|
| 198 |
</div>
|
| 199 |
</div>
|
| 200 |
</div>
|
| 201 |
+
{% endraw %}
|
| 202 |
|
| 203 |
<script>
|
| 204 |
+
const { createApp, ref, computed, watch, onMounted } = Vue;
|
| 205 |
|
| 206 |
createApp({
|
| 207 |
setup() {
|
|
|
|
| 230 |
]
|
| 231 |
});
|
| 232 |
|
| 233 |
+
// Auto-save logic
|
| 234 |
+
onMounted(() => {
|
| 235 |
+
const saved = localStorage.getItem('checklist_pro_draft');
|
| 236 |
+
if (saved) {
|
| 237 |
+
try {
|
| 238 |
+
checklist.value = JSON.parse(saved);
|
| 239 |
+
} catch (e) {
|
| 240 |
+
console.error('Failed to load draft', e);
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
});
|
| 244 |
+
|
| 245 |
+
watch(checklist, (newVal) => {
|
| 246 |
+
localStorage.setItem('checklist_pro_draft', JSON.stringify(newVal));
|
| 247 |
+
}, { deep: true });
|
| 248 |
+
|
| 249 |
const isExporting = ref(false);
|
| 250 |
|
| 251 |
const themeColors = [
|
|
|
|
| 362 |
}).mount('#app');
|
| 363 |
</script>
|
| 364 |
</body>
|
| 365 |
+
</html>
|
| 366 |
+
const file = e.target.files[0];
|
| 367 |
+
if (!file) return;
|
| 368 |
+
const reader = new FileReader();
|
| 369 |
+
reader.readAsText(file, 'UTF-8');
|
| 370 |
+
reader.onload = readerEvent => {
|
| 371 |
+
try {
|
| 372 |
+
const content = readerEvent.target.result;
|
| 373 |
+
checklist.value = JSON.parse(content);
|
| 374 |
+
} catch (e) {
|
| 375 |
+
alert('Invalid JSON file');
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
}
|
| 379 |
+
input.click();
|
| 380 |
+
};
|
| 381 |
+
|
| 382 |
+
const clearStorage = () => {
|
| 383 |
+
if(confirm('确定要清空本地缓存并重置为默认值吗?此操作无法撤销。')) {
|
| 384 |
+
localStorage.removeItem('checklist_pro_draft');
|
| 385 |
+
location.reload();
|
| 386 |
+
}
|
| 387 |
+
};
|
| 388 |
+
|
| 389 |
+
return {
|
| 390 |
+
checklist,
|
| 391 |
+
themeColors,
|
| 392 |
+
currentThemeClass,
|
| 393 |
+
currentThemeTextClass,
|
| 394 |
+
totalItems,
|
| 395 |
+
addGroup, removeGroup, moveGroup,
|
| 396 |
+
addItem, removeItem, moveItem,
|
| 397 |
+
downloadHTML, isExporting,
|
| 398 |
+
exportJSON, importJSON, clearStorage
|
| 399 |
+
};
|
| 400 |
+
}
|
| 401 |
+
}).mount('#app');
|
| 402 |
+
</script>
|
| 403 |
+
</body>
|
| 404 |
</html>
|