Docker

Change-Id: I2aefd96a43bcf3a3c41c079ecfc04a3fee48bed6
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;