This commit is contained in:
2025-12-01 02:19:44 +08:00
parent 84775dd878
commit a51a366f00
21 changed files with 2628 additions and 229 deletions

20
src/apps/auth.tsx Normal file
View File

@@ -0,0 +1,20 @@
import { queryLogin } from "@/modules/query"
import { useState, useEffect } from "react";
export const AuthProvider = (props: { children: React.ReactNode }) => {
const [isLogin, setIsLogin] = useState<boolean>(false);
const init = async () => {
const token = await queryLogin.checkLocalToken();
if (token) {
console.log('User is logged in');
setIsLogin(true);
}
}
useEffect(() => {
init();
}, []);
return (
<div>
{isLogin ? props.children : <div>Please log in to access this application.</div>}
</div>
)
}

70
src/apps/home/index.tsx Normal file
View File

@@ -0,0 +1,70 @@
import { app } from '../ai';
import { Sender, XProvider } from '@ant-design/x';
import { useEffect, useRef } from 'react';
import { Nav } from '../nav';
const useFocus = () => {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// Focus the input element inside Sender component
const focusInput = () => {
if (inputRef.current) {
const input = inputRef.current.querySelector('input, textarea') as HTMLInputElement | HTMLTextAreaElement;
if (input) {
input.focus();
}
}
};
// Focus on mount
focusInput();
// Also focus after a short delay to ensure everything is rendered
const timeoutId = setTimeout(focusInput, 100);
return () => clearTimeout(timeoutId);
}, []);
return inputRef;
}
export const App = () => {
const inputRef = useFocus();
return <div className='container mx-auto p-4'>
<div className='fixed bottom-8 w-1/2 justify-self-center' ref={inputRef}>
<Sender allowSpeech onSubmit={() => {
console.log('Submitted');
}} />
</div>
</div >;
}
export const AppProvider = () => {
return <XProvider
theme={{
token: {
colorPrimary: '#000000',
colorBgBase: '#ffffff',
colorTextBase: '#000000',
colorBorder: '#d9d9d9',
colorBgContainer: '#ffffff',
colorBgElevated: '#ffffff',
colorBgLayout: '#ffffff',
colorText: '#000000',
colorTextSecondary: '#666666',
colorTextTertiary: '#999999',
}
}}
>
<Nav />
<App />
</XProvider>;
}

80
src/apps/nav/index.tsx Normal file
View File

@@ -0,0 +1,80 @@
import { useEffect, useState } from "react";
import { useUserStore } from "./store.ts";
import { useShallow } from 'zustand/shallow';
import '@kevisual/kv-login';
import { useContextKey } from "@kevisual/context";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "@/components/ui/dialog";
export const LoginComponent = ({ onLoginSuccess }: { onLoginSuccess: () => void }) => {
useEffect(() => {
// 监听登录成功事件
const handleLoginSuccess = () => {
console.log('监听到登录成功事件,关闭弹窗');
onLoginSuccess();
};
const loginEmitter = useContextKey('login-emitter')
console.log('KvLogin Types:', loginEmitter);
loginEmitter.on('login-success', handleLoginSuccess);
// 清理监听器
return () => {
loginEmitter.off('login-success', handleLoginSuccess);
};
}, [onLoginSuccess]);
// @ts-ignore
return (<kv-login><div id="weixinLogin"></div></kv-login>)
}
export const Nav = () => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const store = useUserStore(useShallow((state) => ({
user: state.user,
setUser: state.setUser,
clearUser: state.clearUser,
queryUser: state.queryUser
})));
useEffect(() => {
store.queryUser();
}, []);
const handleLoginSuccess = () => {
// 关闭弹窗
setIsDialogOpen(false);
// 重新查询用户信息
};
return <header>
<nav className="bg-black p-4 text-white flex justify-between">
<div className="text-lg font-bold"></div>
<div>
{store.user ? (
<div className="flex items-center space-x-4">
{store.user.avatar && <img src={store.user.avatar} alt="Avatar" className="w-8 h-8 rounded-full" />}
<span>{store.user.username}</span>
<button
onClick={() => store.clearUser()}
className="bg-gray-700 text-white px-3 py-1 rounded hover:bg-gray-600 transition-colors"
>
退
</button>
</div>
) : (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<button className="bg-gray-700 text-white px-3 py-1 rounded hover:bg-gray-600 transition-colors">
</button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="text-black text-xl font-bold border-b border-black pb-3"></DialogHeader>
<LoginComponent onLoginSuccess={handleLoginSuccess} />
</DialogContent>
</Dialog>
)}
</div>
</nav>
</header>
}

