Browse Source

Update 添加题目收藏删除功能

Yue 11 months ago
parent
commit
850ba0d5a5

+ 112 - 6
app/core/db/database.py

@@ -63,6 +63,8 @@ class Database:
             question TEXT NOT NULL,
             numbers TEXT NOT NULL,
             answers TEXT NOT NULL,
+            is_favorite BOOLEAN DEFAULT 0,
+            is_deleted BOOLEAN DEFAULT 0,
             created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
         )
         """)
@@ -129,14 +131,15 @@ class Database:
         record = history_dto.to_db_record()
         current_time = datetime.now()
         cursor.execute(
-            "INSERT INTO history (game_type, question, numbers, answers, created_at) VALUES (?, ?, ?, ?, ?)",
-            (record['game_type'], record['question'], record['numbers'], record['answers'],current_time)
+            "INSERT INTO history (game_type, question, numbers, answers, created_at, is_favorite, is_deleted) VALUES (?, ?, ?, ?, ?, ?, ?)",
+            (record['game_type'], record['question'], record['numbers'], record['answers'], current_time, 
+             record.get('is_favorite', False), record.get('is_deleted', False))
         )
         
         conn.commit()
         return cursor.lastrowid
     
-    def get_history(self, game_type: Optional[str] = None,start_date: Optional[str] = None, end_date: Optional[str] = None, page: int = 1, page_size: int = 10) -> Tuple[List[Dict[str, Any]], int]:
+    def get_history(self, game_type: Optional[str] = None, start_date: Optional[str] = None, end_date: Optional[str] = None, page: int = 1, page_size: int = 10, only_favorite: bool = False, include_deleted: bool = False) -> Tuple[List[Dict[str, Any]], int]:
         """获取历史记录,支持分页查询
         
         Args:
@@ -145,6 +148,8 @@ class Database:
             end_date: 结束日期,格式为 YYYY-MM-DD
             page: 页码,从1开始
             page_size: 每页记录数
+            only_favorite: 是否只查询收藏的记录
+            include_deleted: 是否包含已删除的记录
             
         Returns:
             Tuple[List[Dict[str, Any]], int]: 历史记录列表和总记录数
@@ -157,8 +162,13 @@ class Database:
         params = []
 
         if game_type:
-            base_query += " AND game_type = ?"
-            params.append(game_type)
+            if game_type == '0':
+                base_query += " AND is_deleted = 1"
+                include_deleted=True
+            else:
+                base_query += " AND game_type = ?"
+                params.append(game_type)
+            
         # 添加日期过滤条件
         if start_date or end_date:
             if start_date:
@@ -168,6 +178,13 @@ class Database:
                 base_query += " AND date(created_at) <= ?"
                 params.append(end_date)
         
+        # 添加收藏和删除过滤条件
+        if only_favorite:
+            base_query += " AND is_favorite = 1"
+        
+        if not include_deleted:
+            base_query += " AND is_deleted = 0"
+        
         # 先查询总记录数
         count_query = f"SELECT COUNT(*) {base_query}"
         cursor.execute(count_query, params)
@@ -332,4 +349,93 @@ class Database:
         cursor.execute("SELECT COUNT(*) FROM game_data")
         count = cursor.fetchone()[0]
         
-        return count > 0
+        return count > 0
+
+    def toggle_favorite(self, history_id: int) -> bool:
+        """切换历史记录的收藏状态
+        
+        Args:
+            history_id: 历史记录ID
+            
+        Returns:
+            bool: 操作是否成功
+        """
+        conn = self.get_connection()
+        cursor = conn.cursor()
+        
+        try:
+            # 先查询当前收藏状态
+            cursor.execute("SELECT is_favorite FROM history WHERE id = ?", (history_id,))
+            result = cursor.fetchone()
+            
+            if not result:
+                return False  # 记录不存在
+                
+            current_state = bool(result[0])
+            new_state = not current_state
+            
+            # 更新收藏状态
+            cursor.execute("UPDATE history SET is_favorite = ? WHERE id = ?", (int(new_state), history_id))
+            conn.commit()
+            
+            return True
+        except Exception as e:
+            print(f"切换收藏状态失败: {e}")
+            conn.rollback()
+            return False
+
+    def toggle_delete(self, history_id: int) -> bool:
+        """切换历史记录的删除状态
+
+        Args:
+            history_id: 历史记录ID
+
+        Returns:
+            bool: 操作是否成功
+        """
+        conn = self.get_connection()
+        cursor = conn.cursor()
+
+        try:
+            # 先查询当前收藏状态
+            cursor.execute("SELECT is_deleted FROM history WHERE id = ?", (history_id,))
+            result = cursor.fetchone()
+
+            if not result:
+                return False  # 记录不存在
+
+            current_state = bool(result[0])
+            new_state = not current_state
+
+            # 更新收藏状态
+            cursor.execute("UPDATE history SET is_deleted = ? WHERE id = ?", (int(new_state), history_id))
+            conn.commit()
+
+            return True
+        except Exception as e:
+            print(f"切换删除状态失败: {e}")
+            conn.rollback()
+            return False
+            
+    def soft_delete(self, history_id: int) -> bool:
+        """软删除历史记录
+        
+        Args:
+            history_id: 历史记录ID
+            
+        Returns:
+            bool: 操作是否成功
+        """
+        conn = self.get_connection()
+        cursor = conn.cursor()
+        
+        try:
+            # 更新删除状态
+            cursor.execute("UPDATE history SET is_deleted = 1 WHERE id = ?", (history_id,))
+            conn.commit()
+            
+            return cursor.rowcount > 0
+        except Exception as e:
+            print(f"软删除记录失败: {e}")
+            conn.rollback()
+            return False

