- Register ACTION_SEND IntentFilter for image/* in AndroidManifest - Kotlin MethodChannel bridge: extract image URI, original filename, resolve to file - Flutter bottom sheet UI: thumbnail preview, note input (200 char), submit/cancel - Upload service: multipart POST, Bearer auth, 10MB limit, offline cache, exponential backoff retry - Configurable server URL and API token via SharedPreferences - Minimal home screen with usage instructions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
131 lines
3.4 KiB
Dart
131 lines
3.4 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../services/upload_service.dart';
|
|
|
|
class RecordBottomSheet extends StatefulWidget {
|
|
final File imageFile;
|
|
final String originalFilename;
|
|
|
|
const RecordBottomSheet({
|
|
super.key,
|
|
required this.imageFile,
|
|
required this.originalFilename,
|
|
});
|
|
|
|
@override
|
|
State<RecordBottomSheet> createState() => _RecordBottomSheetState();
|
|
}
|
|
|
|
class _RecordBottomSheetState extends State<RecordBottomSheet> {
|
|
final _noteController = TextEditingController();
|
|
bool _isUploading = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_noteController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _submit() async {
|
|
if (_isUploading) return;
|
|
setState(() => _isUploading = true);
|
|
|
|
final result = await UploadService.upload(
|
|
imageFile: widget.imageFile,
|
|
originalFilename: widget.originalFilename,
|
|
userNote: _noteController.text.trim(),
|
|
);
|
|
|
|
if (!mounted) return;
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
result.success ? '记账凭证已保存' : (result.error ?? '上传失败'),
|
|
),
|
|
duration: const Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: EdgeInsets.only(
|
|
left: 16,
|
|
right: 16,
|
|
top: 16,
|
|
bottom: MediaQuery.of(context).viewInsets.bottom + 16,
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Thumbnail preview
|
|
Container(
|
|
height: 200,
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade200,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Image.file(
|
|
widget.imageFile,
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (_, _, _) => const Icon(
|
|
Icons.broken_image,
|
|
size: 48,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Note input
|
|
TextField(
|
|
controller: _noteController,
|
|
maxLength: 200,
|
|
maxLines: 3,
|
|
decoration: const InputDecoration(
|
|
hintText: '添加备注(可选)...',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Buttons
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: _isUploading ? null : () => Navigator.of(context).pop(),
|
|
child: const Text('取消'),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: FilledButton(
|
|
onPressed: _isUploading ? null : _submit,
|
|
child: _isUploading
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Text('提交'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|