This commit is contained in:
熊潇 2025-03-08 22:01:49 +08:00
parent 3064673014
commit 7f82e37ea2
17 changed files with 1082 additions and 14 deletions

View File

@ -2,9 +2,8 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>Assistant Base App</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -1,16 +1,16 @@
{ {
"name": "vite-react", "name": "assistant-base-app",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.1",
"type": "module", "type": "module",
"basename": "/", "basename": "/root/assistant-base-app",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"dev:web": "cross-env WEB_DEV=true vite --mode web", "dev:web": "cross-env WEB_DEV=true vite --mode web",
"build": "vite build", "build": "vite build",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview", "preview": "vite preview",
"pub": "envision deploy ./dist -k vite-react -v 0.0.1", "pub": "envision deploy ./dist -k assistant-base-app -v 0.0.1 ",
"ev": "npm run build && npm run deploy" "ev": "npm run build && npm run deploy"
}, },
"stackblitz": { "stackblitz": {
@ -29,6 +29,8 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"immer": "^10.1.1", "immer": "^10.1.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lucide": "^0.479.0",
"lucide-react": "^0.479.0",
"nanoid": "^5.1.2", "nanoid": "^5.1.2",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",

20
pnpm-lock.yaml generated
View File

@ -38,6 +38,12 @@ importers:
lodash-es: lodash-es:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
lucide:
specifier: ^0.479.0
version: 0.479.0
lucide-react:
specifier: ^0.479.0
version: 0.479.0(react@19.0.0)
nanoid: nanoid:
specifier: ^5.1.2 specifier: ^5.1.2
version: 5.1.2 version: 5.1.2
@ -1248,6 +1254,14 @@ packages:
lru-cache@5.1.1: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-react@0.479.0:
resolution: {integrity: sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
lucide@0.479.0:
resolution: {integrity: sha512-TsoBbi2sUM1GHTYPH+KuT5KFqBDZ7HBiWfFA8RbbQXwXJBw9r9k/sguR61YXH/IV9pRqDEM6lNsyb8S9uQn/1g==}
merge2@1.4.1: merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -2985,6 +2999,12 @@ snapshots:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
lucide-react@0.479.0(react@19.0.0):
dependencies:
react: 19.0.0
lucide@0.479.0: {}
merge2@1.4.1: {} merge2@1.4.1: {}
micromatch@4.0.8: micromatch@4.0.8:

View File

@ -1,12 +1,50 @@
import React, { useEffect, useMemo } from 'react';
import { basename } from './modules/basename'; import { basename } from './modules/basename';
import { ToastContainer } from 'react-toastify';
import { useClientStore } from './store';
import { useConfigStore } from './store/config';
console.log('basename', basename); console.log('basename', basename);
const PackageManager = React.lazy(() => import('./package/index'));
const Enter = React.lazy(() => import('./page/Enter'));
export const App = () => { export const App = () => {
return ( const url = new URL(window.location.href);
<div className='bg-slate-200 w-full h-full border'> const link = url.searchParams.get('link');
<div className='test-loading bg-black'> const { checkClient, mount, isClient } = useClientStore();
<div></div> const { getConfig } = useConfigStore();
useEffect(() => {
checkClient();
}, []);
useEffect(() => {
if (isClient) {
getConfig();
}
}, [isClient]);
const isEnter = useMemo(() => {
if (!link) return true;
return link.includes('enter');
}, [link]);
if (!mount)
return (
<div className='w-full h-full flex justify-center items-center'>
<div className='w-10 h-10 bg-amber-500 rounded animate-spin'></div>
</div> </div>
</div> );
return (
<>
<nav className='bg-amber-500 p-4 sticky top-0 z-10'>
<ul className='flex space-x-4'>
<li className={isEnter ? 'text-white' : 'text-white/70'}>
<a href='?link=enter'></a>
</li>
<li className={!isEnter ? 'text-white' : 'text-white/70'}>
<a href='?link=packages'>Packages</a>
</li>
</ul>
</nav>
<div className='w-full ' style={{ height: 'calc(100vh - 4rem)' }}>
{isEnter ? <Enter /> : <PackageManager />}
</div>
<ToastContainer />
</>
); );
}; };

214
src/delete/enter.html Normal file
View File

@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Page Enter Configuration</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
background: linear-gradient(135deg, #fef3c7 0%, #fffbeb 100%);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding: 1.5rem;
position: relative;
overflow-x: hidden;
}
.container {
max-width: 42rem;
margin: 0 auto;
}
.header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 2rem;
}
.header svg {
width: 2rem;
height: 2rem;
color: #d97706;
animation: spin 8s linear infinite;
}
.header h1 {
font-size: 1.875rem;
font-weight: bold;
color: #92400e;
}
.form-container {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
border-radius: 1rem;
box-shadow: 0 4px 6px rgba(217, 119, 6, 0.1);
padding: 2rem;
transition: all 0.3s ease;
}
.form-container:hover {
box-shadow: 0 8px 12px rgba(217, 119, 6, 0.15);
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #92400e;
margin-bottom: 0.25rem;
}
input[type="text"] {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #fbbf24;
border-radius: 0.5rem;
font-size: 1rem;
transition: all 0.2s;
}
input[type="text"]:focus {
outline: none;
border-color: #d97706;
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.2);
}
button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
background-color: #d97706;
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #b45309;
}
.particles {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.particle {
position: absolute;
color: #fbbf24;
opacity: 0.3;
animation: float 5s ease-in-out infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes float {
0%, 100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-20px) rotate(10deg);
}
}
</style>
</head>
<body>
<div class="particles" id="particles"></div>
<div class="container">
<div class="header">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<h1>Page Enter Configuration</h1>
</div>
<div class="form-container">
<form id="configForm">
<div class="form-group">
<label for="pageApi">Page Enter Api</label>
<input type="text" id="pageApi" placeholder="Enter page api configuration">
</div>
<button type="submit" id="save-button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>
Save Configuration
</button>
</form>
<div id="save-result"></div>
</div>
</div>
<script>
// Create floating particles
function createParticles() {
const particles = document.getElementById('particles');
const particleCount = 20;
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>
<path d="M5 3v4"/>
<path d="M19 17v4"/>
<path d="M3 5h4"/>
<path d="M17 19h4"/>
</svg>
`;
const size = 10 + Math.random() * 20;
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
particle.style.left = `${Math.random() * 100}%`;
particle.style.top = `${Math.random() * 100}%`;
particle.style.animationDuration = `${5 + Math.random() * 5}s`;
particle.style.animationDelay = `${Math.random() * 5}s`;
particles.appendChild(particle);
}
}
// Initialize particles
createParticles();
// Form handling
</script>
<script src="./main.js" type="module"></script>
</body>
</html>

33
src/delete/main.js Normal file
View File

@ -0,0 +1,33 @@
// import { saveAppConfig } from './electron.js';
const saveAppConfig = async () => {
return {
pageApi: 'https://kevisual.silkyai.cn',
};
};
window.onload = async () => {
const config = await saveAppConfig();
const pageApi = document.getElementById('pageApi');
const saveResult = document.getElementById('save-result');
pageApi.value = config?.pageApi || 'https://kevisual.silkyai.cn';
console.log('config', config);
const form = document.getElementById('configForm');
// Handle form submission
form.addEventListener('submit', async (e) => {
e.preventDefault();
const config = {
pageApi: pageApi.value,
};
const result = await saveAppConfig(config);
const newPageApi = result?.pageApi || '';
saveResult.innerHTML = `<h1>保存成功</h1>
<p>new pageApi: ${newPageApi}</p>
<button id="relunch">重启</button>`;
const relunchButton = document.getElementById('relunch');
relunchButton.addEventListener('click', () => {
window.electron.ipcRenderer.invoke('relunch');
});
});
};

View File

@ -5,3 +5,29 @@
@apply w-20 h-20 bg-gray-300 rounded-full animate-spin; @apply w-20 h-20 bg-gray-300 rounded-full animate-spin;
} }
} }
html,
body {
height: 100%;
width: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
#root {
height: 100%;
width: 100%;
overflow: auto;
}
#root::-webkit-scrollbar {
width: 1px; /* 设置滚动条宽度为1像素 */
}
#root::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.5); /* 滚动条的颜色 */
border-radius: 10px; /* 可选:使滚动条圆角 */
}
#root::-webkit-scrollbar-track {
background: transparent;
}

47
src/modules/electron.ts Normal file
View File

@ -0,0 +1,47 @@
export const checkIsElectron = () => {
// @ts-ignore
return typeof window !== 'undefined' && typeof window.electron === 'object';
};
export const getElectron = () => {
// @ts-ignore
return window.electron;
};
export const getAppList = async () => {
const check = checkIsElectron();
if (!check) {
console.log('not electron');
return [];
}
const electron = getElectron();
console.log('electron', electron);
const appList = await electron.ipcRenderer.invoke('get-app-list');
console.log('appList', appList);
return appList;
};
export const installApp = async (app) => {
const check = checkIsElectron();
if (!check) {
console.log('not electron');
return [];
}
const electron = getElectron();
console.log('installApp', app);
const result = await electron.ipcRenderer.invoke('install-app', app);
console.log('installApp result', result);
return result;
};
export const uninstallApp = async (app) => {
const check = checkIsElectron();
if (!check) {
console.log('not electron');
return [];
}
const electron = getElectron();
console.log('uninstallApp', app);
const result = await electron.ipcRenderer.invoke('uninstall-app', app);
console.log('uninstallApp result', result);
return result;
};

6
src/modules/query.ts Normal file
View File

@ -0,0 +1,6 @@
import { QueryClient } from '@kevisual/query';
export const client = new QueryClient({
url: '/client/router',
io: false,
});
export const query = new QueryClient({});

125
src/package/index.tsx Normal file
View File

@ -0,0 +1,125 @@
import { useState, useEffect } from 'react';
import './style.css';
import { usePackageStore, Package } from './store';
import { Link2, SquareArrowOutUpRight } from 'lucide-react';
import { useConfigStore } from '@/store/config';
export const PackageManager = () => {
const { shopPackages, installedPackages, getInstalledPackages, getShopPackages, uninstallPackage, installPackage } = usePackageStore();
const { pageApi } = useConfigStore();
useEffect(() => {
getInstalledPackages();
getShopPackages();
}, []);
const getPackageStatus = (pkg: Package): string => {
const installed = installedPackages.find((p) => p.user === pkg.user && p.key === pkg.key);
if (!installed) return 'not-installed';
if (installed.version !== pkg.version) return 'update-available';
return 'installed';
};
const handleInstall = (id: string) => {
const pkg = shopPackages.find((p) => p.id === id);
if (pkg) {
installPackage(pkg);
}
};
const handleUpdate = (id: string) => {
const pkg = shopPackages.find((p) => p.id === id);
if (pkg) {
installPackage(pkg);
}
};
const handleReinstall = (id: string) => {
const pkg = shopPackages.find((p) => p.id === id);
if (pkg) {
installPackage(pkg);
}
};
const handleUninstall = (id: string) => {
const pkg = shopPackages.find((p) => p.id === id);
if (pkg) {
uninstallPackage(pkg);
}
};
const getActionButton = (status: string, pkg: Package) => {
switch (status) {
case 'not-installed':
return (
<button className='button button-install' onClick={() => handleInstall(pkg.id)}>
Install
</button>
);
case 'update-available':
return (
<button className='button button-update' onClick={() => handleUpdate(pkg.id)}>
Update
</button>
);
case 'installed':
return (
<button className='button button-reinstall' onClick={() => handleReinstall(pkg.id)}>
Reinstall
</button>
);
}
};
const handleOpenWindow = (pkg: Package) => {
const baseUrl = 'https://kevisual.silkyai.cn';
const path = `/${pkg.user}/${pkg.key}`;
window.open(`${baseUrl}${path}`, '_blank');
};
const handleOpenClientWindow = (pkg: Package) => {
if (!pageApi) return;
const baseUrl = pageApi;
const path = `/${pkg.user}/${pkg.key}`;
window.open(`${baseUrl}${path}`, '_blank');
};
return (
<div id='app'>
<h1>Package Manager</h1>
<div className='package-list'>
{shopPackages.map((pkg) => {
const status = getPackageStatus(pkg);
const isInstalled = status !== 'not-installed';
return (
<div key={pkg.id} className='package-card'>
<h2>{pkg.title}</h2>
<p className='description'>{pkg.description}</p>
<div className='package-info'>
<span>Version: {pkg.version}</span>
<span>User: {pkg.user}</span>
</div>
<div className='actions'>
{getActionButton(status, pkg)}
{status !== 'not-installed' && (
<button className='button button-uninstall' onClick={() => handleUninstall(pkg.id)}>
Uninstall
</button>
)}
<div className='flex gap-2'>
<div className='cursor-pointer p-2 rounded-md bg-amber-500 text-white'>
<SquareArrowOutUpRight onClick={() => handleOpenWindow(pkg)} />
</div>
{pageApi && isInstalled && (
<div className='cursor-pointer p-2 rounded-md bg-amber-500 text-white'>
<Link2 onClick={() => handleOpenClientWindow(pkg)} />
</div>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
);
};
export default PackageManager;

112
src/package/store/index.ts Normal file
View File

@ -0,0 +1,112 @@
import { create } from 'zustand';
import { client, query } from '@/modules/query';
import { toast } from 'react-toastify';
export type Package = {
id: string;
name?: string;
version?: string;
description?: string;
title?: string;
user?: string;
key?: string;
};
type PackageStore = {
installedPackages: Package[];
shopPackages: Package[];
setInstalledPackages: (packages: Package[]) => void;
setShopPackages: (packages: Package[]) => void;
getInstalledPackages: () => Promise<Package[]>;
getShopPackages: () => Promise<Package[]>;
uninstallPackage: (pkg: Package) => Promise<void>;
installPackage: (pkg: Package) => Promise<void>;
};
export const usePackageStore = create<PackageStore>((set, get) => ({
installedPackages: [],
shopPackages: [],
setInstalledPackages: (packages) => set({ installedPackages: packages }),
setShopPackages: (packages) => set({ shopPackages: packages }),
getInstalledPackages: async () => {
const res = await client.post({
path: 'shop',
key: 'list-installed',
});
if (res.code === 200) {
set({ installedPackages: res.data });
}
return res.data;
},
getShopPackages: async () => {
// path=app&key=public-list
const res = await query.post({
path: 'app',
key: 'public-list',
});
if (res.code === 200) {
set({ shopPackages: res.data });
}
return res.data;
},
uninstallPackage: async (pkg: Package) => {
const res = await client.post({
path: 'shop',
key: 'uninstall',
data: { pkg },
});
if (res.code === 200) {
get().getInstalledPackages();
toast.success('Package uninstalled successfully');
} else {
toast.error(res.message || 'Failed to uninstall package');
}
console.log('uninstallPackage', res);
},
installPackage: async (pkg: Package) => {
const toastId = toast.loading('Installing package...');
const res = await client.post({
path: 'shop',
key: 'install',
data: { pkg },
});
toast.dismiss(toastId);
if (res.code === 200) {
get().getInstalledPackages();
toast.success('Package installed successfully');
} else {
toast.error(res.message || 'Failed to install package');
}
console.log('installPackage', res);
},
}));
const installedPackages: Package[] = [
{ user: 'test', key: 'test-key', version: '1.0.0', id: '1', title: '', description: '' },
{ user: 'demo', key: 'demo-package', version: '1.2.0', id: '2', title: '', description: '' },
];
const mockPackages: Package[] = [
{
id: '1',
title: 'Demo Package 1',
description: 'A test package for demonstration',
version: '1.0.0',
user: 'test',
key: 'test-key',
},
{
id: '2',
title: 'Demo Package 2',
description: 'Another test package with updates',
version: '2.0.0',
user: 'demo',
key: 'demo-package',
},
{
id: '3',
title: 'New Package',
description: "A package that hasn't been installed yet",
version: '1.0.0',
user: 'demo',
key: 'new-package',
},
];

120
src/package/style.css Normal file
View File

@ -0,0 +1,120 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
background-color: #fff8e1;
color: #213547;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
}
h1 {
text-align: center;
color: #ff8f00;
}
.package-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
padding: 1rem;
}
.package-card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(255, 143, 0, 0.1);
border: 1px solid #ffe0b2;
}
.package-card h2 {
margin: 0 0 0.5rem 0;
color: #f57c00;
}
.package-card .description {
color: #666;
margin-bottom: 1rem;
font-size: 0.9rem;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.5;
max-height: 6em;
}
.package-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
font-size: 0.9rem;
color: #666;
}
.actions {
display: flex;
gap: 0.5rem;
}
.button {
padding: 0.5rem 1rem;
border-radius: 4px;
border: none;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
}
.button-install {
background-color: #ffa000;
color: white;
}
.button-update {
background-color: #ff8f00;
color: white;
}
.button-reinstall {
background-color: #ffb300;
color: white;
}
.button-uninstall {
background-color: #ff6f00;
color: white;
}
.button:hover {
opacity: 0.9;
}
.button:disabled {
background-color: #ffe0b2;
cursor: not-allowed;
}
.error-message {
text-align: center;
color: #ff6f00;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(255, 143, 0, 0.1);
grid-column: 1 / -1;
}

133
src/page/Enter.css Normal file
View File

@ -0,0 +1,133 @@
* {
/* margin: 0;
padding: 0; */
box-sizing: border-box;
}
body {
min-height: 100vh;
background: linear-gradient(135deg, #fef3c7 0%, #fffbeb 100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* padding: 1.5rem; */
position: relative;
overflow-x: hidden;
}
.container {
max-width: 42rem;
margin: 0 auto;
}
.header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 2rem;
}
.header svg {
width: 2rem;
height: 2rem;
color: #d97706;
animation: spin 8s linear infinite;
}
.header h1 {
font-size: 1.875rem;
font-weight: bold;
color: #92400e;
}
.form-container {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
border-radius: 1rem;
box-shadow: 0 4px 6px rgba(217, 119, 6, 0.1);
padding: 2rem;
transition: all 0.3s ease;
}
.form-container:hover {
box-shadow: 0 8px 12px rgba(217, 119, 6, 0.15);
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #92400e;
margin-bottom: 0.25rem;
}
input[type='text'] {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #fbbf24;
border-radius: 0.5rem;
font-size: 1rem;
transition: all 0.2s;
}
input[type='text']:focus {
outline: none;
border-color: #d97706;
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.2);
}
button {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
background-color: #d97706;
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: #b45309;
}
.particles {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.particle {
position: absolute;
color: #fbbf24;
opacity: 0.3;
animation: float 5s ease-in-out infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes float {
0%,
100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-20px) rotate(10deg);
}
}

105
src/page/Enter.tsx Normal file
View File

@ -0,0 +1,105 @@
import React, { useEffect } from 'react';
import './Enter.css'; // Assuming you move the styles to a separate CSS file
import { useConfigStore } from '@/store/config';
const Enter: React.FC = () => {
const { config, getConfig, saveConfig } = useConfigStore();
useEffect(() => {
createParticles();
getConfig();
}, []);
useEffect(() => {
if (config.pageApi) {
const pageApi = document.getElementById('pageApi') as HTMLInputElement;
pageApi.value = config.pageApi;
}
}, [config]);
const createParticles = () => {
const particles = document.getElementById('particles');
const particleCount = 20;
if (particles) {
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>
<path d="M5 3v4"/>
<path d="M19 17v4"/>
<path d="M3 5h4"/>
<path d="M17 19h4"/>
</svg>
`;
const size = 10 + Math.random() * 20;
particle.style.width = `${size}px`;
particle.style.height = `${size}px`;
particle.style.left = `${Math.random() * 100}%`;
particle.style.top = `${Math.random() * 100}%`;
particle.style.animationDuration = `${5 + Math.random() * 5}s`;
particle.style.animationDelay = `${Math.random() * 5}s`;
particles.appendChild(particle);
}
}
};
const onSave = () => {
const pageApi = document.getElementById('pageApi') as HTMLInputElement;
saveConfig(pageApi.value);
};
return (
<div className='h-full w-full p-4 pt-10'>
<div className='particles' id='particles'></div>
<div className='container'>
<div className='header'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'>
<path d='M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z' />
<circle cx='12' cy='12' r='3' />
</svg>
<h1>Page Enter Configuration</h1>
</div>
<div className='form-container'>
<form id='configForm'>
<div className='form-group'>
<label htmlFor='pageApi'>Page Enter Api</label>
<input type='text' id='pageApi' placeholder='Enter page api configuration' />
</div>
<button type='submit' id='save-button' onClick={onSave}>
<svg
xmlns='http://www.w3.org/2000/svg'
width='20'
height='20'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'>
<path d='M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z' />
<polyline points='17 21 17 13 7 13 7 21' />
<polyline points='7 3 7 8 15 8' />
</svg>
Save Configuration
</button>
</form>
<div id='save-result'></div>
</div>
</div>
</div>
);
};
export default Enter;

