1
This commit is contained in:
@@ -9,7 +9,9 @@ import 'package:remixicon/remixicon.dart';
|
||||
import '../widgets/edit_desc_dialog.dart';
|
||||
import 'widgets/avatar_card.dart';
|
||||
import 'widgets/coach_message.dart';
|
||||
import 'widgets/plan_list.dart';
|
||||
import 'widgets/scroll_box.dart';
|
||||
import 'widgets/suggested.dart';
|
||||
|
||||
class PlanDetailPage extends StatefulWidget {
|
||||
final String? id;
|
||||
@@ -27,6 +29,20 @@ class PlanDetailPage extends StatefulWidget {
|
||||
|
||||
class _PlanDetailPageState extends State<PlanDetailPage> {
|
||||
bool _isEdit = false;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
///store对象
|
||||
late PlanDetailStore store;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
store = PlanDetailStore(
|
||||
planId: widget.id.toString(),
|
||||
planContent: widget.planName ?? "",
|
||||
scrollController: scrollController,
|
||||
);
|
||||
}
|
||||
|
||||
///popup菜单
|
||||
void _onPopupActionSelected(String value) {
|
||||
@@ -54,51 +70,9 @@ class _PlanDetailPageState extends State<PlanDetailPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider<PlanDetailStore>(
|
||||
create: (_) {
|
||||
return PlanDetailStore(
|
||||
planId: widget.id.toString(),
|
||||
planContent: widget.planName ?? "",
|
||||
showRoleTalk: widget.planName == null,
|
||||
);
|
||||
return store;
|
||||
},
|
||||
child: CupertinoPageScaffold(
|
||||
backgroundColor: Colors.white,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text('计划详情'),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min, // 关键:Row 只占实际内容宽度
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) {
|
||||
// 仅使用渐变动画
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _isEdit
|
||||
? InkWell(
|
||||
onTap: _cancelEdit,
|
||||
child: Icon(RemixIcons.check_fill),
|
||||
)
|
||||
: PopupAction(
|
||||
onSelected: _onPopupActionSelected,
|
||||
items: [
|
||||
PopupMenuItem(
|
||||
value: 'edit_step',
|
||||
child: Text("编辑步骤"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'edit_desc',
|
||||
child: Text("编辑摘要"),
|
||||
),
|
||||
],
|
||||
child: Icon(RemixIcons.more_fill),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Consumer<PlanDetailStore>(
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -110,33 +84,12 @@ class _PlanDetailPageState extends State<PlanDetailPage> {
|
||||
child: Container(
|
||||
decoration: shadowDecoration,
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
CoachMessage(),
|
||||
// SliverToBoxAdapter(
|
||||
// child: SizedBox(height: 20),
|
||||
// ),
|
||||
// SliverList.builder(
|
||||
// itemBuilder: (BuildContext context, int index) {
|
||||
// return PlanItem(
|
||||
// showEdit: _isEdit,
|
||||
// title: "测试 ${index + 1}",
|
||||
// desc: "测测 ${index + 1}",
|
||||
// onDelete: (id) {},
|
||||
// );
|
||||
// },
|
||||
// itemCount: 10,
|
||||
// ),
|
||||
// SliverToBoxAdapter(
|
||||
// child: SuggestedTitle(),
|
||||
// ),
|
||||
// SliverList.builder(
|
||||
// itemBuilder: (BuildContext context, int index) {
|
||||
// return SuggestedItem(
|
||||
// title: "测试",
|
||||
// );
|
||||
// },
|
||||
// itemCount: 5,
|
||||
// ),
|
||||
PlanList(),
|
||||
SuggestedTitle(),
|
||||
SuggestedList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -146,6 +99,49 @@ class _PlanDetailPageState extends State<PlanDetailPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
builder: (context, store, child) {
|
||||
return CupertinoPageScaffold(
|
||||
backgroundColor: Colors.white,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(store.planDetail.summary ?? ""),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min, // 关键:Row 只占实际内容宽度
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) {
|
||||
// 仅使用渐变动画
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: _isEdit
|
||||
? InkWell(
|
||||
onTap: _cancelEdit,
|
||||
child: Icon(RemixIcons.check_fill),
|
||||
)
|
||||
: PopupAction(
|
||||
onSelected: _onPopupActionSelected,
|
||||
items: [
|
||||
PopupMenuItem(
|
||||
value: 'edit_step',
|
||||
child: Text("编辑步骤"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: 'edit_desc',
|
||||
child: Text("编辑摘要"),
|
||||
),
|
||||
],
|
||||
child: Icon(RemixIcons.more_fill),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:plan/api/dto/plan_detail_dto.dart';
|
||||
import 'package:plan/api/endpoints/plan_api.dart';
|
||||
import 'package:plan/utils/stream.dart';
|
||||
|
||||
class PlanDetailStore extends ChangeNotifier {
|
||||
///构造函数
|
||||
PlanDetailStore({
|
||||
this.planContent = "",
|
||||
this.planId = "",
|
||||
bool showRoleTalk = true,
|
||||
}) : _showRoleTalk = showRoleTalk;
|
||||
required this.scrollController,
|
||||
}) {
|
||||
//如果没有id进行初始化
|
||||
if (planId == "0") {
|
||||
createPlan();
|
||||
}else{
|
||||
//获取详情
|
||||
getPlanDetail();
|
||||
}
|
||||
}
|
||||
|
||||
///滚动控制器
|
||||
ScrollController scrollController;
|
||||
|
||||
///角色话语是否显示
|
||||
bool _showRoleTalk = true;
|
||||
bool _showRoleTalk = false;
|
||||
|
||||
bool get showRoleTalk => _showRoleTalk;
|
||||
|
||||
@@ -27,11 +40,123 @@ class PlanDetailStore extends ChangeNotifier {
|
||||
String planId = "";
|
||||
|
||||
///计划详情
|
||||
PlanDetailDto planDetail = PlanDetailDto();
|
||||
PlanDetailDto planDetail = PlanDetailDto(summary: "计划详情");
|
||||
|
||||
///流请求工具
|
||||
StreamUtils streamUtils = StreamUtils();
|
||||
|
||||
///创建计划
|
||||
void createPlan() async {
|
||||
var id = await initPlanApi(planContent, 1);
|
||||
planId = id.toString();
|
||||
// planId = "3";
|
||||
|
||||
///生成摘要---------------------------
|
||||
String summary = "";
|
||||
streamUtils.sendStream(
|
||||
"/plan/make_summary",
|
||||
data: {"plan_id": planId},
|
||||
onCall: (chunk) {
|
||||
summary = chunk['text'];
|
||||
},
|
||||
onEnd: () {
|
||||
planDetail.summary = summary;
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
/// 生成对白-------------------------------
|
||||
String dialog = "";
|
||||
await streamUtils.sendStream(
|
||||
"/plan/make_dialog",
|
||||
data: {"plan_id": planId},
|
||||
onCall: (chunk) {
|
||||
dialog = chunk['text'];
|
||||
},
|
||||
onEnd: () {
|
||||
planDetail.dialog = dialog;
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
/// 生成步骤-------------------------------
|
||||
await streamUtils.sendStream(
|
||||
"/plan/make_step",
|
||||
data: {"plan_id": planId},
|
||||
onCall: (chunk) {
|
||||
final text = chunk['text'].toString();
|
||||
|
||||
// 拆分出完整句子(以 [NEXT] 结尾)
|
||||
List<String> sentences = text
|
||||
.split("\n")
|
||||
.where((e) => e.contains("[NEXT]"))
|
||||
.map((e) => e.replaceAll("[NEXT]", "").trim())
|
||||
.toList();
|
||||
|
||||
// 直接覆盖 stepsList
|
||||
planDetail.stepsList = sentences.map((s) => PlanStepDto(stepContent: s)).toList();
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
|
||||
/// 生成步骤解释 -------------------------------
|
||||
await streamUtils.sendStream(
|
||||
"/plan/make_step_explain",
|
||||
data: {
|
||||
"plan_id": planId,
|
||||
"steps": planDetail.stepsList.map((e) => e.toJson()).toList(),
|
||||
},
|
||||
onCall: (chunk) {
|
||||
final text = chunk['text'].toString();
|
||||
List<String> sentences = text
|
||||
.split("\n")
|
||||
.where((e) => e.contains("[NEXT]"))
|
||||
.map((e) => e.replaceAll("[NEXT]", "").trim())
|
||||
.toList();
|
||||
for (int i = 0; i < sentences.length; i++) {
|
||||
planDetail.stepsList[i] = planDetail.stepsList[i]..stepExplain = sentences[i];
|
||||
}
|
||||
notifyListeners();
|
||||
},
|
||||
onEnd: () {},
|
||||
);
|
||||
|
||||
/// 生成建议 ------------------
|
||||
await streamUtils.sendStream(
|
||||
"/plan/make_suggestion",
|
||||
data: {
|
||||
"plan_id": planId,
|
||||
"steps": planDetail.stepsList.map((e) => e.toJson()).toList(),
|
||||
},
|
||||
onCall: (chunk) {
|
||||
final text = chunk['text'].toString();
|
||||
List<String> sentences = text
|
||||
.split("\n")
|
||||
.where((e) => e.contains("[NEXT]"))
|
||||
.map((e) => e.replaceAll("[NEXT]", "").trim())
|
||||
.toList();
|
||||
planDetail.suggestionsList = sentences;
|
||||
notifyListeners();
|
||||
},
|
||||
);
|
||||
await savePlan();
|
||||
}
|
||||
|
||||
///保存计划
|
||||
Future<void> savePlan() async {
|
||||
await savePlanApi(
|
||||
planId: planId,
|
||||
summary: planDetail.summary ?? "",
|
||||
dialog: planDetail.dialog ?? "",
|
||||
steps: planDetail.stepsList,
|
||||
suggestions: planDetail.suggestionsList,
|
||||
);
|
||||
EasyLoading.showToast("计划创建成功");
|
||||
}
|
||||
|
||||
///获取详情
|
||||
Future<void> getPlanDetail() async {
|
||||
planDetail = await getPlanDetailApi(planId);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,13 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _animation;
|
||||
|
||||
///对话框值
|
||||
String _dialog = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
//初始化动画
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: 300),
|
||||
@@ -28,6 +32,7 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
_initDialog();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -36,8 +41,31 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
|
||||
_controller.dispose();
|
||||
}
|
||||
|
||||
///初始化是否显示对话
|
||||
void _initDialog() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final store = context.read<PlanDetailStore>();
|
||||
|
||||
void listener() {
|
||||
if (store.planDetail.dialog != null && !store.showRoleTalk) {
|
||||
setState(() {
|
||||
_dialog = store.planDetail.dialog!;
|
||||
});
|
||||
_toggleShow();
|
||||
store.removeListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
store.addListener(listener);
|
||||
});
|
||||
}
|
||||
|
||||
///切换显示show
|
||||
void _toggleShow() {
|
||||
var store = context.read<PlanDetailStore>();
|
||||
if (store.planDetail.dialog == null) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
store.showRoleTalk = !store.showRoleTalk;
|
||||
if (store.showRoleTalk) {
|
||||
@@ -71,10 +99,7 @@ class _AvatarCardState extends State<AvatarCard> with SingleTickerProviderStateM
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
border: Border.all(color: Colors.black, width: 1),
|
||||
),
|
||||
child: Text(
|
||||
"好的,让我们把学习软件开发这个目标分解成最简单的小步骤,这样你明天就能轻松开始行动",
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
child: Text(_dialog, style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plan/widgets/business/delete_row_item.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class PlanItem extends StatefulWidget {
|
||||
final String title;
|
||||
final String desc;
|
||||
final bool showEdit;
|
||||
final Function(int) onDelete;
|
||||
|
||||
const PlanItem({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.desc,
|
||||
this.showEdit = false,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PlanItem> createState() => _PlanItemState();
|
||||
}
|
||||
|
||||
class _PlanItemState extends State<PlanItem> with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
child: DeleteRowItem(
|
||||
showDelete: widget.showEdit,
|
||||
onDelete: () {},
|
||||
builder: (_, animate) {
|
||||
return [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 3),
|
||||
child: Text(
|
||||
"完成以上步骤后,给自己倒杯水休息一下。",
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"就从这里开始,写下基本信息只需要2分钟,这是最简单的一部",
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizeTransition(
|
||||
axis: Axis.horizontal,
|
||||
sizeFactor: animate,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(left: 10),
|
||||
child: Opacity(
|
||||
opacity: 0.4,
|
||||
child: Icon(RemixIcons.menu_line),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
156
lib/page/plan/detail/widgets/plan_list.dart
Normal file
156
lib/page/plan/detail/widgets/plan_list.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plan/api/dto/plan_detail_dto.dart';
|
||||
import 'package:plan/page/plan/detail/viewmodel/plan_detail_store.dart';
|
||||
import 'package:plan/utils/common.dart';
|
||||
import 'package:plan/widgets/business/delete_row_item.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class PlanList extends StatefulWidget {
|
||||
const PlanList({super.key});
|
||||
|
||||
@override
|
||||
State<PlanList> createState() => _PlanListState();
|
||||
}
|
||||
|
||||
class _PlanListState extends State<PlanList> {
|
||||
final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>();
|
||||
List<PlanStepDto> _stepsList = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initListen();
|
||||
}
|
||||
|
||||
void _initListen() {
|
||||
final store = context.read<PlanDetailStore>();
|
||||
//先初始化,只在详情时才会有意义
|
||||
_stepsList = store.planDetail.stepsList;
|
||||
store.addListener(() {
|
||||
final newList = store.planDetail.stepsList;
|
||||
|
||||
// 找出新增的 item
|
||||
if (newList.length > _stepsList.length) {
|
||||
final addedItems = newList.sublist(_stepsList.length);
|
||||
|
||||
for (var item in addedItems) {
|
||||
final index = _stepsList.length;
|
||||
_stepsList.add(item);
|
||||
|
||||
// 插入动画
|
||||
_listKey.currentState?.insertItem(
|
||||
index,
|
||||
duration: Duration(milliseconds: 300),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_stepsList = store.planDetail.stepsList;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverAnimatedList(
|
||||
key: _listKey,
|
||||
initialItemCount: _stepsList.length,
|
||||
itemBuilder: (_, index, animation) {
|
||||
//动画
|
||||
final curvedAnimation = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
return PlanItem(
|
||||
step: _stepsList[index],
|
||||
curvedAnimation: curvedAnimation,
|
||||
onDelete: (id) {},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlanItem extends StatelessWidget {
|
||||
final PlanStepDto step;
|
||||
final bool showEdit;
|
||||
final CurvedAnimation curvedAnimation;
|
||||
final Function(int) onDelete;
|
||||
|
||||
const PlanItem({
|
||||
super.key,
|
||||
required this.step,
|
||||
required this.curvedAnimation,
|
||||
this.showEdit = false,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: curvedAnimation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: curvedAnimation,
|
||||
axis: Axis.vertical,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
child: DeleteRowItem(
|
||||
showDelete: showEdit,
|
||||
onDelete: () {},
|
||||
builder: (_, animate) {
|
||||
return [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 3),
|
||||
child: Text(
|
||||
step.stepContent ?? "",
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 300),
|
||||
transitionBuilder: (child, animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axis: Axis.vertical,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: getNotEmpty(step.stepExplain) == null
|
||||
? SizedBox.shrink(key: ValueKey("empty"))
|
||||
: Text(
|
||||
step.stepExplain!,
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
key: ValueKey(step.stepExplain),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizeTransition(
|
||||
axis: Axis.horizontal,
|
||||
sizeFactor: animate,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(left: 10),
|
||||
child: Opacity(
|
||||
opacity: 0.4,
|
||||
child: Icon(RemixIcons.menu_line),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ScrollBox extends StatelessWidget {
|
||||
final ScrollController? scrollController;
|
||||
final Widget child;
|
||||
|
||||
const ScrollBox({super.key, required this.child});
|
||||
const ScrollBox({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.scrollController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ScrollbarTheme(
|
||||
data: ScrollbarThemeData(
|
||||
thumbColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surfaceContainerHigh),
|
||||
thumbColor: WidgetStateProperty.all(
|
||||
Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
thickness: WidgetStateProperty.all(3),
|
||||
crossAxisMargin: 3,
|
||||
mainAxisMargin: 2,
|
||||
radius: const Radius.circular(5),
|
||||
),
|
||||
child: Scrollbar(
|
||||
controller: scrollController,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plan/page/plan/detail/viewmodel/plan_detail_store.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
///模块标题
|
||||
@@ -7,66 +9,165 @@ class SuggestedTitle extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 20, bottom: 5),
|
||||
child: Opacity(
|
||||
opacity: 0.6,
|
||||
child: Row(
|
||||
spacing: 20,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 15,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
var list = context.select<PlanDetailStore, List<String>>(
|
||||
(store) => store.planDetail.suggestionsList,
|
||||
);
|
||||
if (list.isEmpty) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: SizedBox(),
|
||||
);
|
||||
}
|
||||
return SliverToBoxAdapter(
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: 1), // 从透明到完全显示
|
||||
duration: const Duration(milliseconds: 300),
|
||||
builder: (context, value, child) {
|
||||
return Opacity(
|
||||
opacity: value,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 20, bottom: 5),
|
||||
child: Opacity(
|
||||
opacity: 0.6,
|
||||
child: Row(
|
||||
spacing: 20,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 15,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
Text(
|
||||
"额外建议",
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
Container(
|
||||
width: 15,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"额外建议",
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
Container(
|
||||
width: 15,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SuggestedItem extends StatelessWidget {
|
||||
final String title;
|
||||
///额外建议列表
|
||||
class SuggestedList extends StatefulWidget {
|
||||
const SuggestedList({super.key});
|
||||
|
||||
const SuggestedItem({super.key, required this.title});
|
||||
@override
|
||||
State<SuggestedList> createState() => _SuggestedListState();
|
||||
}
|
||||
|
||||
class _SuggestedListState extends State<SuggestedList> {
|
||||
final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>();
|
||||
List<String> _suggestionsList = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initListen();
|
||||
}
|
||||
|
||||
void _initListen() {
|
||||
final store = context.read<PlanDetailStore>();
|
||||
//先初始化,只在详情时才会有意义
|
||||
_suggestionsList = store.planDetail.suggestionsList;
|
||||
store.addListener(() {
|
||||
final newList = store.planDetail.suggestionsList;
|
||||
|
||||
// 找出新增的 item
|
||||
if (newList.length > _suggestionsList.length) {
|
||||
final addedItems = newList.sublist(_suggestionsList.length);
|
||||
|
||||
for (var item in addedItems) {
|
||||
final index = _suggestionsList.length;
|
||||
_suggestionsList.add(item);
|
||||
|
||||
// 插入动画
|
||||
_listKey.currentState?.insertItem(
|
||||
index,
|
||||
duration: Duration(milliseconds: 300),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_suggestionsList = store.planDetail.suggestionsList;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: const Offset(0, 5),
|
||||
child: Icon(
|
||||
RemixIcons.lightbulb_flash_fill,
|
||||
color: Color(0xfff2a529),
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Opacity(
|
||||
opacity: 0.5,
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
return SliverAnimatedList(
|
||||
key: _listKey,
|
||||
initialItemCount: _suggestionsList.length,
|
||||
itemBuilder: (_, index, animation) {
|
||||
final curvedAnimation = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
return SuggestedItem(
|
||||
title: _suggestionsList[index],
|
||||
curvedAnimation: curvedAnimation,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///建议item
|
||||
class SuggestedItem extends StatelessWidget {
|
||||
final String title;
|
||||
final CurvedAnimation curvedAnimation;
|
||||
|
||||
const SuggestedItem({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.curvedAnimation,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: curvedAnimation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: curvedAnimation,
|
||||
axis: Axis.vertical,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 10,
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: const Offset(0, 5),
|
||||
child: Icon(
|
||||
RemixIcons.lightbulb_flash_fill,
|
||||
color: Color(0xfff2a529),
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Opacity(
|
||||
opacity: 0.5,
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user