1.根据ui大致完成登录界面。

This commit is contained in:
luocai 2023-06-07 16:31:18 +08:00
parent 99d12b483d
commit fcaae1860c
18 changed files with 630 additions and 79 deletions

View File

@ -6,14 +6,21 @@
"@emotion/react": "^11.11.0", "@emotion/react": "^11.11.0",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.11.16", "@mui/icons-material": "^5.11.16",
"@mui/lab": "^5.0.0-alpha.133",
"@mui/material": "^5.13.3", "@mui/material": "^5.13.3",
"@reduxjs/toolkit": "^1.9.5",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"http-proxy-middleware": "^2.0.6",
"md5": "^2.3.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-redux": "^8.0.7",
"react-router-dom": "^6.11.2", "react-router-dom": "^6.11.2",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"sha1": "^1.1.1",
"sha256": "^0.2.0",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {

View File

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>纽曼AI语记</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -11,11 +11,6 @@
"src": "logo192.png", "src": "logo192.png",
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
} }
], ],
"start_url": ".", "start_url": ".",

View File

@ -1,13 +1,64 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import yzs from "./business/request.js";
import styles from './LoginPage.module.css'; import styles from './LoginPage.module.css';
import { useSelector, useDispatch } from 'react-redux'
import { setAddress } from "./business/ipSlice.js"
import { setFlushToken, setAccessToken, setUserInfo, selectFlushToken } from "./business/userSlice.js"
import logo from './assets/logo.png'; // Tell webpack this JS file uses this image
import { Container, Tab, Box } from '@mui/material';
import TabPanel from '@mui/lab/TabPanel';
import { TabList } from '@mui/lab';
import TabContext from '@mui/lab/TabContext';
import DynamicCodeForm from './components/DynamicCodeForm.js';
import PasswordForm from './components/PasswordForm.js';
import { createTheme, ThemeProvider } from '@mui/material/styles';
const theme = createTheme({
status: {
danger: '#e53e3e',
},
palette: {
primary: {
main: '#FF595A',
darker: '#FF595A',
},
neutral: {
main: '#64748B',
contrastText: '#fff',
},
},
});
export default function () { export default function () {
const [value, setValue] = useState("1");
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const handleChange = (event, newValue) => {
setValue(newValue);
};
const ip = useSelector(state => state.ip.value)
const flushToken = useSelector(selectFlushToken)
const dispatch = useDispatch()
const getIp = async () => {
// Connect ipapi.co with fetch()
const response = await fetch("https://ipapi.co/json/")
const data = await response.json()
// Set the IP address to the constant `ip`
dispatch(setAddress(data.ip));
}
// Run `getIP` function above just once when the page is rendered
useEffect(() => {
getIp()
}, [])
const handleInputChange = (event) => { const handleInputChange = (event) => {
@ -18,33 +69,76 @@ export default function () {
const handleSubmit = (event) => { const handleSubmit = (event) => {
event.preventDefault(); event.preventDefault();
console.log(`Username: ${username}\nPassword: ${password}`); console.log(`Username: ${username}\nPassword: ${password} ip: ${ip}`);
yzs.login(ip.payload).then(token => {
dispatch(setFlushToken(token));
yzs.get_access_token(ip.payload, token).then(token => {
// yzs.update_access_token(ip.payload, token);
dispatch(setAccessToken(token));
yzs.get_user_info(ip.payload, token).then(info => {
dispatch(setUserInfo(info));
let passportId = info.passportId;
yzs.user_select(ip.payload, token).then(info => {
yzs.get_record_list(token, passportId)
})
});
})
});
}; };
return ( return (
<form className={styles.form} onSubmit={handleSubmit}> <div className={styles.loginPage}>
<TextField <div className={styles.title}>
name="username" <img className={styles.titleIcon} src={logo} />
label="请输入手机号码" <h1 className={styles.titleText}>纽曼AI语记</h1>
variant="outlined" </div>
value={username}
onChange={handleInputChange} <div className={styles.loginFrame}>
/> <ThemeProvider theme={theme}>
<TextField <Container component="form" className={styles.form} onSubmit={handleSubmit}
name="password" sx={{
label="请输入密码" width: 360,
type="password" height: 418,
variant="outlined" backgroundColor: 'white',
value={password} display: "flex",
onChange={handleInputChange} flexDirection: "column",
/> justifyContent: "center",
<Button alignItems: "center",
type="submit" boxShadow: "0px 5px 20px 0px rgba(146,0,1,0.1)",
variant="contained" borderRadius: 4,
color="primary"
}}
> >
登录 <TabContext value={value}>
</Button> <Box>
</form>
<TabList
aria-label="basic tabs example" value={value} onChange={handleChange} >
<Tab label="手机动态码登录" value="1" />
<Tab label="账号密码登录" value="2" />
</TabList>
</Box>
<TabPanel value="1" >
<DynamicCodeForm />
</TabPanel>
<TabPanel value="2" >
<PasswordForm />
</TabPanel>
</TabContext>
</Container>
</ThemeProvider >
</div>
</div>
); );
} }

View File

@ -1,4 +1,29 @@
.form { .loginPage {
background-image: url(./assets/background@2x.png);
background-size: cover;
height: 100vh;
}
.title {
position: absolute;
display: flex;
align-items: center;
margin-left: 72px;
padding-top: 30px;
}
.titleIcon {
width: 54px;
height: 57px;
margin-right: 24px;
}
.titleText {
color: #FF595A;
}
.loginFrame {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
src/assets/logo@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

22
src/business/ipSlice.js Normal file
View File

@ -0,0 +1,22 @@
import { createSlice } from '@reduxjs/toolkit'
export const ipSlice = createSlice({
name: 'ip',
initialState: {
value: ""
},
reducers: {
setAddress: (state, ip) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value = ip;
},
}
})
// Action creators are generated for each case reducer function
export const { setAddress } = ipSlice.actions
export default ipSlice.reducer

170
src/business/request.js Normal file
View File

@ -0,0 +1,170 @@
const appKey = "k5hfiei5eevouvjohkapjaudpk2gakpaxha22fiy";
const appSecret = "e65ffb25148b08207088148d5bce114d";
function constructParameter(body) {
let params = [];
for (let key in body) {
params.push(body[key].toString());
}
params.sort();
let digest = "";
for (let param of params) {
console.log(param)
digest += param;
}
let sha1 = require('sha1');
body.signature = sha1(digest).toUpperCase();
let p = '';
for (let key in body) {
p += key;
p += "=";
p += encodeURIComponent(body[key]);
p += "&";
}
p = p.slice(0, -1);
return p;
}
const yzs = {
get_access_token: function (ip, flushToken) {
let body = {};
body.subsystemId = 16;
body.clientId = ip;
body.timestamp = parseInt(new Date().getTime() / 1000);
body.flushToken = flushToken;
return fetch("http://116.198.37.53:8080/rest/v2/token/get_access_token", {
method: "POST",
body: constructParameter(body),
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
},
}).then(response => response.json()).then((json) => {
console.log(json);
return json.result.accessToken;
}).catch(error => {
console.log(error);
});
},
update_access_token: function (ip, accessToken) {
let body = {};
body.subsystemId = 16;
body.clientId = ip;
body.timestamp = parseInt(new Date().getTime() / 1000);
body.accessToken = accessToken;
return fetch("http://116.198.37.53:8080/rest/v2/token/refresh_access_token", {
method: "POST",
body: constructParameter(body),
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
},
}).then(response => response.json()).then((json) => {
console.log(json);
return json.result.accessToken;
}).catch(error => {
console.log(error);
});
},
get_user_info: function (ip, accessToken) {
let body = {};
body.subsystemId = 16;
body.clientId = ip;
body.timestamp = parseInt(new Date().getTime() / 1000);
body.accessToken = accessToken;
return fetch("http://116.198.37.53:8080/rest/v2/user/get_user_info", {
method: "POST",
body: constructParameter(body),
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
},
}).then(response => response.json()).then((json) => {
console.log(json);
return json.result;
}).catch(error => {
console.log(error);
});
},
user_select: function (ip, accessToken) {
let sha256 = require('sha256');
let timestamp = new Date().getTime();
let sig = appKey + timestamp.toString() + appSecret;
sig = sha256(sig).toUpperCase();;
let url = `/api/app/app-voice-recorder/rest/v1/user/select?accessToken=${encodeURIComponent(accessToken)}&phoneUdid=${encodeURIComponent(ip)}`;
console.log("url: ", url)
return fetch(url, {
headers: {
'appKey': appKey,
'timestamp': timestamp,
'signature': sig,
},
}).then(response => {
console.log(response);
response.text()
}).then((json) => {
console.log(json)
return json;
});
},
get_record_list: function (accessToken, passportId) {
let sha256 = require('sha256');
let timestamp = new Date().getTime();
let sig = appKey + timestamp.toString() + appSecret;
sig = sha256(sig).toUpperCase();;
let url = `/api/app/app-voice-recorder/rest/v1/trans/info/list?accessToken=${encodeURIComponent(accessToken)}&passportId=${passportId}`;
console.log("url: ", url)
return fetch(url, {
headers: {
'appKey': appKey,
'timestamp': timestamp,
'signature': sig,
},
}).then(response => {
console.log(response);
response.text()
}).then((json) => {
console.log(json)
return json;
});
},
login: function (ip) {
let md5 = require('md5');
let body = {};
body.subsystemId = 16;
body.clientId = ip;
body.timestamp = parseInt(new Date().getTime() / 1000);
body.account = "13682423271";
body.password = md5("yzs123456");
return fetch("http://116.198.37.53:8080/rest/v2/user/login", {
method: "POST",
body: constructParameter(body),
// mode: "no-cors",
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
"Access-Control-Allow-Origin": "*",
},
}).then(response => response.json()).then((json) => {
console.log("flushToken: ", json.result.flushToken);
return json.result.flushToken;
}).catch(error => {
console.log(error);
});
}
};
export default yzs;

