add detail page

This commit is contained in:
jyuesong
2022-01-13 14:10:29 +08:00
parent 6acf427fb6
commit 80670821cd
17 changed files with 502 additions and 42 deletions

View File

@@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'base_viewmodel.dart';
class BaseStateWidget<T extends BaseViewModel> extends ConsumerStatefulWidget {
final Widget Function(BuildContext context, T value, Widget? child) builder;
final Widget Function(WidgetRef context, T value, Widget? child) builder;
final ProviderBase<T> model;
final Widget? child;
final Function(T)? onReady;
@@ -37,7 +37,7 @@ class _BaseStateWidgetState<T extends BaseViewModel> extends ConsumerState<BaseS
Widget build(BuildContext context) {
var viewModel = ref.watch<T>(widget.model);
if (viewModel.currentState == PageState.CONTENT) {
return widget.builder(context, viewModel, widget.child);
return widget.builder(ref, viewModel, widget.child);
}
if (viewModel.currentState == PageState.LOADING) {

View File

@@ -1,6 +1,9 @@
import 'dart:ffi';
import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/module/login/login_bean.dart';
import 'package:qinglong_app/module/task/task_bean.dart';
import 'package:qinglong_app/module/task/task_detail/task_detail_bean.dart';
import 'url.dart';
@@ -18,4 +21,21 @@ class Api {
static Future<HttpResponse<List<TaskBean>>> crons() async {
return await Http.get<List<TaskBean>>(Url.TASKS, {"searchValue": ""});
}
static Future<HttpResponse<NullResponse>> startTasks(List<String> crons) async {
return await Http.put<NullResponse>(Url.RUN_TASKS, crons);
}
static Future<HttpResponse<NullResponse>> stopTasks(List<String> crons) async {
return await Http.put<NullResponse>(Url.RUN_TASKS, crons);
}
static Future<HttpResponse<String>> inTimeLog(String cron) async {
return await Http.get<String>(Url.INTIME_LOG(cron), null);
}
static Future<HttpResponse<TaskDetailBean>> taskDetail(String cron) async {
return await Http.get<TaskDetailBean>(Url.TASK_DETAIL+cron, null);
}
}

View File

@@ -1,12 +1,11 @@
import 'dart:convert';
import 'dart:ffi';
import 'dart:core';
import 'package:dio/dio.dart';
import 'package:dio_log/dio_log.dart';
import 'package:flutter/foundation.dart';
import 'package:qinglong_app/base/http/token_interceptor.dart';
import 'package:qinglong_app/base/userinfo_viewmodel.dart';
import 'package:qinglong_app/main.dart';
import '../../json.jc.dart';
@@ -36,7 +35,7 @@ class Http {
}
}
static Future<HttpResponse<T>> get<T>(String uri, Map<String, String> json, {bool compute = true}) async {
static Future<HttpResponse<T>> get<T>(String uri, Map<String, String>? json, {bool compute = true}) async {
_init();
var response = await _dio!.get(uri, queryParameters: json);
@@ -74,7 +73,7 @@ class Http {
try {
if (response.data["code"] == 200) {
if (response.data["data"] != null) {
if (T is Void) {
if (T == NullResponse) {
return HttpResponse<T>(
success: true,
code: 200,
@@ -83,7 +82,7 @@ class Http {
dynamic data = response.data["data"];
T t;
if (T is String) {
if (T == String) {
if (data is String) {
t = data as T;
} else {
@@ -109,7 +108,7 @@ class Http {
}
} else {
return HttpResponse<T>(
success: false,
success: true,
code: 200,
);
}
@@ -173,3 +172,5 @@ class CronBean with BaseBean<CronBean> {
void decode<T>() async {
compute(DeserializeAction.invokeJson, DeserializeAction<T>({}));
}
class NullResponse{}

View File

@@ -1,4 +1,11 @@
class Url {
static const LOGIN = "/api/user/login";
static const TASKS = "/api/crons";
static const RUN_TASKS = "/api/crons/run";
static const STOP_TASKS = "/api/crons/stop";
static const TASK_DETAIL = "/api/crons/";
static INTIME_LOG(String cronId) {
return "/api/crons/$cronId/log";
}
}

View File

@@ -8,7 +8,7 @@ class Routes {
static Route<dynamic>? generateRoute(RouteSettings settings) {
switch (settings.name) {
case route_HomePage:
return CupertinoPageRoute(builder: (context) => const HomePage());
return MaterialPageRoute(builder: (context) => const HomePage());
}
return null;

View File

@@ -22,14 +22,16 @@ class ThemeViewModel extends ChangeNotifier {
}
ThemeData darkTheme = ThemeData.dark().copyWith(
primaryColor: Color(0xffffffff),
primaryColor: const Color(0xffffffff),
);
ThemeData lightTheme = ThemeData.light().copyWith(
primaryColor: Color(0xFF0F77FE),
primaryColor: const Color(0xFF0F77FE),
scaffoldBackgroundColor: Colors.white,
);
abstract class ThemeColors {
Color taskTitleColor();
Color searchBarBg();
}
class LightartThemeColors extends ThemeColors {
@@ -37,6 +39,12 @@ class LightartThemeColors extends ThemeColors {
Color taskTitleColor() {
return Color(0xff333333);
}
@override
Color searchBarBg() {
return const Color(0xffF7F7F7);
}
}
class DartThemeColors extends ThemeColors {
@@ -44,4 +52,11 @@ class DartThemeColors extends ThemeColors {
Color taskTitleColor() {
return Colors.white;
}
@override
Color searchBarBg() {
return const Color(0xff2E312E);
}
}

View File

@@ -0,0 +1,74 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:qinglong_app/base/http/api.dart';
import 'package:qinglong_app/base/http/http.dart';
class InTimeLogPage extends StatefulWidget {
final String cronId;
final bool needTimer;
const InTimeLogPage(this.cronId, this.needTimer, {Key? key}) : super(key: key);
@override
_InTimeLogPageState createState() => _InTimeLogPageState();
}
class _InTimeLogPageState extends State<InTimeLogPage> {
Timer? _timer;
String? content;
@override
void initState() {
super.initState();
if (widget.needTimer) {
_timer = Timer.periodic(
const Duration(seconds: 2),
(timer) {
getLogData();
},
);
} else {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
getLogData();
});
}
}
getLogData() async {
HttpResponse<String> response = await Api.inTimeLog(widget.cronId);
if (response.success) {
content = response.bean;
setState(() {});
}
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
child: content == null
? const Center(
child: CupertinoActivityIndicator(),
)
: CupertinoScrollbar(
child: SelectableText(
content!,
),
),
);
}
}

View File

@@ -16,7 +16,7 @@ class TaskBean {
int? isDisabled;
String? logPath;
int? isPinned;
num? lastExecutionTime;
int? lastExecutionTime;
int? lastRunningTime;
String? pid;

View File

@@ -0,0 +1,63 @@
import 'package:json_conversion_annotation/json_conversion_annotation.dart';
@JsonConversion()
class TaskDetailBean {
String? name;
String? command;
String? schedule;
bool? saved;
String? sId;
int? created;
int? status;
String? timestamp;
int? isSystem;
int? isDisabled;
String? logPath;
int? isPinned;
TaskDetailBean(
{this.name,
this.command,
this.schedule,
this.saved,
this.sId,
this.created,
this.status,
this.timestamp,
this.isSystem,
this.isDisabled,
this.logPath,
this.isPinned});
TaskDetailBean.fromJson(Map<String, dynamic> json) {
name = json['name'];
command = json['command'];
schedule = json['schedule'];
saved = json['saved'];
sId = json['_id'];
created = json['created'];
status = json['status'];
timestamp = json['timestamp'];
isSystem = json['isSystem'];
isDisabled = json['isDisabled'];
logPath = json['log_path'];
isPinned = json['isPinned'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['name'] = this.name;
data['command'] = this.command;
data['schedule'] = this.schedule;
data['saved'] = this.saved;
data['_id'] = this.sId;
data['created'] = this.created;
data['status'] = this.status;
data['timestamp'] = this.timestamp;
data['isSystem'] = this.isSystem;
data['isDisabled'] = this.isDisabled;
data['log_path'] = this.logPath;
data['isPinned'] = this.isPinned;
return data;
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qinglong_app/base/base_state_widget.dart';
import 'package:qinglong_app/base/ql_app_bar.dart';
import 'package:qinglong_app/module/task/task_bean.dart';
import 'package:qinglong_app/module/task/task_detail/task_detail_viewmodel.dart';
class TaskDetailPage extends ConsumerStatefulWidget {
final TaskBean taskBean;
const TaskDetailPage(this.taskBean, {Key? key}) : super(key: key);
@override
_TaskDetailPageState createState() => _TaskDetailPageState();
}
class _TaskDetailPageState extends ConsumerState<TaskDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: QlAppBar(
canBack: true,
title: widget.taskBean.name ?? "",
),
body: BaseStateWidget<TaskDetailViewModel>(
model: taskDetailProvider,
builder: (WidgetRef context, TaskDetailViewModel value, Widget? child) {
return Container();
},
onReady: (model){
model.loadDetail(widget.taskBean.sId!);
},
),
);
}
}

View File

@@ -0,0 +1,28 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qinglong_app/base/base_viewmodel.dart';
import 'package:qinglong_app/base/http/api.dart';
import 'package:qinglong_app/base/http/http.dart';
import 'package:qinglong_app/module/task/task_detail/task_detail_bean.dart';
var taskDetailProvider = AutoDisposeChangeNotifierProvider<TaskDetailViewModel>((ref) {
return TaskDetailViewModel();
});
class TaskDetailViewModel extends BaseViewModel {
TaskDetailBean? bean;
TaskDetailViewModel();
Future<void> loadDetail(String id) async {
loading(notify: true);
HttpResponse<TaskDetailBean> response = await Api.taskDetail(id);
if (response.success) {
bean = response.bean;
success();
} else {
failed(response.message, notify: true);
}
}
}

View File

@@ -4,8 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:qinglong_app/base/base_state_widget.dart';
import 'package:qinglong_app/base/theme.dart';
import 'package:qinglong_app/module/task/intime_log/intime_log_page.dart';
import 'package:qinglong_app/module/task/task_bean.dart';
import 'package:qinglong_app/module/task/task_viewmodel.dart';
import 'package:qinglong_app/utils/utils.dart';
class TaskPage extends StatefulWidget {
const TaskPage({Key? key}) : super(key: key);
@@ -15,27 +17,38 @@ class TaskPage extends StatefulWidget {
}
class _TaskPageState extends State<TaskPage> {
String? _searchKey = null;
TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return BaseStateWidget<TaskViewModel>(
builder: (context, model, child) {
builder: (ref, model, child) {
return RefreshIndicator(
onRefresh: () async {
return model.loadData(false);
},
child: ListView.separated(
padding: const EdgeInsets.symmetric(
vertical: 15,
),
child: ListView.builder(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
itemBuilder: (context, index) {
return TaskItemCell(model.list[index]);
},
itemCount: model.list.length,
separatorBuilder: (BuildContext context, int index) {
return const SizedBox(
height: 10,
);
if (index == 0) {
return searchCell(ref);
}
TaskBean item = model.list[index - 1];
if ((item.name == null || item.name!.contains(_searchKey ?? "")) || (item.command == null || item.command!.contains(_searchKey ?? ""))) {
return TaskItemCell(item);
} else {
return const SizedBox.shrink();
}
},
itemCount: model.list.length + 1,
),
);
},
@@ -45,6 +58,43 @@ class _TaskPageState extends State<TaskPage> {
},
);
}
Widget searchCell(WidgetRef context) {
return Container(
color: context.watch(themeProvider).themeColor.searchBarBg(),
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
child: CupertinoSearchTextField(
onSubmitted: (value) {
setState(() {
_searchKey = value;
});
},
onSuffixTap: () {
_searchController.text = "";
setState(() {
_searchKey = "";
});
},
controller: _searchController,
borderRadius: BorderRadius.circular(
30,
),
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 5,
),
prefixInsets: const EdgeInsets.only(
top: 10,
bottom: 6,
left: 15,
),
placeholder: "搜索",
),
);
}
}
class TaskItemCell extends ConsumerWidget {
@@ -63,35 +113,47 @@ class TaskItemCell extends ConsumerWidget {
children: [
SlidableAction(
flex: 1,
icon: CupertinoIcons.memories,
icon: bean.status == 0 ? CupertinoIcons.stop_circle : CupertinoIcons.memories,
foregroundColor: Colors.white,
backgroundColor: Color(0xFF0F77FE),
onPressed: (BuildContext context) {},
backgroundColor: const Color(0xFF0F77FE),
onPressed: (BuildContext context) {
if (bean.status == 0) {
stopCron(context, ref);
} else {
startCron(context, ref);
}
},
),
SlidableAction(
flex: 1,
backgroundColor: Colors.green,
foregroundColor: Colors.white,
icon: CupertinoIcons.clock_fill,
onPressed: (BuildContext context) {},
onPressed: (BuildContext context) {
logCron(context, ref);
},
),
],
),
endActionPane: ActionPane(
motion: ScrollMotion(),
motion: const ScrollMotion(),
extentRatio: 0.15,
children: [
SlidableAction(
backgroundColor: Colors.cyan,
flex: 1,
onPressed: (_) {},
onPressed: (_) {
more(context, ref);
},
foregroundColor: Colors.white,
icon: CupertinoIcons.ellipsis,
),
],
),
child: Container(
padding: EdgeInsets.symmetric(
color: bean.isPinned == 1 ? ref.watch(themeProvider).themeColor.searchBarBg() : Colors.transparent,
margin: const EdgeInsets.only(bottom: 7, top: 7),
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 5,
),
@@ -126,7 +188,7 @@ class TaskItemCell extends ConsumerWidget {
),
const Spacer(),
Text(
"${bean.lastRunningTime ?? "-"}",
(bean.lastExecutionTime == null || bean.lastExecutionTime == 0) ? "-" : Utils.formatMessageTime(bean.lastExecutionTime!),
maxLines: 1,
style: const TextStyle(
overflow: TextOverflow.ellipsis,
@@ -153,4 +215,112 @@ class TaskItemCell extends ConsumerWidget {
),
);
}
startCron(BuildContext context, WidgetRef ref) {
showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text("确认运行"),
content: Text("确认运行定时任务 ${bean.name ?? ""}"),
actions: [
CupertinoDialogAction(
child: const Text("取消"),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: const Text("确定"),
onPressed: () {
Navigator.of(context).pop();
ref.read(taskProvider).runCrons(bean.sId!);
},
),
],
),
);
}
stopCron(BuildContext context, WidgetRef ref) {
showCupertinoDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: const Text("确认停止"),
content: Text("确认停止定时任务 ${bean.name ?? ""}"),
actions: [
CupertinoDialogAction(
child: const Text("取消"),
onPressed: () {
Navigator.of(context).pop();
},
),
CupertinoDialogAction(
child: const Text("确定"),
onPressed: () {
Navigator.of(context).pop();
ref.read(taskProvider).stopCrons(bean.sId!);
},
),
],
),
);
}
logCron(BuildContext context, WidgetRef ref) {
showCupertinoDialog(
builder: (BuildContext context) {
return CupertinoAlertDialog(
title: Text(
"${bean.name}运行日志",
maxLines: 1,
style: const TextStyle(overflow: TextOverflow.ellipsis),
),
content: InTimeLogPage(bean.sId!, bean.status == 0),
actions: [
CupertinoDialogAction(
child: const Text("知道了"),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
context: context);
}
more(BuildContext context, WidgetRef ref) {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) => CupertinoActionSheet(
title: const Text('更多操作'),
actions: <CupertinoActionSheetAction>[
CupertinoActionSheetAction(
child: const Text('编辑'),
onPressed: () {
Navigator.pop(context);
},
),
CupertinoActionSheetAction(
child: Text(bean.isDisabled! == 1 ? "启用" : "禁用"),
onPressed: () {
Navigator.pop(context);
},
),
CupertinoActionSheetAction(
child: const Text('删除'),
onPressed: () {
Navigator.pop(context);
},
),
CupertinoActionSheetAction(
child: Text(bean.isPinned! == 0 ? "置顶" : "取消置顶"),
onPressed: () {
Navigator.pop(context);
},
),
],
),
);
}
}

View File

@@ -20,18 +20,37 @@ class TaskViewModel extends BaseViewModel {
list.clear();
list.addAll(result.bean!);
list.sort((a, b) {
return a.isDisabled! - b.isDisabled!;
});
list.sort((a, b) {
return a.status! - b.status!;
});
sortList();
success();
} else {
list.clear();
failed(result.message, notify: true);
}
}
void sortList() {
list.sort((a, b) {
return a.isDisabled! - b.isDisabled!;
});
list.sort((a, b) {
return a.status! - b.status!;
});
}
Future<void> runCrons(String cron) async {
HttpResponse<NullResponse> result = await Api.startTasks([cron]);
if (result.success) {
list.firstWhere((element) => element.sId == cron).status = 0;
notifyListeners();
}
}
Future<void> stopCrons(String cron) async {
HttpResponse<NullResponse> result = await Api.stopTasks([cron]);
if (result.success) {
list.firstWhere((element) => element.sId == cron).status = 1;
notifyListeners();
}
}
}

View File

@@ -1,7 +1,28 @@
import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
class Utils {
static void hideKeyBoard(BuildContext context) {
FocusScope.of(context).requestFocus(FocusNode());
}
static String formatMessageTime(int time) {
DateTime current = DateTime.now();
DateTime chatTime = DateTime.fromMillisecondsSinceEpoch(time * 1000);
if (current.year == chatTime.year) {
if (current.day == chatTime.day) {
if (chatTime.hour <= 12) {
return DateFormat("上午 H:mm").format(chatTime);
} else {
return DateFormat("下午 H:mm").format(chatTime);
}
} else if (chatTime.day == current.day - 1) {
return DateFormat("昨天 H:mm").format(chatTime);
} else {
return DateFormat("M/d H:mm").format(chatTime);
}
} else {
return DateFormat("yyyy/M/d HH:mm").format(chatTime);
}
}
}