generated from template/astro-simple-template
udpate
This commit is contained in:
@@ -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' };
|
||||
let proxy = {
|
||||
'/root/': {
|
||||
target: `${target}/root/`,
|
||||
target: `${target}`,
|
||||
},
|
||||
'/api': apiProxy,
|
||||
'/client': apiProxy,
|
||||
|
||||
11
package.json
11
package.json
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "@kevisual/astro-simplate-template",
|
||||
"name": "@kevisual/hot-api",
|
||||
"version": "0.0.2",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"basename": "/root/astro-simplate-template-docs",
|
||||
"basename": "/root/hot-api",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"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:build": "slidev build slides/index.md --base /root/astro-simplate-template-slide/",
|
||||
"slide:pub": "envision deploy ./slides/dist -k astro-simplate-template-slide -v 0.0.2 -u",
|
||||
"slide:build": "slidev build slides/index.md --base /root/hot-api-slide/",
|
||||
"slide:pub": "envision deploy ./slides/dist -k hot-api-slide -v 0.0.2 -u",
|
||||
"ui": "pnpm dlx shadcn@latest add "
|
||||
},
|
||||
"keywords": [],
|
||||
|
||||
18
src/apps/bg.tsx
Normal file
18
src/apps/bg.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
60
src/apps/hotkeys/components/icon.tsx
Normal file
60
src/apps/hotkeys/components/icon.tsx
Normal 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
187
src/apps/hotkeys/index.tsx
Normal 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
58
src/apps/hotkeys/store.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Query } from '@kevisual/query'
|
||||
import { QueryClient } from '@kevisual/query'
|
||||
|
||||
const getUrl = () => {
|
||||
const host = window.location.host
|
||||
@@ -10,6 +10,6 @@ const getUrl = () => {
|
||||
return '/client/router'
|
||||
}
|
||||
|
||||
export const query = new Query({
|
||||
url: getUrl()
|
||||
export const query = new QueryClient({
|
||||
url: 'http://localhost:51015/client/router',
|
||||
});
|
||||
@@ -1,47 +1,10 @@
|
||||
---
|
||||
// import { query } from '@/modules/query.ts';
|
||||
console.log('Hello from index.astro');
|
||||
import '../styles/global.css';
|
||||
import Html from '@/components/html.astro';
|
||||
import { App } from '../apps/hotkeys/index.tsx';
|
||||
---
|
||||
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<title>My Homepage</title>
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
<Html>
|
||||
<main>
|
||||
<App client:only />
|
||||
</main>
|
||||
</Html>
|
||||
|
||||
Reference in New Issue
Block a user