1
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
class PlanStepDto {
|
class PlanStepDto {
|
||||||
num? id;
|
int? id;
|
||||||
String? stepIcon;
|
String? stepIcon;
|
||||||
String? stepContent;
|
String? stepContent;
|
||||||
String? stepExplain;
|
String? stepExplain;
|
||||||
num? stepStatus;
|
int? stepStatus;
|
||||||
|
|
||||||
PlanStepDto({this.id, this.stepIcon, this.stepContent, this.stepExplain, this.stepStatus});
|
PlanStepDto({this.id, this.stepIcon, this.stepContent, this.stepExplain, this.stepStatus});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:plan/api/dto/plan_detail_dto.dart';
|
|||||||
import 'package:plan/api/dto/plan_item_dto.dart';
|
import 'package:plan/api/dto/plan_item_dto.dart';
|
||||||
import 'package:plan/api/network/request.dart';
|
import 'package:plan/api/network/request.dart';
|
||||||
|
|
||||||
|
import '../../data/models/plan_acttion_type.dart';
|
||||||
|
|
||||||
///初始化计划
|
///初始化计划
|
||||||
Future<int> initPlanApi(String need, int agentId) async {
|
Future<int> initPlanApi(String need, int agentId) async {
|
||||||
var res = await Request().post("/plan/init", {
|
var res = await Request().post("/plan/init", {
|
||||||
@@ -56,3 +58,28 @@ Future<void> deletePlanApi(int planId) async {
|
|||||||
"plan_id": planId,
|
"plan_id": planId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///编辑用户计划步骤
|
||||||
|
Future<void> editPlanStepApi(
|
||||||
|
int planId, {
|
||||||
|
required PlanActionType act,
|
||||||
|
int? stepId,
|
||||||
|
String? content,
|
||||||
|
String? explain,
|
||||||
|
}) async {
|
||||||
|
await Request().post("/plan/edit_plan_steps_info", {
|
||||||
|
"plan_id": planId,
|
||||||
|
"act": act.value,
|
||||||
|
"step_id": stepId,
|
||||||
|
"step_content": content,
|
||||||
|
"step_explain": explain,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
///修改步骤顺序
|
||||||
|
Future<void> editPlanStepOrderApi(int planId,List<PlanStepDto> list) async {
|
||||||
|
await Request().post("/plan/change_plan_steps", {
|
||||||
|
"plan_id": planId,
|
||||||
|
"steps": list.map((e) => e.toJson()).toList(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
10
lib/data/models/plan_acttion_type.dart
Normal file
10
lib/data/models/plan_acttion_type.dart
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
enum PlanActionType {
|
||||||
|
delete(1), //删除
|
||||||
|
complete(2), //完成
|
||||||
|
edit(3), //编辑
|
||||||
|
completeAll(99); //全部完成
|
||||||
|
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
const PlanActionType(this.value);
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ class PlanFormCard extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PlanFormCardState extends State<PlanFormCard> {
|
class _PlanFormCardState extends State<PlanFormCard> {
|
||||||
final TextEditingController _inputController = TextEditingController(text: "");
|
final TextEditingController _inputController = TextEditingController(text: "刷牙");
|
||||||
|
|
||||||
void _handSubmit() {
|
void _handSubmit() {
|
||||||
if (_inputController.text.isEmpty) {
|
if (_inputController.text.isEmpty) {
|
||||||
|
|||||||
80
lib/page/plan/detail/navigation/bar_actions.dart
Normal file
80
lib/page/plan/detail/navigation/bar_actions.dart
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:plan/api/endpoints/plan_api.dart';
|
||||||
|
import 'package:plan/widgets/ui_kit/popup/popup_action.dart';
|
||||||
|
import 'package:remixicon/remixicon.dart';
|
||||||
|
|
||||||
|
import '../../widgets/edit_desc_dialog.dart';
|
||||||
|
import '../viewmodel/plan_detail_store.dart';
|
||||||
|
|
||||||
|
class BarActions extends StatefulWidget {
|
||||||
|
final PlanDetailStore store;
|
||||||
|
|
||||||
|
const BarActions({super.key, required this.store});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BarActions> createState() => _BarActionsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BarActionsState extends State<BarActions> {
|
||||||
|
///popup菜单
|
||||||
|
void _onPopupActionSelected(String value) {
|
||||||
|
if (value == 'edit_step') {
|
||||||
|
widget.store.setEdit(true);
|
||||||
|
} else if (value == 'edit_desc') {
|
||||||
|
showEditDescDialog(
|
||||||
|
context,
|
||||||
|
value: widget.store.planDetail.summary ?? "",
|
||||||
|
onConfirm: (value) async {
|
||||||
|
widget.store.updatePlanDetail((dto) {
|
||||||
|
dto.summary = value;
|
||||||
|
});
|
||||||
|
await editPlanSummaryApi(int.parse(widget.store.planId), value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///取消编辑
|
||||||
|
void _cancelEdit() {
|
||||||
|
widget.store.setEdit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min, // 关键:Row 只占实际内容宽度
|
||||||
|
children: [
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: Duration(milliseconds: 300),
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
// 仅使用渐变动画
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: widget.store.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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:plan/page/plan/detail/navigation/bar_actions.dart';
|
||||||
import 'package:plan/page/plan/detail/viewmodel/plan_detail_store.dart';
|
import 'package:plan/page/plan/detail/viewmodel/plan_detail_store.dart';
|
||||||
import 'package:plan/theme/decorations/app_shadows.dart';
|
import 'package:plan/theme/decorations/app_shadows.dart';
|
||||||
import 'package:plan/widgets/ui_kit/popup/popup_action.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
|
||||||
|
|
||||||
import '../widgets/edit_desc_dialog.dart';
|
|
||||||
import 'widgets/avatar_card.dart';
|
import 'widgets/avatar_card.dart';
|
||||||
import 'widgets/coach_message.dart';
|
import 'widgets/coach_message.dart';
|
||||||
import 'widgets/plan_list.dart';
|
import 'widgets/plan_list.dart';
|
||||||
@@ -28,7 +26,6 @@ class PlanDetailPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PlanDetailPageState extends State<PlanDetailPage> {
|
class _PlanDetailPageState extends State<PlanDetailPage> {
|
||||||
bool _isEdit = false;
|
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
|
|
||||||
///store对象
|
///store对象
|
||||||
@@ -44,28 +41,6 @@ class _PlanDetailPageState extends State<PlanDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///popup菜单
|
|
||||||
void _onPopupActionSelected(String value) {
|
|
||||||
if (value == 'edit_step') {
|
|
||||||
setState(() {
|
|
||||||
_isEdit = true;
|
|
||||||
});
|
|
||||||
} else if (value == 'edit_desc') {
|
|
||||||
showEditDescDialog(
|
|
||||||
context,
|
|
||||||
value: "你好",
|
|
||||||
onConfirm: (value) {},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///取消编辑
|
|
||||||
void _cancelEdit() {
|
|
||||||
setState(() {
|
|
||||||
_isEdit = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider<PlanDetailStore>(
|
return ChangeNotifierProvider<PlanDetailStore>(
|
||||||
@@ -83,9 +58,8 @@ class _PlanDetailPageState extends State<PlanDetailPage> {
|
|||||||
child: ScrollBox(
|
child: ScrollBox(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: shadowDecoration,
|
decoration: shadowDecoration,
|
||||||
child: CustomScrollView(
|
child: Column(
|
||||||
controller: scrollController,
|
children: [
|
||||||
slivers: [
|
|
||||||
CoachMessage(),
|
CoachMessage(),
|
||||||
PlanList(),
|
PlanList(),
|
||||||
SuggestedTitle(),
|
SuggestedTitle(),
|
||||||
@@ -104,40 +78,7 @@ class _PlanDetailPageState extends State<PlanDetailPage> {
|
|||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
navigationBar: CupertinoNavigationBar(
|
navigationBar: CupertinoNavigationBar(
|
||||||
middle: Text(store.planDetail.summary ?? ""),
|
middle: Text(store.planDetail.summary ?? ""),
|
||||||
trailing: Row(
|
trailing: BarActions(store: store),
|
||||||
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!,
|
child: child!,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class PlanDetailStore extends ChangeNotifier {
|
|||||||
//如果没有id进行初始化
|
//如果没有id进行初始化
|
||||||
if (planId == "0") {
|
if (planId == "0") {
|
||||||
createPlan();
|
createPlan();
|
||||||
}else{
|
} else {
|
||||||
//获取详情
|
//获取详情
|
||||||
getPlanDetail();
|
getPlanDetail();
|
||||||
}
|
}
|
||||||
@@ -42,6 +42,15 @@ class PlanDetailStore extends ChangeNotifier {
|
|||||||
///计划详情
|
///计划详情
|
||||||
PlanDetailDto planDetail = PlanDetailDto(summary: "计划详情");
|
PlanDetailDto planDetail = PlanDetailDto(summary: "计划详情");
|
||||||
|
|
||||||
|
///是否正在编辑
|
||||||
|
bool isEdit = false;
|
||||||
|
|
||||||
|
///切换编辑模式
|
||||||
|
void setEdit(bool value) {
|
||||||
|
isEdit = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
///流请求工具
|
///流请求工具
|
||||||
StreamUtils streamUtils = StreamUtils();
|
StreamUtils streamUtils = StreamUtils();
|
||||||
|
|
||||||
@@ -49,7 +58,6 @@ class PlanDetailStore extends ChangeNotifier {
|
|||||||
void createPlan() async {
|
void createPlan() async {
|
||||||
var id = await initPlanApi(planContent, 1);
|
var id = await initPlanApi(planContent, 1);
|
||||||
planId = id.toString();
|
planId = id.toString();
|
||||||
// planId = "3";
|
|
||||||
|
|
||||||
///生成摘要---------------------------
|
///生成摘要---------------------------
|
||||||
String summary = "";
|
String summary = "";
|
||||||
@@ -92,9 +100,12 @@ class PlanDetailStore extends ChangeNotifier {
|
|||||||
.where((e) => e.contains("[NEXT]"))
|
.where((e) => e.contains("[NEXT]"))
|
||||||
.map((e) => e.replaceAll("[NEXT]", "").trim())
|
.map((e) => e.replaceAll("[NEXT]", "").trim())
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// 直接覆盖 stepsList
|
// 直接覆盖 stepsList
|
||||||
planDetail.stepsList = sentences.map((s) => PlanStepDto(stepContent: s)).toList();
|
planDetail.stepsList = sentences
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((e) => PlanStepDto(id: e.key, stepContent: e.value))
|
||||||
|
.toList();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -159,4 +170,10 @@ class PlanDetailStore extends ChangeNotifier {
|
|||||||
planDetail = await getPlanDetailApi(planId);
|
planDetail = await getPlanDetailApi(planId);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///更新计划详情
|
||||||
|
void updatePlanDetail(void Function(PlanDetailDto dto) updater) {
|
||||||
|
updater(planDetail);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,23 +14,21 @@ class _CoachMessageState extends State<CoachMessage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var store = context.read<PlanDetailStore>();
|
var store = context.read<PlanDetailStore>();
|
||||||
if (store.planContent.isEmpty) {
|
if (store.planContent.isEmpty) {
|
||||||
return SliverToBoxAdapter();
|
return SizedBox();
|
||||||
}
|
}
|
||||||
return SliverToBoxAdapter(
|
return Container(
|
||||||
child: Container(
|
padding: EdgeInsets.all(20),
|
||||||
padding: EdgeInsets.all(20),
|
child: Column(
|
||||||
child: Column(
|
children: [
|
||||||
children: [
|
Text(
|
||||||
Text(
|
"你的教练正在拆分",
|
||||||
"你的教练正在拆分",
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
),
|
||||||
),
|
Text(
|
||||||
Text(
|
'"${store.planContent}"',
|
||||||
'"${store.planContent}"',
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
),
|
||||||
),
|
],
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import 'package:animated_reorderable_list/animated_reorderable_list.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:plan/api/dto/plan_detail_dto.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/page/plan/detail/viewmodel/plan_detail_store.dart';
|
||||||
import 'package:plan/utils/common.dart';
|
import 'package:plan/utils/common.dart';
|
||||||
import 'package:plan/widgets/business/delete_row_item.dart';
|
import 'package:plan/widgets/business/delete_row_item.dart';
|
||||||
@@ -14,143 +18,197 @@ class PlanList extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PlanListState extends State<PlanList> {
|
class _PlanListState extends State<PlanList> {
|
||||||
final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>();
|
|
||||||
List<PlanStepDto> _stepsList = [];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initListen();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initListen() {
|
///删除步骤
|
||||||
final store = context.read<PlanDetailStore>();
|
void _handDelete(int id) async {
|
||||||
//先初始化,只在详情时才会有意义
|
var store = context.read<PlanDetailStore>();
|
||||||
_stepsList = store.planDetail.stepsList;
|
store.updatePlanDetail((dto) {
|
||||||
store.addListener(() {
|
dto.stepsList = dto.stepsList.where((element) => element.id != id).toList();
|
||||||
final newList = store.planDetail.stepsList;
|
});
|
||||||
|
await editPlanStepApi(
|
||||||
|
int.parse(store.planId),
|
||||||
|
act: PlanActionType.delete,
|
||||||
|
stepId: id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 找出新增的 item
|
///确认排序
|
||||||
if (newList.length > _stepsList.length) {
|
void _confirmSort(List<PlanStepDto> list) async {
|
||||||
final addedItems = newList.sublist(_stepsList.length);
|
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;
|
void _handComplete(int id) async {
|
||||||
_stepsList.add(item);
|
HapticFeedback.vibrate();
|
||||||
|
var store = context.read<PlanDetailStore>();
|
||||||
|
|
||||||
// 插入动画
|
store.updatePlanDetail((dto) {
|
||||||
_listKey.currentState?.insertItem(
|
dto.stepsList = dto.stepsList.map((step) {
|
||||||
index,
|
if (step.id == id) {
|
||||||
duration: Duration(milliseconds: 300),
|
// 只更新匹配的项
|
||||||
|
step.stepStatus = step.stepStatus == 2 ? 0 : 2;
|
||||||
|
editPlanStepApi(
|
||||||
|
int.parse(store.planId),
|
||||||
|
act: PlanActionType.complete,
|
||||||
|
stepId: id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
return step;
|
||||||
setState(() {
|
}).toList();
|
||||||
_stepsList = store.planDetail.stepsList;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SliverAnimatedList(
|
final isEdit = context.select<PlanDetailStore, bool>((s) => s.isEdit);
|
||||||
key: _listKey,
|
return Selector<PlanDetailStore, List<String>>(
|
||||||
initialItemCount: _stepsList.length,
|
selector: (_, store) => store.planDetail.stepsList
|
||||||
itemBuilder: (_, index, animation) {
|
.map((e) => '${e.id}_${e.stepExplain}_${e.stepStatus}')
|
||||||
//动画
|
.toList(),
|
||||||
final curvedAnimation = CurvedAnimation(
|
builder: (context, list, _) {
|
||||||
parent: animation,
|
final list = context.read<PlanDetailStore>().planDetail.stepsList;
|
||||||
curve: Curves.easeInOut,
|
return AnimatedReorderableListView(
|
||||||
);
|
items: list,
|
||||||
return PlanItem(
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
step: _stepsList[index],
|
shrinkWrap: true,
|
||||||
curvedAnimation: curvedAnimation,
|
itemBuilder: (BuildContext context, int index) {
|
||||||
onDelete: (id) {},
|
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 {
|
class PlanItem extends StatelessWidget {
|
||||||
final PlanStepDto step;
|
final PlanStepDto step;
|
||||||
final bool showEdit;
|
final bool isEdit;
|
||||||
final CurvedAnimation curvedAnimation;
|
|
||||||
final Function(int) onDelete;
|
final Function(int) onDelete;
|
||||||
|
final Function(int) onComplete;
|
||||||
|
|
||||||
const PlanItem({
|
const PlanItem({
|
||||||
super.key,
|
super.key,
|
||||||
required this.step,
|
required this.step,
|
||||||
required this.curvedAnimation,
|
this.isEdit = false,
|
||||||
this.showEdit = false,
|
|
||||||
required this.onDelete,
|
required this.onDelete,
|
||||||
|
required this.onComplete,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FadeTransition(
|
return GestureDetector(
|
||||||
opacity: curvedAnimation,
|
onTap: () {
|
||||||
child: SizeTransition(
|
if (!isEdit) {
|
||||||
sizeFactor: curvedAnimation,
|
onComplete(step.id!);
|
||||||
axis: Axis.vertical,
|
}
|
||||||
child: Container(
|
},
|
||||||
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
child: Container(
|
||||||
child: DeleteRowItem(
|
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
showDelete: showEdit,
|
color: Colors.white,
|
||||||
onDelete: () {},
|
child: DeleteRowItem(
|
||||||
builder: (_, animate) {
|
showDelete: isEdit,
|
||||||
return [
|
onDelete: () {
|
||||||
Expanded(
|
onDelete(step.id!);
|
||||||
child: Column(
|
},
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
builder: (_, animate) {
|
||||||
children: [
|
return [
|
||||||
Container(
|
Expanded(
|
||||||
margin: EdgeInsets.only(bottom: 3),
|
child: Column(
|
||||||
child: Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
step.stepContent ?? "",
|
children: [
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ class ScrollBox extends StatelessWidget {
|
|||||||
radius: const Radius.circular(5),
|
radius: const Radius.circular(5),
|
||||||
),
|
),
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
controller: scrollController,
|
child: SingleChildScrollView(
|
||||||
child: child,
|
child: child,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,51 +9,48 @@ class SuggestedTitle extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var list = context.select<PlanDetailStore, List<String>>(
|
return Consumer<PlanDetailStore>(
|
||||||
(store) => store.planDetail.suggestionsList,
|
builder: (context, store, _) {
|
||||||
);
|
if (store.planDetail.suggestionsList.isEmpty) {
|
||||||
if (list.isEmpty) {
|
return SizedBox();
|
||||||
return const SliverToBoxAdapter(
|
}
|
||||||
child: SizedBox(),
|
return TweenAnimationBuilder<double>(
|
||||||
);
|
tween: Tween(begin: 0, end: 1), // 从透明到完全显示
|
||||||
}
|
duration: const Duration(milliseconds: 300),
|
||||||
return SliverToBoxAdapter(
|
builder: (context, value, child) {
|
||||||
child: TweenAnimationBuilder<double>(
|
return Opacity(
|
||||||
tween: Tween(begin: 0, end: 1), // 从透明到完全显示
|
opacity: value,
|
||||||
duration: const Duration(milliseconds: 300),
|
child: child,
|
||||||
builder: (context, value, child) {
|
);
|
||||||
return Opacity(
|
},
|
||||||
opacity: value,
|
child: Container(
|
||||||
child: child,
|
margin: const EdgeInsets.only(top: 20, bottom: 5),
|
||||||
);
|
child: Opacity(
|
||||||
},
|
opacity: 0.6,
|
||||||
child: Container(
|
child: Row(
|
||||||
margin: const EdgeInsets.only(top: 20, bottom: 5),
|
spacing: 20,
|
||||||
child: Opacity(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
opacity: 0.6,
|
children: [
|
||||||
child: Row(
|
Container(
|
||||||
spacing: 20,
|
width: 15,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
height: 1,
|
||||||
children: [
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
Container(
|
),
|
||||||
width: 15,
|
Text(
|
||||||
height: 1,
|
"额外建议",
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
),
|
),
|
||||||
Text(
|
Container(
|
||||||
"额外建议",
|
width: 15,
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
height: 1,
|
||||||
),
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
Container(
|
),
|
||||||
width: 15,
|
],
|
||||||
height: 1,
|
),
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,13 +64,14 @@ class SuggestedList extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SuggestedListState extends State<SuggestedList> {
|
class _SuggestedListState extends State<SuggestedList> {
|
||||||
final GlobalKey<SliverAnimatedListState> _listKey = GlobalKey<SliverAnimatedListState>();
|
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
|
||||||
List<String> _suggestionsList = [];
|
List<String> _suggestionsList = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initListen();
|
_initListen();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _initListen());
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initListen() {
|
void _initListen() {
|
||||||
@@ -82,7 +80,6 @@ class _SuggestedListState extends State<SuggestedList> {
|
|||||||
_suggestionsList = store.planDetail.suggestionsList;
|
_suggestionsList = store.planDetail.suggestionsList;
|
||||||
store.addListener(() {
|
store.addListener(() {
|
||||||
final newList = store.planDetail.suggestionsList;
|
final newList = store.planDetail.suggestionsList;
|
||||||
|
|
||||||
// 找出新增的 item
|
// 找出新增的 item
|
||||||
if (newList.length > _suggestionsList.length) {
|
if (newList.length > _suggestionsList.length) {
|
||||||
final addedItems = newList.sublist(_suggestionsList.length);
|
final addedItems = newList.sublist(_suggestionsList.length);
|
||||||
@@ -107,8 +104,10 @@ class _SuggestedListState extends State<SuggestedList> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SliverAnimatedList(
|
return AnimatedList(
|
||||||
key: _listKey,
|
key: _listKey,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
initialItemCount: _suggestionsList.length,
|
initialItemCount: _suggestionsList.length,
|
||||||
itemBuilder: (_, index, animation) {
|
itemBuilder: (_, index, animation) {
|
||||||
final curvedAnimation = CurvedAnimation(
|
final curvedAnimation = CurvedAnimation(
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:plan/api/dto/plan_item_dto.dart';
|
import 'package:plan/api/dto/plan_item_dto.dart';
|
||||||
import 'package:plan/api/endpoints/plan_api.dart';
|
import 'package:plan/api/endpoints/plan_api.dart';
|
||||||
import 'package:remixicon/remixicon.dart';
|
|
||||||
|
|
||||||
import 'widgets/history_item.dart';
|
import 'widgets/history_item.dart';
|
||||||
import '../../../widgets/ui_kit/popup/popup_action.dart';
|
|
||||||
import 'widgets/loading_box.dart';
|
import 'widgets/loading_box.dart';
|
||||||
|
|
||||||
class PlanHistoryPage extends StatefulWidget {
|
class PlanHistoryPage extends StatefulWidget {
|
||||||
@@ -38,23 +36,11 @@ class _PlanHistoryPageState extends State<PlanHistoryPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
///popup事件
|
|
||||||
void _onPopupActionSelected(String value) {
|
|
||||||
switch (value) {
|
|
||||||
case 'edit':
|
|
||||||
setState(() {
|
|
||||||
_isDelete = !_isDelete;
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///确认删除
|
///确认删除
|
||||||
void _confirmDelete(int id) {
|
void _confirmDelete(int id) {
|
||||||
print(_record.map((e) => e.toJson()));
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_record.removeWhere((element) => element.id == id);
|
_record.removeWhere((element) => element.id == id);
|
||||||
print(_record.map((e) => e.toJson()));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ class _DeleteRowItemState extends State<DeleteRowItem> with TickerProviderStateM
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SizeTransition(
|
SizeTransition(
|
||||||
axis: Axis.horizontal,
|
axis: Axis.horizontal,
|
||||||
@@ -98,6 +99,7 @@ class _DeleteRowItemState extends State<DeleteRowItem> with TickerProviderStateM
|
|||||||
onTap: _handleDelete,
|
onTap: _handleDelete,
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: EdgeInsets.only(right: 10),
|
margin: EdgeInsets.only(right: 10),
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
RemixIcons.indeterminate_circle_fill,
|
RemixIcons.indeterminate_circle_fill,
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
animated_reorderable_list:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: animated_reorderable_list
|
||||||
|
sha256: "5de5cca556a8c9c8f7b65234ae4b683593dc6e167db498744a5e389302f24d13"
|
||||||
|
url: "https://pub.flutter-io.cn"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ dependencies:
|
|||||||
flutter_image_compress: ^2.4.0
|
flutter_image_compress: ^2.4.0
|
||||||
cached_network_image: ^3.4.1
|
cached_network_image: ^3.4.1
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
|
animated_reorderable_list: ^1.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user