1768 lines
47 KiB
Vue
1768 lines
47 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="ai-sidebar-wrapper">
|
|||
|
|
<!-- 侧边栏切换按钮 - 放在侧边栏外部 -->
|
|||
|
|
<div class="sidebar-toggle"
|
|||
|
|
@click="toggleSidebar"
|
|||
|
|
:title="isCollapsed ? '展开AI助手' : '折叠AI助手'"
|
|||
|
|
:style="{ left: isCollapsed ? '10px' : '420px' }">
|
|||
|
|
<svg v-if="isCollapsed" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|||
|
|
<path d="M9 18l6-6-6-6"/>
|
|||
|
|
</svg>
|
|||
|
|
<svg v-else width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
|||
|
|
<path d="M15 18l-6-6 6-6"/>
|
|||
|
|
</svg>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="ai-sidebar" :class="{ 'sidebar-collapsed': isCollapsed }">
|
|||
|
|
|
|||
|
|
<!-- 侧边栏内容 -->
|
|||
|
|
<div class="sidebar-content" v-show="!isCollapsed">
|
|||
|
|
<div class="sidebar-header">
|
|||
|
|
<h3>🤖 AI 助手</h3>
|
|||
|
|
<p>文档转思维导图工具</p>
|
|||
|
|
<div class="collapse-hint">
|
|||
|
|
<small>💡 点击右侧按钮可折叠侧边栏</small>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- AI生成Markdown部分 -->
|
|||
|
|
<!-- 文件上传AI生成Markdown -->
|
|||
|
|
<div class="section">
|
|||
|
|
<h4>📁 生成思维导图</h4>
|
|||
|
|
<div class="input-group">
|
|||
|
|
<label>上传文件:</label>
|
|||
|
|
<div class="file-upload-area"
|
|||
|
|
@drop="handleFileDrop"
|
|||
|
|
@dragover="handleDragOver"
|
|||
|
|
@dragleave="handleDragLeave">
|
|||
|
|
<input
|
|||
|
|
type="file"
|
|||
|
|
ref="fileInput"
|
|||
|
|
@change="handleFileUpload"
|
|||
|
|
accept=".txt,.md,.doc,.docx,.pdf"
|
|||
|
|
class="file-input"
|
|||
|
|
/>
|
|||
|
|
<div class="file-upload-placeholder" :class="{ 'drag-over': isDragOver }">
|
|||
|
|
<span class="upload-icon">📎</span>
|
|||
|
|
<span class="upload-text">点击选择文件或拖拽文件到此处</span>
|
|||
|
|
<span class="upload-hint">支持 .txt, .md, .doc, .docx, .pdf 格式</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 文件信息显示 -->
|
|||
|
|
<div v-if="uploadedFile" class="file-info">
|
|||
|
|
<div class="file-details">
|
|||
|
|
<div class="file-info-left">
|
|||
|
|
<span class="file-name">📄 {{ uploadedFile.name }}</span>
|
|||
|
|
<span class="file-size">({{ formatFileSize(uploadedFile.size) }})</span>
|
|||
|
|
</div>
|
|||
|
|
<button @click="removeFile" class="btn-remove" title="删除文件">
|
|||
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|||
|
|
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6"/>
|
|||
|
|
</svg>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="button-group file-action-buttons">
|
|||
|
|
<button
|
|||
|
|
@click="generateMarkdownFromFile"
|
|||
|
|
:disabled="!uploadedFile || isGenerating"
|
|||
|
|
class="btn-primary"
|
|||
|
|
>
|
|||
|
|
<span v-if="isGenerating">AI生成中...</span>
|
|||
|
|
<span v-else>AI生成思维导图</span>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- AI生成的Markdown结果 -->
|
|||
|
|
<div class="section" v-if="markdownContent">
|
|||
|
|
<h4>📝 AI生成的Markdown结果</h4>
|
|||
|
|
<div class="input-group">
|
|||
|
|
<label>Markdown内容:</label>
|
|||
|
|
<textarea
|
|||
|
|
v-model="markdownContent"
|
|||
|
|
placeholder="AI生成的Markdown内容将显示在这里"
|
|||
|
|
rows="8"
|
|||
|
|
readonly
|
|||
|
|
class="markdown-result"
|
|||
|
|
></textarea>
|
|||
|
|
</div>
|
|||
|
|
<div class="button-group">
|
|||
|
|
<button @click="convertToJSON" :disabled="isConverting" class="btn-secondary">
|
|||
|
|
<span v-if="isConverting">转换中...</span>
|
|||
|
|
<span v-else>🔄 转换为JSON</span>
|
|||
|
|
</button>
|
|||
|
|
<button @click="clearContent" class="btn-clear">清空</button>
|
|||
|
|
<button @click="copyMarkdown" class="btn-copy">📋 复制Markdown</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 转换结果 -->
|
|||
|
|
<div class="section" v-if="convertedJSON" style="display: none;">
|
|||
|
|
<h4>📊 转换结果</h4>
|
|||
|
|
|
|||
|
|
<!-- 处理状态显示 -->
|
|||
|
|
<div v-if="isProcessing" class="processing-status">
|
|||
|
|
<div class="spinner"></div>
|
|||
|
|
<span>{{ processingMessage }}</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="result-container">
|
|||
|
|
<pre class="json-result">{{ convertedJSON }}</pre>
|
|||
|
|
<div class="button-group">
|
|||
|
|
<button @click="copyJSON" class="btn-copy">📋 复制JSON</button>
|
|||
|
|
<button @click="previewMindmap" :disabled="isProcessing" class="btn-copy">
|
|||
|
|
{{ isProcessing ? '处理中...' : '👁️ 预览' }}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 快速测试 -->
|
|||
|
|
<div class="section" style="display: none;">
|
|||
|
|
<h4>🧪 快速测试</h4>
|
|||
|
|
<div class="button-group">
|
|||
|
|
<button @click="loadTestData" class="btn-test">📊 加载测试数据</button>
|
|||
|
|
<button @click="clearAll" class="btn-clear">🗑️ 清空所有</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 历史记录 -->
|
|||
|
|
<div class="section" v-if="history.length > 0">
|
|||
|
|
<h4>📚 历史记录</h4>
|
|||
|
|
<div class="history-list">
|
|||
|
|
<div
|
|||
|
|
v-for="(item, index) in history"
|
|||
|
|
:key="index"
|
|||
|
|
class="history-item"
|
|||
|
|
@click="loadHistoryItem(item)"
|
|||
|
|
>
|
|||
|
|
<div class="history-title">{{ item.title }}</div>
|
|||
|
|
<div class="history-time">{{ formatTime(item.timestamp) }}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, reactive } from 'vue';
|
|||
|
|
import axios from 'axios';
|
|||
|
|
import { marked } from 'marked';
|
|||
|
|
|
|||
|
|
// 响应式数据
|
|||
|
|
const isCollapsed = ref(false); // 默认展开状态
|
|||
|
|
const aiPrompt = ref('');
|
|||
|
|
const markdownContent = ref('');
|
|||
|
|
const convertedJSON = ref('');
|
|||
|
|
const isGenerating = ref(false);
|
|||
|
|
const isConverting = ref(false);
|
|||
|
|
const history = ref([]);
|
|||
|
|
const isDragOver = ref(false);
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 保存用户输入状态
|
|||
|
|
const savedAiPrompt = ref('');
|
|||
|
|
const savedMarkdownContent = ref('');
|
|||
|
|
|
|||
|
|
// 添加状态管理
|
|||
|
|
const isProcessing = ref(false);
|
|||
|
|
const processingMessage = ref('');
|
|||
|
|
|
|||
|
|
// 文件上传相关
|
|||
|
|
const fileInput = ref(null);
|
|||
|
|
const uploadedFile = ref(null);
|
|||
|
|
|
|||
|
|
// 切换侧边栏
|
|||
|
|
const toggleSidebar = () => {
|
|||
|
|
isCollapsed.value = !isCollapsed.value;
|
|||
|
|
|
|||
|
|
// 发送折叠状态改变事件
|
|||
|
|
window.dispatchEvent(new CustomEvent('ai-sidebar-toggle', {
|
|||
|
|
detail: {
|
|||
|
|
isCollapsed: isCollapsed.value
|
|||
|
|
}
|
|||
|
|
}));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 文件上传处理
|
|||
|
|
const handleFileUpload = (event) => {
|
|||
|
|
const file = event.target.files[0];
|
|||
|
|
if (file) {
|
|||
|
|
uploadedFile.value = file;
|
|||
|
|
showNotification('文件上传成功!', 'success');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 拖拽文件处理
|
|||
|
|
const handleFileDrop = (event) => {
|
|||
|
|
event.preventDefault();
|
|||
|
|
isDragOver.value = false;
|
|||
|
|
|
|||
|
|
const files = event.dataTransfer.files;
|
|||
|
|
if (files.length > 0) {
|
|||
|
|
const file = files[0];
|
|||
|
|
// 检查文件类型
|
|||
|
|
const allowedTypes = ['.txt', '.md', '.doc', '.docx', '.pdf'];
|
|||
|
|
const fileExtension = '.' + file.name.split('.').pop().toLowerCase();
|
|||
|
|
|
|||
|
|
if (allowedTypes.includes(fileExtension)) {
|
|||
|
|
uploadedFile.value = file;
|
|||
|
|
showNotification('文件拖拽上传成功!', 'success');
|
|||
|
|
} else {
|
|||
|
|
showNotification('不支持的文件格式!请上传 .txt, .md, .doc, .docx, .pdf 格式的文件', 'error');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 拖拽悬停处理
|
|||
|
|
const handleDragOver = (event) => {
|
|||
|
|
event.preventDefault();
|
|||
|
|
isDragOver.value = true;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 拖拽离开处理
|
|||
|
|
const handleDragLeave = (event) => {
|
|||
|
|
event.preventDefault();
|
|||
|
|
isDragOver.value = false;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 移除文件
|
|||
|
|
const removeFile = () => {
|
|||
|
|
uploadedFile.value = null;
|
|||
|
|
|
|||
|
|
if (fileInput.value) {
|
|||
|
|
fileInput.value.value = '';
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 格式化文件大小
|
|||
|
|
const formatFileSize = (bytes) => {
|
|||
|
|
if (bytes === 0) return '0 Bytes';
|
|||
|
|
const k = 1024;
|
|||
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|||
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|||
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
// 从文件生成Markdown
|
|||
|
|
const generateMarkdownFromFile = async () => {
|
|||
|
|
if (!uploadedFile.value) {
|
|||
|
|
showNotification('请先上传文件', 'error');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isGenerating.value = true;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 读取文件内容
|
|||
|
|
const fileContent = await readFileContent(uploadedFile.value);
|
|||
|
|
console.log('📄 文件内容预览:', fileContent.substring(0, 200) + '...');
|
|||
|
|
|
|||
|
|
// 调用AI API生成Markdown
|
|||
|
|
const systemPrompt = '你是一个专业的文档分析专家。请分析上传的文档内容,生成结构化的Markdown格式思维导图。要求:1. 提取主要主题和关键概念 2. 组织成层次分明的结构 3. 使用清晰的标题和子标题 4. 保持内容的逻辑性和完整性';
|
|||
|
|
const userPrompt = `请分析以下文档内容并生成结构化Markdown:\n\n${fileContent}`;
|
|||
|
|
|
|||
|
|
console.log('🤖 系统提示词:', systemPrompt);
|
|||
|
|
console.log('👤 用户提示词预览:', userPrompt.substring(0, 200) + '...');
|
|||
|
|
|
|||
|
|
const markdown = await callAIMarkdownAPI(systemPrompt, userPrompt);
|
|||
|
|
|
|||
|
|
markdownContent.value = markdown;
|
|||
|
|
console.log('📝 生成Markdown成功!内容长度:', markdown.length, '字符');
|
|||
|
|
|
|||
|
|
// 自动转换为JSON
|
|||
|
|
await convertToJSON();
|
|||
|
|
|
|||
|
|
showNotification('AI生成Markdown成功!正在自动保存...', 'success');
|
|||
|
|
|
|||
|
|
// 延迟一下让用户看到成功消息,然后自动保存
|
|||
|
|
setTimeout(async () => {
|
|||
|
|
try {
|
|||
|
|
await previewMindmap();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('自动保存失败:', error);
|
|||
|
|
showNotification('自动保存失败,请手动点击预览按钮', 'error');
|
|||
|
|
}
|
|||
|
|
}, 1500);
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('从文件生成Markdown失败:', error);
|
|||
|
|
showNotification('生成失败: ' + error.message, 'error');
|
|||
|
|
} finally {
|
|||
|
|
isGenerating.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 读取文件内容
|
|||
|
|
const readFileContent = (file) => {
|
|||
|
|
return new Promise(async (resolve, reject) => {
|
|||
|
|
try {
|
|||
|
|
// 检查文件类型和扩展名
|
|||
|
|
const isTextFile = file.type.includes('text') ||
|
|||
|
|
file.name.endsWith('.txt') ||
|
|||
|
|
file.name.endsWith('.md');
|
|||
|
|
|
|||
|
|
const isOfficeFile = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || // .docx
|
|||
|
|
file.type === 'application/msword' || // .doc
|
|||
|
|
file.name.endsWith('.docx') ||
|
|||
|
|
file.name.endsWith('.doc');
|
|||
|
|
|
|||
|
|
const isPDFFile = file.type === 'application/pdf' ||
|
|||
|
|
file.name.endsWith('.pdf');
|
|||
|
|
|
|||
|
|
if (isTextFile) {
|
|||
|
|
// 文本文件直接读取
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
reader.onload = (e) => resolve(e.target.result);
|
|||
|
|
reader.onerror = () => reject(new Error('文本文件读取失败'));
|
|||
|
|
reader.readAsText(file);
|
|||
|
|
} else if (isOfficeFile) {
|
|||
|
|
// Office文档处理
|
|||
|
|
const content = await extractOfficeContent(file);
|
|||
|
|
resolve(content);
|
|||
|
|
} else if (isPDFFile) {
|
|||
|
|
// PDF文件处理
|
|||
|
|
const content = await extractPDFContent(file);
|
|||
|
|
resolve(content);
|
|||
|
|
} else {
|
|||
|
|
// 对于其他格式,先尝试作为文本读取
|
|||
|
|
const reader = new FileReader();
|
|||
|
|
reader.onload = (e) => resolve(e.target.result);
|
|||
|
|
reader.onerror = () => reject(new Error('文件读取失败'));
|
|||
|
|
reader.readAsText(file);
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
reject(error);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 提取Office文档内容
|
|||
|
|
const extractOfficeContent = async (file) => {
|
|||
|
|
try {
|
|||
|
|
if (file.name.endsWith('.docx')) {
|
|||
|
|
// 使用mammoth.js解析.docx文件
|
|||
|
|
const mammoth = await import('mammoth');
|
|||
|
|
const arrayBuffer = await file.arrayBuffer();
|
|||
|
|
const result = await mammoth.extractRawText({ arrayBuffer });
|
|||
|
|
return result.value;
|
|||
|
|
} else if (file.name.endsWith('.doc')) {
|
|||
|
|
// 对于.doc文件,提示用户转换为.docx格式
|
|||
|
|
throw new Error('请将.doc文件转换为.docx格式,或安装相应的解析库');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
throw new Error(`Office文档解析失败: ${error.message}`);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 提取PDF文件内容
|
|||
|
|
const extractPDFContent = async (file) => {
|
|||
|
|
try {
|
|||
|
|
// 使用pdf.js解析PDF文件
|
|||
|
|
const pdfjsLib = await import('pdfjs-dist');
|
|||
|
|
|
|||
|
|
// 设置worker路径为本地文件
|
|||
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs';
|
|||
|
|
|
|||
|
|
const arrayBuffer = await file.arrayBuffer();
|
|||
|
|
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
|||
|
|
|
|||
|
|
let fullText = '';
|
|||
|
|
for (let i = 1; i <= pdf.numPages; i++) {
|
|||
|
|
const page = await pdf.getPage(i);
|
|||
|
|
const textContent = await page.getTextContent();
|
|||
|
|
const pageText = textContent.items.map(item => item.str).join(' ');
|
|||
|
|
fullText += pageText + '\n';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return fullText;
|
|||
|
|
} catch (error) {
|
|||
|
|
throw new Error(`PDF文件解析失败: ${error.message}`);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// AI生成Markdown
|
|||
|
|
const generateMarkdown = async () => {
|
|||
|
|
if (!aiPrompt.value.trim()) {
|
|||
|
|
showNotification('请输入主题描述', 'error');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存用户输入
|
|||
|
|
savedAiPrompt.value = aiPrompt.value;
|
|||
|
|
|
|||
|
|
isGenerating.value = true;
|
|||
|
|
try {
|
|||
|
|
// 调用真实的AI API
|
|||
|
|
const markdown = await callAIMarkdownAPI(null, aiPrompt.value);
|
|||
|
|
markdownContent.value = markdown;
|
|||
|
|
savedMarkdownContent.value = markdown;
|
|||
|
|
|
|||
|
|
console.log('📝 生成Markdown成功!内容长度:', markdown.length, '字符');
|
|||
|
|
|
|||
|
|
// 自动转换为JSON
|
|||
|
|
await convertToJSON();
|
|||
|
|
|
|||
|
|
// 添加到历史记录
|
|||
|
|
addToHistory('AI生成: ' + aiPrompt.value.substring(0, 30) + '...', markdown);
|
|||
|
|
|
|||
|
|
// 确保AI提示词不被清空
|
|||
|
|
if (aiPrompt.value !== savedAiPrompt.value) {
|
|||
|
|
aiPrompt.value = savedAiPrompt.value;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showNotification('AI生成Markdown成功!正在自动保存...', 'success');
|
|||
|
|
|
|||
|
|
// 延迟一下让用户看到成功消息,然后自动保存
|
|||
|
|
setTimeout(async () => {
|
|||
|
|
try {
|
|||
|
|
await previewMindmap();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('自动保存失败:', error);
|
|||
|
|
showNotification('自动保存失败,请手动点击预览按钮', 'error');
|
|||
|
|
}
|
|||
|
|
}, 1500);
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('生成Markdown失败:', error);
|
|||
|
|
showNotification('生成失败: ' + error.message, 'error');
|
|||
|
|
} finally {
|
|||
|
|
isGenerating.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 调用AI API生成Markdown
|
|||
|
|
const callAIMarkdownAPI = async (systemPrompt, userPrompt) => {
|
|||
|
|
const defaultSystemPrompt = `你是一位Markdown格式转换专家。你的任务是将用户提供的文章内容精确转换为结构化的Markdown格式。请遵循以下步骤:
|
|||
|
|
|
|||
|
|
提取主标题: 识别文章最顶层的主标题(通常为文章题目或书名),并使用Markdown的 # 级别表示。
|
|||
|
|
|
|||
|
|
识别层级标题: 从文章内容中提取所有层级的内容标题(从主标题后的第一个标题开始,Level 1 至 Level 4)。判断层级依据:
|
|||
|
|
|
|||
|
|
视觉与结构特征: 如独立成行/段、位置(行首)、格式(加粗、编号如 1., 1.1, (1), - 等)。
|
|||
|
|
|
|||
|
|
语义逻辑: 标题之间的包含和并列关系。
|
|||
|
|
|
|||
|
|
在Markdown中,使用相应标题级别:
|
|||
|
|
|
|||
|
|
Level 1 标题用 ##
|
|||
|
|
|
|||
|
|
Level 2 标题用 ###
|
|||
|
|
|
|||
|
|
Level 3 标题用 ####
|
|||
|
|
|
|||
|
|
Level 4 标题用 #####
|
|||
|
|
|
|||
|
|
精确保留原文标题文字,不得修改、概括或润色。
|
|||
|
|
|
|||
|
|
处理正文内容: 对于每个标题下的正文内容区块(从该标题后开始,直到下一个同级或更高级别标题前):
|
|||
|
|
|
|||
|
|
直接保留原文文本,但根据内容结构适当格式化为Markdown。
|
|||
|
|
|
|||
|
|
如果内容是列表(如项目符号或编号列表),使用Markdown列表语法(例如 - 用于无序列表,1. 用于有序列表)。
|
|||
|
|
|
|||
|
|
保持段落和换行不变。
|
|||
|
|
|
|||
|
|
输出格式: 输出必须是纯Markdown格式的文本,不得包含任何额外说明、JSON或非Markdown元素。确保输出与示例风格一致。`;
|
|||
|
|
|
|||
|
|
// 如果没有提供系统提示词,使用默认的
|
|||
|
|
const finalSystemPrompt = systemPrompt || defaultSystemPrompt;
|
|||
|
|
|
|||
|
|
// 如果没有提供用户提示词,使用传入的prompt作为用户提示词
|
|||
|
|
const finalUserPrompt = userPrompt || `请将以下内容转换为结构化的Markdown格式:`;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
console.log('🚀 开始调用AI API...');
|
|||
|
|
console.log('📋 系统提示词:', finalSystemPrompt.substring(0, 100) + '...');
|
|||
|
|
console.log('👤 用户提示词:', finalUserPrompt.substring(0, 100) + '...');
|
|||
|
|
|
|||
|
|
// 显示进度提示
|
|||
|
|
showNotification('AI正在分析文档,请耐心等待(最多2分钟)...', 'info');
|
|||
|
|
|
|||
|
|
// 添加超时处理 - 增加超时时间,处理复杂文档
|
|||
|
|
const controller = new AbortController();
|
|||
|
|
const timeoutId = setTimeout(() => controller.abort(), 120000); // 增加到2分钟超时
|
|||
|
|
|
|||
|
|
const response = await fetch('http://127.0.0.1:8000/api/ai/generate-markdown', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
system_prompt: finalSystemPrompt,
|
|||
|
|
user_prompt: finalUserPrompt,
|
|||
|
|
model: "glm-4.5",
|
|||
|
|
base_url: "https://open.bigmodel.cn/api/paas/v4/",
|
|||
|
|
api_key: "ce39bdd4fcf34ec0aec75072bc9ff988.hAp7HZTVUwy7vImn"
|
|||
|
|
}),
|
|||
|
|
signal: controller.signal
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
clearTimeout(timeoutId);
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const data = await response.json();
|
|||
|
|
console.log('📡 AI API响应:', data);
|
|||
|
|
|
|||
|
|
if (data.success && data.markdown) {
|
|||
|
|
console.log('✅ 从success.markdown获取内容');
|
|||
|
|
return data.markdown;
|
|||
|
|
} else if (data.markdown) {
|
|||
|
|
console.log('✅ 从markdown字段获取内容');
|
|||
|
|
return data.markdown;
|
|||
|
|
} else if (data.content) {
|
|||
|
|
console.log('✅ 从content字段获取内容');
|
|||
|
|
return data.content;
|
|||
|
|
} else {
|
|||
|
|
console.error('❌ AI API响应格式错误:', data);
|
|||
|
|
return '生成失败,请重试';
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
if (error.name === 'AbortError') {
|
|||
|
|
throw new Error('AI API请求超时(2分钟),文档可能较复杂,请重试或尝试上传较小的文档');
|
|||
|
|
}
|
|||
|
|
console.error('AI API调用失败:', error);
|
|||
|
|
// 如果API调用失败,抛出异常
|
|||
|
|
throw new Error(`AI API调用失败: ${error.message}`);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 移除模拟数据生成函数,只使用真实的AI API
|
|||
|
|
|
|||
|
|
// 格式化Markdown为结构化文本
|
|||
|
|
const formatMarkdownToText = (markdown) => {
|
|||
|
|
return markdown
|
|||
|
|
// 处理标题
|
|||
|
|
.replace(/^### (.*$)/gim, '📋 $1') // 三级标题
|
|||
|
|
.replace(/^## (.*$)/gim, '📌 $1') // 二级标题
|
|||
|
|
.replace(/^# (.*$)/gim, '🎯 $1') // 一级标题
|
|||
|
|
// 处理粗体 - 改进处理逻辑,确保冒号等标点符号正确处理
|
|||
|
|
.replace(/\*\*(.*?)\*\*/g, (match, content) => {
|
|||
|
|
// 如果内容包含冒号,保持冒号,只处理粗体部分
|
|||
|
|
if (content.includes(':')) {
|
|||
|
|
const parts = content.split(':');
|
|||
|
|
if (parts.length > 1) {
|
|||
|
|
return `【${parts[0]}】: ${parts.slice(1).join(':')}`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return `【${content}】`;
|
|||
|
|
})
|
|||
|
|
// 处理斜体
|
|||
|
|
.replace(/\*(.*?)\*/g, '《$1》')
|
|||
|
|
// 处理列表项
|
|||
|
|
.replace(/^- (.*$)/gim, ' • $1')
|
|||
|
|
.replace(/^\d+\. (.*$)/gim, ' $&')
|
|||
|
|
// 处理代码块
|
|||
|
|
.replace(/```(.*?)```/gims, '💻 $1')
|
|||
|
|
// 处理行内代码
|
|||
|
|
.replace(/`(.*?)`/g, '「$1」')
|
|||
|
|
// 处理链接
|
|||
|
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '🔗 $1')
|
|||
|
|
// 处理换行
|
|||
|
|
.replace(/\n\n/g, '\n')
|
|||
|
|
.replace(/\n/g, '\n ');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Markdown转JSON
|
|||
|
|
const convertToJSON = async () => {
|
|||
|
|
if (!markdownContent.value.trim()) {
|
|||
|
|
showNotification('请输入Markdown内容', 'error');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isConverting.value = true;
|
|||
|
|
try {
|
|||
|
|
const json = markdownToJSON(markdownContent.value);
|
|||
|
|
convertedJSON.value = JSON.stringify(json, null, 2);
|
|||
|
|
console.log('🔄 生成JSON成功!节点数量:', countNodes(json), 'JSON长度:', convertedJSON.value.length, '字符');
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('转换失败:', error);
|
|||
|
|
showNotification('转换失败,请检查Markdown格式', 'error');
|
|||
|
|
} finally {
|
|||
|
|
isConverting.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 统计节点数量的辅助函数
|
|||
|
|
const countNodes = (node) => {
|
|||
|
|
if (!node) return 0;
|
|||
|
|
let count = 1; // 当前节点
|
|||
|
|
if (node.children && node.children.length > 0) {
|
|||
|
|
for (const child of node.children) {
|
|||
|
|
count += countNodes(child);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return count;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Markdown转JSON的核心逻辑
|
|||
|
|
const markdownToJSON = (markdown) => {
|
|||
|
|
const lines = markdown.split('\n');
|
|||
|
|
let root = null;
|
|||
|
|
const stack = [];
|
|||
|
|
let nodeCounter = 0;
|
|||
|
|
let currentContent = [];
|
|||
|
|
|
|||
|
|
lines.forEach((line, index) => {
|
|||
|
|
const trimmed = line.trim();
|
|||
|
|
|
|||
|
|
// 检测标题级别
|
|||
|
|
const match = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|||
|
|
if (match) {
|
|||
|
|
// 如果有累积的内容,先保存到当前节点
|
|||
|
|
if (currentContent.length > 0 && stack.length > 0) {
|
|||
|
|
const content = currentContent.join('\n').trim();
|
|||
|
|
if (content) {
|
|||
|
|
// 将内容直接添加到当前节点的标题中,并清理Markdown语法
|
|||
|
|
const lastNode = stack[stack.length - 1];
|
|||
|
|
const formattedContent = formatMarkdownToText(content);
|
|||
|
|
lastNode.topic = lastNode.topic + '\n\n' + formattedContent;
|
|||
|
|
}
|
|||
|
|
currentContent = [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const level = match[1].length;
|
|||
|
|
const title = match[2].trim();
|
|||
|
|
|
|||
|
|
// 清理标题中的Markdown语法
|
|||
|
|
const cleanTitle = formatMarkdownToText(title);
|
|||
|
|
|
|||
|
|
// 创建节点
|
|||
|
|
const node = {
|
|||
|
|
id: `node_${nodeCounter++}`,
|
|||
|
|
topic: cleanTitle,
|
|||
|
|
children: [],
|
|||
|
|
level: level,
|
|||
|
|
data: {}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 如果有累积的内容,将其添加到标题中
|
|||
|
|
if (currentContent.length > 0) {
|
|||
|
|
const content = currentContent.join('\n').trim();
|
|||
|
|
if (content) {
|
|||
|
|
// 格式化内容,将Markdown转换为结构化的文本
|
|||
|
|
const formattedContent = formatMarkdownToText(content);
|
|||
|
|
node.topic = cleanTitle + '\n\n' + formattedContent;
|
|||
|
|
}
|
|||
|
|
currentContent = [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果是第一个节点(最高级别),设为根节点
|
|||
|
|
if (level === 1 && !root) {
|
|||
|
|
root = node;
|
|||
|
|
stack.length = 0; // 清空栈
|
|||
|
|
stack.push(root);
|
|||
|
|
} else {
|
|||
|
|
// 找到合适的父节点
|
|||
|
|
while (stack.length > 1 && stack[stack.length - 1].level >= level) {
|
|||
|
|
stack.pop();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加到父节点
|
|||
|
|
if (stack.length > 0) {
|
|||
|
|
stack[stack.length - 1].children.push(node);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新栈
|
|||
|
|
stack.push(node);
|
|||
|
|
}
|
|||
|
|
} else if (trimmed) {
|
|||
|
|
// 非标题行,累积内容
|
|||
|
|
currentContent.push(trimmed);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检测列表项
|
|||
|
|
const listMatch = trimmed.match(/^[-*+]\s+(.+)$/);
|
|||
|
|
if (listMatch) {
|
|||
|
|
const title = listMatch[1].trim();
|
|||
|
|
// 处理列表项中的Markdown语法,确保内容可读
|
|||
|
|
const cleanTitle = formatMarkdownToText(title);
|
|||
|
|
const node = {
|
|||
|
|
id: `node_${nodeCounter++}`,
|
|||
|
|
topic: cleanTitle,
|
|||
|
|
children: [],
|
|||
|
|
level: stack.length > 0 ? stack[stack.length - 1].level + 1 : 1,
|
|||
|
|
data: {}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (stack.length > 0) {
|
|||
|
|
stack[stack.length - 1].children.push(node);
|
|||
|
|
} else if (!root) {
|
|||
|
|
// 如果没有根节点,创建一个
|
|||
|
|
root = {
|
|||
|
|
id: `node_${nodeCounter++}`,
|
|||
|
|
topic: '主题',
|
|||
|
|
children: [node],
|
|||
|
|
level: 0,
|
|||
|
|
data: {}
|
|||
|
|
};
|
|||
|
|
stack.push(root);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 处理最后的内容
|
|||
|
|
if (currentContent.length > 0 && stack.length > 0) {
|
|||
|
|
const content = currentContent.join('\n').trim();
|
|||
|
|
if (content) {
|
|||
|
|
// 将最后的内容添加到最后一个节点的标题中,并清理Markdown语法
|
|||
|
|
const lastNode = stack[stack.length - 1];
|
|||
|
|
const formattedContent = formatMarkdownToText(content);
|
|||
|
|
lastNode.topic = lastNode.topic + '\n\n' + formattedContent;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果没有找到任何内容,返回默认根节点
|
|||
|
|
if (!root) {
|
|||
|
|
root = {
|
|||
|
|
id: 'root',
|
|||
|
|
topic: '根节点',
|
|||
|
|
children: [],
|
|||
|
|
data: {}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return root;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 复制Markdown
|
|||
|
|
const copyMarkdown = async () => {
|
|||
|
|
if (!markdownContent.value) {
|
|||
|
|
showNotification('没有Markdown内容可复制', 'error');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await navigator.clipboard.writeText(markdownContent.value);
|
|||
|
|
showNotification('Markdown已复制到剪贴板', 'success');
|
|||
|
|
} catch (error) {
|
|||
|
|
// 降级方案
|
|||
|
|
const textArea = document.createElement('textarea');
|
|||
|
|
textArea.value = markdownContent.value;
|
|||
|
|
document.body.appendChild(textArea);
|
|||
|
|
textArea.select();
|
|||
|
|
document.execCommand('copy');
|
|||
|
|
textArea.remove();
|
|||
|
|
showNotification('Markdown已复制到剪贴板', 'success');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 复制JSON
|
|||
|
|
const copyJSON = async () => {
|
|||
|
|
try {
|
|||
|
|
await navigator.clipboard.writeText(convertedJSON.value);
|
|||
|
|
showNotification('JSON已复制到剪贴板', 'success');
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('复制失败:', error);
|
|||
|
|
showNotification('复制失败', 'error');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 导入到思维导图
|
|||
|
|
const importToMindmap = () => {
|
|||
|
|
if (!convertedJSON.value) {
|
|||
|
|
showNotification('请先生成或转换JSON数据', 'error');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const mindmapData = JSON.parse(convertedJSON.value);
|
|||
|
|
|
|||
|
|
// 触发事件,通知父组件导入数据
|
|||
|
|
const event = new CustomEvent('import-mindmap', {
|
|||
|
|
detail: {
|
|||
|
|
json: convertedJSON.value,
|
|||
|
|
data: mindmapData,
|
|||
|
|
title: mindmapData.topic || '导入的思维导图'
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
window.dispatchEvent(event);
|
|||
|
|
|
|||
|
|
// 显示成功通知而不是alert
|
|||
|
|
showNotification('已触发导入事件,正在处理...', 'success');
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('JSON解析失败:', error);
|
|||
|
|
showNotification('JSON格式错误,请检查数据', 'error');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 定义组件事件
|
|||
|
|
// 删除导入功能,只保留预览和保存功能
|
|||
|
|
|
|||
|
|
// 预览功能(自动保存到数据库)
|
|||
|
|
const previewMindmap = async () => {
|
|||
|
|
if (!convertedJSON.value) {
|
|||
|
|
showNotification('请先生成或转换JSON数据', 'error');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
isProcessing.value = true;
|
|||
|
|
processingMessage.value = '正在保存思维导图...';
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const mindmapData = JSON.parse(convertedJSON.value);
|
|||
|
|
|
|||
|
|
// 使用更智能的标题生成
|
|||
|
|
const title = mindmapData.topic ||
|
|||
|
|
mindmapData.title ||
|
|||
|
|
`AI生成的思维导图_${new Date().toLocaleString()}`;
|
|||
|
|
|
|||
|
|
// 触发保存到数据库事件
|
|||
|
|
const event = new CustomEvent('save-preview-to-database', {
|
|||
|
|
detail: {
|
|||
|
|
data: mindmapData,
|
|||
|
|
title: title,
|
|||
|
|
source: 'ai-generated', // 标记来源
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
window.dispatchEvent(event);
|
|||
|
|
|
|||
|
|
console.log('💾 创建思维导图成功!标题:', title, '数据大小:', JSON.stringify(mindmapData).length, '字符');
|
|||
|
|
console.log('📊 思维导图结构:', mindmapData);
|
|||
|
|
|
|||
|
|
// 延迟显示成功消息
|
|||
|
|
setTimeout(() => {
|
|||
|
|
isProcessing.value = false;
|
|||
|
|
processingMessage.value = '';
|
|||
|
|
showNotification('思维导图已保存成功!', 'success');
|
|||
|
|
|
|||
|
|
// 清空输入框和文件
|
|||
|
|
aiPrompt.value = '';
|
|||
|
|
markdownContent.value = '';
|
|||
|
|
convertedJSON.value = '';
|
|||
|
|
|
|||
|
|
uploadedFile.value = null;
|
|||
|
|
if (fileInput.value) {
|
|||
|
|
fileInput.value.value = '';
|
|||
|
|
}
|
|||
|
|
}, 2000);
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
isProcessing.value = false;
|
|||
|
|
processingMessage.value = '';
|
|||
|
|
console.error('JSON解析失败:', error);
|
|||
|
|
showNotification('JSON格式错误,请检查数据', 'error');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 显示通知的函数
|
|||
|
|
const showNotification = (message, type = 'info') => {
|
|||
|
|
const notification = document.createElement('div');
|
|||
|
|
notification.className = `notification notification-${type}`;
|
|||
|
|
notification.textContent = message;
|
|||
|
|
notification.style.cssText = `
|
|||
|
|
position: fixed;
|
|||
|
|
top: 20px;
|
|||
|
|
right: 20px;
|
|||
|
|
padding: 12px 20px;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
z-index: 10000;
|
|||
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|||
|
|
animation: slideIn 0.3s ease;
|
|||
|
|
max-width: 300px;
|
|||
|
|
word-wrap: break-word;
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
// 根据类型设置颜色
|
|||
|
|
switch (type) {
|
|||
|
|
case 'success':
|
|||
|
|
notification.style.background = '#4CAF50';
|
|||
|
|
notification.style.color = 'white';
|
|||
|
|
break;
|
|||
|
|
case 'error':
|
|||
|
|
notification.style.background = '#f44336';
|
|||
|
|
notification.style.color = 'white';
|
|||
|
|
break;
|
|||
|
|
case 'info':
|
|||
|
|
default:
|
|||
|
|
notification.style.background = '#2196F3';
|
|||
|
|
notification.style.color = 'white';
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加动画样式
|
|||
|
|
if (!document.querySelector('#notification-styles')) {
|
|||
|
|
const style = document.createElement('style');
|
|||
|
|
style.id = 'notification-styles';
|
|||
|
|
style.textContent = `
|
|||
|
|
@keyframes slideIn {
|
|||
|
|
from { transform: translateX(100%); opacity: 0; }
|
|||
|
|
to { transform: translateX(0); opacity: 1; }
|
|||
|
|
}
|
|||
|
|
@keyframes slideOut {
|
|||
|
|
from { transform: translateX(0); opacity: 1; }
|
|||
|
|
to { transform: translateX(100%); opacity: 0; }
|
|||
|
|
}
|
|||
|
|
`;
|
|||
|
|
document.head.appendChild(style);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.body.appendChild(notification);
|
|||
|
|
|
|||
|
|
// 3秒后自动移除
|
|||
|
|
setTimeout(() => {
|
|||
|
|
notification.style.animation = 'slideOut 0.3s ease';
|
|||
|
|
setTimeout(() => {
|
|||
|
|
if (notification.parentNode) {
|
|||
|
|
notification.parentNode.removeChild(notification);
|
|||
|
|
}
|
|||
|
|
}, 300);
|
|||
|
|
}, 3000);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 删除不再需要的保存函数,预览功能已包含保存
|
|||
|
|
|
|||
|
|
// 清空内容
|
|||
|
|
const clearContent = () => {
|
|||
|
|
markdownContent.value = '';
|
|||
|
|
convertedJSON.value = '';
|
|||
|
|
|
|||
|
|
// 显示成功通知
|
|||
|
|
showNotification('内容已清空', 'info');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 清空所有内容
|
|||
|
|
const clearAll = () => {
|
|||
|
|
aiPrompt.value = '';
|
|||
|
|
markdownContent.value = '';
|
|||
|
|
convertedJSON.value = '';
|
|||
|
|
|
|||
|
|
// 显示成功通知
|
|||
|
|
showNotification('所有内容已清空', 'info');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 加载测试数据
|
|||
|
|
const loadTestData = () => {
|
|||
|
|
const testJSON = {
|
|||
|
|
"id": "node_0",
|
|||
|
|
"topic": "数字教育平台设计要点",
|
|||
|
|
"children": [
|
|||
|
|
{
|
|||
|
|
"id": "node_1",
|
|||
|
|
"topic": "用户体验设计",
|
|||
|
|
"children": [
|
|||
|
|
{
|
|||
|
|
"id": "node_2",
|
|||
|
|
"topic": "界面交互设计",
|
|||
|
|
"children": [
|
|||
|
|
{
|
|||
|
|
"id": "node_3",
|
|||
|
|
"topic": "核心功能入口应控制在3次点击内可达",
|
|||
|
|
"children": []
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "node_4",
|
|||
|
|
"topic": "采用响应式设计适配多端设备",
|
|||
|
|
"children": []
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "node_5",
|
|||
|
|
"topic": "内容架构规划",
|
|||
|
|
"children": [
|
|||
|
|
{
|
|||
|
|
"id": "node_6",
|
|||
|
|
"topic": "合理的内容组织帮助用户构建知识体系",
|
|||
|
|
"children": []
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "node_7",
|
|||
|
|
"topic": "课程结构设计",
|
|||
|
|
"children": []
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "node_8",
|
|||
|
|
"topic": "互动功能开发",
|
|||
|
|
"children": [
|
|||
|
|
{
|
|||
|
|
"id": "node_9",
|
|||
|
|
"topic": "学习互动设计",
|
|||
|
|
"children": [
|
|||
|
|
{
|
|||
|
|
"id": "node_10",
|
|||
|
|
"topic": "平台应提供即时反馈机制",
|
|||
|
|
"children": []
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "node_11",
|
|||
|
|
"topic": "开发协作学习工具",
|
|||
|
|
"children": []
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "node_12",
|
|||
|
|
"topic": "数据安全保障",
|
|||
|
|
"children": [
|
|||
|
|
{
|
|||
|
|
"id": "node_13",
|
|||
|
|
"topic": "隐私保护措施",
|
|||
|
|
"children": [
|
|||
|
|
{
|
|||
|
|
"id": "node_14",
|
|||
|
|
"topic": "个人信息和学习数据采用AES-256加密存储",
|
|||
|
|
"children": []
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"id": "node_15",
|
|||
|
|
"topic": "实施基于角色的权限管理",
|
|||
|
|
"children": []
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
convertedJSON.value = JSON.stringify(testJSON, null, 2);
|
|||
|
|
markdownContent.value = `# 数字教育平台设计要点
|
|||
|
|
|
|||
|
|
## 用户体验设计
|
|||
|
|
|
|||
|
|
### 界面交互设计
|
|||
|
|
- 核心功能入口应控制在3次点击内可达
|
|||
|
|
- 采用响应式设计适配多端设备
|
|||
|
|
|
|||
|
|
### 内容架构规划
|
|||
|
|
- 合理的内容组织帮助用户构建知识体系
|
|||
|
|
- 课程结构设计
|
|||
|
|
|
|||
|
|
## 互动功能开发
|
|||
|
|
|
|||
|
|
### 学习互动设计
|
|||
|
|
- 平台应提供即时反馈机制
|
|||
|
|
- 开发协作学习工具
|
|||
|
|
|
|||
|
|
## 数据安全保障
|
|||
|
|
|
|||
|
|
### 隐私保护措施
|
|||
|
|
- 个人信息和学习数据采用AES-256加密存储
|
|||
|
|
- 实施基于角色的权限管理
|
|||
|
|
|
|||
|
|
## 测试Markdown语法处理
|
|||
|
|
|
|||
|
|
### 粗体语法测试
|
|||
|
|
- **街拍**: 抓拍方式捕捉自然瞬间
|
|||
|
|
- **人像**: 专业人像摄影技巧
|
|||
|
|
- **风景**: 自然风光拍摄要点
|
|||
|
|
|
|||
|
|
### 混合语法测试
|
|||
|
|
- **重要提示**: 这是*斜体*和**粗体**的混合内容
|
|||
|
|
- **代码示例**: 使用\`console.log\`进行调试
|
|||
|
|
- **链接测试**: [点击这里](https://example.com)查看更多信息`;
|
|||
|
|
|
|||
|
|
// 添加到历史记录(不包含思维导图ID,使用传统方式)
|
|||
|
|
addToHistory('测试数据: 数字教育平台设计要点', markdownContent.value);
|
|||
|
|
|
|||
|
|
// 显示成功通知
|
|||
|
|
showNotification('测试数据加载成功!', 'success');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 添加到历史记录
|
|||
|
|
const addToHistory = (title, content, mindmapId = null) => {
|
|||
|
|
const item = {
|
|||
|
|
title,
|
|||
|
|
content,
|
|||
|
|
mindmapId,
|
|||
|
|
timestamp: new Date()
|
|||
|
|
};
|
|||
|
|
history.value.unshift(item);
|
|||
|
|
|
|||
|
|
// 限制历史记录数量
|
|||
|
|
if (history.value.length > 10) {
|
|||
|
|
history.value = history.value.slice(0, 10);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存到本地存储
|
|||
|
|
localStorage.setItem('ai-sidebar-history', JSON.stringify(history.value));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 加载历史记录项
|
|||
|
|
const loadHistoryItem = async (item) => {
|
|||
|
|
// 如果有思维导图ID,直接调用后端接口加载
|
|||
|
|
if (item.mindmapId) {
|
|||
|
|
console.log('🎯 从历史记录加载思维导图ID:', item.mindmapId);
|
|||
|
|
|
|||
|
|
// 发送事件通知MindMap组件加载数据
|
|||
|
|
window.dispatchEvent(new CustomEvent('loadMindmapFromHistory', {
|
|||
|
|
detail: {
|
|||
|
|
mindmapId: item.mindmapId,
|
|||
|
|
title: item.title
|
|||
|
|
}
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
// 显示加载通知
|
|||
|
|
showNotification(`正在加载: ${item.title}`, 'info');
|
|||
|
|
} else {
|
|||
|
|
// 如果没有ID,使用传统方式加载内容
|
|||
|
|
markdownContent.value = item.content;
|
|||
|
|
|
|||
|
|
// 自动转换为JSON
|
|||
|
|
await convertToJSON();
|
|||
|
|
|
|||
|
|
// 发送事件通知MindMap组件加载数据
|
|||
|
|
window.dispatchEvent(new CustomEvent('loadMindmapFromHistory', {
|
|||
|
|
detail: {
|
|||
|
|
markdown: item.content,
|
|||
|
|
json: convertedJSON.value,
|
|||
|
|
title: item.title
|
|||
|
|
}
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
// 显示加载通知
|
|||
|
|
showNotification(`正在加载: ${item.title}`, 'info');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 格式化时间
|
|||
|
|
const formatTime = (timestamp) => {
|
|||
|
|
const date = new Date(timestamp);
|
|||
|
|
return date.toLocaleString('zh-CN');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 组件挂载时加载历史记录
|
|||
|
|
import { onMounted, watch } from 'vue';
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
const savedHistory = localStorage.getItem('ai-sidebar-history');
|
|||
|
|
if (savedHistory) {
|
|||
|
|
try {
|
|||
|
|
history.value = JSON.parse(savedHistory);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加载历史记录失败:', error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 监听添加历史记录事件
|
|||
|
|
window.addEventListener('add-to-history', (event) => {
|
|||
|
|
console.log('📝 收到添加历史记录事件:', event.detail);
|
|||
|
|
const { title, content, timestamp } = event.detail;
|
|||
|
|
|
|||
|
|
// 添加历史记录
|
|||
|
|
addToHistory(title, content, null);
|
|||
|
|
console.log('✅ 已添加历史记录:', title);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 监听思维导图保存成功事件,更新历史记录
|
|||
|
|
window.addEventListener('mindmap-saved', (event) => {
|
|||
|
|
console.log('🎯 收到思维导图保存成功事件:', event.detail);
|
|||
|
|
const { mindmapId, title, timestamp } = event.detail;
|
|||
|
|
|
|||
|
|
// 查找对应的历史记录并更新思维导图ID
|
|||
|
|
const historyItem = history.value.find(item =>
|
|||
|
|
item.title === title ||
|
|||
|
|
(item.timestamp && Math.abs(item.timestamp - timestamp) < 5000) // 5秒内的记录
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (historyItem) {
|
|||
|
|
historyItem.mindmapId = mindmapId;
|
|||
|
|
console.log('✅ 已更新历史记录,思维导图ID:', mindmapId);
|
|||
|
|
|
|||
|
|
// 保存到本地存储
|
|||
|
|
localStorage.setItem('ai-sidebar-history', JSON.stringify(history.value));
|
|||
|
|
} else {
|
|||
|
|
console.log('⚠️ 未找到对应的历史记录,创建新的');
|
|||
|
|
// 如果没有找到对应的记录,创建一个新的
|
|||
|
|
addToHistory(title, '', mindmapId);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 监听AI提示词的变化
|
|||
|
|
watch(aiPrompt, (newVal, oldVal) => {
|
|||
|
|
// AI提示词变化监听
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 监听Markdown内容的变化
|
|||
|
|
watch(markdownContent, (newVal, oldVal) => {
|
|||
|
|
// Markdown内容变化监听
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.ai-sidebar-wrapper {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
z-index: 1000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.ai-sidebar {
|
|||
|
|
position: relative;
|
|||
|
|
width: 420px;
|
|||
|
|
height: 100vh;
|
|||
|
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|||
|
|
color: #37352f;
|
|||
|
|
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|||
|
|
box-shadow: none;
|
|||
|
|
overflow: visible;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.ai-sidebar::before {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
right: 0;
|
|||
|
|
bottom: 0;
|
|||
|
|
background:
|
|||
|
|
radial-gradient(circle at 20% 80%, rgba(102, 8, 116, 0.1) 0%, transparent 50%),
|
|||
|
|
radial-gradient(circle at 80% 20%, rgba(102, 8, 116, 0.1) 0%, transparent 50%),
|
|||
|
|
radial-gradient(circle at 40% 40%, rgba(102, 8, 116, 0.05) 0%, transparent 50%);
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-collapsed {
|
|||
|
|
transform: translateX(-420px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-toggle {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 20px;
|
|||
|
|
width: 45px;
|
|||
|
|
height: 45px;
|
|||
|
|
background: #660874;
|
|||
|
|
border-radius: 0 8px 8px 0;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
cursor: pointer;
|
|||
|
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2), 0 0 20px rgba(102, 8, 116, 0.3);
|
|||
|
|
color: white;
|
|||
|
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|||
|
|
font-weight: bold;
|
|||
|
|
z-index: 100000;
|
|||
|
|
border: 2px solid #660874;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-toggle:hover {
|
|||
|
|
background: #5a0666;
|
|||
|
|
transform: scale(1.1);
|
|||
|
|
color: white;
|
|||
|
|
box-shadow: 3px 0 15px rgba(0, 0, 0, 0.15);
|
|||
|
|
border-color: #5a0666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-content {
|
|||
|
|
height: 100%;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
padding: 20px;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 10;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-header {
|
|||
|
|
text-align: center;
|
|||
|
|
margin-bottom: 35px;
|
|||
|
|
padding-bottom: 25px;
|
|||
|
|
border-bottom: 1px solid rgba(102, 8, 116, 0.1);
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 10;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-header h3 {
|
|||
|
|
margin: 0 0 15px 0;
|
|||
|
|
font-size: 28px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #000000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-header p {
|
|||
|
|
margin: 0;
|
|||
|
|
color: #333333;
|
|||
|
|
font-size: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collapse-hint {
|
|||
|
|
margin-top: 10px;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collapse-hint small {
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 12px;
|
|||
|
|
opacity: 0.8;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.section {
|
|||
|
|
margin-bottom: 35px;
|
|||
|
|
background: rgba(255, 255, 255, 0.95);
|
|||
|
|
border-radius: 12px;
|
|||
|
|
padding: 25px;
|
|||
|
|
backdrop-filter: blur(10px);
|
|||
|
|
border: 2px solid rgba(102, 8, 116, 0.2);
|
|||
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 10;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.section h4 {
|
|||
|
|
margin: 0 0 20px 0;
|
|||
|
|
font-size: 20px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
color: #000000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.input-group {
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.input-group label {
|
|||
|
|
display: block;
|
|||
|
|
margin-bottom: 10px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
font-size: 16px;
|
|||
|
|
color: #000000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.input-group textarea {
|
|||
|
|
width: 100%;
|
|||
|
|
padding: 12px;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: rgba(255, 255, 255, 0.9);
|
|||
|
|
color: #333;
|
|||
|
|
font-size: 14px;
|
|||
|
|
resize: vertical;
|
|||
|
|
min-height: 80px;
|
|||
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.input-group textarea:focus {
|
|||
|
|
outline: none;
|
|||
|
|
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.button-group {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 10px;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 为保存按钮组添加顶部间距 */
|
|||
|
|
.save-button-group {
|
|||
|
|
margin-top: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-primary, .btn-secondary, .btn-clear, .btn-copy, .btn-import, .btn-preview, .btn-test, .btn-save {
|
|||
|
|
padding: 12px 18px;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-primary {
|
|||
|
|
background: #660874;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-primary:hover:not(:disabled) {
|
|||
|
|
background: #5a0666;
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-secondary {
|
|||
|
|
background: #2196F3;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-secondary:hover:not(:disabled) {
|
|||
|
|
background: #1976D2;
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-clear {
|
|||
|
|
background: #f44336;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-clear:hover {
|
|||
|
|
background: #d32f2f;
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-copy {
|
|||
|
|
background: #FF9800;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-copy:hover {
|
|||
|
|
background: #F57C00;
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-import {
|
|||
|
|
background: #9C27B0;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-import:hover {
|
|||
|
|
background: #7B1FA2;
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-preview {
|
|||
|
|
background: #00BCD4;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-preview:hover {
|
|||
|
|
background: #0097A7;
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-test {
|
|||
|
|
background: #FF5722;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 文件上传样式 */
|
|||
|
|
.file-upload-area {
|
|||
|
|
position: relative;
|
|||
|
|
border: 2px dashed rgba(102, 8, 116, 0.5);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 20px;
|
|||
|
|
text-align: center;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
cursor: pointer;
|
|||
|
|
background: rgba(255, 255, 255, 0.9);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-upload-area:hover {
|
|||
|
|
border-color: rgba(102, 8, 116, 0.5);
|
|||
|
|
background: rgba(255, 255, 255, 0.8);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-upload-area.drag-over {
|
|||
|
|
border-color: rgba(102, 8, 116, 0.8);
|
|||
|
|
background: rgba(102, 8, 116, 0.1);
|
|||
|
|
transform: scale(1.02);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-input {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0;
|
|||
|
|
left: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
opacity: 0;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-upload-placeholder {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-icon {
|
|||
|
|
font-size: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-text {
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
color: #000000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.upload-hint {
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #333333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-info {
|
|||
|
|
margin-top: 20px;
|
|||
|
|
padding: 16px;
|
|||
|
|
background: rgba(255, 255, 255, 0.9);
|
|||
|
|
border-radius: 12px;
|
|||
|
|
border: 1px solid rgba(102, 8, 116, 0.15);
|
|||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-details {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-info-left {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 4px;
|
|||
|
|
flex: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-name {
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #37352f;
|
|||
|
|
font-size: 16px;
|
|||
|
|
line-height: 1.4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.file-size {
|
|||
|
|
color: #6b7280;
|
|||
|
|
font-size: 13px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-remove {
|
|||
|
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 8px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
min-width: 32px;
|
|||
|
|
height: 32px;
|
|||
|
|
box-shadow: 0 2px 4px rgba(255, 107, 107, 0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-remove:hover {
|
|||
|
|
background: linear-gradient(135deg, #ff5252 0%, #d32f2f 100%);
|
|||
|
|
transform: translateY(-1px);
|
|||
|
|
box-shadow: 0 4px 8px rgba(255, 107, 107, 0.4);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-remove:active {
|
|||
|
|
transform: translateY(0);
|
|||
|
|
box-shadow: 0 2px 4px rgba(255, 107, 107, 0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 文件操作按钮组特殊样式 */
|
|||
|
|
.file-action-buttons {
|
|||
|
|
margin-top: 20px;
|
|||
|
|
padding-top: 20px;
|
|||
|
|
border-top: 1px solid rgba(102, 8, 116, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-test:hover {
|
|||
|
|
background: #E64A19;
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-save {
|
|||
|
|
background: #660874;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-save:hover {
|
|||
|
|
background: #5a0666;
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
button:disabled {
|
|||
|
|
opacity: 0.6;
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
transform: none !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.result-container {
|
|||
|
|
background: rgba(255, 255, 255, 0.8);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 18px;
|
|||
|
|
margin-top: 15px;
|
|||
|
|
border: 1px solid rgba(102, 8, 116, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.markdown-result {
|
|||
|
|
background: rgba(255, 255, 255, 0.95);
|
|||
|
|
border: 1px solid rgba(102, 8, 116, 0.15);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 15px;
|
|||
|
|
margin: 0 0 20px 0;
|
|||
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|||
|
|
font-size: 14px;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
color: #333;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
white-space: pre-wrap;
|
|||
|
|
word-break: break-word;
|
|||
|
|
max-height: 300px;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
resize: vertical;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.json-result {
|
|||
|
|
background: rgba(102, 8, 116, 0.1);
|
|||
|
|
border-radius: 6px;
|
|||
|
|
padding: 15px;
|
|||
|
|
margin: 0 0 20px 0;
|
|||
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #37352f;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
white-space: pre-wrap;
|
|||
|
|
word-break: break-all;
|
|||
|
|
max-height: 200px;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-list {
|
|||
|
|
max-height: 200px;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
overflow-x: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-item {
|
|||
|
|
background: rgba(255, 255, 255, 0.8);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 12px;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
border: 1px solid rgba(102, 8, 116, 0.1);
|
|||
|
|
overflow: hidden;
|
|||
|
|
word-wrap: break-word;
|
|||
|
|
word-break: break-word;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-item:hover {
|
|||
|
|
background: rgba(255, 255, 255, 0.95);
|
|||
|
|
transform: translateX(5px);
|
|||
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-title {
|
|||
|
|
font-weight: 500;
|
|||
|
|
margin-bottom: 6px;
|
|||
|
|
font-size: 16px;
|
|||
|
|
color: #37352f;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.history-time {
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #6b7280;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 处理状态样式 */
|
|||
|
|
.processing-status {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
padding: 15px;
|
|||
|
|
background: rgba(102, 8, 116, 0.1);
|
|||
|
|
border: 1px solid rgba(102, 8, 116, 0.3);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
margin: 20px 0;
|
|||
|
|
color: #660874;
|
|||
|
|
font-size: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.spinner {
|
|||
|
|
width: 20px;
|
|||
|
|
height: 20px;
|
|||
|
|
border: 2px solid #f3f3f3;
|
|||
|
|
border-top: 2px solid #660874;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
animation: spin 1s linear infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes spin {
|
|||
|
|
0% { transform: rotate(0deg); }
|
|||
|
|
100% { transform: rotate(360deg); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 禁用状态的按钮样式 */
|
|||
|
|
.btn-preview:disabled {
|
|||
|
|
opacity: 0.6;
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
background: rgba(255, 255, 255, 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 滚动条样式 */
|
|||
|
|
.sidebar-content::-webkit-scrollbar,
|
|||
|
|
.json-result::-webkit-scrollbar,
|
|||
|
|
.history-list::-webkit-scrollbar {
|
|||
|
|
width: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-content::-webkit-scrollbar-track,
|
|||
|
|
.json-result::-webkit-scrollbar-track,
|
|||
|
|
.history-list::-webkit-scrollbar-track {
|
|||
|
|
background: rgba(102, 8, 116, 0.1);
|
|||
|
|
border-radius: 3px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-content::-webkit-scrollbar-thumb,
|
|||
|
|
.json-result::-webkit-scrollbar-thumb,
|
|||
|
|
.history-list::-webkit-scrollbar-thumb {
|
|||
|
|
background: rgba(102, 8, 116, 0.3);
|
|||
|
|
border-radius: 3px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-content::-webkit-scrollbar-thumb:hover,
|
|||
|
|
.json-result::-webkit-scrollbar-thumb:hover,
|
|||
|
|
.history-list::-webkit-scrollbar-thumb:hover {
|
|||
|
|
background: rgba(102, 8, 116, 0.5);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 响应式设计 */
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
.ai-sidebar {
|
|||
|
|
width: 300px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sidebar-content {
|
|||
|
|
padding: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.section {
|
|||
|
|
padding: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.button-group {
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.btn-primary, .btn-secondary, .btn-clear, .btn-copy, .btn-import {
|
|||
|
|
width: 100%;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|