diff --git a/lib/api/dto/plan_detail_dto.dart b/lib/api/dto/plan_detail_dto.dart index 31d1172..b0f3041 100644 --- a/lib/api/dto/plan_detail_dto.dart +++ b/lib/api/dto/plan_detail_dto.dart @@ -47,11 +47,13 @@ class PlanDetailDto { String? dialog; List stepsList; List suggestionsList; + String? planEndTime; PlanDetailDto({ this.agentName, this.summary, this.dialog, + this.planEndTime, List? stepsList, List? suggestionsList, }) : stepsList = stepsList ?? [], @@ -68,6 +70,7 @@ class PlanDetailDto { suggestionsList: json["suggestions"] != null ? List.from(json["suggestions"]) : [], + planEndTime: json["plan_end_time"], ); } @@ -78,6 +81,7 @@ class PlanDetailDto { "dialog": dialog, "steps": stepsList.map((v) => v.toJson()).toList(), "suggestions": suggestionsList, + "plan_end_time": planEndTime, }; } } diff --git a/lib/page/plan/detail/other/done_stamp.dart b/lib/page/plan/detail/other/done_stamp.dart index 957856e..ee15d89 100644 --- a/lib/page/plan/detail/other/done_stamp.dart +++ b/lib/page/plan/detail/other/done_stamp.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:plan/page/plan/detail/viewmodel/plan_detail_store.dart'; +import 'package:plan/utils/common.dart'; +import 'package:plan/utils/format.dart'; import 'package:provider/provider.dart'; class DoneStamp extends StatefulWidget { @@ -38,6 +40,13 @@ class _DoneStampState extends State { "Completed", style: TextStyle(fontWeight: FontWeight.w700), ), + Visibility( + visible: getNotEmpty(store.planDetail.planEndTime) != null, + child: Text( + formatDateUS(store.planDetail.planEndTime!), + style: TextStyle(fontSize: 12), + ), + ), ], ), ), diff --git a/lib/page/plan/detail/other/footer_btn.dart b/lib/page/plan/detail/other/footer_btn.dart index 804cbe9..b9aa94b 100644 --- a/lib/page/plan/detail/other/footer_btn.dart +++ b/lib/page/plan/detail/other/footer_btn.dart @@ -5,7 +5,7 @@ import 'package:plan/api/endpoints/plan_api.dart'; import 'package:plan/data/models/plan_acttion_type.dart'; import 'package:plan/page/plan/detail/viewmodel/plan_detail_store.dart'; import 'package:provider/provider.dart'; -import 'package:remixicon/remixicon.dart'; +import 'package:intl/intl.dart'; class FooterBtn extends StatefulWidget { const FooterBtn({super.key}); @@ -30,6 +30,17 @@ class _FooterBtnState extends State { return item.copyWith(stepStatus: allDone ? 0 : 2); }).toList(); }); + + //如果是全部完成 + if (!allDone) { + store.updatePlanDetail((dto) { + final now = DateTime.now(); + final formatted = DateFormat('yyyy-MM-dd HH:mm:ss').format(now); + print(formatted); + dto.planEndTime = formatted; + }); + } + //接口 editPlanStepApi( store.planId, diff --git a/lib/page/plan/detail/widgets/plan_list.dart b/lib/page/plan/detail/widgets/plan_list.dart index 7d80f2f..15b506f 100644 --- a/lib/page/plan/detail/widgets/plan_list.dart +++ b/lib/page/plan/detail/widgets/plan_list.dart @@ -152,80 +152,105 @@ class PlanItem extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - if (!isEdit) { - onComplete(step); - } - }, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), - color: Colors.white, - child: DeleteRowItem( - showDelete: isEdit, - onDelete: () { - onDelete(step.id!); + return Selector( + selector: (_, store) => store.planContent, + builder: (context, planContent, _) { + return GestureDetector( + onTap: () { + if (!isEdit && planContent == "") { + onComplete(step); + } }, - builder: (_, animate) { - return [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: EdgeInsets.only(bottom: 3), - child: Text( - step.stepContent ?? "", - style: stepTextStyle(context, Theme.of(context).textTheme.bodyMedium), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), + color: Colors.white, + child: DeleteRowItem( + showDelete: isEdit, + onDelete: () { + onDelete(step.id!); + }, + builder: (_, animate) { + return [ + Visibility( + visible: !isEdit && planContent == "", + child: Transform.translate( + offset: Offset(0, -10), + child: Container( + width: 24, + height: 24, + margin: EdgeInsets.only(right: 10), + child: Checkbox( + value: step.stepStatus == 2, + onChanged: (value) { + onComplete(step); + }, + ), ), ), - AnimatedSwitcher( - duration: Duration(milliseconds: 300), - transitionBuilder: (child, animation) { - return FadeTransition( - opacity: animation, - child: SizeTransition( - sizeFactor: animation, - axis: Axis.vertical, - child: child, - ), - ); - }, - child: getNotEmpty(step.stepExplain) == null - ? SizedBox.shrink(key: ValueKey("empty")) - : Text( - step.stepExplain!, - style: stepTextStyle( - context, - Theme.of(context).textTheme.labelSmall, - ), - key: ValueKey(step.stepExplain), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.only(bottom: 3), + child: Text( + step.stepContent ?? "", + style: stepTextStyle( + context, + Theme.of(context).textTheme.bodyMedium, ), - ), - ], - ), - ), - SizeTransition( - axis: Axis.horizontal, - sizeFactor: animate, - child: GestureDetector( - onTap: () { - onEdit(step); - }, - child: Container( - alignment: Alignment.center, - margin: EdgeInsets.only(left: 10), - child: Opacity( - opacity: 0.4, - child: Icon(RemixIcons.edit_box_line), + ), + ), + AnimatedSwitcher( + duration: Duration(milliseconds: 300), + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + sizeFactor: animation, + axis: Axis.vertical, + child: child, + ), + ); + }, + child: getNotEmpty(step.stepExplain) == null + ? SizedBox.shrink(key: ValueKey("empty")) + : Text( + step.stepExplain!, + style: stepTextStyle( + context, + Theme.of(context).textTheme.labelSmall, + ), + key: ValueKey(step.stepExplain), + ), + ), + ], ), ), - ), - ), - ]; - }, - ), - ), + SizeTransition( + axis: Axis.horizontal, + sizeFactor: animate, + child: GestureDetector( + onTap: () { + onEdit(step); + }, + child: Container( + alignment: Alignment.center, + margin: EdgeInsets.only(left: 10), + child: Opacity( + opacity: 0.4, + child: Icon(RemixIcons.edit_box_line), + ), + ), + ), + ), + ]; + }, + ), + ), + ); + }, ); } diff --git a/lib/page/plan/history/plan_history_page.dart b/lib/page/plan/history/plan_history_page.dart index 0dcbdb9..c45d437 100644 --- a/lib/page/plan/history/plan_history_page.dart +++ b/lib/page/plan/history/plan_history_page.dart @@ -1,10 +1,11 @@ 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:provider/provider.dart'; -import 'widgets/history_item.dart'; -import 'widgets/loading_box.dart'; +import 'viewmodel/plan_list_store.dart'; +import 'widgets/custom_indicator.dart'; +import 'widgets/history_list.dart'; +import 'widgets/bar_actions.dart'; class PlanHistoryPage extends StatefulWidget { const PlanHistoryPage({super.key}); @@ -14,81 +15,43 @@ class PlanHistoryPage extends StatefulWidget { } class _PlanHistoryPageState extends State { - ///是否显示删除 - bool _isDelete = false; - - ///数据 - List _record = []; - bool _isInit = true; - @override void initState() { super.initState(); - _onRefresh(); - } - - ///刷新 - Future _onRefresh() async { - var list = await getPlanListApi(); - setState(() { - _record = list; - _isInit = false; - }); - } - - ///确认删除 - void _confirmDelete(int id) { - setState(() { - _record.removeWhere((element) => element.id == id); - }); } @override Widget build(BuildContext context) { - return CupertinoPageScaffold( - backgroundColor: Theme.of(context).colorScheme.surfaceContainer, - navigationBar: CupertinoNavigationBar( - middle: const Text("Plan History"), - ), - child: SafeArea( - child: CustomScrollView( - physics: AlwaysScrollableScrollPhysics( - parent: BouncingScrollPhysics(), - ), - slivers: [ - //下拉刷新组件 - CupertinoSliverRefreshControl( - onRefresh: _onRefresh, + return ChangeNotifierProvider( + create: (_) { + return PlanListStore(); + }, + child: Consumer( + child: SafeArea( + child: CustomScrollView( + physics: AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), ), - //列表 - 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, - onInit: _onRefresh, - ); - }, - separatorBuilder: (BuildContext context, int index) { - return SizedBox(height: 15); - }, - itemCount: _record.length, - ), - ], - ), + slivers: [ + //下拉刷新组件 + CustomIndicator(), + //列表 + HistoryList(), + ], + ), + ), + builder: (context, store, child) { + return CupertinoPageScaffold( + backgroundColor: Theme.of(context).colorScheme.surfaceContainer, + navigationBar: CupertinoNavigationBar( + middle: const Text("Plan History"), + trailing: BarActions( + store: store, ), ), - ], - ), + child: child!, + ); + }, ), ); } diff --git a/lib/page/plan/history/viewmodel/plan_list_store.dart b/lib/page/plan/history/viewmodel/plan_list_store.dart new file mode 100644 index 0000000..ca00fb3 --- /dev/null +++ b/lib/page/plan/history/viewmodel/plan_list_store.dart @@ -0,0 +1,63 @@ +import 'package:flutter/cupertino.dart'; +import 'package:plan/api/dto/plan_item_dto.dart'; +import 'package:plan/api/endpoints/plan_api.dart'; + +///筛选已完成和没完成 +class PlanStatusFilter { + bool archived; + bool unarchived; + + PlanStatusFilter({required this.archived, required this.unarchived}); + + PlanStatusFilter copyWith({ + bool? archived, + bool? unarchived, + }) { + return PlanStatusFilter( + archived: archived ?? this.archived, + unarchived: unarchived ?? this.unarchived, + ); + } +} + +class PlanListStore extends ChangeNotifier { + bool isInit = true; + + ///原始数据 + List _originList = []; + + List get planList => _originList; + + ///显示已完成和未完成 + PlanStatusFilter planStatus = PlanStatusFilter(archived: true, unarchived: true); + + PlanListStore() { + getPlanList(); + } + + Future getPlanList() async { + _originList = await getPlanListApi(); + isInit = false; + notifyListeners(); + } + + ///修改状态 + void setPlanStatus(PlanStatusFilter planStatus) { + this.planStatus = planStatus; + notifyListeners(); + } + + ///更新子项 + void updateStep(PlanItemDto newStep) { + final int index = _originList.indexWhere((e) => e.id == newStep.id); + if (index != -1) { + _originList[index] = newStep; // 引用变了 → 框架感知 + notifyListeners(); + } + } + ///移除子项 + void removeStep(PlanItemDto step) { + _originList.removeWhere((e) => e.id == step.id); + notifyListeners(); + } +} diff --git a/lib/page/plan/history/widgets/bar_actions.dart b/lib/page/plan/history/widgets/bar_actions.dart new file mode 100644 index 0000000..5c95db4 --- /dev/null +++ b/lib/page/plan/history/widgets/bar_actions.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:plan/widgets/ui_kit/popup/popup_action.dart'; +import 'package:remixicon/remixicon.dart'; + +import '../viewmodel/plan_list_store.dart'; + +class BarActions extends StatefulWidget { + final PlanListStore store; + const BarActions({super.key, required this.store}); + + @override + State createState() => _BarActionsState(); +} + +class _BarActionsState extends State { + void _onSelected(String value) { + switch (value) { + case "Unarchived": + widget.store.setPlanStatus( + widget.store.planStatus.copyWith( + unarchived: !widget.store.planStatus.unarchived, + ), + ); + break; + case "Archived": + widget.store.setPlanStatus( + widget.store.planStatus.copyWith( + archived: !widget.store.planStatus.archived, + ), + ); + break; + } + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + PopupAction( + onSelected: _onSelected, + items: [ + PopupMenuItem( + value: "Unarchived", + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Unarchived"), + Visibility( + visible: widget.store.planStatus.unarchived, + child: Icon(RemixIcons.check_fill, size: 20), + ), + ], + ), + ), + PopupMenuItem( + value: "Archived", + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Archived"), + Visibility( + visible: widget.store.planStatus.archived, + child: Icon(RemixIcons.check_fill, size: 20), + ), + ], + ), + ), + ], + child: Icon(RemixIcons.more_fill), + ), + ], + ); + } +} diff --git a/lib/page/plan/history/widgets/custom_indicator.dart b/lib/page/plan/history/widgets/custom_indicator.dart new file mode 100644 index 0000000..18b5bce --- /dev/null +++ b/lib/page/plan/history/widgets/custom_indicator.dart @@ -0,0 +1,25 @@ +import 'package:flutter/cupertino.dart'; +import 'package:provider/provider.dart'; + +import '../viewmodel/plan_list_store.dart'; + +class CustomIndicator extends StatefulWidget { + const CustomIndicator({super.key}); + + @override + State createState() => _CustomIndicatorState(); +} + +class _CustomIndicatorState extends State { + Future _onRefresh() async { + PlanListStore store = context.read(); + await store.getPlanList(); + } + + @override + Widget build(BuildContext context) { + return CupertinoSliverRefreshControl( + onRefresh: _onRefresh, + ); + } +} diff --git a/lib/page/plan/history/widgets/history_item.dart b/lib/page/plan/history/widgets/history_list.dart similarity index 60% rename from lib/page/plan/history/widgets/history_item.dart rename to lib/page/plan/history/widgets/history_list.dart index 3b3cc64..25a8f58 100644 --- a/lib/page/plan/history/widgets/history_item.dart +++ b/lib/page/plan/history/widgets/history_list.dart @@ -3,86 +3,105 @@ 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/page/plan/history/viewmodel/plan_list_store.dart'; import 'package:plan/router/config/route_paths.dart'; import 'package:plan/utils/format.dart'; +import 'package:provider/provider.dart'; import 'package:remixicon/remixicon.dart'; import '../../widgets/edit_desc_dialog.dart'; +import 'loading_box.dart'; -class HistoryItem extends StatefulWidget { - final PlanItemDto item; - final bool showDelete; - final Function(int) onDelete; - final Function() onInit; - - const HistoryItem({ - super.key, - required this.item, - this.showDelete = false, - required this.onDelete, - required this.onInit, - }); +class HistoryList extends StatefulWidget { + const HistoryList({super.key}); @override - State createState() => _HistoryItemState(); + State createState() => _HistoryListState(); } -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; - }); - } - } - +class _HistoryListState extends State { ///跳转详情 - void _goDetail() async { - await context.push(RoutePaths.planDetail(_data.id)); - widget.onInit(); + void _goDetail(PlanItemDto data) async { + PlanListStore store = context.read(); + await context.push(RoutePaths.planDetail(data.id)); + store.getPlanList(); } ///编辑摘要 - void _handEdit() { + void _handEdit(PlanItemDto data) { context.pop(); + PlanListStore store = context.read(); showEditDescDialog( context, - value: _data.summary ?? "", + value: data.summary ?? "", onConfirm: (value) async { - setState(() { - _data.summary = value; - }); - await editPlanSummaryApi(_data.id!, value); + data.summary = value; + store.updateStep(data); + await editPlanSummaryApi(data.id!, value); }, ); } ///删除计划 - void _handDelete() async { + void _handDelete(PlanItemDto data) async { context.pop(); - widget.onDelete(_data.id!); - await deletePlanApi(_data.id!); + PlanListStore store = context.read(); + store.removeStep(data); + await deletePlanApi(data.id!); } @override Widget build(BuildContext context) { - var progress = (_data.completedSteps! / _data.totalSteps!); + return Consumer( + builder: (context, store, _) { + var record = store.planList.where((item) { + var progress = (item.completedSteps! / item.totalSteps!); + //不显示已完成 + if (!store.planStatus.archived && progress == 1) { + return false; + } + //不显示未完成 + if (!store.planStatus.unarchived && progress < 1) { + return false; + } + return true; + }).toList(); + + return LoadingBox( + loading: store.isInit, + isEmpty: record.isEmpty, + child: CustomScrollView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + slivers: [ + SliverList.separated( + itemBuilder: (BuildContext context, int index) { + return _buildItem( + item: record[index], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return SizedBox(height: 15); + }, + itemCount: record.length, + ), + ], + ), + ); + }, + ); + } + + Widget _buildItem({required PlanItemDto item}) { + var progress = (item.completedSteps! / item.totalSteps!); return CupertinoContextMenu( enableHapticFeedback: true, actions: [ // 编辑摘要 CupertinoContextMenuAction( - onPressed: _handEdit, + onPressed: () { + _handEdit(item); + }, child: _actionItem( "Edit Summary", CupertinoIcons.pencil, @@ -90,7 +109,9 @@ class _HistoryItemState extends State { ), CupertinoContextMenuAction( isDestructiveAction: true, - onPressed: _handDelete, + onPressed: () { + _handDelete(item); + }, child: _actionItem( "Delete Summary", CupertinoIcons.delete, @@ -98,7 +119,9 @@ class _HistoryItemState extends State { ), ], child: GestureDetector( - onTap: _goDetail, + onTap: () { + _goDetail(item); + }, child: Container( width: 400, margin: EdgeInsets.symmetric(horizontal: 15), @@ -117,7 +140,7 @@ class _HistoryItemState extends State { children: [ Row( children: [ - Expanded(child: Text(_data.summary ?? "")), + Expanded(child: Text(item.summary ?? "")), Visibility( visible: progress == 1, child: Container( @@ -140,7 +163,7 @@ class _HistoryItemState extends State { Container( margin: const EdgeInsets.only(bottom: 5, top: 5), child: Text( - "${formatDateUS(_data.createdAt!, withTime: true)} · Coach ${_data.agentName ?? ""}", + "${formatDateUS(item.createdAt!, withTime: true)} · Coach ${item.agentName ?? ""}", style: Theme.of(context).textTheme.labelSmall, // 小号文字 ), ), @@ -154,7 +177,7 @@ class _HistoryItemState extends State { ), ), Text( - "${_data.completedSteps}/${_data.totalSteps}", + "${item.completedSteps}/${item.totalSteps}", style: Theme.of(context).textTheme.labelSmall, ), ], diff --git a/lib/page/system/code/login_code_page.dart b/lib/page/system/code/login_code_page.dart new file mode 100644 index 0000000..a9a6890 --- /dev/null +++ b/lib/page/system/code/login_code_page.dart @@ -0,0 +1,228 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:go_router/go_router.dart'; +import 'package:plan/api/endpoints/user_api.dart'; +import 'package:plan/api/network/safe.dart'; +import 'package:provider/provider.dart'; + +import '../../../providers/app_store.dart'; +import '../../../router/config/route_paths.dart'; + +class LoginCodePage extends StatefulWidget { + final String email; + final String password; + + const LoginCodePage({super.key, required this.email, required this.password}); + + @override + State createState() => _LoginCodePageState(); +} + +class _LoginCodePageState extends State { + final List _focusNodes = List.generate(4, (_) => FocusNode()); + final List _controllers = List.generate( + 4, + (_) => TextEditingController(), + ); + + //倒计时 + int _count = 60; + Timer? _timer; + + @override + void initState() { + super.initState(); + _handSendCode(); + _handClear(); + } + + @override + void dispose() { + _timer?.cancel(); + _handClear(); + super.dispose(); + } + + ///小输入框改变时 + void _onChanged(String value, int index) async { + //一键复制 + if (value.length == 4) { + _handlePaste(value); + } + //提交 + if (value.isNotEmpty && index == 3) { + _handSubmit(); + return; + } + // 自动跳到下一格 + if (value.length == 1 && index < 3) { + _focusNodes[index + 1].requestFocus(); + } + } + + void _handlePaste(String pastedText) { + // 只取前4位数字 + final digits = pastedText.replaceAll(RegExp(r'[^0-9]'), ''); + for (int i = 0; i < 4; i++) { + _controllers[i].text = i < digits.length ? digits[i] : ''; + } + if (digits.length >= 4) { + _focusNodes[3].requestFocus(); + _handSubmit(); + } else if (digits.isNotEmpty) { + _focusNodes[digits.length].requestFocus(); + } + } + + ///删除键 + void _onDelete(KeyEvent event, int index) { + if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.backspace) { + final currentController = _controllers[index]; + if (currentController.text.isEmpty && index > 0) { + _focusNodes[index - 1].requestFocus(); + _controllers[index - 1].clear(); + } + } + } + + ///发送验证码 + void _handSendCode() { + if (_count != 60) { + return; + } + _timer = Timer.periodic(Duration(seconds: 1), (timer) { + setState(() { + _count--; + }); + if (_count == 0) { + setState(() { + _count = 60; + }); + timer.cancel(); + } + }); + sendEmailCodeApi(widget.email); + EasyLoading.showToast("Send success"); + } + + ///提交 + void _handSubmit() async { + String code = _controllers.map((controller) => controller.text).join(); + if (code.length == 4) { + EasyLoading.show(); + var res = await safeRequest( + registerApi( + widget.email, + widget.password, + code, + ), + onError: (error) { + _handClear(); + EasyLoading.showToast("Login Error"); + }, + ); + var appStore = context.read(); + await appStore.setInfo(res); + context.go(RoutePaths.layout); + } + } + + ///清空 + void _handClear() { + for (var controller in _controllers) { + controller.clear(); + } + _focusNodes.first.requestFocus(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar(), + body: ListView( + physics: NeverScrollableScrollPhysics(), + padding: EdgeInsets.all(20), + children: [ + Container( + margin: EdgeInsets.only(bottom: 20), + child: Text( + "Check your inbox", + style: Theme.of(context).textTheme.titleLarge, + ), + ), + Container( + margin: EdgeInsets.only(bottom: 60), + child: Text( + "Verification code has been sent to ${widget.email}", + style: Theme.of(context).textTheme.labelLarge, + ), + ), + Row( + spacing: 20, + children: List.generate(4, (index) { + return Expanded( + child: AspectRatio( + aspectRatio: 1, + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(10), + ), + child: KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: (event) { + _onDelete(event, index); + }, + child: TextField( + controller: _controllers[index], + focusNode: _focusNodes[index], + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 4, + style: TextStyle(fontSize: 32), + decoration: InputDecoration( + counterText: "", + border: InputBorder.none, + isCollapsed: true, + ), + onChanged: (value) { + _onChanged(value, index); + }, + ), + ), + ), + ), + ); + }), + ), + Container( + margin: EdgeInsets.only(top: 30), + child: Row( + children: [ + GestureDetector( + onTap: _handSendCode, + child: Visibility( + visible: _count != 60, + replacement: Text( + "Resend Code", + style: Theme.of(context).textTheme.bodySmall, + ), + child: Text( + "Resend Code(${_count}s)", + style: Theme.of(context).textTheme.labelMedium, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/page/system/login/login_code_page.dart b/lib/page/system/login/login_code_page.dart deleted file mode 100644 index 96d416c..0000000 --- a/lib/page/system/login/login_code_page.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'package:go_router/go_router.dart'; -import 'package:provider/provider.dart'; - -import '../../../api/endpoints/user_api.dart'; -import '../../../api/network/safe.dart'; -import '../../../providers/app_store.dart'; -import '../../../router/config/route_paths.dart'; -import '../../../widgets/ui_kit/button/custom_button.dart'; -import 'widget/widget.dart'; - -class LoginCodePage extends StatefulWidget { - final String email; - final String password; - - const LoginCodePage({super.key, required this.email, required this.password}); - - @override - State createState() => _LoginCodePageState(); -} - -class _LoginCodePageState extends State { - final _codeController = TextEditingController(); - var _subLoading = false; - - @override - void initState() { - super.initState(); - _handSendCode(); - } - - ///发送验证码 - void _handSendCode() { - sendEmailCodeApi(widget.email); - EasyLoading.showSuccess("Send success"); - } - - ///提交 - void _handSubmit() async { - if (_codeController.text.isNotEmpty) { - setState(() { - _subLoading = true; - }); - var res = await safeRequest( - registerApi( - widget.email, - widget.password, - _codeController.text, - ), - onError: (error) { - setState(() { - _subLoading = false; - }); - }, - ); - var appStore = context.read(); - await appStore.setInfo(res); - context.go(RoutePaths.layout); - setState(() { - _subLoading = false; - }); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: SafeArea( - child: Container( - width: double.infinity, - padding: EdgeInsets.only(left: 20, right: 20, top: 40), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "Check your inbox", - style: Theme.of(context).textTheme.titleLarge, - ), - Container( - margin: EdgeInsets.only(top: 20, bottom: 40), - child: Text( - "Enter the verification code we just sent to ${widget.email}.", - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.labelMedium, - ), - ), - InputBox(hintText: "Code", controller: _codeController), - Container( - margin: EdgeInsets.only(top: 20), - child: CustomButton( - loading: _subLoading, - onPressed: _handSubmit, - child: Text("Continue"), - ), - ), - Container( - margin: EdgeInsets.only(top: 20), - child: TextButton( - onPressed: () { - _handSendCode(); - }, - child: Text( - "Resend code", - style: Theme.of(context).textTheme.labelSmall, - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/page/system/login/login_page.dart b/lib/page/system/login/login_page.dart index da32040..48c4eb8 100644 --- a/lib/page/system/login/login_page.dart +++ b/lib/page/system/login/login_page.dart @@ -14,7 +14,9 @@ import '../../../router/config/route_paths.dart'; import '../../../utils/common.dart'; import '../../../widgets/ui_kit/button/custom_button.dart'; import 'widget/agreement_box.dart'; -import 'widget/widget.dart'; +import 'widget/login_input.dart'; +import 'widget/login_header.dart'; +import 'widget/login_other.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @@ -48,7 +50,11 @@ class _LoginPageState extends State { try { await Dio().get( 'https://captive.apple.com/hotspot-detect.html', - options: Options(sendTimeout: const Duration(seconds: 3), receiveTimeout: const Duration(seconds: 3), headers: {'Cache-Control': 'no-cache'}), + options: Options( + sendTimeout: const Duration(seconds: 3), + receiveTimeout: const Duration(seconds: 3), + headers: {'Cache-Control': 'no-cache'}, + ), ); return true; } catch (_) { @@ -58,11 +64,16 @@ class _LoginPageState extends State { void _initGoogleSign() { if (isAndroid()) { - _googleSignIn.initialize(clientId: null, serverClientId: "512878764950-0bsl98c4q4p695mlmfn35qhmr2ld5n0o.apps.googleusercontent.com"); + _googleSignIn.initialize( + clientId: null, + serverClientId: + "512878764950-0bsl98c4q4p695mlmfn35qhmr2ld5n0o.apps.googleusercontent.com", + ); } else { _googleSignIn.initialize( clientId: "512878764950-4hpppthg6c8p98mkfcro99echkftbbmo.apps.googleusercontent.com", - serverClientId: "512878764950-0bsl98c4q4p695mlmfn35qhmr2ld5n0o.apps.googleusercontent.com", + serverClientId: + "512878764950-0bsl98c4q4p695mlmfn35qhmr2ld5n0o.apps.googleusercontent.com", ); } @@ -76,7 +87,7 @@ class _LoginPageState extends State { } ///谷歌登录 - void _handleGoogleSignIn() async { + void _handGoogleSignIn() async { try { // 如果用户未登录,则启动标准的 Google 登录 if (_googleSignIn.supportsAuthenticate()) { @@ -112,7 +123,9 @@ class _LoginPageState extends State { ///apple登录 void _handAppleSignIn() async { try { - final credential = await SignInWithApple.getAppleIDCredential(scopes: [AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName]); + final credential = await SignInWithApple.getAppleIDCredential( + scopes: [AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName], + ); EasyLoading.show(status: "Logging in..."); var res = await thirdLoginApi(credential.identityToken!, OtherLoginType.apple); EasyLoading.dismiss(); @@ -138,7 +151,10 @@ class _LoginPageState extends State { }); var isRegister = await checkRegisterApi(_emailController.text); if (!isRegister) { - context.push(RoutePaths.loginCode, extra: {"email": _emailController.text, "password": _passwordController.text}); + context.push( + RoutePaths.loginCode, + extra: {"email": _emailController.text, "password": _passwordController.text}, + ); } else { var res = await loginApi(_emailController.text, _passwordController.text); _onLogin(res); @@ -174,9 +190,9 @@ class _LoginPageState extends State { children: [ LogoBox(), PageHeader(), - InputBox(hintText: "Email", controller: _emailController), + LoginInput(hintText: "Email", controller: _emailController), SizedBox(height: 15), - InputBox( + LoginInput( obscureText: _hidePassword, hintText: "Password", controller: _passwordController, @@ -196,23 +212,12 @@ class _LoginPageState extends State { Container( margin: EdgeInsets.only(top: 20), height: 45, - child: CustomButton(loading: _subLoading, round: false, onPressed: _handSubmit, child: Text("Continue")), - ), - LoginDivider(), - OtherButton( - title: "Continue with Google", - icon: "assets/image/google.png", - onTap: () { - _handleGoogleSignIn(); - }, - ), - SizedBox(height: 15), - OtherButton( - title: "Continue with Apple", - icon: "assets/image/apple.png", - onTap: () { - _handAppleSignIn(); - }, + child: CustomButton( + loading: _subLoading, + round: false, + onPressed: _handSubmit, + child: Text("Continue"), + ), ), Container( width: double.infinity, @@ -220,6 +225,26 @@ class _LoginPageState extends State { alignment: Alignment.center, child: AgreementBox(), ), + LoginDivider(), + Row( + spacing: 20, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + OtherButton( + onTap: () { + _handGoogleSignIn(); + }, + icon: "assets/image/google.png", + ), + OtherButton( + onTap: () { + _handAppleSignIn(); + }, + icon: "assets/image/apple.png", + ), + ], + ), ], ), ), diff --git a/lib/page/system/login/widget/login_header.dart b/lib/page/system/login/widget/login_header.dart new file mode 100644 index 0000000..8e49e1c --- /dev/null +++ b/lib/page/system/login/widget/login_header.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +///登陆Box +class LogoBox extends StatelessWidget { + const LogoBox({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 40), + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Image.asset( + "assets/image/logo.png", + width: 43, + ), + ), + Text( + "PlanCura", + style: Theme.of(context).textTheme.titleSmall, + ), + ], + ), + ); + } +} + +///头部文案 +class PageHeader extends StatelessWidget { + const PageHeader({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 30), + child: Column( + children: [ + Text( + "Create an account", + style: TextStyle(fontWeight: FontWeight.w700), + ), + Text( + "Use your email to get started", + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ); + } +} + diff --git a/lib/page/system/login/widget/login_input.dart b/lib/page/system/login/widget/login_input.dart new file mode 100644 index 0000000..736c78c --- /dev/null +++ b/lib/page/system/login/widget/login_input.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +///输入框 +class LoginInput extends StatelessWidget { + final bool obscureText; + final String hintText; + final TextEditingController controller; + final Widget? suffix; + + const LoginInput({ + super.key, + this.obscureText = false, + required this.hintText, + required this.controller, + this.suffix, + }); + + @override + Widget build(BuildContext context) { + //边框 + var inputBorder = OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.surfaceContainer, + ), + ); + return TextField( + controller: controller, + maxLength: 100, + obscureText: obscureText, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + hintText: hintText, + hintStyle: Theme.of(context).textTheme.labelMedium, + counterText: '', + border: inputBorder, + enabledBorder: inputBorder, + suffix: suffix, + suffixIconConstraints: BoxConstraints( + minWidth: 0, + minHeight: 0, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/page/system/login/widget/login_other.dart b/lib/page/system/login/widget/login_other.dart new file mode 100644 index 0000000..c21c81d --- /dev/null +++ b/lib/page/system/login/widget/login_other.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +///分割线 +class LoginDivider extends StatelessWidget { + const LoginDivider({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(top: 20, bottom: 20), + child: Row( + spacing: 8, + children: [ + Expanded( + child: Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainer, + ), + ), + Text( + "or", + style: Theme.of(context).textTheme.labelMedium, + ), + Expanded( + child: Container( + height: 1, + color: Theme.of(context).colorScheme.surfaceContainer, + ), + ), + ], + ), + ); + } +} + +class OtherButton extends StatelessWidget { + final Function() onTap; + final String icon; + + const OtherButton({ + super.key, + required this.onTap, + required this.icon, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 45, + padding: EdgeInsets.all(10), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainer, + ), + ), + child: AspectRatio( + aspectRatio: 1, + child: Image.asset(icon), + ), + ), + ); + } +} diff --git a/lib/page/system/login/widget/widget.dart b/lib/page/system/login/widget/widget.dart deleted file mode 100644 index b046263..0000000 --- a/lib/page/system/login/widget/widget.dart +++ /dev/null @@ -1,169 +0,0 @@ -import 'package:flutter/material.dart'; - -///登陆Box -class LogoBox extends StatelessWidget { - const LogoBox({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - margin: EdgeInsets.only(bottom: 40), - child: Column( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(5), - child: Image.asset( - "assets/image/logo.png", - width: 43, - ), - ), - Text( - "PlanCura", - style: Theme.of(context).textTheme.titleSmall, - ), - ], - ), - ); - } -} - -///头部文案 -class PageHeader extends StatelessWidget { - const PageHeader({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - margin: EdgeInsets.only(bottom: 30), - child: Column( - children: [ - Text( - "Create an account", - style: TextStyle(fontWeight: FontWeight.w700), - ), - Text( - "Use your email to get started", - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ); - } -} - -///输入框 -class InputBox extends StatelessWidget { - final bool obscureText; - final String hintText; - final TextEditingController controller; - final Widget? suffix; - - const InputBox({ - super.key, - this.obscureText = false, - required this.hintText, - required this.controller, - this.suffix, - }); - - @override - Widget build(BuildContext context) { - //边框 - var inputBorder = OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: Theme.of(context).colorScheme.surfaceContainer, - ), - ); - return TextField( - controller: controller, - maxLength: 100, - obscureText: obscureText, - style: Theme.of(context).textTheme.bodyMedium, - decoration: InputDecoration( - hintText: hintText, - hintStyle: Theme.of(context).textTheme.labelMedium, - counterText: '', - border: inputBorder, - enabledBorder: inputBorder, - suffix: suffix, - suffixIconConstraints: BoxConstraints( - minWidth: 0, - minHeight: 0, - ), - ), - ); - } -} - -///分割线 -class LoginDivider extends StatelessWidget { - const LoginDivider({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - margin: EdgeInsets.only(top: 20, bottom: 20), - child: Row( - spacing: 8, - children: [ - Expanded( - child: Container( - height: 1, - color: Theme.of(context).colorScheme.surfaceContainer, - ), - ), - Text( - "or", - style: Theme.of(context).textTheme.labelMedium, - ), - Expanded( - child: Container( - height: 1, - color: Theme.of(context).colorScheme.surfaceContainer, - ), - ), - ], - ), - ); - } -} - -///其他登陆按钮 -class OtherButton extends StatelessWidget { - final Function() onTap; - final String title; - final String icon; - - const OtherButton({ - super.key, - required this.onTap, - required this.title, - required this.icon, - }); - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Container( - padding: EdgeInsets.all(15), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Theme.of(context).colorScheme.surfaceContainer, - ), - child: Row( - spacing: 10, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image.asset(icon, width: 20), - Text( - title, - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ), - ); - } -} diff --git a/lib/router/modules/base.dart b/lib/router/modules/base.dart index cdae644..0bfe4a9 100644 --- a/lib/router/modules/base.dart +++ b/lib/router/modules/base.dart @@ -1,8 +1,8 @@ import 'package:plan/page/home/home_page.dart'; +import 'package:plan/page/system/code/login_code_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'; import '../../page/system/login/login_page.dart'; import '../../page/system/splash/splash_page.dart'; import '../config/route_paths.dart'; @@ -32,7 +32,10 @@ List baseRoutes = [ path: RoutePaths.loginCode, child: (state) { final args = state.extra as Map; - return LoginCodePage(email: args['email'], password: args['password']); + return LoginCodePage( + email: args['email'], + password: args['password'], + ); }, ), RouteType( diff --git a/lib/utils/format.dart b/lib/utils/format.dart index 1655b98..3af9f80 100644 --- a/lib/utils/format.dart +++ b/lib/utils/format.dart @@ -1,42 +1,3 @@ - - -// /// 格式化日期时间 -// 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}) { diff --git a/pubspec.yaml b/pubspec.yaml index 596950a..38dd173 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: plan description: "A new Flutter project." publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.0 +version: 1.0.2 environment: sdk: ^3.8.1