10
src/business/store.js Normal file
View File

@ -0,0 +1,10 @@
import { configureStore } from '@reduxjs/toolkit'
import ipReducer from "./ipSlice.js"
import userReducer from "./userSlice.js"
export default configureStore({
reducer: {
ip: ipReducer,
user: userReducer,
}
})

33
src/business/userSlice.js Normal file
View File

@ -0,0 +1,33 @@
import { createSlice } from '@reduxjs/toolkit'
export const userSlice = createSlice({
name: 'user',
initialState: {
flushToken: "",
accessToken: "",
info: {},
},
reducers: {
setFlushToken: (state, token) => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.flushToken = token;
},
setAccessToken: (state, token) => {
state.accessToken = token;
},
setUserInfo: (state, info) => {
state.info = info;
// state.createTime = info.createTime;
// state.userName = info.userName;
},
}
})
// Action creators are generated for each case reducer function
export const { setFlushToken, setAccessToken, setUserInfo } = userSlice.actions
export const selectFlushToken = (state) => state.user.flushToken
export default userSlice.reducer

View File

@ -0,0 +1,90 @@
import { Container, TextField, InputAdornment, Link, Button, Stack, Typography } from "@mui/material";
import { CheckBox } from '@mui/icons-material';
import React, { useState } from 'react';
import PhoneIphoneIcon from '@mui/icons-material/PhoneIphone';
import LockIcon from '@mui/icons-material/Lock';
export default function () {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleInputChange = (event) => {
const { name, value } = event.target;
if (name === 'username') setUsername(value);
if (name === 'password') setPassword(value);
};
return <Container disableGutters={true}
sx={{
width: 300,
height: 200,
}}
>
<TextField
name="username"
label="请输入手机号码"
variant="outlined"
value={username}
color="primary"
fullWidth
onChange={handleInputChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<PhoneIphoneIcon />
</InputAdornment>
),
}}
/>
<TextField
// sx={{ paddingTop: 4 }}
fullWidth
margin="normal"
name="password"
label="请输入验证码"
type="password"
variant="outlined"
value={password}
onChange={handleInputChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<LockIcon />
</InputAdornment>
),
endAdornment: <InputAdornment position="end">
<Link>发送动态码</Link>
</InputAdornment>
}}
/>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
sx={{
backgroundColor: "#FF595A",
'&:hover': {
backgroundColor: '#FF595A',
},
'&:active': {
backgroundColor: '#FF595A',
},
}}
>
注册/登录
</Button>
<Container>
<Stack direction="row" spacing={1}
sx={{ paddingTop: 2 }}
>
<CheckBox color="primary" />
<Typography>同意 <Link>纽曼隐私协议</Link></Typography>
</Stack>
</Container>
</Container>
}

