初始化

This commit is contained in:
yj 2024-07-02 16:43:46 +08:00
parent a461703004
commit 130064b8ba
92 changed files with 29242 additions and 0 deletions

7
.env.development Normal file
View File

@ -0,0 +1,7 @@
#基础API 绝对的
VITE_BASE_URL_API = 'http://192.168.2.9:6500'
VITE_BASE_URL_DRAW_API = 'http://192.168.2.9:6555'
#当前IP 相对的
VITE_BASE_CURRENT_API = '.'
#开发环境
VITE_ENV = 'development'

7
.env.production Normal file
View File

@ -0,0 +1,7 @@
#基础API 绝对的
VITE_BASE_URL_API = 'http://192.168.2.9:6500'
VITE_BASE_URL_DRAW_API = 'http://192.168.2.9:6555'
#当前IP 相对的
VITE_BASE_CURRENT_API = '.'
#生产环境
VITE_ENV = 'production'

7
.env.test Normal file
View File

@ -0,0 +1,7 @@
#基础API 绝对的
VITE_BASE_URL_API = 'http://192.168.2.9:6500'
VITE_BASE_URL_DRAW_API = 'http://192.168.2.9:6555'
#当前IP 相对的
VITE_BASE_CURRENT_API = '.'
#测试环境
VITE_ENV = 'test'

18
.eslintrc.cjs Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
out/

30
README.md Normal file
View File

@ -0,0 +1,30 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list

58
forge.config.js Normal file
View File

@ -0,0 +1,58 @@
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
module.exports = {
packagerConfig: {
"name": "MyElectronApp", // 应用程序的名称
"files": [],
"productName": "My Electron App", // 产品名称(用于生成安装包的名称)
// "icon": "path/to/icon.png", // 应用程序的图标路径
"out": "build/", // 输出目录的路径
"overwrite": true, // 是否覆盖已存在的打包文件
"asar": true, // 是否使用asar打包格式
"version": "0.0.1", // 应用程序版本号
// "copyright": "Copyright © 2023", // 版权信息
// "ignore": [ // 不需要打包的文件和文件夹的路径列表
// ".git",
// ".vscode",
// "node_modules/.cache",
// "src"
// ],
},
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {}
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
},
{
name: '@electron-forge/maker-deb',
config: {},
},
{
name: '@electron-forge/maker-rpm',
config: {},
},
],
plugins: [
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {},
},
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
};

19
index.html Normal file
View File

@ -0,0 +1,19 @@
<!doctype html>
<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" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self'">
<!-- <meta http-equiv="Content-Security-Policy"
content="script-src 'self' https://www.google-analytics.com; style-src 'self' https://animate.style"> -->
<title>智汇享</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

182
main.js Normal file
View File

@ -0,0 +1,182 @@
const { app, systemPreferences, BrowserWindow, screen, Tray, nativeImage, Menu, ipcMain } = require('electron');
const path = require('node:path')
app.allowRendererProcessReuse = false;
let mainWindow = null;
let isMaximized = false;
class AppWindow extends BrowserWindow {
constructor(config) {
const basicConfig = {
webPreferences: {
contextIsolation: true,
nodeIntegration: true,
enableRemoteModule: true,
nodeIntegrationInWorker: true,
allowMediaDevices: true,
preload: path.join(__dirname, 'preload.js')
},
show: false,
frame: false,
icon: '',
icon: '',
backgroundColor: '#00000000',
transparent: true,
};
const finalConfig = { ...basicConfig, ...config };
super(finalConfig);
const env = process.argv.find((arg) => arg.startsWith('--env='))?.split('=')[1];
if (env) {
// 开发
this.loadURL('http://localhost:3000');
} else {
// 打包
this.loadFile(path.resolve(__dirname, './dist/index.html'));
}
this.once('ready-to-show', () => {
this.show();
});
}
}
function showWindow() {
// 如果主窗口已经存在但被最小化了,则恢复显示
if (mainWindow && mainWindow.isMinimized()) {
mainWindow.show();
}
// 如果主窗口已存在但不是焦点窗口,则将其置为焦点
if (mainWindow && !mainWindow.isFocused()) {
mainWindow.show();
mainWindow.focus();
}
// 如果主窗口还没有被创建,则创建它
if (!mainWindow) {
createWindow();
}
}
function createTray() {
const iconPath = `${__dirname}/src/assets/icon.png`;
const trayIcon = nativeImage.createFromPath(iconPath);
const tray = new Tray(trayIcon);
const contextMenu = Menu.buildFromTemplate([
{
label: '打开', click: () => {
showWindow()
},
icon: iconPath,
},
{
label: '退出', click: () => {
app.quit();
mainWindow = null;
},
icon: iconPath,
},
{
label: '退出到系统托盘', click: () => {
mainWindow.hide();
},
icon: iconPath,
},
]);
tray.setToolTip('智汇享');
tray.setContextMenu(contextMenu);
tray.on('click', () => {
if (mainWindow.isVisible()) {
mainWindow.hide()
} else {
mainWindow.show()
}
});
}
function createWindow() {
mainWindow = new AppWindow();
mainWindow.focus();
}
app.on('ready', () => {
createWindow()
createTray()
// 监听f12打开控制台
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.key === 'F12') {
mainWindow.webContents.openDevTools()
}
});
// 监听移动
mainWindow.on('move', () => {
// 如果是全屏自动恢复到上次窗口大小
if (isMaximized) {
mainWindow.setResizable(true)
mainWindow.unmaximize()
isMaximized = false;
}
if (mainWindow.isMaximized()) {
isMaximized = true;
}
});
// 放大缩小退出窗口
ipcMain.handle('setViewStatus', (event, status) => {
switch (status) {
case 'quit':
app.quit();
mainWindow = null;
break;
case 'maximize':
mainWindow.maximize()
mainWindow.setResizable(false)
break;
case 'unmaximize':
mainWindow.setResizable(true)
mainWindow.unmaximize()
break;
case 'minimize':
mainWindow.minimize()
break;
}
});
// 导出是否全屏
ipcMain.handle('getIsMaximized', () => {
return mainWindow.isMaximized();
});
// 设置桌面应用基础属性
ipcMain.handle('setMainWindowSize', (event, config) => {
// 设置最小窗口尺寸
mainWindow.setMinimumSize(config.width, config.height);
// 设置最大尺寸
const primaryDisplay = screen.getPrimaryDisplay()
const { width, height } = primaryDisplay.workAreaSize
if (config.key === 'login') {
mainWindow.setMaximumSize(config.width, config.height);
} else {
mainWindow.setMaximumSize(width, height);
}
// 设置窗口尺寸
mainWindow.setSize(config.width, config.height)
// 设置窗口位置使其居中于当前屏幕
const display = screen.getDisplayMatching({ ...mainWindow.getBounds() });
const x = Math.round((display.workArea.width - mainWindow.getSize()[0]) / 2);
const y = Math.round((display.workArea.height - mainWindow.getSize()[1]) / 2);
mainWindow.setPosition(x, y);
});
});
// 检查并获取设备权限
async function checkAndApplyDeviceAccessPrivilege() {
// 检查并获取摄像头权限
const cameraPrivilege = systemPreferences.getMediaAccessStatus('camera');
if (cameraPrivilege !== 'granted') {
await systemPreferences.askForMediaAccess('camera');
}
// 检查并获取麦克风权限
const micPrivilege = systemPreferences.getMediaAccessStatus('microphone');
if (micPrivilege !== 'granted') {
await systemPreferences.askForMediaAccess('microphone');
}
}
checkAndApplyDeviceAccessPrivilege();

25461
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

63
package.json Normal file
View File

@ -0,0 +1,63 @@
{
"name": "multi.person.meeting",
"private": true,
"version": "0.0.0",
"main": "main.js",
"authors": "yj",
"description": "test",
"scripts": {
"dev": "concurrently \"electron . --env=development\" \"cross-env BROWSER=none vite\"",
"test": "concurrently \"electron . --env=test\" \"cross-env BROWSER=none vite\"",
"prod": "concurrently \"electron . --env=production\" \"cross-env BROWSER=none vite\"",
"build": "vite build --mode development",
"build:test": "vite build --mode test",
"build:prod": "vite build --mode production",
"preview": "vite preview",
"start": "electron-forge start",
"package": "electron-forge package",
"make": "vite build --mode development & electron-forge make",
"make:test": "vite build --mode test & electron-forge make",
"make:prod": "vite build --mode production & electron-forge make"
},
"agora_electron": {
"prebuilt": true
},
"dependencies": {
"@ant-design/icons": "^5.3.7",
"@types/node": "^20.14.9",
"agora-electron-sdk": "^4.3.2",
"antd": "^5.18.2",
"axios": "^1.7.2",
"dayjs": "^1.11.11",
"electron-squirrel-startup": "^1.0.1",
"path": "^0.12.7",
"postcss-px-to-viewport-8-plugin": "^1.2.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1",
"sass": "^1.77.5",
"swiper": "^11.1.4",
"tldraw": "^2.2.0"
},
"devDependencies": {
"@electron-forge/cli": "^7.4.0",
"@electron-forge/maker-deb": "^7.4.0",
"@electron-forge/maker-rpm": "^7.4.0",
"@electron-forge/maker-squirrel": "^7.4.0",
"@electron-forge/maker-zip": "^7.4.0",
"@electron-forge/plugin-auto-unpack-natives": "^7.4.0",
"@electron-forge/plugin-fuses": "^7.4.0",
"@electron/fuses": "^1.8.0",
"@types/react": "^17.0.33",
"@types/react-dom": "^17.0.25",
"@vitejs/plugin-react": "^1.0.7",
"concurrently": "^7.6.0",
"cross-env": "^7.0.3",
"electron": "^17.4.11",
"typescript": "^4.5.4",
"vite": "^2.8.0"
},
"config": {
"forge": "./forge.config.js"
}
}

163
preload.js Normal file
View File

