This commit is contained in:
zhutao
2025-09-08 15:30:09 +08:00
parent 193d29b0ce
commit 4623094bad
15 changed files with 374 additions and 246 deletions

View File

@@ -14,23 +14,21 @@ class _CoachMessageState extends State<CoachMessage> {
Widget build(BuildContext context) {
var store = context.read<PlanDetailStore>();
if (store.planContent.isEmpty) {
return SliverToBoxAdapter();
return SizedBox();
}
return SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.all(20),
child: Column(
children: [
Text(
"你的教练正在拆分",
style: Theme.of(context).textTheme.bodyMedium,
),
Text(
'"${store.planContent}"',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
return Container(
padding: EdgeInsets.all(20),
child: Column(
children: [
Text(
"你的教练正在拆分",
style: Theme.of(context).textTheme.bodyMedium,
),
Text(
'"${store.planContent}"',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
);
}

View File

@@ -1,5 +1,9 @@
import 'package:animated_reorderable_list/animated_reorderable_list.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:plan/api/dto/plan_detail_dto.dart';
import 'package:plan/api/endpoints/plan_api.dart';
import 'package:plan/data/models/plan_acttion_type.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';
@@ -14,143 +18,197 @@ class PlanList extends StatefulWidget {
}
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;
///删除步骤
void _handDelete(int id) async {
var store = context.read<PlanDetailStore>();
store.updatePlanDetail((dto) {
dto.stepsList = dto.stepsList.where((element) => element.id != id).toList();
});
await editPlanStepApi(
int.parse(store.planId),
act: PlanActionType.delete,
stepId: id,
);
}
// 找出新增的 item
if (newList.length > _stepsList.length) {
final addedItems = newList.sublist(_stepsList.length);
///确认排序
void _confirmSort(List<PlanStepDto> list) async {
var store = context.read<PlanDetailStore>();
store.updatePlanDetail((dto) {
dto.stepsList = list;
});
await editPlanStepOrderApi(int.parse(store.planId), list);
}
for (var item in addedItems) {
final index = _stepsList.length;
_stepsList.add(item);
///确认完成或者取消
void _handComplete(int id) async {
HapticFeedback.vibrate();
var store = context.read<PlanDetailStore>();
// 插入动画
_listKey.currentState?.insertItem(
index,
duration: Duration(milliseconds: 300),
store.updatePlanDetail((dto) {
dto.stepsList = dto.stepsList.map((step) {
if (step.id == id) {
// 只更新匹配的项
step.stepStatus = step.stepStatus == 2 ? 0 : 2;
editPlanStepApi(
int.parse(store.planId),
act: PlanActionType.complete,
stepId: id,
);
}
} else {
setState(() {
_stepsList = store.planDetail.stepsList;
});
}
return step;
}).toList();
});
}
@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) {},
final isEdit = context.select<PlanDetailStore, bool>((s) => s.isEdit);
return Selector<PlanDetailStore, List<String>>(
selector: (_, store) => store.planDetail.stepsList
.map((e) => '${e.id}_${e.stepExplain}_${e.stepStatus}')
.toList(),
builder: (context, list, _) {
final list = context.read<PlanDetailStore>().planDetail.stepsList;
return AnimatedReorderableListView(
items: list,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
var item = list[index];
return PlanItem(
key: ValueKey('${item.id ?? index}_${item.hashCode}'),
step: item,
isEdit: isEdit,
onDelete: _handDelete,
onComplete: _handComplete,
);
},
enterTransition: [SlideInDown()],
exitTransition: [SlideInUp()],
insertDuration: const Duration(milliseconds: 300),
removeDuration: const Duration(milliseconds: 300),
dragStartDelay: const Duration(milliseconds: 300),
onReorder: (int oldIndex, int newIndex) {
setState(() {
final item = list.removeAt(oldIndex);
list.insert(newIndex, item);
_confirmSort(list);
});
},
nonDraggableItems: isEdit ? <PlanStepDto>[] : list,
buildDefaultDragHandles: false,
longPressDraggable: false,
isSameItem: (a, b) => a.stepContent == b.stepContent,
);
},
);
}
}
///计划item
class PlanItem extends StatelessWidget {
final PlanStepDto step;
final bool showEdit;
final CurvedAnimation curvedAnimation;
final bool isEdit;
final Function(int) onDelete;
final Function(int) onComplete;
const PlanItem({
super.key,
required this.step,
required this.curvedAnimation,
this.showEdit = false,
this.isEdit = false,
required this.onDelete,
required this.onComplete,
});
@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,
),
return GestureDetector(
onTap: () {
if (!isEdit) {
onComplete(step.id!);
}
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
color: Colors.white,
child: DeleteRowItem(
showDelete: isEdit,
onDelete: () {
onDelete(step.id!);
},
builder: (_, animate) {
return [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(bottom: 3),
child: Text(
step.stepContent ?? "",
style: stepTextStyle(context, 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),
),
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: stepTextStyle(
context,
Theme.of(context).textTheme.labelSmall,
),
key: ValueKey(step.stepExplain),
),
),
],
),
),
SizeTransition(
axis: Axis.horizontal,
sizeFactor: animate,
child: Container(
alignment: Alignment.center,
margin: EdgeInsets.only(left: 10),
child: Opacity(
opacity: 0.4,
child: Icon(RemixIcons.menu_line),
),
),
];
},
),
),
];
},
),
),
);
}
TextStyle? stepTextStyle(BuildContext context, TextStyle? baseStyle) {
if (step.stepStatus == 2) {
return baseStyle?.copyWith(
decoration: TextDecoration.lineThrough,
decorationThickness: 3,
decorationColor: Theme.of(context).colorScheme.onSurfaceVariant,
color: Theme.of(context).colorScheme.onSurfaceVariant,
);
}
return baseStyle;
}
}

View File

@@ -23,8 +23,9 @@ class ScrollBox extends StatelessWidget {
radius: const Radius.circular(5),
),
child: Scrollbar(
controller: scrollController,
child: child,
child: SingleChildScrollView(
child: child,
),
),
);
}

View File

@@ -9,51 +9,48 @@ class SuggestedTitle extends StatelessWidget {
@override
Widget build(BuildContext context) {
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,
),
],
return Consumer<PlanDetailStore>(
builder: (context, store, _) {
if (store.planDetail.suggestionsList.isEmpty) {
return SizedBox();
}
return 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,
),
],
),
),
),
),
),
);
},
);
}
}
@@ -67,13 +64,14 @@ class SuggestedList extends StatefulWidget {
}
class _SuggestedListState extends State<SuggestedList> {
final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>();
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
List<String> _suggestionsList = [];
@override
void initState() {
super.initState();
_initListen();
WidgetsBinding.instance.addPostFrameCallback((_) => _initListen());
}
void _initListen() {
@@ -82,7 +80,6 @@ class _SuggestedListState extends State<SuggestedList> {
_suggestionsList = store.planDetail.suggestionsList;
store.addListener(() {
final newList = store.planDetail.suggestionsList;
// 找出新增的 item
if (newList.length > _suggestionsList.length) {
final addedItems = newList.sublist(_suggestionsList.length);
@@ -107,8 +104,10 @@ class _SuggestedListState extends State<SuggestedList> {
@override
Widget build(BuildContext context) {
return SliverAnimatedList(
return AnimatedList(
key: _listKey,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
initialItemCount: _suggestionsList.length,
itemBuilder: (_, index, animation) {
final curvedAnimation = CurvedAnimation(