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

@ -1,43 +1,43 @@
<!DOCTYPE html> <!DOCTYPE html>
<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
name="description" name="description"
content="Web site created using create-react-app" content="Web site created using create-react-app"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
--> -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!-- <!--
Notice the use of %PUBLIC_URL% in the tags above. Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML. Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
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>
<div id="root"></div> <div id="root"></div>
<!-- <!--
This HTML file is a template. This HTML file is a template.
If you open it directly in the browser, you will see an empty page. If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file. You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag. The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`. To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
</body> </body>
</html> </html>

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"
> }}
登录 >
</Button> <TabContext value={value}>
</form> <Box>
<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>
<BrowserRouter > <Provider store={store}>
<App /> <BrowserRouter >
</BrowserRouter> <App />
</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');
},
})
);
};