Files
2025-09-10 00:07:52 +08:00

250 lines
7.7 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'ai_shimmer_placeholder.dart';
class ChapterPreviewData {
final String title;
final String outline;
final String content;
const ChapterPreviewData({
required this.title,
required this.outline,
required this.content,
});
ChapterPreviewData copyWith({String? title, String? outline, String? content}) {
return ChapterPreviewData(
title: title ?? this.title,
outline: outline ?? this.outline,
content: content ?? this.content,
);
}
}
class ResultsPreviewPanel extends StatefulWidget {
final List<ChapterPreviewData> chapters;
final bool isGenerating;
final void Function(int index, ChapterPreviewData updated) onChapterChanged;
const ResultsPreviewPanel({
Key? key,
required this.chapters,
required this.isGenerating,
required this.onChapterChanged,
}) : super(key: key);
@override
State<ResultsPreviewPanel> createState() => _ResultsPreviewPanelState();
}
class _ResultsPreviewPanelState extends State<ResultsPreviewPanel> with TickerProviderStateMixin {
TabController? _tabController; // 允许为空:当无章节时不创建
List<TextEditingController> _outlineCtrls = const [];
List<TextEditingController> _contentCtrls = const [];
int _selectedTabIndex = 0;
@override
void initState() {
super.initState();
// 仅当有章节时初始化控制器,避免 TabController 长度为 0 的错误
if (widget.chapters.isNotEmpty) {
_initControllers();
}
}
@override
void didUpdateWidget(covariant ResultsPreviewPanel oldWidget) {
super.didUpdateWidget(oldWidget);
// 当从无到有或长度变化时,重建控制器
if (oldWidget.chapters.length != widget.chapters.length) {
_disposeControllers();
if (widget.chapters.isNotEmpty) {
_initControllers();
}
return;
}
// 同步内容(有章节时)
if (widget.chapters.isNotEmpty &&
_outlineCtrls.length == widget.chapters.length &&
_contentCtrls.length == widget.chapters.length) {
for (int i = 0; i < widget.chapters.length; i++) {
_outlineCtrls[i].text = widget.chapters[i].outline;
_contentCtrls[i].text = widget.chapters[i].content;
}
}
}
void _initControllers() {
final tabLen = (widget.chapters.length * 2).clamp(1, 1000); // 至少为1
_tabController = TabController(length: tabLen, vsync: this);
_tabController!.addListener(() {
final currentIndex = _tabController?.index ?? _selectedTabIndex;
if (_selectedTabIndex != currentIndex) {
setState(() {
_selectedTabIndex = currentIndex;
});
}
});
_outlineCtrls = List.generate(widget.chapters.length, (i) => TextEditingController(text: widget.chapters[i].outline));
_contentCtrls = List.generate(widget.chapters.length, (i) => TextEditingController(text: widget.chapters[i].content));
}
void _disposeControllers() {
_tabController?.dispose();
_tabController = null;
for (final c in _outlineCtrls) {
c.dispose();
}
for (final c in _contentCtrls) {
c.dispose();
}
_outlineCtrls = const [];
_contentCtrls = const [];
}
@override
void dispose() {
_disposeControllers();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.chapters.isEmpty) {
return widget.isGenerating
? const AIShimmerPlaceholder()
: _buildEmptyResults(context, '暂无结果,点击右上角生成');
}
// 确保在首次有章节时已初始化控制器(防御性)
if (_tabController == null) {
_initControllers();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 多行自适应子Tab
Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: _buildMultiLineTabs(context),
),
Expanded(
child: TabBarView(
controller: _tabController!,
children: _buildTabViews(context),
),
),
],
);
}
// 多行自适应标签头
Widget _buildMultiLineTabs(BuildContext context) {
final chips = <Widget>[];
for (int i = 0; i < widget.chapters.length; i++) {
final title = (widget.chapters[i].title.isNotEmpty) ? widget.chapters[i].title : '无标题';
chips.add(_buildTabChip(context, index: i * 2, label: '${i + 1}章-$title-大纲'));
chips.add(_buildTabChip(context, index: i * 2 + 1, label: '${i + 1}章-$title-正文'));
}
return Wrap(
spacing: 8,
runSpacing: 8,
children: chips,
);
}
Widget _buildTabChip(BuildContext context, {required int index, required String label}) {
final bool selected = index == _selectedTabIndex;
final theme = Theme.of(context);
final selectedBg = theme.colorScheme.primary.withOpacity(0.12);
final borderColor = selected ? theme.colorScheme.primary : theme.dividerColor;
final textStyle = selected
? theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.primary, fontWeight: FontWeight.w600)
: theme.textTheme.bodyMedium;
return InkWell(
onTap: () {
setState(() {
_selectedTabIndex = index;
_tabController?.animateTo(index);
});
},
borderRadius: BorderRadius.circular(10),
child: Container(
constraints: const BoxConstraints(maxWidth: 220),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: selected ? selectedBg : Colors.transparent,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: borderColor, width: 1),
),
child: Text(
label,
softWrap: true,
overflow: TextOverflow.fade,
maxLines: 2,
style: textStyle,
),
),
);
}
List<Widget> _buildTabViews(BuildContext context) {
final List<Widget> views = [];
for (int i = 0; i < widget.chapters.length; i++) {
views.add(_buildPlainEditor(context, i, isOutline: true));
views.add(_buildPlainEditor(context, i, isOutline: false));
}
return views;
}
// 极简编辑器:
// - 无背景、无内边距
// - 自适应高度minLines=1, maxLines=null
// - 无头部小标签
Widget _buildPlainEditor(BuildContext context, int index, {required bool isOutline}) {
final controller = isOutline ? _outlineCtrls[index] : _contentCtrls[index];
final onChanged = (String text) {
if (isOutline) {
widget.onChapterChanged(index, widget.chapters[index].copyWith(outline: text));
} else {
widget.onChapterChanged(index, widget.chapters[index].copyWith(content: text));
}
};
return SingleChildScrollView(
padding: EdgeInsets.zero,
child: TextField(
controller: controller,
decoration: const InputDecoration(
border: InputBorder.none,
isCollapsed: true,
contentPadding: EdgeInsets.zero,
hintText: '',
),
keyboardType: TextInputType.multiline,
minLines: 1,
maxLines: null,
onChanged: onChanged,
),
);
}
Widget _buildEmptyResults(BuildContext context, String message) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.menu_book_outlined, size: 48, color: isDark ? const Color(0xFF9CA3AF) : const Color(0xFF6B7280)),
const SizedBox(height: 12),
Text(message, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium),
],
),
),
);
}
}