1.录音文件加载时,显示模态页面防止用户快速点击。
BIN
public/logo.png
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 821 B |
22
src/App.js
@ -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>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Routes>
|
||||
<Route exact path="/" element={cookies.accessToken ? <MainPage /> : <Navigate to="/login" />} />
|
||||
<Route exact path="/login" element={<LoginPage />} />
|
||||
</Routes>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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,9 +99,7 @@ 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,
|
||||
@ -157,7 +138,6 @@ export default function () {
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
</Container>
|
||||
</ThemeProvider >
|
||||
{/* <Button variant="contained" onClick={debug_test}>测试</Button> */}
|
||||
</div>
|
||||
<Snackbar
|
||||
|
106
src/MainPage.js
@ -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} />
|
||||
<RecordList open={open} recordList={recordList} currentIndex={currentIndex} />
|
||||
<ClickHanlde open={open} onClick={onClick} />
|
||||
<Main open={open}
|
||||
onTransitionEnd={onTransitionEnd}
|
||||
>
|
||||
<RecordPlayer loading={loading}
|
||||
<RecordPlayer loading={loading} empty={recordList.length <= 0}
|
||||
playerBarWidth={playerBarWidth} currentTime={currentTime} hasLyric={hasLyric} currentLyric={currentLyric} />
|
||||
</Main>
|
||||
</ThemeProvider>
|
||||
</Box >
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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
After Width: | Height: | Size: 821 B |
BIN
src/assets/empty_hint.png
Normal file
After Width: | Height: | Size: 27 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,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 };
|
@ -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),
|
||||
|
@ -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 };
|
@ -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"
|
||||
|