Docker
Change-Id: I2aefd96a43bcf3a3c41c079ecfc04a3fee48bed6
diff --git a/src/api/homepage.ts b/src/api/homepage.ts
new file mode 100644
index 0000000..697d0d0
--- /dev/null
+++ b/src/api/homepage.ts
@@ -0,0 +1 @@
+export const getUserMessage="/user/message"
\ No newline at end of file
diff --git a/src/api/upload.ts b/src/api/upload.ts
new file mode 100644
index 0000000..cb768bb
--- /dev/null
+++ b/src/api/upload.ts
@@ -0,0 +1 @@
+export const Upload= '/user/Io';
\ No newline at end of file
diff --git a/src/components/selfStatus/selfStatus.tsx b/src/components/selfStatus/selfStatus.tsx
index a5156f4..3745736 100644
--- a/src/components/selfStatus/selfStatus.tsx
+++ b/src/components/selfStatus/selfStatus.tsx
@@ -1,12 +1,15 @@
import React from "react";
import { useAppSelector } from "../../hooks/store";
import style from "./style.module.css"
+import { useNavigate } from "react-router";
interface SelfStatusProps {
className?: string;
}
const SelfStatus: React.FC<SelfStatusProps> = () => {
+
+ const nav = useNavigate()
const userName = useAppSelector(state => state.user.userName);
const role = useAppSelector(state => state.user.role);
const uploadTraffic = useAppSelector(state => state.user.uploadTraffic);
@@ -14,10 +17,14 @@
const downloadPoints = useAppSelector(state => state.user.downloadPoints);
const avatar = useAppSelector(state => state.user.avatar);
+ function handleAvatarClick(){
+ nav('/homepage')
+ }
+
return (
<div className={style.container}>
<div className={style.left}>
- <img className={style.avatar} src={avatar} alt="User Avatar" />
+ <img className={style.avatar} onClick={handleAvatarClick} src={avatar} alt="User Avatar" />
</div>
<div className={style.right}>
<div className={style.info}>
diff --git a/src/components/upload/upload.module.css b/src/components/upload/upload.module.css
new file mode 100644
index 0000000..2fb6278
--- /dev/null
+++ b/src/components/upload/upload.module.css
@@ -0,0 +1,39 @@
+.uploadContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ border: 2px dashed #1890ff;
+ padding: 40px;
+ border-radius: 8px;
+ background-color: #f0f5ff;
+ transition: border-color 0.3s;
+}
+
+.uploadContainer:hover {
+ border-color: #40a9ff;
+}
+
+.uploadLabel {
+ font-size: 16px;
+ margin-bottom: 10px;
+ color: #333;
+}
+
+.uploadInput {
+ display: none;
+}
+
+.uploadButton {
+ padding: 8px 16px;
+ background-color: #1890ff;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+}
+
+.uploadButton:hover {
+ background-color: #40a9ff;
+}
diff --git a/src/components/upload/upload.tsx b/src/components/upload/upload.tsx
new file mode 100644
index 0000000..708e9d1
--- /dev/null
+++ b/src/components/upload/upload.tsx
@@ -0,0 +1,44 @@
+// src/component/upload/upload.tsx
+import React, { useState } from 'react'
+import './upload.css'
+
+const UploadComponent: React.FC = () => {
+ const [selectedFile, setSelectedFile] = useState<File | null>(null)
+
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ if (e.target.files && e.target.files.length > 0) {
+ setSelectedFile(e.target.files[0])
+ }
+ }
+
+ const handleUpload = () => {
+ if (selectedFile) {
+ console.log('上传文件:', selectedFile)
+ // 此处可以添加实际上传逻辑
+ }
+ }
+
+ return (
+ <div className="uploadContainer">
+ <label className="uploadLabel" htmlFor="upload-input">请选择文件上传:</label>
+ <input
+ type="file"
+ id="upload-input"
+ className="uploadInput"
+ data-testid="upload-input"
+ onChange={handleFileChange}
+ />
+ <label htmlFor="upload-input">
+ <div
+ className="uploadButton"
+ data-testid="upload-button"
+ onClick={handleUpload}
+ >
+ 点击上传
+ </div>
+ </label>
+ </div>
+ )
+}
+
+export default UploadComponent
diff --git a/src/global.css b/src/global.css
index b49490a..81a6670 100644
--- a/src/global.css
+++ b/src/global.css
@@ -28,6 +28,9 @@
--text-color: #000000;
--card-bg: #ffffff;
--border-color: #e0e0e0;
+ --primary-color: #3498db;
+ --primary-hover: #2980b9;
+ --primary-card: #a3d1f0;
}
body.dark {
@@ -35,5 +38,8 @@
--text-color: #f1f1f1;
--card-bg: #1e1e1e;
--border-color: #444444;
+ --primary-color: #3498db;
+ --primary-hover: #2980b9;
+ --primary-card:#280202;
}
\ No newline at end of file
diff --git a/src/hooks/request.ts b/src/hooks/request.ts
index 8594ac2..b53da73 100644
--- a/src/hooks/request.ts
+++ b/src/hooks/request.ts
@@ -1,7 +1,8 @@
import { useState, useEffect, useCallback } from 'react'
import { data } from 'react-router'
-type RequestFunction<T> = () => Promise<T>
+// type RequestFunction<T> = () => Promise<T>
+type RequestFunction<T, P = any> = (params?: P) => Promise<T>;
interface UseApiResult<T> {
data: T | null
@@ -10,18 +11,22 @@
refresh: () => void
}
-export function useApi<T>(
- requestFn: RequestFunction<T>,
- immediate = true
- ): UseApiResult<T> {
+// export function useApi<T>(
+// requestFn: RequestFunction<T>,
+// immediate = true
+// ): UseApiResult<T> {
+export function useApi<T, P = any>(
+ requestFn: RequestFunction<T, P>, // 接收参数
+ immediate = true
+) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<Error | null>(null)
- const execute = useCallback(async () => {
+ const execute = useCallback(async (params?: P) => { // 添加参数
try {
setLoading(true)
- const result = await requestFn()
+ const result = await requestFn(params);//传参
setData(result)
setError(null)
return result // 返回请求结果
diff --git a/src/mock/homepage.d.ts b/src/mock/homepage.d.ts
new file mode 100644
index 0000000..ba70a8a
--- /dev/null
+++ b/src/mock/homepage.d.ts
@@ -0,0 +1,3 @@
+import type MockAdapter from 'axios-mock-adapter';
+
+export declare function setupUserMessageMock(mock: MockAdapter): void;
\ No newline at end of file
diff --git a/src/mock/homepage.js b/src/mock/homepage.js
new file mode 100644
index 0000000..41ca3c7
--- /dev/null
+++ b/src/mock/homepage.js
@@ -0,0 +1,46 @@
+import Mock from 'mockjs';
+import MockAdapter from 'axios-mock-adapter';
+import {getUserMessage} from '@/api/homepage';
+
+/**
+ * 设置用户相关的 Mock 接口
+ * @param {MockAdapter} mock
+ */
+export function setupUserMessageMock(mock) {
+ mock.onGet(getUserMessage).reply((config) => {
+ console.log("visited")
+ let data = Mock.mock({
+ 'username': '阳菜,放睛!',
+ 'inviteCode': '1314520',
+ 'stats': {
+ 'likes': 0,
+ 'following': 25,
+ 'followers': 276,
+ 'mutualFollows': 52
+ },
+ 'upload': '5.2 ',
+ 'level': '荣耀会员',
+ // 'works': [{
+ // 'id': 1,
+ // 'title': '【PC】【ARPG】【开放世界】刺客信条影破解版',
+ // 'publishTime': '2025-3-21',
+ // 'downloadCount': 0,
+ // 'seedCount': 1
+ // }],
+ 'works': [{
+ 'postId': 0,
+ 'userId': 0,
+ 'postTitle': '阳菜',
+ "postContent": "",
+ "createdAt": 0,
+ "postType": "",
+ "viewCount": 0,
+ "hotScore": 0.0,
+ "lastCalculated": 0
+ }],
+ 'petImage': '/assets/pet-blue-star.png',
+ 'trafficImage': '/assets/duck-computer.png'
+ });
+ return [200, data];
+ });
+}
\ No newline at end of file
diff --git a/src/mock/index.ts b/src/mock/index.ts
index fabb34e..f9c6a64 100644
--- a/src/mock/index.ts
+++ b/src/mock/index.ts
@@ -1,8 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import instance from '@/utils/axios'
-import {setupAuthMock} from './auth'
+import { setupAuthMock } from './auth'
import { setupUserMock } from './user';
import { setupPostMock } from './post';
+import {setupUserMessageMock} from './homepage';
+import { setupUploadMock } from './upload';
// 创建 Mock 实例
export const mock = new MockAdapter(instance, {
@@ -18,7 +20,9 @@
setupAuthMock(mock)
setupUserMock(mock)
setupPostMock(mock)
-
+ setupUserMessageMock(mock)
+ setupUploadMock(mock)
+
console.log('Mock 模块已加载')
}
diff --git a/src/mock/upload.d.ts b/src/mock/upload.d.ts
new file mode 100644
index 0000000..3ddb4b9
--- /dev/null
+++ b/src/mock/upload.d.ts
@@ -0,0 +1,3 @@
+import type MockAdapter from 'axios-mock-adapter';
+
+export declare function setupUploadMock(mock: MockAdapter): void;
\ No newline at end of file
diff --git a/src/mock/upload.js b/src/mock/upload.js
new file mode 100644
index 0000000..c3f2705
--- /dev/null
+++ b/src/mock/upload.js
@@ -0,0 +1,26 @@
+import Mock from 'mockjs';
+import MockAdapter from 'axios-mock-adapter';
+import {Upload} from '@/api/upload';
+
+/**
+ * 设置上传种子的 Mock 接口
+ * @param {MockAdapter} mock
+ */
+export function setupUploadMock(mock) {
+ mock.onPost(Upload).reply((config) => {
+ const body = JSON.parse(config.data);
+
+ console.log('收到上传请求,内容如下:');
+ console.log(body);
+
+
+ return [
+ 200,
+ {
+ code: 0,
+ message: '',
+ data: null
+ }
+ ];
+ });
+}
diff --git a/src/route/index.tsx b/src/route/index.tsx
index c56ada3..62bb7a5 100644
--- a/src/route/index.tsx
+++ b/src/route/index.tsx
@@ -2,6 +2,9 @@
import PrivateRoute from './privateRoute'
import { useSelector } from 'react-redux'
import Login from '../views/login/login'
+import Homepage from '../views/homepage/homepage'
+import Upload from '../views/upload/upload'
+import Search from '../views/search/search'
import Frame from '../views/frame/frame'
import React from 'react'
import Forum from '../views/forum'
@@ -24,6 +27,18 @@
element:<Forum/>
},
+ {
+ path:'/homepage',
+ element: <Homepage/>
+ },
+ {
+ path:'/postDetails',
+ element: <Upload/>
+ },
+ {
+ path:'/search',
+ element:<Search/>
+ }
]
},
]
diff --git a/src/utils/axios.ts b/src/utils/axios.ts
index 1eaad87..8c60d4b 100644
--- a/src/utils/axios.ts
+++ b/src/utils/axios.ts
@@ -2,7 +2,8 @@
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
const instance = axios.create({
- baseURL: process.env.API_BASE_URL,
+ // baseURL: process.env.API_BASE_URL,
+ baseURL: 'http://localhost:8080',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
diff --git a/src/views/frame/frame.tsx b/src/views/frame/frame.tsx
index c0c2e0e..c22a528 100644
--- a/src/views/frame/frame.tsx
+++ b/src/views/frame/frame.tsx
@@ -12,12 +12,21 @@
import logo from "&/assets/logo.png";
import { useAppDispatch } from "@/hooks/store";
import { useSelector } from "react-redux";
+
+import { useNavigate } from "react-router-dom";
+
+
const Frame:React.FC = () => {
const dispatch = useAppDispatch();
const showSearch = useSelector((state: any) => state.setting.showSearch);
const theme= useSelector((state: any) => state.setting.theme);
+
+ const navigate = useNavigate(); // ✅ 用于跳转
+ const [searchText, setSearchText] = useState(""); // ✅ 存储搜索输入内容
+
+
const toggleSearch = () => {
dispatch({ type: "setting/toggleSearch" });
}
@@ -30,12 +39,27 @@
dispatch({ type: "setting/toggleTheme" });
};
+ // ✅ 用于跳转
+ const handleSearch = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Enter" && searchText.trim() !== "") {
+ navigate(`/search?keyword=${encodeURIComponent(searchText.trim())}`);
+ }
+ };
+
return (
<div style={{ display: 'block', height: '100vh' }}>
<header className={style.header}>
<img className={style.logo} src={logo} alt="website logo"></img>
- {showSearch && (<input className={style.searchInput} placeholder="输入关键词进行搜索"/>)}
+ {showSearch && (
+ // <input className={style.searchInput} placeholder="输入关键词进行搜索"/>
+ <input
+ className={style.searchInput}
+ placeholder="输入关键词进行搜索"
+ value={searchText}
+ onChange={(e) => setSearchText(e.target.value)}
+ onKeyDown={handleSearch} // ⌨️ 按下回车时执行跳转
+ />)}
<div className={style.toollist}>
<SearchOutlined onClick={toggleSearch}/>
<FontSizeOutlined onClick={toggleFontSize}/>
diff --git a/src/views/homepage/homepage.module.css b/src/views/homepage/homepage.module.css
new file mode 100644
index 0000000..fdcc060
--- /dev/null
+++ b/src/views/homepage/homepage.module.css
@@ -0,0 +1,249 @@
+/* 主题色变量 */
+/* :root {
+ --primary-color: #3498db;
+ --primary-hover: #2980b9;
+ --secondary-color: #f1c40f;
+ --dark-color: #2c3e50;
+ --light-color: #ecf0f1;
+ --text-color: #333;
+ --text-secondary: #7f8c8d;
+ --border-color: #ddd;
+ } */
+
+ /* --bg-color: #2b2b2b;
+ --text-color: #f1f1f1;
+ --card-bg: #1e1e1e;
+ --border-color: #444444; */
+
+ .container {
+ min-height: 100vh;
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ }
+
+ /* 顶部导航栏 */
+ .header {
+ display: flex;
+ align-items: center;
+ padding: 15px 30px;
+ background-color: solid var(--card-bg);
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ }
+
+ .logo {
+ height: 40px;
+ cursor: pointer;
+ transition: transform 0.3s;
+ }
+
+ .logo:hover {
+ transform: scale(1.05);
+ }
+
+ .siteTitle {
+ margin: 0 0 0 15px;
+ color: var(--text-color);
+ font-size: 24px;
+ }
+
+ /* 主内容区 */
+ .mainContent {
+ display: flex;
+ min-height: calc(100vh - 70px);
+ padding: 20px;
+ gap: 20px;
+ }
+
+ /* 左侧用户信息区 */
+ .userProfile {
+ flex: 2;
+ background-color: var(--card-bg);
+ border-radius: 10px;
+ padding: 25px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
+ }
+
+ .userHeader {
+ display: flex;
+ align-items: center;
+ margin-bottom: 20px;
+ }
+
+ .userAvatar {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ object-fit: cover;
+ border: 3px solid var(--primary-color);
+ margin-right: 20px;
+ }
+
+ .userInfo {
+ flex: 1;
+ }
+
+ .username {
+ margin: 0 0 5px 0;
+ color: var(--text-color);
+ font-size: 22px;
+ }
+
+ .inviteCode {
+ color: var(--text-color);
+ font-size: 14px;
+ margin-bottom: 10px;
+ }
+
+ .editButton {
+ padding: 8px 20px;
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ border-radius: 20px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background-color 0.3s;
+ }
+
+ .editButton:hover {
+ background-color: var(--primary-hover);
+ }
+
+ /* 用户统计 */
+ .userStats {
+ display: flex;
+ justify-content: space-between;
+ margin: 25px 0;
+ padding: 15px 0;
+ border-top: 1px solid var(--border-color);
+ border-bottom: 1px solid var(--border-color);
+ }
+
+ .statItem {
+ text-align: center;
+ padding: 0 10px;
+ }
+
+ .statNumber {
+ font-size: 22px;
+ font-weight: bold;
+ color: var(--primary-color);
+ }
+
+ .statLabel {
+ font-size: 14px;
+ color: var(--text-secondary);
+ margin-top: 5px;
+ }
+
+ /* 用户数据 */
+ .userData {
+ background-color: var(--light-color);
+ padding: 15px;
+ border-radius: 8px;
+ margin-bottom: 25px;
+ }
+
+ .dataItem {
+ margin-bottom: 8px;
+ font-size: 15px;
+ }
+
+ .dataItem strong {
+ color: var(--primary-color);
+ }
+
+ /* 作品区 */
+ .worksSection {
+ margin-top: 30px;
+ }
+
+ .sectionTitle {
+ margin: 0 0 20px 0;
+ color: var(--dark-color);
+ font-size: 18px;
+ padding-bottom: 10px;
+ border-bottom: 2px solid var(--primary-color);
+ }
+
+ .workItem {
+ padding: 15px;
+ margin-bottom: 15px;
+ background-color: var(--primary-card);
+ border-radius: 8px;
+ border-left: 4px solid var(--primary-color);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s, box-shadow 0.3s;
+ }
+
+ .workItem:hover {
+ transform: translateY(-3px);
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+ }
+
+ .workTitle {
+ margin: 0 0 10px 0;
+ color: var(--text-color);
+ font-size: 16px;
+ }
+
+ .workMeta {
+ display: flex;
+ justify-content: space-between;
+ font-size: 14px;
+ color: var(--text-color);
+ }
+
+ /* 右侧内容区 */
+ .rightContent {
+ flex: 1;
+ min-width: 300px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ }
+
+ .petSection, .trafficSection {
+ background-color: var(--card-bg);
+ border-radius: 10px;
+ padding: 20px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
+ }
+
+ .petContainer {
+ background-color: var(--card-bg);
+ border-radius: 8px;
+ padding: 15px;
+ text-align: center;
+ }
+
+ .petImage {
+ max-width: 100%;
+ height: auto;
+ border-radius: 5px;
+ }
+
+ .trafficContainer {
+ background-color: white;
+ border-radius: 8px;
+ padding: 10px;
+ text-align: center;
+ border: 1px solid var(--border-color);
+ }
+
+ .trafficImage {
+ max-width: 100%;
+ height: auto;
+ }
+
+ /* 加载和错误状态 */
+ .loading, .error {
+ padding: 20px;
+ text-align: center;
+ color: var(--text-color);
+ }
+
+ .error {
+ color: #e74c3c;
+ }
\ No newline at end of file
diff --git a/src/views/homepage/homepage.tsx b/src/views/homepage/homepage.tsx
new file mode 100644
index 0000000..58c910f
--- /dev/null
+++ b/src/views/homepage/homepage.tsx
@@ -0,0 +1,164 @@
+import React, { useCallback, useEffect } from 'react';
+import styles from './homepage.module.css';
+import { useApi } from '@/hooks/request';
+import { useSelector } from 'react-redux';
+import { RootState } from '@/store';
+import { useNavigate } from 'react-router';
+import logo from '&/assets/logo.png';
+import { getUserMessage } from '@/api/homepage';
+import request from '@/utils/request'
+import { hotPosts } from '@/api/post';
+import { postUserLogin } from '@/api/auth';
+
+interface WorkItem {
+ postId: number,
+ userId: number,
+ postTitle: string,
+ postContent: string,
+ createdAt: number,
+ postType: string,
+ viewCount: number,
+ hotScore: number,
+ lastCalculated: number
+
+}
+
+interface UserStats {
+ likes: number;
+ following: number;
+ followers: number;
+ mutualFollows: number;
+}
+
+interface UserResponse {
+ username: string;
+ inviteCode: string;
+ stats: UserStats;
+ upload: string;
+ level: string;
+ works: WorkItem[];
+ petImage: string;
+ trafficImage: string;
+}
+
+
+const Homepage: React.FC =() => {
+ const navigate = useNavigate();
+ const userInfo = useSelector((state: RootState) => state.user);
+
+ // 获取用户作品数据
+ // const {data: response, loading, error, refresh} =useApi(()=>request.get(getUserMessage), false);
+ const { data: response, loading, error, refresh } = useApi<UserResponse>(() => request.get(getUserMessage), false);
+ useEffect(() => {
+ refresh(); // 页面首次加载时触发请求
+ }, []);
+ // 用户统计数据
+ const userStats = {
+ likes: response?.stats?.likes ?? 0,
+ following: response?.stats?.following ?? 0,
+ followers: response?.stats?.followers ?? 0,
+ mutualFollows: response?.stats?.mutualFollows ?? 0,
+ uploadAmount: response?.upload ?? '--',
+ level: response?.level ?? '--'
+ };
+
+ const handleLogoClick = () => {
+ navigate('/');
+ };
+
+ return (
+ <div className={styles.container}>
+
+
+ {/* 用户信息主区域 */}
+ <div className={styles.mainContent}>
+ {/* 左侧用户信息区 */}
+ <div className={styles.userProfile}>
+ <div className={styles.userHeader}>
+ <img
+ src={userInfo.avatar || '/default-avatar.png'}
+ alt="用户头像"
+ className={styles.userAvatar}
+ />
+ <div className={styles.userInfo}>
+ <h2 className={styles.username}>阳菜,放睛!</h2>
+ <div className={styles.inviteCode}>邀请码:1314520</div>
+ <button className={styles.editButton}>编辑主页</button>
+ <button
+ className={styles.editButton}
+ onClick={() => navigate('/postDetails', {
+ state: { isNewPost: true }
+ })}
+ >
+ 发布种子
+ </button>
+ </div>
+ </div>
+
+ <div className={styles.userStats}>
+ <div className={styles.statItem}>
+ <div className={styles.statNumber}>{userStats.likes}</div>
+ <div className={styles.statLabel}>获赞</div>
+ </div>
+ <div className={styles.statItem}>
+ <div className={styles.statNumber}>{userStats.following}</div>
+ <div className={styles.statLabel}>关注</div>
+ </div>
+ <div className={styles.statItem}>
+ <div className={styles.statNumber}>{userStats.followers}</div>
+ <div className={styles.statLabel}>粉丝</div>
+ </div>
+ <div className={styles.statItem}>
+ <div className={styles.statNumber}>{userStats.mutualFollows}</div>
+ <div className={styles.statLabel}>互关</div>
+ </div>
+ </div>
+
+ <div className={styles.userData}>
+ <div className={styles.dataItem}>
+ <span>您的总上传量为:</span>
+ <strong>{userStats.uploadAmount}</strong>
+ </div>
+ <div className={styles.dataItem}>
+ <span>您的用户等级为:</span>
+ <strong>{userStats.level}</strong>
+ </div>
+ </div>
+
+ <div className={styles.worksSection}>
+ <h3 className={styles.sectionTitle}>我的作品</h3>
+ {loading && <div className={styles.loading}>加载中...</div>}
+ {error && <div className={styles.error}>{error.message}</div>}
+
+ {response && response.works.map(work => (
+ <div key={work.postId} className={styles.workItem}>
+ <h4 className={styles.workTitle}>{work.postTitle}</h4>
+ <div className={styles.workMeta}>
+ <span>发布时间:{work.createdAt}</span>
+ <span>下载量:{work.viewCount} 做种数:{'待定'}</span>
+ </div>
+ </div>
+ )) }
+ </div>
+ </div>
+
+ {/* 右侧内容区 */}
+ <div className={styles.rightContent}>
+ <div className={styles.petSection}>
+ <h3 className={styles.sectionTitle}>宠物图</h3>
+ <div className={styles.petContainer}>
+ <img
+ src="/assets/pet-blue-star.png"
+ alt="蓝色星星宠物"
+ className={styles.petImage}
+ />
+ </div>
+ </div>
+
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default Homepage;
\ No newline at end of file
diff --git a/src/views/login/login.tsx b/src/views/login/login.tsx
index bd429c7..96c5485 100644
--- a/src/views/login/login.tsx
+++ b/src/views/login/login.tsx
@@ -16,13 +16,17 @@
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const dispatch = useAppDispatch();
- const { refresh: postUserLoginRefresh } = useApi(() => request.post(postUserLogin, {}), false);
- const { refresh: getUserInfoRefresh } = useApi(() => request.get(getUserInfo), false);
+
+ const { refresh: postUserLoginRefresh } = useApi(
+ () => request.post(postUserLogin, { email, password}), false);
+ const { refresh: getUserInfoRefresh } = useApi(
+ () => request.get(getUserInfo), false);
const nav = useNavigate();
const handleLogin = debounce(async () => {
try {
- const res =await postUserLoginRefresh();
+ const res =await postUserLoginRefresh({email, password});
+ console.log("res", res);
if (res==null ||(res as any).error) {
alert('Login failed. Please check your credentials.');
return;
diff --git a/src/views/pet/pet.module.css b/src/views/pet/pet.module.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/views/pet/pet.module.css
diff --git a/src/views/pet/pet.tsx b/src/views/pet/pet.tsx
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/views/pet/pet.tsx
diff --git a/src/views/search/search.module.css b/src/views/search/search.module.css
new file mode 100644
index 0000000..2e1f40a
--- /dev/null
+++ b/src/views/search/search.module.css
@@ -0,0 +1,132 @@
+:root {
+ --primary-color: #3498db;
+ --primary-hover: #2980b9;
+ --secondary-color: #f1c40f;
+ --dark-color: #2c3e50;
+ --light-color: #ecf0f1;
+ --text-color: #333;
+ --text-secondary: #7f8c8d;
+ --border-color: #ddd;
+ --bg-color: #2b2b2b;
+ --card-bg: #1e1e1e;
+}
+
+.container {
+ min-height: 100vh;
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ padding: 20px;
+}
+
+.secondaryHeader {
+ background-color: var(--card-bg);
+ padding: 15px 20px;
+ margin-bottom: 20px;
+ border-radius: 8px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+ display: flex;
+ flex-wrap: wrap; /* 可换成 nowrap + overflow-x: auto 实现强制一行 + 横向滚动 */
+ gap: 15px;
+ align-items: center;
+ justify-content: flex-start;
+}
+
+.selectBox {
+ background-color: var(--light-color);
+ color: var(--text-color);
+ border: 1px solid var(--border-color);
+ border-radius: 5px;
+ padding: 8px 10px;
+ font-size: 14px;
+}
+
+.selectBox:focus {
+ outline: none;
+ border-color: var(--primary-color);
+}
+
+.tagFilters {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 10px;
+ background-color: var(--light-color);
+ color: var(--text-color);
+ border: 1px solid var(--border-color);
+ border-radius: 5px;
+ padding: 8px 10px;
+ font-size: 14px;
+}
+
+.tagFilters label {
+ margin: 0;
+ color: var(--text-color);
+}
+
+.filterButton {
+ padding: 8px 16px;
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ border-radius: 5px;
+ font-size: 14px;
+ cursor: pointer;
+ transition: background-color 0.3s;
+}
+
+.filterButton:hover {
+ background-color: var(--primary-hover);
+}
+
+.results {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.postItem {
+ background-color: var(--card-bg);
+ padding: 20px;
+ border-radius: 8px;
+ border-left: 4px solid var(--primary-color);
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
+}
+
+.postItem h3 {
+ margin: 0 0 10px 0;
+ color: var(--text-color);
+}
+
+.postItem p {
+ margin: 4px 0;
+ color: var(--text-secondary);
+ font-size: 14px;
+}
+
+.secondaryHeader {
+ background-color: var(--card-bg);
+ padding: 15px 20px;
+ margin-bottom: 20px;
+ border-radius: 8px;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.leftSection {
+ display: flex;
+ gap: 10px;
+}
+
+.centerSection {
+ flex: 1;
+ display: flex;
+ justify-content: center;
+}
+
+.rightSection {
+ display: flex;
+ justify-content: flex-end;
+}
diff --git a/src/views/search/search.tsx b/src/views/search/search.tsx
new file mode 100644
index 0000000..cd9db91
--- /dev/null
+++ b/src/views/search/search.tsx
@@ -0,0 +1,131 @@
+// search.tsx
+import React, { useState, useEffect } from 'react';
+import styles from './search.module.css';
+import { useLocation } from "react-router-dom";
+
+interface PostItem {
+ postId: number;
+ userId: number;
+ postTitle: string;
+ postContent: string;
+ createdAt: number;
+ postType: string;
+ viewCount: number;
+ hotScore: number;
+ lastCalculated: number;
+ tags?: number[];
+}
+
+const tagMap: Record<number, string> = {
+ 1: "搞笑",
+ 2: "悬疑",
+ 3: "教育",
+ 4: "动作",
+ 5: "剧情"
+};
+
+const SearchPage: React.FC = () => {
+ const [posts, setPosts] = useState<PostItem[]>([]);
+ const [filteredPosts, setFilteredPosts] = useState<PostItem[]>([]);
+ const [selectedPostType, setSelectedPostType] = useState<string>('all');
+ const [selectedRating, setSelectedRating] = useState<number | null>(null);
+ const [selectedTags, setSelectedTags] = useState<number[]>([]);
+
+ const location = useLocation();
+ const params = new URLSearchParams(location.search);
+ const keyword = params.get("keyword");
+
+ useEffect(() => {
+ fetch('/api/posts')
+ .then((res) => res.json())
+ .then((data) => {
+ setPosts(data);
+ setFilteredPosts(data);
+ });
+ }, []);
+
+ const applyFilters = () => {
+ let filtered = posts;
+
+ if (selectedPostType !== 'all') {
+ filtered = filtered.filter((post) => post.postType === selectedPostType);
+ }
+
+ if (selectedRating !== null) {
+ filtered = filtered.filter((post) => post.hotScore >= selectedRating);
+ }
+
+ if (selectedTags.length > 0) {
+ filtered = filtered.filter((post) =>
+ post.tags?.some((tag) => selectedTags.includes(tag))
+ );
+ }
+
+ setFilteredPosts(filtered);
+ };
+
+ return (
+ <div className={styles.secondaryHeader}>
+ <div className={styles.leftSection}>
+ <select
+ value={selectedPostType}
+ onChange={(e) => setSelectedPostType(e.target.value)}
+ className={styles.selectBox}
+ >
+ <option value="all">所有分区</option>
+ <option value="影视">影视</option>
+ <option value="音乐">音乐</option>
+ <option value="游戏">游戏</option>
+ <option value="软件">软件</option>
+ </select>
+
+ <select
+ value={selectedRating || ''}
+ onChange={(e) =>
+ setSelectedRating(e.target.value ? Number(e.target.value) : null)
+ }
+ className={styles.selectBox}
+ >
+ <option value="">所有评分</option>
+ <option value="1">1星及以上</option>
+ <option value="2">2星及以上</option>
+ <option value="3">3星及以上</option>
+ <option value="4">4星及以上</option>
+ <option value="5">5星</option>
+ </select>
+ </div>
+
+ <div className={styles.centerSection}>
+ <div className={styles.tagFilters}>
+ {Object.entries(tagMap).map(([tagId, tagName]) => (
+ <label key={tagId}>
+ <input
+ type="checkbox"
+ value={tagId}
+ checked={selectedTags.includes(Number(tagId))}
+ onChange={(e) => {
+ const value = Number(e.target.value);
+ setSelectedTags((prev) =>
+ prev.includes(value)
+ ? prev.filter((t) => t !== value)
+ : [...prev, value]
+ );
+ }}
+ />
+ {tagName}
+ </label>
+ ))}
+ </div>
+ </div>
+
+ <div className={styles.rightSection}>
+ <button className={styles.filterButton} onClick={applyFilters}>
+ 筛选
+ </button>
+ </div>
+ </div>
+
+ );
+};
+
+export default SearchPage;
diff --git a/src/views/upload/upload.module.css b/src/views/upload/upload.module.css
new file mode 100644
index 0000000..5a9952a
--- /dev/null
+++ b/src/views/upload/upload.module.css
@@ -0,0 +1,150 @@
+.container {
+ background-color: var(--card-bg);
+ padding: 32px;
+ border-radius: 12px;
+ width: 100%;
+ height: 100%;
+ margin: auto;
+ border: 1px solid var(--border-color);
+ color: var(--text-color);
+}
+
+.formGroup {
+ margin-bottom: 20px;
+}
+
+.input,
+.select,
+.textarea {
+ width: 100%;
+ padding: 8px 12px;
+ margin-top: 4px;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ background-color: var(--bg-color);
+ color: var(--text-color);
+}
+
+.upload {
+ margin-top: 8px;
+}
+
+.textarea {
+ height: 100px;
+ resize: none;
+}
+
+.charCount {
+ text-align: right;
+ font-size: 12px;
+ color: var(--text-color);
+}
+
+.requirement {
+ font-size: 14px;
+ color: var(--primary-color);
+ margin-bottom: 12px;
+}
+
+.checkbox {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 20px;
+}
+
+.submitBtn {
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+}
+
+.submitBtn:hover {
+ background-color: var(--primary-hover);
+}
+
+
+.wrapper {
+ display: flex;
+ justify-content: center;
+ margin-top: 50px;
+}
+
+.form {
+ width: 400px;
+ background: #ffffff;
+ border: 1px solid #ddd;
+ border-radius: 12px;
+ padding: 24px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+}
+
+.title {
+ font-size: 22px;
+ margin-bottom: 20px;
+ text-align: center;
+}
+
+.input,
+.textarea {
+ width: 100%;
+ padding: 10px;
+ margin-bottom: 16px;
+ border-radius: 6px;
+ border: 1px solid #ccc;
+ font-size: 14px;
+}
+
+.textarea {
+ resize: vertical;
+ height: 100px;
+}
+
+.uploadArea {
+ padding: 12px;
+ border: 2px dashed #999;
+ border-radius: 8px;
+ text-align: center;
+ cursor: pointer;
+ background: #f9f9f9;
+ margin-bottom: 16px;
+ transition: all 0.2s;
+}
+
+.uploadArea:hover {
+ background: #f0f0f0;
+ border-color: #666;
+}
+
+.fileName {
+ margin-top: 8px;
+ font-size: 14px;
+ color: #333;
+}
+
+.error {
+ color: red;
+ margin-bottom: 10px;
+ font-size: 14px;
+ text-align: center;
+}
+
+.uploadButton {
+ width: 100%;
+ padding: 10px;
+ background-color: #409eff;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: background-color 0.3s;
+}
+
+.uploadButton:hover {
+ background-color: #317ee7;
+}
diff --git a/src/views/upload/upload.tsx b/src/views/upload/upload.tsx
new file mode 100644
index 0000000..4a60330
--- /dev/null
+++ b/src/views/upload/upload.tsx
@@ -0,0 +1,123 @@
+import instance from '@/utils/axios';
+import React, { useState } from 'react';
+import styles from './upload.module.css';
+import { Upload } from '@/api/upload';
+import { useNavigate } from 'react-router-dom'; // 用于跳转
+
+const PostDetails = () => {
+ const [postTitle, setPostTitle] = useState('');
+ const [postType, setPostType] = useState('');
+ const [postContent, setPostContent] = useState('');
+ const [isChecked, setIsChecked] = useState(false);
+
+ const navigate = useNavigate();
+
+ const handleSubmit = async () => {
+ if (!postTitle.trim() || !postType || !postContent.trim()) {
+ alert('请填写完整内容(资源名、类型、内容介绍)');
+ return;
+ }
+
+ if (!isChecked) {
+ alert('请先确认您已知晓以上内容');
+ return;
+ }
+
+ const payload = {
+ post: {
+ postId: 0,
+ userId: 0,
+ postTitle,
+ postContent,
+ createdAt: Date.now(),
+ postType,
+ viewCount: 0,
+ hotScore: 5,
+ lastCalculated: Date.now()
+ },
+ tagIds: [0]
+ };
+
+ try {
+ const res = await instance.post(Upload, payload);
+
+ console.log('mock返回内容:', res.code);
+
+ // 判断返回内容是否成功(根据你 mock 接口返回的 code 字段)
+ if (res.code !== 0) throw new Error('发布失败');
+
+ alert('发布成功!');
+ navigate(-1); // 返回上一页(homepage)
+ } catch (error) {
+ alert('发布失败,请稍后重试');
+ console.error(error);
+ }
+ };
+
+ return (
+ <div className={styles.container}>
+ <div className={styles.formGroup}>
+ <label>资源名:</label>
+ <input
+ type="text"
+ value={postTitle}
+ placeholder="请输入文本"
+ onChange={(e) => setPostTitle(e.target.value)}
+ className={styles.input}
+ />
+ </div>
+
+ <div className={styles.formGroup}>
+ <label>类型选择:</label>
+ <select
+ value={postType}
+ onChange={(e) => setPostType(e.target.value)}
+ className={styles.select}
+ >
+ <option value="">下拉选择</option>
+ <option value="type1">类型一</option>
+ <option value="type2">类型二</option>
+ </select>
+ </div>
+
+ {/* 暂时移除上传文件表单 */}
+ {/* <div className={styles.formGroup}>
+ <label>上传资源:</label>
+ <input
+ type="file"
+ onChange={(e) => setFile(e.target.files?.[0] || null)}
+ className={styles.upload}
+ />
+ </div> */}
+
+ <div className={styles.formGroup}>
+ <label>内容介绍:</label>
+ <textarea
+ placeholder="请输入内容介绍"
+ value={postContent}
+ onChange={(e) => setPostContent(e.target.value)}
+ maxLength={200}
+ className={styles.textarea}
+ />
+ <div className={styles.charCount}>{postContent.length}/200</div>
+ </div>
+
+ <div className={styles.requirement}>【发布内容要求】</div>
+
+ <div className={styles.checkbox}>
+ <input
+ type="checkbox"
+ checked={isChecked}
+ onChange={() => setIsChecked(!isChecked)}
+ />
+ <span>我已知晓以上内容</span>
+ </div>
+
+ <button onClick={handleSubmit} className={styles.submitBtn}>
+ 我已知晓
+ </button>
+ </div>
+ );
+};
+
+export default PostDetails;