jjsos_autosos_flutter/lib/pages/order/photograph_page.dart

227 lines
7.0 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'dart:io';
import 'package:autosos_flutter/provider/providers.dart';
import 'package:autosos_flutter/util/toast.dart';
import 'package:autosos_flutter/widget/photo_grid_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
/// 拍照取证页(对应 Android CheckPhotoActivity
/// 自定义相机 + 照片队列 + 备注 + 上传
class PhotographPage extends ConsumerStatefulWidget {
final int orderId;
const PhotographPage({super.key, required this.orderId});
@override
ConsumerState<PhotographPage> createState() => _PhotographPageState();
}
class _PhotographPageState extends ConsumerState<PhotographPage> {
final List<String> _localPaths = [];
final TextEditingController _remarkCtrl = TextEditingController();
bool _showCamera = false;
bool _uploading = false;
@override
void dispose() {
_remarkCtrl.dispose();
super.dispose();
}
void _onCaptured(String path) {
setState(() {
_localPaths.add(path);
// 拍到一张后自动收起相机,让用户确认后再拍
_showCamera = false;
});
}
Future<void> _pickFromAlbum() async {
final files = await AlbumPicker.pick();
if (files.isEmpty) return;
setState(() => _localPaths.addAll(files));
}
Future<void> _submit() async {
if (_localPaths.isEmpty) {
Toast.error('请先拍照');
return;
}
setState(() => _uploading = true);
EasyLoading.show(status: '上传中…');
try {
// 1) 走七牛
final urls = await ref
.read(uploadServiceProvider)
.uploadBatch(_localPaths);
// 2) 通知后端
final resp = await ref
.read(orderApiProvider)
.uploadPhotos(widget.orderId, urls);
EasyLoading.dismiss();
if (!resp.isSuccess) {
Toast.error(resp.message ?? '保存失败');
return;
}
Toast.success('已上传 ${urls.length} 张照片');
if (mounted) context.pop(true);
} catch (e) {
EasyLoading.dismiss();
Toast.error('上传失败:$e');
} finally {
if (mounted) setState(() => _uploading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
title: const Text('拍照取证'),
iconTheme: const IconThemeData(color: Colors.white),
),
body: SafeArea(
child: Column(
children: [
// 备注
Container(
color: Colors.white,
padding: const EdgeInsets.all(12),
child: TextField(
controller: _remarkCtrl,
maxLines: 2,
decoration: const InputDecoration(
hintText: '可填写现场情况描述(选填)',
border: OutlineInputBorder(),
),
),
),
Expanded(
child: _showCamera
? CameraView(onCaptured: _onCaptured)
: _photoList(),
),
// 底部按钮区
Container(
color: Colors.white,
padding: const EdgeInsets.all(8),
child: Column(
children: [
PhotoThumbStrip(
paths: _localPaths,
onAdd: () => setState(() => _showCamera = true),
onRemove: (i) =>
setState(() => _localPaths.removeAt(i)),
),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: _showCamera
? null
: () =>
setState(() => _showCamera = true),
icon: const Icon(Icons.camera_alt),
label: const Text('拍照'),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: _showCamera
? null
: _pickFromAlbum,
icon: const Icon(Icons.photo_library),
label: const Text('相册'),
),
),
const SizedBox(width: 8),
Expanded(
flex: 2,
child: ElevatedButton.icon(
onPressed: _uploading ? null : _submit,
icon: const Icon(Icons.cloud_upload),
label: Text(
_uploading
? '上传中…'
: '完成(${_localPaths.length})',
),
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).primaryColor,
),
),
),
],
),
],
),
),
],
),
),
);
}
Widget _photoList() {
if (_localPaths.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: const [
Icon(Icons.photo_camera,
size: 80, color: Colors.white24),
SizedBox(height: 8),
Text('点击下方"拍照"开始取证',
style: TextStyle(color: Colors.white54)),
],
),
);
}
return GridView.builder(
padding: const EdgeInsets.all(8),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: _localPaths.length,
itemBuilder: (ctx, i) => Stack(
children: [
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.file(
File(_localPaths[i]),
fit: BoxFit.cover,
),
),
),
Positioned(
top: 2,
right: 2,
child: GestureDetector(
onTap: () => setState(() => _localPaths.removeAt(i)),
child: Container(
decoration: const BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(2),
child: const Icon(
Icons.close,
size: 14,
color: Colors.white,
),
),
),
),
],
),
);
}
}