mirror of
				https://github-proxy.zxj.im/abearxiong/app-show-list
				synced 2025-11-04 21:48:34 +08:00 
			
		
		
		
	feat: add uploads app
This commit is contained in:
		
							
								
								
									
										5
									
								
								.npmrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.npmrc
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
 | 
			
		||||
@abearxiong:registry=https://npm.pkg.github.com
 | 
			
		||||
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
 | 
			
		||||
@build:registry=https://npm.xiongxiao.me
 | 
			
		||||
@kevisual:registry=https://npm.xiongxiao.me
 | 
			
		||||
							
								
								
									
										16
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								README.md
									
									
									
									
									
								
							@@ -2,3 +2,19 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
`/system/lib/app.js` 包函的模块是 `QueryRouterServer` 和 `Page` 和 `useConfigKey`
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## deploy app的过程
 | 
			
		||||
 | 
			
		||||
- `envision pack -p`
 | 
			
		||||
- `curl` 请求
 | 
			
		||||
```sh
 | 
			
		||||
curl https://envision.xiongxiao.me/api/router?path=micro-app&key=deploy -d '{
 | 
			
		||||
    "data": {
 | 
			
		||||
        "id": "17d105b8-9b6b-4cfc-9447-c815f24fe3d7",
 | 
			
		||||
        "key": "mark"
 | 
			
		||||
    }
 | 
			
		||||
}'
 | 
			
		||||
```
 | 
			
		||||
- 启动 `https://kevisual.xiongxiao.me/api/router?path=local-apps&key=updateStatus&appKey=mark&status=start`
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								package.json
									
									
									
									
									
								
							@@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "app-template",
 | 
			
		||||
  "name": "app-show",
 | 
			
		||||
  "version": "0.0.1",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
@@ -7,8 +7,8 @@
 | 
			
		||||
    "dev": "cross-env WEB_DEV=true vite",
 | 
			
		||||
    "build": "vite build",
 | 
			
		||||
    "preview": "vite preview",
 | 
			
		||||
    "prepub": "envision switchOrg user",
 | 
			
		||||
    "pub": "envision deploy ./dist -k app-template -v 0.0.1"
 | 
			
		||||
    "prepub": "envision switchOrg system",
 | 
			
		||||
    "pub": "envision deploy ./dist -k app-show -v 0.0.1"
 | 
			
		||||
  },
 | 
			
		||||
  "keywords": [],
 | 
			
		||||
  "author": "abearxiong <xiongxiao@xiongxiao.me>",
 | 
			
		||||
@@ -19,23 +19,33 @@
 | 
			
		||||
    "@emotion/styled": "^11.14.0",
 | 
			
		||||
    "@kevisual/query": "0.0.7-alpha.3",
 | 
			
		||||
    "@kevisual/system-ui": "^0.0.3",
 | 
			
		||||
    "@mui/icons-material": "^6.3.0",
 | 
			
		||||
    "@mui/material": "^6.3.0",
 | 
			
		||||
    "autoprefixer": "^10.4.20",
 | 
			
		||||
    "clsx": "^2.1.1",
 | 
			
		||||
    "dayjs": "^1.11.13",
 | 
			
		||||
    "lodash-es": "^4.17.21",
 | 
			
		||||
    "react-dom": "^19.0.0",
 | 
			
		||||
    "react-router": "^7.1.1",
 | 
			
		||||
    "react-router-dom": "^7.1.1",
 | 
			
		||||
    "tailwindcss": "^3.4.17",
 | 
			
		||||
    "zustand": "^5.0.2"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@build/tailwind": "1.0.2-alpha-2",
 | 
			
		||||
    "@emotion/css": "^11.13.5",
 | 
			
		||||
    "@kevisual/router": "0.0.6-alpha-4",
 | 
			
		||||
    "@kevisual/store": "0.0.1-alpha.9",
 | 
			
		||||
    "@kevisual/types": "^0.0.5",
 | 
			
		||||
    "@tailwindcss/aspect-ratio": "^0.4.2",
 | 
			
		||||
    "@tailwindcss/typography": "^0.5.15",
 | 
			
		||||
    "@types/react": "^19.0.2",
 | 
			
		||||
    "@types/react-dom": "^19.0.2",
 | 
			
		||||
    "@vitejs/plugin-basic-ssl": "^1.2.0",
 | 
			
		||||
    "@vitejs/plugin-react": "^4.3.4",
 | 
			
		||||
    "cross-env": "^7.0.3",
 | 
			
		||||
    "react": "^19.0.0",
 | 
			
		||||
    "tailwindcss-animate": "^1.0.7",
 | 
			
		||||
    "vite": "^6.0.6"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										870
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										870
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,10 +1,15 @@
 | 
			
		||||
