001project_wildgrowth/backend/public/slide-builder.html

474 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>节点内容批量录入工具 - Wild Growth</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 28px;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 14px;
}
.content {
padding: 30px;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #333;
font-size: 14px;
}
input, textarea {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus, textarea:focus {
outline: none;
border-color: #f5576c;
}
textarea {
min-height: 300px;
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
}
.button-group {
display: flex;
gap: 12px;
margin-top: 20px;
}
button {
flex: 1;
padding: 14px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(245, 87, 108, 0.4);
}
.btn-secondary {
background: #f5f5f5;
color: #333;
}
.btn-secondary:hover {
background: #e0e0e0;
}
.result {
margin-top: 25px;
padding: 20px;
border-radius: 8px;
display: none;
}
.result.success {
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.result.error {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.result pre {
margin-top: 10px;
padding: 12px;
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
overflow-x: auto;
font-size: 12px;
}
.example {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 25px;
border-left: 4px solid #f5576c;
}
.example h3 {
margin-bottom: 12px;
color: #f5576c;
font-size: 16px;
}
.example code {
display: block;
background: white;
padding: 12px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
font-size: 12px;
overflow-x: auto;
white-space: pre;
}
.info-box {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 8px;
padding: 15px;
margin-bottom: 25px;
color: #856404;
}
.info-box strong {
display: block;
margin-bottom: 8px;
}
.info-box ul {
margin-left: 20px;
margin-top: 8px;
}
.info-box li {
margin-bottom: 4px;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.tab {
padding: 12px 24px;
background: none;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: #666;
transition: all 0.3s;
}
.tab.active {
color: #f5576c;
border-bottom-color: #f5576c;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📄 节点内容批量录入工具</h1>
<p>Wild Growth - 批量创建节点幻灯片内容</p>
</div>
<div class="content">
<div class="info-box">
<strong>📋 使用说明:</strong>
<ul>
<li>1. 填写节点 IDnode_01_01</li>
<li>2. 填写 JWT Token从登录接口获取</li>
<li>3. 在 JSON 编辑器中输入幻灯片数据</li>
<li>4. 点击"提交创建"按钮</li>
<li>5. 查看创建结果</li>
</ul>
</div>
<div class="form-group">
<label for="apiUrl">API 地址</label>
<input type="text" id="apiUrl" value="http://localhost:3000" placeholder="http://localhost:3000">
</div>
<div class="form-group">
<label for="nodeId">节点 ID *</label>
<input type="text" id="nodeId" value="node_01_01" placeholder="node_01_01" required>
</div>
<div class="form-group">
<label for="token">JWT Token *</label>
<input type="text" id="token" placeholder="从登录接口获取的 token" required>
</div>
<div class="example">
<h3>📝 JSON 数据格式示例:</h3>
<code>{
"slides": [
{
"id": "node_01_01_slide_01",
"slideType": "text",
"order": 1,
"title": "痛苦的根源",
"paragraphs": [
"痛苦,往往不是来自外界,而是来自我们内心的<span class=\"highlight\">认知偏差</span>。",
"当我们认为\"应该\"发生的事情没有发生时,痛苦就产生了。",
"但真相是:世界不会按照我们的期望运转,痛苦是成长的信号。"
],
"highlightKeywords": ["认知偏差"],
"effect": "fade_in"
},
{
"id": "node_01_01_slide_02",
"slideType": "image",
"order": 2,
"title": null,
"paragraphs": [
"这张卡片没有大标题。",
"而且你会发现,为了配合视觉流,图片被安排在了文字的下方。"
],
"imageUrl": "https://example.com/image.jpg",
"imagePosition": "bottom",
"effect": "fade_in"
},
{
"id": "node_01_01_slide_03",
"slideType": "text",
"order": 3,
"title": "本节小结",
"paragraphs": [
"1. 痛苦是认知偏差的信号",
"2. 调整期望比改变现实更容易",
"3. 接受现实,才能开始成长"
],
"effect": "fade_in"
}
]
}</code>
</div>
<div class="form-group">
<label for="jsonData">幻灯片数据 (JSON) *</label>
<textarea id="jsonData" placeholder='请输入 JSON 数据...'></textarea>
</div>
<div class="button-group">
<button class="btn-secondary" onclick="loadExample()">加载示例数据</button>
<button class="btn-secondary" onclick="formatJSON()">格式化 JSON</button>
<button class="btn-primary" onclick="submitData()">提交创建</button>
</div>
<div id="result" class="result"></div>
</div>
</div>
<script>
function loadExample() {
const example = {
slides: [
{
id: "node_01_01_slide_01",
slideType: "text",
order: 1,
title: "痛苦的根源",
paragraphs: [
"痛苦,往往不是来自外界,而是来自我们内心的<span class=\"highlight\">认知偏差</span>。",
"当我们认为\"应该\"发生的事情没有发生时,痛苦就产生了。",
"但真相是:世界不会按照我们的期望运转,痛苦是成长的信号。"
],
highlightKeywords: ["认知偏差"],
effect: "fade_in"
},
{
id: "node_01_01_slide_02",
slideType: "text",
order: 2,
title: "重新定义痛苦",
paragraphs: [
"痛苦 = 现实与期望的差距",
"缩小这个差距有两种方式:",
"1. 改变现实(往往很难)",
"2. 调整期望(更容易,也更智慧)"
],
effect: "fade_in"
},
{
id: "node_01_01_slide_03",
slideType: "image",
order: 3,
title: null,
paragraphs: [
"这张卡片没有大标题。",
"而且你会发现,为了配合视觉流,图片被安排在了文字的下方。"
],
imageUrl: "https://example.com/image.jpg",
imagePosition: "bottom",
effect: "fade_in"
},
{
id: "node_01_01_slide_04",
slideType: "text",
order: 4,
title: "本节小结",
paragraphs: [
"1. 痛苦是认知偏差的信号",
"2. 调整期望比改变现实更容易",
"3. 接受现实,才能开始成长"
],
effect: "fade_in"
}
]
};
document.getElementById('jsonData').value = JSON.stringify(example, null, 2);
}
function formatJSON() {
const textarea = document.getElementById('jsonData');
try {
const data = JSON.parse(textarea.value);
textarea.value = JSON.stringify(data, null, 2);
showResult('success', 'JSON 格式化成功!');
} catch (e) {
showResult('error', 'JSON 格式错误:' + e.message);
}
}
async function submitData() {
const apiUrl = document.getElementById('apiUrl').value.trim();
const nodeId = document.getElementById('nodeId').value.trim();
const token = document.getElementById('token').value.trim();
const jsonData = document.getElementById('jsonData').value.trim();
if (!nodeId || !token || !jsonData) {
showResult('error', '请填写所有必填字段!');
return;
}
let data;
try {
data = JSON.parse(jsonData);
} catch (e) {
showResult('error', 'JSON 格式错误:' + e.message);
return;
}
// 验证数据结构
if (!data.slides || !Array.isArray(data.slides)) {
showResult('error', 'JSON 数据格式错误:必须包含 slides 数组');
return;
}
const url = `${apiUrl}/api/courses/nodes/${nodeId}/slides`;
try {
showResult('success', '正在提交...', true);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
showResult('success', `✅ 创建成功!\n\n节点 ID${result.data.node_id}\n幻灯片数:${result.data.slides_created}`, false, result);
} else {
showResult('error', `❌ 创建失败:${result.message || '未知错误'}`, false, result);
}
} catch (error) {
showResult('error', `❌ 请求失败:${error.message}`);
}
}
function showResult(type, message, loading = false, data = null) {
const resultDiv = document.getElementById('result');
resultDiv.className = `result ${type}`;
resultDiv.style.display = 'block';
let content = `<strong>${message}</strong>`;
if (data) {
content += `<pre>${JSON.stringify(data, null, 2)}</pre>`;
}
resultDiv.innerHTML = content;
if (!loading) {
resultDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
// 页面加载时自动加载示例数据
window.onload = function() {
loadExample();
};
</script>
</body>
</html>