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

健康调查表单

天气组件效果

相关推荐
小高0072 分钟前
JavaScript 内存管理是如何工作的?
前端·javascript
是大林的林吖9 分钟前
解决 elementui el-cascader组件懒加载时存在选中状态丢失的问题?
前端·javascript·elementui
鹏仔工作室9 分钟前
elemetui中el-date-picker限制开始结束日期只能选择当月
前端·vue.js·elementui
一 乐11 分钟前
个人博客|博客app|基于Springboot+微信小程序的个人博客app系统设计与实现(源码+数据库+文档)
java·前端·数据库·spring boot·后端·小程序·论文
sTone8737521 分钟前
Android Room部件协同使用
android·前端
晴殇i25 分钟前
前端代码规范体系建设与团队落地实践
前端·javascript·面试
用户740546399430926 分钟前
Vite 库模式输出 ESM 格式时的依赖处理方案
前端·vite
开发者小天33 分钟前
React中使用useParams
前端·javascript·react.js
lichong95141 分钟前
Android studio release 包打包配置 build.gradle
android·前端·ide·flutter·android studio·大前端·大前端++
nvvas1 小时前
npm : 无法加载文件 D:\nvm\nodejs\npm.ps1,因为在此系统上禁止运行脚本问题解决
前端·npm·node.js