This commit is contained in:
2025-12-06 19:29:34 +08:00
parent fa24a82568
commit 7b99bc0b54
8 changed files with 340 additions and 53 deletions

View File

@@ -14,7 +14,7 @@ let target = process.env.VITE_API_URL || 'http://localhost:51015';
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' }; const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
let proxy = { let proxy = {
'/root/': { '/root/': {
target: `${target}/root/`, target: `${target}`,
}, },
'/api': apiProxy, '/api': apiProxy,
'/client': apiProxy, '/client': apiProxy,

View File

@@ -1,17 +1,18 @@
{ {
"name": "@kevisual/astro-simplate-template", "name": "@kevisual/hot-api",
"version": "0.0.2", "version": "0.0.2",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"basename": "/root/astro-simplate-template-docs", "basename": "/root/hot-api",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"pub": "envision deploy ./dist -k astro-simplate-template-docs -v 0.0.2 -u", "pub": "envision deploy ./dist -k hot-api -v 0.0.2 -u -y y",
"pub:docs": "envision deploy ./dist -k hot-api-docs -v 0.0.2 -u",
"slide:dev": "slidev --open slides/index.md", "slide:dev": "slidev --open slides/index.md",
"slide:build": "slidev build slides/index.md --base /root/astro-simplate-template-slide/", "slide:build": "slidev build slides/index.md --base /root/hot-api-slide/",
"slide:pub": "envision deploy ./slides/dist -k astro-simplate-template-slide -v 0.0.2 -u", "slide:pub": "envision deploy ./slides/dist -k hot-api-slide -v 0.0.2 -u",
"ui": "pnpm dlx shadcn@latest add " "ui": "pnpm dlx shadcn@latest add "
}, },
"keywords": [], "keywords": [],

18
src/apps/bg.tsx Normal file
View File

@@ -0,0 +1,18 @@
export const BG = (props: { children: React.ReactNode }) => {
return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: -1,
}}
>
{props.children}
</div>
);
}

View File

@@ -0,0 +1,60 @@
import { useStore } from "../store";
export const RefreshButton = () => {
const { isLoading, fetchItems } = useStore();
return (
<button
onClick={() => fetchItems()}
disabled={isLoading}
className={`
group relative p-3 rounded-full
bg-white/10 backdrop-blur-md border border-white/20
hover:bg-gradient-to-br hover:from-white/20 hover:to-white/5
hover:border-white/40 hover:shadow-[0_0_15px_rgba(255,255,255,0.3)]
active:scale-95 transition-all duration-300
flex items-center justify-center
${isLoading ? 'cursor-not-allowed opacity-80' : ''}
`}
aria-label="Refresh"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={`w-6 h-6 text-white transition-transform duration-700 ease-in-out ${isLoading ? 'animate-spin' : 'group-hover:rotate-180'}`}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
</button>
);
};
export const SettingsButton = () => {
return (
<button
className="
group relative p-3 rounded-full
bg-white/10 backdrop-blur-md border border-white/20
hover:bg-gradient-to-br hover:from-white/20 hover:to-white/5
hover:border-white/40 hover:shadow-[0_0_15px_rgba(255,255,255,0.3)]
active:scale-95 transition-all duration-300
flex items-center justify-center
"
aria-label="Settings"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6 text-white group-hover:rotate-90 transition-transform duration-500 ease-in-out"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
);
};

187
src/apps/hotkeys/index.tsx Normal file
View File

