Files
d8d-admin-mobile-starter-pu…/client/mobile/pages_index.tsx
yourname 1df9f7fea2 创建了api目录并拆分为10个功能模块文件
建立了index.ts统一导出
精简了原api.ts文件
2025-05-13 09:00:58 +00:00

282 lines
11 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router';
import { HomeAPI } from './api/index.ts';
import { MessageAPI } from './api/index.ts';
import {
HomeIcon,
UserIcon,
NewspaperIcon,
BellIcon
} from '@heroicons/react/24/outline';
import { useAuth } from './hooks.tsx';
import { formatRelativeTime } from './utils.ts';
import { KnowInfo, UserMessage, MessageType, MessageStatus } from '../share/types.ts';
// 首页组件
const HomePage: React.FC = () => {
const { user } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const [loading, setLoading] = useState(true);
const [banners, setBanners] = useState<KnowInfo[]>([]);
const [news, setNews] = useState<KnowInfo[]>([]);
const [notices, setNotices] = useState<UserMessage[]>([]);
const [activeTab, setActiveTab] = useState('news');
// 模拟加载数据
useEffect(() => {
const fetchData = async () => {
try {
// 获取数据
const [bannersRes, newsRes, messagesRes] = await Promise.all([
HomeAPI.getBanners(),
HomeAPI.getNews(),
MessageAPI.getMessages({ type: MessageType.ANNOUNCE })
]);
setBanners(bannersRes.data.map((item: KnowInfo) => ({
id: item.id,
title: item.title,
cover_url: item.cover_url,
content: item.content,
category: 'banner',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
sort_order: item.sort_order || 0
} as KnowInfo)));
setNews(newsRes.data);
setNotices(messagesRes.data);
setLoading(false);
} catch (error) {
console.error('获取首页数据失败:', error);
setLoading(false);
}
};
fetchData();
}, []);
// 处理轮播图点击
const handleBannerClick = (link: string) => {
navigate(link);
};
// 处理新闻点击
const handleNewsClick = (id: number) => {
navigate(`/news/${id}`);
};
// 处理通知点击
const handleNoticeClick = (id: number) => {
navigate(`/notices/${id}`);
};
return (
<div className="pb-16">
{/* 顶部用户信息 */}
<div className="bg-blue-600 text-white p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-12 h-12 rounded-full bg-white/20 flex items-center justify-center">
{user?.avatar ? (
<img
src={user.avatar}
alt={user?.nickname || user?.username || '用户'}
className="w-10 h-10 rounded-full object-cover"
/>
) : (
<UserIcon className="w-6 h-6" />
)}
</div>
<div>
<h2 className="text-lg font-medium">
{user ? `您好,${user.nickname || user.username}` : '您好,游客'}
</h2>
<p className="text-sm text-white/80">
{user ? '欢迎回来' : '请登录体验更多功能'}
</p>
</div>
</div>
<div className="relative">
<BellIcon className="w-6 h-6" />
{notices.some(notice => notice.user_status === MessageStatus.UNREAD) && (
<span className="absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full"></span>
)}
</div>
</div>
</div>
{/* 轮播图 */}
{!loading && banners.length > 0 && (
<div className="relative w-full h-40 overflow-hidden mt-2">
<div className="flex transition-transform duration-300"
style={{ transform: `translateX(-${0 * 100}%)` }}>
{banners.map((banner) => (
<div
key={banner.id}
className="w-full h-40 flex-shrink-0 relative"
onClick={() => handleBannerClick(banner.content || '')}
>
<img
src={banner.cover_url || ''}
alt={banner.title}
className="w-full h-full object-cover"
/>
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3">
<h3 className="text-white font-medium">{banner.title}</h3>
</div>
</div>
))}
</div>
{/* 指示器 */}
<div className="absolute bottom-2 left-0 right-0 flex justify-center space-x-1">
{banners.map((_, index) => (
<span
key={index}
className={`w-2 h-2 rounded-full ${index === 0 ? 'bg-white' : 'bg-white/50'}`}
></span>
))}
</div>
</div>
)}
{/* 快捷入口 */}
<div className="grid grid-cols-4 gap-2 p-4 bg-white rounded-lg shadow mt-4 mx-2">
{[
{ icon: <HomeIcon className="w-6 h-6" />, name: '首页', path: '/' },
{ icon: <NewspaperIcon className="w-6 h-6" />, name: '资讯', path: '/news' },
{ icon: <BellIcon className="w-6 h-6" />, name: '通知', path: '/notices' },
{ icon: <UserIcon className="w-6 h-6" />, name: '我的', path: '/profile' }
].map((item, index) => (
<div
key={index}
className="flex flex-col items-center justify-center p-2"
onClick={() => navigate(item.path)}
>
<div className="w-12 h-12 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 mb-1">
{item.icon}
</div>
<span className="text-sm">{item.name}</span>
</div>
))}
</div>
{/* 内容标签页 */}
<div className="mt-4 mx-2">
<div className="flex border-b border-gray-200">
<button
className={`flex-1 py-2 text-center ${activeTab === 'news' ? 'text-blue-600 border-b-2 border-blue-600 font-medium' : 'text-gray-500'}`}
onClick={() => setActiveTab('news')}
>
</button>
<button
className={`flex-1 py-2 text-center ${activeTab === 'notices' ? 'text-blue-600 border-b-2 border-blue-600 font-medium' : 'text-gray-500'}`}
onClick={() => setActiveTab('notices')}
>
</button>
</div>
<div className="mt-2">
{activeTab === 'news' ? (
loading ? (
<div className="flex justify-center p-4">
<div className="w-6 h-6 border-2 border-gray-200 border-t-blue-600 rounded-full animate-spin"></div>
</div>
) : (
<div className="space-y-4">
{news.map((item) => (
<div
key={item.id}
className="bg-white p-3 rounded-lg shadow flex items-start space-x-3"
onClick={() => handleNewsClick(item.id)}
>
{item.cover_url && (
<img
src={item.cover_url}
alt={item.title}
className="w-20 h-20 object-cover rounded-md flex-shrink-0"
/>
)}
<div className={item.cover_url ? '' : 'w-full'}>
<h3 className="font-medium text-gray-900 line-clamp-2">{item.title}</h3>
<p className="text-sm text-gray-500 mt-1 line-clamp-2">
{item.content?.substring(0, 100)}...
</p>
<div className="flex justify-between items-center mt-2">
<span className="text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
{item.category}
</span>
<span className="text-xs text-gray-400">
{formatRelativeTime(item.created_at)}
</span>
</div>
</div>
</div>
))}
</div>
)
) : (
loading ? (
<div className="flex justify-center p-4">
<div className="w-6 h-6 border-2 border-gray-200 border-t-blue-600 rounded-full animate-spin"></div>
</div>
) : (
<div className="space-y-3">
{notices.map((item) => (
<div
key={item.id}
className="bg-white p-3 rounded-lg shadow"
onClick={() => handleNoticeClick(item.id)}
>
<div className="flex justify-between items-start">
<h3 className={`font-medium ${item.user_status === MessageStatus.READ ? 'text-gray-700' : 'text-blue-600'}`}>
{item.user_status === MessageStatus.UNREAD && (
<span className="inline-block w-2 h-2 bg-blue-600 rounded-full mr-2"></span>
)}
{item.title}
</h3>
<span className="text-xs text-gray-400 mt-1">
{formatRelativeTime(item.created_at)}
</span>
</div>
<p className="text-sm text-gray-500 mt-2 line-clamp-2">{item.content}</p>
</div>
))}
</div>
)
)}
</div>
</div>
{/* 底部导航 */}
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 flex justify-around py-2">
{[
{ icon: <HomeIcon className="w-6 h-6" />, name: '首页', path: '/' },
{ icon: <NewspaperIcon className="w-6 h-6" />, name: '资讯', path: '/news' },
{ icon: <BellIcon className="w-6 h-6" />, name: '通知', path: '/notices' },
{ icon: <UserIcon className="w-6 h-6" />, name: '我的', path: '/profile' }
].map((item, index) => (
<div
key={index}
className="flex flex-col items-center"
onClick={() => navigate(item.path)}
>
<div className={`${location.pathname === item.path ? 'text-blue-600' : 'text-gray-500'}`}>
{item.icon}
</div>
<span className={`text-xs mt-1 ${location.pathname === item.path ? 'text-blue-600' : 'text-gray-500'}`}>
{item.name}
</span>
</div>
))}
</div>
</div>
);
};
export default HomePage;