+ 40 - 2
app/core/history/history_manager.py

@@ -41,7 +41,7 @@ class HistoryManager:
     
 
     @staticmethod
-    def get_history(game_type: Optional[str] = None,start_date: Optional[str] = None, end_date: Optional[str] = None, page: int = 1, page_size: int = 10) :
+    def get_history(game_type: Optional[str] = None, start_date: Optional[str] = None, end_date: Optional[str] = None, page: int = 1, page_size: int = 10, only_favorite: bool = False, include_deleted: bool = False) :
         """获取历史记录,支持分页查询
         
         Args:
@@ -50,12 +50,14 @@ class HistoryManager:
             end_date: 结束日期,格式为 YYYY-MM-DD
             page: 页码,从1开始
             page_size: 每页记录数
+            only_favorite: 是否只查询收藏的记录
+            include_deleted: 是否包含已删除的记录
             
         Returns:
             Tuple[List[Dict[str, Any]], int]: 历史记录DTO列表和总记录数
         """
         # 从数据库获取历史记录,使用分页
-        db_records, total_count = db.get_history(game_type,start_date, end_date, page, page_size)
+        db_records, total_count = db.get_history(game_type, start_date, end_date, page, page_size, only_favorite, include_deleted)
         
         # 转换为DTO对象
         history_records = []
@@ -70,3 +72,39 @@ class HistoryManager:
                 history_records.append(record)
         
         return history_records, total_count
+    
+    @staticmethod
+    def toggle_favorite(history_id: int) -> bool:
+        """切换历史记录的收藏状态
+        
+        Args:
+            history_id: 历史记录ID
+            
+        Returns:
+            bool: 操作是否成功
+        """
+        return db.toggle_favorite(history_id)
+
+    @staticmethod
+    def toggle_delete(history_id: int) -> bool:
+        """切换历史记录的删除状态
+
+        Args:
+            history_id: 历史记录ID
+
+        Returns:
+            bool: 操作是否成功
+        """
+        return db.toggle_delete(history_id)
+    
+    @staticmethod
+    def soft_delete(history_id: int) -> bool:
+        """软删除历史记录
+        
+        Args:
+            history_id: 历史记录ID
+            
+        Returns:
+            bool: 操作是否成功
+        """
+        return db.soft_delete(history_id)

BIN
app/db/game_data.db


BIN
app/game_data.db


+ 2 - 0
app/models/history.py

@@ -22,6 +22,8 @@ class History(BaseModel):
     numbers: str
     answers: str
     created_at: Optional[datetime] = None
+    is_favorite: bool = False
+    is_deleted: bool = False
     
 
     class Config:

+ 72 - 5
app/routes/api.py

@@ -32,8 +32,8 @@ async def get_question(t: str) -> Dict[str, Any]:
         result = question.generate()
         
         # 记录历史(只有生成题目时记录)
-        history_manager.record_question(result)
-        
+        record_id = history_manager.record_question(result)
+        result["record_id"]=record_id
         return result
     except ValueError as e:
         raise HTTPException(status_code=400, detail=str(e))
@@ -125,23 +125,30 @@ async def get_history(
     start_date: Optional[str] = Query(None, description="开始日期,格式为YYYY-MM-DD"),
     end_date: Optional[str] = Query(None, description="结束日期,格式为YYYY-MM-DD"),
     page: int = Query(1, description="页码,从1开始"),
-    page_size: int = Query(10, description="每页记录数")
+    page_size: int = Query(10, description="每页记录数"),
+    only_favorite: bool = Query(False, description="是否只查询收藏的记录"),
+    include_deleted: bool = Query(False, description="是否包含已删除的记录")
 ):
     """
     获取历史记录
     
     参数:
+        game_type: 游戏类型,可选值为A, B, C, D, E
         start_date: 开始日期,格式为YYYY-MM-DD
         end_date: 结束日期,格式为YYYY-MM-DD
         page: 页码,从1开始
         page_size: 每页记录数
+        only_favorite: 是否只查询收藏的记录
+        include_deleted: 是否包含已删除的记录
         
     返回:
         历史记录列表
     """
     try:
         # 使用分页直接从数据库获取当前页的历史记录
