11 changed files with 619 additions and 3 deletions
@ -0,0 +1,135 @@ |
|||||||
|
import 'package:uuid/uuid.dart'; |
||||||
|
|
||||||
|
class Problem { |
||||||
|
final String id; |
||||||
|
final String description; |
||||||
|
final String location; |
||||||
|
final String imageUrl; |
||||||
|
final DateTime createdAt; |
||||||
|
final DateTime updatedAt; |
||||||
|
|
||||||
|
static final Uuid _uuid = const Uuid(); |
||||||
|
|
||||||
|
Problem._({ |
||||||
|
required this.id, |
||||||
|
required this.description, |
||||||
|
required this.location, |
||||||
|
required this.imageUrl, |
||||||
|
required this.createdAt, |
||||||
|
required this.updatedAt, |
||||||
|
}); |
||||||
|
|
||||||
|
// 主工厂构造函数 |
||||||
|
factory Problem({ |
||||||
|
required String description, |
||||||
|
required String location, |
||||||
|
required String imageUrl, |
||||||
|
String? id, |
||||||
|
DateTime? createdAt, |
||||||
|
DateTime? updatedAt, |
||||||
|
}) { |
||||||
|
// 参数验证 |
||||||
|
_validateParameters(description, location, imageUrl); |
||||||
|
|
||||||
|
final now = DateTime.now(); |
||||||
|
return Problem._( |
||||||
|
id: id ?? _uuid.v4(), |
||||||
|
description: description.trim(), |
||||||
|
location: location.trim(), |
||||||
|
imageUrl: imageUrl, |
||||||
|
createdAt: createdAt ?? now, |
||||||
|
updatedAt: updatedAt ?? now, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// 参数验证 |
||||||
|
static void _validateParameters( |
||||||
|
String description, |
||||||
|
String location, |
||||||
|
String imageUrl, |
||||||
|
) { |
||||||
|
if (description.isEmpty) { |
||||||
|
throw ArgumentError('Description cannot be empty'); |
||||||
|
} |
||||||
|
if (location.isEmpty) { |
||||||
|
throw ArgumentError('Location cannot be empty'); |
||||||
|
} |
||||||
|
if (imageUrl.isEmpty) { |
||||||
|
throw ArgumentError('Image URL cannot be empty'); |
||||||
|
} |
||||||
|
|
||||||
|
// 验证 URL 格式(简单验证) |
||||||
|
if (!imageUrl.startsWith('http')) { |
||||||
|
throw ArgumentError('Image URL must be a valid URL'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 从 JSON 反序列化 |
||||||
|
factory Problem.fromJson(Map<String, dynamic> json) { |
||||||
|
return Problem( |
||||||
|
id: json['id'], |
||||||
|
description: json['description'], |
||||||
|
location: json['location'], |
||||||
|
imageUrl: json['imageUrl'], |
||||||
|
createdAt: json['createdAt'] != null |
||||||
|
? DateTime.parse(json['createdAt']) |
||||||
|
: null, |
||||||
|
updatedAt: json['updatedAt'] != null |
||||||
|
? DateTime.parse(json['updatedAt']) |
||||||
|
: null, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// 转换为 JSON |
||||||
|
Map<String, dynamic> toJson() { |
||||||
|
return { |
||||||
|
'id': id, |
||||||
|
'description': description, |
||||||
|
'location': location, |
||||||
|
'imageUrl': imageUrl, |
||||||
|
'createdAt': createdAt.toIso8601String(), |
||||||
|
'updatedAt': updatedAt.toIso8601String(), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
// 复制方法 |
||||||
|
Problem copyWith({ |
||||||
|
String? id, |
||||||
|
String? description, |
||||||
|
String? location, |
||||||
|
String? imageUrl, |
||||||
|
DateTime? createdAt, |
||||||
|
DateTime? updatedAt, |
||||||
|
}) { |
||||||
|
return Problem( |
||||||
|
id: id ?? this.id, |
||||||
|
description: description ?? this.description, |
||||||
|
location: location ?? this.location, |
||||||
|
imageUrl: imageUrl ?? this.imageUrl, |
||||||
|
createdAt: createdAt ?? this.createdAt, |
||||||
|
updatedAt: updatedAt ?? DateTime.now(), // 更新时间为当前时间 |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// 检查是否有效 |
||||||
|
bool get isValid => description.isNotEmpty && location.isNotEmpty; |
||||||
|
|
||||||
|
// 获取简短描述 |
||||||
|
String get shortDescription { |
||||||
|
if (description.length <= 50) return description; |
||||||
|
return '${description.substring(0, 47)}...'; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
String toString() { |
||||||
|
return 'Problem(id: $id, description: $shortDescription, location: $location)'; |
||||||
|
} |
||||||
|
|
||||||
|
@override |
||||||
|
bool operator ==(Object other) => |
||||||
|
identical(this, other) || |
||||||
|
other is Problem && runtimeType == other.runtimeType && id == other.id; |
||||||
|
|
||||||
|
@override |
||||||
|
int get hashCode => id.hashCode; |
||||||
|
} |
@ -0,0 +1,244 @@ |
|||||||
|
import 'dart:io'; |
||||||
|
|
||||||
|
import 'package:flutter/material.dart'; |
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:problem_check_system/modules/problem/models/problem.dart'; |
||||||
|
import 'package:problem_check_system/modules/problem/view_model/image_controller.dart'; |
||||||
|
import 'package:problem_check_system/modules/problem/view_model/problem_new_controller.dart'; |
||||||
|
|
||||||
|
class ProblemNewPage extends StatelessWidget { |
||||||
|
ProblemNewPage({super.key}); |
||||||
|
|
||||||
|
final ImageController imageController = Get.put(ImageController()); |
||||||
|
final ProblemNewController problemNewController = Get.put( |
||||||
|
ProblemNewController(), |
||||||
|
); |
||||||
|
|
||||||
|
@override |
||||||
|
Widget build(BuildContext context) { |
||||||
|
return Scaffold( |
||||||
|
appBar: AppBar( |
||||||
|
flexibleSpace: Container( |
||||||
|
decoration: const BoxDecoration( |
||||||
|
gradient: LinearGradient( |
||||||
|
colors: [Color(0xFF418CFC), Color(0xFF3DBFFC)], // 渐变颜色 |
||||||
|
begin: Alignment.centerLeft, // 从左开始 |
||||||
|
end: Alignment.centerRight, // 到右结束 |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
leading: IconButton( |
||||||
|
icon: const Icon(Icons.arrow_back_ios_new_rounded), // 返回箭头图标 |
||||||
|
color: Colors.white, |
||||||
|
onPressed: () { |
||||||
|
Navigator.pop(context); // 返回上一页逻辑 |
||||||
|
}, |
||||||
|
), |
||||||
|
title: const Text('新增问题', style: TextStyle(color: Colors.white)), |
||||||
|
centerTitle: true, |
||||||
|
backgroundColor: Colors.transparent, // 设置背景透明以显示渐变 |
||||||
|
), |
||||||
|
body: Column( |
||||||
|
children: [ |
||||||
|
Expanded( |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
Card( |
||||||
|
margin: EdgeInsets.all(17.w), |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
ListTile( |
||||||
|
title: const Text( |
||||||
|
'问题描述', // 标题 |
||||||
|
style: TextStyle( |
||||||
|
fontSize: 16, |
||||||
|
fontWeight: FontWeight.bold, |
||||||
|
), |
||||||
|
), |
||||||
|
subtitle: TextField( |
||||||
|
maxLines: null, |
||||||
|
decoration: const InputDecoration( |
||||||
|
hintText: '请输入问题描述', |
||||||
|
border: InputBorder.none, |
||||||
|
), |
||||||
|
onChanged: (value) { |
||||||
|
// 输入值发生变化时的逻辑 |
||||||
|
}, |
||||||
|
), |
||||||
|
), |
||||||
|
Align( |
||||||
|
alignment: Alignment.centerRight, // 设置为右对齐 |
||||||
|
child: Container( |
||||||
|
margin: const EdgeInsets.all(10), |
||||||
|
child: ElevatedButton( |
||||||
|
onPressed: () { |
||||||
|
print("按钮被点击"); |
||||||
|
}, |
||||||
|
child: const Icon(Icons.keyboard_voice), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
Card( |
||||||
|
margin: EdgeInsets.all(17.w), |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
ListTile( |
||||||
|
title: const Text( |
||||||
|
'所在位置', // 标题 |
||||||
|
style: TextStyle( |
||||||
|
fontSize: 16, |
||||||
|
fontWeight: FontWeight.bold, |
||||||
|
), |
||||||
|
), |
||||||
|
subtitle: TextField( |
||||||
|
maxLines: null, |
||||||
|
decoration: const InputDecoration( |
||||||
|
hintText: '请输入问题所在位置', |
||||||
|
border: InputBorder.none, |
||||||
|
), |
||||||
|
onChanged: (value) { |
||||||
|
// 输入值发生变化时的逻辑 |
||||||
|
}, |
||||||
|
), |
||||||
|
), |
||||||
|
Align( |
||||||
|
alignment: Alignment.centerRight, // 设置为右对齐 |
||||||
|
child: Container( |
||||||
|
margin: const EdgeInsets.all(10), |
||||||
|
child: ElevatedButton( |
||||||
|
onPressed: () { |
||||||
|
print("按钮被点击"); |
||||||
|
}, |
||||||
|
child: const Icon(Icons.keyboard_voice), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
Card( |
||||||
|
margin: EdgeInsets.all(17.w), |
||||||
|
child: Column( |
||||||
|
children: [ |
||||||
|
ListTile( |
||||||
|
title: const Text( |
||||||
|
'问题图片', // 标题 |
||||||
|
style: TextStyle( |
||||||
|
fontSize: 16, |
||||||
|
fontWeight: FontWeight.bold, |
||||||
|
), |
||||||
|
), |
||||||
|
subtitle: Row( |
||||||
|
mainAxisAlignment: MainAxisAlignment.start, |
||||||
|
children: [ |
||||||
|
// 展示图片区域 |
||||||
|
Obx(() { |
||||||
|
final file = imageController.selectedImage.value; |
||||||
|
if (file == null) { |
||||||
|
return const Text('尚未选取图片'); |
||||||
|
} |
||||||
|
return Image.file( |
||||||
|
file, |
||||||
|
width: 100, |
||||||
|
height: 100, |
||||||
|
fit: BoxFit.cover, |
||||||
|
); |
||||||
|
}), |
||||||
|
const SizedBox(width: 20), |
||||||
|
Container( |
||||||
|
color: Color(0xffF7F7F7), |
||||||
|
width: 100, |
||||||
|
height: 100, |
||||||
|
child: IconButton( |
||||||
|
icon: const Icon(Icons.image), |
||||||
|
iconSize: 40, // 图标大小 |
||||||
|
onPressed: imageController.pickAndCopyImage, |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
// 底部按钮 |
||||||
|
Container( |
||||||
|
width: 375.w, |
||||||
|
height: 81.h, |
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), |
||||||
|
decoration: BoxDecoration( |
||||||
|
color: Colors.grey[200], |
||||||
|
borderRadius: BorderRadius.circular(12), |
||||||
|
), |
||||||
|
child: Row( |
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||||
|
children: [ |
||||||
|
Expanded( |
||||||
|
child: ElevatedButton( |
||||||
|
style: ElevatedButton.styleFrom( |
||||||
|
backgroundColor: Colors.white, |
||||||
|
shape: RoundedRectangleBorder( |
||||||
|
borderRadius: BorderRadius.circular(8), |
||||||
|
), |
||||||
|
), |
||||||
|
onPressed: () { |
||||||
|
// 取消按钮逻辑 |
||||||
|
Get.back(); |
||||||
|
}, |
||||||
|
child: const Text( |
||||||
|
'取消', |
||||||
|
style: TextStyle(color: Colors.grey), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
const SizedBox(width: 10), |
||||||
|
Expanded( |
||||||
|
child: ElevatedButton( |
||||||
|
style: ElevatedButton.styleFrom( |
||||||
|
backgroundColor: Color(0xFF418CFC), |
||||||
|
shape: RoundedRectangleBorder( |
||||||
|
borderRadius: BorderRadius.circular(8), |
||||||
|
), |
||||||
|
), |
||||||
|
onPressed: () async { |
||||||
|
// 确定按钮逻辑 |
||||||
|
final problem1 = Problem( |
||||||
|
description: '墙面裂缝需要修复', |
||||||
|
location: 'A栋101房间', |
||||||
|
imageUrl: 'https://example.com/images/wall_crack.jpg', |
||||||
|
); |
||||||
|
await problemNewController.saveJson( |
||||||
|
problem1.toJson(), |
||||||
|
problem1.id, |
||||||
|
); |
||||||
|
Get.back(); |
||||||
|
Get.snackbar( |
||||||
|
'保存成功', |
||||||
|
'${problem1.id}.json 已创建', |
||||||
|
snackPosition: SnackPosition.TOP, |
||||||
|
snackStyle: SnackStyle.FLOATING, |
||||||
|
backgroundColor: Colors.black87, |
||||||
|
colorText: Colors.white, |
||||||
|
); |
||||||
|
}, |
||||||
|
child: const Text( |
||||||
|
'确定', |
||||||
|
style: TextStyle(color: Colors.white), |
||||||
|
), |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
), |
||||||
|
], |
||||||
|
), |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
import 'dart:io'; |
||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:image_picker/image_picker.dart'; |
||||||
|
import 'package:path/path.dart'; |
||||||
|
import 'package:path_provider/path_provider.dart'; |
||||||
|
|
||||||
|
class ImageController extends GetxController { |
||||||
|
// Rxn 表示 nullable 的响应式类型 |
||||||
|
final Rxn<File> selectedImage = Rxn<File>(); |
||||||
|
|
||||||
|
// 从相册选取并复制 |
||||||
|
Future<void> pickAndCopyImage() async { |
||||||
|
// 调用 ImagePicker 选取 |
||||||
|
final picked = await ImagePicker().pickImage(source: ImageSource.gallery); |
||||||
|
if (picked == null) return; |
||||||
|
|
||||||
|
final originalFile = File(picked.path); |
||||||
|
|
||||||
|
// 获取应用文档目录 |
||||||
|
final appDir = await getApplicationDocumentsDirectory(); |
||||||
|
final fileName = basename(picked.path); |
||||||
|
|
||||||
|
// 复制到应用目录 |
||||||
|
final copiedFile = await originalFile.copy('${appDir.path}/$fileName'); |
||||||
|
|
||||||
|
// 更新响应式状态 |
||||||
|
selectedImage.value = copiedFile; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
import 'dart:convert'; |
||||||
|
import 'dart:io'; |
||||||
|
|
||||||
|
import 'package:get/get.dart'; |
||||||
|
import 'package:path_provider/path_provider.dart'; |
||||||
|
|
||||||
|
class ProblemNewController extends GetxController { |
||||||
|
late Directory appDocDir; |
||||||
|
|
||||||
|
@override |
||||||
|
void onInit() { |
||||||
|
super.onInit(); |
||||||
|
_initDirectory(); |
||||||
|
} |
||||||
|
|
||||||
|
Future<void> _initDirectory() async { |
||||||
|
appDocDir = await getApplicationDocumentsDirectory(); |
||||||
|
} |
||||||
|
|
||||||
|
// todo problems文件不存在,需要创建 |
||||||
|
/// 保存 Map 数据为 JSON 文件 |
||||||
|
Future<File> saveJson(Map<String, dynamic> data, String fileName) async { |
||||||
|
final path = '${appDocDir.path}/problems/$fileName.json'; |
||||||
|
final file = File(path); |
||||||
|
final jsonStr = jsonEncode(data); |
||||||
|
return file.writeAsString(jsonStr); |
||||||
|
} |
||||||
|
|
||||||
|
/// 读取本地 JSON 并解析为 Map |
||||||
|
Future<Map<String, dynamic>?> readJson(String fileName) async { |
||||||
|
final path = '${appDocDir.path}/problems/$fileName.json'; |
||||||
|
final file = File(path); |
||||||
|
if (!await file.exists()) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
final content = await file.readAsString(); |
||||||
|
return jsonDecode(content) as Map<String, dynamic>; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue