【山大会议】使用TypeScript为项目进行重构

TypeScript 简介

TypeScript 是 JavaScript 的一个超集,支持 ECMAScript 6 标准。TypeScript 由微软开发的自由和开源的编程语言。其设计目标是开发大型应用,它可以编译成纯 JavaScript,编译出来的 JavaScript 可以运行在任何浏览器上。
简单点说,你可以认为它是一个强类型的 JavaScript 方言,在面对一些大型项目时,拥有类型检查总能协助开发者降低一些 bug 的出现几率。随着本项目的规模不断扩大,我决定为项目引入 TypeScript ,为项目进行重构。

依赖安装

首先我们安装好 typescript

yarn add -D typescript

为了让 webpack 能够识别我们书写的 ts 代码,我们还需要引入 ts-loader:

yarn add -D ts-loader

由于 typescript 与 react 结合得很好,我们不需要再使用 babel 对代码进行转换,typescript 自己会将自己编译好。因此,我们也可以将原来的 babel 依赖从项目中移除了:

yarn remove @babel/core @babel/plugin-proposal-class-properties @babel/preset-env @babel/preset-react babel-loader

不过,由于 reactreact-dom 本身没有实现类型定义,所以我们为了能用 ts 书写 react 代码,还需要额外引入 reactreact-domtypes 依赖:

yarn add -D @types/react @types/react-dom

现在,所需要的依赖已经安装好了,我们可以来书写代码了。

tsconfig.json

我们在项目的根目录下打开终端,键入命令:

npx typescript init

我们会在项目根目录下看到一个自动生成的 tsconfig.json 文件,我们可以根据自己的实际需要进行配置,我的配置如下:

{
    
    
	"compilerOptions": {
    
    
		"target": "es2016",
		"jsx": "react",
		"module": "commonjs",
		"esModuleInterop": true,
		"forceConsistentCasingInFileNames": true,
		"strict": true,
		"skipLibCheck": true,
		"baseUrl": "./",
		"resolveJsonModule": true,
		"paths": {
    
    
			"Components/*": ["src/Components/*"],
			"Utils/*": ["src/Utils/*"],
			"Views/*": ["src/Views/*"]
		}
	},
	"include": ["src/**/*"]
}

修改 webpack.config.js

由于我们将项目重构为了 typescript 的项目,因此,我们编写的不再是 .js.jsx 文件,而是 .ts.tsx 文件,我们的 webpack.config.js 也需要据此做出改动:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {
    
     CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    
    
	devServer: {
    
    
		static: path.join(__dirname, 'public'),
		host: '127.0.0.1',
		port: 9000,
	},
	resolve: {
    
    
		extensions: ['.js', '.json', '.ts', '.tsx'],
		alias: {
    
    
			Components: path.join(__dirname, 'src/Components'),
			Views: path.join(__dirname, 'src/Views'),
			Utils: path.join(__dirname, 'src/Utils'),
		},
	},
	entry: {
    
    
		login: './src/Views/Login/index.tsx',
		register: './src/Views/Register/index.tsx',
	},
	output: {
    
    
		path: path.resolve(__dirname, './build'),
		filename: '[name]/index.[chunkhash:8].js',
	},
	module: {
    
    
		rules: [
			{
    
    
				test: /\.(sa|sc|c)ss$/,
				use: [
					'style-loader',
					'css-loader',
					'resolve-url-loader',
					{
    
    
						loader: 'sass-loader',
						options: {
    
    
							sourceMap: true,
						},
					},
				],
			},
			{
    
    
				test: /\.tsx?$/,
				exclude: /(node_modules|bower_components)/,
				use: [
					{
    
    
						loader: 'ts-loader',
					},
				],
			},
			{
    
    
				test: /\.(png|jpg|gif|mp3)$/,
				use: [
					{
    
    
						loader: 'url-loader',
						options: {
    
    
							limit: 1024, //对文件的大小做限制,1kb
						},
					},
				],
			},
		],
	},
	plugins: [
		new CleanWebpackPlugin(),
		new HtmlWebpackPlugin({
    
    
			filename: 'login/index.html',
			chunks: ['login'],
			template: './public/index.html',
		}),
		new HtmlWebpackPlugin({
    
    
			filename: 'register/index.html',
			chunks: ['register'],
			template: './public/index.html',
		}),
	],
};

修改旧的代码

index.tsx

import React from 'react';
// let rootDiv = document.getElementById('root')
// if (!rootDiv) {
    
    
//     rootDiv = document.createElement('div')
//     rootDiv.setAttribute('id', 'root')
// }
// createRoot(rootDiv).render(<App />)
import {
    
     render } from 'react-dom';
// import { createRoot } from 'react-dom/client';
import App from './App';

render(<App />, document.getElementById('root'));

登录页

import {
    
     LoadingOutlined, LockOutlined, UserOutlined } from '@ant-design/icons';
import {
    
     Checkbox, Form, Input } from 'antd';
import {
    
     globalMessage } from 'Components/GlobalMessage/GlobalMessage';
import {
    
     LogoIcon, MinimizeIcon, RegisterIcon, ShutdownIcon } from 'Components/MyIcon/MyIcon';
import RippleButton from 'Components/RippleButton/RippleButton';
import {
    
     Victor } from 'Components/Victor/Victor';
import React, {
    
     useEffect, useRef, useState } from 'react';
import {
    
     ajax } from 'Utils/Axios/Axios';
import {
    
     decodeJWT } from 'Utils/Global';
import './App.scss';

export default function App() {
    
    
    const [form] = Form.useForm();

    const lastUserId = localStorage.getItem('userId');
    const [userId, setUserId] = useState(lastUserId === 'null' ? '' : lastUserId);

    const [userPassword, setUserPassword] = useState('');
    const [rememberPassword, setRememberPassword] = useState(
        localStorage.getItem('rememberPassword') === 'true'
    );
    useEffect(() => {
    
    
        if (rememberPassword) {
    
    
            (window as any).ipc.invoke('GET_LAST_PASSWORD').then((psw: string) => {
    
    
                form.setFieldsValue({
    
     password: psw });
            });
        }
    }, []);

    const [rotating, setRotating] = useState(false);
    useEffect(() => {
    
    
        if (rotating) {
    
    
            const mainBody = mainBodyRef.current as HTMLDivElement;
            mainBody.style.animationName = 'rotateOut';
            let timeout = setTimeout(() => {
    
    
                mainBody.style.animationName = 'rotateIn';
                setShowRegister(!showRegister);
                clearTimeout(timeout);
            }, 250);
            return () => {
    
    
                if (timeout) {
    
    
                    clearTimeout(timeout);
                }
            };
        }
    }, [rotating]);

    const [showRegister, setShowRegister] = useState(false);
    useEffect(() => {
    
    
        let timeout = setTimeout(() => {
    
    
            setRotating(false);
            clearTimeout(timeout)
        }, 250);
        return () => {
    
    
            if (timeout) {
    
    
                clearTimeout(timeout);
            }
        };
    }, [showRegister]);

    const mainBodyRef = useRef<HTMLDivElement>(null);

    // 设置动态背景
    useEffect(() => {
    
    
        const victor = Victor('header', 'canvas');
        const theme = ['#ff1324', '#ff3851'];
        if (victor)
            victor(theme).set();
    }, []);

    const [isLogining, setIsLogining] = useState(false);
    const login = () => {
    
    
        form.validateFields(['username', 'password'])
            .then(async (values) => {
    
    
                setIsLogining(true);
                const text = values.username;
                const password = values.password;
                const res = await ajax.post('/login_and_register/login', {
    
     text, password });
                if (res.code === 200) {
    
    
                    globalMessage.success('登录成功');
                    localStorage.setItem('rememberPassword', `${
      
      rememberPassword}`);
                    localStorage.setItem('autoLogin', `${
      
      autoLogin}`);
                    (window as any).ipc.send('SAFE_PASSWORD', rememberPassword, password);
                    localStorage.setItem('userId', text);
                    (window as any).ipc.send('USER_LOGIN', res.data.token, decodeJWT(res.data.token).email);
                } else {
    
    
                    globalMessage.error(res.message);
                    setIsLogining(false);
                }
            })
            .catch((err) => {
    
    
                if (err.ajax) {
    
    
                    globalMessage.error('服务器错误,请稍后重试');
                } else {
    
    
                    const {
    
     values } = err;
                    if (values.username === undefined) {
    
    
                        globalMessage.error('请输入用户名或邮箱!');
                    } else if (values.password === undefined) {
    
    
                        globalMessage.error('请输入密码!');
                    }
                }
                setIsLogining(false);
            });
    };

    /**
     * INFO: 自动登录
     */
    const [autoLogin, setAutoLogin] = useState(localStorage.getItem('autoLogin') === 'true');
    useEffect(() => {
    
    
        let timeout = setTimeout(() => {
    
    
            if (autoLogin) login();
            clearTimeout(timeout);
        }, 0);
        return () => {
    
    
            if (timeout) {
    
    
                clearTimeout(timeout);
            }
        };
    }, []);

    return (
        <>
            <div id='dragBar' />
            <div id='mainBody' ref={
    
    mainBodyRef}>
                <div id='header'>
                    <div id='titleBar'>
                        <LogoIcon style={
    
    {
    
     fontSize: '1.5rem' }} />
                        <span style={
    
    {
    
     fontFamily: 'Microsoft Yahei' }}>山大会议</span>
                        <button
                            className='titleBtn'
                            id='shutdown'
                            title='退出'
                            onClick={
    
    () => {
    
    
                                (window as any).ipc.send('QUIT');
                            }}>
                            <ShutdownIcon />
                        </button>
                        <button
                            className='titleBtn'
                            id='minimize'
                            title='最小化'
                            onClick={
    
    () => {
    
    
                                (window as any).ipc.send('MINIMIZE_LOGIN_WINDOW');
                            }}>
                            <MinimizeIcon />
                        </button>
                        <button
                            className='titleBtn'
                            id='switch'
                            title={
    
    showRegister ? '返回登录' : '注册账号'}
                            onClick={
    
    () => {
    
    
                                setRotating(true);
                            }}>
                            <RegisterIcon />
                        </button>
                    </div>
                    <div id='canvas' />
                </div>
                <div className='main'>
                    <div
                        className='form'
                        id='loginForm'
                        style={
    
    {
    
     display: showRegister ? 'none' : 'block' }}>
                        <Form form={
    
    form}>
                            <Form.Item
                                name='username'
                                rules={
    
    [
                                    {
    
    
                                        required: true,
                                        message: '请输入用户名或邮箱',
                                    },
                                ]}
                                initialValue={
    
    userId}>
                                <Input
                                    placeholder='请输入用户名或邮箱'
                                    spellCheck={
    
    false}
                                    prefix={
    
    <UserOutlined />}
                                    size={
    
    'large'}
                                    style={
    
    {
    
     width: '65%' }}
                                    onChange={
    
    (event) => {
    
    
                                        setUserId(event.target.value);
                                    }}
                                />
                            </Form.Item>
                            <Form.Item
                                name='password'
                                rules={
    
    [
                                    {
    
    
                                        required: true,
                                        message: '密码不得为空',
                                    },
                                ]}
                                initialValue={
    
    userPassword}>
                                <Input.Password
                                    placeholder='请输入密码'
                                    spellCheck={
    
    false}
                                    prefix={
    
    <LockOutlined />}
                                    size={
    
    'large'}
                                    style={
    
    {
    
     width: '65%' }}
                                    onChange={
    
    (event) => {
    
    
                                        setUserPassword(event.target.value);
                                    }}
                                />
                            </Form.Item>
                            <Form.Item>
                                <Checkbox
                                    style={
    
    {
    
     fontSize: '0.75rem' }}
                                    checked={
    
    rememberPassword}
                                    onChange={
    
    (e) => {
    
    
                                        setRememberPassword(e.target.checked);
                                    }}>
                                    记住密码
                                </Checkbox>
                                <Checkbox
                                    style={
    
    {
    
     fontSize: '0.75rem' }}
                                    checked={
    
    autoLogin}
                                    onChange={
    
    (e) => {
    
    
                                        setAutoLogin(e.target.checked);
                                    }}>
                                    自动登录
                                </Checkbox>
                            </Form.Item>
                            <Form.Item>
                                <RippleButton
                                    className='submit'
                                    onClick={
    
    login}
                                    disabled={
    
    isLogining}>
                                    <>{
    
    isLogining && <LoadingOutlined />} 登 录</>
                                </RippleButton>
                            </Form.Item>
                        </Form>
                    </div>
                    <div
                        className='form'
                        id='registerForm'
                        style={
    
    {
    
     display: showRegister ? 'flex' : 'none' }}>
                        <RippleButton
                            className='submit'
                            onClick={
    
    () => {
    
    
                                const registerUrl =
                                    process.env.NODE_ENV === 'development'
                                        ? './register/'
                                        : '../register/index.html';
                                window.open(registerUrl);
                            }}>
                            注 册
                        </RippleButton>
                    </div>
                </div>
            </div>
        </>
    );
}

Victor.ts

let CAV: any = {
    
    
	FRONT: 0,
	BACK: 1,
	DOUBLE: 2,
	SVGNS: 'http://www.w3.org/2000/svg',
	Array: typeof Float32Array === 'function' ? Float32Array : Array,
	Utils: {
    
    
		isNumber: function (a: any) {
    
    
			return !isNaN(parseFloat(a)) && isFinite(a);
		},
	}
};
(function () {
    
    
	for (
		var a = 0, b = ['ms', 'moz', 'webkit', 'o'], c = 0;
		c < b.length && !window.requestAnimationFrame;
		++c
	)
		(window.requestAnimationFrame = (window as any)[b[c] + 'RequestAnimationFrame']),
			(window.cancelAnimationFrame =
				(window as any)[b[c] + 'CancelAnimationFrame'] ||
				(window as any)[b[c] + 'CancelRequestAnimationFrame']);
	if (!window.requestAnimationFrame)
		window.requestAnimationFrame = function (b) {
    
    
			var c = new Date().getTime(),
				f = Math.max(0, 16 - (c - a)),
				g = window.setTimeout(function () {
    
    
					b(c + f);
				}, f);
			a = c + f;
			return g;
		};
	if (!window.cancelAnimationFrame)
		window.cancelAnimationFrame = function (a) {
    
    
			clearTimeout(a);
		};
})();

const randomInRange = function (a: number, b: number) {
    
    
	return a + (b - a) * Math.random();
};
const clamp = function (a: number, b: number, c: number) {
    
    
	a = Math.max(a, b);
	return (a = Math.min(a, c));
};