-        current_page_items, total_count = history_manager.get_history(game_type,start_date, end_date, page, page_size)
+        current_page_items, total_count = history_manager.get_history(
+            game_type, start_date, end_date, page, page_size, only_favorite, include_deleted
+        )
         
         return {
             "history": current_page_items,
@@ -151,4 +158,64 @@ async def get_history(
             "total_pages": (total_count + page_size - 1) // page_size
         }
     except Exception as e:
-        raise HTTPException(status_code=500, detail=f"获取历史记录失败: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"获取历史记录失败: {str(e)}")
+
+@router.post("/history/{history_id}/favorite")
+async def toggle_favorite(history_id: int):
+    """
+    切换历史记录的收藏状态
+    
+    参数:
+        history_id: 历史记录ID
+        
+    返回:
+        操作结果
+    """
+    try:
+        success = history_manager.toggle_favorite(history_id)
+        if success:
+            return {"success": True, "message": "收藏状态已更新"}
+        else:
+            raise HTTPException(status_code=404, detail="历史记录不存在或操作失败")
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=f"更新收藏状态失败: {str(e)}")
+
+
+@router.post("/history/{history_id}/delete")
+async def toggle_delete(history_id: int):
+    """
+    切换历史记录的收藏状态
+
+    参数:
+        history_id: 历史记录ID
+
+    返回:
+        操作结果
+    """
+    try:
+        success = history_manager.toggle_delete(history_id)
+        if success:
+            return {"success": True, "message": "删除状态已更新"}
+        else:
+            raise HTTPException(status_code=404, detail="历史记录不存在或操作失败")
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=f"更新删除状态失败: {str(e)}")
+@router.delete("/history/{history_id}")
+async def delete_history(history_id: int):
+    """
+    软删除历史记录
+    
+    参数:
+        history_id: 历史记录ID
+        
+    返回:
+        操作结果
+    """
+    try:
+        success = history_manager.soft_delete(history_id)
+        if success:
+            return {"success": True, "message": "历史记录已删除"}
+        else:
+            raise HTTPException(status_code=404, detail="历史记录不存在或操作失败")
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=f"删除历史记录失败: {str(e)}")

BIN
app/static/favicon.ico


+ 13 - 0
app/static/index.html

@@ -5,6 +5,7 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>数学24点游戏</title>
+    <link rel="icon" href="/static/favicon.ico"></link>
     <link rel="stylesheet" href="/static/styles.css">
 </head>
 
@@ -19,6 +20,7 @@
                     <option value="3">一星数据</option>
                     <option value="4">二星数据</option>
                     <option value="5">历史记录</option>
+                    <option value="6">我的收藏</option>
                 </select>
             </div>
         </div>
@@ -151,6 +153,7 @@
                         <option value="C">一题多解2</option>
                         <option value="D">一星题目</option>
                         <option value="E">二星题目</option>
+                        <option value="0">已删除</option>
                     </select>
                 </div>
                 <div class="search-input">
@@ -170,6 +173,16 @@
                 <button id="history-load-more" class="load-more-btn" style="display: none;">加载更多</button>
             </div>
         </div>
+        <!-- 我的收藏 -->
+        <div class="module module-6 module-container" style="display: none;">
+            <h2>我的收藏</h2>
+            <div class="history-data-container">
+                <div id="favorite-container" class="history-cards-container">
+                    <div class="placeholder">暂无收藏记录</div>
+                </div>
+                <button id="favorite-load-more" class="load-more-btn" style="display: none;">加载更多</button>
+            </div>
+        </div>
     </div>
 
     <!-- 使用模块化的JavaScript结构 -->

+ 297 - 98
app/static/script.js

@@ -22,6 +22,10 @@ document.addEventListener('DOMContentLoaded', function () {
     const historyContainer = document.getElementById('history-container');
     const historyLoadMoreBtn = document.getElementById('history-load-more');
 
+    // 获取DOM元素 - 我的收藏模块
+    const favoriteContainer = document.getElementById('favorite-container');
+    const favoriteLoadMoreBtn = document.getElementById('favorite-load-more');
+
     // 获取DOM元素 - 出题模块
     const problemContent = document.getElementById('problem-content');
     const problemAnswer = document.getElementById('problem-answer');
@@ -41,7 +45,14 @@ document.addEventListener('DOMContentLoaded', function () {
             if (value > 13) this.value = 13;
         });
     });
