| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454 |
- import 'package:flutter/material.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import 'package:go_router/go_router.dart';
- import '../../core/theme/app_colors.dart';
- import '../shell/nav_bar_config.dart';
- import '../../core/utils/responsive.dart';
- import '../../shared/widgets/form_section.dart';
- import '../../shared/widgets/action_bar.dart';
- import '../../core/i18n/app_localizations.dart';
- import 'outing_log_api.dart';
- import 'outing_log_model.dart';
- class OutingLogCreatePage extends ConsumerStatefulWidget {
- const OutingLogCreatePage({super.key});
- @override
- ConsumerState<OutingLogCreatePage> createState() =>
- _OutingLogCreatePageState();
- }
- class _OutingLogCreatePageState extends ConsumerState<OutingLogCreatePage> {
- final _contentCtrl = TextEditingController();
- final _clientCtrl = TextEditingController();
- final _addressCtrl = TextEditingController();
- final _planCtrl = TextEditingController();
- DateTime _date = DateTime.now();
- TimeOfDay _startTime = const TimeOfDay(hour: 9, minute: 0);
- TimeOfDay _endTime = const TimeOfDay(hour: 17, minute: 0);
- String _visitType = '客户拜访';
- final _visitTypes = ['客户拜访', '外出办事', '其他'];
- @override
- void dispose() {
- _contentCtrl.dispose();
- _clientCtrl.dispose();
- _addressCtrl.dispose();
- _planCtrl.dispose();
- super.dispose();
- }
- @override
- Widget build(BuildContext context) {
- final r = ResponsiveHelper.of(context);
- final l10n = AppLocalizations.of(context);
- ref
- .read(navBarConfigProvider.notifier)
- .update(
- NavBarConfig(
- title: l10n.get('outingLogCreate'),
- showBack: true,
- onBack: () => context.pop(),
- ),
- );
- return Column(
- children: [
- Expanded(
- child: Align(
- alignment: Alignment.topCenter,
- child: ConstrainedBox(
- constraints: BoxConstraints(maxWidth: r.formMaxWidth),
- child: SingleChildScrollView(
- padding: const EdgeInsets.symmetric(vertical: 8),
- child: Column(
- children: [
- // GPS 信息卡片
- Container(
- margin: const EdgeInsets.symmetric(
- horizontal: 16,
- vertical: 4,
- ),
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: AppColors.bgCard,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Row(
- children: [
- const Icon(
- Icons.shield_outlined,
- size: 22,
- color: AppColors.success,
- ),
- const SizedBox(width: 10),
- const Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- '深圳市南山区科技园南路 88 号',
- style: TextStyle(
- fontSize: 14,
- color: AppColors.textPrimary,
- ),
- ),
- SizedBox(height: 4),
- Text(
- '22.5431°N, 113.9532°E · 精度 15m',
- style: TextStyle(
- fontSize: 12,
- color: AppColors.textSecondary,
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- const SizedBox(height: 8),
- // 基本信息
- FormSection(
- title: '基本信息',
- children: [
- _buildFormField('创建人', '张三', readOnly: true),
- const Divider(height: 1, color: AppColors.border),
- _buildFormField('部门', '市场部', readOnly: true),
- const Divider(height: 1, color: AppColors.border),
- _buildFormField(
- '日期',
- '${_date.year}-${_date.month.toString().padLeft(2, '0')}-${_date.day.toString().padLeft(2, '0')}',
- onTap: _pickDate,
- ),
- ],
- ),
- const SizedBox(height: 8),
- // 外出详情
- FormSection(
- title: '外出详情',
- children: [
- _buildSelectField('外出类型', _visitType, _pickVisitType),
- const Divider(height: 1, color: AppColors.border),
- _buildFormField(
- '外出地点',
- _addressCtrl.text.isEmpty ? null : _addressCtrl.text,
- hint: '请输入外出地点',
- showArrow: false,
- onTap: () {},
- ),
- if (_addressCtrl.text.isEmpty)
- Padding(
- padding: const EdgeInsets.only(bottom: 4),
- child: TextField(
- controller: _addressCtrl,
- decoration: const InputDecoration(
- hintText: '请输入外出地点',
- hintStyle: TextStyle(
- fontSize: 14,
- color: AppColors.textPlaceholder,
- ),
- border: InputBorder.none,
- contentPadding: EdgeInsets.zero,
- isDense: true,
- ),
- style: const TextStyle(
- fontSize: 14,
- color: AppColors.textPrimary,
- ),
- ),
- ),
- const Divider(height: 1, color: AppColors.border),
- _buildFormField(
- '开始时间',
- _startTime.format(context),
- onTap: () => _pickTime(true),
- ),
- const Divider(height: 1, color: AppColors.border),
- _buildFormField(
- '结束时间',
- _endTime.format(context),
- onTap: () => _pickTime(false),
- ),
- const Divider(height: 1, color: AppColors.border),
- const SizedBox(height: 8),
- const Text(
- '外出事由',
- style: TextStyle(
- fontSize: 14,
- color: AppColors.textSecondary,
- ),
- ),
- const SizedBox(height: 8),
- Container(
- padding: const EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: AppColors.bgPage,
- borderRadius: BorderRadius.circular(4),
- ),
- child: TextField(
- controller: _contentCtrl,
- maxLines: 5,
- decoration: const InputDecoration(
- hintText: '请填写外出事由及工作内容...',
- hintStyle: TextStyle(
- fontSize: 14,
- color: AppColors.textPlaceholder,
- ),
- border: InputBorder.none,
- contentPadding: EdgeInsets.zero,
- isDense: true,
- ),
- style: const TextStyle(
- fontSize: 14,
- color: AppColors.textPrimary,
- ),
- ),
- ),
- ],
- ),
- const SizedBox(height: 8),
- // 附件
- FormSection(
- title: '附件',
- children: [
- Container(
- height: 80,
- decoration: BoxDecoration(
- color: AppColors.bgPage,
- borderRadius: BorderRadius.circular(4),
- ),
- child: const Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- Icon(
- Icons.add_photo_alternate_outlined,
- size: 28,
- color: AppColors.primary,
- ),
- SizedBox(height: 4),
- Text(
- '添加附件',
- style: TextStyle(
- fontSize: 12,
- color: AppColors.primary,
- ),
- ),
- ],
- ),
- ),
- ),
- ],
- ),
- const SizedBox(height: 80),
- ],
- ),
- ),
- ),
- ),
- ),
- // ActionBar
- ActionBar(
- centerLabel: '存草稿',
- rightLabel: '提交',
- showLeft: false,
- onCenterTap: _saveDraft,
- onRightTap: _submit,
- ),
- ],
- );
- }
- Widget _buildFormField(
- String label,
- String? value, {
- String? hint,
- bool readOnly = false,
- bool showArrow = true,
- VoidCallback? onTap,
- }) {
- final hasValue = value != null && value.isNotEmpty;
- return GestureDetector(
- onTap: readOnly ? null : onTap,
- child: Container(
- height: 44,
- alignment: Alignment.centerLeft,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- label,
- style: const TextStyle(
- fontSize: 14,
- color: AppColors.textSecondary,
- ),
- ),
- Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- hasValue ? value : (hint ?? '请选择或填写'),
- style: TextStyle(
- fontSize: 14,
- color: hasValue
- ? AppColors.textPrimary
- : AppColors.textPlaceholder,
- ),
- ),
- if (showArrow && !readOnly) ...[
- const SizedBox(width: 4),
- const Icon(
- Icons.chevron_right,
- size: 14,
- color: AppColors.textPlaceholder,
- ),
- ],
- ],
- ),
- ],
- ),
- ),
- );
- }
- Widget _buildSelectField(String label, String value, VoidCallback onTap) {
- return GestureDetector(
- onTap: onTap,
- child: Container(
- height: 44,
- alignment: Alignment.centerLeft,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- label,
- style: const TextStyle(
- fontSize: 14,
- color: AppColors.textSecondary,
- ),
- ),
- Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(
- value,
- style: const TextStyle(
- fontSize: 14,
- color: AppColors.textPrimary,
- ),
- ),
- const SizedBox(width: 4),
- const Icon(
- Icons.chevron_right,
- size: 14,
- color: AppColors.textPlaceholder,
- ),
- ],
- ),
- ],
- ),
- ),
- );
- }
- Future<void> _pickDate() async {
- final picked = await showDatePicker(
- context: context,
- initialDate: _date,
- firstDate: DateTime(2020),
- lastDate: DateTime(2030),
- );
- if (picked != null) setState(() => _date = picked);
- }
- Future<void> _pickTime(bool isStart) async {
- final picked = await showTimePicker(
- context: context,
- initialTime: isStart ? _startTime : _endTime,
- );
- if (picked != null) {
- setState(() {
- if (isStart) {
- _startTime = picked;
- } else {
- _endTime = picked;
- }
- });
- }
- }
- Future<void> _pickVisitType() async {
- final result = await showDialog<String>(
- context: context,
- builder: (ctx) => SimpleDialog(
- title: const Text('选择外出类型'),
- children: _visitTypes
- .map(
- (t) => SimpleDialogOption(
- onPressed: () => Navigator.pop(ctx, t),
- child: Text(t),
- ),
- )
- .toList(),
- ),
- );
- if (result != null) setState(() => _visitType = result);
- }
- Future<void> _saveDraft() async {
- // TODO: 存草稿逻辑
- if (context.mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(const SnackBar(content: Text('已保存草稿')));
- }
- }
- Future<void> _submit() async {
- try {
- final visitDate = DateTime(_date.year, _date.month, _date.day);
- await ref
- .read(outingLogApiProvider)
- .create(
- OutingLogModel(
- id: '',
- visitNo: '',
- salespersonId: '',
- salespersonName: '张三',
- deptId: '',
- deptName: '市场部',
- customerName: _clientCtrl.text,
- visitDate: visitDate,
- visitStartTime: DateTime(
- visitDate.year,
- visitDate.month,
- visitDate.day,
- _startTime.hour,
- _startTime.minute,
- ),
- visitEndTime: DateTime(
- visitDate.year,
- visitDate.month,
- visitDate.day,
- _endTime.hour,
- _endTime.minute,
- ),
- visitType: _visitType,
- visitPurpose: '',
- visitLocation: _addressCtrl.text,
- visitSummary: _contentCtrl.text,
- nextVisitTime: visitDate,
- createTime: DateTime.now(),
- updateTime: DateTime.now(),
- ),
- );
- if (context.mounted) context.pop();
- } catch (_) {}
- }
- }
|