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 createState() => _JobDetailScreenState(); } enum _JobDetailHeaderAction { delete } class _JobDetailScreenState extends State { 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); final Set _selectedWeekdays = {1}; String _contextSource = 'latest_chat'; String _contextWindowMode = 'day'; int _contextWindowCount = 2; final Set _selectedTools = {'memory.write', 'memory.forget'}; @override void initState() { super.initState(); _cubit = JobDetailCubit(sl()); 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( 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.config.schedule.type)), _buildInfoRow('执行时间', _displayRunAt(job.config.schedule)), _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 _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.config.schedule.type)), ], ), ], ), ); } 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, ), if (_scheduleType == 'weekly') ...[ const SizedBox(height: AppSpacing.sm), _buildWeekdaySelector(), ], 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 _buildWeekdaySelector() { const weekdayLabels = { 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六', 7: '周日', }; 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: [ const Text( '执行日', style: TextStyle( color: AppColors.slate500, fontSize: 12, fontWeight: FontWeight.w500, ), ), const SizedBox(height: AppSpacing.sm), Wrap( spacing: AppSpacing.sm, runSpacing: AppSpacing.sm, children: weekdayLabels.entries.map((entry) { final selected = _selectedWeekdays.contains(entry.key); return AppPressable( onTap: () { setState(() { if (selected) { if (_selectedWeekdays.length > 1) { _selectedWeekdays.remove(entry.key); } } else { _selectedWeekdays.add(entry.key); } }); }, 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( entry.value, style: TextStyle( color: selected ? AppColors.blue600 : AppColors.slate600, fontSize: 12, fontWeight: FontWeight.w600, ), ), ), ); }).toList(), ), ], ), ); } Widget _buildToolWrap(List 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 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 _pickScheduleType() async { final picked = await showAppSelectionSheet( context, title: '选择周期', selectedValue: _scheduleType, items: const [ AppSelectionItem(value: 'daily', label: '每日'), AppSelectionItem(value: 'weekly', label: '每周'), ], ); if (picked != null) { setState(() { _scheduleType = picked; if (_scheduleType == 'weekly' && _selectedWeekdays.isEmpty) { _selectedWeekdays.add(1); } }); } } Future _pickTimezone() async { final picked = await showAppSelectionSheet( 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 _pickContextSource() async { final picked = await showAppSelectionSheet( context, title: '选择上下文来源', selectedValue: _contextSource, items: const [AppSelectionItem(value: 'latest_chat', label: '最近聊天')], ); if (picked != null) { setState(() { _contextSource = picked; }); } } Future _pickWindowMode() async { final picked = await showAppSelectionSheet( context, title: '选择窗口模式', selectedValue: _contextWindowMode, items: const [ AppSelectionItem(value: 'day', label: '按天数'), AppSelectionItem(value: 'number', label: '按消息数'), ], ); if (picked != null) { setState(() { _contextWindowMode = picked; }); } } Future _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(ScheduleConfigModel schedule) { final hour = schedule.runAt.hour.toString().padLeft(2, '0'); final minute = schedule.runAt.minute.toString().padLeft(2, '0'); return '$hour:$minute'; } 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 _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, timezone: _timezone, status: 'active', config: AutomationJobConfigModel( inputTemplate: template, enabledTools: _selectedTools.toList(), context: MessageContextConfigModel( source: _contextSource, windowMode: _contextWindowMode, windowCount: _contextWindowCount, ), schedule: ScheduleConfigModel( type: _scheduleType, runAt: ScheduleRunAtModel(hour: _runAt.hour, minute: _runAt.minute), weekdays: _scheduleType == 'weekly' ? (_selectedWeekdays.toList()..sort()) : null, ), ), ); final success = await _cubit.createJob(request); if (!mounted) { return; } if (success) { Toast.show(context, '创建成功', type: ToastType.success); context.pop(true); } } }