初始化
|
|
@ -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'
|
||||
|
|
@ -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'
|
||||
|
|
@ -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'
|
||||
|
|
@ -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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
@ -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/
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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',
|
||||
})
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 827 B |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 361 B |
|
After Width: | Height: | Size: 386 B |
|
After Width: | Height: | Size: 337 B |
|
After Width: | Height: | Size: 493 B |
|
After Width: | Height: | Size: 267 B |
|
After Width: | Height: | Size: 378 B |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 344 B |
|
After Width: | Height: | Size: 449 B |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 404 B |
|
After Width: | Height: | Size: 130 B |
|
After Width: | Height: | Size: 184 B |
|
After Width: | Height: | Size: 213 B |
|
After Width: | Height: | Size: 337 B |
|
After Width: | Height: | Size: 156 B |
|
After Width: | Height: | Size: 414 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 622 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 477 B |
|
After Width: | Height: | Size: 530 B |
|
After Width: | Height: | Size: 495 B |
|
After Width: | Height: | Size: 675 B |
|
After Width: | Height: | Size: 977 B |
|
After Width: | Height: | Size: 760 B |
|
After Width: | Height: | Size: 572 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 312 B |
|
After Width: | Height: | Size: 769 B |
|
After Width: | Height: | Size: 641 B |
|
After Width: | Height: | Size: 299 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 309 B |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 213 B |
|
After Width: | Height: | Size: 186 B |
|
After Width: | Height: | Size: 237 B |
|
After Width: | Height: | Size: 113 KiB |
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
import { useEffect } from "react";
|
||||
|
||||
const NotFound: React.FC = () => {
|
||||
useEffect(() => {
|
||||
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div className="notfound"></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotFound
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
declare module 'react-dom/client';
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import storage from './package/storage'
|
||||
import request from './request'
|
||||
|
||||
export { storage, request }
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
appid: "dcfc466a6ecb4a1f972630065dfb1e75",
|
||||
token: "007eJxTYNDz89yosT64YN5i3snT6nzY/aVOvs1Tmu3zc9r6qUkXHyxSYEhJTks2MTNLNEtNTjJJNEyzNDcyMzYwMDNNSUsyTDU3FQxuTmsIZGSQn/iShZEBAkF8bgZDc3MjYwMLU2MTMwYGAAHrH7M=",
|
||||
channelId: '17723085346',
|
||||
}
|
||||
|
|
@ -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');
|
||||
};
|
||||
|
|
@ -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()
|
||||
// }
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -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" }]
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -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(),
|
||||
],
|
||||
})
|
||||