diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/api/dto/login_dto.dart b/lib/api/dto/login_dto.dart index d149181..5df91c6 100644 --- a/lib/api/dto/login_dto.dart +++ b/lib/api/dto/login_dto.dart @@ -1,32 +1,22 @@ class UserInfo { int? id; String? name; - dynamic avatar; + String? avatar; + String? description; String? email; - dynamic emailVerifiedAt; - dynamic googleId; - dynamic appleId; - String? lastLoginIp; - String? lastLoginTime; - dynamic lastUsedTime; + String? googleId; + String? appleId; int? status; - String? createdAt; - String? updatedAt; UserInfo({ this.id, this.name, this.avatar, + this.description, this.email, - this.emailVerifiedAt, this.googleId, this.appleId, - this.lastLoginIp, - this.lastLoginTime, - this.lastUsedTime, this.status, - this.createdAt, - this.updatedAt, }); Map toJson() { @@ -34,16 +24,11 @@ class UserInfo { map["id"] = id; map["name"] = name; map["avatar"] = avatar; + map["description"] = description; map["email"] = email; - map["email_verified_at"] = emailVerifiedAt; map["google_id"] = googleId; map["apple_id"] = appleId; - map["last_login_ip"] = lastLoginIp; - map["last_login_time"] = lastLoginTime; - map["last_used_time"] = lastUsedTime; map["status"] = status; - map["created_at"] = createdAt; - map["updated_at"] = updatedAt; return map; } @@ -51,19 +36,38 @@ class UserInfo { id = json["id"] ?? 0; name = json["name"] ?? ""; avatar = json["avatar"]; + description = json["description"] ?? ""; email = json["email"] ?? ""; - emailVerifiedAt = json["email_verified_at"]; googleId = json["google_id"]; appleId = json["apple_id"]; - lastLoginIp = json["last_login_ip"] ?? ""; - lastLoginTime = json["last_login_time"] ?? ""; - lastUsedTime = json["last_used_time"]; status = json["status"] ?? 0; - createdAt = json["created_at"] ?? ""; - updatedAt = json["updated_at"] ?? ""; + } + + /// copyWith 方法 + UserInfo copyWith({ + int? id, + String? name, + String? avatar, + String? description, + String? email, + String? googleId, + String? appleId, + int? status, + }) { + return UserInfo( + id: id ?? this.id, + name: name ?? this.name, + avatar: avatar ?? this.avatar, + description: description ?? this.description, + email: email ?? this.email, + googleId: googleId ?? this.googleId, + appleId: appleId ?? this.appleId, + status: status ?? this.status, + ); } } + class LoginDto { String? accessToken; UserInfo? userInfo; diff --git a/lib/api/endpoints/user_api.dart b/lib/api/endpoints/user_api.dart index c02ddcd..acd4bf8 100644 --- a/lib/api/endpoints/user_api.dart +++ b/lib/api/endpoints/user_api.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import '../../data/models/other_login_type.dart'; import '../dto/login_dto.dart'; @@ -53,3 +54,25 @@ Future thirdLoginApi(String token, OtherLoginType type) async { Future deleteAccountApi() async { return Request().get("/delete_account"); } + +///修改用户资料 +Future updateUserInfoApi({ + String? name, + List? avatar, + String? description, +}) async { + FormData formData = FormData.fromMap({ + "avatar": avatar != null + ? MultipartFile.fromBytes( + avatar, + filename: "upload.jpg", + contentType: DioMediaType("image", "jpeg"), + ) + : null, + "description": description, + "name": name, + }); + //请求 + var res = await Request().post("/user/update_profile", formData); + return UserInfo.fromJson(res); +} diff --git a/lib/api/network/interceptor.dart b/lib/api/network/interceptor.dart index 60af27e..30aee7e 100644 --- a/lib/api/network/interceptor.dart +++ b/lib/api/network/interceptor.dart @@ -1,6 +1,7 @@ import 'package:dio/dio.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import '../../providers/app_store.dart'; import '../dto/base_dto.dart'; ///请求拦截器 @@ -8,8 +9,8 @@ void onRequest( RequestOptions options, RequestInterceptorHandler handler, ) async { - // String token = await AppStore.getToken(); - // options.headers['Authorization'] = 'Bearer $token'; + String token = await AppStore.getToken(); + options.headers['Authorization'] = 'Bearer $token'; return handler.next(options); } diff --git a/lib/config/env.dart b/lib/config/env.dart index d4a38cf..6856343 100644 --- a/lib/config/env.dart +++ b/lib/config/env.dart @@ -11,9 +11,9 @@ class Config { ///获取接口地址 static String baseUrl() { if (getEnv() == 'dev') { - return 'https://food-api.curain.ai/api'; + return 'https://plan-api.curain.ai/api'; } else { - return 'https://food-api.curain.ai/api'; + return 'https://plan-api.curain.ai/api'; } } } diff --git a/lib/main.dart b/lib/main.dart index 5ae872e..94682df 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,9 +4,9 @@ import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:plan/providers/app_store.dart'; import 'package:plan/router/routes.dart'; +import 'package:plan/theme/theme.dart'; import 'package:provider/provider.dart'; -import 'config/theme/theme.dart'; void main() { runApp( diff --git a/lib/page/home/widget/plan_form_card.dart b/lib/page/home/widget/plan_form_card.dart index 57e4cfb..f98af57 100644 --- a/lib/page/home/widget/plan_form_card.dart +++ b/lib/page/home/widget/plan_form_card.dart @@ -1,4 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:plan/theme/decorations/app_shadows.dart'; + +import '../../../router/config/route_paths.dart'; class PlanFormCard extends StatefulWidget { const PlanFormCard({super.key}); @@ -10,6 +14,19 @@ class PlanFormCard extends StatefulWidget { class _PlanFormCardState extends State { final TextEditingController _inputController = TextEditingController(); + void _handSubmit() { + if (_inputController.text.isEmpty) { + return; + } + context.push( + RoutePaths.planDetail(), + extra: { + "name": _inputController.text, + }, + ); + _inputController.clear(); + } + @override Widget build(BuildContext context) { return Stack( @@ -26,20 +43,7 @@ class _PlanFormCardState extends State { width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 40), margin: EdgeInsets.only(top: 120), - decoration: BoxDecoration( - border: Border.all(color: Colors.black, width: 2), - borderRadius: BorderRadius.circular(5), - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Color(0xffb5b5b5), - blurRadius: 2, - offset: Offset(6, 6), - spreadRadius: 0, - blurStyle: BlurStyle.normal, - ), - ], - ), + decoration: shadowDecoration, child: Column( children: [ Container( @@ -49,6 +53,7 @@ class _PlanFormCardState extends State { TextField( controller: _inputController, style: Theme.of(context).textTheme.bodyMedium, + maxLength: 40, decoration: InputDecoration( hintText: "我躺在床上听歌", fillColor: Theme.of(context).colorScheme.surfaceContainerLow, @@ -68,19 +73,22 @@ class _PlanFormCardState extends State { ), ), ), - Container( - margin: EdgeInsets.only(top: 20), - padding: EdgeInsets.symmetric(vertical: 8, horizontal: 20), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - border: Border.all(color: Colors.black, width: 1.5), - ), - child: Text( - "创建计划", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurfaceVariant, + InkWell( + onTap: _handSubmit, + child: Container( + margin: EdgeInsets.only(top: 20), + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + border: Border.all(color: Colors.black, width: 1.5), + ), + child: Text( + "创建计划", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), ), diff --git a/lib/page/my/my_page.dart b/lib/page/my/my_page.dart index b5ba0f6..ece0768 100644 --- a/lib/page/my/my_page.dart +++ b/lib/page/my/my_page.dart @@ -1,7 +1,14 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:go_router/go_router.dart'; +import 'package:plan/api/dto/login_dto.dart'; +import 'package:provider/provider.dart'; import 'package:remixicon/remixicon.dart'; +import '../../api/endpoints/user_api.dart'; +import '../../providers/app_store.dart'; +import '../../router/config/route_paths.dart'; import 'widget/avatar_name.dart'; import 'widget/profile_section.dart'; @@ -15,6 +22,72 @@ class MyPage extends StatefulWidget { } class _MyPageState extends State { + + ///退出登陆 + void _handLogout() async { + await showCupertinoDialog( + context: context, + builder: (_) => CupertinoAlertDialog( + title: Text("Log Out?"), + content: Text("Are you sure you want to log out? You’ll need to sign in again to access your account."), + actions: [ + CupertinoDialogAction( + child: Text("Cancel"), + onPressed: () { + context.pop(); + }, + ), + CupertinoDialogAction( + child: Text("Log Out"), + onPressed: () { + context.pop(); + var appStore = context.read(); + appStore.logout(); + context.go(RoutePaths.login); + }, + ), + ], + ), + ); + } + + ///注销账号 + void _handDelete() async { + await showCupertinoDialog( + context: context, + builder: (_) => CupertinoAlertDialog( + title: Text("Delete Account?"), + content: Text("Are you sure you want to delete your account? You won’t be able to recover your account."), + actions: [ + CupertinoDialogAction( + onPressed: () { + context.pop(); + }, + child: Text("Cancel"), + ), + CupertinoDialogAction( + child: Text("Delete"), + onPressed: () async { + context.pop(); + EasyLoading.show(); + await deleteAccountApi(); + EasyLoading.dismiss(); + var appStore = context.read(); + appStore.logout(); + context.go(RoutePaths.login); + }, + ), + ], + ), + ); + } + + ///编辑资料 + void _updateUserInfo(UserInfo value) { + var appStore = context.read(); + appStore.updateUserInfo(value); + } + @override Widget build(BuildContext context) { return CupertinoPageScaffold( @@ -32,8 +105,31 @@ class _MyPageState extends State { child: ListView( padding: EdgeInsets.symmetric(horizontal: 30, vertical: 20), children: [ - AvatarName(), - ProfileSection(), + AvatarName( + onUpdate: _updateUserInfo, + ), + ProfileSection( + onUpdate: _updateUserInfo, + ), + Container( + margin: EdgeInsets.only(top: 50), + child: CupertinoButton( + color: CupertinoColors.systemRed, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + onPressed: _handLogout, + child: Text( + "退出登录", + style: TextStyle(color: Colors.white, fontSize: 14), + ), + ), + ), + CupertinoButton( + onPressed: _handDelete, + child: const Text( + "删除账号", + style: TextStyle(color: CupertinoColors.systemRed, fontSize: 14), + ), + ), ], ), ), diff --git a/lib/page/my/widget/avatar_name.dart b/lib/page/my/widget/avatar_name.dart index e9d9ce2..6a54bd6 100644 --- a/lib/page/my/widget/avatar_name.dart +++ b/lib/page/my/widget/avatar_name.dart @@ -1,8 +1,18 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:plan/api/dto/login_dto.dart'; +import 'package:plan/api/endpoints/user_api.dart'; +import 'package:plan/providers/app_store.dart'; +import 'package:plan/utils/common.dart'; +import 'package:provider/provider.dart'; import 'package:remixicon/remixicon.dart'; +import 'package:file_picker/file_picker.dart'; class AvatarName extends StatefulWidget { - const AvatarName({super.key}); + final Function(UserInfo) onUpdate; + + const AvatarName({super.key, required this.onUpdate}); @override State createState() => _AvatarNameState(); @@ -19,94 +29,150 @@ class _AvatarNameState extends State { setState(() { _isEdit = true; }); + AppStore appStore = context.read(); WidgetsBinding.instance.addPostFrameCallback((_) { + _inputController.text = appStore.userInfo?.name ?? ""; _focusNode.requestFocus(); }); } - ///确定编辑内容 - void _confirmEdit(String value) { + ///确定编辑名字 + void _confirmEdit(String value) async { setState(() { _isEdit = false; }); + var res = await updateUserInfoApi(name: value); + widget.onUpdate(res); + } + + ///选择图片 + void _handPickImage() async { + var result = await FilePicker.platform.pickFiles( + type: FileType.image, + allowMultiple: false, + ); + if (result != null) { + //压缩文件 + final compress = await FlutterImageCompress.compressWithFile( + result.files[0].path!, + minWidth: 500, + minHeight: 500, + quality: 85, + rotate: 0, + ); + + var res = await updateUserInfoApi(avatar: compress); + widget.onUpdate(res); + } } @override Widget build(BuildContext context) { - return Row( - spacing: 15, - children: [ - Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: Color(0xffcae2fd), - border: Border.all( - color: Color(0xff797e80), - width: 2, - ), - borderRadius: BorderRadius.circular(5), - ), - alignment: Alignment.center, - child: Container( - width: 50, - padding: EdgeInsets.all(5), - decoration: BoxDecoration( - color: Color(0xff8e8d93), - borderRadius: BorderRadius.circular(5), - ), - child: Icon( - RemixIcons.user_fill, - color: Color(0xffcae2fd), - ), - ), - ), - Expanded( - child: Visibility( - visible: _isEdit, - replacement: InkWell( - onTap: _handleEdit, - child: Row( - spacing: 10, - children: [ - Text( - "教练如何称呼你?", - style: Theme.of(context).textTheme.labelMedium, - ), - Icon( - RemixIcons.pencil_fill, - size: 18, - color: Theme.of(context).textTheme.labelMedium?.color, - ), - ], - ), - ), - child: TextField( - focusNode: _focusNode, - controller: _inputController, - style: TextStyle(fontSize: 14), - decoration: InputDecoration( - hintText: "输入你的姓名", - isCollapsed: true, - contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 10), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - width: 1, - color: Theme.of(context).colorScheme.surfaceContainerHigh, + return Consumer( + builder: (context, store, __) { + return Row( + spacing: 15, + children: [ + InkWell( + onTap: _handPickImage, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Color(0xffcae2fd), + border: Border.all( + color: Color(0xff797e80), + width: 2, ), + borderRadius: BorderRadius.circular(5), ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - width: 1, - color: Theme.of(context).colorScheme.surfaceContainerHigh, + alignment: Alignment.center, + child: Visibility( + visible: getNotEmpty(store.userInfo?.avatar) == null, + replacement: CachedNetworkImage( + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + imageUrl: store.userInfo?.avatar ?? "", + errorWidget: (context, url, error) => Icon(Icons.error), + placeholder: (context, url) => Container( + width: 30, + height: 30, + alignment: Alignment.center, + child: CircularProgressIndicator(), + ), + ), + child: Container( + width: 50, + padding: EdgeInsets.all(5), + decoration: BoxDecoration( + color: Color(0xff8e8d93), + borderRadius: BorderRadius.circular(5), + ), + child: Icon( + RemixIcons.user_fill, + color: Color(0xffcae2fd), + ), ), ), ), - onSubmitted: _confirmEdit, ), - ), - ), - ], + Expanded( + child: Visibility( + visible: _isEdit, + replacement: InkWell( + onTap: _handleEdit, + child: Row( + spacing: 10, + children: [ + Visibility( + visible: getNotEmpty(store.userInfo?.name) == null, + replacement: Text( + store.userInfo?.name ?? "", + style: Theme.of(context).textTheme.titleSmall, + ), + child: Text( + "教练如何称呼你?", + style: Theme.of(context).textTheme.labelMedium, + ), + ), + Icon( + RemixIcons.pencil_fill, + size: 18, + color: Theme.of(context).textTheme.labelMedium?.color, + ), + ], + ), + ), + child: TextField( + focusNode: _focusNode, + controller: _inputController, + style: TextStyle(fontSize: 14), + maxLength: 20, + decoration: InputDecoration( + hintText: "输入你的姓名", + isCollapsed: true, + contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 10), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + width: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + width: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + ), + ), + onSubmitted: _confirmEdit, + ), + ), + ), + ], + ); + }, ); } } diff --git a/lib/page/my/widget/profile_section.dart b/lib/page/my/widget/profile_section.dart index 4926916..523e287 100644 --- a/lib/page/my/widget/profile_section.dart +++ b/lib/page/my/widget/profile_section.dart @@ -1,8 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:plan/api/dto/login_dto.dart'; +import 'package:plan/api/endpoints/user_api.dart'; +import 'package:plan/providers/app_store.dart'; +import 'package:plan/utils/debouncer.dart'; +import 'package:provider/provider.dart'; import 'package:remixicon/remixicon.dart'; class ProfileSection extends StatefulWidget { - const ProfileSection({super.key}); + final Function(UserInfo) onUpdate; + + const ProfileSection({super.key, required this.onUpdate}); @override State createState() => _ProfileSectionState(); @@ -12,7 +19,35 @@ class _ProfileSectionState extends State { //输入框 final TextEditingController _inputController = TextEditingController(); - final List _tips = ["教练每次为你制定计划时,都会首先参考这里的信息", "你分享的背景信息越详细,教练就越能为你量身定制,符合你独特情况的行动步骤", "你可以在这里为教练提需求,比如“我不吃香菜”"]; + final List _tips = [ + "教练每次为你制定计划时,都会首先参考这里的信息", + "你分享的背景信息越详细,教练就越能为你量身定制,符合你独特情况的行动步骤", + "你可以在这里为教练提需求,比如“我不吃香菜”", + ]; + + //防抖 + Debouncer debouncer = Debouncer(milliseconds: 2000); + + @override + void initState() { + super.initState(); + AppStore appStore = context.read(); + _inputController.text = appStore.userInfo?.description ?? ""; + } + + @override + void dispose() { + super.dispose(); + debouncer.dispose(); + } + + ///确定编辑画像 + void _onTextChanged(String value) { + debouncer.run(() async { + var res = await updateUserInfoApi(description: value); + widget.onUpdate(res); + }); + } @override Widget build(BuildContext context) { @@ -38,9 +73,10 @@ class _ProfileSectionState extends State { margin: EdgeInsets.only(bottom: 20), child: TextField( maxLines: 5, - maxLength: 200, + maxLength: 500, controller: _inputController, style: TextStyle(fontSize: 14, letterSpacing: 1), + onChanged: _onTextChanged, decoration: InputDecoration( hintText: "我是19岁女生,刷碗时用洗碗机,请不要按手洗拆解步骤..", enabledBorder: OutlineInputBorder( diff --git a/lib/page/plan/detail/plan_detail_page.dart b/lib/page/plan/detail/plan_detail_page.dart index 285fd49..ea1a063 100644 --- a/lib/page/plan/detail/plan_detail_page.dart +++ b/lib/page/plan/detail/plan_detail_page.dart @@ -1,29 +1,148 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:plan/page/plan/detail/viewmodel/plan_detail_store.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:remixicon/remixicon.dart'; +import '../widgets/edit_desc_dialog.dart'; +import 'widgets/avatar_card.dart'; +import 'widgets/plan_item.dart'; +import 'widgets/scroll_box.dart'; +import 'widgets/suggested.dart'; + class PlanDetailPage extends StatefulWidget { - const PlanDetailPage({super.key}); + final String? id; + final String? planName; + + const PlanDetailPage({ + super.key, + this.id, + this.planName, + }); @override State createState() => _PlanDetailPageState(); } class _PlanDetailPageState extends State { + bool _isEdit = false; + + ///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 Widget build(BuildContext context) { - return CupertinoPageScaffold( - backgroundColor: Colors.white, - navigationBar: CupertinoNavigationBar( - middle: Text('计划详情'), - trailing: Row( - mainAxisSize: MainAxisSize.min, // 关键:Row 只占实际内容宽度 - children: [ - Icon(RemixIcons.more_fill), - ], + return ChangeNotifierProvider( + create: (_) { + return PlanDetailStore(); + }, + 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: SafeArea( + child: Column( + children: [ + AvatarCard(), + Expanded( + child: Padding( + padding: EdgeInsets.all(15), + child: ScrollBox( + child: Container( + decoration: shadowDecoration, + child: CustomScrollView( + slivers: [ + 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, + ), + ], + ), + ), + ), + ), + ), + ], + ), ), ), - child: Column(), ); } } diff --git a/lib/page/plan/detail/viewmodel/plan_detail_store.dart b/lib/page/plan/detail/viewmodel/plan_detail_store.dart new file mode 100644 index 0000000..47cd381 --- /dev/null +++ b/lib/page/plan/detail/viewmodel/plan_detail_store.dart @@ -0,0 +1,13 @@ +import 'package:flutter/cupertino.dart'; + +class PlanDetailStore extends ChangeNotifier { + ///角色话语是否显示 + bool _showRoleTalk = true; + + bool get showRoleTalk => _showRoleTalk; + + set showRoleTalk(bool value) { + _showRoleTalk = value; + notifyListeners(); + } +} diff --git a/lib/page/plan/detail/widgets/avatar_card.dart b/lib/page/plan/detail/widgets/avatar_card.dart new file mode 100644 index 0000000..d1d324a --- /dev/null +++ b/lib/page/plan/detail/widgets/avatar_card.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:remixicon/remixicon.dart'; + +class AvatarCard extends StatefulWidget { + const AvatarCard({super.key}); + + @override + State createState() => _AvatarCardState(); +} + +class _AvatarCardState extends State with SingleTickerProviderStateMixin { + bool _isShow = false; + + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: Duration(milliseconds: 400), + ); + } + + void _toggleShow() { + setState(() { + _isShow = !_isShow; + if (_isShow) { + _controller.forward(); + } else { + _controller.reverse(); + } + }); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Transform.translate( + offset: Offset(0, 50), + child: Column( + children: [ + Transform( + alignment: Alignment.center, + transform: Matrix4.identity()..translate(40.0, 40.0)..scale(0.2), + child: Container( + color: Colors.red, + padding: EdgeInsets.only(bottom: 10), + child: InkWell( + onTap: _toggleShow, + child: Stack( + children: [ + Container( + padding: EdgeInsets.all(4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(3), + border: Border.all(color: Colors.black, width: 1), + ), + child: Text( + "好的,让我们把学习软件开发这个目标分解成最简单的小步骤,这样你明天就能轻松开始行动", + style: TextStyle(fontSize: 12), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: CustomPaint( + painter: BubblePainter(), + ), + ), + ], + ), + ), + ), + ), + SizedBox( + width: double.infinity, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Image.asset("assets/image/xiaozhi.png", height: 100), + Positioned( + top: 20, + child: Transform.translate( + offset: Offset(50, -10), + child: GestureDetector( + onTap: _toggleShow, + child: Icon(RemixIcons.message_2_line, size: 26), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class BubblePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + var bottomWidth = 10; + //点坐标 + var start = Offset((size.width - bottomWidth) / 2, 0); + //底线 + final bottomLinePaint = Paint() + ..color = Colors.white + ..strokeWidth = 2 + ..style = PaintingStyle.stroke; + final bottomLinePath = Path() + ..moveTo(start.dx, 0) + ..lineTo(start.dx + bottomWidth, 0); + canvas.drawPath(bottomLinePath, bottomLinePaint); + + //边线 + final sideLinePaint = Paint() + ..color = Colors.black + ..strokeWidth = 1 + ..style = bottomLinePaint.style; + + final sideLinePath = Path() + ..moveTo(start.dx, 0) + ..lineTo(start.dx + bottomWidth / 2, 5) + ..lineTo(start.dx + bottomWidth, 0); + canvas.drawPath(sideLinePath, sideLinePaint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/page/plan/detail/widgets/plan_item.dart b/lib/page/plan/detail/widgets/plan_item.dart new file mode 100644 index 0000000..d7799a7 --- /dev/null +++ b/lib/page/plan/detail/widgets/plan_item.dart @@ -0,0 +1,71 @@ +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/scroll_box.dart b/lib/page/plan/detail/widgets/scroll_box.dart new file mode 100644 index 0000000..1ee966f --- /dev/null +++ b/lib/page/plan/detail/widgets/scroll_box.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +class ScrollBox extends StatelessWidget { + final Widget child; + + const ScrollBox({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return ScrollbarTheme( + data: ScrollbarThemeData( + thumbColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surfaceContainerHigh), + thickness: WidgetStateProperty.all(3), + crossAxisMargin: 3, + mainAxisMargin: 2, + radius: const Radius.circular(5), + ), + child: Scrollbar( + child: child, + ), + ); + } +} diff --git a/lib/page/plan/detail/widgets/suggested.dart b/lib/page/plan/detail/widgets/suggested.dart new file mode 100644 index 0000000..c7b30a6 --- /dev/null +++ b/lib/page/plan/detail/widgets/suggested.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:remixicon/remixicon.dart'; + +///模块标题 +class SuggestedTitle extends StatelessWidget { + const SuggestedTitle({super.key}); + + @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, + ), + 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; + + const SuggestedItem({super.key, required this.title}); + + @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, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/page/plan/history/plan_history_page.dart b/lib/page/plan/history/plan_history_page.dart index be79d41..ecc103b 100644 --- a/lib/page/plan/history/plan_history_page.dart +++ b/lib/page/plan/history/plan_history_page.dart @@ -1,8 +1,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:remixicon/remixicon.dart'; import 'widgets/history_item.dart'; -import 'widgets/popup_action.dart'; +import '../../../widgets/ui_kit/popup/popup_action.dart'; class PlanHistoryPage extends StatefulWidget { const PlanHistoryPage({super.key}); @@ -42,22 +43,19 @@ class _PlanHistoryPageState extends State { return CupertinoPageScaffold( backgroundColor: Theme.of(context).colorScheme.surfaceContainer, navigationBar: CupertinoNavigationBar( - middle: Text("计划历史"), - trailing: CupertinoNavigationBar( - transitionBetweenRoutes: false, - middle: const Text("计划历史"), - trailing: PopupAction( - onSelected: _onPopupActionSelected, - items: [ - PopupMenuItem( - value: 'edit', - child: Text( - _isDelete ? "完成" : "编辑", - style: TextStyle(color: Colors.black), - ), + 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( diff --git a/lib/page/plan/history/widgets/history_item.dart b/lib/page/plan/history/widgets/history_item.dart index fd43ecd..b288388 100644 --- a/lib/page/plan/history/widgets/history_item.dart +++ b/lib/page/plan/history/widgets/history_item.dart @@ -1,7 +1,7 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:plan/router/config/route_paths.dart'; +import 'package:plan/widgets/business/delete_row_item.dart'; import 'package:remixicon/remixicon.dart'; class HistoryItem extends StatefulWidget { @@ -18,76 +18,15 @@ class HistoryItem extends StatefulWidget { State createState() => _HistoryItemState(); } -class _HistoryItemState extends State with TickerProviderStateMixin { - late AnimationController _controller; - late Animation _animation; - +class _HistoryItemState extends State { @override void initState() { super.initState(); - _controller = AnimationController( - vsync: this, - duration: Duration(milliseconds: 300), - ); - _animation = CurvedAnimation( - parent: _controller, - curve: Curves.easeInOut, - ); - - if (widget.showDelete) { - _controller.forward(); - } - } - - @override - void didUpdateWidget(covariant HistoryItem oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.showDelete != widget.showDelete) { - if (widget.showDelete) { - _controller.forward(); - } else { - _controller.reverse(); - } - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - ///点击删除 - void _handleDelete() { - showCupertinoDialog( - context: context, - builder: (_) { - return CupertinoAlertDialog( - title: Text("删除计划"), - actions: [ - CupertinoDialogAction( - child: Text("取消"), - onPressed: () { - context.pop(); - }, - ), - CupertinoDialogAction( - isDestructiveAction: true, - onPressed: () { - context.pop(); - widget.onDelete(0); - }, - child: Text("确定"), - ), - ], - ); - }, - ); } ///跳转详情 void _goDetail() { - context.push(RoutePaths.planDetail); + context.push(RoutePaths.planDetail(1)); } @override @@ -98,62 +37,53 @@ class _HistoryItemState extends State with TickerProviderStateMixin width: double.infinity, padding: EdgeInsets.symmetric(vertical: 15), color: Colors.white, - child: Row( - children: [ - SizeTransition( - axis: Axis.horizontal, - sizeFactor: _animation, - axisAlignment: -1, // 从左向右展开 - child: InkWell( - onTap: _handleDelete, - child: Container( - margin: EdgeInsets.only(right: 10), - child: Icon( - RemixIcons.indeterminate_circle_fill, - color: Colors.red, - ), - ), - ), - ), - 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, + 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("开始学习软件开发"), ), - ), - Row( - spacing: 10, - children: [ - Expanded( - child: LinearProgressIndicator( - value: 0.5, - borderRadius: BorderRadius.circular(5), - ), - ), - Text( - "0/7", + Container( + margin: EdgeInsets.only(bottom: 5), + child: Text( + "创建于 2025/9/3 9:40:51 教练:W教练", style: Theme.of(context).textTheme.labelSmall, ), - ], - ), - ], + ), + Row( + spacing: 10, + children: [ + Expanded( + child: LinearProgressIndicator( + value: 0.5, + borderRadius: BorderRadius.circular(5), + ), + ), + Text( + "0/7", + 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, + ), + ]; + }, ), ), ); diff --git a/lib/page/plan/widgets/edit_desc_dialog.dart b/lib/page/plan/widgets/edit_desc_dialog.dart new file mode 100644 index 0000000..d9be202 --- /dev/null +++ b/lib/page/plan/widgets/edit_desc_dialog.dart @@ -0,0 +1,48 @@ +import 'package:flutter/cupertino.dart'; +import 'package:go_router/go_router.dart'; + +void showEditDescDialog( + BuildContext context, { + String value = "", + required void Function(String) onConfirm, +}) { + final controller = TextEditingController(text: value); + final focusNode = FocusNode(); + showCupertinoDialog( + context: context, + builder: (_) { + // 确保弹窗显示后再获取焦点 + WidgetsBinding.instance.addPostFrameCallback((_) { + focusNode.requestFocus(); + }); + + return CupertinoAlertDialog( + title: Text("编辑摘要"), + content: Padding( + padding: EdgeInsets.only(top: 15), + child: CupertinoTextField( + controller: controller, + focusNode: focusNode, + placeholder: "edit...", + ), + ), + actions: [ + CupertinoDialogAction( + onPressed: () { + context.pop(); + }, + child: Text('取消'), + ), + CupertinoDialogAction( + isDefaultAction: true, + onPressed: () { + context.pop(); + onConfirm(controller.text); + }, + child: Text('确认'), + ), + ], + ); + }, + ); +} diff --git a/lib/page/system/login/login_page.dart b/lib/page/system/login/login_page.dart index f70941d..6704913 100644 --- a/lib/page/system/login/login_page.dart +++ b/lib/page/system/login/login_page.dart @@ -26,9 +26,6 @@ class LoginPage extends StatefulWidget { class _LoginPageState extends State { var _subLoading = false; - ///协议 - bool _agree = false; - ///谷歌登陆 final GoogleSignIn _googleSignIn = GoogleSignIn.instance; @@ -80,11 +77,6 @@ class _LoginPageState extends State { ///谷歌登录 void _handleGoogleSignIn() async { - if (!_agree) { - EasyLoading.showToast('Please read and agree to the terms first.'); - return; - } - try { // 如果用户未登录,则启动标准的 Google 登录 if (_googleSignIn.supportsAuthenticate()) { @@ -120,11 +112,6 @@ class _LoginPageState extends State { ///apple登录 void _handAppleSignIn() async { - if (!_agree) { - EasyLoading.showToast('Please read and agree to the terms first.'); - return; - } - try { final credential = await SignInWithApple.getAppleIDCredential(scopes: [AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName]); EasyLoading.show(status: "Logging in..."); @@ -139,10 +126,6 @@ class _LoginPageState extends State { } void _handSubmit() async { - if (!_agree) { - EasyLoading.showToast('Please read and agree to the terms first.'); - return; - } if (_emailController.text.isEmpty) { //请输入邮箱 EasyLoading.showError("Please enter your email"); @@ -238,14 +221,7 @@ class _LoginPageState extends State { width: double.infinity, margin: EdgeInsets.only(top: 40), alignment: Alignment.center, - child: AgreementBox( - checked: _agree, - onChanged: (value) { - setState(() { - _agree = value; - }); - }, - ), + child: AgreementBox(), ), ], ), diff --git a/lib/page/system/login/widget/agreement_box.dart b/lib/page/system/login/widget/agreement_box.dart index 29d7a7d..473f64f 100644 --- a/lib/page/system/login/widget/agreement_box.dart +++ b/lib/page/system/login/widget/agreement_box.dart @@ -6,68 +6,40 @@ import '../../../../router/config/route_paths.dart'; ///勾中协议 class AgreementBox extends StatelessWidget { - final bool checked; - final Function(bool) onChanged; - const AgreementBox({ super.key, - this.checked = false, - required this.onChanged, }); @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: 25, - child: Transform.scale( - scale: 0.8, - child: Checkbox( - value: checked, - shape: CircleBorder(), - onChanged: (value) { - onChanged(value!); - }, - ), + return Text.rich( + textAlign: TextAlign.center, + TextSpan( + style: Theme.of(context).textTheme.labelSmall, + children: [ + const TextSpan(text: "By logging in, you agree to our "), + TextSpan( + text: "Terms", + style: TextStyle(color: Theme.of(context).primaryColor), + recognizer: TapGestureRecognizer() + ..onTap = () => context.push( + RoutePaths.agreement, + extra: {"title": "Terms of Service", "url": "https://support.curain.ai/privacy/foodcura/terms_service.html"}, + ), ), - ), - GestureDetector( - onTap: () { - onChanged(!checked); - }, - child: RichText( - text: TextSpan( - style: Theme.of(context).textTheme.labelSmall, - children: [ - TextSpan( - text: "I agree to the ", - ), - TextSpan( - text: "Terms", - style: TextStyle(color: Theme.of(context).primaryColor), - recognizer: TapGestureRecognizer() - ..onTap = () => context.push( - RoutePaths.agreement, - extra: {"title": "Terms of Service", "url": "https://support.curain.ai/privacy/foodcura/terms_service.html"}, - ), - ), - TextSpan(text: " & "), - TextSpan( - text: "Privacy Policy", - style: TextStyle(color: Theme.of(context).primaryColor), - recognizer: TapGestureRecognizer() - ..onTap = () => context.push( - RoutePaths.agreement, - extra: {"title": "Privacy", "url": "https://support.curain.ai/privacy/foodcura/privacy_policy.html"}, - ), - ), - ], - ), + const TextSpan(text: " and "), + TextSpan( + text: "Privacy Policy", + style: TextStyle(color: Theme.of(context).primaryColor), + recognizer: TapGestureRecognizer() + ..onTap = () => context.push( + RoutePaths.agreement, + extra: {"title": "Privacy", "url": "https://support.curain.ai/privacy/foodcura/privacy_policy.html"}, + ), ), - ), - ], + const TextSpan(text: "."), + ], + ), ); } } diff --git a/lib/page/system/login/widget/widget.dart b/lib/page/system/login/widget/widget.dart index cf8f757..b046263 100644 --- a/lib/page/system/login/widget/widget.dart +++ b/lib/page/system/login/widget/widget.dart @@ -10,12 +10,15 @@ class LogoBox extends StatelessWidget { margin: EdgeInsets.only(bottom: 40), child: Column( children: [ - Image.asset( - "assets/image/logo.png", - width: 43, + ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Image.asset( + "assets/image/logo.png", + width: 43, + ), ), Text( - "FoodCura", + "PlanCura", style: Theme.of(context).textTheme.titleSmall, ), ], diff --git a/lib/providers/app_store.dart b/lib/providers/app_store.dart index bb8a043..b80ffa2 100644 --- a/lib/providers/app_store.dart +++ b/lib/providers/app_store.dart @@ -21,7 +21,7 @@ class AppStore with ChangeNotifier { notifyListeners(); } - ///设置用户数据 + ///设置数据 Future setInfo(LoginDto data) async { token = data.accessToken!; userInfo = data.userInfo; @@ -30,6 +30,17 @@ class AppStore with ChangeNotifier { await Storage.set('token', token); } + ///更新用户信息 + void updateUserInfo(UserInfo value) async { + userInfo = userInfo?.copyWith( + name: value.name, + avatar: value.avatar, + description: value.description, + ); + await Storage.set('userInfo', userInfo?.toJson()); + notifyListeners(); + } + ///获取token static Future getToken() async { return await Storage.get("token") ?? ''; diff --git a/lib/router/config/route_paths.dart b/lib/router/config/route_paths.dart index 0cdcf2b..3d5ac1b 100644 --- a/lib/router/config/route_paths.dart +++ b/lib/router/config/route_paths.dart @@ -20,5 +20,5 @@ class RoutePaths { static const planHistory = "/planHistory"; ///计划详情页 - static const planDetail = "/planDetail"; + static String planDetail([int? id]) => id != null ? "/planDetail/$id" : "/planDetail/:id"; } diff --git a/lib/router/modules/plan.dart b/lib/router/modules/plan.dart index b043260..754a75b 100644 --- a/lib/router/modules/plan.dart +++ b/lib/router/modules/plan.dart @@ -11,9 +11,15 @@ List planRoutes = [ }, ), RouteType( - path: RoutePaths.planDetail, + path: RoutePaths.planDetail(), child: (state) { - return PlanDetailPage(); + final id = state.pathParameters['id']; + final extraMap = state.extra as Map?; + + return PlanDetailPage( + id: id, + planName: extraMap?['name'], + ); }, ), ]; diff --git a/lib/router/routes.dart b/lib/router/routes.dart index 332e8e1..341c2b1 100644 --- a/lib/router/routes.dart +++ b/lib/router/routes.dart @@ -22,7 +22,7 @@ List routes = routeConfigs.map((item) { //变量命名 GoRouter goRouter = GoRouter( - initialLocation: RoutePaths.layout, + initialLocation: RoutePaths.splash, routes: routes, navigatorKey: navigatorKey, ); diff --git a/lib/config/theme/custom_colors.dart b/lib/theme/custom_colors.dart similarity index 100% rename from lib/config/theme/custom_colors.dart rename to lib/theme/custom_colors.dart diff --git a/lib/theme/decorations/app_shadows.dart b/lib/theme/decorations/app_shadows.dart new file mode 100644 index 0000000..143ab92 --- /dev/null +++ b/lib/theme/decorations/app_shadows.dart @@ -0,0 +1,18 @@ + +import 'package:flutter/material.dart'; + +///阴影卡片 +final shadowDecoration = BoxDecoration( + border: Border.all(color: Colors.black, width: 2), + borderRadius: BorderRadius.circular(5), + color: Colors.white, + boxShadow: const [ + BoxShadow( + color: Color(0xffb5b5b5), + blurRadius: 2, + offset: Offset(6, 6), + spreadRadius: 0, + blurStyle: BlurStyle.normal, + ), + ], +); \ No newline at end of file diff --git a/lib/config/theme/theme.dart b/lib/theme/theme.dart similarity index 100% rename from lib/config/theme/theme.dart rename to lib/theme/theme.dart diff --git a/lib/utils/debouncer.dart b/lib/utils/debouncer.dart new file mode 100644 index 0000000..8f0004e --- /dev/null +++ b/lib/utils/debouncer.dart @@ -0,0 +1,20 @@ +import 'dart:async'; +import 'dart:ui'; + +class Debouncer { + final int milliseconds; + Timer? _timer; + + Debouncer({this.milliseconds = 500}); + + /// 调用时会延迟执行,如果期间再次调用会重置计时器 + void run(VoidCallback action) { + _timer?.cancel(); + _timer = Timer(Duration(milliseconds: milliseconds), action); + } + + /// 页面销毁时要记得调用 + void dispose() { + _timer?.cancel(); + } +} diff --git a/lib/widgets/business/delete_row_item.dart b/lib/widgets/business/delete_row_item.dart new file mode 100644 index 0000000..8f8fbc3 --- /dev/null +++ b/lib/widgets/business/delete_row_item.dart @@ -0,0 +1,113 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:remixicon/remixicon.dart'; + +class DeleteRowItem extends StatefulWidget { + final bool showDelete; + final List Function(BuildContext context, Animation animation) builder; + final Function() onDelete; + + const DeleteRowItem({ + super.key, + this.showDelete = false, + required this.builder, + required this.onDelete, + }); + + @override + State createState() => _DeleteRowItemState(); +} + +class _DeleteRowItemState extends State with TickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: Duration(milliseconds: 300), + ); + _animation = CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ); + + if (widget.showDelete) { + _controller.forward(); + } + } + + @override + void didUpdateWidget(covariant DeleteRowItem oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.showDelete != widget.showDelete) { + if (widget.showDelete) { + _controller.forward(); + } else { + _controller.reverse(); + } + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + ///点击删除 + void _handleDelete() { + showCupertinoDialog( + context: context, + builder: (_) { + return CupertinoAlertDialog( + title: Text("删除计划"), + actions: [ + CupertinoDialogAction( + child: Text("取消"), + onPressed: () { + context.pop(); + }, + ), + CupertinoDialogAction( + isDestructiveAction: true, + onPressed: () { + context.pop(); + widget.onDelete(); + }, + child: Text("确定"), + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizeTransition( + axis: Axis.horizontal, + sizeFactor: _animation, + axisAlignment: -1, // 从左向右展开 + child: InkWell( + onTap: _handleDelete, + child: Container( + margin: EdgeInsets.only(right: 10), + child: Icon( + RemixIcons.indeterminate_circle_fill, + color: Colors.red, + ), + ), + ), + ), + ...widget.builder(context, _animation), + // ...widget.children, + ], + ); + } +} diff --git a/lib/page/plan/history/widgets/popup_action.dart b/lib/widgets/ui_kit/popup/popup_action.dart similarity index 88% rename from lib/page/plan/history/widgets/popup_action.dart rename to lib/widgets/ui_kit/popup/popup_action.dart index dfd6665..d1b6395 100644 --- a/lib/page/plan/history/widgets/popup_action.dart +++ b/lib/widgets/ui_kit/popup/popup_action.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; -import 'package:remixicon/remixicon.dart'; class PopupAction extends StatelessWidget { final List> items; + final Widget child; final Function(String) onSelected; const PopupAction({ super.key, required this.items, + required this.child, required this.onSelected, }); @@ -26,7 +27,7 @@ class PopupAction extends StatelessWidget { shadowColor: Colors.black87, onSelected:onSelected, itemBuilder: (context) => items, - child: const Icon(RemixIcons.more_fill), + child:child , ); } } diff --git a/pubspec.lock b/pubspec.lock index 7254ed6..8877517 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -57,6 +81,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.3.4+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -153,11 +185,27 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.9.3+4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.1" flutter_easyloading: dependency: "direct main" description: @@ -174,6 +222,54 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" + flutter_image_compress: + dependency: "direct main" + description: + name: flutter_image_compress + sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.0" + flutter_image_compress_common: + dependency: transitive + description: + name: flutter_image_compress_common + sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.6" + flutter_image_compress_macos: + dependency: transitive + description: + name: flutter_image_compress_macos + sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.3" + flutter_image_compress_ohos: + dependency: transitive + description: + name: flutter_image_compress_ohos + sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.0.3" + flutter_image_compress_platform_interface: + dependency: transitive + description: + name: flutter_image_compress_platform_interface + sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + flutter_image_compress_web: + dependency: transitive + description: + name: flutter_image_compress_web + sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.5" flutter_lints: dependency: "direct dev" description: @@ -464,6 +560,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.0" path: dependency: transitive description: @@ -472,6 +576,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.18" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" path_provider_linux: dependency: transitive description: @@ -536,6 +664,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.28.0" scroll_to_index: dependency: transitive description: @@ -645,6 +781,54 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: @@ -669,6 +853,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.0" term_glyph: dependency: transitive description: @@ -757,6 +949,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.5.1" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 440e6e7..2d12372 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,8 @@ dependencies: image_picker: ^1.2.0 markdown_widget: ^2.3.2+8 sign_in_with_apple: ^7.0.1 + flutter_image_compress: ^2.4.0 + cached_network_image: ^3.4.1 dev_dependencies: flutter_test: