Flutter进阶:Cusor + claude-3.7 AI编程实战效果研究

一、需求来源

最新代码圈有被 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 时不时就卡顿,不明所以,有知道原因的朋友可以留言。

健康调查表单

天气组件效果

相关推荐
zh73141 小时前
支付宝沙盒模式商家转账经常出现 响应异常: 解包错误
前端·阿里云·php
ZHOU_WUYI1 小时前
用react实现一个简单的三页应用
前端·javascript·react.js
samroom2 小时前
Vue项目---懒加载的应用
前端·javascript·vue.js·性能优化
手机忘记时间2 小时前
在R语言中如何将列的名字改成别的
java·前端·python
郝郝先生--3 小时前
Flutter 异步原理-Zone
前端·flutter
花开花落的博客3 小时前
uniapp 不同路由之间的区别
前端·uni-app
whatever who cares3 小时前
React 中 useMemo 和 useEffect 的区别(计算与监听方面)
前端·javascript·react.js
老兵发新帖3 小时前
前端知识-hook
前端·react.js·前端框架
t_hj3 小时前
Ajax的原理和解析
前端·javascript·ajax
蓝婷儿4 小时前
前端面试每日三题 - Day 29
前端·面试·职场和发展