CAV.Vector3 = {
    
    
	create: function (a: any, b: any, c: any) {
    
    
		var d = new CAV.Array(3);
		this.set(d, a, b, c);
		return d;
	},
	clone: function (a: any) {
    
    
		var b = this.create();
		this.copy(b, a);
		return b;
	},
	set: function (a: any[], b: number, c: number, d: number) {
    
    
		a[0] = b || 0;
		a[1] = c || 0;
		a[2] = d || 0;
		return this;
	},
	setX: function (a: any[], b: number) {
    
    
		a[0] = b || 0;
		return this;
	},
	setY: function (a: any[], b: number) {
    
    
		a[1] = b || 0;
		return this;
	},
	setZ: function (a: any[], b: number) {
    
    
		a[2] = b || 0;
		return this;
	},
	copy: function (a: any[], b: any[]) {
    
    
		a[0] = b[0];
		a[1] = b[1];
		a[2] = b[2];
		return this;
	},
	add: function (a: any[], b: any[]) {
    
    
		a[0] += b[0];
		a[1] += b[1];
		a[2] += b[2];
		return this;
	},
	addVectors: function (a: any[], b: any[], c: any[]) {
    
    
		a[0] = b[0] + c[0];
		a[1] = b[1] + c[1];
		a[2] = b[2] + c[2];
		return this;
	},
	addScalar: function (a: any[], b: any) {
    
    
		a[0] += b;
		a[1] += b;
		a[2] += b;
		return this;
	},
	subtract: function (a: number[], b: number[]) {
    
    
		a[0] -= b[0];
		a[1] -= b[1];
		a[2] -= b[2];
		return this;
	},
	subtractVectors: function (a: number[], b: number[], c: number[]) {
    
    
		a[0] = b[0] - c[0];
		a[1] = b[1] - c[1];
		a[2] = b[2] - c[2];
		return this;
	},
	subtractScalar: function (a: number[], b: number) {
    
    
		a[0] -= b;
		a[1] -= b;
		a[2] -= b;
		return this;
	},
	multiply: function (a: number[], b: number[]) {
    
    
		a[0] *= b[0];
		a[1] *= b[1];
		a[2] *= b[2];
		return this;
	},
	multiplyVectors: function (a: number[], b: number[], c: number[]) {
    
    
		a[0] = b[0] * c[0];
		a[1] = b[1] * c[1];
		a[2] = b[2] * c[2];
		return this;
	},
	multiplyScalar: function (a: number[], b: number) {
    
    
		a[0] *= b;
		a[1] *= b;
		a[2] *= b;
		return this;
	},
	divide: function (a: number[], b: number[]) {
    
    
		a[0] /= b[0];
		a[1] /= b[1];
		a[2] /= b[2];
		return this;
	},
	divideVectors: function (a: number[], b: number[], c: number[]) {
    
    
		a[0] = b[0] / c[0];
		a[1] = b[1] / c[1];
		a[2] = b[2] / c[2];
		return this;
	},
	divideScalar: function (a: number[], b: number) {
    
    
		b !== 0 ? ((a[0] /= b), (a[1] /= b), (a[2] /= b)) : ((a[0] = 0), (a[1] = 0), (a[2] = 0));
		return this;
	},
	cross: function (a: number[], b: number[]) {
    
    
		var c = a[0],
			d = a[1],
			e = a[2];
		a[0] = d * b[2] - e * b[1];
		a[1] = e * b[0] - c * b[2];
		a[2] = c * b[1] - d * b[0];
		return this;
	},
	crossVectors: function (a: number[], b: number[], c: number[]) {
    
    
		a[0] = b[1] * c[2] - b[2] * c[1];
		a[1] = b[2] * c[0] - b[0] * c[2];
		a[2] = b[0] * c[1] - b[1] * c[0];
		return this;
	},
	min: function (a: any[], b: number) {
    
    
		a[0] < b && (a[0] = b);
		a[1] < b && (a[1] = b);
		a[2] < b && (a[2] = b);
		return this;
	},
	max: function (a: any[], b: number) {
    
    
		a[0] > b && (a[0] = b);
		a[1] > b && (a[1] = b);
		a[2] > b && (a[2] = b);
		return this;
	},
	clamp: function (a: any, b: any, c: any) {
    
    
		this.min(a, b);
		this.max(a, c);
		return this;
	},
	limit: function (a: any, b: number | null, c: number | null) {
    
    
		var d = this.length(a);
		b !== null && d < b ? this.setLength(a, b) : c !== null && d > c && this.setLength(a, c);
		return this;
	},
	dot: function (a: number[], b: number[]) {
    
    
		return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
	},
	normalise: function (a: any) {
    
    
		return this.divideScalar(a, this.length(a));
	},
	negate: function (a: any) {
    
    
		return this.multiplyScalar(a, -1);
	},
	distanceSquared: function (a: number[], b: number[]) {
    
    
		var c = a[0] - b[0],
			d = a[1] - b[1],
			e = a[2] - b[2];
		return c * c + d * d + e * e;
	},
	distance: function (a: any, b: any) {
    
    
		return Math.sqrt(this.distanceSquared(a, b));
	},
	lengthSquared: function (a: number[]) {
    
    
		return a[0] * a[0] + a[1] * a[1] + a[2] * a[2];
	},
	length: function (a: any) {
    
    
		return Math.sqrt(this.lengthSquared(a));
	},
	setLength: function (a: any, b: number) {
    
    
		var c = this.length(a);
		c !== 0 && b !== c && this.multiplyScalar(a, b / c);
		return this;
	},
};
CAV.Vector4 = {
    
    
	create: function (a: any, b: any, c: any) {
    
    
		var d = new CAV.Array(4);
		this.set(d, a, b, c);
		return d;
	},
	set: function (a: any[], b: number, c: number, d: number, e: number) {
    
    
		a[0] = b || 0;
		a[1] = c || 0;
		a[2] = d || 0;
		a[3] = e || 0;
		return this;
	},
	setX: function (a: any[], b: number) {
    
    
		a[0] = b || 0;
		return this;
	},
	setY: function (a: any[], b: number) {
    
    
		a[1] = b || 0;
		return this;
	},
	setZ: function (a: any[], b: number) {
    
    
		a[2] = b || 0;
		return this;
	},
	setW: function (a: any[], b: number) {
    
    
		a[3] = b || 0;
		return this;
	},
	add: function (a: any[], b: any[]) {
    
    
		a[0] += b[0];
		a[1] += b[1];
		a[2] += b[2];
		a[3] += b[3];
		return this;
	},
	multiplyVectors: function (a: number[], b: number[], c: number[]) {
    
    
		a[0] = b[0] * c[0];
		a[1] = b[1] * c[1];
		a[2] = b[2] * c[2];
		a[3] = b[3] * c[3];
		return this;
	},
	multiplyScalar: function (a: number[], b: number) {
    
    
		a[0] *= b;
		a[1] *= b;
		a[2] *= b;
		a[3] *= b;
		return this;
	},
	min: function (a: any[], b: number) {
    
    
		a[0] < b && (a[0] = b);
		a[1] < b && (a[1] = b);
		a[2] < b && (a[2] = b);
		a[3] < b && (a[3] = b);
		return this;
	},
	max: function (a: any[], b: number) {
    
    
		a[0] > b && (a[0] = b);
		a[1] > b && (a[1] = b);
		a[2] > b && (a[2] = b);
		a[3] > b && (a[3] = b);
		return this;
	},
	clamp: function (a: any, b: any, c: any) {
    
    
		this.min(a, b);
		this.max(a, c);
		return this;
	},
};
CAV.Color = function (a: string, b: any) {
    
    
	this.rgba = CAV.Vector4.create();
	this.hex = a || '#000000';
	this.opacity = CAV.Utils.isNumber(b) ? b : 1;
	this.set(this.hex, this.opacity);
};
CAV.Color.prototype = {
    
    
	set: function (a: string, b: any) {
    
    
		var a = a.replace('#', ''),
			c = a.length / 3;
		this.rgba[0] = parseInt(a.substring(c * 0, c * 1), 16) / 255;
		this.rgba[1] = parseInt(a.substring(c * 1, c * 2), 16) / 255;
		this.rgba[2] = parseInt(a.substring(c * 2, c * 3), 16) / 255;
		this.rgba[3] = CAV.Utils.isNumber(b) ? b : this.rgba[3];
		return this;
	},
	hexify: function (a: string | number) {
    
    
		a = Math.ceil(Number(a) * 255).toString(16);
		a.length === 1 && (a = '0' + a);
		return a;
	},
	format: function () {
    
    
		var a = this.hexify(this.rgba[0]),
			b = this.hexify(this.rgba[1]),
			c = this.hexify(this.rgba[2]);
		return (this.hex = '#' + a + b + c);
	},
};
CAV.Object = function () {
    
    
	this.position = CAV.Vector3.create();
};
CAV.Object.prototype = {
    
    
	setPosition: function (a: any, b: any, c: any) {
    
    
		CAV.Vector3.set(this.position, a, b, c);
		return this;
	},
};
CAV.Light = function (a: any, b: any) {
    
    
	CAV.Object.call(this);
	this.ambient = new CAV.Color(a || '#FFFFFF');
	this.diffuse = new CAV.Color(b || '#FFFFFF');
	this.ray = CAV.Vector3.create();
};
CAV.Light.prototype = Object.create(CAV.Object.prototype);
CAV.Vertex = function (a: any, b: any, c: any) {
    
    
	this.position = CAV.Vector3.create(a, b, c);
};
CAV.Vertex.prototype = {
    
    
	setPosition: function (a: any, b: any, c: any) {
    
    
		CAV.Vector3.set(this.position, a, b, c);
		return this;
	},
};
CAV.Triangle = function (a: any, b: any, c: any) {
    
    
	this.a = a || new CAV.Vertex();
	this.b = b || new CAV.Vertex();
	this.c = c || new CAV.Vertex();
	this.vertices = [this.a, this.b, this.c];
	this.u = CAV.Vector3.create();
	this.v = CAV.Vector3.create();
	this.centroid = CAV.Vector3.create();
	this.normal = CAV.Vector3.create();
	this.color = new CAV.Color();
	this.polygon = document.createElementNS(CAV.SVGNS, 'polygon');
	this.polygon.setAttributeNS(null, 'stroke-linejoin', 'round');
	this.polygon.setAttributeNS(null, 'stroke-miterlimit', '1');
	this.polygon.setAttributeNS(null, 'stroke-width', '1');
	this.computeCentroid();
	this.computeNormal();
};
CAV.Triangle.prototype = {
    
    
	computeCentroid: function () {
    
    
		this.centroid[0] = this.a.position[0] + this.b.position[0] + this.c.position[0];
		this.centroid[1] = this.a.position[1] + this.b.position[1] + this.c.position[1];
		this.centroid[2] = this.a.position[2] + this.b.position[2] + this.c.position[2];
		CAV.Vector3.divideScalar(this.centroid, 3);
		return this;
	},
	computeNormal: function () {
    
    
		CAV.Vector3.subtractVectors(this.u, this.b.position, this.a.position);
		CAV.Vector3.subtractVectors(this.v, this.c.position, this.a.position);
		CAV.Vector3.crossVectors(this.normal, this.u, this.v);
		CAV.Vector3.normalise(this.normal);
		return this;
	},
};
CAV.Geometry = function () {
    
    
	this.vertices = [];
	this.triangles = [];
	this.dirty = false;
};
CAV.Geometry.prototype = {
    
    
	update: function () {
    
    
		if (this.dirty) {
    
    
			var a, b;
			for (a = this.triangles.length - 1; a >= 0; a--)
				(b = this.triangles[a]), b.computeCentroid(), b.computeNormal();
			this.dirty = false;
		}
		return this;
	},
};
CAV.Plane = function (a: number, b: number, _c: number, d: number) {
    
    
	let t0, t1;
	CAV.Geometry.call(this);
	this.width = a || 100;
	this.height = b || 100;
	this.segments = _c || 4;
	this.slices = d || 4;
	this.segmentWidth = this.width / this.segments;
	this.sliceHeight = this.height / this.slices;
	var e,
		f,
		g,
		c = [];
	e = this.width * -0.5;
	f = this.height * 0.5;
	for (a = 0; a <= this.segments; a++) {
    
    
		c.push([]);
		for (b = 0; b <= this.slices; b++)
			(d = new CAV.Vertex(e + a * this.segmentWidth, f - b * this.sliceHeight)),
				(c[a] as Array<any>).push(d),
				this.vertices.push(d);
	}
	for (a = 0; a < this.segments; a++)
		for (b = 0; b < this.slices; b++)
			(d = c[a + 0][b + 0]),
				(e = c[a + 0][b + 1]),
				(f = c[a + 1][b + 0]),
				(g = c[a + 1][b + 1]),
				(t0 = new CAV.Triangle(d, e, f)),
				(t1 = new CAV.Triangle(f, e, g)),
				this.triangles.push(t0, t1);
};
CAV.Plane.prototype = Object.create(CAV.Geometry.prototype);
CAV.Material = function (a: any, b: any) {
    
    
	this.ambient = new CAV.Color(a || '#444444');
	this.diffuse = new CAV.Color(b || '#FFFFFF');
	this.slave = new CAV.Color();
};
CAV.Mesh = function (a: any, b: any) {
    
    
	CAV.Object.call(this);
	this.geometry = a || new CAV.Geometry();
	this.material = b || new CAV.Material();
	this.side = CAV.FRONT;
	this.visible = true;
};
CAV.Mesh.prototype = Object.create(CAV.Object.prototype);
CAV.Mesh.prototype.update = function (a: string | any[], b: any) {
    
    
	var c, d, e, f, g;
	this.geometry.update();
	if (b)
		for (c = this.geometry.triangles.length - 1; c >= 0; c--) {
    
    
			d = this.geometry.triangles[c];
			CAV.Vector4.set(d.color.rgba);
			for (e = a.length - 1; e >= 0; e--)
				(f = a[e]),
					CAV.Vector3.subtractVectors(f.ray, f.position, d.centroid),
					CAV.Vector3.normalise(f.ray),
					(g = CAV.Vector3.dot(d.normal, f.ray)),
					this.side === CAV.FRONT
						? (g = Math.max(g, 0))
						: this.side === CAV.BACK
							? (g = Math.abs(Math.min(g, 0)))
							: this.side === CAV.DOUBLE && (g = Math.max(Math.abs(g), 0)),
					CAV.Vector4.multiplyVectors(
						this.material.slave.rgba,
						this.material.ambient.rgba,
						f.ambient.rgba
					),
					CAV.Vector4.add(d.color.rgba, this.material.slave.rgba),
					CAV.Vector4.multiplyVectors(
						this.material.slave.rgba,
						this.material.diffuse.rgba,
						f.diffuse.rgba
					),
					CAV.Vector4.multiplyScalar(this.material.slave.rgba, g),
					CAV.Vector4.add(d.color.rgba, this.material.slave.rgba);
			CAV.Vector4.clamp(d.color.rgba, 0, 1);
		}
	return this;
};
CAV.Scene = function () {
    
    
	this.meshes = [];
	this.lights = [];
};
CAV.Scene.prototype = {
    
    
	add: function (a: any) {
    
    
		a instanceof CAV.Mesh && !~this.meshes.indexOf(a)
			? this.meshes.push(a)
			: a instanceof CAV.Light && !~this.lights.indexOf(a) && this.lights.push(a);
		return this;
	},
	remove: function (a: any) {
    
    
		a instanceof CAV.Mesh && ~this.meshes.indexOf(a)
			? this.meshes.splice(this.meshes.indexOf(a), 1)
			: a instanceof CAV.Light &&
			~this.lights.indexOf(a) &&
			this.lights.splice(this.lights.indexOf(a), 1);
		return this;
	},
};
CAV.Renderer = function () {
    
    
	this.halfHeight = this.halfWidth = this.height = this.width = 0;
};
CAV.Renderer.prototype = {
    
    
	setSize: function (a: any, b: any) {
    
    
		if (!(this.width === a && this.height === b))
			return (
				(this.width = a),
				(this.height = b),
				(this.halfWidth = this.width * 0.5),
				(this.halfHeight = this.height * 0.5),
				this
			);
	},
	clear: function () {
    
    
		return this;
	},
	render: function () {
    
    
		return this;
	},
};
CAV.CanvasRenderer = function () {
    
    
	CAV.Renderer.call(this);
	this.element = document.createElement('canvas');
	this.element.style.display = 'block';
	this.context = this.element.getContext('2d');
	this.setSize(this.element.width, this.element.height);
};
CAV.CanvasRenderer.prototype = Object.create(CAV.Renderer.prototype);
CAV.CanvasRenderer.prototype.setSize = function (a: any, b: any) {
    
    
	CAV.Renderer.prototype.setSize.call(this, a, b);
	this.element.width = a;
	this.element.height = b;
	this.context.setTransform(1, 0, 0, -1, this.halfWidth, this.halfHeight);
	return this;
};
CAV.CanvasRenderer.prototype.clear = function () {
    
    
	CAV.Renderer.prototype.clear.call(this);
	this.context.clearRect(-this.halfWidth, -this.halfHeight, this.width, this.height);
	return this;
};
CAV.CanvasRenderer.prototype.render = function (a: {
    
     meshes: string | any[]; lights: any; }) {
    
    
	CAV.Renderer.prototype.render.call(this, a);
	var b, c, d, e, f;
	this.clear();
	this.context.lineJoin = 'round';
	this.context.lineWidth = 1;
	for (b = a.meshes.length - 1; b >= 0; b--)
		if (((c = a.meshes[b]), c.visible)) {
    
    
			c.update(a.lights, true);
			for (d = c.geometry.triangles.length - 1; d >= 0; d--)
				(e = c.geometry.triangles[d]),
					(f = e.color.format()),
					this.context.beginPath(),
					this.context.moveTo(e.a.position[0], e.a.position[1]),
					this.context.lineTo(e.b.position[0], e.b.position[1]),
					this.context.lineTo(e.c.position[0], e.c.position[1]),
					this.context.closePath(),
					(this.context.strokeStyle = f),
					(this.context.fillStyle = f),
					this.context.stroke(),
					this.context.fill();
		}
	return this;
};

