feat: 添加自动化任务(automation_jobs)功能模块
This commit is contained in:
@@ -0,0 +1,783 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/app_input.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../../shared/widgets/app_pressable.dart';
|
||||
import '../../../../shared/widgets/app_selection_sheet.dart';
|
||||
import '../../../../shared/widgets/detail_header_action_menu.dart';
|
||||
import '../../../../shared/widgets/destructive_action_sheet.dart';
|
||||
import '../../../../shared/widgets/toast/toast.dart';
|
||||
import '../../../../shared/widgets/toast/toast_type.dart';
|
||||
import '../../../../shared/utils/tool_name_localizer.dart';
|
||||
import '../../data/models/automation_job_model.dart';
|
||||
import '../../data/services/automation_jobs_api.dart';
|
||||
import '../../presentation/cubits/job_detail_cubit.dart';
|
||||
import '../widgets/settings_page_scaffold.dart';
|
||||
|
||||
class JobDetailScreen extends StatefulWidget {
|
||||
const JobDetailScreen({super.key, this.jobId});
|
||||
|
||||
final String? jobId;
|
||||
|
||||
@override
|
||||
State<JobDetailScreen> createState() => _JobDetailScreenState();
|
||||
}
|
||||
|
||||
enum _JobDetailHeaderAction { delete }
|
||||
|
||||
class _JobDetailScreenState extends State<JobDetailScreen> {
|
||||
late final JobDetailCubit _cubit;
|
||||
final TextEditingController _titleController = TextEditingController();
|
||||
final TextEditingController _templateController = TextEditingController();
|
||||
|
||||
String _scheduleType = 'daily';
|
||||
String _timezone = 'Asia/Shanghai';
|
||||
TimeOfDay _runAt = const TimeOfDay(hour: 8, minute: 0);
|
||||
String _contextSource = 'latest_chat';
|
||||
String _contextWindowMode = 'day';
|
||||
int _contextWindowCount = 2;
|
||||
final Set<String> _selectedTools = <String>{'memory.write', 'memory.forget'};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cubit = JobDetailCubit(sl<AutomationJobsApi>());
|
||||
if (widget.jobId != null) {
|
||||
_cubit.loadJob(widget.jobId!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
_templateController.dispose();
|
||||
_cubit.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cubit,
|
||||
child: BlocConsumer<JobDetailCubit, JobDetailState>(
|
||||
listener: (context, state) {
|
||||
if (state.error != null) {
|
||||
Toast.show(context, state.error!, type: ToastType.error);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return SettingsPageScaffold(
|
||||
title: '加载中',
|
||||
body: const Center(child: AppLoadingIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final job = state.job;
|
||||
final isEditMode = widget.jobId != null;
|
||||
if (isEditMode && job == null && state.error != null) {
|
||||
return SettingsPageScaffold(
|
||||
title: '任务详情',
|
||||
onBack: () => context.pop(),
|
||||
body: _buildLoadFailedView(state.error!),
|
||||
);
|
||||
}
|
||||
|
||||
return SettingsPageScaffold(
|
||||
title: job?.title ?? '新建周期计划',
|
||||
onBack: () => context.pop(),
|
||||
trailing: job != null && !job.isSystem
|
||||
? _buildHeaderActions(job.id, state)
|
||||
: null,
|
||||
body: job == null
|
||||
? _buildCreateForm(state)
|
||||
: _buildDetailPage(job, state),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadFailedView(String error) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('加载失败'),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
error,
|
||||
style: const TextStyle(
|
||||
color: AppColors.error,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
AppButton(text: '重试', onPressed: () => _cubit.loadJob(widget.jobId!)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailPage(AutomationJobModel job, JobDetailState state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildOverviewCard(job),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildSectionTitle('计划配置'),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_buildInfoCard([
|
||||
_buildInfoRow('周期', _scheduleLabel(job.scheduleType)),
|
||||
_buildInfoRow('执行时间', _displayRunAt(job.runAt)),
|
||||
_buildInfoRow('时区', job.timezone),
|
||||
_buildInfoRow('状态', job.isActive ? '已启用' : '未启用'),
|
||||
]),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildSectionTitle('输入模板'),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_buildTextBlock(job.config.inputTemplate),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildSectionTitle('启用工具'),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_buildToolWrap(job.config.enabledTools),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildSectionTitle('上下文消息模式'),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_buildInfoCard([
|
||||
_buildInfoRow('来源', _contextSourceLabel(job.config.context.source)),
|
||||
_buildInfoRow(
|
||||
'窗口模式',
|
||||
_windowModeLabel(job.config.context.windowMode),
|
||||
),
|
||||
_buildInfoRow('窗口数量', '${job.config.context.windowCount}'),
|
||||
]),
|
||||
if (!job.isSystem && state.isSaving)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: AppSpacing.lg),
|
||||
child: Center(child: AppLoadingIndicator(size: AppSpacing.lg)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderActions(String jobId, JobDetailState state) {
|
||||
return DetailHeaderActionMenu<_JobDetailHeaderAction>(
|
||||
items: const [
|
||||
DetailHeaderActionItem<_JobDetailHeaderAction>(
|
||||
value: _JobDetailHeaderAction.delete,
|
||||
label: '删除',
|
||||
icon: Icons.delete_outline,
|
||||
isDestructive: true,
|
||||
),
|
||||
],
|
||||
onSelected: (action) {
|
||||
if (state.isSaving) {
|
||||
return;
|
||||
}
|
||||
if (action == _JobDetailHeaderAction.delete) {
|
||||
unawaited(_confirmAndDelete(jobId));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmAndDelete(String jobId) async {
|
||||
final confirmed = await showDestructiveActionSheet(
|
||||
context,
|
||||
title: '删除周期计划',
|
||||
message: '删除后将无法恢复,是否继续?',
|
||||
confirmText: '确认删除',
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
final success = await _cubit.deleteJob(jobId);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (success) {
|
||||
Toast.show(context, '删除成功', type: ToastType.success);
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildOverviewCard(AutomationJobModel job) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [AppColors.white, AppColors.surfaceInfoLight],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.borderTertiary),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
job.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Wrap(
|
||||
spacing: AppSpacing.sm,
|
||||
runSpacing: AppSpacing.sm,
|
||||
children: [
|
||||
_buildBadge(job.isSystem ? '系统预置' : '自定义'),
|
||||
_buildBadge(job.isActive ? '已启用' : '未启用'),
|
||||
_buildBadge(_scheduleLabel(job.scheduleType)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBadge(String text) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.xs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: AppColors.slate600,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCreateForm(JobDetailState state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildCreateBasicSection(),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildCreateRuleSection(),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildCreateToolSection(),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
_buildCreateContextSection(),
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
AppButton(
|
||||
text: '创建任务',
|
||||
isLoading: state.isSaving,
|
||||
onPressed: state.isSaving ? null : _submitCreate,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCreateBasicSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('基本信息'),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
AppInput(label: '任务名称', hint: '请输入任务名称', controller: _titleController),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
AppInput(
|
||||
label: '输入模板',
|
||||
hint: '例如:请总结今天的记忆内容',
|
||||
controller: _templateController,
|
||||
maxLines: 4,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCreateRuleSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('执行规则'),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_buildPickerTile(
|
||||
label: '周期',
|
||||
value: _scheduleLabel(_scheduleType),
|
||||
onTap: _pickScheduleType,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_buildPickerTile(
|
||||
label: '执行时间',
|
||||
value: _formatTime(_runAt),
|
||||
onTap: _pickRunAt,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_buildPickerTile(label: '时区', value: _timezone, onTap: _pickTimezone),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCreateToolSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('工具选择'),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_buildToolSelector(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCreateContextSection() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildSectionTitle('上下文消息模式'),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_buildPickerTile(
|
||||
label: '来源',
|
||||
value: _contextSourceLabel(_contextSource),
|
||||
onTap: _pickContextSource,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_buildPickerTile(
|
||||
label: '窗口模式',
|
||||
value: _windowModeLabel(_contextWindowMode),
|
||||
onTap: _pickWindowMode,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_buildCounterTile(
|
||||
label: '窗口数量',
|
||||
value: _contextWindowCount,
|
||||
onMinus: _contextWindowCount > 1
|
||||
? () {
|
||||
setState(() {
|
||||
_contextWindowCount -= 1;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
onPlus: _contextWindowCount < 200
|
||||
? () {
|
||||
setState(() {
|
||||
_contextWindowCount += 1;
|
||||
});
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPickerTile({
|
||||
required String label,
|
||||
required String value,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return AppPressable(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: AppColors.slate500,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: AppColors.slate800,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Icon(Icons.keyboard_arrow_down, color: AppColors.slate400),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCounterTile({
|
||||
required String label,
|
||||
required int value,
|
||||
required VoidCallback? onMinus,
|
||||
required VoidCallback? onPlus,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'$label:$value',
|
||||
style: const TextStyle(
|
||||
color: AppColors.slate800,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildCounterAction(icon: Icons.remove, onTap: onMinus),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
_buildCounterAction(icon: Icons.add, onTap: onPlus),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCounterAction({
|
||||
required IconData icon,
|
||||
required VoidCallback? onTap,
|
||||
}) {
|
||||
return AppPressable(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
child: Container(
|
||||
width: AppSpacing.xxl + AppSpacing.md,
|
||||
height: AppSpacing.xxl + AppSpacing.md,
|
||||
decoration: BoxDecoration(
|
||||
color: onTap == null ? AppColors.slate100 : AppColors.surfaceTertiary,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: AppSpacing.lg,
|
||||
color: onTap == null ? AppColors.slate300 : AppColors.blue500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToolSelector() {
|
||||
return Wrap(
|
||||
spacing: AppSpacing.sm,
|
||||
runSpacing: AppSpacing.sm,
|
||||
children: automationToolOptions.map((toolName) {
|
||||
final selected = _selectedTools.contains(toolName);
|
||||
return AppPressable(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
_selectedTools.remove(toolName);
|
||||
} else {
|
||||
_selectedTools.add(toolName);
|
||||
}
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? AppColors.blue50 : AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
border: Border.all(
|
||||
color: selected ? AppColors.blue300 : AppColors.borderSecondary,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
localizeToolName(toolName),
|
||||
style: TextStyle(
|
||||
color: selected ? AppColors.blue600 : AppColors.slate600,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildToolWrap(List<String> tools) {
|
||||
if (tools.isEmpty) {
|
||||
return _buildTextBlock('未启用工具');
|
||||
}
|
||||
return Wrap(
|
||||
spacing: AppSpacing.sm,
|
||||
runSpacing: AppSpacing.sm,
|
||||
children: tools
|
||||
.map((tool) => _buildBadge(localizeToolName(tool)))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextBlock(String text) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
color: AppColors.slate700,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSectionTitle(String title) {
|
||||
return Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate500,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoCard(List<Widget> children) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: AppSpacing.xs),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: AppColors.slate500,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
textAlign: TextAlign.right,
|
||||
style: const TextStyle(
|
||||
color: AppColors.slate800,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pickScheduleType() async {
|
||||
final picked = await showAppSelectionSheet<String>(
|
||||
context,
|
||||
title: '选择周期',
|
||||
selectedValue: _scheduleType,
|
||||
items: const [
|
||||
AppSelectionItem(value: 'daily', label: '每日'),
|
||||
AppSelectionItem(value: 'weekly', label: '每周'),
|
||||
],
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_scheduleType = picked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickTimezone() async {
|
||||
final picked = await showAppSelectionSheet<String>(
|
||||
context,
|
||||
title: '选择时区',
|
||||
selectedValue: _timezone,
|
||||
items: const [
|
||||
AppSelectionItem(value: 'Asia/Shanghai', label: 'Asia/Shanghai'),
|
||||
AppSelectionItem(value: 'UTC', label: 'UTC'),
|
||||
],
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_timezone = picked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickContextSource() async {
|
||||
final picked = await showAppSelectionSheet<String>(
|
||||
context,
|
||||
title: '选择上下文来源',
|
||||
selectedValue: _contextSource,
|
||||
items: const [AppSelectionItem(value: 'latest_chat', label: '最近聊天')],
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_contextSource = picked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickWindowMode() async {
|
||||
final picked = await showAppSelectionSheet<String>(
|
||||
context,
|
||||
title: '选择窗口模式',
|
||||
selectedValue: _contextWindowMode,
|
||||
items: const [
|
||||
AppSelectionItem(value: 'day', label: '按天数'),
|
||||
AppSelectionItem(value: 'number', label: '按消息数'),
|
||||
],
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_contextWindowMode = picked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickRunAt() async {
|
||||
final picked = await showTimePicker(context: context, initialTime: _runAt);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
_runAt = picked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
String _formatTime(TimeOfDay time) {
|
||||
final hour = time.hour.toString().padLeft(2, '0');
|
||||
final minute = time.minute.toString().padLeft(2, '0');
|
||||
return '$hour:$minute:00';
|
||||
}
|
||||
|
||||
String _displayRunAt(String runAtRaw) {
|
||||
try {
|
||||
final dt = DateTime.parse(runAtRaw).toLocal();
|
||||
final hour = dt.hour.toString().padLeft(2, '0');
|
||||
final minute = dt.minute.toString().padLeft(2, '0');
|
||||
return '$hour:$minute';
|
||||
} catch (_) {
|
||||
return runAtRaw;
|
||||
}
|
||||
}
|
||||
|
||||
String _scheduleLabel(String scheduleType) {
|
||||
final normalized = scheduleType.toLowerCase();
|
||||
if (normalized == 'daily') {
|
||||
return '每日';
|
||||
}
|
||||
if (normalized == 'weekly') {
|
||||
return '每周';
|
||||
}
|
||||
return scheduleType;
|
||||
}
|
||||
|
||||
String _contextSourceLabel(String source) {
|
||||
if (source == 'latest_chat') {
|
||||
return '最近聊天';
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
String _windowModeLabel(String mode) {
|
||||
if (mode == 'day') {
|
||||
return '按天数';
|
||||
}
|
||||
if (mode == 'number') {
|
||||
return '按消息数';
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
Future<void> _submitCreate() async {
|
||||
final title = _titleController.text.trim();
|
||||
final template = _templateController.text.trim();
|
||||
if (title.isEmpty || template.isEmpty) {
|
||||
Toast.show(context, '请填写完整信息', type: ToastType.error);
|
||||
return;
|
||||
}
|
||||
|
||||
final request = AutomationJobCreateRequest(
|
||||
title: title,
|
||||
scheduleType: _scheduleType,
|
||||
runAt: _formatTime(_runAt),
|
||||
timezone: _timezone,
|
||||
status: 'active',
|
||||
config: AutomationJobConfigModel(
|
||||
inputTemplate: template,
|
||||
enabledTools: _selectedTools.toList(),
|
||||
context: MessageContextConfigModel(
|
||||
source: _contextSource,
|
||||
windowMode: _contextWindowMode,
|
||||
windowCount: _contextWindowCount,
|
||||
),
|
||||
),
|
||||
);
|
||||
final success = await _cubit.createJob(request);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (success) {
|
||||
Toast.show(context, '创建成功', type: ToastType.success);
|
||||
context.pop(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user