修改提示框样式、完成付费片单、推荐跳转
Change-Id: Ie84c53d4e306435144b1f26ceb39cc182e99d57a
diff --git a/src/App.js b/src/App.js
index af1f022..11219d3 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,4 +1,5 @@
import React from 'react';
+import { Toaster } from 'react-hot-toast';
import { Route, useLocation } from 'wouter';
import { UserProvider, useUser } from './context/UserContext';
@@ -27,6 +28,7 @@
import NewUserGuide from './pages/NewUserGuide/NewUserGuide';
import UserRecharge from './pages/UserCenter/UserRecharge';
import GroupDetail from './pages/InterestGroup/GroupDetail';
+import PlaylistDetailPage from './pages/SeedList/Recommend/PlaylistDetailPage';
function RedirectToAuth() {
if (typeof window !== 'undefined') {
@@ -55,6 +57,7 @@
function App() {
return (
<UserProvider>
+ <Toaster />
<GroupProvider>
<>
{/* 公开路由 */}
@@ -78,6 +81,10 @@
<Route path="/information/:userId" component={({ userId }) => <PrivateRoute component={() => <UserInfo userId={userId} />} />} />
<Route path="/new-user-guide" component={() => <PrivateRoute component={NewUserGuide} />} />
<Route path="/group/:groupId" component={({ groupId }) => <PrivateRoute component={() => <GroupDetail groupId={groupId} />} />} />
+ <Route path="/playlist/:id" component={({ id }) => (
+ <PrivateRoute component={() => <PlaylistDetailPage id={id} />} />
+ )} />
+
{/* 用户中心路由 */}
<Route path="/user/profile" component={() => (
diff --git a/src/api/file.js b/src/api/file.js
new file mode 100644
index 0000000..fdfe571
--- /dev/null
+++ b/src/api/file.js
@@ -0,0 +1,8 @@
+import axios from "axios";
+
+
+export const uploadFile = async (file) => {
+ const formData = new FormData();
+ formData.append('file', file);
+ return axios.post('/file', formData, { headers: { 'Content-Type': 'multipart/form-data' } });
+}
\ No newline at end of file
diff --git a/src/components/AuthButton.jsx b/src/components/AuthButton.jsx
new file mode 100644
index 0000000..b5cc431
--- /dev/null
+++ b/src/components/AuthButton.jsx
@@ -0,0 +1,22 @@
+import {useContext} from "react";
+import { UserContext } from "../context/UserContext";
+import toast from "react-hot-toast";
+const AuthButton = ({children, roles,onClick, ...rest }) => {
+ const {user} = useContext(UserContext);
+ const {levelRole} = user;
+ let clickFunc = onClick
+ if(!roles || roles.length === 0 || roles.includes(levelRole)){
+ clickFunc = onClick;
+ }else{
+ clickFunc = () => {
+ toast.error("权限不足");
+ }
+ }
+
+ return (
+ <button onClick={clickFunc} {...rest}>
+ {children}
+ </button>
+ )
+}
+export default AuthButton;
\ No newline at end of file
diff --git a/src/components/Header.css b/src/components/Header.css
index 017ba1d..66fe1a0 100644
--- a/src/components/Header.css
+++ b/src/components/Header.css
@@ -3,9 +3,9 @@
display: flex;
justify-content: space-between;
align-items: center;
- padding: 10px 20px; /* 增加左右间距,避免元素靠得太近 */
- background-color: #5F4437; /* 深棕色背景 */
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* 增加阴影效果 */
+ padding: 10px 20px;
+ background-color: #6b4f3b; /* 深棕色背景 */
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Logo 和网站名称 */
@@ -20,9 +20,9 @@
}
.site-name {
- font-size:32px;
+ font-size: 32px;
font-weight: bold;
- color: #FBF2E3; /* 更加柔和的字体颜色 */
+ color: #fbf2e3; /* 柔和淡米色字体 */
}
/* 用户头像和消息中心 */
@@ -33,69 +33,68 @@
.user-avatar {
height: 50px;
- width: 50px; /* 确保头像是圆形 */
- border-radius: 50%; /* 圆形 */
+ width: 50px;
+ border-radius: 50%;
margin-right: 10px;
}
.message-center {
font-size: 24px;
- color: #FBF2E3; /* 统一字体颜色 */
+ color: #fbf2e3;
cursor: pointer;
- transition: color 0.3s ease; /* 平滑过渡 */
+ transition: color 0.3s ease;
}
.message-center:hover {
- color: #D8C0A1; /* 当鼠标悬停时字体颜色变亮 */
+ color: #eecfc1; /* 清透粉 hover */
}
/* 导航栏样式 */
.nav {
display: flex;
justify-content: center;
- background-color: #dab8c2;
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* 增加阴影 */
- transition: background 0.3s ease;
+ background-color: #fffaf7; /* 粉米底 */
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
+ border-bottom: 2px solid #e2cfc3;
}
/* 每个导航项 */
.nav-item {
padding: 14px 12px;
text-decoration: none;
- color: #FBF2E3;
+ color: #6b4f3b; /* 主文字棕色 */
font-size: 18px;
font-weight: 500;
text-align: center;
- border-radius: 8px; /* 圆角效果 */
- transition: background-color 0.3s, color 0.3s, transform 0.3s ease; /* 平滑过渡 */
- position: relative; /* 为阴影和动画提供定位 */
+ border-radius: 8px;
+ transition: background-color 0.3s, color 0.3s, transform 0.3s ease;
+ position: relative;
}
-/* 鼠标悬停时 */
.nav-item:hover {
- background-color: #5F4437; /* 背景色变深 */
- color: #FBF2E3; /* 字体颜色与背景形成对比 */
- transform: scale(1.05); /* 微微放大,增加互动感 */
+ background-color: #f3ded7; /* 柔粉 hover 背景 */
+ color: #4b3325;
+ transform: scale(1.05);
}
/* 选中状态 */
.nav-item.active {
- background-color: #5F4437; /* 活动状态时的背景色 */
- color: #FBF2E3; /* 活动状态时的字体颜色 */
+ background-color: #eecfc1;
+ color: #3c271b;
font-weight: bold;
- border-radius: 16px; /* 更圆的圆角效果 */
- padding: 4px 12px; /* 减小上下和左右的padding,框变小 */
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); /* 增加阴影,带来浮动感 */
- transition: background-color 0.3s, box-shadow 0.3s, transform 0.3s ease; /* 平滑过渡 */
- transform: scale(1.1); /* 选中项微微放大 */
- line-height: 40px; /* 调整line-height,使文字更居中 */
+ border-radius: 16px;
+ padding: 4px 12px;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
+ transform: scale(1.1);
+ line-height: 40px;
}
+
/* 新手指南按钮 */
.guide-button {
display: flex;
align-items: center;
- background-color: #a67c6a; /* 柔棕底色 */
- color: #fff;
+ background-color: #eecfc1; /* 清透粉底色 */
+ color: #4e342e; /* 深棕文字 */
border: none;
border-radius: 20px;
padding: 6px 12px;
@@ -106,12 +105,10 @@
}
.guide-button:hover {
- background-color: #8d6e63; /* 深一点 hover */
+ background-color: #f3ded7; /* 更柔和 hover */
transform: scale(1.05);
}
.guide-button span {
user-select: none;
}
-
-
diff --git a/src/pages/FriendMoments/FriendMoments.css b/src/pages/FriendMoments/FriendMoments.css
index cca399f..edab68f 100644
--- a/src/pages/FriendMoments/FriendMoments.css
+++ b/src/pages/FriendMoments/FriendMoments.css
@@ -9,7 +9,7 @@
.friend-moments-container {
margin: 0 auto;
- background: #333;
+ background: #f8f3ef;
padding-bottom: 40px;
}
diff --git a/src/pages/InterestGroup/InterestGroup.css b/src/pages/InterestGroup/InterestGroup.css
index e44eea1..b2cf86f 100644
--- a/src/pages/InterestGroup/InterestGroup.css
+++ b/src/pages/InterestGroup/InterestGroup.css
@@ -1,6 +1,6 @@
/* 设置整个兴趣小组页面的背景色和布局 */
.interest-group-container {
- background: #333;
+ background: #f8f3ef;
width: 100%;
height: 2000px;
}
diff --git a/src/pages/PublishSeed/PublishSeed.css b/src/pages/PublishSeed/PublishSeed.css
index 7525fb7..ff7fcad 100644
--- a/src/pages/PublishSeed/PublishSeed.css
+++ b/src/pages/PublishSeed/PublishSeed.css
@@ -1,12 +1,12 @@
-.publish-seed-container {
- background: #333;
+.ps-container {
+ background: #f8f3ef;
color: #333;
min-height: 100vh;
padding: 20px 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
-.pub-card {
+.ps-card {
background-color: #e9ded2;
border-radius: 16px;
max-width: 800px;
@@ -16,20 +16,20 @@
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
-form > div {
+.ps-form > div {
margin-bottom: 20px;
}
-label {
+.ps-label {
display: block;
font-weight: bold;
margin-bottom: 8px;
color: #5F4437;
}
-input[type="text"],
-textarea,
-select {
+.ps-input-text,
+.ps-textarea,
+.ps-select {
width: 100%;
padding: 10px 12px;
border-radius: 6px;
@@ -40,20 +40,20 @@
transition: border-color 0.3s;
}
-input[type="text"]:focus,
-textarea:focus,
-select:focus {
+.ps-input-text:focus,
+.ps-textarea:focus,
+.ps-select:focus {
outline: none;
border-color: #c49c6b;
}
-textarea {
+.ps-textarea {
resize: vertical;
min-height: 80px;
}
-.seed-file-label,
-.cover-upload button {
+.ps-seed-file-label,
+.ps-cover-upload-button {
display: inline-block;
padding: 8px 16px;
border: 1px solid #e0c4a1;
@@ -66,17 +66,17 @@
transition: all 0.2s ease;
}
-.seed-file-label:hover,
-.cover-upload button:hover {
+.ps-seed-file-label:hover,
+.ps-cover-upload-button:hover {
background-color: #f1e0d0;
}
-.seed-file input,
-.cover-upload input {
+.ps-seed-file-input,
+.ps-cover-upload-input {
display: none;
}
-.message {
+.ps-message {
background-color: #fff3cd;
color: #856404;
padding: 12px;
@@ -85,11 +85,11 @@
border: 1px solid #ffeeba;
}
-.upload-button {
+.ps-upload-button {
text-align: center;
}
-.upload-button button {
+.ps-upload-button button {
background-color: #5F4437;
color: #fff;
padding: 12px 32px;
@@ -100,16 +100,16 @@
transition: background-color 0.2s ease;
}
-.upload-button button:hover {
+.ps-upload-button button:hover {
background-color: #472f23;
}
-.upload-button button:disabled {
+.ps-upload-button button:disabled {
background-color: #9c8c84;
cursor: not-allowed;
}
-img {
+.ps-img-preview {
border-radius: 8px;
border: 1px solid #ccc;
max-width: 100%;
diff --git a/src/pages/PublishSeed/PublishSeed.jsx b/src/pages/PublishSeed/PublishSeed.jsx
index a3ac4ad..27717ca 100644
--- a/src/pages/PublishSeed/PublishSeed.jsx
+++ b/src/pages/PublishSeed/PublishSeed.jsx
@@ -4,6 +4,7 @@
import Header from '../../components/Header';
import './PublishSeed.css';
import { useUser } from '../../context/UserContext';
+import { uploadFile } from '../../api/file';
const PublishSeed = () => {
const [title, setTitle] = useState('');
@@ -86,7 +87,6 @@
formData.append('category', category);
formData.append('tags', tags.join(',')); // 逗号分隔字符串
formData.append('uploader', user.userId);
-
if (imageFile) {
formData.append('coverImage', imageFile);
}
@@ -113,20 +113,22 @@
};
return (
- <div className="publish-seed-container">
+ <div className="ps-container">
<Header />
- <div className="pub-card">
- {message && <div className="message">{message}</div>}
+ <div className="ps-card">
+ {message && <div className="ps-message">{message}</div>}
<form
+ className="ps-form"
onSubmit={(e) => {
console.log('[DEBUG] form onSubmit 触发');
handleSubmit(e);
}}
encType="multipart/form-data"
>
- <div className="title-tag">
- <label>标题</label>
+ <div>
+ <label className="ps-label">标题</label>
<input
+ className="ps-input-text"
type="text"
value={title}
onChange={(e) => {
@@ -137,9 +139,10 @@
/>
</div>
- <div className="discription">
- <label>描述</label>
+ <div>
+ <label className="ps-label">描述</label>
<textarea
+ className="ps-textarea"
value={description}
onChange={(e) => {
console.log('[DEBUG] 描述输入变化:', e.target.value);
@@ -149,9 +152,10 @@
/>
</div>
- <div className="title-tag">
- <label>标签 (逗号分隔)</label>
+ <div>
+ <label className="ps-label">标签 (逗号分隔)</label>
<input
+ className="ps-input-text"
type="text"
value={tags.join(', ')}
onChange={handleTagsChange}
@@ -160,9 +164,10 @@
/>
</div>
- <div className="pub-categoty">
- <label>分类</label>
+ <div>
+ <label className="ps-label">分类</label>
<select
+ className="ps-select"
value={category}
onChange={(e) => {
console.log('[DEBUG] 分类选择变化:', e.target.value);
@@ -184,10 +189,10 @@
</select>
</div>
- <div className="seed-file">
- <label>种子文件</label>
+ <div>
+ <label className="ps-label">种子文件</label>
<div
- className="seed-file-label"
+ className="ps-seed-file-label"
onClick={handleFileButtonClick}
style={{ cursor: 'pointer' }}
>
@@ -198,15 +203,19 @@
accept=".torrent"
ref={fileInputRef}
onChange={handleFileChange}
- style={{ display: 'none' }}
+ className="ps-seed-file-input"
/>
- {fileName && <div style={{ marginTop: '5px' }}>{fileName}</div>}
+ {fileName && <div style={{ marginTop: '5px', color: '#5F4437' }}>{fileName}</div>}
</div>
- <div className="form-group">
- <label>封面图</label>
- <div className="cover-upload">
- <button type="button" onClick={handleImageButtonClick}>
+ <div>
+ <label className="ps-label">封面图</label>
+ <div>
+ <button
+ type="button"
+ onClick={handleImageButtonClick}
+ className="ps-cover-upload-button"
+ >
上传图片
</button>
<input
@@ -214,7 +223,7 @@
accept="image/*"
ref={imageInputRef}
onChange={handleImageChange}
- style={{ display: 'none' }}
+ className="ps-cover-upload-input"
/>
</div>
{previewUrl && (
@@ -222,13 +231,13 @@
<img
src={previewUrl}
alt="封面预览"
- style={{ maxWidth: '100%', maxHeight: '200px' }}
+ className="ps-img-preview"
/>
</div>
)}
</div>
- <div className="upload-button">
+ <div className="ps-upload-button">
<button
type="submit"
disabled={isLoading}
diff --git a/src/pages/SeedList/Recommend/CreatePlaylistModal.css b/src/pages/SeedList/Recommend/CreatePlaylistModal.css
new file mode 100644
index 0000000..022e190
--- /dev/null
+++ b/src/pages/SeedList/Recommend/CreatePlaylistModal.css
@@ -0,0 +1,153 @@
+/* CreatePlaylistModal.css */
+
+.create-playlist-modal.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background-color: rgba(0, 0, 0, 0.65);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 9999;
+ padding: 16px;
+}
+
+.create-playlist-modal .modal {
+ background: #fff;
+ border-radius: 14px;
+ width: 420px;
+ max-width: 100%;
+ padding: 32px 40px;
+ box-shadow:
+ 0 10px 15px rgba(0, 0, 0, 0.1),
+ 0 4px 6px rgba(0, 0, 0, 0.05);
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.create-playlist-modal .modal:hover {
+ transform: translateY(-6px);
+}
+
+.create-playlist-modal .modal h3 {
+ margin: 0 0 28px 0;
+ font-weight: 700;
+ font-size: 1.9rem;
+ color: #222;
+ text-align: center;
+ letter-spacing: 0.02em;
+}
+
+/* Label纵向排列 */
+.create-playlist-modal .modal label {
+ display: flex !important;
+ flex-direction: column !important;
+ margin-bottom: 20px;
+ font-weight: 600;
+ color: #444;
+ font-size: 1.05rem;
+ user-select: none;
+ cursor: default; /* 加上避免label内容被选中时光标变形 */
+}
+
+.create-playlist-modal .modal input[type="text"],
+.create-playlist-modal .modal input[type="number"],
+.create-playlist-modal .modal textarea {
+ margin-top: 8px;
+ padding: 12px 16px;
+ border: 1.6px solid #bbb;
+ border-radius: 8px;
+ font-size: 1rem;
+ color: #333;
+ box-sizing: border-box;
+ resize: vertical;
+ transition: border-color 0.25s ease, box-shadow 0.25s ease;
+ font-family: inherit;
+}
+
+.create-playlist-modal .modal input[type="text"]:focus,
+.create-playlist-modal .modal input[type="number"]:focus,
+.create-playlist-modal .modal textarea:focus {
+ outline: none;
+ border-color: #4a69bd;
+ box-shadow: 0 0 10px rgba(74, 105, 189, 0.5);
+}
+
+.create-playlist-modal .modal textarea {
+ min-height: 90px;
+ line-height: 1.5;
+ font-family: inherit;
+}
+
+.create-playlist-modal .modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 16px;
+ margin-top: 32px;
+}
+
+.create-playlist-modal .modal-actions button {
+ padding: 12px 26px;
+ font-size: 1.15rem;
+ font-weight: 700;
+ border: none;
+ border-radius: 10px;
+ cursor: pointer;
+ user-select: none;
+ transition:
+ background-color 0.35s ease,
+ box-shadow 0.35s ease,
+ transform 0.2s ease;
+ font-family: inherit;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+.create-playlist-modal .modal-actions button:first-child {
+ background: linear-gradient(135deg, #4a69bd 0%, #3f51b5 100%);
+ color: #fff;
+ box-shadow: 0 6px 12px rgba(63, 81, 181, 0.5);
+}
+
+.create-playlist-modal .modal-actions button:first-child:hover {
+ background: linear-gradient(135deg, #3f51b5 0%, #2c3e9f 100%);
+ box-shadow: 0 8px 18px rgba(44, 62, 159, 0.75);
+ transform: translateY(-2px);
+}
+
+.create-playlist-modal .modal-actions button:first-child:disabled {
+ background: #a0a5b8;
+ box-shadow: none;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.create-playlist-modal .modal-actions button:last-child {
+ background-color: #f3f4f6;
+ color: #555;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
+ transition: background-color 0.25s ease;
+}
+
+.create-playlist-modal .modal-actions button:last-child:hover {
+ background-color: #d9dade;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
+}
+.torrent-selector {
+ border: 1px solid #ccc;
+ padding: 8px;
+ max-height: 200px;
+ overflow-y: auto;
+ margin-top: 8px;
+}
+.torrent-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+.torrent-list li {
+ margin: 4px 0;
+}
+.selected-torrents {
+ margin-top: 6px;
+ font-size: 0.9em;
+ color: #555;
+}
diff --git a/src/pages/SeedList/Recommend/CreatePlaylistModal.jsx b/src/pages/SeedList/Recommend/CreatePlaylistModal.jsx
new file mode 100644
index 0000000..60a5b76
--- /dev/null
+++ b/src/pages/SeedList/Recommend/CreatePlaylistModal.jsx
@@ -0,0 +1,153 @@
+import React, { useState } from 'react';
+import axios from 'axios';
+import toast from 'react-hot-toast';
+import TorrentSelector from './TorrentSelector'; // 引入种子选择组件
+import './CreatePlaylistModal.css';
+import { uploadFile } from '../../../api/file';
+
+const CreatePlaylistModal = ({ onClose, onSuccess }) => {
+ const [newPlaylist, setNewPlaylist] = useState({
+ title: '',
+ price: '',
+ description: '',
+ torrentList: [], // 选中的种子ID数组
+ });
+
+ const [coverFile, setCoverFile] = useState(null); // 新增封面文件状态
+ const [previewUrl, setPreviewUrl] = useState(''); // 封面预览URL
+
+ const handleSelect = (id, checked) => {
+ setNewPlaylist(prev => {
+ let newList = [...prev.torrentList];
+ if (checked) {
+ if (!newList.includes(id)) newList.push(id);
+ } else {
+ newList = newList.filter(i => i !== id);
+ }
+ return { ...prev, torrentList: newList };
+ });
+ };
+
+ const handleCoverChange = (e) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ setCoverFile(file);
+ setPreviewUrl(URL.createObjectURL(file)); // 生成预览图
+ } else {
+ setCoverFile(null);
+ setPreviewUrl('');
+ }
+ };
+
+ const handleCreate = async () => {
+ const { title, price, description, torrentList } = newPlaylist;
+
+ if (!title.trim()) {
+ toast.error('标题不能为空');
+ return;
+ }
+ if (!price || isNaN(Number(price)) || Number(price) < 0) {
+ toast.error('请输入合法的价格');
+ return;
+ }
+ if (torrentList.length === 0) {
+ toast.error('请至少选择一个种子');
+ return;
+ }
+
+ const toastId = toast.loading('正在创建...');
+ const playlistData = { title, price, description, torrentList }; // 构建片单数据
+
+ try {
+
+ if (coverFile) {
+ const { data } = await uploadFile(coverFile);
+ if (data.code === 0) {
+ playlistData['coverUrl'] = data.data;
+ } else {
+ toast.error('封面上传失败: ' + data.msg, { id: toastId});
+ return;
+ }
+ }
+
+ const res = await axios.post('/playlist', playlistData, {
+ });
+
+ if (res.data.code === 0) {
+ toast.success('片单创建成功', { id: toastId });
+ onSuccess(res.data.data);
+ onClose();
+ } else {
+ toast.error(`创建失败:${res.data.msg}`, { id: toastId });
+ }
+ } catch (err) {
+ console.error('创建片单失败', err);
+ toast.error('创建失败,请稍后重试', { id: toastId });
+ }
+ };
+
+ return (
+ <div className="modal-overlay create-playlist-modal">
+ <div className="modal">
+ <h3>创建新片单</h3>
+
+ <label>
+ 标题:
+ <input
+ type="text"
+ value={newPlaylist.title}
+ onChange={(e) => setNewPlaylist({ ...newPlaylist, title: e.target.value })}
+ />
+ </label>
+
+ <label>
+ 封面图:
+ <input
+ type="file"
+ accept="image/*"
+ onChange={handleCoverChange}
+ />
+ {previewUrl && (
+ <div style={{ marginTop: '10px' }}>
+ <img src={previewUrl} alt="封面预览" style={{ maxWidth: '100%', maxHeight: '150px' }} />
+ </div>
+ )}
+ </label>
+
+ <label>
+ 价格(元):
+ <input
+ type="number"
+ step="0.01"
+ min="0"
+ value={newPlaylist.price}
+ onChange={(e) => setNewPlaylist({ ...newPlaylist, price: e.target.value })}
+ />
+ </label>
+
+ <label>
+ 片单描述:
+ <textarea
+ value={newPlaylist.description}
+ onChange={(e) => setNewPlaylist({ ...newPlaylist, description: e.target.value })}
+ />
+ </label>
+
+ <label>
+ 关联种子选择:
+ <TorrentSelector
+ selectedIds={newPlaylist.torrentList}
+ onSelect={handleSelect}
+ />
+ </label>
+
+ <div className="modal-actions">
+ <button onClick={handleCreate}>提交</button>
+ <button onClick={onClose}>取消</button>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+export default CreatePlaylistModal;
diff --git a/src/pages/SeedList/Recommend/PlaylistDetailPage.css b/src/pages/SeedList/Recommend/PlaylistDetailPage.css
new file mode 100644
index 0000000..78e7068
--- /dev/null
+++ b/src/pages/SeedList/Recommend/PlaylistDetailPage.css
@@ -0,0 +1,83 @@
+.playlist-detail {
+ background-color: #fffaf7; /* 和上一页主背景色保持一致 */
+ border-radius: 12px; /* 统一圆角 */
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); /* 统一阴影 */
+ color: #6b4f3b; /* 主文字颜色,棕色系 */
+}
+
+.playlist-detail h1 {
+ font-size: 2rem;
+ margin-bottom: 16px;
+ color: #4e342e; /* 深棕色标题 */
+ font-weight: 700;
+}
+
+.cover-image {
+ width: 100%;
+ max-height: 400px;
+ object-fit: cover;
+ border-radius: 12px; /* 与容器圆角匹配 */
+ margin-bottom: 20px;
+ box-shadow: 0 4px 12px rgb(107 79 59 / 0.2); /* 柔和阴影 */
+}
+
+.playlist-detail p {
+ font-size: 1rem;
+ line-height: 1.6;
+ margin-bottom: 20px;
+ color: #6b4f3b;
+}
+
+.playlist-detail h3 {
+ font-size: 1.4rem;
+ margin-bottom: 12px;
+ color: #4e342e;
+ font-weight: 600;
+}
+
+.playlist-detail ul {
+ padding-left: 20px;
+ list-style: disc;
+ color: #6b4f3b;
+}
+
+.playlist-detail li {
+ padding: 6px 0;
+ border-bottom: 1px solid #e2cfc3;
+ font-weight: 500;
+ color: #5d4037;
+}
+
+.playlist-detail li:last-child {
+ border-bottom: none;
+}
+
+/* 错误提示 */
+.error {
+ max-width: 600px;
+ margin: 40px auto;
+ padding: 20px;
+ background: #f8d7da; /* 柔和粉棕 */
+ color: #a94442;
+ border: 1px solid #a94442;
+ border-radius: 12px;
+ text-align: center;
+ font-weight: 600;
+}
+
+/* 重试按钮 */
+.retry-button {
+ margin-top: 12px;
+ padding: 8px 16px;
+ background-color: #6b4f3b;
+ border: none;
+ border-radius: 8px;
+ color: #fffaf7;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.25s ease;
+}
+
+.retry-button:hover {
+ background-color: #4b3325;
+}
diff --git a/src/pages/SeedList/Recommend/PlaylistDetailPage.jsx b/src/pages/SeedList/Recommend/PlaylistDetailPage.jsx
new file mode 100644
index 0000000..07558d6
--- /dev/null
+++ b/src/pages/SeedList/Recommend/PlaylistDetailPage.jsx
@@ -0,0 +1,92 @@
+import React, { useEffect, useState } from 'react';
+import { useRoute } from 'wouter'; // ✅ 修改这里
+import axios from 'axios';
+import Header from '../../../components/Header';
+import './PlaylistDetailPage.css';
+import { useUser } from '../../../context/UserContext';
+import toast from 'react-hot-toast';
+import { confirmAlert } from 'react-confirm-alert';
+import 'react-confirm-alert/src/react-confirm-alert.css';
+
+const PlaylistDetailPage = () => {
+ const [match, params] = useRoute('/playlist/:id'); // ✅ 使用 useRoute
+ const id = params?.id;
+ const { user } = useUser();
+ const userId = user?.userId;
+
+ const [detail, setDetail] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+
+ const fetchDetail = async () => {
+ if (!id) {
+ setError('无效的片单ID');
+ setLoading(false);
+ return;
+ }
+ if (!userId) {
+ setError('请先登录');
+ setLoading(false);
+ return;
+ }
+
+ try {
+ setLoading(true);
+ setError('');
+ const res = await axios.get(`/playlist/${id}`, {
+ params: { userId }
+ });
+ if (res.data.code === 0) {
+ setDetail(res.data.data);
+ } else {
+ setError(res.data.msg || '加载失败');
+ }
+ } catch (err) {
+ console.error(err);
+ setError('请求失败,请检查网络');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ if (id && userId) fetchDetail(); // ✅ 防止空 ID 反复触发
+ }, [id, userId]);
+
+ if (loading) return <div>加载中...</div>;
+
+ if (error) {
+ return (
+ <div className="error">
+ {error}
+ <button onClick={fetchDetail} className="retry-button">重试</button>
+ </div>
+ );
+ }
+
+return (
+ <div className="playlist-detail">
+ <Header />
+ <h1>{detail.title}</h1>
+ <img
+ src={detail.coverUrl || '/default-cover.jpg'}
+ alt={detail.title}
+ onError={e => { e.target.src = '/default-cover.jpg'; }}
+ className="cover-image"
+ />
+ <p>{detail.description}</p>
+
+ <h3>包含的种子:</h3>
+ {detail.torrentList && detail.torrentList.length > 0 ? (
+ <ul>
+ {detail.torrentList.map(seed => (
+ <li key={seed.id}>{seed.title}</li>
+ ))}
+ </ul>
+ ) : (
+ <p>暂无种子数据</p>
+ )}
+ </div>
+);
+};
+export default PlaylistDetailPage;
diff --git a/src/pages/SeedList/Recommend/Recommend.css b/src/pages/SeedList/Recommend/Recommend.css
index cf46b86..f1e34da 100644
--- a/src/pages/SeedList/Recommend/Recommend.css
+++ b/src/pages/SeedList/Recommend/Recommend.css
@@ -1,90 +1,153 @@
-/* .recommend-wrapper {
- padding: 20px;
- background-color: #fff;
+/* 推荐页整体容器 */
+.recommendation-page {
+ padding: 24px 32px;
+ background-color: #f8f3ef; /* 米棕底色 */
+ min-height: 100vh;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ color: #4e342e; /* 深米棕文字 */
}
-.recommend-section-title {
- font-size: 20px;
- font-weight: bold;
- margin-bottom: 15px;
+/* 各个版块标题 */
+.recommendation-page h2 {
+ font-size: 1.75rem;
+ font-weight: 700;
+ margin-bottom: 18px;
+ border-left: 6px solid #eecfc1; /* 粉米色强调条 */
+ padding-left: 12px;
+ color: #4e342e; /* 标题米棕色 */
+ user-select: none;
}
+/* 付费片单 - 横向滚动行 */
.recommend-paid-row {
display: flex;
gap: 20px;
- flex-wrap: nowrap;
overflow-x: auto;
+ padding-bottom: 12px;
+ width: 100%;
+ scroll-behavior: smooth;
}
+.recommend-paid-row::-webkit-scrollbar {
+ height: 8px;
+}
+.recommend-paid-row::-webkit-scrollbar-thumb {
+ background-color: rgba(0, 0, 0, 0.3);
+ border-radius: 4px;
+}
+
+/* 付费片单卡片 */
.paid-card {
- width: 200px;
- flex-shrink: 0;
- border: 1px solid #eee;
- border-radius: 8px;
- overflow: hidden;
- background-color: #f9f9f9;
- text-align: center;
+ width: 450px; /* 固定宽度 */
+ flex-shrink: 0; /* 不允许缩小 */
+ border-radius: 10px;
+ /* overflow: hidden; */
+ overflow: visible;
+ background: #fffaf7; /* 粉米背景 */
+ box-shadow: 0 10px 20px rgba(78, 52, 46, 0.1); /* 柔和阴影 */
+ cursor: pointer;
+ transition: transform 0.25s ease, box-shadow 0.25s ease;
+}
+
+.paid-card:hover {
+ transform: translateY(-6px);
+ box-shadow: 0 14px 28px rgba(78, 52, 46, 0.2);
}
.paid-cover {
width: 100%;
- height: 120px;
+ height: 130px;
object-fit: cover;
+ border-bottom: 1px solid #e2cfc3; /* 米棕分割线 */
+ border-radius: 10px 10px 0 0;
}
.paid-title {
- font-size: 16px;
- padding: 10px;
-} */
-
-
-.recommendation-page {
- padding: 20px;
- background-color: #f8f9fa;
-}
-
-.recommendation-page h2 {
- font-size: 1.5rem;
- margin-bottom: 10px;
- color: #333;
-}
-
-.seed-list {
- display: flex;
- flex-wrap: wrap;
- gap: 16px;
- margin-bottom: 32px;
-}
-
-.seed-card {
- width: 160px;
- background: white;
- border-radius: 12px;
- box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
+ font-size: 1.1rem;
+ font-weight: 600;
+ padding: 12px 10px;
+ color: #6b4f3b; /* 深米棕文字 */
+ white-space: nowrap;
overflow: hidden;
- transition: transform 0.2s ease;
- cursor: pointer;
+ text-overflow: ellipsis;
}
+/* 热门资源 - 一行横向滚动 */
+.seed-list.popular-row {
+ display: flex;
+ overflow-x: auto;
+ white-space: nowrap;
+ gap: 16px;
+ padding-bottom: 12px;
+ scrollbar-width: none;
+}
+.seed-list.popular-row::-webkit-scrollbar {
+ display: none;
+}
+
+/* 热门资源卡片 */
+.seed-card {
+ width: 150px;
+ flex-shrink: 0;
+ background: #fffaf7; /* 粉米背景 */
+ border-radius: 12px;
+ box-shadow: 0 8px 16px rgba(78, 52, 46, 0.08);
+ overflow: hidden;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+ cursor: pointer;
+ user-select: none;
+}
.seed-card:hover {
- transform: translateY(-4px);
+ transform: translateY(-6px);
+ box-shadow: 0 12px 24px rgba(78, 52, 46, 0.15);
}
.seed-card img {
width: 100%;
- height: 220px;
+ height: 210px;
object-fit: cover;
+ border-radius: 12px 12px 0 0;
+ border-bottom: 1px solid #e2cfc3;
}
.seed-card .title {
- padding: 8px;
- font-size: 0.95rem;
+ padding: 10px 8px;
+ font-size: 1rem;
+ color: #6b4f3b; /* 深米棕文字 */
text-align: center;
- color: #333;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
+/* 猜你喜欢 - 多行换行排列 */
+.seed-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 18px;
+ margin-bottom: 32px;
+ user-select: none;
+}
+
+/* 登录提醒 */
.login-reminder {
- font-size: 1rem;
- color: #888;
- padding: 10px;
+ font-size: 1.1rem;
+ color: #a1887f; /* 柔和棕色 */
+ padding: 14px 0;
+ text-align: center;
+ font-style: italic;
+ user-select: none;
+}
+
+/* 统一h2的上下间距 */
+.recommendation-page h2 {
+ margin-top: 36px;
+ margin-bottom: 18px;
+}
+
+
+/* 给滚动区域内种子卡片添加最小宽度避免缩得太小 */
+.seed-list.popular-row .seed-card,
+.recommend-paid-row .paid-card {
+ min-width: 150px;
}
diff --git a/src/pages/SeedList/Recommend/Recommend.jsx b/src/pages/SeedList/Recommend/Recommend.jsx
index 8270145..f58c73d 100644
--- a/src/pages/SeedList/Recommend/Recommend.jsx
+++ b/src/pages/SeedList/Recommend/Recommend.jsx
@@ -2,48 +2,205 @@
import axios from 'axios';
import './Recommend.css';
import { useUser } from '../../../context/UserContext';
+import { useLocation } from 'wouter';
+import toast from 'react-hot-toast';
+import CreatePlaylistModal from './CreatePlaylistModal';
+import { confirmAlert } from 'react-confirm-alert';
+import 'react-confirm-alert/src/react-confirm-alert.css';
const Recommend = () => {
const { user } = useUser();
+ const [paidLists, setPaidLists] = useState([]);
const [popularSeeds, setPopularSeeds] = useState([]);
- const [recommendedSeeds, setRecommendedSeeds] = useState([]);
+ const [recommendedSeeds, setRecommendedSeeds] = useState({
+ movie: [],
+ tv: [],
+ anime: []
+ });
+
+ const [showModal, setShowModal] = useState(false);
+ const [, navigate] = useLocation();
useEffect(() => {
- // 获取热门资源
+ axios
+ .get('/playlist/page', { params: { page: 1, size: 8 } })
+ .then((res) => {
+ if (res.data.code === 0) {
+ setPaidLists(res.data.data);
+ } else {
+ toast.error(`获取片单失败:${res.data.msg}`);
+ }
+ })
+ .catch((err) => {
+ console.error('请求片单失败', err);
+ toast.error('请求片单失败,请稍后重试');
+ });
+
axios
.get('/echo/recommendation/popular', { params: { limit: 16 } })
.then((res) => setPopularSeeds(res.data))
- .catch((err) => console.error('获取热门资源失败', err));
+ .catch((err) => {
+ console.error('获取热门资源失败', err);
+ toast.error('获取热门资源失败');
+ });
}, []);
useEffect(() => {
- // 获取个性化推荐
- if (user && user.userId) {
+ if (user?.userId) {
axios
.get(`/echo/recommendation/seeds/${user.userId}`)
- .then((res) => setRecommendedSeeds(res.data))
- .catch((err) => console.error('获取个性化推荐失败', err));
+ .then((res) => {
+ const categorized = { movie: [], tv: [], anime: [] };
+ res.data.forEach((seed) => {
+ if (seed.category === 'movie') categorized.movie.push(seed);
+ else if (seed.category === 'tv') categorized.tv.push(seed);
+ else if (seed.category === 'anime') categorized.anime.push(seed);
+ });
+ setRecommendedSeeds(categorized);
+ })
+ .catch((err) => {
+ console.error('获取个性化推荐失败', err);
+ toast.error('获取个性化推荐失败');
+ });
}
}, [user]);
- const renderSeedCard = (seed) => (
- <div className="seed-card" key={seed.id}>
- <img src={seed.coverUrl || '/default-cover.jpg'} alt={seed.title} />
- <div className="title">{seed.title}</div>
- </div>
+ const handleDelete = (id) => {
+ confirmAlert({
+ title: '确认删除',
+ message: '确定删除此片单吗?',
+ buttons: [
+ {
+ label: '确定',
+ onClick: async () => {
+ const toastId = toast.loading('正在删除...');
+ try {
+ await axios.delete('/playlist', {
+ params: { ids: id },
+ paramsSerializer: (params) =>
+ `ids=${Array.isArray(params.ids) ? params.ids.join(',') : params.ids}`
+ });
+ setPaidLists(paidLists.filter((list) => list.id !== id));
+ toast.success('删除成功', { id: toastId });
+ } catch (error) {
+ console.error('删除失败', error);
+ toast.error('删除失败,请稍后重试', { id: toastId });
+ }
+ }
+ },
+ { label: '取消' }
+ ]
+ });
+ };
+
+ const handlePurchase = (id) => {
+ confirmAlert({
+ title: '确认购买',
+ message: '确定支付该片单?',
+ buttons: [
+ {
+ label: '确定',
+ onClick: async () => {
+ const toastId = toast.loading('购买中...');
+ try {
+ const res = await axios.post(`/playlist/${id}/pay`);
+ if (res.data.code === 0) {
+ toast.success('购买成功', { id: toastId });
+ navigate(`/playlist/${id}`);
+ } else {
+ toast.error(`购买失败:${res.data.msg}`, { id: toastId });
+ }
+ } catch (err) {
+ console.error('支付失败', err);
+ toast.error('购买失败,请稍后重试', { id: toastId });
+ }
+ }
+ },
+ { label: '取消' }
+ ]
+ });
+ };
+
+const renderSeedCard = (seed) => (
+ <div
+ className="seed-card"
+ key={seed.id}
+ onClick={() => navigate(`/seed/${seed.id}`)}
+ style={{ cursor: 'pointer' }}
+ >
+ <img src={seed.imageUrl || '/default-cover.jpg'} alt={seed.title} />
+ <div className="title">{seed.title}</div>
+ </div>
+);
+
+
+ const renderSection = (title, seeds) => (
+ <>
+ <h2>{title}</h2>
+ <div className="seed-list">{seeds.map(renderSeedCard)}</div>
+ </>
);
return (
<div className="recommendation-page">
- <h2>🎬 正在热映</h2>
- <div className="seed-list">{popularSeeds.map(renderSeedCard)}</div>
+ <h2>💰 付费片单</h2>
+ {user && user.role === 'admin' && (
+ <button className="create-button" onClick={() => setShowModal(true)}>
+ ➕ 创建片单
+ </button>
+ )}
- <h2>🎯 个性化推荐</h2>
+ <div className="recommend-paid-row">
+ {paidLists.map((list) => (
+ <div className="paid-card" key={list.id}>
+ <img
+ className="paid-cover"
+ src={list.coverUrl || '/default-cover.jpg'}
+ alt={list.title}
+ />
+ <div className="paid-title">{list.title}</div>
+
+ {user && user.role === 'admin' ? (
+ <div className="admin-actions">
+ <button onClick={() => handleDelete(list.id)}>删除</button>
+ </div>
+ ) : list.isPaid ? (
+ <button onClick={() => navigate(`/playlist/${list.id}`)}>详情</button>
+ ) : (
+ <button onClick={() => handlePurchase(list.id)}>购买</button>
+ )}
+ </div>
+ ))}
+ </div>
+
+ <h2>🎬 正在热映</h2>
+ <div className="seed-list popular-row">
+ {popularSeeds.slice(0, 8).map(renderSeedCard)}
+ </div>
+
+ <h2>🎯 猜你喜欢</h2>
{user ? (
- <div className="seed-list">{recommendedSeeds.map(renderSeedCard)}</div>
+ <>
+ {recommendedSeeds.movie.length > 0 &&
+ renderSection('🎞️ 电影推荐', recommendedSeeds.movie)}
+ {recommendedSeeds.tv.length > 0 &&
+ renderSection('📺 电视剧推荐', recommendedSeeds.tv)}
+ {recommendedSeeds.anime.length > 0 &&
+ renderSection('🎌 动漫推荐', recommendedSeeds.anime)}
+ </>
) : (
<div className="login-reminder">请登录以获取个性化推荐</div>
)}
+
+ {showModal && (
+ <CreatePlaylistModal
+ onClose={() => setShowModal(false)}
+ onSuccess={(newPlaylist) => {
+ setPaidLists([newPlaylist, ...paidLists]);
+ setShowModal(false);
+ }}
+ />
+ )}
</div>
);
};
diff --git a/src/pages/SeedList/Recommend/TorrentSelector.css b/src/pages/SeedList/Recommend/TorrentSelector.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/pages/SeedList/Recommend/TorrentSelector.css
diff --git a/src/pages/SeedList/Recommend/TorrentSelector.jsx b/src/pages/SeedList/Recommend/TorrentSelector.jsx
new file mode 100644
index 0000000..abe2806
--- /dev/null
+++ b/src/pages/SeedList/Recommend/TorrentSelector.jsx
@@ -0,0 +1,84 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import toast from 'react-hot-toast';
+
+const TorrentSelector = ({ selectedIds, onSelect }) => {
+ const [query, setQuery] = useState('');
+ const [torrents, setTorrents] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (query.trim()) {
+ fetchTorrents(query.trim());
+ } else {
+ setTorrents([]);
+ }
+ }, 500);
+
+ return () => clearTimeout(timer);
+ }, [query]);
+
+ const fetchTorrents = async (keyword) => {
+ setLoading(true);
+ try {
+ const res = await axios.get('/seeds/list', {
+ params: {
+ title: keyword, // 与 SeedList 统一参数名
+ page: 1,
+ size: 20,
+ orderKey: 'upload_time',
+ orderDesc: true,
+ }
+ });
+ if (res.data.code === 0) {
+ // 如果接口返回 data.list 格式
+ setTorrents(res.data.data.list || res.data.data || []);
+ } else {
+ setTorrents([]);
+ }
+ } catch (error) {
+ console.error('请求失败', error);
+ setTorrents([]);
+ }
+ setLoading(false);
+ };
+
+ const toggleSelect = (id, checked) => {
+ onSelect(id, checked);
+ };
+
+ return (
+ <div className="torrent-selector">
+ <input
+ placeholder="搜索种子名称"
+ value={query}
+ onChange={(e) => setQuery(e.target.value)}
+ />
+ {loading && <div>加载中...</div>}
+ <ul>
+ {torrents.map(t => (
+ <li key={t.id}>
+ <label>
+ <input
+ type="checkbox"
+ value={t.id}
+ checked={selectedIds.includes(t.id)}
+ onChange={(e) => toggleSelect(t.id, e.target.checked)}
+ />
+ {t.title || t.name}
+ </label>
+ </li>
+ ))}
+ </ul>
+ {selectedIds.length > 0 && (
+ <div className="selected-torrents">
+ <b>已选种子:</b>
+ {selectedIds.join(', ')}
+ </div>
+ )}
+ </div>
+ );
+};
+
+export default TorrentSelector;
diff --git a/src/pages/SeedList/SeedDetail/SeedDetail.css b/src/pages/SeedList/SeedDetail/SeedDetail.css
index 9e1a10e..d5a5148 100644
--- a/src/pages/SeedList/SeedDetail/SeedDetail.css
+++ b/src/pages/SeedList/SeedDetail/SeedDetail.css
@@ -1,5 +1,5 @@
.seed-detail-page {
- background: #333;
+ background: #f8f3ef;
min-height: 100vh;
padding-bottom: 40px;
font-family: 'Helvetica Neue', sans-serif;
diff --git a/src/pages/SeedList/SeedList.css b/src/pages/SeedList/SeedList.css
index cf5502c..cdbf651 100644
--- a/src/pages/SeedList/SeedList.css
+++ b/src/pages/SeedList/SeedList.css
@@ -1,99 +1,109 @@
.seed-list-container {
- background: #333;
+ background: #f8f3ef; /* 柔和米棕背景 */
}
/* 搜索、排序控件 */
.controls {
+ background-color: #fffaf7; /* 粉米白 */
+ padding: 10px 20px;
display: flex;
justify-content: center;
gap: 16px;
- padding: 10px 20px;
- background-color: #5F4437;
+ border-radius: 12px;
+ border: 1px solid #e2cfc3; /* 浅棕边框 */
}
.search-input {
padding: 6px 10px;
- border-radius: 6px;
- border: none;
+ border-radius: 8px;
+ border: 1px solid #e2cfc3;
width: 200px;
+ font-size: 1rem;
+ color: #6b4f3b;
+ background-color: #fffaf7;
}
.sort-select {
padding: 6px;
- border-radius: 6px;
- border: none;
+ border-radius: 8px;
+ border: 1px solid #e2cfc3;
+ background-color: #fffaf7;
+ color: #6b4f3b;
+ cursor: pointer;
}
/* 标签过滤 */
.tag-filters {
- background-color: #5F4437;
+ background-color: #fffaf7;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
padding: 10px;
+ border-radius: 12px;
+ border: 1px solid #e2cfc3;
}
.tag-button {
- background-color: #b38867;
- color: white;
+ background-color: #eecfc1;
+ color: #6b4f3b;
border: none;
border-radius: 20px;
padding: 6px 12px;
cursor: pointer;
+ font-weight: 600;
+ transition: background-color 0.3s ease;
+}
+
+.tag-button:hover {
+ background-color: #f3ded7;
+ color: #4b3325;
}
.active-tag {
- background-color: #d17c4f;
+ background-color: #6b4f3b;
+ color: #fffaf7;
}
.clear-filter-btn {
background: transparent;
border: none;
- color: #888;
+ color: #6b4f3b;
font-size: 1rem;
cursor: pointer;
margin-left: 4px;
+ transition: color 0.3s ease;
}
.clear-filter-btn:hover {
- color: red;
+ color: #4b3325;
}
-/* 去除 Link 组件默认的下划线和文字颜色变化 */
-.seed-item-link {
- text-decoration: none;
- color: inherit;
- display: block;
-}
-
-/* 卡片展示 */
.seed-list-content {
padding: 20px;
- background-color: #5F4437;
+ background-color: #fffaf7;
+ border-radius: 12px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
}
.seed-list-card {
- background-color: #e9ded2;
- color: #333;
+ background-color: #fffaf7;
+ color: #6b4f3b;
border-radius: 8px;
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 1px 4px rgba(107, 79, 59, 0.15);
overflow: hidden;
}
.seed-list-header {
+ background-color: #eecfc1;
+ font-weight: bold;
+ color: #4e342e;
+ padding: 12px 16px;
display: grid;
grid-template-columns: 180px 2fr 1fr 1fr 1fr 1fr;
align-items: center;
justify-items: center;
- padding: 12px 16px;
- background-color: #BA929A;
- font-weight: bold;
-}
-
-.seed-list-body {
- display: flex;
- flex-direction: column;
+ border-radius: 8px 8px 0 0;
}
.seed-item {
@@ -102,33 +112,31 @@
align-items: center;
justify-items: center;
padding: 12px 16px;
- border-top: 1px solid #ccc;
+ border-top: 1px solid #e2cfc3;
+ color: #6b4f3b;
+ transition: background-color 0.2s ease;
+ cursor: pointer;
+}
+
+.seed-item:hover {
+ background-color: #f3ded7;
}
.seed-item-cover {
width: 100px;
- height: 140px;
+ height: auto;
object-fit: cover;
border-radius: 6px;
- flex-shrink: 0;
+ margin-right: 12px;
+ box-shadow: 0 3px 6px rgb(107 79 59 / 0.2);
}
.seed-item-title {
width: 100%;
text-align: center;
-}
-
-.seed-title-row {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 8px;
-}
-
-.seed-title {
+ font-weight: 600;
font-size: 1.1rem;
- margin: 0;
- font-weight: bold;
+ color: #4e342e;
}
.seed-tags {
@@ -141,81 +149,64 @@
word-break: break-word;
}
-.seed-item-actions {
- display: flex;
- flex-direction: row;
- gap: 8px;
- justify-content: center;
-}
-
-.seed-header-cover {
- width: 180px;
- flex-shrink: 0;
- text-align: center;
-}
-
-.seed-header-title,
-.seed-header-size,
-.seed-header-upload-time,
-.seed-header-downloads,
-.seed-header-actions {
- text-align: center;
-}
-
.seed-info {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
- color: #666;
+ color: #5d4037;
margin-bottom: 8px;
}
.tag-label {
- background-color: #eee;
+ background-color: #fffaf7;
border-radius: 4px;
padding: 2px 6px;
font-size: 12px;
-}
-
-.btn-primary,
-.btn-secondary,
-.btn-outline {
- padding: 6px 12px;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-size: 0.9rem;
- text-align: center;
- white-space: nowrap;
- transition: background-color 0.2s ease;
+ color: #6b4f3b;
+ font-weight: 500;
}
.btn-primary {
- background-color: #007bff;
- color: white;
+ background-color: #6b4f3b;
+ color: #fffaf7;
+ border-radius: 6px;
+ padding: 6px 12px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+ font-weight: 600;
}
.btn-primary:hover {
- background-color: #0056b3;
+ background-color: #4b3325;
}
.btn-secondary {
- background-color: #28a745;
- color: white;
+ background-color: #eecfc1;
+ color: #6b4f3b;
+ border-radius: 6px;
+ padding: 6px 12px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+ font-weight: 600;
}
.btn-secondary:hover {
- background-color: #218838;
+ background-color: #f3ded7;
}
.btn-outline {
background-color: transparent;
- border: 1px solid #ccc;
- color: #333;
+ border: 1px solid #6b4f3b;
+ color: #6b4f3b;
+ border-radius: 6px;
+ padding: 6px 12px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
}
.btn-outline:hover {
- background-color: #f8f9fa;
+ background-color: #fffaf7;
+ color: #4b3325;
}
.seed-cover {
@@ -224,11 +215,5 @@
object-fit: cover;
border-radius: 8px;
margin-bottom: 12px;
-}
-.seed-item-cover {
- width: 100px;
- height: auto;
- object-fit: cover;
- border-radius: 4px;
- margin-right: 12px;
-}
+ box-shadow: 0 4px 12px rgb(107 79 59 / 0.2);
+}
\ No newline at end of file
diff --git a/src/pages/SeedList/SeedList.jsx b/src/pages/SeedList/SeedList.jsx
index a74d008..263038b 100644
--- a/src/pages/SeedList/SeedList.jsx
+++ b/src/pages/SeedList/SeedList.jsx
@@ -1,11 +1,14 @@
-// export default SeedList;
import React, { useState, useEffect } from 'react';
import { Link } from 'wouter';
import axios from 'axios';
import Recommend from './Recommend/Recommend';
-import Header from '../../components/Header'; // 引入 Header 组件
+import Header from '../../components/Header';
import './SeedList.css';
import { useUser } from '../../context/UserContext';
+import toast from 'react-hot-toast';
+import { confirmAlert } from 'react-confirm-alert';
+import 'react-confirm-alert/src/react-confirm-alert.css';
+import AuthButton from '../../components/AuthButton';
const SeedList = () => {
const [seeds, setSeeds] = useState([]);
@@ -15,13 +18,13 @@
const [activeTab, setActiveTab] = useState('种子列表');
const [filters, setFilters] = useState({});
const [selectedFilters, setSelectedFilters] = useState({});
- const [tagMode, setTagMode] = useState('any'); // 与接口对应,any / all
+ const [tagMode, setTagMode] = useState('any');
const [errorMsg, setErrorMsg] = useState('');
const { user } = useUser();
const TAGS = ['猜你喜欢', '电影', '电视剧', '动漫', '音乐', '游戏', '综艺', '软件', '体育', '学习', '纪录片', '其他'];
-const CATEGORY_MAP = {
+ const CATEGORY_MAP = {
'电影': 'movie',
'电视剧': 'tv',
'动漫': 'anime',
@@ -33,10 +36,9 @@
'学习': 'study',
'纪录片': 'documentary',
'其他': 'other',
- '猜你喜欢': '',
- '种子列表': '',
-};
-
+ '猜你喜欢': '',
+ '种子列表': '',
+ };
const buildQueryParams = () => {
const category = CATEGORY_MAP[activeTab] || '';
@@ -61,26 +63,22 @@
if (tags.length > 0) {
params.tags = tags;
- params.tagMode = tagMode; // any 或 all
+ params.tagMode = tagMode;
}
return params;
};
const fetchSeeds = async () => {
- if (activeTab === '猜你喜欢') return;
+ if (activeTab === '猜你喜欢') return;
setLoading(true);
setErrorMsg('');
try {
const params = buildQueryParams();
const response = await axios.get('/seeds/list', { params });
-
const data = response.data;
- if (data.code !== 0) {
- throw new Error(data.msg || '获取失败');
- }
-
+ if (data.code !== 0) throw new Error(data.msg || '获取失败');
setSeeds(data.data || []);
} catch (error) {
console.error('获取种子列表失败:', error);
@@ -120,16 +118,9 @@
}, [activeTab, sortOption, selectedFilters, tagMode, searchTerm]);
const handleDownload = async (seedId) => {
- if (!user || !user.userId) {
- alert('请先登录再下载种子文件');
- return;
- }
-
try {
const response = await axios.get(`/seeds/${seedId}/download`, {
- params: {
- passkey: user.userId,
- },
+ params: { passkey: user.userId },
responseType: 'blob'
});
@@ -142,22 +133,16 @@
URL.revokeObjectURL(downloadUrl);
} catch (error) {
console.error('下载失败:', error);
- alert('下载失败,请稍后再试。');
+ toast.error('下载失败,请稍后再试。');
}
};
const handleFilterChange = (key, value) => {
- setSelectedFilters(prev => ({
- ...prev,
- [key]: value
- }));
+ setSelectedFilters(prev => ({ ...prev, [key]: value }));
};
const clearFilter = (key) => {
- setSelectedFilters(prev => ({
- ...prev,
- [key]: '不限'
- }));
+ setSelectedFilters(prev => ({ ...prev, [key]: '不限' }));
};
return (
@@ -172,19 +157,11 @@
onChange={(e) => setSearchTerm(e.target.value)}
className="search-input"
/>
- <select
- value={sortOption}
- onChange={(e) => setSortOption(e.target.value)}
- className="sort-select"
- >
+ <select value={sortOption} onChange={(e) => setSortOption(e.target.value)} className="sort-select">
<option value="最新">最新</option>
<option value="最热">最热</option>
</select>
- <select
- value={tagMode}
- onChange={(e) => setTagMode(e.target.value)}
- className="tag-mode-select"
- >
+ <select value={tagMode} onChange={(e) => setTagMode(e.target.value)} className="tag-mode-select">
<option value="any">包含任意标签</option>
<option value="all">包含所有标签</option>
</select>
@@ -192,7 +169,8 @@
<div className="tag-filters">
{TAGS.map(tag => (
- <button
+ <AuthButton
+ roles={["test"]}
key={tag}
className={`tag-button ${activeTab === tag ? 'active-tag' : ''}`}
onClick={() => {
@@ -202,7 +180,7 @@
}}
>
{tag}
- </button>
+ </AuthButton>
))}
</div>
@@ -211,21 +189,13 @@
{Object.entries(filters).map(([key, options]) => (
<div className="filter-group" key={key}>
<label>{key}:</label>
- <select
- value={selectedFilters[key]}
- onChange={(e) => handleFilterChange(key, e.target.value)}
- >
+ <select value={selectedFilters[key]} onChange={(e) => handleFilterChange(key, e.target.value)}>
{options.map(opt => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
{selectedFilters[key] !== '不限' && (
- <button
- className="clear-filter-btn"
- onClick={() => clearFilter(key)}
- >
- ✕
- </button>
+ <button className="clear-filter-btn" onClick={() => clearFilter(key)}>✕</button>
)}
</div>
))}
@@ -270,16 +240,11 @@
}
return (
- <Link to={`/seed/${seed.id}`} key={index} className="seed-item-link">
+ <Link to={`/seed/${seed.id}`} key={index} className="seed-item-link">
<div className="seed-item">
{seed.imageUrl && (
- <img
- src={seed.imageUrl}
- alt={seed.title}
- className="seed-item-cover"
- />
+ <img src={seed.imageUrl} alt={seed.title} className="seed-item-cover" />
)}
-
<div className="seed-item-title">
<div className="seed-title-row">
<h3 className="seed-title">{seed.title}</h3>
@@ -293,16 +258,30 @@
<div className="seed-item-size">{seed.size || '未知'}</div>
<div className="seed-item-upload-time">{seed.upload_time?.split('T')[0] || '未知'}</div>
<div className="seed-item-downloads">{seed.downloads ?? 0} 次下载</div>
- <div
- className="seed-item-actions"
- onClick={e => e.stopPropagation()}
- >
+ <div className="seed-item-actions" onClick={e => e.stopPropagation()}>
<button
className="btn-primary"
onClick={e => {
e.preventDefault();
e.stopPropagation();
- handleDownload(seed.id);
+ if (!user || !user.userId) {
+ toast.error('请先登录再下载种子文件');
+ return;
+ }
+ confirmAlert({
+ title: '确认下载',
+ message: `是否下载种子「${seed.title}」?`,
+ buttons: [
+ {
+ label: '确认',
+ onClick: () => handleDownload(seed.id)
+ },
+ {
+ label: '取消',
+ onClick: () => { }
+ }
+ ]
+ });
}}
>
下载
@@ -314,7 +293,7 @@
e.stopPropagation();
if (!user || !user.userId) {
- alert('请先登录再收藏');
+ toast.error('请先登录再收藏');
return;
}
@@ -324,13 +303,13 @@
});
if (res.data.code === 0) {
- alert('操作成功');
+ toast.success('操作成功');
} else {
- alert(res.data.msg || '操作失败');
+ toast.error(res.data.msg || '操作失败');
}
} catch (err) {
console.error('收藏失败:', err);
- alert('收藏失败,请稍后再试。');
+ toast.error('收藏失败,请稍后再试。');
}
}}
>
diff --git a/src/pages/UserCenter/UserNav.css b/src/pages/UserCenter/UserNav.css
index 6a1b405..baf3cb9 100644
--- a/src/pages/UserCenter/UserNav.css
+++ b/src/pages/UserCenter/UserNav.css
@@ -35,11 +35,11 @@
/* 鼠标悬浮时的背景颜色 */
.user-nav-item:hover {
- background-color: #5a1414;
+ background-color: #fffaf7;
}
/* 激活项的样式 */
.user-nav-item.active {
- background-color: #BA929A;
+ background-color: #eecfc1;
color: white;
}
\ No newline at end of file
diff --git a/src/pages/UserCenter/UserProfile.css b/src/pages/UserCenter/UserProfile.css
index edc04e1..18f793d 100644
--- a/src/pages/UserCenter/UserProfile.css
+++ b/src/pages/UserCenter/UserProfile.css
@@ -4,7 +4,7 @@
font-family: Arial, sans-serif;
display: flex;
gap: 10%;
- background: #333;
+ background: #f8f3ef;
}
.right-content {
@@ -47,7 +47,7 @@
}
.common-card {
- background-color: #e9ded2;
+ background-color: #fffaf7;
border-radius: 16px;
margin: 0 auto;
margin-top: 40px;
@@ -72,7 +72,7 @@
position: absolute;
bottom: 0;
right: 0;
- background: #3498db;
+ background: #4b3325;
color: white;
padding: 4px 8px;
font-size: 12px;
@@ -135,7 +135,7 @@
position: absolute;
bottom: 0;
right: 0;
- background: #3498db;
+ background: #4b3325;
color: white;
padding: 4px 8px;
font-size: 12px;
@@ -201,7 +201,7 @@
padding: 6px 12px;
border: none;
border-radius: 6px;
- background-color: #4a90e2;
+ background-color: #4b3325;
color: white;
cursor: pointer;
}
@@ -245,14 +245,14 @@
.profile-actions button {
padding: 8px 16px;
- background-color: #4677f5;
+ background-color: #4b3325;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
-.modal {
+.user-modal {
position: fixed;
top: 0;
left: 0;
@@ -265,21 +265,21 @@
z-index: 99;
}
-.modal-content {
+.user-modal-content {
background-color: white;
padding: 20px;
border-radius: 8px;
width: 300px;
}
-.modal-content input {
+.user-modal-content input {
display: block;
width: 100%;
margin: 10px 0;
padding: 8px;
}
-.modal-buttons {
+.user-modal-buttons {
display: flex;
justify-content: space-between;
}
diff --git a/src/pages/UserCenter/UserProfile.jsx b/src/pages/UserCenter/UserProfile.jsx
index cf52d99..bcfbf0b 100644
--- a/src/pages/UserCenter/UserProfile.jsx
+++ b/src/pages/UserCenter/UserProfile.jsx
@@ -1,266 +1,3 @@
-// import React, { useEffect, useState } from 'react';
-// import axios from 'axios';
-// import './UserProfile.css';
-// import { useUser } from '../../context/UserContext';
-// import { useLocation } from 'wouter';
-
-// const DEFAULT_AVATAR_URL = `${process.env.PUBLIC_URL}/default-avatar.png`;
-
-// const UserProfile = () => {
-// const { user, loading, logout } = useUser();
-// const [userProfile, setUserProfile] = useState(null);
-// const [experienceInfo, setExperienceInfo] = useState(null);
-// const [error, setError] = useState(null);
-
-// // 修改密码状态
-// const [showPwdModal, setShowPwdModal] = useState(false);
-// const [oldPassword, setOldPassword] = useState('');
-// const [newPassword, setNewPassword] = useState('');
-// const [confirmPassword, setConfirmPassword] = useState('');
-
-// // 退出登录
-// const [, setLocation] = useLocation();
-
-// useEffect(() => {
-// if (loading) return;
-// if (!user || !user.userId) {
-// setError('未登录或用户信息缺失');
-// setUserProfile(null);
-// return;
-// }
-
-// const fetchUserProfile = async () => {
-// try {
-// setError(null);
-// const { data: raw } = await axios.get(`/echo/user/${user.userId}/getProfile`);
-// if (!raw) {
-// setError('用户数据为空');
-// setUserProfile(null);
-// return;
-// }
-
-// const profile = {
-// avatarUrl: raw.avatarUrl
-// ? `${process.env.REACT_APP_AVATAR_BASE_URL}${raw.avatarUrl}`
-// : DEFAULT_AVATAR_URL,
-// nickname: raw.username || '未知用户',
-// email: raw.email || '未填写',
-// gender: raw.gender || '保密',
-// bio: raw.description || '无',
-// interests: raw.hobbies ? raw.hobbies.split(',') : [],
-// level: raw.level || '未知',
-// experience: raw.experience ?? 0,
-// uploadAmount: raw.uploadCount ?? 0,
-// downloadAmount: raw.downloadCount ?? 0,
-// shareRate: raw.shareRate ?? 0,
-// joinedDate: raw.registrationTime,
-// };
-
-// setUserProfile(profile);
-// } catch (err) {
-// setError(err.response?.status === 404 ? '用户不存在' : '请求失败,请稍后再试');
-// setUserProfile(null);
-// }
-// };
-
-// const fetchExperienceInfo = async () => {
-// try {
-// const { data } = await axios.get('/echo/level/getExperience', {
-// params: { user_id: user.userId },
-// });
-// setExperienceInfo(data);
-// } catch (err) {
-// console.error('经验信息获取失败:', err);
-// }
-// };
-
-// fetchUserProfile();
-// fetchExperienceInfo();
-// }, [user, loading]);
-
-// const handleAvatarUpload = async (e) => {
-// const file = e.target.files[0];
-// if (!file) return;
-
-// const formData = new FormData();
-// formData.append('file', file);
-
-// try {
-// const { data } = await axios.post(
-// `/echo/user/${user.userId}/uploadAvatar`,
-// formData,
-// { headers: { 'Content-Type': 'multipart/form-data' } }
-// );
-
-// if (data?.avatarUrl) {
-// setUserProfile((prev) => ({
-// ...prev,
-// avatarUrl: `${process.env.REACT_APP_AVATAR_BASE_URL}${data.avatarUrl}`,
-// }));
-// alert('头像上传成功');
-// } else {
-// alert('头像上传成功,但未返回新头像地址');
-// }
-// } catch (err) {
-// console.error('上传失败:', err);
-// alert('头像上传失败,请重试');
-// }
-// };
-
-// const handleLogout = () => {
-// logout();
-// setLocation('/auth'); // 退出后跳转登录页
-// // window.location.reload(); // 或跳转登录页
-// };
-
-// const handleChangePassword = async () => {
-// if (!oldPassword || !newPassword || !confirmPassword) {
-// alert('请填写所有字段');
-// return;
-// }
-// if (newPassword !== confirmPassword) {
-// alert('两次输入的新密码不一致');
-// return;
-// }
-
-// try {
-// // await axios.post('/echo/user/password', {
-// // user_id: user.userId,
-// // oldPassword,
-// // newPassword,
-// // });
-// await axios.post('/echo/user/password', {
-// user_id: user.userId,
-// old_password: oldPassword,
-// new_password: newPassword,
-// confirm_password: confirmPassword,
-// });
-// alert('密码修改成功,请重新登录');
-// logout();
-// window.location.reload();
-// } catch (err) {
-// alert(err.response?.data?.message || '密码修改失败,请检查原密码是否正确');
-// }
-// };
-
-// if (loading) return <p>正在加载用户信息...</p>;
-// if (error) return <p className="error">{error}</p>;
-// if (!userProfile) return null;
-
-// const {
-// avatarUrl,
-// nickname,
-// email,
-// gender,
-// bio,
-// interests,
-// level,
-// experience,
-// uploadAmount,
-// downloadAmount,
-// shareRate,
-// joinedDate,
-// } = userProfile;
-
-// const progressPercent = experienceInfo
-// ? Math.min(
-// 100,
-// ((experienceInfo.current_experience || 0) /
-// (experienceInfo.next_level_experience || 1)) *
-// 100
-// ).toFixed(2)
-// : 0;
-
-// const expToNextLevel = experienceInfo
-// ? (experienceInfo.next_level_experience - experienceInfo.current_experience)
-// : null;
-
-// return (
-// <div className="common-card">
-// <div className="right-content">
-// <div className="profile-header">
-// <div className="avatar-wrapper">
-// <img src={avatarUrl} alt={nickname} className="avatar" />
-// <label htmlFor="avatar-upload" className="avatar-upload-label">
-// 上传头像
-// </label>
-// <input
-// type="file"
-// id="avatar-upload"
-// accept="image/*"
-// style={{ display: 'none' }}
-// onChange={handleAvatarUpload}
-// />
-// </div>
-// <h1>{nickname}</h1>
-// </div>
-
-// <div className="profile-details">
-// <p><strong>邮箱:</strong>{email}</p>
-// <p><strong>性别:</strong>{gender}</p>
-// <p><strong>个人简介:</strong>{bio}</p>
-// <p><strong>兴趣:</strong>{interests.length > 0 ? interests.join(', ') : '无'}</p>
-// <p><strong>等级:</strong>{level}</p>
-// <p><strong>经验:</strong>{experience}</p>
-// <p><strong>上传量:</strong>{uploadAmount}</p>
-// <p><strong>下载量:</strong>{downloadAmount}</p>
-// <p><strong>分享率:</strong>{(shareRate * 100).toFixed(2)}%</p>
-// <p><strong>加入时间:</strong>{new Date(joinedDate).toLocaleDateString()}</p>
-
-// {experienceInfo && (
-// <>
-// <p><strong>距离下一等级还需:</strong>{expToNextLevel} 经验值</p>
-// <div className="exp-bar-wrapper">
-// <div className="exp-bar" style={{ width: `${progressPercent}%` }} />
-// </div>
-// <p className="exp-progress-text">{progressPercent}%</p>
-// </>
-// )}
-
-// {/* 修改密码与退出登录按钮 */}
-// <div className="profile-actions">
-// <button onClick={() => setShowPwdModal(true)}>修改密码</button>
-// <button onClick={handleLogout}>退出登录</button>
-// </div>
-
-// {/* 修改密码弹窗 */}
-// {showPwdModal && (
-// <div className="modal">
-// <div className="modal-content">
-// <h3>修改密码</h3>
-// <input
-// type="password"
-// placeholder="原密码"
-// value={oldPassword}
-// onChange={(e) => setOldPassword(e.target.value)}
-// />
-// <input
-// type="password"
-// placeholder="新密码"
-// value={newPassword}
-// onChange={(e) => setNewPassword(e.target.value)}
-// />
-// <input
-// type="password"
-// placeholder="确认新密码"
-// value={confirmPassword}
-// onChange={(e) => setConfirmPassword(e.target.value)}
-// />
-// <div className="modal-buttons">
-// <button onClick={handleChangePassword}>确认修改</button>
-// <button onClick={() => setShowPwdModal(false)}>取消</button>
-// </div>
-// </div>
-// </div>
-// )}
-// </div>
-// </div>
-// </div>
-// );
-// };
-
-// export default UserProfile;
-
import React from 'react';
import UserProfileBase from './UserProfileBase';
import UserLevelExperience from './UserLevelExperience';
diff --git a/src/pages/UserCenter/UserProfileBase.jsx b/src/pages/UserCenter/UserProfileBase.jsx
index 7a1f726..791baca 100644
--- a/src/pages/UserCenter/UserProfileBase.jsx
+++ b/src/pages/UserCenter/UserProfileBase.jsx
@@ -2,6 +2,9 @@
import axios from 'axios';
import { useUser } from '../../context/UserContext';
import { useLocation } from 'wouter';
+import toast from 'react-hot-toast';
+import { confirmAlert } from 'react-confirm-alert';
+import 'react-confirm-alert/src/react-confirm-alert.css';
const DEFAULT_AVATAR_URL = `${process.env.PUBLIC_URL}/default-avatar.png`;
@@ -10,13 +13,11 @@
const [userProfile, setUserProfile] = useState(null);
const [error, setError] = useState(null);
- // 修改密码状态
const [showPwdModal, setShowPwdModal] = useState(false);
const [oldPassword, setOldPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
- // 退出登录
const [, setLocation] = useLocation();
useEffect(() => {
@@ -55,7 +56,6 @@
};
setUserProfile(profile);
- // 加载经验信息
if (onLoadExperienceInfo) onLoadExperienceInfo(user.userId);
} catch (err) {
setError(err.response?.status === 404 ? '用户不存在' : '请求失败,请稍后再试');
@@ -85,28 +85,44 @@
...prev,
avatarUrl: `${process.env.REACT_APP_AVATAR_BASE_URL}${data.avatarUrl}`,
}));
- alert('头像上传成功');
+ toast.success('头像上传成功');
} else {
- alert('头像上传成功,但未返回新头像地址');
+ toast.success('头像上传成功,但未返回新头像地址');
}
} catch (err) {
console.error('上传失败:', err);
- alert('头像上传失败,请重试');
+ toast.error('头像上传失败,请重试');
}
};
const handleLogout = () => {
logout();
- setLocation('/auth'); // 退出后跳转登录页
+ setLocation('/auth');
+ };
+
+ const confirmPasswordChange = () => {
+ confirmAlert({
+ title: '确认修改密码',
+ message: '确定要修改密码吗?修改成功后将自动登出。',
+ buttons: [
+ {
+ label: '确认',
+ onClick: handleChangePassword,
+ },
+ {
+ label: '取消',
+ },
+ ],
+ });
};
const handleChangePassword = async () => {
if (!oldPassword || !newPassword || !confirmPassword) {
- alert('请填写所有字段');
+ toast.error('请填写所有字段');
return;
}
if (newPassword !== confirmPassword) {
- alert('两次输入的新密码不一致');
+ toast.error('两次输入的新密码不一致');
return;
}
@@ -117,11 +133,14 @@
new_password: newPassword,
confirm_password: confirmPassword,
});
- alert('密码修改成功,请重新登录');
+
+ toast.success('密码修改成功,请重新登录');
logout();
- window.location.reload();
+ setTimeout(() => {
+ window.location.reload();
+ }, 1500);
} catch (err) {
- alert(err.response?.data?.message || '密码修改失败,请检查原密码是否正确');
+ toast.error(err.response?.data?.message || '密码修改失败,请检查原密码是否正确');
}
};
@@ -169,23 +188,19 @@
<p><strong>性别:</strong>{gender}</p>
<p><strong>个人简介:</strong>{bio}</p>
<p><strong>兴趣:</strong>{interests.length > 0 ? interests.join(', ') : '无'}</p>
- {/* <p><strong>等级:</strong>{level}</p>
- <p><strong>经验:</strong>{experience}</p> */}
<p><strong>上传量:</strong>{uploadAmount}</p>
<p><strong>下载量:</strong>{downloadAmount}</p>
<p><strong>分享率:</strong>{(shareRate * 100).toFixed(2)}%</p>
<p><strong>加入时间:</strong>{new Date(joinedDate).toLocaleDateString()}</p>
- {/* 修改密码与退出登录按钮 */}
<div className="profile-actions">
<button onClick={() => setShowPwdModal(true)}>修改密码</button>
<button onClick={handleLogout}>退出登录</button>
</div>
- {/* 修改密码弹窗 */}
{showPwdModal && (
- <div className="modal">
- <div className="modal-content">
+ <div className="user-modal">
+ <div className="user-modal-content">
<h3>修改密码</h3>
<input
type="password"
@@ -205,8 +220,8 @@
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
- <div className="modal-buttons">
- <button onClick={handleChangePassword}>确认修改</button>
+ <div className="user-modal-buttons">
+ <button onClick={confirmPasswordChange}>确认修改</button>
<button onClick={() => setShowPwdModal(false)}>取消</button>
</div>
</div>
@@ -218,4 +233,4 @@
);
};
-export default UserProfileBase;
\ No newline at end of file
+export default UserProfileBase;
diff --git a/src/pages/UserCenter/UserRecharge.jsx b/src/pages/UserCenter/UserRecharge.jsx
index 7a2f9b3..b1dff24 100644
--- a/src/pages/UserCenter/UserRecharge.jsx
+++ b/src/pages/UserCenter/UserRecharge.jsx
@@ -1,42 +1,183 @@
-// export default Recharge;
+// // export default Recharge;
+// import React, { useState } from 'react';
+// import axios from 'axios';
+// import { useUser } from '../../context/UserContext';
+// import toast from 'react-hot-toast';
+// import { confirmAlert } from 'react-confirm-alert';
+// import 'react-confirm-alert/src/react-confirm-alert.css';
+
+// const UserRecharge = () => {
+// const [amount, setAmount] = useState('');
+// const [loading, setLoading] = useState(false);
+// const [message, setMessage] = useState('');
+// const [showPayDialog, setShowPayDialog] = useState(false);
+// const [payProcessing, setPayProcessing] = useState(false);
+
+// const { user } = useUser();
+// const userId = user?.userId;
+
+// // 点击提交充值,打开模拟支付弹窗
+// const handleSubmit = () => {
+// if (!userId) {
+// setMessage('未登录,无法充值');
+// return;
+// }
+// if (!amount || isNaN(amount) || Number(amount) <= 0) {
+// setMessage('请输入有效的充值金额');
+// return;
+// }
+// setMessage('');
+// setShowPayDialog(true);
+// };
+
+// // 模拟支付弹窗点击“确认支付”
+// const handlePayConfirm = async () => {
+// setPayProcessing(true);
+// setMessage('');
+
+// // 模拟支付等待 2 秒
+// setTimeout(async () => {
+// try {
+// // 支付成功后调用后端充值接口
+// const response = await axios.post('/echo/user/recharge', null, {
+// params: {
+// userId,
+// amount: Number(amount),
+// },
+// });
+
+// setMessage(response.data.message || '充值成功!');
+// setAmount('');
+// } catch (error) {
+// setMessage(error.response?.data?.message || '充值失败,请重试');
+// } finally {
+// setPayProcessing(false);
+// setShowPayDialog(false);
+// }
+// }, 2000);
+// };
+
+// // 取消支付弹窗
+// const handlePayCancel = () => {
+// setShowPayDialog(false);
+// setMessage('支付已取消');
+// };
+
+// return (
+// <div className="recharge-page" style={{ maxWidth: 400, margin: 'auto', padding: 20 }}>
+// <h2>充值服务</h2>
+// <div style={{ marginBottom: 12 }}>
+// <label>
+// 充值金额:
+// <input
+// type="number"
+// min="1"
+// value={amount}
+// onChange={(e) => setAmount(e.target.value)}
+// disabled={loading || payProcessing}
+// style={{ marginLeft: 8, width: 120 }}
+// />
+// </label>
+// </div>
+// <button onClick={handleSubmit} disabled={loading || payProcessing}>
+// {loading || payProcessing ? '处理中...' : '提交充值'}
+// </button>
+
+// {message && <p style={{ marginTop: 12 }}>{message}</p>}
+
+// {/* 模拟支付弹窗 */}
+// {showPayDialog && (
+// <div
+// style={{
+// position: 'fixed',
+// top: 0, left: 0, right: 0, bottom: 0,
+// backgroundColor: 'rgba(0,0,0,0.5)',
+// display: 'flex',
+// justifyContent: 'center',
+// alignItems: 'center',
+// zIndex: 1000,
+// }}
+// >
+// <div
+// style={{
+// backgroundColor: '#fff',
+// padding: 20,
+// borderRadius: 8,
+// width: 300,
+// textAlign: 'center',
+// boxShadow: '0 0 10px rgba(0,0,0,0.25)',
+// }}
+// >
+// <h3>模拟支付</h3>
+// <p>支付金额:{amount} 元</p>
+
+// {payProcessing ? (
+// <p>支付处理中,请稍候...</p>
+// ) : (
+// <>
+// <button onClick={handlePayConfirm} style={{ marginRight: 10 }}>
+// 确认支付
+// </button>
+// <button onClick={handlePayCancel}>取消</button>
+// </>
+// )}
+// </div>
+// </div>
+// )}
+// </div>
+// );
+// };
+
+// export default UserRecharge;
import React, { useState } from 'react';
import axios from 'axios';
import { useUser } from '../../context/UserContext';
+import toast from 'react-hot-toast';
+import { confirmAlert } from 'react-confirm-alert';
+import 'react-confirm-alert/src/react-confirm-alert.css';
const UserRecharge = () => {
const [amount, setAmount] = useState('');
- const [loading, setLoading] = useState(false);
- const [message, setMessage] = useState('');
- const [showPayDialog, setShowPayDialog] = useState(false);
const [payProcessing, setPayProcessing] = useState(false);
const { user } = useUser();
const userId = user?.userId;
- // 点击提交充值,打开模拟支付弹窗
+ // 点击提交充值
const handleSubmit = () => {
if (!userId) {
- setMessage('未登录,无法充值');
+ toast.error('未登录,无法充值');
return;
}
if (!amount || isNaN(amount) || Number(amount) <= 0) {
- setMessage('请输入有效的充值金额');
+ toast.error('请输入有效的充值金额');
return;
}
- setMessage('');
- setShowPayDialog(true);
+
+ confirmAlert({
+ title: '确认支付',
+ message: `确认充值 ${amount} 元吗?`,
+ buttons: [
+ {
+ label: '确认支付',
+ onClick: () => handlePayConfirm(),
+ },
+ {
+ label: '取消',
+ onClick: () => toast('已取消充值'),
+ },
+ ],
+ });
};
- // 模拟支付弹窗点击“确认支付”
const handlePayConfirm = async () => {
setPayProcessing(true);
- setMessage('');
- // 模拟支付等待 2 秒
+ toast.loading('正在处理支付...', { id: 'recharge' });
+
setTimeout(async () => {
try {
- // 支付成功后调用后端充值接口
const response = await axios.post('/echo/user/recharge', null, {
params: {
userId,
@@ -44,23 +185,16 @@
},
});
- setMessage(response.data.message || '充值成功!');
+ toast.success(response.data.message || '充值成功!', { id: 'recharge' });
setAmount('');
} catch (error) {
- setMessage(error.response?.data?.message || '充值失败,请重试');
+ toast.error(error.response?.data?.message || '充值失败,请重试', { id: 'recharge' });
} finally {
setPayProcessing(false);
- setShowPayDialog(false);
}
}, 2000);
};
- // 取消支付弹窗
- const handlePayCancel = () => {
- setShowPayDialog(false);
- setMessage('支付已取消');
- };
-
return (
<div className="recharge-page" style={{ maxWidth: 400, margin: 'auto', padding: 20 }}>
<h2>充值服务</h2>
@@ -72,56 +206,14 @@
min="1"
value={amount}
onChange={(e) => setAmount(e.target.value)}
- disabled={loading || payProcessing}
+ disabled={payProcessing}
style={{ marginLeft: 8, width: 120 }}
/>
</label>
</div>
- <button onClick={handleSubmit} disabled={loading || payProcessing}>
- {loading || payProcessing ? '处理中...' : '提交充值'}
+ <button onClick={handleSubmit} disabled={payProcessing}>
+ {payProcessing ? '处理中...' : '提交充值'}
</button>
-
- {message && <p style={{ marginTop: 12 }}>{message}</p>}
-
- {/* 模拟支付弹窗 */}
- {showPayDialog && (
- <div
- style={{
- position: 'fixed',
- top: 0, left: 0, right: 0, bottom: 0,
- backgroundColor: 'rgba(0,0,0,0.5)',
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- zIndex: 1000,
- }}
- >
- <div
- style={{
- backgroundColor: '#fff',
- padding: 20,
- borderRadius: 8,
- width: 300,
- textAlign: 'center',
- boxShadow: '0 0 10px rgba(0,0,0,0.25)',
- }}
- >
- <h3>模拟支付</h3>
- <p>支付金额:{amount} 元</p>
-
- {payProcessing ? (
- <p>支付处理中,请稍候...</p>
- ) : (
- <>
- <button onClick={handlePayConfirm} style={{ marginRight: 10 }}>
- 确认支付
- </button>
- <button onClick={handlePayCancel}>取消</button>
- </>
- )}
- </div>
- </div>
- )}
</div>
);
};