fastapi+angular評論和回復

說明:fastapi+angular評論和回復

效果圖:
在這里插入圖片描述

step1:sql

show databases;
DROP TABLE users;
SHOW CREATE TABLE db_school.users;
show tables;
use db_school;
SELECT * FROM db_school.jewelry_categories;
CREATE DATABASE db_school;
select *from users
-- 用戶表:存儲用戶基礎信息
CREATE TABLE users (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '用戶唯一標識',username VARCHAR(50) NOT NULL UNIQUE COMMENT '用戶名(唯一)',email VARCHAR(100) NOT NULL UNIQUE COMMENT '郵箱(唯一)',created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',status ENUM('active', 'banned', 'deleted') DEFAULT 'active' COMMENT '用戶狀態'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用戶表';-- 評論表:存儲用戶對內容的評論
CREATE TABLE comments (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '評論唯一標識',user_id INT UNSIGNED NOT NULL COMMENT '發表用戶ID',content TEXT NOT NULL COMMENT '評論內容',status ENUM('visible', 'deleted', 'hidden') DEFAULT 'visible' COMMENT '評論狀態',created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='評論表';-- 回復表:存儲對評論的回復
CREATE TABLE replies (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '回復唯一標識',comment_id INT UNSIGNED NOT NULL COMMENT '關聯評論ID',user_id INT UNSIGNED NOT NULL COMMENT '回復用戶ID',content TEXT NOT NULL COMMENT '回復內容',status ENUM('visible', 'deleted', 'hidden') DEFAULT 'visible' COMMENT '回復狀態',created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE,FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='回復表';-- 回復子表:存儲對回復的再回復
CREATE TABLE sub_replies (id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '子回復唯一標識',reply_id INT UNSIGNED NOT NULL COMMENT '關聯回復ID',user_id INT UNSIGNED NOT NULL COMMENT '回復用戶ID',reply_to_user_id INT UNSIGNED NOT NULL COMMENT '被回復用戶ID',content TEXT NOT NULL COMMENT '回復內容',status ENUM('visible', 'deleted', 'hidden') DEFAULT 'visible' COMMENT '回復狀態',created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',FOREIGN KEY (reply_id) REFERENCES replies(id) ON DELETE CASCADE,FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,FOREIGN KEY (reply_to_user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='回復子表';-- 索引優化
CREATE INDEX idx_comments_user ON comments(user_id);
CREATE INDEX idx_comments_status ON comments(status);
CREATE INDEX idx_replies_comment ON replies(comment_id);
CREATE INDEX idx_subreplies_reply ON sub_replies(reply_id);-- 插入用戶數據(10條)
INSERT INTO users (username, email, status) VALUES
('張飛', 'zhangfei@example.com', 'active'),
('劉備', 'liubei@example.com', 'active'),
('關羽', 'guanyu@example.com', 'active');

step2:fastapi

from typing import Dict, List, Optional
from datetime import datetime
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
import pymysql.cursors# ---------------------- FastAPI 初始化 ----------------------
app = FastAPI(title="學校評論系統 API", version="1.0.0")# 允許跨域請求
from fastapi.middleware.cors import CORSMiddlewareapp.add_middleware(CORSMiddleware,allow_origins=["*"],allow_credentials=True,allow_methods=["*"],allow_headers=["*"],
)# ---------------------- 數據庫配置 ----------------------
DB_CONFIG = {'host': 'localhost','user': 'root','password': '123456','db': 'db_school','charset': 'utf8mb4','cursorclass': pymysql.cursors.DictCursor
}# ---------------------- Pydantic 模型 ----------------------
class CommentCreate(BaseModel):user_id: intcontent: strstatus: str = 'visible'class ReplyCreate(BaseModel):user_id: intcontent: strstatus: str = 'visible'class SubReplyCreate(BaseModel):user_id: intreply_to_user_id: intcontent: strstatus: str = 'visible'# ---------------------- 數據庫操作核心函數 ----------------------
def execute_query(query: str, params=None, fetch: bool = True) -> Optional[List[Dict]]:"""執行 SQL 查詢并返回結果"""connection = pymysql.connect(**DB_CONFIG)try:with connection.cursor() as cursor:cursor.execute(query, params)result = cursor.fetchall() if fetch else Noneconnection.commit()return resultexcept Exception as e:connection.rollback()raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,detail=f"數據庫操作失敗: {str(e)}")finally:connection.close()# ---------------------- API 端點 ----------------------
@app.get("/comments", response_model=List[Dict], summary="獲取所有評論")
def get_all_comments():"""獲取所有評論(按時間倒序排列),包含:- 評論基本信息- 關聯的用戶名"""try:query = """SELECT c.*, u.username AS user_username FROM comments cJOIN users u ON c.user_id = u.idORDER BY c.created_at DESC"""comments = execute_query(query)return [{k: v.isoformat() if isinstance(v, datetime) else v for k, v in item.items()}for item in comments]except Exception as e:raise HTTPException(status_code=500, detail=str(e))@app.get("/comments/{comment_id}", response_model=Dict, summary="獲取評論詳情")
def get_comment_detail(comment_id: int):"""獲取指定評論的完整信息,包含:- 評論基本信息- 所有直接回復- 每個回復的子回復"""try:# 獲取基礎評論信息comment_query = """SELECT c.*, u.username AS user_username FROM comments cJOIN users u ON c.user_id = u.idWHERE c.id = %sORDER BY c.created_at ASC"""comment = execute_query(comment_query, (comment_id,))if not comment:raise HTTPException(status_code=404, detail="評論不存在")comment_data = comment[0]# 獲取關聯回復replies_query = """SELECT r.*, u.username AS user_usernameFROM replies rJOIN users u ON r.user_id = u.idWHERE r.comment_id = %sORDER BY r.created_at ASC"""replies = execute_query(replies_query, (comment_id,))# 批量獲取子回復sub_replies_dict = {}if replies:reply_ids = tuple(reply["id"] for reply in replies)sub_query = """SELECT sr.*, u.username AS user_username,ru.username AS reply_to_usernameFROM sub_replies srJOIN users u ON sr.user_id = u.idJOIN users ru ON sr.reply_to_user_id = ru.idWHERE sr.reply_id IN %sORDER BY sr.created_at ASC"""sub_replies = execute_query(sub_query, (reply_ids,))for sr in sub_replies:sub_replies_dict.setdefault(sr["reply_id"], []).append(sr)# 構建嵌套結構comment_data["replies"] = []for reply in replies:reply["sub_replies"] = sub_replies_dict.get(reply["id"], [])comment_data["replies"].append(reply)# 轉換日期格式def convert_dates(obj):if isinstance(obj, datetime):return obj.isoformat()return objreturn {k: convert_dates(v) for k, v in comment_data.items()}except HTTPException as he:raise heexcept Exception as e:raise HTTPException(status_code=500, detail=f"服務器錯誤: {str(e)}")@app.post("/comments", status_code=status.HTTP_201_CREATED, summary="創建新評論")
def create_comment(comment: CommentCreate):"""創建新的評論條目"""try:query = "INSERT INTO comments (user_id, content, status) VALUES (%s, %s, %s)"execute_query(query, (comment.user_id, comment.content, comment.status), fetch=False)return {"message": "評論創建成功"}except Exception as e:raise HTTPException(status_code=500, detail=str(e))@app.post("/comments/{comment_id}/replies", status_code=201, summary="創建回復")
def create_reply(comment_id: int, reply: ReplyCreate):"""在指定評論下創建回復"""try:query = "INSERT INTO replies (comment_id, user_id, content, status) VALUES (%s, %s, %s, %s)"execute_query(query, (comment_id, reply.user_id, reply.content, reply.status), fetch=False)return {"message": "回復創建成功"}except Exception as e:raise HTTPException(status_code=500, detail=str(e))@app.post("/replies/{reply_id}/subreplies", status_code=201, summary="創建子回復")
def create_subreply(reply_id: int, subreply: SubReplyCreate):"""在指定回復下創建子回復"""try:query = """INSERT INTO sub_replies (reply_id, user_id, reply_to_user_id, content, status) VALUES (%s, %s, %s, %s, %s)"""params = (reply_id, subreply.user_id, subreply.reply_to_user_id, subreply.content, subreply.status)execute_query(query, params, fetch=False)return {"message": "子回復創建成功"}except Exception as e:raise HTTPException(status_code=500, detail=str(e))if __name__ == "__main__":import uvicornuvicorn.run(app, host="0.0.0.0", port=8000)

step2.1:fastapi 測試腳本

from typing import Dict, List, Optional
from collections import defaultdict
import json
import pymysql.cursors
from datetime import datetime  # 新增導入# 數據庫連接配置
DB_CONFIG = {'host': 'localhost','user': 'root','password': '123456','db': 'db_school','charset': 'utf8mb4','cursorclass': pymysql.cursors.DictCursor
}# ---------------------- 通用數據庫操作函數 ----------------------
def execute_query(query: str, params=None, fetch: bool = True) -> Optional[List[Dict]]:"""執行 SQL 查詢并返回結果"""connection = pymysql.connect(**DB_CONFIG)try:with connection.cursor() as cursor:cursor.execute(query, params)result = cursor.fetchall() if fetch else Noneconnection.commit()return resultexcept Exception as e:connection.rollback()raise RuntimeError(f"數據庫操作失敗: {str(e)}")finally:connection.close()def get_comment_with_replies(comment_id: int) -> Optional[Dict]:try:# 查詢基礎評論信息comment_query = """SELECT c.*, u.username AS user_username FROM comments cJOIN users u ON c.user_id = u.idWHERE c.id = %s"""comment = execute_query(comment_query, (comment_id,))if not comment:return Nonecomment_data = comment[0]# 轉換datetime字段為字符串def convert_datetime(obj):if isinstance(obj, datetime):return obj.isoformat()return objcomment_data = {k: convert_datetime(v) for k, v in comment_data.items()}comment_data["replies"] = []# 查詢關聯回復replies_query = """SELECT r.*, u.username AS user_usernameFROM replies rJOIN users u ON r.user_id = u.idWHERE r.comment_id = %s"""replies = execute_query(replies_query, (comment_id,))# 批量查詢子回復reply_ids = [reply["id"] for reply in replies]sub_replies_dict = defaultdict(list)if reply_ids:sub_query = """SELECT sr.*, u.username AS user_username,ru.username AS reply_to_usernameFROM sub_replies srJOIN users u ON sr.user_id = u.idJOIN users ru ON sr.reply_to_user_id = ru.idWHERE sr.reply_id IN %s"""sub_replies = execute_query(sub_query, (tuple(reply_ids),))for sr in sub_replies:sr = {k: convert_datetime(v) for k, v in sr.items()}sub_replies_dict[sr["reply_id"]].append(sr)# 構建嵌套結構for reply in replies:reply = {k: convert_datetime(v) for k, v in reply.items()}reply["sub_replies"] = sub_replies_dict.get(reply["id"], [])comment_data["replies"].append(reply)return comment_dataexcept Exception as e:raise RuntimeError(f"查詢失敗: {str(e)}")
# ---------------------- 新增數據插入函數 ----------------------
def insert_comment(user_id: int, content: str, status: str = 'visible') -> None:"""插入評論數據"""query = "INSERT INTO comments (user_id, content, status) VALUES (%s, %s, %s)"params = (user_id, content, status)execute_query(query, params, fetch=False)def insert_reply(comment_id: int, user_id: int, content: str, status: str = 'visible') -> None:"""插入回復數據"""query = "INSERT INTO replies (comment_id, user_id, content, status) VALUES (%s, %s, %s, %s)"params = (comment_id, user_id, content, status)execute_query(query, params, fetch=False)def insert_sub_reply(reply_id: int, user_id: int, reply_to_user_id: int, content: str, status: str = 'visible') -> None:"""插入子回復數據"""query = """INSERT INTO sub_replies (reply_id, user_id, reply_to_user_id, content, status) VALUES (%s, %s, %s, %s, %s)"""params = (reply_id, user_id, reply_to_user_id, content, status)execute_query(query, params, fetch=False)# 1. 新增獲取所有評論的函數
def get_all_comments() -> Optional[List[Dict]]:"""獲取所有評論及其用戶信息,按創建時間倒序排列"""try:query = """SELECT c.*, u.username AS user_username FROM comments cJOIN users u ON c.user_id = u.idORDER BY c.created_at DESC"""comments = execute_query(query)if not comments:return []# 轉換datetime字段為字符串converted = []for comment in comments:converted_comment = {k: v.isoformat() if isinstance(v, datetime) else vfor k, v in comment.items()}converted.append(converted_comment)return convertedexcept Exception as e:raise RuntimeError(f"獲取所有評論失敗: {str(e)}")
# 2. 新增根據ID獲取評論的函數
def get_comment_by_id(comment_id: int) -> Optional[Dict]:"""根據評論ID獲取單個評論信息"""try:query = """SELECT c.*, u.username AS user_username FROM comments cJOIN users u ON c.user_id = u.idWHERE c.id = %s"""result = execute_query(query, (comment_id,))if not result:return Nonecomment = result[0]# 轉換datetime字段為字符串converted_comment = {k: v.isoformat() if isinstance(v, datetime) else vfor k, v in comment.items()}return converted_commentexcept Exception as e:raise RuntimeError(f"獲取評論失敗: {str(e)}")
# 使用示例
if __name__ == "__main__":try:# 插入示例評論insert_comment(1, '這是用戶1的評論內容,歡迎大家討論!')insert_comment(10, '用戶10的最后一個評論。')# 插入示例回復insert_reply(1, 2, '用戶2回復用戶1:同意你的觀點!')insert_reply(9, 1, '用戶1回復用戶9:測試回復。')# 插入示例子回復insert_sub_reply(1, 3, 2, '用戶3回復用戶2:具體哪里同意?')insert_sub_reply(9, 2, 10, '用戶2回復用戶10:我不同意。')print("數據插入成功")comment = get_comment_with_replies(21)print("comment_wrs:",comment)print(json.dumps(comment, indent=2, ensure_ascii=False))# 測試獲取所有評論print("=== 所有評論 ===")all_comments = get_all_comments()print(json.dumps(all_comments, indent=2, ensure_ascii=False))# 測試根據ID獲取評論print("\n=== 評論ID=1 ===")comment = get_comment_by_id(1)print(json.dumps(comment, indent=2, ensure_ascii=False))except RuntimeError as e:print(f"操作失敗: {str(e)}")except Exception as e:print(f"未知錯誤: {str(e)}")

step3:postman

接口1:查詢所有評論
方法:GET
http://localhost:8000/comments
接口2:根據ID查詢評論
方法:GET
http://localhost:8000/comments/1
接口3:創建新評論:
POST http://localhost:8000/comments
Content-Type: application/json
{"user_id": 1,"content": "大飛來了"
}{"message": "評論創建成功"
}
在評選詳情頁面 
1. 新增一個按鈕,開始回復
2.點擊開始回復按鈕,出現彈窗,
3.輸入評論的內容,狀態默認visible
4.開始調用后端的post請求接口
5.請求成功刷新頁面接口4:在指定評論下創建回復:post http://localhost:8000/comments/1/replies
{"user_id": 2,"content": "我提議 今天我們三兄弟 桃園結義吧","status": "visible"
}{"message": "回復創建成功"
}
接口5:創建子回復:
POST  
http://localhost:8000/replies/1/subreplies
{"user_id": 3,"reply_to_user_id": 2,"content": "大哥,關某愿意追隨大哥,生死與共"
}
{"message": "子回復創建成功"
}

step4:評論頁C:\Users\wangrusheng\PycharmProjects\untitled\src\app\user\user.component.ts

// user.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';interface Comment {id: number;user_id: number;content: string;status: string;created_at: string;updated_at: string;user_username: string;
}
// 定義傳遞值接口
interface DialogParams {flag: boolean;message: string;count: number;
}@Component({selector: 'app-user',standalone: true,imports: [CommonModule, RouterModule, FormsModule, ReactiveFormsModule],templateUrl: './user.component.html',styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {comments: Comment[] = [];isLoading = false;errorMessage = '';// 分頁相關屬性currentPage = 1;itemsPerPage = 5;totalItems = 0;showReplyModal = false;newReplyContent = '';selectedStatus: 'visible' | 'deleted' | 'hidden' = 'visible';newUserId: number | null = null;newReplyUserId: number | null = null;submitError = '';isSubmitting = false;// 修改為對象類型存儲多個值passedData: DialogParams = {flag: false,message: '',count: 0};constructor(private http: HttpClient) {}ngOnInit(): void {this.loadComments();}loadComments(page = 1): void {this.isLoading = true;this.currentPage = page;const params = new HttpParams().set('page', page.toString()).set('page_size', this.itemsPerPage.toString());this.http.get<Comment[]>('http://localhost:8000/comments', { params }).subscribe({next: (response) => {// 實際開發中應從接口返回分頁信息,這里模擬分頁this.totalItems = response.length;this.comments = response.slice((this.currentPage - 1) * this.itemsPerPage,this.currentPage * this.itemsPerPage);this.isLoading = false;console.log("UserComponent-coments:",this.comments)},error: (err) => {this.errorMessage = '加載評論失敗,請稍后重試';this.isLoading = false;console.error('API Error:', err);}});}get totalPages(): number {return Math.ceil(this.totalItems / this.itemsPerPage);}prevPage(): void {if (this.currentPage > 1) {this.currentPage--;this.loadComments(this.currentPage);}}nextPage(): void {if (this.currentPage < this.totalPages) {this.currentPage++;this.loadComments(this.currentPage);}}// 修改方法接收對象參數openReplyModal(params: DialogParams = { flag: false, message: '默認消息', count: 0 }): void {this.showReplyModal = true;this.newReplyContent = '';this.selectedStatus = 'visible';this.newUserId = null;this.newReplyUserId = null;this.submitError = '';this.passedData = { ...params }; // 使用展開運算符保持數據不可變}closeReplyModal(): void {this.showReplyModal = false;this.passedData = { flag: false, message: '', count: 0 };}submitReply(): void {if (!this.newUserId || isNaN(this.newUserId)) {this.submitError = '請輸入有效的用戶ID';return;}if (!this.newReplyContent.trim()) {this.submitError = '請輸入回復內容';return;}this.isSubmitting = true;const url2 = `http://localhost:8000/comments`;const body = {user_id: this.newUserId,content: this.newReplyContent};console.log('submitReply_body:', body);this.http.post(url2, body).subscribe({next: () => {this.isSubmitting = false;this.showReplyModal = false;this.loadComments(); // 刷新數據},error: (err) => {this.isSubmitting = false;this.submitError = '提交失敗,請稍后重試';console.error('子回復創建失敗:', err);}});}}
<!-- user.component.html -->
<div class="comments-container"><!-- 加載狀態 --><div *ngIf="isLoading" class="loading"><div class="spinner"></div>正在加載評論...</div><!-- 錯誤提示 --><div *ngIf="errorMessage" class="error">{{ errorMessage }}<button (click)="loadComments()">重試</button></div><!-- 評論列表 --><div *ngIf="!isLoading && !errorMessage"><h2>用戶評論(共 {{ totalItems }} 條)</h2><button class="detail-btn"(click)="openReplyModal({flag: false, message: '普通消息', count: 10})">新增評論</button><div class="comment-list"><div *ngFor="let comment of comments" class="comment-card"><div class="comment-header"><span class="username">{{ comment.user_username }}</span><span class="time">{{ comment.created_at | date: 'yyyy-MM-dd HH:mm' }}</span></div><p class="content">{{ comment.content }}</p><div class="comment-footer"><button[routerLink]="['/comments', comment.id]"class="detail-btn">查看詳情</button></div></div></div><!-- 分頁控件 --><div class="pagination"><button(click)="prevPage()"[disabled]="currentPage === 1">上一頁</button><span class="page-info">{{ currentPage }}/{{ totalPages }}</span><button(click)="nextPage()"[disabled]="currentPage === totalPages">下一頁</button></div></div><!-- 彈窗內容 --><div class="modal-overlay" *ngIf="showReplyModal"><div class="modal-content"><h3>創建評論</h3><!-- 修改展示多個參數 --><div class="passed-values" *ngIf="this.passedData.flag"><div>接收到的參數:</div><div>Flag: {{ passedData.flag ? 'TRUE' : 'FALSE' }}</div><div>Message: {{ passedData.message }}</div><div>Count: {{ passedData.count }}</div></div><form (ngSubmit)="submitReply()"><!-- 原有表單內容保持不變 --><div class="form-group"><label>回復內容:</label><textarea[(ngModel)]="newReplyContent"name="content"requiredrows="4"></textarea></div><div class="form-group"  *ngIf="this.passedData.flag"><label>狀態:</label><select[(ngModel)]="selectedStatus"name="status"class="status-select"><option value="visible">Visible</option><option value="deleted">Deleted</option><option value="hidden">Hidden</option></select></div><div class="form-group"><label>(user_id)回復用戶ID</label><inputtype="number"[(ngModel)]="newUserId"name="userId"requiredclass="user-id-input"></div><div class="form-group"  *ngIf="this.passedData.flag"><label>(reply_to_user_id)被回復用戶ID</label><inputtype="number"[(ngModel)]="newReplyUserId"name="userId"requiredclass="user-id-input"></div><div *ngIf="submitError" class="error-message">{{ submitError }}</div><div class="button-group"><buttontype="button"(click)="closeReplyModal()"class="cancel-btn">取消</button><buttontype="submit"[disabled]="isSubmitting"class="submit-btn">{{ isSubmitting ? '提交中...' : '提交' }}</button></div></form></div></div></div>
/* user.component.css */
.comments-container {max-width: 800px;margin: 2rem auto;padding: 0 1rem;
}.loading {text-align: center;padding: 2rem;color: #666;
}.spinner {display: inline-block;width: 2rem;height: 2rem;border: 3px solid #f3f3f3;border-radius: 50%;border-top-color: #2196F3;animation: spin 1s linear infinite;margin-bottom: 1rem;
}@keyframes spin {to { transform: rotate(360deg); }
}.error {background: #ffebee;color: #b71c1c;padding: 1rem;border-radius: 4px;text-align: center;
}.comment-list {margin-top: 1.5rem;
}.comment-card {background: white;border-radius: 8px;padding: 1.5rem;margin-bottom: 1rem;box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}.comment-header {display: flex;justify-content: space-between;margin-bottom: 0.5rem;font-size: 0.9rem;color: #666;
}.content {font-size: 1.1rem;line-height: 1.6;color: #333;
}.pagination {display: flex;justify-content: center;align-items: center;gap: 1rem;margin: 2rem 0;
}button {padding: 0.5rem 1.5rem;border: 1px solid #ddd;border-radius: 4px;background: #f5f5f5;cursor: pointer;transition: all 0.2s;
}button:hover:not(:disabled) {background: #2196F3;color: white;border-color: transparent;
}button:disabled {opacity: 0.6;cursor: not-allowed;
}.page-info {color: #666;
}.comment-footer {margin-top: 1rem;text-align: right;
}.detail-btn {background: #2196F3;color: white;border: none;padding: 0.5rem 1rem;border-radius: 4px;cursor: pointer;transition: opacity 0.2s;
}.detail-btn:hover {opacity: 0.9;
}
.comment-container {max-width: 800px;margin: 20px auto;padding: 20px;background-color: #f9f9f9;border-radius: 8px;
}.loading, .error {text-align: center;padding: 20px;color: #666;
}.main-comment {background: white;padding: 20px;border-radius: 8px;margin-bottom: 30px;box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}.replies-section {background: white;padding: 20px;border-radius: 8px;box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}.reply-item {margin: 15px 0;padding: 15px;border-left: 3px solid #eee;
}.sub-replies {margin-left: 30px;border-left: 2px solid #ddd;padding-left: 15px;
}.username {font-weight: bold;color: #2c3e50;margin-right: 10px;
}.reply-to {color: #666;font-size: 0.9em;margin: 0 5px;
}.time {color: #95a5a6;font-size: 0.85em;
}.content {margin: 8px 0;color: #34495e;line-height: 1.6;
}
/* 確保容器可見 */
.comment-container {min-height: 300px;  /* 保證最小高度 */position: relative; /* 用于加載層定位 */
}/* 增強加載狀態顯示 */
.loading {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);font-size: 1.2em;
}/* 確保內容層級 */
.main-comment {position: relative;z-index: 1;
}
/* 新增樣式 */
.reply-button {margin-top: 15px;padding: 8px 16px;background-color: #007bff;color: white;border: none;border-radius: 4px;cursor: pointer;
}.modal-overlay {position: fixed;top: 0;left: 0;right: 0;bottom: 0;background-color: rgba(0, 0, 0, 0.5);display: flex;justify-content: center;align-items: center;z-index: 1000;
}.modal-content {background-color: white;padding: 25px;border-radius: 8px;width: 500px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}.form-group {margin-bottom: 15px;
}.form-group label {display: block;margin-bottom: 5px;font-weight: 500;
}.form-group textarea {width: 100%;padding: 8px;border: 1px solid #ddd;border-radius: 4px;
}.status-select {width: 100%;padding: 8px;border: 1px solid #ddd;border-radius: 4px;
}.user-id-input {width: 100%;padding: 8px;border: 1px solid #ddd;border-radius: 4px;
}.error-message {color: #dc3545;margin-bottom: 15px;
}.button-group {display: flex;gap: 10px;justify-content: flex-end;
}.cancel-btn {padding: 8px 16px;background-color: #6c757d;color: white;border: none;border-radius: 4px;cursor: pointer;
}.submit-btn {padding: 8px 16px;background-color: #28a745;color: white;border: none;border-radius: 4px;cursor: pointer;
}.submit-btn:disabled {background-color: #6c757d;cursor: not-allowed;
}
/*fenge分割線*/
/* dialog-test.component.css */
/* 新增樣式 */
.passed-value {margin-bottom: 15px;padding: 10px;border-radius: 4px;font-weight: bold;
}.true-value {background-color: #e8f5e9;color: #2e7d32;
}.false-value {background-color: #ffebee;color: #c62828;
}.reply-button {margin-right: 10px;padding: 8px 16px;
}
/* 新增傳遞值樣式 */
.passed-values {padding: 10px;margin-bottom: 15px;border: 1px solid #ddd;border-radius: 4px;background-color: #f8f9fa;
}.passed-values div:first-child {font-weight: bold;margin-bottom: 8px;
}.passed-values div:not(:first-child) {margin: 4px 0;color: #666;
}

step5:回復頁C:\Users\wangrusheng\PycharmProjects\untitled\src\app\user-detail\user-detail.component.ts

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute } from '@angular/router';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, catchError, throwError } from 'rxjs';
import {FormsModule} from '@angular/forms';// 定義類型接口
interface CommentWithReplies {id: number;user_id: number;content: string;status: string;created_at: string;updated_at: string;user_username: string;replies?: Reply[];
}interface Reply {id: number;comment_id: number;user_id: number;content: string;status: string;created_at: string;updated_at: string;user_username: string;sub_replies?: SubReply[];
}interface SubReply {id: number;reply_id: number;user_id: number;reply_to_user_id: number;content: string;status: string;created_at: string;updated_at: string;user_username: string;reply_to_username: string;
}// 定義傳遞值接口
interface DialogParams {flag: boolean;message: string;count: number;
}@Component({selector: 'app-user-detail',templateUrl: './user-detail.component.html',imports: [CommonModule,FormsModule],styleUrls: ['./user-detail.component.css'],standalone: true
})
export class UserDetailComponent implements OnInit {commentId!: number;replyId!: number;commentDetail!: CommentWithReplies;isLoading = true;errorMessage = '';// 新增變量showReplyModal = false;newReplyContent = '';selectedStatus: 'visible' | 'deleted' | 'hidden' = 'visible';newUserId: number | null = null;newReplyUserId: number | null = null;submitError = '';isSubmitting = false;// 新增傳遞值變量// 修改為對象類型存儲多個值passedData: DialogParams = {flag: false,message: '',count: 0};constructor(private route: ActivatedRoute,private http: HttpClient) {}ngOnInit(): void {this.commentId = Number(this.route.snapshot.paramMap.get('id'));this.loadCommentDetail();}private loadCommentDetail(): void {this.http.get<CommentWithReplies>(`http://localhost:8000/comments/${this.commentId}`).pipe(catchError(this.handleError)).subscribe({next: (response) => {// 如果接口返回的是包裹對象(根據實際情況選擇)// this.commentDetail = response.data;// 如果直接返回數據對象this.commentDetail = response;this.isLoading = false;},error: (err) => {this.errorMessage = '加載評論詳情失敗,請稍后重試';this.isLoading = false;console.error('獲取評論詳情失敗:', err);}});}private handleError(error: HttpErrorResponse) {let errorMessage = '發生未知錯誤';if (error.error instanceof ErrorEvent) {errorMessage = `客戶端錯誤:${error.error.message}`;} else {errorMessage = `服務端錯誤:${error.status}\n${error.message}`;}return throwError(() => new Error(errorMessage));}// 新增方法openReplyModal(params: DialogParams = { flag: false, message: '默認消息', count: 0 }): void {this.showReplyModal = true;this.newReplyContent = '';this.selectedStatus = 'visible';this.newUserId = null;this.newReplyUserId = null;this.submitError = '';this.passedData = { ...params }; // 使用展開運算符保持數據不可變}closeReplyModal(): void {this.showReplyModal = false;}submitReply(): void {if (!this.newUserId || isNaN(this.newUserId)) {this.submitError = '請輸入有效的用戶ID';return;}if (!this.newReplyContent.trim()) {this.submitError = '請輸入回復內容';return;}this.isSubmitting = true;if (this.passedData.flag) {const url2 = `http://localhost:8000/replies/${this.passedData.count}/subreplies`;const body = {user_id: this.newUserId,content: this.newReplyContent,reply_to_user_id: this.newReplyUserId,status: this.selectedStatus};console.log('submitReply_body:', body);this.http.post(url2, body).subscribe({next: () => {this.isSubmitting = false;this.showReplyModal = false;this.loadCommentDetail(); // 刷新數據},error: (err) => {this.isSubmitting = false;this.submitError = '提交失敗,請稍后重試';console.error('子回復創建失敗:', err);}});} else {const url = `http://localhost:8000/comments/${this.commentId}/replies`;const body = {user_id: this.newUserId,content: this.newReplyContent,status: this.selectedStatus};this.http.post(url, body).subscribe({next: () => {this.isSubmitting = false;this.showReplyModal = false;this.loadCommentDetail(); // 刷新數據},error: (err) => {this.isSubmitting = false;this.submitError = '提交失敗,請稍后重試';console.error('回復創建失敗:', err);}});}}
}
<div class="comment-container"><!-- 加載狀態 --><div *ngIf="isLoading" class="loading">加載中...</div><!-- 錯誤提示 --><div *ngIf="errorMessage" class="error">{{ errorMessage }}</div><!-- 評論詳情 --><div *ngIf="commentDetail && !isLoading"><div class="main-comment"><h2>評論詳情</h2><div class="comment-header"><span class="username">{{ commentDetail.user_username }}</span><span class="time">{{ commentDetail.created_at | date:'yyyy-MM-dd HH:mm' }}</span></div><p class="content">{{ commentDetail.content }}</p><button class="reply-button" (click)="openReplyModal({flag: false, message: '普通消息', count: 0})">開始回復</button></div><!-- 回復列表 --><div class="replies-section"><h3>全部回復({{ commentDetail.replies?.length || 0 }}</h3><div class="reply-list"><!-- 主回復 --><div *ngFor="let reply of commentDetail.replies" class="reply-item"><div class="reply-main"><div class="reply-header"><span class="username">{{ reply.user_username }}</span><span class="time">{{ reply.created_at | date:'yyyy-MM-dd HH:mm' }}</span></div><p class="content">{{ reply.content }}</p><!-- 新增的回復按鈕 --><button class="reply-button" (click)="openReplyModal({flag: true, message: '重要消息', count: reply.id})">開始回復</button></div><!-- 子回復 --><div *ngIf="reply.sub_replies?.length" class="sub-replies"><div *ngFor="let subReply of reply.sub_replies" class="sub-reply-item"><div class="reply-header"><span class="username">{{ subReply.user_username }}</span><span class="reply-to">回復 {{ subReply.reply_to_username }}</span><span class="time">{{ subReply.created_at | date:'yyyy-MM-dd HH:mm' }}</span></div><p class="content">{{ subReply.content }}</p></div></div></div></div></div></div><!-- 新增回復彈窗 --><div class="modal-overlay" *ngIf="showReplyModal"><div class="modal-content"><h3>創建回復</h3><form (ngSubmit)="submitReply()"><div class="form-group"><label>回復內容:</label><textarea[(ngModel)]="newReplyContent"name="content"requiredrows="4"></textarea></div><div class="form-group"><label>狀態:</label><select[(ngModel)]="selectedStatus"name="status"class="status-select"><option value="visible">Visible</option><option value="deleted">Deleted</option><option value="hidden">Hidden</option></select></div><div class="form-group"><label>(user_id)回復用戶ID</label><inputtype="number"[(ngModel)]="newUserId"name="userId"requiredclass="user-id-input"></div><div class="form-group" *ngIf="this.passedData.flag"><label>(reply_to_user_id)被回復用戶ID</label><inputtype="number"[(ngModel)]="newReplyUserId"name="userId"requiredclass="user-id-input"></div><div *ngIf="submitError" class="error-message">{{ submitError }}</div><div class="button-group"><buttontype="button"(click)="closeReplyModal()"class="cancel-btn">取消</button><buttontype="submit"[disabled]="isSubmitting"class="submit-btn">{{ isSubmitting ? '提交中...' : '提交' }}</button></div></form></div></div>
</div>
.comment-container {max-width: 800px;margin: 20px auto;padding: 20px;background-color: #f9f9f9;border-radius: 8px;
}.loading, .error {text-align: center;padding: 20px;color: #666;
}.main-comment {background: white;padding: 20px;border-radius: 8px;margin-bottom: 30px;box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}.replies-section {background: white;padding: 20px;border-radius: 8px;box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}.reply-item {margin: 15px 0;padding: 15px;border-left: 3px solid #eee;
}.sub-replies {margin-left: 30px;border-left: 2px solid #ddd;padding-left: 15px;
}.username {font-weight: bold;color: #2c3e50;margin-right: 10px;
}.reply-to {color: #666;font-size: 0.9em;margin: 0 5px;
}.time {color: #95a5a6;font-size: 0.85em;
}.content {margin: 8px 0;color: #34495e;line-height: 1.6;
}
/* 確保容器可見 */
.comment-container {min-height: 300px;  /* 保證最小高度 */position: relative; /* 用于加載層定位 */
}/* 增強加載狀態顯示 */
.loading {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);font-size: 1.2em;
}/* 確保內容層級 */
.main-comment {position: relative;z-index: 1;
}
/* 新增樣式 */
.reply-button {margin-top: 15px;padding: 8px 16px;background-color: #007bff;color: white;border: none;border-radius: 4px;cursor: pointer;
}.modal-overlay {position: fixed;top: 0;left: 0;right: 0;bottom: 0;background-color: rgba(0, 0, 0, 0.5);display: flex;justify-content: center;align-items: center;z-index: 1000;
}.modal-content {background-color: white;padding: 25px;border-radius: 8px;width: 500px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}.form-group {margin-bottom: 15px;
}.form-group label {display: block;margin-bottom: 5px;font-weight: 500;
}.form-group textarea {width: 100%;padding: 8px;border: 1px solid #ddd;border-radius: 4px;
}.status-select {width: 100%;padding: 8px;border: 1px solid #ddd;border-radius: 4px;
}.user-id-input {width: 100%;padding: 8px;border: 1px solid #ddd;border-radius: 4px;
}.error-message {color: #dc3545;margin-bottom: 15px;
}.button-group {display: flex;gap: 10px;justify-content: flex-end;
}.cancel-btn {padding: 8px 16px;background-color: #6c757d;color: white;border: none;border-radius: 4px;cursor: pointer;
}.submit-btn {padding: 8px 16px;background-color: #28a745;color: white;border: none;border-radius: 4px;cursor: pointer;
}.submit-btn:disabled {background-color: #6c757d;cursor: not-allowed;
}

end

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/74389.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/74389.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/74389.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

C++11QT復習 (三)

文章目錄 [toc]Day5-2 文件IO&#xff08;2025.03.24&#xff09;1. 緩沖區與刷新1.1 常見的緩沖刷新方式 2. 文件讀寫操作2.1 讀取文件2.2 寫入文件2.3 追加模式寫入2.3 完整代碼 3. 文件定位操作4. 字符串IO5. 配置文件解析示例6. 完整代碼7. 二進制文件操作總結 Day5-2 文件…

Redis Sentinel 詳解

Redis Sentinel 詳解 1. 什么是 Redis Sentinel&#xff1f;有什么用&#xff1f; Redis Sentinel&#xff08;哨兵&#xff09; 是 Redis 官方提供的高可用性解決方案&#xff0c;主要用于監控、通知和自動故障轉移。當 Redis 主節點&#xff08;master&#xff09;發生故障…

AI日報 - 2025年3月25日

&#x1f31f; 今日概覽&#xff08;60秒速覽&#xff09; ▎&#x1f916; AGI突破 | Nebula&#xff08;Google Gemini 2.0 Pro&#xff09;破解復雜數學謎題 編碼與推理能力再上新臺階 ▎&#x1f4bc; 商業動向 | Sesame AI開源10億參數語音模型CSM-1B 語音AI進入普惠時代 …

AI醫療革命:英偉達GTC 2025醫療健康與生命科學會議全分析

AI醫療革命:英偉達GTC 2025醫療健康與生命科學會議全分析 一、GTC 2025:AI 醫療的算力與生態雙突破 1.1 黃仁勛演講核心:從訓練到推理的代際跨越 在科技界矚目的英偉達 GTC 2025 大會上,英偉達 CEO 黃仁勛的主題演講成為全場焦點,為 AI 醫療領域帶來了極具變革性的消息。…

【機器學習/大模型/八股文 面經 (一)】

1. PPO算法中使用GAE的好處以及參數γ和λ的作用是什么? 參考答案: GAE(Generalized Advantage Estimation) 的優勢在于通過指數加權多步TD誤差,平衡優勢估計的偏差與方差,提升策略優化的穩定性。γ(折扣因子):控制未來獎勵的衰減程度,值越大表示更關注長期收益。λ…

03 Python 基礎:數據類型、運算符與流程控制解析

文章目錄 一、數據類型 內置的六大類數字類型整數類型 int浮點數 float布爾 bool字符串 str 變量命名 二、數字類型的相互轉換顯式類型的轉換整數&#xff0c;浮點數&#xff0c;復數 之間的顯式轉換 隱式類型的轉換 三、標識符算術運算符比較運算符邏輯運算符位運算符賦值運算…

視頻知識庫初步設想

將視頻字幕提取出來作為知識庫來源定位,下一步設想:把視頻上的圖片信息也精簡出來作為定位。 下面是測試例子: 入參: {"model":"deepseek-ai/DeepSeek-R1-Distill-Llama-8B","messages":[{"role":"system","cont…

數據庫原理13

1.關系模式設計不當引起的問題&#xff1a;數據冗余&#xff1b;更新異常&#xff1b;插入異常&#xff1b;刪除異常 2.外碼可以是單個屬性&#xff0c;也可以是屬性組 3.動態SQL是SQL標準提供的一種語句運行機制 4.若一個模式分解保持函數依賴&#xff0c;則該分解一定具有…

初級:異常處理面試題深度解析

一、引言 在Java開發中&#xff0c;異常處理是確保程序健壯性和穩定性的重要機制。面試官通過相關問題考察候選人對異常處理的理解和運用能力&#xff0c;以及在實際開發中處理異常的經驗。本文將深入剖析常見的異常處理面試題&#xff0c;結合實際開發場景&#xff0c;幫助讀…

Apache Spark - 用于大規模數據分析的統一引擎

Apache Spark - 用于大規模數據分析的統一引擎 下載運行示例和 Shell使用 Spark Connect 在 Anywhere 上運行 Spark 客戶端應用程序 在集群上啟動從這里去哪里使用 Spark Shell 進行交互式分析基本有關數據集作的更多信息緩存 自包含應用程序從這里去哪里 Apache Spark 是用于大…

餐飲管理系統的設計與實現(代碼+數據庫+LW)

摘 要 互聯網發展至今&#xff0c;無論是其理論還是技術都已經成熟&#xff0c;而且它廣泛參與在社會中的方方面面。它讓信息都可以通過網絡傳播&#xff0c;搭配信息管理工具可以很好地為人們提供服務。針對信息管理混亂&#xff0c;出錯率高&#xff0c;信息安全性差&#…

【C#】Winform調用NModbus實現Modbus TCP 主站通訊

一、前言 Modbus是一種串行通信協議&#xff0c;是工業領域全球最流行的協議之一。 1.1 環境 系統&#xff1a;Win11 工具&#xff1a;Visual Studio 2022 .Net 版本&#xff1a;.Net Framework4.6.0 依賴庫&#xff1a;NModbus 3.0.81 1.2 協議類型 Modbus RTU&#xff1a;一…

【leetcode題解】貪心算法

目錄 貪心算法 檸檬水找零 將數組和減半的最少操作次數 最大數 擺動序列 最長遞增子序列 遞增的三元子序列 最長連續遞增序列 買賣股票的最佳時機 買賣股票的最佳時機 II K 次取反后最大化的數組和 按身高排序 優勢洗牌 最長回文串 增減字符串匹配 分發餅干 最…

Langchain4J框架相關面試題

以下是關于Langchain4J框架的面試題目及答案 ### Langchain4J基礎概念類 1. **Langchain4J框架是什么&#xff1f;它的核心功能有哪些&#xff1f;** Langchain4J是一個用于構建語言模型應用的Java框架&#xff0c;它為開發者提供了一套簡潔高效的API&#xff0c;使得在Jav…

Apache Doris

Apache Doris介紹 Apache Doris 是一個基于 MPP 架構的高性能、實時的分析型數據庫&#xff0c;以極速易用的特點被人們所熟知&#xff0c;僅需亞秒級響應時間即可返回海量數據下的查詢結果&#xff0c;不僅可以支持高并發的點查詢場景&#xff0c;也能支持高吞吐的復雜分析場…

VLAN間通信

目錄 第一步&#xff1a;配vlan 第二步&#xff1a;配置核心vlanif,MAC地址信息。 第三步&#xff1a;ospf協議 三層交換機&#xff08;匯聚層&#xff09;: 對于交換機、路由器、防火墻等網絡設備而言&#xff0c;接口類型一般存在兩種&#xff1a;二層接口&#xff0c;三…

LeetCode熱題100精講——Top2:字母異位詞分組【哈希】

你好&#xff0c;我是安然無虞。 文章目錄 題目背景字母異位詞分組C解法Python解法 題目背景 如果大家對于 哈希 類型的概念并不熟悉, 可以先看我之前為此專門寫的算法詳解: 藍橋杯算法競賽系列第九章巧解哈希題&#xff0c;用這3種數據類型足矣 字母異位詞分組 題目鏈接&am…

基于python+django的圖書借閱網站-圖書借閱管理系統源碼+運行步驟

該系統是基于pythondjango開發的在線圖書借閱管理系統。系統適合場景&#xff1a;大學生、課程作業、系統設計、畢業設計。 演示地址 前臺地址&#xff1a; http://book.gitapp.cn 后臺地址&#xff1a;http://book.gitapp.cn/#/admin 后臺管理帳號&#xff1a; 用戶名&…

uni-app集成保利威直播、點播SDK經驗FQ(二)|小程序直播/APP直播開發適用

通過uniapp集成保利威直播、點播SDK來開發小程序/APP的視頻直播能力&#xff0c;在實際開發中可能會遇到的疑問和解決方案&#xff0c;下篇。更多疑問請咨詢19924784795。 1.ios不能后臺掛起uniapp插件 ios端使用后臺音頻播放和畫中畫功能&#xff0c;沒有在 manifest.json 進…

數據庫三級填空+應用題(1)

填空 35【答案】TOP 3 WITH TIES 【解析】希望選出商品數量最多的前3類商品&#xff0c;并獲得相應的商品類別和數量。with ties一般是和Top 、 order by相結合使用,表示包括與最后一行order by后面的參數取值并列的結果。 36在SQL Server 2008中&#xff0c;每個數據頁可存儲8…