فهرست منبع

fix: export download buttons + clear cart with confirm + collapse knowledge tree

nonvioce 3 ماه پیش
والد
کامیت
790c6bcb3e

+ 1 - 1
tiku-client/README.md

@@ -26,7 +26,7 @@ flutter build apk --release
 | 环境 | 服务器地址 |
 |------|-----------|
 | 模拟器 | `http://10.0.2.2:8765` |
-| 真机 | `http://服务器IP:8765` 或 `http://数学.top:8765` |
+| 真机 | `http://服务器IP:8765` |
 
 ## 页面导航
 

+ 32 - 1
tiku-client/lib/pages/carts/cart_list_page.dart

@@ -52,6 +52,31 @@ class _CartListPageState extends ConsumerState<CartListPage> {
     if (mounted) setState(() => _isLoading = false);
   }
 
+  Future<void> _clearCart() async {
+    final ok = await showDialog<bool>(
+      context: context,
+      builder: (ctx) => AlertDialog(
+        title: const Text('清空试题篮'),
+        content: const Text('确认清空当前篮子中的所有题目?此操作不可撤销。'),
+        actions: [
+          TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
+          FilledButton(
+            onPressed: () => Navigator.pop(ctx, true),
+            style: FilledButton.styleFrom(backgroundColor: Colors.red),
+            child: const Text('清空'),
+          ),
+        ],
+      ),
+    );
+    if (ok == true) {
+      final api = ref.read(apiServiceProvider);
+      try {
+        await api.clearCart(_cartId);
+        _load();
+      } catch (_) {}
+    }
+  }
+
   Future<void> _removeItem(String pid) async {
     final api = ref.read(apiServiceProvider);
     try {
@@ -68,7 +93,12 @@ class _CartListPageState extends ConsumerState<CartListPage> {
       appBar: AppBar(
         title: const Text('试题篮'),
         actions: [
-          if (_cart != null && _cart!.items != null && _cart!.items!.isNotEmpty)
+          if (_cart != null && _cart!.items != null && _cart!.items!.isNotEmpty) ...[
+            IconButton(
+              icon: const Icon(Icons.delete_sweep),
+              tooltip: '清空',
+              onPressed: _clearCart,
+            ),
             IconButton(
               icon: const Icon(Icons.file_download),
               tooltip: '导出',
@@ -79,6 +109,7 @@ class _CartListPageState extends ConsumerState<CartListPage> {
                 ),
               ).then((_) => _load()),
             ),
+          ],
           IconButton(icon: const Icon(Icons.refresh), onPressed: _load, tooltip: '刷新'),
         ],
       ),

+ 34 - 26
tiku-client/lib/pages/export/export_page.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:path_provider/path_provider.dart';
 import '../../services/api_service.dart';
 import '../../providers/app_provider.dart';
 
@@ -27,10 +28,7 @@ class _ExportPageState extends ConsumerState<ExportPage> {
       } else {
         data = await api.exportChapter(null, null, '全部导出');
       }
-      setState(() {
-        _result = data;
-        _exporting = false;
-      });
+      setState(() { _result = data; _exporting = false; });
     } catch (e) {
       setState(() => _exporting = false);
       if (mounted) {
@@ -41,10 +39,28 @@ class _ExportPageState extends ConsumerState<ExportPage> {
     }
   }
 
-  @override
-  Widget build(BuildContext context) {
+  Future<void> _downloadFile(String filename, String label) async {
     final api = ref.read(apiServiceProvider);
+    try {
+      final dir = await getApplicationDocumentsDirectory();
+      final savePath = '${dir.path}/$filename';
+      await api.downloadExport(filename, savePath);
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text('$label 已下载'), duration: const Duration(seconds: 2)),
+        );
+      }
+    } catch (e) {
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(content: Text('下载失败: $e')),
+        );
+      }
+    }
+  }
 
