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 { Routes, Route, Navigate, useNavigate } from 'react-router-dom'
import { useDispatch } from 'react-redux'
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { setAccessToken, setUserInfo, setSelectInfo } from "./business/userSlice.js"
import { useCookies } from 'react-cookie';
import LoginPage from './LoginPage';
import MainPage from './MainPage';
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() {
const dispatch = useDispatch();
const navigate = useNavigate();
@ -32,10 +52,12 @@ function App() {
return (
<div>
<Routes>
<Route exact path="/" element={cookies.accessToken ? <MainPage /> : <Navigate to="/login" />} />
<Route exact path="/login" element={<LoginPage />} />
</Routes>
<ThemeProvider theme={theme}>
<Routes>
<Route exact path="/" element={cookies.accessToken ? <MainPage /> : <Navigate to="/login" />} />
<Route exact path="/login" element={<LoginPage />} />
</Routes>
</ThemeProvider>
</div>
);
}

View File

@ -9,7 +9,7 @@ import MenuItem from '@mui/material/MenuItem';
import IconButton from '@mui/material/IconButton';
import Toolbar from '@mui/material/Toolbar';
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 { useCookies } from 'react-cookie';
import { useNavigate } from "react-router-dom";
@ -40,11 +40,11 @@ export default function () {
<CssBaseline />
<Container maxWidth={false} >
<Toolbar disableGutters variant="dense">
<Stack direction="row" sx={{ flexGrow: 1 }}>
<img src={logo} style={{
<Stack direction="row" sx={{ flexGrow: 1, alignItems: "center" }}>
<img src={appbar_logo} style={{
width: 28,
height: 30,
marginRight: 24,
height: 28,
marginRight: 10,
}} />
<Typography variant='h6' sx={{ color: "#FFFFFF" }}>纽曼AI语记</Typography>
</Stack>

View File

@ -16,23 +16,6 @@ import TabContext from '@mui/lab/TabContext';
import DynamicCodeForm from './components/DynamicCodeForm.js';
import PasswordForm from './components/PasswordForm.js';
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 () {
const navigate = useNavigate();
@ -116,48 +99,45 @@ export default function () {
<img className={styles.titleIcon} src={logo} />
<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,
<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>
}}
>
<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>
</ThemeProvider >
<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

View File

@ -3,65 +3,25 @@ import { useSelector, useDispatch } from 'react-redux'
import AppBar from './AppBar';
import RecordList from './components/RecordList';
import PlayerBar from './PlayerBar';
import store from './business/store';
import yzs from "./business/request.js";
import { setList, setCurrentLyric, setCurrentBlob, setCurrentWaveData } from "./business/recorderSlice.js"
import { CssBaseline, Box } from '@mui/material';
import MainSkeleton from './MainSkeleton';
import { setList, fetchRecord } from "./business/recorderSlice.js"
import { CssBaseline, Box, Typography } from '@mui/material';
import Backdrop from '@mui/material/Backdrop';
import CircularProgress from '@mui/material/CircularProgress';
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 close from './assets/close.png';
import empty_hint from './assets/empty_hint.png';
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 = {
marginTop: 16,
paddingBottom: 40,
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' })(
({ theme, open }) => ({
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) {
return <MainSkeleton />
return <Backdrop
sx={{
color: '#fff',
zIndex: (theme) => theme.zIndex.drawer + 1,
// marginLeft: "240px",
marginTop: "45px",
}}
open >
<CircularProgress color="inherit" />
</Backdrop>
} else {
return <div>
<PlayerBar width={playerBarWidth} currentTime={currentTime} />
{hasLyric ? <RecordLyrics style={lyricsBrowserStyle} currentLyric={currentLyric} currentTime={currentTime} /> :
<div style={lyricsBrowserStyle}
/>}
<PlayerBar width={playerBarWidth} currentTime={currentTime} lyric={currentLyric} />
<LyricItem empty={empty} />
</div>
}
};
@ -135,7 +123,7 @@ export default function () {
yzs.get_record_list(accessToken, passportId).then(list => {
dispatch(setList(list));
if (list.length > 0) {
fetchRecord(accessToken, list.at(0));
dispatch(fetchRecord(accessToken, 0, list.at(0)));
}
}).catch(error => {
console.log("get list failed", error);
@ -156,7 +144,7 @@ export default function () {
}
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);
}
@ -172,20 +160,20 @@ export default function () {
};
}, []);
useEffect(() => { handleResize(); }, [currentLyric]);
useEffect(() => {
if (!loading) handleResize();
}, [loading]);
return <Box sx={{ display: 'flex' }}>
<ThemeProvider theme={theme}>
<CssBaseline />
<AppBar />
<RecordList open={open} recordList={recordList} currentIndex={currentIndex} fetchRecord={fetchRecord} />
<ClickHanlde open={open} onClick={onClick} />
<Main open={open}
onTransitionEnd={onTransitionEnd}
>
<RecordPlayer loading={loading}
playerBarWidth={playerBarWidth} currentTime={currentTime} hasLyric={hasLyric} currentLyric={currentLyric} />
</Main>
</ThemeProvider>
<CssBaseline />
<AppBar />
<RecordList open={open} recordList={recordList} currentIndex={currentIndex} />
<ClickHanlde open={open} onClick={onClick} />
<Main open={open}
onTransitionEnd={onTransitionEnd}
>
<RecordPlayer loading={loading} empty={recordList.length <= 0}
playerBarWidth={playerBarWidth} currentTime={currentTime} hasLyric={hasLyric} currentLyric={currentLyric} />
</Main>
</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 downloadIcon from "./assets/download.png";
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";
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');
}
export default function ({ width, currentTime }) {
export default function ({ width, lyric, currentTime }) {
const dispatch = useDispatch();
const [duration, setDuration] = useState(0); // 秒,有小数点
const [playbackRate, setPlaybackRate] = useState(1.0);
@ -54,6 +54,7 @@ export default function ({ width, currentTime }) {
link.href = currentBlob;
link.download = recordList.at(currentIndex).name;
link.click();
exportRecordLyric(recordList.at(currentIndex).type, lyric, recordList.at(currentIndex).editName + ".txt");
};
const onDurationChange = (event) => {
@ -86,7 +87,7 @@ export default function ({ width, currentTime }) {
display: "flex",
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}>
<img src={downloadIcon} />
</IconButton>

View File

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

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

View File

@ -15,6 +15,7 @@ const appSecret = "c5eccccfec16d46fe9ac678d69198415";
function constructParameter(body) {
let params = [];
for (let key in body) {
if (key === "smsTemplateId") continue;
params.push(body[key].toString());
}
params.sort();
@ -222,6 +223,7 @@ const yzs = {
body.clientId = udid;
body.timestamp = Math.round(new Date().getTime() / 1000);
body.userCell = userCell;
body.smsTemplateId = 316;
return fetch("/rest/v2/phone/send_phone_code", {
method: "POST",
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) {
if (phoneNumber.length !== 11) {
return false;
@ -47,4 +76,4 @@ function textHintOfValidatePhoneNumber(phoneNumber) {
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 Typography from '@mui/material/Typography';
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';
const drawerWidth = 240;
export default function ({ open, recordList, currentIndex, fetchRecord }) {
export default function ({ open, 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));
fetchRecord(accessToken, recordList.at(index));
dispatch(fecthRecord(index));
dispatch(fetchRecord(accessToken, index, recordList.at(index)));
}
return <Drawer
variant="persistent"