AI 生成: 一个基于 Flask 的 Web 应用,用户输入主题后由 AI 生成结构化的登记手册,包含封面、目录、正文与附录,并支持导出为 Markdown 文件。

This commit is contained in:
admin
2026-09-30 20:41:38 +08:00
commit 1750ff3a83
9 changed files with 498 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
const topicInput = document.getElementById("topic");
const generateBtn = document.getElementById("generate-btn");
const exportBtn = document.getElementById("export-btn");
const statusEl = document.getElementById("status");
const outputEl = document.getElementById("output");
let current = { topic: "", markdown: "" };
function setStatus(text, isError = false) {
statusEl.textContent = text;
statusEl.classList.toggle("error", isError);
}
async function generate() {
const topic = topicInput.value.trim();
if (!topic) {
setStatus("请先输入主题", true);
return;
}
generateBtn.disabled = true;
exportBtn.disabled = true;
setStatus("生成中,请稍候…");
outputEl.textContent = "正在生成…";
try {
const resp = await fetch("/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ topic }),
});
const data = await resp.json();
if (!resp.ok) {
throw new Error(data.error || "生成失败");
}
current = { topic: data.topic, markdown: data.markdown };
outputEl.textContent = data.markdown;
exportBtn.disabled = false;
setStatus("生成完成");
} catch (err) {
outputEl.textContent = "生成失败";
setStatus(err.message, true);
} finally {
generateBtn.disabled = false;
}
}
async function exportMarkdown() {
if (!current.markdown) return;
try {
const resp = await fetch("/api/export", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(current),
});
if (!resp.ok) {
const data = await resp.json().catch(() => ({}));
throw new Error(data.error || "导出失败");
}
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${current.topic}-登记手册.md`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
setStatus("已开始下载");
} catch (err) {
setStatus(err.message, true);
}
}
generateBtn.addEventListener("click", generate);
exportBtn.addEventListener("click", exportMarkdown);
topicInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") generate();
});
+128
View File
@@ -0,0 +1,128 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Microsoft YaHei", sans-serif;
background: #f5f7fb;
color: #1f2937;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 32px 20px 64px;
}
.header h1 {
margin: 0 0 8px;
font-size: 28px;
}
.subtitle {
margin: 0 0 24px;
color: #6b7280;
}
.panel {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}
.panel label {
display: block;
font-weight: 600;
margin-bottom: 8px;
}
.row {
display: flex;
gap: 12px;
}
input[type="text"] {
flex: 1;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 15px;
outline: none;
}
input[type="text"]:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
button {
padding: 10px 18px;
border: none;
border-radius: 8px;
background: #2563eb;
color: #ffffff;
font-size: 15px;
cursor: pointer;
transition: background 0.2s;
}
button:hover:not(:disabled) {
background: #1d4ed8;
}
button:disabled {
background: #9ca3af;
cursor: not-allowed;
}
button.secondary {
background: #ffffff;
color: #2563eb;
border: 1px solid #2563eb;
}
button.secondary:hover:not(:disabled) {
background: #eff6ff;
}
.status {
margin: 12px 0 0;
min-height: 20px;
color: #6b7280;
font-size: 14px;
}
.status.error {
color: #dc2626;
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.panel-head h2 {
margin: 0;
font-size: 18px;
}
.output {
margin: 0;
padding: 16px;
background: #0f172a;
color: #e2e8f0;
border-radius: 8px;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
max-height: 520px;
overflow: auto;
}