]> acesimba.cloud Git - words.git/commitdiff
feat: 新增组别只读预览功能
authorwords <words@local>
Thu, 30 Jul 2026 01:02:30 +0000 (09:02 +0800)
committerwords <words@local>
Thu, 30 Jul 2026 01:02:30 +0000 (09:02 +0800)
- 新建 GroupView 组件: 点击首页 Group 名称进入 /group/:groupId
- 上方显示该组思维导图, 下方完整词汇表(单词/词性/释义/词根/词缀/例句)
- 纯查看, 不触发学习/测试/复习记录
- App.js 加路由, Home.js 组名单元格改为可点击
- 响应式: 移动端表格可横向滚动

frontend/src/App.js
frontend/src/components/GroupView.css [new file with mode: 0644]
frontend/src/components/GroupView.js [new file with mode: 0644]
frontend/src/components/Home.js

index 8d74f68c7bdb7036567bf4ef88f1de3dbe1f7708..ff960f27a8fbcc18a36dc1809c37ddc4b0020f41 100644 (file)
@@ -3,6 +3,7 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
 import Home from './components/Home';
 import StudyMode from './components/StudyMode';
 import TestMode from './components/TestMode';
+import GroupView from './components/GroupView';
 
 function App() {
     return (
@@ -11,6 +12,7 @@ function App() {
                 <Route path="/" element={<Home />} />
                 <Route path="/study/:groupId" element={<StudyMode />} />
                 <Route path="/test/:groupId" element={<TestMode />} />
+                <Route path="/group/:groupId" element={<GroupView />} />
             </Routes>
         </Router>
     );
diff --git a/frontend/src/components/GroupView.css b/frontend/src/components/GroupView.css
new file mode 100644 (file)
index 0000000..bc1d72f
--- /dev/null
@@ -0,0 +1,133 @@
+/* GroupView.css - 组别只读预览(上图下表) */
+
+.group-view-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    padding: 20px;
+    background-color: #f0f7ff;
+    min-height: 100vh;
+}
+
+.group-view-card {
+    display: flex;
+    flex-direction: column;
+    background: white;
+    border-radius: 20px;
+    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+    width: 95%;
+    max-width: 1100px;
+    overflow: hidden;
+}
+
+.group-view-header {
+    text-align: center;
+    padding: 24px 20px 12px;
+}
+
+.group-view-header h1 {
+    font-size: 22px;
+    color: #1e40af;
+    margin: 0 0 6px;
+}
+
+.group-view-count {
+    color: #475569;
+    font-size: 14px;
+    margin: 0;
+}
+
+/* 思维导图(上方) */
+.group-image-section {
+    background: #fafafa;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 16px;
+    border-bottom: 1px solid #eee;
+}
+
+.group-image-section img {
+    max-width: 100%;
+    max-height: 520px;
+    object-fit: contain;
+}
+
+/* 词汇列表(下方,可横向滚动) */
+.group-table-wrap {
+    overflow-x: auto;
+    padding: 12px 16px 20px;
+}
+
+.group-word-table {
+    width: 100%;
+    border-collapse: collapse;
+    table-layout: fixed;
+    min-width: 760px;
+}
+
+.group-word-table th {
+    background-color: #eff6ff;
+    color: #1e40af;
+    font-size: 14px;
+    padding: 12px 10px;
+    text-align: left;
+    border-bottom: 2px solid #dbeafe;
+    white-space: nowrap;
+}
+
+.group-word-table td {
+    padding: 12px 10px;
+    font-size: 14px;
+    color: #1f2937;
+    border-bottom: 1px solid #f1f5f9;
+    vertical-align: top;
+    word-break: break-word;
+}
+
+.group-word-table tr:hover td {
+    background-color: #f8fafc;
+}
+
+/* 列宽分配 */
+.group-word-table th:nth-child(1),
+.group-word-table td:nth-child(1) { width: 15%; font-weight: 600; color: #1d4ed8; }
+.group-word-table th:nth-child(2),
+.group-word-table td:nth-child(2) { width: 8%; }
+.group-word-table th:nth-child(3),
+.group-word-table td:nth-child(3) { width: 22%; }
+.group-word-table th:nth-child(4),
+.group-word-table td:nth-child(4) { width: 10%; color: #64748b; }
+.group-word-table th:nth-child(5),
+.group-word-table td:nth-child(5) { width: 20%; color: #64748b; font-size: 13px; }
+.group-word-table th:nth-child(6),
+.group-word-table td:nth-child(6) { width: 25%; color: #475569; font-size: 13px; }
+
+/* 返回首页按钮 */
+.back-home {
+    margin-top: 20px;
+    background: none;
+    border: none;
+    color: #64748b;
+    text-decoration: underline;
+    cursor: pointer;
+    font-size: 14px;
+}
+
+/* 响应式 */
+@media (max-width: 768px) {
+    .group-view-card {
+        width: 98%;
+    }
+    .group-view-header h1 {
+        font-size: 20px;
+    }
+    .group-image-section img {
+        max-height: 360px;
+    }
+    .group-word-table th,
+    .group-word-table td {
+        padding: 10px 8px;
+        font-size: 13px;
+    }
+}
diff --git a/frontend/src/components/GroupView.js b/frontend/src/components/GroupView.js
new file mode 100644 (file)
index 0000000..c0cc8ff
--- /dev/null
@@ -0,0 +1,78 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import axios from 'axios';
+import './GroupView.css';
+
+const API_BASE = "http://124.222.157.124:1060";
+
+function GroupView() {
+    const { groupId } = useParams();
+    const navigate = useNavigate();
+    const [words, setWords] = useState([]);
+
+    useEffect(() => {
+        axios.get(`${API_BASE}/api/words/${groupId}`)
+            .then(res => setWords(res.data))
+            .catch(err => console.error("获取词汇失败:", err));
+    }, [groupId]);
+
+    // 组内所有单词共享同一张思维导图,取首词的 picture_id(兼容 picture_id==groupid 约定)
+    const pictureId = words.length > 0 ? words[0].picture_id : null;
+
+    if (words.length === 0) {
+        return (
+            <div className="group-view-container">
+                <div className="group-view-card">
+                    <p className="loading">加载中...</p>
+                </div>
+            </div>
+        );
+    }
+
+    return (
+        <div className="group-view-container">
+            <div className="group-view-card">
+                <div className="group-view-header">
+                    <h1>Group {groupId} · 词汇预览</h1>
+                    <p className="group-view-count">共 {words.length} 个单词(只读预览,不记录进度)</p>
+                </div>
+
+                {pictureId && (
+                    <div className="group-image-section">
+                        <img src={`${API_BASE}/api/picture/${pictureId}`} alt="mindmap" />
+                    </div>
+                )}
+
+                <div className="group-table-wrap">
+                    <table className="group-word-table">
+                        <thead>
+                            <tr>
+                                <th>单词</th>
+                                <th>词性</th>
+                                <th>中文释义</th>
+                                <th>词根</th>
+                                <th>词缀</th>
+                                <th>例句</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            {words.map(w => (
+                                <tr key={w.id}>
+                                    <td className="gw-word">{w.word}</td>
+                                    <td className="gw-pos">{w.part_of_speech}</td>
+                                    <td className="gw-cn">{w.chinese_meaning}</td>
+                                    <td className="gw-root">{w.word_root}</td>
+                                    <td className="gw-affix">{w.affix}</td>
+                                    <td className="gw-ex">{w.example_sentence}</td>
+                                </tr>
+                            ))}
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+            <button className="back-home" onClick={() => navigate('/')}>返回首页</button>
+        </div>
+    );
+}
+
+export default GroupView;
index 2f00eb63985181b7fcc2c312ffe0307a4ddbc9b3..4778bf2422d6ba38cd0dee8b03dec4449017ffd3 100644 (file)
@@ -46,6 +46,10 @@ function Home() {
 
     const handleCellClick = (item, type) => {
         const groupId = item.group.replace('Group ', '');
+        if (type === 'group') {
+            navigate(`/group/${groupId}`);
+            return;
+        }
         if (type === 'study' && item.study && item.study.includes('请开始学习')) {
             navigate(`/study/${groupId}`);
         }
@@ -93,7 +97,13 @@ function Home() {
                     <tbody>
                         {filteredData.map((item, index) => (
                             <tr key={index}>
-                                <td className="group-name">{item.group}</td>
+                                <td
+                                    className="group-name"
+                                    onClick={() => handleCellClick(item, 'group')}
+                                    style={{ cursor: 'pointer' }}
+                                >
+                                    {item.group}
+                                </td>
                                 <td 
                                     className={getStatusClass(item.study)}
                                     onClick={() => handleCellClick(item, 'study')}