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 ? ( - <> - - - - - ) : null} - - - )) - ) : ( - - - - - - - ), - )} - -
模型名称分类简介接口(上线时间)
- - {model_info.name} - - - {getServiceCategory(tags) || '-'} - - {remark ?? ''} - - - {op?.vendor_info?.label || '未知厂商'} - {' '} - - {op.name} - {' '} - - {new Date( - op.created_at || created_at, - ).toLocaleDateString('zh-CN')} - -
- - {model_info.name} - - - {getServiceCategory(tags) || '-'} - - {remark ?? ''} - - - -
+ + ); }