newBlog / app /templates /article.html
mistpe's picture
Upload 4 files
50b254a verified
raw
history blame
16.6 kB
{% extends "base.html" %}
{% block title %}{{ article.title }} - 个人博客{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<style>
/* 文章容器样式 */
.article-container {
max-width: 900px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 2px 12px rgba(99, 145, 197, 0.08);
border: 2px solid var(--light-blue);
padding: 2.5rem;
}
/* 文章头部样式 */
.article-header {
margin-bottom: 2.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--light-blue);
}
.article-title {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-dark);
line-height: 1.3;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--primary-blue), var(--soft-purple));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.article-meta {
display: flex;
align-items: center;
gap: 1.5rem;
color: #64748B;
}
.meta-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.meta-item i {
color: var(--primary-blue);
}
/* AI 摘要样式 */
.article-summary {
background: var(--warm-cream);
border-radius: 16px;
padding: 1.5rem;
margin: 2rem 0;
position: relative;
}
.summary-label {
position: absolute;
top: -12px;
left: 16px;
background: var(--primary-blue);
color: white;
padding: 0.25rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 500;
}
/* 文章内容样式 */
.article-content {
line-height: 1.8;
color: var(--text-dark);
}
.markdown-body {
font-size: 1.1rem;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3 {
color: var(--primary-blue);
margin-top: 2em;
margin-bottom: 1em;
font-weight: 600;
}
.markdown-body p {
margin-bottom: 1.5em;
}
.markdown-body a {
color: var(--primary-blue);
text-decoration: none;
border-bottom: 1px dashed var(--light-blue);
transition: all 0.3s;
}
.markdown-body a:hover {
border-bottom-style: solid;
color: var(--soft-purple);
}
.markdown-body code {
background: #F8FAFC;
padding: 0.2em 0.4em;
border-radius: 4px;
font-size: 0.9em;
color: var(--primary-blue);
}
.markdown-body pre {
background: #F8FAFC;
border-radius: 12px;
padding: 1rem;
overflow-x: auto;
border: 1px solid var(--light-blue);
}
.markdown-body pre code {
background: none;
padding: 0;
color: inherit;
}
.markdown-body blockquote {
border-left: 4px solid var(--light-blue);
padding: 0.5rem 0 0.5rem 1rem;
margin: 1.5rem 0;
color: #64748B;
background: #F8FAFC;
}
.markdown-body img {
max-width: 100%;
border-radius: 12px;
margin: 1.5rem 0;
}
/* AI 聊天窗口样式 */
.chat-toggle {
position: fixed;
right: 2rem;
bottom: 2rem;
width: 56px;
height: 56px;
border-radius: 28px;
background: linear-gradient(135deg, var(--primary-blue), var(--light-blue));
color: white;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
box-shadow: 0 4px 12px rgba(99, 145, 197, 0.2);
transition: all 0.3s;
z-index: 998;
}
.chat-toggle:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(99, 145, 197, 0.3);
}
.chat-window {
position: fixed;
right: 2rem;
bottom: 2rem;
width: 380px;
height: 600px;
background: white;
border-radius: 20px;
box-shadow: 0 4px 20px rgba(99, 145, 197, 0.15);
display: flex;
flex-direction: column;
transform: scale(0);
opacity: 0;
transform-origin: bottom right;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 999;
border: 1px solid var(--light-blue);
}
.chat-window.active {
transform: scale(1);
opacity: 1;
}
.chat-header {
padding: 1.25rem;
background: linear-gradient(135deg, var(--primary-blue), var(--light-blue));
color: white;
border-radius: 20px 20px 0 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.chat-title {
font-weight: 600;
flex: 1;
}
.chat-close {
background: none;
border: none;
color: white;
cursor: pointer;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 16px;
transition: all 0.3s;
}
.chat-close:hover {
background: rgba(255, 255, 255, 0.2);
}
.chat-messages {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
}
.chat-message {
max-width: 85%;
padding: 1rem;
border-radius: 16px;
line-height: 1.3;
animation: messageSlide 0.3s ease;
word-wrap: break-word;
overflow-wrap: break-word;
width: fit-content;
}
.chat-message p {
margin: 0;
white-space: pre-wrap;
}
.chat-message img,
.chat-message pre,
.chat-message code {
max-width: 100%;
overflow-x: hidden;
}
.chat-message.user {
background: var(--primary-blue);
color: white;
margin-left: auto;
}
.chat-message.assistant {
background: #7b85b8;
color: white;
margin-right: auto;
}
.chat-input-container {
padding: 1.25rem;
border-top: 1px solid var(--light-blue);
}
.chat-input-wrapper {
display: flex;
gap: 0.75rem;
align-items: flex-end;
}
.chat-input {
flex: 1;
min-height: 44px;
max-height: 120px;
padding: 0.75rem 1rem;
border: 2px solid var(--light-blue);
border-radius: 12px;
resize: none;
font-size: 1rem;
line-height: 1.5;
transition: all 0.3s;
}
.chat-input:focus {
outline: none;
border-color: var(--primary-blue);
box-shadow: 0 0 0 3px rgba(99, 145, 197, 0.1);
}
.chat-send {
background: var(--primary-blue);
color: white;
width: 44px;
height: 44px;
border: none;
border-radius: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.chat-send:hover {
background: var(--light-blue);
transform: translateY(-2px);
}
/* 动画 */
@keyframes messageSlide {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.article-container {
padding: 1.5rem;
border-radius: 12px;
}
.article-title {
font-size: 2rem;
}
.chat-window {
right: 1rem;
bottom: 1rem;
left: 1rem;
width: auto;
height: 500px;
}
.chat-toggle {
right: 1rem;
bottom: 1rem;
}
}
</style>
{% endblock %}
{% block content %}
<!-- 文章内容 -->
<article class="article-container">
<header class="article-header">
<h1 class="article-title">{{ article.title }}</h1>
<div class="article-meta">
<div class="meta-item">
<i class="fas fa-calendar"></i>
<span>{{ article.created_at.strftime('%Y-%m-%d') }}</span>
</div>
</div>
</header>
{% if article.summary %}
<div class="article-summary">
<span class="summary-label">AI 摘要</span>
<p>{{ article.summary }}</p>
</div>
{% endif %}
<div class="article-content markdown-body">
{{ article.content|markdown }}
</div>
</article>
<!-- AI 聊天窗口 -->
<button class="chat-toggle" id="chatToggle">
<i class="fas fa-robot"></i>
</button>
<div class="chat-window" id="chatWindow">
<div class="chat-header">
<i class="fas fa-robot"></i>
<span class="chat-title">AI 智能助手</span>
<button class="chat-close" id="chatClose">
<i class="fas fa-times"></i>
</button>
</div>
<div class="chat-messages" id="chatMessages"></div>
<div class="chat-input-wrapper">
<textarea
id="chatInput"
class="chat-input"
placeholder="输入您的问题..."
rows="1"
></textarea>
<button class="chat-send" onclick="sendMessage()">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
// 初始化文章上下文
window.articleContext = {
title: {{ article.title|tojson|safe }},
content: {{ article.content|tojson|safe }}
};
// 配置Marked
marked.setOptions({
breaks: true,
gfm: true
});
// 聊天窗口控制
const chatToggle = document.getElementById('chatToggle');
const chatWindow = document.getElementById('chatWindow');
const chatClose = document.getElementById('chatClose');
const chatInput = document.getElementById('chatInput');
const chatMessages = document.getElementById('chatMessages');
// 模型上下文
const modelContext = `这是一篇关于"${window.articleContext.title}"的文章。文章内容:\n\n${window.articleContext.content}\n\n请基于以上文章内容来回答用户的问题。`;
// 欢迎消息
const welcomeMessage = `您好!我是这篇《${window.articleContext.title}》的AI助手。我已经仔细阅读了全文,可以解答您关于文章内容的任何问题,也提供更深入的讨论和见解,从而帮助您更好地理解文章要点
让我们开始对话吧!`;
// 初始化消息数组
let messages = [{
role: 'system',
content: modelContext
}];
// 初始化聊天
function initializeChat() {
displayMessage('assistant', welcomeMessage);
}
// 切换聊天窗口
function toggleChat() {
chatWindow.classList.toggle('active');
if (chatWindow.classList.contains('active')) {
chatToggle.style.display = 'none';
chatInput.focus();
if (chatMessages.children.length === 0) {
initializeChat();
}
} else {
chatToggle.style.display = 'flex';
}
}
chatToggle.addEventListener('click', toggleChat);
chatClose.addEventListener('click', toggleChat);
// 自动调整文本框高度
chatInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
// 发送消息
async function sendMessage() {
const messageText = chatInput.value.trim();
if (!messageText) return;
const userMessage = {
role: 'user',
content: messageText
};
// 重置输入框
chatInput.value = '';
chatInput.style.height = 'auto';
// 显示用户消息
displayMessage('user', messageText);
try {
const currentMessages = [...messages, userMessage];
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ messages: currentMessages })
});
if (response.ok) {
const data = await response.json();
// 更新消息历史
messages.push(userMessage);
messages.push({
role: 'assistant',
content: data.response
});
// 显示AI响应
displayMessage('assistant', data.response);
} else {
throw new Error('Network response was not ok');
}
} catch (error) {
console.error('Error:', error);
displayMessage('assistant', '抱歉,发生了错误,请稍后再试。');
}
}
// 显示消息
function displayMessage(role, content) {
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message ${role}`;
// 使用marked渲染Markdown内容
messageDiv.innerHTML = marked.parse(content);
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// 回车发送消息
chatInput.addEventListener('keypress', function(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendMessage();
}
});
// 聊天窗口拖动功能
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
chatWindow.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
function dragStart(e) {
if (e.target.closest('.chat-header') && !e.target.closest('.chat-close')) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
isDragging = true;
chatWindow.style.cursor = 'grabbing';
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
// 确保窗口不会超出视口边界
const rect = chatWindow.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// 限制X轴移动
if (rect.left < 0) {
currentX -= rect.left;
}
if (rect.right > viewportWidth) {
currentX -= (rect.right - viewportWidth);
}
// 限制Y轴移动
if (rect.top < 0) {
currentY -= rect.top;
}
if (rect.bottom > viewportHeight) {
currentY -= (rect.bottom - viewportHeight);
}
setTranslate(currentX, currentY, chatWindow);
}
}
function dragEnd() {
initialX = currentX;
initialY = currentY;
isDragging = false;
chatWindow.style.cursor = 'default';
}
function setTranslate(xPos, yPos, el) {
el.style.transform = `translate(${xPos}px, ${yPos}px)`;
}
</script>
{% endblock %}