-
Notifications
You must be signed in to change notification settings - Fork 43
refactor(lib): Restructure infrastructure with API client, type definitions and modular utilities #423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
huangli28926
wants to merge
3
commits into
LianjiaTech:develop
from
huangli28926:pr4-addModelsV3-0107
Closed
refactor(lib): Restructure infrastructure with API client, type definitions and modular utilities #423
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,24 @@ | ||
| import { Sidebar } from '@/components/layout'; | ||
| import { SidebarProvider } from '@/components/providers/sidebar-provider'; | ||
|
|
||
| export default function DashboardLayout({ | ||
| children, | ||
| }: { | ||
| children: React.ReactNode; | ||
| }) { | ||
| return ( | ||
| <div className="flex min-h-screen bg-sidebar"> | ||
| {/* 左侧边栏 */} | ||
| <Sidebar /> | ||
| <SidebarProvider> | ||
| <div className="flex min-h-screen bg-sidebar"> | ||
| {/* 左侧边栏 */} | ||
| <Sidebar /> | ||
|
|
||
| {/* 主内容区域 */} | ||
| <main className="flex-1 ml-64 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> | ||
| <div className="h-full"> | ||
| {children} | ||
| </div> | ||
| </main> | ||
| </div> | ||
| {/* 主内容区域 */} | ||
| <main className="flex-1 ml-64 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> | ||
| <div className="h-full"> | ||
| {children} | ||
| </div> | ||
| </main> | ||
| </div> | ||
| </SidebarProvider> | ||
| ); | ||
| } |
105 changes: 105 additions & 0 deletions
105
web_v2/src/app/[locale]/(dashboard)/models/hooks/useEndpointData.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import { useState, useEffect, useCallback, useRef } from "react" | ||
| import { EndpointDetails } from "@/lib/types/openapi" | ||
| import { getEndpointDetails } from "@/lib/api/meta" | ||
| import { processModelsArray } from "@/components/ui/models/utils" | ||
|
|
||
| /** | ||
| * 自定义 Hook: 管理端点数据的获取和状态 | ||
| */ | ||
| export const useEndpointData = (endpoint: string, selectedTags: string[]) => { | ||
| // 拆分为三个独立的 state | ||
| const [endpointInfo, setEndpointInfo] = useState<EndpointDetails['endpoint'] | null>(null) | ||
| const [features, setFeatures] = useState<EndpointDetails['features']>([]) | ||
| const [models, setModels] = useState<EndpointDetails['models']>([]) | ||
|
|
||
| // 区分两种 loading 状态 | ||
| const [initialLoading, setInitialLoading] = useState(false) // 首次加载或切换 endpoint 时的 loading | ||
| const [modelsLoading, setModelsLoading] = useState(false) // 仅更新模型列表时的 loading | ||
| const [error, setError] = useState<Error | null>(null) | ||
|
|
||
| // 使用 ref 保存上次的 endpoint,用于判断是否需要更新 features | ||
| const prevEndpointRef = useRef<string>("") | ||
|
|
||
| /** | ||
| * 获取并处理端点详情数据 | ||
| */ | ||
| const fetchAndProcessData = useCallback(async (endpoint: string, modelName: string = "", tags: string[], shouldUpdateFeatures: boolean) => { | ||
| if (!endpoint) return | ||
|
|
||
| try { | ||
| // 根据是否更新 features 决定使用哪种 loading 状态 | ||
| if (shouldUpdateFeatures) { | ||
| setInitialLoading(true) | ||
| } else { | ||
| setModelsLoading(true) | ||
| } | ||
| setError(null) | ||
|
|
||
| // 获取原始数据 | ||
| const data = await getEndpointDetails(endpoint, modelName, tags) | ||
|
|
||
| // 处理模型数据 | ||
| const processedModels = data?.models ? processModelsArray(data.models) : [] | ||
|
|
||
| // 根据 shouldUpdateFeatures 决定是否更新 features | ||
| if (shouldUpdateFeatures) { | ||
| // endpoint 变化时,完全更新所有数据(包括 endpoint、features 和 models) | ||
| setEndpointInfo(data?.endpoint || null) | ||
| setFeatures(data?.features || []) | ||
| setModels(processedModels) | ||
| } else { | ||
| // 仅 tags 变化时,只更新 models,保持 endpoint 和 features 不变 | ||
| setModels(processedModels) | ||
| } | ||
| } catch (err) { | ||
| console.error('Error fetching endpoint details:', err) | ||
| setError(err instanceof Error ? err : new Error('Unknown error')) | ||
| setEndpointInfo(null) | ||
| setFeatures([]) | ||
| setModels([]) | ||
| } finally { | ||
| // 清除对应的 loading 状态 | ||
| if (shouldUpdateFeatures) { | ||
| setInitialLoading(false) | ||
| } else { | ||
| setModelsLoading(false) | ||
| } | ||
| } | ||
| }, []) | ||
|
|
||
| /** | ||
| * 当 endpoint 或 selectedTags 变化时,重新获取数据 | ||
| */ | ||
| useEffect(() => { | ||
| if (endpoint) { | ||
| // 判断是否是 endpoint 变化 | ||
| const isEndpointChanged = prevEndpointRef.current !== endpoint | ||
| // 更新 ref | ||
| if (isEndpointChanged) { | ||
| prevEndpointRef.current = endpoint | ||
| } | ||
|
|
||
| // endpoint 变化时更新 features,仅 tags 变化时不更新 features | ||
| fetchAndProcessData(endpoint, "", selectedTags, isEndpointChanged) | ||
| } | ||
| }, [endpoint, selectedTags, fetchAndProcessData]) | ||
|
|
||
| /** | ||
| * 手动刷新数据 | ||
| */ | ||
| const refetch = useCallback(() => { | ||
| if (endpoint) { | ||
| fetchAndProcessData(endpoint, "", selectedTags, true) | ||
| } | ||
| }, [endpoint, selectedTags, fetchAndProcessData]) | ||
|
|
||
| return { | ||
| endpoint: endpointInfo, | ||
| features, | ||
| models, | ||
| initialLoading, | ||
| modelsLoading, | ||
| error, | ||
| refetch, | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,176 @@ | ||
| 'use client' | ||
| import { TopBar } from "@/components/layout" | ||
| export default function ModelsPage() { | ||
| import { useLanguage } from "@/components/providers/language-provider" | ||
| import { useSidebar } from "@/components/providers" | ||
| import { useMemo, useDeferredValue } from "react" | ||
| import { ModelFilterPanel } from "@/components/ui/models" | ||
| import { useSearchParams } from "next/navigation" | ||
| import { useState, useEffect, useCallback } from "react" | ||
| import { useEndpointData } from "./hooks/useEndpointData" | ||
| import { getInitialEndpoint } from "@/lib/utils" | ||
| import { Model } from "@/lib/types/openapi" | ||
| import { Loader, AlertCircle } from "lucide-react" | ||
| import { ModelCard } from "@/components/ui/modelCard" | ||
| import { Button } from "@/components/common/button" | ||
|
|
||
| // 标签颜色配置 | ||
| const tagColors = [ | ||
| "bg-green-500/10 text-green-500 border-green-500/20", | ||
| "bg-blue-500/10 text-blue-500 border-blue-500/20", | ||
| "bg-purple-500/10 text-purple-500 border-purple-500/20", | ||
| "bg-orange-500/10 text-orange-500 border-orange-500/20", | ||
| "bg-pink-500/10 text-pink-500 border-pink-500/20", | ||
| "bg-indigo-500/10 text-indigo-500 border-indigo-500/20", | ||
| "bg-cyan-500/10 text-cyan-500 border-cyan-500/20", | ||
| "bg-red-500/10 text-red-500 border-red-500/20", | ||
| ] | ||
|
|
||
| /** | ||
| * 模型目录页面组件 | ||
| */ | ||
| const ModelsPage = () => { | ||
| const searchParams = useSearchParams() | ||
| const { t } = useLanguage() | ||
| const { categoryTrees } = useSidebar() | ||
| const [selectedCapability, setSelectedCapability] = useState<string>("") | ||
| const [searchQuery, setSearchQuery] = useState("") | ||
| const [selectedTags, setSelectedTags] = useState<string[]>([]) | ||
| // 使用自定义 Hook 获取端点数据 | ||
| const { features, models, initialLoading, modelsLoading, error, refetch } = useEndpointData(selectedCapability, selectedTags) | ||
|
|
||
| // 使用 useDeferredValue 实现搜索防抖,优化大数据量场景下的性能 | ||
| const deferredSearchQuery = useDeferredValue(searchQuery) | ||
|
|
||
| /** | ||
| * 根据搜索关键词筛选模型列表 | ||
| * 使用 deferredSearchQuery 而不是 searchQuery,避免用户快速输入时频繁计算 | ||
| */ | ||
| const filteredModels = useMemo(() => { | ||
| if (!models) return [] | ||
| if (!deferredSearchQuery.trim()) return models | ||
|
|
||
| const query = deferredSearchQuery.toLowerCase().trim() | ||
| return models.filter((model) => { | ||
| // 搜索模型名称 | ||
| if (model.modelName?.toLowerCase().includes(query)) return true | ||
| // 搜索拥有者名称 | ||
| if (model.ownerName?.toLowerCase().includes(query)) return true | ||
| // 搜索端点 | ||
| if (model.endpoints?.some(ep => ep.toLowerCase().includes(query))) return true | ||
| // 搜索特性标签 | ||
| const modelFeatures = typeof model.features === 'string' | ||
| ? model.features.split(',').map(f => f.trim()) | ||
| : model.features || [] | ||
| if (modelFeatures.some(f => f.toLowerCase().includes(query))) return true | ||
| return false | ||
| }) | ||
| }, [models, deferredSearchQuery]) | ||
|
|
||
| /** | ||
| * 初始化选中的能力分类选项 endpoint | ||
| */ | ||
| useEffect(() => { | ||
| const endpoint = getInitialEndpoint(searchParams.get("endpoint")) | ||
| setSelectedCapability(endpoint) | ||
| }, [searchParams]) | ||
|
|
||
| /** | ||
| * 处理能力分类变化 | ||
| */ | ||
| const handleCapabilityChange = useCallback((endpoint: string) => { | ||
| setSelectedCapability(endpoint) | ||
| setSelectedTags([]) | ||
| }, []) | ||
|
|
||
| /** | ||
| * 处理标签变化 | ||
| */ | ||
| const handleTagsChange = useCallback((tags: string[]) => { | ||
| setSelectedTags(tags) | ||
| }, []) | ||
|
|
||
| /** | ||
| * 处理搜索变化 | ||
| */ | ||
| const handleSearchChange = useCallback((query: string) => { | ||
| setSearchQuery(query) | ||
| }, []) | ||
|
|
||
| /** | ||
| * 处理添加渠道操作 | ||
| */ | ||
| const handleAddChannel = (model: Model) => { | ||
| console.log("添加私有渠道:", model.modelName) | ||
| } | ||
|
|
||
| return ( | ||
| <> | ||
| <TopBar /> | ||
| <div className="p-8"> | ||
| 模型目录 | ||
| <TopBar title={t("modelCatalog")} description={t("modelCatalogDesc")} /> | ||
| <div className="flex h-[calc(100vh-4rem)] flex-col overflow-hidden"> | ||
| <div className="flex-1 overflow-y-auto"> | ||
| <div className="container px-6 py-8"> | ||
| {/* 模型筛选面板 */} | ||
| <ModelFilterPanel | ||
| categoryTrees={categoryTrees} | ||
| features={features} | ||
| initialEndpoint={selectedCapability} | ||
| initialTags={selectedTags} | ||
| isLoadingFeatures={initialLoading} | ||
| onCapabilityChange={handleCapabilityChange} | ||
| onTagsChange={handleTagsChange} | ||
| onSearchChange={handleSearchChange} | ||
| /> | ||
|
|
||
| {/* 错误提示 */} | ||
| {error && ( | ||
| <div className="mb-4 rounded-lg border border-red-500/20 bg-red-500/10 p-4"> | ||
| <div className="flex items-center justify-between"> | ||
| <div className="flex items-center gap-2"> | ||
| <AlertCircle className="h-5 w-5 flex-shrink-0 text-red-500" /> | ||
| <p className="text-sm text-red-500">{error.message}</p> | ||
| </div> | ||
| <Button | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={refetch} | ||
| className="ml-4 flex-shrink-0" | ||
| > | ||
| {t("retry")} | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| )} | ||
|
|
||
| {/* 模型列表 */} | ||
| <div className="mb-4"> | ||
| <h2 className="text-sm font-medium text-muted-foreground"> | ||
| {t("foundModels")} {filteredModels.length} {t("modelsCount")} | ||
| </h2> | ||
| </div> | ||
|
|
||
| {modelsLoading ? ( | ||
| <div className="flex items-center justify-center py-12 text-muted-foreground"> | ||
| <Loader className="h-6 w-6 animate-spin mr-2" /> | ||
| <span className="text-sm">{t("loadingModels")}</span> | ||
| </div> | ||
| ) : ( | ||
| <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> | ||
| {filteredModels.map((model) => ( | ||
| <ModelCard | ||
| key={model.modelName} | ||
| model={model} | ||
| tagColors={tagColors} | ||
| onAddChannel={handleAddChannel} | ||
| /> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </> | ||
| ); | ||
| ) | ||
| } | ||
|
|
||
| export default ModelsPage | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import * as React from "react" | ||
| import { cva, type VariantProps } from "class-variance-authority" | ||
|
|
||
| import { cn } from "@/lib/utils" | ||
|
|
||
| const badgeVariants = cva( | ||
| "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", | ||
| { | ||
| variants: { | ||
| variant: { | ||
| default: | ||
| "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", | ||
| secondary: | ||
| "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", | ||
| destructive: | ||
| "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", | ||
| outline: "text-foreground", | ||
| }, | ||
| }, | ||
| defaultVariants: { | ||
| variant: "default", | ||
| }, | ||
| } | ||
| ) | ||
|
|
||
| export interface BadgeProps | ||
| extends React.HTMLAttributes<HTMLDivElement>, | ||
| VariantProps<typeof badgeVariants> {} | ||
|
|
||
| function Badge({ className, variant, ...props }: BadgeProps) { | ||
| return ( | ||
| <div className={cn(badgeVariants({ variant }), className)} {...props} /> | ||
| ) | ||
| } | ||
|
|
||
| export { Badge, badgeVariants } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import * as React from "react" | ||
|
|
||
| import { cn } from "@/lib/utils" | ||
|
|
||
| const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>( | ||
| ({ className, type, ...props }, ref) => { | ||
| return ( | ||
| <input | ||
| type={type} | ||
| className={cn( | ||
| "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", | ||
| className | ||
| )} | ||
| ref={ref} | ||
| {...props} | ||
| /> | ||
| ) | ||
| } | ||
| ) | ||
| Input.displayName = "Input" | ||
|
|
||
| export { Input } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export { ThemeProvider, useTheme } from "./theme-provider" | ||
| export { LanguageProvider, useLanguage } from "./language-provider" | ||
| export { SidebarProvider, useSidebar } from "./sidebar-provider" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
看起来 endpoint、models这些基础数据非常通用,建议规划到一个 context 对象结构, 把定义和 获取拆开。可以单开issues处理