feat: 添加自动化任务(automation_jobs)功能模块

This commit is contained in:
qzl
2026-03-24 12:38:11 +08:00
parent f4b7eb7e09
commit 23359c2d01
43 changed files with 4266 additions and 1139 deletions
@@ -1,7 +1,18 @@
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/router/app_routes.dart';
import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/app_loading_indicator.dart';
import '../../../../shared/widgets/app_pressable.dart';
import '../../../../shared/widgets/app_toggle_switch.dart';
import '../../../../shared/widgets/toast/toast.dart';
import '../../../../shared/widgets/toast/toast_type.dart';
import '../../data/models/automation_job_model.dart';
import '../../data/services/automation_jobs_api.dart';
import '../../presentation/cubits/automation_jobs_cubit.dart';
import '../widgets/settings_page_scaffold.dart';
class FeaturesScreen extends StatefulWidget {
@@ -12,27 +23,88 @@ class FeaturesScreen extends StatefulWidget {
}
class _FeaturesScreenState extends State<FeaturesScreen> {
bool _dailyReminderEnabled = true;
bool _dailySummaryEnabled = false;
bool _weeklyReportEnabled = true;
bool _weeklyDigestEnabled = false;
late final AutomationJobsCubit _cubit;
@override
void initState() {
super.initState();
_cubit = AutomationJobsCubit(sl<AutomationJobsApi>());
_cubit.loadJobs();
}
@override
void dispose() {
_cubit.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SettingsPageScaffold(
title: '周期计划',
onBack: () => context.pop(),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('每日'),
const SizedBox(height: 8),
_buildDailyList(),
const SizedBox(height: 16),
_buildSectionTitle('每周'),
const SizedBox(height: 8),
_buildWeeklyList(),
return BlocProvider.value(
value: _cubit,
child: SettingsPageScaffold(
title: '周期计划',
onBack: () => context.pop(),
body: BlocBuilder<AutomationJobsCubit, AutomationJobsState>(
builder: (context, state) {
if (state.isLoading) {
return const Center(child: AppLoadingIndicator());
}
if (state.error != null) {
return Center(child: Text(state.error!));
}
return _buildJobList(state);
},
),
),
);
}
Widget _buildJobList(AutomationJobsState state) {
final dailyJobs = state.dailyJobs;
final weeklyJobs = state.weeklyJobs;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('每日'),
const SizedBox(height: AppSpacing.sm),
if (dailyJobs.isEmpty)
_buildEmptyHint('暂无每日计划')
else
...dailyJobs.map(_buildJobCard),
const SizedBox(height: AppSpacing.lg),
_buildSectionTitle('每周'),
const SizedBox(height: AppSpacing.sm),
if (weeklyJobs.isEmpty)
_buildEmptyHint('暂无每周计划')
else
...weeklyJobs.map(_buildJobCard),
if (state.canCreateMore) ...[
const SizedBox(height: AppSpacing.lg),
_buildCreateButton(),
],
],
);
}
Widget _buildEmptyHint(String text) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: AppSpacing.sm),
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: AppColors.borderSecondary),
),
child: Text(
text,
style: const TextStyle(
color: AppColors.slate500,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
);
}
@@ -48,122 +120,95 @@ class _FeaturesScreenState extends State<FeaturesScreen> {
);
}
Widget _buildDailyList() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildFeatureCard(
icon: Icons.alarm,
iconColor: const Color(0xFF14B8A6),
iconBg: const Color(0xFFECFEFF),
iconBorder: const Color(0xFFC9F4F2),
title: '会议提醒',
subtitle: '每次会议前 15 分钟提醒',
value: _dailyReminderEnabled,
onChanged: (v) => setState(() => _dailyReminderEnabled = v),
Widget _buildJobCard(AutomationJobModel job) {
return AppPressable(
onTap: () async {
await context.push(AppRoutes.settingsJobDetail(job.id));
if (!mounted) {
return;
}
_cubit.loadJobs();
},
borderRadius: BorderRadius.circular(AppRadius.lg),
child: Container(
margin: const EdgeInsets.only(bottom: AppSpacing.sm),
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(AppRadius.lg),
border: Border.all(color: AppColors.borderSecondary),
),
const SizedBox(height: 10),
_buildFeatureCard(
icon: Icons.summarize,
iconColor: const Color(0xFF2563EB),
iconBg: const Color(0xFFEEF6FF),
iconBorder: const Color(0xFFDCEAFF),
title: '每日摘要',
subtitle: '每天 18:00 发送当日摘要',
value: _dailySummaryEnabled,
onChanged: (v) => setState(() => _dailySummaryEnabled = v),
child: Row(
children: [
Container(
width: AppSpacing.xxl + AppSpacing.lg,
height: AppSpacing.xxl + AppSpacing.lg,
decoration: BoxDecoration(
color: AppColors.surfaceTertiary,
borderRadius: BorderRadius.circular(AppRadius.md),
),
child: const Icon(
Icons.auto_awesome,
size: AppSpacing.lg,
color: AppColors.blue500,
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
job.title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: AppSpacing.xs),
Text(
_buildSubtitle(job),
style: const TextStyle(
fontSize: 12,
color: AppColors.slate500,
),
),
],
),
),
const SizedBox(width: AppSpacing.sm),
AppToggleSwitch(
value: job.isActive,
onChanged: (next) {
if (job.isSystem) {
Toast.show(context, '系统预置任务状态不可修改', type: ToastType.info);
return;
}
_cubit.updateJobStatus(id: job.id, enabled: next);
},
),
],
),
],
),
);
}
Widget _buildWeeklyList() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildFeatureCard(
icon: Icons.calendar_view_week,
iconColor: AppColors.success,
iconBg: const Color(0xFFECFDF5),
iconBorder: const Color(0xFFCDEEDC),
title: '周报生成',
subtitle: '每周一自动生成周报',
value: _weeklyReportEnabled,
onChanged: (v) => setState(() => _weeklyReportEnabled = v),
),
const SizedBox(height: 10),
_buildFeatureCard(
icon: Icons.article,
iconColor: AppColors.warning,
iconBg: const Color(0xFFFFF7ED),
iconBorder: const Color(0xFFFDE6CD),
title: '每周摘要',
subtitle: '每周日发送本周活动汇总',
value: _weeklyDigestEnabled,
onChanged: (v) => setState(() => _weeklyDigestEnabled = v),
),
],
);
String _buildSubtitle(AutomationJobModel job) {
final statusText = job.isActive ? '已启用' : '未启用';
final sourceText = job.isSystem ? '系统预置' : '自定义';
return '$sourceText$statusText';
}
Widget _buildFeatureCard({
required IconData icon,
required Color iconColor,
required Color iconBg,
required Color iconBorder,
required String title,
required String subtitle,
required bool value,
required ValueChanged<bool> onChanged,
}) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppColors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.borderSecondary),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: iconBg,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: iconBorder),
),
child: Icon(icon, size: 18, color: iconColor),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppColors.slate900,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: AppColors.slate500,
),
),
],
),
),
AppToggleSwitch(value: value, onChanged: onChanged),
],
),
Widget _buildCreateButton() {
return AppButton(
text: '创建任务',
onPressed: () async {
await context.push(AppRoutes.settingsJobNew);
if (!mounted) {
return;
}
_cubit.loadJobs();
},
);
}
}
@@ -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);
}
}
}
@@ -17,6 +17,7 @@ import 'package:social_app/features/auth/presentation/bloc/auth_event.dart';
import 'package:social_app/features/auth/presentation/bloc/auth_state.dart';
import 'package:social_app/features/friends/data/friends_api.dart';
import 'package:social_app/features/settings/data/settings_api.dart';
import 'package:social_app/features/settings/data/services/automation_jobs_api.dart';
import 'package:social_app/features/settings/data/services/settings_user_cache.dart';
import 'package:social_app/features/users/data/models/user_response.dart';
import 'package:social_app/features/home/ui/navigation/home_return_policy.dart';
@@ -34,12 +35,15 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> {
final FriendsApi _friendsApi = sl<FriendsApi>();
final AutomationJobsApi _automationJobsApi = sl<AutomationJobsApi>();
final SettingsUserCache _userCache = sl<SettingsUserCache>();
UserResponse? _user;
bool _isLoading = true;
int _friendsCount = 0;
String? _firstFriendName;
int _enabledJobsCount = 0;
String? _firstEnabledJobTitle;
@override
void initState() {
@@ -83,6 +87,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
} catch (e) {
// Keep profile available even when contacts fail.
}
try {
final jobs = await _automationJobsApi.list();
final enabledJobs = jobs.where((job) => job.isActive).toList();
if (mounted) {
setState(() {
_enabledJobsCount = enabledJobs.length;
_firstEnabledJobTitle = enabledJobs.isNotEmpty
? enabledJobs.first.title
: null;
});
}
} catch (e) {
// Keep profile available even when automation jobs fail.
}
}
@override
@@ -298,6 +317,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
return '已添加 $_friendsCount 位联系人';
}
String _buildAutomationSubtitle() {
if (_enabledJobsCount == 0) {
return '暂无启用计划';
}
if (_enabledJobsCount == 1) {
return '已启用:${_firstEnabledJobTitle ?? '周期计划'}';
}
return '已启用 $_enabledJobsCount 个计划';
}
Widget _buildQuickActions(BuildContext context) {
return Row(
children: [
@@ -314,9 +343,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
Expanded(
child: _buildActionCard(
icon: Icons.auto_awesome,
iconColor: AppColors.violet500,
iconColor: AppColors.blue500,
title: '周期计划',
subtitle: '已启用:会议提醒',
subtitle: _buildAutomationSubtitle(),
onTap: () => context.push(AppRoutes.settingsFeatures),
),
),