export function Victor(container: string, anitOut: string) {
    
    
	let J, l;

	if (!!document.createElement('canvas').getContext) {
    
    
		var t = {
    
    
			width: 1.5,
			height: 1.5,
			depth: 10,
			segments: 12,
			slices: 6,
			xRange: 0.8,
			yRange: 0.1,
			zRange: 1,
			ambient: '#525252',
			diffuse: '#FFFFFF',
			speed: 0.0002,
		};
		var G = {
    
    
			count: 2,
			xyScalar: 1,
			zOffset: 100,
			ambient: '#002c4a',
			diffuse: '#005584',
			speed: 0.001,
			gravity: 1200,
			dampening: 0.95,
			minLimit: 10,
			maxLimit: null,
			minDistance: 20,
			maxDistance: 400,
			autopilot: false,
			draw: false,
			bounds: CAV.Vector3.create(),
			step: CAV.Vector3.create(
				randomInRange(0.2, 1),
				randomInRange(0.2, 1),
				randomInRange(0.2, 1)
			),
		};
		var m = 'canvas';
		var E = 'svg';
		var x = {
    
    
			renderer: m,
		};
		var i: number,
			n = Date.now();
		var L = CAV.Vector3.create();
		var k = CAV.Vector3.create();
		var z = document.getElementById(container || 'container');
		var w = document.getElementById(anitOut || 'anitOut');
		var D: {
    
     element: any; setSize: (arg0: number, arg1: number) => void; clear: () => void; width: number; height: number; halfWidth: any; halfHeight: any; render: (arg0: any) => void; }, I: {
    
     remove: (arg0: any) => void; add: (arg0: any) => void; lights: string | any[]; }, h: any, q: {
    
     vertices: string | any[]; segmentWidth: number; sliceHeight: number; dirty: boolean; }, y;
		var g: any;
		var r;

		function C() {
    
    
			F();
			p();
			s();
			B();
			v();
			K((z as HTMLElement).offsetWidth, (z as HTMLElement).offsetHeight);
			o();
		}

		function F() {
    
    
			g = new CAV.CanvasRenderer();
			H(x.renderer);
		}

		function H(N: string) {
    
    
			if (D) {
    
    
				(w as HTMLElement).removeChild(D.element);
			}
			switch (N) {
    
    
				case m:
					D = g;
					break;
			}
			D.setSize((z as HTMLElement).offsetWidth, (z as HTMLElement).offsetHeight);
			(w as HTMLElement).appendChild(D.element);
		}

		function p() {
    
    
			I = new CAV.Scene();
		}

		function s() {
    
    
			I.remove(h);
			D.clear();
			q = new CAV.Plane(t.width * D.width, t.height * D.height, t.segments, t.slices);
			y = new CAV.Material(t.ambient, t.diffuse);
			h = new CAV.Mesh(q, y);
			I.add(h);
			var N, O;
			for (N = q.vertices.length - 1; N >= 0; N--) {
    
    
				O = q.vertices[N];
				O.anchor = CAV.Vector3.clone(O.position);
				O.step = CAV.Vector3.create(
					randomInRange(0.2, 1),
					randomInRange(0.2, 1),
					randomInRange(0.2, 1)
				);
				O.time = randomInRange(0, Math.PI * 2);
			}
		}

		function B() {
    
    
			var O, N;
			for (O = I.lights.length - 1; O >= 0; O--) {
    
    
				N = I.lights[O];
				I.remove(N);
			}
			D.clear();
			for (O = 0; O < G.count; O++) {
    
    
				N = new CAV.Light(G.ambient, G.diffuse);
				N.ambientHex = N.ambient.format();
				N.diffuseHex = N.diffuse.format();
				I.add(N);
				N.mass = randomInRange(0.5, 1);
				N.velocity = CAV.Vector3.create();
				N.acceleration = CAV.Vector3.create();
				N.force = CAV.Vector3.create();
			}
		}

		function K(O: number, N: number) {
    
    
			D.setSize(O, N);
			CAV.Vector3.set(L, D.halfWidth, D.halfHeight);
			s();
		}

		function o() {
    
    
			i = Date.now() - n;
			u();
			M();
			requestAnimationFrame(o);
		}

		function u() {
    
    
			var Q,
				P,
				O,
				R,
				T,
				V,
				U,
				S = t.depth / 2;
			CAV.Vector3.copy(G.bounds, L);
			CAV.Vector3.multiplyScalar(G.bounds, G.xyScalar);
			CAV.Vector3.setZ(k, G.zOffset);
			for (R = I.lights.length - 1; R >= 0; R--) {
    
    
				T = I.lights[R];
				CAV.Vector3.setZ(T.position, G.zOffset);
				var N = clamp(
					CAV.Vector3.distanceSquared(T.position, k),
					G.minDistance,
					G.maxDistance
				);
				var W = (G.gravity * T.mass) / N;
				CAV.Vector3.subtractVectors(T.force, k, T.position);
				CAV.Vector3.normalise(T.force);
				CAV.Vector3.multiplyScalar(T.force, W);
				CAV.Vector3.set(T.acceleration);
				CAV.Vector3.add(T.acceleration, T.force);
				CAV.Vector3.add(T.velocity, T.acceleration);
				CAV.Vector3.multiplyScalar(T.velocity, G.dampening);
				CAV.Vector3.limit(T.velocity, G.minLimit, G.maxLimit);
				CAV.Vector3.add(T.position, T.velocity);
			}
			for (V = q.vertices.length - 1; V >= 0; V--) {
    
    
				U = q.vertices[V];
				Q = Math.sin(U.time + U.step[0] * i * t.speed);
				P = Math.cos(U.time + U.step[1] * i * t.speed);
				O = Math.sin(U.time + U.step[2] * i * t.speed);
				CAV.Vector3.set(
					U.position,
					t.xRange * q.segmentWidth * Q,
					t.yRange * q.sliceHeight * P,
					t.zRange * S * O - S
				);
				CAV.Vector3.add(U.position, U.anchor);
			}
			q.dirty = true;
		}

		function M() {
    
    
			D.render(I);
		}

		J = (O: any) => {
    
    
			var Q,
				N,
				S = O;
			var P = function (T: any) {
    
    
				for (Q = 0, l = I.lights.length; Q < l; Q++) {
    
    
					N = I.lights[Q];
					N.ambient.set(T);
					N.ambientHex = N.ambient.format();
				}
			};
			var R = function (T: any) {
    
    
				for (Q = 0, l = I.lights.length; Q < l; Q++) {
    
    
					N = I.lights[Q];
					N.diffuse.set(T);
					N.diffuseHex = N.diffuse.format();
				}
			};
			return {
    
    
				set: function () {
    
    
					P(S[0]);
					R(S[1]);
				},
			};
		};

		function v() {
    
    
			window.addEventListener('resize', j);
		}

		function A(N: {
    
     x: any; y: number; }) {
    
    
			CAV.Vector3.set(k, N.x, D.height - N.y);
			CAV.Vector3.subtract(k, L);
		}

		function j(N: any) {
    
    
			K((z as HTMLElement).offsetWidth, (z as HTMLElement).offsetHeight);
			M();
		}
		C();
	}
	return J;
}

