Compare commits
10 Commits
deee6211b0
...
f45f472e47
Author | SHA1 | Date | |
---|---|---|---|
f45f472e47 | |||
3e5ad994cd | |||
30ee4cd888 | |||
bc2550f64f | |||
807df7f747 | |||
561ce68b70 | |||
f30bf34432 | |||
16bc07d970 | |||
69b07afc78 | |||
8bd230ba0b |
BIN
public/logo.png
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 821 B |
72
src/App.js
@ -1,37 +1,73 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
import { Routes, Route, Navigate, useNavigate } from 'react-router-dom'
|
import { Routes, Route, Navigate, useNavigate } from 'react-router-dom'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
|
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||||
import { setAccessToken, setUserInfo, setSelectInfo } from "./business/userSlice.js"
|
import { setAccessToken, setUserInfo, setSelectInfo } from "./business/userSlice.js"
|
||||||
import { useCookies } from 'react-cookie';
|
import { useCookies } from 'react-cookie';
|
||||||
import LoginPage from './LoginPage';
|
import LoginPage from './LoginPage';
|
||||||
import MainPage from './MainPage';
|
import MainPage from './MainPage';
|
||||||
import yzs from "./business/request.js";
|
import yzs from "./business/request.js";
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
status: {
|
||||||
|
danger: '#e53e3e',
|
||||||
|
},
|
||||||
|
palette: {
|
||||||
|
primary: {
|
||||||
|
main: '#FF595A',
|
||||||
|
darker: '#FF595A',
|
||||||
|
},
|
||||||
|
neutral: {
|
||||||
|
main: '#64748B',
|
||||||
|
contrastText: '#fff',
|
||||||
|
},
|
||||||
|
black: {
|
||||||
|
main: "#222222",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mixins: {
|
||||||
|
drawer: {
|
||||||
|
width: 330,
|
||||||
|
},
|
||||||
|
dense: {
|
||||||
|
toolbar: {
|
||||||
|
height: 48, // 在 @mui/material/Toolbar/Toolbar.js 找到
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [cookies, setCookie, removeCookie] = useCookies(['accessToken']);
|
const [cookies, setCookie, removeCookie] = useCookies(['accessToken']);
|
||||||
|
|
||||||
if (cookies.accessToken) {
|
useEffect(() => {
|
||||||
dispatch(setAccessToken(cookies.accessToken));
|
if (cookies.accessToken) {
|
||||||
yzs.get_user_info(yzs.uniqueDeviceIdentifier(), cookies.accessToken).then(info => {
|
dispatch(setAccessToken(cookies.accessToken));
|
||||||
dispatch(setUserInfo(info));
|
yzs.get_user_info(yzs.uniqueDeviceIdentifier(), cookies.accessToken).then(info => {
|
||||||
yzs.user_select(yzs.uniqueDeviceIdentifier(), cookies.accessToken).then(info => {
|
dispatch(setUserInfo(info));
|
||||||
dispatch(setSelectInfo(info));
|
yzs.user_select(yzs.uniqueDeviceIdentifier(), cookies.accessToken).then(info => {
|
||||||
|
dispatch(setSelectInfo(info));
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
console.log(error)
|
||||||
|
if (error.returnCode === "uc_0106") {
|
||||||
|
removeCookie("accessToken");
|
||||||
|
navigate("/login");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}).catch(error => {
|
}
|
||||||
console.log(error)
|
}, []);
|
||||||
if (error.returnCode === "uc_0106") {
|
|
||||||
removeCookie("accessToken");
|
|
||||||
navigate("/login");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Routes>
|
<ThemeProvider theme={theme}>
|
||||||
<Route exact path="/" element={cookies.accessToken ? <MainPage /> : <Navigate to="/login" />} />
|
<Routes>
|
||||||
<Route exact path="/login" element={<LoginPage />} />
|
<Route exact path="/" element={cookies.accessToken ? <MainPage /> : <Navigate to="/login" />} />
|
||||||
</Routes>
|
<Route exact path="/login" element={<LoginPage />} />
|
||||||
|
</Routes>
|
||||||
|
</ThemeProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import MenuItem from '@mui/material/MenuItem';
|
|||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import logo from './assets/logo.png';
|
import appbar_logo from './assets/appbar_logo.png';
|
||||||
import { Stack, CssBaseline } from '@mui/material';
|
import { Stack, CssBaseline } from '@mui/material';
|
||||||
import { useCookies } from 'react-cookie';
|
import { useCookies } from 'react-cookie';
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@ -40,11 +40,11 @@ export default function () {
|
|||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Container maxWidth={false} >
|
<Container maxWidth={false} >
|
||||||
<Toolbar disableGutters variant="dense">
|
<Toolbar disableGutters variant="dense">
|
||||||
<Stack direction="row" sx={{ flexGrow: 1 }}>
|
<Stack direction="row" sx={{ flexGrow: 1, alignItems: "center" }}>
|
||||||
<img src={logo} style={{
|
<img src={appbar_logo} style={{
|
||||||
width: 28,
|
width: 28,
|
||||||
height: 30,
|
height: 28,
|
||||||
marginRight: 24,
|
marginRight: 10,
|
||||||
}} />
|
}} />
|
||||||
<Typography variant='h6' sx={{ color: "#FFFFFF" }}>纽曼AI语记</Typography>
|
<Typography variant='h6' sx={{ color: "#FFFFFF" }}>纽曼AI语记</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
161
src/LoginPage.js
@ -1,55 +1,70 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import yzs from "./business/request.js";
|
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 { useSelector, useDispatch } from 'react-redux'
|
||||||
import { setFlushToken, setAccessToken, setUserInfo } from "./business/userSlice.js"
|
import {
|
||||||
|
setAccount, setPassword, setVerificationCode,
|
||||||
|
setFlushToken, setAccessToken, setUserInfo, setAgreeAgreement,
|
||||||
|
setSelectInfo,
|
||||||
|
} from "./business/userSlice.js"
|
||||||
import logo from './assets/logo.png';
|
import logo from './assets/logo.png';
|
||||||
import { Container, Tab, Box } from '@mui/material';
|
import { Container, Tab, Box, Snackbar, Alert, Button } from '@mui/material';
|
||||||
import TabPanel from '@mui/lab/TabPanel';
|
import TabPanel from '@mui/lab/TabPanel';
|
||||||
import { TabList } from '@mui/lab';
|
import { TabList } from '@mui/lab';
|
||||||
import TabContext from '@mui/lab/TabContext';
|
import TabContext from '@mui/lab/TabContext';
|
||||||
import DynamicCodeForm from './components/DynamicCodeForm.js';
|
import DynamicCodeForm from './components/DynamicCodeForm.js';
|
||||||
import PasswordForm from './components/PasswordForm.js';
|
import PasswordForm from './components/PasswordForm.js';
|
||||||
import { useCookies } from 'react-cookie';
|
import { useCookies } from 'react-cookie';
|
||||||
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 navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [cookies, setCookie] = useCookies(['accessToken']);
|
const [cookies, setCookie] = useCookies(['accessToken']);
|
||||||
|
const [firstEnter, setFirstEnter] = useState(true);
|
||||||
const [value, setValue] = useState("1");
|
const [value, setValue] = useState("1");
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
const [openTooltip, setOpenTooltip] = useState(false);
|
||||||
const account = useSelector(state => state.user.account)
|
const account = useSelector(state => state.user.account)
|
||||||
const password = useSelector(state => state.user.password)
|
const password = useSelector(state => state.user.password)
|
||||||
const verificationCode = useSelector(state => state.user.verificationCode)
|
const verificationCode = useSelector(state => state.user.verificationCode)
|
||||||
|
const agreeAgreement = useSelector(state => state.user.agreeAgreement)
|
||||||
|
|
||||||
const handleChange = (event, newValue) => {
|
const handleChange = (event, newValue) => {
|
||||||
setValue(newValue);
|
setValue(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTooltipClose = () => {
|
||||||
|
setOpenTooltip(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAgreeChange = (event) => {
|
||||||
|
dispatch(setAgreeAgreement(!agreeAgreement));
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = useSelector(state => state.user.accessToken)
|
const accessToken = useSelector(state => state.user.accessToken)
|
||||||
const flushToken = useSelector(state => state.user.flushToken)
|
const flushToken = useSelector(state => state.user.flushToken)
|
||||||
|
|
||||||
// const debug_test = () => {
|
// const debug_test = () => {
|
||||||
// console.log("accessToken", accessToken, yzs.uniqueDeviceIdentifier());
|
// console.log("accessToken", accessToken, yzs.uniqueDeviceIdentifier());
|
||||||
|
// console.log("userExists", yzs.userExists(yzs.uniqueDeviceIdentifier(), account));
|
||||||
|
// testPromiseLoading(2000, "你好").then(v => {
|
||||||
|
// console.log(v);
|
||||||
|
// })
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
const handleInputChange = (event) => {
|
||||||
|
const { name, value } = event.target;
|
||||||
|
if (name === 'username') {
|
||||||
|
dispatch(setAccount(value));
|
||||||
|
if (firstEnter) setFirstEnter(false);
|
||||||
|
} else if (name === 'password') {
|
||||||
|
dispatch(setPassword(value));
|
||||||
|
} else if (name === 'verification_code') {
|
||||||
|
dispatch(setVerificationCode(value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = (event) => {
|
const handleSubmit = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let result = null;
|
let result = null;
|
||||||
@ -67,57 +82,71 @@ export default function () {
|
|||||||
yzs.get_user_info(yzs.uniqueDeviceIdentifier(), token).then(info => {
|
yzs.get_user_info(yzs.uniqueDeviceIdentifier(), token).then(info => {
|
||||||
dispatch(setUserInfo(info));
|
dispatch(setUserInfo(info));
|
||||||
yzs.user_select(yzs.uniqueDeviceIdentifier(), token).then(info => {
|
yzs.user_select(yzs.uniqueDeviceIdentifier(), token).then(info => {
|
||||||
|
dispatch(setSelectInfo(info));
|
||||||
navigate("/");
|
navigate("/");
|
||||||
})
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
}).catch(error => {
|
||||||
|
console.log(error)
|
||||||
|
setMessage(error);
|
||||||
|
setOpenTooltip(true);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <div className={styles.loginPage}>
|
||||||
<div className={styles.loginPage}>
|
<div className={styles.title}>
|
||||||
<div className={styles.title}>
|
<img className={styles.titleIcon} src={logo} />
|
||||||
<img className={styles.titleIcon} src={logo} />
|
<h1 className={styles.titleText}>纽曼AI语记</h1>
|
||||||
<h1 className={styles.titleText}>纽曼AI语记</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.loginFrame}>
|
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
<Container component="form" className={styles.form} onSubmit={handleSubmit}
|
|
||||||
sx={{
|
|
||||||
width: 360,
|
|
||||||
height: 418,
|
|
||||||
backgroundColor: 'white',
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
boxShadow: "0px 5px 20px 0px rgba(146,0,1,0.1)",
|
|
||||||
borderRadius: 4,
|
|
||||||
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TabContext value={value}>
|
|
||||||
<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 udid={yzs.uniqueDeviceIdentifier()} />
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel value="2" >
|
|
||||||
<PasswordForm />
|
|
||||||
</TabPanel>
|
|
||||||
</TabContext>
|
|
||||||
</Container>
|
|
||||||
</ThemeProvider >
|
|
||||||
{/* <Button variant="contained" onClick={debug_test}>测试</Button> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className={styles.loginFrame}>
|
||||||
|
<Container component="form" className={styles.form} onSubmit={handleSubmit}
|
||||||
|
sx={{
|
||||||
|
width: 360,
|
||||||
|
height: 418,
|
||||||
|
backgroundColor: 'white',
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
boxShadow: "0px 5px 20px 0px rgba(146,0,1,0.1)",
|
||||||
|
borderRadius: 4,
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabContext value={value}>
|
||||||
|
<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 udid={yzs.uniqueDeviceIdentifier()}
|
||||||
|
firstEnter={firstEnter}
|
||||||
|
agreeAgreement={agreeAgreement} onAgreeChange={onAgreeChange}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value="2" >
|
||||||
|
<PasswordForm firstEnter={firstEnter}
|
||||||
|
agreeAgreement={agreeAgreement} onAgreeChange={onAgreeChange}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</TabContext>
|
||||||
|
</Container>
|
||||||
|
{/* <Button variant="contained" onClick={debug_test}>测试</Button> */}
|
||||||
|
</div>
|
||||||
|
<Snackbar
|
||||||
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||||
|
open={openTooltip}
|
||||||
|
autoHideDuration={3500}
|
||||||
|
onClose={handleTooltipClose}
|
||||||
|
>
|
||||||
|
<Alert severity="error">{message}</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</div>
|
||||||
}
|
}
|
154
src/MainPage.js
@ -1,50 +1,26 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { React, useEffect, useState } from 'react';
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
import AppBar from './AppBar';
|
import AppBar from './AppBar';
|
||||||
import RecordList from './RecordList';
|
import RecordList from './components/RecordList';
|
||||||
import PlayerBar from './PlayerBar';
|
import PlayerBar from './PlayerBar';
|
||||||
import store from './business/store';
|
|
||||||
import yzs from "./business/request.js";
|
import yzs from "./business/request.js";
|
||||||
import { setList, setCurrentLyric, setCurrentBlob, setCurrentWaveData } from "./business/recorderSlice.js"
|
import { setList, fetchRecord } from "./business/recorderSlice.js"
|
||||||
import { CssBaseline, Box } from '@mui/material';
|
import Backdrop from '@mui/material/Backdrop';
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import RecordLyrics from './RecordLyrics';
|
import RecordLyrics from './RecordLyrics';
|
||||||
import { createTheme, ThemeProvider, styled } from '@mui/material/styles';
|
import { styled, useTheme } from '@mui/material/styles';
|
||||||
import expand from './assets/expand.png';
|
import expand from './assets/expand.png';
|
||||||
import close from './assets/close.png';
|
import close from './assets/close.png';
|
||||||
|
import empty_hint from './assets/empty_hint.png';
|
||||||
|
|
||||||
const drawerWidth = 240;
|
const lyricsBrowserStyle = {
|
||||||
|
marginTop: 16,
|
||||||
const theme = createTheme({
|
paddingBottom: 40,
|
||||||
status: {
|
padding: 24,
|
||||||
danger: '#e53e3e',
|
|
||||||
},
|
|
||||||
palette: {
|
|
||||||
primary: {
|
|
||||||
main: '#FF595A',
|
|
||||||
darker: '#FF595A',
|
|
||||||
},
|
|
||||||
neutral: {
|
|
||||||
main: '#64748B',
|
|
||||||
contrastText: '#fff',
|
|
||||||
},
|
|
||||||
black: {
|
|
||||||
main: "#222222",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
function fetchRecord(accessToken, record) {
|
|
||||||
yzs.download(accessToken, record.transResultUrl).then(
|
|
||||||
blob => blob.text()
|
|
||||||
).then(text => {
|
|
||||||
// console.log("type", record.type, text);
|
|
||||||
let payload = record.type === 1 ? JSON.parse(text) : text;
|
|
||||||
store.dispatch(setCurrentLyric(payload));
|
|
||||||
});
|
|
||||||
yzs.download(accessToken, record.audioUrl).then(blob => {
|
|
||||||
store.dispatch(setCurrentBlob(URL.createObjectURL(blob)));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClickHanlde = styled('div', { shouldForwardProp: (prop) => prop !== 'open' })(
|
const ClickHanlde = styled('div', { shouldForwardProp: (prop) => prop !== 'open' })(
|
||||||
@ -65,7 +41,7 @@ const ClickHanlde = styled('div', { shouldForwardProp: (prop) => prop !== 'open'
|
|||||||
easing: theme.transitions.easing.easeOut,
|
easing: theme.transitions.easing.easeOut,
|
||||||
duration: theme.transitions.duration.enteringScreen,
|
duration: theme.transitions.duration.enteringScreen,
|
||||||
}),
|
}),
|
||||||
left: drawerWidth - 18,
|
left: theme.mixins.drawer.width - 18,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -73,13 +49,18 @@ const ClickHanlde = styled('div', { shouldForwardProp: (prop) => prop !== 'open'
|
|||||||
|
|
||||||
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(
|
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(
|
||||||
({ theme, open }) => ({
|
({ theme, open }) => ({
|
||||||
|
backgroundColor: "#FAFAFA",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: theme.spacing(3),
|
paddingTop: 0,
|
||||||
|
paddingLeft: theme.spacing(3),
|
||||||
|
paddingRight: theme.spacing(3),
|
||||||
|
paddingBottom: theme.spacing(3),
|
||||||
|
marginTop: theme.mixins.dense.toolbar.height,
|
||||||
transition: theme.transitions.create('margin', {
|
transition: theme.transitions.create('margin', {
|
||||||
easing: theme.transitions.easing.sharp,
|
easing: theme.transitions.easing.sharp,
|
||||||
duration: theme.transitions.duration.leavingScreen,
|
duration: theme.transitions.duration.leavingScreen,
|
||||||
}),
|
}),
|
||||||
marginLeft: `-${drawerWidth}px`,
|
marginLeft: `-${theme.mixins.drawer.width}px`,
|
||||||
...(open && {
|
...(open && {
|
||||||
transition: theme.transitions.create('margin', {
|
transition: theme.transitions.create('margin', {
|
||||||
easing: theme.transitions.easing.easeOut,
|
easing: theme.transitions.easing.easeOut,
|
||||||
@ -90,34 +71,83 @@ const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const RecordPlayer = ({ loading, empty, playerBarWidth, currentTime, hasLyric, currentLyric }) => {
|
||||||
|
if (loading) {
|
||||||
|
return <Backdrop
|
||||||
|
sx={{
|
||||||
|
color: '#fff',
|
||||||
|
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||||
|
// marginLeft: "240px",
|
||||||
|
marginTop: "45px",
|
||||||
|
}}
|
||||||
|
open >
|
||||||
|
<CircularProgress color="inherit" />
|
||||||
|
</Backdrop>
|
||||||
|
} else {
|
||||||
|
if (empty) {
|
||||||
|
return <Paper sx={{
|
||||||
|
height: (theme) => `calc(100vh - 48px - 24px - 24px)`,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginTop: (theme) => theme.spacing(3)
|
||||||
|
}} >
|
||||||
|
<img style={{ maxWidth: "100%", marginBottom: 40, }} src={empty_hint} />
|
||||||
|
<Typography align='center' color="#929292">这里像我看不到你时的心情一样空空荡荡</Typography>
|
||||||
|
</Paper>
|
||||||
|
} else {
|
||||||
|
return <div>
|
||||||
|
<PlayerBar width={playerBarWidth} currentTime={currentTime} lyric={currentLyric} />
|
||||||
|
{hasLyric ? <RecordLyrics style={lyricsBrowserStyle} currentLyric={currentLyric} currentTime={currentTime} /> :
|
||||||
|
<div style={lyricsBrowserStyle}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
|
const theme = useTheme();
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const accessToken = useSelector(state => state.user.accessToken);
|
const accessToken = useSelector(state => state.user.accessToken);
|
||||||
const passportId = useSelector(state => state.user.passportId);
|
const passportId = useSelector(state => state.user.passportId);
|
||||||
const currentTime = useSelector(state => state.recorder.currentTime);
|
const currentTime = useSelector(state => state.recorder.currentTime);
|
||||||
const currentLyric = useSelector(state => state.recorder.currentLyric);
|
const currentLyric = useSelector(state => state.recorder.currentLyric);
|
||||||
|
const currentIndex = useSelector(state => state.recorder.currentIndex);
|
||||||
|
const recordList = useSelector(state => state.recorder.list);
|
||||||
|
const loading = useSelector(state => state.recorder.loading);
|
||||||
const [playerBarWidth, setPlayerBarWidth] = useState(0);
|
const [playerBarWidth, setPlayerBarWidth] = useState(0);
|
||||||
const [open, setOpen] = useState(true);
|
const [open, setOpen] = useState(true);
|
||||||
|
const [hasLyric, setHasLyric] = useState(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (passportId <= 0) return;
|
if (passportId <= 0) return;
|
||||||
yzs.get_record_list(accessToken, passportId).then(list => {
|
yzs.get_record_list(accessToken, passportId).then(list => {
|
||||||
dispatch(setList(list.result));
|
dispatch(setList(list));
|
||||||
if (list.result.length > 0) {
|
if (list.length > 0) {
|
||||||
fetchRecord(accessToken, list.result.at(0));
|
dispatch(fetchRecord(accessToken, 0, list.at(0)));
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.log("get list failed", error);
|
console.log("get list failed", error);
|
||||||
});
|
});
|
||||||
}, [accessToken, passportId]);
|
}, [passportId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (recordList.length <= 0) {
|
||||||
|
setHasLyric(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setHasLyric((recordList.at(currentIndex).transResultUrl));
|
||||||
|
}, [currentIndex, currentLyric]);
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
setOpen(!open);
|
setOpen(!open);
|
||||||
setPlayerBarWidth(document.documentElement.clientWidth - 240 - 48); // 防止中途底部出现scrollbar
|
setPlayerBarWidth(document.documentElement.clientWidth - theme.mixins.drawer.width - 48); // 防止中途底部出现scrollbar
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
console.log("innerWidth", document.documentElement.clientWidth, document.documentElement.clientWidth - (open ? 240 : 0) - 48)
|
// let scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
|
||||||
setPlayerBarWidth(document.documentElement.clientWidth - (open ? 240 : 0) - 48);
|
setPlayerBarWidth(document.documentElement.clientWidth - (open ? theme.mixins.drawer.width : 0) - 48);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTransitionEnd = () => {
|
const onTransitionEnd = () => {
|
||||||
@ -132,20 +162,20 @@ export default function () {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => { handleResize(); }, [currentLyric]);
|
useEffect(() => {
|
||||||
|
if (!loading) handleResize();
|
||||||
|
}, [loading]);
|
||||||
|
|
||||||
return <Box sx={{ display: 'flex' }}>
|
return <Box sx={{ display: 'flex' }}>
|
||||||
<ThemeProvider theme={theme}>
|
<CssBaseline />
|
||||||
<CssBaseline />
|
<AppBar />
|
||||||
<AppBar />
|
<RecordList open={open} recordList={recordList} currentIndex={currentIndex} />
|
||||||
<RecordList open={open} fetchRecord={fetchRecord} />
|
<ClickHanlde open={open} onClick={onClick} />
|
||||||
<ClickHanlde open={open} onClick={onClick} />
|
<Main open={open}
|
||||||
<Main open={open}
|
onTransitionEnd={onTransitionEnd}
|
||||||
onTransitionEnd={onTransitionEnd}
|
>
|
||||||
>
|
<RecordPlayer loading={loading} empty={recordList.length <= 0}
|
||||||
<PlayerBar width={playerBarWidth} currentTime={currentTime} />
|
playerBarWidth={playerBarWidth} currentTime={currentTime} hasLyric={hasLyric} currentLyric={currentLyric} />
|
||||||
<RecordLyrics currentLyric={currentLyric} currentTime={currentTime} />
|
</Main>
|
||||||
</Main>
|
|
||||||
</ThemeProvider>
|
|
||||||
</Box >
|
</Box >
|
||||||
}
|
}
|
@ -4,8 +4,8 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import pauseIcon from "./assets/play.png";
|
import pauseIcon from "./assets/play.png";
|
||||||
import playIcon from "./assets/pause.png";
|
import playIcon from "./assets/pause.png";
|
||||||
import downloadIcon from "./assets/download.png";
|
import downloadIcon from "./assets/download.png";
|
||||||
import { setCurrentTime, togglePauseState, setCurrentWaveData } from "./business/recorderSlice.js"
|
import { setCurrentTime, setPauseState, togglePauseState, setCurrentWaveData } from "./business/recorderSlice.js"
|
||||||
import { audioWaveData } from "./business/utilities"
|
import { audioWaveData, sampleInterval, exportRecordLyric } from "./business/utilities"
|
||||||
import ProgressBar from "./components/ProgressBar";
|
import ProgressBar from "./components/ProgressBar";
|
||||||
|
|
||||||
const durationFormat = (time) => {
|
const durationFormat = (time) => {
|
||||||
@ -17,9 +17,10 @@ const durationFormat = (time) => {
|
|||||||
return hour.toString().padStart(2, '0') + ":" + minute.toString().padStart(2, '0') + ":" + second.toString().padStart(2, '0');
|
return hour.toString().padStart(2, '0') + ":" + minute.toString().padStart(2, '0') + ":" + second.toString().padStart(2, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ({ width, currentTime }) {
|
export default function ({ width, lyric, currentTime }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [duration, setDuration] = useState(0); // 秒,有小数点
|
const [duration, setDuration] = useState(0); // 秒,有小数点
|
||||||
|
const [playbackRate, setPlaybackRate] = useState(1.0);
|
||||||
const currentIndex = useSelector(state => state.recorder.currentIndex);
|
const currentIndex = useSelector(state => state.recorder.currentIndex);
|
||||||
const recordList = useSelector(state => state.recorder.list);
|
const recordList = useSelector(state => state.recorder.list);
|
||||||
const currentBlob = useSelector(state => state.recorder.currentBlob);
|
const currentBlob = useSelector(state => state.recorder.currentBlob);
|
||||||
@ -28,11 +29,14 @@ export default function ({ width, currentTime }) {
|
|||||||
const player = useRef(null);
|
const player = useRef(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
player.current.url = currentBlob
|
player.current.url = currentBlob
|
||||||
|
setPlaybackRate(1.0); // 恢复默认
|
||||||
|
dispatch(setCurrentTime(0));
|
||||||
console.log(player.current.url);
|
console.log(player.current.url);
|
||||||
}, [currentBlob]);
|
}, [currentBlob]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
audioWaveData(currentBlob, (duration > 20 * 60) ? 200 : 100)
|
if (currentBlob.length <= 0) return;
|
||||||
|
audioWaveData(currentBlob, sampleInterval(duration))
|
||||||
.then(data => dispatch(setCurrentWaveData(data)));
|
.then(data => dispatch(setCurrentWaveData(data)));
|
||||||
}, [duration]);
|
}, [duration]);
|
||||||
|
|
||||||
@ -51,6 +55,7 @@ export default function ({ width, currentTime }) {
|
|||||||
link.href = currentBlob;
|
link.href = currentBlob;
|
||||||
link.download = recordList.at(currentIndex).name;
|
link.download = recordList.at(currentIndex).name;
|
||||||
link.click();
|
link.click();
|
||||||
|
exportRecordLyric(recordList.at(currentIndex).type, lyric, recordList.at(currentIndex).editName + ".txt");
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDurationChange = (event) => {
|
const onDurationChange = (event) => {
|
||||||
@ -65,20 +70,26 @@ export default function ({ width, currentTime }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onChange = (event) => {
|
const onChange = (event) => {
|
||||||
|
setPlaybackRate(event.target.value);
|
||||||
player.current.playbackRate = event.target.value;
|
player.current.playbackRate = event.target.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onEnded = (event) => {
|
||||||
|
dispatch(setPauseState(true));
|
||||||
|
};
|
||||||
|
|
||||||
return <Stack sx={{
|
return <Stack sx={{
|
||||||
position: "sticky",
|
position: "sticky",
|
||||||
top: 48,
|
top: (theme) => theme.mixins.dense.toolbar.height,
|
||||||
backgroundColor: "#FAFAFA",
|
backgroundColor: "#FAFAFA",
|
||||||
}} >
|
}} >
|
||||||
<Container disableGutters maxWidth={false} sx={{
|
<Container disableGutters maxWidth={false} sx={{
|
||||||
height: 60,
|
height: 60,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
backgroundColor: "#FAFAFA",
|
||||||
}}>
|
}}>
|
||||||
<Typography variant="h6" sx={{ flexGrow: 1 }} >{recordList.length > 0 ? recordList.at(currentIndex).editName : ""}</Typography>
|
<Typography variant="h6" sx={{ flexGrow: 1 }} >{recordList.length > 0 ? recordList.at(currentIndex).editName : "暂无内容"}</Typography>
|
||||||
<IconButton onClick={onDownload}>
|
<IconButton onClick={onDownload}>
|
||||||
<img src={downloadIcon} />
|
<img src={downloadIcon} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -94,13 +105,15 @@ export default function ({ width, currentTime }) {
|
|||||||
<img src={pause ? pauseIcon : playIcon} />
|
<img src={pause ? pauseIcon : playIcon} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<audio ref={player} src={currentBlob} onDurationChange={onDurationChange} onTimeUpdate={onTimeUpdate} />
|
<audio ref={player} src={currentBlob} onDurationChange={onDurationChange}
|
||||||
|
onTimeUpdate={onTimeUpdate}
|
||||||
|
onEnded={onEnded} />
|
||||||
<ProgressBar width={isNaN(width) ? 0 : (width - 70 - 90)} duration={Math.ceil(duration * 1000)}
|
<ProgressBar width={isNaN(width) ? 0 : (width - 70 - 90)} duration={Math.ceil(duration * 1000)}
|
||||||
currentTime={currentTime} playing={!pause} seek={seekRecord}
|
currentTime={currentTime} playing={!pause} seek={seekRecord}
|
||||||
waveData={currentWaveData}
|
waveData={currentWaveData}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
defaultValue={1.0}
|
value={playbackRate}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
sx={{ width: 90, height: 70 }}
|
sx={{ width: 90, height: 70 }}
|
||||||
>
|
>
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
|
||||||
import Box from '@mui/material/Box';
|
|
||||||
import Drawer from '@mui/material/Drawer';
|
|
||||||
import List from '@mui/material/List';
|
|
||||||
import ListItem from '@mui/material/ListItem';
|
|
||||||
import ListItemButton from '@mui/material/ListItemButton';
|
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
|
||||||
import { setCurrentIndex } from "./business/recorderSlice.js"
|
|
||||||
import AccessTimeFilledIcon from '@mui/icons-material/AccessTimeFilled';
|
|
||||||
|
|
||||||
const drawerWidth = 240;
|
|
||||||
|
|
||||||
export default function ({ open, fetchRecord }) {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const accessToken = useSelector(state => state.user.accessToken);
|
|
||||||
const currentIndex = useSelector(state => state.recorder.currentIndex);
|
|
||||||
const recordList = useSelector(state => state.recorder.list);
|
|
||||||
const onSelected = (event, index) => {
|
|
||||||
console.log("onSelected", index, recordList.at(index).transResultUrl)
|
|
||||||
dispatch(setCurrentIndex(index));
|
|
||||||
fetchRecord(accessToken, recordList.at(index));
|
|
||||||
}
|
|
||||||
return <Drawer
|
|
||||||
variant="persistent"
|
|
||||||
anchor="left"
|
|
||||||
open={open}
|
|
||||||
sx={{
|
|
||||||
width: drawerWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
'& .MuiDrawer-paper': {
|
|
||||||
width: drawerWidth,
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Toolbar variant="dense" />
|
|
||||||
<Box sx={{ overflow: 'auto' }}>
|
|
||||||
<List>
|
|
||||||
{recordList === undefined ? <React.Fragment /> : recordList.map((item, index) => (
|
|
||||||
<ListItem key={index} disablePadding>
|
|
||||||
<ListItemButton selected={currentIndex === index} onClick={(event) => onSelected(event, index)}>
|
|
||||||
<ListItemText primary={item.editName} secondary={
|
|
||||||
<React.Fragment>
|
|
||||||
<Typography component="span" variant="body1">{item.content.slice(0, 50) + '......'}</Typography>
|
|
||||||
<br />
|
|
||||||
<AccessTimeFilledIcon sx={{ fontSize: 12 }} />
|
|
||||||
<Typography component="span" variant="body2"> 更新于 {new Date(item.createTime).toLocaleString()}</Typography>
|
|
||||||
</React.Fragment>
|
|
||||||
} />
|
|
||||||
</ListItemButton>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Box>
|
|
||||||
</Drawer>
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import { Typography, Paper } from "@mui/material";
|
import { Typography, Paper } from "@mui/material";
|
||||||
import styles from './RecordLyrics.module.css';
|
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
|
||||||
function isHighlight(currentTime, { start, end }) {
|
function isHighlight(currentTime, { start, end }) {
|
||||||
@ -8,19 +7,59 @@ function isHighlight(currentTime, { start, end }) {
|
|||||||
return (currentTime > start) && (currentTime <= end);
|
return (currentTime > start) && (currentTime <= end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type: 0 --> 声文速记 纯文本,已适配
|
||||||
|
// type: 1 --> 导入音频
|
||||||
|
// type: 2 --> 同传翻译 纯文本,已适配
|
||||||
|
// type: 3 --> 双语对话
|
||||||
|
|
||||||
|
const PlainText = ({ lyrics }) => {
|
||||||
|
if (typeof lyrics !== "string") return <React.Fragment />;
|
||||||
|
return <div style={{ whiteSpace: "pre-wrap" }}>{lyrics}</div>
|
||||||
|
}
|
||||||
|
|
||||||
export default function ({ currentLyric, currentTime }) {
|
const ImportAudio = ({ lyrics, currentTime }) => { // 导入音频
|
||||||
|
if (typeof lyrics !== "object") return <React.Fragment />;
|
||||||
|
const onClick = (index) => {
|
||||||
|
console.log("onClick", index);
|
||||||
|
}
|
||||||
|
return <div>{lyrics.map((lyric, index) => {
|
||||||
|
return <div style={{ paddingBottom: 10, display: "inline-block" }} onDoubleClick={() => onClick(index)}>
|
||||||
|
<Typography align="left" color={isHighlight(currentTime, lyric) ? "red" : "black"}>{lyric.text}</Typography>
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
||||||
|
const BilingualDialogue = ({ lyrics }) => { // 双语对话
|
||||||
|
if (typeof lyrics !== "object") return <React.Fragment />;
|
||||||
|
return <div> {lyrics.map((lyric, index) => {
|
||||||
|
return <div index={index} style={{ paddingBottom: 40 }}>
|
||||||
|
<Typography align="left" >{lyric.asr}</Typography>
|
||||||
|
<Typography align="left" >{lyric.translate}</Typography>
|
||||||
|
</div>
|
||||||
|
})}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const LyricsContent = ({ type, lyrics, currentTime }) => {
|
||||||
|
if (type === 0 || type === 2) {
|
||||||
|
return <PlainText lyrics={lyrics} />
|
||||||
|
} else if (type === 1) {
|
||||||
|
return <ImportAudio lyrics={lyrics} currentTime={currentTime} />
|
||||||
|
} else if (type === 3) {
|
||||||
|
return <BilingualDialogue lyrics={lyrics} />
|
||||||
|
} else {
|
||||||
|
return <React.Fragment />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ({ style, currentLyric, currentTime }) {
|
||||||
const currentIndex = useSelector(state => state.recorder.currentIndex);
|
const currentIndex = useSelector(state => state.recorder.currentIndex);
|
||||||
const recordList = useSelector(state => state.recorder.list);
|
const recordList = useSelector(state => state.recorder.list);
|
||||||
|
const currentType = useMemo(() => {
|
||||||
if (recordList.length === 0) return <React.Fragment />;
|
if (recordList.length <= 0) return -1;
|
||||||
|
return recordList.at(currentIndex).type;
|
||||||
return <Paper className={styles.lyricsBrowser}>
|
}, [currentLyric]);
|
||||||
{recordList.at(currentIndex).type === 1 ? (typeof currentLyric === "object" ? currentLyric.map((lyric, index) => {
|
return <Paper style={style}>
|
||||||
return <div className={styles.lyricItem}>
|
<LyricsContent type={currentType} lyrics={currentLyric} currentTime={currentTime} />
|
||||||
<Typography align="left" color={isHighlight(currentTime, lyric) ? "red" : "black"}>{lyric.text}</Typography>
|
|
||||||
</div>
|
|
||||||
}) : <React.Fragment />) : <div style={{ whiteSpace: "pre-wrap" }}>{typeof currentLyric === "string" ? currentLyric : ""}</div>}
|
|
||||||
</Paper>
|
</Paper>
|
||||||
}
|
}
|
@ -1,9 +0,0 @@
|
|||||||
.lyricsBrowser {
|
|
||||||
margin-top: 16px;
|
|
||||||
padding-bottom: 40px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lyricItem {
|
|
||||||
padding-bottom: 40px;
|
|
||||||
}
|
|
BIN
src/assets/appbar_logo.png
Normal file
After Width: | Height: | Size: 821 B |
BIN
src/assets/empty_hint.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/empty_list.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 12 KiB |
@ -1,4 +1,10 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit'
|
import { createSlice } from '@reduxjs/toolkit'
|
||||||
|
import yzs from "./request.js";
|
||||||
|
|
||||||
|
// type: 0 --> 声文速记 纯文本,已适配
|
||||||
|
// type: 1 --> 导入音频
|
||||||
|
// type: 2 --> 同传翻译 纯文本,已适配
|
||||||
|
// type: 3 --> 双语对话
|
||||||
|
|
||||||
export const recorderSlice = createSlice({
|
export const recorderSlice = createSlice({
|
||||||
name: 'recorder',
|
name: 'recorder',
|
||||||
@ -10,6 +16,7 @@ export const recorderSlice = createSlice({
|
|||||||
currentWaveData: [],
|
currentWaveData: [],
|
||||||
currentTime: 0, // 当前音频播放时间
|
currentTime: 0, // 当前音频播放时间
|
||||||
pause: true,
|
pause: true,
|
||||||
|
loading: false,
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
setList: (state, action) => {
|
setList: (state, action) => {
|
||||||
@ -34,10 +41,57 @@ export const recorderSlice = createSlice({
|
|||||||
togglePauseState: (state) => {
|
togglePauseState: (state) => {
|
||||||
state.pause = !state.pause;
|
state.pause = !state.pause;
|
||||||
},
|
},
|
||||||
|
setPauseState: (state, action) => {
|
||||||
|
state.pause = action.payload;
|
||||||
|
},
|
||||||
|
setLoading: (state, action) => {
|
||||||
|
state.loading = true;
|
||||||
|
},
|
||||||
|
setLoadFinished: (state, action) => {
|
||||||
|
state.loading = false;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Action creators are generated for each case reducer function
|
// Action creators are generated for each case reducer function
|
||||||
export const { setCurrentIndex, setList, setCurrentLyric, setCurrentBlob, togglePauseState, setCurrentTime, setCurrentWaveData } = recorderSlice.actions
|
export const {
|
||||||
|
setCurrentIndex, setList, setCurrentLyric, setCurrentBlob, togglePauseState, setPauseState, setCurrentTime,
|
||||||
|
setCurrentWaveData,
|
||||||
|
setLoading,
|
||||||
|
setLoadFinished,
|
||||||
|
} = recorderSlice.actions
|
||||||
|
|
||||||
export default recorderSlice.reducer
|
export default recorderSlice.reducer
|
||||||
|
|
||||||
|
const fetchRecord = (accessToken, index, record) => {
|
||||||
|
return (dispatch) => {
|
||||||
|
dispatch(setLoading());
|
||||||
|
let promises = [];
|
||||||
|
if (record.transResultUrl) {
|
||||||
|
let promise1 = yzs.download(accessToken, record.transResultUrl).then(
|
||||||
|
blob => blob.text()
|
||||||
|
).then(text => {
|
||||||
|
// console.log("type", record.type, text);
|
||||||
|
let payload = null;
|
||||||
|
if (record.type === 1 || record.type === 3) {
|
||||||
|
payload = JSON.parse(text)
|
||||||
|
} else {
|
||||||
|
payload = text;
|
||||||
|
}
|
||||||
|
dispatch(setCurrentLyric(payload));
|
||||||
|
});
|
||||||
|
promises.push(promise1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let promise2 = yzs.download(accessToken, record.audioUrl).then(blob => {
|
||||||
|
dispatch(setCurrentBlob(URL.createObjectURL(blob)));
|
||||||
|
});
|
||||||
|
|
||||||
|
promises.push(promise2);
|
||||||
|
Promise.all(promises).then(() => {
|
||||||
|
dispatch(setLoadFinished());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { fetchRecord };
|
@ -15,6 +15,7 @@ const appSecret = "c5eccccfec16d46fe9ac678d69198415";
|
|||||||
function constructParameter(body) {
|
function constructParameter(body) {
|
||||||
let params = [];
|
let params = [];
|
||||||
for (let key in body) {
|
for (let key in body) {
|
||||||
|
if (key === "smsTemplateId") continue;
|
||||||
params.push(body[key].toString());
|
params.push(body[key].toString());
|
||||||
}
|
}
|
||||||
params.sort();
|
params.sort();
|
||||||
@ -139,8 +140,11 @@ const yzs = {
|
|||||||
if (json.errorCode !== "0") {
|
if (json.errorCode !== "0") {
|
||||||
throw json;
|
throw json;
|
||||||
}
|
}
|
||||||
console.log(json)
|
let list = json.result;
|
||||||
return json;
|
list.sort((lfs, rhs) => { // 要求倒序排序
|
||||||
|
return rhs.modifyTime - lfs.modifyTime;
|
||||||
|
})
|
||||||
|
return list;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
download: function (accessToken, url) {
|
download: function (accessToken, url) {
|
||||||
@ -183,10 +187,11 @@ const yzs = {
|
|||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||||
},
|
},
|
||||||
}).then(response => response.json()).then((json) => {
|
}).then(response => response.json()).then((json) => {
|
||||||
console.log("flushToken: ", json.result.flushToken);
|
if (json.returnCode != "uc_0000") {
|
||||||
|
throw json.message;
|
||||||
|
}
|
||||||
|
// console.log("flushToken: ", json.result.flushToken);
|
||||||
return json.result.flushToken;
|
return json.result.flushToken;
|
||||||
}).catch(error => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
dynamic_code_login: function (udid, userCell, phoneCode) {
|
dynamic_code_login: function (udid, userCell, phoneCode) {
|
||||||
@ -196,6 +201,7 @@ const yzs = {
|
|||||||
body.timestamp = Math.round(new Date().getTime() / 1000);
|
body.timestamp = Math.round(new Date().getTime() / 1000);
|
||||||
body.userCell = userCell;
|
body.userCell = userCell;
|
||||||
body.phoneCode = phoneCode;
|
body.phoneCode = phoneCode;
|
||||||
|
body.smsTemplateId = 316; // 纽曼短信模板
|
||||||
return fetch("/rest/v2/phone/login", {
|
return fetch("/rest/v2/phone/login", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: constructParameter(body),
|
body: constructParameter(body),
|
||||||
@ -205,10 +211,11 @@ const yzs = {
|
|||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
},
|
},
|
||||||
}).then(response => response.json()).then((json) => {
|
}).then(response => response.json()).then((json) => {
|
||||||
console.log("flushToken: ", json.result.flushToken);
|
if (json.returnCode != "uc_0000") {
|
||||||
|
throw json.message;
|
||||||
|
}
|
||||||
|
// console.log("flushToken: ", json.result.flushToken);
|
||||||
return json.result.flushToken;
|
return json.result.flushToken;
|
||||||
}).catch(error => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
send_phone_code: function (udid, userCell) {
|
send_phone_code: function (udid, userCell) {
|
||||||
@ -217,6 +224,7 @@ const yzs = {
|
|||||||
body.clientId = udid;
|
body.clientId = udid;
|
||||||
body.timestamp = Math.round(new Date().getTime() / 1000);
|
body.timestamp = Math.round(new Date().getTime() / 1000);
|
||||||
body.userCell = userCell;
|
body.userCell = userCell;
|
||||||
|
body.smsTemplateId = 316;
|
||||||
return fetch("/rest/v2/phone/send_phone_code", {
|
return fetch("/rest/v2/phone/send_phone_code", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: constructParameter(body),
|
body: constructParameter(body),
|
||||||
@ -232,6 +240,31 @@ const yzs = {
|
|||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
userExists: function (udid, account) {
|
||||||
|
let body = {};
|
||||||
|
body.subsystemId = 16;
|
||||||
|
body.clientId = udid;
|
||||||
|
body.timestamp = Math.round(new Date().getTime() / 1000);
|
||||||
|
body.account = account;
|
||||||
|
return fetch("/rest/v2/user/is_user_exist", {
|
||||||
|
method: "POST",
|
||||||
|
body: constructParameter(body),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||||
|
},
|
||||||
|
}).then(response => response.json()).then((json) => {
|
||||||
|
console.log("userExists: ", json);
|
||||||
|
if (json.returnCode === "uc_0206") { // 用户已存在
|
||||||
|
return true;
|
||||||
|
} else if (json.returnCode === "uc_0205") {// 用户不存在
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw json;
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
uniqueDeviceIdentifier: function () {
|
uniqueDeviceIdentifier: function () {
|
||||||
let udid = localStorage.getItem('uniqueDeviceIdentifier');
|
let udid = localStorage.getItem('uniqueDeviceIdentifier');
|
||||||
if (!udid) {
|
if (!udid) {
|
||||||
|
@ -10,9 +10,9 @@ export const userSlice = createSlice({
|
|||||||
userName: "",
|
userName: "",
|
||||||
nickName: "",
|
nickName: "",
|
||||||
avatarUrl: "",
|
avatarUrl: "",
|
||||||
agreeAgreement: true,
|
agreeAgreement: false,
|
||||||
account: "13682423271",
|
account: "",
|
||||||
password: "yzs123456",
|
password: "",
|
||||||
verificationCode: "",
|
verificationCode: "",
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
@ -40,10 +40,13 @@ export const userSlice = createSlice({
|
|||||||
setVerificationCode: (state, action) => {
|
setVerificationCode: (state, action) => {
|
||||||
state.verificationCode = action.payload;
|
state.verificationCode = action.payload;
|
||||||
},
|
},
|
||||||
|
setAgreeAgreement: (state, action) => {
|
||||||
|
state.agreeAgreement = action.payload;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Action creators are generated for each case reducer function
|
// Action creators are generated for each case reducer function
|
||||||
export const { setFlushToken, setAccessToken, setUserInfo, setSelectInfo, setAccount, setPassword, setVerificationCode } = userSlice.actions
|
export const { setFlushToken, setAccessToken, setUserInfo, setSelectInfo, setAccount, setPassword, setVerificationCode, setAgreeAgreement } = userSlice.actions
|
||||||
|
|
||||||
export default userSlice.reducer
|
export default userSlice.reducer
|
@ -1,3 +1,22 @@
|
|||||||
|
// 间隔多长时间取一个采样点
|
||||||
|
// duration 秒,有小数点
|
||||||
|
const sampleInterval = (duration) => {
|
||||||
|
let interval = 100;
|
||||||
|
let isFirefox = window.navigator.userAgent.includes("Firefox");
|
||||||
|
if (isFirefox) {// firefox canvas width 不能过长
|
||||||
|
if (duration > 20 * 60) {
|
||||||
|
interval = 400;
|
||||||
|
} else if (duration > 10 * 60) {
|
||||||
|
interval = 200;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (duration > 20 * 60) {
|
||||||
|
interval = 200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return interval;
|
||||||
|
}
|
||||||
|
|
||||||
// interval 间隔ms采点
|
// interval 间隔ms采点
|
||||||
function audioWaveData(url, interval) {
|
function audioWaveData(url, interval) {
|
||||||
if (url.length <= 0) return;
|
if (url.length <= 0) return;
|
||||||
@ -24,4 +43,47 @@ function audioWaveData(url, interval) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { audioWaveData };
|
// type: 0 --> 声文速记 纯文本,已适配
|
||||||
|
// type: 1 --> 导入音频
|
||||||
|
// type: 2 --> 同传翻译 纯文本,已适配
|
||||||
|
// type: 3 --> 双语对话
|
||||||
|
function exportRecordLyric(type, lyric, filename) {
|
||||||
|
let element = document.createElement('a');
|
||||||
|
|
||||||
|
let text = "";
|
||||||
|
if (type === 0 || type === 2) {
|
||||||
|
text = lyric;
|
||||||
|
} else if (type === 1) {
|
||||||
|
text = lyric.reduce((accumulator, currentValue) => accumulator + currentValue.text, text);
|
||||||
|
} else if (type === 3) {
|
||||||
|
text = lyric.reduce((accumulator, currentValue) => {
|
||||||
|
if (currentValue.head) return accumulator;
|
||||||
|
return accumulator + currentValue.asr + "\n" + currentValue.translate + "\n\n";
|
||||||
|
}, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||||
|
element.setAttribute('download', filename);
|
||||||
|
|
||||||
|
element.style.display = 'none';
|
||||||
|
document.body.appendChild(element);
|
||||||
|
|
||||||
|
element.click();
|
||||||
|
document.body.removeChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validatePhoneNumber(phoneNumber) {
|
||||||
|
if (phoneNumber.length !== 11) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let reg = /^1\d{10}$/;
|
||||||
|
return reg.test(phoneNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
function textHintOfValidatePhoneNumber(phoneNumber) {
|
||||||
|
if (validatePhoneNumber(phoneNumber)) return "";
|
||||||
|
if (phoneNumber.length === 0) return "请输入手机号码"
|
||||||
|
return "请输入正确的手机号码";
|
||||||
|
}
|
||||||
|
|
||||||
|
export { sampleInterval, audioWaveData, validatePhoneNumber, textHintOfValidatePhoneNumber, exportRecordLyric };
|
16
src/components/Agreement.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Container, Checkbox, Link, Stack, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
export default function ({ agree, onChange }) {
|
||||||
|
return <Container>
|
||||||
|
<Stack direction="row" spacing={1}
|
||||||
|
sx={{ paddingTop: 2, alignItems: "center" }}
|
||||||
|
>
|
||||||
|
<Checkbox checked={agree} onChange={onChange} />
|
||||||
|
<Typography align="justify">同意
|
||||||
|
<Link target="_blank"
|
||||||
|
href="https://ai-api.hivoice.cn/api/app/app-voice-recorder/html/app_privacy.html/">
|
||||||
|
《纽曼隐私协议》</Link>
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
}
|
@ -1,26 +1,18 @@
|
|||||||
import { Container, TextField, InputAdornment, Link, Button, Stack, Typography } from "@mui/material";
|
import { Container, TextField, InputAdornment, Link, Button } from "@mui/material";
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import PhoneIphoneIcon from '@mui/icons-material/PhoneIphone';
|
import PhoneIphoneIcon from '@mui/icons-material/PhoneIphone';
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { setAccount, setVerificationCode } from "../business/userSlice.js"
|
|
||||||
import yzs from "../business/request.js";
|
import yzs from "../business/request.js";
|
||||||
|
import Agreement from "./Agreement.js";
|
||||||
|
import { validatePhoneNumber, textHintOfValidatePhoneNumber } from "../business/utilities.js"
|
||||||
|
|
||||||
export default function ({ udid }) {
|
export default function DynamicCodeForm({ udid, firstEnter, onChange, agreeAgreement, onAgreeChange }) {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const code = useRef(null);
|
const code = useRef(null);
|
||||||
const [checked, setChecked] = useState(true);
|
|
||||||
const [seconds, setSeconds] = useState(0); // 倒计时
|
const [seconds, setSeconds] = useState(0); // 倒计时
|
||||||
|
|
||||||
const account = useSelector(state => state.user.account)
|
const account = useSelector(state => state.user.account)
|
||||||
const verificationCode = useSelector(state => state.user.verificationCode)
|
const verificationCode = useSelector(state => state.user.verificationCode)
|
||||||
|
|
||||||
const handleInputChange = (event) => {
|
|
||||||
const { name, value } = event.target;
|
|
||||||
if (name === 'username') dispatch(setAccount(value));
|
|
||||||
if (name === 'password') dispatch(setVerificationCode(value));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = window.setInterval(() => {
|
const timer = window.setInterval(() => {
|
||||||
if (seconds > 0) {
|
if (seconds > 0) {
|
||||||
@ -36,15 +28,12 @@ export default function ({ udid }) {
|
|||||||
|
|
||||||
const onClick = (event) => {
|
const onClick = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (!validatePhoneNumber(account)) return;
|
||||||
code.current.disabled = true;
|
code.current.disabled = true;
|
||||||
yzs.send_phone_code(udid, account)
|
yzs.send_phone_code(udid, account)
|
||||||
setSeconds(60);
|
setSeconds(60);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAgreeChange = (event) => {
|
|
||||||
setChecked(!checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Container disableGutters={true}
|
return <Container disableGutters={true}
|
||||||
sx={{
|
sx={{
|
||||||
width: 300,
|
width: 300,
|
||||||
@ -54,12 +43,16 @@ export default function ({ udid }) {
|
|||||||
<TextField
|
<TextField
|
||||||
name="username"
|
name="username"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
label="请输入手机号码"
|
hiddenLabel
|
||||||
|
placeholder="请输入手机号码"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={account}
|
value={account}
|
||||||
color="primary"
|
color="primary"
|
||||||
|
size="small"
|
||||||
fullWidth
|
fullWidth
|
||||||
onChange={handleInputChange}
|
error={firstEnter ? false : !validatePhoneNumber(account)}
|
||||||
|
helperText={firstEnter ? "" : textHintOfValidatePhoneNumber(account)}
|
||||||
|
onChange={onChange}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<InputAdornment position="start">
|
<InputAdornment position="start">
|
||||||
@ -67,18 +60,24 @@ export default function ({ udid }) {
|
|||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
sx={{
|
||||||
|
minHeight: 64,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
// sx={{ paddingTop: 4 }}
|
sx={{
|
||||||
|
minHeight: 50,
|
||||||
|
}}
|
||||||
|
hiddenLabel
|
||||||
fullWidth
|
fullWidth
|
||||||
margin="normal"
|
name="verification_code"
|
||||||
name="password"
|
placeholder="请输入验证码"
|
||||||
label="请输入验证码"
|
type="text"
|
||||||
type="password"
|
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
value={verificationCode}
|
value={verificationCode}
|
||||||
onChange={handleInputChange}
|
onChange={onChange}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<InputAdornment position="start">
|
<InputAdornment position="start">
|
||||||
@ -98,8 +97,9 @@ export default function ({ udid }) {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={!checked}
|
disabled={!agreeAgreement}
|
||||||
sx={{
|
sx={{
|
||||||
|
marginTop: 1.5,
|
||||||
backgroundColor: "#FF595A",
|
backgroundColor: "#FF595A",
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: '#FF595A',
|
backgroundColor: '#FF595A',
|
||||||
@ -109,16 +109,8 @@ export default function ({ udid }) {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
注册/登录
|
登录
|
||||||
</Button>
|
</Button>
|
||||||
<Container>
|
<Agreement agree={agreeAgreement} onChange={onAgreeChange} />
|
||||||
<Stack direction="row" spacing={1}
|
|
||||||
sx={{ paddingTop: 2 }}
|
|
||||||
>
|
|
||||||
<input type="checkbox" checked={checked} onChange={onAgreeChange} />
|
|
||||||
<Typography>同意 <Link>《纽曼隐私协议》</Link></Typography>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
|
|
||||||
</Container>
|
</Container>
|
||||||
}
|
}
|
@ -3,17 +3,12 @@ import { Container, TextField, Button, InputAdornment } from "@mui/material";
|
|||||||
import PhoneIphoneIcon from '@mui/icons-material/PhoneIphone';
|
import PhoneIphoneIcon from '@mui/icons-material/PhoneIphone';
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
import { useSelector, useDispatch } from 'react-redux'
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
import { setAccount, setPassword } from "../business/userSlice.js"
|
import Agreement from './Agreement.js';
|
||||||
|
import { validatePhoneNumber, textHintOfValidatePhoneNumber } from "../business/utilities.js"
|
||||||
|
|
||||||
export default function () {
|
export default function ({ firstEnter, onChange, agreeAgreement, onAgreeChange }) {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const account = useSelector(state => state.user.account)
|
const account = useSelector(state => state.user.account)
|
||||||
const password = useSelector(state => state.user.password)
|
const password = useSelector(state => state.user.password)
|
||||||
const handleInputChange = (event) => {
|
|
||||||
const { name, value } = event.target;
|
|
||||||
if (name === 'username') dispatch(setAccount(value));
|
|
||||||
if (name === 'password') dispatch(setPassword(value));
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Container disableGutters={true}
|
return <Container disableGutters={true}
|
||||||
sx={{
|
sx={{
|
||||||
@ -24,11 +19,15 @@ export default function () {
|
|||||||
<TextField
|
<TextField
|
||||||
name="username"
|
name="username"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
label="请输入手机号码"
|
hiddenLabel
|
||||||
|
placeholder="请输入手机号码"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
error={firstEnter ? false : !validatePhoneNumber(account)}
|
||||||
|
helperText={firstEnter ? "" : textHintOfValidatePhoneNumber(account)}
|
||||||
value={account}
|
value={account}
|
||||||
fullWidth
|
fullWidth
|
||||||
onChange={handleInputChange}
|
onChange={onChange}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<InputAdornment position="start">
|
<InputAdornment position="start">
|
||||||
@ -36,18 +35,24 @@ export default function () {
|
|||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
sx={{
|
||||||
|
minHeight: 64,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
// sx={{ paddingTop: 4 }}
|
sx={{
|
||||||
|
minHeight: 50,
|
||||||
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
margin="normal"
|
|
||||||
name="password"
|
name="password"
|
||||||
label="请输入密码"
|
hiddenLabel
|
||||||
|
placeholder="请输入密码"
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={handleInputChange}
|
onChange={onChange}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<InputAdornment position="start">
|
<InputAdornment position="start">
|
||||||
@ -61,7 +66,9 @@ export default function () {
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
disabled={!agreeAgreement}
|
||||||
sx={{
|
sx={{
|
||||||
|
marginTop: 1.5,
|
||||||
backgroundColor: "#FF595A",
|
backgroundColor: "#FF595A",
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: '#FF595A',
|
backgroundColor: '#FF595A',
|
||||||
@ -73,5 +80,6 @@ export default function () {
|
|||||||
>
|
>
|
||||||
登录
|
登录
|
||||||
</Button>
|
</Button>
|
||||||
|
<Agreement agree={agreeAgreement} onChange={onAgreeChange} />
|
||||||
</Container>
|
</Container>
|
||||||
}
|
}
|
@ -1,4 +1,6 @@
|
|||||||
import { useRef, useCallback, useState, useEffect } from "react";
|
import { useRef, useCallback, useState, useEffect } from "react";
|
||||||
|
import { sampleInterval } from "../business/utilities"
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
const pointWidth = 2;
|
const pointWidth = 2;
|
||||||
const pointMargin = 3;
|
const pointMargin = 3;
|
||||||
@ -56,33 +58,37 @@ const paintCanvas = ({
|
|||||||
canvas, waveformData, duration, scrollLeft, leftPadding, canvasHeight, pointWidth, pointMargin, interval
|
canvas, waveformData, duration, scrollLeft, leftPadding, canvasHeight, pointWidth, pointMargin, interval
|
||||||
}) => {
|
}) => {
|
||||||
// console.log("paintCanvas", duration, canvasHeight, canvas.width, scrollLeft);
|
// console.log("paintCanvas", duration, canvasHeight, canvas.width, scrollLeft);
|
||||||
const context = canvas.getContext('2d');
|
try {
|
||||||
context.save();
|
const context = canvas.getContext('2d');
|
||||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
context.save();
|
||||||
context.translate(leftPadding, 0);;
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
context.translate(leftPadding, 0);;
|
||||||
|
|
||||||
drawText(context, duration, interval); // 画刻度尺
|
drawText(context, duration, interval); // 画刻度尺
|
||||||
|
|
||||||
waveformData.forEach((p, i) => {
|
waveformData.forEach((p, i) => {
|
||||||
context.beginPath()
|
context.beginPath()
|
||||||
const coordinates = pointCoordinates({
|
const coordinates = pointCoordinates({
|
||||||
index: i,
|
index: i,
|
||||||
pointWidth,
|
pointWidth,
|
||||||
pointMargin,
|
pointMargin,
|
||||||
canvasHeight,
|
canvasHeight,
|
||||||
maxAmplitude: canvasHeight - 30, // 留出空间画时间轴
|
maxAmplitude: canvasHeight - 30, // 留出空间画时间轴
|
||||||
amplitude: p,
|
amplitude: p,
|
||||||
})
|
})
|
||||||
context.rect(...coordinates)
|
context.rect(...coordinates)
|
||||||
context.fillStyle = (coordinates[0] <= scrollLeft) ? '#FF595A' : '#ABB5BC'
|
context.fillStyle = (coordinates[0] <= scrollLeft) ? '#FF595A' : '#ABB5BC'
|
||||||
context.fill()
|
context.fill()
|
||||||
});
|
});
|
||||||
context.restore();
|
context.restore();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// duration ms
|
// duration ms
|
||||||
export default function ({ width, duration, currentTime, playing, seek, waveData }) {
|
export default function ({ width, duration, currentTime, playing, seek, waveData }) {
|
||||||
const interval = (duration > 20 * 60 * 1000) ? 200 : 100; // ms
|
const interval = useMemo(() => sampleInterval(duration / 1000), [duration]);
|
||||||
const container = useRef(null);
|
const container = useRef(null);
|
||||||
const canvas = useRef(null);
|
const canvas = useRef(null);
|
||||||
const [scrollLeft, setScrollLeft] = useState(0);
|
const [scrollLeft, setScrollLeft] = useState(0);
|
||||||
|
78
src/components/RecordList.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux'
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Drawer from '@mui/material/Drawer';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
import { setCurrentIndex, fetchRecord } from "../business/recorderSlice.js"
|
||||||
|
import AccessTimeFilledIcon from '@mui/icons-material/AccessTimeFilled';
|
||||||
|
import empty_list from '../assets/empty_list.png';
|
||||||
|
|
||||||
|
const EmptyList = () => {
|
||||||
|
return <div style={{
|
||||||
|
height: "100vh",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}>
|
||||||
|
<img style={{ maxWidth: "100%", marginBottom: 40, }} src={empty_list} />
|
||||||
|
<Typography fontSize={14} align='center' color="#929292">这里空空如也,添加些东西吧</Typography>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const ListContent = ({ recordList, currentIndex }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const accessToken = useSelector(state => state.user.accessToken);
|
||||||
|
const onSelected = (event, index) => {
|
||||||
|
console.log("onSelected", index, recordList.at(index).transResultUrl)
|
||||||
|
dispatch(setCurrentIndex(index));
|
||||||
|
dispatch(fetchRecord(accessToken, index, recordList.at(index)));
|
||||||
|
}
|
||||||
|
return <Box sx={{ overflow: 'auto' }}>
|
||||||
|
<List>
|
||||||
|
{recordList === undefined ? <React.Fragment /> : recordList.map((item, index) => (
|
||||||
|
<ListItem key={index} disablePadding>
|
||||||
|
<ListItemButton selected={currentIndex === index} onClick={(event) => onSelected(event, index)}>
|
||||||
|
<ListItemText primary={item.editName} sx={{ color: currentIndex === index ? "#FF595A" : "#000000" }} secondary={
|
||||||
|
<React.Fragment>
|
||||||
|
<Typography component="span" variant="body1"
|
||||||
|
sx={{
|
||||||
|
overflowWrap: "anywhere"
|
||||||
|
}}
|
||||||
|
>{item.content.slice(0, 50) + '......'}</Typography>
|
||||||
|
<br />
|
||||||
|
<AccessTimeFilledIcon sx={{ fontSize: 12 }} />
|
||||||
|
<Typography component="span" variant="body2"> 更新于 {new Date(item.createTime).toLocaleString()}</Typography>
|
||||||
|
</React.Fragment>
|
||||||
|
} />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ({ open, recordList, currentIndex }) {
|
||||||
|
return <Drawer
|
||||||
|
variant="persistent"
|
||||||
|
anchor="left"
|
||||||
|
open={open}
|
||||||
|
sx={{
|
||||||
|
width: (theme) => theme.mixins.drawer.width,
|
||||||
|
flexShrink: 0,
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
width: (theme) => theme.mixins.drawer.width,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toolbar variant="dense" />
|
||||||
|
{(recordList === undefined || recordList.length === 0) ? <EmptyList /> : <ListContent recordList={recordList} currentIndex={currentIndex} />}
|
||||||
|
</Drawer>
|
||||||
|
}
|
@ -9,6 +9,14 @@ const server = "https://ai-api.hivoice.cn";
|
|||||||
const accessServer = "https://uc.hivoice.cn";
|
const accessServer = "https://uc.hivoice.cn";
|
||||||
|
|
||||||
module.exports = function (app) {
|
module.exports = function (app) {
|
||||||
|
app.use(
|
||||||
|
'/rest/v2/user/is_user_exist',
|
||||||
|
createProxyMiddleware({
|
||||||
|
target: accessServer,
|
||||||
|
changeOrigin: true,
|
||||||
|
logger: console,
|
||||||
|
})
|
||||||
|
);
|
||||||
app.use(
|
app.use(
|
||||||
'/rest/v2/phone/login',
|
'/rest/v2/phone/login',
|
||||||
createProxyMiddleware({
|
createProxyMiddleware({
|
||||||
|