diff --git a/src/components/ServerlessAPIServices.jsx b/src/components/ServerlessAPIServices.jsx
index 6154a4621ce7e43ac6d1c0a292fec16dc55c2694..e6714a8a129b9c7f022f325b0ad2714c4ae2eead 100644
--- a/src/components/ServerlessAPIServices.jsx
+++ b/src/components/ServerlessAPIServices.jsx
@@ -1,6 +1,115 @@
import React, { useEffect, useState } from 'react';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
+/** API 请求配置 */
+const API_CONFIG = {
+ SERVICES_PAGE_SIZE: 200,
+ SERVERLESS_SERVICE_CATEGORY: 'serverless-service',
+};
+
+/** URL 参数键名 */
+const URL_PARAMS = {
+ VENDOR: 'vendor',
+ TAG: 'tag',
+};
+
+/** 筛选器默认值 */
+const FILTER_DEFAULTS = {
+ ALL: 'all',
+};
+
+// ============================================================================
+// 工具函数 - 数据处理相关
+// ============================================================================
+
+/**
+ * 获取服务的唯一厂商列表
+ * @param {Array} services - 服务列表
+ * @returns {Array} 排序后的厂商列表
+ */
+function extractUniqueVendors(services) {
+ const vendorSet = new Set();
+
+ services.forEach((service) => {
+ service.operations?.forEach((operation) => {
+ if (operation.vendor_info?.label) {
+ vendorSet.add(operation.vendor_info.label);
+ }
+ });
+ });
+
+ return Array.from(vendorSet).sort();
+}
+
+/**
+ * 获取服务的唯一分类列表(按 sort_order 排序)
+ * @param {Array} services - 服务列表
+ * @returns {Array} 排序后的分类列表
+ */
+function extractUniqueCategories(services) {
+ const categoryMap = new Map();
+
+ services.forEach((service) => {
+ if (!service.tags?.length) return;
+
+ const categoryTag = service.tags.find((tag) =>
+ tag.categories?.some(
+ ({ slug }) => slug === API_CONFIG.SERVERLESS_SERVICE_CATEGORY,
+ ),
+ );
+
+ if (categoryTag?.name && categoryTag.name !== '-') {
+ categoryMap.set(categoryTag.name, categoryTag);
+ }
+ });
+
+ // 按 sort_order 排序(倒序)
+ return Array.from(categoryMap.values()).sort((a, b) => {
+ const orderA = a.sort_order ?? Number.MAX_SAFE_INTEGER;
+ const orderB = b.sort_order ?? Number.MAX_SAFE_INTEGER;
+
+ return orderA !== orderB ? orderB - orderA : 0;
+ });
+}
+
+/**
+ * 获取服务的分类名称
+ * @param {Array} tags - 标签列表
+ * @returns {string} 分类名称,未找到时返回 '-'
+ */
+function getServiceCategoryName(tags) {
+ if (!tags?.length) return '-';
+
+ const categoryTag = tags.find((tag) =>
+ tag.categories?.some(
+ ({ slug }) => slug === API_CONFIG.SERVERLESS_SERVICE_CATEGORY,
+ ),
+ );
+
+ return categoryTag?.name || '-';
+}
+
+/**
+ * 按日期排序操作列表
+ * @param {Array} operations - 操作列表
+ * @param {string} fallbackDate - 备用日期
+ * @returns {Array} 排序后的操作列表
+ */
+function sortOperationsByDate(operations, fallbackDate) {
+ return operations.sort((a, b) => {
+ const dateA = a.created_at || fallbackDate;
+ const dateB = b.created_at || fallbackDate;
+ return new Date(dateA) - new Date(dateB);
+ });
+}
+
+// ============================================================================
+// UI 组件 - 错误和加载状态
+// ============================================================================
+
+/**
+ * 错误消息组件
+ */
function ErrorMessage({ error }) {
return (
{
- service.operations?.forEach((op) => {
- if (op.vendor_info?.label) {
- vendors.add(op.vendor_info.label);
- }
- });
- });
- return Array.from(vendors).sort();
+/**
+ * 加载状态组件
+ */
+function LoadingMessage() {
+ return (
+
+ 加载 Serverless API 服务列表...
+
+ );
}
-function getServiceCategory(tags) {
- const found = tags.filter((tag) =>
- tag.categories?.find(({ slug }) => slug === 'serverless-service'),
+// ============================================================================
+// UI 组件 - 筛选器
+// ============================================================================
+
+/**
+ * 筛选器组件
+ */
+function FilterControls({
+ selectedVendor,
+ selectedTag,
+ vendors,
+ tags,
+ serviceCount,
+ operationCount,
+ onVendorChange,
+ onTagChange,
+}) {
+ const selectStyle = {
+ padding: '5px 8px',
+ borderRadius: 4,
+ border: 0,
+ outline: '1px solid #ccc',
+ borderRight: '8px solid transparent',
+ };
+
+ const mobileSelectStyle = {
+ ...selectStyle,
+ maxWidth: '100px',
+ fontSize: '12px',
+ };
+
+ const isMobile = typeof window !== 'undefined' && window.innerWidth <= 768;
+
+ return (
+
+
+ {/* 厂商筛选器 */}
+
+
+
+
+
+ {/* 分类筛选器 */}
+
+
+
+
+
+
+ {/* 统计信息 */}
+
+ 共 {serviceCount} 个模型, {operationCount} 个接口
+
+
);
- return found.length > 0 ? found[0].name : '-';
}
-export default function ServerlessAPIServices() {
- const { siteConfig } = useDocusaurusContext();
- const HOST = siteConfig.url;
- const [loading, setLoading] = useState(true);
- const [services, setServices] = useState([]);
- const [filteredServices, setFilteredServices] = useState([]);
- const [filteredOpsCount, setFilteredOpsCount] = useState([]);
- const [selectedVendor, setSelectedVendor] = useState('all');
- const [vendors, setVendors] = useState([]);
- const [showOperations, setShowOperations] = useState(false);
- const [error, setError] = useState(null);
+// ============================================================================
+// UI 组件 - 服务表格
+// ============================================================================
+
+/**
+ * 服务表格组件
+ */
+function ServicesTable({ services, showOperations, hostUrl }) {
+ const renderOperationCell = (operation, ident, createdAt) => (
+
+
+ {operation?.vendor_info?.label || '未知厂商'}
+ {' '}
+
+ {operation.name}
+ {' '}
+
+ {new Date(operation.created_at || createdAt).toLocaleDateString(
+ 'zh-CN',
+ )}
+
+ |
+ );
+
+ /**
+ * 渲染服务基本信息单元格
+ */
+ const renderServiceInfoCells = (service, operationCount) => (
+ <>
+
+
+ {service.model_info.name}
+
+ |
+
+ {getServiceCategoryName(service.tags) || '-'}
+ |
+
+ {service.remark ?? ''}
+ |
+ >
+ );
+
+ return (
+
+
+
+ 模型名称 |
+ 分类 |
+ 简介 |
+ {showOperations && (
+ 接口(上线时间) |
+ )}
+
+
+
+ {services.map((service, serviceIndex) => {
+ const { filteredOps = [], created_at, ident } = service;
+ const hasOperations = filteredOps.length > 0;
+
+ return hasOperations ? (
+ // 有操作的服务 - 每个操作一行
+ filteredOps.map((operation, opIndex) => (
+
+ {opIndex === 0 &&
+ renderServiceInfoCells(service, filteredOps.length)}
+ {renderOperationCell(operation, ident, created_at)}
+
+ ))
+ ) : (
+ // 无操作的服务 - 单行显示
+
+
+
+ {service.model_info.name}
+
+ |
+
+ {getServiceCategoryName(service.tags) || '-'}
+ |
+
+ {service.remark ?? ''}
+ |
+
+ -
+ |
+
+ );
+ })}
+
+
+ );
+}
+// ============================================================================
+// 自定义 Hooks - 状态管理
+// ============================================================================
+
+/**
+ * URL 参数管理 Hook
+ */
+function useURLParams() {
+ const [selectedVendor, setSelectedVendor] = useState(FILTER_DEFAULTS.ALL);
+ const [selectedTag, setSelectedTag] = useState(FILTER_DEFAULTS.ALL);
+
+ // 初始化:从 URL 读取参数
useEffect(() => {
- const params = new URLSearchParams(window.location.search);
- const vendorParam = params.get('vendor');
- if (vendorParam) {
- setSelectedVendor(vendorParam);
- }
+ const urlParams = new URLSearchParams(window.location.search);
+ const vendorParam = urlParams.get(URL_PARAMS.VENDOR);
+ const tagParam = urlParams.get(URL_PARAMS.TAG);
+
+ if (vendorParam) setSelectedVendor(vendorParam);
+ if (tagParam) setSelectedTag(tagParam);
}, []);
+ // 同步:更新 URL 参数
useEffect(() => {
- const params = new URLSearchParams(window.location.search);
- if (selectedVendor === 'all') {
- params.delete('vendor');
+ const urlParams = new URLSearchParams(window.location.search);
+
+ // 处理厂商参数
+ if (selectedVendor === FILTER_DEFAULTS.ALL) {
+ urlParams.delete(URL_PARAMS.VENDOR);
} else {
- params.set('vendor', selectedVendor);
+ urlParams.set(URL_PARAMS.VENDOR, selectedVendor);
}
- const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
+ // 处理分类参数
+ if (selectedTag === FILTER_DEFAULTS.ALL) {
+ urlParams.delete(URL_PARAMS.TAG);
+ } else {
+ urlParams.set(URL_PARAMS.TAG, selectedTag);
+ }
+
+ const newUrl = `${window.location.pathname}${urlParams.toString() ? '?' + urlParams.toString() : ''}`;
window.history.pushState({}, '', newUrl);
- }, [selectedVendor]);
+ }, [selectedVendor, selectedTag]);
+
+ return {
+ selectedVendor,
+ selectedTag,
+ setSelectedVendor,
+ setSelectedTag,
+ };
+}
+
+/**
+ * 服务数据获取 Hook
+ */
+function useServicesData(hostUrl) {
+ const [loading, setLoading] = useState(true);
+ const [services, setServices] = useState([]);
+ const [vendors, setVendors] = useState([]);
+ const [tags, setTags] = useState([]);
+ const [showOperations, setShowOperations] = useState(false);
+ const [error, setError] = useState(null);
useEffect(() => {
- async function getOperations() {
- setLoading(true);
- const resp = await fetch(`${HOST}/api/pay/service/operations`);
- if (resp.status !== 200) {
- await getServices();
- return;
+ let isMounted = true;
+
+ /**
+ * 获取操作数据(优先方案)
+ */
+ async function fetchOperationsData() {
+ try {
+ const response = await fetch(`${hostUrl}/api/pay/service/operations`);
+
+ if (response.status !== 200) {
+ await fetchServicesData();
+ return;
+ }
+
+ const data = await response.json();
+ const processedServices = data
+ .map(({ operations, service }) => {
+ // 计算厂商最早日期
+ const vendorEarliestDates = operations.reduce((acc, op) => {
+ const vendor = op?.vendor_info?.label || '未知厂商';
+ const date = op.created_at || service.created_at;
+ if (!acc[vendor] || date < acc[vendor]) {
+ acc[vendor] = date;
+ }
+ return acc;
+ }, {});
+
+ return {
+ ...service,
+ operations: operations.sort((a, b) => {
+ const labelA = a.vendor_info?.label || '';
+ const labelB = b.vendor_info?.label || '';
+ return labelA.localeCompare(labelB);
+ }),
+ vendorEarliestDates,
+ };
+ })
+ .filter(({ tags }) => tags?.length > 0)
+ .sort(({ created_at: c1 }, { created_at: c2 }) => (c1 < c2 ? 1 : -1));
+
+ if (isMounted) {
+ setServices(processedServices);
+ setVendors(extractUniqueVendors(processedServices));
+ setTags(extractUniqueCategories(processedServices));
+ setShowOperations(true);
+ setLoading(false);
+ }
+ } catch (err) {
+ console.error('Failed to fetch operations:', err);
+ await fetchServicesData();
}
- const items = await resp.json();
- const svs = items
- .map(({ operations, service }) => {
- const vendorEarliestDates = operations.reduce((acc, op) => {
- const vendor = op?.vendor_info?.label || '未知厂商';
- const date = op.created_at || service.created_at;
- if (!acc[vendor] || date < acc[vendor]) {
- acc[vendor] = date;
- }
- return acc;
- }, {});
-
- return {
- ...service,
- operations: operations.sort((a, b) => {
- const labelA = a.vendor_info?.label || '';
- const labelB = b.vendor_info?.label || '';
- return labelA.localeCompare(labelB);
- }),
- vendorEarliestDates,
- };
- })
- .filter(({ tags }) => tags && tags.length > 0)
- .sort(({ created_at: c1 }, { created_at: c2 }) => (c1 < c2 ? 1 : -1));
- setServices(svs);
- setFilteredServices(svs);
- setVendors(getUniqueVendors(svs));
- setShowOperations(true);
- setLoading(false);
}
- async function getServices() {
+
+ /**
+ * 获取服务数据(备用方案)
+ */
+ async function fetchServicesData() {
try {
- setLoading(true);
- const resp = await fetch(
- `${HOST}/api/pay/services?type=serverless&page=1&size=100`,
+ const response = await fetch(
+ `${hostUrl}/api/pay/services?type=serverless&page=1&size=${API_CONFIG.SERVICES_PAGE_SIZE}`,
);
- const { items } = await resp.json();
- setServices(
- items
- .filter(({ tags }) => tags && tags.length > 0)
- .sort(({ created_at: c1 }, { created_at: c2 }) =>
- c1 < c2 ? 1 : -1,
- ),
- );
- } catch (e) {
- console.error(e);
- setError(e.message);
- setServices([]);
- } finally {
- setLoading(false);
+ const { items } = await response.json();
+
+ const processedServices = items
+ .filter(({ tags }) => tags?.length > 0)
+ .sort(({ created_at: c1 }, { created_at: c2 }) => (c1 < c2 ? 1 : -1));
+
+ if (isMounted) {
+ setServices(processedServices);
+ setTags(extractUniqueCategories(processedServices));
+ setLoading(false);
+ }
+ } catch (err) {
+ console.error('Failed to fetch services:', err);
+ if (isMounted) {
+ setError(err.message);
+ setServices([]);
+ setLoading(false);
+ }
}
}
- getOperations();
- }, []);
+
+ fetchOperationsData();
+
+ return () => {
+ isMounted = false;
+ };
+ }, [hostUrl]);
+
+ return {
+ loading,
+ services,
+ vendors,
+ tags,
+ showOperations,
+ error,
+ };
+}
+
+/**
+ * 服务筛选 Hook
+ */
+function useFilteredServices(services, selectedVendor, selectedTag) {
+ const [filteredServices, setFilteredServices] = useState([]);
+ const [filteredOpsCount, setFilteredOpsCount] = useState(0);
useEffect(() => {
- if (selectedVendor === 'all') {
- setFilteredServices(
- services.map((service) => ({
- ...service,
- filteredOps: (service.operations || []).sort((a, b) => {
- const dateA = a.created_at || service.created_at;
- const dateB = b.created_at || service.created_at;
- return new Date(dateA) - new Date(dateB);
- }),
- })),
- );
+ // 步骤1:按分类筛选服务
+ let servicesByTag = services;
+ if (selectedTag !== FILTER_DEFAULTS.ALL) {
+ servicesByTag = services.filter((service) => {
+ const categoryName = getServiceCategoryName(service.tags || []);
+ return categoryName === selectedTag;
+ });
+ }
+
+ // 步骤2:按厂商筛选操作
+ if (selectedVendor === FILTER_DEFAULTS.ALL) {
+ // 显示所有厂商的操作
+ const processedServices = servicesByTag.map((service) => ({
+ ...service,
+ filteredOps: sortOperationsByDate(
+ service.operations || [],
+ service.created_at,
+ ),
+ }));
+
+ setFilteredServices(processedServices);
setFilteredOpsCount(
- services.reduce((acc, service) => {
- return acc + (service.operations || []).length;
- }, 0),
+ processedServices.reduce(
+ (total, service) => total + (service.operations?.length || 0),
+ 0,
+ ),
);
} else {
- const fss = services
+ // 只显示选中厂商的操作
+ const processedServices = servicesByTag
.map((service) => ({
...service,
- filteredOps: (service.operations || [])
- .filter((op) => op?.vendor_info?.label === selectedVendor)
- .sort((a, b) => {
- const dateA = a.created_at || service.created_at;
- const dateB = b.created_at || service.created_at;
- return new Date(dateA) - new Date(dateB);
- }),
+ filteredOps: sortOperationsByDate(
+ (service.operations || []).filter(
+ (op) => op?.vendor_info?.label === selectedVendor,
+ ),
+ service.created_at,
+ ),
}))
- .filter((service) => (service.filteredOps || []).length > 0);
- setFilteredServices(fss);
+ .filter((service) => service.filteredOps.length > 0);
+
+ setFilteredServices(processedServices);
setFilteredOpsCount(
- fss.reduce((acc, service) => {
- return acc + (service.filteredOps || []).length;
- }, 0),
+ processedServices.reduce(
+ (total, service) => total + service.filteredOps.length,
+ 0,
+ ),
);
}
- }, [selectedVendor, services]);
+ }, [selectedVendor, selectedTag, services]);
+
+ return {
+ filteredServices,
+ filteredOpsCount,
+ };
+}
+
+// ============================================================================
+// 主组件
+// ============================================================================
+/**
+ * ServerlessAPIServices 主组件
+ */
+export default function ServerlessAPIServices() {
+ const { siteConfig } = useDocusaurusContext();
+ const hostUrl = siteConfig.url;
+
+ // 状态管理
+ const { selectedVendor, selectedTag, setSelectedVendor, setSelectedTag } =
+ useURLParams();
+ const { loading, services, vendors, tags, showOperations, error } =
+ useServicesData(hostUrl);
+ const { filteredServices, filteredOpsCount } = useFilteredServices(
+ services,
+ selectedVendor,
+ selectedTag,
+ );
+
+ // 加载状态
if (loading) {
- return (
-
- 加载 Serverless API 服务列表...
-
- );
+ return ;
}
+ // 错误状态
if (services.length === 0) {
return ;
}
+ // 正常渲染
return (
<>
-
-
-
-
- 共 {filteredServices.length} 个模型, {filteredOpsCount} 个接口
-
-
-
-
-
- 模型名称 |
- 分类 |
- 简介 |
- {showOperations && (
- 接口(上线时间) |
- )}
-
-
-
- {filteredServices.map(
- (
- {
- created_at,
- model_info,
- id,
- remark,
- tags,
- filteredOps = [],
- ident,
- },
- index,
- ) =>
- (filteredOps || []).length > 0 ? (
- filteredOps.map((op, opIndex) => (
-
- {opIndex === 0 ? (
- <>
-
-
- {model_info.name}
-
- |
-
- {getServiceCategory(tags) || '-'}
- |
-
- {remark ?? ''}
- |
- >
- ) : null}
-
-
- {op?.vendor_info?.label || '未知厂商'}
- {' '}
-
- {op.name}
- {' '}
-
- {new Date(
- op.created_at || created_at,
- ).toLocaleDateString('zh-CN')}
-
- |
-
- ))
- ) : (
-
-
-
- {model_info.name}
-
- |
-
- {getServiceCategory(tags) || '-'}
- |
-
- {remark ?? ''}
- |
-
- -
- |
-
- ),
- )}
-
-
+
+
>
);
}