generated from template/vite-react-template
temp
This commit is contained in:
parent
3064673014
commit
7f82e37ea2
@ -2,9 +2,8 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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" />
|
||||
<title>Vite + React + TS</title>
|
||||
<title>Assistant Base App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "vite-react",
|
||||
"name": "assistant-base-app",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"basename": "/",
|
||||
"basename": "/root/assistant-base-app",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:web": "cross-env WEB_DEV=true vite --mode web",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"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"
|
||||
},
|
||||
"stackblitz": {
|
||||
@ -29,6 +29,8 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"immer": "^10.1.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide": "^0.479.0",
|
||||
"lucide-react": "^0.479.0",
|
||||
"nanoid": "^5.1.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@ -38,6 +38,12 @@ importers:
|
||||
lodash-es:
|
||||
specifier: ^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:
|
||||
specifier: ^5.1.2
|
||||
version: 5.1.2
|
||||
@ -1248,6 +1254,14 @@ packages:
|
||||
lru-cache@5.1.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -2985,6 +2999,12 @@ snapshots:
|
||||
dependencies:
|
||||
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: {}
|
||||
|
||||
micromatch@4.0.8:
|
||||
|
50
src/App.tsx
50
src/App.tsx
@ -1,12 +1,50 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { basename } from './modules/basename';
|
||||
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { useClientStore } from './store';
|
||||
import { useConfigStore } from './store/config';
|
||||
console.log('basename', basename);
|
||||
const PackageManager = React.lazy(() => import('./package/index'));
|
||||
const Enter = React.lazy(() => import('./page/Enter'));
|
||||
export const App = () => {
|
||||
return (
|
||||
<div className='bg-slate-200 w-full h-full border'>
|
||||
<div className='test-loading bg-black'>
|
||||
<div></div>
|
||||
const url = new URL(window.location.href);
|
||||
const link = url.searchParams.get('link');
|
||||
const { checkClient, mount, isClient } = useClientStore();
|
||||
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>
|
||||
);
|
||||
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
214
src/delete/enter.html
Normal 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
33
src/delete/main.js
Normal 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');
|
||||
});
|
||||
});
|
||||
};
|
@ -4,4 +4,30 @@
|
||||
.test-loading {
|
||||
@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
47
src/modules/electron.ts
Normal 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
6
src/modules/query.ts
Normal 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
125
src/package/index.tsx
Normal 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
112
src/package/store/index.ts
Normal 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
120
src/package/style.css
Normal 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
133
src/page/Enter.css
Normal 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
105
src/page/Enter.tsx
Normal 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
47
src/store/config.ts
Normal 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
36
src/store/index.ts
Normal 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 });
|
||||
},
|
||||
}));
|
@ -38,17 +38,22 @@ export default defineConfig({
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
target: 'http://localhost:4005',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
'/api/router': {
|
||||
target: 'ws://localhost:3000',
|
||||
target: 'ws://localhost:4005',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
rewriteWsOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api'),
|
||||
},
|
||||
'/client': {
|
||||
target: 'https://localhost:51015',
|
||||
changeOrigin: true,
|
||||
secure: false, // 允许自签名证书
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
Reference in New Issue
Block a user