47
src/store/config.ts Normal file
View File

@ -0,0 +1,47 @@
import { create } from 'zustand';
import { client } from '@/modules/query';
import { toast } from 'react-toastify';
type ConfigStore = {
config: any;
setConfig: (config: any) => void;
getConfig: () => Promise<void>;
saveConfig: (config: any) => Promise<void>;
pageApi: string;
setPageApi: (pageApi: string) => void;
};
export const useConfigStore = create<ConfigStore>((set) => ({
config: {},
setConfig: (config) => set({ config }),
getConfig: async () => {
const res = await client.post({
path: 'config',
});
if (res.code === 200) {
console.log(res.data);
set({ config: res.data, pageApi: res.data?.pageApi || '' });
} else {
toast.error(res.message || '获取配置失败');
}
},
pageApi: '',
setPageApi: (pageApi) => set({ pageApi }),
saveConfig: async (config) => {
console.log(config);
if (!config) {
toast.error('配置不能为空');
return;
}
const res = await client.post({
path: 'config',
key: 'set',
data: { pageApi: config },
});
if (res.code === 200) {
toast.success('保存配置成功');
} else {
toast.error(res.message || '保存配置失败');
}
},
}));

36
src/store/index.ts Normal file
View File

@ -0,0 +1,36 @@
import { create } from 'zustand';
import { client } from '@/modules/query';
type ClientStore = {
isClient: boolean;
setIsClient: (isClient: boolean) => void;
checkClient: () => Promise<void>;
mount: boolean;
setMount: (mount: boolean) => void;
};
export const useClientStore = create<ClientStore>((set) => ({
isClient: false,
setIsClient: (isClient) => set({ isClient }),
mount: false,
setMount: (mount) => set({ mount }),
checkClient: async () => {
// @ts-ignore
let isClient = window?.electron;
if (isClient) {
set({ isClient: true, mount: true });
return;
}
try {
const res = await client.post({
path: 'check',
});
if (res.code === 200) {
set({ isClient: true, mount: true });
return;
}
} catch (error) {
console.error(error);
}
set({ mount: true });
},
}));

View File

@ -38,17 +38,22 @@ export default defineConfig({
host: '0.0.0.0', host: '0.0.0.0',
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3000', target: 'http://localhost:4005',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api'), rewrite: (path) => path.replace(/^\/api/, '/api'),
}, },
'/api/router': { '/api/router': {
target: 'ws://localhost:3000', target: 'ws://localhost:4005',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
rewriteWsOrigin: true, rewriteWsOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api'), rewrite: (path) => path.replace(/^\/api/, '/api'),
}, },
'/client': {
target: 'https://localhost:51015',
changeOrigin: true,
secure: false, // 允许自签名证书
},
}, },
}, },
}); });