1.录音文件加载时,显示模态页面防止用户快速点击。

This commit is contained in:
amass 2023-06-26 16:39:04 +08:00
parent 807df7f747
commit bc2550f64f
16 changed files with 192 additions and 181 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 821 B

View File

@ -1,12 +1,32 @@
import { useEffect } from 'react'; 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",
}
},
});
function App() { function App() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
@ -32,10 +52,12 @@ function App() {
return ( return (
<div> <div>
<ThemeProvider theme={theme}>
<Routes> <Routes>
<Route exact path="/" element={cookies.accessToken ? <MainPage /> : <Navigate to="/login" />} /> <Route exact path="/" element={cookies.accessToken ? <MainPage /> : <Navigate to="/login" />} />
<Route exact path="/login" element={<LoginPage />} /> <Route exact path="/login" element={<LoginPage />} />
</Routes> </Routes>
</ThemeProvider>
</div> </div>
); );
} }

View File

@ -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>

View File

@ -16,23 +16,6 @@ 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();
@ -116,9 +99,7 @@ export default function () {
<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>
<div className={styles.loginFrame}> <div className={styles.loginFrame}>
<ThemeProvider theme={theme}>
<Container component="form" className={styles.form} onSubmit={handleSubmit} <Container component="form" className={styles.form} onSubmit={handleSubmit}
sx={{ sx={{
width: 360, width: 360,
@ -157,7 +138,6 @@ export default function () {
</TabPanel> </TabPanel>
</TabContext> </TabContext>
</Container> </Container>
</ThemeProvider >
{/* <Button variant="contained" onClick={debug_test}>测试</Button> */} {/* <Button variant="contained" onClick={debug_test}>测试</Button> */}
</div> </div>
<Snackbar <Snackbar

View File

@ -3,65 +3,25 @@ import { useSelector, useDispatch } from 'react-redux'
import AppBar from './AppBar'; import AppBar from './AppBar';
import RecordList from './components/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 { CssBaseline, Box, Typography } from '@mui/material';
import MainSkeleton from './MainSkeleton'; import Backdrop from '@mui/material/Backdrop';
import CircularProgress from '@mui/material/CircularProgress';
import RecordLyrics from './RecordLyrics'; import RecordLyrics from './RecordLyrics';
import { createTheme, ThemeProvider, styled } from '@mui/material/styles'; import { styled } 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 drawerWidth = 240;
const theme = createTheme({
status: {
danger: '#e53e3e',
},
palette: {
primary: {
main: '#FF595A',
darker: '#FF595A',
},
neutral: {
main: '#64748B',
contrastText: '#fff',
},
black: {
main: "#222222",
}
},
});
const lyricsBrowserStyle = { const lyricsBrowserStyle = {
marginTop: 16, marginTop: 16,
paddingBottom: 40, paddingBottom: 40,
padding: 24, padding: 24,
} }
function fetchRecord(accessToken, record) {
if (record.transResultUrl) {
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;
}
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' })(
({ theme, open }) => ({ ({ theme, open }) => ({
width: 18, width: 18,
@ -105,15 +65,43 @@ const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(
}), }),
); );
const RecordPlayer = ({ loading, playerBarWidth, currentTime, hasLyric, currentLyric }) => { const LyricItem = ({ empty, hasLyric, lyricsBrowserStyle, currentLyric, currentTime }) => {
if (empty) {
return <div style={{
height: "calc(100vh - 250px)",
marginTop: 48,
display: "flex",
alignItems: "center",
justifyContent: "center",
}} >
<div>
<img style={{ maxWidth: "100%", marginBottom: 40, }} src={empty_hint} />
<Typography align='center' color="#929292">这里空空如也添加些东西吧</Typography>
</div>
</div>
} else {
return hasLyric ? <RecordLyrics style={lyricsBrowserStyle} currentLyric={currentLyric} currentTime={currentTime} /> :
<div style={lyricsBrowserStyle}
/>
}
}
const RecordPlayer = ({ loading, empty, playerBarWidth, currentTime, hasLyric, currentLyric }) => {
if (loading) { if (loading) {
return <MainSkeleton /> return <Backdrop
sx={{
color: '#fff',
zIndex: (theme) => theme.zIndex.drawer + 1,
// marginLeft: "240px",
marginTop: "45px",
}}
open >
<CircularProgress color="inherit" />
</Backdrop>
} else { } else {
return <div> return <div>
<PlayerBar width={playerBarWidth} currentTime={currentTime} /> <PlayerBar width={playerBarWidth} currentTime={currentTime} lyric={currentLyric} />
{hasLyric ? <RecordLyrics style={lyricsBrowserStyle} currentLyric={currentLyric} currentTime={currentTime} /> : <LyricItem empty={empty} />
<div style={lyricsBrowserStyle}
/>}
</div> </div>
} }
}; };
@ -135,7 +123,7 @@ export default function () {
yzs.get_record_list(accessToken, passportId).then(list => { yzs.get_record_list(accessToken, passportId).then(list => {
dispatch(setList(list)); dispatch(setList(list));
if (list.length > 0) { if (list.length > 0) {
fetchRecord(accessToken, list.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);
@ -156,7 +144,7 @@ export default function () {
} }
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 ? 240 : 0) - 48);
} }
@ -172,20 +160,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} fetchRecord={fetchRecord} /> <RecordList open={open} recordList={recordList} currentIndex={currentIndex} />
<ClickHanlde open={open} onClick={onClick} /> <ClickHanlde open={open} onClick={onClick} />
<Main open={open} <Main open={open}
onTransitionEnd={onTransitionEnd} onTransitionEnd={onTransitionEnd}
> >
<RecordPlayer loading={loading} <RecordPlayer loading={loading} empty={recordList.length <= 0}
playerBarWidth={playerBarWidth} currentTime={currentTime} hasLyric={hasLyric} currentLyric={currentLyric} /> playerBarWidth={playerBarWidth} currentTime={currentTime} hasLyric={hasLyric} currentLyric={currentLyric} />
</Main> </Main>
</ThemeProvider>
</Box > </Box >
} }

View File

@ -1,34 +0,0 @@
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import Skeleton from '@mui/material/Skeleton';
import { Container } from '@mui/material';
export default function () {
return (
<Box spacing={1} sx={{
display: "flex",
flexDirection: "column",
// justifyContent: "space-between",
height: `calc(100vh - 48px)`,
}}>
<Skeleton variant="text" sx={{ fontSize: '1rem' }} />
<Container disableGutters maxWidth={false} sx={{
height: 60,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginTop: 2,
marginBottom: 0.5,
}}>
<Skeleton variant="rounded" width={"30%"} height={60} />
<Skeleton variant="circular" width={30} height={30} />
</Container>
{/* For variant="text", adjust the height via font-size */}
{/* For other variants, adjust the size with `width` and `height` */}
<Skeleton variant="rounded" animation="wave" width="100%" height={70} sx={{ marginBottom: 0.5 }} />
<Skeleton variant="rounded" animation="wave" width="100%" height={32} sx={{ marginBottom: 2 }} />
<Skeleton variant="rounded" width="100%" sx={{ marginBottom: 2, flexGrow: 1 }} />
</Box>
);
}

View File

@ -5,7 +5,7 @@ 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, setPauseState, togglePauseState, setCurrentWaveData } from "./business/recorderSlice.js" import { setCurrentTime, setPauseState, togglePauseState, setCurrentWaveData } from "./business/recorderSlice.js"
import { audioWaveData, sampleInterval } 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,7 +17,7 @@ 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 [playbackRate, setPlaybackRate] = useState(1.0);
@ -54,6 +54,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) => {
@ -86,7 +87,7 @@ export default function ({ width, currentTime }) {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
}}> }}>
<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>

View File

@ -13,10 +13,12 @@ function isHighlight(currentTime, { start, end }) {
// type: 3 --> 双语对话 // type: 3 --> 双语对话
const PlainText = ({ lyrics }) => { const PlainText = ({ lyrics }) => {
if (typeof lyrics !== "string") return <React.Fragment />;
return <div style={{ whiteSpace: "pre-wrap" }}>{lyrics}</div> return <div style={{ whiteSpace: "pre-wrap" }}>{lyrics}</div>
} }
const ImportAudio = ({ lyrics, currentTime }) => { // 导入音频 const ImportAudio = ({ lyrics, currentTime }) => { // 导入音频
if (typeof lyrics !== "object") return <React.Fragment />;
const onClick = (index) => { const onClick = (index) => {
console.log("onClick", index); console.log("onClick", index);
} }
@ -29,6 +31,7 @@ const ImportAudio = ({ lyrics, currentTime }) => { // 导入音频
}; };
const BilingualDialogue = ({ lyrics }) => { // 双语对话 const BilingualDialogue = ({ lyrics }) => { // 双语对话
if (typeof lyrics !== "object") return <React.Fragment />;
return <div> {lyrics.map((lyric, index) => { return <div> {lyrics.map((lyric, index) => {
return <div index={index} style={{ paddingBottom: 40 }}> return <div index={index} style={{ paddingBottom: 40 }}>
<Typography align="left" >{lyric.asr}</Typography> <Typography align="left" >{lyric.asr}</Typography>

BIN
src/assets/appbar_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

BIN
src/assets/empty_hint.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,4 +1,5 @@
import { createSlice } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit'
import yzs from "./request.js";
// type: 0 --> 声文速记 纯文本,已适配 // type: 0 --> 声文速记 纯文本,已适配
// type: 1 --> 导入音频 // type: 1 --> 导入音频
@ -62,15 +63,35 @@ export const {
export default recorderSlice.reducer export default recorderSlice.reducer
const fecthRecord = (index) => { const fetchRecord = (accessToken, index, record) => {
console.log("begin fetch record item", index);
return (dispatch) => { return (dispatch) => {
dispatch(setLoading()); dispatch(setLoading());
// testPromiseLoading(2000, true).then(e => { let promises = [];
// console.log("end fetch record item", index); if (record.transResultUrl) {
// dispatch(setLoadFinished()); 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 { fecthRecord }; export { fetchRecord };

View File

@ -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();
@ -222,6 +223,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),

View File

@ -33,6 +33,35 @@ function audioWaveData(url, interval) {
}); });
} }
// 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) { function validatePhoneNumber(phoneNumber) {
if (phoneNumber.length !== 11) { if (phoneNumber.length !== 11) {
return false; return false;
@ -47,4 +76,4 @@ function textHintOfValidatePhoneNumber(phoneNumber) {
return "请输入正确的手机号码"; return "请输入正确的手机号码";
} }
export { sampleInterval, audioWaveData, validatePhoneNumber, textHintOfValidatePhoneNumber }; export { sampleInterval, audioWaveData, validatePhoneNumber, textHintOfValidatePhoneNumber, exportRecordLyric };

View File

@ -8,19 +8,18 @@ import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Toolbar from '@mui/material/Toolbar'; import Toolbar from '@mui/material/Toolbar';
import { setCurrentIndex, fecthRecord } from "../business/recorderSlice.js" import { setCurrentIndex, fetchRecord } from "../business/recorderSlice.js"
import AccessTimeFilledIcon from '@mui/icons-material/AccessTimeFilled'; import AccessTimeFilledIcon from '@mui/icons-material/AccessTimeFilled';
const drawerWidth = 240; const drawerWidth = 240;
export default function ({ open, recordList, currentIndex, fetchRecord }) { export default function ({ open, recordList, currentIndex }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const accessToken = useSelector(state => state.user.accessToken); const accessToken = useSelector(state => state.user.accessToken);
const onSelected = (event, index) => { const onSelected = (event, index) => {
console.log("onSelected", index, recordList.at(index).transResultUrl) console.log("onSelected", index, recordList.at(index).transResultUrl)
dispatch(setCurrentIndex(index)); dispatch(setCurrentIndex(index));
fetchRecord(accessToken, recordList.at(index)); dispatch(fetchRecord(accessToken, index, recordList.at(index)));
dispatch(fecthRecord(index));
} }
return <Drawer return <Drawer
variant="persistent" variant="persistent"