|
|
@@ -1,32 +1,346 @@
|
|
|
-import React from 'react';
|
|
|
+import React, { useState, useRef } from 'react';
|
|
|
+import { useNavigate } from 'react-router-dom';
|
|
|
+import { useQueryClient } from '@tanstack/react-query';
|
|
|
+import { usePolicyNewsData, type PolicyNewsItem } from '../hooks/usePolicyNewsData';
|
|
|
+import PolicyNewsCard from '../components/PolicyNewsCard';
|
|
|
+import { SkeletonLoader } from '../components/SkeletonLoader';
|
|
|
+import {
|
|
|
+ CalendarDaysIcon,
|
|
|
+ EyeIcon,
|
|
|
+ ArrowPathIcon,
|
|
|
+ FunnelIcon,
|
|
|
+ MagnifyingGlassIcon
|
|
|
+} from '@heroicons/react/24/outline';
|
|
|
+
|
|
|
+// 水墨风格配置
|
|
|
+const COLORS = {
|
|
|
+ ink: {
|
|
|
+ light: '#f5f3f0',
|
|
|
+ medium: '#d4c4a8',
|
|
|
+ dark: '#8b7355',
|
|
|
+ deep: '#3a2f26',
|
|
|
+ },
|
|
|
+ accent: {
|
|
|
+ red: '#a85c5c',
|
|
|
+ blue: '#4a6b7c',
|
|
|
+ green: '#5c7c5c',
|
|
|
+ },
|
|
|
+ text: {
|
|
|
+ primary: '#2f1f0f',
|
|
|
+ secondary: '#5d4e3b',
|
|
|
+ light: '#8b7355',
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const FONT_STYLES = {
|
|
|
+ title: 'font-serif text-3xl font-bold tracking-wide',
|
|
|
+ sectionTitle: 'font-serif text-2xl font-semibold tracking-wide',
|
|
|
+ body: 'font-sans text-base leading-relaxed',
|
|
|
+ caption: 'font-sans text-sm',
|
|
|
+ small: 'font-sans text-xs',
|
|
|
+};
|
|
|
+
|
|
|
+// 骨架屏组件
|
|
|
+const PolicyNewsSkeleton: React.FC = () => (
|
|
|
+ <div className="p-4 space-y-4">
|
|
|
+ {[1, 2, 3].map((i) => (
|
|
|
+ <div key={i} className="bg-white bg-opacity-80 rounded-xl p-4 shadow-sm">
|
|
|
+ <div className="flex space-x-4">
|
|
|
+ <div className="w-32 h-32 bg-gray-200 rounded-lg flex-shrink-0"></div>
|
|
|
+ <div className="flex-1 space-y-2">
|
|
|
+ <div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
|
|
+ <div className="h-3 bg-gray-200 rounded w-1/2"></div>
|
|
|
+ <div className="h-3 bg-gray-200 rounded w-full"></div>
|
|
|
+ <div className="h-3 bg-gray-200 rounded w-2/3"></div>
|
|
|
+ <div className="flex space-x-4">
|
|
|
+ <div className="h-3 bg-gray-200 rounded w-16"></div>
|
|
|
+ <div className="h-3 bg-gray-200 rounded w-20"></div>
|
|
|
+ <div className="h-3 bg-gray-200 rounded w-12"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+);
|
|
|
|
|
|
const PolicyNewsPage: React.FC = () => {
|
|
|
- return (
|
|
|
- <div className="min-h-screen bg-gray-50 p-4">
|
|
|
- <div className="text-center">
|
|
|
- <h1 className="text-2xl font-bold text-gray-900 mb-4">政策资讯</h1>
|
|
|
- <p className="text-gray-600">最新政策解读和资讯推送</p>
|
|
|
-
|
|
|
- <div className="mt-8 space-y-4">
|
|
|
- <div className="bg-white p-6 rounded-lg shadow">
|
|
|
- <h3 className="text-lg font-semibold mb-2">养老政策解读</h3>
|
|
|
- <p className="text-gray-600 mb-2">2025年最新养老政策全面解读</p>
|
|
|
- <span className="text-sm text-gray-500">2025-07-15</span>
|
|
|
+ const navigate = useNavigate();
|
|
|
+ const queryClient = useQueryClient();
|
|
|
+
|
|
|
+ const [searchQuery, setSearchQuery] = useState('');
|
|
|
+ const [filterCategory, setFilterCategory] = useState('');
|
|
|
+ const [showFilters, setShowFilters] = useState(false);
|
|
|
+ const [isRefreshing, setIsRefreshing] = useState(false);
|
|
|
+
|
|
|
+ const {
|
|
|
+ data: policyNewsData,
|
|
|
+ isLoading,
|
|
|
+ isError,
|
|
|
+ error,
|
|
|
+ } = usePolicyNewsData(1, 20);
|
|
|
+
|
|
|
+ const categories = [
|
|
|
+ { value: '', label: '全部' },
|
|
|
+ { value: '政策法规', label: '政策法规' },
|
|
|
+ { value: '就业政策', label: '就业政策' },
|
|
|
+ { value: '社区建设', label: '社区建设' },
|
|
|
+ { value: '产业发展', label: '产业发展' },
|
|
|
+ { value: '健康政策', label: '健康政策' },
|
|
|
+ { value: '教育政策', label: '教育政策' },
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 下拉刷新
|
|
|
+ const handleRefresh = async () => {
|
|
|
+ setIsRefreshing(true);
|
|
|
+ await queryClient.invalidateQueries({ queryKey: ['policy-news'] });
|
|
|
+ setIsRefreshing(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 搜索和过滤逻辑
|
|
|
+ const filteredNews = policyNewsData?.data.filter(news => {
|
|
|
+ const matchesSearch = searchQuery === '' ||
|
|
|
+ news.newsTitle.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
|
+ news.summary?.toLowerCase().includes(searchQuery.toLowerCase());
|
|
|
+
|
|
|
+ const matchesCategory = filterCategory === '' ||
|
|
|
+ news.category === filterCategory;
|
|
|
+
|
|
|
+ return matchesSearch && matchesCategory;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 处理搜索
|
|
|
+ const handleSearch = (e: React.FormEvent) => {
|
|
|
+ e.preventDefault();
|
|
|
+ };
|
|
|
+
|
|
|
+ // 清除搜索
|
|
|
+ const clearSearch = () => {
|
|
|
+ setSearchQuery('');
|
|
|
+ };
|
|
|
+
|
|
|
+ if (isLoading) {
|
|
|
+ return (
|
|
|
+ <div className="min-h-screen" style={{ backgroundColor: COLORS.ink.light }}>
|
|
|
+ {/* 头部骨架屏 */}
|
|
|
+ <div className="sticky top-0 z-10 shadow-sm" style={{ backgroundColor: COLORS.ink.light }}>
|
|
|
+ <div className="px-4 py-4">
|
|
|
+ <div className="h-8 bg-gray-200 rounded w-32 mb-4"></div>
|
|
|
+ <div className="h-10 bg-gray-200 rounded-full"></div>
|
|
|
</div>
|
|
|
-
|
|
|
- <div className="bg-white p-6 rounded-lg shadow">
|
|
|
- <h3 className="text-lg font-semibold mb-2">银龄就业新政</h3>
|
|
|
- <p className="text-gray-600 mb-2">支持银龄群体再就业的新政策</p>
|
|
|
- <span className="text-sm text-gray-500">2025-07-10</span>
|
|
|
+ </div>
|
|
|
+ <PolicyNewsSkeleton />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isError) {
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ className="min-h-screen flex items-center justify-center"
|
|
|
+ style={{ backgroundColor: COLORS.ink.light }}
|
|
|
+ >
|
|
|
+ <div className="text-center">
|
|
|
+ <div
|
|
|
+ className="text-6xl mb-4"
|
|
|
+ style={{ color: COLORS.text.light }}
|
|
|
+ >
|
|
|
+ 📰
|
|
|
</div>
|
|
|
-
|
|
|
- <div className="bg-white p-6 rounded-lg shadow">
|
|
|
- <h3 className="text-lg font-semibold mb-2">社区服务动态</h3>
|
|
|
- <p className="text-gray-600 mb-2">社区银龄服务最新动态</p>
|
|
|
- <span className="text-sm text-gray-500">2025-07-08</span>
|
|
|
+ <h3
|
|
|
+ className={`${FONT_STYLES.sectionTitle} mb-2`}
|
|
|
+ style={{ color: COLORS.text.primary }}
|
|
|
+ >
|
|
|
+ 加载失败
|
|
|
+ </h3>
|
|
|
+ <p
|
|
|
+ className={FONT_STYLES.caption}
|
|
|
+ style={{ color: COLORS.text.secondary }}
|
|
|
+ >
|
|
|
+ {error instanceof Error ? error.message : '获取政策资讯失败'}
|
|
|
+ </p>
|
|
|
+ <button
|
|
|
+ onClick={handleRefresh}
|
|
|
+ className="mt-4 px-4 py-2 rounded-full transition-all duration-300 hover:shadow-md"
|
|
|
+ style={{
|
|
|
+ backgroundColor: COLORS.ink.dark,
|
|
|
+ color: 'white'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 重试
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="min-h-screen pb-16" style={{ backgroundColor: COLORS.ink.light }}>
|
|
|
+ {/* 头部导航 */}
|
|
|
+ <header
|
|
|
+ className="sticky top-0 z-10 shadow-sm border-b border-opacity-20"
|
|
|
+ style={{
|
|
|
+ backgroundColor: COLORS.ink.light,
|
|
|
+ borderColor: COLORS.ink.medium
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div className="px-4 py-4">
|
|
|
+ <div className="flex items-center justify-between mb-3">
|
|
|
+ <h1
|
|
|
+ className={FONT_STYLES.title}
|
|
|
+ style={{ color: COLORS.text.primary }}
|
|
|
+ >
|
|
|
+ 政策资讯
|
|
|
+ </h1>
|
|
|
+ <button
|
|
|
+ onClick={handleRefresh}
|
|
|
+ disabled={isRefreshing}
|
|
|
+ className="p-2 rounded-full transition-all duration-300 hover:shadow-md"
|
|
|
+ style={{
|
|
|
+ backgroundColor: 'rgba(255,255,255,0.7)',
|
|
|
+ color: COLORS.text.primary
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <ArrowPathIcon
|
|
|
+ className={`w-5 h-5 ${isRefreshing ? 'animate-spin' : ''}`}
|
|
|
+ />
|
|
|
+ </button>
|
|
|
</div>
|
|
|
+
|
|
|
+ {/* 搜索栏 */}
|
|
|
+ <form onSubmit={handleSearch} className="mb-3">
|
|
|
+ <div className="flex items-center space-x-2">
|
|
|
+ <div className="flex-1 relative">
|
|
|
+ <MagnifyingGlassIcon
|
|
|
+ className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4"
|
|
|
+ style={{ color: COLORS.text.light }}
|
|
|
+ />
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ placeholder="搜索政策资讯..."
|
|
|
+ value={searchQuery}
|
|
|
+ onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
+ className="w-full pl-10 pr-4 py-2 rounded-full border transition-all duration-300 focus:shadow-md"
|
|
|
+ style={{
|
|
|
+ backgroundColor: 'rgba(255,255,255,0.7)',
|
|
|
+ borderColor: COLORS.ink.medium,
|
|
|
+ color: COLORS.text.primary
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={() => setShowFilters(!showFilters)}
|
|
|
+ className="p-2 rounded-full transition-all duration-300 hover:shadow-md"
|
|
|
+ style={{
|
|
|
+ backgroundColor: 'rgba(255,255,255,0.7)',
|
|
|
+ color: COLORS.text.primary
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <FunnelIcon className="w-5 h-5" />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+
|
|
|
+ {/* 分类过滤器 */}
|
|
|
+ {showFilters && (
|
|
|
+ <div className="grid grid-cols-3 gap-2 mt-3">
|
|
|
+ {categories.map((category) => (
|
|
|
+ <button
|
|
|
+ key={category.value}
|
|
|
+ onClick={() => setFilterCategory(category.value)}
|
|
|
+ className={`px-3 py-2 rounded-full text-sm transition-all duration-300 ${
|
|
|
+ filterCategory === category.value
|
|
|
+ ? 'shadow-md'
|
|
|
+ : 'hover:shadow-md'
|
|
|
+ }`}
|
|
|
+ style={{
|
|
|
+ backgroundColor: filterCategory === category.value
|
|
|
+ ? COLORS.accent.blue
|
|
|
+ : 'rgba(255,255,255,0.7)',
|
|
|
+ color: filterCategory === category.value ? 'white' : COLORS.text.primary,
|
|
|
+ borderColor: COLORS.ink.medium
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {category.label}
|
|
|
+ </button>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 清除搜索/过滤 */}
|
|
|
+ {(searchQuery || filterCategory) && (
|
|
|
+ <button
|
|
|
+ onClick={() => {
|
|
|
+ setSearchQuery('');
|
|
|
+ setFilterCategory('');
|
|
|
+ }}
|
|
|
+ className="mt-3 text-sm"
|
|
|
+ style={{ color: COLORS.accent.red }}
|
|
|
+ >
|
|
|
+ 清除筛选
|
|
|
+ </button>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ {/* 内容区域 */}
|
|
|
+ <div className="p-4">
|
|
|
+ {/* 统计信息 */}
|
|
|
+ <div className="mb-4 text-sm" style={{ color: COLORS.text.secondary }}>
|
|
|
+ 共找到 {filteredNews?.length || 0} 条政策资讯
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 资讯列表 */}
|
|
|
+ <div className="space-y-4">
|
|
|
+ {filteredNews?.map((news) => (
|
|
|
+ <PolicyNewsCard
|
|
|
+ key={news.id}
|
|
|
+ news={news}
|
|
|
+ onClick={() => navigate(`/policy-news/${news.id}`)}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
</div>
|
|
|
+
|
|
|
+ {/* 空状态 */}
|
|
|
+ {filteredNews?.length === 0 && (
|
|
|
+ <div className="text-center py-8">
|
|
|
+ <div
|
|
|
+ className="text-6xl mb-4"
|
|
|
+ style={{ color: COLORS.text.light }}
|
|
|
+ >
|
|
|
+ 📋
|
|
|
+ </div>
|
|
|
+ <h3
|
|
|
+ className={`${FONT_STYLES.sectionTitle} mb-2`}
|
|
|
+ style={{ color: COLORS.text.primary }}
|
|
|
+ >
|
|
|
+ 暂无政策资讯
|
|
|
+ </h3>
|
|
|
+ <p
|
|
|
+ className={FONT_STYLES.caption}
|
|
|
+ style={{ color: COLORS.text.secondary }}
|
|
|
+ >
|
|
|
+ {searchQuery || filterCategory
|
|
|
+ ? '请尝试调整搜索条件或清除筛选'
|
|
|
+ : '暂无政策资讯内容,敬请期待'}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
</div>
|
|
|
+
|
|
|
+ {/* 回到顶部按钮 */}
|
|
|
+ <button
|
|
|
+ onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
|
|
+ className="fixed bottom-20 right-4 p-3 rounded-full shadow-lg transition-all duration-300 hover:shadow-xl"
|
|
|
+ style={{
|
|
|
+ backgroundColor: COLORS.ink.dark,
|
|
|
+ color: 'white'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ ↑
|
|
|
+ </button>
|
|
|
</div>
|
|
|
);
|
|
|
};
|