注册页

import {
    
    
	KeyOutlined,
	LockFilled,
	LockOutlined,
	MailOutlined,
	UserOutlined
} from '@ant-design/icons';
import {
    
     Button, Form, Input, notification, Select } from 'antd';
import React, {
    
     useEffect, useState } from 'react';
import {
    
     ajax } from 'Utils/Axios/Axios';
import './App.scss';

export default function App() {
    
    
	const [sendCaptchaTick, setSendCaptchaTick] = useState(0);
	const [sendCaptchaInterval, setSendCaptchaInterval] = useState<NodeJS.Timeout | null>(null);
	useEffect(() => {
    
    
		return () => {
    
    
			if (sendCaptchaInterval) {
    
    
				clearInterval(sendCaptchaInterval);
				setSendCaptchaInterval(null);
			}
		};
	}, []);

	const [chosenEmail, setChosenEmail] = useState('@mail.sdu.edu.cn');

	const [form] = Form.useForm();

	const [isRegistering, setIsRegistering] = useState(false);

	const {
    
     Option } = Select;

	return (
		<div className='register' style={
    
    {
    
     backgroundImage: `url(${
      
      require('./bg.jpg').default})` }}>
			<div className='container'>
				<div className='title'>山大会议 注册账号</div>
				<div className='inputs'>
					<Form
						onFinish={
    
    (values) => {
    
    
							submitForm(values, chosenEmail, setIsRegistering);
						}}
						autoComplete='off'
						form={
    
    form}>
						<Form.Item
							rules={
    
    [
								{
    
    
									required: true,
									message: '请输入注册用的昵称',
								},
								{
    
    
									pattern: /^[^@]+$/,
									message: '昵称中不允许出现"@"',
								},
							]}
							name={
    
    'username'}>
							<Input placeholder='请输入昵称' prefix={
    
    <UserOutlined />} />
						</Form.Item>
						<Form.Item
							rules={
    
    [
								{
    
     required: true, message: '请输入密码' },
								{
    
    
									min: 6,
									message: '请输入长度超过6位的密码',
								},
							]}
							name={
    
    'password'}>
							<Input.Password placeholder='请输入密码' prefix={
    
    <LockOutlined />} />
						</Form.Item>
						<Form.Item
							validateTrigger='onBlur'
							rules={
    
    [
								{
    
    
									required: true,
									message: '请再次输入密码',
								},
								({
    
     getFieldValue }) => ({
    
    
									validator(rule, value) {
    
    
										if (getFieldValue('password') === value) {
    
    
											return Promise.resolve();
										}
										return Promise.reject('两次输入的密码不一致');
									},
								}),
							]}
							name={
    
    'passwordCheck'}>
							<Input.Password placeholder='请再次输入密码' prefix={
    
    <LockFilled />} />
						</Form.Item>
						<Form.Item
							rules={
    
    [
								{
    
    
									required: true,
									message: '请输入邮箱',
								},
								{
    
    
									pattern: /^[^@]+$/,
									message: '请不要再次输入"@"',
								},
							]}
							name={
    
    'email'}>
							<Input
								placeholder='请输入邮箱'
								addonAfter={
    
    
									<Select defaultValue={
    
    chosenEmail} onSelect={
    
    setChosenEmail}>
										<Option value='@mail.sdu.edu.cn'>@mail.sdu.edu.cn</Option>
										<Option value='@sdu.edu.cn'>@sdu.edu.cn</Option>
									</Select>
								}
								prefix={
    
    <MailOutlined />}
							/>
						</Form.Item>
						<Form.Item
							rules={
    
    [
								{
    
    
									required: true,
									message: '请输入验证码',
								},
							]}
							name={
    
    'captcha'}>
							<Input placeholder='请输入邮箱验证码' prefix={
    
    <KeyOutlined />} />
						</Form.Item>
						<Form.Item>
							<div style={
    
    {
    
     display: 'flex', justifyContent: 'space-around' }}>
								<Button
									disabled={
    
    sendCaptchaTick > 0}
									loading={
    
    sendCaptchaTick === -1}
									onClick={
    
    () => {
    
    
										form.validateFields(['username', 'email'])
											.then((values) => {
    
    
												setSendCaptchaTick(-1);
												const {
    
     username, email } = values;
												ajax.post('/login_and_register/code', {
    
    
													username,
													email: `${
      
      email}${
      
      chosenEmail}`,
												}).then((response) => {
    
    
													if (response.code === 200) {
    
    
														notification.success({
    
    
															message: '验证码发送成功',
															description:
																'验证码已发送,请前往邮箱查询验证码',
														});
														sendCaptcha(
															setSendCaptchaTick,
															setSendCaptchaInterval
														);
													} else {
    
    
														notification.error({
    
    
															message: '验证码发送失败',
															description: response.message,
														});
													}
												});
											})
											.catch(() => {
    
     });
									}}>
									{
    
    sendCaptchaTick > 0
										? `${
      
      sendCaptchaTick}秒后可再次发送`
										: '发送验证码'}
								</Button>
								<Button loading={
    
    isRegistering} type='primary' htmlType='submit'>
									注册
								</Button>
							</div>
						</Form.Item>
					</Form>
				</div>
			</div>
		</div>
	);
}

async function submitForm(values: {
    
     username: any; password: any; captcha: any; email: any; }, chosenEmail: string, setIsRegistering: {
    
     (value: React.SetStateAction<boolean>): void; (arg0: boolean): void; }) {
    
    
	setIsRegistering(true);
	const {
    
     username, password, captcha, email } = values;
	const res = await ajax.post('/login_and_register/register', {
    
    
		username,
		password,
		email: `${
      
      email}${
      
      chosenEmail}`,
		code: captcha,
	});
	if (res.code === 200) {
    
    
		notification.success({
    
     message: '注册成功', description: '注册成功,请前往登录吧' });
	} else {
    
    
		notification.error({
    
     message: '注册失败', description: res.message });
	}
	setIsRegistering(false);
}

function sendCaptcha(setSendCaptchaTick: {
    
     (value: React.SetStateAction<number>): void; (arg0: number): void; }, setSendCaptchaInterval: {
    
     (value: React.SetStateAction<NodeJS.Timeout | null>): void; (arg0: NodeJS.Timer | null): void; }) {
    
    
	let sendCaptchaTick = 60;
	setSendCaptchaTick(sendCaptchaTick);
	const interval = setInterval(() => {
    
    
		setSendCaptchaTick(--sendCaptchaTick);
		if (sendCaptchaTick === 0) {
    
    
			clearInterval(interval);
			setSendCaptchaInterval(null);
		}
	}, 1000);
	setSendCaptchaInterval(interval);
}

Redux

actions.ts

/**
 * action 类型
 */

import {
    
     ChatWebSocketType, DEVICE_TYPE } from "Utils/Constraints";
import {
    
     DeviceInfo } from "Utils/Types";

const UNDEFINED_ACTION = 'UNDEFINED_ACTION'

// 选择当前聊天的对象 ID
export const SET_NOW_CHATTING_ID = 'SET_NOW_CHATTING_ID';
export function setNowChattingId(nowChattingId: number | null) {
    
    
    return {
    
     type: SET_NOW_CHATTING_ID, nowChattingId };
}

export const SET_NOW_WEBRTC_FRIEND_ID = 'SET_NOW_WEBRTC_FRIEND_ID';
export function setNowWebrtcFriendId(nowWebrtcFriendId: number | null) {
    
    
    return {
    
     type: SET_NOW_WEBRTC_FRIEND_ID, nowWebrtcFriendId };
}


