755 lines
24 KiB
TypeScript
755 lines
24 KiB
TypeScript
'use client'
|
||
|
||
import React from 'react'
|
||
|
||
import { Suspense } from 'react'
|
||
|
||
import { auth } from '@/auth'
|
||
import { clearChats } from '@/app/actions'
|
||
import { Sidebar } from '@/components/sidebar'
|
||
import { SidebarList } from '@/components/sidebar-list'
|
||
import { IconSeparator } from '@/components/ui/icons'
|
||
import { SidebarFooter } from '@/components/sidebar-footer'
|
||
import { ClearHistory } from '@/components/clear-history'
|
||
import { UserMenu, UserData } from '@/components/user-menu'
|
||
import { LoginButton } from '@/components/login-button'
|
||
import {
|
||
Tooltip,
|
||
TooltipContent,
|
||
TooltipTrigger
|
||
} from '@/components/ui/tooltip'
|
||
import { Badge } from '@/components/ui/badge'
|
||
import { ConnectButton } from '@/components/connect-button'
|
||
import { SettingsDropDown } from './settings-drop-down'
|
||
|
||
|
||
import { useLocalStorage } from '@/lib/hooks/use-local-storage'
|
||
import { Button } from './ui/button'
|
||
|
||
import Image from 'next/image';
|
||
import logoImage from '@/components/images/logo.png';
|
||
import { useRouter } from 'next/navigation'
|
||
|
||
import { Flex, Text } from '@radix-ui/themes';
|
||
import Link from 'next/link'
|
||
import { useTranslation } from 'react-i18next'
|
||
import { message } from 'antd'
|
||
import { LogoAI } from '@/components/chat'
|
||
|
||
import { useState, useRef, } from "react";
|
||
|
||
import { Trash2, CloudDownload } from "lucide-react";
|
||
|
||
import { useEffect } from "react";
|
||
|
||
import {
|
||
BadgeInfo,
|
||
Tags,
|
||
CalendarClock,
|
||
Building2
|
||
} from "lucide-react";
|
||
|
||
|
||
export function Header() {
|
||
|
||
const router = useRouter();
|
||
const { t } = useTranslation();
|
||
|
||
const [userData, setUserData] = useLocalStorage(
|
||
'UserData',
|
||
{
|
||
auth_token: "",
|
||
id: 1,
|
||
login_ip: "",
|
||
login_time: 0,
|
||
role: "",
|
||
user_name: "",
|
||
version: ""
|
||
} as UserData
|
||
)
|
||
|
||
const soonFunc = () => {
|
||
message.info(t("soon"))
|
||
}
|
||
|
||
return (
|
||
<header className="sticky top-0 z-50 flex shrink-0 items-center justify-between bg-background px-4 py-[1.5rem] w-[80%] mx-auto">
|
||
|
||
<a href="/">
|
||
<LogoAI className="flex text-center ml-0" />
|
||
</a>
|
||
|
||
<div className="flex items-center justify-between ">
|
||
<div className="flex items-center ">
|
||
{(!!userData && !!userData.user_name) ? (
|
||
<div className='flex text-[#1A1A1A]'>
|
||
<UserMenu user={userData} />
|
||
</div>
|
||
|
||
) : (
|
||
<div className='flex gap-4 grid-cols-2 text-[#1A1A1A]'>
|
||
<Button variant="ghost" className='text-base bg-[#f4f4f5] hover:bg-[#e5e7eb]'>
|
||
<Link href="/#subscribe-target"
|
||
>
|
||
{t('subscribe.subscribe')}
|
||
</Link>
|
||
</Button>
|
||
|
||
|
||
<LoginButton
|
||
variant="ghost"
|
||
// variant="link"
|
||
showGithubIcon={true}
|
||
text={t('login')}
|
||
className="-ml-2 text-base bg-[#f4f4f5] hover:bg-[#e5e7eb]"
|
||
/>
|
||
</div>
|
||
|
||
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
</header>
|
||
)
|
||
}
|
||
|
||
import { getRuntimeEnv } from "@/lib/ipconfig";
|
||
|
||
export async function getWsBase() {
|
||
let ip = await getRuntimeEnv("SUPABASE_URL");
|
||
if (!ip) throw new Error("SUPABASE_URL 获取失败,无法构建 wsBase");
|
||
|
||
let wsProtocol = "ws";
|
||
|
||
if (typeof window !== "undefined") {
|
||
// ✅ 客户端环境(浏览器执行)
|
||
if (window.location.protocol === "https:") {
|
||
wsProtocol = "wss";
|
||
ip = window.location.hostname; // ✅ 用 hostname 避免 HTTPS + IP 的 TLS 报错
|
||
}
|
||
} else {
|
||
// ✅ SSR 环境,延迟导入 headers(不能放顶层)
|
||
const { headers } = await import("next/headers");
|
||
const hdrs = headers();
|
||
|
||
const forwardedProto = hdrs.get("x-forwarded-proto");
|
||
const hostHeader = hdrs.get("host");
|
||
|
||
if (forwardedProto === "https") {
|
||
wsProtocol = "wss";
|
||
}
|
||
|
||
if (hostHeader) {
|
||
// ✅ host 可能带端口,需处理
|
||
ip = hostHeader.includes(":") ? hostHeader.split(":")[0] : hostHeader;
|
||
}
|
||
}
|
||
|
||
const finalUrl = `${wsProtocol}://${ip}/api/v1/deploy/ws`;
|
||
console.log("✅ [WebSocket] Final URL:", finalUrl);
|
||
return finalUrl;
|
||
}
|
||
|
||
|
||
export function DetailPageHeader({ data }: { data: any }) {
|
||
const [loading, setLoading] = useState(false);
|
||
const [statusText, setStatusText] = useState(data?.statusText || "加载中...");
|
||
const [progress, setProgress] = useState(data?.progress || "0%");
|
||
const [progressBarColor, setProgressBarColor] = useState("bg-blue-500"); // ✅ 默认蓝色
|
||
const [showProgressBar, setShowProgressBar] = useState(false);
|
||
const [showDelete, setShowDelete] = useState(false);
|
||
const [hasWSConnected, setHasWSConnected] = useState(false);
|
||
const [statusLoaded, setStatusLoaded] = useState(false);
|
||
const [canDeploy, setCanDeploy] = useState(true);
|
||
|
||
const [currentStatus, setCurrentStatus] = useState(""); // 当前部署状态:running / stopped
|
||
const [switchLoading, setSwitchLoading] = useState(false); // 控制按钮 loading 状态
|
||
|
||
const [downloadPercent, setDownloadPercent] = useState("0"); // 下载百分比(0 ~ 100)
|
||
const [showDownloadBar, setShowDownloadBar] = useState(false);
|
||
|
||
const [newVersionLoading, setNewVersionLoading] = useState(false);
|
||
|
||
const socketRef = useRef<WebSocket | null>(null);
|
||
|
||
const { t } = useTranslation();
|
||
|
||
console.log("...........model_parameter =", data?.model_parameter);
|
||
|
||
// const hasNonEmptyExtraData = data?.extra_data && typeof data.extra_data === 'object' && !Array.isArray(data.extra_data) && Object.keys(data.extra_data).length > 0 data?.model_parameter !== 0 &&
|
||
// data?.model_parameter !== "";
|
||
const hasNonEmptyExtraData =
|
||
data?.extra_data &&
|
||
typeof data.extra_data === 'object' &&
|
||
!Array.isArray(data.extra_data) &&
|
||
Object.keys(data.extra_data).length > 0 &&
|
||
data?.model_parameter !== 0 &&
|
||
data?.model_parameter !== "";
|
||
|
||
|
||
const initWebSocket = async (userName: string, id: number) => {
|
||
if (socketRef.current) socketRef.current.close();
|
||
|
||
|
||
//const wsBase = process.env.NEXT_PUBLIC_CLIENT_BASE_WS;
|
||
const wsBase = await getWsBase();
|
||
const socket = new WebSocket(`${wsBase}/status/${userName}/${id}`);
|
||
socketRef.current = socket;
|
||
|
||
socket.onopen = () => console.log("WebSocket 已连接");
|
||
|
||
socket.onmessage = (event) => {
|
||
const msg = event.data;
|
||
console.log("收到进度信息:", msg);
|
||
setStatusText(msg);
|
||
|
||
const percentMatch = msg.match(/(\d+)%/);
|
||
if (percentMatch && percentMatch[1]) {
|
||
setProgress(percentMatch[1] + "%");
|
||
|
||
if (percentMatch[1] === "100") {
|
||
console.log("部署完成 ✅,重新拉取状态!");
|
||
fetchDeployStatus();
|
||
}
|
||
}
|
||
|
||
|
||
setProgressBarColor("bg-blue-500"); // 保持默认颜色
|
||
};
|
||
|
||
socket.onerror = (err) => {
|
||
console.error("WebSocket 出错:", err);
|
||
setStatusText(t("deploy.ws_error"));
|
||
};
|
||
|
||
socket.onclose = () => {
|
||
console.log("🔌 WebSocket 已关闭");
|
||
setHasWSConnected(false);
|
||
socketRef.current = null;
|
||
};
|
||
|
||
setHasWSConnected(true);
|
||
};
|
||
|
||
const handleClick = async (source: "icon" | "info") => {
|
||
setLoading(true);
|
||
setStatusText(source === "icon" ? "正在处理图标操作..." : "正在处理信息操作...");
|
||
setShowProgressBar(true)
|
||
|
||
try {
|
||
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
|
||
if (!userData?.user_name) {
|
||
setStatusText("未登录,跳转中...");
|
||
window.location.href = "/auth/sign-in/";
|
||
return;
|
||
}
|
||
|
||
const userName = userData.user_name;
|
||
const id = data?.id;
|
||
if (!id) {
|
||
setStatusText("缺少组件 ID");
|
||
return;
|
||
}
|
||
|
||
const res = await fetch("/api/v1/deploy/deploy", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ id, user_name: userName }),
|
||
});
|
||
|
||
const json = await res.json();
|
||
if (json?.header?.code === 0) {
|
||
setStatusText("部署已启动,监听中...");
|
||
setProgressBarColor("bg-blue-500");
|
||
initWebSocket(userName, id);
|
||
} else {
|
||
setStatusText(json.header.message || "操作失败(后端)");
|
||
}
|
||
} catch (err) {
|
||
console.error("请求出错:", err);
|
||
setStatusText("请求失败");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleDelete = async () => {
|
||
if (loading) return;
|
||
|
||
const confirmed = window.confirm(t("deploy.confirm_delete"));
|
||
if (!confirmed) return;
|
||
|
||
setLoading(true);
|
||
setStatusText(t("deploy.deleting"));
|
||
setProgressBarColor("bg-gray-400");
|
||
|
||
try {
|
||
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
|
||
const userName = userData?.user_name;
|
||
const id = data?.id;
|
||
|
||
if (!userName || !id) {
|
||
setStatusText(t("deploy.missing_user_or_id"));
|
||
return;
|
||
}
|
||
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), 60_000); // ⏰ 60秒超时
|
||
|
||
const res = await fetch("/api/v1/deploy/delete", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
signal: controller.signal,
|
||
body: JSON.stringify({ user_name: userName, n_id: id }),
|
||
});
|
||
|
||
clearTimeout(timeoutId); // ✅ 成功返回前清除 timeout
|
||
|
||
if (!res.ok) {
|
||
throw new Error(`HTTP 请求失败: ${res.status}`);
|
||
}
|
||
|
||
const json = await res.json();
|
||
if (json?.header?.code === 0) {
|
||
setStatusText(t("deploy.deletion_success"));
|
||
setProgress("0%");
|
||
setShowDelete(false); // 删除成功后隐藏按钮
|
||
await fetchDeployStatus(); // ✅ 删除后刷新真实状态
|
||
} else {
|
||
setStatusText(json?.header?.message || "删除失败(后端返回错误)");
|
||
}
|
||
} catch (err) {
|
||
if ((err as any).name === "AbortError") {
|
||
setStatusText(t("deploy.timeout"));
|
||
} else {
|
||
console.error("删除请求出错:", err);
|
||
setStatusText(t("deploy.deletion_failed_network"));
|
||
}
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const fetchDeployStatus = async () => {
|
||
try {
|
||
const result = await fetch("/api/v1/deploy/status", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ id: data?.id }),
|
||
}).then((res) => res.json());
|
||
|
||
console.log("====> deploy status result:", result);
|
||
setStatusLoaded(true);
|
||
|
||
if (!result) {
|
||
setStatusText(t("deploy.empty_response"));
|
||
setShowDelete(false);
|
||
return;
|
||
}
|
||
|
||
const code = result?.header?.code;
|
||
const status = result?.data?.data?.status;
|
||
setCurrentStatus(status || ""); //...............................................
|
||
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
|
||
const userName = userData?.user_name;
|
||
const id = data?.id;
|
||
|
||
console.log("🟡 状态码 code =", code, "状态 status =", status);
|
||
|
||
if (code === 1006) {
|
||
setStatusText(t("deploy.not_deployed"));
|
||
setShowDelete(false);
|
||
setProgress("0%");
|
||
setShowProgressBar(false);
|
||
setCanDeploy(true); // ✅ 允许部署
|
||
return;
|
||
}
|
||
|
||
if (status === "deploying" && userName && id && !hasWSConnected) {
|
||
setStatusText(t("deploy.connecting"));
|
||
initWebSocket(userName, id);
|
||
setShowProgressBar(true);
|
||
setCanDeploy(false); // ✅ 正在部署中,禁止点击
|
||
}
|
||
|
||
if (status === "running" || status === "stopped") {
|
||
console.log("✅ 允许删除(status =", status, ")");
|
||
setShowDelete(true);
|
||
setProgress("100%");
|
||
setShowProgressBar(true);
|
||
setCanDeploy(false); // ✅ 已部署/已停止,不允许再次 deploy
|
||
|
||
if (status === "running") {
|
||
setStatusText(t("deploy.running"));
|
||
setProgressBarColor("bg-green-500");
|
||
} else if (status === "stopped") {
|
||
setStatusText(t("deploy.stopped"));
|
||
setProgressBarColor("bg-gray-400");
|
||
}
|
||
} else {
|
||
console.log("❌ 不允许删除(status =", status, ")");
|
||
setShowDelete(false);
|
||
setProgressBarColor("bg-blue-500");
|
||
}
|
||
} catch (err) {
|
||
console.error("获取状态失败:", err);
|
||
setStatusText(t("deploy.fetch_failed"));
|
||
setShowDelete(false);
|
||
}
|
||
};
|
||
|
||
|
||
const handleSwitchStatus = async () => {
|
||
if (!data?.id) return;
|
||
|
||
setSwitchLoading(true);
|
||
const userData = JSON.parse(localStorage.getItem("UserData") || "null");
|
||
const userName = userData?.user_name;
|
||
|
||
const id = data?.id;
|
||
|
||
if (!userName || !id) {
|
||
setStatusText("缺少组件 ID 或用户信息");
|
||
return;
|
||
}
|
||
|
||
if (!userName) {
|
||
setStatusText("未登录,跳转中...");
|
||
window.location.href = "/auth/sign-in/";
|
||
return;
|
||
}
|
||
|
||
const endpoint = currentStatus === "running" ? "/api/v1/deploy/stop" : "/api/v1/deploy/start";
|
||
|
||
try {
|
||
const res = await fetch(endpoint, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ user_name: userName, n_id: id }),
|
||
});
|
||
|
||
const json = await res.json();
|
||
if (json?.header?.code === 0) {
|
||
setStatusText(currentStatus === "running" ? "已停止" : "已启动");
|
||
fetchDeployStatus(); // ✅ 状态切换成功后刷新
|
||
} else {
|
||
setStatusText(json?.header?.message || "操作失败(后端)");
|
||
}
|
||
} catch (err) {
|
||
console.error("切换请求失败:", err);
|
||
setStatusText("网络异常");
|
||
} finally {
|
||
setSwitchLoading(false);
|
||
}
|
||
};
|
||
|
||
|
||
const handleDownloadNewVersion = async () => {
|
||
const download_url = data?.extra_data?.download_url;
|
||
const callback_url = data?.extra_data?.callback_url;
|
||
const digest = data?.extra_data?.digest;
|
||
const version = data?.extra_data?.version;
|
||
const size = data?.extra_data?.size;
|
||
const date1 = data?.extra_data?.date;
|
||
const id = data?.org_id;
|
||
|
||
if (!download_url || !size || !id) {
|
||
message.warning("缺少必要的下载参数");
|
||
return;
|
||
}
|
||
|
||
setNewVersionLoading(true);
|
||
setShowDownloadBar(true); // 复用原进度条
|
||
|
||
try {
|
||
const res = await fetch("https://ai.szaiai.com/api/v1/cloud/newversion", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
date: String(date1),
|
||
url: download_url,
|
||
callback_url,
|
||
digest,
|
||
version,
|
||
size,
|
||
id,
|
||
}),
|
||
});
|
||
|
||
const result = await res.json();
|
||
|
||
if (result?.header?.code === 0) {
|
||
const percent = (parseFloat(result?.data?.percent ?? "0") * 100).toFixed(1);
|
||
setDownloadPercent(percent);
|
||
message.success(`📦 更新进度:${percent}%`);
|
||
if (percent === "100.0") message.success("🎉 新版本下载完成!");
|
||
} else {
|
||
message.warning(`❌ 更新失败:${result?.header?.message || "未知错误"}`);
|
||
}
|
||
} catch (err) {
|
||
console.error(err);
|
||
message.error("请求出错,无法更新");
|
||
} finally {
|
||
setNewVersionLoading(false);
|
||
}
|
||
};
|
||
|
||
|
||
useEffect(() => {
|
||
fetchDeployStatus();
|
||
}, [data?.id]);
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
if (socketRef.current) {
|
||
socketRef.current.close();
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
const isImagePath =
|
||
typeof data?.icon === "string" &&
|
||
(data.icon.startsWith("http") || data.icon.startsWith("/"));
|
||
|
||
const resolvedIconSrc =
|
||
isImagePath && !data.icon.startsWith("http")
|
||
? process.env.NEXT_PUBLIC_CLIENT_IMAGE_URL + data.icon
|
||
: data.icon;
|
||
|
||
return (
|
||
<div className="sticky top-0 z-30 bg-white">
|
||
<div className="mt-4 mb-1 px-6 lg:px-8 w-11/12 lg:w-2/3 xl:w-3/5 mx-auto">
|
||
<div className="flex items-start space-x-6">
|
||
<button
|
||
// className="group flex items-center justify-center w-24 h-24 md:w-32 md:h-32 border transition"
|
||
className={`group flex items-center justify-center w-24 h-24 md:w-32 md:h-32 border transition ${(!data?.model_parameter) ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||
onClick={() => handleClick("icon")}
|
||
disabled={loading || !canDeploy || !data?.model_parameter}
|
||
>
|
||
{isImagePath ? (
|
||
<img
|
||
src={resolvedIconSrc}
|
||
alt="icon"
|
||
className="w-full h-full object-contain transition-all duration-200 group-hover:border-2 group-hover:border-blue-500 group-active:border-2 group-active:border-green-500 group-hover:scale-105 group-active:scale-95"
|
||
/>
|
||
) : (
|
||
data?.icon || "Deploy"
|
||
)}
|
||
</button>
|
||
|
||
<div className="flex justify-between flex-1 items-end">
|
||
<div className="text-sm leading-7 space-y-1.5">
|
||
<div className="flex items-center gap-2">
|
||
<BadgeInfo size={16} /> {data?.name || "未命名组件"}
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Tags size={16} /> {data?.category || "未知"}
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<CalendarClock size={16} /> {data?.updated_at || "未提供"}
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Building2 size={16} /> {data?.company || "未知公司"}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-[5px] self-end">
|
||
{hasNonEmptyExtraData && (
|
||
// <button
|
||
// onClick={() => {
|
||
// console.log("🟢 点击了更新按钮,extra_data = ", data.extra_data);
|
||
// // TODO: 后续处理逻辑写在这里
|
||
// }}
|
||
// className="hover:text-gray-700 transition self-end text-sm border border-gray-300 rounded px-2 py-1 bg-white"
|
||
// title="更新"
|
||
// >
|
||
// 🔄 更新
|
||
// </button>
|
||
<div className="flex flex-col items-center gap-[2px]">
|
||
<button
|
||
onClick={handleDownloadNewVersion}
|
||
disabled={newVersionLoading || loading}
|
||
className="hover:text-gray-700 transition self-end text-sm border border-gray-300 rounded px-2 py-1 bg-white mt-1"
|
||
title="下载并安装新版本"
|
||
>
|
||
{newVersionLoading ? "处理中..." : "⬇️ 更新"}
|
||
</button>
|
||
|
||
{showDownloadBar && (
|
||
<div className="w-full h-[4px] bg-gray-200 rounded overflow-hidden mt-1">
|
||
<div
|
||
className="h-full bg-blue-500 transition-all duration-300"
|
||
style={{ width: `${downloadPercent}%` }}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{statusLoaded && (currentStatus === "running" || currentStatus === "stopped") && (
|
||
<button
|
||
onClick={handleSwitchStatus}
|
||
disabled={switchLoading || loading}
|
||
className="hover:text-gray-700 transition self-end text-sm border border-gray-300 rounded px-2 py-1 bg-white"
|
||
title={currentStatus === "running" ? "停止运行" : "启动运行"}
|
||
>
|
||
{switchLoading
|
||
? "处理中..."
|
||
: currentStatus === "running"
|
||
? "⏹ 停止"
|
||
: "▶️ 启动"}
|
||
</button>
|
||
)}
|
||
|
||
{statusLoaded && showDelete && (
|
||
<button
|
||
onClick={handleDelete}
|
||
className="hover:text-gray-700 transition self-end"
|
||
disabled={loading}
|
||
title="删除"
|
||
>
|
||
<Trash2 size={20} />
|
||
</button>
|
||
)}
|
||
|
||
{!data?.model_parameter && (
|
||
<div className="flex flex-col items-center gap-[2px]">
|
||
<button
|
||
onClick={async () => {
|
||
try {
|
||
const download_url = data?.extra_data?.download_url;
|
||
const callback_url = data?.extra_data?.callback_url;
|
||
const digest = data?.extra_data?.digest;
|
||
const version = data?.extra_data?.version;
|
||
const size = data?.extra_data?.size;
|
||
const date1 = data?.extra_data?.date;
|
||
const id = data?.org_id;
|
||
|
||
|
||
if (!download_url || !size || !id) {
|
||
message.warning("缺少必要的下载参数");
|
||
return;
|
||
}
|
||
|
||
console.log("📥 调用下载接口...", { url: download_url, size, id });
|
||
|
||
const res = await fetch("https://ai.szaiai.com/api/v1/cloud/download", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
date: String(date1),
|
||
url: download_url,
|
||
callback_url: callback_url,
|
||
digest: digest,
|
||
version: version,
|
||
size: size,
|
||
id: id,
|
||
}),
|
||
});
|
||
|
||
const result = await res.json();
|
||
console.log("✅ 下载接口返回:", result);
|
||
|
||
if (result?.header?.code === 0) {
|
||
const percentStr = result?.data?.percent ?? "0";
|
||
const percentNum = parseFloat(percentStr);
|
||
const percent = (percentNum * 100).toFixed(1);
|
||
|
||
setDownloadPercent(percent);
|
||
setShowDownloadBar(true);
|
||
|
||
message.success(`📦 下载进度:${percent}%`);
|
||
} else {
|
||
message.warning(`❌ 下载失败:${result?.header?.message || "未知错误"}`);
|
||
}
|
||
} catch (err) {
|
||
console.error("❌ 下载请求异常:", err);
|
||
message.error("请求出错,无法下载");
|
||
}
|
||
}}
|
||
className="hover:text-gray-700 transition self-end"
|
||
title="下载模型文件"
|
||
>
|
||
<CloudDownload size={20} />
|
||
</button>
|
||
|
||
|
||
{showDownloadBar && (
|
||
<div className="w-[20px] h-[4px] bg-gray-200 rounded overflow-hidden">
|
||
<div
|
||
className="h-full bg-blue-500 transition-all duration-300"
|
||
style={{ width: `${downloadPercent}%` }}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
{showProgressBar && (progress !== "0%" || statusText) && (
|
||
<div className="relative w-full bg-gray-200 h-6 rounded overflow-hidden">
|
||
{/* 蓝色进度条 */}
|
||
<div
|
||
className={`${progressBarColor} h-full transition-all duration-300`}
|
||
style={{ width: typeof progress === "string" ? progress : `${progress}%` }}
|
||
/>
|
||
|
||
{/* 浮动在中间的文字层 */}
|
||
<div className="absolute inset-0 flex items-center justify-center text-sm font-medium text-white">
|
||
{loading ? t("deploy.processing") : statusText}
|
||
</div>
|
||
</div>
|
||
|
||
)}
|
||
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function NavBack() {
|
||
|
||
|
||
const router = useRouter();
|
||
const { t } = useTranslation();
|
||
|
||
// const query = router.query; // 假设页面 URL 是 /mypage?id=123
|
||
|
||
console.log("query", router)
|
||
return (
|
||
<nav className="fixed top-[1rem] container flex items-center justify-between " >
|
||
<Link href="/">
|
||
{t("home")}
|
||
</Link>
|
||
</nav>
|
||
)
|
||
}
|
||
|
||
export function TimeP({
|
||
date,
|
||
}: {
|
||
date: string
|
||
}) {
|
||
|
||
|
||
const router = useRouter();
|
||
const { t } = useTranslation();
|
||
|
||
|
||
return (
|
||
<p className=" text-left text-[1rem] text-[#808080]">{date} {t("JellyAI")}</p>
|
||
)
|
||
}
|
||
|
||
|
||
|
||
|