Browse Source

trae03 口语练习页

pull/11591/head
子阳 4 months ago
parent
commit
d6fe22e0e5
  1. 8
      config/routes.ts
  2. 10
      package.json
  3. 367
      src/pages/spokenPages/SpokenPractice.less
  4. 246
      src/pages/spokenPages/SpokenPractice.tsx

8
config/routes.ts

@ -1,4 +1,4 @@
/**
/**
* @name umi
* @description path,component,routes,redirect,wrappers,name,icon
* @param path path 第一种是动态参数 :id *
@ -59,6 +59,12 @@ export default [
path: '/tts-mobile-pro',
component: './tts_mobile_pro',
},
{
name: 'spoken-practice',
icon: 'mic',
path: '/spoken-practice',
component: './spokenPages/SpokenPractice',
},
{
component: '404',
layout: false,

10
package.json

@ -21,11 +21,11 @@
"preview": "npm run build && max preview --port 8000",
"record": "cross-env NODE_ENV=development UMI_ENV=test max record --scene=login",
"serve": "umi-serve",
"start": "cross-env UMI_ENV=dev max dev",
"start:dev": "cross-env UMI_ENV=dev MOCK=none max dev",
"start:no-mock": "cross-env MOCK=none max dev",
"start:pre": "cross-env UMI_ENV=pre MOCK=none max dev",
"start:test": "cross-env UMI_ENV=test MOCK=none max dev",
"start": "cross-env UMI_ENV=dev PORT=8003 max dev",
"start:dev": "cross-env UMI_ENV=dev MOCK=none PORT=8003 max dev",
"start:no-mock": "cross-env MOCK=none PORT=8003 max dev",
"start:pre": "cross-env UMI_ENV=pre MOCK=none PORT=8003 max dev",
"start:test": "cross-env UMI_ENV=test MOCK=none PORT=8003 max dev",
"test": "jest",
"test:coverage": "npm run jest -- --coverage",
"test:update": "npm run jest -- -u",

367
src/pages/spokenPages/SpokenPractice.less

@ -0,0 +1,367 @@
.spoken-practice-layout {
height: 100vh;
background-color: #f5f5f5;
.spoken-practice-header {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 0 24px;
height: 64px;
.header-left {
display: flex;
align-items: center;
}
.app-icon {
background-color: #1890ff;
}
.header-center {
flex: 1;
text-align: center;
.app-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #333;
}
}
.header-right {
display: flex;
align-items: center;
}
.header-button {
border: none;
color: #666;
&:hover {
background-color: #f0f0f0;
color: #1890ff;
}
}
.user-avatar {
background-color: #52c41a;
}
}
.spoken-practice-content {
display: flex;
flex-direction: column;
height: calc(100vh - 64px);
padding: 24px;
gap: 24px;
.conversation-area {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
padding: 24px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
.message-item {
display: flex;
max-width: 70%;
&.user {
align-self: flex-end;
flex-direction: row-reverse;
.message-bubble {
background-color: #1890ff;
color: #fff;
border-radius: 12px 12px 0 12px;
}
}
&.ai {
align-self: flex-start;
.message-bubble {
background-color: #f0f0f0;
color: #333;
border-radius: 12px 12px 12px 0;
}
}
.message-bubble {
padding: 12px 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.message-content {
margin: 0 0 8px 0;
font-size: 14px;
line-height: 1.5;
}
.message-timestamp {
margin: 0;
font-size: 12px;
opacity: 0.7;
text-align: right;
}
}
}
}
.input-area {
display: flex;
gap: 16px;
align-items: flex-end;
padding: 16px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
.text-input {
flex: 1;
border-radius: 8px;
border: 1px solid #d9d9d9;
&:focus {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
.input-buttons {
display: flex;
gap: 8px;
}
.voice-button,
.send-button {
border-radius: 8px;
}
}
.score-area {
padding: 16px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
.score-title {
margin: 0 0 16px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.score-details {
display: flex;
flex-direction: column;
gap: 16px;
.score-item {
display: flex;
align-items: center;
gap: 16px;
.score-label {
width: 120px;
font-size: 14px;
color: #666;
}
.score-progress {
flex: 1;
}
.score-value {
width: 60px;
font-size: 14px;
font-weight: 600;
color: #333;
text-align: right;
}
}
}
}
}
}
.recording-modal {
.ant-modal-content {
background-color: transparent;
box-shadow: none;
border: none;
}
.recording-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 32px;
padding: 48px;
.mic-icon-container {
width: 120px;
height: 120px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
animation: pulse 1.5s infinite;
}
.mic-icon {
font-size: 60px;
color: #ff4d4f;
}
.recording-title {
margin: 0;
font-size: 24px;
font-weight: 600;
color: #fff;
}
.recording-buttons {
display: flex;
gap: 16px;
}
.cancel-button {
width: 120px;
height: 48px;
border-radius: 24px;
font-size: 16px;
background-color: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: #fff;
&:hover {
background-color: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.4);
color: #fff;
}
}
.finish-button {
width: 120px;
height: 48px;
border-radius: 24px;
font-size: 16px;
background-color: #1890ff;
border: none;
&:hover {
background-color: #40a9ff;
}
}
}
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
// 响应式设计
@media (max-width: 768px) {
.spoken-practice-layout {
.spoken-practice-header {
padding: 0 16px;
.app-title {
font-size: 18px;
}
}
.spoken-practice-content {
padding: 16px;
.conversation-area {
max-width: 100%;
padding: 16px;
.message-item {
max-width: 85%;
}
}
.input-area {
padding: 12px;
flex-direction: column;
align-items: stretch;
.input-buttons {
justify-content: flex-end;
}
}
.score-area {
padding: 12px;
.score-details {
.score-item {
flex-direction: column;
align-items: stretch;
gap: 8px;
.score-label {
width: auto;
}
.score-value {
width: auto;
text-align: left;
}
}
}
}
}
}
.recording-modal {
.recording-content {
padding: 32px;
.mic-icon-container {
width: 80px;
height: 80px;
}
.mic-icon {
font-size: 40px;
}
.recording-title {
font-size: 20px;
}
.recording-buttons {
flex-direction: column;
width: 100%;
.cancel-button,
.finish-button {
width: 100%;
}
}
}
}
}

246
src/pages/spokenPages/SpokenPractice.tsx

@ -0,0 +1,246 @@
import {
CloseOutlined,
MicrophoneOutlined,
SendOutlined,
SettingOutlined,
UserOutlined,
} from '@ant-design/icons';
import { Avatar, Button, Input, Layout, Modal, Progress, Space } from 'antd';
import React, { useState } from 'react';
import './SpokenPractice.less';
const { Header, Content } = Layout;
const { TextArea } = Input;
interface Message {
id: number;
content: string;
sender: 'user' | 'ai';
timestamp: string;
}
interface Score {
accuracy: number;
grammar: number;
fluency: number;
}
const SpokenPractice: React.FC = () => {
const [messages, setMessages] = useState<Message[]>([
{
id: 1,
content: '你好!我是你的AI口语练习伙伴。今天想练习什么话题呢?',
sender: 'ai',
timestamp: '10:00',
},
]);
const [inputValue, setInputValue] = useState('');
const [isRecording, setIsRecording] = useState(false);
const [score, setScore] = useState<Score>({
accuracy: 85,
grammar: 90,
fluency: 80,
});
const handleSendMessage = () => {
if (inputValue.trim()) {
const newMessage: Message = {
id: messages.length + 1,
content: inputValue,
sender: 'user',
timestamp: new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
}),
};
setMessages([...messages, newMessage]);
setInputValue('');
// 模拟AI回复
setTimeout(() => {
const aiMessage: Message = {
id: messages.length + 2,
content: '你的回答很好!让我给你一些反馈...',
sender: 'ai',
timestamp: new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
}),
};
setMessages([...messages, newMessage, aiMessage]);
}, 1000);
}
};
const handleStartRecording = () => {
setIsRecording(true);
};
const handleStopRecording = () => {
setIsRecording(false);
// 模拟录音处理
const newMessage: Message = {
id: messages.length + 1,
content: '(语音消息)',
sender: 'user',
timestamp: new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
}),
};
setMessages([...messages, newMessage]);
// 模拟AI回复和评分
setTimeout(() => {
const aiMessage: Message = {
id: messages.length + 2,
content: '你的发音很清晰!让我给你详细的评分反馈。',
sender: 'ai',
timestamp: new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
}),
};
setMessages([...messages, newMessage, aiMessage]);
// 模拟评分
setScore({
accuracy: Math.floor(Math.random() * 20) + 80,
grammar: Math.floor(Math.random() * 20) + 80,
fluency: Math.floor(Math.random() * 20) + 80,
});
}, 1500);
};
const handleCancelRecording = () => {
setIsRecording(false);
};
return (
<Layout className="spoken-practice-layout">
<Header className="spoken-practice-header">
<div className="header-left">
<Avatar
size={40}
icon={<MicrophoneOutlined />}
className="app-icon"
/>
</div>
<div className="header-center">
<h1 className="app-title">AI口语练习</h1>
</div>
<div className="header-right">
<Space size="middle">
<Button
icon={<SettingOutlined />}
ghost
className="header-button"
/>
<Avatar size={40} icon={<UserOutlined />} className="user-avatar" />
</Space>
</div>
</Header>
<Content className="spoken-practice-content">
<div className="conversation-area">
{messages.map((message) => (
<div key={message.id} className={`message-item ${message.sender}`}>
<div className="message-bubble">
<p className="message-content">{message.content}</p>
<p className="message-timestamp">{message.timestamp}</p>
</div>
</div>
))}
</div>
<div className="input-area">
<TextArea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="输入内容..."
rows={3}
className="text-input"
onPressEnter={(e) => {
if (e.ctrlKey || e.metaKey) handleSendMessage();
}}
/>
<Space size="middle" className="input-buttons">
<Button
icon={<MicrophoneOutlined />}
type="primary"
onClick={handleStartRecording}
className="voice-button"
/>
<Button
icon={<SendOutlined />}
type="primary"
onClick={handleSendMessage}
className="send-button"
/>
</Space>
</div>
<div className="score-area">
<h2 className="score-title"></h2>
<div className="score-details">
<div className="score-item">
<span className="score-label"></span>
<Progress
percent={score.accuracy}
strokeColor="#52c41a"
className="score-progress"
/>
<span className="score-value">{score.accuracy}</span>
</div>
<div className="score-item">
<span className="score-label"></span>
<Progress
percent={score.grammar}
strokeColor="#1890ff"
className="score-progress"
/>
<span className="score-value">{score.grammar}</span>
</div>
<div className="score-item">
<span className="score-label"></span>
<Progress
percent={score.fluency}
strokeColor="#faad14"
className="score-progress"
/>
<span className="score-value">{score.fluency}</span>
</div>
</div>
</div>
</Content>
<Modal
visible={isRecording}
footer={null}
closable={false}
maskStyle={{ backgroundColor: 'rgba(0, 0, 0, 0.8)' }}
className="recording-modal"
>
<div className="recording-content">
<div className="mic-icon-container">
<MicrophoneOutlined className="mic-icon" />
</div>
<h2 className="recording-title"></h2>
<div className="recording-buttons">
<Button
icon={<CloseOutlined />}
onClick={handleCancelRecording}
className="cancel-button"
>
</Button>
<Button
type="primary"
onClick={handleStopRecording}
className="finish-button"
>
</Button>
</div>
</div>
</Modal>
</Layout>
);
};
export default SpokenPractice;
Loading…
Cancel
Save