Compare commits

..

No commits in common. "f45f472e47fa4deb713c57a63c0681025b3ff38c" and "deee6211b0e6007ba0733ff9662a57dfe7f40a0a" have entirely different histories.

24 changed files with 323 additions and 662 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 821 B

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,73 +1,37 @@
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']);
useEffect(() => { if (cookies.accessToken) {
if (cookies.accessToken) { dispatch(setAccessToken(cookies.accessToken));
dispatch(setAccessToken(cookies.accessToken)); yzs.get_user_info(yzs.uniqueDeviceIdentifier(), cookies.accessToken).then(info => {
yzs.get_user_info(yzs.uniqueDeviceIdentifier(), cookies.accessToken).then(info => { dispatch(setUserInfo(info));
dispatch(setUserInfo(info)); yzs.user_select(yzs.uniqueDeviceIdentifier(), cookies.accessToken).then(info => {
yzs.user_select(yzs.uniqueDeviceIdentifier(), cookies.accessToken).then(info => { dispatch(setSelectInfo(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>
<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 appbar_logo from './assets/appbar_logo.png'; import logo from './assets/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, alignItems: "center" }}> <Stack direction="row" sx={{ flexGrow: 1 }}>
<img src={appbar_logo} style={{ <img src={logo} style={{
width: 28, width: 28,
height: 28, height: 30,
marginRight: 10, marginRight: 24,
}} /> }} />
<Typography variant='h6' sx={{ color: "#FFFFFF" }}>纽曼AI语记</Typography> <Typography variant='h6' sx={{ color: "#FFFFFF" }}>纽曼AI语记</Typography>
</Stack> </Stack>

View File

@ -1,70 +1,55 @@
import React, { useState } from 'react'; import React, { useState, useEffect } 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 { import { setFlushToken, setAccessToken, setUserInfo } from "./business/userSlice.js"
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, Snackbar, Alert, Button } from '@mui/material'; import { Container, Tab, Box } 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;
@ -82,71 +67,57 @@ 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 <div className={styles.loginPage}> return (
<div className={styles.title}> <div className={styles.loginPage}>
<img className={styles.titleIcon} src={logo} /> <div className={styles.title}>
<h1 className={styles.titleText}>纽曼AI语记</h1> <img className={styles.titleIcon} src={logo} />
</div> <h1 className={styles.titleText}>纽曼AI语记</h1>
<div className={styles.loginFrame}> </div>
<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,
}} <div className={styles.loginFrame}>
> <ThemeProvider theme={theme}>
<TabContext value={value}> <Container component="form" className={styles.form} onSubmit={handleSubmit}
<Box> sx={{
<TabList width: 360,
aria-label="basic tabs example" value={value} onChange={handleChange} > height: 418,
<Tab label="手机动态码登录" value="1" /> backgroundColor: 'white',
<Tab label="账号密码登录" value="2" /> display: "flex",
</TabList> flexDirection: "column",
</Box> justifyContent: "center",
alignItems: "center",
boxShadow: "0px 5px 20px 0px rgba(146,0,1,0.1)",
borderRadius: 4,
<TabPanel value="1" > }}
<DynamicCodeForm udid={yzs.uniqueDeviceIdentifier()} >
firstEnter={firstEnter} <TabContext value={value}>
agreeAgreement={agreeAgreement} onAgreeChange={onAgreeChange} <Box>
onChange={handleInputChange} <TabList
/> aria-label="basic tabs example" value={value} onChange={handleChange} >
</TabPanel> <Tab label="手机动态码登录" value="1" />
<TabPanel value="2" > <Tab label="账号密码登录" value="2" />
<PasswordForm firstEnter={firstEnter} </TabList>
agreeAgreement={agreeAgreement} onAgreeChange={onAgreeChange} </Box>
onChange={handleInputChange}
/> <TabPanel value="1" >
</TabPanel> <DynamicCodeForm udid={yzs.uniqueDeviceIdentifier()} />
</TabContext> </TabPanel>
</Container> <TabPanel value="2" >
{/* <Button variant="contained" onClick={debug_test}>测试</Button> */} <PasswordForm />
</TabPanel>
</TabContext>
</Container>
</ThemeProvider >
{/* <Button variant="contained" onClick={debug_test}>测试</Button> */}
</div>
</div> </div>
<Snackbar );
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
open={openTooltip}
autoHideDuration={3500}
onClose={handleTooltipClose}
>
<Alert severity="error">{message}</Alert>
</Snackbar>
</div>
} }

View File

@ -1,26 +1,50 @@
import { React, useEffect, useState } from 'react'; import { 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 './components/RecordList'; import RecordList from './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, fetchRecord } from "./business/recorderSlice.js" import { setList, setCurrentLyric, setCurrentBlob, setCurrentWaveData } from "./business/recorderSlice.js"
import Backdrop from '@mui/material/Backdrop'; import { CssBaseline, Box } from '@mui/material';
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 { styled, useTheme } from '@mui/material/styles'; import { createTheme, ThemeProvider, 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 lyricsBrowserStyle = { const drawerWidth = 240;
marginTop: 16,
paddingBottom: 40, const theme = createTheme({
padding: 24, status: {
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' })(
@ -41,7 +65,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: theme.mixins.drawer.width - 18, left: drawerWidth - 18,
}), }),
}) })
); );
@ -49,18 +73,13 @@ 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,
paddingTop: 0, padding: theme.spacing(3),
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: `-${theme.mixins.drawer.width}px`, marginLeft: `-${drawerWidth}px`,
...(open && { ...(open && {
transition: theme.transitions.create('margin', { transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut, easing: theme.transitions.easing.easeOut,
@ -71,83 +90,34 @@ 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)); dispatch(setList(list.result));
if (list.length > 0) { if (list.result.length > 0) {
dispatch(fetchRecord(accessToken, 0, list.at(0))); fetchRecord(accessToken, list.result.at(0));
} }
}).catch(error => { }).catch(error => {
console.log("get list failed", error); console.log("get list failed", error);
}); });
}, [passportId]); }, [accessToken, 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 - theme.mixins.drawer.width - 48); // 防止中途底部出现scrollbar setPlayerBarWidth(document.documentElement.clientWidth - 240 - 48); // 防止中途底部出现scrollbar
} }
const handleResize = () => { const handleResize = () => {
// let scrollBarWidth = window.innerWidth - document.documentElement.clientWidth; console.log("innerWidth", document.documentElement.clientWidth, document.documentElement.clientWidth - (open ? 240 : 0) - 48)
setPlayerBarWidth(document.documentElement.clientWidth - (open ? theme.mixins.drawer.width : 0) - 48); setPlayerBarWidth(document.documentElement.clientWidth - (open ? 240 : 0) - 48);
} }
const onTransitionEnd = () => { const onTransitionEnd = () => {
@ -162,20 +132,20 @@ export default function () {
}; };
}, []); }, []);
useEffect(() => { useEffect(() => { handleResize(); }, [currentLyric]);
if (!loading) handleResize();
}, [loading]);
return <Box sx={{ display: 'flex' }}> return <Box sx={{ display: 'flex' }}>
<CssBaseline /> <ThemeProvider theme={theme}>
<AppBar /> <CssBaseline />
<RecordList open={open} recordList={recordList} currentIndex={currentIndex} /> <AppBar />
<ClickHanlde open={open} onClick={onClick} /> <RecordList open={open} fetchRecord={fetchRecord} />
<Main open={open} <ClickHanlde open={open} onClick={onClick} />
onTransitionEnd={onTransitionEnd} <Main open={open}
> onTransitionEnd={onTransitionEnd}
<RecordPlayer loading={loading} empty={recordList.length <= 0} >
playerBarWidth={playerBarWidth} currentTime={currentTime} hasLyric={hasLyric} currentLyric={currentLyric} /> <PlayerBar width={playerBarWidth} currentTime={currentTime} />
</Main> <RecordLyrics currentLyric={currentLyric} currentTime={currentTime} />
</Main>
</ThemeProvider>
</Box > </Box >
} }

View File

@ -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, setPauseState, togglePauseState, setCurrentWaveData } from "./business/recorderSlice.js" import { setCurrentTime, togglePauseState, setCurrentWaveData } from "./business/recorderSlice.js"
import { audioWaveData, sampleInterval, exportRecordLyric } from "./business/utilities" import { audioWaveData } from "./business/utilities"
import ProgressBar from "./components/ProgressBar"; import ProgressBar from "./components/ProgressBar";
const durationFormat = (time) => { const durationFormat = (time) => {
@ -17,10 +17,9 @@ 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, lyric, currentTime }) { export default function ({ width, 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);
@ -29,14 +28,11 @@ export default function ({ width, lyric, 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(() => {
if (currentBlob.length <= 0) return; audioWaveData(currentBlob, (duration > 20 * 60) ? 200 : 100)
audioWaveData(currentBlob, sampleInterval(duration))
.then(data => dispatch(setCurrentWaveData(data))); .then(data => dispatch(setCurrentWaveData(data)));
}, [duration]); }, [duration]);
@ -55,7 +51,6 @@ export default function ({ width, lyric, 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) => {
@ -70,26 +65,20 @@ export default function ({ width, lyric, 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: (theme) => theme.mixins.dense.toolbar.height, top: 48,
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>
@ -105,15 +94,13 @@ export default function ({ width, lyric, currentTime }) {
<img src={pause ? pauseIcon : playIcon} /> <img src={pause ? pauseIcon : playIcon} />
</IconButton> </IconButton>
<audio ref={player} src={currentBlob} onDurationChange={onDurationChange} <audio ref={player} src={currentBlob} onDurationChange={onDurationChange} onTimeUpdate={onTimeUpdate} />
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
value={playbackRate} defaultValue={1.0}
onChange={onChange} onChange={onChange}
sx={{ width: 90, height: 70 }} sx={{ width: 90, height: 70 }}
> >

59
src/RecordList.js Normal file
View File

@ -0,0 +1,59 @@
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>
}

View File

@ -1,5 +1,6 @@
import React, { useMemo } from "react"; import React 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 }) {
@ -7,59 +8,19 @@ 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>
}
const ImportAudio = ({ lyrics, currentTime }) => { // 导入音频 export default function ({ currentLyric, 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 -1; if (recordList.length === 0) return <React.Fragment />;
return recordList.at(currentIndex).type;
}, [currentLyric]); return <Paper className={styles.lyricsBrowser}>
return <Paper style={style}> {recordList.at(currentIndex).type === 1 ? (typeof currentLyric === "object" ? currentLyric.map((lyric, index) => {
<LyricsContent type={currentType} lyrics={currentLyric} currentTime={currentTime} /> return <div className={styles.lyricItem}>
<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>
} }

View File

@ -0,0 +1,9 @@
.lyricsBrowser {
margin-top: 16px;
padding-bottom: 40px;
padding: 24px;
}
.lyricItem {
padding-bottom: 40px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -1,10 +1,4 @@
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',
@ -16,7 +10,6 @@ export const recorderSlice = createSlice({
currentWaveData: [], currentWaveData: [],
currentTime: 0, // 当前音频播放时间 currentTime: 0, // 当前音频播放时间
pause: true, pause: true,
loading: false,
}, },
reducers: { reducers: {
setList: (state, action) => { setList: (state, action) => {
@ -41,57 +34,10 @@ 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 { export const { setCurrentIndex, setList, setCurrentLyric, setCurrentBlob, togglePauseState, setCurrentTime, setCurrentWaveData } = recorderSlice.actions
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 };

View File

@ -15,7 +15,6 @@ 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();
@ -140,11 +139,8 @@ const yzs = {
if (json.errorCode !== "0") { if (json.errorCode !== "0") {
throw json; throw json;
} }
let list = json.result; console.log(json)
list.sort((lfs, rhs) => { // 要求倒序排序 return json;
return rhs.modifyTime - lfs.modifyTime;
})
return list;
}); });
}, },
download: function (accessToken, url) { download: function (accessToken, url) {
@ -187,11 +183,10 @@ 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) => {
if (json.returnCode != "uc_0000") { console.log("flushToken: ", json.result.flushToken);
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) {
@ -201,7 +196,6 @@ 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),
@ -211,11 +205,10 @@ const yzs = {
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
}, },
}).then(response => response.json()).then((json) => { }).then(response => response.json()).then((json) => {
if (json.returnCode != "uc_0000") { console.log("flushToken: ", json.result.flushToken);
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) {
@ -224,7 +217,6 @@ 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),
@ -240,31 +232,6 @@ 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) {

View File

@ -10,9 +10,9 @@ export const userSlice = createSlice({
userName: "", userName: "",
nickName: "", nickName: "",
avatarUrl: "", avatarUrl: "",
agreeAgreement: false, agreeAgreement: true,
account: "", account: "13682423271",
password: "", password: "yzs123456",
verificationCode: "", verificationCode: "",
}, },
reducers: { reducers: {
@ -40,13 +40,10 @@ 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, setAgreeAgreement } = userSlice.actions export const { setFlushToken, setAccessToken, setUserInfo, setSelectInfo, setAccount, setPassword, setVerificationCode } = userSlice.actions
export default userSlice.reducer export default userSlice.reducer

View File

@ -1,22 +1,3 @@
// 间隔多长时间取一个采样点
// 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;
@ -43,47 +24,4 @@ function audioWaveData(url, interval) {
}); });
} }
// type: 0 --> 声文速记 纯文本,已适配 export { audioWaveData };
// 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 };

View File

@ -1,16 +0,0 @@
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>
}

View File

@ -1,18 +1,26 @@
import { Container, TextField, InputAdornment, Link, Button } from "@mui/material"; import { Container, TextField, InputAdornment, Link, Button, Stack, Typography } 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 } from 'react-redux' import { useSelector, useDispatch } 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 DynamicCodeForm({ udid, firstEnter, onChange, agreeAgreement, onAgreeChange }) { export default function ({ udid }) {
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) {
@ -28,12 +36,15 @@ export default function DynamicCodeForm({ udid, firstEnter, onChange, agreeAgree
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,
@ -43,16 +54,12 @@ export default function DynamicCodeForm({ udid, firstEnter, onChange, agreeAgree
<TextField <TextField
name="username" name="username"
autoComplete="username" autoComplete="username"
hiddenLabel label="请输入手机号码"
placeholder="请输入手机号码"
variant="outlined" variant="outlined"
value={account} value={account}
color="primary" color="primary"
size="small"
fullWidth fullWidth
error={firstEnter ? false : !validatePhoneNumber(account)} onChange={handleInputChange}
helperText={firstEnter ? "" : textHintOfValidatePhoneNumber(account)}
onChange={onChange}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
@ -60,24 +67,18 @@ export default function DynamicCodeForm({ udid, firstEnter, onChange, agreeAgree
</InputAdornment> </InputAdornment>
), ),
}} }}
sx={{
minHeight: 64,
}}
/> />
<TextField <TextField
sx={{ // sx={{ paddingTop: 4 }}
minHeight: 50,
}}
hiddenLabel
fullWidth fullWidth
name="verification_code" margin="normal"
placeholder="请输入验证码" name="password"
type="text" label="请输入验证码"
type="password"
autoComplete="current-password" autoComplete="current-password"
variant="outlined" variant="outlined"
size="small"
value={verificationCode} value={verificationCode}
onChange={onChange} onChange={handleInputChange}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
@ -97,9 +98,8 @@ export default function DynamicCodeForm({ udid, firstEnter, onChange, agreeAgree
variant="contained" variant="contained"
color="primary" color="primary"
fullWidth fullWidth
disabled={!agreeAgreement} disabled={!checked}
sx={{ sx={{
marginTop: 1.5,
backgroundColor: "#FF595A", backgroundColor: "#FF595A",
'&:hover': { '&:hover': {
backgroundColor: '#FF595A', backgroundColor: '#FF595A',
@ -109,8 +109,16 @@ export default function DynamicCodeForm({ udid, firstEnter, onChange, agreeAgree
}, },
}} }}
> >
登录 注册/登录
</Button> </Button>
<Agreement agree={agreeAgreement} onChange={onAgreeChange} /> <Container>
<Stack direction="row" spacing={1}
sx={{ paddingTop: 2 }}
>
<input type="checkbox" checked={checked} onChange={onAgreeChange} />
<Typography>同意 <Link>纽曼隐私协议</Link></Typography>
</Stack>
</Container>
</Container> </Container>
} }

View File

@ -3,12 +3,17 @@ 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 Agreement from './Agreement.js'; import { setAccount, setPassword } from "../business/userSlice.js"
import { validatePhoneNumber, textHintOfValidatePhoneNumber } from "../business/utilities.js"
export default function ({ firstEnter, onChange, agreeAgreement, onAgreeChange }) { export default function () {
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={{
@ -19,15 +24,11 @@ export default function ({ firstEnter, onChange, agreeAgreement, onAgreeChange }
<TextField <TextField
name="username" name="username"
autoComplete="username" autoComplete="username"
hiddenLabel label="请输入手机号码"
placeholder="请输入手机号码"
variant="outlined" variant="outlined"
size="small"
error={firstEnter ? false : !validatePhoneNumber(account)}
helperText={firstEnter ? "" : textHintOfValidatePhoneNumber(account)}
value={account} value={account}
fullWidth fullWidth
onChange={onChange} onChange={handleInputChange}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
@ -35,24 +36,18 @@ export default function ({ firstEnter, onChange, agreeAgreement, onAgreeChange }
</InputAdornment> </InputAdornment>
), ),
}} }}
sx={{
minHeight: 64,
}}
/> />
<TextField <TextField
sx={{ // sx={{ paddingTop: 4 }}
minHeight: 50,
}}
fullWidth fullWidth
margin="normal"
name="password" name="password"
hiddenLabel label="请输入密码"
placeholder="请输入密码"
type="password" type="password"
autoComplete="current-password" autoComplete="current-password"
variant="outlined" variant="outlined"
size="small"
value={password} value={password}
onChange={onChange} onChange={handleInputChange}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
@ -66,9 +61,7 @@ export default function ({ firstEnter, onChange, agreeAgreement, onAgreeChange }
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',
@ -80,6 +73,5 @@ export default function ({ firstEnter, onChange, agreeAgreement, onAgreeChange }
> >
登录 登录
</Button> </Button>
<Agreement agree={agreeAgreement} onChange={onAgreeChange} />
</Container> </Container>
} }

View File

@ -1,6 +1,4 @@
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;
@ -58,37 +56,33 @@ 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);
try { const context = canvas.getContext('2d');
const context = canvas.getContext('2d'); context.save();
context.save(); context.clearRect(0, 0, canvas.width, canvas.height);
context.clearRect(0, 0, canvas.width, canvas.height); context.translate(leftPadding, 0);;
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 = useMemo(() => sampleInterval(duration / 1000), [duration]); const interval = (duration > 20 * 60 * 1000) ? 200 : 100; // ms
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);

View File

@ -1,78 +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, 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>
}

View File

@ -9,14 +9,6 @@ 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({