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