بنقرة واحدة
add-widget-from-prototype
从 code_snippet 原型文件创建可预览的 Dart 小组件。自动分析 HTML 设计、生成 Flutter 组件、注册路由并添加到组件展示列表。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
从 code_snippet 原型文件创建可预览的 Dart 小组件。自动分析 HTML 设计、生成 Flutter 组件、注册路由并添加到组件展示列表。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
迁移小组件到事件携带数据模式,优化刷新性能。将数据直接放在事件中传递,小组件直接使用事件数据,避免重复的缓存访问操作。
确保小组件组件根据 HomeWidgetSize 正确调整各元素大小,包括容器、图标、字体、间距、线条粗细等
为插件主页添加带过滤器的列表小组件,使用 commonWidgetsProvider 支持多种公共组件样式,根据过滤条件选择使用多步骤 data selectors 或自定义表单
将插件中内置的dataRenderer(自定义UI)封装成标准的公共小组件,使其可被其他插件或功能复用。核心特性:(1)提取dataRenderer的UI逻辑创建独立组件文件,(2)在common_widgets.dart中注册新组件,(3)在_provideCommonWidgets中添加数据提供函数,(4)移除旧的内置渲染器代码
为Flutter插件添加可配置的选择器小组件(HomeWidget),支持用户点击配置、数据选择和动态数据渲染。核心特性:(1) 配置dataSelector保存必要数据,(2) 通过controller传递id获取最新数据,(3) 支持导航到详情页
为Flutter插件视图添加事件监听容器,自动管理事件订阅/取消订阅生命周期,避免内存泄漏并减少约84%的模板代码。适用场景:(1) 插件视图需要响应全局事件更新UI,(2) 需要避免手动管理订阅导致的内存泄漏,(3) 简化事件监听实现
| name | add-widget-from-prototype |
| description | 从 code_snippet 原型文件创建可预览的 Dart 小组件。自动分析 HTML 设计、生成 Flutter 组件、注册路由并添加到组件展示列表。 |
从 code_snippet 目录中的原型文件(HTML + 截图)创建 Flutter 小组件,自动完成组件生成、路由注册和列表入口添加。
# 基础用法
/add-widget-from-prototype <prototype-path>
# 示例:从 card_widgets/widget3 创建组件
/add-widget-from-prototype code_snippet/card_widgets/widget3
<prototype-path>: 原型目录路径,相对于项目根目录
code.html 文件screen.png 截图文件--name <name>: 组件名称(驼峰命名,如 SegmentedProgressCard)--route <route>: 路由路径(如 segmented_progress_card)--title <title>: 列表显示标题(中文)--subtitle <subtitle>: 列表副标题--icon <icon>: 列表图标(Material Icons 名称)读取并分析原型文件:
code.html 获取 HTML 结构和样式screen.png 获取视觉参考根据设计功能推断通用组件名称:
SpendingWidget、GroceriesWidget)SegmentedProgressCard、CategoryDistributionChart)常用命名模式:
| 设计类型 | 推荐命名 |
|---|---|
| 分段进度条 | SegmentedProgressCard |
| 环形/半圆仪表 | GaugeWidget / HalfCircleGaugeWidget |
| 分类统计卡片 | CategoryStatsCard |
| 时间线视图 | TimelineView / DailyTimelineWidget |
| 列表卡片 | ListCard / ItemListWidget |
| 数据网格 | DataGridWidget / StatsGrid |
创建组件文件 lib/screens/widgets_gallery/screens/[component_name]_example.dart:
文件结构:
import 'package:flutter/material.dart';
/// [组件描述]示例
class [ComponentName]Example extends StatelessWidget {
const [ComponentName]Example({super.key});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(title: const Text('[组件标题]')),
body: Container(
color: isDark ? Colors.black : const Color(0xFFF2F2F7),
child: const Center(
child: [ComponentName]Widget(
// 示例数据
),
),
),
);
}
}
/// [组件描述]小组件
class [ComponentName]Widget extends StatelessWidget {
// 组件参数
final [Type] [param1];
final [Type] [param2];
const [ComponentName]Widget({
super.key,
required this.[param1],
// ...
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
// 根据原型实现 UI
}
}
// 辅助类和函数
class _[HelperName] extends StatelessWidget {
// ...
}
在 lib/screens/routing/routes/widget_gallery_routes.dart 中:
// 文件顶部添加导入
import 'package:Memento/screens/widgets_gallery/screens/[component_name]_example.dart';
// 在 routes 列表中添加(在 half_circle_gauge_widget 后面)
RouteDefinition(
path: '/widgets_gallery/[route_name]',
handler: (settings) => RouteHelpers.createRoute(const [ComponentName]Example(), settings: settings),
description: '[组件描述]',
),
在 lib/screens/widgets_gallery/screens/home_widgets_gallery_screen.dart 中添加列表入口:
_buildListItem(
context,
icon: Icons.[icon_name],
title: '[中文标题]',
subtitle: '[ComponentName] - [副标题]',
route: '/widgets_gallery/[route_name]',
),
从 HTML 中提取颜色:
// 从 HTML 提取颜色示例
final backgroundColor = isDark ? const Color(0xFF1C1C1E) : Colors.white;
final primaryColor = const Color(0xFF7B57E0);
final textColor = isDark ? Colors.white : Colors.grey.shade900;
重要:组件必须适配 Flutter 主题颜色系统
当原型中的主色调与主题颜色接近时(如 Rose、Purple、Blue 等),应优先使用 Theme.of(context).colorScheme 中的颜色,而非硬编码颜色值。
// ✅ 推荐:使用主题颜色
final primaryColor = Theme.of(context).colorScheme.primary;
// ❌ 避免:硬编码颜色(除非与主题差异较大)
final primaryColor = const Color(0xFFF43F5E);
// 示例:根据情况选择颜色
final primaryColor = isDark
? const Color(0xFFFB7185) // 深色模式使用原型颜色
: Theme.of(context).colorScheme.primary; // 浅色模式使用主题色
主题颜色映射参考:
| 原型颜色 | 主题颜色 | 使用场景 |
|---|---|---|
| Rose (#F43F5E) | colorScheme.error 或 colorScheme.primary | 主色调 |
| Purple (#7B57E0) | colorScheme.primary | 主色调 |
| Blue (#3B82F6) | colorScheme.primary | 主色调 |
| Green (#10B981) | colorScheme.secondary | 辅助色 |
| Orange/Amber | colorScheme.tertiary | 第三色 |
HTML/Tailwind → Flutter 转换:
| HTML/Tailwind | Flutter |
|---|---|
flex row | Row() |
flex col | Column() |
rounded-lg | BorderRadius.circular(8) |
p-4 | EdgeInsets.all(16) |
gap-2 | SizedBox(height/width: 8) |
w-full | Expanded() 或 double.infinity |
text-center | TextAlign.center |
根据原型固定尺寸或使用响应式布局:
// 固定尺寸(桌面小组件风格)
Container(
width: 280,
height: 280,
// ...
)
// 响应式尺寸
LayoutBuilder(
builder: (context, constraints) {
return Container(
width: constraints.maxWidth,
// ...
);
},
)
原型中包含图表时:
fl_chart 包实现图表CustomPainter 自定义绘制// fl_chart 示例
import 'package:fl_chart/fl_chart.dart';
PieChart(
PieChartData(
sections: [
PieChartSectionData(value: 30, color: Colors.blue),
PieChartSectionData(value: 70, color: Colors.green),
],
),
)
| 功能 | 组件名称 | 原因 |
|---|---|---|
| 分段进度条 | SegmentedProgressCard | 描述布局结构 |
| 分类统计 | CategoryStatsCard | 通用,可复用 |
| 圆形仪表 | CircularGaugeWidget | 描述形状 |
| 时间线 | TimelineWidget | 描述视图类型 |
| 功能 | 避免命名 | 原因 |
|---|---|---|
| 消费展示 | SpendingWidget | 过于具体 |
| 购物统计 | ShoppingWidget | 限于购物场景 |
| 任务列表 | TodoListWidget | 仅适用于 Todo |
创建的小组件必须包含动画效果以提升用户体验:
必需实现:
实现方式:
class MyWidget extends StatefulWidget {
// ...
}
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
);
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Opacity(
opacity: _animation.value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - _animation.value)),
child: // 组件内容
),
);
},
);
}
}
适用场景: 组件包含进度条、仪表盘等数据展示
实现方式:
CustomPainter 绘制进度条progress * animation.valueclass _ProgressPainter extends CustomPainter {
final double progress;
final Color progressColor;
final Color backgroundColor;
_ProgressPainter({
required this.progress,
required this.progressColor,
required this.backgroundColor,
});
@override
void paint(Canvas canvas, Size size) {
// 绘制背景圆环
// 绘制进度圆弧(使用 progress 值)
}
@override
bool shouldRepaint(covariant _ProgressPainter oldDelegate) {
return oldDelegate.progress != progress;
}
}
必需实现: 组件中的数值显示必须使用 AnimatedFlipCounter
⚠️ 防止布局抖动的固定尺寸约束(必须遵守)
AnimatedFlipCounter 在动画过程中会改变内容尺寸,必须添加固定尺寸约束防止布局抖动:
// ✅ 正确:完整的固定尺寸约束模式
SizedBox(
height: 54, // 1. 外层 Row 固定高度
child: Row(
crossAxisAlignment: CrossAxisAlignment.center, // 2. 使用 center 而非 baseline
children: [
SizedBox(
width: 160, // 3. AnimatedFlipCounter 固定宽度和高度
height: 52,
child: AnimatedFlipCounter(
value: data.value * itemAnimation.value,
fractionDigits: data.value % 1 != 0 ? 2 : 0,
textStyle: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w800,
height: 1.0, // 4. 固定行高
),
),
),
const SizedBox(width: 6),
SizedBox(
height: 22, // 5. 单位 Text 固定高度
child: Text(
'unit',
style: TextStyle(
fontSize: 14,
height: 1.0, // 6. 固定行高
),
),
),
],
),
)
❌ 错误示例(会导致布局抖动):
// ❌ 错误 1: 使用 baseline 对齐
Row(
crossAxisAlignment: CrossAxisAlignment.baseline, // 会导致抖动
textBaseline: TextBaseline.alphabetic,
children: [
AnimatedFlipCounter(...),
Text('unit'),
],
)
// ❌ 错误 2: 没有固定宽度
Row(
children: [
SizedBox(
height: 52, // 只有高度,没有宽度
child: AnimatedFlipCounter(...),
),
Text('unit'),
],
)
// ❌ 错误 3: Text 没有固定高度
Row(
children: [
AnimatedFlipCounter(...),
Text('unit'), // 没有包裹 SizedBox
],
)
固定尺寸计算公式:
// AnimatedFlipCounter 的宽度应足够容纳最大值
final counterWidth = maxWidthDigitCount * fontSize * 0.6 + padding;
// 示例计算:
// fontSize: 48, 最大3位整数 + 1位小数 = 4个字符
// width = 4 * 48 * 0.6 ≈ 115,取安全值 160
// 外层 Row 高度 = max(counterHeight, unitHeight) + alignment
final rowHeight = max(52, 22) = 54;
完整实现模式:
import 'package:animated_flip_counter/animated_flip_counter.dart';
// 在 AnimatedBuilder 中使用
SizedBox(
height: 54,
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 160, // 根据最大值计算
height: 52,
child: AnimatedFlipCounter(
value: data.value * itemAnimation.value, // 从 0 增长到目标值
fractionDigits: data.value % 1 != 0 ? 2 : 0, // 自动识别整数/小数
textStyle: TextStyle(
color: Colors.white,
fontSize: 48,
fontWeight: FontWeight.w800,
height: 1.0, // 固定行高防止文字变化影响布局
),
),
),
const SizedBox(width: 6),
SizedBox(
height: 22, // 单位固定高度
child: Text(
'unit',
style: TextStyle(
fontSize: 14,
height: 1.0, // 固定行高
),
),
),
],
),
),
检查清单:
width 和 heightheight(使用 SizedBox 包裹)crossAxisAlignment: center 而非 baselineSizedBoxheight: 1.0value * animation.value适用场景: 组件包含多个列表项或卡片
实现方式:
for (int i = 0; i < items.length; i++) ...[
if (i > 0) const SizedBox(height: 24),
_ItemWidget(
data: items[i],
animation: _animation,
index: i, // 传递索引用于计算延迟
),
]
// 在子组件中
final itemAnimation = CurvedAnimation(
parent: animation,
curve: Interval(
index * 0.15, // 延迟开始
0.6 + index * 0.15, // 延迟结束
curve: Curves.easeOutCubic,
),
);
时间轴 (0-1200ms), 3个元素:
├── 0-180ms: 元素1 开始淡入+上滑
├── 0-930ms: 元素1 进度条/数字增长
├── 180-360ms: 元素2 开始淡入+上滑
├── 180-1110ms: 元素2 进度条/数字增长
├── 360-540ms: 元素3 开始淡入+上滑
└── 360-1200ms: 元素3 进度条/数字增长
import 'package:flutter/material.dart';
/// 分段进度条统计卡片示例
class SegmentedProgressCardExample extends StatelessWidget {
const SegmentedProgressCardExample({super.key});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(title: const Text('分段进度条统计卡片')),
body: Container(
color: isDark ? Colors.black : const Color(0xFFF2F2F7),
child: const Center(
child: SegmentedProgressCardWidget(
segments: [
SegmentData(label: '类别A', value: 45, color: Color(0xFFE14462)),
SegmentData(label: '类别B', value: 30, color: Color(0xFF7B57E0)),
],
total: 100,
unit: '单位',
),
),
),
);
}
}
/// 分段数据模型
class SegmentData {
final String label;
final double value;
final Color color;
const SegmentData({
required this.label,
required this.value,
required this.color,
});
}
/// 分段进度条统计小组件
class SegmentedProgressCardWidget extends StatelessWidget {
final List<SegmentData> segments;
final double total;
final String unit;
const SegmentedProgressCardWidget({
super.key,
required this.segments,
required this.total,
this.unit = '',
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final backgroundColor = isDark ? const Color(0xFF1C1C1E) : Colors.white;
return Container(
width: 280,
height: 280,
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(24),
),
child: Column(
children: [
// 组件内容
],
),
);
}
}
当用户请求从原型创建组件时:
验证原型路径
code.html 是否存在screen.png 作为参考分析原型设计
推断组件名称
生成 Dart 代码
注册路由
widget_gallery_routes.dart 添加导入添加列表入口
home_widgets_gallery_screen.dart 添加入口验证代码
完成后验证:
文件创建:
lib/screens/widgets_gallery/screens/[name]_example.dart路由注册:
widget_gallery_routes.dart 已添加导入列表入口:
home_widgets_gallery_screen.dart 已添加列表项代码质量:
Theme.of(context).colorScheme 而非硬编码颜色动画效果:
原型路径: code_snippet/card_widgets/widget2
推断组件名: SegmentedProgressCard (而非 SpendingCard)
生成结果:
segmented_progress_card_example.dart/widgets_gallery/segmented_progress_card原型路径: code_snippet/gauge_widgets/widget1
推断组件名: HalfCircleGaugeWidget
生成结果:
half_circle_gauge_widget_example.dart/widgets_gallery/half_circle_gauge_widget检查路径格式:
// 分隔目录重新命名:
从 HTML 精确提取:
0xFF 格式错误信息:
A RenderFlex overflowed by X pixels on the bottom.
The relevant error-causing widget was: Column
原因:
在固定高度的容器中使用 Spacer() 或 Column 内容超出容器高度。
解决方案:
// ❌ 错误:使用 Spacer 导致布局不可控
Column(
children: [
HeaderWidget(),
Spacer(),
ContentWidget(),
FooterWidget(),
],
)
// ✅ 正确:使用固定的 SizedBox
Column(
mainAxisSize: MainAxisSize.min, // 让 Column 只占用必需空间
children: [
HeaderWidget(),
const SizedBox(height: 20), // 固定间距
ContentWidget(),
const SizedBox(height: 14), // 固定间距
FooterWidget(),
],
)
布局计算建议:
mainAxisSize: MainAxisSize.min 让 Column 自适应SizedBox 而非 Spacer()现象: AnimatedFlipCounter 导致布局在动画执行时上下抖动或整体跳动。
根本原因:
AnimatedFlipCounter 在动画过程中数字位数变化(如 0 → 547),导致宽度变化CrossAxisAlignment.baseline 对齐时,Flutter 会重新计算基线位置完整解决方案:
// ❌ 错误 1: 使用 baseline 对齐(最常见错误)
Row(
crossAxisAlignment: CrossAxisAlignment.baseline, // ❌ 会导致抖动
textBaseline: TextBaseline.alphabetic,
children: [
AnimatedFlipCounter(value: value * animation.value),
Text('km'),
],
)
// ❌ 错误 2: 只固定高度,没有固定宽度
SizedBox(
height: 48, // ❌ 只有高度,宽度仍会变化
child: Row(
children: [
AnimatedFlipCounter(value: value * animation.value),
Text('km'),
],
),
)
// ❌ 错误 3: Text 没有固定高度
SizedBox(
height: 48,
child: Row(
children: [
SizedBox(
width: 170,
height: 48,
child: AnimatedFlipCounter(value: value * animation.value),
),
Text('km'), // ❌ 没有包裹 SizedBox,高度不固定
],
),
)
// ✅ 正确:完整固定尺寸约束模式
SizedBox(
height: 54, // 1. 外层固定高度(足够容纳最大元素)
child: Row(
crossAxisAlignment: CrossAxisAlignment.center, // 2. 使用 center 而非 baseline
children: [
SizedBox(
width: 160, // 3. AnimatedFlipCounter 固定宽度(根据最大值计算)
height: 52, // 4. AnimatedFlipCounter 固定高度
child: AnimatedFlipCounter(
value: value * animation.value,
textStyle: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w800,
height: 1.0, // 5. 固定行高(防止文字行高变化)
),
),
),
const SizedBox(width: 6),
SizedBox(
height: 22, // 6. 单位 Text 固定高度
child: Text(
'km',
style: TextStyle(
fontSize: 14,
height: 1.0, // 7. 固定行高
),
),
),
],
),
)
尺寸计算公式:
// AnimatedFlipCounter 宽度计算
// 预估最大位数 × 字体大小 × 字符宽度系数 + 安全余量
final counterWidth = (maxIntegerDigits + maxFractionDigits + 1) * fontSize * 0.6 + 20;
// 示例:
// 最大值: 999.99 (3位整数 + 2位小数 + 1个小数点 = 6个字符)
// fontSize: 48
// width = 6 * 48 * 0.6 + 20 ≈ 193,取整到 200 或更大安全值
// 外层 Row 高度计算
final rowHeight = max(counterHeight, unitHeight) + verticalPadding;
// 示例:
// counterHeight: 52, unitHeight: 22
// rowHeight = max(52, 22) = 54
快速参考尺寸表:
| 字体大小 | 预估位数 | 推荐宽度 | Row高度 |
|---|---|---|---|
| 24px | 3-4位 | 80-100px | 32px |
| 36px | 3-4位 | 120-140px | 42px |
| 48px | 3-4位 | 160-180px | 54px |
| 60px | 3-4位 | 200-220px | 66px |
检查清单:
width(必须)height(必须)height(使用 SizedBox 包裹)CrossAxisAlignment.center 而非 baselineSizedBoxheight: 1.0value * animation.value原因: 使用 Container 装饰时,点阵绘制超出了圆角范围。
解决方案:
// ❌ 错误:点阵可能超出圆角边界
Positioned(
top: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.2),
borderRadius: BorderRadius.only(topRight: Radius.circular(26)),
),
child: CustomPaint(painter: _DotPatternPainter(...)),
),
)
// ✅ 正确:使用 ClipRRect 裁剪 + 将透明度应用到颜色
Positioned(
top: 0,
right: 0,
child: ClipRRect(
borderRadius: BorderRadius.only(topRight: Radius.circular(26)),
child: ShaderMask(
shaderCallback: (bounds) => RadialGradient(...).createShader(bounds),
blendMode: BlendMode.dstIn,
child: CustomPaint(
size: const Size(128, 128), // 明确指定尺寸
painter: _DotPatternPainter(
color: primaryColor.withOpacity(0.2), // 透明度在颜色上
),
),
),
),
)
错误信息:
'package:flutter/src/animation/curves.dart': Failed assertion:
line 180 pos 12: 'end <= 1.0': is not true.
原因:
使用 Interval 实现多元素延迟动画时,最后一个元素的 end 值超过了 1.0。
解决方案:
// ❌ 错误:step 太大导致 end 超出 1.0
final step = 0.12; // 对于 8 个元素,最大 end = 0.6 + 7 * 0.12 = 1.44
// ✅ 正确:计算合适的 step 确保最大 end <= 1.0
// 公式:step <= (1.0 - baseEnd) / (elementCount - 1)
// 示例:8 个元素,baseEnd = 0.6
// step <= (1.0 - 0.6) / 7 = 0.057
final step = 0.05; // 最大 end = 0.6 + 7 * 0.05 = 0.95
final itemAnimation = CurvedAnimation(
parent: _animationController,
curve: Interval(
index * step,
0.6 + index * step,
curve: Curves.easeOutCubic,
),
);
通用计算公式:
// 确保所有 Interval 的 end 值不超过 1.0
final elementCount = items.length;
final baseEnd = 0.6; // 第一个元素的结束位置
final maxStep = (1.0 - baseEnd) / (elementCount - 1);
final step = maxStep * 0.9; // 留 10% 安全余量
half_circle_gauge_widget_example.dart 作为模板fl_chart 包实现animated_flip_counter 包实现数字翻转效果(项目已包含此依赖)Curves.easeOutCubicInterval 实现依次延迟效果(每个延迟约 15%)Theme.of(context).colorScheme 中的颜色,确保组件与 App 主题一致