From 193d29b0ce187151a949fe4ae067684177cede92 Mon Sep 17 00:00:00 2001 From: zhutao <1812073942@qq.com> Date: Fri, 5 Sep 2025 18:00:26 +0800 Subject: [PATCH] 1 --- assets/image/empty_data.png | Bin 34985 -> 31559 bytes lib/api/dto/plan_detail_dto.dart | 2 +- lib/api/dto/plan_item_dto.dart | 41 ++++ lib/api/endpoints/plan_api.dart | 48 +++++ lib/page/home/widget/plan_form_card.dart | 4 +- lib/page/plan/detail/plan_detail_page.dart | 134 ++++++------ .../detail/viewmodel/plan_detail_store.dart | 133 +++++++++++- lib/page/plan/detail/widgets/avatar_card.dart | 33 ++- lib/page/plan/detail/widgets/plan_item.dart | 71 ------- lib/page/plan/detail/widgets/plan_list.dart | 156 ++++++++++++++ lib/page/plan/detail/widgets/scroll_box.dart | 12 +- lib/page/plan/detail/widgets/suggested.dart | 195 +++++++++++++----- lib/page/plan/history/plan_history_page.dart | 96 ++++----- .../plan/history/widgets/history_item.dart | 176 +++++++++++----- .../plan/history/widgets/loading_box.dart | 48 +++++ lib/page/plan/widgets/edit_desc_dialog.dart | 18 +- lib/page/test/test_page.dart | 132 ++++++++++++ lib/router/config/route_paths.dart | 2 + lib/router/modules/base.dart | 7 + lib/utils/format.dart | 82 +++++--- lib/utils/stream.dart | 10 +- pubspec.lock | 8 + pubspec.yaml | 1 + 23 files changed, 1071 insertions(+), 338 deletions(-) create mode 100644 lib/api/dto/plan_item_dto.dart delete mode 100644 lib/page/plan/detail/widgets/plan_item.dart create mode 100644 lib/page/plan/detail/widgets/plan_list.dart create mode 100644 lib/page/plan/history/widgets/loading_box.dart create mode 100644 lib/page/test/test_page.dart diff --git a/assets/image/empty_data.png b/assets/image/empty_data.png index 3d3f30a79fae1eec869071b0a662193747e185f7..aaacf7df44f1268a1ff54b3b38c0a83ef9b1371f 100644 GIT binary patch literal 31559 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 literal 34985 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 toJson() { final map = {}; - map["id"] = id; + map["id"] = id ?? 0; map["step_icon"] = stepIcon; map["step_content"] = stepContent; map["step_explain"] = stepExplain; diff --git a/lib/api/dto/plan_item_dto.dart b/lib/api/dto/plan_item_dto.dart new file mode 100644 index 0000000..ad6a5ef --- /dev/null +++ b/lib/api/dto/plan_item_dto.dart @@ -0,0 +1,41 @@ +class PlanItemDto { + int? id; + String? agentName; + String? summary; + int? completedSteps; + int? totalSteps; + int? planStatus; + String? createdAt; + + PlanItemDto({ + this.id, + this.agentName, + this.summary, + this.completedSteps, + this.totalSteps, + this.planStatus, + this.createdAt, + }); + + Map toJson() { + final map = {}; + map["plan_id"] = id; + map["agent_name"] = agentName; + map["summary"] = summary; + map["completed_steps"] = completedSteps; + map["total_steps"] = totalSteps; + map["plan_status"] = planStatus; + map["created_at"] = createdAt; + return map; + } + + PlanItemDto.fromJson(dynamic json) { + id = json["plan_id"] ?? 0; + agentName = json["agent_name"] ?? ""; + summary = json["summary"] ?? ""; + completedSteps = json["completed_steps"] ?? 0; + totalSteps = json["total_steps"] ?? 0; + planStatus = json["plan_status"] ?? 0; + createdAt = json["created_at"] ?? ""; + } +} diff --git a/lib/api/endpoints/plan_api.dart b/lib/api/endpoints/plan_api.dart index 1ee6c2a..2cdb583 100644 --- a/lib/api/endpoints/plan_api.dart +++ b/lib/api/endpoints/plan_api.dart @@ -1,3 +1,5 @@ +import 'package:plan/api/dto/plan_detail_dto.dart'; +import 'package:plan/api/dto/plan_item_dto.dart'; import 'package:plan/api/network/request.dart'; ///初始化计划 @@ -8,3 +10,49 @@ Future initPlanApi(String need, int agentId) async { }); return res['plan_id']; } + +///保存用户计划 +Future savePlanApi({ + required String planId, + required String summary, + required String dialog, + required List steps, + required List suggestions, +}) async { + await Request().post("/plan/save_plan", { + "plan_id": planId, + "summary": summary, + "dialog": dialog, + "steps": steps.map((e) => e.toJson()).toList(), + "suggestions": suggestions, + }); +} + +///获取计划列表 +Future> getPlanListApi() async { + var res = await Request().get("/plan/plan_list"); + return res['list'].map((e) => PlanItemDto.fromJson(e)).toList(); +} + +///编辑计划摘要 +Future editPlanSummaryApi(int planId, String summary) async { + await Request().post("/plan/edit_plan_summary", { + "plan_id": planId, + "summary": summary, + }); +} + +///获取计划详情 +Future getPlanDetailApi(String planId) async { + var res = await Request().get("/plan/plan_detail", { + "plan_id": planId, + }); + return PlanDetailDto.fromJson(res); +} + +///删除计划 +Future deletePlanApi(int planId) async { + await Request().get("/plan/delete_plan", { + "plan_id": planId, + }); +} diff --git a/lib/page/home/widget/plan_form_card.dart b/lib/page/home/widget/plan_form_card.dart index f98af57..5fa893c 100644 --- a/lib/page/home/widget/plan_form_card.dart +++ b/lib/page/home/widget/plan_form_card.dart @@ -12,14 +12,14 @@ class PlanFormCard extends StatefulWidget { } class _PlanFormCardState extends State { - final TextEditingController _inputController = TextEditingController(); + final TextEditingController _inputController = TextEditingController(text: ""); void _handSubmit() { if (_inputController.text.isEmpty) { return; } context.push( - RoutePaths.planDetail(), + RoutePaths.planDetail(0), extra: { "name": _inputController.text, }, diff --git a/lib/page/plan/detail/plan_detail_page.dart b/lib/page/plan/detail/plan_detail_page.dart index 6946734..0e89108 100644 --- a/lib/page/plan/detail/plan_detail_page.dart +++ b/lib/page/plan/detail/plan_detail_page.dart @@ -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 { 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 { Widget build(BuildContext context) { return ChangeNotifierProvider( 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( child: SafeArea( child: Column( children: [ @@ -110,33 +84,12 @@ class _PlanDetailPageState extends State { 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 { ], ), ), + 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!, + ); + }, ), ); } diff --git a/lib/page/plan/detail/viewmodel/plan_detail_store.dart b/lib/page/plan/detail/viewmodel/plan_detail_store.dart index b82907e..5fc31e5 100644 --- a/lib/page/plan/detail/viewmodel/plan_detail_store.dart +++ b/lib/page/plan/detail/viewmodel/plan_detail_store.dart @@ -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 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 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 sentences = text + .split("\n") + .where((e) => e.contains("[NEXT]")) + .map((e) => e.replaceAll("[NEXT]", "").trim()) + .toList(); + planDetail.suggestionsList = sentences; + notifyListeners(); + }, + ); + await savePlan(); + } + + ///保存计划 + Future savePlan() async { + await savePlanApi( + planId: planId, + summary: planDetail.summary ?? "", + dialog: planDetail.dialog ?? "", + steps: planDetail.stepsList, + suggestions: planDetail.suggestionsList, + ); + EasyLoading.showToast("计划创建成功"); + } + + ///获取详情 + Future getPlanDetail() async { + planDetail = await getPlanDetailApi(planId); + notifyListeners(); } } diff --git a/lib/page/plan/detail/widgets/avatar_card.dart b/lib/page/plan/detail/widgets/avatar_card.dart index 561920e..f67d821 100644 --- a/lib/page/plan/detail/widgets/avatar_card.dart +++ b/lib/page/plan/detail/widgets/avatar_card.dart @@ -15,9 +15,13 @@ class _AvatarCardState extends State with SingleTickerProviderStateM late AnimationController _controller; late Animation _animation; + ///对话框值 + String _dialog = ""; + @override void initState() { super.initState(); + //初始化动画 _controller = AnimationController( vsync: this, duration: Duration(milliseconds: 300), @@ -28,6 +32,7 @@ class _AvatarCardState extends State with SingleTickerProviderStateM curve: Curves.easeInOut, ), ); + _initDialog(); } @override @@ -36,8 +41,31 @@ class _AvatarCardState extends State with SingleTickerProviderStateM _controller.dispose(); } + ///初始化是否显示对话 + void _initDialog() { + WidgetsBinding.instance.addPostFrameCallback((_) { + final store = context.read(); + + 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(); + if (store.planDetail.dialog == null) { + return; + } setState(() { store.showRoleTalk = !store.showRoleTalk; if (store.showRoleTalk) { @@ -71,10 +99,7 @@ class _AvatarCardState extends State 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, diff --git a/lib/page/plan/detail/widgets/plan_item.dart b/lib/page/plan/detail/widgets/plan_item.dart deleted file mode 100644 index d7799a7..0000000 --- a/lib/page/plan/detail/widgets/plan_item.dart +++ /dev/null @@ -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 createState() => _PlanItemState(); -} - -class _PlanItemState extends State 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; -} diff --git a/lib/page/plan/detail/widgets/plan_list.dart b/lib/page/plan/detail/widgets/plan_list.dart new file mode 100644 index 0000000..b3e8969 --- /dev/null +++ b/lib/page/plan/detail/widgets/plan_list.dart @@ -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 createState() => _PlanListState(); +} + +class _PlanListState extends State { + final GlobalKey _listKey = GlobalKey(); + List _stepsList = []; + + @override + void initState() { + super.initState(); + _initListen(); + } + + void _initListen() { + final store = context.read(); + //先初始化,只在详情时才会有意义 + _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), + ), + ), + ), + ]; + }, + ), + ), + ), + ); + } +} diff --git a/lib/page/plan/detail/widgets/scroll_box.dart b/lib/page/plan/detail/widgets/scroll_box.dart index 1ee966f..364c40c 100644 --- a/lib/page/plan/detail/widgets/scroll_box.dart +++ b/lib/page/plan/detail/widgets/scroll_box.dart @@ -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, ), ); diff --git a/lib/page/plan/detail/widgets/suggested.dart b/lib/page/plan/detail/widgets/suggested.dart index c7b30a6..6e8a7a8 100644 --- a/lib/page/plan/detail/widgets/suggested.dart +++ b/lib/page/plan/detail/widgets/suggested.dart @@ -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>( + (store) => store.planDetail.suggestionsList, + ); + if (list.isEmpty) { + return const SliverToBoxAdapter( + child: SizedBox(), + ); + } + return SliverToBoxAdapter( + child: TweenAnimationBuilder( + 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 createState() => _SuggestedListState(); +} + +class _SuggestedListState extends State { + final GlobalKey _listKey = GlobalKey(); + List _suggestionsList = []; + + @override + void initState() { + super.initState(); + _initListen(); + } + + void _initListen() { + final store = context.read(); + //先初始化,只在详情时才会有意义 + _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, + ), + ), + ), + ], ), - ], + ), ), ); } diff --git a/lib/page/plan/history/plan_history_page.dart b/lib/page/plan/history/plan_history_page.dart index ecc103b..201b6c5 100644 --- a/lib/page/plan/history/plan_history_page.dart +++ b/lib/page/plan/history/plan_history_page.dart @@ -1,9 +1,12 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:plan/api/dto/plan_item_dto.dart'; +import 'package:plan/api/endpoints/plan_api.dart'; import 'package:remixicon/remixicon.dart'; import 'widgets/history_item.dart'; import '../../../widgets/ui_kit/popup/popup_action.dart'; +import 'widgets/loading_box.dart'; class PlanHistoryPage extends StatefulWidget { const PlanHistoryPage({super.key}); @@ -16,12 +19,23 @@ class _PlanHistoryPageState extends State { ///是否显示删除 bool _isDelete = false; + ///数据 + List _record = []; + bool _isInit = true; + + @override + void initState() { + super.initState(); + _onRefresh(); + } + ///刷新 Future _onRefresh() async { - //模拟网络请求 - await Future.delayed(Duration(milliseconds: 1000)); - //结束刷新 - return Future.value(true); + var list = await getPlanListApi(); + setState(() { + _record = list; + _isInit = false; + }); } ///popup事件 @@ -36,7 +50,14 @@ class _PlanHistoryPageState extends State { } ///确认删除 - void _confirmDelete(int id) {} + void _confirmDelete(int id) { + print(_record.map((e) => e.toJson())); + setState(() { + _record.removeWhere((element) => element.id == id); + print(_record.map((e) => e.toJson())); + }); + + } @override Widget build(BuildContext context) { @@ -44,19 +65,6 @@ class _PlanHistoryPageState extends State { backgroundColor: Theme.of(context).colorScheme.surfaceContainer, navigationBar: CupertinoNavigationBar( middle: const Text("计划历史"), - trailing: PopupAction( - onSelected: _onPopupActionSelected, - items: [ - PopupMenuItem( - value: 'edit', - child: Text( - _isDelete ? "完成" : "编辑", - style: TextStyle(color: Colors.black), - ), - ), - ], - child: Icon(RemixIcons.more_fill), - ), ), child: SafeArea( child: CustomScrollView( @@ -69,36 +77,28 @@ class _PlanHistoryPageState extends State { onRefresh: _onRefresh, ), //列表 - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(15), - child: Container( - padding: EdgeInsets.symmetric(horizontal: 15), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - ), - child: CustomScrollView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - slivers: [ - SliverList.separated( - itemBuilder: (BuildContext context, int index) { - return HistoryItem( - showDelete: _isDelete, - onDelete: _confirmDelete, - ); - }, - separatorBuilder: (BuildContext context, int index) { - return Divider( - height: 1, - color: Theme.of(context).colorScheme.surfaceContainer, - ); - }, - itemCount: 5, - ), - ], - ), + LoadingBox( + loading: _isInit, + isEmpty: _record.isEmpty, + child: ClipRRect( + child: CustomScrollView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + slivers: [ + SliverList.separated( + itemBuilder: (BuildContext context, int index) { + return HistoryItem( + item: _record[index], + showDelete: _isDelete, + onDelete: _confirmDelete, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(height: 15); + }, + itemCount: _record.length, + ), + ], ), ), ), diff --git a/lib/page/plan/history/widgets/history_item.dart b/lib/page/plan/history/widgets/history_item.dart index b288388..9253ad3 100644 --- a/lib/page/plan/history/widgets/history_item.dart +++ b/lib/page/plan/history/widgets/history_item.dart @@ -1,15 +1,23 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:plan/api/dto/plan_item_dto.dart'; +import 'package:plan/api/endpoints/plan_api.dart'; import 'package:plan/router/config/route_paths.dart'; +import 'package:plan/utils/format.dart'; import 'package:plan/widgets/business/delete_row_item.dart'; import 'package:remixicon/remixicon.dart'; +import '../../widgets/edit_desc_dialog.dart'; + class HistoryItem extends StatefulWidget { + final PlanItemDto item; final bool showDelete; final Function(int) onDelete; const HistoryItem({ super.key, + required this.item, this.showDelete = false, required this.onDelete, }); @@ -19,73 +27,139 @@ class HistoryItem extends StatefulWidget { } class _HistoryItemState extends State { + late PlanItemDto _data; + @override void initState() { super.initState(); + _data = widget.item; + } + + @override + void didUpdateWidget(covariant HistoryItem oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.item != widget.item) { + setState(() { + _data = widget.item; + }); + } } ///跳转详情 void _goDetail() { - context.push(RoutePaths.planDetail(1)); + context.push(RoutePaths.planDetail(_data.id)); + } + + ///编辑摘要 + void _handEdit() { + context.pop(); + showEditDescDialog( + context, + value: _data.summary ?? "", + onConfirm: (value) async { + setState(() { + _data.summary = value; + }); + await editPlanSummaryApi(_data.id!, value); + }, + ); + } + + ///删除计划 + void _handDelete() async { + context.pop(); + widget.onDelete(_data.id!); + await deletePlanApi(_data.id!); } @override Widget build(BuildContext context) { - return GestureDetector( - onTap: _goDetail, - child: Container( - width: double.infinity, - padding: EdgeInsets.symmetric(vertical: 15), - color: Colors.white, - child: DeleteRowItem( - showDelete: widget.showDelete, - onDelete: () { - widget.onDelete(0); - }, - builder: (_, __) { - return [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: EdgeInsets.only(bottom: 5), - child: Text("开始学习软件开发"), - ), - Container( - margin: EdgeInsets.only(bottom: 5), - child: Text( - "创建于 2025/9/3 9:40:51 教练:W教练", - style: Theme.of(context).textTheme.labelSmall, + return CupertinoContextMenu( + actions: [ + // 编辑摘要 + CupertinoContextMenuAction( + onPressed: _handEdit, + child: _actionItem( + "Edit Summary", + CupertinoIcons.pencil, + ), + ), + CupertinoContextMenuAction( + isDestructiveAction: true, + onPressed: _handDelete, + child: _actionItem( + "Delete Summary", + CupertinoIcons.delete, + ), + ), + ], + child: GestureDetector( + onTap: _goDetail, + child: Container( + width: 400, + margin: EdgeInsets.symmetric(horizontal: 15), + padding: EdgeInsets.symmetric(horizontal: 15), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + child: Container( + padding: EdgeInsets.symmetric(vertical: 15), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.only(bottom: 5), + child: Text(_data.summary ?? ""), ), - ), - Row( - spacing: 10, - children: [ - Expanded( - child: LinearProgressIndicator( - value: 0.5, - borderRadius: BorderRadius.circular(5), + Container( + margin: const EdgeInsets.only(bottom: 5), + child: Text( + "${formatDateUS(_data.createdAt!, withTime: true)} · Coach ${_data.agentName ?? ""}", + style: Theme.of(context).textTheme.labelSmall, // 小号文字 + ), + ), + Row( + spacing: 10, + children: [ + Expanded( + child: LinearProgressIndicator( + value: (_data.completedSteps! / _data.totalSteps!), + borderRadius: BorderRadius.circular(5), + ), ), - ), - Text( - "0/7", - style: Theme.of(context).textTheme.labelSmall, - ), - ], - ), - ], + Text( + "${_data.completedSteps}/${_data.totalSteps}", + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ), + ], + ), ), - ), - Icon( - RemixIcons.arrow_right_s_line, - size: 30, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ]; - }, + Icon( + RemixIcons.arrow_right_s_line, + size: 30, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ], + ), + ), ), ), ); } + + Widget _actionItem(String title, IconData icon) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title), + Icon(icon, size: 20), // 右边图标 + ], + ); + } } diff --git a/lib/page/plan/history/widgets/loading_box.dart b/lib/page/plan/history/widgets/loading_box.dart new file mode 100644 index 0000000..a0c6e46 --- /dev/null +++ b/lib/page/plan/history/widgets/loading_box.dart @@ -0,0 +1,48 @@ +import 'package:flutter/cupertino.dart'; +import 'package:plan/widgets/ui_kit/empty/index.dart'; + +class LoadingBox extends StatelessWidget { + final bool loading; + final bool isEmpty; + final Widget child; + + const LoadingBox({ + super.key, + required this.loading, + required this.isEmpty, + required this.child, + }); + + @override + Widget build(BuildContext context) { + ///加载中 + if (loading) { + return SliverFillRemaining( + child: Center( + child: CupertinoActivityIndicator( + radius: 14, + ), + ), + ); + } + + ///空状态 + if (isEmpty) { + return SliverFillRemaining( + child: Empty( + title: "No data", + ), + ); + } + + ///正常 + return SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 15), + child: Container( + child: child, + ), + ), + ); + } +} diff --git a/lib/page/plan/widgets/edit_desc_dialog.dart b/lib/page/plan/widgets/edit_desc_dialog.dart index d9be202..8fce841 100644 --- a/lib/page/plan/widgets/edit_desc_dialog.dart +++ b/lib/page/plan/widgets/edit_desc_dialog.dart @@ -20,10 +20,14 @@ void showEditDescDialog( title: Text("编辑摘要"), content: Padding( padding: EdgeInsets.only(top: 15), - child: CupertinoTextField( - controller: controller, - focusNode: focusNode, - placeholder: "edit...", + child: Container( + constraints: BoxConstraints(minHeight: 40, maxHeight: 80), + child: CupertinoTextField( + controller: controller, + maxLines: null, + expands: true, + keyboardType: TextInputType.multiline, + ), ), ), actions: [ @@ -36,8 +40,10 @@ void showEditDescDialog( CupertinoDialogAction( isDefaultAction: true, onPressed: () { - context.pop(); - onConfirm(controller.text); + if (controller.text.trim().isNotEmpty) { + context.pop(); + onConfirm(controller.text); + } }, child: Text('确认'), ), diff --git a/lib/page/test/test_page.dart b/lib/page/test/test_page.dart new file mode 100644 index 0000000..7d8c974 --- /dev/null +++ b/lib/page/test/test_page.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; + +class TestPage extends StatefulWidget { + const TestPage({super.key}); + + @override + State createState() => _TestPageState(); +} + +class _TestPageState extends State { + final GlobalKey _listKey = GlobalKey(); + List _list = [ + Item("测试1", ""), + Item("测试2", ""), + Item("测试4", ""), + Item("测试5", ""), + ]; + + void _add() { + _list.add(Item("测试${_list.length + 1}", "")); + _listKey.currentState?.insertItem( + _list.length - 1, + duration: Duration(milliseconds: 200), + ); + } + + void _remove() { + final index = _list.length - 1; // 删除的索引 + final removedItem = _list.removeAt(index); // 移除元素 + + _listKey.currentState?.removeItem( + index, + (context, animation) { + final curvedAnimation = CurvedAnimation( + parent: animation, + curve: Curves.easeInOut, // 这里设置曲线 + ); + return buildItem(removedItem, curvedAnimation); + }, + duration: Duration(milliseconds: 200), + ); + } + void _update() { + if (_list.isNotEmpty) { + setState(() { + _list[0] = Item("测试1", "这这是新描述这是新描述这是新描述这是新描述这是新描述这是新描述这是新描述这是新描述这是新描述是新描述"); // 改变 desc + }); + } + } + + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("长度${_list.length}"), + actions: [ + ElevatedButton( + onPressed: _update, + child: Text("更新子元素"), + ), + ElevatedButton( + onPressed: _add, + child: Text("增加"), + ), + ElevatedButton( + onPressed: _remove, + child: Text("减少"), + ), + ], + ), + body: AnimatedList( + key: _listKey, + itemBuilder: (_, index, animation) { + final item = _list[index]; + // 新增元素的动画 + final curvedAnimation = CurvedAnimation( + parent: animation, + curve: Curves.easeInOut, + ); + return buildItem(item, curvedAnimation); + }, + initialItemCount: _list.length, + ), + ); + } + + Widget buildItem(Item item, CurvedAnimation curvedAnimation) { + return FadeTransition( + opacity: curvedAnimation, + child: SizeTransition( + sizeFactor: curvedAnimation, + axis: Axis.vertical, + child: Container( + padding: EdgeInsets.all(15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("序号${item.title}"), + AnimatedSwitcher( + duration: Duration(milliseconds: 300), + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axis: Axis.vertical, + child: child, + ), + ); + }, + child: item.desc.isEmpty + ? SizedBox.shrink(key: ValueKey("empty")) + : Text( + "描述${item.desc}", + key: ValueKey(item.desc), // ValueKey 用于 AnimatedSwitcher 判断变化 + ), + ) + ], + ), + ), + ), + ); + } +} + +class Item { + final String title; + final String desc; + + Item(this.title, this.desc); +} diff --git a/lib/router/config/route_paths.dart b/lib/router/config/route_paths.dart index 3d5ac1b..fefeb53 100644 --- a/lib/router/config/route_paths.dart +++ b/lib/router/config/route_paths.dart @@ -21,4 +21,6 @@ class RoutePaths { ///计划详情页 static String planDetail([int? id]) => id != null ? "/planDetail/$id" : "/planDetail/:id"; + + static const test = "/test"; } diff --git a/lib/router/modules/base.dart b/lib/router/modules/base.dart index 97d2f5c..cdae644 100644 --- a/lib/router/modules/base.dart +++ b/lib/router/modules/base.dart @@ -1,4 +1,5 @@ import 'package:plan/page/home/home_page.dart'; +import 'package:plan/page/test/test_page.dart'; import '../../page/system/agree/agree_page.dart'; import '../../page/system/login/login_code_page.dart'; @@ -40,4 +41,10 @@ List baseRoutes = [ return HomePage(); }, ), + RouteType( + path: RoutePaths.test, + child: (state) { + return TestPage(); + }, + ), ]; diff --git a/lib/utils/format.dart b/lib/utils/format.dart index 9f1080e..1655b98 100644 --- a/lib/utils/format.dart +++ b/lib/utils/format.dart @@ -1,35 +1,53 @@ -/// 格式化日期时间 -String formatDateUS(dynamic date, [String format = 'MM/DD/YYYY hh:mm:ss a']) { - DateTime dateTime; - if (date is String) { - dateTime = DateTime.tryParse(date) ?? DateTime.now(); - } else if (date is DateTime) { - dateTime = date; + +// /// 格式化日期时间 +// String formatDateUS(dynamic date, [String format = 'MM/DD/YYYY hh:mm:ss a']) { +// DateTime dateTime; +// +// if (date is String) { +// dateTime = DateTime.tryParse(date) ?? DateTime.now(); +// } else if (date is DateTime) { +// dateTime = date; +// } else { +// dateTime = DateTime.now(); +// } +// +// final yyyy = dateTime.year.toString(); +// final MM = dateTime.month.toString().padLeft(2, '0'); +// final dd = dateTime.day.toString().padLeft(2, '0'); +// +// // 12小时制 +// final hour12 = (dateTime.hour % 12 == 0 ? 12 : dateTime.hour % 12).toString().padLeft(2, '0'); +// final HH = dateTime.hour.toString().padLeft(2, '0'); // 24小时制备用 +// final mm = dateTime.minute.toString().padLeft(2, '0'); +// final ss = dateTime.second.toString().padLeft(2, '0'); +// final ampm = dateTime.hour >= 12 ? 'PM' : 'AM'; +// +// String result = format +// .replaceFirst(RegExp('YYYY'), yyyy) +// .replaceFirst(RegExp('MM'), MM) +// .replaceFirst(RegExp('DD'), dd) +// .replaceFirst(RegExp('hh'), hour12) +// .replaceFirst(RegExp('HH'), HH) +// .replaceFirst(RegExp('mm'), mm) +// .replaceFirst(RegExp('ss'), ss) +// .replaceFirst(RegExp('a'), ampm); +// +// return result; +// } + + +import 'package:intl/intl.dart'; + +String formatDateUS(String dateStr, {bool withTime = false}) { + // 假设后端返回的格式是 "2025-09-05T15:25:00Z" + final date = DateTime.parse(dateStr); + + if (withTime) { + return DateFormat("MMM d, yyyy 'at' h:mm a", 'en_US').format(date.toLocal()); + // 输出: Sep 5, 2025 at 11:25 PM (取本地时区) } else { - dateTime = DateTime.now(); + return DateFormat("MMM d, yyyy", 'en_US').format(date.toLocal()); + // 输出: Sep 5, 2025 } - - final yyyy = dateTime.year.toString(); - final MM = dateTime.month.toString().padLeft(2, '0'); - final dd = dateTime.day.toString().padLeft(2, '0'); - - // 12小时制 - final hour12 = (dateTime.hour % 12 == 0 ? 12 : dateTime.hour % 12).toString().padLeft(2, '0'); - final HH = dateTime.hour.toString().padLeft(2, '0'); // 24小时制备用 - final mm = dateTime.minute.toString().padLeft(2, '0'); - final ss = dateTime.second.toString().padLeft(2, '0'); - final ampm = dateTime.hour >= 12 ? 'PM' : 'AM'; - - String result = format - .replaceFirst(RegExp('YYYY'), yyyy) - .replaceFirst(RegExp('MM'), MM) - .replaceFirst(RegExp('DD'), dd) - .replaceFirst(RegExp('hh'), hour12) - .replaceFirst(RegExp('HH'), HH) - .replaceFirst(RegExp('mm'), mm) - .replaceFirst(RegExp('ss'), ss) - .replaceFirst(RegExp('a'), ampm); - - return result; -} +} \ No newline at end of file diff --git a/lib/utils/stream.dart b/lib/utils/stream.dart index e92ce42..a9805db 100644 --- a/lib/utils/stream.dart +++ b/lib/utils/stream.dart @@ -45,7 +45,7 @@ class StreamUtils { } //数据 var bufferText = ""; //吐出来的内容 - + final completer = Completer(); //处理响应 response.data?.stream .transform(_unit8Transformer()) @@ -63,6 +63,9 @@ class StreamUtils { } Map dataJSON = jsonDecode(streamStr); //提取响应数据 + if (dataJSON['choices'].length == 0) { + continue; + } Map choices = dataJSON['choices'][0]; //提取文字 var word = choices['delta']['content']; @@ -80,12 +83,17 @@ class StreamUtils { }, onDone: () { onEnd?.call(); + completer.complete(); }, onError: (error) { onError?.call(); logger.e("流错误: $error"); + if (!completer.isCompleted) { + completer.completeError(error); + } }, ); + return completer.future; } /// 将Uint8List转换为List diff --git a/pubspec.lock b/pubspec.lock index 8877517..7c80337 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -456,6 +456,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.2.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.19.0" leak_tracker: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2d12372..d3904cf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: sign_in_with_apple: ^7.0.1 flutter_image_compress: ^2.4.0 cached_network_image: ^3.4.1 + intl: ^0.19.0 dev_dependencies: flutter_test: