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