+  @override
+  Widget build(BuildContext context) {
     return Scaffold(
       appBar: AppBar(title: Text(widget.cartName != null ? '导出: ${widget.cartName}' : '导出')),
       body: Center(
@@ -59,16 +75,11 @@ class _ExportPageState extends ConsumerState<ExportPage> {
                 const Text('将生成学生版和教师版两份 docx 文件'),
                 const SizedBox(height: 24),
                 SizedBox(
-                  width: 200,
-                  height: 48,
+                  width: 200, height: 48,
                   child: FilledButton.icon(
                     onPressed: _exporting ? null : _doExport,
                     icon: _exporting
-                        ? const SizedBox(
-                            width: 20,
-                            height: 20,
-                            child: CircularProgressIndicator(strokeWidth: 2),
-                          )
+                        ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
                         : const Icon(Icons.file_download),
                     label: Text(_exporting ? '生成中...' : '导出 docx'),
                   ),
@@ -78,29 +89,26 @@ class _ExportPageState extends ConsumerState<ExportPage> {
                 const SizedBox(height: 24),
                 Text('导出成功!共 ${_result!['count']} 题'),
                 const SizedBox(height: 16),
-                // Download links
                 FilledButton.tonal(
-                  onPressed: () {
-                    // Launch download URL
-                  },
-                  child: const Row(
+                  onPressed: () => _downloadFile(_result!['student'] as String, '学生版'),
+                  child: Row(
                     mainAxisSize: MainAxisSize.min,
                     children: [
-                      Icon(Icons.download),
-                      SizedBox(width: 8),
-                      Text('学生版: ...'),
+                      const Icon(Icons.download, size: 18),
+                      const SizedBox(width: 8),
+                      Text('学生版: ${_result!['student']}'),
                     ],
                   ),
                 ),
                 const SizedBox(height: 8),
                 FilledButton.tonal(
-                  onPressed: () {},
-                  child: const Row(
+                  onPressed: () => _downloadFile(_result!['teacher'] as String, '教师版'),
+                  child: Row(
                     mainAxisSize: MainAxisSize.min,
                     children: [
-                      Icon(Icons.download),
-                      SizedBox(width: 8),
-                      Text('教师版: ...'),
+                      const Icon(Icons.download, size: 18),
+                      const SizedBox(width: 8),
+                      Text('教师版: ${_result!['teacher']}'),
                     ],
                   ),
                 ),

+ 10 - 0
tiku-client/lib/services/api_service.dart

@@ -224,6 +224,12 @@ class ApiService {
 
   String get downloadUrl => '$_baseUrl/api/exports/';
 
+  Future<String> downloadExport(String filename, String savePath) async {
+    final url = '$_baseUrl/api/exports/$filename';
+    await _dio.download(url, savePath, options: _authOptions);
+    return savePath;
+  }
+
   // ── 知识点 ────────────────────────────────────────
 
   Future<Map<String, dynamic>> knowledgeTree() async {
@@ -264,4 +270,8 @@ class ApiService {
   Future<void> removeFromCart(int cartId, String problemId) async {
     await _dio.delete('/api/carts/$cartId/items/$problemId', options: _authOptions);
   }
+
+  Future<void> clearCart(int cartId) async {
+    await _dio.post('/api/carts/$cartId/clear', options: _authOptions);
+  }
 }

+ 1 - 1
tiku-client/lib/widgets/knowledge_tree.dart

@@ -104,7 +104,7 @@ class _KnowledgeTreeWidgetState extends ConsumerState<KnowledgeTreeWidget> {
                 ? Theme.of(context).colorScheme.primary
                 : null,
           )),
-      initiallyExpanded: depth < 2,
+      initiallyExpanded: false,
       children: node.children.map((c) => _buildNode(c, depth + 1)).toList(),
     );
   }

+ 13 - 0
tiku-server/app/routers/carts.py

@@ -88,3 +88,16 @@ def remove_item(request: Request, cart_id: int, problem_id: str, _=Depends(verif
         return {"ok": True}
     finally:
         conn.close()
+
+
+@router.post("/carts/{cart_id}/clear")
+def clear_cart(request: Request, cart_id: int, _=Depends(verify_token)):
+    """清空篮子"""
+    config = request.app.state.config
+    conn = get_connection(config.db_path)
+    try:
+        conn.execute("DELETE FROM cart_items WHERE cart_id = ?", (cart_id,))
+        conn.commit()
+        return {"ok": True}
+    finally:
+        conn.close()