初始化鸿蒙应用展示平台项目 - 前后端分离架构

This commit is contained in:
Nvex
2025-10-25 11:45:17 +08:00
commit c0f81dbbe2
92 changed files with 40210 additions and 0 deletions

572
templates/my_comments.html Executable file
View File

@@ -0,0 +1,572 @@
{% extends "base.html" %}
{% block title %}我的评论{% endblock %}
{% block content %}
<div class="comments-container">
<div class="header">
<a href="{{ url_for('more') }}" class="back-link" title="返回">
<i class="fas fa-arrow-left"></i>
</a>
<h1>我的评论</h1>
</div>
<div class="comments-filter">
<div class="filter-tabs">
<button class="filter-tab active" data-status="all">全部</button>
<button class="filter-tab" data-status="pending">待审核</button>
<button class="filter-tab" data-status="approved">已通过</button>
<button class="filter-tab" data-status="rejected">已拒绝</button>
</div>
</div>
<div class="comments-list-wrapper">
<div id="comments-list" class="comments-list">
<!-- Comments will be dynamically loaded here -->
</div>
<div id="comments-empty-state" class="empty-state" style="display: none;">
<div class="empty-state-icon">
<i class="fas fa-comment-slash"></i>
</div>
<h2>暂无评论</h2>
<p>您还没有发表任何评论。在 资讯 页面开始您的探索吧!</p>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const commentsList = document.getElementById('comments-list');
const emptyState = document.getElementById('comments-empty-state');
const filterTabs = document.querySelectorAll('.filter-tab');
let allComments = [];
function fetchMyComments() {
commentsList.innerHTML = `
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
<p>正在加载评论...</p>
</div>
`;
fetch('/my_comments', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
// 检查响应状态
if (response.status === 401) {
// 未登录,重定向到登录页
window.location.href = '/login';
throw new Error('未登录');
}
if (!response.ok) {
throw new Error(`HTTP错误状态码${response.status}`);
}
return response.json();
})
.then(data => {
if (data.success) {
allComments = Array.isArray(data.comments) ? data.comments : [];
renderComments('all');
} else {
showErrorState(data.error || '获取评论失败');
}
})
.catch(error => {
if (error.message === '未登录') {
// 已在上面处理重定向
return;
}
console.error('获取评论时发生错误:', error);
showErrorState(error.message || '网络错误,请稍后重试');
});
}
function renderComments(status) {
// Update filter tab styles
filterTabs.forEach(tab => {
tab.classList.toggle('active', tab.dataset.status === status);
});
const filteredComments = status === 'all'
? allComments
: allComments.filter(comment => comment.status === status);
if (filteredComments.length === 0) {
commentsList.style.display = 'none';
emptyState.style.display = 'flex';
return;
}
commentsList.style.display = 'block';
emptyState.style.display = 'none';
const commentsHtml = filteredComments.map(comment => {
const statusClasses = {
'pending': {
icon: 'fa-clock',
text: '待审核',
color: 'warning'
},
'approved': {
icon: 'fa-check-circle',
text: '已通过',
color: 'success'
},
'rejected': {
icon: 'fa-times-circle',
text: '已拒绝',
color: 'danger'
}
}[comment.status] || {
icon: 'fa-question-circle',
text: '未知状态',
color: 'info'
};
const formatDate = (dateString) => {
try {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
} catch (error) {
console.error('日期格式化错误:', error);
return dateString;
}
};
return `
<div class="comment-card" data-comment-id="${comment.id}">
<div class="comment-header">
<div class="comment-header-left">
<div class="comment-status ${statusClasses.color}">
<i class="fas ${statusClasses.icon}"></i>
${statusClasses.text}
</div>
<div class="comment-date">
${formatDate(comment.created_at)}
</div>
</div>
<div class="comment-header-right">
<button class="btn btn-view btn-small" onclick="window.location.href='/wiki/${comment.wiki_entry_id}'">
<i class="fas fa-eye"></i>
</button>
${status === 'all' || ['pending', 'draft'].includes(comment.status) ? `
<button class="btn btn-delete btn-small" data-comment-id="${comment.id}">
<i class="fas fa-trash"></i>
</button>
` : ''}
</div>
</div>
<div class="comment-body">
<p class="comment-text">${comment.content}</p>
<a href="/wiki/${comment.wiki_entry_id}" class="comment-wiki-link">
${comment.wiki_title || '未知条目'}
</a>
</div>
</div>
`;
}).join('');
commentsList.innerHTML = commentsHtml;
// Add delete event listeners
document.querySelectorAll('.btn-delete').forEach(button => {
button.addEventListener('click', function() {
const commentId = this.getAttribute('data-comment-id');
deleteComment(commentId);
});
});
}
function deleteComment(commentId) {
if (!confirm('确定要删除这条评论吗?')) return;
fetch(`/wiki/comment/${commentId}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
throw new Error('删除失败');
}
return response.json();
})
.then(data => {
if (data.success) {
allComments = allComments.filter(comment => comment.id !== parseInt(commentId));
const activeTab = document.querySelector('.filter-tab.active');
renderComments(activeTab.dataset.status);
} else {
alert(data.error || '删除评论失败');
}
})
.catch(error => {
alert(error.message || '删除评论时发生网络错误');
});
}
function showErrorState(message) {
commentsList.innerHTML = `
<div class="error-state">
<i class="fas fa-exclamation-triangle"></i>
<h2>出错了</h2>
<p>${message}</p>
</div>
`;
emptyState.style.display = 'none';
}
// Add filter tab event listeners
filterTabs.forEach(tab => {
tab.addEventListener('click', function() {
renderComments(this.dataset.status);
});
});
// Initial load
fetchMyComments();
// Back link navigation
document.querySelector('.back-link').addEventListener('click', function(e) {
e.preventDefault();
window.history.back();
});
});
</script>
{% block styles %}
<style>
:root {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f7;
--text-primary: #1a1a1a;
--text-secondary: #666666;
--text-light: #8a8a8a;
--border-color: #e0e0e4;
--accent-color: #007aff;
--success-color: #34c759;
--warning-color: #ff9500;
--danger-color: #ff3b30;
}
[data-theme="dark"] {
--bg-primary: #121212;
--bg-secondary: #1e1e1e;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--text-light: #6a6a6a;
--border-color: #333;
--accent-color: #5e9eff;
--success-color: #30d158;
--warning-color: #ff9f0a;
--danger-color: #ff453a;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
transition: all 0.3s ease;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: var(--bg-secondary);
color: var(--text-primary);
line-height: 1.6;
}
.comments-container {
max-width: 1200px;
margin: 0 auto;
padding: 60px 15px 20px 15px;
}
.header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 30px;
background: white;
padding: 10px 15px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.back-link {
color: #666;
text-decoration: none;
padding: 8px;
border-radius: 50%;
background: #f5f5f7;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
}
.back-link:hover {
background: #e5e5e7;
color: #333;
}
.header h1 {
margin: 0;
font-size: 20px;
color: #333;
font-weight: 500;
font-family: "SimHei", "黑体", sans-serif;
}
.comments-filter {
margin-bottom: 20px;
display: flex;
justify-content: center;
}
.filter-tabs {
display: flex;
background-color: var(--bg-secondary);
border-radius: 20px;
overflow: hidden;
border: 1px solid var(--border-color);
justify-content: center;
}
.filter-tab {
padding: 8px 16px;
background-color: transparent;
border: none;
cursor: pointer;
color: var(--text-secondary);
font-weight: 500;
font-size: 0.9em;
transition: all 0.3s;
}
.filter-tab.active {
color: var(--accent-color);
background-color: rgba(0,122,255,0.1);
}
.comments-list-wrapper {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 20px;
}
.comment-card {
background-color: var(--bg-secondary);
border-radius: 12px;
padding: 15px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.comment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.comment-header-left {
display: flex;
align-items: center;
gap: 10px;
}
.comment-header-right {
display: flex;
align-items: center;
gap: 5px;
}
.btn-small {
width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.btn-small i {
margin: 0;
}
.comment-status {
font-size: 0.7em;
padding: 3px 8px;
border-radius: 12px;
font-weight: 600;
text-transform: uppercase;
display: inline-flex;
align-items: center;
gap: 5px;
}
.comment-status.warning {
background-color: rgba(255,149,0,0.1);
color: var(--warning-color);
}
.comment-status.success {
background-color: rgba(52,199,89,0.1);
color: var(--success-color);
}
.comment-status.danger {
background-color: rgba(255,59,48,0.1);
color: var(--danger-color);
}
.comment-date {
color: var(--text-light);
font-size: 0.8em;
}
.comment-body {
margin-bottom: 15px;
}
.comment-text {
color: var(--text-primary);
font-size: 0.95em;
margin-bottom: 10px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.comment-wiki-link {
color: var(--accent-color);
text-decoration: none;
font-size: 0.85em;
font-weight: 500;
}
.comment-actions {
display: none; /* Remove old actions container */
}
.btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 12px;
border: none;
border-radius: 20px;
font-size: 0.8em;
cursor: pointer;
transition: all 0.3s;
}
.btn-view {
background-color: rgba(0,122,255,0.1);
color: var(--accent-color);
}
.btn-delete {
background-color: rgba(255,59,48,0.1);
color: var(--danger-color);
}
.btn:hover {
opacity: 0.8;
}
.empty-state, .error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 50px 20px;
color: var(--text-secondary);
}
.empty-state-icon, .error-state i {
font-size: 3rem;
opacity: 0.3;
margin-bottom: 15px;
}
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 50px;
color: var(--text-secondary);
}
@media (max-width: 600px) {
.comments-container {
border-radius: 0;
}
.filter-tabs {
flex-direction: row;
width: 100%;
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
justify-content: center;
}
.filter-tabs::-webkit-scrollbar {
display: none;
}
.filter-tab {
flex-shrink: 0;
}
}
/* Dark mode styles */
[data-theme="dark"] .header {
background: #242424;
}
[data-theme="dark"] .header h1 {
color: #fff;
}
[data-theme="dark"] .back-link {
color: #ccc;
background: #333;
}
[data-theme="dark"] .back-link:hover {
background: #444;
color: #fff;
}
[data-theme="dark"] .comments-list-wrapper {
background: #242424;
}
</style>
{% endblock %}
{% endblock %}