From d6fe22e0e56bbe31aa2a3cef7e3799feee805ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=90=E9=98=B3?= <2260837959@qq.com> Date: Mon, 1 Dec 2025 14:19:03 +0800 Subject: [PATCH] =?UTF-8?q?trae03=20=E5=8F=A3=E8=AF=AD=E7=BB=83=E4=B9=A0?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/routes.ts | 8 +- package.json | 10 +- src/pages/spokenPages/SpokenPractice.less | 367 ++++++++++++++++++++++ src/pages/spokenPages/SpokenPractice.tsx | 246 +++++++++++++++ 4 files changed, 625 insertions(+), 6 deletions(-) create mode 100644 src/pages/spokenPages/SpokenPractice.less create mode 100644 src/pages/spokenPages/SpokenPractice.tsx diff --git a/config/routes.ts b/config/routes.ts index fc82e6db..abf80634 100644 --- a/config/routes.ts +++ b/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, diff --git a/package.json b/package.json index b49a5b8d..9837fedf 100644 --- a/package.json +++ b/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", diff --git a/src/pages/spokenPages/SpokenPractice.less b/src/pages/spokenPages/SpokenPractice.less new file mode 100644 index 00000000..8a601347 --- /dev/null +++ b/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%; + } + } + } + } +} \ No newline at end of file diff --git a/src/pages/spokenPages/SpokenPractice.tsx b/src/pages/spokenPages/SpokenPractice.tsx new file mode 100644 index 00000000..8d446277 --- /dev/null +++ b/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([ + { + id: 1, + content: '你好!我是你的AI口语练习伙伴。今天想练习什么话题呢?', + sender: 'ai', + timestamp: '10:00', + }, + ]); + const [inputValue, setInputValue] = useState(''); + const [isRecording, setIsRecording] = useState(false); + const [score, setScore] = useState({ + 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 ( + +
+
+ } + className="app-icon" + /> +
+
+

AI口语练习

+
+
+ +
+
+ +
+ {messages.map((message) => ( +
+
+

{message.content}

+

{message.timestamp}

+
+
+ ))} +
+
+