baima_home_main/app/admin/page.tsx

572 lines
23 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import {
RefreshCw,
Lock,
Newspaper,
Plus,
Trash2,
LogOut,
Rss,
Settings,
Check,
X,
Eye,
Globe
} from 'lucide-react'
type News = {
id: number
title: string
category: string
excerpt: string
createdAt: string
}
type AutoFetchedNews = {
id: number
title: string
source: string
url: string
imageUrl: string
excerpt: string
category: string
publishTime: string
status: string
isDigitalEconomy: boolean
createdAt: string
}
export default function AdminPage() {
const [activeTab, setActiveTab] = useState<'news' | 'autofetch'>('news')
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [password, setPassword] = useState('')
const [newsList, setNewsList] = useState<News[]>([])
const [autoFetchedNews, setAutoFetchedNews] = useState<AutoFetchedNews[]>([])
const [isLoading, setIsLoading] = useState(false)
// 新闻发布表单状态
const [showAddNews, setShowAddNews] = useState(false)
const [newNews, setNewNews] = useState({
title: '',
category: '公司新闻',
excerpt: '',
content: ''
})
// 自动抓取状态
const [isAutoFetching, setIsAutoFetching] = useState(false)
const [fetchStatus, setFetchStatus] = useState<{
lastFetch: string | null
nextFetch: string | null
}>({
lastFetch: null,
nextFetch: null
})
const handleLogin = (e: React.FormEvent) => {
e.preventDefault()
if (password === 'admin123') {
setIsAuthenticated(true)
fetchAllData()
} else {
alert('密码错误')
}
}
const fetchAllData = async () => {
setIsLoading(true)
try {
const [newsRes, autoFetchRes] = await Promise.all([
fetch('/api/news'),
fetch('/api/auto-fetch')
])
if (newsRes.ok) {
const data = await newsRes.json()
setNewsList(data.news || [])
}
if (autoFetchRes.ok) {
const data = await autoFetchRes.json()
// 合并待审核和已批准的内容
const allNews = [
...(data.pending || []),
...(data.approved || []).filter((a: AutoFetchedNews) =>
!data.pending?.some((p: AutoFetchedNews) => p.id === a.id)
)
]
setAutoFetchedNews(allNews)
setFetchStatus({
lastFetch: data.lastFetch,
nextFetch: data.nextFetch
})
}
} catch (error) {
console.error('获取数据失败', error)
} finally {
setIsLoading(false)
}
}
const handleAddNews = async (e: React.FormEvent) => {
e.preventDefault()
try {
const res = await fetch('/api/news', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newNews)
})
if (res.ok) {
setNewNews({ title: '', category: '公司新闻', excerpt: '', content: '' })
setShowAddNews(false)
fetchAllData()
}
} catch (error) {
alert('发布失败')
}
}
// 自动抓取相关函数
const triggerAutoFetch = async () => {
setIsAutoFetching(true)
try {
const res = await fetch('/api/auto-fetch', { method: 'POST' })
const data = await res.json()
if (data.success) {
alert(`成功抓取 ${data.articles?.length || 0} 条资讯`)
fetchAllData()
} else {
alert('抓取失败:' + data.error)
}
} catch (error) {
alert('抓取失败,请稍后重试')
} finally {
setIsAutoFetching(false)
}
}
const approveArticle = async (id: number) => {
try {
const res = await fetch(`/api/auto-fetch/${id}/approve`, { method: 'POST' })
if (res.ok) {
fetchAllData()
}
} catch (error) {
alert('操作失败')
}
}
const rejectArticle = async (id: number) => {
const reason = prompt('请输入拒绝原因:')
if (reason === null) return
try {
const res = await fetch(`/api/auto-fetch/${id}/reject`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason })
})
if (res.ok) {
fetchAllData()
}
} catch (error) {
alert('操作失败')
}
}
const deleteArticle = async (id: number) => {
if (!confirm('确定要删除这条资讯吗?')) return
try {
const res = await fetch(`/api/auto-fetch/${id}`, { method: 'DELETE' })
if (res.ok) {
fetchAllData()
}
} catch (error) {
alert('删除失败')
}
}
if (!isAuthenticated) {
return (
<div className="min-h-screen flex items-center justify-center bg-[#0A1931]">
<Card className="w-[350px] shadow-2xl border-0">
<CardHeader className="text-center pb-8 pt-10">
<CardTitle className="text-2xl font-black text-[#0A1931] flex flex-col items-center gap-4">
<div className="w-16 h-16 bg-[#FF6600] rounded-2xl flex items-center justify-center text-white shadow-lg">
<Lock className="w-8 h-8" />
</div>
</CardTitle>
</CardHeader>
<CardContent className="pb-10 px-8">
<form onSubmit={handleLogin} className="space-y-6">
<div className="space-y-2">
<label className="text-sm font-bold text-gray-500"></label>
<Input
type="password"
placeholder="请输入密码"
className="h-12 border-gray-200 focus:border-[#FF6600] focus:ring-[#FF6600]"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<Button type="submit" className="w-full h-12 bg-[#0A1931] hover:bg-[#185ADB] font-bold text-lg rounded-xl transition-all">
</Button>
</form>
</CardContent>
</Card>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50 flex">
{/* Sidebar */}
<aside className="w-64 bg-[#0A1931] text-white hidden md:flex flex-col">
<div className="p-8 border-b border-white/10">
<span className="text-xl font-black"></span>
</div>
<nav className="flex-grow p-4 space-y-2">
<button
onClick={() => setActiveTab('news')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all font-bold ${activeTab === 'news' ? 'bg-[#FF6600] text-white shadow-lg' : 'hover:bg-white/5 text-gray-400'}`}
>
<Newspaper className="w-5 h-5" />
</button>
<button
onClick={() => setActiveTab('autofetch')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl transition-all font-bold ${activeTab === 'autofetch' ? 'bg-[#FF6600] text-white shadow-lg' : 'hover:bg-white/5 text-gray-400'}`}
>
<Rss className="w-5 h-5" />
</button>
</nav>
<div className="p-6">
<Button variant="ghost" className="w-full text-gray-400 hover:text-white" onClick={() => setIsAuthenticated(false)}>
<LogOut className="w-4 h-4 mr-2" /> 退
</Button>
</div>
</aside>
{/* Main Content */}
<main className="flex-grow pt-24 pb-12 px-4 md:px-8 overflow-y-auto">
<div className="max-w-6xl mx-auto">
<div className="flex justify-between items-center mb-10">
<div>
<h1 className="text-3xl font-black text-[#0A1931]">
{activeTab === 'news' && '公司/行业动态'}
{activeTab === 'autofetch' && '资讯自动抓取'}
</h1>
<p className="text-gray-500 mt-1">
{activeTab === 'news' && '发布和管理最新的公司动态与行业资讯'}
{activeTab === 'autofetch' && '自动抓取白马精选公众号等资讯,智能筛选后展示'}
</p>
</div>
<div className="flex gap-3">
{activeTab === 'news' && (
<Button
onClick={() => setShowAddNews(!showAddNews)}
className="bg-[#FF6600] hover:bg-[#FF8C00] font-bold"
>
<Plus className="w-4 h-4 mr-2" />
</Button>
)}
{activeTab === 'autofetch' && (
<Button
onClick={triggerAutoFetch}
disabled={isAutoFetching}
className="bg-[#FF6600] hover:bg-[#FF8C00] font-bold"
>
<RefreshCw className={`w-4 h-4 mr-2 ${isAutoFetching ? 'animate-spin' : ''}`} />
{isAutoFetching ? '抓取中...' : '立即抓取'}
</Button>
)}
<Button variant="outline" onClick={fetchAllData} disabled={isLoading} className="border-gray-200">
<RefreshCw className={`w-4 h-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
</Button>
</div>
</div>
{/* 新闻发布表单 */}
{activeTab === 'news' && showAddNews && (
<Card className="mb-10 border-2 border-[#FF6600]/20 shadow-xl overflow-hidden">
<CardHeader className="bg-[#FF6600]/5 border-b">
<CardTitle className="text-lg"></CardTitle>
</CardHeader>
<CardContent className="pt-6">
<form onSubmit={handleAddNews} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-bold"></label>
<Input
placeholder="输入新闻标题"
required
onChange={(e) => setNewNews({...newNews, title: e.target.value})}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold"></label>
<select
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
value={newNews.category}
onChange={(e) => setNewNews({...newNews, category: e.target.value})}
>
<option></option>
<option></option>
<option></option>
<option></option>
<option></option>
</select>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-bold"></label>
<Input
placeholder="输入新闻简介"
value={newNews.excerpt}
onChange={(e) => setNewNews({...newNews, excerpt: e.target.value})}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-bold"></label>
<Textarea
placeholder="输入详细报道内容..."
className="min-h-[150px]"
value={newNews.content}
onChange={(e) => setNewNews({...newNews, content: e.target.value})}
/>
</div>
<div className="flex gap-3 justify-end">
<Button variant="ghost" onClick={() => setShowAddNews(false)}></Button>
<Button type="submit" className="bg-[#0A1931] hover:bg-[#185ADB] px-8"></Button>
</div>
</form>
</CardContent>
</Card>
)}
{/* 自动抓取状态面板 */}
{activeTab === 'autofetch' && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-10">
<Card className="bg-gradient-to-br from-[#FF6600] to-[#FF8C00] text-white">
<CardContent className="pt-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center">
<Rss className="w-6 h-6" />
</div>
<div>
<p className="text-sm opacity-80"></p>
<p className="text-lg font-bold">
{fetchStatus.lastFetch
? new Date(fetchStatus.lastFetch).toLocaleString('zh-CN')
: '尚未抓取'}
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-[#0A1931] to-[#185ADB] text-white">
<CardContent className="pt-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center">
<Settings className="w-6 h-6" />
</div>
<div>
<p className="text-sm opacity-80"></p>
<p className="text-lg font-bold">
{fetchStatus.nextFetch
? new Date(fetchStatus.nextFetch).toLocaleString('zh-CN')
: '每6小时自动抓取'}
</p>
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-green-500 to-emerald-600 text-white">
<CardContent className="pt-6">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center">
<Check className="w-6 h-6" />
</div>
<div>
<p className="text-sm opacity-80"></p>
<p className="text-2xl font-bold">
{autoFetchedNews.filter(n => n.status === 'PENDING').length}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
)}
{/* 数据列表 */}
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
{activeTab === 'news' && (
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="text-xs text-gray-400 uppercase bg-gray-50/50 border-b">
<tr>
<th className="px-6 py-4"></th>
<th className="px-6 py-4"></th>
<th className="px-6 py-4"></th>
<th className="px-6 py-4"></th>
<th className="px-6 py-4 text-right"></th>
</tr>
</thead>
<tbody className="divide-y">
{newsList.length === 0 ? (
<tr><td colSpan={5} className="text-center py-20 text-gray-400"></td></tr>
) : (
newsList.map((n) => (
<tr key={n.id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4 text-xs text-gray-400 whitespace-nowrap">{new Date(n.createdAt).toLocaleDateString()}</td>
<td className="px-6 py-4 font-bold text-[#0A1931]">{n.title}</td>
<td className="px-6 py-4">
<span className="bg-orange-50 text-[#FF6600] px-2.5 py-1 rounded-full text-xs font-bold">{n.category}</span>
</td>
<td className="px-6 py-4 text-gray-500 max-w-xs truncate">{n.excerpt}</td>
<td className="px-6 py-4 text-right">
<Button variant="ghost" size="icon" className="text-gray-300 hover:text-red-500">
<Trash2 className="w-4 h-4" />
</Button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
{activeTab === 'autofetch' && (
<div className="overflow-x-auto">
<table className="w-full text-sm text-left">
<thead className="text-xs text-gray-400 uppercase bg-gray-50/50 border-b">
<tr>
<th className="px-6 py-4"></th>
<th className="px-6 py-4"></th>
<th className="px-6 py-4"></th>
<th className="px-6 py-4"></th>
<th className="px-6 py-4"></th>
<th className="px-6 py-4 text-right"></th>
</tr>
</thead>
<tbody className="divide-y">
{autoFetchedNews.length === 0 ? (
<tr>
<td colSpan={6} className="text-center py-20">
<div className="flex flex-col items-center gap-4">
<Rss className="w-16 h-16 text-gray-300" />
<div>
<p className="text-gray-500 font-medium"></p>
<p className="text-gray-400 text-sm">"立即抓取"</p>
</div>
</div>
</td>
</tr>
) : (
autoFetchedNews.map((news) => (
<tr key={news.id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4 text-xs text-gray-400 whitespace-nowrap">
{new Date(news.publishTime).toLocaleDateString()}
</td>
<td className="px-6 py-4">
<div className="font-bold text-[#0A1931] max-w-xs truncate">{news.title}</div>
{news.excerpt && (
<div className="text-gray-400 text-xs mt-1 max-w-xs truncate">{news.excerpt}</div>
)}
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<Globe className="w-4 h-4 text-[#FF6600]" />
<span className="text-gray-600">{news.source}</span>
</div>
</td>
<td className="px-6 py-4">
<span className="bg-blue-50 text-blue-600 px-2.5 py-1 rounded-full text-xs font-bold">
{news.category}
</span>
</td>
<td className="px-6 py-4">
<span className={`px-2.5 py-1 rounded-full text-xs font-bold ${
news.status === 'APPROVED' ? 'bg-green-50 text-green-600' :
news.status === 'REJECTED' ? 'bg-red-50 text-red-600' :
'bg-yellow-50 text-yellow-600'
}`}>
{news.status === 'APPROVED' ? '已发布' :
news.status === 'REJECTED' ? '已拒绝' : '待审核'}
</span>
</td>
<td className="px-6 py-4 text-right">
<div className="flex justify-end gap-2">
{news.status === 'PENDING' && (
<>
<Button
variant="ghost"
size="icon"
className="text-green-500 hover:text-green-600"
onClick={() => approveArticle(news.id)}
title="通过并发布"
>
<Check className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-red-500 hover:text-red-600"
onClick={() => rejectArticle(news.id)}
title="拒绝"
>
<X className="w-4 h-4" />
</Button>
</>
)}
{news.url && (
<a
href={news.url}
target="_blank"
rel="noopener noreferrer"
className="text-gray-400 hover:text-[#FF6600] p-2"
title="查看原文"
>
<Eye className="w-4 h-4" />
</a>
)}
<Button
variant="ghost"
size="icon"
className="text-gray-300 hover:text-red-500"
onClick={() => deleteArticle(news.id)}
title="删除"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
</div>
</div>
</main>
</div>
)
}