import { createRoot } from 'react-dom/client';
 | 
			
		||||
export const App = () => {
 | 
			
		||||
import { BrowserRouter, Route, Routes } from 'react-router';
 | 
			
		||||
import { List } from './pages/List';
 | 
			
		||||
import { LayoutMain } from './layouts';
 | 
			
		||||
 | 
			
		||||
export const ReactApp = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='flex justify-center items-center h-screen bg-gray-200'>
 | 
			
		||||
      <h1 className='text-4xl font-bold text-gray-800'>Hello Vite + React!</h1>
 | 
			
		||||
    </div>
 | 
			
		||||
    <BrowserRouter basename='/system/app-show'>
 | 
			
		||||
      <Routes>
 | 
			
		||||
        <Route element={<LayoutMain />}>
 | 
			
		||||
          <Route path='/show-home' element={<List />} />
 | 
			
		||||
        </Route>
 | 
			
		||||
      </Routes>
 | 
			
		||||
    </BrowserRouter>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
createRoot(document.getElementById('root')!).render(<App />);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								src/app.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/app.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
 | 
			
		||||
import { useContextKey } from '@kevisual/store/config';
 | 
			
		||||
import { Page } from "@kevisual/store/page";
 | 
			
		||||
 | 
			
		||||
export const page = useContextKey('page', () => {
 | 
			
		||||
  return new Page({
 | 
			
		||||
    basename: '/system/app-show',
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
import { useStore } from '@/store/app';
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
 | 
			
		||||
const containerStyle = css`
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const itemStyle = css`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  min-width: 240px;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
  border: 1px solid #ccc;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
 | 
			
		||||
  background-color: #fff;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const List = () => {
 | 
			
		||||
  const store = useStore();
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    store.init();
 | 
			
		||||
  }, []);
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={containerStyle}>
 | 
			
		||||
      <div>
 | 
			
		||||
        {store.list.map((item) => {
 | 
			
		||||
          return (
 | 
			
		||||
            <div className={itemStyle} key={item.key}>
 | 
			
		||||
              <span>{item.key}</span>
 | 
			
		||||
              <span>{item.description}</span>
 | 
			
		||||
              <span>{item.status}</span>
 | 
			
		||||
              <span>{item.type}</span>
 | 
			
		||||
              <span>{item.version}</span>
 | 
			
		||||
            </div>
 | 
			
		||||
          );
 | 
			
		||||
        })}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										13
									
								
								src/layouts/LayoutMain.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/layouts/LayoutMain.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
import { Outlet } from 'react-router-dom';
 | 
			
		||||
import { LayoutMenu } from './LayoutMenu';
 | 
			
		||||
 | 
			
		||||
export const LayoutMain = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='bg-slate-200' style={{ display: 'flex', height: '100%' }}>
 | 
			
		||||
      <LayoutMenu />
 | 
			
		||||
      <main style={{ flexGrow: 1, height: '100%', overflow: 'auto' }}>
 | 
			
		||||
        <Outlet />
 | 
			
		||||
      </main>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										25
									
								
								src/layouts/LayoutMenu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/layouts/LayoutMenu.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material';
 | 
			
		||||
import InboxIcon from '@mui/icons-material/Inbox';
 | 
			
		||||
import MailIcon from '@mui/icons-material/Mail';
 | 
			
		||||
 | 
			
		||||
// ...existing code...
 | 
			
		||||
 | 
			
		||||
const menuItems = [
 | 
			
		||||
  { text: 'Inbox', icon: <InboxIcon /> },
 | 
			
		||||
  { text: 'Mail', icon: <MailIcon /> },
 | 
			
		||||
  // 可以根据需要添加更多菜单项
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const LayoutMenu = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Menu open={false} anchorOrigin={{ vertical: 'top', horizontal: 'left' }}>
 | 
			
		||||
      {menuItems.map((item, index) => (
 | 
			
		||||
        <MenuItem key={index}>
 | 
			
		||||
          <ListItemIcon>{item.icon}</ListItemIcon>
 | 
			
		||||
          <ListItemText primary={item.text} />
 | 
			
		||||
        </MenuItem>
 | 
			
		||||
      ))}
 | 
			
		||||
    </Menu>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										1
									
								
								src/layouts/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/layouts/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export * from './LayoutMain'
 | 
			
		||||
							
								
								
									
										48
									
								
								src/main.tsx
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								src/main.tsx
									
									
									
									
									
								
							@@ -1,29 +1,41 @@
 | 
			
		||||
import { useContextKey } from '@kevisual/store/config';
 | 
			
		||||
import { Page } from '@kevisual/store/page';
 | 
			
		||||
import { QueryRouterServer } from '@kevisual/router';
 | 
			
		||||
import { createRoot } from 'react-dom/client';
 | 
			
		||||
import { List } from './app/List';
 | 
			
		||||
import { List } from './pages/List';
 | 
			
		||||
import { List as UploadAppList } from './pages/upload-apps/List';
 | 
			
		||||
import { page } from './app';
 | 
			
		||||
 | 
			
		||||
import '@build/tailwind/main.css';
 | 
			
		||||
// import './tailwind.css';
 | 
			
		||||
 | 
			
		||||
export const initRoot = (renderRoot?: any) => {
 | 
			
		||||
  return useContextKey('root', () => {
 | 
			
		||||
    if (!renderRoot) {
 | 
			
		||||
      console.error('renderRoot is required');
 | 
			
		||||
    }
 | 
			
		||||
    return createRoot(renderRoot);
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
export const render = ({ renderRoot }) => {
 | 
			
		||||
  // renderRoot.innerHTML = `
 | 
			
		||||
  //   <h1>Hello, World!</h1>
 | 
			
		||||
  // `;
 | 
			
		||||
  const root = createRoot(renderRoot);
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  const root = initRoot(renderRoot);
 | 
			
		||||
  root.render(<List />);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const page = useContextKey('page', () => {
 | 
			
		||||
  return new Page({
 | 
			
		||||
    basename: '',
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
if (page) {
 | 
			
		||||
  page.addPage('/app-template', 'home');
 | 
			
		||||
  initRoot(document.getElementById('ai-root'));
 | 
			
		||||
  page.addPage('/', 'home');
 | 
			
		||||
  page.addPage('/local-apps', 'local-apps');
 | 
			
		||||
  page.subscribe('local-apps', () => {
 | 
			
		||||
    const root = initRoot();
 | 
			
		||||
    root.render(<List />);
 | 
			
		||||
  });
 | 
			
		||||
  page.addPage('/upload-apps', 'upload-apps');
 | 
			
		||||
  page.subscribe('upload-apps', () => {
 | 
			
		||||
    const root = initRoot();
 | 
			
		||||
    root.render(<UploadAppList />);
 | 
			
		||||
  });
 | 
			
		||||
  page.subscribe('home', () => {
 | 
			
		||||
    render({
 | 
			
		||||
      renderRoot: document.getElementById('ai-root'),
 | 
			
		||||
    });
 | 
			
		||||
    page.navigate('/local-apps');
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -34,7 +46,7 @@ const app = useContextKey('app', () => {
 | 
			
		||||
if (app) {
 | 
			
		||||
  app
 | 
			
		||||
    .route({
 | 
			
		||||
      path: 'app-template',
 | 
			
		||||
      path: 'show-home',
 | 
			
		||||
      key: 'render',
 | 
			
		||||
    })
 | 
			
		||||
    .define(async (ctx) => {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										105
									
								
								src/pages/List.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/pages/List.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,105 @@
 | 
			
		||||
import { useStore } from '@/store/app';
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import clsx from 'clsx';
 | 
			
		||||
import { Page } from '@kevisual/store/page';
 | 
			
		||||
import { page } from '@/app';
 | 
			
		||||
import { useOperateStore } from '@/store/operate';
 | 
			
		||||
import { message } from '@/modules/message';
 | 
			
		||||
 | 
			
		||||
const containerStyle = css`
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const itemStyle = css`
 | 
			
		||||
  display: flex;
 | 
			
		||||
  min-width: 240px;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
  border: 1px solid #ccc;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
 | 
			
		||||
  background-color: #fff;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export const List = () => {
 | 
			
		||||
  const store = useStore();
 | 
			
		||||
  const operateStore = useOperateStore();
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    store.init();
 | 
			
		||||
  }, []);
 | 
			
		||||
  return (
 | 
			
		||||
    <div className={clsx(containerStyle, 'h-full w-full ')}>
 | 
			
		||||
      <div
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          page.navigate('/upload-apps');
 | 
			
		||||
        }}>
 | 
			
		||||
        To Upload Apps
 | 
			
		||||
      </div>
 | 
			
		||||
      <h1 className='text-2xl font-bold'>Local Apps</h1>
 | 
			
		||||
      <div>
 | 
			
		||||
        <div className='local-apps-headers flex flex-row-reverse'>
 | 
			
		||||
          <button
 | 
			
		||||
            className='px-4 py-2 mt-2 text-white bg-blue-500 rounded hover:bg-blue-700'
 | 
			
		||||
            onClick={async () => {
 | 
			
		||||
              const res = await operateStore.detect();
 | 
			
		||||
              if (res.code === 200) {
 | 
			
		||||
                store.init();
 | 
			
		||||
              }
 | 
			
		||||
            }}>
 | 
			
		||||
            Detect
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='flex flex-row flex-wrap gap-4'>
 | 
			
		||||
          {store.list.map((item) => {
 | 
			
		||||
            return (
 | 
			
		||||
              <div className={itemStyle} key={item.key}>
 | 
			
		||||
                <span>{item.key}</span>
 | 
			
		||||
                <span>{item.description}</span>
 | 
			
		||||
                <span>{item.status}</span>
 | 
			
		||||
                <span>{item.type}</span>
 | 
			
		||||
                <span>{item.version}</span>
 | 
			
		||||
                <div className='flex flex-row gap-2'>
 | 
			
		||||
                  <button
 | 
			
		||||
                    className='px-4 py-2 mt-2 text-white bg-blue-500 rounded hover:bg-blue-700'
 | 
			
		||||
                    onClick={async () => {
 | 
			
		||||
                      if (item.status !== 'stop') {
 | 
			
		||||
                        const res = await operateStore.updateStatus({ status: 'stop', appKey: item.key });
 | 
			
		||||
                        if (res.code === 200) {
 | 
			
		||||
                          store.init();
 | 
			
		||||
                        }
 | 
			
		||||
                      }
 | 
			
		||||
                    }}>
 | 
			
		||||
                    Stop
 | 
			
		||||
                  </button>
 | 
			
		||||
                  <button
 | 
			
		||||
                    className='px-4 py-2 mt-2 text-white bg-blue-500 rounded hover:bg-blue-700'
 | 
			
		||||
                    onClick={async () => {
 | 
			
		||||
                      if (item.status !== 'running') {
 | 
			
		||||
                        const res = await operateStore.updateStatus({ status: 'start', appKey: item.key });
 | 
			
		||||
                        if (res.code === 200) {
 | 
			
		||||
                          store.init();
 | 
			
		||||
                        }
 | 
			
		||||
                      }
 | 
			
		||||
                    }}>
 | 
			
		||||
                    Start
 | 
			
		||||
                  </button>
 | 
			
		||||
 | 
			
		||||
                  <button
 | 
			
		||||
                    className='px-4 py-2 mt-2 text-white bg-blue-500 rounded hover:bg-blue-700'
 | 
			
		||||
                    onClick={async () => {
 | 
			
		||||
                      const res = await store.deleteData(item.key, { refresh: true });
 | 
			
		||||
                      //
 | 
			
		||||
                    }}>
 | 
			
		||||
                    Delete
 | 
			
		||||
                  </button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            );
 | 
			
		||||
          })}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										54
									
								
								src/pages/upload-apps/List.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/pages/upload-apps/List.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
import { useAppUploadStore } from '@/store/app-upload';
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
import { KeyEditModal } from './modal/Edit';
 | 
			
		||||
import { page } from '@/app';
 | 
			
		||||
import dayjs from 'dayjs';
 | 
			
		||||
export const List = () => {
 | 
			
		||||
  const store = useAppUploadStore();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    store.init();
 | 
			
		||||
  }, []);
 | 
			
		||||
  const onDeploy = (item: { id: string; [key: string]: any }) => {
 | 
			
		||||
    console.log(item);
 | 
			
		||||
    store.setFormData({
 | 
			
		||||
      id: item.id,
 | 
			
		||||
      key: '',
 | 
			
		||||
    });
 | 
			
		||||
    store.setShowModal(true);
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='p-4'>
 | 
			
		||||
      <div
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          page.navigate('/local-apps');
 | 
			
		||||
        }}>
 | 
			
		||||
        To Local Apps
 | 
			
		||||
      </div>
 | 
			
		||||
      <h1 className='text-2xl font-bold'>Upload Apps</h1>
 | 
			
		||||
      <div className='flex flex-wrap gap-4'>
 | 
			
		||||
        {store.list.map((item) => {
 | 
			
		||||
          return (
 | 
			
		||||
            <div className='p-4 mb-4 border rounded shadow bg-white w-[300px] flex flex-col' key={item.id}>
 | 
			
		||||
              <span>{item.title}</span>
 | 
			
		||||
              <span>{item.description}</span>
 | 
			
		||||
              <span>{item.tags.join(', ')}</span>
 | 
			
		||||
              <span>{item.type}</span>
 | 
			
		||||
              <span>{dayjs(item.updatedTime).format('YYYY-MM-DD HH:mm:ss')}</span>
 | 
			
		||||
              <div>
 | 
			
		||||
                <button
 | 
			
		||||
                  className='px-4 py-2 mt-2 text-white bg-blue-500 rounded hover:bg-blue-700'
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    onDeploy(item);
 | 
			
		||||
                  }}>
 | 
			
		||||
                  Deploy
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          );
 | 
			
		||||
        })}
 | 
			
		||||
      </div>
 | 
			
		||||
      <KeyEditModal />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										40
									
								
								src/pages/upload-apps/modal/Edit.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/pages/upload-apps/modal/Edit.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
import { useAppUploadStore } from '@/store/app-upload';
 | 
			
		||||
import { Modal } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
export const KeyEditModal = () => {
 | 
			
		||||
  const store = useAppUploadStore();
 | 
			
		||||
  const onSave = () => {
 | 
			
		||||
    console.log('save');
 | 
			
		||||
    store.setShowModal(false);
 | 
			
		||||
    const fromData = store.formData;
 | 
			
		||||
    store.pubApp(fromData);
 | 
			
		||||
    console.log(fromData);
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      className='fixed flex items-center justify-center'
 | 
			
		||||
      open={store.showModal}
 | 
			
		||||
      onClose={() => store.setShowModal(false)}
 | 
			
		||||
    >
 | 
			
		||||
      <div className='p-4 bg-white w-[400px]'>
 | 
			
		||||
        <h1 className='text-2xl font-bold'>Edit Key</h1>
 | 
			
		||||
        <div className='mt-4'>
 | 
			
		||||
          <label className='block'>Key</label>
 | 
			
		||||
          <input
 | 
			
		||||
            type='text'
 | 
			
		||||
            className='w-full p-2 border rounded'
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              const value = e.target.value;
 | 
			
		||||
              store.setFormData({ ...store.formData, key: value });
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className='mt-4'>
 | 
			
		||||
          <button className='px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-700' onClick={onSave}>
 | 
			
		||||
            Deploy
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										107
									
								
								src/store/app-upload.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/store/app-upload.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
import { create } from 'zustand';
 | 
			
		||||
import { query } from '@/modules/query';
 | 
			
		||||
import { message } from '@/modules/message';
 | 
			
		||||
 | 
			
		||||
type Store = {
 | 
			
		||||
  list: any[];
 | 
			
		||||
  setList: (list: any[]) => void;
 | 
			
		||||
  data: any;
 | 
			
		||||
  setData: (data: any) => void;
 | 
			
		||||
  loading: boolean;
 | 
			
		||||
  setLoading: (loading: boolean) => void;
 | 
			
		||||
  formData: any;
 | 
			
		||||
  setFormData: (data: any) => void;
 | 
			
		||||
  getList: () => Promise<any>;
 | 
			
		||||
  init: () => Promise<void>;
 | 
			
		||||
  getData: (id: number) => Promise<any>;
 | 
			
		||||
  updateData: (data: any, opts?: { refresh?: boolean }) => Promise<any>;
 | 
			
		||||
  deleteData: (id: number, opts?: { refresh?: boolean }) => Promise<any>;
 | 
			
		||||
  showModal: boolean;
 | 
			
		||||
  setShowModal: (showModal: boolean) => void;
 | 
			
		||||
  pubApp: (data: any) => Promise<any>;
 | 
			
		||||
};
 | 
			
		||||
export const useAppUploadStore = create<Store>((set, get) => ({
 | 
			
		||||
  list: [],
 | 
			
		||||
  setList: (list) => set({ list }),
 | 
			
		||||
  data: null,
 | 
			
		||||
  setData: (data) => set({ data }),
 | 
			
		||||
  loading: false,
 | 
			
		||||
  setLoading: (loading) => set({ loading }),
 | 
			
		||||
  formData: null,
 | 
			
		||||
  setFormData: (formData) => set({ formData }),
 | 
			
		||||
  getList: async () => {
 | 
			
		||||
    set({ loading: true });
 | 
			
		||||
    const res = await query.post({ path: 'micro-app-upload', key: 'list' });
 | 
			
		||||
    set({ loading: false });
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
      set({ list: res.data });
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
  init: async () => {
 | 
			
		||||
    await get().getList();
 | 
			
		||||
  },
 | 
			
		||||
  getData: async (id) => {
 | 
			
		||||
    set({ loading: true });
 | 
			
		||||
    const res = await query.post({
 | 
			
		||||
      path: 'micro-app-upload',
 | 
			
		||||
      key: 'get',
 | 
			
		||||
      id,
 | 
			
		||||
    });
 | 
			
		||||
    set({ loading: false });
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
      const data = res.data;
 | 
			
		||||
      set({ data });
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
  updateData: async (data, opts = { refresh: true }) => {
 | 
			
		||||
    set({ loading: true });
 | 
			
		||||
    const res = await query.post({
 | 
			
		||||
      path: 'micro-app-upload',
 | 
			
		||||
      key: 'update',
 | 
			
		||||
      data,
 | 
			
		||||
    });
 | 
			
		||||
    set({ loading: false });
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
      set({ data: res.data });
 | 
			
		||||
    }
 | 
			
		||||
    if (opts.refresh) {
 | 
			
		||||
      await get().getList();
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
  deleteData: async (id, opts = { refresh: true }) => {
 | 
			
		||||
    set({ loading: true });
 | 
			
		||||
    const res = await query.post({
 | 
			
		||||
      path: 'micro-app-upload',
 | 
			
		||||
      key: 'delete',
 | 
			
		||||
      id,
 | 
			
		||||
    });
 | 
			
		||||
    set({ loading: false });
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
      set({ data: null });
 | 
			
		||||
    }
 | 
			
		||||
    if (opts.refresh) {
 | 
			
		||||
      await get().getList();
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
  showModal: false,
 | 
			
		||||
  setShowModal: (showModal) => set({ showModal }),
 | 
			
		||||
  pubApp: async (data) => {
 | 
			
		||||
    set({ loading: true });
 | 
			
		||||
    const res = await query.post({
 | 
			
		||||
      path: 'micro-app',
 | 
			
		||||
      key: 'deploy',
 | 
			
		||||
      data,
 | 
			
		||||
    });
 | 
			
		||||
    set({ loading: false });
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
      message.success('发布成功');
 | 
			
		||||
      set({ data: null });
 | 
			
		||||
    } else {
 | 
			
		||||
      message.error(res.message || '发布失败');
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
}));
 | 
			
		||||
@@ -72,7 +72,7 @@ export const useStore = create<Store>((set, get) => ({
 | 
			
		||||
    const res = await query.post({
 | 
			
		||||
      path: 'local-apps',
 | 
			
		||||
      key: 'delete',
 | 
			
		||||
      id,
 | 
			
		||||
      appKey: id,
 | 
			
		||||
    });
 | 
			
		||||
    set({ loading: false });
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,53 +2,66 @@ import { create } from 'zustand';
 | 
			
		||||
import { query } from '@/modules/query';
 | 
			
		||||
import { message } from '@/modules/message';
 | 
			
		||||
interface OperateStore {
 | 
			
		||||
  updateStatus: () => Promise<void>;
 | 
			
		||||
  detect: () => Promise<void>;
 | 
			
		||||
  download: () => Promise<void>;
 | 
			
		||||
  updateStatus: (data: { status: 'stop' | 'start'; appKey: string }) => Promise<any>;
 | 
			
		||||
  detect: () => Promise<any>;
 | 
			
		||||
  download: () => Promise<any>;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 微应用的上传和卸载和部署
 | 
			
		||||
   */
 | 
			
		||||
  upload: () => Promise<void>;
 | 
			
		||||
  unload: () => Promise<void>;
 | 
			
		||||
  deploy: () => Promise<void>;
 | 
			
		||||
  upload: () => Promise<any>;
 | 
			
		||||
  unload: () => Promise<any>;
 | 
			
		||||
  deploy: () => Promise<any>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useOperateStore = create<OperateStore>((set) => ({
 | 
			
		||||
  updateStatus: async () => {
 | 
			
		||||
  updateStatus: async (data) => {
 | 
			
		||||
    const res = await query.post({
 | 
			
		||||
      path: 'local-apps',
 | 
			
		||||
      key: 'updateStatus',
 | 
			
		||||
      status: data.status,
 | 
			
		||||
      appKey: data.appKey,
 | 
			
		||||
    });
 | 
			
		||||
    if (res.code === 200) {
 | 
			
		||||
      message.success('操作成功');
 | 
			
		||||
    } else {
 | 
			
		||||
      message.error(res.message || '操作失败');
 | 
			
		||||
    }
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
  detect: async () => {
 | 
			
		||||
    const res = await query.post({
 | 
			
		||||
      path: 'local-apps',
 | 
			
		||||
      key: 'detect',
 | 
			
		||||
    });
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
  download: async () => {
 | 
			
		||||
    const res = await query.post({
 | 
			
		||||
      path: 'local-apps',
 | 
			
		||||
      key: 'download',
 | 
			
		||||
    });
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
  upload: async () => {
 | 
			
		||||
    const res = await query.post({
 | 
			
		||||
      path: 'micro-apps',
 | 
			
		||||
      key: 'upload',
 | 
			
		||||
    });
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
  unload: async () => {
 | 
			
		||||
    const res = await query.post({
 | 
			
		||||
      path: 'micro-apps',
 | 
			
		||||
      key: 'unload',
 | 
			
		||||
    });
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
  deploy: async () => {
 | 
			
		||||
    const res = await query.post({
 | 
			
		||||
      path: 'micro-apps',
 | 
			
		||||
      key: 'deploy',
 | 
			
		||||
    });
 | 
			
		||||
    return res;
 | 
			
		||||
  },
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								src/tailwind.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/tailwind.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
@tailwind base;
 | 
			
		||||
@tailwind components;
 | 
			
		||||
@tailwind utilities;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										47
									
								
								tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
import path from 'path';
 | 
			
		||||
 | 
			
		||||
const root = path.resolve(process.cwd());
 | 
			
		||||
const contents = ['./src/**/*.{ts,tsx,html}', './src/**/*.css']
 | 
			
		||||
const content = contents.map((item) => path.join(root, item));
 | 
			
		||||
/** @type {import('tailwindcss').Config} */
 | 
			
		||||
export default {
 | 
			
		||||
  // darkMode: ['class'],
 | 
			
		||||
  content: content,
 | 
			
		||||
  plugins: [
 | 
			
		||||
    require('@tailwindcss/aspect-ratio'), //
 | 
			
		||||
    require('@tailwindcss/typography'),
 | 
			
		||||
    require('tailwindcss-animate'),
 | 
			
		||||
    require('@build/tailwind'),
 | 
			
		||||
  ],
 | 
			
		||||
  theme: {
 | 
			
		||||
    extend: {
 | 
			
		||||
      fontFamily: {
 | 
			
		||||
        mon: ['Montserrat', 'sans-serif'], // 定义自定义字体族
 | 
			
		||||
        rob: ['Roboto', 'sans-serif'],
 | 
			
		||||
        int: ['Inter', 'sans-serif'],
 | 
			
		||||
        orb: ['Orbitron', 'sans-serif'],
 | 
			
		||||
        din: ['DIN', 'sans-serif'],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    screen: {
 | 
			
		||||
      sm: '640px',
 | 
			
		||||
      // => @media (min-width: 640px) { ... }
 | 
			
		||||
 | 
			
		||||
      md: '768px',
 | 
			
		||||
      // => @media (min-width: 768px) { ... }
 | 
			
		||||
 | 
			
		||||
      lg: '1024px',
 | 
			
		||||
      // => @media (min-width: 1024px) { ... }
 | 
			
		||||
 | 
			
		||||
      xl: '1280px',
 | 
			
		||||
      // => @media (min-width: 1280px) { ... }
 | 
			
		||||
 | 
			
		||||
      '2xl': '1536px',
 | 
			
		||||
      // => @media (min-width: 1536px) { ... }
 | 
			
		||||
      '3xl': '1920px',
 | 
			
		||||
      // => @media (min-width: 1920) { ... }
 | 
			
		||||
      '4xl': '2560px',
 | 
			
		||||
      // => @media (min-width: 2560) { ... }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -3,6 +3,9 @@ import react from '@vitejs/plugin-react'
 | 
			
		||||
// import basicSsl from '@vitejs/plugin-basic-ssl';
 | 
			
		||||
import dayjs from 'dayjs';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import tailwindcss from 'tailwindcss';
 | 
			
		||||
import autoprefixer from 'autoprefixer';
 | 
			
		||||
import nesting from 'tailwindcss/nesting';
 | 
			
		||||
 | 
			
		||||
const isDev = process.env.NODE_ENV === 'development';
 | 
			
		||||
const BUILD_TIME = dayjs().format('YYYY-MM-DD HH:mm:ss');
 | 
			
		||||
@@ -10,6 +13,13 @@ console.log('process', isDev, process.env.WEB_DEV)
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  // plugins: [basicSsl()],
 | 
			
		||||
  plugins: [react()],
 | 
			
		||||
  css: {
 | 
			
		||||
    postcss: {
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      plugins: [nesting, tailwindcss, autoprefixer],
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  base: isDev ? '/' : './',
 | 
			
		||||
  resolve: {
 | 
			
		||||
    alias: {
 | 
			
		||||
      '@': path.resolve(__dirname, './src'),
 | 
			
		||||
@@ -32,13 +42,13 @@ export default defineConfig({
 | 
			
		||||
    proxy: {
 | 
			
		||||
      '/api': {
 | 
			
		||||
        target: 'https://kevisual.xiongxiao.me',
 | 
			
		||||
        // target: 'http://localhost:9787',
 | 
			
		||||
        changeOrigin: true,
 | 
			
		||||
        rewrite: (path) => path.replace(/^\/api/, '/api'),
 | 
			
		||||
      },
 | 
			
		||||
      '/system': {
 | 
			
		||||
      '/system/lib': {
 | 
			
		||||
        target: 'https://kevisual.xiongxiao.me',
 | 
			
		||||
        changeOrigin: true,
 | 
			
		||||
        rewrite: (path) => path.replace(/^\/system/, '/system'),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user