Commit 50dc23f5 authored by 健杭 徐's avatar 健杭 徐
Browse files

finish

parent 646b90d2
...@@ -29,74 +29,46 @@ type Response struct { ...@@ -29,74 +29,46 @@ type Response struct {
// 获取评论 // 获取评论
func GetCommentsHandler(c *gin.Context) { func GetCommentsHandler(c *gin.Context) {
// 参数验证 pageStr := c.DefaultQuery("page", "1")
page, err := strconv.Atoi(c.DefaultQuery("page", "1")) sizeStr := c.DefaultQuery("size", "10")
if err != nil || page < 1 { page, _ := strconv.Atoi(pageStr)
size, _ := strconv.Atoi(sizeStr)
if page < 1 {
page = 1 page = 1
} }
size, err := strconv.Atoi(c.DefaultQuery("size", "10")) comments := make([]Comment, 0)
if err != nil || size < -1 { var total int64
size = 10
}
// 查询总数 // 统计总数
var total int err := db.QueryRow("SELECT COUNT(*) FROM comments").Scan(&total)
countQuery := "SELECT COUNT(*) FROM comments"
err = db.QueryRow(countQuery).Scan(&total)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusOK, Response{Code: 500, Msg: err.Error()})
"code": 500,
"msg": "查询总数失败: " + err.Error(),
})
return return
} }
// 构建查询 var rows *sql.Rows
query := "SELECT id, name, content, TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI') AS created_at FROM comments"
args := []interface{}{}
// 始终排序
query += " ORDER BY created_at DESC"
// 分页处理
if size != -1 { if size != -1 {
offset := (page - 1) * size offset := (page - 1) * size
query += " LIMIT $1 OFFSET $2" rows, err = db.Query("SELECT id, name, content, created_at FROM comments ORDER BY created_at ASC LIMIT $1 OFFSET $2", size, offset)
args = append(args, size, offset) } else {
rows, err = db.Query("SELECT id, name, content, created_at FROM comments ORDER BY created_at ASC")
} }
// 执行查询
rows, err := db.Query(query, args...)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusOK, Response{Code: 500, Msg: err.Error()})
"code": 500,
"msg": "查询评论失败: " + err.Error(),
})
return return
} }
defer rows.Close() defer rows.Close()
// 处理结果
var comments []Comment
for rows.Next() { for rows.Next() {
var comment Comment var comment Comment
if err := rows.Scan( if err := rows.Scan(&comment.ID, &comment.Name, &comment.Content, &comment.CreatedAt); err != nil {
&comment.ID, c.JSON(http.StatusOK, Response{Code: 500, Msg: err.Error()})
&comment.Name,
&comment.Content,
&comment.CreatedAt,
); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "解析评论失败: " + err.Error(),
})
return return
} }
comments = append(comments, comment) comments = append(comments, comment)
} }
// 返回标准响应结构
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"code": 0, "code": 0,
"msg": "success", "msg": "success",
......
...@@ -81,7 +81,6 @@ func main() { ...@@ -81,7 +81,6 @@ func main() {
log.Fatal(router.Run(":8080")) log.Fatal(router.Run(":8080"))
} }
// 新增表创建函数
func createTable() { func createTable() {
query := ` query := `
CREATE TABLE IF NOT EXISTS comments ( CREATE TABLE IF NOT EXISTS comments (
......
...@@ -9,6 +9,5 @@ ...@@ -9,6 +9,5 @@
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
<script src="/src/load.js"></script>
</body> </body>
</html> </html>
\ No newline at end of file
import { format } from "date-fns"; async function AddComment({ name, onSuccess }: { name: string; onSuccess: () => void; }) {
const currentTime = new Date();
const formattedTime = format(currentTime, 'yyyy-MM-dd HH:mm');
async function AddComment({name, profilePhoto}:{name: string, profilePhoto:string}) {
const textInput = document.getElementById('textInput') as HTMLInputElement; const textInput = document.getElementById('textInput') as HTMLInputElement;
const content = textInput.value; const content = textInput.value;
if (content === '') if (content === '') {
{ alert('Text input cannot be empty');
alert('Text input cannot be empty') return;
return
} }
if (name === '') if (name === '') {
{ name = '小黑子';
name = '小黑子'
} }
try { try {
const response = await fetch('http://localhost:8080/comment/add', { const response = await fetch('http://localhost:8080/comment/add', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, content, created_at: formattedTime}) body: JSON.stringify({ name, content, created_at: new Date().toISOString() })
}); });
const result = await response.json(); const result = await response.json();
if (result.code !== 0) if (result.code !== 0) {
{
alert(`操作失败: ${result.msg}`); alert(`操作失败: ${result.msg}`);
throw new Error(result.msg); throw new Error(result.msg);
} }
const newComment = result.data;
textInput.value = '';
const NewCommentPart = document.createElement('div') (document.getElementById('nameInput') as HTMLInputElement).value = '';
const UserPhoto = document.createElement('img') onSuccess();
UserPhoto.className = 'comment-avatar'
UserPhoto.src = profilePhoto } catch (error) {
UserPhoto.alt = name
UserPhoto.style.width = '30px'
UserPhoto.style.height = '30px'
UserPhoto.style.borderRadius = '50%'
NewCommentPart.appendChild(UserPhoto)
const UserCommentContent = document.createElement('div')
UserCommentContent.className = 'comment-content'
const UserName = document.createElement('span')
UserName.textContent = name
UserName.className = 'comment-meta'
UserCommentContent.appendChild(UserName)
const UserDate = document.createElement('span')
UserDate.textContent = formattedTime
UserDate.className = 'comment-date'
UserCommentContent.appendChild(UserDate)
const UserComment = document.createElement('p')
UserComment.textContent = textInput.value
UserComment.style.marginLeft = '10px'
textInput.value = '';
(document.getElementById('nameInput') as HTMLInputElement).value = '';
UserCommentContent.appendChild(UserComment)
const DeleteButtonArea = document.createElement('div')
DeleteButtonArea.className = 'comment-delete'
const DeleteButton = document.createElement('button')
DeleteButton.textContent = 'Delete'
DeleteButton.id = 'deleteButton'
DeleteButton.onclick = async () => {
const commentItem = DeleteButton.closest('.comment-item')
if (commentItem) {
commentItem.remove()
try {
await fetch(`http://localhost:8080/comment/delete?id=${newComment.id}`, {
method: 'POST'
});
commentItem.remove();
} catch {
alert('删除失败:');
}
}
}
DeleteButtonArea.appendChild(DeleteButton)
UserCommentContent.appendChild(DeleteButtonArea)
NewCommentPart.appendChild(UserCommentContent)
const NewComment = document.createElement('li')
NewComment.className = 'comment-item'
NewComment.style.listStyle = 'none'
NewComment.appendChild(NewCommentPart)
const CommentList = document.getElementById('commentlist') as HTMLUListElement
CommentList.appendChild(NewComment)
}
catch (error) {
console.error('添加评论失败:', error); console.error('添加评论失败:', error);
}; }
} }
export default AddComment export default AddComment;
\ No newline at end of file \ No newline at end of file
import { useState, useEffect, useRef } from 'react';
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef<() => void>(undefined);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
if (savedCallback.current) {
savedCallback.current();
}
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
interface Comment {
id: number;
name: string;
content: string;
created_at: string;
}
const pageSize = 5;
const POLLING_INTERVAL = 1000;
export function CommentSection({ refreshTrigger }: { refreshTrigger: number }) {
const [comments, setComments] = useState<Comment[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalComments, setTotalComments] = useState(0);
const loadComments = async (page: number) => {
try {
const response = await fetch(`http://localhost:8080/comment/get?page=${page}&size=${pageSize}`);
const result = await response.json();
if (result.code !== 0) throw new Error(result.msg);
setComments(result.data.comments);
setTotalComments(result.data.total);
} catch (error) {
console.error('加载评论错误:', error);
}
};
const deleteComment = async (commentId: number) => {
if (!confirm('确定要删除这条评论吗?'))
return;
try {
const response = await fetch(`http://localhost:8080/comment/delete?id=${commentId}`, {
method: 'POST'
});
const result = await response.json();
if (result.code !== 0) throw new Error(result.msg);
alert('评论已删除');
loadComments(currentPage);
} catch (error) {
console.error('删除评论错误:', error);
alert('删除评论失败: ' + (error as Error).message);
}
};
useEffect(() => {
loadComments(currentPage);
}, [currentPage, refreshTrigger]);
useInterval(() => {
loadComments(currentPage);
}, POLLING_INTERVAL);
const totalPages = Math.ceil(totalComments / pageSize);
return (
<div className="comment">
<div>
<h3 style={{ textAlign: 'center' }}>All Comment</h3>
</div>
<ul id="commentlist">
{comments.length > 0 ? (
comments.map(comment => (
<li key={comment.id} className="comment-item">
<div>
<img src='https://pic4.zhimg.com/v2-bf5f58e7b583cd69ac228db9fdff377f_r.jpg'
className='comment-avatar' alt={comment.name} />
<div className='comment-content'>
<div>
<span className='comment-meta'>{comment.name}</span>
<span className='comment-date'>{new Date(comment.created_at).toLocaleString()}</span>
</div>
<p>{comment.content}</p>
<div className='comment-delete'>
<button id='deleteButton' onClick={() => deleteComment(comment.id)}>Delete</button>
</div>
</div>
</div>
</li>
))
) : (
<div className="no-comments">No Comment Yet</div>
)}
</ul>
<div id="pagination-controls" className="pagination-controls">
<button onClick={() => setCurrentPage(prev => prev - 1)} disabled={currentPage <= 1}>
&laquo; Last
</button>
<span>
{totalComments > 0 ? `Page ${currentPage} / ${totalPages}` : 'No Comment Yet'}
</span>
<button onClick={() => setCurrentPage(prev => prev + 1)} disabled={currentPage >= totalPages}>
Next &raquo;
</button>
</div>
</div>
);
}
\ No newline at end of file
...@@ -539,4 +539,43 @@ h3::after { ...@@ -539,4 +539,43 @@ h3::after {
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: var(--primary-color); background: var(--primary-color);
}
.pagination-controls {
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
gap: 15px;
}
.pagination-controls button {
padding: 8px 16px;
border: 1px solid #ddd;
background-color: #f8f8f8;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
}
.pagination-controls button:hover:not(:disabled) {
background-color: #e2e2e2;
}
.pagination-controls button:disabled {
cursor: not-allowed;
color: #ccc;
background-color: #fdfdfd;
}
#page-info {
font-size: 14px;
color: #555;
}
.no-comments {
text-align: center;
color: #888;
padding: 20px;
font-style: italic;
} }
\ No newline at end of file
async function loadComments() { let currentPage = 1;
const pageSize = 5;
let totalComments = 0;
async function loadComments(page) {
currentPage = page;
try { try {
const response = await fetch('http://localhost:8080/comment/get'); const response = await fetch(`http://localhost:8080/comment/get?page=${page}&size=${pageSize}`);
const result = await response.json(); const result = await response.json();
// 检查业务状态码
if (result.code !== 0) { if (result.code !== 0) {
throw new Error(result.msg || '未知错误'); throw new Error(result.msg || '未知错误');
} }
// 正确的数据结构访问 totalComments = result.data.total;
displayComments(result.data.comments); displayComments(result.data.comments);
updatePaginationControls();
} catch (error) { } catch (error) {
console.error('加载评论错误:', error); console.error('加载评论错误:', error);
alert('加载评论失败: ' + error.message); alert('加载评论失败: ' + error.message);
...@@ -19,7 +25,7 @@ async function loadComments() { ...@@ -19,7 +25,7 @@ async function loadComments() {
function displayComments(comments) { function displayComments(comments) {
const commentsContainer = document.getElementById('commentlist'); const commentsContainer = document.getElementById('commentlist');
commentsContainer.innerHTML = ''; commentsContainer.innerHTML = '';
if (!comments || comments.length === 0) { if (!comments || comments.length === 0) {
commentsContainer.innerHTML = '<div class="no-comments">No Comments Yet</div>'; commentsContainer.innerHTML = '<div class="no-comments">No Comments Yet</div>';
return; return;
...@@ -29,6 +35,8 @@ function displayComments(comments) { ...@@ -29,6 +35,8 @@ function displayComments(comments) {
const commentElement = document.createElement('li'); const commentElement = document.createElement('li');
commentElement.className = 'comment-item'; commentElement.className = 'comment-item';
const displayDate = new Date(comment.created_at).toLocaleString();
commentElement.innerHTML = ` commentElement.innerHTML = `
<div> <div>
<img src='https://pic4.zhimg.com/v2-bf5f58e7b583cd69ac228db9fdff377f_r.jpg' <img src='https://pic4.zhimg.com/v2-bf5f58e7b583cd69ac228db9fdff377f_r.jpg'
...@@ -36,7 +44,7 @@ function displayComments(comments) { ...@@ -36,7 +44,7 @@ function displayComments(comments) {
<div class='comment-content'> <div class='comment-content'>
<div> <div>
<span class='comment-meta'>${comment.name}</span> <span class='comment-meta'>${comment.name}</span>
<span class='comment-date'>${comment.created_at}</span> <span class='comment-date'>${displayDate}</span>
</div> </div>
<p>${comment.content}</p> <p>${comment.content}</p>
<div class='comment-delete'> <div class='comment-delete'>
...@@ -63,7 +71,7 @@ function deleteComment(commentId) { ...@@ -63,7 +71,7 @@ function deleteComment(commentId) {
throw new Error(result.msg || '删除失败'); throw new Error(result.msg || '删除失败');
} }
alert('评论已删除'); alert('评论已删除');
loadComments(); // 重新加载评论列表 loadComments(currentPage);
}) })
.catch(error => { .catch(error => {
console.error('删除评论错误:', error); console.error('删除评论错误:', error);
...@@ -71,9 +79,50 @@ function deleteComment(commentId) { ...@@ -71,9 +79,50 @@ function deleteComment(commentId) {
}); });
} }
function setupPagination() {
const paginationContainer = document.getElementById('pagination-controls');
if (!paginationContainer) return;
paginationContainer.innerHTML = `
<button id="prev-button">&laquo; 上一页</button>
<span id="page-info"></span>
<button id="next-button">下一页 &raquo;</button>
`;
document.getElementById('prev-button').addEventListener('click', () => {
if (currentPage > 1) {
loadComments(currentPage - 1);
}
});
document.getElementById('next-button').addEventListener('click', () => {
const totalPages = Math.ceil(totalComments / pageSize);
if (currentPage < totalPages) {
loadComments(currentPage + 1);
}
});
}
function updatePaginationControls() {
const prevButton = document.getElementById('prev-button');
const nextButton = document.getElementById('next-button');
const pageInfo = document.getElementById('page-info');
if (!prevButton || !nextButton || !pageInfo) return;
const totalPages = Math.ceil(totalComments / pageSize);
if (totalPages <= 0) {
pageInfo.textContent = '暂无评论';
} else {
pageInfo.textContent = `第 ${currentPage} / ${totalPages} 页 (共 ${totalComments} 条)`;
}
prevButton.disabled = currentPage <= 1;
nextButton.disabled = currentPage >= totalPages;
}
// 页面加载时调用
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
loadComments(); setupPagination();
loadComments(1);
}); });
\ No newline at end of file
import { StrictMode } from 'react' import { StrictMode, useState } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import AddComment from './App.tsx' import AddComment from './App.tsx'
import DateTimeDisplay from './date.tsx' import DateTimeDisplay from './date.tsx'
import { CommentSection } from './CommentSection.tsx'
import './index.css' import './index.css'
const profilePhoto:string = 'https://pic4.zhimg.com/v2-bf5f58e7b583cd69ac228db9fdff377f_r.jpg' const profilePhoto: string = 'https://pic4.zhimg.com/v2-bf5f58e7b583cd69ac228db9fdff377f_r.jpg'
createRoot(document.getElementById('root')!).render( function App() {
<StrictMode> const [refreshTrigger, setRefreshTrigger] = useState(0);
<div>
<h1 style={{textAlign: 'center'}}>Welcome To Xanadu`s Comment</h1> const handleCommentAdded = () => {
</div> setRefreshTrigger(prev => prev + 1);
<Input_Area /> };
<div className='comment'>
return (
<StrictMode>
<div> <div>
<h3 style={{textAlign: 'center'}}> <h1 style={{ textAlign: 'center' }}>Welcome To Xanadu`s Comment</h1>
All Comment
</h3>
</div> </div>
<ul id='commentlist'></ul> <Input_Area onCommentAdded={handleCommentAdded} />
</div> <CommentSection refreshTrigger={refreshTrigger} />
</StrictMode> </StrictMode>
) );
}
function Input_Area() { function Input_Area({ onCommentAdded }: { onCommentAdded: () => void }) {
return ( return (
<div className='inputarea'> <div className='inputarea'>
<img src={profilePhoto} alt={'小黑子'} <img src={profilePhoto} alt={'小黑子'}
style={{width: '40px', height: '40px', borderRadius: '50%'}} /> style={{ width: '40px', height: '40px', borderRadius: '50%' }} />
<DateTimeDisplay /> <DateTimeDisplay />
<input type="text" placeholder="Please enter your name" id="nameInput" /> <input type="text" placeholder="Please enter your name" id="nameInput" />
<input type="text" placeholder="Please follow the Community Guidelines" id="textInput" onKeyUpCapture={ <input type="text" placeholder="Please follow the Community Guidelines" id="textInput" onKeyUpCapture={
(e) => { (e) => {
const name:string = (document.getElementById('nameInput') as HTMLInputElement).value;
if (e.key === 'Enter') { if (e.key === 'Enter') {
AddComment({name, profilePhoto}); const name: string = (document.getElementById('nameInput') as HTMLInputElement).value;
AddComment({ name, onSuccess: onCommentAdded });
} }
} }
}/> } />
<button <button
id="submitButton" id="submitButton"
onClick={() => AddComment({ onClick={() => {
name: (document.getElementById('nameInput') as HTMLInputElement).value, const name: string = (document.getElementById('nameInput') as HTMLInputElement).value;
profilePhoto: profilePhoto AddComment({ name, onSuccess: onCommentAdded });
})} }}
> >
Submit Submit
</button> </button>
...@@ -54,4 +56,4 @@ function Input_Area() { ...@@ -54,4 +56,4 @@ function Input_Area() {
) )
} }
// Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass createRoot(document.getElementById('root')!).render(<App />);
\ No newline at end of file \ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment