Compare commits

..

13 Commits

36 changed files with 1516 additions and 2304 deletions

View File

@@ -78,7 +78,7 @@ android {
dependencies {
// 添加核心库脱糖依赖
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
flutter {

58
lib/layout/app_body.dart Normal file
View File

@@ -0,0 +1,58 @@
import 'package:flisp_app/pages/calendar_page.dart';
import 'package:flisp_app/pages/flisp_page.dart';
import 'package:flisp_app/pages/stats_page.dart';
import 'package:flisp_app/pages/todo_page.dart';
import 'package:flisp_app/provider/app_provider.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AppBody extends StatefulWidget {
const AppBody({super.key});
@override
State<AppBody> createState() => AppBodyState();
}
class AppBodyState extends State<AppBody> {
final GlobalKey<FlispPageState> _flispPageKey = GlobalKey();
final GlobalKey<TodoPageState> _todoPageKey = GlobalKey();
final GlobalKey<CalendarPageState> _calendarPageKey = GlobalKey();
Widget _buildPage(int index) {
switch (index) {
case 0:
return FlispPage(key: _flispPageKey);
case 1:
return TodoPage(key: _todoPageKey);
case 2:
return CalendarPage(key: _calendarPageKey);
case 3:
return StatsPage();
default:
return FlispPage(key: _flispPageKey);
}
}
void showPageAddDialog(int index) {
if (index == 0) {
if (_flispPageKey.currentState != null) {
_flispPageKey.currentState!.showAddDialog();
}
} else if (index == 1) {
if (_todoPageKey.currentState != null) {
_todoPageKey.currentState!.showAddDialog();
}
} else if (index == 2) {
if (_calendarPageKey.currentState != null) {
_calendarPageKey.currentState!.showAddDialog();
}
}
}
@override
Widget build(BuildContext context) {
final appProvider = context.watch<AppProvider>();
return _buildPage(appProvider.currentTab);
}
}

View File

@@ -1,12 +1,17 @@
import 'package:flisp_app/provider/app_provider.dart';
import 'package:flisp_app/utils/theme_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/flutter_common.dart';
import 'package:provider/provider.dart';
class AppDrawer extends StatelessWidget {
class AppDrawer extends StatefulWidget {
const AppDrawer({super.key});
DrawerHeader buildDrawerHeader(BuildContext context) {
@override
State<AppDrawer> createState() => AppDrawerState();
}
class AppDrawerState extends State<AppDrawer> {
DrawerHeader _buildDrawerHeader() {
return DrawerHeader(
decoration: BoxDecoration(color: Theme.of(context).colorScheme.primary),
child: Column(
@@ -38,43 +43,9 @@ class AppDrawer extends StatelessWidget {
),
);
}
Widget _buildDarkModeSection(BuildContext context, AppProvider appProvider) {
return ListTile(
leading: Icon(
appProvider.isDarkMode ? Icons.dark_mode : Icons.light_mode,
color: Theme.of(context).colorScheme.primary,
),
title: const Text('暗黑模式'),
trailing: Switch(
value: appProvider.isDarkMode,
onChanged: (value) {
appProvider.toggleDarkMode(value);
},
),
onTap: () {
appProvider.toggleDarkMode(!appProvider.isDarkMode);
},
);
}
Widget _buildThemeSection(BuildContext context, AppProvider appProvider) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text('主题颜色', style: Theme.of(context).textTheme.titleMedium),
),
_buildDarkModeSection(context, appProvider),
buildThemeColorList(context, appProvider),
],
);
}
// 构建抽屉菜单项
Widget _buildDrawerItem({
required BuildContext context,
required IconData icon,
required String title,
required VoidCallback onTap,
@@ -86,7 +57,7 @@ class AppDrawer extends StatelessWidget {
);
}
Widget buildVersionInfo({
Widget _buildVersionInfo({
required BuildContext context,
required String fullVersion,
}) {
@@ -109,22 +80,19 @@ class AppDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
final appProvider = Provider.of<AppProvider>(context);
final appProvider = context.watch<AppProvider>();
return Drawer(
child: Column(
children: [
// 主要内容区域,可以滚动
Expanded(
child: ListView(
padding: EdgeInsets.zero,
children: [
// 抽屉头部
buildDrawerHeader(context),
_buildThemeSection(context, appProvider),
_buildDrawerHeader(),
ThemeLayout(),
const Divider(),
_buildDrawerItem(
context: context,
icon: Icons.help,
title: '帮助与反馈',
onTap: () {
@@ -135,14 +103,9 @@ class AppDrawer extends StatelessWidget {
],
),
),
Consumer<AppProvider>(
builder: (context, appProvider, child) {
return buildVersionInfo(
context: context,
fullVersion: appProvider.fullVersion,
);
},
_buildVersionInfo(
context: context,
fullVersion: appProvider.fullVersion,
),
],
),

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class AppFloatingButton extends StatelessWidget {
final VoidCallback onPressed;
const AppFloatingButton({super.key, required this.onPressed});
@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: onPressed,
backgroundColor: Colors.transparent,
elevation: 0,
shape: CircleBorder(),
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary,
),
child: Icon(Icons.add, color: Colors.white, size: 36),
),
);
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flisp_app/provider/app_provider.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class AppNavbar extends StatelessWidget {
AppNavbar({super.key});
final List<BottomNavigationBarItem> navItems = [
BottomNavigationBarItem(icon: Icon(Icons.flash_on), label: '闪灵'),
BottomNavigationBarItem(icon: Icon(Icons.checklist), label: '待办'),
BottomNavigationBarItem(icon: Icon(Icons.calendar_month), label: '日程'),
BottomNavigationBarItem(icon: Icon(Icons.insert_chart), label: '统计'),
];
@override
Widget build(BuildContext context) {
final appProvider = context.watch<AppProvider>();
final colors = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: colors.shadow.withAlpha(20),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: BottomNavigationBar(
currentIndex: appProvider.currentTab,
onTap: (index) => appProvider.changeTab(index),
type: BottomNavigationBarType.fixed,
backgroundColor: colors.surface,
selectedItemColor: colors.primary,
unselectedItemColor: colors.onSurface.withAlpha(120),
showSelectedLabels: true,
showUnselectedLabels: true,
items: navItems,
),
);
}
}

55
lib/layout/home.dart Normal file
View File

@@ -0,0 +1,55 @@
import 'package:flisp_app/layout/app_body.dart';
import 'package:flisp_app/layout/app_drawer.dart';
import 'package:flisp_app/layout/app_floating_button.dart';
import 'package:flisp_app/layout/app_navbar.dart';
import 'package:flisp_app/provider/app_provider.dart';
import 'package:flisp_app/utils/notify_utils.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
final GlobalKey<AppBodyState> _appBodyKey = GlobalKey();
final NotifyService notifyService = NotifyService();
void _onPressedFloatingButton(int index) {
_appBodyKey.currentState?.showPageAddDialog(index);
}
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final provider = context.watch<AppProvider>();
return Scaffold(
drawer: const AppDrawer(),
appBar: AppBar(
title: Text(provider.currentAppBarTitle),
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
elevation: 0,
),
body: Padding(
padding: EdgeInsets.all(10),
child: AppBody(key: _appBodyKey),
),
bottomNavigationBar: AppNavbar(),
floatingActionButton:
provider.currentTab == 3
? null
: AppFloatingButton(
onPressed: () => _onPressedFloatingButton(provider.currentTab),
),
);
}
}

View File

@@ -1,133 +0,0 @@
import 'package:flisp_app/layout/app_drawer.dart';
import 'package:flisp_app/pages/calendar_page.dart';
import 'package:flisp_app/pages/flisp_page.dart';
import 'package:flisp_app/pages/stats_page.dart';
import 'package:flisp_app/pages/todo_page.dart';
import 'package:flisp_app/provider/app_provider.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
final GlobalKey<FlispPageState> _flispPageKey = GlobalKey();
final GlobalKey<TodoPageState> _todoPageKey = GlobalKey();
final GlobalKey<CalendarPageState> _calendarPageKey = GlobalKey();
List<BottomNavigationBarItem> navItems = [
BottomNavigationBarItem(icon: Icon(Icons.flash_on), label: '闪灵'),
BottomNavigationBarItem(icon: Icon(Icons.checklist), label: '待办'),
BottomNavigationBarItem(
icon: Icon(Icons.calendar_month_rounded),
label: '日程',
),
BottomNavigationBarItem(icon: Icon(Icons.insert_chart), label: '统计'),
];
@override
Widget build(BuildContext context) {
return Consumer<AppProvider>(
builder: (context, appProvider, child) {
return Scaffold(
drawer: const AppDrawer(),
appBar: AppBar(
title: Text(_getAppBarTitle(appProvider.currentTab)),
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
elevation: 0,
),
body: _buildPage(appProvider.currentTab),
bottomNavigationBar: Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Theme.of(context).colorScheme.shadow.withAlpha(20),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: BottomNavigationBar(
currentIndex: appProvider.currentTab,
onTap: (index) => appProvider.changeTab(index),
type: BottomNavigationBarType.fixed,
backgroundColor: Theme.of(context).colorScheme.surface,
selectedItemColor: Theme.of(context).colorScheme.primary,
unselectedItemColor: Theme.of(
context,
).colorScheme.onSurface.withAlpha(120),
showSelectedLabels: true,
showUnselectedLabels: true,
items: navItems,
),
),
floatingActionButton: _buildFloatingButton(
context,
appProvider.currentTab,
),
);
},
);
}
void _onPressFloatingButton(int index) {
if (index == 0) {
if (_flispPageKey.currentState != null) {
_flispPageKey.currentState!.showAddDialog();
}
} else if (index == 1) {
if (_todoPageKey.currentState != null) {
_todoPageKey.currentState!.showAddDialog();
}
} else if (index == 2) {
if (_calendarPageKey.currentState != null) {
_calendarPageKey.currentState!.showAddDialog();
}
}
}
Widget? _buildFloatingButton(BuildContext context, int index) {
if (index == 3) return null;
return FloatingActionButton(
onPressed: () => _onPressFloatingButton(index),
backgroundColor: Colors.transparent,
elevation: 0,
shape: CircleBorder(),
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary,
),
child: Icon(Icons.add, color: Colors.white, size: 36),
),
);
}
Widget _buildPage(int index) {
switch (index) {
case 0:
return FlispPage(key: _flispPageKey);
case 1:
return TodoPage(key: _todoPageKey);
case 2:
return CalendarPage(key: _calendarPageKey);
case 3:
return StatsPage();
default:
return FlispPage(key: _flispPageKey);
}
}
String _getAppBarTitle(int index) {
final titles = {0: '闪灵', 1: '待办', 2: '日程', 3: '统计'};
return titles[index] ?? '闪灵';
}
}

View File

@@ -5,17 +5,25 @@ import 'package:flisp_app/provider/calendar_provider.dart';
import 'package:flisp_app/provider/flisp_provider.dart';
import 'package:flisp_app/utils/notify_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/flutter_common.dart';
import 'package:flutter_common/utils/log_utils.dart';
import 'package:flutter_common/utils/sp_utils.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:provider/provider.dart';
import 'package:syncfusion_localizations/syncfusion_localizations.dart';
import 'layout/main_screen.dart';
import 'layout/home.dart';
import 'models/todo.dart';
import 'provider/todo_provider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化存储
await SPUtil.init();
// 初始化日志
await initLogger();
// 初始化提醒服务
final notifyService = NotifyService();
await notifyService.initialize();
@@ -44,8 +52,18 @@ void main() async {
// calendarBox.clear();
// notifyService.cancelAllNotifications();
runApp(const MyApp());
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AppProvider()),
ChangeNotifierProvider(create: (_) => ThemeProvider()),
ChangeNotifierProvider(create: (_) => TodoProvider()),
ChangeNotifierProvider(create: (_) => FlispProvider()),
ChangeNotifierProvider(create: (_) => CalendarProvider()),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@@ -53,31 +71,21 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AppProvider()),
ChangeNotifierProvider(create: (_) => TodoProvider()),
ChangeNotifierProvider(create: (_) => FlispProvider()),
ChangeNotifierProvider(create: (_) => CalendarProvider()),
final themeProvider = context.watch<ThemeProvider>();
return MaterialApp(
title: '闪灵',
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
SfGlobalLocalizations.delegate,
],
child: Consumer<AppProvider>(
builder: (context, appProvider, child) {
return MaterialApp(
title: '闪灵',
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
SfGlobalLocalizations.delegate,
],
supportedLocales: [const Locale('zh'), const Locale('zh', 'CN')],
locale: Locale('zh', 'CN'),
theme: appProvider.currentThemeData,
home: const MainScreen(),
debugShowCheckedModeBanner: false,
);
},
),
supportedLocales: [const Locale('zh'), const Locale('zh', 'CN')],
locale: Locale('zh', 'CN'),
theme: themeProvider.currentThemeData,
home: const Home(),
debugShowCheckedModeBanner: false,
);
}
}

View File

@@ -111,3 +111,5 @@ enum TodoTab {
const TodoTab(this.label);
}
enum TodoSortMode { time, priority }

View File

@@ -2,10 +2,10 @@ import 'package:flisp_app/models/calendar.dart';
import 'package:flisp_app/provider/calendar_provider.dart';
import 'package:flisp_app/service/calendar_service.dart';
import 'package:flisp_app/utils/notify_utils.dart';
import 'package:flisp_app/widgets/awesome_dialog.dart';
import 'package:flisp_app/widgets/calendar_form.dart';
import 'package:flisp_app/widgets/calendar_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:provider/provider.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
@@ -107,9 +107,9 @@ class CalendarPageState extends State<CalendarPage> {
});
if (isSuccess) {
showSuccessDialog(context, isEditing ? '更新成功' : '添加成功');
showSuccessTip(context, isEditing ? '更新成功' : '添加成功');
} else {
showErrorDialog(context, isEditing ? '更新失败' : '添加失败');
showErrorTip(context, isEditing ? '更新失败' : '添加失败');
}
}
@@ -122,56 +122,57 @@ class CalendarPageState extends State<CalendarPage> {
});
if (isSuccess) {
showSuccessDialog(context, '删除成功');
showSuccessTip(context, '删除成功');
} else {
showErrorDialog(context, '删除失败');
showErrorTip(context, '删除失败');
}
}
Widget _buildCalendarTabs() {
return buildTabs(
context: context,
currentTab: _currentTab,
onTabChanged: (value) {
setState(() {
_currentTab = value;
_calendarController.view = value.view;
});
},
);
}
@override
Widget build(BuildContext context) {
final provider = Provider.of<CalendarProvider>(context, listen: false);
return Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
buildTabs(
return Column(
children: [
_buildCalendarTabs(),
Expanded(
child: buildCalendar(
context: context,
currentTab: _currentTab,
onTabChanged: (value) {
setState(() {
_currentTab = value;
_calendarController.view = value.view;
});
controller: _calendarController,
dataSource: AppointmentDataSource(_appointments),
onAdd: () {
if (_calendarController.view == CalendarView.week) {
_showDialog(false, provider.formItem);
}
},
onEdit: (appointment) {
final calendar = calendarService.getCalendarById(
appointment.id as int,
);
_showDialog(true, calendar);
},
onDelete: (appointment) {
final calendar = calendarService.getCalendarById(
appointment.id as int,
);
_deleteCalendar(calendar);
},
),
Expanded(
child: buildCalendar(
context: context,
controller: _calendarController,
dataSource: AppointmentDataSource(_appointments),
onAdd: () {
if (_calendarController.view == CalendarView.week) {
_showDialog(false, provider.formItem);
}
},
onEdit: (appointment) {
final calendar = calendarService.getCalendarById(
appointment.id as int,
);
_showDialog(true, calendar);
},
onDelete: (appointment) {
final calendar = calendarService.getCalendarById(
appointment.id as int,
);
_deleteCalendar(calendar);
},
),
),
],
),
),
],
);
}
}

View File

@@ -2,10 +2,10 @@ import 'package:flisp_app/models/flisp.dart';
import 'package:flisp_app/provider/flisp_provider.dart';
import 'package:flisp_app/service/flisp_service.dart';
import 'package:flisp_app/utils/flisp_utils.dart';
import 'package:flisp_app/widgets/awesome_dialog.dart';
import 'package:flisp_app/widgets/flisp_form.dart';
import 'package:flisp_app/widgets/flisp_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:provider/provider.dart';
import 'package:flisp_app/widgets/common.dart';
@@ -40,23 +40,23 @@ class FlispPageState extends State<FlispPage> {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
buildTabs(
context: context,
currentTab: _currentTab,
onTabChanged: (value) {
setState(() {
_currentTab = value;
});
},
),
// 待办事项列表
Expanded(child: _buildActiveFlispList(context)),
],
),
return Column(
children: [
_buildFlispTabs(),
Expanded(child: _buildActiveFlispList(context)),
],
);
}
Widget _buildFlispTabs() {
return buildTabs(
context: context,
currentTab: _currentTab,
onTabChanged: (value) {
setState(() {
_currentTab = value;
});
},
);
}
@@ -117,9 +117,9 @@ class FlispPageState extends State<FlispPage> {
});
if (isSuccess) {
showSuccessDialog(context, isEditing ? '更新成功' : '添加成功');
showSuccessTip(context, isEditing ? '更新成功' : '添加成功');
} else {
showErrorDialog(context, isEditing ? '更新失败' : '添加失败');
showErrorTip(context, isEditing ? '更新失败' : '添加失败');
}
}
@@ -131,9 +131,9 @@ class FlispPageState extends State<FlispPage> {
});
if (isSuccess) {
showSuccessDialog(context, '删除成功');
showSuccessTip(context, '删除成功');
} else {
showErrorDialog(context, '删除失败');
showErrorTip(context, '删除失败');
}
}
}

View File

@@ -2,13 +2,13 @@ import 'package:flisp_app/models/flisp.dart';
import 'package:flisp_app/models/todo.dart';
import 'package:flisp_app/service/flisp_service.dart';
import 'package:flisp_app/service/todo_service.dart';
import 'package:flisp_app/widgets/chart.dart';
import 'package:flisp_app/widgets/common.dart';
import 'package:flisp_app/widgets/flisp_widget.dart';
import 'package:flisp_app/widgets/stats.dart';
import 'package:flisp_app/widgets/todo_widget.dart';
import 'package:flisp_app/widgets/year_selector.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/models/common_model.dart';
import 'package:flutter_common/widget/chart.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:flutter_common/widget/year_selector.dart';
class StatsPage extends StatefulWidget {
const StatsPage({super.key});
@@ -124,45 +124,7 @@ class StatsPageState extends State<StatsPage> {
],
),
const SizedBox(height: 5),
Expanded(child: buildFlispStatsCard(allFlisps)),
],
);
}
Widget _buildFlispMonthlyStats(BuildContext context) {
return Column(
children: [
buildChartTitle(context, '每月闪灵数量统计', Icons.show_chart),
const SizedBox(height: 3),
buildChartDivider(context),
const SizedBox(height: 3),
Expanded(
child: lineChart(
context: context,
xAxisName: '月份',
yAxisName: '数量',
unit: '',
data: flispMonthlyStats,
),
),
],
);
}
Widget _buildFlispCategoryStats(BuildContext context) {
return Column(
children: [
buildChartTitle(context, '闪灵分类统计', Icons.pie_chart),
const SizedBox(height: 3),
buildChartDivider(context),
const SizedBox(height: 3),
Expanded(
child: pieChart(
context: context,
unit: '',
data: flispCategoryStats,
),
),
buildFlispStatsCard(context, allFlisps),
],
);
}
@@ -179,44 +141,7 @@ class StatsPageState extends State<StatsPage> {
],
),
const SizedBox(height: 5),
Expanded(child: buildTodoStatsCard(allTodos)),
],
);
}
Widget _buildTodoMonthlyStats(BuildContext context) {
return Column(
children: [
buildChartTitle(context, '每月待办数量统计', Icons.bar_chart),
const SizedBox(height: 3),
buildChartDivider(context),
const SizedBox(height: 3),
Expanded(
child: doubleBarChart(
context: context,
xAxisName: '月份',
yAxisName: '数量',
unit: '',
data1: todoMonthlyStats,
data2: todoMonthlyCompleteStats,
series1Name: '总数',
series2Name: '完成数',
),
),
],
);
}
Widget _buildTodoPriorityStats(BuildContext context) {
return Column(
children: [
buildChartTitle(context, '待办分类统计', Icons.pie_chart),
const SizedBox(height: 3),
buildChartDivider(context),
const SizedBox(height: 3),
Expanded(
child: pieChart(context: context, unit: '', data: todoPriorityStats),
),
Expanded(child: buildTodoStatsCard(context, allTodos)),
],
);
}
@@ -224,52 +149,73 @@ class StatsPageState extends State<StatsPage> {
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
YearSelector(
initialYear: DateTime.now().year,
minYear: 2000,
maxYear: 2100,
onYearChanged: (year) => {
setState(() {
currentYear = year;
refreshStats();
})
},
child: Column(
children: [
YearSelector(
currentYear: DateTime.now().year,
onYearChanged:
(year) => {
setState(() {
currentYear = year;
refreshStats();
}),
},
),
SizedBox(height: statsHeight, child: _buildFlispStats(context)),
SizedBox(height: 10),
SizedBox(height: statsHeight, child: _buildTodoStats(context)),
SizedBox(height: 10),
SizedBox(
height: chartHeight,
child: CommonCard(
child: LineChart(
title: '每月闪灵数量统计',
xAxisName: '月份',
yAxisName: '数量',
unit: '',
data: flispMonthlyStats,
),
),
SizedBox(
height: statsHeight,
child: _buildFlispStats(context),
),
SizedBox(height: 10),
SizedBox(
height: chartHeight,
child: CommonCard(
child: PieChart(
title: '闪灵分类统计',
unit: '',
data: flispCategoryStats,
),
),
SizedBox(height: 10),
SizedBox(
height: statsHeight,
child: _buildTodoStats(context),
),
SizedBox(height: 10),
SizedBox(
height: chartHeight,
child: CommonCard(
child: DoubleBarChart(
title: '每月待办数量统计',
xAxisName: '月份',
yAxisName: '数量',
unit: '',
data1: todoMonthlyStats,
data2: todoMonthlyCompleteStats,
series1Name: '总数',
series2Name: '完成数',
),
),
SizedBox(height: 10),
SizedBox(
height: chartHeight,
child: BuildCard(child: _buildFlispMonthlyStats(context)),
),
SizedBox(height: 10),
SizedBox(
height: chartHeight,
child: CommonCard(
child: PieChart(
title: '待办分类统计',
unit: '',
data: todoPriorityStats,
),
),
SizedBox(height: 10),
SizedBox(
height: chartHeight,
child: BuildCard(child: _buildFlispCategoryStats(context)),
),
SizedBox(height: 10),
SizedBox(
height: chartHeight,
child: BuildCard(child: _buildTodoMonthlyStats(context)),
),
SizedBox(height: 10),
SizedBox(
height: chartHeight,
child: BuildCard(child: _buildTodoPriorityStats(context)),
),
],
),
),
],
),
);
}

View File

@@ -1,9 +1,9 @@
import 'package:flisp_app/provider/todo_provider.dart';
import 'package:flisp_app/service/todo_service.dart';
import 'package:flisp_app/utils/notify_utils.dart';
import 'package:flisp_app/widgets/awesome_dialog.dart';
import 'package:flisp_app/widgets/todo_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:provider/provider.dart';
import 'package:flisp_app/models/todo.dart';
@@ -26,6 +26,7 @@ class TodoPageState extends State<TodoPage> {
late List<Todo> _todos;
TodoTab _currentTab = TodoTab.active;
late TodoSortMode _selectedMode = TodoSortMode.time;
// 获取过滤后的待办事项
List<Todo> get _activeTodos => getActiveTodos(_currentTab, _todos);
@@ -37,28 +38,66 @@ class TodoPageState extends State<TodoPage> {
@override
void initState() {
super.initState();
_todos = todoService.getAllTodos();
_refreshTodos();
}
void _refreshTodos() {
setState(() {
_todos = todoService.getAllTodos(_selectedMode);
});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(10),
child: Column(
children: [
buildTabs(
context: context,
currentTab: _currentTab,
onTabChanged: (value) {
setState(() {
_currentTab = value;
});
},
),
// 待办事项列表
Expanded(child: _buildActiveTodoList(context)),
],
),
return Column(
children: [
_buildTodoTabs(),
_buildTodoSegment(),
SizedBox(height: 6),
Expanded(child: _buildActiveTodoList(context)),
],
);
}
Widget _buildTodoTabs() {
return buildTabs(
context: context,
currentTab: _currentTab,
onTabChanged: (value) {
setState(() {
_currentTab = value;
});
},
);
}
Widget _buildTodoSegment() {
final colors = Theme.of(context).colorScheme;
return SegmentedButton<TodoSortMode>(
showSelectedIcon: false,
emptySelectionAllowed: false,
multiSelectionEnabled: false,
segments: [
ButtonSegment<TodoSortMode>(
value: TodoSortMode.time,
icon: Icon(Icons.access_time_filled, size: 16),
label: const Text('时间', style: TextStyle(fontSize: 12)),
),
ButtonSegment<TodoSortMode>(
value: TodoSortMode.priority,
icon: Icon(Icons.flag_outlined, size: 16),
label: const Text('优先级', style: TextStyle(fontSize: 12)),
),
],
selected: {_selectedMode},
onSelectionChanged: (Set<TodoSortMode> newSelection) {
setState(() {
_selectedMode = newSelection.first;
});
_refreshTodos();
},
style: buildSegmentStyle(colors),
);
}
@@ -84,13 +123,16 @@ class TodoPageState extends State<TodoPage> {
// 切换待办事项完成状态
void _toggleTodo(Todo todo) async {
await notifyService.cancelNotification(todo.id);
final bool? result = await showConfirmDialog(context, '确定已完成该待办吗?');
if (result == true) {
await notifyService.cancelNotification(todo.id);
setState(() {
todo.isCompleted = !todo.isCompleted;
todo.scheduledTime = null;
todoService.updateTodo(todo);
});
setState(() {
todo.isCompleted = !todo.isCompleted;
todo.scheduledTime = null;
todoService.updateTodo(todo);
});
}
}
// 显示对话框
@@ -142,14 +184,12 @@ class TodoPageState extends State<TodoPage> {
);
}
setState(() {
_todos = todoService.getAllTodos();
});
_refreshTodos();
if (isSuccess) {
showSuccessDialog(context, isEditing ? '更新成功' : '添加成功');
showSuccessTip(context, isEditing ? '更新成功' : '添加成功');
} else {
showErrorDialog(context, isEditing ? '更新失败' : '添加失败');
showErrorTip(context, isEditing ? '更新失败' : '添加失败');
}
}
@@ -157,14 +197,12 @@ class TodoPageState extends State<TodoPage> {
await notifyService.cancelNotification(todo.id);
bool isSuccess = await todoService.deleteTodo(todo);
setState(() {
_todos = todoService.getAllTodos();
});
_refreshTodos();
if (isSuccess) {
showSuccessDialog(context, '删除成功');
showSuccessTip(context, '删除成功');
} else {
showErrorDialog(context, '删除失败');
showErrorTip(context, '删除失败');
}
}
}

View File

@@ -1,107 +1,19 @@
import 'package:flisp_app/utils/theme_utils.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AppProvider with ChangeNotifier {
static const String _themeKey = 'selected_theme';
static const String _darkModeKey = 'is_dark_mode';
static const List<String> appBarTitles = ['闪灵', '待办', '日程', '统计'];
int _currentTab = 0;
bool _isDarkMode = false;
ThemeColor _currentTheme = defaultThemes[0];
String _appVersion = '1.0.0';
String _buildNumber = '1';
late SharedPreferences _prefs;
bool _isInitialized = false;
AppProvider() {
_initPreferences();
}
// 初始化 SharedPreferences
Future<void> _initPreferences() async {
_prefs = await SharedPreferences.getInstance();
_loadPreferences();
await _getVersionInfo();
_isInitialized = true;
notifyListeners();
}
// 加载存储的设置
void _loadPreferences() {
_isDarkMode = _prefs.getBool(_darkModeKey) ?? false;
// 加载主题色
final themeName = _prefs.getString(_themeKey);
if (themeName != null) {
_currentTheme = getThemeColor(themeName);
}
}
Future<void> _getVersionInfo() async {
try {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
_appVersion = packageInfo.version;
_buildNumber = packageInfo.buildNumber;
notifyListeners();
} catch (e) {
print('获取版本信息失败: $e');
}
}
// 保存主题色
Future<void> _saveTheme() async {
await _prefs.setString(_themeKey, _currentTheme.name);
}
// 保存暗黑模式
Future<void> _saveDarkMode() async {
await _prefs.setBool(_darkModeKey, _isDarkMode);
}
int get currentTab => _currentTab;
bool get isDarkMode => _isDarkMode;
ThemeColor get currentTheme => _currentTheme;
List<ThemeColor> get availableThemes => defaultThemes;
bool get isInitialized => _isInitialized;
String get fullVersion => '$_appVersion+$_buildNumber';
int currentTab = 0;
String currentAppBarTitle = '闪灵';
String appVersion = '1.0.0';
String buildNumber = '1';
String get fullVersion => '$appVersion+$buildNumber';
void changeTab(int index) {
_currentTab = index;
currentTab = index;
currentAppBarTitle = appBarTitles[index];
notifyListeners();
}
// 切换明暗模式
void toggleDarkMode(bool value) {
_isDarkMode = value;
_saveDarkMode();
notifyListeners();
}
// 更改主题色
void changeTheme(ThemeColor theme) {
_currentTheme = theme;
_saveTheme();
notifyListeners();
}
ThemeData get currentThemeData {
return ThemeData(
primarySwatch: _currentTheme.materialColor,
colorScheme: ColorScheme.fromSeed(
seedColor: _currentTheme.primaryColor,
brightness: _isDarkMode ? Brightness.dark : Brightness.light,
),
useMaterial3: true,
fontFamily: 'CustomFont',
);
}
}

View File

@@ -15,9 +15,29 @@ class TodoService {
}
}
List<Todo> getAllTodos() {
return box.values.toList()
..sort((a, b) => b.updateTime.compareTo(a.updateTime));
List<Todo> getAllTodos(TodoSortMode mode) {
if (mode == TodoSortMode.time) {
return box.values.toList()
..sort((a, b) {
// 先按是否有 dueDate 分组:有时间的排前面
if (a.dueDate != null && b.dueDate == null) {
return -1; // a有时间b没有时间a排在前面
} else if (a.dueDate == null && b.dueDate != null) {
return 1; // a没有时间b有时间b排在前面
}
// 都有 dueDate按 dueDate 排序
else if (a.dueDate != null && b.dueDate != null) {
return a.dueDate!.compareTo(b.dueDate!); // 最近的在前面
}
// 都没有 dueDate按 updateTime 排序
else {
return b.updateTime.compareTo(a.updateTime); // 最近的在前面
}
});
} else {
return box.values.toList()
..sort((a, b) => b.priorityIndex.compareTo(a.priorityIndex));
}
}
List<Todo> getAllTodosByYear(int year) {

View File

@@ -1,9 +0,0 @@
import 'package:intl/intl.dart';
String formatDate(DateTime date) {
return DateFormat('yyyy-MM-dd').format(date);
}
String formatTime(DateTime datetime) {
return DateFormat('yyyy-MM-dd HH:mm:ss').format(datetime);
}

View File

@@ -1,57 +0,0 @@
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:file_picker/file_picker.dart';
import 'package:minio/io.dart';
import 'package:minio/minio.dart';
class MinIOHelper {
static final MinIOHelper _instance = MinIOHelper._internal();
factory MinIOHelper() => _instance;
final String rustfsIp = '14.103.235.151';
final String rustfsFileUrl = 'http://14.103.235.151:9100';
final String bucketName = 'flisp';
MinIOHelper._internal() {
_minio = Minio(
endPoint: rustfsIp,
port: 9100,
accessKey: "tHSFfcDW8qpCzKa2Xg6Y",
secretKey: "oq79EeYJ4jdczRp2IHUMCnbKtSw58NgDlG3sOkvX",
useSSL: false,
);
}
late Minio _minio;
Future<String> uploadFile({
required PlatformFile file,
Function(double)? onProgress,
}) async {
try {
String hashName = await _generateMD5HashName(file.path!);
String fileName = '$hashName${_getFileExtension(file.name)}';
await _minio.fPutObject(bucketName, fileName, file.path!);
return fileName;
} catch (e) {
throw Exception('文件上传失败: $e');
}
}
String _getFileExtension(String fileName) {
if (fileName.contains('.')) {
return '.${fileName.split('.').last.toLowerCase()}';
}
return '';
}
Future<String> _generateMD5HashName(String filePath) async {
final file = File(filePath);
final bytes = await file.readAsBytes();
final hash = md5.convert(bytes);
return hash.toString();
}
}

View File

@@ -1,3 +1,10 @@
import 'package:flisp_app/models/calendar.dart';
import 'package:flisp_app/models/todo.dart';
import 'package:flisp_app/service/calendar_service.dart';
import 'package:flisp_app/service/todo_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/date_utils.dart';
import 'package:flutter_common/utils/log_utils.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
@@ -11,9 +18,6 @@ class NotifyService {
late FlutterLocalNotificationsPlugin _notifications;
// 使用绝对时间(而不是相对时间),避免时区转换问题。
final dateInterpretation = UILocalNotificationDateInterpretation.absoluteTime;
// Android特殊模式即使设备处于省电模式也能准时触发。
final scheduleMode = AndroidScheduleMode.exactAllowWhileIdle;
@@ -23,7 +27,6 @@ class NotifyService {
// 初始化时区
tz.initializeTimeZones();
// String timeZoneName = await FlutterNativeTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation("Asia/Shanghai"));
// 设置Android平台的初始化配置 使用应用图标作为通知图标
@@ -45,6 +48,57 @@ class NotifyService {
);
await _notifications.initialize(settings);
WidgetsBinding.instance.addPostFrameCallback((_) {
_refreshScheduled();
});
}
/// 重新刷新提醒 防止每次重启设备后提醒丢失的问题
void _refreshScheduled() async {
logger.i('开始刷新提醒');
await cancelAllNotifications();
TodoService todoService = TodoService();
final List<Todo> todoResult = todoService.getAllTodos(
TodoSortMode.priority,
);
final List<Todo> todayTodos =
todoResult
.where(
(todo) => !todo.isCompleted && isAfterToday(todo.scheduledTime),
)
.toList();
for (Todo todo in todayTodos) {
await scheduleNotification(
id: todo.id,
title: '待办提醒',
body: todo.title,
scheduledTime: todo.scheduledTime!,
);
}
logger.i('刷新 ${todayTodos.length}个 待办提醒');
CalendarService calendarService = CalendarService();
final List<Calendar> calendarResult = calendarService.getAllCalendars();
final List<Calendar> todayCalendars =
calendarResult
.where((item) => isAfterToday(item.scheduledTime))
.toList();
for (Calendar calendar in todayCalendars) {
await scheduleNotification(
id: calendar.id,
title: '日程提醒',
body: calendar.title,
scheduledTime: calendar.scheduledTime!,
);
}
logger.i('刷新 ${todayCalendars.length}个 日程提醒');
logger.i('结束刷新提醒');
}
// 创建Android通知详情
@@ -101,9 +155,7 @@ class NotifyService {
body,
tz.TZDateTime.from(scheduledTime, tz.local),
_details,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
androidScheduleMode: scheduleMode,
);
}
@@ -111,8 +163,8 @@ class NotifyService {
Future<void> cancelNotification(int id) async {
try {
await _notifications.cancel(id);
} catch(e) {
print('取消通知失败: $e');
} catch (e) {
logger.i('取消通知失败: $e');
}
}

View File

@@ -1,236 +0,0 @@
import 'dart:ui';
import 'package:flisp_app/provider/app_provider.dart';
import 'package:flutter/material.dart';
class ThemeColor {
final String name;
final Color primaryColor;
final MaterialColor materialColor;
ThemeColor({
required this.name,
required this.primaryColor,
required this.materialColor,
});
}
final List<ThemeColor> defaultThemes = [
ThemeColor(
name: '科技蓝',
primaryColor: Color(0xFF2563EB),
materialColor: MaterialColor(0xFF2563EB, {
50: Color(0xFFDBEAFE),
100: Color(0xFFBFDBFE),
200: Color(0xFF93C5FD),
300: Color(0xFF60A5FA),
400: Color(0xFF3B82F6),
500: Color(0xFF2563EB),
600: Color(0xFF1D4ED8),
700: Color(0xFF1E40AF),
800: Color(0xFF1E3A8A),
900: Color(0xFF1E3A8A),
}),
),
ThemeColor(
name: '翡翠绿',
primaryColor: Color(0xFF10B981),
materialColor: MaterialColor(0xFF10B981, {
50: Color(0xFFECFDF5),
100: Color(0xFFD1FAE5),
200: Color(0xFFA7F3D0),
300: Color(0xFF6EE7B7),
400: Color(0xFF34D399),
500: Color(0xFF10B981),
600: Color(0xFF059669),
700: Color(0xFF047857),
800: Color(0xFF065F46),
900: Color(0xFF064E3B),
}),
),
ThemeColor(
name: '活力橙',
primaryColor: Color(0xFFF59E0B),
materialColor: MaterialColor(0xFFF59E0B, {
50: Color(0xFFFFFBEB),
100: Color(0xFFFEF3C7),
200: Color(0xFFFDE68A),
300: Color(0xFFFCD34D),
400: Color(0xFFFBBF24),
500: Color(0xFFF59E0B),
600: Color(0xFFD97706),
700: Color(0xFFB45309),
800: Color(0xFF92400E),
900: Color(0xFF78350F),
}),
),
ThemeColor(
name: '梦幻紫',
primaryColor: Color(0xFF8B5CF6),
materialColor: MaterialColor(0xFF8B5CF6, {
50: Color(0xFFF5F3FF),
100: Color(0xFFEDE9FE),
200: Color(0xFFDDD6FE),
300: Color(0xFFC4B5FD),
400: Color(0xFFA78BFA),
500: Color(0xFF8B5CF6),
600: Color(0xFF7C3AED),
700: Color(0xFF6D28D9),
800: Color(0xFF5B21B6),
900: Color(0xFF4C1D95),
}),
),
ThemeColor(
name: '浪漫粉',
primaryColor: Color(0xFFEC4899),
materialColor: MaterialColor(0xFFEC4899, {
50: Color(0xFFFDF2F8),
100: Color(0xFFFCE7F3),
200: Color(0xFFFBCFE8),
300: Color(0xFFF9A8D4),
400: Color(0xFFF472B6),
500: Color(0xFFEC4899),
600: Color(0xFFDB2777),
700: Color(0xFFBE185D),
800: Color(0xFF9D174D),
900: Color(0xFF831843),
}),
),
ThemeColor(
name: '清新青',
primaryColor: Color(0xFF06B6D4),
materialColor: MaterialColor(0xFF06B6D4, {
50: Color(0xFFF0FDFA),
100: Color(0xFFCCFBF1),
200: Color(0xFF99F6E4),
300: Color(0xFF5EEAD4),
400: Color(0xFF2DD4BF),
500: Color(0xFF06B6D4),
600: Color(0xFF0891B2),
700: Color(0xFF0E7490),
800: Color(0xFF155E75),
900: Color(0xFF164E63),
}),
),
ThemeColor(
name: '深空蓝',
primaryColor: Color(0xFF1E40AF),
materialColor: MaterialColor(0xFF1E40AF, {
50: Color(0xFFEFF6FF),
100: Color(0xFFDBEAFE),
200: Color(0xFFBFDBFE),
300: Color(0xFF93C5FD),
400: Color(0xFF60A5FA),
500: Color(0xFF3B82F6),
600: Color(0xFF2563EB),
700: Color(0xFF1D4ED8),
800: Color(0xFF1E40AF),
900: Color(0xFF1E3A8A),
}),
),
ThemeColor(
name: '落日红',
primaryColor: Color(0xFFEF4444),
materialColor: MaterialColor(0xFFEF4444, {
50: Color(0xFFFEF2F2),
100: Color(0xFFFEE2E2),
200: Color(0xFFFECACA),
300: Color(0xFFFCA5A5),
400: Color(0xFFF87171),
500: Color(0xFFEF4444),
600: Color(0xFFDC2626),
700: Color(0xFFB91C1C),
800: Color(0xFF991B1B),
900: Color(0xFF7F1D1D),
}),
),
];
ThemeColor getThemeColor(String themeName) {
return defaultThemes.firstWhere(
(theme) => theme.name == themeName,
orElse: () => defaultThemes[0],
);
}
Widget buildThemeColorList(BuildContext context, AppProvider appProvider) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 1.2,
),
itemCount: appProvider.availableThemes.length,
itemBuilder: (context, index) {
final themeColor = appProvider.availableThemes[index];
final isSelected = appProvider.currentTheme == themeColor;
return _buildThemeColorItem(
themeColor: themeColor,
isSelected: isSelected,
onTap: () => appProvider.changeTheme(themeColor),
);
},
),
);
}
Widget _buildThemeColorItem({
required ThemeColor themeColor,
required bool isSelected,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color:
isSelected
? themeColor.primaryColor.withAlpha(50)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? themeColor.primaryColor : Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 颜色圆点
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: themeColor.primaryColor,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(10),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
),
const SizedBox(height: 4),
Text(
themeColor.name,
style: TextStyle(
fontSize: 10,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color:
isSelected ? themeColor.primaryColor : Colors.grey.shade600,
),
),
],
),
),
);
}

View File

@@ -1,48 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
class ToastUtil {
static void success(String message) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 16.0,
);
}
static void error(String message) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.red,
textColor: Colors.white,
fontSize: 16.0,
);
}
static void warning(String message) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.orange,
textColor: Colors.white,
fontSize: 16.0,
);
}
static void info(String message) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.TOP,
backgroundColor: Colors.blue,
textColor: Colors.white,
fontSize: 16.0,
);
}
}

View File

@@ -25,16 +25,23 @@ List<Todo> getActiveTodos(TodoTab currentTab, List<Todo> todos) {
// 格式化截止日期
String formatDueDate(DateTime dueDate, bool isToday, bool isTomorrow) {
if (isToday) return '今天截止';
if (isTomorrow) return '明天截止';
final now = DateTime.now();
final difference = dueDate.difference(DateTime(now.year, now.month, now.day));
final today = DateTime(now.year, now.month, now.day);
final dueDateStart = DateTime(dueDate.year, dueDate.month, dueDate.day);
final difference = dueDateStart.difference(today);
if (difference.inDays < 7) {
return '${difference.inDays}天后截止';
// 处理过期情况
if (difference.inDays < 0) {
final daysPast = -difference.inDays;
return '已过期$daysPast天';
}
// 处理未来日期
if (difference.inDays == 0) return '今天截止';
if (difference.inDays == 1) return '明天截止';
if (difference.inDays == 2) return '后天截止';
if (difference.inDays < 7) return '${difference.inDays}天后截止';
return DateFormat("MM-dd").format(dueDate);
}

View File

@@ -1,68 +0,0 @@
import 'package:awesome_dialog/awesome_dialog.dart';
import 'package:flutter/material.dart';
void showAwesomeDialog({
required BuildContext context,
required Widget body,
required VoidCallback onOk,
required VoidCallback onCancel,
}) {
AwesomeDialog(
context: context,
dialogType: DialogType.noHeader,
animType: AnimType.scale,
body: body,
dialogBackgroundColor: Theme.of(context).colorScheme.surface,
btnOkText: "确认",
btnCancelText: "取消",
btnOkColor: Colors.orange,
btnCancelColor: Colors.grey,
buttonsBorderRadius: BorderRadius.circular(10),
headerAnimationLoop: false,
dismissOnTouchOutside: false,
dismissOnBackKeyPress: true,
btnOk: ElevatedButton(
onPressed: onOk,
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 6),
textStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
child: Text("确认"),
),
btnCancelOnPress: onCancel,
).show();
}
// 显示成功提示
void showSuccessDialog(BuildContext context, String message) {
AwesomeDialog(
context: context,
dialogType: DialogType.success,
animType: AnimType.scale,
title: message,
btnOkText: "好的",
btnOkColor: Colors.green,
btnOkOnPress: () {},
autoHide: Duration(seconds: 2),
).show();
}
// 显示失败提示
void showErrorDialog(BuildContext context, String message) {
AwesomeDialog(
context: context,
dialogType: DialogType.error,
animType: AnimType.scale,
title: message,
btnOkText: "好的",
btnOkColor: Colors.red,
btnOkOnPress: () {},
autoHide: Duration(seconds: 2),
).show();
}

View File

@@ -1,6 +1,5 @@
import 'package:flisp_app/models/calendar.dart';
import 'package:flisp_app/provider/calendar_provider.dart';
import 'package:flisp_app/utils/date_utils.dart';
import 'package:flisp_app/widgets/common.dart';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
@@ -24,267 +23,269 @@ class CalendarForm extends StatefulWidget {
}
class _CalendarFormState extends State<CalendarForm> {
final int maxTitleCount = 10;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final int maxTitleCount = 10;
final provider = Provider.of<CalendarProvider>(context);
Widget buildTitle() {
final colors = Theme.of(context).colorScheme;
Widget buildTitle() {
return Row(
children: [
Icon(
widget.isEditing ? Icons.edit_note : Icons.add_task,
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
widget.isEditing ? Icons.edit_note : Icons.add_task,
color: colors.primary,
size: 24,
),
SizedBox(width: 8),
Text(
widget.isEditing ? '编辑日程事项' : '添加日程事项',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: colors.primary,
size: 24,
),
SizedBox(width: 8),
Text(
widget.isEditing ? '编辑日程事项' : '添加日程事项',
),
],
);
}
String? titleFieldValidator(value) {
if (value == null || value.isEmpty) {
return '请输入标题';
}
if (value.length > maxTitleCount) {
return '标题不能超过$maxTitleCount个字符';
}
return null;
}
FormBuilderTextField buildTitleField(CalendarProvider provider) {
final colors = Theme.of(context).colorScheme;
return FormBuilderTextField(
name: 'title',
initialValue: provider.formItem.title,
onChanged: (value) {
setState(() {
provider.formItem.title = value ?? '';
});
},
decoration: InputDecoration(
label: RichText(
text: TextSpan(
text: '内容',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: colors.primary,
color: Colors.grey,
fontSize: 16,
fontFamily: 'CustomFont',
),
),
],
);
}
String? titleFieldValidator(value) {
if (value == null || value.isEmpty) {
return '请输入标题';
}
if (value.length > maxTitleCount) {
return '标题不能超过$maxTitleCount个字符';
}
return null;
}
FormBuilderTextField buildTitleField() {
return FormBuilderTextField(
name: 'title',
initialValue: provider.formItem.title,
onChanged: (value) {
setState(() {
provider.formItem.title = value ?? '';
});
},
decoration: InputDecoration(
label: RichText(
text: TextSpan(
text: '内容',
style: TextStyle(color: Colors.grey.shade700, fontSize: 16),
children: const [
TextSpan(
text: '*',
style: TextStyle(
color: Colors.red,
fontSize: 18,
fontWeight: FontWeight.bold,
),
children: const [
TextSpan(
text: '*',
style: TextStyle(
color: Colors.red,
fontSize: 18,
fontWeight: FontWeight.bold,
),
],
),
),
],
),
hintText: '请输入日程标题...',
hintStyle: TextStyle(color: Colors.grey),
counterText: '',
suffixText: '${provider.formItem.title.length}/$maxTitleCount',
border: buildFormBoard(),
enabledBorder: buildFormEnabledBoard(),
focusedBorder: buildFormFocusedBoard(colors),
filled: true,
fillColor: colors.surface,
prefixIcon: Icon(Icons.title, color: Colors.blue),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
maxLength: maxTitleCount,
validator: (value) => titleFieldValidator(value),
);
hintText: '请输入日程标题...',
hintStyle: TextStyle(color: Colors.grey),
counterText: '',
suffixText: '${provider.formItem.title.length}/$maxTitleCount',
border: buildFormBoard(),
enabledBorder: buildFormEnabledBoard(),
focusedBorder: buildFormFocusedBoard(colors),
filled: true,
fillColor: colors.surface,
prefixIcon: Icon(Icons.title, color: Colors.blue),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
maxLength: maxTitleCount,
validator: (value) => titleFieldValidator(value),
);
}
String? startTimeFieldValidator(CalendarProvider provider, value) {
if (value == null) {
return '请选择开始时间';
}
if (value.isBefore(DateTime.now())) {
return '不能选择过去的日期';
}
String? startTimeFieldValidator(value) {
if (value == null) {
return '请选择开始时间';
}
if (value.isBefore(DateTime.now())) {
return '不能选择过去的日期';
}
final endTime = provider.formItem.endTime;
if (endTime != null && endTime.isBefore(value)) {
return '结束时间不能早于开始时间';
}
if (endTime != null && endTime.isAtSameMomentAs(value)) {
return '开始时间不能等于结束时间';
}
return null;
final endTime = provider.formItem.endTime;
if (endTime != null && endTime.isBefore(value)) {
return '结束时间不能早于开始时间';
}
if (endTime != null && endTime.isAtSameMomentAs(value)) {
return '开始时间不能等于结束时间';
}
FormBuilderDateTimePicker buildStartTimeField() {
var startTime = provider.formItem.startTime;
return null;
}
return FormBuilderDateTimePicker(
name: 'startTime',
format: DateFormat('yyyy-MM-dd HH:mm:ss'),
initialValue: startTime,
inputType: InputType.both,
onChanged: (value) {
setState(() {
provider.formItem.startTime = value!;
});
},
decoration: InputDecoration(
labelText:
startTime == null ? '选择开始时间' : '开始时间: ${formatTime(startTime)}',
labelStyle: TextStyle(
color: startTime == null ? Colors.grey : Colors.black87,
),
prefixIcon: Icon(Icons.calendar_month, color: Colors.purple),
border: buildFormBoard(),
enabledBorder: buildFormEnabledBoard(),
focusedBorder: buildFormFocusedBoard(colors),
filled: true,
fillColor: colors.surface,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
validator: (value) => startTimeFieldValidator(value),
);
FormBuilderDateTimePicker buildStartTimeField(CalendarProvider provider) {
final colors = Theme.of(context).colorScheme;
var startTime = provider.formItem.startTime;
return FormBuilderDateTimePicker(
name: 'startTime',
format: DateFormat('yyyy-MM-dd HH:mm:ss'),
initialValue: startTime,
inputType: InputType.both,
onChanged: (value) {
setState(() {
provider.formItem.startTime = value!;
});
},
decoration: InputDecoration(
labelText: startTime == null ? '选择开始时间' : '开始时间: ',
labelStyle: TextStyle(color: Colors.grey),
prefixIcon: Icon(Icons.calendar_month, color: Colors.purple),
border: buildFormBoard(),
enabledBorder: buildFormEnabledBoard(),
focusedBorder: buildFormFocusedBoard(colors),
filled: true,
fillColor: colors.surface,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
validator: (value) => startTimeFieldValidator(provider, value),
);
}
String? endTimeFieldValidator(CalendarProvider provider, value) {
if (value == null) {
return '请选择结束时间';
}
if (value.isBefore(DateTime.now())) {
return '不能选择过去的日期';
}
String? endTimeFieldValidator(value) {
if (value == null) {
return '请选择结束时间';
}
if (value.isBefore(DateTime.now())) {
return '不能选择过去的日期';
}
final startTime = provider.formItem.startTime;
if (startTime != null && value.isBefore(startTime)) {
return '结束时间不能早于开始时间';
}
if (startTime != null && startTime.isAtSameMomentAs(value)) {
return '开始时间不能等于结束时间';
}
return null;
final startTime = provider.formItem.startTime;
if (startTime != null && value.isBefore(startTime)) {
return '结束时间不能早于开始时间';
}
if (startTime != null && startTime.isAtSameMomentAs(value)) {
return '开始时间不能等于结束时间';
}
FormBuilderDateTimePicker buildEndTimeField() {
var endTime = provider.formItem.endTime;
return null;
}
return FormBuilderDateTimePicker(
name: 'endTime',
format: DateFormat('yyyy-MM-dd HH:mm:ss'),
initialValue: endTime,
inputType: InputType.both,
onChanged: (value) {
setState(() {
provider.formItem.endTime = value!;
});
},
decoration: InputDecoration(
labelText:
endTime == null ? '选择结束时间' : '结束时间: ${formatTime(endTime)}',
labelStyle: TextStyle(
color: endTime == null ? Colors.grey : Colors.black87,
),
prefixIcon: Icon(Icons.calendar_month, color: Colors.purple),
border: buildFormBoard(),
enabledBorder: buildFormEnabledBoard(),
focusedBorder: buildFormFocusedBoard(colors),
filled: true,
fillColor: colors.surface,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
validator: (value) => endTimeFieldValidator(value),
);
FormBuilderDateTimePicker buildEndTimeField(CalendarProvider provider) {
final colors = Theme.of(context).colorScheme;
var endTime = provider.formItem.endTime;
return FormBuilderDateTimePicker(
name: 'endTime',
format: DateFormat('yyyy-MM-dd HH:mm:ss'),
initialValue: endTime,
inputType: InputType.both,
onChanged: (value) {
setState(() {
provider.formItem.endTime = value!;
});
},
decoration: InputDecoration(
labelText: endTime == null ? '选择结束时间' : '结束时间: ',
labelStyle: TextStyle(color: Colors.grey),
prefixIcon: Icon(Icons.calendar_month, color: Colors.purple),
border: buildFormBoard(),
enabledBorder: buildFormEnabledBoard(),
focusedBorder: buildFormFocusedBoard(colors),
filled: true,
fillColor: colors.surface,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
validator: (value) => endTimeFieldValidator(provider, value),
);
}
IconButton buildClearScheduled(CalendarProvider provider) {
return IconButton(
icon: Icon(Icons.clear, size: 18),
onPressed: () {
setState(() {
provider.formItem.scheduledTime = null;
});
widget.formKey.currentState?.fields['scheduledTime']?.didChange(null);
},
);
}
String? scheduledTimeFieldValidator(value) {
if (value != null && value.isBefore(DateTime.now())) {
return '不能选择过去的日期';
}
return null;
}
IconButton buildClearScheduled() {
return IconButton(
icon: Icon(Icons.clear, size: 18),
onPressed: () {
setState(() {
provider.formItem.scheduledTime = null;
});
widget.formKey.currentState?.fields['scheduledTime']?.didChange(null);
},
);
}
FormBuilderDateTimePicker buildScheduledTimeField(CalendarProvider provider) {
final colors = Theme.of(context).colorScheme;
var scheduledTime = provider.formItem.scheduledTime;
String? scheduledTimeFieldValidator(value) {
if (value != null && value.isBefore(DateTime.now())) {
return '不能选择过去的日期';
}
return null;
}
return FormBuilderDateTimePicker(
name: 'scheduledTime',
format: DateFormat('yyyy-MM-dd HH:mm:ss'),
initialValue: scheduledTime,
inputType: InputType.both,
onChanged: (value) {
setState(() {
provider.formItem.scheduledTime = value;
});
},
decoration: InputDecoration(
labelText: scheduledTime == null ? '选择提醒时间' : '提醒时间: ',
labelStyle: TextStyle(color: Colors.grey),
prefixIcon: Icon(Icons.calendar_month, color: Colors.purple),
border: buildFormBoard(),
enabledBorder: buildFormEnabledBoard(),
focusedBorder: buildFormFocusedBoard(colors),
filled: true,
fillColor: colors.surface,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
suffixIcon:
scheduledTime != null ? buildClearScheduled(provider) : null,
),
validator: (value) => scheduledTimeFieldValidator(value),
);
}
FormBuilderDateTimePicker buildScheduledTimeField() {
var scheduledTime = provider.formItem.scheduledTime;
return FormBuilderDateTimePicker(
name: 'scheduledTime',
format: DateFormat('yyyy-MM-dd HH:mm:ss'),
initialValue: scheduledTime,
inputType: InputType.both,
onChanged: (value) {
setState(() {
provider.formItem.scheduledTime = value;
});
},
decoration: InputDecoration(
labelText:
scheduledTime == null
? '选择提醒日期'
: '提醒: ${formatTime(scheduledTime)}',
labelStyle: TextStyle(
color: scheduledTime == null ? Colors.grey : Colors.black87,
),
prefixIcon: Icon(Icons.calendar_month, color: Colors.purple),
border: buildFormBoard(),
enabledBorder: buildFormEnabledBoard(),
focusedBorder: buildFormFocusedBoard(colors),
filled: true,
fillColor: colors.surface,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
suffixIcon: scheduledTime != null ? buildClearScheduled() : null,
),
validator: (value) => scheduledTimeFieldValidator(value),
);
}
FormBuilder buildForm() {
return FormBuilder(
key: widget.formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
buildTitleField(),
SizedBox(height: 12),
buildStartTimeField(),
SizedBox(height: 12),
buildEndTimeField(),
SizedBox(height: 12),
buildScheduledTimeField(),
],
),
);
}
return Padding(
padding: const EdgeInsets.all(16),
FormBuilder buildForm(CalendarProvider provider) {
return FormBuilder(
key: widget.formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [buildTitle(), SizedBox(height: 20), buildForm()],
children: [
buildTitleField(provider),
SizedBox(height: 12),
buildStartTimeField(provider),
SizedBox(height: 12),
buildEndTimeField(provider),
SizedBox(height: 12),
buildScheduledTimeField(provider),
],
),
);
}
@override
Widget build(BuildContext context) {
final provider = Provider.of<CalendarProvider>(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [buildTitle(), SizedBox(height: 20), buildForm(provider)],
),
);
}

View File

@@ -2,6 +2,7 @@ import 'package:flisp_app/models/calendar.dart';
import 'package:flisp_app/provider/calendar_provider.dart';
import 'package:flisp_app/widgets/common.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:provider/provider.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
@@ -61,6 +62,10 @@ Widget buildCalendar({
return SfCalendar(
view: CalendarView.week,
controller: controller,
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
headerStyle: CalendarHeaderStyle(
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
),
dataSource: dataSource,
firstDayOfWeek: 1,
showDatePickerButton: true,
@@ -75,8 +80,8 @@ Widget buildCalendar({
scheduleViewSettings: ScheduleViewSettings(
monthHeaderSettings: MonthHeaderSettings(
backgroundColor: Theme.of(context).colorScheme.primary,
height: 85
)
height: 85,
),
),
// 时间区域设置
timeSlotViewSettings: TimeSlotViewSettings(
@@ -103,7 +108,7 @@ Widget buildCalendar({
onLongPress: (CalendarLongPressDetails details) async {
if (details.targetElement == CalendarElement.appointment) {
if (details.appointments?.length == 1) {
final bool? result = await showDeleteConfirmationDialog(context);
final bool? result = await showConfirmDialog(context, '确定要删除这个项目吗?');
if (result == true) {
onDelete(details.appointments!.first);
}

View File

@@ -1,291 +0,0 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
class ChartData {
final String name;
final double value;
ChartData({required this.name, required this.value});
}
Widget buildChartTitle(BuildContext context, String title, IconData icon) {
return Row(
children: [
Icon(icon, color: Theme.of(context).colorScheme.primary),
Text(title),
],
);
}
Widget buildChartDivider(BuildContext context) {
return Divider(
height: 1,
thickness: 1,
color: Theme.of(context).colorScheme.primary,
indent: 0,
endIndent: 0,
);
}
Widget lineChart({
required BuildContext context,
required String xAxisName,
required String yAxisName,
required String unit,
required List<ChartData> data,
}) {
return SfCartesianChart(
// 图表标题
// title: ChartTitle(text: '2023年上半年销售额万元'),
// X轴配置类别轴
primaryXAxis: CategoryAxis(majorGridLines: MajorGridLines(width: 0)),
// Y轴配置数值轴
// primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName$unit')),
// 启用图例
legend: Legend(isVisible: true, position: LegendPosition.top),
// 启用交互提示(点击数据点显示详情)
tooltipBehavior: TooltipBehavior(
enable: true,
format: 'point.x: point.y $unit',
),
// 折线图数据系列
series: [
LineSeries<ChartData, String>(
dataSource: data,
// X轴数据映射
xValueMapper: (ChartData chart, _) => chart.name,
// Y轴数据映射
yValueMapper: (ChartData chart, _) => chart.value,
// 线条颜色
color: Theme.of(context).colorScheme.primary,
// 线条宽度
width: 3,
// 数据点样式
markerSettings: const MarkerSettings(
isVisible: true,
color: Colors.white,
shape: DataMarkerType.circle,
height: 6,
width: 6,
),
// 折线名称(会显示在图例中)
name: yAxisName,
// 启用数据标签(直接显示数值)
dataLabelSettings: const DataLabelSettings(
isVisible: true,
color: Colors.white,
opacity: 0,
),
// 动画效果
animationDuration: 2000, // 动画时长(毫秒)
),
],
);
}
Widget barChart({
required BuildContext context,
required String xAxisName,
required String yAxisName,
required String unit,
required List<ChartData> data,
}) {
return SfCartesianChart(
// X轴配置类别轴
primaryXAxis: CategoryAxis(majorGridLines: MajorGridLines(width: 0)),
// Y轴配置数值轴
// primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName$unit')),
// 启用交互提示
tooltipBehavior: TooltipBehavior(
enable: true,
format: 'point.x: point.y $unit',
),
// 柱状图数据系列
series: [
ColumnSeries<ChartData, String>(
dataSource: data,
// X轴数据映射
xValueMapper: (ChartData chart, _) => chart.name,
// Y轴数据映射
yValueMapper: (ChartData chart, _) => chart.value,
// 名称
name: yAxisName,
// 柱子颜色
color: Theme.of(context).colorScheme.primary,
// 柱子宽度0-1之间1表示占满类别间隔
width: 0.6,
// 柱子边框
borderWidth: 1,
borderColor: Colors.black12,
// 数据标签
dataLabelSettings: const DataLabelSettings(
isVisible: true,
color: Colors.white,
opacity: 0,
alignment: ChartAlignment.center,
),
// 动画效果
animationDuration: 2000,
),
],
);
}
Widget doubleBarChart({
required BuildContext context,
required String xAxisName,
required String yAxisName,
required String unit,
required List<ChartData> data1,
required List<ChartData> data2,
required String series1Name,
required String series2Name,
}) {
return SfCartesianChart(
// X轴配置类别轴
primaryXAxis: CategoryAxis(majorGridLines: const MajorGridLines(width: 0)),
// Y轴配置数值轴
// primaryYAxis: NumericAxis(title: AxisTitle(text: '$yAxisName$unit')),
// 图例配置
legend: Legend(
isVisible: true,
position: LegendPosition.top,
overflowMode: LegendItemOverflowMode.wrap,
),
// 启用交互提示
tooltipBehavior: TooltipBehavior(
enable: true,
format: 'series.name: point.y $unit',
),
// 双柱状图数据系列
series: <ColumnSeries<ChartData, String>>[
ColumnSeries<ChartData, String>(
dataSource: data1,
// X轴数据映射
xValueMapper: (ChartData chart, _) => chart.name,
// Y轴数据映射
yValueMapper: (ChartData chart, _) => chart.value,
// 系列名称
name: series1Name,
// 柱子颜色
color: Theme.of(context).colorScheme.primary,
// 柱子宽度
width: 0.3,
// 柱子边框
borderWidth: 1,
borderColor: Colors.black12,
// 数据标签
dataLabelSettings: const DataLabelSettings(
isVisible: true,
color: Colors.white,
opacity: 0,
alignment: ChartAlignment.center,
),
// 动画效果
animationDuration: 2000,
),
ColumnSeries<ChartData, String>(
dataSource: data2,
// X轴数据映射
xValueMapper: (ChartData chart, _) => chart.name,
// Y轴数据映射
yValueMapper: (ChartData chart, _) => chart.value,
// 系列名称
name: series2Name,
// 柱子颜色
color: Theme.of(context).colorScheme.inversePrimary,
// 柱子宽度
width: 0.3,
// 柱子边框
borderWidth: 1,
borderColor: Colors.black12,
// 数据标签
dataLabelSettings: const DataLabelSettings(
isVisible: true,
color: Colors.white,
opacity: 0,
alignment: ChartAlignment.center,
),
// 动画效果
animationDuration: 2000,
),
],
);
}
Widget pieChart({
required BuildContext context,
required String unit,
required List<ChartData> data,
}) {
// 计算 value 的总和
double sumValue = data.fold(0.0, (sum, item) => sum + item.value);
return SfCircularChart(
// 饼图标题
// title: ChartTitle(text: '菜谱类别占比分布'),
// 启用图例
legend: const Legend(isVisible: true, position: LegendPosition.right),
// 启用交互提示(点击扇区显示详情)
tooltipBehavior: TooltipBehavior(
enable: true,
format: 'point.x: point.y $unit',
),
// 饼图系列配置
series: [
PieSeries<ChartData, String>(
dataSource: data,
// 类别映射(饼图扇区名称)
xValueMapper: (ChartData data, _) => data.name,
// 数值映射(扇区大小占比)
yValueMapper: (ChartData data, _) => data.value,
// 扇区半径0-1之间1表示充满容器
// radius: '50%',
// 启用扇区分离效果
explode: true,
// 指定分离的扇区索引(这里分离第一个扇区)
explodeIndex: 0,
// 分离距离
explodeOffset: '5%',
dataLabelMapper: (ChartData data, _) {
final percentage = (data.value / sumValue * 100).toStringAsFixed(0);
return '$percentage%';
},
// 数据标签(显示在扇区上的文本)
dataLabelSettings: DataLabelSettings(isVisible: true),
// 动画效果
animationDuration: 2000,
),
],
);
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/dialog_widget.dart';
import 'package:toggle_switch/toggle_switch.dart';
OutlineInputBorder buildFormBoard() {
@@ -22,28 +23,6 @@ OutlineInputBorder buildFormFocusedBoard(ColorScheme colors) {
);
}
// 卡片
class BuildCard extends StatelessWidget {
final Widget? child;
const BuildCard({super.key, this.child});
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(
color: colors.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colors.outline.withAlpha(50), width: 1),
),
padding: const EdgeInsets.all(16),
child: child,
);
}
}
// 底部弹出
void buildModalBottom({
required BuildContext context,
@@ -70,7 +49,7 @@ void buildModalBottom({
builder: (_, controller) {
return Container(
decoration: BoxDecoration(
color: colors.surface,
color: colors.surfaceContainer,
border: Border.all(
color: colors.outline.withAlpha(50),
width: 1,
@@ -143,7 +122,7 @@ Widget buildDismissible({
background: _buildDismissBackground(),
confirmDismiss: (direction) async {
// 这里实现二次确认
return await showDeleteConfirmationDialog(context);
return await showConfirmDialog(context, '确定要删除这个项目吗?');
},
onDismissed: (direction) {
onDelete();
@@ -152,30 +131,6 @@ Widget buildDismissible({
);
}
// 确认对话框
Future<bool?> showDeleteConfirmationDialog(BuildContext context) async {
return showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('确认删除'),
backgroundColor: Colors.white,
content: const Text('确定要删除这个项目吗?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
);
},
);
}
Widget _buildDismissBackground() {
return Container(
color: Colors.red,
@@ -193,16 +148,18 @@ Widget buildToggleSwitch<T extends Enum>({
required List<String> labels,
required ValueChanged<T> onTabChanged,
}) {
final colors = Theme.of(context).colorScheme;
return ToggleSwitch(
minWidth: 90.0,
minHeight: 40.0,
initialLabelIndex: tabValues.indexOf(currentTab),
totalSwitches: tabValues.length,
labels: labels,
activeBgColor: [Theme.of(context).colorScheme.primary],
activeBgColor: [colors.primary],
activeFgColor: Colors.white,
inactiveBgColor: Colors.grey.shade200,
inactiveFgColor: Colors.grey.shade700,
inactiveBgColor: colors.surfaceContainer,
inactiveFgColor: colors.onSurface,
cornerRadius: 12.0,
customTextStyles: [TextStyle(fontSize: 12, fontWeight: FontWeight.w500)],
onToggle: (index) {
@@ -213,6 +170,41 @@ Widget buildToggleSwitch<T extends Enum>({
);
}
ButtonStyle buildSegmentStyle(ColorScheme colors) {
return ButtonStyle(
side: WidgetStateProperty.all<BorderSide>(
BorderSide(color: colors.surface, width: 0),
),
iconColor: WidgetStateProperty.resolveWith<Color>((
Set<WidgetState> states,
) {
if (states.contains(WidgetState.selected)) {
return colors.onPrimary;
}
return Colors.grey;
}),
backgroundColor: WidgetStateProperty.resolveWith<Color>((
Set<WidgetState> states,
) {
if (states.contains(WidgetState.selected)) {
return colors.primary;
}
return colors.surfaceContainer;
}),
foregroundColor: WidgetStateProperty.resolveWith<Color>((
Set<WidgetState> states,
) {
if (states.contains(WidgetState.selected)) {
return colors.onPrimary;
}
return colors.onSurface;
}),
shape: WidgetStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
}
Widget circleIconButton({
required IconData icon,
required VoidCallback onPressed,

View File

@@ -1,9 +1,9 @@
import 'package:file_picker/file_picker.dart';
import 'package:flisp_app/models/flisp.dart';
import 'package:flisp_app/provider/flisp_provider.dart';
import 'package:flisp_app/utils/minio_utils.dart';
import 'package:flisp_app/widgets/flisp_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/utils/minio_utils.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:provider/provider.dart';
@@ -27,6 +27,8 @@ class FlispForm extends StatefulWidget {
class _FlispFormState extends State<FlispForm> {
bool _showTagOptions = false;
final int maxContentCount = 100;
final String bucketName = 'flisp';
@override
void initState() {
@@ -51,7 +53,7 @@ class _FlispFormState extends State<FlispForm> {
final provider = Provider.of<FlispProvider>(context, listen: false);
setState(() {
provider.formItem.imageUrl =
'${MinIOHelper().rustfsFileUrl}/${MinIOHelper().bucketName}/$fileName';
'${MinIOHelper().fileUrl}/$bucketName/$fileName';
});
}
@@ -62,223 +64,228 @@ class _FlispFormState extends State<FlispForm> {
});
}
@override
Widget build(BuildContext context) {
final int maxContentCount = 100;
final provider = Provider.of<FlispProvider>(context);
Widget buildContentField(FlispProvider provider) {
final colors = Theme.of(context).colorScheme;
Widget buildContentField() {
return FormBuilderTextField(
name: 'content',
initialValue: provider.formItem.content,
onChanged: (value) {
setState(() {
provider.formItem.content = value ?? '';
});
},
decoration: InputDecoration(
hintText: '记录你的灵感瞬间...',
hintStyle: TextStyle(color: Colors.grey),
counterText: '',
suffixText: '${provider.formItem.content.length}/$maxContentCount',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.primary),
),
filled: true,
fillColor: colors.surface,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
return FormBuilderTextField(
name: 'content',
initialValue: provider.formItem.content,
onChanged: (value) {
setState(() {
provider.formItem.content = value ?? '';
});
},
decoration: InputDecoration(
hintText: '记录你的灵感瞬间...',
hintStyle: TextStyle(color: Colors.grey),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
minLines: 4,
maxLines: 8,
maxLength: maxContentCount,
validator: (value) {
if (value != null && value.isEmpty) {
return '请输入内容';
}
if (value != null && value.length > maxContentCount) {
return '内容不能超过$maxContentCount个字符';
}
return null;
},
);
}
Widget buildFormBody() {
return Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildContentField(),
SizedBox(height: 8),
buildFlispTag(provider.formItem),
SizedBox(height: 12),
if (provider.formItem.imageUrl.isNotEmpty)
buildFlispImage(
flisp: provider.formItem,
showDelete: true,
onDelete: _deleteImage,
),
],
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
);
}
void selectImage() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['jpg', 'jpeg', 'png'],
allowMultiple: false,
);
if (result != null) {
PlatformFile file = result.files.first;
final fileName = await MinIOHelper().uploadFile(file: file);
_selectImage(fileName);
}
}
Widget buildSelectImageButton() {
return GestureDetector(
onTap: () => selectImage(),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.blue.withAlpha(50),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.withAlpha(80), width: 1),
),
child: Icon(
Icons.photo_library_rounded,
size: 20,
color: Colors.blue,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.primary),
),
);
}
Widget buildSelectTagButton() {
return GestureDetector(
onTap: _toggleTagOptions,
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.orange.withAlpha(50),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.withAlpha(80), width: 1),
filled: true,
fillColor: colors.surface,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
minLines: 4,
maxLines: 8,
maxLength: maxContentCount,
buildCounter: (
context, {
required currentLength,
required isFocused,
maxLength,
}) {
return Text(
'$currentLength/$maxContentCount',
style: TextStyle(
color:
currentLength > maxLength!
? Colors.red
: Theme.of(context).colorScheme.primary,
),
child: Icon(
Icons.local_offer_rounded,
size: 20,
color: Colors.orange,
),
),
);
}
);
},
validator: (value) {
if (value != null && value.isEmpty) {
return '请输入内容';
}
Widget buildConfirmButton() {
return Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: colors.primary,
borderRadius: BorderRadius.circular(24),
),
child: IconButton(
icon: Icon(Icons.arrow_forward_rounded, size: 24),
color: Colors.white,
onPressed: () => widget.onOk(),
),
);
}
if (value != null && value.length > maxContentCount) {
return '内容不能超过$maxContentCount个字符';
}
return null;
},
);
}
Widget buildShowTagOptions() {
// 计算弹窗位置(基于标签按钮位置)
return Positioned(
bottom: 50, // 调整垂直位置
left: 60, // 调整水平位置
child: Container(
width: 90,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children:
FlispTag.values.map((tag) {
return GestureDetector(
onTap: () => _selectTag(tag),
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 8),
child: Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: tag.color,
shape: BoxShape.circle,
),
child: Icon(
tag.icon,
size: 12,
color: Colors.white,
),
),
SizedBox(width: 8),
Text(tag.label, style: TextStyle(fontSize: 12)),
],
),
),
);
}).toList(),
),
),
);
}
Widget buildBottomButton() {
return Container(
padding: EdgeInsets.only(top: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Widget buildFormBody(FlispProvider provider) {
return Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
buildSelectImageButton(),
SizedBox(width: 12),
buildSelectTagButton(),
],
),
buildConfirmButton(),
buildContentField(provider),
SizedBox(height: 8),
buildFlispTag(context, provider.formItem),
SizedBox(height: 12),
if (provider.formItem.imageUrl.isNotEmpty)
buildFlispImage(
flisp: provider.formItem,
showDelete: true,
onDelete: _deleteImage,
),
],
),
),
);
}
void selectImage() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['jpg', 'jpeg', 'png'],
allowMultiple: false,
);
if (result != null) {
PlatformFile file = result.files.first;
final fileName = await MinIOHelper().uploadFile(
bucketName: bucketName,
file: file,
);
_selectImage(fileName);
}
}
Widget buildSelectImageButton() {
return GestureDetector(
onTap: () => selectImage(),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.blue.withAlpha(50),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.withAlpha(80), width: 1),
),
child: Icon(Icons.photo_library_rounded, size: 20, color: Colors.blue),
),
);
}
Widget buildSelectTagButton() {
return GestureDetector(
onTap: _toggleTagOptions,
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.orange.withAlpha(50),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange.withAlpha(80), width: 1),
),
child: Icon(Icons.local_offer_rounded, size: 20, color: Colors.orange),
),
);
}
Widget buildConfirmButton() {
return Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(24),
),
child: IconButton(
icon: Icon(Icons.arrow_forward_rounded, size: 24),
color: Colors.white,
onPressed: () => widget.onOk(),
),
);
}
Widget buildShowTagOptions() {
// 计算弹窗位置(基于标签按钮位置)
return Positioned(
bottom: 50, // 调整垂直位置
left: 60, // 调整水平位置
child: Container(
width: 90,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children:
FlispTag.values.map((tag) {
return GestureDetector(
onTap: () => _selectTag(tag),
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 8),
child: Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: tag.color,
shape: BoxShape.circle,
),
child: Icon(tag.icon, size: 12, color: Colors.white),
),
SizedBox(width: 8),
Text(tag.label, style: TextStyle(fontSize: 12)),
],
),
),
);
}).toList(),
),
),
);
}
Widget buildBottomButton() {
return Container(
padding: EdgeInsets.only(top: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
buildSelectImageButton(),
SizedBox(width: 12),
buildSelectTagButton(),
],
),
buildConfirmButton(),
],
),
);
}
@override
Widget build(BuildContext context) {
final provider = Provider.of<FlispProvider>(context);
// 整体使用Stack布局分离事件层级
return Stack(
@@ -300,7 +307,9 @@ class _FlispFormState extends State<FlispForm> {
// 主表单内容
FormBuilder(
key: widget.formKey,
child: Column(children: [buildFormBody(), buildBottomButton()]),
child: Column(
children: [buildFormBody(provider), buildBottomButton()],
),
),
// 标签弹窗(放在最上层,确保事件优先响应)

View File

@@ -1,30 +1,31 @@
import 'package:flisp_app/models/flisp.dart';
import 'package:flisp_app/widgets/common.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:intl/intl.dart';
import 'package:toggle_switch/toggle_switch.dart';
Widget buildFlispStatsCard(List<Flisp> flisps) {
Widget buildFlispStatsCard(BuildContext context, List<Flisp> flisps) {
int totalCount = flisps.length;
int lifeCount = flisps.where((flisp) => flisp.tag == FlispTag.life).length;
int workCount = flisps.where((flisp) => flisp.tag == FlispTag.work).length;
int studyCount = flisps.where((flisp) => flisp.tag == FlispTag.study).length;
return BuildCard(
return CommonCard(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildStatItem('总计', totalCount, Colors.orange),
_buildStatItem('生活', lifeCount, FlispTag.life.color),
_buildStatItem('工作', workCount, FlispTag.work.color),
_buildStatItem('学习', studyCount, FlispTag.study.color),
_buildStatItem(context, '总计', totalCount),
_buildStatItem(context, '生活', lifeCount),
_buildStatItem(context, '工作', workCount),
_buildStatItem(context, '学习', studyCount),
],
),
);
}
Widget _buildStatItem(String label, int count, Color color) {
Widget _buildStatItem(BuildContext context, String label, int count) {
return Column(
children: [
Text(
@@ -32,7 +33,7 @@ Widget _buildStatItem(String label, int count, Color color) {
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 4),
@@ -68,10 +69,9 @@ Widget buildFlispList({
itemCount: flisps.length,
separatorBuilder: (context, index) => SizedBox(height: 8),
itemBuilder: (context, index) {
final flisp = flisps[index];
return _buildFlispItem(
context: context,
flisp: flisp,
flisp: flisps[index],
onEdit: onEdit,
onDelete: onDelete,
);
@@ -80,12 +80,14 @@ Widget buildFlispList({
}
Widget _buildFlispContent(BuildContext context, Flisp flisp) {
final colors = Theme.of(context).colorScheme;
return RichText(
text: TextSpan(
text: flisp.content,
style: TextStyle(
fontFamily: 'CustomFont',
color: Theme.of(context).colorScheme.onSurface,
color: colors.onSurface,
fontSize: 16,
),
),
@@ -208,13 +210,15 @@ Widget _buildFlispVideo(Flisp flisp) {
);
}
Widget buildFlispTag(Flisp flisp) {
Widget buildFlispTag(BuildContext context, Flisp flisp) {
final colors = Theme.of(context).colorScheme;
return Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: flisp.tag.color.withAlpha(30),
color: colors.secondaryContainer,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: flisp.tag.color.withAlpha(60)),
// border: Border.all(color: flisp.tag.color.withAlpha(60)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -223,7 +227,7 @@ Widget buildFlispTag(Flisp flisp) {
width: 14,
height: 14,
decoration: BoxDecoration(
color: flisp.tag.color,
color: colors.secondary,
shape: BoxShape.circle,
),
child: Icon(flisp.tag.icon, size: 8, color: Colors.white),
@@ -233,7 +237,7 @@ Widget buildFlispTag(Flisp flisp) {
flisp.tag.label,
style: TextStyle(
fontSize: 12,
color: flisp.tag.color,
color: colors.secondary,
fontWeight: FontWeight.w500,
),
),
@@ -249,9 +253,13 @@ Widget buildFlispTime(Flisp flisp) {
);
}
Widget _buildFlispSuffix(Flisp flisp) {
Widget _buildFlispSuffix(BuildContext context, Flisp flisp) {
return Row(
children: [buildFlispTag(flisp), const Spacer(), buildFlispTime(flisp)],
children: [
buildFlispTag(context, flisp),
const Spacer(),
buildFlispTime(flisp),
],
);
}
@@ -268,7 +276,7 @@ Widget _buildFlispItem({
context: context,
id: flisp.id,
onDelete: () => onDelete(flisp),
child: BuildCard(
child: CommonCard(
child: InkWell(
onTap: () => onEdit(flisp),
child: Column(
@@ -280,7 +288,7 @@ Widget _buildFlispItem({
const SizedBox(height: 4),
if (hasVideo) _buildFlispVideo(flisp),
const SizedBox(height: 4),
_buildFlispSuffix(flisp),
_buildFlispSuffix(context, flisp),
],
),
),

View File

@@ -1,5 +1,5 @@
import 'package:flisp_app/widgets/common.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
class StatisticCard extends StatelessWidget {
final IconData icon;
@@ -19,7 +19,7 @@ class StatisticCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BuildCard(
return CommonCard(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [

View File

@@ -22,350 +22,348 @@ class TodoForm extends StatefulWidget {
}
class _TodoFormState extends State<TodoForm> {
final int maxTitleCount = 10;
final int maxContentCount = 20;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final int maxTitleCount = 10;
final int maxContentCount = 20;
final provider = Provider.of<TodoProvider>(context);
Widget buildTitle() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
widget.isEditing ? Icons.edit_note : Icons.add_task,
color: Theme.of(context).colorScheme.primary,
size: 24,
),
SizedBox(width: 8),
Text(
widget.isEditing ? '编辑待办事项' : '添加待办事项',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
],
);
}
FormBuilderTextField buildTitleField(TodoProvider provider) {
final colors = Theme.of(context).colorScheme;
Widget buildTitle() {
return Row(
children: [
Icon(
widget.isEditing ? Icons.edit_note : Icons.add_task,
color: colors.primary,
size: 24,
),
SizedBox(width: 8),
Text(
widget.isEditing ? '编辑待办事项' : '添加待办事项',
return FormBuilderTextField(
name: 'title',
initialValue: provider.formItem.title,
onChanged: (value) {
setState(() {
provider.formItem.title = value ?? '';
});
},
decoration: InputDecoration(
label: RichText(
text: TextSpan(
text: '标题',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: colors.primary,
color: Colors.grey,
fontSize: 16,
fontFamily: 'CustomFont',
),
),
],
);
}
FormBuilderTextField buildTitleField() {
return FormBuilderTextField(
name: 'title',
initialValue: provider.formItem.title,
onChanged: (value) {
setState(() {
provider.formItem.title = value ?? '';
});
},
decoration: InputDecoration(
label: RichText(
text: TextSpan(
text: '标题',
style: TextStyle(color: Colors.grey.shade700, fontSize: 16),
children: const [
TextSpan(
text: '*',
style: TextStyle(
color: Colors.red,
fontSize: 18,
fontWeight: FontWeight.bold,
),
children: const [
TextSpan(
text: '*',
style: TextStyle(
color: Colors.red,
fontSize: 18,
fontWeight: FontWeight.bold,
),
],
),
),
],
),
hintText: '请输入待办事项标题...',
hintStyle: TextStyle(color: Colors.grey),
counterText: '',
suffixText: '${provider.formItem.title.length}/$maxTitleCount',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.primary),
),
filled: true,
fillColor: colors.surface,
prefixIcon: Icon(Icons.title, color: Colors.blue),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
maxLength: maxTitleCount,
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入标题';
}
if (value.length > maxTitleCount) {
return '标题不能超过$maxTitleCount个字符';
}
return null;
},
);
}
FormBuilderTextField buildContentField() {
return FormBuilderTextField(
name: 'content',
initialValue: provider.formItem.content,
onChanged: (value) {
setState(() {
provider.formItem.content = value ?? '';
});
},
decoration: InputDecoration(
labelText: '内容',
hintText: '请输入待办事项内容...',
hintStyle: TextStyle(color: Colors.grey),
counterText: '',
suffixText: '${provider.formItem.content.length}/$maxContentCount',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.primary),
),
filled: true,
fillColor: colors.surface,
prefixIcon: Icon(Icons.description, color: Colors.green),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
maxLines: 2,
maxLength: maxContentCount,
validator: (value) {
if (value != null && value.length > maxContentCount) {
return '内容不能超过$maxContentCount个字符';
}
return null;
},
);
}
IconButton buildClearDueDateSuffixIcon() {
return IconButton(
icon: Icon(Icons.clear, size: 18),
onPressed: () {
setState(() {
provider.formItem.dueDate = null;
});
widget.formKey.currentState?.fields['dueDate']?.didChange(null);
},
);
}
FormBuilderDateTimePicker buildDueDateField() {
return FormBuilderDateTimePicker(
name: 'dueDate',
format: DateFormat('yyyy-MM-dd'),
initialValue: provider.formItem.dueDate,
inputType: InputType.date,
onChanged: (value) {
setState(() {
provider.formItem.dueDate = value;
});
},
decoration: InputDecoration(
labelText:
provider.formItem.dueDate == null
? '选择截止日期'
: '截止: ${DateFormat('yyyy-MM-dd').format(provider.formItem.dueDate!)}',
labelStyle: TextStyle(
color:
provider.formItem.dueDate == null
? Colors.grey
: Colors.black87,
),
prefixIcon: Icon(Icons.calendar_month, color: Colors.purple),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.primary),
),
filled: true,
fillColor: colors.surface,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
suffixIcon:
provider.formItem.dueDate != null
? buildClearDueDateSuffixIcon()
: null,
),
validator: (value) {
if (value != null &&
value.isBefore(DateTime.now().subtract(Duration(days: 1)))) {
return '不能选择过去的日期';
}
return null;
},
);
}
IconButton buildClearScheduledTimeSuffixIcon() {
return IconButton(
icon: Icon(Icons.clear, size: 18),
onPressed: () {
setState(() {
provider.formItem.scheduledTime = null;
});
widget.formKey.currentState?.fields['scheduledTime']?.didChange(null);
},
);
}
FormBuilderDateTimePicker buildScheduledTimeField() {
return FormBuilderDateTimePicker(
name: 'scheduledTime',
format: DateFormat('yyyy-MM-dd HH:mm:ss'),
initialValue: provider.formItem.scheduledTime,
inputType: InputType.both,
onChanged: (value) {
setState(() {
provider.formItem.scheduledTime = value;
});
},
decoration: InputDecoration(
labelText:
provider.formItem.scheduledTime == null
? '选择提醒日期'
: '提醒: ${DateFormat('yyyy-MM-dd HH:mm:ss').format(provider.formItem.scheduledTime!)}',
labelStyle: TextStyle(
color:
provider.formItem.scheduledTime == null
? Colors.grey
: Colors.black87,
),
prefixIcon: Icon(Icons.calendar_month, color: Colors.purple),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.primary),
),
filled: true,
fillColor: colors.surface,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
suffixIcon:
provider.formItem.scheduledTime != null
? buildClearScheduledTimeSuffixIcon()
: null,
),
validator: (value) {
if (value != null && value.isBefore(DateTime.now())) {
return '不能选择过去的日期';
}
return null;
},
);
}
FormBuilderRadioGroup builderRadioGroup() {
return FormBuilderRadioGroup<TodoPriority>(
name: 'priority',
initialValue: provider.formItem.priority,
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
orientation: OptionsOrientation.horizontal,
wrapSpacing: 6,
options:
TodoPriority.values.map((priority) {
return FormBuilderFieldOption<TodoPriority>(
value: priority,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [Text(priority.label)],
),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
provider.formItem.priority = value;
});
}
},
);
}
Container buildPriorityField() {
return Container(
decoration: BoxDecoration(
color: colors.surface,
hintText: '请输入待办事项标题...',
hintStyle: TextStyle(color: Colors.grey),
counterText: '',
suffixText: '${provider.formItem.title.length}/$maxTitleCount',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300, width: 1),
borderSide: BorderSide(color: Colors.grey.shade200),
),
padding: EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.flag, color: Colors.orange),
SizedBox(width: 6),
Text('优先级'),
],
),
builderRadioGroup(),
],
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
);
}
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.primary),
),
filled: true,
fillColor: colors.surface,
prefixIcon: Icon(Icons.title, color: Colors.blue),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
maxLength: maxTitleCount,
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入标题';
}
if (value.length > maxTitleCount) {
return '标题不能超过$maxTitleCount个字符';
}
return null;
},
);
}
FormBuilder buildForm() {
return FormBuilder(
key: widget.formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
buildTitleField(),
SizedBox(height: 12),
buildContentField(),
SizedBox(height: 12),
buildDueDateField(),
SizedBox(height: 12),
buildScheduledTimeField(),
SizedBox(height: 12),
buildPriorityField(),
],
),
);
}
FormBuilderTextField buildContentField(TodoProvider provider) {
final colors = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(16),
return FormBuilderTextField(
name: 'content',
initialValue: provider.formItem.content,
onChanged: (value) {
setState(() {
provider.formItem.content = value ?? '';
});
},
decoration: InputDecoration(
labelText: '内容',
labelStyle: TextStyle(color: Colors.grey),
hintText: '请输入待办事项内容...',
hintStyle: TextStyle(color: Colors.grey),
counterText: '',
suffixText: '${provider.formItem.content.length}/$maxContentCount',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.primary),
),
filled: true,
fillColor: colors.surface,
prefixIcon: Icon(Icons.description, color: Colors.green),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
maxLines: 2,
maxLength: maxContentCount,
validator: (value) {
if (value != null && value.length > maxContentCount) {
return '内容不能超过$maxContentCount个字符';
}
return null;
},
);
}
IconButton buildClearDueDateSuffixIcon(TodoProvider provider) {
return IconButton(
icon: Icon(Icons.clear, size: 18),
onPressed: () {
setState(() {
provider.formItem.dueDate = null;
});
widget.formKey.currentState?.fields['dueDate']?.didChange(null);
},
);
}
FormBuilderDateTimePicker buildDueDateField(TodoProvider provider) {
final colors = Theme.of(context).colorScheme;
return FormBuilderDateTimePicker(
name: 'dueDate',
format: DateFormat('yyyy-MM-dd'),
initialValue: provider.formItem.dueDate,
inputType: InputType.date,
onChanged: (value) {
setState(() {
provider.formItem.dueDate = value;
});
},
decoration: InputDecoration(
labelText: provider.formItem.dueDate == null ? '选择截止日期' : '截止日期: ',
labelStyle: TextStyle(color: Colors.grey),
prefixIcon: Icon(Icons.calendar_month, color: Colors.purple),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.primary),
),
filled: true,
fillColor: colors.surface,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
suffixIcon:
provider.formItem.dueDate != null
? buildClearDueDateSuffixIcon(provider)
: null,
),
validator: (value) {
if (value != null &&
value.isBefore(DateTime.now().subtract(Duration(days: 1)))) {
return '不能选择过去的日期';
}
return null;
},
);
}
IconButton buildClearScheduledTimeSuffixIcon(TodoProvider provider) {
return IconButton(
icon: Icon(Icons.clear, size: 18),
onPressed: () {
setState(() {
provider.formItem.scheduledTime = null;
});
widget.formKey.currentState?.fields['scheduledTime']?.didChange(null);
},
);
}
FormBuilderDateTimePicker buildScheduledTimeField(TodoProvider provider) {
final colors = Theme.of(context).colorScheme;
return FormBuilderDateTimePicker(
name: 'scheduledTime',
format: DateFormat('yyyy-MM-dd HH:mm:ss'),
initialValue: provider.formItem.scheduledTime,
inputType: InputType.both,
onChanged: (value) {
setState(() {
provider.formItem.scheduledTime = value;
});
},
decoration: InputDecoration(
labelText:
provider.formItem.scheduledTime == null ? '选择提醒时间' : '提醒时间: ',
labelStyle: TextStyle(color: Colors.grey),
prefixIcon: Icon(Icons.calendar_month, color: Colors.purple),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colors.primary),
),
filled: true,
fillColor: colors.surface,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
suffixIcon:
provider.formItem.scheduledTime != null
? buildClearScheduledTimeSuffixIcon(provider)
: null,
),
validator: (value) {
if (value != null && value.isBefore(DateTime.now())) {
return '不能选择过去的日期';
}
return null;
},
);
}
FormBuilderRadioGroup builderRadioGroup(TodoProvider provider) {
return FormBuilderRadioGroup<TodoPriority>(
name: 'priority',
initialValue: provider.formItem.priority,
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
orientation: OptionsOrientation.horizontal,
wrapSpacing: 6,
options:
TodoPriority.values.map((priority) {
return FormBuilderFieldOption<TodoPriority>(
value: priority,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [Text(priority.label)],
),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
provider.formItem.priority = value;
});
}
},
);
}
Container buildPriorityField(TodoProvider provider) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.flag, color: Colors.orange),
SizedBox(width: 6),
Text('优先级'),
],
),
builderRadioGroup(provider),
],
),
);
}
FormBuilder buildForm(TodoProvider provider) {
return FormBuilder(
key: widget.formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [buildTitle(), SizedBox(height: 20), buildForm()],
children: [
buildTitleField(provider),
SizedBox(height: 12),
buildContentField(provider),
SizedBox(height: 12),
buildDueDateField(provider),
SizedBox(height: 12),
buildScheduledTimeField(provider),
SizedBox(height: 12),
buildPriorityField(provider),
],
),
);
}
@override
Widget build(BuildContext context) {
final provider = Provider.of<TodoProvider>(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [buildTitle(), SizedBox(height: 20), buildForm(provider)],
),
);
}

View File

@@ -2,29 +2,31 @@ import 'package:flisp_app/models/todo.dart';
import 'package:flisp_app/utils/todo_utils.dart';
import 'package:flisp_app/widgets/common.dart';
import 'package:flutter/material.dart';
import 'package:flutter_common/widget/common_widget.dart';
import 'package:intl/intl.dart';
// 统计卡片
Widget buildTodoStatsCard(List<Todo> todos) {
Widget buildTodoStatsCard(BuildContext context, List<Todo> todos) {
int totalCount = todos.length;
int activeCount = todos.where((todo) => !todo.isCompleted).length;
int completedCount = todos.where((todo) => todo.isCompleted).length;
return BuildCard(
return CommonCard(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_buildStatItem('总计', totalCount, Colors.blue),
_buildStatItem('待完成', activeCount, Colors.orange),
_buildStatItem('已完成', completedCount, Colors.green),
_buildStatItem(context, '总计', totalCount),
_buildStatItem(context, '待完成', activeCount),
_buildStatItem(context, '已完成', completedCount),
],
),
);
}
Widget _buildStatItem(String label, int count, Color color) {
Widget _buildStatItem(BuildContext context, String label, int count) {
return Column(
children: [
Text(
@@ -32,7 +34,7 @@ Widget _buildStatItem(String label, int count, Color color) {
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 4),
@@ -93,7 +95,7 @@ Widget _buildTodoItem({
context: context,
id: todo.id,
onDelete: () => onDelete(todo),
child: BuildCard(
child: CommonCard(
child: ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 0),
leading: SizedBox(

View File

@@ -1,119 +0,0 @@
import 'package:flisp_app/widgets/common.dart';
import 'package:flutter/material.dart';
class YearSelector extends StatefulWidget {
final int initialYear;
final int? minYear;
final int? maxYear;
final Function(int) onYearChanged;
const YearSelector({
super.key,
required this.initialYear,
required this.onYearChanged,
this.minYear,
this.maxYear,
});
@override
State<YearSelector> createState() => _YearSelectorState();
}
// SingleTickerProviderStateMixin 动画控制器
class _YearSelectorState extends State<YearSelector>
with SingleTickerProviderStateMixin {
late int _currentYear;
// 用于动画效果
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_currentYear = widget.initialYear;
// 初始化动画控制器
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
// 缩放动画
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.1).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
/// 切换到上一年
void _previousYear() {
if (widget.minYear == null || _currentYear > widget.minYear!) {
_animateYearChange(() {
setState(() {
_currentYear--;
});
widget.onYearChanged(_currentYear);
});
}
}
/// 切换到下一年
void _nextYear() {
if (widget.maxYear == null || _currentYear < widget.maxYear!) {
_animateYearChange(() {
setState(() {
_currentYear++;
});
widget.onYearChanged(_currentYear);
});
}
}
/// 年份变化时的动画效果
void _animateYearChange(VoidCallback onComplete) {
_animationController.forward().then((_) {
onComplete();
_animationController.reverse();
});
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
circleIconButton(
context: context,
icon: Icons.chevron_left,
onPressed: () => _previousYear(),
),
// 年份显示
AnimatedBuilder(
animation: _scaleAnimation,
builder: (context, child) {
return Transform.scale(scale: _scaleAnimation.value, child: child);
},
child: Text(
'$_currentYear',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
),
circleIconButton(
context: context,
icon: Icons.chevron_right,
onPressed: () => _nextYear(),
)
],
);
}
}

View File

@@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation
import file_picker
import flutter_image_compress_macos
import flutter_local_notifications
import package_info_plus
import path_provider_foundation
@@ -14,6 +15,7 @@ import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))

View File

@@ -39,7 +39,7 @@ packages:
source: hosted
version: "2.12.0"
awesome_dialog:
dependency: "direct main"
dependency: transitive
description:
name: awesome_dialog
sha256: "4c5821a0a637ceee022084e78c1b8237dd4b8bfca4dd24ac2484662a56707338"
@@ -183,7 +183,7 @@ packages:
source: hosted
version: "0.3.5"
crypto:
dependency: "direct main"
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
@@ -214,6 +214,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.11"
dio:
dependency: transitive
description:
name: dio
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.9.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
fake_async:
dependency: transitive
description:
@@ -259,6 +275,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_common:
dependency: "direct main"
description:
path: "../flutter_common"
relative: true
source: path
version: "1.0.0+1"
flutter_form_builder:
dependency: "direct main"
description:
@@ -267,6 +290,54 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.0.1"
flutter_image_compress:
dependency: transitive
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:
@@ -279,26 +350,34 @@ packages:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610
sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875"
url: "https://pub.flutter-io.cn"
source: hosted
version: "18.0.1"
version: "19.5.0"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01"
sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.0"
version: "6.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52"
sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe"
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.0.0"
version: "9.1.0"
flutter_local_notifications_windows:
dependency: transitive
description:
name: flutter_local_notifications_windows
sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.3"
flutter_localizations:
dependency: "direct main"
description: flutter
@@ -323,7 +402,7 @@ packages:
source: sdk
version: "0.0.0"
fluttertoast:
dependency: "direct main"
dependency: transitive
description:
name: fluttertoast
sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8"
@@ -474,6 +553,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.1.1"
logger:
dependency: transitive
description:
name: logger
sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.6.2"
logging:
dependency: transitive
description:
@@ -523,7 +610,7 @@ packages:
source: hosted
version: "2.0.0"
minio:
dependency: "direct main"
dependency: transitive
description:
name: minio
sha256: ee2ce47766e46c7d164f960f2f5ed6a9a82844d877f6b82574f6876ec50c56d1
@@ -691,7 +778,7 @@ packages:
source: hosted
version: "0.0.16"
shared_preferences:
dependency: "direct main"
dependency: transitive
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
@@ -832,7 +919,7 @@ packages:
source: hosted
version: "30.2.7"
syncfusion_flutter_charts:
dependency: "direct main"
dependency: transitive
description:
name: syncfusion_flutter_charts
sha256: "68fdb029dad34a46e4c9cfad8ad66fe29db7b303bd96849261ab2b23a168d0e8"

View File

@@ -1,64 +1 @@
name: flisp_app
description: "闪灵"
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1
environment:
sdk: ^3.7.0
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
# 状态管理
provider: ^6.1.1
# 本地存储
shared_preferences: ^2.2.2
# 对话框组件
awesome_dialog: ^3.3.0
# 切换开关组件
toggle_switch: ^2.3.0
# 轻量级提示
fluttertoast: ^8.2.2
# 表单构建器
flutter_form_builder: ^10.0.0
# 表单验证器
form_builder_validators: ^10.0.0
# 国际化支持
intl: ^0.19.0
# 轻量级NoSQL数据库
hive: ^2.2.3
# Hive的Flutter集成
hive_flutter: ^1.1.0
# 路径提供器
path_provider: ^2.1.1
# 本地通知
flutter_local_notifications: ^18.0.0
# 时区支持
timezone: ^0.10.1
file_picker: ^10.3.3
minio: ^3.5.8
crypto: ^3.0.7
syncfusion_flutter_calendar: ^30.1.37
syncfusion_localizations: ^30.1.37
syncfusion_flutter_charts: ^30.1.41
package_info_plus: ^8.0.0
dev_dependencies:
flutter_test:
sdk: flutter
hive_generator: ^2.0.1
build_runner: ^2.4.6
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
fonts:
- family: CustomFont
fonts:
- asset: fonts/custom.ttf
name: flisp_app

View File

@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_local_notifications_windows
)
set(PLUGIN_BUNDLED_LIBRARIES)