View File

@ -0,0 +1,74 @@
import React, { useState } from 'react';
import { Container, TextField, Button, InputAdornment } from "@mui/material";
import PhoneIphoneIcon from '@mui/icons-material/PhoneIphone';
import LockIcon from '@mui/icons-material/Lock';
export default function () {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const handleInputChange = (event) => {
const { name, value } = event.target;
if (name === 'username') setUsername(value);
if (name === 'password') setPassword(value);
};
return <Container disableGutters={true}
sx={{
width: 300,
height: 200,
}}
>
<TextField
name="username"
label="请输入手机号码"
variant="outlined"
value={username}
fullWidth
onChange={handleInputChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<PhoneIphoneIcon />
</InputAdornment>
),
}}
/>
<TextField
// sx={{ paddingTop: 4 }}
fullWidth
margin="normal"
name="password"
label="请输入密码"
type="password"
variant="outlined"
value={password}
onChange={handleInputChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<LockIcon />
</InputAdornment>
),
}}
/>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
sx={{
backgroundColor: "#FF595A",
'&:hover': {
backgroundColor: '#FF595A',
},
'&:active': {
backgroundColor: '#FF595A',
},
}}
>
登录
</Button>
</Container>
}

View File

@ -2,15 +2,19 @@ import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import './index.css'; import './index.css';
import App from './App'; import App from './App';
import store from './business/store';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<Provider store={store}>
<BrowserRouter > <BrowserRouter >
<App /> <App />
</BrowserRouter> </BrowserRouter>
</Provider>
</React.StrictMode> </React.StrictMode>
); );

27
src/setupProxy.js Normal file
View File

@ -0,0 +1,27 @@
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(
'/api/app/app-voice-recorder/rest/v1/trans/info/list',
createProxyMiddleware({
target: 'http://ai-api.uat.hivoice.cn',
changeOrigin: true,
logger: console,
onProxyReq: (proxyReq, req, res) => {
proxyReq.setHeader('appKey', 'k5hfiei5eevouvjohkapjaudpk2gakpaxha22fiy');
// console.log("proxyReq", req)
},
})
);
app.use(
'/api/app/app-voice-recorder/rest/v1/user/select',
createProxyMiddleware({
target: 'http://ai-api.uat.hivoice.cn',
changeOrigin: true,
logger: console,
onProxyReq: (proxyReq, req, res) => {
proxyReq.setHeader('appKey', 'k5hfiei5eevouvjohkapjaudpk2gakpaxha22fiy');
},
})
);
};