@@ -0,0 +1,187 @@
/**
* title: Hotkeys App Component
* description: A React component displaying a grid of glassmorphism cards with icons or abbreviated titles, using Zustand for state management and Tailwind CSS for styling. Includes an advanced search feature with glassmorphism and hover effects.
* tags: react, zustand, tailwindcss, glassmorphism, component, search
* createdAt: 2025-12-05
*/
import { useEffect, useState } from 'react';
import { SettingsButton, RefreshButton } from './components/icon';
import { useStore, CardItem } from './store';
const Card = ({ item }: { item: CardItem }) => {
const [isPressed, setIsPressed] = useState(false);
const { sendEvent } = useStore();
// 动效逻辑:点击后动一下恢复
const handleClick = () => {
setIsPressed(true);
setTimeout(() => setIsPressed(false), 150);
sendEvent(item);
};
return (
<div
onClick={handleClick}
className={`
group relative flex flex-col items-center justify-center p-4
cursor-pointer select-none
bg-white/10 backdrop-blur-md border border-white/20 shadow-lg rounded-2xl
transition-all duration-500 ease-out
hover:bg-gradient-to-br hover:from-cyan-500/20 hover:via-blue-500/20 hover:to-purple-500/20
hover:border-cyan-400/50
hover:shadow-[0_0_20px_rgba(34,211,238,0.5),0_0_40px_rgba(59,130,246,0.3)]
hover:-translate-y-1
${isPressed ? 'scale-95 brightness-125' : 'scale-100'}
w-full aspect-square overflow-hidden
`}
>
{/* Tech Glow Lines */}
<div className="absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none">
<div className="absolute inset-x-0 top-0 h-[1px] bg-gradient-to-r from-transparent via-cyan-400 to-transparent shadow-[0_0_10px_rgba(34,211,238,0.8)]" />
<div className="absolute inset-x-0 bottom-0 h-[1px] bg-gradient-to-r from-transparent via-purple-400 to-transparent shadow-[0_0_10px_rgba(192,132,252,0.8)]" />
<div className="absolute inset-y-0 left-0 w-[1px] bg-gradient-to-b from-transparent via-blue-400 to-transparent shadow-[0_0_10px_rgba(96,165,250,0.8)]" />
<div className="absolute inset-y-0 right-0 w-[1px] bg-gradient-to-b from-transparent via-cyan-400 to-transparent shadow-[0_0_10px_rgba(34,211,238,0.8)]" />
</div>
{/* Shine effect on hover */}
<div className="absolute inset-0 bg-gradient-to-tr from-transparent via-white/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none rounded-2xl" />
{/* Content Container */}
<div className="flex flex-col items-center justify-center w-full h-full space-y-3 relative z-10">
{/* Icon or Title Placeholder */}
<div className="flex items-center justify-center w-16 h-16 rounded-2xl bg-black/10 shadow-inner text-white font-bold text-2xl overflow-hidden transition-transform duration-300 group-hover:scale-110 group-hover:rotate-3 border border-white/10">
{item.iconUrl ? (
<img
src={item.iconUrl}
alt={item.title}
className="w-full h-full object-cover"
onError={(e) => {
// Fallback if image fails to load: hide image and show text
e.currentTarget.style.display = 'none';
const parent = e.currentTarget.parentElement;
if (parent) {
const span = document.createElement('span');
span.innerText = item.title.slice(0, 2);
parent.appendChild(span);
}
}}
/>
) : (
<span>{item.title.slice(0, 2)}</span>
)}
</div>
{/* Full Title (Truncated if too long) */}
<div className="w-full text-center px-2">
<p className="text-sm font-medium text-white/50 truncate tracking-wide transition-all duration-300 group-hover:text-white group-hover:drop-shadow-md">
{item.title}
</p>
</div>
</div>
</div>
);
};
const AdvancedSearch = () => {
const [isFocused, setIsFocused] = useState(false);
const handleSearch = () => {
// 搜索功能预留
console.log("Search triggered");
};
return (
<div
className={`
group relative flex items-center ml-2 h-12
bg-white/10 backdrop-blur-md border border-white/20 rounded-full
transition-all duration-500 ease-out
${isFocused
? 'w-64 border-cyan-400/60 shadow-[0_0_20px_rgba(34,211,238,0.4)] bg-white/20 ring-1 ring-cyan-300/30'
: 'w-12 hover:w-64 hover:border-cyan-400/50 hover:shadow-[0_0_15px_rgba(34,211,238,0.3)]'
}
overflow-hidden
`}
onClick={() => {
setIsFocused(true)
}}
>
<input
type="text"
placeholder="Search..."
className={`
w-full h-full bg-transparent border-none outline-none text-white placeholder-white/50 pl-4 pr-10 text-sm
transition-opacity duration-300
${isFocused ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}
`}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
<button
onClick={handleSearch}
className="absolute right-1 top-1/2 -translate-y-1/2 p-2 rounded-full hover:bg-white/10 transition-colors text-white/70 hover:text-white"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
</div>
);
};
export const App = () => {
const { items, isLoading, fetchItems } = useStore();
useEffect(() => {
fetchItems();
}, [fetchItems]);
return (
<div
className="min-h-screen w-full bg-cover bg-center bg-no-repeat bg-fixed p-4 sm:p-8 relative"
style={{ backgroundImage: "url('https://kevisual.cn/root/resources/upload/1.0.0/2025-12/rRpZIM_IJmc.webp')" }}
>
{/* Overlay for premium look and contrast */}
<div className="absolute inset-0 bg-black/30 backdrop-blur-[4px] z-0 pointer-events-none" />
<div className="max-w-7xl mx-auto relative z-10">
<header className="mb-8 flex items-center justify-between sm:justify-start sm:space-x-6">
<h1 className="hidden sm:block text-5xl font-black relative group cursor-default perspective-1000 italic">
<span className="relative inline-block text-white/70 transition-all duration-700 ease-out group-hover:scale-110 drop-shadow-[0_0_30px_rgba(255,255,255,0.4)] group-hover:drop-shadow-[0_0_50px_rgba(255,255,255,0.6)] tracking-wider transform-gpu" style={{ textShadow: '0 4px 20px rgba(255,255,255,0.3), 0 0 40px rgba(255,255,255,0.2)' }}>
HotKeys
</span>
{/* 3D层次感背景文字 */}
<span className="absolute top-0 left-0 inline-block text-white/20 opacity-20 blur-sm translate-x-1 translate-y-1 transition-all duration-700 group-hover:translate-x-2 group-hover:translate-y-2 tracking-wider" aria-hidden="true">
HotKeys
</span>
{/* 发光边缘效果 */}
<span className="absolute inset-0 bg-white opacity-0 group-hover:opacity-10 blur-xl transition-opacity duration-700" aria-hidden="true" />
</h1>
<div className="flex items-center space-x-3">
<RefreshButton />
<SettingsButton />
<AdvancedSearch />
</div>
</header>
{isLoading ? (
<div className="flex justify-center items-center h-64">
<div className="animate-pulse flex flex-col items-center">
<div className="h-12 w-12 bg-gray-300 rounded-full mb-4"></div>
<div className="h-4 w-32 bg-gray-300 rounded"></div>
</div>
</div>
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4 sm:gap-6">
{items.map((item) => (
<Card key={item.id} item={item} />
))}
</div>
)}
</div>
</div>
);
};

58
src/apps/hotkeys/store.ts Normal file
View File

@@ -0,0 +1,58 @@
import { create } from 'zustand';
import { query } from '../../modules/query'
// --- Types ---
export interface CardItem {
id: string;
title: string;
iconUrl?: string;
description?: string;
data?: any;
}
export interface StoreState {
items: CardItem[];
isLoading: boolean;
fetchItems: () => Promise<void>;
sendEvent: (item: CardItem) => Promise<void>;
}
// --- Store ---
export const useStore = create<StoreState>((set) => ({
items: [],
isLoading: false,
fetchItems: async () => {
set({ isLoading: true });
// TODO: Replace with actual API call
// const response = await fetch('/api/hotkeys');
// const data = await response.json();
// Mock data for demonstration
const mockData: CardItem[] = Array.from({ length: 12 }).map((_, i) => ({
id: `item-${i}`,
title: i % 4 === 0 ? `工具 ${i + 1}` : `Application Long Name ${i + 1}`,
iconUrl: i % 3 === 0 ? `https://api.dicebear.com/7.x/icons/svg?seed=${i}` : undefined,
description: `Description for item ${i + 1}`
}));
mockData.unshift({
id: 'item-search',
title: 'win+d 显示桌面',
iconUrl: 'https://api.dicebear.com/7.x/icons/svg?seed=search',
description: '显示桌面'
})
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 800));
set({ items: mockData, isLoading: false });
},
sendEvent: async (item: CardItem) => {
// client/router?path=key-sender&keys=win+d
const res = await query.post({
path: 'key-sender',
keys: 'win+d'
});
console.log('Event sent for item:', item, 'Response:', res);
if (res.code !== 200) {
alert('Failed to send event');
}
}
}));

View File

@@ -1,4 +1,4 @@
import { Query } from '@kevisual/query' import { QueryClient } from '@kevisual/query'
const getUrl = () => { const getUrl = () => {
const host = window.location.host const host = window.location.host
@@ -10,6 +10,6 @@ const getUrl = () => {
return '/client/router' return '/client/router'
} }
export const query = new Query({ export const query = new QueryClient({
url: getUrl() url: 'http://localhost:51015/client/router',
}); });

View File

@@ -1,47 +1,10 @@
--- ---
// import { query } from '@/modules/query.ts'; import Html from '@/components/html.astro';
console.log('Hello from index.astro'); import { App } from '../apps/hotkeys/index.tsx';
import '../styles/global.css';
--- ---
<html lang='en'> <Html>
<head> <main>
<title>My Homepage</title> <App client:only />
</head> </main>
<body> </Html>
<h1 onclick="{onClick}">Welcome to my website!</h1>
<div class='bg-amber-50 w-20 h-20 rounded-full'></div>
<div id='root'></div>
<script type='importmap' data-vite-ignore is:inline>
{
"imports": {
"react": "https://esm.sh/react@19.1.0",
"react-dom": "https://esm.sh/react-dom@19.1.0/client.js",
"react-toastify": "https://esm.sh/react-toastify@11.0.5"
}
}
</script>
<script type='module' data-vite-ignore is:inline>
import { Button, message } from 'https://esm.sh/antd?standalone';
import React from 'react';
import { ToastContainer, toast } from 'react-toastify';
import { createRoot } from 'react-dom';
setTimeout(() => {
toast.loading('Hello from index.astro');
window.toast = toast;
console.log('message', toast);
}, 1000);
console.log('Hello from index.astro', Button);
const root = document.getElementById('root');
const render = createRoot(root);
const App = () => {
const button = React.createElement(Button, null, 'Hello');
const messageEl = React.createElement(ToastContainer, null, 'Hello');
const wrapperMessage = React.createElement('div', null, [button, messageEl]);
return wrapperMessage;
};
// render.render(React.createElement(Button, null, 'Hello'), root);
render.render(App(), root);
</script>
</body>
</html>