feat: add Flutter schema-driven form cards
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import 'package:aioa_mobile/app/router.dart';
|
||||
import 'package:aioa_mobile/app/theme.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AioaApp extends StatelessWidget {
|
||||
const AioaApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
title: 'AIOA',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: buildAioaTheme(),
|
||||
routerConfig: appRouter,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:aioa_mobile/app/shell.dart';
|
||||
import 'package:aioa_mobile/features/assistant/presentation/assistant_page.dart';
|
||||
import 'package:aioa_mobile/features/form/presentation/leave_form_page.dart';
|
||||
import 'package:aioa_mobile/features/profile/presentation/profile_page.dart';
|
||||
import 'package:aioa_mobile/features/tasks/presentation/tasks_page.dart';
|
||||
import 'package:aioa_mobile/features/workspace/presentation/workspace_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
final appRouter = GoRouter(
|
||||
initialLocation: '/workspace',
|
||||
routes: [
|
||||
ShellRoute(
|
||||
builder: (context, state, child) =>
|
||||
AioaShell(location: state.uri.path, child: child),
|
||||
routes: [
|
||||
GoRoute(path: '/workspace', builder: (_, _) => const WorkspacePage()),
|
||||
GoRoute(path: '/assistant', builder: (_, _) => const AssistantPage()),
|
||||
GoRoute(path: '/tasks', builder: (_, _) => const TasksPage()),
|
||||
GoRoute(path: '/profile', builder: (_, _) => const ProfilePage()),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/leave/new',
|
||||
pageBuilder: (context, state) => MaterialPage<void>(
|
||||
fullscreenDialog: true,
|
||||
child: const LeaveFormPage(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class AioaShell extends StatelessWidget {
|
||||
const AioaShell({super.key, required this.location, required this.child});
|
||||
|
||||
final String location;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final index = switch (location) {
|
||||
String path when path.startsWith('/assistant') => 1,
|
||||
String path when path.startsWith('/tasks') => 2,
|
||||
String path when path.startsWith('/profile') => 3,
|
||||
_ => 0,
|
||||
};
|
||||
return Scaffold(
|
||||
body: SafeArea(child: child),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: index,
|
||||
onDestinationSelected: (value) {
|
||||
const paths = ['/workspace', '/assistant', '/tasks', '/profile'];
|
||||
context.go(paths[value]);
|
||||
},
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.grid_view_rounded),
|
||||
label: '工作台',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.auto_awesome_rounded),
|
||||
label: 'AI 助手',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.task_alt_rounded),
|
||||
label: '待办',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.person_outline_rounded),
|
||||
label: '我的',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
ThemeData buildAioaTheme() {
|
||||
const seed = Color(0xFF3157D5);
|
||||
final scheme = ColorScheme.fromSeed(
|
||||
seedColor: seed,
|
||||
brightness: Brightness.light,
|
||||
surface: const Color(0xFFF7F8FC),
|
||||
);
|
||||
return ThemeData(
|
||||
colorScheme: scheme,
|
||||
useMaterial3: true,
|
||||
scaffoldBackgroundColor: scheme.surface,
|
||||
cardTheme: const CardThemeData(
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
side: BorderSide(color: Color(0xFFE6E8F0)),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: const InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(14)),
|
||||
borderSide: BorderSide(color: Color(0xFFDDE1EA)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(14)),
|
||||
borderSide: BorderSide(color: Color(0xFFDDE1EA)),
|
||||
),
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import 'package:aioa_mobile/core/forms/schema/form_schema.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DynamicFormCard extends StatelessWidget {
|
||||
const DynamicFormCard({
|
||||
super.key,
|
||||
required this.definition,
|
||||
required this.state,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final DynamicFormDefinition definition;
|
||||
final DynamicFormState state;
|
||||
final void Function(String field, Object? value) onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
definition.uiSchema.description,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
for (final section in definition.uiSchema.sections) ...[
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
section.title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
for (
|
||||
var index = 0;
|
||||
index < section.controls.length;
|
||||
index++
|
||||
) ...[
|
||||
_buildControl(context, section.controls[index]),
|
||||
if (index != section.controls.length - 1)
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildControl(BuildContext context, FormControlSchema control) {
|
||||
final field = definition.dataSchema.properties[control.field];
|
||||
if (field == null) {
|
||||
return Text(
|
||||
'未识别字段:${control.field}',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
);
|
||||
}
|
||||
final required = definition.dataSchema.required.contains(control.field);
|
||||
final label = required ? '${control.label} *' : control.label;
|
||||
final error = state.errors[control.field];
|
||||
return switch (control.control) {
|
||||
FormControlType.text || FormControlType.textArea => TextFormField(
|
||||
key: ValueKey(control.field),
|
||||
initialValue: state.values[control.field] as String?,
|
||||
minLines: control.control == FormControlType.textArea ? 3 : 1,
|
||||
maxLines: control.control == FormControlType.textArea ? 6 : 1,
|
||||
maxLength: field.maxLength,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
hintText: control.placeholder,
|
||||
helperText: control.helperText,
|
||||
errorText: error,
|
||||
),
|
||||
onChanged: (value) => onChanged(control.field, value),
|
||||
),
|
||||
FormControlType.select => DropdownButtonFormField<String>(
|
||||
key: ValueKey(control.field),
|
||||
initialValue: state.values[control.field] as String?,
|
||||
decoration: InputDecoration(labelText: label, errorText: error),
|
||||
items: field.enumValues
|
||||
.map(
|
||||
(value) => DropdownMenuItem(
|
||||
value: value,
|
||||
child: Text(control.optionLabels[value] ?? value),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) => onChanged(control.field, value),
|
||||
),
|
||||
FormControlType.dateTime => _DateTimeControl(
|
||||
label: label,
|
||||
value: state.values[control.field] as String?,
|
||||
errorText: error,
|
||||
onChanged: (value) => onChanged(control.field, value),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _DateTimeControl extends StatelessWidget {
|
||||
const _DateTimeControl({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.errorText,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String? value;
|
||||
final String? errorText;
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final parsed = value == null ? null : DateTime.tryParse(value!);
|
||||
final display = parsed == null
|
||||
? '请选择日期和时间'
|
||||
: DateFormat('yyyy年MM月dd日 HH:mm').format(parsed.toLocal());
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
onTap: () => _pick(context, parsed),
|
||||
child: InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
errorText: errorText,
|
||||
suffixIcon: const Icon(Icons.calendar_month_outlined),
|
||||
),
|
||||
child: Text(
|
||||
display,
|
||||
style: TextStyle(
|
||||
color: parsed == null ? Theme.of(context).hintColor : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _pick(BuildContext context, DateTime? current) async {
|
||||
final now = DateTime.now();
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: current?.toLocal() ?? now,
|
||||
firstDate: now.subtract(const Duration(days: 30)),
|
||||
lastDate: now.add(const Duration(days: 730)),
|
||||
);
|
||||
if (date == null || !context.mounted) return;
|
||||
final time = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(current?.toLocal() ?? now),
|
||||
);
|
||||
if (time == null) return;
|
||||
onChanged(
|
||||
DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
time.hour,
|
||||
time.minute,
|
||||
).toUtc().toIso8601String(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
enum JsonValueType { string, number, boolean }
|
||||
|
||||
enum FormControlType { text, textArea, select, dateTime }
|
||||
|
||||
class DynamicFormDefinition {
|
||||
const DynamicFormDefinition({
|
||||
required this.dataSchema,
|
||||
required this.uiSchema,
|
||||
});
|
||||
|
||||
final JsonFormSchema dataSchema;
|
||||
final FormUiSchema uiSchema;
|
||||
}
|
||||
|
||||
class JsonFormSchema {
|
||||
const JsonFormSchema({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.properties,
|
||||
this.required = const {},
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String title;
|
||||
final Map<String, JsonFieldSchema> properties;
|
||||
final Set<String> required;
|
||||
}
|
||||
|
||||
class JsonFieldSchema {
|
||||
const JsonFieldSchema({
|
||||
required this.type,
|
||||
this.format,
|
||||
this.enumValues = const [],
|
||||
this.minLength,
|
||||
this.maxLength,
|
||||
});
|
||||
|
||||
final JsonValueType type;
|
||||
final String? format;
|
||||
final List<String> enumValues;
|
||||
final int? minLength;
|
||||
final int? maxLength;
|
||||
}
|
||||
|
||||
class FormUiSchema {
|
||||
const FormUiSchema({required this.description, required this.sections});
|
||||
|
||||
final String description;
|
||||
final List<FormSectionSchema> sections;
|
||||
}
|
||||
|
||||
class FormSectionSchema {
|
||||
const FormSectionSchema({required this.title, required this.controls});
|
||||
|
||||
final String title;
|
||||
final List<FormControlSchema> controls;
|
||||
}
|
||||
|
||||
class FormControlSchema {
|
||||
const FormControlSchema({
|
||||
required this.field,
|
||||
required this.label,
|
||||
required this.control,
|
||||
this.placeholder,
|
||||
this.helperText,
|
||||
this.optionLabels = const {},
|
||||
});
|
||||
|
||||
final String field;
|
||||
final String label;
|
||||
final FormControlType control;
|
||||
final String? placeholder;
|
||||
final String? helperText;
|
||||
final Map<String, String> optionLabels;
|
||||
}
|
||||
|
||||
class DynamicFormState {
|
||||
const DynamicFormState({this.values = const {}, this.errors = const {}});
|
||||
|
||||
final Map<String, Object?> values;
|
||||
final Map<String, String> errors;
|
||||
|
||||
DynamicFormState copyWith({
|
||||
Map<String, Object?>? values,
|
||||
Map<String, String>? errors,
|
||||
}) {
|
||||
return DynamicFormState(
|
||||
values: values ?? this.values,
|
||||
errors: errors ?? this.errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:aioa_mobile/core/forms/schema/form_schema.dart';
|
||||
|
||||
Map<String, String> validateDynamicForm(
|
||||
JsonFormSchema schema,
|
||||
Map<String, Object?> values,
|
||||
) {
|
||||
final errors = <String, String>{};
|
||||
for (final entry in schema.properties.entries) {
|
||||
final key = entry.key;
|
||||
final value = values[key];
|
||||
final field = entry.value;
|
||||
final empty = value == null || (value is String && value.trim().isEmpty);
|
||||
if (schema.required.contains(key) && empty) {
|
||||
errors[key] = '此项为必填项';
|
||||
continue;
|
||||
}
|
||||
if (empty) continue;
|
||||
if (value is String) {
|
||||
if (field.minLength != null && value.trim().length < field.minLength!) {
|
||||
errors[key] = '至少输入 ${field.minLength} 个字符';
|
||||
} else if (field.maxLength != null && value.length > field.maxLength!) {
|
||||
errors[key] = '最多输入 ${field.maxLength} 个字符';
|
||||
} else if (field.enumValues.isNotEmpty &&
|
||||
!field.enumValues.contains(value)) {
|
||||
errors[key] = '选项无效';
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AssistantPage extends StatelessWidget {
|
||||
const AssistantPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
const Center(child: Text('AI 助手将在下一阶段接入'));
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:aioa_mobile/core/forms/schema/form_schema.dart';
|
||||
import 'package:aioa_mobile/core/forms/schema/form_validator.dart';
|
||||
import 'package:aioa_mobile/features/form/domain/leave_form_definition.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
final leaveDraftProvider =
|
||||
NotifierProvider<LeaveDraftController, DynamicFormState>(
|
||||
LeaveDraftController.new,
|
||||
);
|
||||
|
||||
class LeaveDraftController extends Notifier<DynamicFormState> {
|
||||
@override
|
||||
DynamicFormState build() => const DynamicFormState();
|
||||
|
||||
void setValue(String field, Object? value) {
|
||||
final values = {...state.values, field: value};
|
||||
final errors = {...state.errors}..remove(field);
|
||||
state = state.copyWith(values: values, errors: errors);
|
||||
}
|
||||
|
||||
void applyAiSuggestion() {
|
||||
final start = DateTime.now().add(const Duration(days: 1));
|
||||
final normalizedStart = DateTime(
|
||||
start.year,
|
||||
start.month,
|
||||
start.day,
|
||||
13,
|
||||
30,
|
||||
);
|
||||
final end = normalizedStart.add(const Duration(hours: 4));
|
||||
state = DynamicFormState(
|
||||
values: {
|
||||
'type': 'PERSONAL',
|
||||
'startsAt': normalizedStart.toUtc().toIso8601String(),
|
||||
'endsAt': end.toUtc().toIso8601String(),
|
||||
'reason': '办理个人事务,已提前完成工作交接。',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool validate() {
|
||||
final errors = validateDynamicForm(
|
||||
leaveFormDefinition.dataSchema,
|
||||
state.values,
|
||||
);
|
||||
final startsAt = DateTime.tryParse(
|
||||
state.values['startsAt'] as String? ?? '',
|
||||
);
|
||||
final endsAt = DateTime.tryParse(state.values['endsAt'] as String? ?? '');
|
||||
if (startsAt != null && endsAt != null && !endsAt.isAfter(startsAt)) {
|
||||
errors['endsAt'] = '结束时间必须晚于开始时间';
|
||||
}
|
||||
state = state.copyWith(errors: errors);
|
||||
return errors.isEmpty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:aioa_mobile/core/forms/schema/form_schema.dart';
|
||||
|
||||
const leaveFormDefinition = DynamicFormDefinition(
|
||||
dataSchema: JsonFormSchema(
|
||||
id: 'leave-request-v1',
|
||||
title: '请假申请',
|
||||
required: {'type', 'startsAt', 'endsAt', 'reason'},
|
||||
properties: {
|
||||
'type': JsonFieldSchema(
|
||||
type: JsonValueType.string,
|
||||
enumValues: ['PERSONAL', 'SICK', 'ANNUAL'],
|
||||
),
|
||||
'startsAt': JsonFieldSchema(
|
||||
type: JsonValueType.string,
|
||||
format: 'date-time',
|
||||
),
|
||||
'endsAt': JsonFieldSchema(
|
||||
type: JsonValueType.string,
|
||||
format: 'date-time',
|
||||
),
|
||||
'reason': JsonFieldSchema(
|
||||
type: JsonValueType.string,
|
||||
minLength: 1,
|
||||
maxLength: 2000,
|
||||
),
|
||||
},
|
||||
),
|
||||
uiSchema: FormUiSchema(
|
||||
description: '表单卡片由服务端 Schema 自动生成,字段、顺序、控件和校验均可版本化。',
|
||||
sections: [
|
||||
FormSectionSchema(
|
||||
title: '请假信息',
|
||||
controls: [
|
||||
FormControlSchema(
|
||||
field: 'type',
|
||||
label: '请假类型',
|
||||
control: FormControlType.select,
|
||||
optionLabels: {'PERSONAL': '事假', 'SICK': '病假', 'ANNUAL': '年假'},
|
||||
),
|
||||
FormControlSchema(
|
||||
field: 'startsAt',
|
||||
label: '开始时间',
|
||||
control: FormControlType.dateTime,
|
||||
),
|
||||
FormControlSchema(
|
||||
field: 'endsAt',
|
||||
label: '结束时间',
|
||||
control: FormControlType.dateTime,
|
||||
),
|
||||
],
|
||||
),
|
||||
FormSectionSchema(
|
||||
title: '补充说明',
|
||||
controls: [
|
||||
FormControlSchema(
|
||||
field: 'reason',
|
||||
label: '请假原因',
|
||||
control: FormControlType.textArea,
|
||||
placeholder: '请简要说明请假原因',
|
||||
helperText: 'AI 可以帮助整理表达,但提交前必须由你确认。',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,143 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:aioa_mobile/core/forms/presentation/dynamic_form_card.dart';
|
||||
import 'package:aioa_mobile/features/form/application/leave_draft_controller.dart';
|
||||
import 'package:aioa_mobile/features/form/domain/leave_form_definition.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class LeaveFormPage extends ConsumerWidget {
|
||||
const LeaveFormPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final formState = ref.watch(leaveDraftProvider);
|
||||
final controller = ref.read(leaveDraftProvider.notifier);
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(leaveFormDefinition.dataSchema.title)),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
|
||||
children: [
|
||||
_AiAssistCard(onApply: controller.applyAiSuggestion),
|
||||
const SizedBox(height: 14),
|
||||
DynamicFormCard(
|
||||
definition: leaveFormDefinition,
|
||||
state: formState,
|
||||
onChanged: controller.setValue,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
if (!controller.validate()) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('请检查表单中的必填项和时间范围')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (context) => _ConfirmationSheet(
|
||||
values: ref.read(leaveDraftProvider).values,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.check_circle_outline),
|
||||
label: const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 14),
|
||||
child: Text('检查并确认'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AiAssistCard extends StatelessWidget {
|
||||
const _AiAssistCard({required this.onApply});
|
||||
|
||||
final VoidCallback onApply;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryContainer.withValues(alpha: 0.45),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
const CircleAvatar(child: Icon(Icons.auto_awesome)),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'AI 表单助手',
|
||||
style: TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text('示例:帮我填写明天下午的事假申请'),
|
||||
],
|
||||
),
|
||||
),
|
||||
TextButton(onPressed: onApply, child: const Text('自动填写')),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConfirmationSheet extends StatelessWidget {
|
||||
const _ConfirmationSheet({required this.values});
|
||||
|
||||
final Map<String, Object?> values;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'提交前确认',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text('AI 或 Schema 只能生成草稿,实际业务写操作必须在用户确认后执行。'),
|
||||
const SizedBox(height: 16),
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Text(const JsonEncoder.withIndent(' ').convert(values)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('演示模式:草稿已通过本地校验,尚未调用后端')),
|
||||
);
|
||||
},
|
||||
child: const Text('确认创建草稿'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ProfilePage extends StatelessWidget {
|
||||
const ProfilePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
const Center(child: Text('员工小明 · 产品研发部'));
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TasksPage extends StatelessWidget {
|
||||
const TasksPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const Center(child: Text('暂无待办'));
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class WorkspacePage extends StatelessWidget {
|
||||
const WorkspacePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(18),
|
||||
children: [
|
||||
Text(
|
||||
'早上好,员工小明',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w800),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text('今天有什么需要处理?', style: Theme.of(context).textTheme.bodyLarge),
|
||||
const SizedBox(height: 22),
|
||||
Card(
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () => context.push('/leave/new'),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(18),
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
child: Icon(Icons.event_available_outlined),
|
||||
),
|
||||
SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'发起请假',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text('由 Schema 自动生成移动表单卡片'),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const _StatusCard(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatusCard extends StatelessWidget {
|
||||
const _StatusCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'我的工作',
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_Metric(value: '0', label: '待我处理'),
|
||||
_Metric(value: '0', label: '进行中'),
|
||||
_Metric(value: '0', label: '本月完成'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Metric extends StatelessWidget {
|
||||
const _Metric({required this.value, required this.label});
|
||||
final String value;
|
||||
final String label;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w800),
|
||||
),
|
||||
Text(label),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'package:aioa_mobile/app/app.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(const ProviderScope(child: AioaApp()));
|
||||
}
|
||||
Reference in New Issue
Block a user