一、需求来源
最新代码圈有被 claude-3.7 刷屏的情况出现,感觉 AI 代码指导编程的临界点悄然已至。有点类似 Swift4.2、Flutter2.2版本的推出,搞app开发的朋友懂这些关键版本节点的重要性,它代表了从一小部分极客玩家到大众参与的裂变点。如果有还在等待AI辅助编程的朋友,那么可以加入了。最近花了两周多时间体验了 Cusor + claude-3.7 AI编程实战效果。
二、使用示例
所有代码都是通过多次谈话自动生成!
这是一个简单的健康调查问卷生成效果:
scss
.
├── lib
│ ├── controllers
│ │ ├── survey_controller.dart (调查表控制器)
│ ├── models
│ │ └── survey_model.dart (调查模型)
│ ├── pages
│ │ ├── survey_page.dart (调查表单)
│ └── widgets
│ └── questions
│ ├── question_container.dart (每个组件外边的修饰盒子)
│ ├── question_image_upload.dart (上传图片组件)
│ ├── question_multi_choice.dart (多选组件)
│ ├── question_rating.dart (星星评价组件)
│ ├── question_single_choice.dart (单选组件)
│ └── question_text.dart (输入框组件)
1、SurveyPage 调查表主页面
dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/survey_controller.dart';
import '../widgets/questions/question_container.dart';
import '../widgets/questions/question_text.dart';
import '../widgets/questions/question_single_choice.dart';
import '../widgets/questions/question_multi_choice.dart';
import '../widgets/questions/question_image_upload.dart';
import '../widgets/questions/question_rating.dart';
class SurveyPage extends StatefulWidget {
const SurveyPage({super.key});
@override
State<SurveyPage> createState() => _SurveyPageState();
}
class _SurveyPageState extends State<SurveyPage> {
late final SurveyController controller;
final RxInt selectedQuestionIndex = (-1).obs;
@override
void initState() {
super.initState();
controller = Get.find<SurveyController>();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: _buildAppBar(),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
_buildQuestionList(),
],
),
),
bottomNavigationBar: _buildSubmitButton(),
);
}
PreferredSizeWidget _buildAppBar() {
return AppBar(
backgroundColor: Colors.white,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, size: 20),
onPressed: () => Get.back(),
),
title: const Text(
'量表',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
),
centerTitle: true,
);
}
Widget _buildHeader() {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16.0),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(4),
bottomRight: Radius.circular(4),
),
),
child: const Text(
'住院病人调查统计表',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
);
}
Widget _buildQuestionList() {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Obx(() => Column(
children: [
QuestionContainer(
title: '1.近期运动情况,请详细描述',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
QuestionText(
initialValue: controller.textAnswer.value,
onChanged: (value) {
controller.updateTextAnswer(value);
selectedQuestionIndex.value = 0;
},
tip: controller.getTip('1'),
),
],
),
),
const SizedBox(height: 16),
QuestionContainer(
title: '2.住院期间对医院服务总体感觉',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
QuestionSingleChoice(
choices: const [
Choice('好', '(52)'),
Choice('一般', '(49)'),
Choice('差', '(32)'),
Choice('其他', '(12)'),
],
initialValue: controller.singleChoiceAnswer.value,
onChanged: controller.updateSingleChoice,
onSelect: (_) => selectedQuestionIndex.value = 1,
tip: controller.getTip('2'),
),
],
),
),
const SizedBox(height: 16),
QuestionContainer(
title: '3.您有以下哪些症状',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
QuestionMultiChoice(
options: const [
'头痛头晕',
'恶心呕吐',
'睡眠困难',
'呼吸困难',
'晕血症状',
'四肢发麻',
],
initialValues: controller.multiChoiceAnswers,
onChanged: (values) {
controller.updateMultiChoice(values);
selectedQuestionIndex.value = 2;
},
tip: controller.getTip('3'),
),
],
),
),
const SizedBox(height: 16),
QuestionContainer(
title: '4.住院期间您做了哪些辅助检查',
subtitle: '注:最多可上传10张,每张图片大小不超过2M',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
QuestionImageUpload(
initialImages: controller.imageUrls,
tip: controller.getTip('4'),
onImagesChanged: (images) {
selectedQuestionIndex.value = 3;
controller.updateImages(images);
},
),
],
),
),
const SizedBox(height: 16),
QuestionContainer(
title: '5.住院期间对医院服务总体感觉是好',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
QuestionSingleChoice(
choices: const [
Choice('是', ''),
Choice('否', ''),
],
initialValue: controller.isGoodService.value ? '是' : '否',
onChanged: (value) {
controller.updateServiceSatisfaction(value == '是');
selectedQuestionIndex.value = 4;
},
onSelect: (_) => selectedQuestionIndex.value = 4,
tip: controller.getTip('5'),
),
],
),
),
const SizedBox(height: 16),
QuestionContainer(
title: '6.近1个月,晚上上床时间通常在几点?',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
QuestionSingleChoice(
choices: const [
Choice('八点', ''),
Choice('十一点', ''),
],
initialValue: controller.bedTime.value,
onChanged: controller.updateBedTime,
onSelect: (_) => selectedQuestionIndex.value = 5,
tip: controller.getTip('6'),
),
],
),
),
const SizedBox(height: 16),
QuestionContainer(
title: '7.对医生健康建议满意度',
isRequired: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
QuestionRating(
options: const [
RatingOption('不满意', 1),
RatingOption('一般', 2),
RatingOption('非常满意', 3),
],
initialValue: controller.satisfactionRating.value,
onChanged: (value) {
controller.updateSatisfactionRating(value);
selectedQuestionIndex.value = 6;
},
tip: controller.getTip('7'),
),
],
),
),
],
)),
);
}
Widget _buildSubmitButton() {
return Container(
padding: const EdgeInsets.all(16.0),
child: Obx(() => ElevatedButton(
onPressed: controller.isSubmitting.value ? null : controller.submitSurvey,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
child: controller.isSubmitting.value
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Text(
'提交',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
)),
);
}
}
2、主页面控制器 SurveyController
dart
import 'package:get/get.dart';
class SurveyController extends GetxController {
final RxString textAnswer = ''.obs;
final RxString singleChoiceAnswer = ''.obs;
final RxList<String> multiChoiceAnswers = <String>[].obs;
final RxList<String> imageUrls = <String>[].obs;
final RxBool isGoodService = false.obs;
final RxString bedTime = ''.obs;
final RxInt satisfactionRating = 0.obs;
final RxBool isSubmitting = false.obs;
final Map<String, String?> _warnings = {};
// 文本输入
void updateTextAnswer(String value) => textAnswer.value = value;
// 单选题
void updateSingleChoice(String value) => singleChoiceAnswer.value = value;
// 多选题
void updateMultiChoice(List<String> values) => multiChoiceAnswers.value = values;
// 图片上传
void addImage(String url) {
if (imageUrls.length < 10) {
imageUrls.add(url);
}
}
void removeImage(int index) {
if (index >= 0 && index < imageUrls.length) {
imageUrls.removeAt(index);
}
}
void updateImages(List<String> images) {
imageUrls.clear();
imageUrls.addAll(images);
}
// 服务满意度
void updateServiceSatisfaction(bool value) => isGoodService.value = value;
// 上床时间
void updateBedTime(String value) => bedTime.value = value;
// 满意度评分
void updateSatisfactionRating(int value) => satisfactionRating.value = value;
// 警告消息
String? getTip(String questionId) {
return _warnings[questionId];
}
void setTip(String questionId, String? message) {
_warnings[questionId] = message;
update();
}
void clearWarningMessage(String questionId) {
_warnings.remove(questionId);
update();
}
// 提交问卷
Future<void> submitSurvey() async {
try {
isSubmitting.value = true;
// 构建提交数据
final Map<String, dynamic> data = {
'textAnswer': textAnswer.value,
'singleChoiceAnswer': singleChoiceAnswer.value,
'multiChoiceAnswers': multiChoiceAnswers,
'imageUrls': imageUrls,
'isGoodService': isGoodService.value,
'bedTime': bedTime.value,
'satisfactionRating': satisfactionRating.value,
};
// TODO: 调用API提交数据
await Future.delayed(const Duration(seconds: 2)); // 模拟网络请求
Get.snackbar(
'提交成功',
'感谢您的反馈!',
snackPosition: SnackPosition.BOTTOM,
);
} catch (e) {
Get.snackbar(
'提交失败',
'请稍后重试',
snackPosition: SnackPosition.BOTTOM,
);
} finally {
isSubmitting.value = false;
}
}
// 重置问卷
void resetSurvey() {
textAnswer.value = '';
singleChoiceAnswer.value = '';
multiChoiceAnswers.clear();
imageUrls.clear();
isGoodService.value = false;
bedTime.value = '';
satisfactionRating.value = 0;
_warnings.clear();
update();
}
}
3、没到题目包裹的壳 QuestionContainer
dart
import 'package:flutter/material.dart';
class QuestionContainer extends StatelessWidget {
final String title;
final String? subtitle;
final Widget child;
final bool isRequired;
const QuestionContainer({
Key? key,
required this.title,
this.subtitle,
required this.child,
this.isRequired = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
if (isRequired)
const Text(
'*',
style: TextStyle(
color: Colors.red,
fontSize: 16,
),
),
],
),
if (subtitle != null) ...[
const SizedBox(height: 8),
Text(
subtitle!,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
const SizedBox(height: 16),
child,
],
),
);
}
}
4、输入框组件 QuestionText
dart
import 'package:flutter/material.dart';
class QuestionText extends StatelessWidget {
final String? initialValue;
final ValueChanged<String> onChanged;
final String? tip;
const QuestionText({
super.key,
this.initialValue,
required this.onChanged,
this.tip,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: TextEditingController(text: initialValue),
onChanged: onChanged,
maxLines: 3,
decoration: InputDecoration(
hintText: '请输入',
filled: true,
fillColor: Colors.grey[50],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide.none,
),
),
),
if (tip != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
children: [
Icon(
Icons.warning_amber_rounded,
size: 16,
color: Colors.orange[700],
),
const SizedBox(width: 4),
Expanded(
child: Text(
tip!,
style: TextStyle(
fontSize: 12,
color: Colors.orange[700],
),
),
),
],
),
),
],
);
}
}
5、单选组件
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class Choice {
final String title;
final String? subtitle;
const Choice(this.title, [this.subtitle]);
}
class QuestionSingleChoice extends StatefulWidget {
final List<Choice> choices;
final ValueChanged<String>? onChanged;
final ValueChanged<bool>? onSelect;
final String? initialValue;
final String? tip;
const QuestionSingleChoice({
Key? key,
required this.choices,
this.onChanged,
this.onSelect,
this.initialValue,
this.tip,
}) : super(key: key);
@override
State<QuestionSingleChoice> createState() => _QuestionSingleChoiceState();
}
class _QuestionSingleChoiceState extends State<QuestionSingleChoice> {
String? _selectedValue;
@override
void initState() {
super.initState();
_selectedValue = widget.initialValue;
}
@override
void didUpdateWidget(QuestionSingleChoice oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.initialValue != oldWidget.initialValue) {
setState(() {
_selectedValue = widget.initialValue;
});
}
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...widget.choices.map((choice) => _buildChoiceItem(context, choice)).toList(),
if (widget.tip != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
children: [
Icon(
Icons.warning_amber_rounded,
size: 16,
color: Colors.orange[700],
),
const SizedBox(width: 4),
Expanded(
child: Text(
widget.tip!,
style: TextStyle(
fontSize: 12,
color: Colors.orange[700],
),
),
),
],
),
),
],
);
}
Widget _buildChoiceItem(BuildContext context, Choice choice) {
final isSelected = _selectedValue == choice.title;
final theme = Theme.of(context);
return GestureDetector(
onTap: () {
setState(() {
_selectedValue = choice.title;
});
widget.onChanged?.call(choice.title);
widget.onSelect?.call(true);
},
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
border: Border.all(
color: isSelected ? theme.primaryColor : Colors.transparent,
width: isSelected ? 1 : 1,
),
borderRadius: BorderRadius.circular(4),
),
child: Row(
children: [
Expanded(
child: Text(
choice.title,
style: TextStyle(
fontSize: 16,
color: isSelected ? theme.primaryColor : Colors.black87,
),
),
),
if (choice.subtitle != null)
Text(
choice.subtitle!,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(width: 8),
Icon(
isSelected ? Icons.check_circle : Icons.circle_outlined,
color: isSelected ? theme.primaryColor : Colors.grey[400],
size: 20,
),
],
),
),
);
}
}
6、多选组件
dart
import 'package:flutter/material.dart';
class QuestionMultiChoice extends StatefulWidget {
final List<String> options;
final List<String> initialValues;
final ValueChanged<List<String>> onChanged;
final String? tip;
const QuestionMultiChoice({
super.key,
required this.options,
required this.initialValues,
required this.onChanged,
this.tip,
});
@override
State<QuestionMultiChoice> createState() => _QuestionMultiChoiceState();
}
class _QuestionMultiChoiceState extends State<QuestionMultiChoice> {
late List<String> _selectedValues;
@override
void initState() {
super.initState();
_selectedValues = List.from(widget.initialValues);
}
@override
void didUpdateWidget(QuestionMultiChoice oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.initialValues != oldWidget.initialValues) {
setState(() {
_selectedValues = List.from(widget.initialValues);
});
}
}
void _toggleOption(String option) {
setState(() {
if (_selectedValues.contains(option)) {
_selectedValues.remove(option);
} else {
_selectedValues.add(option);
}
widget.onChanged(_selectedValues);
});
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...widget.options.map((option) => _buildOptionItem(context, option)).toList(),
if (widget.tip != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
children: [
Icon(
Icons.warning_amber_rounded,
size: 16,
color: Colors.orange[700],
),
const SizedBox(width: 4),
Expanded(
child: Text(
widget.tip!,
style: TextStyle(
fontSize: 12,
color: Colors.orange[700],
),
),
),
],
),
),
],
);
}
Widget _buildOptionItem(BuildContext context, String option) {
final isSelected = _selectedValues.contains(option);
final theme = Theme.of(context);
return GestureDetector(
onTap: () => _toggleOption(option),
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
border: Border.all(
color: isSelected ? theme.primaryColor : Colors.transparent,
width: isSelected ? 1 : 1,
),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Expanded(
child: Text(
option,
style: TextStyle(
fontSize: 16,
color: isSelected ? theme.primaryColor : Colors.black87,
),
),
),
Icon(
isSelected ? Icons.check_box : Icons.check_box_outline_blank,
color: isSelected ? theme.primaryColor : Colors.grey[400],
size: 20,
),
],
),
),
);
}
}
7、上传图片组件
dart
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
class QuestionImageUpload extends StatefulWidget {
final List<String>? initialImages;
final Function(List<String>) onImagesChanged;
final String? tip;
final int maxCount;
final int numOfRow;
const QuestionImageUpload({
super.key,
this.initialImages,
required this.onImagesChanged,
this.tip,
this.maxCount = 10,
this.numOfRow = 4,
});
@override
State<QuestionImageUpload> createState() => _QuestionImageUploadState();
}
class _QuestionImageUploadState extends State<QuestionImageUpload> {
late List<String> _images;
@override
void initState() {
super.initState();
_images = widget.initialImages?.toList() ?? [];
}
@override
void didUpdateWidget(QuestionImageUpload oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.initialImages != oldWidget.initialImages) {
setState(() {
_images = widget.initialImages?.toList() ?? [];
});
}
}
// 检查并请求权限
Future<bool> _checkPermission() async {
// 检查平台
if (Platform.isIOS) {
// iOS 权限检查
PermissionStatus photoStatus = await Permission.photos.status;
if (!photoStatus.isGranted) {
photoStatus = await Permission.photos.request();
if (!photoStatus.isGranted) {
_showPermissionDeniedDialog('相册');
return false;
}
}
// 如果需要相机权限
PermissionStatus cameraStatus = await Permission.camera.status;
if (!cameraStatus.isGranted) {
cameraStatus = await Permission.camera.request();
if (!cameraStatus.isGranted) {
_showPermissionDeniedDialog('相机');
return false;
}
}
return true;
} else if (Platform.isAndroid) {
// Android 权限检查
// 检查 Android 版本
if (await _isAndroid13OrAbove()) {
// Android 13+ 使用 READ_MEDIA_IMAGES
PermissionStatus mediaImagesStatus = await Permission.photos.status;
if (!mediaImagesStatus.isGranted) {
mediaImagesStatus = await Permission.photos.request();
if (!mediaImagesStatus.isGranted) {
_showPermissionDeniedDialog('相册');
return false;
}
}
} else {
// Android 12 及以下使用 READ_EXTERNAL_STORAGE
PermissionStatus storageStatus = await Permission.storage.status;
if (!storageStatus.isGranted) {
storageStatus = await Permission.storage.request();
if (!storageStatus.isGranted) {
_showPermissionDeniedDialog('存储');
return false;
}
}
}
// 如果需要相机权限
PermissionStatus cameraStatus = await Permission.camera.status;
if (!cameraStatus.isGranted) {
cameraStatus = await Permission.camera.request();
if (!cameraStatus.isGranted) {
_showPermissionDeniedDialog('相机');
return false;
}
}
return true;
}
// 其他平台
return true;
}
// 检查是否为 Android 13 或更高版本
Future<bool> _isAndroid13OrAbove() async {
if (Platform.isAndroid) {
final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
return androidInfo.version.sdkInt >= 33; // Android 13 是 API 33
}
return false;
}
// 显示权限被拒绝的对话框
void _showPermissionDeniedDialog(String permissionName) {
if (!mounted) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('需要$permissionName权限'),
content: Text('请在设置中允许应用访问您的$permissionName,以便上传图片'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
openAppSettings();
},
child: const Text('去设置'),
),
],
),
);
}
// 选择图片
Future<void> _pickImage() async {
if (_images.length >= widget.maxCount) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('最多只能上传${widget.maxCount}张图片'),
backgroundColor: Colors.red,
),
);
return;
}
// // 检查权限
// bool hasPermission = await _checkPermission();
// if (!hasPermission) return;
// 计算还可以选择的图片数量
final int remainingCount = widget.maxCount - _images.length;
// 使用wechat_assets_picker选择图片
final List<AssetEntity>? result = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
maxAssets: remainingCount,
requestType: RequestType.image,
),
);
if (result != null && result.isNotEmpty) {
// 处理选中的图片
for (final AssetEntity asset in result) {
final File? file = await asset.file;
if (file != null) {
setState(() {
_images.add(file.path);
});
}
}
// 通知父组件图片变化
widget.onImagesChanged(_images);
}
}
void _removeImage(int index) {
setState(() {
_images.removeAt(index);
});
// 通知父组件图片变化
widget.onImagesChanged(_images);
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LayoutBuilder(
builder: (context, constraints) {
// 计算图片项的宽度,基于每行显示的图片数量和可用宽度
final availableWidth = constraints.maxWidth;
final spacing = 8.0 * (widget.numOfRow - 1); // 图片之间的间距总和
final itemWidth = (availableWidth - spacing) / widget.numOfRow;
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
..._images.map((path) => _buildImageItem(context, path, itemWidth)).toList(),
if (_images.length < widget.maxCount) _buildAddButton(context, itemWidth),
],
);
},
),
if (widget.tip != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
children: [
Icon(
Icons.warning_amber_rounded,
size: 16,
color: Colors.orange[700],
),
const SizedBox(width: 4),
Expanded(
child: Text(
widget.tip!,
style: TextStyle(
fontSize: 12,
color: Colors.orange[700],
),
),
),
],
),
),
],
);
}
Widget _buildImageItem(BuildContext context, String path, double width) {
final index = _images.indexOf(path);
return Stack(
children: [
Container(
width: width.truncateToDouble(),
height: width.truncateToDouble(),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: path.startsWith('http') ? NetworkImage(path) as ImageProvider : FileImage(File(path)),
fit: BoxFit.cover,
),
),
),
Positioned(
top: 4,
right: 4,
child: GestureDetector(
onTap: () => _removeImage(index),
child: Container(
padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
size: 16,
color: Colors.white,
),
),
),
),
],
);
}
Widget _buildAddButton(BuildContext context, double width) {
return GestureDetector(
onTap: _pickImage,
child: Container(
width: width,
height: width,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[300]!),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.add_photo_alternate_outlined,
color: Colors.grey[400],
size: 32,
),
),
);
}
}
8、星星满意度组件
dart
import 'package:flutter/material.dart';
class RatingOption {
final String text;
final int value;
const RatingOption(this.text, this.value);
}
class QuestionRating extends StatefulWidget {
final List<RatingOption> options;
final int? initialValue;
final ValueChanged<int>? onChanged;
final String? tip;
const QuestionRating({
super.key,
required this.options,
this.initialValue,
this.onChanged,
this.tip,
});
@override
State<QuestionRating> createState() => _QuestionRatingState();
}
class _QuestionRatingState extends State<QuestionRating> {
int? _selectedValue;
@override
void initState() {
super.initState();
_selectedValue = widget.initialValue;
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: widget.options
.map((option) => _buildRatingOption(option))
.toList(),
);
}
Widget _buildRatingOption(RatingOption option) {
final isSelected = _selectedValue == option.value;
return GestureDetector(
onTap: () {
setState(() {
_selectedValue = option.value;
});
widget.onChanged?.call(option.value);
},
child: Column(
children: [
Icon(
Icons.star,
color: isSelected ? Colors.amber : Colors.grey[300],
size: 32,
),
const SizedBox(height: 4),
Text(
option.text,
style: TextStyle(
fontSize: 12,
color: isSelected ? Colors.amber : Colors.grey[600],
),
),
if (widget.tip != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Row(
children: [
Icon(
Icons.warning_amber_rounded,
size: 16,
color: Colors.orange[700],
),
const SizedBox(width: 4),
Expanded(
child: Text(
widget.tip!,
style: TextStyle(
fontSize: 12,
color: Colors.orange[700],
),
),
),
],
),
),
],
),
);
}
}
结论
直接说结论: 细节和 UI 差很多,还原度只能达到 50%左右。 如果对UI要求不高的自研公司,生成差不多的就可以接受(独立开发者/独立创业者
),那确实是超神器。但如果是追求 UI 100%还原度的公司,只能说见仁见智,任重而道远。
疑问:网上看到一段提示语生成产品原型,效果确实不错。但是生成 flutter 页面代码就差挺多。难道是 web 和 flutter 组件的实现差异?
优点:
1、细节虽然差但是大体框架能用,你可以等它修改差不多时,自己调整细节。这块感觉实际开发中初期提效 20% - 30% 问题不大。
2、你可以让它实现一些复杂组件封装或者实现,比如雷雨、雪花、云动等效果组件实现。以前过于复杂,不敢想的特效和组件,现在和随便想,然后尽可能详细的提示语,让AI帮忙生成。
缺点:
1、即使你将 UI 截图完全喂给 Claude-3.7,让它自己生成提示语,然后根据提示语生成对应的代码,也无法百分百完全还原。寻找优化方法中
2、Flutter SDK 不支持的不规则组件,很容易直接就不生成,或者随意生成一个一看就不能用的。原本期望是绘制成组件显示出来,可惜不是。
3、生成的代码时你需要告诉它遵守一些规则,比如高内聚低耦合、Flutter最佳实现等,否则代码是平铺的。需要寻找让 AI 学习自己编码风格的解决办法
4、生成速度比较慢,生成之后代码可能和之前的代码有参数匹配错误问题,它不会自己修复,需要你告诉他修复错误。
5、如果感觉效果差不多要及时用 git 进行保存,二次重新生成的代码可能不如第一版,开发人员要注意。
其他问题:
实际开发中 intel i9 + 32g内存配置的 mac book 运行 Cursor 时不时就卡顿,不明所以,有知道原因的朋友可以留言。