Commit f96b22e4 by 肖玉娟

初始化项目

parents
{
"presets": [
"@babel/preset-env",
[
"@babel/preset-react",
{
"runtime": "automatic"
}
],
"@babel/preset-typescript"
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"regenerator": true
}
],
[
"import",
{
"libraryName": "@arco-design/mobile-react",
"libraryDirectory": "esm", // 注意如果是 SSR 环境,这里需使用 `cjs`
"style": true
}
]
]
}
API_HOST="http://192.168.31.15:8081"
PUBLIC_PATH = "/"
\ No newline at end of file
config/
scripts/
build/
node_modules/
\.eslintrc.js
\ No newline at end of file
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'plugin:react/recommended',
'standard'
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: [
'react',
'@typescript-eslint',
'eslint-plugin-import-helpers'
],
rules: {
'import-helpers/order-imports': [
'error',
{ // example configuration
newlinesBetween: 'always',
groups: [
'/^react/',
'module',
'/antd/',
'/^@//',
'absolute'
],
alphabetize: { order: 'asc', ignoreCase: true }
}
],
'react/jsx-uses-react': 'off',
'react/react-in-jsx-scope': 'off',
'no-fallthrough': 'off',
'no-debugger': 'off',
'no-async-promise-executor': 'off'
}
}
.DS_Store
node_modules/
dist/
dist.zip
npm-debug.log*
yarn-debug.log*
yarn-error.log*
test/unit/coverage
test/e2e/reports
selenium-debug.log
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
engine-strict = true
**/*.md
**/*.svg
**/*.html
package.json
{
"printWidth": 120,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"jsxBracketSameLine": true,
"arrowParens": "avoid",
"insertPragma": false,
"tabWidth": 2,
"useTabs": false,
"overrides": [
{
"files": ".prettierrc",
"options": {
"parser": "json"
}
}
]
}
\ No newline at end of file
{
"extends": "stylelint-config-standard"
}
\ No newline at end of file
# 按钮权限
## 用户管理
```
查看 : sys:tbuser:page;sys:tbuser:info
保存 : sys:tbuser:save
修改 : sys:tbuser:update
删除 : sys:tbuser:delete
```
## 房间管理
```
查看 : sys:tbroom:page,sys:tbroom:info;sys:tbuser:list
保存 : sys:tbroom:save
修改 : sys:tbroom:update
删除 : sys:tbroom:delete
```
## 账号管理
```
查看 : sys:user:page,sys:user:info
新增 : sys:user:save
修改 : sys:user:update
删除 : sys:user:delete
```
## 角色管理
```
查看 : sys:role:page,sys:role:info,sys:role:list
新增 : sys:role:save
修改 : sys:role:update
删除 : sys:role:delete
```
## 菜单管理
```
查看 : sys:menu:list,sys:menu:info
新增 : sys:menu:save
修改 : sys:menu:update
删除 : sys:menu:delete
```
## 比赛管理
```
查看 : sys:tbschedule:page,sys:tbschedule:info
删除 : sys:tbschedule:delete
保存 : sys:tbschedule:save
修改 : sys:tbschedule:update
擂台报名管理-查看 :sys:tbapplylt:page,sys:tbapplylt:info
擂台报名管理-保存 :sys:tbapplylt:save
擂台报名管理-修改 :sys:tbapplylt:update
擂台报名管理-删除 :sys:tbapplylt:delete
海选报名管理-查看 :sys:tbapplyxb:page,sys:tbapplyxb:info
海选报名管理-保存 :sys:tbapplyxb:save
海选报名管理-修改 :sys:tbapplyxb:update
海选报名管理-删除 :sys:tbapplyxb:delete
赛程选拔-查看 :sys:tbschedulexb:page,sys:tbschedulexb:info
赛程选拔-保存 :sys:tbschedulexb:save
赛程选拔-修改 :sys:tbschedulexb:update
赛程选拔-删除 :sys:tbschedulexb:delete
```
\ No newline at end of file
// import AddAssetHtmlPlugin from 'add-asset-html-webpack-plugin'
import chalk from 'chalk'
import CopyWebpackPlugin from 'copy-webpack-plugin'
import dotenv from 'dotenv'
import ESLintPlugin from 'eslint-webpack-plugin'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import path from 'path'
import ProgressBarPlugin from 'progress-bar-webpack-plugin'
import webpack from 'webpack'
// import AntdDayjsWebpackPlugin from 'antd-dayjs-webpack-plugin'
dotenv.config()
const Resolve = (...args: string[]) => {
return path.resolve(__dirname, '../', ...args)
}
const isProd = process.env.NODE_ENV === 'production'
const exclude = /node_modules/
const include = /src/
const BaseConfig: webpack.Configuration = {
entry: ['./src/main.tsx'],
output: {
path: Resolve('dist'),
filename: 'js/[name].[contenthash].js',
chunkFilename: 'js/[name].chunk.js',
publicPath: process.env.PUBLIC_PATH
},
module: {
rules: [
{
// 同时认识ts jsx js tsx 文件
test: /\.(t|j)sx?$/,
exclude,
include,
use: [
{
loader: 'babel-loader',
options: {
// 缓存:第二次构建时,会读取之前的缓存
cacheDirectory: true
}
}
]
},
{
test: /\.less$/,
include: [/src/, /node_modules/],
use: [
isProd ? MiniCssExtractPlugin.loader : 'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env',
'autoprefixer'
]
},
sourceMap: true
}
},
{
loader: 'less-loader',
options: {
lessOptions: { // 如果使用less-loader@5,请移除 lessOptions 这一级直接配置选项。
modifyVars: {
// 'primary-color': '#1DA57A',
// 'link-color': '#1DA57A',
// 'border-radius-base': '2px'
},
javascriptEnabled: true
}
}
}
]
},
{
test: /\.css$/,
include: [/src/, /node_modules\/@ant-design/, /node_modules\/antd/, /node_modules\/@wangeditor/],
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader'
}
]
},
{
test: /\.(png|svg|jpg|gif)$/,
exclude,
include,
use: [
{
loader: 'file-loader',
options: {
limit: 5000,
esModule: false,
// 分离图片至imgs文件夹
name () {
if (process.env.NODE_ENV === 'development') {
return '[path][name].[ext]'
}
return 'images/[contenthash].[ext]'
}
}
}
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
exclude,
include,
use: [
{
loader: 'file-loader',
options: {
name () {
if (process.env.NODE_ENV === 'development') {
return '[path][name].[ext]'
}
return 'fonts/[contenthash].[ext]'
}
}
}
]
}
]
},
plugins: [
/**
* @description 定义全局变量插件
*/
new webpack.EnvironmentPlugin(['NODE_ENV', 'API_HOST']),
/**
* @description 复制资源插件
*/
new CopyWebpackPlugin({
patterns: [
{
from: Resolve('public'),
to: Resolve('dist')
}
]
}),
/**
* @description webpack 构建进度条
*/
new (ProgressBarPlugin as any)({
width: 50, // 默认20,进度格子数量即每个代表进度数,如果是20,那么一格就是5。
format: chalk.blue.bold('build') + chalk.yellow('[:bar] ') + chalk.green.bold(':percent') + ' (:elapsed秒)',
complete: '-', // 默认“=”,完成字符
clear: false // 默认true,完成时清除栏的选项
}),
new ESLintPlugin({
fix: true /* 自动帮助修复 */,
extensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'react'],
exclude: 'node_modules'
}),
new HtmlWebpackPlugin({
inject: true,
template: path.resolve(__dirname, '../src/index.html')
})
// new AntdDayjsWebpackPlugin()
],
resolve: {
extensions: ['.js', '.json', '.jsx', '.less', '.css', '.vue', '.json', '.ts', '.tsx'],
alias: {
'@': Resolve('src')
}
},
cache: {
type: 'filesystem'
},
watchOptions: {
ignored: /node_modules/
},
/**
* @description 设置webpack 构建时候信息输出
*/
stats: {
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}
}
export default BaseConfig
// import webpack from 'webpack'
import path from 'path'
import UnusedWebpackPlugin from 'unused-webpack-plugin'
import merge from 'webpack-merge'
import BaseConfig from './webpack.base.config'
const DevConfig = merge(BaseConfig, {
mode: 'development',
devtool: 'eval',
output: {
filename: '[name].bundle.js',
pathinfo: false
},
devServer: {
hot: true,
open: false,
// host: HOST,
// stats: 'errors-warnings',
compress: false,
historyApiFallback: true
},
// plugins: [new webpack.HotModuleReplacementPlugin()],
plugins: [
new UnusedWebpackPlugin({
directories: [path.join(__dirname, 'src')],
root: path.join(__dirname, '../')
})
]
} as any)
module.exports = DevConfig
import { CleanWebpackPlugin } from 'clean-webpack-plugin'
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import TerserPlugin from 'terser-webpack-plugin'
// import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
// import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import merge from 'webpack-merge'
import BaseConfig from './webpack.base.config'
const DevConfig = merge(BaseConfig, {
mode: 'production',
optimization: {
splitChunks: {
chunks: 'all', // initial(初始块)、async(按需加载块)、all(全部块),默认为all
minChunks: 2, // 表示被引用次数,默认为1;
// maxInitialRequests: 5, //最大的按需(异步)加载次数,默认为1;
// minSize: 2, //表示在压缩前的最小模块大小,默认为0;
// maxInitialRequests: 1, //最大的初始化加载次数,默认为1;
minSize: 1,
cacheGroups: {
commons: {
name: 'common' // 拆分出来块的名字(Chunk Names),默认由块名和hash值自动生成
}
}
},
minimizer: [
new TerserPlugin({
parallel: true
// sourceMap: true,
})
]
},
plugins: [
new CleanWebpackPlugin({
}),
new MiniCssExtractPlugin({
filename: 'css/[name].[chunkhash].css'
}),
new CssMinimizerPlugin()
// new BundleAnalyzerPlugin()
]
})
module.exports = DevConfig
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "base-react-ts-web",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"preinstall": "only-allow npm",
"dev": "cross-env ENV=local NODE_ENV=development node --no-deprecation node_modules/webpack-dev-server/bin/webpack-dev-server.js --config build/webpack.dev.config.ts",
"build": "cross-env ENV=prod NODE_ENV=production webpack --config build/webpack.prod.config.ts",
"fix": "eslint --fix"
},
"repository": {
"type": "git",
"url": "git@gitlab.viz-cloud.top:fe/product/ig-packag-manage-web.git"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.18.6",
"@babel/plugin-transform-modules-commonjs": "^7.18.6",
"@babel/plugin-transform-runtime": "^7.18.6",
"@babel/preset-env": "^7.18.6",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@types/koa": "^2.13.5",
"@types/koa-static": "^4.0.2",
"@types/lodash": "^4.14.182",
"@types/progress-bar-webpack-plugin": "^2.1.2",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@types/styled-components": "^5.1.25",
"@types/unused-webpack-plugin": "^2.4.2",
"@types/webpack-bundle-analyzer": "^4.4.1",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.5",
"add-asset-html-webpack-plugin": "^5.0.2",
"autoprefixer": "^10.4.7",
"babel-loader": "^8.2.5",
"babel-plugin-import": "^1.13.5",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3",
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^4.0.0",
"dotenv": "^16.0.1",
"eslint": "^8.19.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-import-helpers": "^1.2.1",
"eslint-plugin-n": "^15.2.4",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.30.1",
"eslint-webpack-plugin": "^3.2.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"less": "^4.1.3",
"less-loader": "^11.0.0",
"mini-css-extract-plugin": "^2.6.1",
"node-loader": "^2.0.0",
"nodemon": "^2.0.20",
"only-allow": "^1.1.1",
"postcss-loader": "^7.0.0",
"postcss-preset-env": "^7.7.2",
"progress-bar-webpack-plugin": "^2.1.0",
"style-loader": "^3.3.1",
"stylelint-config-standard": "^26.0.0",
"terser-webpack-plugin": "^5.3.3",
"ts-node": "^10.8.2",
"typescript": "^4.7.4",
"unused-webpack-plugin": "^2.4.0",
"webpack": "^5.73.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"@arco-design/mobile-react": "^2.21.2",
"@reduxjs/toolkit": "^1.8.3",
"ahooks": "^3.5.2",
"axios": "^0.27.2",
"lodash": "^4.17.21",
"rc-tween-one": "^3.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.2",
"react-router-dom": "^6.3.0",
"redux-persist": "^6.0.0",
"styled-components": "^5.3.5"
},
"engines": {
"node": "18.x || 16.x"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
"eslint --fix",
"git add"
]
}
}
// ? 全局不动配置项 只做导出不做修改
// * 首页地址(默认)
export const HOME_URL: string = '/home'
export const API_HOST = process.env.API_HOST
export const COS_HOST = process.env.COS_HOST
import { useMemo } from 'react'
export const useGetParams = () => {
const { params } = useMemo(() => {
const url = window.location.href
const d: Record<string, string | undefined> = {}
if (url?.indexOf('?') !== -1) {
const arr = url.slice(url?.indexOf('?') + 1).split('&')
arr.forEach(item => {
const [key, val] = item.split('=') || []
d[key] = val
})
}
return {
params: d
}
}, [])
return {
params
}
}
import { useCallback, useEffect, useState } from 'react'
import { get, isFunction, isString } from 'lodash'
type Cb = (parma?: any) => any
type Options = {
defaultValue?: any;
deps?: any[];
immediate?: boolean;
formatResult?: string | ((res: any) => any);
onSuccess?: (res: any) => void;
onError?: (res: any) => void;
}
type Result<T> = {
data: T;
loading: boolean;
run: (params?: any) => Promise<any>;
}
export function useRequest<T = any> (fn: Cb, options: Options): Result<T> {
const [loading, setLoading] = useState(false)
const [data, setData] = useState<T>(options?.defaultValue)
const {
immediate = true,
formatResult,
onSuccess,
onError
} = options || {}
const run = useCallback(async (...args: any[]) => {
return new Promise((resolve, reject) => {
if (isFunction(fn)) {
if (loading) return
setLoading(true)
fn(...args)
.then((res) => {
if (formatResult) {
if (isFunction(formatResult)) {
res = formatResult(res)
}
if (isString(formatResult)) {
res = get(res, formatResult)
}
}
setData(res)
onSuccess && onSuccess(res)
resolve(res)
setLoading(false)
}).catch((error) => {
console.log('接口请求失败:', error)
onError && onError(error)
reject(error)
setLoading(false)
})
}
})
}, [fn])
useEffect(() => {
if (immediate) run()
}, [])
return {
loading,
data,
run
}
}
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from '../store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>视频旋转</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
\ No newline at end of file
import { Suspense } from 'react'
import { Outlet } from 'react-router-dom'
const App = () => {
return (
<main>
<Suspense
// fallback={
// () => (null)
// }
>
<Outlet />
</Suspense>
</main>
)
}
export default App
@import './reset.less'
\ No newline at end of file
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font-family: "Microsoft YaHei", "PingFang SC";
vertical-align: middle;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
a {
text-decoration: none;
}
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { HashRouter } from 'react-router-dom'
import setRootPixel from '@arco-design/mobile-react/tools/flexible'
import dayjs from 'dayjs'
import { PersistGate } from 'redux-persist/integration/react'
import { store, persistor } from '@/store'
import Routes from './router'
import AuthRouter from './router/AuthRouter'
import 'dayjs/locale/zh-cn'
dayjs.locale('zh-cn')
const ROOT = document.getElementById('root')
setRootPixel()
const App = () => (
// <React.StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<HashRouter>
<AuthRouter>
<Routes />
</AuthRouter>
</HashRouter>
</PersistGate>
</Provider>
// </React.StrictMode>
)
ROOT && ReactDOM.createRoot(ROOT).render(<App />)
import React from 'react'
import { Button, Cell, Toast } from '@arco-design/mobile-react'
import styled from 'styled-components'
import { useRequest } from '@/hooks/useRequest'
import { DeleteMahjongQueenApi, GetMahjongQueenListApi } from './services'
const LoginPage: React.FC = () => {
const { data: dataList, run: refreshList } = useRequest<any[]>(GetMahjongQueenListApi, {
defaultValue: [],
formatResult: (res) => (res?.data || []),
onSuccess: () => {
Toast.success('刷新成功')
},
onError: () => {
Toast.error('刷新失败')
}
})
const { loading, run: handleClickClear } = useRequest<any[]>(DeleteMahjongQueenApi, {
immediate: false,
onSuccess: () => {
Toast.success('清空成功')
refreshList()
},
onError: () => {
Toast.error('清空失败')
}
})
return (
<Layout>
<Headers>
<Button inline onClick={refreshList}>刷新</Button>
<Button inline type="ghost" loading={loading} onClick={handleClickClear}>清空</Button>
</Headers>
<Cell.Group>
{
dataList.map((item) => (<Cell key={item.userId} label={item.userId} />))
}
</Cell.Group>
</Layout>
)
}
export default LoginPage
const Layout = styled.div`
position: relative;
width: 100%;
height: 100vh;
/* background: #000
url('https://fe-internal-files-1301947356.cos.ap-beijing.myqcloud.com/public/image/background.svg')
no-repeat 50%; */
`
const Headers = styled.div`
display:flex;
justify-content: center;
> button {
margin:10px;
}
`
import { TLogin } from '@/typings/login'
import { Delete, Get, TResult } from '@/utils/request'
/**
* @description: 查询麻将队列
*/
export const GetMahjongQueenListApi = (): TResult<any> => {
return Get(`${process.env.API_HOST}/hzy-api/api/getMahjongQueue`)
}
/**
* @description: 查询麻将队列
*/
export const DeleteMahjongQueenApi = (params: TLogin): TResult<any> => {
return Delete(`${process.env.API_HOST}/hzy-api/api/delMahjongQueue`)
}
import React, { useEffect, useRef, useState } from 'react'
// import { Button, Cell, Toast } from '@arco-design/mobile-react'
import _ from 'lodash'
import styled from 'styled-components'
// import { useRequest } from '@/hooks/useRequest'
const Home: React.FC = () => {
// 绑定dom元素
const body = useRef<any>(null) // 视窗
const landscape = useRef<any>(null) // 视频盒子
const video1 = useRef<any>(null) // 视频
const videoBox = useRef<any>(null) // canvas
const animation = useRef<any>(null) // 动画
const control = useRef<any>(null) // 控制栏
const land = useRef<any>(null) // 横屏按钮
const vertical = useRef<any>(null) // 竖屏按钮
const rorate = useRef<any>(null) // 旋转1按钮
const rorateFix = useRef<any>(null) // 旋转2按钮
const rorateLand = useRef<any>(null) // 旋转3按钮
const blackBox1 = useRef<any>(null) // 黑边1
const blackBox2 = useRef<any>(null) // 黑边2
const [sys, setSys] = useState<string>('') // 系统
const direction = useRef<string>('vertical') // 横屏模式:"land"; 竖屏模式:"vertical", 旋转固定方向:"rorateFix", rorate:"旋转带缩放";rorateLand: "横屏视频流旋转"
const stateLed = useRef<string>('front') // 旋转模式下---横竖屏状态 "land":横屏; "vertical":竖屏
const stateLedFB = useRef<string>('vertical') // "front": 左横、正竖; "back": 右横、倒竖
const oldAlpha = useRef<number>(361)// 滤波
const oldBeta = useRef<number>(361)
const oldGamma = useRef<number>(361)
const rotateAngle = useRef<number>(0)// 旋转角度
const typeVer = useRef<number>(0) // 竖屏两个临界范围状态
const typeLand = useRef<number>(0) // 横屏连两个临界范围状态
const [statusPer, setStatusPer] = useState<boolean>(false) // 权限蒙层是否展示
const [playStatus, setPlayStatus] = useState<boolean>(true) // 播放状态
const [controlStatus, setControlStatus] = useState<boolean>(false) // 控制栏状态
const isDraw = useRef<boolean>(false) // 是否将视频画到canvas中
const videoStatus = useRef<boolean>(true) // 视频状态
// 全局参数
const verValue = 90 // 切竖屏临界参数
const landValue = 20 // 切横屏临界参数
const landValue2 = 90 - landValue // 互余角
const range = 5 // 临界角度上下加减范围
const frame = 12 // 动画再第6帧进行横竖切换 1秒25帧
// const w = videoBox?.current?.width
// const h = videoBox?.current?.height
useEffect(() => {
if (videoBox.current) {
console.log('canvas放大')
videoBox.current.width = videoBox.current.width * 10
videoBox.current.height = videoBox.current.height * 10
}
getIosAnd()
handleBodyClick()
}, [])
useEffect(() => {
videoStatus.current = playStatus
}, [playStatus])
// 视频播放时候
video1?.current?.addEventListener('play', function () {
isDraw.current = true
if (sys === 'Android') return
switchToCanvas()
})
// 视频暂停时候
video1?.current?.addEventListener('pasued', function () {
isDraw.current = false
})
// 绘制视频
function switchToCanvas () {
if (!isDraw.current || sys === 'Android') {
return
}
if (videoBox.current && video1.current) {
const ctx = videoBox.current.getContext('2d')
ctx?.drawImage(video1.current, 0, 0, videoBox.current.width, videoBox.current.height)
}
window.requestAnimationFrame(switchToCanvas)
}
const boxStyle = () => {
const bodyWidth = body?.current?.offsetWidth
const bodyHeight = body?.current?.offsetHeight
// 补黑边
if (blackBox2.current) {
blackBox2.current.style.width = bodyWidth + 'px'
blackBox2.current.style.height = (bodyHeight - (16 / 9) * bodyWidth) / 2 + 'px'
blackBox2.current.style.bottom = 0
blackBox2.current.style.left = 0
}
if (blackBox1.current) {
blackBox1.current.style.width = bodyWidth + 'px'
blackBox1.current.style.height = (bodyHeight - (16 / 9) * bodyWidth) / 2 + 'px'
blackBox1.current.style.top = 0
blackBox1.current.style.left = 0
}
}
// 获取手机系统
const getIosAnd = () => {
const u = navigator.userAgent
const isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/) // ios终端
if (isIOS) {
setSys('IOS')
setStatusPer(true)
// if (videoBox.current) {
// console.log(99999)
// videoBox.current.width = videoBox.current.width * 4
// videoBox.current.height = videoBox.current.height * 4
// console.log(videoBox.current.width, videoBox.current.height)
// }
if (video1.current) {
video1.current.play()
video1.current.muted = false
video1.current.style.visibility = 'hidden'
}
} else {
setTimeout(() => {
if (video1.current) {
video1.current.play()
video1.current.muted = false
}
}, 1000)
setSys('Android')
boxStyle()
handleControl() // 控制栏
handleBoost() // 缩放
handleRorateDeal() // 旋转
handleMonitor()
}
}
// 监听陀螺仪事件
const handleMonitor = () => {
if (window.addEventListener) {
window.addEventListener(
'deviceorientation',
_.throttle(updateGravity, 0),
false
)
} else {
alert('您的手机不支持陀螺仪哦~')
}
}
// 处理旋转数据
const updateGravity = (event: any) => {
boxStyle()
if (videoStatus.current && video1.current) {
video1.current.play()
}
// 滤波机制
const angle = 0.1
oldAlpha.current === 361 && (oldAlpha.current = event.alpha)
oldBeta.current === 361 && (oldBeta.current = event.beta)
oldGamma.current === 361 && (oldGamma.current = event.gamma)
const subAlpha = event.alpha - oldAlpha.current
const subBeta = event.beta - oldBeta.current
const subGamma = event.gamma - oldGamma.current
if (Math.abs(subAlpha) >= angle) {
oldAlpha.current = event.alpha
}
if (Math.abs(subBeta) >= angle) {
oldBeta.current = event.beta
}
if (Math.abs(subGamma) >= angle) {
oldGamma.current = event.gamma
}
if (
Math.abs(subAlpha) <= angle &&
Math.abs(subBeta) <= angle &&
Math.abs(subGamma) <= angle
) {
return
}
const useBeta = oldBeta.current
const useGamma = oldGamma.current
// value2:旋转过程所取角度
let beta2 = useBeta
if (useBeta > 90) {
beta2 = 180 - useBeta
} else if (useBeta < -90) {
beta2 = -(useBeta + 180)
}
let gamma2 = Math.abs(useBeta) > 90 ? -useGamma : useGamma
const a = Math.abs(beta2 / 20)
const temp = (a * a * a) / 10 + 1
gamma2 = gamma2 / temp
const value = Math.atan2(beta2, gamma2) / (Math.PI / 180)
rotateAngle.current = value < 0 ? value + 360 : value
// 水平放置始终保持放置前状态
if (
Number(useBeta) <= 20 &&
Number(useBeta) >= -10 &&
Number(useGamma) <= 15 &&
Number(useGamma) >= -20
) {
if (direction.current === 'rorateLand' || direction.current === 'rorate') {
handleLevel()
}
return
}
handleCritical()
event?.stopPropagation()
}
// 水平数据处理
const handleLevel = () => {
const bodyWidth = body?.current?.offsetWidth
if (stateLed.current === 'vertical') {
verticalPosition(90)
control.current.style.width = bodyWidth + 'px'
if (stateLedFB.current === 'front') {
handleRorate(0)
control.current.setAttribute('class', 'button-box')
} else {
handleRorate(180)
control.current.setAttribute('class', 'button-box-top')
}
} else {
landscapePosition(0)
control.current.style.width = (16 / 9) * bodyWidth + 'px'
if (stateLedFB.current === 'front') {
handleRorate(90)
control.current.setAttribute('class', 'button-box-landscape')
} else {
handleRorate(270)
control.current.setAttribute('class', 'button-box-landscape-right')
}
}
}
// 临界点横竖屏切换
const handleCritical = () => {
if (rotateAngle.current >= verValue - range && rotateAngle.current <= verValue + range) {
stateLed.current = 'vertical'
stateLedFB.current = 'front'
} else if (
rotateAngle.current >= verValue + 180 - range &&
rotateAngle.current <= verValue + 180 + range
) {
stateLed.current = 'vertical'
stateLedFB.current = 'back'
} else if (
(rotateAngle.current <= landValue + range && rotateAngle.current >= 0) ||
(rotateAngle.current >= landValue + 180 + 2 * landValue2 - range &&
rotateAngle.current <= 360)
) {
stateLed.current = 'land'
stateLedFB.current = 'back'
} else if (
rotateAngle.current > landValue + 2 * landValue2 - range &&
rotateAngle.current < landValue + 180 + range
) {
stateLed.current = 'land'
stateLedFB.current = 'front'
}
if (direction.current === 'rorateFix') {
handleAnimStatus()
} else {
handleControl() // 控制栏
handleBoost() // 缩放
handleRorateDeal() // 旋转
}
}
// 横屏
const landscapePosition = (value: number) => {
const bodyWidth = body?.current?.offsetWidth
const bodyHeight = body?.current?.offsetHeight
if (bodyWidth && bodyHeight) {
const num = Number(((value * Math.PI) / 180).toFixed(5)) // 角度
const d =
Math.abs(bodyHeight * Math.sin(num)) +
Math.abs(bodyWidth * Math.cos(num)) // 横屏视频流最小高度
// 横向视频流宽高
const landVideoW = (16 / 9) * d
const landVideoH = d
// 根据原视频得出放大比例为 1920 / 1312.5
const zoomPct = 1920 / 1312.5
// 求出当前视频放大之后的宽高为
const videoWidth = landVideoW * zoomPct
const videoHeight = landVideoH * zoomPct
const vh = videoHeight // 竖屏高
const vw = vh * (9 / 16) // 竖屏宽
const lw = videoWidth - vw // 横屏宽
const lh = lw * (9 / 16) // 横屏高
// 求出横屏xy中心点
const lx = lw / 2
const ly = lh / 2
// 求出横屏中心点在整个视频中占的比例
const lxPct = (lx / videoWidth) * 100
const lyPct = (ly / videoHeight) * 100
// 求出超出距离
const lLeft = lx - bodyWidth / 2
const lTop = ly - bodyHeight / 2
// moveBox框样式赋值
if (landscape.current) {
landscape.current.style.width = bodyWidth + 'px'
landscape.current.style.height = bodyHeight + 'px'
}
// 视频样式赋值
if (sys === 'IOS') {
if (videoBox.current) {
videoBox.current.style.width = videoWidth + 'px'
videoBox.current.style.height = videoHeight + 'px'
videoBox.current.style['margin-left'] = -lLeft + 'px'
videoBox.current.style['margin-top'] = -lTop + 'px'
videoBox.current.style['transform-origin'] = `${lxPct}% ${lyPct}%`
}
} else {
if (video1.current) {
video1.current.style.width = videoWidth + 'px'
video1.current.style.height = videoHeight + 'px'
video1.current.style['margin-left'] = -lLeft + 'px'
video1.current.style['margin-top'] = -lTop + 'px'
video1.current.style['transform-origin'] = `${lxPct}% ${lyPct}%`
}
}
}
}
// 竖屏
const verticalPosition = (value: number) => {
const bodyWidth = body?.current?.offsetWidth
const bodyHeight = body?.current?.offsetHeight
if (bodyWidth && bodyHeight) {
const num = Number((((value - 90) * Math.PI) / 180).toFixed(5)) // 角度
const d =
Math.abs(bodyHeight * Math.sin(num)) +
Math.abs(bodyWidth * Math.cos(num)) // 竖屏视频流最小宽度
// 竖屏视频流宽高
const verVideoW = d
const verVideoH = (16 / 9) * d
const zoomPct = 1920 / 607.5
// 求出第一步放大之后的视频大小
const videoWidth = verVideoW * zoomPct
const videoHeight = verVideoH
const vh = videoHeight // 竖屏高
const vw = d // 竖屏宽
const lw = videoWidth - vw // 横屏宽
// 竖屏xy中心点
const vx = vw / 2
const vy = vh / 2
// 计算竖屏中心点在整个视频中所占的比例
// 横屏加竖屏宽度一半除以整体宽
const vxPct = ((vx + lw) / videoWidth) * 100
const vhPct = (vy / videoHeight) * 100
// 计算偏移量
const vLeft = lw + vw / 2 - bodyWidth / 2
const vTop = vh / 2 - bodyHeight / 2
// moveBox框样式赋值
if (landscape.current) {
landscape.current.style.width = bodyWidth + 'px'
landscape.current.style.height = bodyHeight + 'px'
}
if (sys === 'IOS') {
if (videoBox.current) {
videoBox.current.style.width = videoWidth + 'px'
videoBox.current.style.height = videoHeight + 'px'
videoBox.current.style['margin-left'] = -vLeft + 'px'
videoBox.current.style['margin-top'] = -vTop + 'px'
videoBox.current.style['transform-origin'] = `${vxPct}% ${vhPct}%`
}
} else {
if (video1.current) {
video1.current.style.width = videoWidth + 'px'
video1.current.style.height = videoHeight + 'px'
video1.current.style['margin-left'] = -vLeft + 'px'
video1.current.style['margin-top'] = -vTop + 'px'
video1.current.style['transform-origin'] = `${vxPct}% ${vhPct}%`
}
}
}
}
// 旋转
const handleRorate = (value: number) => {
const objAttr: any = {
transform: `rotate(${value}deg)`
}
// 循环属性对象
for (const i in objAttr) {
let newi = i
// 判断是否存在transform-origin这样格式的属性
if (newi.indexOf('-') > 0) {
const num = newi.indexOf('-')
newi = newi.replace(
newi.substr(num, 2),
newi.substr(num + 1, 1).toUpperCase()
)
}
// 考虑到css3的兼容性问题,所以这些属性都必须加前缀才行
if (sys === 'IOS') {
if (videoBox.current) {
videoBox.current.style[newi] = objAttr[i]
videoBox.current.style['-webkit-' + newi] = objAttr[i]
videoBox.current.style['-moz-' + newi] = objAttr[i]
videoBox.current.style['-o-' + newi] = objAttr[i]
videoBox.current.style['-ms-' + newi] = objAttr[i]
}
} else {
if (video1.current) {
video1.current.style[newi] = objAttr[i]
video1.current.style['-webkit-' + newi] = objAttr[i]
video1.current.style['-moz-' + newi] = objAttr[i]
video1.current.style['-o-' + newi] = objAttr[i]
video1.current.style['-ms-' + newi] = objAttr[i]
}
}
}
}
// 不同模式 控制栏处理
const handleControl = () => {
const bodyWidth = body?.current?.offsetWidth
const bodyHeight = body?.current?.offsetHeight
const marginNum = (bodyHeight - (16 / 9) * bodyWidth) / 2
land.current && (land.current.style.color = direction.current === 'land' ? '#24adf3' : '#e7eaed')
vertical.current && (vertical.current.style.color = direction.current === 'vertical' ? '#24adf3' : '#e7eaed')
rorate.current && (rorate.current.style.color = direction.current === 'rorate' ? '#24adf3' : '#e7eaed')
rorateFix.current && (rorateFix.current.style.color = direction.current === 'rorateFix' ? '#24adf3' : '#e7eaed')
rorateLand.current && (rorateLand.current.style.color = direction.current === 'rorateLand' ? '#24adf3' : '#e7eaed')
switch (direction.current) {
case 'land':
control?.current?.setAttribute('class', 'button-box-landscape')
control.current && (control.current.style.width = (16 / 9) * bodyWidth + 'px')
break
case 'vertical':
control?.current?.setAttribute('class', 'button-box')
control.current && (control.current.style.width = bodyWidth + 'px')
break
default:
if (stateLed.current === 'vertical' && control.current) {
stateLedFB.current === 'front'
? control.current.setAttribute('class', 'button-box')
: control.current.setAttribute('class', 'button-box-top')
control.current.style.width = bodyWidth + 'px'
}
if (stateLed.current === 'land' && control.current) {
stateLedFB.current === 'front'
? control.current.setAttribute('class', 'button-box-landscape')
: control.current.setAttribute('class', 'button-box-landscape-right')
control.current.style.width = (16 / 9) * bodyWidth + 'px'
}
}
if (control.current) {
control.current.style['margin-bottom'] = marginNum + 'px'
control.current.style['margin-top'] = marginNum + 'px'
}
}
// 不同模式 缩放处理
const handleBoost = () => {
switch (direction.current) {
case 'land':
landscapePosition(0) // 横屏
break
case 'vertical':
verticalPosition(90) // 竖屏
break
case 'rorateFix':
stateLed.current === 'land' && landscapePosition(0)
stateLed.current === 'vertical' && verticalPosition(90)
break
case 'rorateLand':
landscapePosition(rotateAngle.current)
break
default:
stateLed.current === 'land' && landscapePosition(rotateAngle.current)
stateLed.current === 'vertical' && verticalPosition(rotateAngle.current)
}
}
// 不同模式 旋转处理
const handleRorateDeal = () => {
switch (direction.current) {
case 'land':
handleRorate(90)
break
case 'vertical':
handleRorate(0)
break
case 'rorateFix':
if (stateLed.current === 'land') {
stateLedFB.current === 'front' ? handleRorate(90) : handleRorate(270)
}
if (stateLed.current === 'vertical') {
stateLedFB.current === 'front' ? handleRorate(0) : handleRorate(180)
}
break
default:
handleRorate(rotateAngle.current - 90)
}
}
// 处理动画 限制频率
const handleAnimation = () => {
if (animation.current) {
animation.current.setAttribute('src', '')
animation.current.setAttribute(
'src',
'https://fe-assets-1301947356.cos.ap-beijing.myqcloud.com/test-ios/transition-animation/3_shu25.gif'
)
animation.current.style.visibility = 'visible'
setTimeout(function () {
handleControl() // 控制栏
handleBoost() // 缩放
handleRorateDeal() // 旋转
setTimeout(function () {
animation.current.style.visibility = 'hidden'
}, (18 / 25) * 1000)
}, (12 / 25) * 1000)
}
}
const functionDe = _.debounce(handleAnimation, (frame / 25) * 2 * 1000, {
leading: true
})
// 动画:范围值内只触发一次
const handleAnimStatus = () => {
if (stateLed.current === 'vertical') {
if (typeVer.current !== 1) {
typeVer.current = 1
typeLand.current = 0
functionDe()
}
}
if (stateLed.current === 'land') {
if (typeLand.current !== 1) {
typeVer.current = 0
typeLand.current = 1
functionDe()
}
}
}
//= ====================================点击事件=========================================
// ios获取权限点击事件
const handlePermissionClick = (event: any) => {
if (video1.current) {
video1.current.play()
}
if ((window.DeviceOrientationEvent as any).requestPermission) {
console.log('获取ios权限333');
(window.DeviceOrientationEvent as any).requestPermission()
.then((state: string) => {
setStatusPer(false)
if (state === 'granted') {
// 允许
console.log('获取ios权限')
handleControl() // 控制栏
handleBoost() // 缩放
handleRorateDeal() // 旋转
handleMonitor()
} else if (state === 'denied') {
// 拒绝
alert('您已拒绝授权,请退出重新进入授权')
} else if (state === 'prompt') {
alert('遇到未知错误,请退出重新进入')
}
})
.catch((code: any) => {
setStatusPer(false)
})
} else {
setStatusPer(false)
alert('当前浏览器不支持,请使用Safari浏览器')
}
event?.stopPropagation()
}
// 屏幕点击事件
let timeOut: any
const handleBodyClick = () => {
console.log('屏幕点击事件')
const visible = !controlStatus
control.current && (control.current.style.display = visible ? 'block' : 'none')
setControlStatus(Boolean(visible))
if (visible) {
timeOut = setTimeout(function () {
control.current && (control.current.style.display = 'none')
setControlStatus(false)
}, 5000)
} else {
clearTimeout(timeOut)
}
}
// 暂停播放按钮点击事件
const handlePlayClick = (value: number, event: any) => {
console.log('播放状态', value)
setPlayStatus(Boolean(value))
if (video1.current) {
value ? video1.current.play() : video1.current.pause()
}
event.stopPropagation()
}
// 不同模式按钮点击事件
const handleModelClick = (value: number, event: any) => {
switch (value) {
case 1:
direction.current = 'land'
stateLed.current = 'land'
stateLedFB.current = 'front'
vertical.current && (vertical.current.removeEventListener('click', handleModelClick))
break
case 2:
direction.current = 'vertical'
stateLed.current = 'vertical'
stateLedFB.current = 'front'
land.current && (land.current.removeEventListener('click', handleModelClick))
break
default:
direction.current = value === 4 ? 'rorateFix' : value === 5 ? 'rorateLand' : 'rorate'
stateLed.current = 'vertical'
stateLedFB.current = 'front'
land.current && (land.current.removeEventListener('click', handleModelClick))
vertical.current && (vertical.current.removeEventListener('click', handleModelClick))
// if (value === 4 || value === 5) {
// land.current && (land.current.style.color = "#e7eaed")
// vertical.current && (vertical.current.style.color = "#e7eaed")
// rorate.current && (rorate.current.style.color = "#e7eaed")
// rorateFix.current && (rorateFix.current.style.color = value === 4 ? "#24adf3" : "#e7eaed")
// rorateLand.current && (rorateLand.current.style.color = value === 5 ? "#24adf3" : "#e7eaed")
// }
}
handleControl() // 控制栏
handleBoost() // 缩放
handleRorateDeal() // 旋转
event?.stopPropagation()
}
return (
<Layout>
<div className='App' ref={body} onClick={handleBodyClick}>
{/* 黑边限制可视化区域 */}
<div className="blackBox" ref={blackBox1}></div>
<div className="blackBox" ref={blackBox2}></div>
{/* 动画 */}
<img ref={animation} className="animation" alt=""
src="https://fe-assets-1301947356.cos.ap-beijing.myqcloud.com/test-ios/transition-animation/3_shu25.gif"></img>
{/* ios获取权限蒙层 */}
{statusPer && (<div className="permissionLoading">
<div className="getPermission" onClick={handlePermissionClick}>点击获取权限</div>
</div>)}
{/* 视频 */}
<div ref={landscape} className="landscape">
{sys === 'IOS' && (<canvas ref={videoBox} className="videoBox"></canvas>)}
<video
ref={video1}
muted
loop
autoPlay
poster="文件地址"
playsInline
className="video2"
src="https://fe-assets-1301947356.cos.ap-beijing.myqcloud.com/test-ios/transition-animation/1108_20M.mp4"
></video>
</div>
{/* 控制栏 */}
<div id='control' ref={control}>
<div className="button-model">
<div className="button-play">
{!playStatus && (
<img
className="playIcon"
src="https://ig-internal--files-1301947356.cos.ap-beijing.myqcloud.com/icons/11515429d65ac392d8b930087f451e3.png"
onClick={(event) => handlePlayClick(1, event)}
alt=""
/>
)}
{playStatus && (
<img
className="stopIcon"
src="https://ig-internal--files-1301947356.cos.ap-beijing.myqcloud.com/icons/cb47c56c95e7b940b9ac0fec7d967ff.png"
onClick={(event) => handlePlayClick(0, event)}
alt=""
/>
)}
</div>
<div className="button-text">
<div ref={land} className="button" onClick={(evnet) => handleModelClick(1, evnet)}>横屏</div>
<div ref={vertical} className="button " onClick={(evnet) => handleModelClick(2, evnet)}>竖屏</div>
<div ref={rorate} className="button" onClick={(evnet) => handleModelClick(3, evnet)}>旋转1</div>
<div ref={rorateFix} className="button" onClick={(evnet) => handleModelClick(4, evnet)}>
旋转2
</div>
<div ref={rorateLand} className="button" onClick={(evnet) => handleModelClick(5, evnet)}>
旋转3
</div>
</div>
</div>
</div>
</div>
</Layout>
)
}
export default Home
const Layout = styled.div`
position: relative;
width: 100%;
height: 100vh;
/* background: #000
url('https://fe-internal-files-1301947356.cos.ap-beijing.myqcloud.com/public/image/background.svg')
no-repeat 50%; */
.App {
width: 100vw;
height: 100vh;
font-family: Monospace;
background-color: #000;
margin: 0px;
overflow: hidden;
}
.animation {
width: 100vw;
height: 100vh;
position: absolute;
visibility: hidden;
object-fit: cover;
z-index: 12;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.landscape {
width: 100vw;
height: 100vh;
overflow: hidden;
}
.videoBox {
width: 100%;
height: 100%;
transform: translateZ(0);
position: absolute;
}
.video2 {
visibility: visible;
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
overflow: hidden;
}
.blackBox {
width: 0;
height: 0;
position: absolute;
background-color: #000;
z-index: 10;
}
.permissionLoading {
position: absolute;
z-index: 12;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ffffffb0;
}
.getPermission {
width: 100%;
height: 80px;
position: absolute;
font-size: 30px;
font-weight: 600;
line-height: 80px;
text-align: center;
margin: 0 auto;
top: 50%;
transform: translateY(-50%);
color: red;
}
#control {
display: block;
width: 100vw;
height: 50px;
margin-bottom: 0;
position: absolute;
z-index: 10;
background: rgba(0, 0, 0, 0.5);
}
.button-box {
width: 100%;
bottom: 0;
left: 0;
}
.button-box-top {
width: 100%;
top: 0;
left: 0;
transform: rotate(180deg);
}
.button-box-landscape {
width: 100%;
top: -25px;
left: 25px;
transform-origin: left;
transform: rotate(90deg);
}
.button-box-landscape-right {
width: 100%;
top: -25px;
right: 25px;
transform-origin: right;
transform: rotate(270deg);
}
.button-model {
display: flex;
width: 100%;
height: 100%;
position: absolute;
justify-content: space-between;
align-items: center;
/* margin: 0 40px; */
}
.button-text {
display: flex;
}
.button-play {
width: 30px;
height: 30px;
display: flex;
}
.button {
width: 50px;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
line-height: 50px;
padding: 0 5px;
color: #e7eaed;
font-size: 16px;
}
`
import { TLogin } from '@/typings/login'
import { Delete, Get, TResult } from '@/utils/request'
/**
* @description: 查询麻将队列
*/
export const GetMahjongQueenListApi = (): TResult<any> => {
return Get(`${process.env.API_HOST}/hzy-api/api/getMahjongQueue`)
}
/**
* @description: 查询麻将队列
*/
export const DeleteMahjongQueenApi = (params: TLogin): TResult<any> => {
return Delete(`${process.env.API_HOST}/hzy-api/api/delMahjongQueue`)
}
import { TLogin } from '@/typings/login'
import { Post, TResult } from '@/utils/request'
const PATHS = {
LOGIN: `${process.env.API_HOST}/hzy-admin/login`
}
/**
* 登录响应参数
*/
type LoginRes = { token: string }
/**
* @description: 列表查询
* @param {TLogin} params
* @return {*}
*/
export const LoginApi = (params: TLogin): TResult<LoginRes> => {
return Post(PATHS.LOGIN, { params })
}
import React, { useState, useEffect, useRef, ChangeEvent, MouseEvent, TouchEvent } from 'react'
import { useNavigate } from 'react-router-dom'
import { Button, Input } from '@arco-design/mobile-react'
import styled from 'styled-components'
const LoginPage: React.FC = () => {
const uuid = useRef('')
const [captchaUrl, setCaptchaUrl] = useState<string>('')
console.log(captchaUrl)
const navigate = useNavigate()
useEffect(() => {
getCaptcha()
}, [])
// 获取uuid 验证码
const getCaptcha = () => {
const s: any = []
const hexDigits = '0123456789abcdef'
for (let i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1)
}
s[14] = '4' // bits 12-15 of the time_hi_and_version field to 0010
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1) // bits 6-7 of the clock_seq_hi_and_reserved to 01
s[8] = s[13] = s[18] = s[23] = '-'
const _uuid = s.join('')
uuid.current = _uuid
setCaptchaUrl(`${process.env.API_HOST}/hzy-admin/captcha?uuid=${_uuid}`)
}
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const onUsernameChange = (e: ChangeEvent<HTMLInputElement>) => {
setUsername(e.target.value)
}
const onPasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value)
}
const handleLogin = (e: MouseEvent | TouchEvent) => {
console.log('登录::', username, password)
navigate('/game-room-tools')
}
return (
<LoginLayout>
<LoginContainer>
<LoginContainerHeader>移动端模板</LoginContainerHeader>
<Input label="用户名" onChange={onUsernameChange} placeholder="请输入用户名" clearable border="none" />
<Input
label="密码"
onChange={onPasswordChange}
type="password"
placeholder="请输入密码"
clearable
border="none"
/>
<LoginButton needActive onClick={handleLogin}>
Login
</LoginButton>
</LoginContainer>
</LoginLayout>
)
}
export default LoginPage
const LoginLayout = styled.div`
position: relative;
width: 100%;
height: 100vh;
/* background: #000
url('https://fe-internal-files-1301947356.cos.ap-beijing.myqcloud.com/public/image/background.svg')
no-repeat 50%; */
`
const LoginContainer = styled.div`
width: 100%;
height: 500px;
box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto auto;
background: transparent;
padding: 20px;
.ant-space-align-center {
align-items: flex-start;
}
`
const LoginContainerHeader = styled.h1`
height: 44px;
line-height: 44px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-bottom: 20px;
font-size: 32px;
> img {
display: block;
height: 44px;
margin-bottom: 10px;
}
> span {
color: rgba(0, 0, 0, 0.85);
font-weight: 600;
}
`
const LoginButton = styled(Button)`
width: 90%;
margin: 20px auto 0;
`
import { Navigate, useLocation } from 'react-router-dom'
import { HOME_URL } from '@/config/config'
import { useAppSelector } from '@/hooks/useStore'
import { store } from '@/store'
import { searchRoute } from '@/utils/utils'
import { rootRouter } from './index'
/**
* @description 路由守卫组件
* */
const AuthRouter = (props: any) => {
const userStore = useAppSelector(state => state.user)
const { pathname } = useLocation()
const route = searchRoute(pathname, rootRouter)
// console.log('[ route ]', rootRouter)
// * 在跳转路由之前,清除所有的请求
// axiosCanceler.removeAllPending()
// * 判断当前路由是否需要访问权限(不需要权限直接放行)
if (!route?.meta?.auth) return props.children
// * 判断是否有Token
const token = store.getState().user.token
if (!token) return <Navigate to="/login" replace />
// * Dynamic Router(动态路由,根据后端返回的菜单数据生成的一维数组)
const dynamicRouter = userStore.authRouter
// * Static Router(静态路由,必须配置首页地址,否则不能进首页获取菜单、按钮权限等数据),获取数据的时候会loading,所有配置首页地址也没问题
const staticRouter = [HOME_URL, '/403']
const routerList = dynamicRouter?.concat(staticRouter)
// * 如果访问的地址没有在路由表中重定向到403页面
if (routerList?.indexOf(pathname) === -1) return <Navigate to="/403" />
// * 当前账号有权限返回 Router,正常访问页面
return props.children
}
export default AuthRouter
import { lazy } from 'react'
import { Navigate, useRoutes } from 'react-router-dom'
import BaseLayout from '@/layouts/BaseLayout'
import Login from '@/pages/Login'
import { RouteObject } from './interface'
const GameRoomTools = lazy(() => import('@/pages/GameRoomTools/index'))
const Home = lazy(() => import('@/pages/Home/index'))
export const rootRouter: RouteObject[] = [
{
path: '/',
element: <Navigate to="/home" />
},
{
path: '/Login',
element: <Login />,
meta: {
auth: false,
title: '登录页',
key: 'login'
}
},
{
element: <BaseLayout />,
children: [
{
path: '/game-room-tools',
name: '首页',
element: <GameRoomTools />,
meta: {
auth: false,
title: '首页',
key: 'home'
}
},
{
path: '/home',
name: '视频旋转',
element: <Home />,
meta: {
auth: false,
title: '视频旋转',
key: 'home'
}
}
]
}
// 异常页面
// { path: '*', element: <Pack /> }
]
const Routes = () => {
const routes = useRoutes(rootRouter)
return routes
}
export default Routes
import { ReactNode } from 'react'
export interface MetaProps {
keepAlive?: boolean
auth?: boolean
title: string
key?: string
}
export interface RouteObject {
caseSensitive?: boolean
children?: RouteObject[]
element?: ReactNode
index?: boolean
path?: string
meta?: MetaProps
isLink?: string
name?: string
}
import { TResult, Post } from '@/utils/request'
/**
* @description 登录
* @api http://api-center-bigdata.huan.tv/project/529/interface/api/8925
*/
export const LoginApi = (params: Record<string, any>): TResult => {
// return new Promise((resolve, reject) => {
// const { password: inputPassword, account } = params;
// if (!UserDb.has(account)) {
// reject("用户名不存在");
// }
// const { res, password } = UserDb.get(account);
// if (!inputPassword || inputPassword !== password) {
// reject("密码错误");
// }
// resolve({
// code: 200,
// msg: "登录成功",
// data: res
// });
// });
return Post('/api/admin/outer/auth/login?encrypt=false', { params })
}
import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/es/storage'
import UserReducer from './user'
// 创建reducer(拆分reducer)
const reducer = combineReducers({
user: UserReducer
})
// redux 持久化配置
const persistConfig = {
key: 'redux-state',
storage
}
const persistedReducer = persistReducer(persistConfig, reducer)
// 创建 store
const store = configureStore({
reducer: persistedReducer,
devTools: process.env.NODE_ENV !== 'production',
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false
})
})
// 创建持久化 store
const persistor = persistStore(store)
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export { store, persistor }
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
export const NAMESPACE_KEY = 'user'
type State = {
status: 'idle' | 'loading' | 'succeeded' | 'failed',
isLogin: boolean
username: string
password: string
token: string
remember: boolean
avatar?: string
createTime: number
dept: string
deptId: number
deptList: string
email: string
enabled: boolean
licenseExpired: boolean
nickname: string
phone: string
roleIdList: string
roles: string[]
userLoginInfo: {
address: string
device: string
ip: string
lastLoginTime: number
},
authRouter: string[]
}
const initialState: State = {
status: 'idle',
isLogin: false,
username: '',
password: '',
token: '',
remember: false,
avatar: '',
createTime: 0,
dept: '',
deptId: 0,
deptList: '',
email: '',
enabled: false,
licenseExpired: false,
nickname: '',
phone: '',
roleIdList: '',
roles: [],
userLoginInfo: {
address: '',
device: '',
ip: '',
lastLoginTime: 0
},
authRouter: []
}
const { reducer, actions } = createSlice({
name: NAMESPACE_KEY,
initialState,
reducers: {
// login(state, { payload }: PayloadAction<Partial<State>>) {
// Object.assign(state, payload)
// },
setAuthRouter (state, { payload }: PayloadAction<string[]>) {
state.authRouter = payload
},
setLogin (state, { payload }: PayloadAction<any>) {
state.token = payload?.token
state.username = payload?.username
state.isLogin = true
},
setLogout (state) {
state.token = ''
state.isLogin = false
}
}
})
export const { setAuthRouter, setLogin, setLogout } = actions
export default reducer
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'
export type TLogin = {
captcha: string, // 验证码
password: string, // 密码
username: string, // 账户
uuid: string, // uuid
grant_type: string // 登录类型 默认值:password
}
import React from 'react'
/**
* @description 路由懒加载
* @param {Element} Comp 需要访问的组件
* @returns element
*/
const lazyLoad = (Comp: React.LazyExoticComponent<any>): React.ReactNode => {
return (
<Comp />
)
}
export default lazyLoad
import axios, { Method, AxiosRequestConfig } from 'axios'
// import { message } from 'antd'
import { store } from '@/store'
export type TResult<T = any> = Promise<{ code: number, data: T, msg: string }>;
const request = axios.create({
baseURL: process.env.API_HOST,
headers: { 'Content-Type': 'application/json' },
withCredentials: false
})
request.interceptors.request.use(
(config) => {
const _config = Object.assign({}, config)
return _config
},
(error) => {
return Promise.reject(error)
}
)
request.interceptors.response.use(
(res: any) => {
// console.log('res===>', res)
const { code } = res.data || {}
// const { code, msg } = res.data || {}
switch (code) {
case 0:
return res.data
case 10020:
case 10021:
window.location.href = '/login' // react 路由组件跳转需要使用hooks方式,所以写成原生跳转
default:
// message.error(msg || '服务器错误')
return Promise.reject(res)
}
},
(err) => {
switch (err?.response?.status) {
case 400:
break
case 401:
break
case 405:
case 500:
default:
break
}
return Promise.reject(err.response)
}
)
interface RequestOptions extends Partial<AxiosRequestConfig> {
isAuth?: boolean;
urlFields?: string[];
isForm?: boolean;
transformParams?: (params: any) => any;
}
const createMethod =
(method: Method = 'get') =>
(url: string, options?: RequestOptions) => {
const {
urlFields,
isAuth = true,
isForm = false,
headers = {},
transformParams,
params,
...args
} = options || {}
let data = ['get'].includes(method) ? { params } : { data: params }
// : { data: Qs.stringify(params, { arrayFormat: "brackets" }) };
// 处理formdata形式
if (isForm) {
const formParams = new FormData()
for (const key in params) {
formParams.append(key, params[key])
}
data.data = formParams;
(options as any).headers = Object.assign({}, options?.headers, {
'Content-Type': 'multipart/form-data'
})
}
if (typeof transformParams === 'function') {
data = transformParams(params)
}
// 过滤URl参数
if (urlFields) {
for (const key of urlFields) {
url = url.replace(new RegExp(`:${key}`), (params as any)[key])
}
}
// 接口增加权限token
if (isAuth) {
const data: any = store.getState()
if (data.user) {
// (headers as any)["token"] = data.user["token"];
(headers as any).token = store.getState().user.token
}
}
return request(url, {
method,
headers,
...data,
...args
} as any) as Promise<any>
}
export const Post = createMethod('post')
export const Get = createMethod('get')
export const Delete = createMethod('delete')
export const Put = createMethod('put')
export default request
import { RouteObject } from '@/router/interface'
interface MenuOptions {
path: string
label: string
icon?: string
isLink?: string
close?: boolean
children?: MenuOptions[]
}
/**
* @description 获取需要展开的 subMenu
* @param {String} path 当前访问地址
* @returns array
*/
export const getOpenKeys = (path: string) => {
let newStr: string = ''
const newArr: any[] = []
const arr = path.split('/').map(i => '/' + i)
for (let i = 1; i < arr.length - 1; i++) {
newStr += arr[i]
newArr.push(newStr)
}
return newArr
}
/**
* @description 递归查询对应的路由
* @param {String} path 当前访问地址
* @param {Array} routes 路由列表
* @returns array
*/
export const searchRoute = (path: string, routes: RouteObject[] = []): RouteObject => {
let result: RouteObject = {}
for (const item of routes) {
if (item.path === path) return item
if (item.children) {
const res = searchRoute(path, item.children)
if (Object.keys(res).length) result = res
}
}
return result
}
/**
* @description 双重递归 找出 所有面包屑生成对象存到 redux 中,就不用每次都去递归查找了
* @param {String} menuList 当前菜单列表
* @returns object
*/
export const findAllBreadcrumb = (menuList: MenuOptions[]): { [key: string]: any } => {
const handleBreadcrumbList: any = {}
const loop = (menuItem: MenuOptions) => {
// 下面判断代码解释 *** !item?.children?.length ==> (item.children && item.children.length > 0)
// console.log('[ menulist ]', menuItem.path, menuList)
if (menuItem?.children?.length) menuItem.children.forEach(item => loop(item))
else handleBreadcrumbList[menuItem.path] = getBreadcrumbList(menuItem.path, menuList)
}
menuList.forEach(item => loop(item))
return handleBreadcrumbList
}
/**
* @description 递归当前路由的 所有 关联的路由,生成面包屑导航栏
* @param {String} path 当前访问地址
* @param {Array} menuList 菜单列表
* @returns array
*/
export const getBreadcrumbList = (path: string, menuList: MenuOptions[]) => {
const tempPath: any[] = []
try {
const getNodePath = (node: MenuOptions) => {
tempPath.push(node)
// 找到符合条件的节点,通过throw终止掉递归
if (node.path === path) {
throw new Error('GOT IT!')
}
if (node.children && node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
getNodePath(node.children[i])
}
// 当前节点的子节点遍历完依旧没找到,则删除路径中的该节点
tempPath.pop()
} else {
// 找到叶子节点时,删除路径当中的该叶子节点
tempPath.pop()
}
}
for (let i = 0; i < menuList.length; i++) {
getNodePath(menuList[i])
}
} catch (e) {
return tempPath.map(item => item.label)
}
}
/**
* @description 使用递归处理路由菜单,生成一维数组,做菜单权限判断
* @param {Array} menuList 所有菜单列表
* @param {Array} newArr 菜单的一维数组
* @return array
*/
export function handleRouter (routerList: MenuOptions[], newArr: string[] = []) {
routerList.forEach((item: MenuOptions) => {
typeof item === 'object' && item.path && newArr.push(item.path)
item.children && item.children.length && handleRouter(item.children, newArr)
})
return newArr
}
{
"compilerOptions": {
"strict": true,
"module": "commonjs",
"target": "es5",
"esModuleInterop": true,
"jsx": "react-jsx",
"allowJs": true,
"checkJs": true,
// "noImplicitAny": true,
// "importHelpers": true,
"noEmit": true,
"sourceMap": true,
"experimentalDecorators": true,
"lib": ["esnext", "dom"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"resolveJsonModule": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment