import 'package:flutter/material.dart'; /// 文档与帮助页:内容区留空,底部是可上滑的 LLM 助手对话框。 class DocsPage extends StatelessWidget { const DocsPage({super.key}); @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final ColorScheme colors = theme.colorScheme; return Scaffold( appBar: AppBar(title: const Text('文档与帮助')), body: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Stack( children: [ // 内容区:整体留白,只保留底色和边框,深浅色都跟随主题。 Padding( padding: const EdgeInsets.all(16), child: Container( width: double.infinity, decoration: BoxDecoration( color: colors.surfaceContainerLow, borderRadius: BorderRadius.circular(20), border: Border.all(color: colors.outlineVariant), ), alignment: Alignment.center, child: Text( '文档内容将在这里显示', style: theme.textTheme.bodyMedium?.copyWith( color: colors.onSurfaceVariant, ), ), ), ), _AssistantSheet(availableHeight: constraints.maxHeight), ], ); }, ), ); } } /// 底部助手:收起时只露出一条,上滑或点击后展开成对话框。 class _AssistantSheet extends StatefulWidget { const _AssistantSheet({required this.availableHeight}); /// 父级可用高度,用于把拖拽像素换算成比例。 final double availableHeight; @override State<_AssistantSheet> createState() => _AssistantSheetState(); } class _AssistantSheetState extends State<_AssistantSheet> { static const double _collapsed = 0.13; static const double _expandedSize = 0.92; final DraggableScrollableController _controller = DraggableScrollableController(); final TextEditingController _input = TextEditingController(); static const List<(bool, String)> _messages = <(bool, String)>[ ( false, '你好,我是 KiriMemo 的助手。文档还没接入,但关于用法和概念的问题可以先问我。', ), ]; @override void dispose() { _controller.dispose(); _input.dispose(); super.dispose(); } void _animateTo(double target) { if (!_controller.isAttached) { return; } _controller.animateTo( target, duration: const Duration(milliseconds: 260), curve: Curves.easeOutCubic, ); } void _toggle() { if (!_controller.isAttached) { return; } final bool open = _controller.size > 0.4; _animateTo(open ? _collapsed : _expandedSize); } void _onDragUpdate(DragUpdateDetails details) { if (!_controller.isAttached || widget.availableHeight <= 0) { return; } final double next = (_controller.size - details.delta.dy / widget.availableHeight) .clamp(_collapsed, _expandedSize); _controller.jumpTo(next); } void _onDragEnd(DragEndDetails details) { if (!_controller.isAttached) { return; } final bool open = _controller.size > (_collapsed + _expandedSize) / 2; _animateTo(open ? _expandedSize : _collapsed); } void _send() { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('助手尚未接入模型'), behavior: SnackBarBehavior.floating, ), ); } @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final ColorScheme colors = theme.colorScheme; return DraggableScrollableSheet( controller: _controller, initialChildSize: _collapsed, minChildSize: _collapsed, maxChildSize: _expandedSize, builder: (BuildContext context, ScrollController scrollController) { return Container( decoration: BoxDecoration( color: colors.surfaceContainerHigh, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), border: Border(top: BorderSide(color: colors.outlineVariant)), ), child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final bool expanded = constraints.maxHeight > 200; return Column( children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: _toggle, onVerticalDragUpdate: _onDragUpdate, onVerticalDragEnd: _onDragEnd, child: _header(theme, colors, expanded: expanded), ), // 列表必须始终挂载:DraggableScrollableController 的 // isAttached 取决于内部 ScrollController 是否有 client, // 收起时若拿掉列表,就再也展不开了。 Expanded( child: ListView( controller: scrollController, padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), children: [ if (expanded) for (final (bool fromUser, String text) in _messages) _Bubble(fromUser: fromUser, text: text), ], ), ), if (expanded) _inputRow(theme, colors), ], ); }, ), ); }, ); } Widget _header( ThemeData theme, ColorScheme colors, { required bool expanded, }) { return Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 10), child: Column( children: [ Container( width: 36, height: 4, decoration: BoxDecoration( color: colors.onSurfaceVariant.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 12), Row( children: [ Icon(Icons.auto_awesome_rounded, size: 18, color: colors.primary), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('问问 KiriMemo', style: theme.textTheme.titleSmall), // 收起时只有一条横栏,放不下副标题就不放。 if (expanded) Text( '文档暂未接入,可以先问概念与用法', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: colors.onSurfaceVariant, ), ), ], ), ), Icon( expanded ? Icons.keyboard_arrow_down_rounded : Icons.keyboard_arrow_up_rounded, color: colors.onSurfaceVariant, ), ], ), ], ), ); } Widget _inputRow(ThemeData theme, ColorScheme colors) { return Padding( padding: EdgeInsets.fromLTRB( 16, 0, 16, 12 + MediaQuery.viewPaddingOf(context).bottom, ), child: Row( children: [ Expanded( child: TextField( controller: _input, textInputAction: TextInputAction.send, onSubmitted: (_) => _send(), decoration: InputDecoration( hintText: '问点什么…', isDense: true, filled: true, fillColor: colors.surfaceContainerHighest, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none, ), ), ), ), const SizedBox(width: 8), IconButton.filled( onPressed: _send, icon: const Icon(Icons.arrow_upward_rounded), tooltip: '发送', ), ], ), ); } } class _Bubble extends StatelessWidget { const _Bubble({required this.fromUser, required this.text}); final bool fromUser; final String text; @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); final ColorScheme colors = theme.colorScheme; return Align( alignment: fromUser ? Alignment.centerRight : Alignment.centerLeft, child: Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), constraints: const BoxConstraints(maxWidth: 320), decoration: BoxDecoration( color: fromUser ? colors.primaryContainer : colors.surfaceContainerHighest, borderRadius: BorderRadius.only( topLeft: const Radius.circular(16), topRight: const Radius.circular(16), bottomLeft: Radius.circular(fromUser ? 16 : 4), bottomRight: Radius.circular(fromUser ? 4 : 16), ), ), child: Text( text, style: theme.textTheme.bodyMedium?.copyWith( color: fromUser ? colors.onPrimaryContainer : colors.onSurface, ), ), ), ); } }