-
+    // 定义SVG图标
+    const svgIcons = {
+        star: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>`,
+        starFilled: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>`,
+        trash: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>`,
+        eye: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>`,
+        eyeOff: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>`
+    };
     // 存储当前解法和问题数据
     let currentSolutions = [];
     let currentProblem = null;
@@ -69,21 +80,32 @@ document.addEventListener('DOMContentLoaded', function () {
             console.log('currentProblem:', currentProblem.answers)
             if (problemAnswer.style.display === 'block') {
                 problemAnswer.style.display = 'none';
-                showAnswerBtn.textContent = '显示答案';
+                showAnswerBtn.innerHTML = '显示答案';
             } else {
                 problemAnswer.style.display = 'block';
                 renderProblemAnswers(currentProblem.answers);
-                showAnswerBtn.textContent = '隐藏答案';
+                showAnswerBtn.innerHTML = '隐藏答案';
             }
 
         }
     });
 
+    // 初始化显示答案按钮图标
+    if (showAnswerBtn) {
+        showAnswerBtn.innerHTML = '显示答案';
+    }
+
     // 为计算按钮添加点击事件
     calculateBtn.addEventListener('click', fetchSolutions);
 
     // 为历史记录查询按钮添加点击事件
-    historySearchBtn.addEventListener('click', ()=>{fetchHistory(false)});
+    historySearchBtn.addEventListener('click', () => { fetchHistory(false) });
+
+    // 为历史记录加载更多按钮添加点击事件
+    historyLoadMoreBtn.addEventListener('click', () => fetchHistory(true));
+
+    // 为收藏加载更多按钮添加点击事件
+    favoriteLoadMoreBtn.addEventListener('click', () => fetchFavorites(true));
 
     // 根据题型生成题目
     let answerTimer = null;
@@ -113,13 +135,17 @@ document.addEventListener('DOMContentLoaded', function () {
             if (response.ok && data.question) {
                 // 保存当前问题数据
                 currentProblem = data;
-
+                problemContent.dataset.id = currentProblem.record_id;
                 // 显示问题内容
                 problemContent.innerHTML = `<div class="problem-text">
-                <div class="timer-container">
-                    <span id="timer-display" class="timer-display">00:00</span>
+                <div class="pre-container">
+                    <button  type="button" class="icon-btn favorite-btn" title="添加收藏">${svgIcons.star}</button>
+                    <div class="timer-container">
+                        <span id="timer-display" class="timer-display">00:00</span>
+                    </div>
                 </div>
-                ${data.question}</div>`;
+                ${data.question}
+                </div>`;
 
                 // 如果有数字,显示数字
                 if (data.numbers) {
@@ -137,7 +163,7 @@ document.addEventListener('DOMContentLoaded', function () {
 
                 // 显示答案按钮
                 answerTimer = setTimeout(() => {
-                    showAnswerBtn.textContent = '显示答案';
+                    showAnswerBtn.innerHTML = ' 显示答案';
                     showAnswerBtn.style.display = 'block';
                 }, 10 * 1000);
             } else {
@@ -148,6 +174,38 @@ document.addEventListener('DOMContentLoaded', function () {
             problemContent.innerHTML = '<p class="error">请求失败,请检查网络连接</p>';
         }
 
+        // 为收藏按钮添加点击事件
+        problemContent.querySelectorAll('.favorite-btn').forEach((btn, index) => {
+            btn.addEventListener('click', async function () {
+                const card = this.closest('.problem-content'), is_favorite = this.classList.contains('active');
+                const recordId = card.dataset.id;
+
+                try {
+                    const response = await fetch(`/api/history/${recordId}/favorite`, {
+                        method: 'POST'
+                    });
+
+                    if (response.ok) {
+                        if (is_favorite) {
+                            this.innerHTML = svgIcons.star;
+                            this.classList.remove('active');
+                            alert('取消成功');
+                        } else {
+                            this.innerHTML = svgIcons.starFilled;
+                            this.classList.add('active');
+                            alert('收藏成功');
+                        }
+                    } else {
+                        alert('收藏失败,请重试');
+                    }
+                } catch (error) {
+                    console.error('收藏操作失败:', error);
+                    alert('收藏失败,请检查网络连接');
+                }
+            });
+        });
+
+
         // 开始计时器
         function startTimer() {
             // 清除之前的计时器
@@ -325,7 +383,7 @@ document.addEventListener('DOMContentLoaded', function () {
     // 为二星数据查询按钮添加点击事件
     flag2SearchBtn.addEventListener('click', async function () {
         const min = flag2MinInput.value || 1;
-       await  fetchFlagData(2, min, flag2Table);
+        await fetchFlagData(2, min, flag2Table);
     });
 
     // 获取一星/二星数据
@@ -452,6 +510,11 @@ document.addEventListener('DOMContentLoaded', function () {
         if (this.value === '5' && historyContainer) {
             fetchHistory(false);
         }
+
+        // 切换到我的收藏模块时加载数据
+        if (this.value === '6' && favoriteContainer) {
+            fetchFavorites(false);
+        }
     });
 
     // 初始化默认显示模块
@@ -464,6 +527,11 @@ document.addEventListener('DOMContentLoaded', function () {
     let isLoadingHistory = false;
     let hasMoreHistory = true;
 
+    // 收藏记录相关变量
+    let currentFavoritePage = 1;
+    let isLoadingFavorite = false;
+    let hasMoreFavorite = true;
+
 
     // 获取历史记录数据
     async function fetchHistory(loadMore = false) {
@@ -495,14 +563,15 @@ document.addEventListener('DOMContentLoaded', function () {
                 params.append('end_date', historyEndDateInput.value);
             }
             params.append('page', currentHistoryPage.toString());
-            params.append('page_size', '10');
+            params.append('page_size', '5');
 
             // 调用API获取历史记录
             const response = await fetch(`/api/history?${params.toString()}`);
             const data = await response.json();
 
             if (response.ok && data.history) {
-                renderHistoryData(data.history, loadMore);
+                // 渲染历史记录
+                renderHistoryData(data.history, historyContainer, loadMore, false);
 
                 // 更新分页状态
                 hasMoreHistory = currentHistoryPage < data.total_pages;
@@ -529,19 +598,72 @@ document.addEventListener('DOMContentLoaded', function () {
             }
         }
     }
+    // 获取收藏记录数据
+    async function fetchFavorites(loadMore = false) {
+        if (isLoadingFavorite) return;
+        isLoadingFavorite = true;
+
+        // 如果不是加载更多,则重置状态
+        if (!loadMore) {
+            currentFavoritePage = 1;
+            hasMoreFavorite = true;
+            favoriteContainer.innerHTML = '<div class="placeholder">正在加载收藏记录...</div>';
+            favoriteLoadMoreBtn.style.display = 'none';
+        } else {
+            // 显示加载中状态
+            favoriteLoadMoreBtn.textContent = '加载中...';
+            favoriteLoadMoreBtn.disabled = true;
+        }
+
+        try {
+            // 构建查询参数
+            const params = new URLSearchParams();
+            params.append('page', currentFavoritePage);
+            params.append('page_size', 5);
+            params.append('only_favorite', true);
+            params.append('include_deleted', true);
+
+            // 调用API获取收藏记录
+            const response = await fetch(`/api/history?${params.toString()}`);
+            const data = await response.json();
 
-    // 渲染历史记录数据
-    function renderHistoryData(historyItems, append = false) {
+            if (response.ok) {
+                // 渲染历史记录
+                renderHistoryData(data.history, favoriteContainer, loadMore, true);
+                // 更新分页状态
+                hasMoreFavorite = currentFavoritePage < data.total_pages;
+                favoriteLoadMoreBtn.style.display = hasMoreFavorite ? 'block' : 'none';
+                // 更新分页状态
+                currentFavoritePage++;
+            } else {
+                if (!loadMore) {
+                    favoriteContainer.innerHTML = `<div class="error">${data.detail || '获取收藏记录失败'}</div>`;
+                }
+            }
+        } catch (error) {
+            console.error('Error:', error);
+            if (!loadMore) {
+                favoriteContainer.innerHTML = '<div class="error">请求失败,请检查网络连接</div>';
+            }
+        } finally {
+            isLoadingFavorite = false;
+            if (loadMore) {
+                favoriteLoadMoreBtn.textContent = '加载更多';
+                favoriteLoadMoreBtn.disabled = false;
+            }
+        }
+    }
+
+    function renderHistoryData(historyItems, container, append = false, isFavorite = false) {
         if (!historyItems || historyItems.length === 0) {
             if (!append) {
-                historyContainer.innerHTML = '<div class="placeholder">没有找到历史记录</div>';
+                container.innerHTML = `<div class="placeholder">${isFavorite ? '暂无收藏记录' : '没有找到历史记录'}</div>`;
             }
             return;
         }
 
         // 构建卡片内容
         let cardsHTML = '';
-
         historyItems.forEach(item => {
             // 格式化日期时间
             const date = new Date(item.created_at);
@@ -563,13 +685,13 @@ document.addEventListener('DOMContentLoaded', function () {
             }
 
             // 解析答案数据
-            let answersContent = '';
             let answers = null;
 
             try {
                 // 尝试解析JSON字符串
                 if (item.answers && typeof item.answers === 'string') {
-                    answers = JSON.parse(item.answers);
+                    const jsonString = item.answers.replace(/'/g, '"');
+                    answers = JSON.parse(jsonString);
                 } else if (item.answers) {
                     answers = item.answers;
                 }
@@ -577,36 +699,40 @@ document.addEventListener('DOMContentLoaded', function () {
                 // 如果解析失败,直接使用原始字符串
                 answers = item.answers;
             }
-
             // 构建卡片HTML
             cardsHTML += `
-            <div class="history-card ${typeClass}">
+            <div class="history-card ${typeClass}" data-id="${item.id}">
                 <div class="history-card-header">
                     <span class="history-card-type">${typeText}</span>
-                    <button class="secondary-btn show-history-answer-btn">显示答案</button>
+                    <div class="history-card-actions">
+                        <button type="button" class="icon-btn show-history-answer-btn" title="显示答案">${svgIcons.eye}</button>
+                        ${item.is_favorite ? `<button  type="button" class="icon-btn favorite-btn active" title="取消收藏">${svgIcons.starFilled}</button>` : `<button  type="button" class="icon-btn favorite-btn" title="添加收藏">${svgIcons.star}</button>`}
+                        ${item.is_deleted ? `<button  type="button" class="icon-btn delete-btn active" title="恢复">${svgIcons.trash}</button>` : isFavorite ? '' : `<button class="icon-btn delete-btn" title="删除">${svgIcons.trash}</button>`}
+                    </div>
                     <span class="history-card-time">${formattedDate}</span>
                 </div>
                 <div class="history-card-content">
                     <div class="problem-text">${item.question || ''}</div>
                     ${item.numbers ? `<div class="problem-numbers">
-                        ${item.numbers.split(',').map(num => `<span class="problem-number">${num.trim()==='None'?"?":num.trim()}</span>`).join('')}
+                        ${item.numbers.split(',').map(num => `<span class="problem-number">${num.trim() === 'None' ? "?" : num.trim()}</span>`).join('')}
                     </div>` : ''}
                 </div>
-                <div class="history-card-answers" style="display: none;"></div>
-                
+                <div class="history-card-answers" style="display: none;">
+                ${renderHistoryAnswers(answers)}
+                </div>
             </div>
             `;
         });
 
         // 如果是追加模式,则添加到现有内容后面
         if (append) {
-            historyContainer.innerHTML += cardsHTML;
+            container.innerHTML += cardsHTML;
         } else {
-            historyContainer.innerHTML = cardsHTML;
+            container.innerHTML = cardsHTML;
         }
 
         // 为所有显示答案按钮添加点击事件
-        document.querySelectorAll('.show-history-answer-btn').forEach((btn, index) => {
+        container.querySelectorAll('.show-history-answer-btn').forEach((btn, index) => {
             btn.addEventListener('click', function () {
                 const card = this.closest('.history-card');
                 const answersContainer = card.querySelector('.history-card-answers');
@@ -614,96 +740,155 @@ document.addEventListener('DOMContentLoaded', function () {
                 if (answersContainer.style.display === 'block') {
                     // 隐藏答案
                     answersContainer.style.display = 'none';
-                    this.textContent = '显示答案';
+                    this.innerHTML = svgIcons.eye;
+                    this.title = '显示答案';
                 } else {
                     // 显示答案
                     answersContainer.style.display = 'block';
-                    this.textContent = '隐藏答案';
-
-                    // 如果答案容器为空,则渲染答案内容
-                    if (!answersContainer.innerHTML.trim()) {
-                        const historyItem = historyItems[index];
-                        let answers = null;
-
-                        try {
-                            // 尝试解析JSON字符串
-                            if (historyItem.answers && typeof historyItem.answers === 'string') {
-                                const jsonString = historyItem.answers.replace(/'/g, '"');
-                                answers = JSON.parse(jsonString);
-                            } else if (historyItem.answers) {
-                                answers = historyItem.answers;
+                    this.innerHTML = svgIcons.eyeOff;
+                    this.title = '隐藏答案';
+                }
+            });
+        });
+
+        // 为收藏按钮添加点击事件
+        container.querySelectorAll('.favorite-btn').forEach((btn, index) => {
+            btn.addEventListener('click', async function () {
+                const card = this.closest('.history-card'), is_favorite = this.classList.contains('active');
+                const recordId = card.dataset.id;
+
+                try {
+                    const response = await fetch(`/api/history/${recordId}/favorite`, {
+                        method: 'POST'
+                    });
+
+                    if (response.ok) {
+                        if (is_favorite) {
+                            this.innerHTML = svgIcons.star;
+                            this.title = '未收藏';
+                            this.classList.remove('active');
+                            if (isFavorite) {
+                                card.remove();
+                                // 如果没有卡片了,显示空提示
+                                if (container.querySelectorAll('.history-card').length === 0) {
+                                    container.innerHTML = '<div class="placeholder">暂无记录</div>';
+                                }
                             }
-                        } catch (e) {
-                            console.error('解析答案失败:', e);
-                            answers = historyItem.answers;
+                            alert('取消成功');
+                        } else {
+                            this.innerHTML = svgIcons.starFilled;
+                            this.title = '已收藏';
+                            this.classList.add('active');
+
+                            alert('收藏成功');
                         }
+                    } else {
+                        alert('收藏失败,请重试');
+                    }
+                } catch (error) {
+                    console.error('收藏操作失败:', error);
+                    alert('收藏失败,请检查网络连接');
+                }
+            });
+        });
+
+        // 为删除收藏按钮添加点击事件
+        container.querySelectorAll('.delete-btn').forEach((btn, index) => {
+            btn.addEventListener('click', async function () {
+                const card = this.closest('.history-card'), is_recover = this.classList.contains('active');
+                const recordId = card.dataset.id;
 
-                        renderHistoryAnswers(answers, answersContainer);
+                if (confirm(`确定要${is_recover ? '恢复' : '删除'}这条记录吗?`)) {
+                    try {
+                        const response = await fetch(`/api/history/${recordId}/delete`, {
+                            method: 'POST'
+                        });
+
+                        if (response.ok) {
+                            card.remove();
+                            // 如果没有卡片了,显示空提示
+                            if (container.querySelectorAll('.history-card').length === 0) {
+                                container.innerHTML = '<div class="placeholder">暂无记录</div>';
+                            }
+                            if (is_recover) {
+                                alert('恢复成功');
+                            } else {
+                                alert('删除成功');
+                            }
+                        } else {
+                            alert('删除失败,请重试');
+                        }
+                    } catch (error) {
+                        console.error('删除操作失败:', error);
+                        alert('删除失败,请检查网络连接');
                     }
                 }
             });
         });
-    }
 
-    function renderHistoryAnswers(answers, container) {
-        if (!answers || answers.length === 0) {
-            container.innerHTML = '<p class="placeholder">没有找到答案</p>';
-            return;
-        }
+        function renderHistoryAnswers(answers) {
+            let str=''
+            if (!answers || answers.length === 0) {
+                str = '<p class="placeholder">没有找到答案</p>';
+                return str;
+            }
 
-        // 根据答案类型渲染不同的内容
-        if (Array.isArray(answers)) {
-            // 如果是数组,渲染为列表
-            const answersHTML = answers.map(answer => {
-                if (typeof answer === 'object' && answer.expression) {
-                    // 如果是解法对象
-                    return `
-                        <div class="solution-item flag-${answer.flag || 0}">
-                            <div class="solution-expression">${answer.expression}</div>
-                            <div class="solution-flag flag-${answer.flag || 0}">
-                                ${answer.flag === 1 ? '★' : answer.flag === 2 ? '★★' : ''}
+            // 根据答案类型渲染不同的内容
+            if (Array.isArray(answers)) {
+                // 如果是数组,渲染为列表
+                const answersHTML = answers.map(answer => {
+                    if (typeof answer === 'object' && answer.expression) {
+                        // 如果是解法对象
+                        return `
+                            <div class="solution-item flag-${answer.flag || 0}">
+                                <div class="solution-expression">${answer.expression}</div>
+                                <div class="solution-flag flag-${answer.flag || 0}">
+                                    ${answer.flag === 1 ? '★' : answer.flag === 2 ? '★★' : ''}
+                                </div>
                             </div>
+                        `;
+                    } else {
+                        // 如果是普通文本
+                        return `<div class="answer-item">${answer}</div>`;
+                    }
+                }).join('');
+                str = `
+                    <div class="answers-container">
+                        ${answersHTML}
+                    </div>
+                `;
+            } else if (typeof answers === 'object') {
+                // 如果是单个对象
+                const answerKeys = Object.keys(answers);
+                const answersHTML = answerKeys.map(key => {
+                    return `
+                        <div class="answer-group">
+                            <div class="answer-key">${key}:</div>
+                            <div class="answer-value">${answers[key]}</div>
                         </div>
                     `;
-                } else {
-                    // 如果是普通文本
-                    return `<div class="answer-item">${answer}</div>`;
-                }
-            }).join('');
+                }).join('');
 
-            container.innerHTML = `
-                <div class="answers-container">
-                    ${answersHTML}
-                </div>
-            `;
-        } else if (typeof answers === 'object') {
-            // 如果是单个对象
-            const answerKeys = Object.keys(answers);
-            const answersHTML = answerKeys.map(key => {
-                return `
-                    <div class="answer-group">
-                        <div class="answer-key">${key}:</div>
-                        <div class="answer-value">${answers[key]}</div>
+                str = `
+                    <h3>答案</h3>
+                    <div class="answers-container">
+                        ${answersHTML}
                     </div>
                 `;
-            }).join('');
-
-            container.innerHTML = `
-                <h3>答案</h3>
-                <div class="answers-container">
-                    ${answersHTML}
-                </div>
-            `;
-        } else {
-            // 如果是单个值
-            container.innerHTML = `
-                <h3>答案</h3>
-                <div class="answers-container">
-                    <div class="answer-item">${answers}</div>
-                </div>
-            `;
+            } else {
+                // 如果是单个值
+                str = `
+                    <h3>答案</h3>
+                    <div class="answers-container">
+                        <div class="answer-item">${answers}</div>
+                    </div>
+                `;
+            }
+            return str;
         }
+
     }
+
     // 为加载更多按钮添加事件监听
     if (historyLoadMoreBtn) {
         historyLoadMoreBtn.addEventListener('click', () => {
@@ -713,9 +898,23 @@ document.addEventListener('DOMContentLoaded', function () {
         });
     }
 
+    // 为收藏加载更多按钮添加事件监听
+    if (favoriteLoadMoreBtn) {
+        favoriteLoadMoreBtn.addEventListener('click', () => {
+            if (!isLoadingFavorite && hasMoreFavorite) {
+                fetchFavorites(true);
+            }
+        });
+    }
+
     // 设置日期输入框的默认值为当前日期
     const today = new Date();
-    historyStartDateInput.value = today.toISOString().split('T')[0];
+    // 计算三天前的日期
+    const threeDaysAgo = new Date();
+    threeDaysAgo.setDate(today.getDate() - 3);
+
+    // 设置开始日期输入框的值为三天前
+    historyStartDateInput.value = threeDaysAgo.toISOString().split('T')[0];
     historyEndDateInput.value = today.toISOString().split('T')[0];
 });
 

+ 118 - 13
app/static/styles.css

@@ -217,14 +217,17 @@ button:hover {
     gap: 15px;
     margin-bottom: 20px;
 }
-
+.pre-container{
+    display: inline-flex;
+    gap: 8px;
+    align-items: center;
+}
 .timer-container {
     display: inline-flex;
     align-items: center;
     background-color: #f2f2f2;
     padding: 5px 10px;
     border-radius: 4px;
-    margin-bottom: 10px;
     width: fit-content;
     margin-right: 15px;
 }
@@ -508,15 +511,18 @@ button:hover {
     background-color: #f9f9f9;
     flex-wrap: wrap;
 }
-.history-search-container select,.history-search-container input{
+
+.history-search-container select,
+.history-search-container input {
     outline: none;
     padding: 5px 10px;
     border-radius: 4px;
     border: 2px solid #3498db;
 }
+
 .history-search-container .search-btn {
     width: auto;
-    padding:5px 15px;
+    padding: 5px 15px;
 }
 
 .history-data-container {
@@ -537,8 +543,8 @@ button:hover {
     border-radius: 8px;
     box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
     padding: 8px;
-    transition: transform 0.2s;
-    margin-bottom:5px;
+    transition: transform 0.2s, opacity 0.3s, border-left-color 0.3s;
+    margin-bottom: 5px;
     font-size: 14px;
     border: 1px solid;
 }
@@ -558,6 +564,25 @@ button:hover {
     font-size: 13px;
 }
 
+.history-card-actions {
+    display: flex;
+    gap: 5px;
+    align-items: center;
+}
+
+
+.history-card.favorite {
+    border-left: 4px solid #ffcc00;
+}
+
+/* 确保收藏模块中的删除按钮样式一致 */
+.favorites-module .history-card.deleted {
+    opacity: 0.6;
+    background-color: #f9f9f9;
+    border-color: #ddd;
+    border-left: 4px solid #e74c3c !important;
+}
+
 .history-card-type {
     font-weight: bold;
     color: #3498db;
@@ -601,11 +626,7 @@ button:hover {
     font-size: 16px;
 }
 
-.history-card .show-history-answer-btn {
-    width: auto;
-    padding: 3px 8px;
-    font-size: 13px;
-}
+
 
 .history-card-answers {
     padding-top: 8px;
@@ -695,12 +716,12 @@ button:hover {
 
 .type-e {
     border-color: #9b59b6;
-    border-left: 4px solid  #9b59b6;
+    border-left: 4px solid #9b59b6;
 }
 
 .type-unknown {
     border-color: #9b59b6;
-    border-left: 4px solid  #9b59b6;
+    border-left: 4px solid #9b59b6;
 }
 
 .flag-table .placeholder {
@@ -743,6 +764,10 @@ button:hover {
     display: none !important;
 }
 
+
+
+/* 收藏和删除操作的过渡效果 */
+
 .flag-table .expression-container {
     margin: 5px 0;
     display: flex;
@@ -781,4 +806,84 @@ button:hover {
 
 .secondary-btn:hover {
     background-color: #7f8c8d;
+}
+
+
+
+.icon-btn {
+            display: inline-flex;
+            align-items: center;
+            justify-content: center;
+            padding: 5px;
+            border: none;
+            background: transparent;
+            color: #666;
+            cursor: pointer;
+            border-radius: 4px;
+            transition: all 0.2s;
+        }
+
+/* 确保按钮在不同状态下的一致性 */
+.show-history-answer-btn,
+.favorite-btn,
+.delete-btn {
+    outline: none;
+    box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.3);
+}
+.show-history-answer-btn{
+    color:#3498db;
+}
+
+.favorite-btn{
+    color:#f39c12;
+}
+.delete-btn {
+    color:#c0392b;
+}
+.delete-btn.active{
+    color: #4CAF50;
+}
+/* 禁用状态的按钮样式 */
+.favorite-btn:disabled,
+.delete-btn:disabled {
+    opacity: 0.6;
+    cursor: not-allowed;
+}
+
+.icon-btn:hover {
+    background: rgba(0, 0, 0, 0.05);
+    /*color: #333;*/
+}
+
+.history-card-actions {
+    display: flex;
+    gap: 8px;
+}
+.show-history-answer-btn.active {
+    color: #3498db;
+}
+.favorite-btn.active {
+    color: #f39c12;
+}
+
+/* 已删除历史卡片样式 */
+.history-card.deleted {
+    opacity: 0.6;
+    background-color: #f9f9f9;
+    border-color: #ddd;
+    border-left: 4px solid #e74c3c;
+}
+
+.history-card.deleted .history-card-header {
+    color: #7f8c8d;
+}
+
+.history-card.deleted .delete-btn {
+    background-color: #e74c3c;
+    color: white;
+    border-color: #c0392b;
+}
+
+.history-card.deleted .delete-btn:hover {
+    background-color: #c0392b;
 }

+ 4 - 2
docker/docker-compose.yml

@@ -1,13 +1,15 @@
 services:
   24game-app:
-    image: 24game_yue:1.0.1
+    image: 24game_yue:1.0.2
     container_name: 24game_yue
     environment:
       - TZ=Asia/Shanghai
     networks:
       - 24game_yue-net
+    volumes:
+      - /vol2/1000/000Tools/apps/24game/db:/app/db
     ports:
-       - "5780:8080"
+      - "8810:8080"
     restart: always
 
 networks: