sync from local backup
This commit is contained in:
@@ -0,0 +1,398 @@
|
||||
import "./ApiDirectoryDesktop.css";
|
||||
|
||||
import { Pagination, Tag, Empty, Spin, Button } from "antd";
|
||||
import { Tabs } from "antd";
|
||||
import type { TabsProps } from "antd";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Radio } from "antd";
|
||||
import type { RadioChangeEvent } from "antd";
|
||||
import { Image } from "antd";
|
||||
import { EyeOutlined, StarOutlined, StarFilled, LoadingOutlined, ApiOutlined, LinkOutlined, ReloadOutlined } from "@ant-design/icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { api_request } from "@/utils/request";
|
||||
import { getApiImageUrl } from "@/utils/apiImageUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectIsLogin } from "@/features/login";
|
||||
import { useLoginModal } from "@/contexts/LoginModalContext";
|
||||
import { message } from "@/utils/message";
|
||||
|
||||
interface ApiCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
sort_order: number;
|
||||
item_count: number;
|
||||
}
|
||||
|
||||
interface ApiItem {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
url_path: string;
|
||||
method: string;
|
||||
category: number | null;
|
||||
category_name: string;
|
||||
is_enabled: boolean;
|
||||
is_new: boolean;
|
||||
is_coming_soon: boolean;
|
||||
views_count: number;
|
||||
color: string;
|
||||
image_url: string;
|
||||
sort_order: number;
|
||||
favorites_count: number;
|
||||
is_favorited: boolean;
|
||||
}
|
||||
|
||||
const methodColors: Record<string, string> = {
|
||||
GET: "green",
|
||||
POST: "orange",
|
||||
PUT: "blue",
|
||||
DELETE: "red",
|
||||
};
|
||||
|
||||
const ApiDirectoryDesktop: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const isLogin = useSelector(selectIsLogin);
|
||||
const { openLoginModal } = useLoginModal();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [categories, setCategories] = useState<ApiCategory[]>([]);
|
||||
const [items, setItems] = useState<ApiItem[]>([]);
|
||||
const [activeCategory, setActiveCategory] = useState<string>("all");
|
||||
const [sortOrder, setSortOrder] = useState<number>(1);
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const [pageSize, setPageSize] = useState<number>(12);
|
||||
const [favoriteLoading, setFavoriteLoading] = useState<Record<number, boolean>>({});
|
||||
|
||||
const handleApiClick = async (api: ApiItem) => {
|
||||
if (api.is_coming_soon) return;
|
||||
if (isLogin) {
|
||||
try {
|
||||
await api_request.history.create({
|
||||
type: 'api',
|
||||
title: api.name,
|
||||
description: api.description,
|
||||
image: getApiImageUrl(api),
|
||||
category: api.category_name || 'API接口',
|
||||
link: `/api-detail/${api.id}`,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('记录浏览历史失败:', err);
|
||||
}
|
||||
}
|
||||
navigate(`/api-detail/${api.id}`);
|
||||
};
|
||||
|
||||
const handleFavoriteToggle = async (apiId: number, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!isLogin) {
|
||||
openLoginModal();
|
||||
return;
|
||||
}
|
||||
if (favoriteLoading[apiId]) return;
|
||||
setFavoriteLoading(prev => ({ ...prev, [apiId]: true }));
|
||||
try {
|
||||
const res: any = await api_request.apidirectory.toggleFavorite(apiId);
|
||||
if (res?.data) {
|
||||
setItems(prev => prev.map(item =>
|
||||
item.id === apiId
|
||||
? {
|
||||
...item,
|
||||
is_favorited: res.data.favorited,
|
||||
favorites_count: res.data.favorites_count ?? (item.is_favorited ? item.favorites_count - 1 : item.favorites_count + 1),
|
||||
}
|
||||
: item
|
||||
));
|
||||
}
|
||||
} catch {
|
||||
message.error("收藏操作失败");
|
||||
} finally {
|
||||
setFavoriteLoading(prev => ({ ...prev, [apiId]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const res: any = await api_request.apidirectory.getCategories();
|
||||
if (res && res.data) {
|
||||
setCategories(res.data);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('获取分类失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const getOrderingValue = (order: number): string => {
|
||||
const orderingMap: Record<number, string> = {
|
||||
1: 'sort_order',
|
||||
2: '-views_count',
|
||||
3: '-created_at',
|
||||
4: '-favorites_count',
|
||||
};
|
||||
return orderingMap[order] || 'sort_order';
|
||||
};
|
||||
|
||||
const fetchItems = async (categoryId?: number, order?: number) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setCurrentPage(1);
|
||||
try {
|
||||
const params: any = {};
|
||||
if (categoryId) {
|
||||
params.category_id = categoryId;
|
||||
}
|
||||
const ordering = getOrderingValue(order ?? sortOrder);
|
||||
if (ordering) {
|
||||
params.ordering = ordering;
|
||||
}
|
||||
const res: any = await api_request.apidirectory.getItems(params);
|
||||
if (res && res.data) {
|
||||
const sorted = [...res.data].sort((a: ApiItem, b: ApiItem) => {
|
||||
if (a.is_coming_soon && !b.is_coming_soon) return 1;
|
||||
if (!a.is_coming_soon && b.is_coming_soon) return -1;
|
||||
return 0;
|
||||
});
|
||||
setItems(sorted);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError('获取API列表失败,请稍后重试');
|
||||
console.error('获取API列表失败:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories();
|
||||
fetchItems();
|
||||
}, []);
|
||||
|
||||
const handleCategoryChange = (key: string) => {
|
||||
setActiveCategory(key);
|
||||
setCurrentPage(1);
|
||||
if (key === 'all') {
|
||||
fetchItems(undefined, sortOrder);
|
||||
} else {
|
||||
const category = categories.find(c => c.name === key);
|
||||
if (category) {
|
||||
fetchItems(category.id, sortOrder);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSortChange = (e: RadioChangeEvent) => {
|
||||
const newSortOrder = e.target.value;
|
||||
setSortOrder(newSortOrder);
|
||||
setCurrentPage(1);
|
||||
if (activeCategory === 'all') {
|
||||
fetchItems(undefined, newSortOrder);
|
||||
} else {
|
||||
const category = categories.find(c => c.name === activeCategory);
|
||||
if (category) {
|
||||
fetchItems(category.id, newSortOrder);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number, size: number) => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(size);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const paginatedItems = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
return items.slice(startIndex, endIndex);
|
||||
}, [items, currentPage, pageSize]);
|
||||
|
||||
const getCategoryColor = (categoryName: string): string => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'天气API': 'blue',
|
||||
'数据API': 'cyan',
|
||||
'AI接口': 'purple',
|
||||
'实用API': 'green',
|
||||
};
|
||||
return colorMap[categoryName] || 'default';
|
||||
};
|
||||
|
||||
const tabItems: TabsProps["items"] = [
|
||||
{ key: 'all', label: '全部分类' },
|
||||
...categories.map(cat => ({
|
||||
key: cat.name,
|
||||
label: cat.name,
|
||||
})),
|
||||
].map(cat => ({
|
||||
key: cat.key,
|
||||
label: cat.label,
|
||||
children: (
|
||||
<div className="option-radio-group">
|
||||
<Radio.Group
|
||||
onChange={handleSortChange}
|
||||
value={sortOrder}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
options={[
|
||||
{ value: 1, label: '推荐排序' },
|
||||
{ value: 2, label: '最多使用' },
|
||||
{ value: 3, label: '最新上线' },
|
||||
{ value: 4, label: '最多收藏' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
if (loading && items.length === 0) {
|
||||
return (
|
||||
<div className="tools-hero">
|
||||
<div className="tools-hero-content">
|
||||
<Spin size="large" />
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="tools-hero">
|
||||
<div className="tools-hero-content">
|
||||
<h1 className="tools-hero-title">
|
||||
<ApiOutlined /> API接口大全
|
||||
</h1>
|
||||
<p className="tools-hero-subtitle">海量免费API接口,助力快速开发</p>
|
||||
<p className="tools-hero-desc">提供天气查询、数据服务、AI接口等多种API,即调即用,轻松集成到您的项目中</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="tools-error-wrapper">
|
||||
<Empty description={error}>
|
||||
<Button type="primary" onClick={() => fetchItems()}>
|
||||
<ReloadOutlined /> 重试
|
||||
</Button>
|
||||
</Empty>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && (
|
||||
<>
|
||||
<div className="tools-tabs-wrapper">
|
||||
<Tabs
|
||||
defaultActiveKey="all"
|
||||
activeKey={activeCategory}
|
||||
items={tabItems}
|
||||
centered
|
||||
className="tools-tabs"
|
||||
onChange={handleCategoryChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="tools-card-grid">
|
||||
{paginatedItems.length === 0 ? (
|
||||
<div className="tools-empty-wrapper">
|
||||
<Empty description="暂无API数据" />
|
||||
</div>
|
||||
) : (
|
||||
paginatedItems.map((api, index) => (
|
||||
<div
|
||||
key={api.id}
|
||||
className={`tools-feature-card fade-in${api.is_coming_soon ? " tools-feature-card--coming-soon" : ""}`}
|
||||
style={{ animationDelay: `${index * 0.05}s`, cursor: api.is_coming_soon ? "default" : "pointer" }}
|
||||
onClick={() => handleApiClick(api)}
|
||||
>
|
||||
<div className="tools-feature-card-cover">
|
||||
<Image
|
||||
height={160}
|
||||
width="100%"
|
||||
draggable={false}
|
||||
alt={api.name}
|
||||
src={getApiImageUrl(api)}
|
||||
preview={false}
|
||||
fallback="https://cdn.free-api.com/sjtxsc.webp"
|
||||
className={api.name === '百度翻译' ? 'tools-feature-card-img--contain' : ''}
|
||||
/>
|
||||
<div className="tools-category-tag">
|
||||
<Tag color={getCategoryColor(api.category_name)}>
|
||||
{api.category_name || '其他'}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className="api-method-tag">
|
||||
<Tag color={methodColors[api.method] || "default"}>{api.method}</Tag>
|
||||
</div>
|
||||
{api.is_coming_soon && (
|
||||
<div className="api-coming-soon-badge">
|
||||
<Tag color="warning">敬请期待</Tag>
|
||||
</div>
|
||||
)}
|
||||
{api.is_new && (
|
||||
<div className="api-new-badge">
|
||||
<Tag color="error">NEW</Tag>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="tools-feature-card-body">
|
||||
<div className="tools-feature-card-title">
|
||||
<span>{api.name}</span>
|
||||
<span className="tools-feature-card-views">
|
||||
<EyeOutlined /> {api.views_count.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="tools-feature-card-desc">{api.description}</div>
|
||||
<div className="api-url">
|
||||
<code>{api.url_path}</code>
|
||||
</div>
|
||||
<div className="tools-feature-card-footer">
|
||||
<span
|
||||
className={`tools-feature-card-stars api-favorite-btn ${api.is_favorited ? "is-favorited" : ""}`}
|
||||
onClick={(e) => handleFavoriteToggle(api.id, e)}
|
||||
>
|
||||
{favoriteLoading[api.id] ? (
|
||||
<LoadingOutlined />
|
||||
) : api.is_favorited ? (
|
||||
<StarFilled />
|
||||
) : (
|
||||
<StarOutlined />
|
||||
)}
|
||||
{" "}{api.favorites_count}
|
||||
</span>
|
||||
<div className="tools-feature-card-actions">
|
||||
{api.is_coming_soon ? (
|
||||
<Button type="default" size="small" disabled>
|
||||
敬请期待
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="primary" size="small" icon={<LinkOutlined />} onClick={(e) => { e.stopPropagation(); handleApiClick(api); }}>
|
||||
调用
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="tools-pagination">
|
||||
<Pagination
|
||||
align="center"
|
||||
total={items.length}
|
||||
current={currentPage}
|
||||
pageSize={pageSize}
|
||||
showSizeChanger
|
||||
showQuickJumper
|
||||
showTotal={(total) => `共 ${total} 个接口`}
|
||||
onChange={handlePageChange}
|
||||
pageSizeOptions={['6', '12', '24']}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiDirectoryDesktop;
|
||||
Reference in New Issue
Block a user