diff --git a/index.html b/index.html
index e4b78ea..262e9c0 100644
--- a/index.html
+++ b/index.html
@@ -2,9 +2,8 @@
-
-
+ 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 (
+
-
+ );
+ return (
+ <>
+
+
+
+ >
);
};
diff --git a/src/delete/enter.html b/src/delete/enter.html
new file mode 100644
index 0000000..09ee7a6
--- /dev/null
+++ b/src/delete/enter.html
@@ -0,0 +1,214 @@
+
+
+
+
+
+
Page Enter Configuration
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/delete/main.js b/src/delete/main.js
new file mode 100644
index 0000000..ad78562
--- /dev/null
+++ b/src/delete/main.js
@@ -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 = `
保存成功
+
new pageApi: ${newPageApi}
+
`;
+ const relunchButton = document.getElementById('relunch');
+ relunchButton.addEventListener('click', () => {
+ window.electron.ipcRenderer.invoke('relunch');
+ });
+ });
+};
diff --git a/src/index.css b/src/index.css
index ad74ee7..4294f87 100644
--- a/src/index.css
+++ b/src/index.css
@@ -4,4 +4,30 @@
.test-loading {
@apply w-20 h-20 bg-gray-300 rounded-full animate-spin;
}
-}
\ No newline at end of file
+}
+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;
+}
diff --git a/src/modules/electron.ts b/src/modules/electron.ts
new file mode 100644
index 0000000..5a0e4d1
--- /dev/null
+++ b/src/modules/electron.ts
@@ -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;
+};
diff --git a/src/modules/query.ts b/src/modules/query.ts
new file mode 100644
index 0000000..ad21c80
--- /dev/null
+++ b/src/modules/query.ts
@@ -0,0 +1,6 @@
+import { QueryClient } from '@kevisual/query';
+export const client = new QueryClient({
+ url: '/client/router',
+ io: false,
+});
+export const query = new QueryClient({});
diff --git a/src/package/index.tsx b/src/package/index.tsx
new file mode 100644
index 0000000..da9dc14
--- /dev/null
+++ b/src/package/index.tsx
@@ -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 (
+
+ );
+ case 'update-available':
+ return (
+
+ );
+ case 'installed':
+ return (
+
+ );
+ }
+ };
+ 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 (
+
+
Package Manager
+
+ {shopPackages.map((pkg) => {
+ const status = getPackageStatus(pkg);
+ const isInstalled = status !== 'not-installed';
+ return (
+
+
{pkg.title}
+
{pkg.description}
+
+ Version: {pkg.version}
+ User: {pkg.user}
+
+
+ {getActionButton(status, pkg)}
+
+ {status !== 'not-installed' && (
+
+ )}
+
+
+ handleOpenWindow(pkg)} />
+
+ {pageApi && isInstalled && (
+
+ handleOpenClientWindow(pkg)} />
+
+ )}
+
+
+
+ );
+ })}
+
+
+ );
+};
+
+export default PackageManager;
diff --git a/src/package/store/index.ts b/src/package/store/index.ts
new file mode 100644
index 0000000..b0432de
--- /dev/null
+++ b/src/package/store/index.ts
@@ -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
;
+ getShopPackages: () => Promise;
+ uninstallPackage: (pkg: Package) => Promise;
+ installPackage: (pkg: Package) => Promise;
+};
+export const usePackageStore = create((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',
+ },
+];
diff --git a/src/package/style.css b/src/package/style.css
new file mode 100644
index 0000000..c908878
--- /dev/null
+++ b/src/package/style.css
@@ -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;
+}
\ No newline at end of file
diff --git a/src/page/Enter.css b/src/page/Enter.css
new file mode 100644
index 0000000..0a6aa0f
--- /dev/null
+++ b/src/page/Enter.css
@@ -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);
+ }
+}
diff --git a/src/page/Enter.tsx b/src/page/Enter.tsx
new file mode 100644
index 0000000..dce17af
--- /dev/null
+++ b/src/page/Enter.tsx
@@ -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 = `
+
+ `;
+
+ 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 (
+
+
+
+
+
+
Page Enter Configuration
+
+
+
+
+
+ );
+};
+
+export default Enter;
diff --git a/src/store/config.ts b/src/store/config.ts
new file mode 100644
index 0000000..bc1f2eb
--- /dev/null
+++ b/src/store/config.ts
@@ -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;
+ saveConfig: (config: any) => Promise;
+ pageApi: string;
+ setPageApi: (pageApi: string) => void;
+};
+
+export const useConfigStore = create((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 || '保存配置失败');
+ }
+ },
+}));
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 0000000..0088dd5
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,36 @@
+import { create } from 'zustand';
+import { client } from '@/modules/query';
+
+type ClientStore = {
+ isClient: boolean;
+ setIsClient: (isClient: boolean) => void;
+ checkClient: () => Promise;
+ mount: boolean;
+ setMount: (mount: boolean) => void;
+};
+export const useClientStore = create((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 });
+ },
+}));
diff --git a/vite.config.ts b/vite.config.ts
index e57c207..2e088b7 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -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, // 允许自签名证书
+ },
},
},
});