feat: 完善AI询问功能 - 优化UI样式和交互体验
- 修复AI节点内容显示问题,确保完整回答内容正确显示 - 优化Markdown转JSON逻辑,改进列表项处理 - 增加AI API调用重试机制,解决内容截断问题 - 实现Ask AI功能:为每个节点添加AI询问能力 - 优化AI输入框样式:扩展宽度、居中按钮、支持回车键提交 - 为Ask AI菜单项添加特殊渐变样式,提升视觉识别度 - 简化菜单项文本和悬停提示 - 修复思维导图显示和菜单同步问题
This commit is contained in:
parent
5b73857835
commit
0a64d46ea3
Binary file not shown.
|
|
@ -50,7 +50,7 @@ def call_ai_api(system_prompt, user_prompt, model="glm-4.5", base_url="https://o
|
||||||
model=model,
|
model=model,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
temperature=0.7,
|
temperature=0.7,
|
||||||
max_tokens=2000,
|
max_tokens=8000, # 增加token限制,避免内容截断
|
||||||
stream=False
|
stream=False
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -77,15 +77,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- AI生成的Markdown结果 -->
|
<!-- AI生成的Markdown结果 - 固定显示区域 -->
|
||||||
<div class="section" v-if="markdownContent">
|
<div class="section">
|
||||||
<h4>📝 AI生成的Markdown结果</h4>
|
<h4>📝 AI生成的Markdown结果</h4>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label>Markdown内容:</label>
|
<label>Markdown内容:</label>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="markdownContent"
|
v-model="markdownContent"
|
||||||
placeholder="AI生成的Markdown内容将显示在这里"
|
placeholder="AI生成的Markdown内容将显示在这里"
|
||||||
rows="8"
|
rows="6"
|
||||||
readonly
|
readonly
|
||||||
class="markdown-result"
|
class="markdown-result"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
@ -100,9 +100,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 转换结果 -->
|
<!-- Markdown转JSON结果 - 固定显示区域 -->
|
||||||
<div class="section" v-if="convertedJSON" style="display: none;">
|
<div class="section">
|
||||||
<h4>📊 转换结果</h4>
|
<h4>📊 Markdown转JSON结果</h4>
|
||||||
|
|
||||||
<!-- 处理状态显示 -->
|
<!-- 处理状态显示 -->
|
||||||
<div v-if="isProcessing" class="processing-status">
|
<div v-if="isProcessing" class="processing-status">
|
||||||
|
|
@ -111,7 +111,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="result-container">
|
<div class="result-container">
|
||||||
<pre class="json-result">{{ convertedJSON }}</pre>
|
<pre class="json-result">{{ convertedJSON || 'JSON转换结果将显示在这里' }}</pre>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button @click="copyJSON" class="btn-copy">📋 复制JSON</button>
|
<button @click="copyJSON" class="btn-copy">📋 复制JSON</button>
|
||||||
<button @click="previewMindmap" :disabled="isProcessing" class="btn-copy">
|
<button @click="previewMindmap" :disabled="isProcessing" class="btn-copy">
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 快速测试 -->
|
<!-- 快速测试 -->
|
||||||
<div class="section" style="display: none;">
|
<div class="section">
|
||||||
<h4>🧪 快速测试</h4>
|
<h4>🧪 快速测试</h4>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button @click="loadTestData" class="btn-test">📊 加载测试数据</button>
|
<button @click="loadTestData" class="btn-test">📊 加载测试数据</button>
|
||||||
|
|
@ -440,7 +440,7 @@ const generateMarkdown = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// 调用AI API生成Markdown
|
// 调用AI API生成Markdown
|
||||||
const callAIMarkdownAPI = async (systemPrompt, userPrompt) => {
|
const callAIMarkdownAPI = async (systemPrompt, userPrompt, retryCount = 0) => {
|
||||||
const defaultSystemPrompt = `你是一位Markdown格式转换专家。你的任务是将用户提供的文章内容精确转换为结构化的Markdown格式。请遵循以下步骤:
|
const defaultSystemPrompt = `你是一位Markdown格式转换专家。你的任务是将用户提供的文章内容精确转换为结构化的Markdown格式。请遵循以下步骤:
|
||||||
|
|
||||||
提取主标题: 识别文章最顶层的主标题(通常为文章题目或书名),并使用Markdown的 # 级别表示。
|
提取主标题: 识别文章最顶层的主标题(通常为文章题目或书名),并使用Markdown的 # 级别表示。
|
||||||
|
|
@ -515,19 +515,52 @@ Level 4 标题用 #####
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('📡 AI API响应:', data);
|
console.log('📡 AI API响应:', data);
|
||||||
|
|
||||||
|
let markdownContent = '';
|
||||||
if (data.success && data.markdown) {
|
if (data.success && data.markdown) {
|
||||||
console.log('✅ 从success.markdown获取内容');
|
console.log('✅ 从success.markdown获取内容');
|
||||||
return data.markdown;
|
markdownContent = data.markdown;
|
||||||
} else if (data.markdown) {
|
} else if (data.markdown) {
|
||||||
console.log('✅ 从markdown字段获取内容');
|
console.log('✅ 从markdown字段获取内容');
|
||||||
return data.markdown;
|
markdownContent = data.markdown;
|
||||||
} else if (data.content) {
|
} else if (data.content) {
|
||||||
console.log('✅ 从content字段获取内容');
|
console.log('✅ 从content字段获取内容');
|
||||||
return data.content;
|
markdownContent = data.content;
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ AI API响应格式错误:', data);
|
console.error('❌ AI API响应格式错误:', data);
|
||||||
return '生成失败,请重试';
|
return '生成失败,请重试';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查内容是否完整
|
||||||
|
console.log('📝 获取到的Markdown内容长度:', markdownContent.length);
|
||||||
|
console.log('📝 内容预览:', markdownContent.substring(0, 200) + '...');
|
||||||
|
console.log('📝 内容结尾:', '...' + markdownContent.substring(markdownContent.length - 100));
|
||||||
|
|
||||||
|
// 检查是否以不完整的句子结尾
|
||||||
|
const isTruncated = markdownContent.trim().endsWith('-') ||
|
||||||
|
markdownContent.trim().endsWith('•') ||
|
||||||
|
markdownContent.trim().endsWith('*') ||
|
||||||
|
markdownContent.trim().endsWith('**') ||
|
||||||
|
markdownContent.trim().endsWith('###') ||
|
||||||
|
markdownContent.trim().endsWith('##') ||
|
||||||
|
markdownContent.trim().endsWith('#') ||
|
||||||
|
markdownContent.trim().endsWith('时') ||
|
||||||
|
markdownContent.trim().endsWith('的') ||
|
||||||
|
markdownContent.trim().endsWith('和') ||
|
||||||
|
markdownContent.trim().endsWith('或');
|
||||||
|
|
||||||
|
if (isTruncated && retryCount < 2) {
|
||||||
|
console.warn(`⚠️ 检测到内容可能被截断,进行第${retryCount + 1}次重试`);
|
||||||
|
showNotification(`检测到内容可能被截断,正在重试...(${retryCount + 1}/2)`, 'warning');
|
||||||
|
|
||||||
|
// 等待1秒后重试
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
return await callAIMarkdownAPI(systemPrompt, userPrompt, retryCount + 1);
|
||||||
|
} else if (isTruncated) {
|
||||||
|
console.warn('⚠️ 内容可能被截断,但已达到最大重试次数');
|
||||||
|
showNotification('内容可能被截断,请尝试上传较小的文档或稍后重试', 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdownContent;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === 'AbortError') {
|
||||||
throw new Error('AI API请求超时(2分钟),文档可能较复杂,请重试或尝试上传较小的文档');
|
throw new Error('AI API请求超时(2分钟),文档可能较复杂,请重试或尝试上传较小的文档');
|
||||||
|
|
@ -606,7 +639,7 @@ const countNodes = (node) => {
|
||||||
return count;
|
return count;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Markdown转JSON的核心逻辑
|
// Markdown转JSON的核心逻辑 - 智能层次化版本
|
||||||
const markdownToJSON = (markdown) => {
|
const markdownToJSON = (markdown) => {
|
||||||
const lines = markdown.split('\n');
|
const lines = markdown.split('\n');
|
||||||
let root = null;
|
let root = null;
|
||||||
|
|
@ -624,10 +657,9 @@ const markdownToJSON = (markdown) => {
|
||||||
if (currentContent.length > 0 && stack.length > 0) {
|
if (currentContent.length > 0 && stack.length > 0) {
|
||||||
const content = currentContent.join('\n').trim();
|
const content = currentContent.join('\n').trim();
|
||||||
if (content) {
|
if (content) {
|
||||||
// 将内容直接添加到当前节点的标题中,并清理Markdown语法
|
// 智能处理内容:检测是否需要创建子节点
|
||||||
const lastNode = stack[stack.length - 1];
|
const processedContent = processContentIntelligently(content, stack[stack.length - 1], nodeCounter);
|
||||||
const formattedContent = formatMarkdownToText(content);
|
nodeCounter = processedContent.nodeCounter;
|
||||||
lastNode.topic = lastNode.topic + '\n\n' + formattedContent;
|
|
||||||
}
|
}
|
||||||
currentContent = [];
|
currentContent = [];
|
||||||
}
|
}
|
||||||
|
|
@ -647,17 +679,6 @@ const markdownToJSON = (markdown) => {
|
||||||
data: {}
|
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) {
|
if (level === 1 && !root) {
|
||||||
root = node;
|
root = node;
|
||||||
|
|
@ -678,48 +699,17 @@ const markdownToJSON = (markdown) => {
|
||||||
stack.push(node);
|
stack.push(node);
|
||||||
}
|
}
|
||||||
} else if (trimmed) {
|
} else if (trimmed) {
|
||||||
// 非标题行,累积内容
|
// 累积内容行
|
||||||
currentContent.push(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) {
|
if (currentContent.length > 0 && stack.length > 0) {
|
||||||
const content = currentContent.join('\n').trim();
|
const content = currentContent.join('\n').trim();
|
||||||
if (content) {
|
if (content) {
|
||||||
// 将最后的内容添加到最后一个节点的标题中,并清理Markdown语法
|
const processedContent = processContentIntelligently(content, stack[stack.length - 1], nodeCounter);
|
||||||
const lastNode = stack[stack.length - 1];
|
nodeCounter = processedContent.nodeCounter;
|
||||||
const formattedContent = formatMarkdownToText(content);
|
|
||||||
lastNode.topic = lastNode.topic + '\n\n' + formattedContent;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -736,6 +726,80 @@ const markdownToJSON = (markdown) => {
|
||||||
return root;
|
return root;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 智能处理内容,检测是否需要创建子节点
|
||||||
|
const processContentIntelligently = (content, parentNode, nodeCounter) => {
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let currentNodeCounter = nodeCounter;
|
||||||
|
let remainingContent = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmed = line.trim();
|
||||||
|
|
||||||
|
// 检测是否是【】标记的叶节点
|
||||||
|
const leafMatch = trimmed.match(/^[-*+]\s*【(.+)】/);
|
||||||
|
if (leafMatch) {
|
||||||
|
// 创建叶节点
|
||||||
|
const leafTitle = leafMatch[1].trim();
|
||||||
|
const leafNode = {
|
||||||
|
id: `node_${currentNodeCounter++}`,
|
||||||
|
topic: leafTitle,
|
||||||
|
children: [],
|
||||||
|
level: (parentNode.level || 0) + 1,
|
||||||
|
data: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 收集这个叶节点的所有子项内容
|
||||||
|
let leafContent = [];
|
||||||
|
let j = i + 1;
|
||||||
|
while (j < lines.length) {
|
||||||
|
const nextLine = lines[j].trim();
|
||||||
|
const nextLeafMatch = nextLine.match(/^[-*+]\s*【(.+)】/);
|
||||||
|
|
||||||
|
if (nextLeafMatch) {
|
||||||
|
// 遇到下一个【】标记,停止收集
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集子项内容
|
||||||
|
const listMatch = nextLine.match(/^[-*+]\s+(.+)$/);
|
||||||
|
if (listMatch) {
|
||||||
|
const listItem = listMatch[1].trim();
|
||||||
|
const cleanListItem = formatMarkdownToText(listItem);
|
||||||
|
leafContent.push('• ' + cleanListItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有子项内容,添加到叶节点
|
||||||
|
if (leafContent.length > 0) {
|
||||||
|
const formattedContent = formatMarkdownToText(leafContent.join('\n'));
|
||||||
|
leafNode.topic = leafNode.topic + '\n\n' + formattedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
parentNode.children.push(leafNode);
|
||||||
|
|
||||||
|
// 跳过已处理的行
|
||||||
|
i = j - 1;
|
||||||
|
} else if (trimmed) {
|
||||||
|
// 普通内容,累积到剩余内容中
|
||||||
|
remainingContent.push(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有剩余内容,添加到父节点
|
||||||
|
if (remainingContent.length > 0) {
|
||||||
|
const finalContent = remainingContent.join('\n').trim();
|
||||||
|
if (finalContent) {
|
||||||
|
const formattedContent = formatMarkdownToText(finalContent);
|
||||||
|
parentNode.topic = parentNode.topic + '\n\n' + formattedContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { nodeCounter: currentNodeCounter };
|
||||||
|
};
|
||||||
|
|
||||||
// 复制Markdown
|
// 复制Markdown
|
||||||
const copyMarkdown = async () => {
|
const copyMarkdown = async () => {
|
||||||
if (!markdownContent.value) {
|
if (!markdownContent.value) {
|
||||||
|
|
@ -838,11 +902,13 @@ const previewMindmap = async () => {
|
||||||
processingMessage.value = '';
|
processingMessage.value = '';
|
||||||
showNotification('思维导图已保存成功!', 'success');
|
showNotification('思维导图已保存成功!', 'success');
|
||||||
|
|
||||||
// 清空输入框和文件
|
// 注释掉自动清空功能,保留数据用于调试
|
||||||
aiPrompt.value = '';
|
// 用户可以通过"清空"按钮手动清空数据
|
||||||
markdownContent.value = '';
|
// aiPrompt.value = '';
|
||||||
convertedJSON.value = '';
|
// markdownContent.value = '';
|
||||||
|
// convertedJSON.value = '';
|
||||||
|
|
||||||
|
// 只清空文件上传,保留生成的内容
|
||||||
uploadedFile.value = null;
|
uploadedFile.value = null;
|
||||||
if (fileInput.value) {
|
if (fileInput.value) {
|
||||||
fileInput.value.value = '';
|
fileInput.value.value = '';
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,38 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- AI输入区域 -->
|
||||||
|
<div v-if="showAIDialog" class="ai-input-area" :style="aiInputStyle">
|
||||||
|
<div class="ai-input-header">
|
||||||
|
<span class="ai-input-title">🤖 询问AI</span>
|
||||||
|
<button @click="closeAIDialog" class="ai-close-btn">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ai-input-content">
|
||||||
|
<textarea
|
||||||
|
v-model="aiQuestion"
|
||||||
|
placeholder="请输入您的问题..."
|
||||||
|
rows="2"
|
||||||
|
:disabled="isAIProcessing"
|
||||||
|
@keydown.enter.exact="handleEnterKey"
|
||||||
|
@keydown.enter.ctrl="submitAIQuestion"
|
||||||
|
@keydown.enter.meta="submitAIQuestion"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div class="ai-input-actions">
|
||||||
|
<button @click="closeAIDialog" class="btn-cancel" :disabled="isAIProcessing">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button @click="submitAIQuestion" class="btn-submit" :disabled="!aiQuestion.trim() || isAIProcessing">
|
||||||
|
<span v-if="isAIProcessing">AI思考中...</span>
|
||||||
|
<span v-else>询问AI</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 思维导图容器 -->
|
<!-- 思维导图容器 -->
|
||||||
<div v-else ref="mindmapEl" class="mindmap-el"></div>
|
<div v-if="!showWelcome" ref="mindmapEl" class="mindmap-el"></div>
|
||||||
|
|
||||||
<!-- 保存和刷新按钮 -->
|
<!-- 保存和刷新按钮 -->
|
||||||
<div v-if="!showWelcome" class="save-controls">
|
<div v-if="!showWelcome" class="save-controls">
|
||||||
|
|
@ -112,6 +142,17 @@
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4503 2.99952C16.4016 2.18908 15.7289 1.54688 14.9062 1.54688H6.46875L6.37452 1.5497C5.56408 1.5984 4.92188 2.27108 4.92188 3.09375V3.54688C4.92188 3.68495 5.0338 3.79688 5.17188 3.79688H6.35938C6.49745 3.79688 6.60938 3.68495 6.60938 3.54688V3.375L6.61309 3.34276C6.62766 3.28063 6.68343 3.23438 6.75 3.23438H14.625L14.6572 3.23809C14.7261 3.23438 14.7656 3.30029 14.7656 3.375V11.25L14.7619 11.2822C14.7473 11.3444 14.6916 11.3906 14.625 11.3906H14.4531C14.3151 11.3906 14.2031 11.5026 14.2031 11.6406V12.8281C14.2031 12.9662 14.3151 13.0781 14.4531 13.0781H14.9062L15.0005 13.0753C15.8109 13.0266 16.4531 12.3539 16.4531 11.5312V3.09375L16.4503 2.99952ZM11.5312 4.92188H3.09375C2.23943 4.92188 1.54688 5.61443 1.54688 6.46875V14.9062C1.54688 15.7606 2.23943 16.4531 3.09375 16.4531H11.5312C12.3856 16.4531 13.0781 15.7606 13.0781 14.9062V6.46875C13.0781 5.61443 12.3856 4.92188 11.5312 4.92188ZM3.37032 6.615H11.2635C11.3361 6.615 11.395 6.6739 11.395 6.74655V14.6397C11.395 14.7124 11.3361 14.7712 11.2635 14.7712H3.37032C3.29767 14.7712 3.23877 14.7124 3.23877 14.6397V6.74655C3.23877 6.6739 3.29767 6.615 3.37032 6.615ZM4.5 8.5C4.5 8.27909 4.67909 8.1 4.9 8.1H9.725C9.94591 8.1 10.125 8.27909 10.125 8.5V9.5C10.125 9.72091 9.94591 9.9 9.725 9.9H4.9C4.67909 9.9 4.5 9.72091 4.5 9.5V8.5ZM4.9 11.475C4.67909 11.475 4.5 11.6541 4.5 11.875V12.875C4.5 13.0959 4.67909 13.275 4.9 13.275H9.725C9.94591 13.275 10.125 13.0959 10.125 12.875V11.875C10.125 11.6541 9.94591 11.475 9.725 11.475H4.9Z" fill="currentColor" fill-opacity="1"></path>
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4503 2.99952C16.4016 2.18908 15.7289 1.54688 14.9062 1.54688H6.46875L6.37452 1.5497C5.56408 1.5984 4.92188 2.27108 4.92188 3.09375V3.54688C4.92188 3.68495 5.0338 3.79688 5.17188 3.79688H6.35938C6.49745 3.79688 6.60938 3.68495 6.60938 3.54688V3.375L6.61309 3.34276C6.62766 3.28063 6.68343 3.23438 6.75 3.23438H14.625L14.6572 3.23809C14.7261 3.23438 14.7656 3.30029 14.7656 3.375V11.25L14.7619 11.2822C14.7473 11.3444 14.6916 11.3906 14.625 11.3906H14.4531C14.3151 11.3906 14.2031 11.5026 14.2031 11.6406V12.8281C14.2031 12.9662 14.3151 13.0781 14.4531 13.0781H14.9062L15.0005 13.0753C15.8109 13.0266 16.4531 12.3539 16.4531 11.5312V3.09375L16.4503 2.99952ZM11.5312 4.92188H3.09375C2.23943 4.92188 1.54688 5.61443 1.54688 6.46875V14.9062C1.54688 15.7606 2.23943 16.4531 3.09375 16.4531H11.5312C12.3856 16.4531 13.0781 15.7606 13.0781 14.9062V6.46875C13.0781 5.61443 12.3856 4.92188 11.5312 4.92188ZM3.37032 6.615H11.2635C11.3361 6.615 11.395 6.6739 11.395 6.74655V14.6397C11.395 14.7124 11.3361 14.7712 11.2635 14.7712H3.37032C3.29767 14.7712 3.23877 14.7124 3.23877 14.6397V6.74655C3.23877 6.6739 3.29767 6.615 3.37032 6.615ZM4.5 8.5C4.5 8.27909 4.67909 8.1 4.9 8.1H9.725C9.94591 8.1 10.125 8.27909 10.125 8.5V9.5C10.125 9.72091 9.94591 9.9 9.725 9.9H4.9C4.67909 9.9 4.5 9.72091 4.5 9.5V8.5ZM4.9 11.475C4.67909 11.475 4.5 11.6541 4.5 11.875V12.875C4.5 13.0959 4.67909 13.275 4.9 13.275H9.725C9.94591 13.275 10.125 13.0959 10.125 12.875V11.875C10.125 11.6541 9.94591 11.475 9.725 11.475H4.9Z" fill="currentColor" fill-opacity="1"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Ask AI 功能 -->
|
||||||
|
<div class="context-menu-item ask-ai" @click="askAIForNode" title="Ask AI">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||||
|
<path d="M13 8H7"/>
|
||||||
|
<path d="M17 12H7"/>
|
||||||
|
<circle cx="17" cy="8" r="1"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="context-menu-item delete" @click="deleteSelectedNode" title="Delete">
|
<div class="context-menu-item delete" @click="deleteSelectedNode" title="Delete">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" fill="currentColor"/>
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" fill="currentColor"/>
|
||||||
|
|
@ -136,11 +177,18 @@ const mindmapEl = ref(null);
|
||||||
const mindElixir = ref(null);
|
const mindElixir = ref(null);
|
||||||
const selectedNode = ref(null);
|
const selectedNode = ref(null);
|
||||||
const contextMenuStyle = ref({});
|
const contextMenuStyle = ref({});
|
||||||
|
const aiInputStyle = ref({});
|
||||||
const currentMindmapId = ref(null); // 存储当前思维导图ID
|
const currentMindmapId = ref(null); // 存储当前思维导图ID
|
||||||
const zoomLevel = ref(1); // 缩放级别
|
const zoomLevel = ref(1); // 缩放级别
|
||||||
const showWelcome = ref(true); // 控制欢迎页面显示
|
const showWelcome = ref(true); // 控制欢迎页面显示
|
||||||
const isAISidebarCollapsed = ref(false); // AI侧边栏折叠状态
|
const isAISidebarCollapsed = ref(false); // AI侧边栏折叠状态
|
||||||
|
|
||||||
|
// Ask AI 相关状态
|
||||||
|
const showAIDialog = ref(false); // 控制AI对话框显示
|
||||||
|
const currentQuestionNode = ref(null); // 当前询问的节点
|
||||||
|
const aiQuestion = ref(''); // 用户输入的问题
|
||||||
|
const isAIProcessing = ref(false); // AI处理状态
|
||||||
|
|
||||||
// 节点拖拽状态管理
|
// 节点拖拽状态管理
|
||||||
const nodePositions = ref(new Map()); // 存储节点位置偏移
|
const nodePositions = ref(new Map()); // 存储节点位置偏移
|
||||||
const isDragging = ref(false);
|
const isDragging = ref(false);
|
||||||
|
|
@ -350,6 +398,8 @@ const loadMindmapData = async (data, keepPosition = false) => {
|
||||||
centerMindMap();
|
centerMindMap();
|
||||||
}
|
}
|
||||||
bindEventListeners();
|
bindEventListeners();
|
||||||
|
// 添加节点描述显示
|
||||||
|
addNodeDescriptions();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
@ -386,6 +436,8 @@ const loadMindmapData = async (data, keepPosition = false) => {
|
||||||
centerMindMap();
|
centerMindMap();
|
||||||
}
|
}
|
||||||
bindEventListeners();
|
bindEventListeners();
|
||||||
|
// 添加节点描述显示
|
||||||
|
addNodeDescriptions();
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
|
|
@ -409,12 +461,16 @@ const loadMindmapData = async (data, keepPosition = false) => {
|
||||||
if (keepPosition && currentPosition) {
|
if (keepPosition && currentPosition) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
restorePosition(currentPosition);
|
restorePosition(currentPosition);
|
||||||
|
// 添加节点描述显示
|
||||||
|
addNodeDescriptions();
|
||||||
}, 500);
|
}, 500);
|
||||||
} else {
|
} else {
|
||||||
// 延迟再次居中,确保所有节点都渲染完成
|
// 延迟再次居中,确保所有节点都渲染完成
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
centerMindMap();
|
centerMindMap();
|
||||||
console.log('🔄 思维导图二次居中完成');
|
console.log('🔄 思维导图二次居中完成');
|
||||||
|
// 添加节点描述显示
|
||||||
|
addNodeDescriptions();
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
@ -444,7 +500,7 @@ const convertToMindElixirFormat = (data) => {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
topic: node.title || node.content || "无标题",
|
topic: node.title || node.content || "无标题",
|
||||||
data: {
|
data: {
|
||||||
des: node.des || node.description || ""
|
des: node.des || ""
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
// 添加思维导图ID到节点
|
// 添加思维导图ID到节点
|
||||||
|
|
@ -1033,6 +1089,199 @@ const deleteSelectedNode = async () => {
|
||||||
selectedNode.value = null;
|
selectedNode.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ask AI 功能
|
||||||
|
const askAIForNode = async () => {
|
||||||
|
if (!selectedNode.value) return;
|
||||||
|
|
||||||
|
console.log('Ask AI for node:', selectedNode.value);
|
||||||
|
|
||||||
|
// 计算AI输入区域位置 - 显示在横向菜单下方
|
||||||
|
const menuLeft = parseFloat(contextMenuStyle.value.left) || 0;
|
||||||
|
const menuTop = parseFloat(contextMenuStyle.value.top) || 0;
|
||||||
|
|
||||||
|
aiInputStyle.value = {
|
||||||
|
left: `${menuLeft}px`,
|
||||||
|
top: `${menuTop + 60}px`, // 在菜单下方60px处
|
||||||
|
transform: 'translateX(-50%)'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示AI输入区域
|
||||||
|
showAIDialog.value = true;
|
||||||
|
currentQuestionNode.value = selectedNode.value;
|
||||||
|
|
||||||
|
// 不清空selectedNode,保持横向菜单显示
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭AI对话框
|
||||||
|
const closeAIDialog = () => {
|
||||||
|
showAIDialog.value = false;
|
||||||
|
currentQuestionNode.value = null;
|
||||||
|
aiQuestion.value = '';
|
||||||
|
isAIProcessing.value = false;
|
||||||
|
// 关闭AI输入框时也清空选中的节点,隐藏横向菜单
|
||||||
|
selectedNode.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取节点上下文信息(包括父节点、祖父节点)
|
||||||
|
const getNodeContext = (node) => {
|
||||||
|
if (!node) return '';
|
||||||
|
|
||||||
|
const context = [];
|
||||||
|
|
||||||
|
// 获取父节点信息
|
||||||
|
if (node.parent && node.parent.topic) {
|
||||||
|
context.push(`父节点: ${node.parent.topic}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取祖父节点信息
|
||||||
|
if (node.parent && node.parent.parent && node.parent.parent.topic) {
|
||||||
|
context.push(`祖父节点: ${node.parent.parent.topic}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.join(' | ');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理回车键事件
|
||||||
|
const handleEnterKey = (event) => {
|
||||||
|
// 如果按的是Ctrl+Enter或Meta+Enter,不处理(让原有的处理函数处理)
|
||||||
|
if (event.ctrlKey || event.metaKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通回车键:阻止默认换行行为,直接提交问题
|
||||||
|
event.preventDefault();
|
||||||
|
submitAIQuestion();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交AI问题
|
||||||
|
const submitAIQuestion = async () => {
|
||||||
|
if (!aiQuestion.value.trim() || !currentQuestionNode.value || isAIProcessing.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAIProcessing.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建AI提示词
|
||||||
|
const systemPrompt = `你是一个专业的思维导图分析助手。请根据用户的问题和提供的节点信息,给出专业、有用的回答。`;
|
||||||
|
|
||||||
|
const userPrompt = `节点信息:
|
||||||
|
当前节点:${currentQuestionNode.value.topic}
|
||||||
|
上下文:${getNodeContext(currentQuestionNode.value)}
|
||||||
|
|
||||||
|
用户问题:${aiQuestion.value}
|
||||||
|
|
||||||
|
请给出详细的回答,回答应该:
|
||||||
|
1. 直接回答用户的问题
|
||||||
|
2. 提供具体的建议或改进方案
|
||||||
|
3. 保持专业和有用的语调
|
||||||
|
4. 回答长度适中,便于在思维导图中展示`;
|
||||||
|
|
||||||
|
console.log('发送AI请求:', { systemPrompt, userPrompt });
|
||||||
|
|
||||||
|
// 调用AI API
|
||||||
|
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: systemPrompt,
|
||||||
|
user_prompt: userPrompt,
|
||||||
|
model: "glm-4.5",
|
||||||
|
base_url: "https://open.bigmodel.cn/api/paas/v4/",
|
||||||
|
api_key: "ce39bdd4fcf34ec0aec75072bc9ff988.hAp7HZTVUwy7vImn"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
let aiResponse = '';
|
||||||
|
|
||||||
|
if (data.success && data.markdown) {
|
||||||
|
aiResponse = data.markdown;
|
||||||
|
} else if (data.markdown) {
|
||||||
|
aiResponse = data.markdown;
|
||||||
|
} else if (data.content) {
|
||||||
|
aiResponse = data.content;
|
||||||
|
} else {
|
||||||
|
throw new Error('AI响应格式错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('AI回答:', aiResponse);
|
||||||
|
|
||||||
|
// 在合适位置创建新节点展示AI回答
|
||||||
|
await createAINode(currentQuestionNode.value, aiQuestion.value, aiResponse);
|
||||||
|
|
||||||
|
// 关闭对话框
|
||||||
|
closeAIDialog();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI请求失败:', error);
|
||||||
|
alert('AI请求失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
isAIProcessing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建AI回答节点
|
||||||
|
const createAINode = async (parentNode, question, answer) => {
|
||||||
|
try {
|
||||||
|
console.log('开始创建AI节点...', { parentNode, question, answer });
|
||||||
|
|
||||||
|
// 使用现有的Markdown转JSON逻辑
|
||||||
|
const formatAnswer = (text) => {
|
||||||
|
return text
|
||||||
|
.replace(/^#+\s*/gm, '') // 移除标题标记
|
||||||
|
.replace(/\*\*(.*?)\*\*/g, '$1') // 移除粗体标记
|
||||||
|
.replace(/\*(.*?)\*/g, '$1') // 移除斜体标记
|
||||||
|
.replace(/^\s*[-*+]\s*/gm, '• ') // 统一列表标记
|
||||||
|
.replace(/\n{3,}/g, '\n\n') // 限制连续换行
|
||||||
|
.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedAnswer = formatAnswer(answer);
|
||||||
|
|
||||||
|
// 使用API直接创建节点到数据库
|
||||||
|
const nodeData = {
|
||||||
|
title: `问题:${question}\n\n回答:${formattedAnswer}`, // 将问题和回答都存储到title中
|
||||||
|
des: `AI追问产生的节点 - ${new Date().toLocaleString()}`, // 标注这是AI追问产生的节点
|
||||||
|
parentId: parentNode.id,
|
||||||
|
isRoot: false
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('准备创建节点数据:', nodeData);
|
||||||
|
console.log('当前思维导图ID:', currentMindmapId.value);
|
||||||
|
|
||||||
|
// 检查是否有思维导图ID
|
||||||
|
if (!currentMindmapId.value) {
|
||||||
|
throw new Error('没有找到当前思维导图ID,无法创建节点');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API创建节点
|
||||||
|
const response = await mindmapAPI.addNodes(currentMindmapId.value, [nodeData]);
|
||||||
|
console.log('API创建节点响应:', response);
|
||||||
|
|
||||||
|
if (response.data && response.data.success) {
|
||||||
|
console.log('✅ AI节点创建成功,开始刷新思维导图...');
|
||||||
|
|
||||||
|
// 刷新思维导图以显示新节点
|
||||||
|
await refreshMindMap();
|
||||||
|
|
||||||
|
console.log('✅ AI节点创建并刷新完成');
|
||||||
|
} else {
|
||||||
|
throw new Error('API创建节点失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建AI节点失败:', error);
|
||||||
|
alert('创建AI回答节点失败: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const copyNodeText = async () => {
|
const copyNodeText = async () => {
|
||||||
if (!selectedNode.value) return;
|
if (!selectedNode.value) return;
|
||||||
|
|
||||||
|
|
@ -1680,7 +1929,7 @@ const createNodesRecursively = async (node, mindmapId, parentId) => {
|
||||||
// 创建当前节点
|
// 创建当前节点
|
||||||
const nodeResponse = await mindmapAPI.addNodes(mindmapId, {
|
const nodeResponse = await mindmapAPI.addNodes(mindmapId, {
|
||||||
title: node.topic || node.title || "无标题",
|
title: node.topic || node.title || "无标题",
|
||||||
des: node.data?.des || node.description || "",
|
des: node.data?.des || "",
|
||||||
parentId: parentId
|
parentId: parentId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1803,6 +2052,13 @@ const bindEventListeners = () => {
|
||||||
};
|
};
|
||||||
} else if (!isNodeClick) {
|
} else if (!isNodeClick) {
|
||||||
selectedNode.value = null;
|
selectedNode.value = null;
|
||||||
|
// 点击空白区域时也关闭AI询问框
|
||||||
|
if (showAIDialog.value) {
|
||||||
|
showAIDialog.value = false;
|
||||||
|
currentQuestionNode.value = null;
|
||||||
|
aiQuestion.value = '';
|
||||||
|
isAIProcessing.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2979,6 +3235,26 @@ defineExpose({
|
||||||
color: #dc3545;
|
color: #dc3545;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ask AI 特殊样式 - 让它一眼就突出 */
|
||||||
|
.context-menu-item.ask-ai {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: 2px solid #5a67d8;
|
||||||
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item.ask-ai:hover {
|
||||||
|
background: linear-gradient(135deg, #5a67d8 0%, #667eea 100%);
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item.ask-ai svg {
|
||||||
|
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
.context-menu-item svg {
|
.context-menu-item svg {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
|
@ -3028,6 +3304,132 @@ defineExpose({
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AI输入区域样式 */
|
||||||
|
.ai-input-area {
|
||||||
|
position: absolute;
|
||||||
|
width: 300px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
z-index: 1001;
|
||||||
|
animation: slideInDown 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-50%) translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-input-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-input-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-close-btn:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-input-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-input-content textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
resize: none;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
font-family: inherit;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-input-content textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-input-content textarea:disabled {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-input-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel, .btn-submit {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover:not(:disabled) {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit:hover:not(:disabled) {
|
||||||
|
background: #0056b3;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:disabled, .btn-submit:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue