feat: dash command

This commit is contained in:
hamster1963 2025-01-24 01:08:25 +08:00
parent 733767a363
commit c5f81d70d4
9 changed files with 230 additions and 127 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -36,7 +36,7 @@
"country-flag-icons": "^1.5.14", "country-flag-icons": "^1.5.14",
"d3-geo": "^3.1.1", "d3-geo": "^3.1.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"framer-motion": "^12.0.1", "framer-motion": "^12.0.3",
"i18n-iso-countries": "^7.13.0", "i18n-iso-countries": "^7.13.0",
"i18next": "^24.2.1", "i18next": "^24.2.1",
"lucide-react": "^0.460.0", "lucide-react": "^0.460.0",
@ -53,8 +53,8 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@types/node": "^22.10.8", "@types/node": "^22.10.9",
"@types/react": "^19.0.7", "@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3", "@types/react-dom": "^19.0.3",
"@vitejs/plugin-react-swc": "^3.7.2", "@vitejs/plugin-react-swc": "^3.7.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",

View File

@ -15,6 +15,7 @@ import ErrorPage from "./pages/ErrorPage"
import NotFound from "./pages/NotFound" import NotFound from "./pages/NotFound"
import Server from "./pages/Server" import Server from "./pages/Server"
import ServerDetail from "./pages/ServerDetail" import ServerDetail from "./pages/ServerDetail"
import { DashCommand } from "./components/DashCommand"
const App: React.FC = () => { const App: React.FC = () => {
const { data: settingData, error } = useQuery({ const { data: settingData, error } = useQuery({
@ -90,6 +91,7 @@ const App: React.FC = () => {
<main className="flex z-20 min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 p-4 md:p-10 md:pt-8"> <main className="flex z-20 min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 p-4 md:p-10 md:pt-8">
<RefreshToast /> <RefreshToast />
<Header /> <Header />
<DashCommand />
<Routes> <Routes>
<Route path="/" element={<Server />} /> <Route path="/" element={<Server />} />
<Route path="/server/:id" element={<ServerDetail />} /> <Route path="/server/:id" element={<ServerDetail />} />

View File

@ -0,0 +1,114 @@
"use client"
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command"
import { useTheme } from "@/hooks/use-theme"
import { useWebSocketContext } from "@/hooks/use-websocket-context"
import { formatNezhaInfo } from "@/lib/utils"
import { NezhaWebsocketResponse } from "@/types/nezha-api"
import { Home, Moon, Sun, SunMoon } from "lucide-react"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
export function DashCommand() {
const [open, setOpen] = useState(false)
const [search, setSearch] = useState("")
const navigate = useNavigate()
const { t } = useTranslation()
const { setTheme } = useTheme()
const { lastMessage, connected } = useWebSocketContext()
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down)
}, [])
if (!connected || !nezhaWsData) return null
const shortcuts = [
{
keywords: ["home", "homepage"],
icon: <Home />,
label: t("Home"),
action: () => navigate("/"),
},
{
keywords: ["light", "theme", "lightmode"],
icon: <Sun />,
label: t("ToggleLightMode"),
action: () => setTheme("light"),
},
{
keywords: ["dark", "theme", "darkmode"],
icon: <Moon />,
label: t("ToggleDarkMode"),
action: () => setTheme("dark"),
},
{
keywords: ["system", "theme", "systemmode"],
icon: <SunMoon />,
label: t("ToggleSystemMode"),
action: () => setTheme("system"),
},
].map((item) => ({
...item,
value: `${item.keywords.join(" ")} ${item.label}`,
}))
return (
<>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder={t("TypeCommand")} value={search} onValueChange={setSearch} />
<CommandList>
<CommandEmpty>{t("NoResults")}</CommandEmpty>
<CommandGroup heading={t("Servers")}>
{nezhaWsData.servers.map((server) => (
<CommandItem
key={server.id}
value={server.name}
onSelect={() => {
navigate(`/server/${server.id}`)
setOpen(false)
}}
>
{formatNezhaInfo(nezhaWsData.now, server).online ? (
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center" />
) : (
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" />
)}
<span>{server.name}</span>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
<CommandGroup heading={t("Shortcuts")}>
{shortcuts.map((item) => (
<CommandItem
key={item.label}
value={item.value}
onSelect={() => {
item.action()
setOpen(false)
}}
>
{item.icon}
<span>{item.label}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
</>
)
}

View File

@ -24,17 +24,24 @@ const Footer: React.FC = () => {
</a> </a>
<p>{settingData?.data?.version || ""}</p> <p>{settingData?.data?.version || ""}</p>
</div> </div>
<p className="server-footer-theme"> <div className="server-footer-theme flex flex-col items-center sm:items-end">
{t("footer.themeBy")} <p className="mt-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
<a href={"https://github.com/hamster1963/nezha-dash"} target="_blank"> <kbd className="pointer-events-none mx-1 inline-flex h-4 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
nezha-dash <span className="text-xs"></span>K
</a> </kbd>
{import.meta.env.VITE_GIT_HASH && ( </p>
<a href={"https://github.com/hamster1963/nezha-dash-v1/commit/" + import.meta.env.VITE_GIT_HASH} className="ml-1"> <section>
({import.meta.env.VITE_GIT_HASH}) {t("footer.themeBy")}
<a href={"https://github.com/hamster1963/nezha-dash"} target="_blank">
nezha-dash
</a> </a>
)} {import.meta.env.VITE_GIT_HASH && (
</p> <a href={"https://github.com/hamster1963/nezha-dash-v1/commit/" + import.meta.env.VITE_GIT_HASH} className="ml-1">
({import.meta.env.VITE_GIT_HASH})
</a>
)}
</section>
</div>
</section> </section>
</section> </section>
</footer> </footer>

View File

@ -1,31 +1,29 @@
import * as React from "react" "use client"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { cn } from "@/lib/utils"
import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk" import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react" import { Search } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils" const Command = React.forwardRef<React.ElementRef<typeof CommandPrimitive>, React.ComponentPropsWithoutRef<typeof CommandPrimitive>>(
import { Dialog, DialogContent } from "@/components/ui/dialog" ({ className, ...props }, ref) => (
<CommandPrimitive
const Command = React.forwardRef< ref={ref}
React.ElementRef<typeof CommandPrimitive>, className={cn("flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", className)}
React.ComponentPropsWithoutRef<typeof CommandPrimitive> {...props}
>(({ className, ...props }, ref) => ( />
<CommandPrimitive ),
ref={ref} )
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, ...props }: DialogProps) => { const CommandDialog = ({ children, ...props }: DialogProps) => {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogTitle />
<DialogContent className="overflow-hidden p-0 shadow-lg"> <DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4">
{children} {children}
</Command> </Command>
</DialogContent> </DialogContent>
@ -33,119 +31,77 @@ const CommandDialog = ({ children, ...props }: DialogProps) => {
) )
} }
const CommandInput = React.forwardRef< const CommandInput = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Input>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>>(
React.ElementRef<typeof CommandPrimitive.Input>, ({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> <div className="flex items-center border-b bg-stone-100 dark:bg-stone-900 px-3" cmdk-input-wrapper="">
>(({ className, ...props }, ref) => ( <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<div className="flex items-center border-b px-3" cmdk-input-wrapper=""> <CommandPrimitive.Input
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> ref={ref}
<CommandPrimitive.Input className={cn(
ref={ref} "flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className={cn( className,
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50", )}
className {...props}
)} />
{...props} </div>
/> ),
</div> )
))
CommandInput.displayName = CommandPrimitive.Input.displayName CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef< const CommandList = React.forwardRef<React.ElementRef<typeof CommandPrimitive.List>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>>(
React.ElementRef<typeof CommandPrimitive.List>, ({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> <CommandPrimitive.List ref={ref} className={cn("max-h-[300px] mb-1 overflow-y-auto overflow-x-hidden", className)} {...props} />
>(({ className, ...props }, ref) => ( ),
<CommandPrimitive.List )
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef< const CommandEmpty = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Empty>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>>(
React.ElementRef<typeof CommandPrimitive.Empty>, (props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> )
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef< const CommandGroup = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Group>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>>(
React.ElementRef<typeof CommandPrimitive.Group>, ({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> <CommandPrimitive.Group
>(({ className, ...props }, ref) => ( ref={ref}
<CommandPrimitive.Group className={cn(
ref={ref} "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className={cn( className,
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground", )}
className {...props}
)} />
{...props} ),
/> )
))
CommandGroup.displayName = CommandPrimitive.Group.displayName CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef< const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>, React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => <CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />)
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef< const CommandItem = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Item>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>>(
React.ElementRef<typeof CommandPrimitive.Item>, ({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> <CommandPrimitive.Item
>(({ className, ...props }, ref) => ( ref={ref}
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn( className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground", "relative flex cursor-default gap-2 select-none items-center rounded-[8px] px-2 py-1.5 text-xs outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-stone-100 dark:data-[selected='true']:bg-stone-900 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className className,
)} )}
{...props} {...props}
/> />
) ),
)
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
} }
CommandShortcut.displayName = "CommandShortcut" CommandShortcut.displayName = "CommandShortcut"
export { export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator }
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@ -116,5 +116,13 @@
"price": "Price", "price": "Price",
"free": "Free", "free": "Free",
"usage-baseed": "Usage-based" "usage-baseed": "Usage-based"
} },
"TypeCommand": "Type a command or search...",
"NoResults": "No results found.",
"Servers": "Servers",
"Shortcuts": "Shortcuts",
"ToggleLightMode": "Toggle Light Mode",
"ToggleDarkMode": "Toggle Dark Mode",
"ToggleSystemMode": "Toggle System Mode",
"Home": "Home"
} }

View File

@ -116,5 +116,13 @@
"price": "价格", "price": "价格",
"free": "免费", "free": "免费",
"usage-baseed": "按量计费" "usage-baseed": "按量计费"
} },
"TypeCommand": "输入命令或搜索",
"NoResults": "结果为空",
"Servers": "服务器",
"Shortcuts": "快捷键",
"ToggleLightMode": "切换亮色模式",
"ToggleDarkMode": "切换暗色模式",
"ToggleSystemMode": "切换系统模式",
"Home": "首页"
} }

View File

@ -112,5 +112,13 @@
"price": "價格", "price": "價格",
"free": "免費", "free": "免費",
"usage-baseed": "按量計費" "usage-baseed": "按量計費"
} },
"TypeCommand": "輸入命令或搜尋",
"NoResults": "沒有結果",
"Servers": "伺服器",
"Shortcuts": "快捷鍵",
"ToggleLightMode": "切換亮色模式",
"ToggleDarkMode": "切換暗色模式",
"ToggleSystemMode": "切換系統模式",
"Home": "首頁"
} }