diff --git a/frontend/src/components/console/common-data.tsx b/frontend/src/components/console/common-data.tsx new file mode 100644 index 00000000..047ca849 --- /dev/null +++ b/frontend/src/components/console/common-data.tsx @@ -0,0 +1,59 @@ +import { createContext, useContext } from 'react'; +import type { + DomainGitIdentity, + DomainHost, + DomainImage, + DomainModel, + DomainProject, + DomainProjectTask, + DomainUser, + DomainVirtualMachine, +} from '@/api/Api'; + +export type CommonData = { + user: DomainUser; + reloadUser: () => void; + + hosts: DomainHost[]; + vms: DomainVirtualMachine[]; + loadingHosts: boolean; + hostsInited: boolean; + reloadHosts: () => void; + + models: DomainModel[]; + loadingModels: boolean; + reloadModels: () => void; + + images: DomainImage[]; + loadingImages: boolean; + reloadImages: () => void; + + identities: DomainGitIdentity[]; + loadingIdentities: boolean; + reloadIdentities: () => void; + + balance: number; + bonus: number; + reloadWallet: () => void; + + members: DomainUser[]; + loadingMembers: boolean; + reloadMembers: () => void; + + projects: DomainProject[]; + loadingProjects: boolean; + reloadProjects: () => void; + + /** 未关联项目的任务(quick_start),用于侧边栏「默认」分组展示 */ + unlinkedTasks: DomainProjectTask[]; + loadingUnlinkedTasks: boolean; + reloadUnlinkedTasks: () => void; +}; + +export const DataContext = createContext(null); + +export const useCommonData = () => { + const ctx = useContext(DataContext); + if (!ctx) throw new Error('useCommonData must be used within DataProvider'); + return ctx; +}; diff --git a/frontend/src/components/console/data-provider.tsx b/frontend/src/components/console/data-provider.tsx index d0a26b93..fb9bb058 100644 --- a/frontend/src/components/console/data-provider.tsx +++ b/frontend/src/components/console/data-provider.tsx @@ -1,51 +1,10 @@ -import { ConstsGitPlatform, ConstsOwnerType, type DomainGitIdentity, type DomainHost, type DomainImage, type DomainModel, type DomainProject, type DomainProjectTask, type DomainUser, type DomainVirtualMachine } from '@/api/Api'; +import { ConstsGitPlatform, ConstsOwnerType, type DomainGitIdentity, type DomainHost, type DomainImage, type DomainModel, type DomainProject, type DomainProjectTask, type DomainUser } from '@/api/Api'; import { getImageShortName } from '@/utils/common'; import { apiRequest } from '@/utils/requestUtils'; -import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import { DataContext } from '@/components/console/common-data'; +import React, { useEffect, useMemo, useState } from 'react'; import { toast } from 'sonner'; -type CommonData = { - user: DomainUser; - reloadUser: () => void; - - hosts: DomainHost[]; - vms: DomainVirtualMachine[]; - loadingHosts: boolean; - hostsInited: boolean; - reloadHosts: () => void; - - models: DomainModel[]; - loadingModels: boolean; - reloadModels: () => void; - - images: DomainImage[]; - loadingImages: boolean; - reloadImages: () => void; - - identities: DomainGitIdentity[]; - loadingIdentities: boolean; - reloadIdentities: () => void; - - balance: number; - bonus: number; - reloadWallet: () => void; - - members: DomainUser[]; - loadingMembers: boolean; - reloadMembers: () => void; - - projects: DomainProject[]; - loadingProjects: boolean; - reloadProjects: () => void; - - /** 未关联项目的任务(quick_start),用于侧边栏「默认」分组展示 */ - unlinkedTasks: DomainProjectTask[]; - loadingUnlinkedTasks: boolean; - reloadUnlinkedTasks: () => void; -}; - -const DataContext = createContext(null); - export const DataProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [userInfo, setUserInfo] = useState({}); @@ -228,8 +187,8 @@ export const DataProvider: React.FC<{ children: React.ReactNode }> = ({ children const fetchWallet = () => { apiRequest('v1UsersWalletList', {}, [], (resp) => { if (resp.code === 0) { - setBalance(resp.data?.balance / 1000); - setBonus(resp.data?.bonus / 1000); + setBalance((resp.data?.balance || 0) / 1000); + setBonus((resp.data?.bonus || 0) / 1000); } else { toast.error("获取余额失败: " + resp.message); } @@ -334,9 +293,3 @@ export const DataProvider: React.FC<{ children: React.ReactNode }> = ({ children ); }; - -export const useCommonData = () => { - const ctx = useContext(DataContext); - if (!ctx) throw new Error('useCommonData must be used within DataProvider'); - return ctx; -}; diff --git a/frontend/src/components/console/files/file-picker-dialog.tsx b/frontend/src/components/console/files/file-picker-dialog.tsx index 1645808e..faf8d360 100644 --- a/frontend/src/components/console/files/file-picker-dialog.tsx +++ b/frontend/src/components/console/files/file-picker-dialog.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from "react" +import React, { useState, useEffect, useCallback } from "react" import { Dialog, DialogContent, @@ -375,7 +375,11 @@ export default function FilePickerDialog({ (空目录) ) : ( - node.children.map(child => renderNode(child)) + node.children.map(child => ( + + {renderNode(child)} + + )) )} )} @@ -400,7 +404,11 @@ export default function FilePickerDialog({ ) : (
- {treeNodes.map(node => renderNode(node))} + {treeNodes.map(node => ( + + {renderNode(node)} + + ))}
)} diff --git a/frontend/src/components/console/git-bot/create-git-bot-dialog.tsx b/frontend/src/components/console/git-bot/create-git-bot-dialog.tsx index 8f715a1c..19ada28c 100644 --- a/frontend/src/components/console/git-bot/create-git-bot-dialog.tsx +++ b/frontend/src/components/console/git-bot/create-git-bot-dialog.tsx @@ -22,7 +22,7 @@ import { toast } from "sonner" import type { DomainGitBot, DomainHost } from "@/api/Api" import Icon from "@/components/common/Icon" import { Badge } from "@/components/ui/badge" -import { useCommonData } from "../data-provider" +import { useCommonData } from "../common-data" import { getHostBadges } from "@/utils/common" interface CreateGitBotDialogProps { diff --git a/frontend/src/components/console/git-bot/edit-git-bot-dialog.tsx b/frontend/src/components/console/git-bot/edit-git-bot-dialog.tsx index 46bdfc4a..c04783c5 100644 --- a/frontend/src/components/console/git-bot/edit-git-bot-dialog.tsx +++ b/frontend/src/components/console/git-bot/edit-git-bot-dialog.tsx @@ -22,7 +22,7 @@ import type { DomainGitBot } from "@/api/Api" import { ConstsGitPlatform, ConstsHostStatus } from "@/api/Api" import Icon from "@/components/common/Icon" import { Badge } from "@/components/ui/badge" -import { useCommonData } from "../data-provider" +import { useCommonData } from "../common-data" import { getHostBadges } from "@/utils/common" interface EditGitBotDialogProps { diff --git a/frontend/src/components/console/git-bot/edit-git-bot-permission-dialog.tsx b/frontend/src/components/console/git-bot/edit-git-bot-permission-dialog.tsx index 453ae3ee..8db39fa0 100644 --- a/frontend/src/components/console/git-bot/edit-git-bot-permission-dialog.tsx +++ b/frontend/src/components/console/git-bot/edit-git-bot-permission-dialog.tsx @@ -8,7 +8,7 @@ import { toast } from "sonner" import { IconLoader } from "@tabler/icons-react" import { ChevronDown } from "lucide-react" import { cn } from "@/lib/utils" -import { useCommonData } from "../data-provider" +import { useCommonData } from "../common-data" interface EditGitBotPermissionDialogProps { open: boolean diff --git a/frontend/src/components/console/nav/nav-balance.tsx b/frontend/src/components/console/nav/nav-balance.tsx index f9582a60..815194c8 100644 --- a/frontend/src/components/console/nav/nav-balance.tsx +++ b/frontend/src/components/console/nav/nav-balance.tsx @@ -16,7 +16,7 @@ import { Item, ItemContent, ItemGroup, ItemTitle } from "@/components/ui/item"; import dayjs from "dayjs"; import { cn } from "@/lib/utils"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { useCommonData } from "../data-provider"; +import { useCommonData } from "../common-data"; interface NavBalanceProps { variant?: "sidebar" | "header"; diff --git a/frontend/src/components/console/nav/nav-project.tsx b/frontend/src/components/console/nav/nav-project.tsx index 0cb0e7e3..5efda4b5 100644 --- a/frontend/src/components/console/nav/nav-project.tsx +++ b/frontend/src/components/console/nav/nav-project.tsx @@ -23,7 +23,7 @@ import { } from "@/components/ui/alert-dialog" import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { useCommonData } from "../data-provider" +import { useCommonData } from "../common-data" import { IconChevronDown, IconChevronRight, IconCircleMinus, IconDotsVertical, IconLoader, IconPlus, IconReload, IconTrash } from "@tabler/icons-react" import { Button } from "@/components/ui/button" import AddProjectDialog from "../project/add-project" @@ -44,14 +44,18 @@ const loadExpandedFromStorage = (): Record => { try { const cached = localStorage.getItem(STORAGE_KEY) if (cached) return JSON.parse(cached) - } catch {} + } catch { + // ignore: storage unavailable or cached value is not valid JSON + } return {} } const saveExpandedToStorage = (state: Record) => { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) - } catch {} + } catch { + // ignore: storage unavailable (e.g. private mode / quota exceeded) + } } export default function NavProject() { diff --git a/frontend/src/components/console/nav/nav-user.tsx b/frontend/src/components/console/nav/nav-user.tsx index ec3afe51..df69a091 100644 --- a/frontend/src/components/console/nav/nav-user.tsx +++ b/frontend/src/components/console/nav/nav-user.tsx @@ -47,7 +47,7 @@ import { Spinner } from "@/components/ui/spinner" import { Button } from "@/components/ui/button" import { toast } from "sonner" import { IconLockCode, IconLogout, IconUserHexagon, IconUpload } from "@tabler/icons-react" -import { useCommonData } from "@/components/console/data-provider" +import { useCommonData } from "@/components/console/common-data" export default function NavUser() { const { isMobile } = useSidebar() @@ -69,7 +69,7 @@ export default function NavUser() { const handleLogout = () => { apiRequest('v1UsersLogoutCreate', {}, [], (resp) => { - if (resp.code === 0) { + if (Number(resp.code) === 0) { navigate('/'); } else { toast.error('登出失败: ' + resp.message); @@ -94,7 +94,6 @@ export default function NavUser() { await apiRequest('v1UsersPasswordsChangeUpdate', { current_password: currentPassword, new_password: newPassword, - confirm_password: confirmPassword, }, [], (resp) => { if (resp?.code === 0) { toast.success('密码修改成功'); @@ -212,7 +211,7 @@ export default function NavUser() { >
- setShowChangeAvatarDialog(true)} > diff --git a/frontend/src/components/console/project/add-project.tsx b/frontend/src/components/console/project/add-project.tsx index 44e0912d..0769d51d 100644 --- a/frontend/src/components/console/project/add-project.tsx +++ b/frontend/src/components/console/project/add-project.tsx @@ -38,7 +38,7 @@ import { useNavigate } from "react-router-dom" import { useSettingsDialog } from "@/pages/console/user/page" import { toast } from "sonner" import { Spinner } from "@/components/ui/spinner" -import { useCommonData } from "@/components/console/data-provider" +import { useCommonData } from "@/components/console/common-data" interface RepoOption { gitIdentityId: string diff --git a/frontend/src/components/console/project/edit-project-image.tsx b/frontend/src/components/console/project/edit-project-image.tsx index 58728562..5d371b0f 100644 --- a/frontend/src/components/console/project/edit-project-image.tsx +++ b/frontend/src/components/console/project/edit-project-image.tsx @@ -3,7 +3,7 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from " import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import type { DomainImage, DomainProject } from "@/api/Api" import Icon from "@/components/common/Icon" -import { useCommonData } from "@/components/console/data-provider" +import { useCommonData } from "@/components/console/common-data" import { apiRequest } from "@/utils/requestUtils" import { getImageShortName, getOSFromImageName } from "@/utils/common" import { useEffect, useState } from "react" diff --git a/frontend/src/components/console/project/issue-design-dialog.tsx b/frontend/src/components/console/project/issue-design-dialog.tsx index b7f1f2df..b16415cb 100644 --- a/frontend/src/components/console/project/issue-design-dialog.tsx +++ b/frontend/src/components/console/project/issue-design-dialog.tsx @@ -1,5 +1,5 @@ import { type DomainProjectIssue, type DomainProject, ConstsCliName, ConstsGitPlatform, ConstsTaskSubType, ConstsTaskType, type DomainBranch } from "@/api/Api" -import { useCommonData } from "@/components/console/data-provider" +import { useCommonData } from "@/components/console/common-data" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" diff --git a/frontend/src/components/console/project/issue-dev-dialog.tsx b/frontend/src/components/console/project/issue-dev-dialog.tsx index 7282a35a..803fe8aa 100644 --- a/frontend/src/components/console/project/issue-dev-dialog.tsx +++ b/frontend/src/components/console/project/issue-dev-dialog.tsx @@ -1,5 +1,5 @@ import { type DomainProjectIssue, type DomainProject, ConstsCliName, ConstsGitPlatform, ConstsTaskType, type DomainBranch } from "@/api/Api" -import { useCommonData } from "@/components/console/data-provider" +import { useCommonData } from "@/components/console/common-data" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" diff --git a/frontend/src/components/console/project/project-info.tsx b/frontend/src/components/console/project/project-info.tsx index 9d3be094..07dfba84 100644 --- a/frontend/src/components/console/project/project-info.tsx +++ b/frontend/src/components/console/project/project-info.tsx @@ -1,5 +1,5 @@ import { type DomainProject } from "@/api/Api" -import { useCommonData } from "@/components/console/data-provider" +import { useCommonData } from "@/components/console/common-data" import EditProjectEnvDialog from "@/components/console/project/edit-project-env" import EditProjectImageDialog from "@/components/console/project/edit-project-image" import EditProjectNameDialog from "@/components/console/project/edit-project-name" diff --git a/frontend/src/components/console/project/start-develop-task-dialog.tsx b/frontend/src/components/console/project/start-develop-task-dialog.tsx index 6edbe93d..2d1c1da0 100644 --- a/frontend/src/components/console/project/start-develop-task-dialog.tsx +++ b/frontend/src/components/console/project/start-develop-task-dialog.tsx @@ -1,6 +1,6 @@ import { ConstsCliName, ConstsTaskType, ConstsGitPlatform, ConstsInterfaceType, ConstsOwnerType, type DomainModel, type DomainProject, type DomainBranch } from "@/api/Api" import Icon from "@/components/common/Icon" -import { useCommonData } from "@/components/console/data-provider" +import { useCommonData } from "@/components/console/common-data" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" @@ -12,7 +12,7 @@ import { Spinner } from "@/components/ui/spinner" import { getBrandFromModelName, getInterfaceTypeBadge, getModelHealthBadge, getOwnerTypeBadge, selectHost, selectImage, selectModel } from "@/utils/common" import { apiRequest } from "@/utils/requestUtils" import { IconSparkles } from "@tabler/icons-react" -import { useState, useEffect, useMemo, useRef } from "react" +import { useState, useEffect, useMemo, useRef, useCallback } from "react" import { useNavigate } from "react-router-dom" import { toast } from "sonner" @@ -26,7 +26,7 @@ export default function StartDevelopTaskDialog({ open, onOpenChange, project -}: StartDevelopTaskDialogProps) { +}: Readonly) { const navigate = useNavigate() const [submitting, setSubmitting] = useState(false) const [branches, setBranches] = useState([]) @@ -48,7 +48,7 @@ export default function StartDevelopTaskDialog({ return [economyModel, ...models] }, [models]) - const fetchBranches = async () => { + const fetchBranches = useCallback(async () => { if (!project?.git_identity_id || !project?.repo_url) { return } @@ -61,14 +61,13 @@ export default function StartDevelopTaskDialog({ } setLoadingBranches(true) - + try { // 直接使用 full_name 字段 const escapedRepoFullName = project?.full_name || '' - + if (!escapedRepoFullName) { toast.error('无法获取仓库信息') - setLoadingBranches(false) return } @@ -79,15 +78,14 @@ export default function StartDevelopTaskDialog({ if (resp.code === 0 && resp.data) { const branchList = resp.data.map((b: DomainBranch) => b.name || '').filter(Boolean) setBranches(branchList) - - // 优先选择 main 或 master,否则选择第一个 - if (branchList.includes('main')) { - setSelectedBranch('main') - } else if (branchList.includes('master')) { - setSelectedBranch('master') - } else if (branchList.length > 0) { - setSelectedBranch(branchList[0]) - } + + // 仅在当前选择不存在时才重置,避免刷新覆盖用户选择 + setSelectedBranch((prev) => { + if (prev && branchList.includes(prev)) return prev + if (branchList.includes('main')) return 'main' + if (branchList.includes('master')) return 'master' + return branchList[0] || '' + }) } else { toast.error('获取分支列表失败: ' + resp.message) } @@ -98,22 +96,31 @@ export default function StartDevelopTaskDialog({ } finally { setLoadingBranches(false) } - } + }, [project]) + + const prevOpenRef = useRef(false) + const prevProjectIdRef = useRef(undefined) - const prevOpenRef = useRef(false) useEffect(() => { - if (open) { - const justOpened = !prevOpenRef.current - prevOpenRef.current = true - if (justOpened) { - setUserMessage('') - setSelectedModelId(selectModel(modelsWithEconomy, true)) - } - fetchBranches() - } else { + if (!open) { prevOpenRef.current = false + prevProjectIdRef.current = project?.id + return + } + + const projectId = project?.id + const firstOpen = !prevOpenRef.current + const projectChanged = projectId !== prevProjectIdRef.current + + if (firstOpen || projectChanged) { + setUserMessage('') + setSelectedModelId(selectModel(modelsWithEconomy, true)) + fetchBranches() } - }, [open, project, modelsWithEconomy]) + + prevOpenRef.current = true + prevProjectIdRef.current = projectId + }, [open, project?.id, modelsWithEconomy, fetchBranches]) const handleSubmit = async () => { if (!userMessage.trim()) { diff --git a/frontend/src/components/console/settings/add-model.tsx b/frontend/src/components/console/settings/add-model.tsx index 4eb61225..9f26f78b 100644 --- a/frontend/src/components/console/settings/add-model.tsx +++ b/frontend/src/components/console/settings/add-model.tsx @@ -13,7 +13,7 @@ import { Field, FieldContent, FieldDescription, FieldLabel } from "@/components/ import { apiRequest } from "@/utils/requestUtils" import { toast } from "sonner" import type { DomainProviderModelListItem } from "@/api/Api" -import { ConstsInterfaceType } from "@/api/Api" +import { ConstsInterfaceType, ConstsModelProvider } from "@/api/Api" import { Select, SelectContent, @@ -100,15 +100,15 @@ export default function AddModel({ model: model.trim(), base_url: baseUrl.trim(), interface_type: interfaceType, - provider: "BaiZhiCloud", + provider: ConstsModelProvider.ModelProviderBaiZhiCloud, } await apiRequest('v1UsersModelsHealthCheckCreate', healthCheckData, [], async (resp) => { if (resp.code === 0) { if (resp.data?.success) { // 健康检查通过,继续保存 - const requestData: any = { - provider: "BaiZhiCloud", + const requestData = { + provider: ConstsModelProvider.ModelProviderBaiZhiCloud, model: model.trim(), base_url: baseUrl.trim(), api_key: apiToken.trim(), diff --git a/frontend/src/components/console/settings/edit-model.tsx b/frontend/src/components/console/settings/edit-model.tsx index bca4aa6d..5fd3ba06 100644 --- a/frontend/src/components/console/settings/edit-model.tsx +++ b/frontend/src/components/console/settings/edit-model.tsx @@ -11,8 +11,8 @@ import { Input } from "@/components/ui/input" import { Field, FieldContent, FieldDescription, FieldLabel } from "@/components/ui/field" import { apiRequest } from "@/utils/requestUtils" import { toast } from "sonner" -import type { DomainModel, DomainProviderModelListItem } from "@/api/Api" -import { ConstsInterfaceType } from "@/api/Api" +import type { DomainModel, DomainProviderModelListItem, DomainUpdateModelReq } from "@/api/Api" +import { ConstsInterfaceType, ConstsModelProvider } from "@/api/Api" import { Select, SelectContent, @@ -69,10 +69,11 @@ export default function EditModel({ } setLoadingModels(true) - await apiRequest('getProviderModelList', { + const provider = (model?.provider as ConstsModelProvider | undefined) ?? ConstsModelProvider.ModelProviderBaiZhiCloud + await apiRequest('getProviderModelList', { api_key: apiToken.trim(), base_url: baseUrl.trim() || model?.base_url || "https://model-square.app.baizhi.cloud/v1", - provider: model?.provider || "BaiZhiCloud", + provider, }, [], (resp) => { if (resp.code === 0) { const models = resp.data?.models || [] @@ -112,7 +113,7 @@ export default function EditModel({ setSaving(true) // 先进行健康检查 - const provider = model.provider || "BaiZhiCloud" + const provider = (model.provider as ConstsModelProvider | undefined) ?? ConstsModelProvider.ModelProviderBaiZhiCloud const healthCheckData = { api_key: apiToken.trim(), model: selectedModel.trim(), @@ -125,7 +126,7 @@ export default function EditModel({ if (resp.code === 0) { if (resp.data?.success) { // 健康检查通过,继续保存 - const requestData: any = { + const requestData: DomainUpdateModelReq = { api_key: apiToken.trim(), model: selectedModel.trim(), base_url: baseUrl.trim(), diff --git a/frontend/src/components/console/settings/hosts.tsx b/frontend/src/components/console/settings/hosts.tsx index d62a417d..9873b53a 100644 --- a/frontend/src/components/console/settings/hosts.tsx +++ b/frontend/src/components/console/settings/hosts.tsx @@ -50,7 +50,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { IconAlertHexagon, IconPencil, IconStar, IconTrash } from "@tabler/icons-react" -import { useCommonData } from "../data-provider" +import { useCommonData } from "../common-data" export default function Hosts() { const [open, setOpen] = useState(false) diff --git a/frontend/src/components/console/settings/identities.tsx b/frontend/src/components/console/settings/identities.tsx index a9606bce..40d9aea3 100644 --- a/frontend/src/components/console/settings/identities.tsx +++ b/frontend/src/components/console/settings/identities.tsx @@ -39,7 +39,7 @@ import { Badge } from "@/components/ui/badge" import { IconPasswordFingerprint, IconPencil, IconPlugConnected, IconTrash } from "@tabler/icons-react" import { getGitPlatformIcon, getGithubAppInstallUrl } from "@/utils/common" import Icon from "@/components/common/Icon" -import { useCommonData } from "../data-provider" +import { useCommonData } from "../common-data" import { Spinner } from "@/components/ui/spinner" export default function Identities() { diff --git a/frontend/src/components/console/settings/images.tsx b/frontend/src/components/console/settings/images.tsx index 83e7e2c7..a0de4dff 100644 --- a/frontend/src/components/console/settings/images.tsx +++ b/frontend/src/components/console/settings/images.tsx @@ -42,7 +42,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { IconAlertHexagon, IconPencil, IconStar, IconTrash } from "@tabler/icons-react" -import { useCommonData } from "../data-provider" +import { useCommonData } from "../common-data" import { Badge } from "@/components/ui/badge" export default function Images() { diff --git a/frontend/src/components/console/settings/models.tsx b/frontend/src/components/console/settings/models.tsx index b337e856..3095e273 100644 --- a/frontend/src/components/console/settings/models.tsx +++ b/frontend/src/components/console/settings/models.tsx @@ -48,7 +48,7 @@ import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia } from " import { Spinner } from "@/components/ui/spinner" import { Badge } from "@/components/ui/badge" import { IconAlertHexagon, IconCheck, IconHeartRateMonitor, IconLoader, IconPencil, IconStar, IconTrash, IconX } from "@tabler/icons-react" -import { useCommonData } from "../data-provider" +import { useCommonData } from "../common-data" export default function Models() { const [isDialogOpen, setIsDialogOpen] = useState(false) diff --git a/frontend/src/components/console/settings/settings-dialog.tsx b/frontend/src/components/console/settings/settings-dialog.tsx index 0a703185..2a681e69 100644 --- a/frontend/src/components/console/settings/settings-dialog.tsx +++ b/frontend/src/components/console/settings/settings-dialog.tsx @@ -30,7 +30,7 @@ import { SidebarProvider, } from "@/components/ui/sidebar" import { useGitHubSetupCallback } from "@/hooks/useGitHubSetupCallback" -import { useCommonData } from "@/components/console/data-provider" +import { useCommonData } from "@/components/console/common-data" import { AlertDialog, AlertDialogAction, diff --git a/frontend/src/components/console/settings/vms.tsx b/frontend/src/components/console/settings/vms.tsx index 7e34ce48..043a75bb 100644 --- a/frontend/src/components/console/settings/vms.tsx +++ b/frontend/src/components/console/settings/vms.tsx @@ -55,7 +55,7 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { HoverCard, HoverCardTrigger } from "@/components/ui/hover-card"; -import { useCommonData } from "@/components/console/data-provider"; +import { useCommonData } from "@/components/console/common-data"; import { VmRenewDialog } from "@/components/console/vm/vm-renew"; import { Switch } from "@/components/ui/switch"; diff --git a/frontend/src/components/console/task/task-input.tsx b/frontend/src/components/console/task/task-input.tsx index 619633eb..2dd04666 100644 --- a/frontend/src/components/console/task/task-input.tsx +++ b/frontend/src/components/console/task/task-input.tsx @@ -1,6 +1,6 @@ import { Api, ConstsCliName, ConstsGitPlatform, ConstsHostStatus, ConstsInterfaceType, ConstsOwnerType, ConstsTaskSubType, ConstsTaskType, ConstsUserRole, type DomainAuthRepository, type DomainGitIdentity, type DomainModel, type DomainSkill } from "@/api/Api"; import Icon from "@/components/common/Icon"; -import { useCommonData } from "@/components/console/data-provider"; +import { useCommonData } from "@/components/console/common-data"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; diff --git a/frontend/src/components/console/task/task-preview-panel.tsx b/frontend/src/components/console/task/task-preview-panel.tsx index b44bd622..c4168fa6 100644 --- a/frontend/src/components/console/task/task-preview-panel.tsx +++ b/frontend/src/components/console/task/task-preview-panel.tsx @@ -54,7 +54,7 @@ export function TaskPreviewPanel({ const [whitelistSaving, setWhitelistSaving] = useState(false) const confirmDeletePort = () => { - if (!hostId || !vmId || !portToDelete) { + if (!hostId || !vmId || !portToDelete?.forward_id) { setDeleteDialogOpen(false) return } @@ -63,7 +63,7 @@ export function TaskPreviewPanel({ setDeleteDialogOpen(false) apiRequest('v1UsersHostsVmsPortsDelete', { - forward_id: portToDelete.forward_id + forward_id: portToDelete.forward_id, }, [hostId, vmId, String(portToDelete.port)], (resp) => { if (resp.code === 0) { toast.success("端口已关闭访问") @@ -137,9 +137,9 @@ export function TaskPreviewPanel({ setWhitelistSaving(true) await apiRequest('v1UsersHostsVmsPortsCreate', { forward_id: portToEditWhitelist.forward_id, - port: portToEditWhitelist.port, - white_list: whitelistArray - }, [hostId, vmId], (resp: WebResp) => { + port: portToEditWhitelist.port ?? 0, + white_list: whitelistArray, + }, [hostId!, vmId!], (resp: WebResp) => { if (resp.code === 0) { toast.success("白名单更新成功") setWhitelistDialogOpen(false) diff --git a/frontend/src/components/console/vm/vm-add.tsx b/frontend/src/components/console/vm/vm-add.tsx index 528e5b18..335e1f9a 100644 --- a/frontend/src/components/console/vm/vm-add.tsx +++ b/frontend/src/components/console/vm/vm-add.tsx @@ -28,7 +28,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip" import { Badge } from "@/components/ui/badge" -import { useCommonData } from "../data-provider" +import { useCommonData } from "../common-data" interface VmAddDialogProps { open: boolean diff --git a/frontend/src/components/console/vm/vm-port-forward.tsx b/frontend/src/components/console/vm/vm-port-forward.tsx index 7b491192..19931dbe 100644 --- a/frontend/src/components/console/vm/vm-port-forward.tsx +++ b/frontend/src/components/console/vm/vm-port-forward.tsx @@ -54,7 +54,7 @@ export function VmPortForwardDialog({ const [whitelistSaving, setWhitelistSaving] = useState(false) const confirmDeletePort = () => { - if (!hostId || !vmId || !portToDelete) { + if (!hostId || !vmId || !portToDelete?.forward_id) { setDeleteDialogOpen(false) return } @@ -86,7 +86,7 @@ export function VmPortForwardDialog({ } const data = await resp.json() return data.ip - } catch (e) { + } catch { return null } } @@ -98,7 +98,7 @@ export function VmPortForwardDialog({ setPortToOpen(port) - let ip = await getMyIP() + const ip = await getMyIP() if (!ip) { toast.error("获取本机 IP 失败") setPortToOpen(0) @@ -129,7 +129,7 @@ export function VmPortForwardDialog({ } const handleSaveWhitelist = async () => { - if (!hostId || !vmId || !portToEditWhitelist) { + if (!hostId || !vmId || !portToEditWhitelist?.forward_id) { return } @@ -146,8 +146,8 @@ export function VmPortForwardDialog({ setWhitelistSaving(true) await apiRequest('v1UsersHostsVmsPortsCreate', { forward_id: portToEditWhitelist.forward_id, - port: portToEditWhitelist.port, - white_list: whitelistArray + port: portToEditWhitelist.port ?? 0, + white_list: whitelistArray, }, [hostId, vmId], (resp: WebResp) => { if (resp.code === 0) { toast.success("白名单更新成功") diff --git a/frontend/src/components/manager/add-image.tsx b/frontend/src/components/manager/add-image.tsx index c515acdb..09f9c289 100644 --- a/frontend/src/components/manager/add-image.tsx +++ b/frontend/src/components/manager/add-image.tsx @@ -35,6 +35,16 @@ export default function AddImage({ const [selectOpen, setSelectOpen] = useState(false) const selectRef = useRef(null) + const fetchGroups = async () => { + await apiRequest('v1TeamsGroupsList', {}, [], (resp) => { + if (resp.code === 0) { + setGroups(resp.data?.groups || []) + } else { + toast.error("获取分组列表失败: " + resp.message); + } + }) + } + useEffect(() => { if (open) { fetchGroups() @@ -57,16 +67,6 @@ export default function AddImage({ } }, [selectOpen]) - const fetchGroups = async () => { - await apiRequest('v1TeamsGroupsList', {}, [], (resp) => { - if (resp.code === 0) { - setGroups(resp.data?.groups || []) - } else { - toast.error("获取分组列表失败: " + resp.message); - } - }) - } - const handleGroupCheckboxChange = (groupId: string, checked: boolean) => { if (checked) { setSelectedGroupIds([...selectedGroupIds, groupId]) diff --git a/frontend/src/components/manager/add-model.tsx b/frontend/src/components/manager/add-model.tsx index 1b2673ab..d921d759 100644 --- a/frontend/src/components/manager/add-model.tsx +++ b/frontend/src/components/manager/add-model.tsx @@ -14,9 +14,8 @@ import { Checkbox } from "@/components/ui/checkbox" import { apiRequest } from "@/utils/requestUtils" import { toast } from "sonner" import type { DomainProviderModelListItem, DomainTeamGroup } from "@/api/Api" -import { ConstsInterfaceType } from "@/api/Api" +import { ConstsInterfaceType, ConstsModelProvider } from "@/api/Api" import { getModelUrlDescription, modelProviderList } from "@/utils/common" -import { ChevronDown } from "lucide-react" import { cn } from "@/lib/utils" import { Select, @@ -28,7 +27,7 @@ import { SelectValue, } from "@/components/ui/select" import { Spinner } from "@/components/ui/spinner" -import { CircleQuestionMark } from 'lucide-react' +import { CircleQuestionMark, ChevronDown } from 'lucide-react' interface AddModelProps { open: boolean @@ -53,6 +52,16 @@ export default function AddModel({ const [selectOpen, setSelectOpen] = useState(false) const selectRef = useRef(null) + const fetchGroups = async () => { + await apiRequest('v1TeamsGroupsList', {}, [], (resp) => { + if (resp.code === 0) { + setGroups(resp.data?.groups || []) + } else { + toast.error("获取分组列表失败: " + resp.message); + } + }) + } + useEffect(() => { if (open) { fetchGroups() @@ -75,16 +84,6 @@ export default function AddModel({ } }, [selectOpen]) - const fetchGroups = async () => { - await apiRequest('v1TeamsGroupsList', {}, [], (resp) => { - if (resp.code === 0) { - setGroups(resp.data?.groups || []) - } else { - toast.error("获取分组列表失败: " + resp.message); - } - }) - } - const handleGroupCheckboxChange = (groupId: string, checked: boolean) => { if (checked) { setSelectedGroupIds([...selectedGroupIds, groupId]) @@ -106,22 +105,22 @@ export default function AddModel({ setLoadingModels(true) await apiRequest('getProviderModelList', { - api_key: apiToken.trim(), - base_url: baseUrl.trim() || "https://model-square.app.baizhi.cloud/v1", - provider: "BaiZhiCloud", - }, [], (resp) => { - if (resp.code === 0) { - const models = resp.data?.models || [] - setModelList(models) - if (models.length === 0) { - toast.warning("未获取到可用模型") - } else { - toast.success(`获取到 ${models.length} 个可用模型`) - } + api_key: apiToken.trim(), + base_url: baseUrl.trim() || "https://model-square.app.baizhi.cloud/v1", + provider: "BaiZhiCloud", + }, [], (resp) => { + if (resp.code === 0) { + const models = resp.data?.models || [] + setModelList(models) + if (models.length === 0) { + toast.warning("未获取到可用模型") } else { - toast.error("获取模型列表失败: " + resp.message); + toast.success(`获取到 ${models.length} 个可用模型`) } - }) + } else { + toast.error("获取模型列表失败: " + resp.message); + } + }) setLoadingModels(false) } @@ -147,15 +146,15 @@ export default function AddModel({ model: model.trim(), base_url: baseUrl.trim(), interface_type: interfaceType, - provider: "BaiZhiCloud", + provider: ConstsModelProvider.ModelProviderBaiZhiCloud, } await apiRequest('v1TeamsModelsHealthCheckCreate', healthCheckData, [], async (resp) => { if (resp.code === 0) { if (resp.data?.success) { // 健康检查通过,继续保存 - const requestData: any = { - provider: "BaiZhiCloud", + const requestData = { + provider: ConstsModelProvider.ModelProviderBaiZhiCloud, model: model.trim(), base_url: baseUrl.trim(), api_key: apiToken.trim(), @@ -184,7 +183,7 @@ export default function AddModel({ } } }) - + setSaving(false) } @@ -202,18 +201,18 @@ export default function AddModel({ // 对模型列表进行分组和排序 const getGroupedModels = () => { const groups: Record = {} - + modelList.forEach((item) => { const modelName = item.model || "" const parts = modelName.split("-") const groupKey = parts.length > 0 ? parts[0] : "其他" - + if (!groups[groupKey]) { groups[groupKey] = [] } groups[groupKey].push(item) }) - + // 对每个组内的模型按字符串排序 Object.keys(groups).forEach((key) => { groups[key].sort((a, b) => { @@ -222,10 +221,10 @@ export default function AddModel({ return aName.localeCompare(bName) }) }) - + // 对组名进行排序 const sortedGroupKeys = Object.keys(groups).sort() - + return { groups, sortedGroupKeys } } @@ -360,8 +359,8 @@ export default function AddModel({ {selectedGroupIds.length === 0 ? "请选择分组" : selectedGroupIds.length === 1 - ? groups.find((g) => g.id === selectedGroupIds[0])?.name || "已选择 1 个分组" - : `已选择 ${selectedGroupIds.length} 个分组`} + ? groups.find((g) => g.id === selectedGroupIds[0])?.name || "已选择 1 个分组" + : `已选择 ${selectedGroupIds.length} 个分组`} diff --git a/frontend/src/components/manager/edit-host.tsx b/frontend/src/components/manager/edit-host.tsx index 6f417f52..00c1b205 100644 --- a/frontend/src/components/manager/edit-host.tsx +++ b/frontend/src/components/manager/edit-host.tsx @@ -38,6 +38,16 @@ export default function EditHost({ const [remark, setRemark] = useState("") const selectRef = useRef(null) + const fetchGroups = async () => { + await apiRequest('v1TeamsGroupsList', {}, [], (resp) => { + if (resp.code === 0) { + setGroups(resp.data?.groups || []) + } else { + toast.error("获取分组列表失败: " + resp.message); + } + }) + } + useEffect(() => { if (open) { fetchGroups() @@ -69,16 +79,6 @@ export default function EditHost({ } }, [selectOpen]) - const fetchGroups = async () => { - await apiRequest('v1TeamsGroupsList', {}, [], (resp) => { - if (resp.code === 0) { - setGroups(resp.data?.groups || []) - } else { - toast.error("获取分组列表失败: " + resp.message); - } - }) - } - const handleGroupCheckboxChange = (groupId: string, checked: boolean) => { if (checked) { setSelectedGroupIds([...selectedGroupIds, groupId]) diff --git a/frontend/src/components/manager/edit-image.tsx b/frontend/src/components/manager/edit-image.tsx index 8deffb19..f5fc8b26 100644 --- a/frontend/src/components/manager/edit-image.tsx +++ b/frontend/src/components/manager/edit-image.tsx @@ -39,6 +39,16 @@ export default function EditImage({ const [selectOpen, setSelectOpen] = useState(false) const selectRef = useRef(null) + const fetchGroups = async () => { + await apiRequest('v1TeamsGroupsList', {}, [], (resp) => { + if (resp.code === 0) { + setGroups(resp.data?.groups || []) + } else { + toast.error("获取分组列表失败: " + resp.message); + } + }) + } + useEffect(() => { if (open) { fetchGroups() @@ -70,16 +80,6 @@ export default function EditImage({ } }, [selectOpen]) - const fetchGroups = async () => { - await apiRequest('v1TeamsGroupsList', {}, [], (resp) => { - if (resp.code === 0) { - setGroups(resp.data?.groups || []) - } else { - toast.error("获取分组列表失败: " + resp.message); - } - }) - } - const handleGroupCheckboxChange = (groupId: string, checked: boolean) => { if (checked) { setSelectedGroupIds([...selectedGroupIds, groupId]) diff --git a/frontend/src/components/manager/edit-model.tsx b/frontend/src/components/manager/edit-model.tsx index b679b28f..0e3f5022 100644 --- a/frontend/src/components/manager/edit-model.tsx +++ b/frontend/src/components/manager/edit-model.tsx @@ -12,8 +12,8 @@ import { Field, FieldContent, FieldDescription, FieldLabel } from "@/components/ import { Checkbox } from "@/components/ui/checkbox" import { apiRequest } from "@/utils/requestUtils" import { toast } from "sonner" -import type { DomainTeamModel, DomainProviderModelListItem, DomainTeamGroup } from "@/api/Api" -import { ConstsInterfaceType } from "@/api/Api" +import type { DomainTeamModel, DomainProviderModelListItem, DomainTeamGroup, DomainUpdateTeamModelReq } from "@/api/Api" +import { ConstsInterfaceType, ConstsModelProvider } from "@/api/Api" import { getModelUrlDescription, modelProviderList } from "@/utils/common" import { ChevronDown } from "lucide-react" import { cn } from "@/lib/utils" @@ -53,6 +53,16 @@ export default function EditModel({ const [selectOpen, setSelectOpen] = useState(false) const selectRef = useRef(null) + const fetchGroups = async () => { + await apiRequest('v1TeamsGroupsList', {}, [], (resp) => { + if (resp.code === 0) { + setGroups(resp.data?.groups || []) + } else { + toast.error("获取分组列表失败: " + resp.message); + } + }) + } + useEffect(() => { if (open) { fetchGroups() @@ -87,16 +97,6 @@ export default function EditModel({ } }, [selectOpen]) - const fetchGroups = async () => { - await apiRequest('v1TeamsGroupsList', {}, [], (resp) => { - if (resp.code === 0) { - setGroups(resp.data?.groups || []) - } else { - toast.error("获取分组列表失败: " + resp.message); - } - }) - } - const handleGroupCheckboxChange = (groupId: string, checked: boolean) => { if (checked) { setSelectedGroupIds([...selectedGroupIds, groupId]) @@ -117,23 +117,24 @@ export default function EditModel({ } setLoadingModels(true) + const provider = (model?.provider as ConstsModelProvider | undefined) ?? ConstsModelProvider.ModelProviderBaiZhiCloud await apiRequest('getProviderModelList', { - api_key: apiToken.trim(), - base_url: baseUrl.trim() || model?.base_url || "https://model-square.app.baizhi.cloud/v1", - provider: model?.provider || "BaiZhiCloud", - }, [], (resp) => { - if (resp.code === 0) { - const models = resp.data?.models || [] - setModelList(models) - if (models.length === 0) { - toast.warning("未获取到可用模型") - } else { - toast.success(`获取到 ${models.length} 个可用模型`) - } + api_key: apiToken.trim(), + base_url: baseUrl.trim() || model?.base_url || "https://model-square.app.baizhi.cloud/v1", + provider, + }, [], (resp) => { + if (resp.code === 0) { + const models = resp.data?.models || [] + setModelList(models) + if (models.length === 0) { + toast.warning("未获取到可用模型") } else { - toast.error("获取模型列表失败: " + resp.message); + toast.success(`获取到 ${models.length} 个可用模型`) } - }) + } else { + toast.error("获取模型列表失败: " + resp.message); + } + }) setLoadingModels(false) } @@ -159,7 +160,7 @@ export default function EditModel({ setSaving(true) // 先进行健康检查 - const provider = model.provider || "BaiZhiCloud" + const provider = (model.provider as ConstsModelProvider | undefined) ?? ConstsModelProvider.ModelProviderBaiZhiCloud const healthCheckData = { api_key: apiToken.trim(), model: selectedModel.trim(), @@ -172,17 +173,13 @@ export default function EditModel({ if (resp.code === 0) { if (resp.data?.success) { // 健康检查通过,继续保存 - const requestData: any = { + const requestData: DomainUpdateTeamModelReq = { api_key: apiToken.trim(), model: selectedModel.trim(), base_url: baseUrl.trim(), interface_type: interfaceType, - group_ids: selectedGroupIds - } - - // 如果 provider 存在,也一起更新 - if (model.provider) { - requestData.provider = model.provider + group_ids: selectedGroupIds, + provider, } await apiRequest('v1TeamsModelsUpdate', requestData, [model.id!], (resp) => { @@ -206,7 +203,7 @@ export default function EditModel({ } } }) - + setSaving(false) } @@ -224,18 +221,18 @@ export default function EditModel({ // 对模型列表进行分组和排序 const getGroupedModels = () => { const groups: Record = {} - + modelList.forEach((item) => { const modelName = item.model || "" const parts = modelName.split("-") const groupKey = parts.length > 0 ? parts[0] : "其他" - + if (!groups[groupKey]) { groups[groupKey] = [] } groups[groupKey].push(item) }) - + // 对每个组内的模型按字符串排序 Object.keys(groups).forEach((key) => { groups[key].sort((a, b) => { @@ -244,10 +241,10 @@ export default function EditModel({ return aName.localeCompare(bName) }) }) - + // 对组名进行排序 const sortedGroupKeys = Object.keys(groups).sort() - + return { groups, sortedGroupKeys } } @@ -372,8 +369,8 @@ export default function EditModel({ {selectedGroupIds.length === 0 ? "请选择分组" : selectedGroupIds.length === 1 - ? groups.find((g) => g.id === selectedGroupIds[0])?.name || "已选择 1 个分组" - : `已选择 ${selectedGroupIds.length} 个分组`} + ? groups.find((g) => g.id === selectedGroupIds[0])?.name || "已选择 1 个分组" + : `已选择 ${selectedGroupIds.length} 个分组`} diff --git a/frontend/src/components/welcome/git-bot.tsx b/frontend/src/components/welcome/git-bot.tsx index e7de63c9..595a27bd 100644 --- a/frontend/src/components/welcome/git-bot.tsx +++ b/frontend/src/components/welcome/git-bot.tsx @@ -17,20 +17,29 @@ const GitBot = () => { useEffect(() => { let currentIndex = 0; - const interval = setInterval(() => { + let timeoutId: ReturnType | null = null; + + const tick = () => { if (currentIndex <= typewriterText.length) { setDisplayedText(typewriterText.slice(0, currentIndex)); - currentIndex++; - } else { - // 重置动画,循环播放 - setTimeout(() => { - setDisplayedText(""); - currentIndex = 0; - }, 2000); + currentIndex += 1; + timeoutId = setTimeout(tick, 100); + return; } - }, 100); - return () => clearInterval(interval); + // 完整播放结束后,停顿 2s 再重置并重新开始 + timeoutId = setTimeout(() => { + setDisplayedText(""); + currentIndex = 0; + tick(); + }, 2000); + }; + + tick(); + + return () => { + if (timeoutId) clearTimeout(timeoutId); + }; }, []); const advantages = [ diff --git a/frontend/src/pages/console/user/project/overview/tasks-tab.tsx b/frontend/src/pages/console/user/project/overview/tasks-tab.tsx index 38fa3a46..2bb919ae 100644 --- a/frontend/src/pages/console/user/project/overview/tasks-tab.tsx +++ b/frontend/src/pages/console/user/project/overview/tasks-tab.tsx @@ -28,7 +28,7 @@ import { } from "@/components/ui/empty" import { IconListDetails, IconCircleCheck, IconAlertTriangle, IconDotsVertical, IconTrash } from "@tabler/icons-react" import { cn } from "@/lib/utils" -import { useCommonData } from "@/components/console/data-provider" +import { useCommonData } from "@/components/console/common-data" import { getRepoNameFromUrl, renderHoverCardContent, stripMarkdown } from "@/utils/common" import dayjs from "dayjs" @@ -190,7 +190,10 @@ export default function ProjectOverviewTasksTab({ projectId, refreshKey }: Proje { title: "任务名称", content: task.summary || "" }, { title: "任务内容", content: task.content || "" }, { title: "任务状态", content: task.status || "" }, - { title: "任务类型", content: `${task.type}/${task.sub_type}` || "" }, + { + title: "任务类型", + content: task.type ? [task.type, task.sub_type].filter(Boolean).join("/") : "", + }, task.repo_url ? { title: "代码仓库", content: task.repo_url } : null, task.repo_filename ? { title: "代码文件", content: task.repo_filename } : null, task.repo_url ? { title: "仓库分支", content: task.branch || "" } : null, diff --git a/frontend/src/pages/console/user/tasks.tsx b/frontend/src/pages/console/user/tasks.tsx index facf901b..e4e98673 100644 --- a/frontend/src/pages/console/user/tasks.tsx +++ b/frontend/src/pages/console/user/tasks.tsx @@ -24,7 +24,7 @@ import { IconAlertTriangle, IconCircleCheck, IconDotsVertical, IconTrash } from import dayjs from "dayjs"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; -import { useCommonData } from "@/components/console/data-provider"; +import { useCommonData } from "@/components/console/common-data"; import { toast } from "sonner"; const PAGE_SIZE = 24; diff --git a/frontend/src/pages/playground-detail.tsx b/frontend/src/pages/playground-detail.tsx index 16579b5b..2454d62a 100644 --- a/frontend/src/pages/playground-detail.tsx +++ b/frontend/src/pages/playground-detail.tsx @@ -2,7 +2,7 @@ import Header from "@/components/welcome/header" import Footer from "@/components/welcome/footer"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import dayjs from "dayjs"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { apiRequest } from "@/utils/requestUtils"; import { ConstsPostKind, type DomainPlaygroundPost } from "@/api/Api"; @@ -167,9 +167,20 @@ const PlaygroundDetailPage = () => { const fileTree = buildTree(zipFiles); + const fetchPost = useCallback(async () => { + if (!postId) { + return + } + await apiRequest('v1PlaygroundPostsDetail', {}, [postId], (resp) => { + if (resp.code === 0) { + setPost(resp.data) + } + }) + }, [postId]) + useEffect(() => { fetchPost() - }, [postId]) + }, [fetchPost]) useEffect(() => { if (post?.task_post?.code) { @@ -196,17 +207,6 @@ const PlaygroundDetailPage = () => { } }, [zipFiles]) - const fetchPost = async () => { - if (!postId) { - return - } - await apiRequest('v1PlaygroundPostsDetail', {}, [postId], (resp) => { - if (resp.code === 0) { - setPost(resp.data) - } - }) - } - const loadZipFile = async (url: string) => { setLoadingZip(true) try { @@ -321,7 +321,11 @@ const PlaygroundDetailPage = () => {
{isExpanded && node.children.length > 0 && (
- {node.children.map(child => renderTreeNode(child, depth + 1))} + {node.children.map(child => ( + + {renderTreeNode(child, depth + 1)} + + ))}
)} @@ -435,7 +439,11 @@ const PlaygroundDetailPage = () => { ) : (
- {fileTree.map(node => renderTreeNode(node, 0))} + {fileTree.map(node => ( + + {renderTreeNode(node, 0)} + + ))}
)} } diff --git a/frontend/src/utils/requestUtils.ts b/frontend/src/utils/requestUtils.ts index f22477aa..fd388f57 100644 --- a/frontend/src/utils/requestUtils.ts +++ b/frontend/src/utils/requestUtils.ts @@ -1,15 +1,71 @@ import { Api } from '@/api/Api'; -import type { HttpResponse, RequestParams, WebResp } from '@/api/Api'; +import type { HttpResponse, WebResp } from '@/api/Api'; import { toast } from 'sonner'; -export const apiRequest = async ( - apiMethodName: keyof Api['api'], - params: RequestParams | Record = {}, - extrax: string[] = [], - onSuccess?: (data: any) => void, +type ApiInstance = Api; +type ApiMethods = ApiInstance['api']; +type ApiMethodName = keyof ApiMethods; +type ApiMethod = ApiMethods[M] extends (...args: infer A) => infer R ? (...args: A) => R : never; +type ApiMethodResponse = Awaited>>; +type ApiMethodData = ApiMethodResponse extends HttpResponse ? D : unknown; +type ApiMethodArgs = Parameters>; + +type StartsWith = + Args extends [...Prefix, ...unknown[]] ? Prefix : never; + +type ArgAt = + Args extends { [K in Index]: infer V } ? V : never; + +type ApiMethodParamArg = ArgAt, E['length']>; + +type WebRespWithData = Omit & { data?: D }; +type WithData = R extends { data?: unknown } ? Omit & { data?: D } : R; +/** + * 默认情况下(不显式指定 D),保持 OpenAPI 生成的 data 类型。 + * 只有显式指定 D 时,才覆写 resp.data 的类型。 + */ +type ApiMethodWebResp = ApiMethodData extends { code?: number } + ? ([D] extends [never] ? ApiMethodData : WithData, D>) + : ([D] extends [never] ? WebResp : WebRespWithData); + +const ensureWebResp = (apiMethodName: string, data: unknown): WebResp => { + const maybe = data as Partial | null | undefined; + if (maybe?.code === undefined) { + console.log(data); + throw new Error(`API 返回的数据格式不正确 (${apiMethodName})`); + } + return maybe as WebResp; +}; + +const handleUnauthorized = () => { + const loc = globalThis.location; + if (loc?.pathname?.includes('/console') || loc?.pathname?.includes('/manager')) { + loc.href = '/login'; + } +}; + +const getErrorMessage = (e: unknown) => { + const anyErr = e as { error?: { message?: string } } | null | undefined; + return anyErr?.error?.message ?? (e instanceof Error ? e.message : '网络错误'); +}; + +export function apiRequest( + apiMethodName: M, + params: ApiMethodParamArg, + extrax: E & StartsWith, E>, + onSuccess?: (resp: ApiMethodWebResp) => void, onError?: (error: Error) => void, - formData: Record | null = null -): Promise => { + formData?: Record | null, +): Promise | undefined>; + +export async function apiRequest( + apiMethodName: M, + params: ApiMethodParamArg, + extrax: E & StartsWith, E>, + onSuccess?: (resp: ApiMethodWebResp) => void, + onError?: (error: Error) => void, + formData: Record | null = null, +): Promise | undefined> { try { const api = new Api(); @@ -19,38 +75,35 @@ export const apiRequest = async ( } // 调用API方法 - let response: HttpResponse; + let response: HttpResponse; if (formData) { - response = await (api.api[apiMethodName] as any)(...extrax, params, formData); + response = await (api.api[apiMethodName] as (...args: unknown[]) => Promise>)(...extrax, params, formData); } else { - response = await (api.api[apiMethodName] as any)(...extrax, params); + response = await (api.api[apiMethodName] as (...args: unknown[]) => Promise>)(...extrax, params); } - if (response.data?.code === undefined) { - console.log(response); - throw new Error('API 返回的数据格式不正确'); - } + ensureWebResp(String(apiMethodName), response.data); - const resp = response.data as WebResp; + const resp = response.data as ApiMethodWebResp; if (onSuccess) { onSuccess(resp); } - return; + return resp; } catch (e) { if (e instanceof Response && e.status === 401){ - if (window.location.pathname.includes('/console') || window.location.pathname.includes('/manager')) { - window.location.href = '/login'; - } - return; + handleUnauthorized(); + return undefined; } if (onError) { onError(e as Error); } else { - toast.error(`${apiMethodName} 请求失败:${((e as any)?.error) ? (e as any).error.message : (e as Error)?.message || '网络错误'}`); + const msg = getErrorMessage(e); + toast.error(`${apiMethodName} 请求失败:${msg}`); } console.log(`${apiMethodName} 请求失败:`, e); + return undefined; } }; \ No newline at end of file