33
src/apps/nav/store.ts Normal file
View File

@@ -0,0 +1,33 @@
import { queryLogin } from '@/modules/query';
import { create } from 'zustand';
interface UserState {
user: {
avatar?: string;
description?: string;
id?: string;
needChangePassword?: boolean;
orgs?: string[];
type?: string;
username?: string;
} | null;
setUser: (user: UserState['user']) => void;
clearUser: () => void;
queryUser: () => void;
queryMe: () => void;
}
export const useUserStore = create<UserState>((set) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
queryUser: async () => {
const user = await queryLogin.checkLocalUser();
set({ user });
},
queryMe: async () => {
const user = await queryLogin.getMe();
set({ user });
}
}));

View File

@@ -1,7 +1,8 @@
import { app } from '../ai';
import { useEffect, useState } from 'react';
import { local } from '@/modules/query';
import '@kevisual/kv-login'
import { AuthProvider } from '../auth';
const getAppRoutes = () => {
const appRoutes = app.routes.map((route) => {
return {
@@ -14,12 +15,6 @@ const getAppRoutes = () => {
return appRoutes;
}
const fetchTest = async () => {
const url = 'http://localhost:51015/api/router';
const response = await fetch(url);
const data = await response.json();
console.log('Fetch Test Data:', data);
}
const fetchLocal = async () => {
const res = await local.post({
path: 'client',
@@ -34,6 +29,7 @@ const dynamicImport = async () => {
console.log('Test Function Output:', module.test());
}
export const App = () => {
const [appRoutes, setAppRoutes] = useState(getAppRoutes());
@@ -53,18 +49,12 @@ export const App = () => {
setAppRoutes(getAppRoutes());
}
}>{JSON.stringify(appRoutes, null, 2)}</pre>
<kv-login>
<div id="weixinLogin"></div>
</kv-login>
</div >;
}
// Add custom element to JSX namespace for TypeScript
declare global {
namespace JSX {
interface IntrinsicElements {
'kv-login': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
}
}
export const AppProvider = () => {
return <AuthProvider>
<App />
</AuthProvider>
}

View File

@@ -0,0 +1,145 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { VisuallyHidden } from "./visually-hidden"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
<VisuallyHidden>
<DialogPrimitive.Title>Dialog</DialogPrimitive.Title>
</VisuallyHidden>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,168 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function NavigationMenu({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
)
}
function NavigationMenuList({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-1",
className
)}
{...props}
/>
)
}
function NavigationMenuItem({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
)
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
)
function NavigationMenuTrigger({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
)
}
function NavigationMenuContent({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className
)}
{...props}
/>
)
}
function NavigationMenuViewport({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center"
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
{...props}
/>
</div>
)
}
function NavigationMenuLink({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className
)}
{...props}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
)
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
}

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -0,0 +1,16 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function VisuallyHidden({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"absolute h-px w-px p-0 -m-px overflow-hidden whitespace-nowrap border-0",
className
)}
{...props}
/>
)
}
export { VisuallyHidden }

View File

@@ -1,8 +1,11 @@
import { Query } from "@kevisual/query";
import { QueryLoginBrowser } from '@kevisual/query-login';
export const query = new Query();
export const queryLogin = new QueryLoginBrowser({
query
})
export const local = new Query({
url: '/client/router'
});

View File

@@ -1,10 +1,10 @@
---
import Html from '@/components/html.astro';
import { App } from '@/apps/web-command';
import { AppProvider } from '@/apps/home';
---
<Html>
<Html title='可视化平台'>
<main>
<App client:only/>
<AppProvider client:only />
</main>
</Html>