// 更新可用的音视频设备
export const UPDATE_AVAILABLE_VIDEO_DEVICES = 'UPDATE_AVAILABLE_VIDEO_DEVICES';
export const UPDATE_AVAILABLE_AUDIO_DEVICES = 'UPDATE_AVAILABLE_AUDIO_DEVICES';
export function updateAvailableDevices(deviceType: string, devices: DeviceInfo[]) {
    
    
    switch (deviceType) {
    
    
        case DEVICE_TYPE.VIDEO_DEVICE:
            return {
    
     type: UPDATE_AVAILABLE_VIDEO_DEVICES, devices };
        case DEVICE_TYPE.AUDIO_DEVICE:
            return {
    
     type: UPDATE_AVAILABLE_AUDIO_DEVICES, devices };
        default:
            return {
    
     type: UNDEFINED_ACTION };
    }
}

// 更换选中的音视频设备
export const EXCHANGE_VIDEO_DEVICE = 'EXCHANGE_VIDEO_DEVICE';
export const EXCHANGE_AUDIO_DEVICE = 'EXCHANGE_AUDIO_DEVICE';
export function exchangeMediaDevice(deviceType: string, deviceInfo: DeviceInfo) {
    
    
    switch (deviceType) {
    
    
        case DEVICE_TYPE.VIDEO_DEVICE:
            return {
    
     type: EXCHANGE_VIDEO_DEVICE, deviceInfo };
        case DEVICE_TYPE.AUDIO_DEVICE:
            return {
    
     type: EXCHANGE_AUDIO_DEVICE, deviceInfo };
        default:
            return {
    
     type: UNDEFINED_ACTION };
    }
}

// 设置用户 Token
export const SET_AUTH_TOKEN = 'SET_AUTH_TOKEN';
export function setAuthToken(token: string) {
    
    
    return {
    
     type: SET_AUTH_TOKEN, token };
}

// 管理未读消息
export const ADD_UNREAD_MESSAGE = 'ADD_UNREAD_MESSAGE';
export const REMOVE_UNREAD_MESSAGES = 'REMOVE_UNREAD_MESSAGES';
export function setUnreadMessages(operation: string, payload: any) {
    
    
    return {
    
     type: operation, payload };
}

// 管理消息记录
export const INIT_MESSAGE_HISTORY = 'INIT_MESSAGE_HISTORY';
export const SYNC_CLOUD_MESSAGE_HISTORY = 'SYNC_CLOUD_MESSAGE_HISTORY';
export const GET_MORE_MESSAGE_HISTORY = 'GET_MORE_MESSAGE_HISTORY';
export const ADD_MESSAGE_HISTORY = 'ADD_MESSAGE_HISTORY';
export const REMOVE_MESSAGE_HISTORY = 'REMOVE_MESSAGE_HISTORY';
export function setMessageHistory(operation: string, payload: any) {
    
    
    return {
    
     type: operation, payload };
}

// 应用通话状态
export const SET_CALL_STATUS = 'SET_CALL_STATUS';
export function setCallStatus(status: ChatWebSocketType) {
    
    
    return {
    
     type: SET_CALL_STATUS, status };
}

reducers.ts

import {
    
     combineReducers } from '@reduxjs/toolkit';
import {
    
     CALL_STATUS_FREE, ChatWebSocketType } from 'Utils/Constraints';
import {
    
     ChatMessage, DeviceInfo } from 'Utils/Types';
import {
    
    
    ADD_MESSAGE_HISTORY,
    ADD_UNREAD_MESSAGE,
    EXCHANGE_AUDIO_DEVICE,
    EXCHANGE_VIDEO_DEVICE,
    GET_MORE_MESSAGE_HISTORY,
    INIT_MESSAGE_HISTORY,
    REMOVE_MESSAGE_HISTORY,
    REMOVE_UNREAD_MESSAGES,
    SET_AUTH_TOKEN,
    SET_CALL_STATUS,
    SET_NOW_CHATTING_ID,
    SET_NOW_WEBRTC_FRIEND_ID,
    SYNC_CLOUD_MESSAGE_HISTORY,
    UPDATE_AVAILABLE_AUDIO_DEVICES,
    UPDATE_AVAILABLE_VIDEO_DEVICES
} from './actions';

function setNowChattingId(state = null, action: {
    
     type: string, nowChattingId: number | null }) {
    
    
    if (action.type === SET_NOW_CHATTING_ID) {
    
    
        return action.nowChattingId;
    }
    return state;
}

function setNowWebrtcFriendId(state = null, action: {
    
     type: string, nowWebrtcFriendId: number | null }) {
    
    
    if (action.type === SET_NOW_WEBRTC_FRIEND_ID) {
    
    
        return action.nowWebrtcFriendId;
    }
    return state;
}

function updateAvailableVideoDevices(state = new Array(), action: {
    
     type: string, devices: DeviceInfo[] }): Array<DeviceInfo> {
    
    
    switch (action.type) {
    
    
        case UPDATE_AVAILABLE_VIDEO_DEVICES:
            return action.devices;
        default:
            return state;
    }
}

function updateAvailableAudioDevices(state = new Array(), action: {
    
     type: string, devices: DeviceInfo[] }): Array<DeviceInfo> {
    
    
    switch (action.type) {
    
    
        case UPDATE_AVAILABLE_AUDIO_DEVICES:
            return action.devices;
        default:
            return state;
    }
}

function exchangeVideoDevice(state = null, action: {
    
     type: string, deviceInfo: DeviceInfo }) {
    
    
    switch (action.type) {
    
    
        case EXCHANGE_VIDEO_DEVICE:
            localStorage.setItem('usingVideoDevice', action.deviceInfo.deviceId);
            return action.deviceInfo;
        default:
            return state;
    }
}

function exchangeAudioDevice(state = null, action: {
    
     type: string, deviceInfo: DeviceInfo }) {
    
    
    switch (action.type) {
    
    
        case EXCHANGE_AUDIO_DEVICE:
            localStorage.setItem('usingAudioDevice', action.deviceInfo.deviceId);
            return action.deviceInfo;
        default:
            return state;
    }
}

function setAuthToken(state = null, action: {
    
     type: string, token: string }) {
    
    
    if (action.type === SET_AUTH_TOKEN) return action.token;
    return state;
}

function setUnreadMessages(state = {
    
    }, action: {
    
     type: string, payload: any })
    : {
    
    
        [user: string]: ChatMessage[]
    } {
    
    
    switch (action.type) {
    
    
        case ADD_UNREAD_MESSAGE:
            const {
    
     fromId, toId, myId } = action.payload;
            const messageOwnerId = fromId === myId ? toId : fromId;
            const newArr = state[`${
      
      messageOwnerId}` as keyof typeof state]
                ? [...state[`${
      
      messageOwnerId}` as keyof typeof state]]
                : new Array();
            newArr.push(action.payload);
            return Object.assign({
    
    }, state, {
    
    
                [`${
      
      messageOwnerId}`]: newArr,
            });
        case REMOVE_UNREAD_MESSAGES:
            const {
    
     userId } = action.payload;
            const newState = Object.assign({
    
    }, state);
            delete newState[`${
      
      userId}` as keyof typeof newState];
            return newState;
        default:
            return state;
    }
}

function setMessageHistory(state = {
    
    }, action: {
    
     type: string, payload: any }) {
    
    
    switch (action.type) {
    
    
        case INIT_MESSAGE_HISTORY:
            return action.payload;
        case SYNC_CLOUD_MESSAGE_HISTORY:
            return Object.assign({
    
    }, state, action.payload);
        case GET_MORE_MESSAGE_HISTORY:
            const chatId = action.payload.chatId as keyof typeof state
            const newArr1 = state[`${
      
      chatId}`] ? [...state[`${
      
      chatId}`]] : new Array();
            newArr1.unshift(action.payload);
            return Object.assign({
    
    }, state, {
    
    
                [`${
      
      chatId}`]: newArr1,
            });
        case ADD_MESSAGE_HISTORY:
            const {
    
     fromId, toId, myId } = action.payload;
            const messageOwnerId = (fromId === myId ? toId : fromId) as keyof typeof state;
            const newArr2 = state[`${
      
      messageOwnerId}`]
                ? [...state[`${
      
      messageOwnerId}`]]
                : new Array();
            newArr2.push(action.payload);
            const newMessages = Object.assign({
    
    }, state, {
    
    
                [`${
      
      messageOwnerId}`]: newArr2,
            });
            return newMessages;
        case REMOVE_MESSAGE_HISTORY:
            const newState = Object.assign({
    
    }, state);
            const userId = action.payload.userId as keyof typeof newState;
            delete newState[`${
      
      userId}`];
            return newState;
        default:
            return state;
    }
}

