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