frontend: add community
Change-Id: I929c21d82ddab39d8b210b229ff7559320c1d853
diff --git "a/src/app/community/community-detail/\133communityId\135/page.tsx" "b/src/app/community/community-detail/\133communityId\135/page.tsx"
new file mode 100644
index 0000000..40200f7
--- /dev/null
+++ "b/src/app/community/community-detail/\133communityId\135/page.tsx"
@@ -0,0 +1,289 @@
+'use client';
+
+import React, { useEffect, useState, useRef } from "react";
+import { InputText } from 'primereact/inputtext';
+import { Button } from 'primereact/button';
+import { Card } from 'primereact/card';
+import { Image } from 'primereact/image';
+import { Dropdown } from 'primereact/dropdown';
+// 页面跳转
+import { useParams } from 'next/navigation'
+import { useRouter } from 'next/navigation';
+// 分页
+import { Paginator, type PaginatorPageChangeEvent } from 'primereact/paginator';
+// 评分图标
+import { Fire } from '@icon-park/react';
+// 消息提醒
+import { Toast } from 'primereact/toast';
+// 发布帖子
+import { Dialog } from 'primereact/dialog';
+import { FileUpload } from 'primereact/fileupload';
+import { InputTextarea } from 'primereact/inputtextarea';
+// 接口传输
+import axios from 'axios';
+// 防抖函数
+import { debounce } from 'lodash';
+// 样式
+import './resource-community.scss';
+
+// 帖子列表数据
+interface Thread {
+ threadId: number;
+ userId: number;
+ threadPicture: string;
+ title: string;
+ createdAt: string;
+ communityId: string;
+ likes: number;
+}
+interface ThreadList {
+ records: Thread[];
+}
+// 社区信息
+interface CommunityInfo {
+ communityId: string;
+ communityName: string;
+ communityPicture: string;
+ description: string;
+ hot: number;
+ type: string;
+ threadNumber: number;
+}
+
+
+// 社区详情页面
+export default function CommunityDetailPage() {
+ // 获取URL参数,页面跳转
+ const params = useParams<{ communityId: string }>()
+ const communityId = decodeURIComponent(params.communityId); // 防止中文路径乱码
+ const router = useRouter();
+ // 社区数据
+ const [communityInfo, setCommunityInfo] = useState<CommunityInfo | null>(null);
+ // 帖子列表数据
+ const [threads, setThreads] = useState<Thread[]>([]);
+ const [totalThreads, setTotalThreads] = useState<number>(0);
+ // 搜索框
+ const [searchValue, setSearchValue] = useState("");
+ const debouncedSearch = useRef(
+ debounce((value: string) => {
+ setSearchValue(value);
+ }, 600)
+ ).current;
+ // 消息提醒
+ const toast = useRef<Toast>(null);
+ // 筛选器选项数据
+ const [selectedOption, setSelectedOption] = useState({ name: '热度最高' });
+ const options = [
+ { name: '来自好友' },
+ { name: '热度最高' }
+ ];
+ // 分页
+ const [first, setFirst] = useState(0);
+ const [rows, setRows] = useState(6);
+ const onPageChange = (event: PaginatorPageChangeEvent) => {
+ setFirst(event.first);
+ setRows(event.rows);
+ };
+ // 获取社区信息
+ useEffect(() => {
+ const fetchThreadInfo = async () => {
+ try {
+ const { data } = await axios.get(`http://127.0.0.1:4523/m1/6387307-6083949-default/community/info?communityId=${communityId}`);
+ setCommunityInfo(data);
+ setTotalThreads(data.threadNumber);
+ } catch (err) {
+ console.error(err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取社区信息失败' });
+ }
+ };
+ fetchThreadInfo();
+ }, [communityId]);
+
+ // 获取帖子列表
+ useEffect(() => {
+ fetchThreads();
+ }, [communityId, first, rows, selectedOption, searchValue]);
+
+ const fetchThreads = async () => {
+ try {
+ const page = first / rows + 1;
+ console.log("当前页" + page + "size" + rows + "搜索内容" + searchValue);
+ const option = selectedOption.name // 添加排序参数
+ const response = await axios.get<ThreadList>(
+ `http://127.0.0.1:4523/m1/6387307-6083949-default/community/threads`, {
+ params: { communityId, page, rows, option, searchValue }
+ }
+ );
+ console.log('获取帖子列表:', response.data.records);
+ setThreads(response.data.records);
+ } catch (err) {
+ console.error('获取帖子失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取帖子失败' });
+ }
+ };
+
+ // 发布帖子弹窗
+ const [visible, setVisible] = useState(false);
+ const [formData, setFormData] = useState({
+ title: '',
+ content: '',
+ threadPicture: ''
+ });
+ // 图片上传消息通知
+ const onUpload = () => {
+ toast.current?.show({ severity: 'info', summary: 'Success', detail: 'File Uploaded' });
+ };
+
+ // 发帖接口
+ const handleSubmit = async () => {
+ try {
+ const currentDate = new Date().toISOString();
+ const postData = {
+ userId: 22301145, // 记得用户登录状态获取
+ threadPicture: formData.threadPicture,
+ title: formData.title,
+ content: formData.content,
+ createdAt: currentDate,
+ communityId: communityId // 从URL参数获取的社区ID
+ };
+ // 发送POST请求
+ const response = await axios.post('http://127.0.0.1:4523/m1/6387307-6083949-default/thread', postData);
+
+ if (response.status === 200) {
+ toast.current?.show({ severity: 'success', summary: 'Success', detail: '帖子发布成功' });
+ // 发帖成功
+ setVisible(false);
+ // 重置表单
+ setFormData({
+ title: '',
+ content: '',
+ threadPicture: ''
+ });
+ // 可以刷新帖子列表
+ fetchThreads();
+ }
+ } catch (error) {
+ console.error('发帖失败:', error);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '帖子发布失败' });
+ }
+ };
+ return (
+ <div className="resource-community">
+ <Toast ref={toast}></Toast>
+ {/* 社区标题和介绍 */}
+ <div className="community-header">
+ <div className="title-section">
+ <h1>{communityInfo?.communityName}</h1>
+ <p className="subtitle">{communityInfo?.description}</p>
+ <div className="community-states">
+ <div className="state-item">
+ <Fire theme="outline" size="16" fill="#FF8D1A" />
+ <span>热度: {communityInfo?.hot}</span>
+ </div>
+ <div className="state-item">
+ <i className="pi pi-book" />
+ <span>帖子数: {totalThreads}</span>
+ </div>
+ </div>
+ </div>
+ <div className="input">
+ <Button label="返回列表" link onClick={() => router.push(`/community/resource-community-list/${communityInfo?.type}`)} />
+ <div className="action-section">
+ <div className="communities-searchBar">
+ <i className="pi pi-search" />
+ <InputText type="search" className="search-helper" placeholder="搜索你感兴趣的帖子" onChange={(e) => { const target = e.target as HTMLInputElement; debouncedSearch(target.value); }} />
+ </div>
+ <Dropdown
+ value={selectedOption}
+ onChange={(e) => setSelectedOption(e.value)}
+ options={options}
+ optionLabel="name"
+ className="select-dropdown"
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* 帖子列表 */}
+ <div className="thread-list">
+ <div className="resource-grid">
+ {threads.map((thread) => (
+ <Card key={thread.threadId} className="resource-card" onClick={() => router.push(`/community/thread-detail/${thread.threadId}`)}>
+ <Image
+ src={process.env.NEXT_PUBLIC_NGINX_URL + thread.threadPicture}
+ alt={thread.title}
+ width="368"
+ height="200"
+ />
+ <div className="card-content">
+ <h3>{thread.title}</h3>
+ <div className="view-count">
+ <i className="pi pi-face-smile" />
+ <span>{thread.likes}</span>
+ </div>
+ </div>
+ </Card>
+ ))}
+ </div>
+ {totalThreads > 6 && (<Paginator className="Paginator" first={first} rows={rows} totalRecords={totalThreads} rowsPerPageOptions={[6, 12]} onPageChange={onPageChange} />)}
+ </div>
+
+ {/* 添加按钮 */}
+ <Button className="add-resource-button" icon="pi pi-plus" rounded aria-label="add-thread" onClick={() => setVisible(true)} />
+
+ {/* 发布帖子弹窗 */}
+ <Dialog
+ header="发布新帖子"
+ visible={visible}
+ onHide={() => setVisible(false)}
+ className="publish-dialog"
+ modal
+ footer={
+ <div className="dialog-footer">
+ <Button label="发布" icon="pi pi-check" onClick={handleSubmit} autoFocus />
+ <Button label="取消" icon="pi pi-times" onClick={() => setVisible(false)} className="p-button-text" />
+ </div>
+ }
+ >
+ <div className="publish-form">
+ <div className="form-field">
+ <label htmlFor="title">标题</label>
+ <InputText
+ id="title"
+ value={formData.title}
+ onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
+ placeholder="请输入帖子标题"
+ className="w-full"
+ />
+ </div>
+
+ <div className="form-field">
+ <label htmlFor="content">内容</label>
+ <InputTextarea
+ id="content"
+ value={formData.content}
+ onChange={(e) => setFormData(prev => ({ ...prev, content: e.target.value }))}
+ rows={5}
+ placeholder="请输入帖子内容"
+ className="w-full"
+ />
+ </div>
+
+ <div className="form-field">
+ <label>封面图片</label>
+ <FileUpload
+ mode="basic"
+ name="thread-image"
+ url="/file" // 与后端交互的URL
+ accept="image/*"
+ maxFileSize={10000000000}
+ chooseLabel="选择图片"
+ className="w-full"
+ onUpload={onUpload}
+ />
+ </div>
+ </div>
+ </Dialog>
+ </div>
+ );
+}
\ No newline at end of file
diff --git "a/src/app/community/community-detail/\133communityId\135/resource-community.scss" "b/src/app/community/community-detail/\133communityId\135/resource-community.scss"
new file mode 100644
index 0000000..a40ebaa
--- /dev/null
+++ "b/src/app/community/community-detail/\133communityId\135/resource-community.scss"
@@ -0,0 +1,243 @@
+@import '../../../globals.scss';
+
+.resource-community {
+ padding: 2rem;
+ position: relative;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 2rem;
+
+ .community-header {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ margin-bottom: 2rem;
+
+ .title-section {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ align-items: flex-start;
+
+ h1 {
+ font-size: 3rem;
+ margin: 0;
+ margin-top: 32px;
+ }
+
+ .subtitle {
+ margin: 0;
+ color: #718096;
+ font-size: 1rem;
+ }
+
+ .stars {
+ font-size: 1.2rem;
+ }
+
+ .community-states {
+ display: flex;
+ gap: 2rem;
+ align-items: center;
+
+ .state-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: #666;
+
+ span {
+ font-size: 0.9rem;
+ }
+ }
+ }
+ }
+
+ .input {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: flex-end;
+
+ .p-button {
+ width: 100px;
+
+ padding: 0;
+ margin-top: 10px;
+ }
+ }
+ .p-dropdown-item p-focus{
+ background-color: rgba(182, 238, 235, 0.4) !important;
+ }
+ .p-dropdown-item p-highlight p-focus {
+ background-color: rgba(182, 238, 235, 0.4) !important;
+ }
+ .action-section {
+ display: flex;
+ align-items: center;
+
+ .communities-searchBar {
+ max-width: 100%;
+ position: relative;
+
+ .pi-search {
+ position: absolute;
+ left: 1rem;
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 1;
+ }
+
+ .search-helper {
+ width: 100%;
+ height: 3rem;
+ padding-left: 2.5rem;
+ border-radius: 10px 0px 0px 10px;
+ font-size: 1.1rem;
+ border: 1px solid #ddd;
+ }
+ }
+
+ .select-dropdown {
+ width: 100px;
+ height: 48px;
+ border-radius: 0px 10px 10px 0px;
+
+ .p-dropdown-items {
+ max-height: 20px;
+ }
+ }
+ }
+ }
+
+ .thread-list {
+ display: flex;
+ gap: 1rem;
+ flex-direction: column;
+
+ .resource-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 1rem;
+
+ .resource-card {
+ transition: transform 0.2s ease;
+ cursor: pointer;
+ box-shadow: none !important;
+
+ .p-image {
+ img {
+ border-radius: 0.5rem 0.5rem 0 0;
+ object-fit: cover;
+ }
+ }
+
+ .p-card-body {
+ padding: 0;
+ }
+
+ .p-card-content {
+ padding: 0;
+ }
+
+ &:hover {
+ transform: translateY(-4px);
+ }
+
+ .card-content {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ margin-left: 16px;
+ margin-right: 16px;
+ margin-bottom: 16px;
+
+ h3 {
+ margin: 1rem 0;
+ font-size: 1rem;
+ line-height: 1.5;
+ color: #2d3748;
+ }
+
+ .view-count {
+ position: absolute;
+ bottom: 0rem;
+ right: 0rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: #718096;
+ font-size: 0.9rem;
+ }
+ }
+ }
+ }
+ }
+
+ .add-resource-button {
+ position: fixed;
+ bottom: 2rem;
+ right: 2rem;
+ width: 56px;
+ height: 56px;
+
+ .pi-plus {
+ font-size: 1.5rem;
+ }
+ }
+}
+
+// ========== 弹窗发布样式 ==========
+
+.publish-dialog {
+ width: 600px !important;
+ max-width: 90vw;
+
+ .p-dialog-header {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: $heading-color;
+ padding-bottom: 0.5rem;
+ }
+
+ .p-dialog-content {
+ padding-top: 0;
+ padding-bottom: 0;
+
+ .publish-form {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5rem;
+ margin-top: 1rem;
+
+ .form-field {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+
+ label {
+ font-weight: 600;
+ color: $heading-color;
+ }
+
+ input,
+ textarea {
+ padding: 0.75rem 1rem;
+ border-radius: 8px;
+ font-size: 1rem;
+ color: #2d3748;
+ }
+
+
+ .p-fileupload {
+ .p-button {
+ width: 100%;
+ justify-content: center;
+ border: none;
+ margin-bottom: 1rem;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/community/community.scss b/src/app/community/community.scss
new file mode 100644
index 0000000..42bfda1
--- /dev/null
+++ b/src/app/community/community.scss
@@ -0,0 +1,219 @@
+// 全局容器样式
+.community-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 2rem;
+}
+
+
+// 热门社区样式
+.hot-communities {
+ margin: 3rem 0;
+
+ h1 {
+ margin-bottom: 1.5rem;
+ text-align: center; // 页面居中
+ }
+
+ .p-card-body {
+ height: 80px;
+ padding: 0;
+ padding-left: 10px;
+ padding-bottom: 10px;
+ padding-right: 10px;
+ }
+
+ .p-card-content {
+ padding: 0;
+ }
+
+ &-carousel {
+ .p-carousel-container {
+ padding: 1rem 0;
+ }
+ }
+
+ &-card {
+ margin: 0.5rem;
+ transition: transform 0.3s ease;
+ box-shadow: none !important; // 取消阴影
+
+ &:hover {
+ transform: translateY(-5px);
+ }
+
+ p {
+ margin: 0;
+ }
+
+ .card-header {
+ position: relative;
+
+ .card-tag {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background-color: #93C4C1;
+ color: white;
+ padding: 0.3rem 0.8rem;
+ border-radius: 0.5rem 0 0.5rem 0;
+ font-size: 2rem;
+ z-index: 1;
+ }
+
+ img {
+ border-radius: 0.5rem 0.5rem 0 0;
+ object-fit: cover;
+ }
+ }
+ }
+}
+
+h1 {
+ text-align: center; // 页面居中
+}
+
+// 全部分类样式
+.all-communities-classifications {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 1.5rem;
+ margin: 2rem 0;
+
+ .communities-classification-card {
+ position: relative;
+ overflow: hidden;
+ border-radius: 1rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ //鼠标悬浮效果
+ img:hover {
+ transform: translateY(-5px);
+ }
+
+ //图片样式
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 0.5rem;
+ transition: all 0.3s ease;
+ }
+ }
+}
+
+// 全部社区样式
+.all-communities {
+ width: 100%;
+ padding: 1rem;
+
+ &-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ &-card {
+ height: 140px;
+ padding: 1.5rem;
+ margin-bottom: 1rem;
+ border-radius: 0.5rem;
+ transition: transform 0.3s ease;
+ box-shadow: none !important; // 取消阴影
+
+ //填充卡片
+ &.p-card.p-component {
+ padding: 0;
+ }
+
+ .p-card-body {
+ padding: 0;
+ }
+
+ &:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ }
+
+ .p-card-content {
+ height: 140px;
+ display: flex;
+ justify-content: space-between;
+ padding: 0;
+ }
+
+ img {
+ border-radius: 0.5rem 0 0 0.5rem;
+ object-fit: cover;
+ }
+
+ .community-header {
+ display: flex;
+ flex: 1;
+ max-width: 850px;
+ padding-left: 20px;
+ padding-right: 20px;
+ margin-bottom: 20px;
+ }
+
+ .community-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+
+ h3 {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: #2c3e50;
+ }
+
+ .community-introduction {
+ color: #666;
+ font-size: 1rem;
+ margin-bottom: 0;
+ }
+ }
+
+ .community-states {
+ min-width: 120px;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: flex-end;
+ gap: 0.5rem;
+
+ .state-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: #666;
+ font-size: 1rem;
+ }
+ }
+ }
+}
+
+
+// 响应式设计
+@media (max-width: 1024px) {
+ .all-communities-classifications {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media (max-width: 768px) {
+ .community-container {
+ padding: 0 1rem;
+ }
+
+ .all-communities-classifications {
+ grid-template-columns: 1fr;
+ }
+
+ .hot-communities-carousel {
+ .p-carousel-items-container {
+ padding: 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx
new file mode 100644
index 0000000..a47cb47
--- /dev/null
+++ b/src/app/community/page.tsx
@@ -0,0 +1,191 @@
+'use client';
+
+import { useState, useEffect, useRef } from 'react';
+import { Button } from 'primereact/button';
+import { Card } from 'primereact/card';
+import { Image } from 'primereact/image';
+//幻灯片
+import { Carousel } from 'primereact/carousel';
+//评分图标
+import { Fire } from '@icon-park/react';
+// 页面跳转
+import { useRouter } from 'next/navigation';
+import Link from 'next/link';
+// 消息提醒
+import { Toast } from 'primereact/toast';
+// 接口传输
+import axios from 'axios';
+// 样式
+import './community.scss';
+
+
+// 热门社区信息
+interface hotCommunity {
+ communityId: number;
+ communityName: string;
+ hot: number;
+ status: number; // 热门状态
+ threadNumber: number;
+ description: string;
+ communityPicture: string;
+ type: string;
+}
+interface Community {
+ communityId: number;
+ communityName: string;
+ hot: number;
+ threadNumber: number;
+ description: string;
+ communityPicture: string;
+ type: string;
+}
+
+
+// 社区主页面
+export default function CommunityPage() {
+ // 路由
+ const router = useRouter();
+ // 消息提醒
+ const toast = useRef<Toast>(null);
+ // 热门社区数据
+ const [hotCommunities, setHotCommunities] = useState<hotCommunity[]>([]);
+ // 社区数据
+ const [communities, setCommunities] = useState<Community[]>([]);
+ // 获取热门社区信息
+ useEffect(() => {
+ const fetchHotCommunity = async () => {
+ try {
+ const { data } = await axios.get(`http://127.0.0.1:4523/m1/6387307-6083949-default/community/hot`);
+ setHotCommunities(data.communityList);
+ } catch (err) {
+ console.error(err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取热门社区失败' });
+ }
+ };
+ fetchHotCommunity();
+ }, []);
+
+ // 获取社区信息
+ useEffect(() => {
+ const fetchCommunity = async () => {
+ try {
+ const { data } = await axios.get(`http://127.0.0.1:4523/m1/6387307-6083949-default/community/common`);
+ setCommunities(data.communityList);
+ console.log(data.communityList);
+ } catch (err) {
+ console.error(err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取社区失败' });
+ }
+ };
+ fetchCommunity();
+ }, []);
+ return (
+ <div className="community-container">
+
+ {/* 热门社区 */}
+ <div className="hot-communities">
+ <h1>热门社区</h1>
+ <Carousel
+ showIndicators={false}
+ showNavigators={false}
+ value={hotCommunities}
+ numVisible={3}
+ numScroll={1}
+ className="hot-communities-carousel"
+ itemTemplate={(item) => (
+ <div className="hot-communities-card">
+ <Card
+ onClick={() => router.push(`/community/community-detail/${item.communityId}`)}
+ title={item.communityName}
+ header={
+ <div className="card-header">
+ <div className="card-tag">
+ <Image
+ src={`/images/${item.status}.svg`}
+ alt="热门标签"
+ width="24"
+ height="24"
+ /></div>
+ <Image src={process.env.NEXT_PUBLIC_NGINX_URL + item.communityPicture} alt={item.communityName} height="200" className="w-full h-48 object-cover" />
+ </div>
+ }
+ >
+ <p>{item.description}</p>
+ </Card>
+ </div>
+ )}
+ />
+ </div>
+
+ {/* 全部分类 */}
+ <h1>全部分类</h1>
+ <div className="all-communities-classifications">
+ <Link href="/community/resource-community-list/材质包">
+ <Image
+ className='communities-classification-card'
+ src="/images/材质包.png"
+ alt="Image"
+ width="250"
+ />
+ </Link>
+ <Link href="/community/resource-community-list/整合包">
+ <Image
+ className='communities-classification-card'
+ src="/images/整合包.png"
+ alt="Image"
+ width="250"
+ />
+ </Link>
+ <Link href="/community/resource-community-list/模组">
+ <Image
+ className='communities-classification-card'
+ src="/images/模组.png"
+ alt="Image"
+ width="250"
+ />
+ </Link>
+ <Link href="/community/resource-community-list/地图">
+ <Image
+ className='communities-classification-card'
+ src="/images/地图.png"
+ alt="Image"
+ width="250"
+ />
+ </Link>
+ </div>
+
+ {/* 全部社区 */}
+ <div className="all-communities">
+ <div className="all-communities-header">
+ <h1>全部社区</h1>
+ <Link href="/community/resource-community-list/all">
+ <Button label="查看更多" link />
+ </Link>
+ </div>
+ <div className="all-communities-list">
+ {communities.map((community) => (
+ <Card key={community.communityId} className="all-communities-card" onClick={() => router.push(`/community/community-detail/${community.communityId}`)}>
+ <Image alt="avatar" src={process.env.NEXT_PUBLIC_NGINX_URL + community.communityPicture} className="community-avatar" width="250" height="140" />
+ <div className="community-header">
+ <div className="community-content">
+ <h3>{community.communityName}</h3>
+ <p className="community-introduction">{community.description}</p>
+ </div>
+ <div className="community-states">
+ <div className="state-item">
+ <Fire theme="outline" size="16" fill="#FF8D1A" />
+ <span>热度: {community.hot}</span>
+ </div>
+ <div className="state-item">
+ <i className="pi pi-book" />
+ <span>贴子数: {community.threadNumber}</span>
+ </div>
+ </div>
+ </div>
+ </Card>
+ ))}
+ </div>
+ </div>
+ </div>
+ );
+}
\ No newline at end of file
diff --git "a/src/app/community/resource-community-list/\133category\135/page.tsx" "b/src/app/community/resource-community-list/\133category\135/page.tsx"
new file mode 100644
index 0000000..a34fc07
--- /dev/null
+++ "b/src/app/community/resource-community-list/\133category\135/page.tsx"
@@ -0,0 +1,137 @@
+'use client';
+
+import { useState, useEffect, useRef } from 'react';
+import { InputText } from 'primereact/inputtext';
+import { Card } from 'primereact/card';
+import { Image } from 'primereact/image';
+// 评分图标
+import { Fire } from '@icon-park/react';
+// 分页
+import { Paginator, type PaginatorPageChangeEvent } from 'primereact/paginator';
+// 页面跳转
+import { useParams } from 'next/navigation'
+import { useRouter } from 'next/navigation';
+// 消息提醒
+import { Toast } from 'primereact/toast';
+// 接口传输
+import axios from 'axios';
+// 防抖函数
+import { debounce } from 'lodash';
+// 样式
+import './resource-community-list.scss';
+
+// 单元社区信息
+interface Community {
+ communityId: number;
+ communityName: string;
+ hot: number; // 热度
+ threadNumber: number; // 帖子数量
+ description: string; // 简介
+ communityPicture: string; // 头像
+}
+
+// 资源社区列表
+interface CommunityList {
+ total: number;
+ records: Community[];
+}
+
+
+// 资源社区界面
+export default function ResourceCommunityPage() {
+ // 获取URL参数
+ const params = useParams<{ category: string }>()
+ const category = decodeURIComponent(params.category); // 防止中文路径乱码
+ const router = useRouter();
+
+ // 社区列表数据
+ const [communities, setCommunities] = useState<Community[]>([]);
+ const [totalCommunities, setTotalCommunities] = useState<number>(0);
+ // 消息提醒
+ const toast = useRef<Toast>(null);
+ //搜索框
+ const [searchValue, setSearchValue] = useState('');
+ const debouncedSearch = useRef(
+ debounce((value: string) => {
+ setSearchValue(value);
+ }, 600)
+ ).current;
+
+ //分页
+ const [first, setFirst] = useState(0);
+ const [rows, setRows] = useState(5);
+ const onPageChange = (event: PaginatorPageChangeEvent) => {
+ setFirst(event.first);
+ setRows(event.rows);
+ };
+
+
+ // 获取社区列表
+ useEffect(() => {
+ fetchCommunities();
+ }, [category, first, rows, searchValue]);
+
+ const fetchCommunities = async () => {
+ try {
+ const page = first / rows + 1;
+ const type = category === 'all' ? '全部' : category;
+ console.log("当前页" + page + "size" + rows + "type" + type + "searchValue" + searchValue);
+ const response = await axios.get<CommunityList>(
+ `http://127.0.0.1:4523/m1/6387307-6083949-default/community`, {
+ params: { type, page, rows, searchValue }
+ }
+ );
+ console.log('获取社区列表:', response.data.records);
+ setTotalCommunities(response.data.total);
+ setCommunities(response.data.records);
+ } catch (err) {
+ console.error('获取社区失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取社区失败' });
+ }
+ };
+
+
+ return (
+ <div className="resource-community-list-container">
+ <Toast ref={toast}></Toast>
+ <div className="resource-communities-header">
+ <h1 className="title">
+ {category === 'all' ? '全部社区' : `${category}社区`}
+ </h1>
+ </div>
+
+ {/* 社区搜索栏 */}
+ <div className="communities-searchBar">
+ <i className="pi pi-search" />
+ <InputText type="search" className="search-helper" placeholder="搜索你感兴趣的社区" onChange={(e) => { const target = e.target as HTMLInputElement; debouncedSearch(target.value); }} />
+ </div>
+
+
+ {/* 社区列表 */}
+ <div className="resource-communities-list">
+ {communities.map((community) => (
+ <Card key={community.communityId} className="resource-communities-list-card" onClick={() => router.push(`/community/community-detail/${community.communityId}`)}>
+ <Image alt="avatar" src={process.env.NEXT_PUBLIC_NGINX_URL + community.communityPicture} className="community-avatar" width="250" height="140" />
+ <div className="community-header">
+ <div className="community-content">
+ <h3>{community.communityName}</h3>
+ <p className="community-introduction">{community.description}</p>
+ </div>
+ <div className="community-states">
+ <div className="state-item">
+ <Fire theme="outline" size="16" fill="#FF8D1A" />
+ <span>热度: {community.hot}</span>
+ </div>
+ <div className="state-item">
+ <i className="pi pi-book" />
+ <span>帖子数: {community.threadNumber}</span>
+ </div>
+ </div>
+ </div>
+ </Card>
+ ))}
+ {totalCommunities > 5 && <Paginator className="Paginator" first={first} rows={rows} totalRecords={totalCommunities} rowsPerPageOptions={[5, 10]} onPageChange={onPageChange} />}
+ </div>
+ </div>
+ );
+}
\ No newline at end of file
diff --git "a/src/app/community/resource-community-list/\133category\135/resource-community-list.scss" "b/src/app/community/resource-community-list/\133category\135/resource-community-list.scss"
new file mode 100644
index 0000000..c8a0018
--- /dev/null
+++ "b/src/app/community/resource-community-list/\133category\135/resource-community-list.scss"
@@ -0,0 +1,104 @@
+// 全局容器样式
+.resource-community-list-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 2rem;
+}
+
+.resource-communities-header {
+ h1 {
+ text-align: center; // 页面居中
+ }
+}
+
+// 全部社区样式
+.resource-communities-list {
+ width: 100%;
+ padding: 1rem;
+
+ &-card {
+ height: 140px;
+ padding: 1.5rem;
+ margin-bottom: 1rem;
+ border-radius: 0.5rem;
+ transition: transform 0.3s ease;
+ box-shadow: none !important; // 取消阴影
+
+ //填充卡片
+ &.p-card.p-component {
+ padding: 0;
+ }
+
+ .p-card-body {
+ padding: 0;
+ }
+
+ &:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ }
+
+ .p-card-content {
+ height: 140px;
+ display: flex;
+ justify-content: space-between;
+ padding: 0;
+ }
+
+ img {
+ border-radius: 0.5rem 0 0 0.5rem;
+ object-fit: cover;
+ }
+
+ .community-header {
+ display: flex;
+ flex: 1;
+ max-width: 800px;
+ padding-left: 20px;
+ padding-right: 20px;
+ margin-bottom: 20px;
+ }
+
+
+ .community-avatar {
+ width: 80px;
+ height: 80px;
+ border-radius: 8px;
+ object-fit: cover;
+ }
+
+ .community-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ h3 {
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: #2c3e50;
+ }
+
+ .community-introduction {
+ color: #666;
+ font-size: 1rem;
+ margin-bottom: 0;
+ }
+ }
+
+ .community-states {
+ min-width: 120px;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: flex-end;
+ gap: 0.5rem;
+
+ .state-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: #666;
+ font-size: 1rem;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git "a/src/app/community/thread-detail/\133threadId\135/page.tsx" "b/src/app/community/thread-detail/\133threadId\135/page.tsx"
new file mode 100644
index 0000000..2189402
--- /dev/null
+++ "b/src/app/community/thread-detail/\133threadId\135/page.tsx"
@@ -0,0 +1,396 @@
+'use client';
+
+import React, { useEffect, useState, useRef } from 'react';
+import { Image } from 'primereact/image';
+import { Avatar } from 'primereact/avatar';
+import { Button } from 'primereact/button';
+import { InputText } from "primereact/inputtext";
+// 页面跳转
+import { useParams } from 'next/navigation'
+import { useRouter } from 'next/navigation';
+// 接口传输
+import axios from 'axios';
+// 回复评论
+import { OverlayPanel } from 'primereact/overlaypanel';
+import { Sidebar } from 'primereact/sidebar';
+// 分页
+import { Paginator, type PaginatorPageChangeEvent } from 'primereact/paginator';
+// 消息提醒
+import { Toast } from 'primereact/toast';
+// 样式
+import './thread.scss';
+
+
+// 评论信息
+interface Comment {
+ commentId: number;
+ userId: number | null;
+ replyId: number;
+ content: string;
+ createdAt: string;
+}
+// 评论列表
+interface CommentList {
+ records: Comment[]; // 当前页评论数组
+}
+// 帖子信息
+interface ThreadInfo {
+ threadId: number;
+ userId: number;
+ threadPicture: string;
+ title: string;
+ content: string;
+ likes: number;
+ isLike: boolean;
+ createdAt: string;
+ commentNumber: number;
+ communityId: number;
+}
+// 用户信息
+interface UserInfo {
+ userId: number;
+ username: string;
+ avatar: string;
+ signature: string;
+}
+// 新评论接口
+interface NewComment {
+ userId: number;
+ threadId: number;
+ resourceId: number;
+ replyId: number;
+ content: string;
+ createdAt: string;
+}
+
+
+//帖子详情界面
+export default function ThreadDetailPage() {
+ // 获取URL参数,页面跳转
+ const params = useParams<{ threadId: string }>()
+ const threadId = decodeURIComponent(params.threadId); // 防止中文路径乱码
+ const router = useRouter();
+ // 消息提醒
+ const toast = useRef<Toast>(null);
+ // 帖子信息
+ const [threadInfo, setThreadInfo] = useState<ThreadInfo | null>(null);
+ // 发帖人信息
+ const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
+ // 评论人信息
+ const [commentUserInfos, setCommentUserInfos] = useState<Map<number, UserInfo>>(new Map());
+ //评论
+ const [comments, setComments] = useState<Comment[]>([]);
+ const [commentValue, setCommentValue] = useState<string>('');
+ const [totalComments, setTotalComments] = useState<number>(0);
+ // 回复
+ const [replyValue, setReplyValue] = useState<string>('');
+ const [visibleReply, setVisibleReply] = useState<boolean>(false);// 回复评论可视
+ // 评论选择框
+ const ops = useRef<OverlayPanel[]>([]);
+
+ // 分页
+ const [first, setFirst] = useState<number>(0);
+ const [rows, setRows] = useState<number>(5);
+ const onPageChange = (event: PaginatorPageChangeEvent) => {
+ setFirst(event.first);
+ setRows(event.rows);
+ };
+
+ // 获取帖子信息
+ useEffect(() => {
+ fetchThreadInfo();
+ }, [threadId, threadInfo]);
+
+ const fetchThreadInfo = async () => {
+ try {
+ const { data } = await axios.get(`http://127.0.0.1:4523/m1/6387307-6083949-default/thread?threadId=${threadId}`);
+ setThreadInfo(data);
+ setTotalComments(data.commentNumber);
+ } catch (err) {
+ console.error(err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取帖子信息失败' });
+ }
+ };
+
+ // 获取发帖人
+ useEffect(() => {
+ if (!threadInfo) return;
+ // 发帖人
+ axios.get(`http://127.0.0.1:4523/m1/6387307-6083949-default/user/info?userId=${threadInfo.userId}`)
+ .then(res => setUserInfo(res.data))
+ .catch(console.error);
+ }, [threadInfo]);
+
+ // 处理点赞
+ const handleLike = async () => {
+ if (!threadInfo) return;
+ if (!threadInfo.isLike) {
+ try {
+ const response = await axios.post(
+ `http://127.0.0.1:4523/m1/6387307-6083949-default/thread/like`, {
+ params: { threadId, userId: 22301145 }
+ }
+ );
+ fetchThreadInfo(); // 刷新帖子信息
+ if (response.status === 200) {
+ console.log('点赞成功:', response.data);
+ toast.current?.show({ severity: 'success', summary: 'Success', detail: '点赞成功' });
+ }
+ } catch (error) {
+ console.error('点赞失败:', error);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '点赞失败' });
+ }
+ } else {
+ try {
+ const response = await axios.delete(
+ `http://127.0.0.1:4523/m1/6387307-6083949-default/thread/like`, {
+ params: { threadId, userId: 22301145 }
+ }
+ );
+ fetchThreadInfo(); // 刷新帖子信息
+ if (response.status === 200) {
+ console.log('取消点赞成功:', response.data);
+ toast.current?.show({ severity: 'success', summary: 'Success', detail: '取消点赞成功' });
+ }
+ } catch (error) {
+ console.error('取消点赞失败:', error);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '取消点赞失败' });
+ }
+ }
+
+ };
+
+
+ // 当 threadId 或分页参数变化时重新拉评论
+ useEffect(() => {
+ if (!threadId) return;
+
+ fetchComments();
+ }, [threadId, first, rows]);
+
+
+ //通过评论ID获取评论人信息
+ const getReplyUserName = (replyId: number) => {
+ if (replyId == null || replyId == 0) return '';
+ const replyComment = comments.find(comment => comment.commentId === replyId);
+ if (!replyComment?.userId) return '匿名用户';
+ return "回复 " + commentUserInfos.get(replyComment.userId)?.username || '匿名用户';
+ };
+
+ // 获取评论列表
+ const fetchComments = async () => {
+ try {
+ const page = first / rows + 1;
+ console.log("当前页" + page + "size" + rows);
+ const response = await axios.get<CommentList>(
+ `http://127.0.0.1:4523/m1/6387307-6083949-default/comments`, {
+ params: { threadId, page, rows }
+ }
+ );
+ console.log('获取评论列表:', response.data.records);
+ setComments(response.data.records);
+ // 拉取评论对应用户信息
+ response.data.records.forEach(comment => {
+ if (comment.userId != null && !commentUserInfos.has(comment.userId)) {
+ axios.get<UserInfo>(
+ `http://127.0.0.1:4523/m1/6387307-6083949-default/user/info`,
+ { params: { userId: comment.userId } }
+ ).then(res => {
+ setCommentUserInfos(prev => new Map(prev).set(comment.userId!, res.data));
+ });
+ }
+ });
+ } catch (err) {
+ console.error('获取评论失败', err);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '获取评论失败' });
+ }
+ };
+
+ // 回复评论接口
+ const publishReply = async (commentId: number) => {
+ if (!replyValue.trim() || !threadInfo) return;
+ console.log('发布评论:', commentId);
+ try {
+ const newComment: NewComment = {
+ userId: 22301145,
+ threadId: threadInfo.threadId,
+ resourceId: 0,
+ replyId: commentId,
+ content: commentValue,
+ createdAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+ };
+
+ const response = await axios.post('http://127.0.0.1:4523/m1/6387307-6083949-default/comment', newComment);
+
+ if (response.status === 200) {
+ toast.current?.show({ severity: 'success', summary: 'Success', detail: '回复成功' });
+ // 更新评论列表
+ fetchComments();
+ setVisibleReply(false)
+ // 清空输入框
+ setReplyValue('');
+ }
+ } catch (error) {
+ console.error('发布评论失败:', error);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '回复失败' });
+ }
+ };
+
+ // 发布评论接口
+ const publishComment = async () => {
+ if (!commentValue.trim() || !threadInfo) return;
+
+ try {
+ const newComment: NewComment = {
+ userId: 22301145,
+ threadId: threadInfo.threadId,
+ resourceId: 0,
+ replyId: 0, // 直接评论,不是回复
+ content: commentValue,
+ createdAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+ };
+
+ const response = await axios.post('http://127.0.0.1:4523/m1/6387307-6083949-default/comment', newComment);
+
+ if (response.status === 200) {
+ toast.current?.show({ severity: 'success', summary: 'Success', detail: '评论成功' });
+ // 更新评论列表
+ fetchComments();
+ // 清空输入框
+ setCommentValue('');
+ }
+ } catch (error) {
+ console.error('发布评论失败:', error);
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '发布评论失败' });
+ }
+ };
+
+ // 删除评论接口
+ const deleteComment = async (commentId: number) => {
+ if (!threadInfo) return;
+
+ try {
+ // 调用 DELETE 接口,URL 中最后一段是要删除的 commentId
+ const response = await axios.delete(
+ `http://127.0.0.1:4523/m1/6387307-6083949-default/comment?commentId=${commentId}`
+ );
+
+ if (response.status === 200) {
+ fetchComments();
+ toast.current?.show({ severity: 'success', summary: 'Success', detail: '删除评论成功' });
+ } else {
+ toast.current?.show({ severity: 'error', summary: 'error', detail: '删除评论失败' });
+ console.error('删除评论失败,状态码:', response.status);
+ }
+ } catch (error) {
+ console.error('删除评论接口报错:', error);
+ }
+ };
+
+ const ReplyHeader = (
+ <div className="flex align-items-center gap-1">
+ <h3>回复评论</h3>
+ </div>
+ );
+ if (!threadInfo || !userInfo) return <div>Loading...</div>;
+ return (
+ <div className="thread-detail">
+ <Toast ref={toast}></Toast>
+ {/* 帖子头部 */}
+ <div className="thread-header">
+ <div className="user-info">
+ <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <div className="user-meta">
+ <h3>{userInfo.username}</h3>
+ <span>{userInfo.signature}</span>
+ </div>
+ </div>
+ <span className="post-time">{threadInfo.createdAt}</span>
+ </div>
+
+ {/* 帖子内容 */}
+ <div className="thread-content">
+ <h1>{threadInfo.title}</h1>
+ <div className="content-body">
+ <Image
+ src={process.env.NEXT_PUBLIC_NGINX_URL + threadInfo.threadPicture}
+ alt={threadInfo.title}
+ width="800"
+ height="400"
+ className="thread-image"
+ />
+ <p>{threadInfo.content}</p>
+ </div>
+ <div className="thread-actions">
+ <Button
+ icon={"pi pi-face-smile"}
+ onClick={handleLike}
+ className={`like-button ${threadInfo.isLike ? 'liked' : ''}`}
+ label={threadInfo.likes.toString()}
+ />
+ </div>
+ </div>
+ {/* 评论列表 */}
+ <div className="comments-section">
+ <div className="comments-header">
+ <h2>评论 ({totalComments})</h2>
+ <Button label="返回社区" link onClick={() => router.push(`/community/community-detail/${threadInfo.communityId}`)} />
+ </div>
+ <div className="comments-input">
+ <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <InputText value={commentValue} placeholder="发布你的评论" onChange={(e) => setCommentValue(e.target.value)} />
+ <Button label="发布评论" onClick={publishComment} disabled={!commentValue.trim()} />
+ </div>
+ <div className="comments-list">
+ {comments.map((comment, index) => (
+ <div key={comment.commentId} className="comment-item">
+ <div className="comment-user">
+ <Avatar
+ image={comment.userId ? process.env.NEXT_PUBLIC_NGINX_URL + "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'}
+ size="normal"
+ shape="circle"
+ />
+ <div className="comment-meta">
+ <span className="username">
+ {comment.userId ? commentUserInfos.get(comment.userId)?.username : '匿名用户'}
+ </span>
+ <div className="comment-time">
+ <span className="floor">#{index + 1}楼</span>
+ <span className="time">{comment.createdAt}</span>
+ </div>
+ </div>
+ <i className='pi pi-ellipsis-v' onClick={(e) => ops.current[index].toggle(e)} />
+ </div>
+ <div className="comment-content">
+ {<span className="reply-to">{getReplyUserName(comment.replyId)}</span>}
+ <p>{comment.content}</p>
+ </div>
+ <OverlayPanel // 回调 ref:把实例放到 ops.current 对应的位置
+ ref={el => {
+ if (el) ops.current[index] = el;
+ }}>
+ <Button label="回复" text size="small" onClick={() => setVisibleReply(true)} />
+ {comment.userId === 22301145 &&
+ <Button
+ label="删除"
+ text
+ size="small"
+ onClick={() => { console.log('Deleting comment:', comment.commentId, 'by user:', comment.userId); deleteComment(comment.commentId) }}
+ />
+ }
+ </OverlayPanel>
+ <Sidebar className='reply' header={ReplyHeader} visible={visibleReply} position="bottom" onHide={() => setVisibleReply(false)}>
+ <div className="reply-input">
+ <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+ <InputText value={replyValue} placeholder="发布你的评论" onChange={(e) => setReplyValue(e.target.value)} />
+ <Button label="发布评论" onClick={() => publishReply(comment.commentId)} disabled={!replyValue.trim()} />
+ </div>
+ </Sidebar>
+ </div>
+ ))}
+ {totalComments > 5 && (<Paginator className="Paginator" first={first} rows={rows} totalRecords={totalComments} rowsPerPageOptions={[5, 10]} onPageChange={onPageChange} />)}
+ </div>
+ </div>
+ </div>
+ );
+}
\ No newline at end of file
diff --git "a/src/app/community/thread-detail/\133threadId\135/thread.scss" "b/src/app/community/thread-detail/\133threadId\135/thread.scss"
new file mode 100644
index 0000000..b2524f0
--- /dev/null
+++ "b/src/app/community/thread-detail/\133threadId\135/thread.scss"
@@ -0,0 +1,201 @@
+.thread-detail {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 2rem;
+
+ // 帖子头部
+ .thread-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+
+ .user-info {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+
+ .user-meta {
+ h3 {
+ margin: 0;
+ font-size: 1.25rem;
+ color: #2d3748;
+ }
+
+ span {
+ color: #718096;
+ font-size: 0.875rem;
+ }
+ }
+ }
+
+ .post-time {
+ color: #718096;
+ font-size: 0.875rem;
+ }
+ }
+
+ // 帖子内容
+ .thread-content {
+ h1 {
+ font-size: 2rem;
+ color: #1a202c;
+ margin-bottom: 1.5rem;
+ }
+
+ .content-body {
+ .thread-image {
+ width: 100%;
+ height: auto;
+ border-radius: 0.5rem;
+ margin-bottom: 1.5rem;
+ }
+
+ .thread-image {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ p {
+ font-size: 1rem;
+ line-height: 1.75;
+ color: #4a5568;
+ margin-bottom: 2rem;
+ }
+ }
+
+ .thread-actions {
+ display: flex;
+ gap: 1rem;
+ margin-bottom: 2rem;
+
+ .like-button {
+ &.liked {
+ background: #f49c79;
+ }
+ }
+ }
+ }
+
+ // 评论区域
+ .comments-section {
+ .comments-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ h2 {
+ font-size: 1.5rem;
+ color: #2d3748;
+ margin-bottom: 1.5rem;
+ }
+ }
+
+ // 评论输入区
+ .comments-input {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem;
+ border-radius: 0.5rem;
+
+ .p-inputtext {
+ flex: 1; // 输入框占据剩余空间
+ height: 3rem;
+ }
+
+ .p-button {
+ height: 3rem;
+ }
+ }
+
+ // 评论列表
+ .comments-list {
+ display: flex;
+ flex-direction: column;
+ margin-top: 1rem;
+ gap: 0.5rem;
+
+ .comment-item {
+ padding: 1.5rem;
+ border-radius: 0.5rem;
+
+ .comment-user {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 1rem;
+
+ .comment-meta {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ justify-content: space-between;
+ gap: 0.5rem;
+
+ .comment-time {
+ justify-content: space-between;
+ gap: 0.75rem
+ }
+
+ .username {
+ margin-left: 0.5rem;
+ font-weight: 600;
+ color: #2d3748;
+ }
+
+ .floor {
+ color: #718096;
+ margin-right: 0.75rem;
+ font-size: 0.875rem;
+ }
+
+ .time {
+ color: #a0aec0;
+ font-size: 0.875rem;
+ }
+ }
+ }
+
+ .comment-content {
+ padding-left: 3.5rem;
+
+ .reply-to {
+ display: inline-block;
+ color: #93C4C1;
+ font-size: 0.875rem;
+ margin-bottom: 0.5rem;
+ }
+
+ p {
+ color: #4a5568;
+ margin: 0;
+ line-height: 1.5;
+ }
+ }
+ }
+ }
+ }
+}
+
+.p-sidebar-header,
+.p-sidebar-custom-header {
+ padding: 10px !important;
+}
+
+.p-overlaypanel-content {
+ padding: 0 !important;
+}
+
+.reply {
+
+ .reply-input {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-self: center;
+ gap: 3rem;
+ padding: 1rem;
+ }
+}
\ No newline at end of file