22301009 | afbcf4b | 2025-04-10 16:08:39 +0800 | [diff] [blame] | 1 | import React, { useEffect, useState } from 'react'; |
Krishya | 3dc6b35 | 2025-06-07 19:02:25 +0800 | [diff] [blame] | 2 | import axios from 'axios'; |
22301009 | ecc1c1c | 2025-04-09 21:56:23 +0800 | [diff] [blame] | 3 | import './Recommend.css'; |
Krishya | 3dc6b35 | 2025-06-07 19:02:25 +0800 | [diff] [blame] | 4 | import { useUser } from '../../../context/UserContext'; |
22301009 | 1e2aea7 | 2025-06-08 16:35:54 +0800 | [diff] [blame] | 5 | import { useLocation } from 'wouter'; |
| 6 | import toast from 'react-hot-toast'; |
| 7 | import CreatePlaylistModal from './CreatePlaylistModal'; |
| 8 | import { confirmAlert } from 'react-confirm-alert'; |
| 9 | import 'react-confirm-alert/src/react-confirm-alert.css'; |
22301009 | 207e2db | 2025-06-09 00:27:28 +0800 | [diff] [blame^] | 10 | import AuthButton from '../../../components/AuthButton'; |
22301009 | ecc1c1c | 2025-04-09 21:56:23 +0800 | [diff] [blame] | 11 | |
22301009 | ecc1c1c | 2025-04-09 21:56:23 +0800 | [diff] [blame] | 12 | const Recommend = () => { |
Krishya | 3dc6b35 | 2025-06-07 19:02:25 +0800 | [diff] [blame] | 13 | const { user } = useUser(); |
22301009 | 1e2aea7 | 2025-06-08 16:35:54 +0800 | [diff] [blame] | 14 | const [paidLists, setPaidLists] = useState([]); |
Krishya | 3dc6b35 | 2025-06-07 19:02:25 +0800 | [diff] [blame] | 15 | const [popularSeeds, setPopularSeeds] = useState([]); |
22301009 | 1e2aea7 | 2025-06-08 16:35:54 +0800 | [diff] [blame] | 16 | const [recommendedSeeds, setRecommendedSeeds] = useState({ |
| 17 | movie: [], |
| 18 | tv: [], |
| 19 | anime: [] |
| 20 | }); |
| 21 | |
| 22 | const [showModal, setShowModal] = useState(false); |
| 23 | const [, navigate] = useLocation(); |
22301009 | afbcf4b | 2025-04-10 16:08:39 +0800 | [diff] [blame] | 24 | |
| 25 | useEffect(() => { |
22301009 | 1e2aea7 | 2025-06-08 16:35:54 +0800 | [diff] [blame] | 26 | axios |
| 27 | .get('/playlist/page', { params: { page: 1, size: 8 } }) |
| 28 | .then((res) => { |
| 29 | if (res.data.code === 0) { |
| 30 | setPaidLists(res.data.data); |
| 31 | } else { |
| 32 | toast.error(`获取片单失败:${res.data.msg}`); |
| 33 | } |
| 34 | }) |
| 35 | .catch((err) => { |
| 36 | console.error('请求片单失败', err); |
| 37 | toast.error('请求片单失败,请稍后重试'); |
| 38 | }); |
| 39 | |
Krishya | 3dc6b35 | 2025-06-07 19:02:25 +0800 | [diff] [blame] | 40 | axios |
| 41 | .get('/echo/recommendation/popular', { params: { limit: 16 } }) |
| 42 | .then((res) => setPopularSeeds(res.data)) |
22301009 | 1e2aea7 | 2025-06-08 16:35:54 +0800 | [diff] [blame] | 43 | .catch((err) => { |
| 44 | console.error('获取热门资源失败', err); |
| 45 | toast.error('获取热门资源失败'); |
| 46 | }); |
22301009 | afbcf4b | 2025-04-10 16:08:39 +0800 | [diff] [blame] | 47 | }, []); |
| 48 | |
Krishya | 3dc6b35 | 2025-06-07 19:02:25 +0800 | [diff] [blame] | 49 | useEffect(() => { |
22301009 | 1e2aea7 | 2025-06-08 16:35:54 +0800 | [diff] [blame] | 50 | if (user?.userId) { |
Krishya | 3dc6b35 | 2025-06-07 19:02:25 +0800 | [diff] [blame] | 51 | axios |
| 52 | .get(`/echo/recommendation/seeds/${user.userId}`) |
22301009 | 1e2aea7 | 2025-06-08 16:35:54 +0800 | [diff] [blame] | 53 | .then((res) => { |
| 54 | const categorized = { movie: [], tv: [], anime: [] }; |
| 55 | res.data.forEach((seed) => { |
| 56 | if (seed.category === 'movie') categorized.movie.push(seed); |
| 57 | else if (seed.category === 'tv') categorized.tv.push(seed); |
| 58 | else if (seed.category === 'anime') categorized.anime.push(seed); |
| 59 | }); |
| 60 | setRecommendedSeeds(categorized); |
| 61 | }) |
| 62 | .catch((err) => { |
| 63 | console.error('获取个性化推荐失败', err); |
| 64 | toast.error('获取个性化推荐失败'); |
| 65 | }); |
Krishya | 3dc6b35 | 2025-06-07 19:02:25 +0800 | [diff] [blame] | 66 | } |
| 67 | }, [user]); |
| 68 | |
22301009 | 1e2aea7 | 2025-06-08 16:35:54 +0800 | [diff] [blame] | 69 | const handleDelete = (id) => { |
| 70 | confirmAlert({ |
| 71 | title: '确认删除', |
| 72 | message: '确定删除此片单吗?', |
| 73 | buttons: [ |
| 74 | { |
| 75 | label: '确定', |
| 76 | onClick: async () => { |
| 77 | const toastId = toast.loading('正在删除...'); |
| 78 | try { |
| 79 | await axios.delete('/playlist', { |
| 80 | params: { ids: id }, |
| 81 | paramsSerializer: (params) => |
| 82 | `ids=${Array.isArray(params.ids) ? params.ids.join(',') : params.ids}` |
| 83 | }); |
| 84 | setPaidLists(paidLists.filter((list) => list.id !== id)); |
| 85 | toast.success('删除成功', { id: toastId }); |
| 86 | } catch (error) { |
| 87 | console.error('删除失败', error); |
| 88 | toast.error('删除失败,请稍后重试', { id: toastId }); |
| 89 | } |
| 90 | } |
| 91 | }, |
| 92 | { label: '取消' } |
| 93 | ] |
| 94 | }); |
| 95 | }; |
| 96 | |
| 97 | const handlePurchase = (id) => { |
| 98 | confirmAlert({ |
| 99 | title: '确认购买', |
| 100 | message: '确定支付该片单?', |
| 101 | buttons: [ |
| 102 | { |
| 103 | label: '确定', |
| 104 | onClick: async () => { |
| 105 | const toastId = toast.loading('购买中...'); |
| 106 | try { |
| 107 | const res = await axios.post(`/playlist/${id}/pay`); |
| 108 | if (res.data.code === 0) { |
| 109 | toast.success('购买成功', { id: toastId }); |
| 110 | navigate(`/playlist/${id}`); |
| 111 | } else { |
| 112 | toast.error(`购买失败:${res.data.msg}`, { id: toastId }); |
| 113 | } |
| 114 | } catch (err) { |
| 115 | console.error('支付失败', err); |
| 116 | toast.error('购买失败,请稍后重试', { id: toastId }); |
| 117 | } |
| 118 | } |
| 119 | }, |
| 120 | { label: '取消' } |
| 121 | ] |
| 122 | }); |
| 123 | }; |
| 124 | |
| 125 | const renderSeedCard = (seed) => ( |
| 126 | <div |
| 127 | className="seed-card" |
| 128 | key={seed.id} |
| 129 | onClick={() => navigate(`/seed/${seed.id}`)} |
| 130 | style={{ cursor: 'pointer' }} |
| 131 | > |
| 132 | <img src={seed.imageUrl || '/default-cover.jpg'} alt={seed.title} /> |
| 133 | <div className="title">{seed.title}</div> |
| 134 | </div> |
| 135 | ); |
| 136 | |
| 137 | |
| 138 | const renderSection = (title, seeds) => ( |
| 139 | <> |
| 140 | <h2>{title}</h2> |
| 141 | <div className="seed-list">{seeds.map(renderSeedCard)}</div> |
| 142 | </> |
Krishya | 3dc6b35 | 2025-06-07 19:02:25 +0800 | [diff] [blame] | 143 | ); |
| 144 | |
22301009 | ecc1c1c | 2025-04-09 21:56:23 +0800 | [diff] [blame] | 145 | return ( |
Krishya | 3dc6b35 | 2025-06-07 19:02:25 +0800 | [diff] [blame] | 146 | <div className="recommendation-page"> |
22301009 | 1e2aea7 | 2025-06-08 16:35:54 +0800 | [diff] [blame] | 147 | <h2>💰 付费片单</h2> |
| 148 | {user && user.role === 'admin' && ( |
| 149 | <button className="create-button" onClick={() => setShowModal(true)}> |
| 150 | ➕ 创建片单 |
| 151 | </button> |
| 152 | )} |
Krishya | 3dc6b35 | 2025-06-07 19:02:25 +0800 | [diff] [blame] | 153 | |
22301009 | 1e2aea7 | 2025-06-08 16:35:54 +0800 | [diff] [blame] | 154 | <div className="recommend-paid-row"> |
| 155 | {paidLists.map((list) => ( |
| 156 | <div className="paid-card" key={list.id}> |
| 157 | <img |
| 158 | className="paid-cover" |
| 159 | src={list.coverUrl || '/default-cover.jpg'} |
| 160 | alt={list.title} |
| 161 | /> |
| 162 | <div className="paid-title">{list.title}</div> |
| 163 | |
22301009 | 207e2db | 2025-06-09 00:27:28 +0800 | [diff] [blame^] | 164 | {user && user.role === 'admin' ? ( |
| 165 | <div className="admin-actions"> |
| 166 | <button onClick={() => handleDelete(list.id)}>删除</button> |
| 167 | </div> |
| 168 | ) : list.isPaid ? ( |
| 169 | <button onClick={() => navigate(`/playlist/${list.id}`)}>详情</button> |
| 170 | ) : ( |
| 171 | <AuthButton roles={['chocolate', 'ice-cream']} onClick={() => handlePurchase(list.id)}> |
| 172 | 购买 |
| 173 | </AuthButton> |
| 174 | )} |
| 175 | |
22301009 | 1e2aea7 | 2025-06-08 16:35:54 +0800 | [diff] [blame] | 176 | </div> |
| 177 | ))} |
| 178 | </div> |
| 179 | |
| 180 | <h2>🎬 正在热映</h2> |
| 181 | <div className="seed-list popular-row"> |
| 182 | {popularSeeds.slice(0, 8).map(renderSeedCard)} |
| 183 | </div> |
| 184 | |
| 185 | <h2>🎯 猜你喜欢</h2> |
Krishya | 3dc6b35 | 2025-06-07 19:02:25 +0800 | [diff] [blame] | 186 | {user ? ( |
22301009 | 1e2aea7 | 2025-06-08 16:35:54 +0800 | [diff] [blame] | 187 | <> |
| 188 | {recommendedSeeds.movie.length > 0 && |
| 189 | renderSection('🎞️ 电影推荐', recommendedSeeds.movie)} |
| 190 | {recommendedSeeds.tv.length > 0 && |
| 191 | renderSection('📺 电视剧推荐', recommendedSeeds.tv)} |
| 192 | {recommendedSeeds.anime.length > 0 && |
| 193 | renderSection('🎌 动漫推荐', recommendedSeeds.anime)} |
| 194 | </> |
22301009 | afbcf4b | 2025-04-10 16:08:39 +0800 | [diff] [blame] | 195 | ) : ( |
Krishya | 3dc6b35 | 2025-06-07 19:02:25 +0800 | [diff] [blame] | 196 | <div className="login-reminder">请登录以获取个性化推荐</div> |
22301009 | afbcf4b | 2025-04-10 16:08:39 +0800 | [diff] [blame] | 197 | )} |
22301009 | 1e2aea7 | 2025-06-08 16:35:54 +0800 | [diff] [blame] | 198 | |
| 199 | {showModal && ( |
| 200 | <CreatePlaylistModal |
| 201 | onClose={() => setShowModal(false)} |
| 202 | onSuccess={(newPlaylist) => { |
| 203 | setPaidLists([newPlaylist, ...paidLists]); |
| 204 | setShowModal(false); |
| 205 | }} |
| 206 | /> |
| 207 | )} |
22301009 | ecc1c1c | 2025-04-09 21:56:23 +0800 | [diff] [blame] | 208 | </div> |
| 209 | ); |
| 210 | }; |
| 211 | |
| 212 | export default Recommend; |