function setCallStatus(state = CALL_STATUS_FREE, action: {
    
     type: string, status: ChatWebSocketType }) {
    
    
    if (action.type === SET_CALL_STATUS) {
    
    
        return action.status;
    }
    return state;
}

const reducers = combineReducers({
    
    
    nowChattingId: setNowChattingId,
    nowWebrtcFriendId: setNowWebrtcFriendId,
    availableVideoDevices: updateAvailableVideoDevices,
    availableAudioDevices: updateAvailableAudioDevices,
    usingVideoDevice: exchangeVideoDevice,
    usingAudioDevice: exchangeAudioDevice,
    authToken: setAuthToken,
    unreadMessages: setUnreadMessages,
    messageHistory: setMessageHistory,
    callStatus: setCallStatus,
});

export default reducers;

store.ts

import {
    
     configureStore } from '@reduxjs/toolkit';
import reducers from './reducers';

const store = configureStore({
    
    
    reducer: reducers,
});

export default store;

增加新的代码

为了提高开发效率和代码复用能力,我在 src/Utils 文件夹下定义了三个新的文件:

  • Constraints.ts:定义了一些全局常量
  • Global.ts:定义了一些零散的、但是经常被各种组件引用的全局函数
  • Types.ts:为了开发方便而定义的一些类型

Constraints.ts

/**
 * 这个文件用来存放一些常量
 */
// 音视频设备
export const DEVICE_TYPE = {
    
    
    VIDEO_DEVICE: 'video',
    AUDIO_DEVICE: 'audio',
};

/**
 * 通话状态
 */
export const CALL_STATUS_FREE = 0;
export const CALL_STATUS_OFFERING = 1;
export const CALL_STATUS_OFFERED = 2;
export const CALL_STATUS_ANSWERING = 3;
export const CALL_STATUS_CALLING = 4;

/**
 * 回复好友申请
 */
export const ACCEPT_FRIEND_REQUEST = 2;
export const REJECT_FRIEND_REQUEST = 1;
export const NO_OPERATION_FRIEND_REQUEST = -1;

/**
 * 聊天系统 WebSocket type 参数
 */
export enum ChatWebSocketType {
    
    
    UNDEFINED_0, // 未定义 0 占位
    CHAT_SEND_PRIVATE_MESSAGE, // 发送私聊消息
    CHAT_READ_MESSAGE, // 签收私聊消息
    CHAT_SEND_FRIEND_REQUEST, // 发送好友请求
    CHAT_ANSWER_FRIEND_REQUEST, // 响应好友请求
    CHAT_PRIVATE_WEBRTC_OFFER, // 发送视频聊天请求 OFFER
    CHAT_PRIVATE_WEBRTC_ANSWER, // 响应视频聊天请求 ANSWER
    CHAT_PRIVATE_WEBRTC_CANDIDATE, // 视频聊天 ICE 候选者
    CHAT_PRIVATE_WEBRTC_DISCONNECT, // 断开视频聊天
}

Global.ts

/**
 * 这个文件用来存放一些不好分类的全局函数
 */

import jwtDecode from 'jwt-decode';
import {
    
     DEVICE_TYPE } from './Constraints';
import store from './Store/store';
import {
    
     DeviceInfo } from './Types';

/**
 * 用来返回 mainContent 模态屏遮罩层挂载DOM
 * @returns Id值为'mainContent'的DOM
 */
function getMainContent() {
    
    
    const content = document.getElementById('mainContent');
    if (content) {
    
    
        return content
    } else {
    
    
        return document.body
    }
}

/**
 * 由于直接使用 jwtDecode 解析非法 token 会报错,因此进行封装
 * @param {string} token
 * @returns 解析后的 token
 */
function decodeJWT(token: string): any {
    
    
    try {
    
    
        return jwtDecode(token);
    } catch (error: any) {
    
    
        if (error.message === 'Invalid token specified') return undefined;
        console.log(error);
    }
}

/**
 * 封装后的获取设备流函数
 * @param {string} device 设备类型 DEVICE_TYPE
 * @returns
 */
function getDeviceStream(device: string) {
    
    
    switch (device) {
    
    
        case DEVICE_TYPE.AUDIO_DEVICE:
            const audioDevice = store.getState().usingAudioDevice as DeviceInfo;
            const audioConstraints = {
    
    
                deviceId: {
    
    
                    exact: audioDevice.deviceId,
                },
                noiseSuppression: localStorage.getItem('noiseSuppression') !== 'false',
                echoCancellation: localStorage.getItem('echoCancellation') !== 'false',
            };
            return navigator.mediaDevices.getUserMedia({
    
     audio: audioConstraints });
        case DEVICE_TYPE.VIDEO_DEVICE:
            const videoDevice = store.getState().usingVideoDevice as DeviceInfo;
            const videoConstraints = {
    
    
                deviceId: {
    
    
                    exact: videoDevice.deviceId,
                },
                width: 1920,
                height: 1080,
                frameRate: {
    
    
                    max: 30,
                },
            };
            return navigator.mediaDevices.getUserMedia({
    
    
                video: videoConstraints,
            });
        default:
            return Promise.resolve(new MediaStream());
    }
}

export const A_SECOND_TIME = 1000;
export const A_MINUTE_TIME = 60 * A_SECOND_TIME;
export const AN_HOUR_TIME = 60 * A_MINUTE_TIME;
export const A_DAY_TIME = 24 * AN_HOUR_TIME;
export const isSameDay = (timeStampA: string | number | Date, timeStampB: string | number | Date) => {
    
    
    const dateA = new Date(timeStampA);
    const dateB = new Date(timeStampB);
    return dateA.setHours(0, 0, 0, 0) === dateB.setHours(0, 0, 0, 0);
};
export const isSameWeek = (timeStampA: string | number | Date, timeStampB: string | number | Date) => {
    
    
    let A = new Date(timeStampA).setHours(0, 0, 0, 0);
    let B = new Date(timeStampB).setHours(0, 0, 0, 0);
    const timeDistance = Math.abs(A - B);
    return timeDistance / A_DAY_TIME;
};
export const isSameYear = (timeStampA: string | number | Date, timeStampB: string | number | Date) => {
    
    
    const dateA = new Date(timeStampA);
    const dateB = new Date(timeStampB);
    dateA.setHours(0, 0, 0, 0);
    dateB.setHours(0, 0, 0, 0);
    dateA.setMonth(0, 1);
    dateB.setMonth(0, 1);
    return dateA.getFullYear() === dateB.getFullYear();
};
export const translateDayNumberToDayChara = (day: any) => {
    
    
    if (typeof day === 'number') {
    
    
        day = day % 7;
    }
    switch (day) {
    
    
        case 0:
            return '星期天';
        case 1:
            return '星期一';
        case 2:
            return '星期二';
        case 3:
            return '星期三';
        case 4:
            return '星期四';
        case 5:
            return '星期五';
        case 6:
            return '星期六';
        default:
            return String(day);
    }
};

export {
    
     decodeJWT, getMainContent, getDeviceStream };

Types.ts

import {
    
     ReactNode } from "react"

export interface ChatMessage {
    
    
    date: number,
    fromId: number,
    id: number,
    message: string,
    toId: number,
    myId?: number,
    userId: number
}

export interface DeviceInfo {
    
    
    webLabel?: ReactNode
    deviceId: string,
    label: string,
}

export interface UserInfo {
    
    
    email: string,
    exp: number,
    iat: number,
    id: number,
    iss: string,
    profile: string,
    role: [
        {
    
    
            authority: string,
            id: number
        }
    ],
    sub: string,
    username: string
}

猜你喜欢

转载自blog.csdn.net/qq_53126706/article/details/125069092