Spaces:
Sleeping
Sleeping
<!DOCTYPE html> | |
<html lang="zh"> | |
<head> | |
<!-- Google Analytics tag (gtag.js) --> | |
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Y2Z5KKXXL7"></script> | |
<script> | |
window.dataLayer = window.dataLayer || []; | |
function gtag(){dataLayer.push(arguments);} | |
gtag('js', new Date()); | |
gtag('config', 'G-Y2Z5KKXXL7'); | |
</script> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><g transform='translate(256,256) scale(1.1) translate(-256,-256)'><path fill='%2387CEEB' d='M411.1,217.9c-9.7-43.5-48.4-76.2-94.6-76.2c-28.6,0-54.3,12.9-71.9,33.2c-9.2-3.2-19.1-5-29.4-5 c-32.1,0-58.4,24.7-62.3,56.5c-0.6,0-1.2,0-1.8,0c-38.6,0-70.1,31.6-70.1,70.1c0,38.5,31.5,70,70.1,70h229.9 c38.5,0,70.1-31.5,70.1-70C480,248.9,449.8,217.9,411.1,217.9z'/></g></svg>"> | |
<title>书梦 - 书写你的每一个梦</title> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/animate.min.css" rel="stylesheet"> | |
<link href="https://cdn.jsdelivr.net/npm/@fortawesome/[email protected]/css/all.min.css" rel="stylesheet"> | |
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700&display=swap" rel="stylesheet"> | |
<link href="/static/live2d/js/live2d.css" rel="stylesheet"> | |
<style> | |
:root { | |
--primary-color: #FF6B6B; | |
--secondary-color: #4ECDC4; | |
--accent-color: #FFE66D; | |
--text-color: #2C3E50; | |
--bg-color: #F7F9FC; | |
--chat-bg: #FFFFFF; | |
--message-user: #E3F2FD; | |
--message-ai: #F5F5F5; | |
} | |
body { | |
font-family: 'Noto Serif SC', "PingFang SC", "Microsoft YaHei", serif; | |
background-color: var(--bg-color); | |
color: var(--text-color); | |
line-height: 1.8; | |
} | |
.navbar { | |
background: rgba(255, 255, 255, 0.95); | |
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); | |
padding-top: 0.3rem; | |
padding-bottom: 0.3rem; | |
} | |
.navbar-brand { | |
font-size: 2rem; | |
font-weight: bold; | |
background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); | |
-webkit-background-clip: text; | |
background-clip: text; | |
-webkit-text-fill-color: transparent; | |
padding: 0.3rem 0; | |
} | |
.brand-subtitle { | |
font-size: 1.05rem; | |
color: #FF6B6B; | |
font-weight: bold; | |
opacity: 0.8; | |
margin: 0; | |
padding: 0.3rem 0; | |
position: absolute; | |
left: 50%; | |
transform: translateX(-50%); | |
white-space: nowrap; | |
} | |
.brand-subtitle2 { | |
font-size: 1rem; | |
color: #FF6B6B; | |
} | |
.hero-section { | |
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); | |
position: relative; | |
overflow: hidden; | |
padding: 8rem 0; | |
margin-bottom: -6rem; | |
} | |
.hero-section::before { | |
content: ''; | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
background: url('data:image/svg+xml,<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><circle cx="2" cy="2" r="1" fill="rgba(255,255,255,0.1)"/></svg>') repeat; | |
opacity: 0.3; | |
animation: float 20s linear infinite; | |
} | |
@keyframes float { | |
from { transform: translate(0, 0); } | |
to { transform: translate(20px, 20px); } | |
} | |
.chat-container { | |
background: var(--chat-bg); | |
border-radius: 20px; | |
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); | |
margin-top: -2rem; | |
position: relative; | |
z-index: 1; | |
min-height: 600px; | |
} | |
.chat-messages { | |
height: 400px; | |
overflow-y: auto; | |
padding: 1.5rem; | |
} | |
.message { | |
margin-bottom: 1.5rem; | |
max-width: 80%; | |
position: relative; | |
} | |
.message-user { | |
margin-left: auto; | |
background: var(--message-user); | |
border-radius: 20px 20px 5px 20px; | |
padding: 1rem 1.5rem; | |
} | |
.message-ai { | |
margin-right: auto; | |
background: var(--message-ai); | |
border-radius: 20px 20px 20px 5px; | |
padding: 1rem 1.5rem; | |
} | |
.chat-input { | |
border-top: 1px solid rgba(0, 0, 0, 0.1); | |
padding: 1.5rem; | |
background: white; | |
border-radius: 0 0 20px 20px; | |
} | |
.btn-primary { | |
background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); | |
border: none; | |
border-radius: 25px; | |
padding: 0.8rem 2rem; | |
font-weight: 600; | |
transition: all 0.3s ease; | |
} | |
.btn-primary:hover { | |
transform: translateY(-2px); | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); | |
} | |
.form-control { | |
border-radius: 15px; | |
padding: 0.8rem 1.2rem; | |
border: 2px solid #eee; | |
transition: all 0.3s ease; | |
} | |
.form-control:focus { | |
border-color: var(--secondary-color); | |
box-shadow: 0 0 0 0.2rem rgba(78, 205, 196, 0.25); | |
} | |
.feature-card { | |
background: white; | |
border-radius: 20px; | |
padding: 2rem; | |
text-align: center; | |
transition: all 0.3s ease; | |
border: none; | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); | |
} | |
.feature-card:hover { | |
transform: translateY(-10px); | |
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.12); | |
} | |
.feature-icon { | |
width: 80px; | |
height: 80px; | |
border-radius: 50%; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
margin: 0 auto 1.5rem; | |
background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); | |
color: white; | |
font-size: 2rem; | |
} | |
.footer { | |
background: var(--text-color); | |
color: white; | |
padding: 4rem 0 2rem; | |
margin-top: 4rem; | |
} | |
.social-links a { | |
display: inline-flex; | |
align-items: center; | |
justify-content: center; | |
width: 40px; | |
height: 40px; | |
border-radius: 50%; | |
background: rgba(255, 255, 255, 0.1); | |
color: white; | |
margin: 0 0.5rem; | |
transition: all 0.3s ease; | |
} | |
.social-links a:hover { | |
background: var(--secondary-color); | |
transform: translateY(-3px); | |
} | |
.nav-link { | |
color: var(--text-color) !important; | |
font-weight: 500; | |
padding: 0.5rem 1.2rem; | |
border-radius: 20px; | |
transition: all 0.3s ease; | |
} | |
.nav-link:hover { | |
background: rgba(255, 107, 107, 0.1); | |
color: var(--primary-color) !important; | |
} | |
.loading-dots:after { | |
content: '.'; | |
animation: dots 1.5s steps(5, end) infinite; | |
} | |
@keyframes dots { | |
0%, 20% { content: '.'; } | |
40% { content: '..'; } | |
60% { content: '...'; } | |
80% { content: '....'; } | |
100% { content: '.....'; } | |
} | |
.fancy-poetry2 { | |
font-family: "STKaiti", "KaiTi", serif; | |
background: linear-gradient(120deg, #4568dc, #b06ab3); | |
background-clip: text; | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
font-size: 1.25rem; | |
letter-spacing: 1px; | |
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); | |
display: inline-block; | |
vertical-align: middle; | |
line-height: normal; | |
margin-bottom: 7px; | |
} | |
.navbar-toggler { | |
border: none; | |
padding: 0.5rem; | |
color: var(--text-color); | |
background: transparent; | |
} | |
.navbar-toggler:focus { | |
box-shadow: none; | |
outline: none; | |
} | |
.navbar-toggler-icon { | |
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(44, 62, 80, 1)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); | |
width: 24px; | |
height: 24px; | |
} | |
@media (max-width: 991.98px) { | |
.navbar-nav { | |
background: rgba(255, 255, 255, 0.98); | |
padding: 1rem; | |
border-radius: 10px; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
margin-top: 1rem; | |
} | |
.nav-item { | |
margin: 0.5rem 0; | |
} | |
.brand-subtitle { | |
display: none; | |
} | |
} | |
</style> | |
{% block extra_css %}{% endblock %} | |
</head> | |
<body> | |
<nav class="navbar navbar-expand-lg fixed-top"> | |
<div class="container"> | |
<a class="navbar-brand" href="/"> | |
<i class="fas fa-feather-alt me-2"></i>书梦 | |
</a> | |
<div class="brand-subtitle">创造你的热爱</div> | |
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> | |
<span class="navbar-toggler-icon"></span> | |
</button> | |
<div class="collapse navbar-collapse" id="navbarNav"> | |
<ul class="navbar-nav ms-auto"> | |
<li class="nav-item"> | |
<a class="nav-link" href="/"><i class="fas fa-home me-1"></i>首页</a> | |
</li> | |
<li class="nav-item"> | |
<a class="nav-link" href="{{ url_for('text_to_speech_page') }}"><i class="fas fa-microphone me-1"></i><span class="fancy-poetry2">梦语</span></a> | |
</li> | |
<li class="nav-item"> | |
<a class="nav-link" href="{{ url_for('forum') }}"><i class="fas fa-comments me-1"></i><span class="fancy-poetry2">书梦社</span></a> | |
</li> | |
{% if current_user.is_authenticated %} | |
<li class="nav-item dropdown position-relative"> | |
{% if request.endpoint != 'index' %} | |
<a class="nav-link" href="#" id="notificationsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> | |
<i class="fas fa-bell"></i> | |
<span id="notificationCount" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="display: none;"> | |
0 | |
</span> | |
</a> | |
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="notificationsDropdown" style="width: 300px; max-height: 400px; overflow-y: auto;"> | |
<div id="notificationList" class="px-2"> | |
<!-- Notifications will be loaded here --> | |
</div> | |
</div> | |
{% endif %} | |
</li> | |
<li class="nav-item"> | |
<a class="nav-link" href="/my-stories"><i class="fas fa-book me-1"></i>我的梦</a> | |
</li> | |
<li class="nav-item"> | |
<a class="nav-link" href="/settings"><i class="fas fa-cog me-1"></i>设置</a> | |
</li> | |
{% else %} | |
<li class="nav-item"> | |
<a class="nav-link" href="/login"><i class="fas fa-sign-in-alt me-1"></i>登录</a> | |
</li> | |
{% endif %} | |
</ul> | |
</div> | |
</div> | |
</nav> | |
{% block content %}{% endblock %} | |
<footer class="footer"> | |
<div class="container"> | |
<div class="row"> | |
<div class="col-md-4 mb-4"> | |
<h4><i class="fas fa-feather-alt me-2"></i>书梦</h4> | |
<p class="mb-0 brand-subtitle2" style="margin-top: 10px; letter-spacing: 0.2em;">创造你的热爱</p> | |
<p class="mb-0" style="margin-top: 10px;">宇宙这么大,总有一个属于你的梦</p> | |
</div> | |
<div class="col-md-4 mb-4"> | |
<h5>快速链接</h5> | |
<ul class="list-unstyled"> | |
<li><a href="/" class="text-white text-decoration-none">首页</a></li> | |
<li><a href="/login" class="text-white text-decoration-none">登录</a></li> | |
<li><a href="https://www.bilibili.com/video/BV1JBPkeQEt5" target="_blank" class="text-white text-decoration-none">使用指南</a></li> | |
</ul> | |
</div> | |
<div class="col-md-4 mb-4"> | |
<h5>关注我们</h5> | |
<div class="social-links mt-3"> | |
<a href="#" data-bs-toggle="modal" data-bs-target="#weixinQRModal"><i class="fab fa-weixin"></i></a> | |
<a href="https://space.bilibili.com/3546380060592726"><i class="fab fa-bilibili"></i></a> | |
<p class="mb-0" style="margin-top: 10px;">官方邮箱:[email protected]</p> | |
</div> | |
</div> | |
</div> | |
<div class="text-center mt-4"> | |
<p class="mb-0">© 2025 书梦 DoingDream. All rights reserved.</p> | |
</div> | |
</div> | |
</footer> | |
<!-- Add the QR code modal --> | |
<div class="modal fade" id="weixinQRModal" tabindex="-1" aria-labelledby="weixinQRModalLabel" aria-hidden="true"> | |
<div class="modal-dialog modal-dialog-centered"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title" id="weixinQRModalLabel">微信扫码关注我们吧~</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
</div> | |
<div class="modal-body text-center"> | |
<img src="{{ url_for('static', filename='qrcode.jpg') }}" alt="WeChat QR Code" class="img-fluid"> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> | |
<script src="/static/live2d/core/live2dcubismcore.min.js"></script> | |
<script src="/static/live2d/js/live2d.js"></script> | |
{% block extra_js %} | |
<script> | |
// Add notification handling functions | |
function updateNotificationCount() { | |
fetch('/notifications/count') | |
.then(response => response.json()) | |
.then(data => { | |
const countBadge = document.getElementById('notificationCount'); | |
if (data.count > 0) { | |
countBadge.textContent = data.count; | |
countBadge.style.display = 'inline-block'; | |
} else { | |
countBadge.style.display = 'none'; | |
} | |
}); | |
} | |
// Function to get post_id for a comment | |
async function getPostIdForComment(comment_id) { | |
try { | |
const response = await fetch(`/get_post_id_for_comment/${comment_id}`); | |
const data = await response.json(); | |
return data.post_id; | |
} catch (error) { | |
console.error('Error getting post_id for comment:', error); | |
return null; | |
} | |
} | |
async function loadNotifications() { | |
try { | |
const response = await fetch('/notifications'); | |
const data = await response.json(); | |
const notificationList = document.getElementById('notificationList'); | |
notificationList.innerHTML = ''; | |
if (data.notifications.length === 0) { | |
notificationList.innerHTML = '<div class="text-center py-3">暂无新消息</div>'; | |
return; | |
} | |
// Create a map to store post IDs for comments to avoid multiple requests | |
const commentPostIds = new Map(); | |
// First, gather all comment IDs that need post IDs | |
const commentPromises = data.notifications | |
.filter(n => n.comment_id && !n.post_id) | |
.map(async n => { | |
if (!commentPostIds.has(n.comment_id)) { | |
try { | |
const response = await fetch(`/get_post_id_for_comment/${n.comment_id}`); | |
const data = await response.json(); | |
commentPostIds.set(n.comment_id, data.post_id); | |
} catch (error) { | |
console.error('Error fetching post ID:', error); | |
commentPostIds.set(n.comment_id, null); | |
} | |
} | |
}); | |
// Wait for all post ID requests to complete | |
await Promise.all(commentPromises); | |
// Now render all notifications | |
for (const notification of data.notifications) { | |
let message = ''; | |
let link = '#'; | |
switch (notification.type) { | |
case 'follow': | |
message = `${notification.sender.username} 关注了你`; | |
link = `/user/${notification.sender.id}/profile`; | |
break; | |
case 'like': | |
if (notification.comment_id) { | |
message = `${notification.sender.username} 点赞了你的评论`; | |
const post_id = notification.post_id || commentPostIds.get(notification.comment_id); | |
link = `/forum/post/${post_id}#comment-${notification.comment_id}`; | |
} else { | |
message = `${notification.sender.username} 点赞了你的分享`; | |
link = `/forum/post/${notification.post_id}`; | |
} | |
break; | |
case 'comment': | |
if (notification.comment_id) { | |
message = `${notification.sender.username} 回复了你的评论`; | |
const post_id = notification.post_id || commentPostIds.get(notification.comment_id); | |
link = `/forum/post/${post_id}#comment-${notification.comment_id}`; | |
} else { | |
message = `${notification.sender.username} 评论了你的分享`; | |
link = `/forum/post/${notification.post_id}`; | |
} | |
break; | |
} | |
const notificationHtml = ` | |
<a href="${link}" class="dropdown-item py-2 border-bottom" onclick="handleNotificationClick(event, '${link}')"> | |
<div class="d-flex align-items-center"> | |
<div class="flex-shrink-0"> | |
<img src="${notification.sender.avatar_url ? notification.sender.avatar_url : notification.sender.get_avatar()}" | |
class="rounded-circle" | |
width="40" height="40" | |
alt="${notification.sender.username}" | |
style="object-fit: cover; border: 1px solid rgba(0, 0, 0, 0.1);" | |
onerror="this.onerror=null; this.src='/static/default_avatar.png';"> | |
</div> | |
<div class="flex-grow-1 ms-3"> | |
<div class="text-wrap mb-1">${message}</div> | |
<small class="text-muted">${new Date(new Date(notification.created_at).getTime() + 8 * 60 * 60 * 1000).toLocaleString()}</small> | |
</div> | |
</div> | |
</a> | |
`; | |
notificationList.insertAdjacentHTML('beforeend', notificationHtml); | |
} | |
} catch (error) { | |
console.error('Error loading notifications:', error); | |
const notificationList = document.getElementById('notificationList'); | |
notificationList.innerHTML = '<div class="text-center py-3 text-danger">加载消息失败</div>'; | |
} | |
} | |
// Function to handle notification clicks | |
function handleNotificationClick(event, link) { | |
event.preventDefault(); | |
// Close the dropdown | |
const dropdownMenu = document.querySelector('.dropdown-menu'); | |
if (dropdownMenu) { | |
const dropdown = bootstrap.Dropdown.getInstance(document.getElementById('notificationsDropdown')); | |
if (dropdown) { | |
dropdown.hide(); | |
} | |
} | |
// Navigate to the link | |
window.location.href = link; | |
} | |
// Add styles for notification items | |
const style = document.createElement('style'); | |
style.textContent = ` | |
.dropdown-item:hover { | |
background-color: rgba(0, 0, 0, 0.05); | |
} | |
#notificationList .dropdown-item { | |
white-space: normal; | |
border-bottom: 1px solid rgba(0, 0, 0, 0.1); | |
} | |
#notificationList .dropdown-item:last-child { | |
border-bottom: none; | |
} | |
#notificationList .dropdown-menu { | |
max-height: 400px; | |
overflow-y: auto; | |
} | |
`; | |
document.head.appendChild(style); | |
// Update notification count every minute | |
setInterval(updateNotificationCount, 60000); | |
// Load notifications when dropdown is opened | |
document.getElementById('notificationsDropdown')?.addEventListener('show.bs.dropdown', loadNotifications); | |
// Initial load | |
if (document.getElementById('notificationCount')) { | |
updateNotificationCount(); | |
} | |
</script> | |
{% endblock %} | |
</body> | |
</html> |