@ -0,0 +1,163 @@
// 在 preload 脚本中。
const { ipcRenderer, contextBridge } = require('electron')
const {
createAgoraRtcEngine,
ClientRoleType,
VideoSourceType,
VideoViewSetupMode,
ScreenCaptureSourceType,
RenderModeType,
} = require("agora-electron-sdk");
const agoraAonfig = require('./src/utils/package/agoraConfig')
const rtcEngine = createAgoraRtcEngine();
rtcEngine.initialize({
appId: agoraAonfig.appid,
});
let videoID = '';
const getDom = () => {
return document.getElementById(videoID);
}
const EventHandles = {
// 监听本地用户加入频道事件
onJoinChannelSuccess: ({ channelId, localUid }, elapsed) => {
const dom = document.getElementById('video1')
// 本地用户加入频道后,设置本地视频窗口
rtcEngine.setupLocalVideo({
renderMode: RenderModeType.RenderModeFit,
sourceType: VideoSourceType.VideoSourceScreen,
uid: localUid,
view: dom,
// view: getDom(),
setupMode: VideoViewSetupMode.VideoViewSetupAdd,
});
},
// 监听远端用户加入频道事件
onUserJoined: ({ channelId, localUid }, remoteUid, elapsed) => {
console.log('远端用户 ' + remoteUid + ' 已加入');
const dom = document.getElementById('video2')
// 远端用户加入频道后,设置远端视频窗口
rtcEngine.setupRemoteVideo(
{
renderMode: RenderModeType.RenderModeFit,
sourceType: VideoSourceType.VideoSourceRemote,
uid: remoteUid,
view: dom,
setupMode: VideoViewSetupMode.VideoViewSetupAdd,
},
{ channelId },
);
},
// 监听用户离开频道事件
onUserOffline: (info, remoteUid, reason) => {
console.log('远端用户 ' + remoteUid + ' 已离开频道');
// 远端用户离开频道后,关闭远端视频窗口
const dom = document.getElementById('video2')
rtcEngine.setupRemoteVideo(
{
renderMode: RenderModeType.RenderModeFit,
sourceType: VideoSourceType.VideoSourceRemote,
uid: remoteUid,
view: dom,
setupMode: VideoViewSetupMode.VideoViewSetupRemove,
},
);
},
};
rtcEngine.registerEventHandler(EventHandles);
contextBridge.exposeInMainWorld(
'electron',
{
// 桌面捕获音频和视频的媒体源的信息
getDesktopCapturerVideo: async () => {
return rtcEngine.getScreenCaptureSources({ width: 300, height: 300 }, { width: 300, height: 300 }, true);
},
// 设置视频播放
setDesktopCapturerVideo: (targetSource) => {
rtcEngine.stopScreenCapture()
if (
targetSource.type ===
ScreenCaptureSourceType.ScreencapturesourcetypeScreen
) {
rtcEngine.startScreenCaptureByDisplayId(
targetSource.sourceId,
{},
{
windowFocus: true,
enableHighLight: true,
highLightColor: 0xFF99CC00,
}
);
} else {
rtcEngine.startScreenCaptureByWindowId(
targetSource.sourceId,
{},
{
windowFocus: true,
enableHighLight: true,
highLightColor: 0xFF99CC00,
}
);
}
videoID = `vidoe-${123}-${agoraAonfig.channelId}`;
rtcEngine.joinChannel(agoraAonfig.token, agoraAonfig.channelId, 123, {
autoSubscribeAudio: true, //设置是否自动订阅所有音频流
autoSubscribeVideo: true, //设置是否自动订阅所有视频流
publishMicrophoneTrack: false, //设置是否发布麦克风采集到的音频
publishCameraTrack: false, //设置是否发布摄像头采集的视频
clientRoleType: ClientRoleType.ClientRoleBroadcaster, //用户角色 1主播 2观众
publishScreenTrack: true, //设置是否发布屏幕采集的视频
});
},
// 加入频道
setJoinChannel: () => {
videoID = `vidoe-${234}-${agoraAonfig.channelId}`;
rtcEngine.joinChannelEx(agoraAonfig.token, {
channelId: agoraAonfig.channelId,
localUid: 234,
}, {
autoSubscribeAudio: true, //设置是否自动订阅所有音频流
autoSubscribeVideo: true, //设置是否自动订阅所有视频流
publishMicrophoneTrack: false, //设置是否发布麦克风采集到的音频
publishCameraTrack: false, //设置是否发布摄像头采集的视频
clientRoleType: ClientRoleType.ClientRoleAudience, //用户角色 1主播 2观众
publishScreenTrack: true, //设置是否发布屏幕采集的视频
});
},
// 获取当前生成的视频id
getVideoId: () => {
return videoID;
},
// 获取摄像头以及音频内容
getCameraAndMicrophoneMedia: async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
stream.getTracks().forEach(track => track.stop());
return stream
} catch (error) {
return false;
}
},
// 设置窗口大小
setMainWindowSize: (config) => {
ipcRenderer.invoke('setMainWindowSize', { ...config })
},
// 设置窗口状态
setViewStatus: (status) => {
ipcRenderer.invoke('setViewStatus', status)
},
// 获取当前是否全屏
getIsMaximized: () => {
return ipcRenderer.invoke('getIsMaximized')
}
}
)

66
src/App.tsx Normal file
View File

@ -0,0 +1,66 @@
import { useEffect, useState } from "react";
import '@/utils/styles/App.scss'
import { Route, Routes, useNavigate, Navigate } from 'react-router-dom';
import Home from '@/page/Home/index'
import Index from '@/page/Home/Index/index'
import User from '@/page/Home/User/index'
import Login from '@/page/Login/index'
import Meeting from '@/page/Meeting/index'
import NotFound from '@/page/NotFound/index'
import { storage } from '@/utils'
const App: React.FC = () => {
const navigate = useNavigate();
const [_windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
if (storage.getItem('TOKEN')) {
window.electron.setMainWindowSize({
width: 1200,
height: 800,
})
navigate('/home')
} else {
window.electron.setMainWindowSize({
width: 752,
height: 520,
key: 'login'
})
navigate('/login')
}
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
window.electron.getIsMaximized().then((res: boolean) => {
const dom = document.getElementById('root') as any;
dom.style.borderRadius = res ? '0px' : '10px'
})
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/home' element={<Home />}>
<Route path='/home' element={<Navigate to='/home/index' />} />
<Route path='/home/index' element={<Index />} />
<Route path='/home/user' element={<User />} />
</Route>
<Route path='/login' element={<Login />} />
<Route path='/meeting' element={<Meeting />} />
<Route path='*' element={<NotFound />} />
</Routes>
</>
)
}
export default App

25
src/api/Home/index.ts Normal file
View File

@ -0,0 +1,25 @@
import { request } from '@/utils'
export const GetViewSize = (data: any) =>
request({
url: `/draw/position?X=${data.X}&Y=${data.Y}&Width=${data.Width}&Height=${data.Height}&ImageUrl=${data.ImageUrl}&ImageWidth=${data.ImageWidth}&ImageHeight=${data.ImageHeight}`,
method: 'get'
})
export const GetOcrDetail = (mid: string) =>
request({
url: `/api/ocr/${mid}`,
method: 'get'
})
export const PostSave = (data: any) =>
request({
url: `/api/ocr/save`,
method: 'post',
data,
})
export const GetLock = (mid: string) =>
request({
url: `/api/ocr/lock?mid=${mid}`,
method: 'get',
})

BIN
src/assets/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
src/assets/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
src/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

BIN
src/assets/icon1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

BIN
src/assets/icon10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

BIN
src/assets/icon11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

BIN
src/assets/icon12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

BIN
src/assets/icon13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

BIN
src/assets/icon14.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

BIN
src/assets/icon15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

BIN
src/assets/icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

BIN
src/assets/icon17.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

BIN
src/assets/icon18.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 B

BIN
src/assets/icon19.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

BIN
src/assets/icon2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

BIN
src/assets/icon20.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

BIN
src/assets/icon21.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/icon22.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/icon23.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

BIN
src/assets/icon24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

BIN
src/assets/icon25.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

BIN
src/assets/icon26.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

BIN
src/assets/icon27.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

BIN
src/assets/icon28.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

BIN
src/assets/icon29.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/icon3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

BIN
src/assets/icon30.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

BIN
src/assets/icon31.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

BIN
src/assets/icon32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

BIN
src/assets/icon4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/assets/icon5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

BIN
src/assets/icon6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

BIN
src/assets/icon7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

BIN
src/assets/icon8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

BIN
src/assets/icon9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

BIN
src/assets/videoImg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -0,0 +1,27 @@
.operation {
display: flex;
align-items: center;
flex-shrink: 0;
>div {
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
>div:nth-child(1),
>div:nth-child(2) {
&:hover {
background-color: #424242;
}
}
>div:nth-child(3) {
&:hover {
background-color: #880F0F;
}
}
}

View File

@ -0,0 +1,99 @@
import styles from '@/components/Operation/index.module.scss'
import { useEffect, useState } from "react";
type OperationKeyType = 'minimize' | 'quit' | 'maximize' | 'unmaximize';
type OperationType = {
icon: string;
key: OperationKeyType;
title: string;
onClick: Function;
show: boolean;
}
const Operation: React.FC = () => {
const [_windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
const [operationList, setOperationList] = useState<OperationType[]>([{
icon: '/src/assets/icon17.png',
key: 'minimize',
title: '最小化',
onClick: (key: OperationKeyType) => {
window.electron.setViewStatus(key)
},
show: true,
},
{
icon: '/src/assets/icon20.png',
key: 'maximize',
title: '最大化',
onClick: (key: OperationKeyType) => {
window.electron.setViewStatus(key)
},
show: true,
},
{
icon: '/src/assets/icon19.png',
key: 'unmaximize',
title: '还原大小',
onClick: (key: OperationKeyType) => {
window.electron.setViewStatus(key)
},
show: false,
},
{
icon: '/src/assets/icon18.png',
key: 'quit',
title: '关闭',
onClick: (key: OperationKeyType) => {
window.electron.setViewStatus(key)
},
show: true,
},])
useEffect(() => {
getIsMaximized()
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
getIsMaximized()
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
const getIsMaximized = (): void => {
window.electron.getIsMaximized().then((res: boolean) => {
changeOperationList(res ? 'maximize' : 'unmaximize')
})
}
const changeOperationList = (str: OperationKeyType): void => {
const newOperationList = [...operationList]
const unmaximize = newOperationList.find((item: OperationType) => item.key === 'unmaximize') as OperationType;
const maximize = newOperationList.find((item: OperationType) => item.key === 'maximize') as OperationType;
unmaximize.show = str === 'maximize' ? true : false;
maximize.show = str === 'maximize' ? false : true;
setOperationList(newOperationList)
}
return (
<>
<div className={`${styles.operation} drag`}>
{
operationList.map((item: OperationType, index: number) => {
return (item.show ?
<div title={item.title} key={index} onClick={() => item.onClick(item.key)}>
<img src={item.icon} alt="" />
</div> : null
)
})
}
</div>
</>
)
}
export default Operation

View File

@ -0,0 +1,47 @@
import { useEffect } from "react";
import '@/components/TldrawView/index.scss'
import {
Tldraw,
createShapeId,
Editor,
} from 'tldraw'
import 'tldraw/tldraw.css'
const TldrawView: React.FC = () => {
useEffect(() => {
});
return (
<>
<Tldraw
components={{
// ContextMenu: null,
// ActionsMenu: null,
// HelpMenu: null,
// ZoomMenu: null,
// MainMenu: null,
// Minimap: null,
// StylePanel: null,
PageMenu: null,
// NavigationPanel: null,
// Toolbar: null,
// KeyboardShortcutsDialog: null,
// QuickActions: null,
// HelperButtons: null,
DebugPanel: null,
DebugMenu: null,
// SharePanel: null,
// MenuPanel: null,
// TopPanel: null,
}}
onMount={(editor: Editor) => {
editor.sideEffects.registerAfterChangeHandler('shape', (_prevShape, _nextShape) => {
})
// editor.getRenderingShapes()
// editor.getPages()
// editor.createShapes().zoomToFit({ animation: { duration: 0 } })
}}>
</Tldraw>
</>
)
}
export default TldrawView

17
src/config/index.ts Normal file
View File

@ -0,0 +1,17 @@
// 常量配置
enum constant {
CONFIG_TITLE = 'vue3-vite-ts-pinia',
CONFIG_REQUEST_TIMEOUT_TIME = 10000, // 请求超时时间
CONFIG_TOKEN = 'TOKEN', // token
CONFIG_USERINFO = 'USERINFO', // 用户信息
CONFIG_STATUS_CODE_SUCCESS = 100, // 自定义代码 100成功、101失败
CONFIG_STATUS_CODE_ERROR = 101,
CONFIG_USERNAME_KEY = 'USERNAME', // 用户名
CONFIG_PASSWORD_KEY = 'PASSWORD', // 密码
CONFIG_IS_REMEMBER_KEY = 'REMEMBER', // 是否记住密码
CONFIG_CODE_SUCCESS = 200, // 成功码
}
// 常规配置
const config = {}
export { config, constant }

11
src/main.tsx Normal file
View File

@ -0,0 +1,11 @@
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import '@/utils/styles/main.css'
import { HashRouter } from 'react-router-dom';
ReactDOM.createRoot(document.getElementById('root')!).render(
<HashRouter>
<App />
</HashRouter>
)

View File

@ -0,0 +1,122 @@
.index {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.indexOperation {
display: flex;
justify-content: flex-end;
}
.indexBtns {
padding: 10px 0 30px;
margin: 0 30px;
box-sizing: border-box;
border-bottom: 1px solid #2C2C2C;
.indexBtnsJoin {
background-color: #FFCFEB;
color: red;
margin-left: 22px;
&:hover {
background-color: lighten(#FFCFEB, 5%) !important;
color: red;
}
&:active {
background-color: darken(#FFCFEB, 5%) !important;
color: red;
}
}
}
.indexContent {
flex-grow: 1;
display: flex;
flex-direction: column;
padding: 30px;
box-sizing: border-box;
.indexContentTitle {
flex-shrink: 0;
color: white;
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
}
.indexContentList {
overflow-y: scroll;
flex-grow: 1;
height: 0px;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
>div {
border: 1px solid #353741;
margin-bottom: 34px;
background-color: #1E1E1F;
width: calc(98% / 3);
padding: 20px 16px;
box-sizing: border-box;
border-radius: 10px;
>div:nth-child(1) {
display: flex;
align-items: center;
>div:nth-child(1) {
font-size: 18px;
color: white;
flex-grow: 1;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
>div:nth-child(2) {
display: flex;
align-items: center;
flex-shrink: 0;
margin-left: 10px;
>span {
color: #767676;
margin-left: 4px;
}
}
}
>div:nth-child(2) {
margin-top: 22px;
display: flex;
align-items: center;
justify-content: space-between;
>div:nth-child(1) {
display: flex;
align-items: center;
cursor: pointer;
>span {
color: #767676;
margin-right: 4px;
}
}
>div:nth-child(2) {
display: flex;
align-items: center;
>button {
margin-left: 10px;
}
}
}
}
}
}
}

View File

@ -0,0 +1,75 @@
import styles from '@/page/Home/Index/index.module.scss'
import { useEffect, useState } from "react";
import Operation from '@/components/Operation';
import { useNavigate } from 'react-router-dom';
import { Button } from "antd";
const Index: React.FC = () => {
const navigate = useNavigate();
const [list, setList] = useState([{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}])
useEffect(() => {
}, []);
return (
<>
<div className={styles.index}>
<div className={styles.indexOperation}>
<Operation></Operation>
</div>
<div className={styles.indexBtns}>
<Button type="primary"
icon={<img src="/src/assets/icon8.png" alt="" />}
className='m-ant-btn drag'>
</Button>
<Button type="primary"
icon={<img src="/src/assets/icon7.png" alt="" />}
className={`${styles.indexBtnsJoin} drag`}>
</Button>
</div>
<div className={styles.indexContent}>
<div className={styles.indexContentTitle}>
</div>
<div className={`${styles.indexContentList} drag`}>
{list.map((_item, index: number) => {
return (
<div className={styles.indexContentListItem} key={index}>
<div>
<div></div>
<div>
<img src="/src/assets/icon11.png" alt="" />
<span>2</span>
</div>
</div>
<div>
<div>
<span>252535356565</span>
<img src="/src/assets/icon10.png" alt="" />
</div>
<div>
<Button type="primary" danger></Button>
<Button type="primary"
iconPosition={'end'}
onClick={() => {
navigate('/meeting')
}}
icon={<img src="/src/assets/icon9.png" alt="" />}
className='m-ant-btn'>
</Button>
</div>
</div>
</div>
)
})}
<div style={{ visibility: 'hidden' }}></div>
<div style={{ visibility: 'hidden' }}></div>
</div>
</div>
</div>
</>
)
}
export default Index

View File

@ -0,0 +1,69 @@
.user {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.userOperation {
display: flex;
justify-content: flex-end;
}
.userBtns {
padding: 10px 0 30px;
margin: 0 30px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
.userBtnsLeft {
display: flex;
align-items: center;
.userBtnsDel {
background-color: #FFCFEB;
color: red;
margin-left: 22px;
&:hover {
background-color: lighten(#FFCFEB, 5%) !important;
color: red;
}
&:active {
background-color: darken(#FFCFEB, 5%) !important;
color: red;
}
}
}
.userBtnsRight {
display: flex;
align-items: center;
>button {
margin-left: 20px;
}
}
}
.userContent {
flex-grow: 1;
padding: 30px 20px 30px 30px;
box-sizing: border-box;
display: flex;
flex-direction: column;
.userContentPagination {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
>span {
color: #8B8787;
}
}
}
}

View File

@ -0,0 +1,96 @@
import styles from '@/page/Home/User/index.module.scss'
import { useEffect, useState } from "react";
import Operation from '@/components/Operation';
import { Button, Input, Table, Pagination } from "antd";
import { SearchOutlined } from '@ant-design/icons';
import type { TableColumnsType } from 'antd';
const columns: TableColumnsType = [
{
title: '姓名',
dataIndex: 'name',
},
{
title: '账号',
dataIndex: 'account',
},
{
title: '角色',
dataIndex: 'role',
},
{
title: '账号状态',
dataIndex: 'status',
render: (text) => {
return (
<div style={{ color: '#02B188' }}>{text}</div>
)
},
},
];
const data = [] as any;
for (let i = 0; i < 46; i++) {
data.push({
key: i,
name: `潇潇`,
account: 5256589545,
role: `教师`,
status: `在线`,
});
}
const User: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
useEffect(() => {
}, []);
const onSelectChange = (newSelectedRowKeys: React.Key[]) => {
setSelectedRowKeys(newSelectedRowKeys);
};
const rowSelection = {
selectedRowKeys,
onChange: onSelectChange,
};
return (
<>
<div className={styles.user}>
<div className={styles.userOperation}>
<Operation></Operation>
</div>
<div className={styles.userBtns}>
<div className={`${styles.userBtnsLeft} drag`}>
<Button type="primary"
icon={<img src="/src/assets/icon8.png" alt="" />}
className='m-ant-btn'>
</Button>
<Button type="primary"
icon={<img src="/src/assets/icon21.png" alt="" />}
className={styles.userBtnsDel}>
</Button>
</div>
<div className={`${styles.userBtnsRight} drag`}>
<Input placeholder="请输入用户名" prefix={<SearchOutlined style={{ color: 'white' }} />} />
<Button className='m-border-ant-button'></Button>
</div>
</div>
<div className={`${styles.userContent} drag`}>
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={data}
pagination={false}
scroll={{ y: '64vh' }}
style={{ width: '77.6vw', flexGrow: 1 }}
/>
<div className={styles.userContentPagination}>
<span>653</span>
<Pagination size="small" total={50} />
</div>
</div>
</div>
</>
)
}
export default User

View File

@ -0,0 +1,191 @@
.home {
width: 100%;
height: 100%;
display: flex;
.homeLeft {
width: 300px;
background-color: rgb(31, 33, 37);
flex-shrink: 0;
padding: 20px 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
@for $i from 1 through 4 {
>div:nth-child(#{$i}) {
@if $i ==1 {
display: flex;
align-items: center;
flex-shrink: 0;
cursor: pointer;
>div {
height: 50px;
width: 50px;
border-radius: 50%;
overflow: hidden;
margin-right: 16px;
flex-shrink: 0;
>img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
>span {
color: white;
font-size: 16px;
}
}
@else if $i ==2 {
display: flex;
align-items: center;
margin: 20px 0 20px;
flex-shrink: 0;
>img {
width: 82px;
margin-right: 12px;
}
>div {
display: flex;
flex-direction: column;
align-items: flex-start;
>div:nth-child(1) {
display: flex;
align-items: center;
>span:nth-child(1) {
font-size: 36px;
color: white;
}
>span:nth-child(2) {
font-size: 16px;
color: #979797;
width: 16px;
margin-left: 10px;
}
}
>div:nth-child(2) {
font-size: 16px;
color: #979797;
}
}
}
@else if $i ==3 {
flex-grow: 1;
overflow-y: auto;
@for $i from 1 through 20 {
>div:nth-child(#{$i}) {
display: flex;
align-items: center;
height: 48px;
cursor: pointer;
padding: 0px 10px;
box-sizing: border-box;
border-radius: 10px;
margin-bottom: 10px;
>div {
>img {
width: 24px;
}
}
>span {
font-size: 16px;
color: #98989A;
margin-left: 20px;
}
&:hover {
background-color: rgb(47, 48, 50);
>span {
color: white;
}
}
}
.active {
background-color: #5575F2 !important;
>span {
color: white !important;
}
}
}
}
@else if $i ==4 {
border-top: #565656 solid 1px;
padding-top: 10px;
margin-top: 10px;
@for $i from 1 through 2 {
>div:nth-child(#{$i}) {
display: flex;
align-items: center;
height: 48px;
cursor: pointer;
padding: 0px 10px;
box-sizing: border-box;
>div {
width: 24px;
height: 24px;
@if $i ==1 {
background: url('/src/assets/icon16.png') no-repeat center/cover;
}
@else if $i ==2 {
background: url('/src/assets/icon15.png') no-repeat center/cover;
}
}
>span {
font-size: 16px;
color: #98989A;
margin-left: 20px;
}
&:hover {
>div {
@if $i ==1 {
background: url('/src/assets/icon16-active.png') no-repeat center/cover;
}
@else if $i ==2 {
background: url('/src/assets/icon15-active.png') no-repeat center/cover;
}
}
>span {
color: white;
}
}
}
}
}
}
}
}
.homeRight {
flex-grow: 1;
background-color: rgb(22, 25, 30);
}
}

147
src/page/Home/index.tsx Normal file
View File

@ -0,0 +1,147 @@
import styles from '@/page/Home/index.module.scss'
import { useEffect, useState } from "react";
import { Outlet, useNavigate } from 'react-router-dom';
import { Popconfirm } from 'antd';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn'
dayjs.locale('zh-cn');
type navListType = {
title: string;
icon: string;
path: string;
isHover: boolean;
isActive: boolean;
}
const Home: React.FC = () => {
const navigate = useNavigate();
const [navList, setNavList] = useState<navListType[]>([
{
title: '首页',
icon: '/src/assets/icon12',
isHover: false,
isActive: true,
path: '/home/index'
},
{
title: '人员',
icon: '/src/assets/icon13',
isHover: false,
isActive: false,
path: '/home/user'
},
]);
const [dateInfo, setDateInfo] = useState<{
work: string;
time: string;
specific: string;
}>({
work: '',
time: '',
specific: '',
})
useEffect(() => {
const updateTime = () => {
setDateInfo({
work: dayjs().format('ddd'),
time: dayjs().format('HH:mm'),
specific: dayjs().format('YYYY/MM/DD')
})
};
const timer = setInterval(updateTime, 1000);
return () => {
clearInterval(timer);
};
}, []);
const changtNavList = (index: number, bool?: boolean): void => {
const newNavList = [...navList];
if (typeof bool === 'boolean') {
newNavList[index].isHover = bool;
} else {
newNavList.forEach((item) => {
item.isActive = false;
})
newNavList[index].isActive = true;
navigate(newNavList[index].path)
}
setNavList(newNavList)
}
return (
<>
<div className={styles.home}>
<div className={styles.homeLeft}>
<div className='drag'>
<div>
<img src="/src/assets/avatar.png" alt="" />
</div>
<span>u0001</span>
</div>
<div>
<img src="/src/assets/icon14.png" alt="" />
<div>
<div>
<span>{dateInfo.time}</span>
<span>{dateInfo.work}</span>
</div>
<div>{dateInfo.specific}</div>
</div>
</div>
<div>
{navList.map((item: navListType, index: number) => {
return (
<div
key={index}
className={`${item.isActive ? styles.active : ''} drag`}
title={item.title}
onMouseLeave={() => changtNavList(index, false)}
onMouseEnter={() => changtNavList(index, true)}
onClick={() => changtNavList(index)}>
<div>
{item.isHover || item.isActive ?
<img src={item.icon + '-active.png'} alt="" /> :
<img src={item.icon + '.png'} alt="" />}
</div>
<span>{item.title}</span>
</div>
)
})}
</div>
<div>
<div className='drag' title='设置'>
<div></div>
<span></span>
</div>
<Popconfirm
title="提示"
description="确认退出吗?"
onConfirm={() => {
window.electron.setMainWindowSize({
width: 752,
height: 520,
key: 'login'
})
navigate('/login')
}}
onCancel={() => {
}}
okText="确认"
cancelText="取消"
>
<div className='drag' title='退出'>
<div></div>
<span>退</span>
</div>
</Popconfirm>
</div>
</div>
<div className={styles.homeRight}>
<Outlet></Outlet>
</div>
</div>
</>
)
}
export default Home

View File

@ -0,0 +1,150 @@
.login {
width: 100%;
height: 100%;
background-color: #16191E;
display: flex;
.loginBg {
flex-shrink: 0;
width: 370px;
height: 100%;
img {
width: 100%;
height: 100%;
}
}
.loginContent {
flex-grow: 1;
padding: 20px 30px;
box-sizing: border-box;
display: flex;
flex-direction: column;
@for $i from 1 through 3 {
>div:nth-child(#{$i}) {
@if $i ==1 {
flex-shrink: 0;
.quit {
display: flex;
justify-content: flex-end;
>img {
font-size: 16px;
cursor: pointer;
}
}
.logo {
margin-top: 40px;
>img {
width: 126px;
}
}
}
@else if $i ==2 {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
.operation {
margin-top: 10px;
:global {
.ant-checkbox-group {
display: flex;
align-items: center;
justify-content: space-between;
}
}
}
}
@else if $i ==3 {
flex-shrink: 0;
.footer {
display: flex;
align-items: center;
>div {
flex-grow: 1;
height: 1px;
background-color: #3F3F3F;
}
>span {
flex-shrink: 0;
margin: 0 4px;
color: #7A7A7A;
font-size: 12px;
}
}
.code {
display: flex;
align-items: center;
margin-top: 34px;
@for $i from 1 through 2 {
>div:nth-child(#{$i}) {
@if $i ==1 {
flex-grow: 1;
}
@else if $i ==2 {
flex-shrink: 0;
cursor: pointer;
height: 44px;
line-height: 44px;
display: flex;
justify-content: center;
align-items: center;
background-color: #FFCFEB;
border-radius: 10px;
width: 56px;
margin-left: 4px;
transition: 0.3s;
&:hover {
background-color: lighten(#FFCFEB, 5%) !important;
}
&:active {
background-color: darken(#FFCFEB, 5%) !important;
}
}
}
}
}
}
}
}
}
// 登录页固定大小不允许放大缩小固需要单独写样式~
.loginInput {
height: 44px;
line-height: 44px;
}
.loginInputIcon {
:global {
.ant-input {
height: 34px;
line-height: 34px;
}
}
}
.loginButton {
height: 44px;
line-height: 44px;
}
}

192
src/page/Login/index.tsx Normal file
View File

@ -0,0 +1,192 @@
import styles from '@/page/Login/index.module.scss'
import { useEffect, useState } from "react";
import { useNavigate } from 'react-router-dom';
import { Input, Button, Checkbox } from "antd"
import { storage } from '@/utils'
const Login: React.FC = () => {
const navigate = useNavigate();
const [accountPasswordStatus, setAccountPasswordStatus] = useState<boolean>(false);
const [operation, setOperation] = useState<{
isRememberPassword: boolean;
isAutoLogin: boolean;
account: string;
password: string;
options: { label: string; value: string }[];
optionsValue: string[];
}>({
isRememberPassword: false,
isAutoLogin: false,
account: '',
password: '',
options: [
{ label: '记住密码', value: 'isRememberPassword' },
{ label: '自动登录', value: 'isAutoLogin' },
],
optionsValue: []
});
useEffect(() => {
if (storage.getItem('login')) {
const login = JSON.parse(storage.getItem('login') as string);
const data = {
isRememberPassword: login.isRememberPassword,
isAutoLogin: login.isAutoLogin,
optionsValue: login.optionsValue,
}
if (login.isRememberPassword) {
setOperation({
...operation,
...data,
account: login.account,
password: login.password,
})
} else {
setOperation({
...operation,
...data,
})
}
}
}, []);
// 退出
const quitClick = (): void => {
window.electron.setViewStatus('quit')
}
// 重置
const resetClick = (): void => {
setOperation({
...operation,
account: '',
password: '',
})
setAccountPasswordStatus(false)
}
// 继续
const continueClick = (): void => {
setAccountPasswordStatus(true);
}
// 设置勾选
const changeOptionsValue = (checkedValues: string[]): void => {
setOperation({
...operation,
optionsValue: checkedValues,
})
storage.setItem('login', JSON.stringify({
isRememberPassword: checkedValues.includes('isRememberPassword'),
isAutoLogin: checkedValues.includes('isAutoLogin'),
account: operation.account,
password: operation.password,
optionsValue: checkedValues,
}))
};
// 登录
const loginClick = (): void => {
storage.setItem('login', JSON.stringify({
isRememberPassword: operation.optionsValue.includes('isRememberPassword'),
isAutoLogin: operation.optionsValue.includes('isAutoLogin'),
account: operation.account,
password: operation.password,
optionsValue: operation.optionsValue,
}))
window.electron.setMainWindowSize({
width: 1200,
height: 800,
})
navigate('/home')
}
return (
<>
<div className={styles.login}>
<div className={styles.loginBg}>
<img src="/src/assets/icon1.png" alt="" />
</div>
<div className={styles.loginContent}>
<div>
<div className={styles.quit}>
<img src="/src/assets/icon2.png" alt="" title='退出' className='drag' onClick={quitClick} />
</div>
<div className={styles.logo}>
<img src="/src/assets/icon4.png" alt="" />
</div>
</div>
<div>
<div>
<Input
value={operation.account}
onChange={e => {
setOperation({
...operation,
account: e.target.value
})
}}
className={`${styles.loginInputIcon} drag`}
style={{ marginBottom: '12px' }}
placeholder="请输入账号"
prefix={<img src="/src/assets/icon5.png" alt="" />}
suffix={
<span
style={{ color: '#47D3D0', cursor: 'pointer' }}
onClick={resetClick}
>
</span>
}
/>
{operation.account && !accountPasswordStatus ? <div style={{ marginTop: '36px' }} className='drag'>
<Button type="primary"
onClick={continueClick}
style={{ width: '100%' }}
className={`${styles.loginButton} m-ant-btn`}
></Button>
</div> : null}
{accountPasswordStatus ? <div>
<Input.Password
value={operation.password}
onChange={e => {
setOperation({
...operation,
password: e.target.value
})
}}
className={`${styles.loginInputIcon} drag`}
style={{ marginBottom: '36px' }}
placeholder="请输入密码"
prefix={<img src="/src/assets/icon6.png" alt="" />}
/>
<Button type="primary" className={`${styles.loginButton} drag m-ant-btn`} onClick={loginClick} style={{ width: '100%' }}></Button>
<div className={`${styles.operation} drag`}>
<Checkbox.Group
options={operation.options}
value={operation.optionsValue}
onChange={changeOptionsValue}
/>
</div>
</div> : null}
</div>
</div>
<div>
<div className={styles.footer}>
<div></div>
<span>or</span>
<div></div>
</div>
<div className={`${styles.code} drag`}>
<div>
<Input placeholder="输入会议号" className={`${styles.loginInput}`} />
</div>
<div><img src="/src/assets/icon3.png" alt="" /></div>
</div>
</div>
</div>
</div>
</>
)
}
export default Login

View File

@ -0,0 +1,579 @@
.meeting {
width: 100%;
height: 100%;
background-color: rgba(16, 19, 23);
display: flex;
flex-direction: column;
.meetingHeader {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #2C2C2C;
padding: 10px 0 10px 52px;
box-sizing: border-box;
flex-shrink: 0;
>div:nth-child(1) {
display: flex;
align-items: center;
>div:nth-child(1) {
margin-right: 20px;
display: flex;
transform: rotate(180deg) scaleX(-1);
>span {
background-color: #02B188;
width: 4px;
margin-right: 4px;
}
@for $i from 1 through 4 {
>span:nth-child(#{$i}) {
height: calc(6px * #{$i} + 2px)
}
}
}
>div:nth-child(2) {
font-size: 20px;
color: #EEEEEE;
}
}
>div:nth-child(2) {
color: #EEEEEE;
font-size: 20px;
}
>div:nth-child(3) {
display: flex;
align-items: center;
.meetingGrayButton {
cursor: pointer;
background-color: #31353A;
color: #EEEEEE;
width: 154px;
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 5px;
margin-right: 20px;
&:hover {
background-color: lighten(#31353A, 4%);
}
&:active {
background-color: darken(#31353A, 4%);
}
}
>div:nth-child(2) {
margin: -10px 0 0;
}
}
}
.meetingContent {
flex-grow: 1;
display: flex;
flex-direction: column;
.meetingContentBody {
flex-grow: 1;
display: flex;
.meetingContentBodyLeft {
height: 100%;
width: 0px;
flex-grow: 1;
display: flex;
flex-direction: column;
padding-bottom: 18px;
box-sizing: border-box;
.meetingContentSwiper {
margin: 20px 0 12px;
flex-shrink: 0;
:global {
.swiper-slide-active {
border: 1px solid #EBEBEB;
box-sizing: border-box;
}
}
.meetingContentSwiperCard {
height: 196px;
border-radius: 10px;
overflow: hidden;
position: relative;
cursor: pointer;
margin: 0 auto;
.meetingContentSwiperCardVdeio {
width: 100%;
height: 100%;
background:black url('/src/assets/error.png') no-repeat center/30%;
}
}
}
.meetingContentVideo {
flex-grow: 1;
position: relative;
width: 1378px;
margin: 0 auto;
height: 0px;
display: flex;
.meetingContentVideoDom {
border: 1px red solid;
width: 50%;
height: 100%;
background:black url('/src/assets/error.png') no-repeat center/30%;
}
}
.meetingContentUser {
position: absolute;
left: 10px;
bottom: 10px;
display: flex;
align-items: center;
.meetingContentUserRole {
background: #FDC229;
border-radius: 6px;
width: 44px;
height: 34px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 6px;
>img {
width: 20px;
}
}
.meetingContentUserName {
display: flex;
align-items: center;
background-color: #0000009E;
border-radius: 6px;
height: 34px;
padding: 0 4px;
>img {
width: 28px;
}
>span {
color: #EEEEEE;
margin-left: 4px;
}
}
}
}
.meetingContentBodyRight {
width: 374px;
flex-shrink: 0;
height: 100%;
.meetingUserList {
height: 100%;
padding: 20px;
box-sizing: border-box;
background-color: #16191E;
display: flex;
flex-direction: column;
.meetingUserListTitle {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
>span {
color: #EEEEEE;
font-size: 20px;
}
>img {
cursor: pointer;
}
}
.meetingUserListContent {
flex-grow: 1;
height: 0px;
overflow-y: auto;
margin: 20px 0;
>div {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 20px;
>div:nth-child(1) {
display: flex;
align-items: center;
>span {
font-size: 14px;
color: #F3F3F5;
margin-left: 4px;
}
>div {
width: 36px;
height: 36px;
overflow: hidden;
border-radius: 50%;
>img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
>div:nth-child(2) {
display: flex;
align-items: center;
>img {
width: 17px;
margin-left: 4px;
}
}
}
}
.meetingUserListFooter {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
>div {
cursor: pointer;
background-color: #31353A;
color: #EEEEEE;
width: 154px;
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 5px;
&:hover {
background-color: lighten(#31353A, 4%);
}
&:active {
background-color: darken(#31353A, 4%);
}
}
}
}
.meetingUserChat {
height: 100%;
background-color: #16191E;
display: flex;
flex-direction: column;
.meetingUserChatTitle {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
box-sizing: border-box;
border-bottom: 1px solid #23272E;
>span {
color: #EEEEEE;
font-size: 20px;
}
>img {
cursor: pointer;
}
}
.meetingUserChatContent {
flex-grow: 1;
height: 0px;
overflow-y: auto;
padding: 20px;
box-sizing: border-box;
border-bottom: 1px solid #23272E;
.meetingUserChatContentLeft {
display: flex;
flex-direction: column;
align-items: flex-start;
>div:nth-child(1) {
display: flex;
align-items: center;
>span {
font-size: 14px;
color: #F3F3F5;
margin-left: 4px;
}
>div {
width: 36px;
height: 36px;
overflow: hidden;
border-radius: 50%;
>img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
>div:nth-child(2) {
background-color: #5575F2;
color: #F3F3F5;
max-width: 266px;
padding: 10px;
box-sizing: border-box;
border-radius: 0 25px 25px 25px;
margin: 10px 0 10px 40px;
}
}
.meetingUserChatContentRight {
display: flex;
flex-direction: column;
align-items: flex-end;
>div:nth-child(1) {
display: flex;
align-items: center;
flex-direction: row-reverse;
>span {
font-size: 14px;
color: #F3F3F5;
}
>div {
margin-left: 4px;
width: 36px;
height: 36px;
overflow: hidden;
border-radius: 50%;
>img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
>div:nth-child(2) {
background-color: #464E6B;
color: #F3F3F5;
max-width: 266px;
padding: 10px;
box-sizing: border-box;
border-radius: 25px 0 25px 25px;
margin: 10px 40px 10px 0;
}
}
}
.meetingUserChatInput {
flex-shrink: 0;
padding: 20px;
box-sizing: border-box;
height: 220px;
display: flex;
flex-direction: column;
align-items: flex-end;
}
}
}
}
.meetingContentFooter {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
background-color: #07090B;
padding: 10px 50px;
box-sizing: border-box;
>div {
display: flex;
align-items: center;
>div {
display: flex;
flex-direction: column;
align-items: center;
margin-right: 30px;
cursor: pointer;
&:last-child {
margin: 0;
}
>img {
height: 50px;
}
>span {
color: #EEEEEE;
font-size: 14px;
}
}
}
}
}
}
:global {
.meetingContentFooterPopover {
>div {
width: 220px;
height: 40px;
line-height: 40px;
border-radius: 5px;
color: #EEEEEE;
text-align: center;
margin-bottom: 16px;
cursor: pointer;
&:last-child {
margin: 0;
}
}
>div:nth-child(1) {
background-color: #FF5219;
&:hover {
background-color: lighten(#FF5219, 4%);
}
&:active {
background-color: darken(#FF5219, 4%);
}
}
>div:nth-child(2) {
background-color: #31353A;
&:hover {
background-color: lighten(#31353A, 4%);
}
&:active {
background-color: darken(#31353A, 4%);
}
}
>div:nth-child(3) {
background-color: #101418;
&:hover {
background-color: lighten(#101418, 4%);
}
&:active {
background-color: darken(#101418, 4%);
}
}
}
}
.sharedScreenModal {
display: flex;
flex-direction: column;
>div:nth-child(1) {
display: flex;
flex-wrap: wrap;
max-height: 50vh;
overflow-y: auto;
align-content: flex-start;
padding: 50px 0 0;
>div {
display: flex;
flex-direction: column;
align-items: center;
width: calc(100% / 4);
box-sizing: border-box;
margin-bottom: 20px;
cursor: pointer;
position: relative;
>span {
color: #EEEEEE;
font-size: 14px;
margin-top: 4px;
width: 144px;
text-align: center;
}
>div {
width: 144px;
height: 94px;
box-sizing: border-box;
>img {
width: 100%;
height: 100%;
}
}
>img {
position: absolute;
top: -20px;
left: 0;
width: 40px;
}
&:hover {
>div {
border: 1px solid #EBEBEB;
}
}
}
.active {
>div {
border: 1px solid #EBEBEB;
}
}
}
>div:nth-child(2) {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 20px;
}
}

355
src/page/Meeting/index.tsx Normal file
View File

@ -0,0 +1,355 @@
import styles from '@/page/Meeting/index.module.scss'
import { useEffect, useState } from "react";
import Operation from '@/components/Operation';
import { Navigation, Pagination } from 'swiper/modules';
import { Swiper, SwiperSlide } from 'swiper/react';
import 'swiper/css';
import 'swiper/css/navigation';
import 'swiper/css/pagination';
import { Button, Input, Popover, Modal, Checkbox, message } from "antd";
import { SearchOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { thumbImageBufferToBase64 } from '@/utils/package/base64'
const Meeting: React.FC = () => {
const navigate = useNavigate();
const [statusList, setStatusList] = useState({
userList: false,
userChatList: false,
})
const [isSharedScreenModal, setIsSharedScreenModal] = useState(false);
const [sharedScreenList, setSharedScreenList] = useState<any>([]);
const [sharedScreenItem, setSharedScreenItem] = useState<any>('');
const [footerList] = useState([
[
{
title: '关闭声音',
icon: '/src/assets/icon22.png',
active: false,
},
{
title: '关闭视频',
icon: '/src/assets/icon23.png',
active: false,
},
],
[
{
title: '共享屏幕',
icon: '/src/assets/icon24.png',
active: false,
},
{
title: '共享文件',
icon: '/src/assets/icon25.png',
active: false,
},
{
title: '邀请人员',
icon: '/src/assets/icon26.png',
active: false,
},
{
title: '录制',
icon: '/src/assets/icon27.png',
active: false,
},
{
title: '设置向导',
icon: '/src/assets/icon28.png',
active: false,
},
{
title: '结束',
icon: '/src/assets/icon29.png',
active: false,
},
],
[
{
title: '成员列表',
icon: '/src/assets/icon30.png',
active: false,
},
{
title: '聊天',
icon: '/src/assets/icon31.png',
active: false
},
],
])
const [list] = useState<number[]>([1, 2, 3, 4, 5, 6, 7])
const [open, setOpen] = useState(false)
const [videoID, setVideoID] = useState('')
useEffect(() => {
}, []);
const changeStatusList = (row: any): void => {
switch (row.title) {
case '成员列表':
setStatusList({
userList: true,
userChatList: false,
})
break;
case '聊天':
setStatusList({
userList: false,
userChatList: true,
})
break;
case '共享屏幕':
getDesktopCapturerVideo()
setIsSharedScreenModal(true)
break;
case '关闭声音':
window.electron.setJoinChannel()
setVideoID(window.electron.getVideoId())
break;
default:
break;
}
}
const changeVdeio = async (bool: boolean): Promise<void> => {
if (bool) {
} else {
let data = sharedScreenList.find((item: any) => item.sourceId === sharedScreenItem.sourceId)
if (data) {
setIsSharedScreenModal(false)
window.electron.setDesktopCapturerVideo(sharedScreenItem)
setVideoID(window.electron.getVideoId())
} else {
message.error('请选择应用!')
}
}
}
const getDesktopCapturerVideo = (): void => {
window.electron.getDesktopCapturerVideo().then((res: any) => {
if (sharedScreenList.length !== res.length) {
res.forEach((item: any) => {
if (item.thumbImage.buffer) {
item.thumbnailUrl = thumbImageBufferToBase64(item.thumbImage)
}
if (item.iconImage.buffer) {
item.iconDataUrl = thumbImageBufferToBase64(item.iconImage)
}
})
setSharedScreenList(res)
}
})
};
return (
<>
<div className={styles.meeting}>
<div className={styles.meetingHeader}>
<div>
<div>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div>00:13:45</div>
</div>
<div>2323235</div>
<div className='drag'>
<div className={styles.meetingGrayButton}></div>
<Operation></Operation>
</div>
</div>
<div className={styles.meetingContent}>
<div className={styles.meetingContentBody}>
<div className={styles.meetingContentBodyLeft}>
<div className={`${styles.meetingContentSwiper} drag`}>
<Swiper
loop={false}
centeredSlides={true}
modules={[Navigation, Pagination]}
spaceBetween={20}
slidesPerView={6}
navigation
pagination={false}
scrollbar={{ draggable: true }}
onSwiper={(swiper: any) => {
swiper.on('click', (e: any) => {
swiper.slideTo(e.clickedIndex)
})
}}
onSlideChange={() => { }}
>
{list.map((item) =>
<SwiperSlide key={item}>
<div className={styles.meetingContentSwiperCard}>
<video src="" className={styles.meetingContentSwiperCardVdeio} ></video>
<div style={{ color: 'white', position: 'absolute', left: 0, top: 0 }}>{item}</div>
{meetingContentUser()}
</div>
</SwiperSlide>
)}
</Swiper>
</div>
<div className={`${styles.meetingContentVideo} drag`}>
<div className={styles.meetingContentVideoDom} id='video1'></div>
<div className={styles.meetingContentVideoDom} id='video2'></div>
{/* <div className={styles.meetingContentVideoDom} id={videoID}></div> */}
{/* {meetingContentUser()} */}
</div>
</div>
{
(statusList.userList || statusList.userChatList) ? (
<div className={styles.meetingContentBodyRight}>
{statusList.userList ?
<div className={styles.meetingUserList}>
<div className={styles.meetingUserListTitle}>
<span></span>
<img src="/src/assets/icon18.png" alt="" className='drag' onClick={() => {
setStatusList({
userList: false,
userChatList: false,
})
}} />
</div>
<Input placeholder="请输入用户名" className='drag' prefix={<SearchOutlined style={{ color: 'white' }} />} />
<div className={styles.meetingUserListContent}>
{list.map((item: number) =>
<div key={item} className='drag'>
<div>
<div><img src="/src/assets/avatar.png" alt="" /></div>
<span><span style={{ color: '#02B188', marginLeft: '4px' }}></span></span>
</div>
<div>
<img src="/src/assets/icon22.png" alt="" />
<img src="/src/assets/icon23.png" alt="" />
</div>
</div>
)}
</div>
<div className={`${styles.meetingUserListFooter} drag`}>
<div></div>
<div></div>
</div>
</div>
:
<div className={styles.meetingUserChat}>
<div className={styles.meetingUserChatTitle}>
<span></span>
<img src="/src/assets/icon18.png" alt="" className='drag' onClick={() => {
setStatusList({
userList: false,
userChatList: false,
})
}} />
</div>
<div className={styles.meetingUserChatContent}>
{list.map((item: number) =>
<div key={item} className={`${styles.meetingUserChatContentLeft} drag`}>
<div>
<div><img src="/src/assets/avatar.png" alt="" /></div>
<span></span>
</div>
<div></div>
</div>
)}
</div>
<div className={`${styles.meetingUserChatInput} drag`}>
<Input.TextArea placeholder="请输入消息" style={{ flexGrow: 1 }}></Input.TextArea>
<Button type="primary" className='m-ant-btn' style={{ flexShrink: 0, marginTop: '4px' }}></Button>
</div>
</div>
}
</div>
) : null
}
</div>
<div className={styles.meetingContentFooter}>
{footerList.map((item, itemIndex) => {
return (
<div key={itemIndex}>
{item.map((row, rowIndex) => {
return (
row.title === '结束' ?
<Popover key={rowIndex}
content={
<div className='meetingContentFooterPopover'>
<div onClick={() => {
navigate(-1)
}}></div>
<div onClick={() => { navigate(-1) }}></div>
<div onClick={() => { setOpen(false) }}></div>
</div>
}
title=""
trigger="click"
open={open}
onOpenChange={() => setOpen(true)}
>
<div className='drag'>
<img src={row.icon} alt="" />
<span>{row.title}</span>
</div>
</Popover> :
<div className='drag' onClick={() => changeStatusList(row)} key={rowIndex}>
<img src={row.icon} alt="" />
<span>{row.title}</span>
</div>
)
})}
</div>
)
})}
</div>
</div>
</div>
<Modal title="共享屏幕" open={isSharedScreenModal} footer={null} closable={false} centered width={'40vw'}>
<div className={styles.sharedScreenModal}>
<div>
{sharedScreenList.map((item: any, index: number) => {
return (
<div
className={sharedScreenItem.sourceId === item.sourceId ? styles.active : ''}
key={index}
onClick={() => {
setSharedScreenItem(item)
}}>
{item.iconDataUrl ? <img src={item.iconDataUrl} alt="" /> : ''}
<div><img src={item.thumbnailUrl} alt="" /></div>
<span>{item.sourceTitle}</span>
</div>
)
})}
</div>
<div>
<Checkbox onChange={() => {
}}></Checkbox>
<div>
<Button type="primary" onClick={() => { setIsSharedScreenModal(false) }} style={{ backgroundColor: '#31353A', marginRight: '14px' }}></Button>
<Button type="primary" className='m-ant-btn' onClick={() => changeVdeio(false)}></Button>
</div>
</div>
</div>
</Modal>
</>
)
}
const meetingContentUser = () => {
return (
<>
<div className={styles.meetingContentUser}>
<div className={styles.meetingContentUserRole}>
<img src="/src/assets/icon32.png" alt="" />
</div>
<div className={styles.meetingContentUserName}>
<img src="/src/assets/icon22.png" alt="" />
<span></span>
</div>
</div>
</>
)
}
export default Meeting

View File

@ -0,0 +1,15 @@
import { useEffect } from "react";
const NotFound: React.FC = () => {
useEffect(() => {
});
return (
<>
<div className="notfound"></div>
</>
)
}
export default NotFound

17
src/render.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
// electron-env.d.ts
export interface IElectronAPI {
getDesktopCapturerVideo: () => Promise<void>;
setDesktopCapturerVideo: (data: any) => Promise<void>;
getCameraAndMicrophoneMedia: () => Promise<void>;
setMainWindowSize: (config: any) => void;
getIsMaximized: () => Promise<boolean>;
setViewStatus: (status: 'quit' | 'maximize' | 'minimize' | 'unmaximize') => void;
setJoinChannel: () => Promise<void>;
getVideoId: () => string;
}
declare global {
interface Window {
electron: IElectronAPI;
}
}

1
src/shims-react.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'react-dom/client';

4
src/utils/index.ts Normal file
View File

@ -0,0 +1,4 @@
import storage from './package/storage'
import request from './request'
export { storage, request }

View File

@ -0,0 +1,5 @@
module.exports = {
appid: "dcfc466a6ecb4a1f972630065dfb1e75",
token: "007eJxTYNDz89yosT64YN5i3snT6nzY/aVOvs1Tmu3zc9r6qUkXHyxSYEhJTks2MTNLNEtNTjJJNEyzNDcyMzYwMDNNSUsyTDU3FQxuTmsIZGSQn/iShZEBAkF8bgZDc3MjYwMLU2MTMwYGAAHrH7M=",
channelId: '17723085346',
}

View File

@ -0,0 +1,73 @@
import { ThumbImageBuffer } from 'agora-electron-sdk';
export default (input: any) => {
const keyStr =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
let output = '';
let chr1, chr2, chr3, enc1, enc2, enc3, enc4;
let i = 0;
while (i < input.length) {
chr1 = input[i++];
chr2 = i < input.length ? input[i++] : Number.NaN; // Not sure if the index
chr3 = i < input.length ? input[i++] : Number.NaN; // checks are needed here
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output +=
keyStr.charAt(enc1) +
keyStr.charAt(enc2) +
keyStr.charAt(enc3) +
keyStr.charAt(enc4);
}
return output;
};
export const thumbImageBufferToBase64 = (target?: ThumbImageBuffer) => {
if (!target) {
return '';
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return '';
const width = (canvas.width = target.width!);
const height = (canvas.height = target.height!);
const rowBytes = width * 4;
for (let row = 0; row < height; row++) {
const srow = row;
const imageData = ctx.createImageData(width, 1);
const start = srow * width * 4;
for (let i = 0; i < rowBytes; i += 4) {
imageData.data[i] = target.buffer![start + i + 2]!;
imageData.data[i + 1] = target.buffer![start + i + 1]!;
imageData.data[i + 2] = target.buffer![start + i]!;
imageData.data[i + 3] = target.buffer![start + i + 3]!;
}
// if (process.platform === 'win32') {
// for (let i = 0; i < rowBytes; i += 4) {
// imageData.data[i] = target.buffer![start + i + 2]!;
// imageData.data[i + 1] = target.buffer![start + i + 1]!;
// imageData.data[i + 2] = target.buffer![start + i]!;
// imageData.data[i + 3] = target.buffer![start + i + 3]!;
// }
// } else {
// for (let i = 0; i < rowBytes; ++i) {
// imageData.data[i] = target.buffer![start + i]!;
// }
// }
ctx.putImageData(imageData, 0, row);
}
return canvas.toDataURL('image/png');
};

View File

@ -0,0 +1,95 @@
// import AgoraRTC from "agora-rtc-sdk-ng"
// let rtcClient = null as any;
// const options = {
// appId: "dcfc466a6ecb4a1f972630065dfb1e75",
// channel: '17723085346',
// token: "007eJxTYNDlkfJ9fD3lzh2e0KI5Oyy6vdU7phyIXOpV8F36/xPOeS8VGFKS05JNzMwSzVKTk0wSDdMszY3MjA0MzExT0pIMU81Np/g0pTUEMjJExEqyMjJAIIjPzWBobm5kbGBhamxixsAAAC6mH9k=",
// uid: 'eaecab',
// };
// rtcClient = AgoraRTC.createClient({ mode: "live", codec: "vp8" });
// // 以观众身份加入房间
// export async function AudienceJoinChannel(channel: any, uid: string | number) {
// await SetRule(1)
// await rtcClient.join(options.appId, channel, options.token, uid)
// }
// // 加入房间
// export async function JoinChannel(channel: any, uid: string | number) {
// await rtcClient.join(options.appId, channel, options.token, uid)
// }
// // 设置用户角色 1=观众 2=主播
// export async function SetRule(type: number) {
// await rtcClient.setClientRole(type == 1 ? "audience" : "host");
// }
// // 接流
// export async function ReceiveFLow(flowHandld: any) {
// rtcClient.on("user-published", async (user: any, mediaType: any) => {
// await rtcClient.subscribe(user, mediaType);
// console.log(user,'嘻嘻');
// flowHandld(user)
// });
// }
// // 远端取消推流
// export function ChannelFlow(channelHandle: any) {
// rtcClient.on("user-unpublished", (user: any) => {
// channelHandle(user)
// });
// }
// // 离开频道
// export function LeaveChannel() {
// rtcClient.leave()
// }
// // 发布本地音频轨道
// export async function PublishAudio(deviceId: string) {
// var audioTrack = await AgoraRTC.createMicrophoneAudioTrack({
// AEC: true, // 是否开启回声消除
// AGC: true, // 是否开启自动增益
// ANS: true, // 是否开启噪声抑制
// microphoneId: deviceId // 麦克风的设备 ID
// })
// await rtcClient.publish(audioTrack)
// }
// // 发布本地视频轨道 硬件设备
// export async function PublishVideo(deviceId: string) {
// var videoTrack = await AgoraRTC.createCameraVideoTrack({
// cameraId: deviceId,
// encoderConfig: '1080p_3'
// })
// await rtcClient.publish(videoTrack)
// }
// // 发布本地视频轨道 屏幕共享
// export async function PublishVideoScreen(sharedScreenItem: any) {
// var screenTrack = await AgoraRTC.createScreenVideoTrack({
// encoderConfig: "1080p_1",
// electronScreenSourceId: sharedScreenItem.id,
// })
// await rtcClient.publish(screenTrack)
// }
// // 获取麦克风设备列表
// export async function GetMicrophones() {
// return await AgoraRTC.getMicrophones()
// }
// // 获取摄像头设备列表
// export async function GetCameras() {
// return await AgoraRTC.getCameras()
// }
// // 取消发布所有流
// export async function UnpublishAll() {
// await rtcClient.unpublish()
// }

View File

@ -0,0 +1,30 @@
class LocalStorage {
private constructor() {}
private static instance: LocalStorage | null = null
static getInstance() {
if (LocalStorage.instance === null) {
LocalStorage.instance = new LocalStorage()
}
return LocalStorage.instance
}
setItem(key: string, value: any) {
localStorage.setItem(key, value)
}
getItem(key: string) {
return localStorage.getItem(key)
}
removeItem(key: string) {
localStorage.removeItem(key)
}
removeAll() {
localStorage.clear()
}
}
export default LocalStorage.getInstance()

View File

@ -0,0 +1,26 @@
import { AxiosRequestConfig, AxiosResponse } from 'axios'
import Request from './request'
import { constant } from '@/config'
// 实例化
const req = new Request({
baseURL: import.meta.env.VITE_BASE_URL_API,
timeout: constant.CONFIG_REQUEST_TIMEOUT_TIME as number,
interceptors: {
// 请求拦截器
requestInterceptors: (config: AxiosRequestConfig) => config,
// 响应拦截器 <T = AxiosResponse>(result: T)
responseInterceptors: <T = AxiosResponse>(result: T) => result,
},
})
const request = (config: any) => {
const { method = 'GET' } = config
if (method === 'get' || method === 'GET') {
config.params = config.data
}
return req.request<any>(config)
}
export default request

View File

@ -0,0 +1,108 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios'
import { RequestConfig, RequestInterceptors } from './types'
import { storage } from '@/utils'
import { constant } from '@/config'
import { message } from 'antd';
class Request {
// axios实例
instance: AxiosInstance
// 拦截器对象
interceptorsObj?: RequestInterceptors
constructor(config: RequestConfig) {
// 创建实例
this.instance = axios.create(config)
// 类请求拦截器
this.instance.interceptors.request.use(
(req: any) => {
const token = localStorage.getItem('token')
if (token) {
// 如果有token给请求头加上
req.headers.Authorization = `${token}`
req.timeout = constant.CONFIG_REQUEST_TIMEOUT_TIME
}
if (req.contentType) {
req.headers["Content-Type"] = req.contentType;
}
return req
},
(_err: any) => {
}
)
// 类响应拦截器
this.instance.interceptors.response.use(
(res: AxiosResponse) => {
const { data: resData, status, config } = res
if (status == constant.CONFIG_CODE_SUCCESS && resData && Object.prototype.toString.call(resData) == '[object Object]') {
resData.success = false
resData.code == constant.CONFIG_CODE_SUCCESS && (resData.success = true)
}
resData.headers = res.headers['content-disposition'];
if (resData.code !== 200) {
if (config.responseType === 'blob') {
const reader = new FileReader() as any;
reader.readAsText(res.data, "utf-8");
reader.onload = function () {
if (reader.result) {
try {
message.error(JSON.parse(reader.result).message)
} catch (error) {
}
}
};
} else {
message.error(resData.message)
}
}
return resData
},
(err: any) => {
function toLogin() {
storage.removeItem(constant.CONFIG_TOKEN)
}
// 根据自己业务/接口返回做相应调整
if (err.response) {
const { status, data } = err.response
message.error(data.message || data.title)
switch (status) {
case 401:
toLogin()
break
case 403:
toLogin()
break
}
} else {
message.error(err.message)
}
}
)
}
request<T>(config: RequestConfig): Promise<T> {
return new Promise((resolve, reject) => {
if (config.interceptors?.requestInterceptors) {
config = config.interceptors.requestInterceptors(config)
}
this.instance
.request<any, T>(config)
.then((res: T) => {
// 如果我们为单个响应设置拦截器,这里使用单个响应的拦截器
if (config.interceptors?.responseInterceptors) {
res = config.interceptors.responseInterceptors<T>(res)
}
resolve(res || { code: 0 } as any)
})
.catch((err: any) => {
reject(err)
})
})
}
}
export default Request

View File

@ -0,0 +1,20 @@
import { AxiosRequestConfig, AxiosResponse } from 'axios'
// 实例拦截器
export interface RequestInterceptors {
// 请求拦截
requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig
requestInterceptorsCatch?: (err: any) => any
// 响应拦截
responseInterceptors?: <T = AxiosResponse>(config: T) => T
responseInterceptorsCatch?: (err: any) => any
}
// 自定义传入的参数
export interface RequestConfig extends AxiosRequestConfig {
interceptors?: RequestInterceptors
}
export interface useRequestConfig<T> extends RequestConfig {
data?: T
}

172
src/utils/styles/App.css Normal file
View File

@ -0,0 +1,172 @@
.ant-input {
background-color: #28282C !important;
border: 1px solid #404145;
color: white;
box-sizing: border-box;
}
.ant-input::placeholder {
color: #E6E6E6;
}
.ant-input-affix-wrapper {
background-color: #28282C !important;
border: 1px solid #404145;
box-sizing: border-box;
}
.ant-input-affix-wrapper .ant-input {
color: white !important;
}
.ant-input-affix-wrapper .ant-input-password-icon {
color: white !important;
}
.m-ant-btn.ant-btn {
background-color: #3F51B5;
box-shadow: none;
}
.m-ant-btn.ant-btn:hover {
background-color: #606fc7 !important;
}
.m-ant-btn.ant-btn:active {
background-color: #32408f !important;
}
.m-border-ant-button.ant-btn {
border: 1px solid #5575F2 !important;
color: #5575F2 !important;
background-color: #1E1E1F !important;
}
.ant-checkbox-wrapper {
color: #848484;
}
.ant-checkbox-wrapper .ant-checkbox .ant-checkbox-inner {
background-color: #28282C !important;
border: 1px solid #404145;
}
.ant-checkbox-wrapper .ant-checkbox .ant-checkbox-inner::after {
background-color: #3F51B5 !important;
}
.ant-checkbox-wrapper .ant-checkbox-checked .ant-checkbox-inner {
background-color: #3F51B5 !important;
border: 1px solid #404145;
}
.ant-table {
background-color: #1B1E24 !important;
border-radius: 0px !important;
}
.ant-table .ant-table-header .ant-table-cell {
background-color: #1B1E24;
color: #808080;
box-shadow: none;
border-bottom: 1px solid transparent;
}
.ant-table .ant-table-header .ant-table-cell::before {
visibility: hidden;
}
.ant-table .ant-table-body .ant-table-row {
background-color: #16191e;
color: white;
}
.ant-table .ant-table-body .ant-table-row .ant-table-cell {
border-bottom: 1px solid #292F3A;
}
.ant-table .ant-table-body .ant-table-row-selected .ant-table-cell {
background-color: #0d0f12 !important;
color: white;
}
.ant-table .ant-table-body .ant-table-cell-row-hover {
background-color: #0d0f12 !important;
}
.ant-pagination .ant-pagination-prev {
margin-right: 10px !important;
}
.ant-pagination .ant-pagination-prev,
.ant-pagination .ant-pagination-next {
width: 30px !important;
height: 30px !important;
border-radius: 50%;
background: #20242C;
}
.ant-pagination .ant-pagination-prev .anticon,
.ant-pagination .ant-pagination-next .anticon {
color: #808080;
}
.ant-pagination .ant-pagination-item {
width: 30px !important;
height: 30px !important;
line-height: 30px !important;
border-radius: 50%;
background: #20242C !important;
margin-right: 10px !important;
}
.ant-pagination .ant-pagination-item:hover {
background: #5575F2 !important;
border: none;
box-shadow: 0px 0px 10px 0px #66C8FF;
}
.ant-pagination .ant-pagination-item:hover a {
color: black !important;
}
.ant-pagination .ant-pagination-item-active {
background: #5575F2 !important;
border: none;
box-shadow: 0px 0px 10px 0px #66C8FF;
}
.ant-pagination .ant-pagination-item-active a {
color: black !important;
}
.ant-popover:not(.ant-popconfirm) .ant-popover-arrow::before {
background-color: #07090B !important;
}
.ant-popover:not(.ant-popconfirm) .ant-popover-content .ant-popover-inner {
background-color: #07090B;
}
.ant-modal-mask {
background-color: rgba(0, 0, 0, 0.25) !important;
}
.ant-modal .ant-modal-content {
background-color: #07090B;
}
.ant-modal .ant-modal-content .ant-modal-header {
background-color: #07090B;
}
.ant-modal .ant-modal-content .ant-modal-header .ant-modal-title {
text-align: center;
color: #EEEEEE;
font-weight: bold;
}
.ant-modal .ant-modal-content .ant-modal-body {
max-height: 70vh;
overflow-y: auto;
}

1
src/utils/styles/App.min.css vendored Normal file
View File

@ -0,0 +1 @@
.ant-input{background-color:#28282C !important;border:1px solid #404145;color:white;box-sizing:border-box}.ant-input::placeholder{color:#E6E6E6}.ant-input-affix-wrapper{background-color:#28282C !important;border:1px solid #404145;box-sizing:border-box}.ant-input-affix-wrapper .ant-input{color:white !important}.ant-input-affix-wrapper .ant-input-password-icon{color:white !important}.m-ant-btn.ant-btn{background-color:#3F51B5;box-shadow:none}.m-ant-btn.ant-btn:hover{background-color:#606fc7 !important}.m-ant-btn.ant-btn:active{background-color:#32408f !important}.m-border-ant-button.ant-btn{border:1px solid #5575F2 !important;color:#5575F2 !important;background-color:#1E1E1F !important}.ant-checkbox-wrapper{color:#848484}.ant-checkbox-wrapper .ant-checkbox .ant-checkbox-inner{background-color:#28282C !important;border:1px solid #404145}.ant-checkbox-wrapper .ant-checkbox .ant-checkbox-inner::after{background-color:#3F51B5 !important}.ant-checkbox-wrapper .ant-checkbox-checked .ant-checkbox-inner{background-color:#3F51B5 !important;border:1px solid #404145}.ant-table{background-color:#1B1E24 !important;border-radius:0px !important}.ant-table .ant-table-header .ant-table-cell{background-color:#1B1E24;color:#808080;box-shadow:none;border-bottom:1px solid transparent}.ant-table .ant-table-header .ant-table-cell::before{visibility:hidden}.ant-table .ant-table-body .ant-table-row{background-color:#16191e;color:white}.ant-table .ant-table-body .ant-table-row .ant-table-cell{border-bottom:1px solid #292F3A}.ant-table .ant-table-body .ant-table-row-selected .ant-table-cell{background-color:#0d0f12 !important;color:white}.ant-table .ant-table-body .ant-table-cell-row-hover{background-color:#0d0f12 !important}.ant-pagination .ant-pagination-prev{margin-right:10px !important}.ant-pagination .ant-pagination-prev,.ant-pagination .ant-pagination-next{width:30px !important;height:30px !important;border-radius:50%;background:#20242C}.ant-pagination .ant-pagination-prev .anticon,.ant-pagination .ant-pagination-next .anticon{color:#808080}.ant-pagination .ant-pagination-item{width:30px !important;height:30px !important;line-height:30px !important;border-radius:50%;background:#20242C !important;margin-right:10px !important}.ant-pagination .ant-pagination-item:hover{background:#5575F2 !important;border:none;box-shadow:0px 0px 10px 0px #66C8FF}.ant-pagination .ant-pagination-item:hover a{color:black !important}.ant-pagination .ant-pagination-item-active{background:#5575F2 !important;border:none;box-shadow:0px 0px 10px 0px #66C8FF}.ant-pagination .ant-pagination-item-active a{color:black !important}.ant-popover:not(.ant-popconfirm) .ant-popover-arrow::before{background-color:#07090B !important}.ant-popover:not(.ant-popconfirm) .ant-popover-content .ant-popover-inner{background-color:#07090B}.ant-modal-mask{background-color:rgba(0,0,0,0.25) !important}.ant-modal .ant-modal-content{background-color:#07090B}.ant-modal .ant-modal-content .ant-modal-header{background-color:#07090B}.ant-modal .ant-modal-content .ant-modal-header .ant-modal-title{text-align:center;color:#EEEEEE;font-weight:bold}.ant-modal .ant-modal-content .ant-modal-body{max-height:70vh;overflow-y:auto}

208
src/utils/styles/App.scss Normal file
View File

@ -0,0 +1,208 @@
$btn-background-color: #3F51B5;
$btn-border-background-color: #1E1E1F;
$btn-border-text-color: #5575F2;
$input-background-color: #28282C;
$input-border-color: #404145;
$table-header-background-color: #1B1E24;
$table-content-background-color: rgb(22, 25, 30);
$pagination-background-color: #20242C;
$pagination-hover-background-color: #5575F2;
// input
.ant-input {
background-color: $input-background-color !important;
border: 1px solid $input-border-color;
color: white;
box-sizing: border-box;
&::placeholder {
color: #E6E6E6;
}
}
.ant-input-affix-wrapper {
background-color: $input-background-color !important;
border: 1px solid $input-border-color;
box-sizing: border-box;
.ant-input {
color: white !important;
}
.ant-input-password-icon {
color: white !important;
}
}
// button
.m-ant-btn.ant-btn {
background-color: $btn-background-color;
box-shadow: none;
&:hover {
background-color: lighten($btn-background-color, 10%) !important;
}
&:active {
background-color: darken($btn-background-color, 10%) !important;
}
}
.m-border-ant-button.ant-btn {
border: 1px solid $btn-border-text-color !important;
color: $btn-border-text-color !important;
background-color: $btn-border-background-color !important;
}
// checkbox
.ant-checkbox-wrapper {
color: #848484;
.ant-checkbox {
.ant-checkbox-inner {
background-color: $input-background-color !important;
border: 1px solid $input-border-color;
&::after {
background-color: $btn-background-color !important;
}
}
}
.ant-checkbox-checked {
.ant-checkbox-inner {
background-color: $btn-background-color !important;
border: 1px solid $input-border-color;
}
}
}
// table
.ant-table {
background-color: $table-header-background-color !important;
border-radius: 0px !important;
.ant-table-header {
.ant-table-cell {
background-color: $table-header-background-color;
color: #808080;
box-shadow: none;
border-bottom: 1px solid transparent;
&::before {
visibility: hidden;
}
}
}
.ant-table-body {
.ant-table-row {
background-color: $table-content-background-color;
color: white;
.ant-table-cell {
border-bottom: 1px solid #292F3A;
}
}
.ant-table-row-selected {
.ant-table-cell {
background-color: darken($table-content-background-color, 4%) !important;
color: white;
}
}
.ant-table-cell-row-hover {
background-color: darken($table-content-background-color, 4%) !important;
}
}
}
// pagination
.ant-pagination {
.ant-pagination-prev {
margin-right: 10px !important;
}
.ant-pagination-prev,
.ant-pagination-next {
width: 30px !important;
height: 30px !important;
border-radius: 50%;
background: $pagination-background-color;
.anticon {
color: #808080;
}
}
.ant-pagination-item {
width: 30px !important;
height: 30px !important;
line-height: 30px !important;
border-radius: 50%;
background: $pagination-background-color !important;
margin-right: 10px !important;
&:hover {
background: $pagination-hover-background-color !important;
border: none;
box-shadow: 0px 0px 10px 0px #66C8FF;
a {
color: black !important;
}
}
}
.ant-pagination-item-active {
background: $pagination-hover-background-color !important;
border: none;
box-shadow: 0px 0px 10px 0px #66C8FF;
a {
color: black !important;
}
}
}
// popover
.ant-popover:not(.ant-popconfirm) {
.ant-popover-arrow {
&::before {
background-color: #07090B !important;
}
}
.ant-popover-content {
.ant-popover-inner {
background-color: #07090B;
}
}
}
// modal
.ant-modal-mask {
background-color: rgba(0, 0, 0, 0.25) !important;
}
.ant-modal {
.ant-modal-content {
background-color: #07090B;
.ant-modal-header {
background-color: #07090B;
.ant-modal-title {
text-align: center;
color: #EEEEEE;
font-weight: bold;
}
}
.ant-modal-body {
max-height: 70vh;
overflow-y: auto;
}
}
}

55
src/utils/styles/main.css Normal file
View File

@ -0,0 +1,55 @@
html,
body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
font-size: 16px;
-webkit-user-drag: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
img {
display: block;
}
#root {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
font-size: 16px;
overflow: hidden;
border-radius: 10px;
-webkit-app-region: drag;
}
.drag {
-webkit-app-region: no-drag;
}
/* 修改垂直滚动条 */
::-webkit-scrollbar {
width: 6px;
}
/* 修改滚动条轨道背景色 */
::-webkit-scrollbar-track {
background-color: transparent;
}
/* 修改滚动条滑块颜色 */
::-webkit-scrollbar-thumb {
background-color: rgb(52, 52, 52);
border-radius: 10px;
}
/* 修改滚动条滑块悬停时的颜色 */
::-webkit-scrollbar-thumb:hover {
background-color: rgb(109, 109, 109);
}

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

37
tsconfig.json Normal file
View File

@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"./auto-imports.d.ts"
],
"references": [{ "path": "./tsconfig.node.json" }]
}

11
tsconfig.node.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

54
vite.config.ts Normal file
View File

@ -0,0 +1,54 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import pxtovw from 'postcss-px-to-viewport-8-plugin'
import { resolve } from 'path'
const loder_pxtovw = pxtovw({
viewportWidth: 1900,
viewportUnit: 'vw',
selectorBlackList: ['.login','.ant-pagination']
})
export default defineConfig({
css: {
postcss: {
plugins: [loder_pxtovw],
},
},
server: {
host: '0.0.0.0',
proxy: {
}
},
resolve: {
alias: [
{
find: '@',
replacement: resolve(__dirname, 'src'),
},
],
},
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
},
},
rollupOptions: {
output: {
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
compact: true,
manualChunks: {
}
},
}
},
base: './', // 这里更改打包相对绝对路径
plugins: [
react(),
],
})